IPC Selection Guide
Choosing the right synchronization primitive is crucial for system correctness and performance.
Decision Flowchart
flowchart TD
Start([Need to synchronize?]) --> Q1{Sharing data<br/>between threads?}
Q1 -->|Yes| Q2{Need to pass<br/>data?}
Q1 -->|No| Q3{Just signaling?}
Q2 -->|Yes| Q4{Fixed-size<br/>messages?}
Q2 -->|No| Mutex[Use Mutex]
Q4 -->|Yes| MsgQ[Use Message Queue]
Q4 -->|No| Q5{Need copies<br/>or pointers?}
Q5 -->|Copies| Pipe[Use Pipe]
Q5 -->|Pointers| Q6{FIFO or LIFO?}
Q6 -->|FIFO| FIFO[Use FIFO]
Q6 -->|LIFO| LIFO[Use LIFO]
Q3 -->|Yes| Q7{Multiple<br/>events?}
Q3 -->|No| Q8{Counting<br/>resources?}
Q7 -->|Yes| Event[Use Events]
Q7 -->|No| Q9{Binary<br/>signal?}
Q9 -->|Yes| Sem[Use Semaphore]
Q9 -->|No| CondVar[Use Condition Variable]
Q8 -->|Yes| Sem
Q8 -->|No| Mutex
style Mutex fill:#4a9eff
style Sem fill:#4a9eff
style CondVar fill:#4a9eff
style MsgQ fill:#22c55e
style FIFO fill:#22c55e
style LIFO fill:#22c55e
style Pipe fill:#22c55e
style Event fill:#f59e0b
Quick Reference Table
| Primitive | Data Transfer | ISR-Safe | Blocking | Use Case |
|---|---|---|---|---|
| Mutex | No | No | Yes (sleep) | Protect shared data |
| Semaphore | No | Give only | Yes (sleep) | Resource counting, signaling |
| Spinlock | No | Yes | Yes (busy) | ISR-thread sync, very short |
| Condition Variable | No | No | Yes (sleep) | Complex conditions |
| Message Queue | Fixed-size copy | Yes | Yes (sleep) | ISR→thread data |
| FIFO | Pointer | Yes | Yes (sleep) | Variable-size, zero-copy |
| LIFO | Pointer | Yes | Yes (sleep) | Stack-like access |
| Pipe | Byte stream | No | Yes (sleep) | Streaming data |
| Mailbox | Variable copy | No | Yes (sleep) | Synchronous exchange |
| Events | Bitmask | Yes | Yes (sleep) | Multiple conditions |
| Poll | Multiple objects | No | Yes (sleep) | Wait on any of several |
| Zbus | Typed messages | No | Yes (sleep) | Publish-subscribe, decoupled modules |
Common Patterns
Pattern 1: ISR to Thread Communication
Problem: ISR needs to send data to a processing thread.
Solution: Message Queue or FIFO
/* Best: Message Queue for fixed-size data */
K_MSGQ_DEFINE(sensor_q, sizeof(struct reading), 16, 4);
void sensor_isr(const void *arg)
{
struct reading data = { .value = read_adc() };
k_msgq_put(&sensor_q, &data, K_NO_WAIT);
}
void process_thread(void)
{
struct reading data;
while (1) {
k_msgq_get(&sensor_q, &data, K_FOREVER);
process(data);
}
}
Pattern 2: Producer-Consumer with Backpressure
Problem: Multiple producers, one consumer, need flow control.
Solution: Bounded Message Queue
K_MSGQ_DEFINE(work_queue, sizeof(struct work), 8, 4);
void producer(struct work *w)
{
/* Block if queue full - natural backpressure */
k_msgq_put(&work_queue, w, K_FOREVER);
}
void consumer(void)
{
struct work w;
while (1) {
k_msgq_get(&work_queue, &w, K_FOREVER);
do_work(&w);
}
}
Pattern 3: Resource Pool
Problem: Manage a pool of reusable buffers.
Solution: Semaphore + FIFO
#define POOL_SIZE 8
struct buffer { void *fifo_reserved; uint8_t data[128]; };
static struct buffer pool[POOL_SIZE];
K_FIFO_DEFINE(free_bufs);
K_SEM_DEFINE(pool_sem, POOL_SIZE, POOL_SIZE);
void init_pool(void)
{
for (int i = 0; i < POOL_SIZE; i++) {
k_fifo_put(&free_bufs, &pool[i]);
}
}
struct buffer *alloc_buf(k_timeout_t timeout)
{
if (k_sem_take(&pool_sem, timeout) == 0) {
return k_fifo_get(&free_bufs, K_NO_WAIT);
}
return NULL;
}
void free_buf(struct buffer *buf)
{
k_fifo_put(&free_bufs, buf);
k_sem_give(&pool_sem);
}
Pattern 4: State Machine Events
Problem: Thread needs to respond to multiple event types.
Solution: Kernel Events
#define EVT_START BIT(0)
#define EVT_STOP BIT(1)
#define EVT_DATA BIT(2)
#define EVT_ERROR BIT(3)
K_EVENT_DEFINE(sm_events);
void state_machine_thread(void)
{
while (1) {
uint32_t events = k_event_wait(&sm_events,
EVT_START | EVT_STOP | EVT_DATA | EVT_ERROR,
false, K_FOREVER);
if (events & EVT_ERROR) {
handle_error();
}
if (events & EVT_STOP) {
state = STOPPED;
}
if (events & EVT_START) {
state = RUNNING;
}
if (events & EVT_DATA) {
process_data();
}
}
}
Pattern 5: Request-Response
Problem: Thread sends request, waits for response.
Solution: Mailbox (synchronous) or Message Queue pair
/* Using Mailbox for synchronous exchange */
K_MBOX_DEFINE(request_mbox);
struct request { int cmd; int param; };
struct response { int status; int result; };
int send_request(struct request *req, struct response *resp)
{
struct k_mbox_msg msg = {
.size = sizeof(*req),
.tx_data = req,
.tx_target_thread = K_ANY
};
/* Send request and wait for ack */
k_mbox_put(&request_mbox, &msg, K_FOREVER);
/* Response comes back in same mailbox */
msg.size = sizeof(*resp);
msg.rx_source_thread = K_ANY;
k_mbox_get(&request_mbox, &msg, resp, K_FOREVER);
return resp->status;
}
Pattern 6: Publish-Subscribe (Decoupled Modules)
Problem: Multiple modules need to react to events without tight coupling.
Solution: Zbus
#include <zephyr/zbus/zbus.h>
/* Define message and channel */
struct sensor_data { int32_t temp; uint32_t ts; };
ZBUS_CHAN_DEFINE(sensor_chan, struct sensor_data, NULL, NULL,
ZBUS_OBSERVERS_EMPTY, ZBUS_MSG_INIT(0));
/* Publisher (sensor module) */
void sensor_publish(int32_t temp)
{
struct sensor_data msg = { .temp = temp, .ts = k_uptime_get_32() };
zbus_chan_pub(&sensor_chan, &msg, K_MSEC(100));
}
/* Subscriber (display module - doesn't know about sensor) */
ZBUS_SUBSCRIBER_DEFINE(display_sub, 4);
ZBUS_CHAN_ADD_OBS(sensor_chan, display_sub, 0);
void display_thread(void)
{
const struct zbus_channel *chan;
while (1) {
zbus_sub_wait(&display_sub, &chan, K_FOREVER);
struct sensor_data msg;
zbus_chan_read(chan, &msg, K_NO_WAIT);
update_display(msg.temp);
}
}
Pattern 7: Multiple Source Waiting
Problem: Thread needs to wait on multiple different objects.
Solution: Polling
K_SEM_DEFINE(uart_ready, 0, 1);
K_MSGQ_DEFINE(cmd_queue, 4, 8, 4);
K_FIFO_DEFINE(data_fifo);
struct k_poll_event events[3];
void multi_wait_thread(void)
{
k_poll_event_init(&events[0], K_POLL_TYPE_SEM_AVAILABLE,
K_POLL_MODE_NOTIFY_ONLY, &uart_ready);
k_poll_event_init(&events[1], K_POLL_TYPE_MSGQ_DATA_AVAILABLE,
K_POLL_MODE_NOTIFY_ONLY, &cmd_queue);
k_poll_event_init(&events[2], K_POLL_TYPE_FIFO_DATA_AVAILABLE,
K_POLL_MODE_NOTIFY_ONLY, &data_fifo);
while (1) {
k_poll(events, 3, K_FOREVER);
if (events[0].state == K_POLL_STATE_SEM_AVAILABLE) {
k_sem_take(&uart_ready, K_NO_WAIT);
handle_uart();
}
/* ... handle other events ... */
/* Reset states */
for (int i = 0; i < 3; i++) {
events[i].state = K_POLL_STATE_NOT_READY;
}
}
}
Anti-Patterns to Avoid
Don’t: Use Spinlocks for Long Operations
/* BAD - spinlock held too long */
k_spinlock_key_t key = k_spin_lock(&lock);
process_large_buffer(); /* Could take milliseconds! */
k_spin_unlock(&lock, key);
/* GOOD - use mutex instead */
k_mutex_lock(&mutex, K_FOREVER);
process_large_buffer();
k_mutex_unlock(&mutex);
Don’t: Block in ISR
/* BAD - will crash or hang */
void my_isr(const void *arg)
{
k_mutex_lock(&mutex, K_FOREVER); /* NO! */
k_msgq_put(&queue, &data, K_FOREVER); /* NO! */
}
/* GOOD - use non-blocking operations */
void my_isr(const void *arg)
{
k_msgq_put(&queue, &data, K_NO_WAIT); /* OK */
k_sem_give(&sem); /* OK */
}
Don’t: Nest Mutexes Inconsistently
/* BAD - deadlock risk */
void thread_a(void) {
k_mutex_lock(&mutex1, K_FOREVER);
k_mutex_lock(&mutex2, K_FOREVER); /* A holds 1, wants 2 */
}
void thread_b(void) {
k_mutex_lock(&mutex2, K_FOREVER);
k_mutex_lock(&mutex1, K_FOREVER); /* B holds 2, wants 1 = DEADLOCK */
}
/* GOOD - consistent ordering */
void thread_a(void) {
k_mutex_lock(&mutex1, K_FOREVER);
k_mutex_lock(&mutex2, K_FOREVER);
}
void thread_b(void) {
k_mutex_lock(&mutex1, K_FOREVER); /* Same order */
k_mutex_lock(&mutex2, K_FOREVER);
}
Performance Considerations
| Operation | Typical Cost | Notes |
|---|---|---|
| Mutex lock (uncontended) | ~100 cycles | Fast path optimized |
| Mutex lock (contended) | Context switch | Thread sleeps |
| Semaphore give/take | ~50-100 cycles | Very lightweight |
| Spinlock | ~10-50 cycles | Disables interrupts |
| Message queue put/get | ~200-500 cycles | Involves copy |
| FIFO put/get | ~100 cycles | Pointer only |
| Event post/wait | ~100-200 cycles | Bitmask operations |
Memory Usage
| Primitive | Approximate Size | Per-Item Cost |
|---|---|---|
| Mutex | 24-32 bytes | N/A |
| Semaphore | 16-24 bytes | N/A |
| Message Queue | 32 bytes + buffer | msg_size × count |
| FIFO/LIFO | 8-16 bytes | 0 (user provides) |
| Event | 16-24 bytes | N/A |
| Pipe | 32 bytes + buffer | buffer_size |
Summary Checklist
Before choosing a primitive, ask:
- Is ISR involved? → Use ISR-safe primitives (semaphore give, msgq, fifo, events)
- Passing data or just signaling? → Data: msgq/fifo/pipe; Signal: sem/events
- Fixed or variable size? → Fixed: msgq; Variable: fifo/pipe
- Need copies or zero-copy? → Copies: msgq/pipe; Zero-copy: fifo
- Multiple conditions? → Events or polling
- Need priority inheritance? → Mutex only
- Synchronous exchange? → Mailbox
- Decoupled modules? → Zbus (publish-subscribe pattern)
Next Steps
Learn about Zbus for publish-subscribe messaging, or continue to Part 5: Device Drivers to learn about hardware interaction.