Mutual Exclusion Semaphores

µC/OS-III supports a special type of binary semaphore called a mutual exclusion semaphore (also known as a mutex) that eliminates unbounded priority inversions. The figure below shows how priority inversions are bounded using a Mutex.


Using a mutex to share a resource

(1) Task H and Task M are both waiting for an event to occur and Task L is executing.

(2) At some point, Task L acquires a mutex, which it needs before it is able to access a shared resource.

(3) Task L performs operations on the acquired resource.

(4) The event that Task H waited for occurs and the kernel suspends Task L and begins executing Task H since Task H has a higher priority.

(5) Task H performs computations based on the event it just received.

(6) Task H now wants to access the resource that Task L currently owns (i.e., it attempts to get the mutex from Task L). Given that Task L owns the resource, µC/OS-III raises the priority of Task L to the same priority as Task H to allow Task L to finish with the resource and prevent Task L from being preempted by medium-priority tasks.

(7) Task L continues accessing the resource, however it now does so while it is running at the same priority as Task H. Note that Task H is not actually running since it is waiting for Task L to release the mutex. In other words, Task H is in the mutex wait list.

(8) Task L finishes working with the resource and releases the mutex. µC/OS-III notices that Task L was raised in priority and thus lowers Task L to its original priority. After doing so, µC/OS-III gives the mutex to Task H, which was waiting for the mutex to be released.

(9) Task H now has the mutex and can access the shared resource.

(10) Task H is finished accessing the shared resource, and frees up the mutex.

(11) There are no higher-priority tasks to execute, therefore Task H continues execution.

(12) Task H completes and decides to wait for an event to occur. At this point, µC/OS-III resumes Task M, which was made ready-to-run while Task H or Task L were executing. Task M was made ready-to-run because an interrupt (not shown in figure 13-5) occurred which Task M was waiting for.

(13) Task M executes.


Note that there is no priority inversion, only resource sharing. Of course, the faster Task L accesses the shared resource and frees up the mutex, the better.

µC/OS-III implements full-priority inheritance and therefore if a higher priority requests the resource, the priority of the owner task will be raised to the priority of the new requestor.

A mutex is a kernel object defined by the OS_MUTEX data type, which is derived from the structure os_mutex (see os.h). An application may have an unlimited number of mutexes (limited only by the RAM available).

Only tasks are allowed to use mutual exclusion semaphores (ISRs are not allowed).

µC/OS-III enables the user to nest ownership of mutexes. If a task owns a mutex, it can own the same mutex up to 250 times. The owner must release the mutex an equivalent number of times. In several cases, an application may not be immediately aware that it called OSMutexPend() multiple times, especially if the mutex is acquired again by calling a function as shown in Listing 13-12.

Nesting calls to OSMutexPend()
          OS_MUTEX           MyMutex;
          SOME_STRUCT        MySharedResource;
           
           
          void  MyTask (void *p_arg)
          {
              OS_ERR  err;
              CPU_TS  ts;
              
              :
              while (DEF_ON) {
                  OSMutexPend((OS_MUTEX *)&MyMutex,                      (1) 
                              (OS_TICK   )0,
                              (OS_OPT    )OS_OPT_PEND_BLOCKING,
                              (CPU_TS   *)&ts,
                              (OS_ERR   *)&err);
                  /* Check "err"                                 */      (2) 
                  /* Acquire shared resource if no error         */
                  MyLibFunction();                                       (3) 
                  OSMutexPost((OS_MUTEX *)&MyMutex,                      (7) 
                              (OS_OPT    )OS_OPT_POST_NONE,
                              (OS_ERR   *)&err);
                  /* Check "err"                                 */
              }
          }
          void  MyLibFunction (void)
          {
              OS_ERR  err;
              CPU_TS  ts;
           
           
              OSMutexPend((OS_MUTEX *)&MyMutex,                          (4) 
                          (OS_TICK   )0,
                          (OS_OPT    )OS_OPT_PEND_BLOCKING,
                          (CPU_TS   *)&ts,
                          (OS_ERR   *)&err);
              /* Check "err"                         */
              /* Access shared resource if no error  */                  (5)
              OSMutexPost((OS_MUTEX *)&MyMutex,                          (6) 
                          (OS_OPT    )OS_OPT_POST_NONE,
                          (OS_ERR   *)&err);
              /* Check "err"                         */
          }

(1) A task starts by pending on a mutex to access shared resources. OSMutexPend() sets a nesting counter to 1.

(2) You should check the error return value. If no errors exist, MyTask() owns MySharedResource.

(3) A function is called that will perform additional work.

(4) The designer of MyLibFunction() knows that, to access MySharedResource, it must acquire the mutex. Since the calling task already owns the mutex, this operation should not be necessary. However, MyLibFunction() could have been called by yet another function that might not need access to MySharedResource. µC/OS-III allows nested mutex pends, so this is not a problem. The mutex nesting counter is thus incremented to 2.

(5) MyLibFunction() can access the shared resource.

(6) The mutex is released and the nesting counter is decremented back to 1. Since this indicates that the mutex is still owned by the same task, nothing further needs to be done, and OSMutexPost() simply returns. MyLibFunction() returns to its caller.

(7) The mutex is released again and, this time, the nesting counter is decremented back to 0 indicating that other tasks can now acquire the mutex.


You should always check the return value of OSMutexPend() (and any kernel call) to ensure that the function returned because you properly obtained the mutex, and not because the return from OSMutexPend() was caused by the mutex being deleted, or because another task called OSMutexPendAbort() on this mutex.

As a general rule, do not make function calls in critical sections. All mutual exclusion semaphore calls should be in the leaf nodes of the source code (e.g., in the low level drivers that actually touches real hardware or in other reentrant function libraries).

There are a number of operations that can be performed on a mutex, as summarized in the table below. However, in this chapter, we will only discuss the three functions that are most often used: OSMutexCreate(), OSMutexPend(), and OSMutexPost(). Other functions are described in µC-OS-III API Reference.

Mutex API summary
Function NameOperation
OSMutexCreate()Create a mutex.
OSMutexDel()Delete a mutex.
OSMutexPend()Wait on a mutex.
OSMutexPendAbort()Abort the wait on a mutex.
OSMutexPost()Release a mutex.