Process & Thread
A thread or sometimes called task in embedded software represents an entity of work. It has an entry function that typically contains an infinite loop. A thread either runs periodically (isochronous) or an event triggers processing (asynchronous). On an embedded system, threads spend most of their time waiting for a specific time or an event. The main benefit of using an RTOS for a user comes from the separation of work into scalable units and a unified communication system between them. One or more threads and a memory region together form a process.
Design
Bern RTOS uses a parallelism abstraction known from computers: processes and threads. Running on the RTOS are at least one process each contains one or more threads. However, there are some differences from a general purpose operating system (GPOS). Similar to GPOS processes are isolated from each other, but there is are no virtual addresses on a microcontroller. All memory and peripherals are mapped into one continuous address space. Also, on a GPOS, processes are started individually, while on Bern RTOS, all processes are compiled at the same time and linked into one image.
Figure: Resource organization with process and thread tables in the kernel [19, p. 109].
Processes and management structures are separated in Bern RTOS. the figure shows threads running within processes in user mode. The accompanying management structures (process and thread table) are stored in kernel memory.
Figure: Memory accessible from Thread Y.
The separation of process memory is illustrated as a memory map in the figure. Every process and the kernel have their own block of memory for data. The CPU starts on the main stack, which is later used for the kernel and cannot be accessed by processes anymore. The memory size of a process is defined at compile time. In the example of process B there is some static data placed in process memory. The rest is automatically available to the process allocator. Thread stacks can be allocated using a process allocator. Each stack will have an overflow barrier to prevent the corruption of data from a stack overflow.
A thread can access data on its own stack as well as stacks of threads of the same process. Globally shared data and static data within the process are also accessible. However, a thread will be terminated if it attempts to read or modify data on the main stack, the kernel memory, or any other process. Zephyrs usermode memory domain [61] inspired the behavior of the process memory.
In most RTOS, a thread can access anything in RAM, including the thread/task control blocks (TCB) [9, pp. 113]. The TCB contains the state of a thread, including the priority. For an RTOS to be secure, control structures such as the TCB must be inaccessible to threads, such that priorities for example cannot be directly changed.
Figure: Thread stack and control block.
As shown in the figure a thread can access its own stack. Only the kernel stores and accesses the thread control block, called Thread
in Bern RTOS. The Thread
structure contains owning pointers to the stack, privilege information including priority and memory access configuration, as well as transitions and wake-up times used by the schedulers finite state machine.
The fields of the Thread
structure can never be directly accessed; they are set via system calls, e.g., bern_kernel::sleep()
, bern_kernel::thread_exit()
, mutex.lock(100)
. It is the responsibility of the kernel to validate system calls before executing them. There are no thread handles at the moment, thus threads can only change their own state.
Figure: Thread stack frame.
Threads in Bern RTOS are spawned using closures (see usage subsection). A closure can capture its environment, which is placed on the main stack. The closure will generate an anonymous structure without memory layout guaranties [48]. Because the thread will not be able to access the main stack, the data structure must be moved to the thread stack — entry()
function.
Data alignment must be considered when copying data manually to a stack ldrd
, [24, p. 135]).
Below the closure starts the usable part of the thread stack entry()
function with the trait object loaded as a parameter in the registers
After some time the stack will contain locally stacked variables and when being paused the current set of registers
Usage
Creating a thread follows the build pattern in the way freertos-rust
does [7]. The thread builder can take any settings regarding a new thread. The kernel initialized the thread and stack, partially because only the kernel can access the stack for the new thread. The builder pattern also allows running threads to spawn new threads.
static PROC: &Process = bern_kernel::new_process!(my_process1 , 8192);
#[link_section=".process.my_process"]2
static mut MY_BUFFER: [u8; 16] = [0; 16];
#[entry]
fn main() -> ! {
let mut board = Board::new();
board.led.set_high();
bern_kernel::kernel::init();3
bern_kernel::time::set_tick_frequency(
1.kHz(),
board.sysclock().Hz()
);
let mut led = board.led;
PROC.init(move |c| {4
Thread::new(c)5
.priority(Priority::new(0))6
.stack(Stack::try_new_in(c, 1024).unwrap()7 )
.spawn(move || 8 {
loop {
led.set_high();9
bern_kernel::sleep(250);
led.set_low();
bern_kernel::sleep(750);10
}
});
}).unwrap();
bern_kernel::start();11
}
Listing: Creating a new thread.
The macro at
Any static data can be placed in the process memory by specifying the memory section
Before any kernel function call, we must initialize the scheduler
Lastly, the spawn()
call consumes the builder and takes the thread entry point as a closure. The closure captures led
. A typical thread contains an infinite loop, in this case an LED
Because a closure can capture its environment, parameters are passed to the thread with type information, no casting of pointers is necessary. Moving captured variables to threads enables the compiler to check ownership and multi-threaded access. The compiler would for example return an error if led
where to be used after
Thread::new(c)
.idle_thread()1
.static_stack(Stack::try_new_in(c, 512).unwrap()))
.spawn(move || {
loop {
cortex_m::asm::nop();
}
});
Listing: Replacing the default idle thread.
To change the behavior when all threads are suspended, the default thread can be replaced