Using RTOS semaphores - Part 1: Resource semaphores

Ralph Moore, Micro Digital, Inc.

November 01, 2014

Ralph Moore, Micro Digital, Inc.November 01, 2014

The semaphore is often disparaged because it cannot prevent unbounded priority inversion like its big brother, the mutex. However, semaphores can do things that big brother cannot do. This three-part series of blogs will discuss the different kinds of semaphores and how they can be applied to solving embedded systems problems. We will start with the resource semaphores.

Resource semaphores are used to regulate access to resources such as printers, critical code sections, and blocks of memory. There are two types of resource semaphores:

  • Binary resource semaphore
  • Multiple resource semaphore

Binary resource semaphore
A binary resource semaphore is used to control sharing a single resource between tasks. Its internal counter can have only the values of 1 (available) and 0 (unavailable). A semaphore test passes if the count is 1, in which case, the current task is allowed to proceed. If the count is 0, the semaphore test fails and the current task is enqueued in the semaphore’s task wait list, in priority order. When the semaphore is signaled, the first task (i.e. the longest-waiting, highest-priority task) is resumed, and the internal semaphore count remains 0. If no task is waiting, the count is increased to 1. The following is an example of using a binary resource semaphore:

     TCB_PTR t2a, t3a; // tasks
     SCB_PTR sbr; // binary resource semaphore

     void Init(void) {
        sbr = smx_SemCreate(RSRC, 1, "sbr");
        t2a = smx_TaskCreate(t2a_main, Pri2, 500, NO_FLAGS, "t2a"); // 500 byte stack
        t3a = smx_TaskCreate(t3a_main, Pri3, 500, NO_FLAGS, "t3a");
        smx_TaskStart(t2a);
     }

     void t2a_main(void) {
        if (smx_SemTest(sbr, TMO)) { // TMO = timeout in ticks
           smx_TaskStart(t3a);
           // use resource here
           smx_SemSignal(sbr);
        }
        else
           // deal with timeout or error
     }

     void t3a_main(void) {
        if (smx_SemTest(sbr, TMO)) {
           // use resource here
           smx_SemSignal(sbr);
        }
        else
           // deal with timeout or error
     }


In this example, semaphore sbr is created and its count is set to 1, making it a binary semaphore and as if it had already been signaled -- i.e., the resource is available. Task t2a (priority 2, task a) runs first and "gets" sbr. t2a then starts task t3a (priority 3, task a), which immediately preempts and tests sbr. Since sbr's count == 0, t3a is suspended on sbr, and t2a resumes. t2a uses the resource, then signals sbr, when it is done with the resource. This causes t3a to be resumed and to preempt t2a. t3a is now able to use the resource. When done, t3a signals sbr so that another task can use the resource and t3a stops. t2a resumes and stops. (The unnecessary task switching in this example can be prevented with task locking -- a subject for a future blog.)

A binary resource semaphore does the same thing as a mutex, but it has the following shortcomings:
  • It cannot do priority inheritance to prevent unbounded priority inversions among tasks.
  • If a task tests a semaphore that it already owns, it will be permanently blocked, as will all other tasks waiting on the same semaphore or testing it later.

The first defect may not be important in simple systems or if low-priority tasks do not share resources with high-priority tasks. The second defect is obviously very serious. The fact that it is a software bug is not of much consolation -- it could occur in a rare, untested path, or be introduced in a software update. To counter it, always specify timeouts on semaphore tests (e.g., TMO above) and test the return value, as shown in the first example. In the case of a timeout or other error, SemTest() returns FALSE, meaning that the test did not pass and the resource is not free. (Dealing with timeouts and errors is also a subject for a future blog.)

So why use binary resource semaphores? Because mutexes are more complicated, thus slower and use more code space. The complete semaphore service suite requires 1500 bytes of code space vs 2700 bytes for the complete mutex service suite. The latter adds to the former, if both semaphores and mutexes are needed. Hence, memory-limited systems often use binary semaphores instead of mutexes, but one must be careful of their limitations!

Multiple resource semaphore
The multiple resource semaphore is a generalization of the binary resource semaphore, intended to regulate access to multiple identical resources. It is commonly called a counting semaphore.This semaphore cannot be replaced by a mutex because the latter can only control access to one resource. The following is an example of using a multiple resource semaphore to control access to a block pool containing NUM blocks (for simplicity, timeouts and return value testing have been omitted -- this is NOT recommended in real code):

     TCB_PTR t2a, t3a; // tasks
     PCB_PTR blk_pool; // block pool
     SCB_PTR sr; // resource semaphore
     #define NUM 10
     #define SIZE 100

     void Init(void) {
        u8* p = (u8*)smx_HeapMalloc(NUM*SIZE);
        sb_BlockPoolCreate(p, blk_pool, NUM, SIZE, "blk pool");
        sr = smx_SemCreate(RSRC, NUM, "sr");
        t2a = smx_TaskCreate(t2a_main, Pri2, 500, NO_FLAGS, "t2a");
        t3a = smx_TaskCreate(t3a_main, Pri3, 500, NO_FLAGS, "t3a");
        smx_TaskStart(t2a);
     }

     void t2a_main(void) {
        u8* bp;
        smx_SemTest(sr, INF);
        bp = sb_BlockGet(blk_pool, 0));
        smx_TaskStart(t3a);
        // use bp to access block
        sb_BlockRel(blk_pool, bp, 0);
        smx_SemSignal(sr);
     }

     void t3a_main(void) {
        u8* bp;
        smx_SemTest(sr, INF);
        bp = sb_BlockGet(blk_pool, 0));
        // use bp to access block
        sb_BlockRel(blk_pool, bp, 0);
        smx_SemSignal(sr);
     }


This example is similar to the previous example, with small differences. The Init() function first creates a block pool of NUM blocks. It then creates sr, but because NUM is used instead of 1, a multiple resource semaphore is created, with a starting count of NUM. t2a tests sr and sr's counter is decremented to 9, which is greater than 0, so t2a is allowed to get a block. As before, t2a starts t3a, which immediately preempts. t3a tests sr and sr's counter is decremented to 8, so t3a is also allowed to get a block and use it. Eight more blocks can be obtained from blk_pool and used by the same or different tasks. However, the eleventh test of sr will suspend the testing task on sr. As shown, when t3a is done with its block, it releases the block back to blk_pool, then signals sr, thus allowing the first waiting task at sr to get the released block. Similarly for t2a.

If no task is waiting, sr’s count is incremented by each signal. Thus the count always equals the number of available blocks. The maximum possible count is NUM. Signals after the count == NUM are ignored in order to protect resources if redundant signals should occur. (This is a programming error, but is protected against, nonetheless.)

Summary
These are the two most common semaphores, but there are many more to come. In following blogs I will cover binary event and multiple event semaphores first, and then threshold and gate semaphores.

Read Part 2: Event semaphores

Ralph Moore, President of Micro Digital, Inc., graduated with a degree in physics from Caltech. He spent his early career in computer research. Then he moved into mainframe design in the 60's and became a consultant in the early 70's. He taught himself programming and became a microprocessor programmer. He founded Micro Digital in 1975, and many years of successful consulting led into architecting and developing the SMX kernel in 1987. For many years he managed the company’s business and sales, but in recent years he has been focused almost solely on v4 development of the SMX multitasking kernel.

References:
[1] SMX User’s Guide and Reference Manual

Loading comments...

Parts Search Datasheets.com

Sponsored Blogs