Understanding Preemptive Multithreading in RTOS

Follow+Star PublicAccount, don’t miss the wonderful content
Understanding Preemptive Multithreading in RTOS
Author | strongerHuang
WeChat Public Account | Embedded Column
Operating systems can be a very “profound” subject for beginners, with many principles that are unclear, leading to giving up before even getting started.
This article starts from the ground up, explaining the preemption principle of preemptive operating systems (most RTOS).
Includes:
  • Basic Principles of Threads
  • Thread Preemption
  • Interrupt Preempting Threads
  • Interrupt Preempting Interrupts

Running Multiple Threads

1. Single-Core “Single Thread”
Strictly speaking, a single-core processor can only execute one instruction at a time, meaning it can only be “single-threaded”.(Of course, multi-core processors are different)
To run multiple threads on a single-core processor, we actually need to perform fast switching between threads regularly, so that users feel that multiple threads are running in parallel.
For example, the processor executes two threads, and the processor actually switches back and forth between the two threads, as shown below:
Understanding Preemptive Multithreading in RTOS
2. How does the processor switch between threads?
The single-core processor we talk about is “single-threaded”; it has a set of registers that we call this set of registers belonging to a “thread”.
For example, when calculating the sum of two numbers:
// Assume we have two integers: a and bint c = a + b ;
The actual process is as follows (of course, it depends on the type of MCU, but the overall idea is the same):
# MIPS Assembly:
LW V0, -32744(GP)   # Load the value of "a" from RAM to register V0LW V1, -32740(GP)   # Load the value "b" from RAM to register V1ADDU V0, V1, V0     # Add a and b, save the result to register V0SW V0, -32496(GP)   # Store the value of register V0 in RAM (where variable c is located)
You will find that the above executes 4 actions, but a preemptive operating system can preempt another thread at any time, including between these 4 actions.
If another thread preempts during this process, it also preempts the current thread’s V0 and V1. If we do not save V0 and V1, then when we return to execute the current thread, the result will be wrong.
Therefore, to address this issue, we need to save the values of V0 and V1 before switching threads, and when we switch back to the current thread, we restore the values of V0 and V1, the general process is as follows:
Understanding Preemptive Multithreading in RTOS
The general idea is: When we need to switch from one thread to another, the kernel gains control, performs necessary housekeeping (at least saving and restoring register values), and then transfers control to the next thread to run.

Thread Stack

Where is the preemption position mentioned above, and which register values does each thread save? This is the content of the thread’s stack.
In operating systems with MMUs, (user) thread stacks can grow dynamically on demand: the more stack space a thread needs, the more stack it has (if the kernel allows)..
However, our general MCUs do not have this “high-end” feature of MMUs, and all RAM is statically mapped to the address space. Thus, each thread will have RAM space for its stack, and if the RAM used by the thread exceeds the stack size, it will lead to memory overflow or subtle errors. (In fact, each thread’s stack space is just a contiguous array space).
Therefore, when we decide how much stack to allocate for each thread, we are just estimating how much stack we might need, but the specific amount may not be very clear.
For example, if this is a GUI thread with multiple layers of nested calls, it may need several kilobytes, but if it is a small thread for a running light, a few dozen bytes may be sufficient.
Assuming we have three threads with the following stack consumption:
Understanding Preemptive Multithreading in RTOS
As mentioned above, the register values of each thread are saved in the thread’s stack. The set of register values of a thread is called the thread’s “context”. As shown below(Thread A is the “active thread” currently running):
Understanding Preemptive Multithreading in RTOS
Note that the context of the currently executing thread A is not saved in the stack, the stack pointer points to the top of thread A’s user data, and the current processor’s registers are dedicated to thread A.
When the kernel decides to switch control to thread B, it will perform the following actions:
  • Save all register values to the stack (save to the top of thread A’s stack);
  • Switch the stack pointer to the top of thread B’s stack;
  • Restore all register values from the stack (from the top of thread B’s stack);
At this point, you will see:
Understanding Preemptive Multithreading in RTOS

Interrupt (ISR) Preemption

During the execution process or context switching mentioned above, there may also be a very important content: interrupts.
MCUs typically have peripherals: TIM, UART, SPI, CAN, etc., which can trigger interrupts at any time due to important events.
The interrupt condition is when the currently executing thread is paused, the processor performs other operations (Handles Interrupt) for a period of time, and then returns. Interrupts can occur at any time, and we should be prepared to handle them.
Interrupt Service Routines are called ISRs:
Interrupts can have different priorities, for example, if a low-priority interrupt is triggered, the currently executing thread will pause, and the ISR will gain control. Then, if a high-priority interrupt is triggered, the currently executing ISR will pause again and run a new ISR for that high-priority interrupt.
In this way, when completed, control will return to the first ISR, and upon completion, the interrupted thread will also be restored.
Important Critical Code:
During the active process of a thread, if there are important things “critical code”, if an interrupt occurs during this process, it can easily lead to unexpected results.
This part of critical code needs to be protected, and our usual practice is to disable global interrupts before the “critical code”, and enable global interrupts after execution.
One thing to note:
When global interrupts are disabled, interrupts will not be responded to, so the “critical code” cannot be too long.

Interrupt Stack

As mentioned above, high-priority interrupts preempt low-priority interrupts, which can lead to a problem: low-priority code needs a stack for saving data just like threads.
There are generally two methods:
  • Using the interrupted thread’s stack;
  • Using a separate stack space for interrupts;
1. Using the Interrupted Thread’s Stack
If using the interrupted thread’s stack, it looks like the following:
Understanding Preemptive Multithreading in RTOS
This situation presents a serious problem: do you know what it is?
Frequent interrupts, or too many interrupts, will quickly exhaust the stack space of the thread itself.
Each thread’s stack should contain the following:
  • Thread’s own data;
  • Thread’s context;
  • Data for executing worst-case ISR.
Therefore, we need to change the method and allocate separate stack space for all ISR interrupts.
2. Using a Separate Stack Space for Interrupts
Understanding Preemptive Multithreading in RTOS
Using a separate stack space for interrupts is roughly as shown in the above figure.
Well, this article discusses the above several points regarding preemption and related content, hoping to be helpful to everyone.
———— END ————
Understanding Preemptive Multithreading in RTOS
● Column “Embedded Tools”
● Column “Embedded Development”
● Column “Keil Tutorials”
● Selected Tutorials from Embedded Column
Follow the public account reply “Join Group” to join the technical exchange group according to the rules, reply “1024” to see more content.
Understanding Preemptive Multithreading in RTOS
Understanding Preemptive Multithreading in RTOS
Click “Read the Original” for more shares.

Leave a Comment

×