What I learned about Zephyr threads
I'm very new to Zephyr, literally, like..., 4 weeks new. Wasn't sure about this new RTOS kid on the block at first, but now it's really starting to grow on me!
When you develop apps running on RTOS (such as FreeRTOS, Zephyr, ThreadX, VxWorks, etc.), most likely you will need to deal with threads of different priorities and determine what tasks should be performed inside the interrupt service routine (ISR) when the current thread is preempted. What's more challenging is when you need to synchronize those threads with Kernel IPC services such as mutexes and semaphores in order to protect critical sections.
To walk through those scenarios, we will use the nRF7002-DK dev board from Nordic Semiconductor for demonstration. I will also use the sample apps (philosophers & button) from Zephyr RTOS repository plus a professional tracing tool: Percepio Tracealyzer? with J-Link RTT streaming to walk through Zephyr's preemptive scheduling of threads in a GUI like this:
The scenario
With my nRF7002DK board, I merged the source code of 2 apps (Dining Philosophers & GPIO button) to demonstrate the Dining Philosophers problem (a classic multi-thread synchronization problem) with multiple preemptible and cooperative threads of differing priorities, as well as dynamic mutexes and thread sleeping. Here are the 4 transitional states of 6 philosophers with 6 forks: THINKING, HOLDING ONE FORK, STARVING, EATING:
Then I added a "twist" to the original dining philosopher application: manually preempt the current thread with random interrupts by pressing the GPIO Button #1 on the board:
When the current thread (could be any one of those six philosophers) is preempted by pressing Button 1, my ISR will execute k_wakeup() to wake up Philosopher #4 from thinking. We will then analyze what happens when ISR returns and how the kernel schedules the next current thread based on priorities.
You probably noticed in the short video above: philosophers 4 & 5 have negative priority values which made them cooperative threads. According to Zephyr's documentation:
A cooperative thread has a negative priority value. Once it becomes the current thread, a cooperative thread remains the current thread until it performs an action that makes it unready.
A preemptible thread has a non-negative priority value. Once it becomes the current thread, a preemptible thread may be supplanted at any time if a cooperative thread, or a preemptible thread of higher or equal priority, becomes ready.
The thread priority level in Zephyr looks like this:
Ok, let's come back to our story here. When Button #1 is pressed, whether the current thread is cooperative or preemptive, the execution of current thread will be replaced by ISR unless interrupts have been masked (well, that is an advanced topic, beyond a rookie like me can explain...)
I would recommend you pause and resume the video above at your own pace to closely observe the behavior of those threads before moving on.
Prerequisites
If you would like to follow along, here are the steps:
Let's do it!
Adding a GPIO button
It's super easy to add a GPIO button. Carefully review the code in Button sample and then add the relevant lines into the Philosophers sample. The procedure to enable interrupt on a GPIO pin is done in a particular order:
Add support for Percepio? Tracealyzer
To enable tracing ISR with Tracealyzer, from the doc:
To trace interrupt handlers, first call xTraceSetISRProperties() to specify the interrupt name and priority. This is typically done in your main function, not in the interrupt handler. This returns a handle, that you need to store in a global variable. Then call vTraceStoreISRBegin() in the beginning of your handler, and vTraceStoreISREnd() in the end.
Here's the code in philosopher app's main():
Analyze the threads in Tracealyzer
Once you have built and flashed the enhanced philosopher app with GPIO button 1 enabled, start the Tracealzyer session to record the traces via J-Link RTT. Now it's time to press Button #1 a few times (I pressed the button as fast as I could in order to get meaningful trace records). Stop the session and let's look into the Trace View together.
Scroll the trace view back and forth in time so you can observe how the philosophers behave nicely when they are eating or thinking. By default, the philosopher sample app uses mutexes for thread synchronization. Also, you will see the ISRs are short and not introducing much latency.
We can also see the ISR trace property has "RICK's button ISR" in color red and Philosopher 4 in orange:
From the trace view screenshot above, at the end of button_pressed() ISR (RICK's button ISR in red), it executed k_wakeup(Philosopher 4) which is circled in purple. Looking good so far! The ISR is doing what was expected.
Next, let's look for the instance when Philosopher 5 (which has priority -2, the highest cooperative thread in this app) was preempted by GPIO button interrupt:
Now this (Test 1) is VERY interesting and looks like I may have found the answer to the questions I had for the past few weeks:
Let's look at the trace together:
Next, let's do one more interesting test (Test 2). This time we will change the ISR to wake up Philosopher 5 (priority -2, the highest one) instead. We will look into the trace records and find the moment when Philosopher 4 was interrupted, will kernel wake up Philosopher 5 or schedule Philosopher 4 to run? The trace below revealed something that confirmed a very promising proof of the cooperative priority behavior:
Conclusion
Through the tracing exercise with Percepio's Tracealyzer, within a few hours, I was able to find the answer I had for weeks! So, here's what I learned about the cooperative vs preemptible threads in Zephyr:
When a cooperative thread is preempted by an interrupt, the kernel will schedule the cooperative thread which was preempted as the current thread (EVEN WHEN there's another higher priority cooperative thread ready). This provides a guarantee of the cooperative priority promise: it will remain the current thread until it performed an action to make itself unready or it's preempted by an interrupt.
References
I really enjoyed working through the exercise above to learn something new! During the exercise, I found several great blog posts by a few folks I know. Kudos to you all who contributed to the success of Zephyr!
Azure Principal Technical Specialist @ Microsoft
1 周Thanks everyone for the overwhelming support. I just added one more interesting test case right before Conclusion. If you are interested, take a look at (Test 2). The cooperative priority has a guarantee to remain the current thread even though my ISR is trying to wake up another cooperative thread with higher priority.
Thanks Rick for the kind words, and thanks for contributing to easing the learning curve for the growing The Zephyr Project community. Lessons learnt? Don't interrupt philosophers lightly; as Archimedes purportedly exclaimed, "Do not disturb my circles!". Or maybe he meant thread? ??