ES: Inter-Process Communication (IPC)

Inter-Task Communication (IPC) is the set of RTOS mechanisms that allow tasks to safely exchange data and synchronize execution without corrupting shared resources.Proper IPC design ensures determinism, data integrity, and real-time responsiveness in multitasking embedded systems.

Inter-Task Communication (IPC) is the nervous system of an RTOS-based embedded system. Tasks do not exist in isolation; they cooperate, synchronize, exchange data, and coordinate timing. Without IPC, multitasking degenerates into isolated threads competing blindly for CPU time.

In embedded systems, IPC is not just about data exchange. It is about determinism, latency control, memory safety, and system integrity.

To understand IPC properly, we must begin with why it is required in the first place.


Why IPC Is Required

In a real embedded product, functionality is decomposed into independent tasks:

  • Sensor task reads ADC periodically
  • Communication task handles UART/Ethernet
  • Control task runs algorithm
  • Logging task stores data
  • UI task updates display

These tasks must cooperate.

Consider this simplified architecture:

txt
+-----------------+
|   Sensor Task   |----+
+-----------------+    |
                       v
                +--------------+
                | Control Task |
                +--------------+
                       |
                       v
                +--------------+
                |  Actuator    |
                +--------------+

The sensor produces data.

The control algorithm consumes data.

The actuator task must wait for a control decision.

If tasks cannot exchange information safely:

  • Data corruption occurs
  • Race conditions appear
  • System becomes non-deterministic
  • Priority inversion may happen
  • Real-time guarantees are lost

Thus IPC is required for two core reasons:

Data sharing

Synchronization

Once we accept that tasks must share information, the next question becomes: _How do they share memory safely?_


Shared Memory

The most primitive IPC mechanism is shared memory.

Two tasks access the same memory region:

txt
          RAM
   +------------------+
   |   Shared Buffer  |
   +------------------+
       ^          ^
       |          |
  Task A       Task B

Example:

cpp
volatile int sensor_value;

void SensorTask() {
    sensor_value = read_adc();
}

void ControlTask() {
    if(sensor_value > threshold) {
        trigger_alarm();
    }
}

This works — but it is dangerous.

Why?

Because access is not atomic. If:

  • Task A updates while Task B reads
  • An interrupt occurs mid-write
  • Compiler reorders operations

You get inconsistent data.

This leads us directly to the need for critical sections.


Critical Sections

A critical section is a protected region of code where shared resources are accessed.

In a single-core MCU, protection is often done by disabling interrupts:

cpp
enter_critical();
shared_variable++;
exit_critical();

Timeline:

txt
Time →
Task A:  |---- critical ----|
Interrupt:        (blocked)
Task B:               (waiting)

The idea is simple:

• Only one execution context accesses the resource at a time

• Prevent preemption during modification

However, disabling interrupts has consequences:

  • Increased interrupt latency
  • Jitter
  • Reduced responsiveness

Critical sections must be:

• Very short

• Deterministic

• Non-blocking

But disabling interrupts is a low-level tool. For task-level synchronization, RTOS kernels provide structured primitives such as mutexes and semaphores.


Mutex vs Binary Semaphore

At first glance, a mutex and a binary semaphore look identical — both can hold values 0 or 1.

But their semantics differ.

Binary Semaphore

A binary semaphore is a signaling mechanism.

Example use case:

  • ISR signals a task
  • One task signals another

txt
Semaphore = 0

ISR:
  give(semaphore)

Task:
  take(semaphore) → blocks until given

Binary semaphores are about event signaling, not ownership.

They do not track who owns them.


Mutex

A mutex is a mutual exclusion lock with ownership.

txt
Mutex State:
   Unlocked
   Locked by Task X

Only the owning task can unlock it.

Important difference:

• Mutex supports priority inheritance

• Binary semaphore typically does not

Why Priority Inheritance Matters

Consider:

txt
High Priority Task ----+
                       |
Medium Priority Task   |
                       v
Low Priority Task (holds mutex)

If low priority holds mutex and high priority waits for it,

but medium priority keeps running — high priority is starved.

This is priority inversion.

A proper mutex boosts the priority of the low-priority task temporarily.

This is why:

• Use mutex for protecting shared resources

• Use binary semaphore for signaling events

Once binary signaling is understood, we can extend it further with counting semaphores.


Counting Semaphores

A counting semaphore maintains an integer count > 1.

It is useful for:

• Resource pools

• Producer-consumer with limited buffer slots

• Tracking available units

Example: 5 identical buffers available.

txt
Semaphore count = 5

Task:
  take() → decrement
  give() → increment

Representation:

txt
Available Buffers:
[ ][ ][ ][ ][ ]
 ^  ^  ^
 taken by tasks

Counting semaphores allow multiple simultaneous holders — unlike mutex.

They are not ownership-based.

This leads naturally to higher-level synchronization structures such as event groups.


Event Groups

Sometimes tasks must wait for multiple conditions simultaneously.

Example:

  • Network ready
  • Sensor calibrated
  • Storage mounted

Instead of using multiple semaphores, an event group uses bit flags.

code
Event Register (8-bit example)

Bit 0 → Network Ready
Bit 1 → Sensor Ready
Bit 2 → Storage Ready

Task waits for: WAIT_FOR( bit0 AND bit1 )

View:

txt
Current State: 0 1 1 0 0 0 0 0
               | | |
               | | +-- Storage Ready
               | +---- Sensor Ready
               +------ Network Ready

Event groups allow:

• Wait for ANY bit

• Wait for ALL bits

• Auto-clear bits

They are synchronization tools, not data transport tools.

For actual data passing, we move toward message-based mechanisms.


Message Queues

A message queue is a buffered communication channel.

Architecture:

txt
        Queue (FIFO)
+--------------------------------+
| Msg1 | Msg2 | Msg3 |    ...    |
+--------------------------------+
    ^                        ^
 Producer               Consumer

Properties:

• FIFO order

• Fixed message size

• Blocking send/receive

• Safe between tasks and ISR (if supported)

Example:

cpp
struct Data {
   int value;
   int timestamp;
};

send(queue, &data);
receive(queue, &data);

Queues provide:

• Decoupling between producer and consumer

• Temporal isolation

• Flow control

If queue fills:

  • Sender blocks
  • Or drops message (depending on design)

Queues are excellent for structured data passing.

But sometimes we only need a single message slot.


Mailboxes

A mailbox is a single-slot message container.

code
Mailbox:
+-------------+
|  Pointer    |
+-------------+

It usually transfers a pointer to data rather than copying data.

Example:

code
Task A:
   ptr = &buffer;
   post(mailbox, ptr);

Task B:
   ptr = pend(mailbox);

Mailboxes are useful when:

• Only the latest message matters

• Overwriting old data is acceptable

• Memory copying must be avoided

They are lighter than queues.

But when continuous byte streams are needed — for example UART data — stream buffers are more appropriate.


Stream Buffers

Stream buffers are optimized for continuous data flow.

Unlike message queues:

• No fixed message boundaries

• Byte-oriented

• Circular buffer internally

Architecture:

code
Circular Buffer

   Head →
+-----------------------+
| A | B | C | D | E | F |
+-----------------------+
                ← Tail

Used for:

• UART RX/TX

• Audio samples

• Network stacks

Producer writes bytes.

Consumer reads bytes.

Blocking occurs if:

  • Buffer full (write blocks)
  • Buffer empty (read blocks)

Stream buffers are efficient but still involve memory copying.

To reduce overhead further, we move toward zero-copy communication.


Zero-Copy Communication

In high-performance embedded systems, copying data costs:

• CPU cycles

• Cache pollution

• Memory bandwidth

Zero-copy avoids copying payload data.

Instead of: Producer → copy → Queue → copy → Consumer

We do:

code
Producer → allocate buffer
          → send pointer
Consumer → process directly
          → free buffer

Architecture:

code
Memory Pool
+----+----+----+----+
| B1 | B2 | B3 | B4 |
+----+----+----+----+

Flow:

  1. Producer takes buffer from pool
  2. Fills data
  3. Sends pointer via queue/mailbox
  4. Consumer processes
  5. Returns buffer to pool

This achieves:

• Minimal latency

• Deterministic timing

• Reduced memory footprint

• Better scalability

Zero-copy is common in:

  • Network stacks
  • DMA-based drivers
  • High-speed logging
  • Audio/video pipelines

But it requires disciplined memory ownership management.


IPC Architecture View

A mature embedded system combines multiple IPC mechanisms:

code
                +----------------+
                |   Event Group  |
                +----------------+
                       |
+--------+      +-------------+      +--------+
| Sensor |----->|   Queue     |----->|Control |
+--------+      +-------------+      +--------+
       |                |
       v                v
  Stream Buffer     Mailbox

Design rule in real systems:

• Use mutex for protection

• Use semaphore for signaling

• Use event groups for multi-condition sync

• Use queues for structured data

• Use stream buffers for byte streams

• Use zero-copy for high throughput

IPC Mechanisms Summary Table

MechanismPurposeData TransferOwnershipBlocking SupportTypical Use CaseReal-Time Notes
Shared MemoryDirect data sharingDirect memory accessNoneNo (needs protection)Fast data accessMust protect with critical section
Critical SectionProtect shared resourceN/AN/ANo (short only)Atomic variable updateIncreases interrupt latency
MutexMutual exclusionNo dataYes (owner-based)YesProtect peripheral, file systemSupports priority inheritance
Binary SemaphoreEvent signalingNo dataNoYesISR → Task notificationNo ownership tracking
Counting SemaphoreResource countingNo dataNoYesBuffer pool, resource poolMultiple simultaneous holders
Event GroupMulti-condition syncBit flagsNoYesWait for multiple eventsEfficient multi-bit wait
Message QueueStructured message passingCopy-basedNoYesProducer–ConsumerDeterministic FIFO
MailboxSingle message slotUsually pointerNoYesLatest-value transferLightweight queue
Stream BufferContinuous byte streamCopy-basedNoYesUART, audio dataCircular buffer based
mr!sM@sterZero-CopyHigh-performance transferPointer-basedManaged externallyYes (via queue/semaphore)Networking, DMAMinimizes latency & CPU load