Signaling Semaphores and Priority Inheritance

Locking vs Signaling Semaphores

Locking Semaphores. POSIX counting semaphores have multiple uses. The typical usage is where the semaphore is used as lock on one or more resources. In this typical case, priority inheritance works perfectly: The holder of a semaphore count must be remembered so that its priority can be boosted if a higher priority task requires a count from the semaphore. It remains the holder until the same task calls sem_post() to release the count on the semaphore.

Mutual Exclusion Example. This usage is very common for providing mutual exclusion. The semaphore is initialized to a value of one. The first task to take the semaphore has access; additional tasks that need access will then block until the first holder calls sem_post() to relinquish access:

TASK A TASK B
sem_wait(sem);
have access
priority boost sem_wait(sem);
blocked
sem_post(sem);
priority restored have access

The important thing to note is that sem_wait() and sem_post() both called on the same thread, TASK A. When sem_wait() succeeds, TASK A becomes the holder of the semaphore and, while it is the holder of the semaphore (1) other threads, such as TASK B, cannot access the protected resource and (2) the priority of TASK A may be modified by the priority inheritance logic. TASK A remains the holder until is calls sem_post() on the same thread. At that time, (1) its priority may be restored and (2) TASK B has access to the resource.

Signaling Semaphores. But a very different usage model for semaphores is for signaling events. In this case, the semaphore count is initialized to zero and the receiving task calls sem_wait() to wait for the next event of interest to occur. When an event of interest is detected by another task (or even an interrupt handler), sem_post() is called which increments the count to 1 and wakes up the receiving task.

Signaling Semaphores and Priority Inheritance

Example. For example, in the following TASK A waits on a semaphore for events and TASK B (or perhaps an interrupt handler) signals task A of the occurence of the events by posting to that semaphore:

TASK A TASK B
sem_init(sem, 0, 0);
sem_wait(sem);
blocked
sem_post(sem);
Awakens as holder

Notice that unlike the mutual exclusion case above, sem_wait() and sem_post() are called on diferent threads.

Usage in Drivers. This usage case is used often within drivers, for example, when the user calls the read() method and there is no data available. sem_wait() is called to wait for new data to be received; sem_post() is called when the new data arrives and the user task is re-awakened.

Priority Inheritance Fails. These two usage models, the locking modeling and the signaling model, are really very different and priority inheritance simply does not apply when the semaphore is used for signalling rather than locking. In this signaling case priority inheritance can interfere with the operation of the semaphore. The problem is that when TASK A is awakened it is a holder of the semaphore. Normally, a task is removed from the holder list when it finally releases the semaphore via sem_post().

In this case, TASK B calls sem_post(sem) but TASK B is not the holder of the semaphore. Since TASK A never calls sem_post(sem) it becomes a permanently a holder of the semaphore and may have its priority boosted at any time when any other task tries to acquire the semaphore.

Who's to Blame In the POSIX case, priority inheritance is specified only in the pthread mutex layer. In NuttX, on the other hand, pthread mutexes are simply built on top of binary locking semaphores. Hence, in NuttX, priority inheritance is implemented in the semaphore layer.

In the case of a mutex this could be simply resolved since there is only one holder but for the case of counting semaphores, there may be many holders and if the holder is not the thread that calls sem_post(), then it is not possible to know which thread/holder should be released.

Selecting the Semaphore Protocol

sem_setprotocol(). The fix is to call non-standard NuttX function sem_setprotocol(SEM_PRIO_NONE) immediately after the sem_init(). The effect of this function call is to disable priority inheritance for that specific semaphore. There should then be no priority inheritance operations on this semaphore that is used for signalling.

sem_t sem

sem_init(&sem, 0, 0);
sem_setprotocol(&sem, SEM_PRIO_NONE);

Here is the rule: If you have priority inheritance enabled and you use semaphores for signaling events, then you must call sem_setprotocol(SEM_PRIO_NONE) immediately after initializing the semaphore.

Why Another Non-Standard OS Interface? The non-standard sem_setprotocol() is the moral equivalent of the POSIX pthread_mutexattr_setprotocol() and its naming reflects that relationship. In most implementations, priority inheritance is implemented only in the pthread mutex layer. In NuttX, on the other hand, pthread mutexes are simply built on top of binary locking semaphores. Hence, in NuttX, priority inheritance is implemented in the semaphore layer. This architecture then requires an interface like sem_setprotocol() in order to manage the protocol of the underlying semaphore.

pthread_mutexattr_setprotocol(). Since NuttX implements pthread mutexes on top of binary semaphores, the above recommendation also applies when pthread mutexes are used for inter-thread signaling. That is, a mutex that is used for signaling should be initialize like this (simplified, no error checking here):

pthread_mutexattr_t attr;
pthread_mutex_t mutex;

pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_PRIO_NONE);
pthread_mutex_init(&mutex, &attr);

Is this Always a Problem?

Ideally sem_setprotocol(SEM_PRIO_NONE) should be called for all signaling semaphores. But, no, often the use of a signaling semaphore with priority inversion is not a problem. It is not a problem if the signaling semaphore is always taken on the same thread. For example:

  • If the driver us used by only a single tasks, or
  • If the semaphore is only taken on the worker thread.

But this can be a serious problem if multiple tasks ever wait on the signaling semaphore. Drivers like the serial driver, for example, have many user threads that may call into the driver.