Semaphores are synchronization primitives used to manage access to critical sections. A code segment protected by a semaphore-controlled critical section is restricted to a specified maximum number of concurrent tasks.
The osCreateSemaphore function is used to create a semaphore object. Semaphores can be created before system initialization, within an interrupt service routine, or by a running task. During creation, an optional name may be assigned to the semaphore, allowing tasks to open it using the osOpenSemaphore function. Both creation and opening functions return a handle. This handle is assigned by the system and serves as a unique identifier for the object; all subsequent operations on the semaphore require this handle. Handles that are no longer required should be closed using osCloseHandle. A semaphore object is deleted only after it has been closed by all tasks that held a handle to it. Further details can be found in the system objects management section.
If semaphores are not required in the application, the OS_USE_SEMAPHORE constant should be set to 0 to reduce the final binary size.
A semaphore maintains an internal counter that determines its signaling state. The semaphore is in the signaled state if the counter is greater than zero, and in the non-signaled state if the counter is zero. The counter is decremented each time a task acquires the semaphore and incremented when a task releases it. The counter cannot exceed a specified maximum value.
Both the maximum and initial counter values are defined during semaphore creation. The initial value must not exceed the maximum value. If the initial value is less than the maximum, the task that created the semaphore effectively reserves entries into the critical section equal to the difference between these values. To enter the critical section, a task must perform a wait operation, such as osWaitForObject or osWaitForObjects. A task may release the semaphore using the osReleaseSemaphore function as many times as it has acquired it, or fewer.
An error condition occurs if a task owning a critical section is terminated or if osCloseHandle is called on a semaphore while it is still held. This suggests that the resource protected by the critical section may be in an inconsistent state (e.g., a process was interrupted mid-execution). In such cases, the kernel automatically releases the object as if osReleaseSemaphore were called and marks it as abandoned. If another task subsequently acquires this abandoned semaphore, the wait operation will return a failure, and the last error code will be set to ERR_WAIT_ABANDON. While this status can be ignored if the user is certain the resource is safe, it is typically treated as an error to trigger recovery logic.
Priority inversion is another critical concern. If a low-priority task (L) holds a critical section and a medium-priority task (M) becomes ready, task L is preempted and task M executes. If a high-priority task (H) then becomes ready and attempts to acquire the same critical section, it will be blocked by task L. Without intervention, task M would continue running, effectively delaying task H. To prevent this, the kernel employs a priority inheritance algorithm. When priority inversion is detected, the kernel temporarily elevates the priority of task L to match the highest priority among the tasks waiting for the resource (task H). This allows task L to finish its operation and release the critical section quickly. Once released, task L's original priority is restored, and task H can proceed.
In poorly designed systems, priority inversion can occur across a chain of multiple tasks owning and waiting for various critical sections. In these instances, the kernel requires linear time to update priorities across the chain. This latency is dangerous for real-time systems and should be mitigated through proper system design.
Deadlocks can also occur during critical section acquisition in improperly designed systems. Sirius RTOS is capable of detecting deadlocks within regions controlled by mutexes and semaphores. If a deadlock is detected, the wait operation will terminate, and the error code will be set to ERR_WAIT_DEADLOCK.
If multiple tasks are waiting for a semaphore, the task with the highest priority will be the first to acquire it. If all waiting tasks have lower priority than the task that just released the semaphore, they will be allowed to proceed when they are ready to run. However, if a higher-priority task (including the one that just released the semaphore) begins waiting, it will preempt the others and acquire the semaphore immediately. Tasks with equal or lower priority are appended to the end of the pending queue.
Semaphores should be used specifically when priority inversion protection is required. While robust applications should be designed to avoid priority inversion entirely, if its occurrence is impossible by design, it is more efficient to use counting semaphores instead. The memory footprint of a semaphore object depends on the maximum number of tasks that can concurrently wait for it.