In the last days, the ELFs were quite busy optimizing the gift pipeline and did not have the time to do their yearly maintenance run on Santa's permanent infrastructure. Now, halfway through the Advent, the issue became quite pressing and an ELF started to walk around the site, inspecting the installations, whereby the catastrophe became clear: Santa's postbox, made from quite durable northern pine, has rotten away to an unidentifiable pile of wood dust. Therefore, it was decided to build a new postbox for Santa that also provides all kind of digital interfaces for all those children who would rather like to provide their wishlist with write()
instead of drawing some pictures. As the postbox should be rather versatile, it was decided to work two days on the postbox.
Today and tomorrow, we will talk about inter-process communication (IPC): How can we pass data between processes and/or we signal new messages to the other process. In our scenario, our postbox is a single-threaded server (with epoll) that accepts data on different communication channels. While we provide you with the epoll
-based framework (postbox.c
), it is your task to implement the specific channels. Today, we will start with (named) FIFOs and Unix domain sockets.
In the epoll exercise we already talked a little bit about pipe(7)s as we used them there to communicate data between processes. There, we created pipe pairs with pipe(2) and gave one end to our filter processes and kept the other end for reading or writing with splice(2). Of course for our postbox scenario, we miss this control instance that creates a pipe pair and gives the write end to our client (the child) and the read end to us (the postbox). Somehow, we have to use a different rendezvous mechanism where child and postbox meet, and that is where named pipes or fifo(7) come in handy.
Named pipes are a special kind of files that have a name but no content. Virtually, each FIFO is backed by an pipe pair and we get the read end if we open the FIFO with O_RDONLY
and the write end if we ask for O_WRONLY
. As named pipes are files after all, they also have permissions, which are checked when calling open()
. So, in the end, we use the file system and its capability to provide a hierarchical, privilege-checked namespace as an rendezvous place for processes that want to connect to each other via pipes. Isn't that cool? We have pipes as unidirectional connection between processes and just by hooking them into the file system, we created a powerful IPC pattern. And even better, as pipes are also used to stream between processes and files (think: cat .. | grep ... > foo
), we can send messages by simply echoing into the FIFO:
echo 1 > fifo-file
However, there is a downside: each named pipe has exactly one pipe pair, whereby only a single parallel connection is possible. Furthermore, when the client (the echo from above) closes its write end, the read end is also closed. While the first problem cannot be fixed, you have to reopen the FIFO again when your read()
returns zero.
To create a FIFO, we could use the library function mkfifo(3), however, we have started Advent(2) to not use the C library but to use the naked syscall interface. So how is mkfifo()
implemented? Let's take a look at the musl C library [musl: src/stat/mkfifo.c]:
int mkfifo(const char *path, mode_t mode) {
return mknod(path, mode | S_IFIFO, 0);
}
On Linux, mkfifo
is just a thin wrapper around the generic mknod(2) function, which can be used to create all kinds of nodes in the file system. With mknod()
, we cannot only create FIFOs, but also regular files, character-device files, block-device files, and others. And why not? Creating a node, no matter how special it is, should be possible with the same system call.
To overcome the one-connection limitation of named pipes, we also want to provide a second access path to our postbox with unix(7) domain sockets. As you might know socket(7)s are the way UNIX handles network connections; both, on the server and on the client. Most commonly, sockets are used for TCP (or UDP) connections, where server and client rendezvous via IP addresses and port numbers to establish a bidirectional connection. However, similar to named pipes, Unix provides a mean to rendezvous sockets via the file system with UNIX domain sockets. By the way, analogous to pipe()
, the socketpair(2) system call allows us to create an anonymous socket pair on the local machine.
To talk directly from the console with an domain socket, you can use netcat(1):
Complete the FIFO and the Unix domain socket connection module for our postbox. For FIFO, you have to implement fifo_prepare()
, which is called once, and fifo_handle()
, which is called if new data arrives on the FIFO. For domain sockets, domain_prepare()
initializes the socket and the framework invokes domain_accept()
on new connections, where you also have to register the client socket with epoll_add()
. In epoll_recv()
, you implement the data and connection handling for domain sockets. After today, the solution produces the following output, if messages are received on both channels:
Santas Postbox is open! Send your requests ...
... by fifo: echo 1 > fifo
... by socket: echo 2 | nc -U socket
... by signal: /bin/kill -USR1 -q 3 781565
... by mq_send: ./mq_send 4 (see also `cat /dev/mqueue/postbox`)
fifo: 1
socket[pid=781581,uid=10104,gid=10150]: 2
SO_PEERCRED
(see unix(7)) allows you identify your client process on an domain socket.sun_path = "\0foobar\0"
. Explore your system and the listening abstract sockets with ss -l 'src = unix:@*'
.Last modified: 2023-12-01 15:52:28.084420, Last author: , Permalink: /p/advent-12-postbox
Technische Universität Braunschweig
Universitätsplatz 2
38106 Braunschweig
Postfach: 38092 Braunschweig
Telefon: +49 (0) 531 391-0