Linux Message-Based IPC Explained-Free Linux Device Drivers Course In Hyderabad

PREV_LECNEXT_LEC

Linux Message-Based IPC Explained

FIFOs, Unix sockets, and POSIX message queues compared — with a working demo you can build today

Any two processes running on Linux live in completely separate address spaces. Neither one can see the other’s memory, variables, or file descriptors unless the kernel deliberately builds a bridge between them. That bridge is called Inter-Process Communication, or IPC, and it is one of the most practical topics in embedded Linux development — every multi-process application, from an init system talking to a supervisor daemon, to a sensor-reading process feeding data to a display process, depends on it.

This lecture is part of our free Linux kernel development course and focuses on the “copy the message” family of IPC — FIFOs, Unix domain sockets, and POSIX message queues. If you’re building embedded Linux systems and want a structured, no-cost path through kernel and user-space internals, this is one stop in a longer free embedded Linux course we publish at EmbeddedPathashala.

What You Will Learn

  • Two fundamental IPC strategies: copying vs sharing
  • FIFO (named pipe) mechanics and limits
  • Unix domain sockets: stream vs datagram
  • POSIX message queues and priority ordering
  • A working POSIX message queue demo in C
  • Choosing the right IPC mechanism for your project

Prerequisites

You should be comfortable with basic C, the Linux command line, and file descriptor concepts (open, read, write, close). No kernel module knowledge is required for this lecture — everything here is user-space IPC, though the kernel implements the underlying plumbing.

Two Ways Processes Can Talk

Every IPC mechanism on Linux boils down to one of two underlying strategies:

  1. Copy the data. The kernel moves bytes from the sending process’s memory into a kernel-managed holding area, then later copies those bytes out into the receiving process’s memory. This is what FIFOs, sockets, and message queues all do. It costs two copies per message but requires no synchronization primitives beyond what the kernel already provides.
  2. Share the memory. A single region of physical memory is mapped into the address space of two or more processes at once. No copying happens after the initial setup, but now the processes must coordinate access themselves, usually with a semaphore or mutex, or they will corrupt each other’s writes.

This lecture covers the first strategy. The next lecture in this series covers POSIX shared memory, the second strategy.

Copy-Based IPC — Data Flow

Process A (sender) Kernel Process B (receiver) +—————-+ +———————-+ +—————-+ | user buffer | write() | FIFO / socket / | read() | user buffer | | “hello” | ——-> | mqueue holding area | ——->| “hello” | +—————-+ +———————-+ +—————-+ Two copies occur: user space -> kernel, then kernel -> user space.

FIFOs (Named Pipes)

A FIFO — short for First In, First Out — is what most people call a named pipe. It is the same mechanism the shell uses internally when you write command1 | command2, except a FIFO has a name on the filesystem so unrelated processes can open it, not just a parent and its child.

Create one with mkfifo(1) or the mkfifo(3) library call. Once created, it behaves like any other file for permission purposes: whoever has read or write access to the FIFO’s inode can use it. Internally, though, no data is ever stored on disk — the FIFO is backed entirely by an in-kernel buffer.

PropertyValue
DirectionalityUni-directional (one reader side, one writer side)
Message boundariesNone — pure byte stream
Default kernel buffer size65536 bytes on current kernels
Resize mechanismfcntl(fd, F_SETPIPE_SZ, size), capped by /proc/sys/fs/pipe-max-size
Atomicity guaranteeWrites up to the pipe’s buffer size are atomic — never interleaved with another writer’s bytes
Priority supportNone

Because a FIFO has no message boundaries, the reader has to agree with the writer on a framing convention — newline-delimited text, a fixed record size, or a length-prefixed protocol are the three common choices.

Unix Domain Sockets

Unix domain sockets use the familiar Berkeley sockets API (socket(), bind(), connect(), accept()) but with address family AF_UNIX instead of AF_INET. Traffic never touches a network stack — it’s routed entirely inside the kernel — which makes them faster and simpler to secure than loopback TCP for local IPC.

They come in two flavours:

TypeDirectionalityMessage boundariesTypical use
SOCK_STREAMBi-directionalNone — byte stream, like TCPLong-lived client/server sessions (e.g. D-Bus, systemd)
SOCK_DGRAMUni-directional per packetPreserved — reliable, ordered discrete messagesRequest/response, logging sockets (e.g. syslog)

An important and often-missed detail: Unix domain datagrams are reliable — unlike UDP over IP, they are not silently dropped or reordered by the local kernel, since there’s no real network in the path. Maximum datagram size is governed by /proc/sys/net/core/wmem_max and is commonly 200 KiB or more on modern distributions — always check the live value on your target rather than assuming a fixed number.

POSIX Message Queues

POSIX message queues are the only mechanism in this group with a built-in concept of priority. Each message carries an integer priority (0 to MQ_PRIO_MAX, typically 32768), and the queue always delivers the highest-priority message first; messages of equal priority are delivered in FIFO order.

Queues are named starting with a single leading / and no further slashes — for example /ep-sensor-queue. They live in a pseudo-filesystem of type mqueue, which on most distributions you can inspect directly:

$ mount -t mqueue
mqueue on /dev/mqueue type mqueue (rw,relatime)
$ ls /dev/mqueue
ep-sensor-queue

Key tunables live under /proc/sys/kernel/:

TunableMeaningTypical default
msg_maxMax messages a single queue can hold10
msgsize_maxMax bytes per message8192

Because the queue descriptor returned by mq_open(3) is a normal file descriptor, you can multiplex it with poll(2), select(2), or epoll(2) alongside sockets and other file descriptors in the same event loop — a detail that makes message queues a clean fit for embedded event-driven designs.

Comparison Table

PropertyFIFOUnix socket (stream)Unix socket (datagram)POSIX message queue
DirectionalityUniBiUni per messageUni
Message boundariesNoneNonePreservedPreserved
PriorityNoneNoneNone0–32767
Typical max messageUnlimited (stream)Unlimited (stream)System-dependent, often 100+ KiB8 KiB default, up to 1 MiB tunable
PortabilityPOSIX systemsPOSIX systemsPOSIX systemsPOSIX systems, less universally implemented

Hands-On: A POSIX Message Queue Demo

Let’s build a tiny two-program demo: ep_mq_sender pushes a few priority-tagged messages into a queue, and ep_mq_receiver drains them, proving that higher-priority messages arrive first regardless of send order.

/* ep_mq_common.h */
#ifndef EP_MQ_COMMON_H
#define EP_MQ_COMMON_H

#define EP_QUEUE_NAME   "/ep-demo-queue"
#define EP_MAX_MSG_SIZE 256
#define EP_MAX_MSGS     10

#endif
/* ep_mq_sender.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <mqueue.h>
#include <fcntl.h>
#include <sys/stat.h>
#include "ep_mq_common.h"

int main(void)
{
    struct mq_attr attr = {
        .mq_flags   = 0,
        .mq_maxmsg  = EP_MAX_MSGS,
        .mq_msgsize = EP_MAX_MSG_SIZE,
        .mq_curmsgs = 0,
    };

    mqd_t mq = mq_open(EP_QUEUE_NAME, O_CREAT | O_WRONLY, 0644, &attr);
    if (mq == (mqd_t)-1) {
        perror("mq_open");
        return 1;
    }

    /* Sent in this order, priority given last: low, high, medium */
    mq_send(mq, "low-priority reading",    strlen("low-priority reading"),    1);
    mq_send(mq, "URGENT: overtemp alert",  strlen("URGENT: overtemp alert"),  9);
    mq_send(mq, "routine status update",   strlen("routine status update"),  5);

    printf("ep_mq_sender: 3 messages queued\n");
    mq_close(mq);
    return 0;
}
/* ep_mq_receiver.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <mqueue.h>
#include <fcntl.h>
#include "ep_mq_common.h"

int main(void)
{
    mqd_t mq = mq_open(EP_QUEUE_NAME, O_RDONLY);
    if (mq == (mqd_t)-1) {
        perror("mq_open");
        return 1;
    }

    char buf[EP_MAX_MSG_SIZE];
    unsigned int prio;

    for (int i = 0; i = 0) {
            buf[n] = '\0';
            printf("received (priority %u): %s\n", prio, buf);
        }
    }

    mq_close(mq);
    mq_unlink(EP_QUEUE_NAME);
    return 0;
}

Build and Run

$ gcc -o ep_mq_sender ep_mq_sender.c -lrt
$ gcc -o ep_mq_receiver ep_mq_receiver.c -lrt
$ ./ep_mq_sender
ep_mq_sender: 3 messages queued
$ ./ep_mq_receiver
received (priority 9): URGENT: overtemp alert
received (priority 5): routine status update
received (priority 1): low-priority reading

Notice the receive order: priority 9 first, then 5, then 1 — completely independent of the send order. That’s the priority-then-age ordering guarantee POSIX message queues give you, and it’s the one feature none of the other mechanisms in this lecture offer natively.

Common Mistakes and Troubleshooting

“Function not implemented” on mq_open — the mqueue kernel module/filesystem isn’t mounted. Check with mount | grep mqueue; most desktop distros mount it automatically, but a minimal embedded rootfs may need mount -t mqueue none /dev/mqueue added to your init sequence.

Forgetting mq_unlink — message queues, like shared memory segments, persist in the kernel after your process exits until explicitly unlinked or the system reboots. Stale queues from crashed test runs are a very common source of confusing “queue already exists” errors.

Undersized mq_msgsize — the attribute struct passed to the first mq_open that creates the queue fixes the max message size for that queue’s lifetime. A later mq_open call with a larger size in its attr struct will not resize an existing queue.

Missing -lrt — on some older glibc versions, POSIX message queue and shared memory functions live in librt, not libc. If you get linker errors about mq_open, add -lrt to your link line.

Best Practices

  • Prefer Unix domain sockets for anything that needs bi-directional, session-oriented communication — they’re the most portable and best-understood option.
  • Reach for POSIX message queues specifically when you need built-in priority delivery, such as alert/alarm channels alongside routine telemetry.
  • Use FIFOs for the simplest possible producer/consumer pipe when you control both ends and don’t need message framing beyond a simple delimiter.
  • Always pair queue/socket/FIFO creation with proper cleanup (mq_unlink, unlink on the FIFO path) in your shutdown path, not just on the happy path.
  • Set explicit, conservative mq_maxmsg/mq_msgsize values on embedded targets — the defaults are tuned for general-purpose systems, not memory-constrained boards.

Performance Considerations

All three mechanisms in this lecture pay a two-copy cost per message: once into the kernel, once back out. For small, infrequent control messages — configuration changes, status events, alerts — this overhead is negligible. If you find yourself streaming large buffers (camera frames, audio blocks) at high rates through a FIFO or socket, that’s usually a sign you actually want shared memory, covered in the next lecture, with a small message-queue “doorbell” to signal when new data is ready.

Security Considerations

FIFOs and Unix sockets are protected by ordinary filesystem permissions on their path — set the owning user/group and mode deliberately rather than leaving them world-writable. POSIX message queue names are not filesystem paths in the traditional sense but are still subject to permission checks at mq_open time via the mode argument; audit that mode the same way you would a file’s permission bits.

Summary and Key Takeaways

Key Takeaways

  • Copy-based IPC (FIFOs, sockets, message queues) trades a two-copy overhead for simplicity — no manual synchronization needed.
  • FIFOs are the simplest but offer no message framing or priority.
  • Unix domain sockets are the most flexible and widely used, especially SOCK_STREAM for session-based protocols.
  • POSIX message queues are the only option here with native priority ordering, and their file-descriptor-based API integrates cleanly into poll/epoll event loops.

Conclusion

Choosing between a FIFO, a Unix socket, and a POSIX message queue isn’t about which one is “best” — it’s about matching the mechanism’s guarantees to what your application actually needs. If you need priority-ordered delivery, message queues win outright. If you need a familiar, flexible, bi-directional channel, Unix sockets are the safe default. And if you just need the simplest possible one-way pipe between two processes you control, a FIFO gets the job done with almost no code. In the next lecture, we move to the second IPC strategy entirely: sharing memory directly, and the synchronization discipline that comes with it.

Frequently Asked Questions

What is the main difference between a FIFO and a Unix domain socket?

A FIFO is uni-directional and has no message boundaries — it’s a pure byte stream. A Unix domain socket (stream type) is bi-directional and, in datagram mode, preserves message boundaries with reliable, ordered delivery.

Do POSIX message queues survive a process crash?

Yes. Like shared memory segments, a POSIX message queue persists in the kernel after the creating process exits, until it is explicitly removed with mq_unlink or the system reboots.

Can I use select() or poll() with a POSIX message queue?

Yes. mq_open returns a regular file descriptor, so it can be added to an fd_set for select, or registered with poll or epoll, alongside sockets and other descriptors.

Why did mq_open fail with “Function not implemented”?

This almost always means the mqueue pseudo-filesystem is not mounted. Check with mount | grep mqueue and mount it if missing — common on minimal embedded root filesystems.

What is the maximum message size for a POSIX message queue?

It’s controlled by /proc/sys/kernel/msgsize_max, defaulting to 8 KiB on many systems but tunable up to roughly 1 MiB. The value used when the queue is first created is fixed for that queue’s lifetime.

Are Unix socket datagrams reliable, unlike UDP over IP?

Yes. Because they never leave the local kernel, Unix domain datagrams are delivered reliably and in order — they don’t suffer the drops or reordering that real network UDP can experience.

Which IPC mechanism should I use for an embedded sensor-to-display pipeline?

For small, infrequent control and status messages, a POSIX message queue (if you need priority) or a Unix domain socket (if you don’t) both work well. For high-rate bulk data like image frames, prefer shared memory with a lightweight queue as a signalling channel.

Do I need root privileges to create a POSIX message queue?

No. Any user with appropriate permissions on the mqueue filesystem mount can create queues; access is then governed by the mode bits passed to mq_open, just like a regular file.

Continue Your Free Linux Kernel Journey

This lecture is part of EmbeddedPathashala’s free embedded systems course covering Linux kernel internals, device drivers, and system programming from the ground up.

Next Lecture Browse Full Course

PREV_LECNEXT_LEC

2 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *