forked from ROMEO/obsw
some more words
This commit is contained in:
parent
3064a5c2e8
commit
c0d82ee7d1
@ -7,19 +7,21 @@ As far as rust goes, static allocation and multithreading with a C RTOS is not p
|
||||
In a multithreaded software, references to data need to be passed to enable communication between objects, to be able to execute a task (which requires passing a reference to the task's data (which contains information on other tasks/data) to the RTOS), to send messages (passing the reference to the Queue, however encapsulated, is a reference to the queue which needs to be located somewhere) or to acess shared data (mutexes, same as with queues).
|
||||
All of these communication techniques are essential for this framework, so solutions need to be provided to be able to write safe code.
|
||||
|
||||
While statically allocating all (shared) data is generally possible and might be a possible solution to static allocation, it is not consistent with either the object oriented style of this framework, nor with general rust coding style (which discourages static data).
|
||||
While statically allocating all (shared) data, using global `'static` variables putting them into static memory instead of the stack, is generally possible and might be a possible solution to static allocation, it is not consistent with either the object oriented style of this framework, nor with general rust coding style (which discourages static data).
|
||||
|
||||
# The framework's approach
|
||||
|
||||
## Task Executor
|
||||
|
||||
Central element in running multithreaded is the task executor. It is borrowing references to all data to be used during runtime. This way, the references are guaranteed to be valid for the lifetime of the executor. By then coupling the availability of tasks to the executor (dropping the executor stops all tasks), the references can be guaranteed to be valid for the whole time tasks are available.
|
||||
Central element in running multithreaded is the task executor. It is borrowing references to all data to be used during runtime. This way, the references are guaranteed to be valid for the lifetime of the executor. This can be seen as 'pinning' the objects on the stack to fixed adresses.
|
||||
By then coupling the availability of tasks to the executor (dropping the executor stops all tasks), the references can be guaranteed to be valid for the whole time tasks are available.
|
||||
|
||||
The step where references to other objects is stored in structs is called initialization. This is (enforced by the compiler via layout of the corresponding functions) the only time where access to other objects is granted and references can be stored. The initialization is performed by the task executor as well, so at that time refereces are already fixed to their threading-time value.
|
||||
The step where references to other objects are stored in structs is called initialization. This is (enforced by the compiler via layout of the corresponding functions) the only time where access to other objects is granted and references can be stored.
|
||||
The initialization is performed by the task executor which controls that only pinned objects are given access to other pinned objects. Pinned objects are given an API to access other pinned objects (provided by an `ObjectManager`) as well as a token. The token can be passed to functions which will return references to objects to be stored by the calling object. This way, these references can only be created during the initialization.
|
||||
|
||||
For an production software, as soon as all tasks are started, the initial task is stopped/deleted. As such, there is no way (and no intention) for the multithreading to ever stop. In that case, the references shared during initialization will be valid for the whole runtime of the program.
|
||||
|
||||
As there might be use cases, for example in test cases, where multithreading is to be stopped, additional safeguards are implemented to ensure that the references shared during initialization are invalidated or their use is restricted. As running outside of the multithreaded environment is not meant for production, failing without corruption, ie panicking, is an acceptable way out. This is implemented by using a global semaphore indicating if the thread executor is alive. If that is not the case, all access to thared ressources will result in a panic. This adds an additional overhead to all access to shared data, namely checking the semaphore.
|
||||
As there might be use cases, for example in test cases, where multithreading is to be stopped, additional safeguards are implemented to ensure that the references shared during initialization are invalidated or their use is restricted. As running outside of the multithreaded environment is not meant for production, failing without corruption, ie panicking, is an acceptable way out. This is implemented by using a global semaphore indicating if the thread executor is alive. If that is not the case, all access to shared ressources will result in a panic. This adds an additional overhead to all access to shared data, namely checking the semaphore.
|
||||
|
||||
## Shared references
|
||||
|
||||
@ -29,9 +31,11 @@ The only time where references to other objects can be acquired is the initializ
|
||||
|
||||
Access to the other objects is granted via an object manager implementation, which will provide other objects as a nonmutable reference. Again, this reference can not be stored, only queried, without violating the borrow checker.
|
||||
|
||||
The reference obtained by the object manager is typed as `dyn SystemObjectIF`, so the ways to obtain references is governed by this trait and its super traits. These traits only offer threadsafe references, ie ones protected by RTOS primitives, to be obtained.
|
||||
The reference obtained by the object manager is typed as `dyn SystemObjectIF`, so the ways to obtain references is governed by this trait and its super traits. These traits only offer threadsafe APIs. Additionally, these APIs require a token which will be passed by the task executor so they can only be used during initialization.
|
||||
|
||||
Those references are either protected by a mutex or implemented using queues, which are the two primitives used to implement threadsafety. As such all access to shared references must use either the mutex or the queue API which is protected by an additional semaphore as described above.
|
||||
Those references are either protected by a mutex or implemented using queues, which are the two primitives used to implement thread safety. As such, all access to shared references must use either the mutex or the queue API which is protected by an additional semaphore as described above.
|
||||
|
||||
In some cases, smart pointers are used. These store raw pointers to other objects offering a threadsafe API. As dereferencing such a pointer is only allowed when objects are pinned, the dereferencing is protected by the additional semaphore as well.
|
||||
|
||||
## RTOS Metadata
|
||||
|
||||
@ -43,7 +47,7 @@ Two options exist for storing the descriptors. Either they are stored in a preal
|
||||
|
||||
The first option has the advantage that in C, the size of the descriptors is known at compile time (it is determined by the RTOS configuration which is implemented in C macros). Its disadvantage is that the size of the array needs to be adapted to the actual number ob instances used by the RTOS, which might not be trivially determined except for running the software.
|
||||
|
||||
The second option does not have this disadvantage, as the memory is provided by the user of the API. The disadvantage here is that the size of the data needs to be encoded manually for the selected configuration of the RTOS, which again can only be verified during runtime, when rust an C interact.
|
||||
The second option does not have this disadvantage, as the memory is provided by the user of the API. The disadvantage here is that the size of the data needs to be encoded manually for the selected configuration of the RTOS, which again can only be verified during runtime, when rust and C interact.
|
||||
|
||||
Both solutions lead to a detection of the configuration error (too few descriptors preallocated, too little memory allocated) only during runtime. As the configuration of the RTOS is expected to be more stable than the number of primitive instances, the second option is implemented.
|
||||
|
||||
@ -62,3 +66,13 @@ The handling of the failure is delegated to the structs using the primitives, as
|
||||
* Uninitialized `MessageQueue`s will return empty from `read()` calls
|
||||
* Uninitialized `Mutex`es [TBCoded] will behave nominally, but not lock any actual mutex
|
||||
* Uninitialized `OwnedDataset`s will behave nominally, but not lock any mutex (which is uninitialized)
|
||||
* Uninitialized `ReferencedDataset`s will return default data on `read()` and do nothing on `commit()`
|
||||
|
||||
## Smart pointers
|
||||
|
||||
Datasets are smart pointers to T, with a read/commit API. Both a mutex and the threading semaphore protect each call. Clones (`ReferencedDataset`) are created invalid and can only made valid during init using the init token.
|
||||
Can be compared to the mutex in rust std with a slightly different API.
|
||||
|
||||
Stores are a statically preallocated slotted allocation scheme:
|
||||
StoreAccessor is a smart pointer to the Store, offering same API as Store. Internally, it dereferences a raw pointer in each call, protected by the threading semaphore. Store in turn protects a shared backend with a mutex. Acessors can only be obtained during init. Together, StoreAccessor and Store are an thread safe allocator to the backend.
|
||||
StoreSlots are smart pointers to memory allocated in a store backend. Can be created/allocated during runtime.
|
Loading…
x
Reference in New Issue
Block a user