Semaphores or Mutexes for sharing resources in RTOS-based designs
Tasks can easily share data when all tasks exist in a single address space and can reference global variables, pointers, buffers, linked lists, ring buffers, etc. Although sharing data simplifies the exchange of information between tasks, it is important to ensure that each task has exclusive access to the data to avoid contention and data corruption.
I’ll use as an example a task that performs a simple time-of-day clock implemented in software. The task only keeps track of hours, minutes and seconds but could easily be expanded to support day, day-of-month, month, year, and leap year. In fact, you can find such a module in Chapter 6 of my Embedded Systems Building Blocks[3] book.
Imagine if this task was preempted (triggered by an interrupt) by another task just after setting the Minutes to 0 and, the other task was more important than the TimeOfDayTask(). Now imagine what would happen if this higher priority task wanted to know (i.e., read) the current time. Since the Hours variable was not incremented prior to the interrupt, the higher-priority task would read the time incorrectly and, in this case, it would be incorrect by a whole hour.
The code that updates variables for the TimeOfDayTask() task must treat all of the variables indivisibly (or atomically) whenever there is possible preemption. Time-of-day variables are considered shared resources and any code that accesses those variables must have exclusive access to all the variables. RTOSs provide such services (i.e., APIs) through semaphores and mutual exclusion semaphores (a.k.a. a mutex). I’ll get into which one to use and when, shortly.
In an RTOS based system, the task would need to wrap the shared resources using a semaphore or a mutex as shown in Listing 2. The exact API to use depends on the RTOS you are using.
uint8_t Hours;
uint8_t Minutes;
uint8_t Seconds;
void TimeOfDayTask (void)
{
Seconds = 0;
Minutes = 0;
Hours = 0;
while (1) {
// Delay (i.e., sleep) task for 1 second
if (Seconds >= 59) {
Seconds = 0;
if (Minutes >= 59) {
Minutes = 0;
if (Hours >= 23) {
Hours = 0;
} else {
Hours++;
}
} else {
Minutes++;
}
} else {
Seconds++;
}
}
}
Listing 1, Simple Time-of-Day Clock without resource sharing protection.
uint8_t Hours;
uint8_t Minutes;
uint8_t Seconds;
void TimeOfDayTask (void)
{
Seconds = 0;
Minutes = 0;
Hours = 0;
while (1) {
// Delay (i.e., sleep) task for 1 second
Acquire Semaphore (or mutex); // API depends on your RTOS
if (Seconds >= 59) {
Seconds = 0;
if (Minutes >= 59) {
Minutes = 0;
if (Hours >= 23) {
Hours = 0;
} else {
Hours++;
}
} else {
Minutes++;
}
} else {
Seconds++;
}
Release Semaphore (or mutex); // API depends on your RTOS
}
}
Listing 2, Simple Time-of-Day Clock with resource sharing protection.
Semaphores
Without getting into too many details, a semaphore is a key that a task needs to acquire before proceeding to access a resource (as we’ve seen in Listing 2). This key needs to be released once the task is done accessing the shared resource. As you might expect, you are not actually getting a physical key, you get a virtual one. A semaphore needs to be initialized before it can be used. Your application can declare any number of semaphores, each one protecting different resources. There are two types of semaphores: binary and counting.
A binary semaphore contains a single key. In this case, the semaphore value is either 0 indicating that the key is in use by another task or 1, the key is available. In the time-of-day clock implementation, you would use a binary semaphore since you would only need a single clock in your application; time-zones can easily be handled with offsets from the time-of-day clock.
A counting semaphore contains multiple keys because there are multiple identical resources available. The counting semaphore has a value of 0 when all keys have been allocated to tasks, and > 0 when keys are available. An example of where you’d use a counting semaphore is in the management of a buffer pool of identical buffers. Each time you’d need a buffer from the buffer pool, you would call the RTOS API to acquire a key. If one is available (the semaphore value was > 0) then your buffer allocator would give the caller a buffer. If no buffer was available, the calling task would be suspended until a buffer was freed by one of the tasks that owns one of the buffers (assuming the waiting task has the highest priority).
So, to use a semaphore:
- You need to decide whether you need a binary or counting semaphore.
- You need to initialize the semaphore. You’d initialize the binary semaphore to 1 and the counting semaphore to N (the number of identical resources you can share).
- Whenever you need access to the resource, you’d call the RTOS API to acquire the semaphore (i.e., get a key) and when done, call the RTOS API to release the semaphore.
Generally speaking, it’s preferable to NOT use semaphores because they are subject to unbounded priority inversions! Instead, you should use mutexes. You can find a detailed description of priority inversions in my µC/OS-III books[1] and specifically, chapter 13 (i.e., section 13.3.5).
Mutual Exclusion Semaphores (a.k.a. Mutexes)
A mutex is a special type of binary semaphore and is used to eliminate unbounded priority inversions. As with the semaphore, a mutex needs to be initialized before you can attempt to acquire/release it. This is done through an API provide by the RTOS.
If a low priority task owns a resource that a higher priority also needs to access, the RTOS changes the priority of the lower priority task to match the priority of the higher priority task so that the owner of the mutex can complete its access to the resource as soon as possible and, free the resource. When the resource is released, the priority of the owner is restored to its original priority. Upon releasing the mutex, the RTOS immediately switches to the waiting higher priority task giving it access to the resource.
Should you use Semaphores or Mutexes?
As a general rule, you should make it a habit to use mutexes when accessing shared resources and reserve your use of semaphores as a signaling mechanism (subject for another article).
However, the execution time of semaphore APIs is typically significantly faster than those of mutexes because semaphores can’t change the priority of the semaphore owner and restore it back when the semaphore is released. Because of this, it’s perfectly fine to use semaphores if all the tasks involved in sharing the shared resource have the same priority, the tasks using the semaphore don’t have deadlines to satisfy or, you don’t mind about the possibility of priority inversions.
Another important thing to consider is that semaphore and mutex APIs consume potentially hundreds of CPU cycles so ideally, the execution time to access the shared resource should be greater than the execution time of these RTOS APIs. In other words, if you are only updating a couple of variables, you might want to consider instead to disable interrupts, update the variables and then re-enable interrupts. This, of course, assumes you can disable/enable interrupts. Some RTOSs run application code in user mode (non-privileged) and thus might not allow to disable interrupts. You should consult your RTOS documentation.
References:
[1] µC/OS-III, The Real-Time Kernel:
µC/OS-III, The Real-Time Kernel, and the Freescale Kinetis ARM Cortex-M4, Jean J. Labrosse, 978-0-9823375-2-3
µC/OS-III, The Real-Time Kernel, and the Infineon XMC4500, Jean J. Labrosse, 978-1-9357722-0-0
µC/OS-III, The Real-Time Kernel, and the NXP LPC1700, Jean J. Labrosse, 978-0-9823375-5-4
µC/OS-III, The Real-Time Kernel, and the Renesas RX62N, Jean J. Labrosse, 978-0-9823375-7-8
µC/OS-III, The Real-Time Kernel, and the Renesas SH7216, Jean J. Labrosse, 978-0-9823375-4-7
µC/OS-III, The Real-Time Kernel, for the STM32 ARM Cortex-M3, Jean J. Labrosse, 978-0-9823375-3-0
µC/OS-III, The Real-Time Kernel, and the Stellaris MCUs,
Jean J. Labrosse, 978-0-9823375-6-1
[2] Weston Embedded Book Downloads
https://weston-embedded.com/micrium-books
[3] Embedded Systems Building Blocks, Complete and Ready-to-Use Modules in C
Jean J. Labrosse
ISBN 978-0879306045
About the author
This article is part of a series on the topic of developing applications with RTOS.
Jean Labrosse, Micrium Founder and author of the widely popular uC/OS-II and uC/OS-III kernels, remains actively involved in the evolution of the uC/ line of embedded software.
Given his wealth of experience and deep understanding of the embedded systems market, Jean serves as a principal advisor and consultant to Weston Embedded Solutions helping to chart the future direction of the current RTOS offerings. Weston Embedded Solutions specializes in the support and development of the highly reliable Cesium RTOS family of products that are derived from the Micrium codebase.
You can contact him at Jean.Labrosse@Weston-Embedded.com.