Linux IPC in Embedded Systems (Short Practical Guide)

IPC (Inter-Process Communication) lets processes exchange data and signals. In embedded Linux, IPC choice affects latency, memory use, complexity, and reliability.

Quick selection

  • Stream logs or simple producer->consumer: pipe / FIFO
  • Request/response between local services: Unix domain socket
  • Structured async messages with priorities: POSIX message queue
  • High-throughput low-latency data path: shared memory + semaphore/mutex
  • Simple notifications/events: signal or eventfd

1) Pipe/FIFO

Best for

  • Simple one-way byte stream
  • Shell-like pipelines or parent-child communication

Pros

  • Very simple API
  • Low overhead

Cons

  • Byte stream only (no message boundaries)
  • Multi-writer design becomes messy
// parent writes, child reads
int fd[2];
pipe(fd);

if (fork() == 0) {
    close(fd[1]);
    char buf[64] = {0};
    read(fd[0], buf, sizeof(buf));
    printf("child got: %s\n", buf);
    _exit(0);
}

close(fd[0]);
write(fd[1], "hello", 5);

2) Unix Domain Socket (AF_UNIX)

Best for

  • Local client/server daemons
  • Need bidirectional communication and framing

Pros

  • Faster than TCP loopback for local IPC
  • Supports stream or datagram modes
  • Can pass file descriptors (SCM_RIGHTS)

Cons

  • More setup than pipes
  • Protocol framing is your responsibility
// server side (minimal)
int s = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr = {.sun_family = AF_UNIX, .sun_path = "/tmp/ipc.sock"};
unlink(addr.sun_path);
bind(s, (struct sockaddr *)&addr, sizeof(addr));
listen(s, 4);
int c = accept(s, NULL, NULL);
write(c, "ok\n", 3);

3) POSIX Message Queue

Best for

  • Discrete messages (not raw streams)
  • Priority-based handling

Pros

  • Message boundaries preserved
  • Priority support

Cons

  • Queue size/limits must be tuned
  • Slightly more kernel/resource management
mqd_t q = mq_open("/sensor_q", O_CREAT | O_RDWR, 0644, NULL);
mq_send(q, "temp:42", 7, 1);
char msg[64];
unsigned prio;
ssize_t n = mq_receive(q, msg, sizeof(msg), &prio);

4) Shared Memory + Synchronization

Best for

  • High data rate (audio/video/sensor buffers)
  • Lowest copy overhead between processes

Pros

  • Very fast for large data
  • Good for ring buffers

Cons

  • Needs synchronization (mutex/semaphore/futex)
  • Harder to debug than queues/sockets
int fd = shm_open("/telemetry", O_CREAT | O_RDWR, 0644);
ftruncate(fd, 4096);
void *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// writer/reader must coordinate with a semaphore or mutex

5) Signals / eventfd

Best for

  • Lightweight notifications (“new data ready”, “stop now”)

Pros

  • Very low overhead event trigger

Cons

  • Signals carry tiny payload and are easy to misuse
  • Not for bulk data transfer
int efd = eventfd(0, 0);
uint64_t one = 1;
write(efd, &one, sizeof(one));   // notify
read(efd, &one, sizeof(one));    // consume

Embedded Linux practical notes

  • For deterministic behavior, avoid overloading one IPC channel for everything.
  • Use shared memory for data plane, and message queue/socket for control plane.
  • If hard real-time matters, pin threads/IRQs and measure worst-case latency.
  • Keep payloads fixed-size when possible; dynamic allocations increase jitter.

A practical default architecture

  • Sensor process -> shared memory ring buffer
  • Control process -> reads shared memory
  • Supervisor/service manager -> Unix socket for commands and health checks
  • Event notification -> eventfd or message queue

This split stays fast and maintainable on most embedded Linux products.

Final rule of thumb

  • Start with Unix sockets for clarity.
  • Move hot paths to shared memory only when profiling shows a real bottleneck.
  • Keep synchronization simple before making it clever.