Session 5 · Operating Systems

Pipes, named pipes and message queues

Moving data between processes: pipes and why every process closes the ends it does not use, sending ints, strings and structs with the right sizes, named pipes with mkfifo, and System V message queues with typed messages.

17 min readpipe() and EOFClosing unused endsFraming datamkfifomsgsnd / msgrcv by type

Signals can tell a process that something happened, but they carry no data. This session is about moving data between processes. After fork() the parent and the child have separate memory – a variable changed in one is not changed in the other – so they need the kernel to pass bytes between them.

Three tools do that: an anonymous pipe between related processes, a named pipe (FIFO) that any process can open by its file name, and a message queue that keeps whole, typed messages. They look similar, but they differ in exactly the details that make programs hang: who holds which end, when a read returns, and what happens to the boundaries between messages.

The model in one breath: a pipe is a one-way byte stream in the kernel; every process closes the ends it does not use, and the reader must know how many bytes to expect. A message queue keeps message boundaries and lets the receiver pick messages by type.

pipe(): two ends of a kernel buffer

pipe(fd) creates a buffer inside the kernel and gives you two file descriptors for it: fd[0] to read and fd[1] to write (think of 0 as stdin, 1 as stdout). Bytes written into fd[1] come out of fd[0] in the same order (Fig. 1). You use them with the ordinary read(), write() and close() calls from the first session.

int fd[2];
if (pipe(fd) == -1) {
    perror("pipe");
    exit(EXIT_FAILURE);
}
/* fd[0]: the read end    fd[1]: the write end */
writerwrite(fd[1], …)fd[1]pipe buffer (kernel)bytes, first in – first outfd[0]readerread(fd[0], …)empty → read() waits · full (64 KiB by default on Linux) → write() waitsevery write end closed → read() returns 0: end of fileevery read end closed → write() gets SIGPIPE (or EPIPE)
Fig. 1 · A pipe is a first-in first-out byte buffer in the kernel, with a write end and a read end.

The rules that decide whether your program works or hangs:

SituationWhat happens
pipe empty, read()Blocks until some data arrives – then returns what is there, possibly less than you asked for.
pipe full, write()Blocks until the reader makes room.
every write end closed, read()Returns 0: end of file. This is how the reader learns that no more data will come.
every read end closed, write()The writer gets SIGPIPE (default: terminate), or -1 with EPIPE if it ignores the signal.

A pipe has no name, so the only way to share it is inheritance: create it before fork(), and the child gets copies of both descriptors.

Close what you do not use

Right after fork() both processes hold both ends: four descriptors point at one pipe (Fig. 2). For a pipe from parent to child, the parent closes the read end and the child closes the write end. What remains is one writer and one reader.

right after fork()pipeparentfd 3 → read endfd 4 → write endchildfd 3 → read endfd 4 → write endafter closing the unused endspipeparentfd 3 → closedfd 4 → write endchildfd 3 → read endfd 4 → closed
Fig. 2 · Left: after fork() each process has both ends. Right: each closed the end it does not need.

This is not tidiness. read() returns end of file only when every write end is closed – in every process. A child that loops until EOF but forgot to close its own inherited write end is waiting for itself: the writer that keeps the pipe alive is the reader (Fig. 3).

the child forgot its own write endparent: close(fd[1])done writingchild: fd[1] still openone writer is leftchild: read() blocksforeverevery process closed what it does not useparent: close(fd[1])done writingchild: close(fd[1])right after fork()child: read() → 0end of file, loop ends
Fig. 3 · One forgotten write end – even in the reader itself – and the read loop never ends.

Parent writes, child reads until end of file

pid_t pid = fork();
if (pid == 0) {                                /* child: reader */
    close(fd[1]);                              /* not writing – close it, or EOF never comes */
    char buf[64];
    ssize_t n;
    while ((n = read(fd[0], buf, sizeof(buf))) > 0)
        write(STDOUT_FILENO, buf, n);
    close(fd[0]);
    exit(EXIT_SUCCESS);
}
close(fd[0]);                                  /* parent: writer, not reading */
write(fd[1], "hello\n", 6);
close(fd[1]);                                  /* this is the child's EOF */
waitpid(pid, NULL, 0);

Parent and child: one pipe per direction

A pipe is one-way. If the parent asks and the child answers, you need two pipes (Fig. 4). Using one pipe for both directions does not work: a process can read back its own message before the other side gets it.

ParentChildio_pipes1parent → childwrite [1]read [0]an int: the random numberio_pipeschild → parentwrite [1]read [0]a string: the replyone pipe, one direction – two-way talk needs two pipes
Fig. 4 · The example of the session: the parent sends a random number through one pipe, the child replies through the other.

In the session example the parent sends a random number, the child reads it and sends back a sentence. The parent closes the child's ends right after fork(), so if the child dies without answering, the parent's read() returns 0 instead of waiting forever, and the buffer is terminated by the number of bytes actually read:

pipe_more.c – the parent side

pid_t child_pid = child(io_pipes1[0], io_pipes[1]);   /* child reads 1[0], writes [1] */

close(io_pipes1[0]);            /* the parent uses neither of the child's ends */
close(io_pipes[1]);

write(io_pipes1[1], &r, sizeof(r));                  /* send an int */

ssize_t n = read(io_pipes[0], pufferchar, sizeof(pufferchar) - 1);
if (n == -1) { perror("read"); exit(EXIT_FAILURE); }
pufferchar[n] = '\0';           /* n == 0 (EOF): empty string, not garbage */

pipe_more.c – the child side

int arrived;
ssize_t got = read(pipe_id_rec, &arrived, sizeof(int));
if (got != sizeof(int)) {                     /* -1: error, 0: EOF, less: partial */
    fprintf(stderr, "no complete number arrived\n");
    exit(EXIT_FAILURE);
}
char str[] = "Child kuldi a szulonek: Megkaptam a szamot!";
write(pipe_id_send, str, sizeof(str));        /* sizeof an ARRAY: all chars + '\0' */

The child checks that it got a whole int: read() returning 0 or 2 bytes would leave arrived half-filled with garbage. Finally the parent waits for the child with waitpid() – a pipe does not replace waiting.

Sending numbers, strings and structs

A pipe moves raw bytes. You can write the memory of any variable – an int, an array, a struct – as long as the reader reads the same number of bytes into a variable of the same type: write(fd, &r, sizeof(r)) on one side, read(fd, &x, sizeof(x)) on the other. Strings need care, because the size is easy to get wrong:

sizeof of an array vs a pointer

char  text[] = "hello";       /* an array of 6 bytes */
char *msg    = "hello";       /* a pointer to them */

write(fd, text, sizeof(text));      /* 6 bytes: h e l l o \0 – correct */
write(fd, msg, sizeof(msg));        /* 8 bytes on 64-bit: the size of the POINTER – wrong */
write(fd, msg, strlen(msg) + 1);    /* 6 bytes – correct for a pointer */

The second problem is boundaries. A pipe is a stream: it does not remember where one write() ended. Two writes can arrive in one read(), and one large write can arrive in several (Fig. 5). Writes of up to PIPE_BUF bytes (4096 on Linux) are atomic – they are not mixed with other writers' data – but a read may still return less than you asked for.

two writes, one read: the boundary is gonewrite "abc"write "defg"abcdefgread(…, 64) → abcdefgframing: the length first, then exactly that many byteslen = 6hello\0reader: 4 bytes (len), then 6 bytes
Fig. 5 · The pipe forgets write boundaries. Agree on sizes: fixed-size records, or a length followed by the data.

So the reader must know how many bytes belong together. Either both sides use fixed sizes (an int, a char[100], a struct), or the sender sends the length first:

Framing a string with its length

/* sender */
int len = strlen(name) + 1;
write(fd, &len, sizeof(len));       /* first: how many bytes follow */
write(fd, name, len);               /* then: exactly that many */

/* receiver */
int len;
read_full(fd, &len, sizeof(len));
read_full(fd, buf, len);            /* buf must be at least len bytes */

And the reader loops until it has the whole thing:

read_full – read exactly size bytes

/* read exactly size bytes, or fail: a pipe may return less than asked for */
int read_full(int fd, void *buf, size_t size)
{
    char *p = buf;
    while (size > 0) {
        ssize_t n = read(fd, p, size);
        if (n == -1 && errno == EINTR) continue;   /* interrupted by a signal: retry */
        if (n <= 0) return -1;                     /* error, or EOF too early */
        p += n;
        size -= n;
    }
    return 0;
}

Structs and arrays of structs

A struct is sent as its bytes. That works only if the struct contains values, not pointers: a char *name is an address in the sender's memory and means nothing in the receiver. Use char name[50]. An array of structs is one block of memory, so a single write() sends all of them:

Sending a count, then an array of structs

struct worker {
    int  id;
    char name[50];          /* arrays, not char * – a pointer means nothing in another process */
    char address[50];
};

struct worker team[3] = { {1, "Bela", "Budapest"}, {2, "Janos", "Miskolc"}, {3, "Mark", "Szeged"} };
int count = 3;
write(fd, &count, sizeof(count));
write(fd, team, count * sizeof(struct worker));      /* the whole array in one write */

Child to child

Two children cannot create a pipe for each other – a pipe created inside child 1 is invisible to child 2. The parent creates all pipes before it forks, and every process closes the ends it does not use (Fig. 6). The parent too: if it keeps the write end of the child-to-child pipe open, child 2 never sees end of file.

Parentkeeps p1[1]p1nChild 1p1[0] · p12[1]p12n + valuesChild 2keeps p12[0]parent closes: p1[0], p12[0], p12[1]child 1 closes: p1[1], p12[0]child 2 closes: p1[0], p1[1], p12[1]Both pipes are created before the first fork() – a pipe made later is invisible to children that already exist.
Fig. 6 · Parent → child 1 → child 2: two pipes, created before the forks, and a list of what each process closes.

Who keeps which end

int p1[2], p12[2];            /* parent -> child1, child1 -> child2 */
pipe(p1);
pipe(p12);                      /* both BEFORE the forks, so every child inherits them */

if (fork() == 0) {              /* child1 */
    close(p1[1]); close(p12[0]);        /* keeps: p1 read, p12 write */
    /* read from p1[0], write to p12[1] */
}
if (fork() == 0) {              /* child2 */
    close(p1[0]); close(p1[1]); close(p12[1]);   /* keeps: p12 read */
    /* read from p12[0] until EOF */
}
close(p1[0]); close(p12[0]); close(p12[1]);      /* parent keeps: p1 write */

Named pipes (FIFO)

An anonymous pipe exists only as inherited descriptors. A named pipe, or FIFO, also has a name in the file system, so any process that knows the name can open it – related or not (Fig. 7). mkfifo(path, mode) creates the name; from then on it is used with the ordinary open(), read(), write() and close().

file system/tmp/4711type p · size 0writerany processreaderany processopen(O_WRONLY)open(O_RDONLY)kernel bufferthe data goes herewritereadopen() waits until the other side opens too · unlink() removes the name when you are done
Fig. 7 · The FIFO's name lives in the file system; the data goes through the kernel, as with an ordinary pipe.

pipe_named.c – creating the FIFO

char pipename[32];
snprintf(pipename, sizeof(pipename), "/tmp/%d", getpid());   /* unique per run */

if (mkfifo(pipename, S_IRUSR | S_IWUSR) == -1) {             /* 0600: only the owner */
    perror("mkfifo");           /* EEXIST: a file with this name is already there */
    exit(EXIT_FAILURE);
}

The mode S_IRUSR | S_IWUSR (0600) lets only the owner open it – reduced further by the umask, as for any new file. The name contains the PID, so two runs (or two students on the same server) do not collide in /tmp. If the name already exists, mkfifo fails with EEXIST.

The important difference from a file: open() blocks. Opening for reading waits until someone opens it for writing, and the other way round. That is the synchronisation point – the two processes meet at open().

pipe_named.c – writer (child) and reader (parent)

/* child – writer */
int fd = open(pipename, O_WRONLY);     /* blocks until someone opens it for reading */
write(fd, "Test value\n", 12);
close(fd);

/* parent – reader */
int fd = open(pipename, O_RDONLY);     /* blocks until someone opens it for writing */
ssize_t n = read(fd, pufferchar, sizeof(pufferchar) - 1);
pufferchar[n > 0 ? n : 0] = '\0';
close(fd);
unlink(pipename);                       /* remove the name – the FIFO file stays otherwise */

The FIFO's name stays in the file system after the processes exit, so remove it with unlink() when you are done (the open descriptors keep working until closed). From the shell you can try the same thing with two commands:

$ mkfifo /tmp/demo
$ ls -l /tmp/demo
prw-r--r-- 1 student student 0 … /tmp/demo     <- type p, size 0: nothing is stored on disk
$ cat /tmp/demo &                              <- the reader waits
$ echo hello > /tmp/demo                       <- the writer: cat prints hello
$ rm /tmp/demo

System V message queues

A message queue is a list of messages kept by the kernel. Unlike a pipe it keeps message boundaries – one msgsnd is one msgrcv – and every message carries a type, a positive long that the receiver can select by (Fig. 8).

child 1child 2msgsndqueue (msqid) · oldest firsttype 5hellotype 4replytype 5datatype 1stopmsgrcv(…, 5, …)→ type 5: hellomsgrcv(…, 0, …)→ oldest: hellomsgrcv(…, -4, …)→ lowest ≤ 4: stopthe queue lives in the kernel until IPC_RMID
Fig. 8 · Messages wait in sending order; the type argument of msgrcv decides which one a receiver takes.

Creating or opening a queue

A queue is identified by a numeric key. ftok(path, id) makes one from an existing file's identity and a non-zero project number – every process that calls it with the same path and number gets the same key. msgget(key, 0600 | IPC_CREAT) creates the queue if it does not exist yet and returns its ID (msqid); the 0600 are the permissions, as for a file.

key_t key = ftok(".", 1);             /* an existing path + a non-zero project id */
if (key == -1) { perror("ftok"); exit(EXIT_FAILURE); }

int queue = msgget(key, 0600 | IPC_CREAT);   /* create it, or open the existing one */
if (queue < 0) { perror("msgget"); exit(EXIT_FAILURE); }

The message and its size

The message is your own struct, with one rule: it starts with a long mtype. After that comes the payload – text, an array, anything. The size you pass to msgsnd and msgrcv counts only the payload, never the long in front of it (Fig. 9).

#define MSG_SIZE 1024

struct message {
    long mtype;               /* must be > 0 – the receiver selects by it */
    char mtext[MSG_SIZE];     /* the payload: any bytes, any layout you like */
};
struct messagelong mtypethe type, > 0char mtext[MSG_SIZE]the payload – text, numbers, a structmsgsz: only this partnot countedmsgsnd(queue, &ms, strlen(ms.mtext) + 1, 0);msgsnd(queue, &uz2, sizeof(uz2.marr), 0);
Fig. 9 · msgsz counts the payload after mtype – passing sizeof(struct message) is one long too many.

msg-queue.c – the child sends a type-5 text message

struct message ms;
ms.mtype = 5;
strcpy(ms.mtext, "This is a test sentence.");

/* size = the payload only, WITHOUT the long mtype; +1 for the closing '\0' */
if (msgsnd(queue, &ms, strlen(ms.mtext) + 1, 0) < 0) {    /* 0: block if the queue is full */
    perror("msgsnd");
    exit(EXIT_FAILURE);
}

mtype must be greater than 0, otherwise msgsnd fails with EINVAL. The last argument is the flag: 0 blocks while the queue is full, IPC_NOWAIT returns -1 with EAGAIN instead.

Receiving by type

msg-queue.c – the parent waits for type 5

struct message ms;
/* the first message of type 5; blocks until there is one */
ssize_t len = msgrcv(queue, &ms, MSG_SIZE, 5, 0);
if (len < 0)
    perror("msgrcv");
else
    printf("type %ld: %s\n", ms.mtype, ms.mtext);
msgtyp argumentmsgrcv takes
0the oldest message, whatever its type
> 0the oldest message of exactly that type
< 0the message with the lowest type that is ≤ the absolute value

msgrcv blocks until a matching message exists (unless IPC_NOWAIT), and returns the number of payload bytes. If the message is longer than the size you gave, it fails with E2BIG and the message stays in the queue. With types, one queue can serve several conversations: in msg-queue2.c the parent sends type 5 and listens for type 4, the child the other way round, so neither reads back its own message. The payload there is an int array:

msg-queue2.c – an int array as payload

struct uzenet2 {
    long mtype;
    int  marr[5];
};
struct uzenet2 uz2 = {5, {1, 2, 3, 4, 5}};

msgsnd(queue, &uz2, sizeof(uz2.marr), 0);                  /* 20 bytes: the array only */
msgrcv(queue, &uz2, sizeof(uz2.marr), 5, 0);               /* in the other process */

Removing the queue

A queue is not tied to a process. It stays in the kernel – with any unread messages – after every process has exited, until someone removes it or the machine restarts. A stale queue from a crashed run is found again by the next run with the same key, old messages included. Remove it when the work is done:

/* after every process is done with it – usually the parent, after waitpid */
if (msgctl(queue, IPC_RMID, NULL) < 0)
    perror("msgctl");

Finding and removing leftover queues

$ ipcs -q                       # your message queues
------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x0118063d 3          student    600        25           1
$ ipcrm -q 3                    # remove the one with msqid 3

Which one to use

PipeNamed pipe (FIFO)Message queue
Who can use itrelated processes (inherited over fork)any process that knows the pathany process that knows the key
Unitbyte streambyte streamwhole messages with a type
Boundarieslost – you frame the datalost – you frame the datakept: one send, one receive
Select what to readno – strictly in orderno – strictly in orderyes – by mtype
End of dataread() returns 0read() returns 0no EOF – agree on a "last" message
Lifetimeuntil the last descriptor is closedthe name: until unlink()until IPC_RMID or restart

A rule of thumb: a pipe for a stream from one process to another, two pipes for a question and an answer, a message queue when several kinds of messages share a channel or a receiver must pick a particular one. Shared memory, the fastest of all, comes in the next session.

pipe
A one-way byte buffer in the kernel with a read end and a write end, shared by inheritance.
EOF
End of file: read() returns 0 because every write end is closed.
framing
Telling the reader where one piece of data ends – fixed sizes or a length prefix.
FIFO
A named pipe: a pipe with a name in the file system, created with mkfifo.
mtype
The positive long at the start of a message; msgrcv selects messages by it.
IPC_RMID
The msgctl command that deletes a message queue.

Practice

Task 1

Counting together

The parent and one child count to three together, taking turns – the parent first. The parent does not send text to tell the child to count: it sends a value of an enum (for example COUNTING) through a pipe. A sleep(1) in the parent's loop helps keep the output in order. The parent waits for the child to finish.

Parent says: 1
Child says: 1
Parent says: 2
Child says: 2
Parent says: 3
Child says: 3
Approach

An enum is an integer, so it is sent like one, with sizeof. The child loops on read() and prints its own counter for every command; the loop ends at end of file, when the parent closes its write end. sleep(1) only makes the right order very likely; a second pipe on which the child answers “done” makes it certain.

enum command { COUNT, STOP };

/* parent */
for (int i = 1; i <= 3; i++) {
    printf("Parent says: %d\n", i);
    fflush(stdout);
    enum command c = COUNT;
    write(to_child[1], &c, sizeof(c));
    read(from_child[0], &c, sizeof(c));      /* wait for "done" – stricter than sleep(1) */
}

Task 2

Child to child

The parent has two children. It reads a number from the keyboard (assume the input is valid) and sends it through a pipe to child 1, which prints it. Child 1 then sends the number on to child 2, followed by that many random numbers. Child 2 prints the number and then every random number it receives. The parent waits for both children.

$ ./child_communications
3
Child_1 read message: 3
Child_2 read message: 3
Child_2 read random number: 749
Child_2 read random number: 489
Child_2 read random number: 529
Approach

Two pipes, both created before the first fork() (Fig. 6). Read the number in the parent before forking or after – but fflush(stdout) before each fork(). Seed the random generator in child 1 (for example with getpid()). Child 2 reads the count first, then exactly that many values.

/* child2: first the count, then exactly that many numbers */
int n;
read_full(p12[0], &n, sizeof(n));
for (int i = 0; i < n; i++) {
    int value;
    if (read_full(p12[0], &value, sizeof(value)) == -1) break;
    printf("Child_2 read random number: %d\n", value);
}

Task 3

Structs through a pipe

The parent hands out work. Workers are structs with an ID, a name and an address – fill in a few by hand. The parent sends the name of the job (a string), then how many workers it needs (an int), then the workers themselves – if you can, all of them with a single write(). The child prints the job and the number, then every worker's data. The parent waits for the child.

Child: the job Disinfection is done by 3 workers.
Received: Id: 1 and Name: Bela and Address: Budapest
Received: Id: 2 and Name: Janos and Address: Miskolc
Received: Id: 3 and Name: Mark and Address: Szeged
Approach

Three pieces, three agreed sizes: a fixed char[100] (or a length prefix) for the job name, an int, then count * sizeof(struct worker) bytes. The struct holds arrays, not pointers. On the receiving side allocate the array after the count has arrived, and read it with a full-read loop.

char job[100];
int count;
read_full(fd, job, sizeof(job));                    /* fixed-size name: both sides agree on 100 */
read_full(fd, &count, sizeof(count));

struct worker *list = malloc(count * sizeof(struct worker));
read_full(fd, list, count * sizeof(struct worker));
/* … print them … */
free(list);

Summary

  • pipe(fd): fd[0] reads, fd[1] writes; create it before fork() so the child inherits it.
  • Right after fork(), every process closes the ends it does not use. read() returns 0 only when every write end is closed.
  • One pipe, one direction: two-way talk needs two pipes; child-to-child pipes are created by the parent before both forks.
  • A pipe forgets write boundaries. Use fixed sizes or a length prefix, check what read() returned, and loop until you have it all. sizeof of a pointer is not the length of the string.
  • A FIFO is a pipe with a name: mkfifo, open() blocks until the other side opens, unlink() at the end.
  • Message queues keep boundaries and types: ftok → msgget → msgsnd/msgrcv (size without mtype, type > 0) → msgctl(IPC_RMID).
  • fflush(stdout) before fork(), and wait for every child – no IPC replaces waitpid.