Mutual Exclusion Semaphores
Mutex Utilization
Mutual Exclusion Semaphores or simply mutexes are used by tasks to gain exclusive access to a resource. Mutexes are binary semaphores that have additional features beyond the normal semaphores mechanism provided by µC/OS-II.
A mutex is used by your application code to reduce the priority inversion problem as described in Real-Time Systems Concepts. A priority inversion occurs when a low priority task owns a resource needed by a high priority task. In order to reduce priority inversion, the kernel can increase the priority of the low priority task to the priority of the higher priority task until the low priority task is done with the resource.
In order to implement mutexes, a real-time kernel needs to provide the ability to support multiple tasks at the same priority. Unfortunately, µC/OS-II doesn’t allow multiple tasks at the same priority. However, there is a way around this problem. What if a priority just above the high priority task was reserved by the mutex to allow a low priority task to be raised in priority.
Let’s use an example to illustrate how µC/OS-II mutexes work. Listing 8.1 shows three tasks that may need to access a common resource. To access the resource, each task must pend on the mutex ResourceMutex. Task #1 has the highest priority (10), task #2 has a medium priority (15) and task #3, the lowest (20). An unused priority just above the highest task priority (i.e., priority 9) will be reserved as the Priority Ceiling Priority (PCP).
Listing - Listing 8.1, Mutex utilization example
OS_EVENT *ResourceMutex;
OS_STK TaskPrio10Stk[1000];
OS_STK TaskPrio15Stk[1000];
OS_STK TaskPrio20Stk[1000];
void main (void)
{
INT8U err;
OSInit(); (1)
---------- Application Initialization ----------
ResourceMutex = OSMutexCreate(9, &err); (2)
OSTaskCreate(TaskPrio10, (void *)0, &TaskPrio10Stk[999], 10); (3)
OSTaskCreate(TaskPrio15, (void *)0, &TaskPrio15Stk[999], 15);
OSTaskCreate(TaskPrio20, (void *)0, &TaskPrio20Stk[999], 20);
---------- Application Initialization ----------
OSStart(); (4)
}
void TaskPrio10 (void *pdata)
{
INT8U err;
pdata = pdata;
while (1) {
--------- Application Code ----------
OSMutexPend(ResourceMutex, 0, &err);
------- Access common resource ------
OSMutexPost(ResourceMutex);
--------- Application Code ----------
}
}
void TaskPrio15 (void *pdata)
{
INT8U err;
pdata = pdata;
while (1) {
--------- Application Code ----------
OSMutexPend(ResourceMutex, 0, &err);
------- Access common resource ------
OSMutexPost(ResourceMutex);
--------- Application Code ----------
}
}
void TaskPrio20 (void *pdata)
{
INT8U err;
pdata = pdata;
while (1) {
--------- Application Code ----------
OSMutexPend(ResourceMutex, 0, &err);
------- Access common resource ------
OSMutexPost(ResourceMutex);
--------- Application Code ----------
}
}(1) & (2) As shown in main(), µC/OS-II is initialized and a mutex is created by calling OSMutexCreate(). You should note that OSMutexCreate() is passed the PCP (i.e., 9).
(3) & (4) The three tasks are then created and µC/OS-II is started.
Suppose that this application has been running for a while and that, at some point, task #3 accesses the common resource first and thus acquires the mutex. Task #3 runs for a while and then gets preempted by task #1. Task #1 needs the resource and thus attempts to acquire the mutex (by calling OSMutexPend()). In this case, OSMutexPend() notices that a higher priority task needs the resource and thus raises the priority of task #3 to 9 which forces a context switch back to task #3. Task #3 will proceed and hopefully release the resource quickly. When done with the resource, task #3 will call OSMutexPost() to release the mutex. OSMutexPost() will notice that the mutex was owned by a lower priority task that got its priority raised and thus, will return task #3 to it’s original priority. OSMutexPost() will notice that a higher priority task (i.e., task #1) needs access to the resource and will give the resource to task #1 and perform a context switch to task #1.
µC/OS-II's mutexes consist of three elements: a flag indicating whether the mutex is available (0 or 1), a priority to assign the task that owns the mutex in case a higher priority task attempts to gain access to the mutex, and a list of tasks waiting for the mutex.
µC/OS-II provides six services to access mutexes: OSMutexCreate(), OSMutexDel(), OSMutexPend(), OSMutexPost(), OSMutexAccept() and OSMutexQuery().
To enable µC/OS-II mutex services, you must set the configuration constants in OS_CFG.H. Specifically, table 8.1 shows which services are compiled based on the value of configuration constants found in OS_CFG.H. You should note that NONE of the mailbox services are enabled when OS_MUTEX_EN is set to 0. To enable specific features (i.e., service) listed in Table 8.1, simply set the configuration constant to 1. You will notice that OSMutexCreate(), OSMutexPend() and OSMutexPost() cannot be individually disabled like the other services. That’s because they are always needed when you enable µC/OS-II mutual exclusion semaphore management.
µC/OS-II Mutex Service | Enabled when set to 1 in |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Figure 8.1 shows a flow diagram to illustrate the relationship between tasks and a mutex. A mutex can only be accessed by tasks. Note that the symbology used to represent a mutex is a ‘key’. The ‘key’ symbology shows that the mutex is used to access shared resources.
Creating a Mutex, OSMutexCreate()
A mutex needs to be created before it can be used. Creating a mutex is accomplished by calling OSMutexCreate(). The initial value of a mutex is always set to 1 indicating that the resource is available. The code to create a mutex is shown in listing 8.2.
Listing - Listing 8.2, Creating a mutex
OS_EVENT *OSMutexCreate (INT8U prio, INT8U *err)
{
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cpu_sr;
#endif
OS_EVENT *pevent;
if (OSIntNesting > 0) { (1)
*err = OS_ERR_CREATE_ISR;
return ((OS_EVENT *)0);
}
#if OS_ARG_CHK_EN
if (prio >= OS_LOWEST_PRIO) { (2)
*err = OS_PRIO_INVALID;
return ((OS_EVENT *)0);
}
#endif
OS_ENTER_CRITICAL();
if (OSTCBPrioTbl[prio] != (OS_TCB *)0) { (3)
*err = OS_PRIO_EXIST;
OS_EXIT_CRITICAL();
return ((OS_EVENT *)0);
}
OSTCBPrioTbl[prio] = (OS_TCB *)1; (4)
pevent = OSEventFreeList; (5)
if (pevent == (OS_EVENT *)0) {
OSTCBPrioTbl[prio] = (OS_TCB *)0;
OS_EXIT_CRITICAL();
*err = OS_ERR_PEVENT_NULL;
return (pevent);
}
OSEventFreeList = (OS_EVENT *)OSEventFreeList->OSEventPtr; (6)
OS_EXIT_CRITICAL();
pevent->OSEventType = OS_EVENT_TYPE_MUTEX; (7)
pevent->OSEventCnt = (prio << 8) | OS_MUTEX_AVAILABLE; (8)
pevent->OSEventPtr = (void *)0; (9)
OSEventWaitListInit(pevent); (10)
*err = OS_NO_ERR;
return (pevent); (11)
}
}(1) OSMutexCreate() starts by making sure it’s not called from an ISR because that’s not allowed.
(2) OSMutexCreate() then verifies that the PIP is within valid ranged based on what you determined the lowest priority is for your application as specified in OS_CFG.H.
(3) OSMutexCreate() then checks to see that there isn’t already a task assigned to the PIP. A NULL pointer in OSTCBPrioTbl[] indicates for the Priority Inheritance Priority (PIP) is available.
(4) If an entry is available, OSMutexCreate() reserves the priority by placing a non-NULL pointer in OSTCBPrioTbl[prio]. This will prevent you from being able to use this priority to create other tasks or other mutexes using this priority.
(5) OSMutexCreate() then attempts to obtain an ECB (Event Control Block) from the free list of ECBs.
(6) The linked list of free ECBs is adjusted to point to the next free ECB.
(7) If there was an ECB available, the ECB type is set to OS_EVENT_TYPE_MUTEX. Other µC/OS-II services will check this field to make sure that the ECB is of the proper type. This prevents you from calling OSMutexPost() on an ECB that was created for use as a message mailbox, for example.
(8) OSMutexCreate() then set the mutex value to ‘available’ and the PIP is stored.
It is worth noting that the .OSEventCnt field is used differently. Specifically, the upper 8 bits of .OSEventCnt are used to hold the PIP and the lower 8 bits are used to hold either the value of the mutex when the resource is available (0xFF) or, the priority of the task that ‘owns’ the mutex (a value between 0 and 62). This prevents having to add extra fields in an OS_EVENT structure and thus reduces the amount of RAM needed by µC/OS-II.
(9) Because the mutex is being initialized, there are no tasks waiting for it.
(10) The wait list is then initialized by calling OSEventWaitListInit().
(11) Finally, OSMutexCreate() returns a pointer to the ECB. This pointer MUST be used in subsequent calls to manipulate mutexes (OSMutexPend(), OSMutexPost(), OSMutexAccept(), OSMutexDel() and OSMutexQuery()). The pointer is basically used as the mutex’s handle. If there were no more ECBs, OSMutexCreate() would have returned a NULL pointer.
Figure 8.2 shows the ECB just before returning from OSMutexCreate().
Deleting a Mutex, OSMutexDel()
The code to delete a mutex is shown in listing 8.3 and this service is available only if OS_MUTEX_DEL_EN is set to 1 in OS_CFG.H. This is a dangerous function to use because multiple tasks could attempt to access a deleted mutex. You should always use this function with great care. Generally speaking, before you would delete a mutex, you should first delete all the tasks that can access the mutex.
Listing - Listing 8.3, Deleting a mutex
OS_EVENT *OSMutexDel (OS_EVENT *pevent, INT8U opt, INT8U *err)
{
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cpu_sr;
#endif
BOOLEAN tasks_waiting;
if (OSIntNesting > 0) { (1)
*err = OS_ERR_DEL_ISR;
return (pevent);
}
#if OS_ARG_CHK_EN
if (pevent == (OS_EVENT *)0) { (2)
*err = OS_ERR_PEVENT_NULL;
return (pevent);
}
if (pevent->OSEventType != OS_EVENT_TYPE_MUTEX) { (3)
OS_EXIT_CRITICAL();
*err = OS_ERR_EVENT_TYPE;
return (pevent);
}
#endif
OS_ENTER_CRITICAL();
if (pevent->OSEventGrp != 0x00) { (4)
tasks_waiting = TRUE;
} else {
tasks_waiting = FALSE;
}
switch (opt) {
case OS_DEL_NO_PEND:
if (tasks_waiting == FALSE) { (5)
pevent->OSEventType = OS_EVENT_TYPE_UNUSED; (6)
pevent->OSEventPtr = OSEventFreeList; (7)
OSEventFreeList = pevent;
OS_EXIT_CRITICAL();
*err = OS_NO_ERR;
return ((OS_EVENT *)0); (8)
} else {
OS_EXIT_CRITICAL();
*err = OS_ERR_TASK_WAITING;
return (pevent);
}
case OS_DEL_ALWAYS: (9)
while (pevent->OSEventGrp != 0x00) { (10)
OS_EventTaskRdy(pevent, (void *)0, OS_STAT_MUTEX);
}
pevent->OSEventType = OS_EVENT_TYPE_UNUSED; (11)
pevent->OSEventPtr = OSEventFreeList; (12)
OSEventFreeList = pevent;
OS_EXIT_CRITICAL();
if (tasks_waiting == TRUE) { (13)
OS_Sched();
}
*err = OS_NO_ERR;
return ((OS_EVENT *)0); (14)
default:
OS_EXIT_CRITICAL();
*err = OS_ERR_INVALID_OPT;
return (pevent);
}
}(1) OSMutexDel() starts by making sure that this function is not called from an ISR because that’s not allowed.
(2)
(3) We then check the arguments passed to it– pevent cannot be a NULL pointer and pevent needs to point to a mutex.
(4) OSMutexDel() then determines whether there are any tasks waiting on the mutex. The flag tasks_waiting is set accordingly.
Based on the option (i.e., opt) specified in the call, OSMutexDel() will either delete the mutex only if no tasks are pending on the mutex (opt == OS_DEL_NO_PEND) or, delete the mutex even if tasks are waiting (opt == OS_DEL_ALWAYS).
(5)
(6)
(7) When opt is set to OS_DEL_NO_PEND and there is no task waiting on the mutex, OSMutexDel() marks the ECB as unused and the ECB is returned to the free list of ECBs. This will allow another mutex (or any other ECB based object) to be created. You will note that OSMutexDel() returns a NULL pointer L8.3(8) since, at this point, the mutex should no longer be accessed through the original pointer.
(9)
(10) When opt is set to OS_DEL_ALWAYS then all tasks waiting on the mutex will be readied. Each task will think it has access to the mutex. Of course, that’s a dangerous outcome since the whole point of having a mutex is to protect against multiple access of a resource. Again, you should delete all the tasks that can access the mutex before you delete the mutex.
(11)
(12) Once all pending tasks are readied, OSMutexDel() marks the ECB as unused and the ECB is returned to the free list of ECBs.
(13) The scheduler is called only if there were tasks waiting on the mutex.
(14) You will note that OSMutexDel() returns a NULL pointer since, at this point, the mutex should no longer be accessed through the original pointer.