Inter-Task Communication

Queues, Semaphores, Mutexes.

Mohith N
Updated: 19 March 2026
8 min read

In any real-time application, multiple tasks must exchange data and coordinate actions. If tasks communicate without proper synchronization, shared data can become corrupted, leading to unpredictable system behavior. The RTOS provides three fundamental inter-task communication primitives: queues, semaphores, and mutexes. Each serves a distinct purpose and understanding when to use each is critical for both embedded development and GATE preparation.

IPC Mechanisms OverviewQueueData transfer between tasksItem NItem N-1Item N-2(empty slot)Producer task writesConsumer task readsFIFO order by defaultBlocks if full/emptyCopies data by valueSemaphoreSignaling and synchronizationCount = 2Binary semaphore: 0 or 1Counting semaphore: 0 to Ngive() increments counttake() decrements countBlocks if count = 0No ownership conceptUse: ISR to task signalingMutexMutual exclusion for resourcesLOCKED / FREEBinary only (0 or 1)Has ownership conceptOnly owner can unlockSupports priority inheritancePrevents priority inversionNot usable from ISRUse: shared resource protectionQueue = data transfer | Semaphore = signaling | Mutex = resource locking with ownership
Figure 1: RTOS Inter-Task Communication Primitives - Queue, Semaphore, and Mutex

Core Concept Explanation

A queue is the primary data passing mechanism between tasks. It is a fixed-size FIFO buffer that stores complete copies of data items. A producer task writes items into the queue and a consumer task reads them. If the queue is full, the producer blocks (waits) until space is available. If the queue is empty, the consumer blocks until data arrives. This blocking behavior automatically synchronizes producer and consumer without any additional code.

A semaphore is a signaling primitive with an internal counter. A binary semaphore has a counter that can only be 0 or 1, and is used to signal events between tasks or from an ISR to a task. A counting semaphore has a counter that can range from 0 to a maximum value, and is used to track the availability of multiple identical resources (like a pool of buffers). The give() operation increments the count; the take() operation decrements it. If count is 0 when take() is called, the calling task blocks.

A mutex (mutual exclusion lock) is similar to a binary semaphore but with two critical differences. First, it has ownership: only the task that locked the mutex can unlock it. Second, it supports priority inheritance, which prevents priority inversion (discussed in the next article). Mutexes are used exclusively to protect shared resources such as global variables, peripheral registers, or memory buffers. Unlike semaphores, mutexes cannot be given from an ISR.

Mathematical Expression

The queue throughput is limited by the smaller of the producer and consumer rates. If the producer sends at rate Rp items per second and the consumer processes at rate Rc items per second, the queue will fill up if Rp exceeds Rc. For a queue of depth D items, the maximum burst the queue can absorb before blocking is D items. The time until the queue fills up from a burst of items at rate Rp with consumer at rate Rc is: T_fill = D / (Rp - Rc) seconds, valid when Rp is greater than Rc.

For semaphore-based synchronization, the wait latency is bounded by the priority of the task calling take() and the time the semaphore is unavailable. In a preemptive RTOS with proper priority assignment, a high-priority task will be unblocked within one context switch time after the semaphore is given.

Practical Understanding

In FreeRTOS, queues are created with xQueueCreate(length, item_size). Items are sent with xQueueSend() and received with xQueueReceive(). Both functions accept a timeout parameter: if the operation cannot complete within the timeout, the function returns errQUEUE_FULL or errQUEUE_EMPTY instead of blocking indefinitely. This prevents deadlocks in production systems.

A common pattern in embedded systems is ISR-to-task communication using binary semaphores or queues. When a hardware interrupt fires (for example, UART data received), the ISR should not process data directly because ISR execution time must be minimal. Instead, the ISR gives a semaphore or writes to a queue, which unblocks a dedicated handler task to process the data. This keeps interrupt latency minimal while allowing complex processing in task context.

Example
Given:
Producer task: sends sensor data at 500 items/second
Consumer task: processes data at 400 items/second
Queue depth: 20 items
Item size: 4 bytes each

Why this formula applies:
Queue fills up because producer is faster than consumer.
T_fill = D / (Rp - Rc)

Formula:
T_fill = Queue_depth / (Producer_rate - Consumer_rate)
Queue RAM = depth x item_size

Substitution:
T_fill = 20 / (500 - 400) = 20 / 100
Queue RAM = 20 x 4 = 80 bytes

Calculation:
T_fill = 0.2 seconds
Queue RAM = 80 bytes

Final Answer:
Queue fills up in 0.2 seconds. Producer will block after 0.2s.
Queue needs only 80 bytes of RAM.
Exam Tip: A key GATE trap - Semaphores have NO ownership. Any task can give a semaphore even if it did not take it. Mutexes have ownership: only the task that took the mutex can give it back. This is why mutexes support priority inheritance but semaphores do not.
Queue Data Flow and Semaphore ISR-to-Task SignalingQueue: Producer-ConsumerProducer TaskConsumer TaskQueuedata[0]data[1]emptyxQueueSendxQueueReceiveSemaphore: ISR to TaskHardware ISRHandler TaskSemaphorecount=0 (task blocked)xSemGiveFromISRxSemTakeMutex: Shared Resource ProtectionTask ATask BMUTEXTask A holds lockTask B blocksOnly Task A can unlockPriority inheritance: if neededUse queues for data, semaphores for events, mutexes for resource ownership
Figure 2: Queue, Semaphore, and Mutex Usage Patterns in RTOS Applications

Mechanism - How Each Primitive Works

  • Queue send: kernel copies item into queue buffer, increments write pointer. If a task was blocked waiting for data, kernel unblocks it and may trigger preemption.
  • Queue receive: kernel copies item out of queue buffer, decrements item count. If a task was blocked waiting for space, kernel unblocks it.
  • Semaphore give: kernel increments counter. If a task was blocked on take(), kernel moves it to Ready state.
  • Semaphore take: if counter is greater than 0, decrement and proceed. If counter is 0, block the calling task until give() is called.
  • Mutex take: if mutex is free, lock it and record owning task. If already locked, the calling task blocks. On mutex give(), ownership is released and waiting task unblocked.

Quick Revision

  • Queue: FIFO data buffer between tasks. Copies data. Blocks on full (producer) or empty (consumer). Suitable for structured data passing.
  • Binary semaphore: value 0 or 1. Used for event signaling. Can be given from ISR. No ownership.
  • Counting semaphore: value 0 to N. Used for resource pool management (N identical resources).
  • Mutex: binary, has ownership. Only locker can unlock. Supports priority inheritance. Cannot be given from ISR.
  • T_fill = Queue_depth / (Rp - Rc). Queue fills in this time when producer is faster than consumer.
  • Exam trap: Never use mutex from an ISR. Use binary semaphore for ISR-to-task signaling. This distinction appears frequently in GATE questions.

Inter-Task Communication Quiz

Test your understanding of queues, semaphores, and mutexes in RTOS inter-task communication.

Question 1 of 3

Q1.A binary semaphore and a mutex both allow mutual exclusion. What is the fundamental difference between them in an RTOS context?