Named pipes (FIFOs)
In the previous article we learned how to connect 2 processes using a pipe. Although interesting, that technique is limited because a process has to fork another process in order to connect to it via pipe.
If we want 2 unrelated processes to communicate, we need to explore other techniques. The first one we'll look at is called named pipes, or FIFOs. The acronym FIFO means that the order in which data is written ("First In") is the same order in which it will be read ("First Out").
FIFO is an IPC technique that allows unrelated processes to communicate using standard file operations, while ensuring data integrity and synchronization.
To see how, read on.
mkfifo() and mknod()
A FIFO is actually a file, and as such it needs to be created on disk before we can use it. All Unix files are represented by an inode
- a data structure storing metadata about the file. It contains the size, owner, permissions, location of the disk block that contains the contents of the file, etc. I would love to dig deeper in that topic, but to avoid too much digression, I'll leave it for a future article or series.
To create a FIFO, we can use a dedicated system call mkfifo()
that accepts the path to the file and the permission set, for example:
mkfifo("myfifo", S_IRUSR | S_IWUSR);
This call will create a new fifo inode in the current directory, which is readable and writable by the owner. The second argument allows for a combination of read, write and execute permissions for different audiences:
- User (owner):
S_IRUSR
,S_IWUSR
,S_IXUSR
- Group:
S_IRGRP
,S_IWGRP
,S_IXGRP
- Others:
S_IROTH
,S_IWOTH
,S_IXOTH
Here's how it looks in the listing of the ls
command. Notice the p
flag (it stands for pipe
), and the rw
permissions for the owner.
prw-r--r-- 1 root root 0 Jul 23 12:19 myfifo
While mkfifo()
is made specifically for creation of FIFOs, the mknod()
is more generic and is used to create a node (a file, a device special file, or a named pipe). In addition to setting permissions, we need to inform the system that we're creating a FIFO with S_IFIFO
flag. There's one more argument, but it's not relevant for creating FIFOs, so we can just pass 0.
mknod("myfifo", S_IFIFO | S_IRUSR | S_IWUSR, 0);
The only remaining system call is open()
- it accepts a file path and flags that specify how to open the file:
O_RDONLY
for readingO_WRONLY
for writingO_RDWR
for both reading and writing
It returns a file descriptor which is then used in read()
and write()
calls. By default, a call to open()
will block until the other side of the named pipe is opened. That behavior can be changed by passing a O_NDELAY
flag.
With system calls covered, we can look at the code sample. This one is a bit more involved because we want to create 2 processes that don't have a parent-child relationship. That's why our main process creates 2 child processes, a writer and a reader. Both of them reexecute the same program by using previously covered execlp()
call and pass an extra flag, -r
or -w
, to specify their roles.
You may notice an extra argument, as argc
is 2 when executing the main program and 3 when executing the reader/writer programs. That's because this code is ran as part of a bigger program that contains all the IPC demos, so argv[0]
is the name of the program (unixipc
), argv[1]
is the name of the demo (fifos
), and argv[2]
is the flag.
Here's how it all looks:
#define FIFO_PATH "myfifo"
void writer()
{
char buffer[1024];
int writefd;
printf("Writer started, opening %s FIFO for writing\n", FIFO_PATH);
writefd = open(FIFO_PATH, O_WRONLY);
printf("Enter a message: ");
fgets(buffer, sizeof(buffer), stdin);
write(writefd, buffer, strlen(buffer) + 1);
close(writefd);
exit(0);
}
void reader()
{
char buffer[1024];
int readfd;
printf("Reader started, opening %s FIFO for reading\n", FIFO_PATH);
readfd = open(FIFO_PATH, O_RDONLY);
read(readfd, buffer, sizeof(buffer));
printf("Received message: %s\n", buffer);
close(readfd);
exit(0);
}
void fifos(int argc, char *argv[])
{
if (argc == 2)
{
printf("Creating FIFO %s\n", FIFO_PATH);
mknod(FIFO_PATH, S_IFIFO | S_IRUSR | S_IWUSR, 0);
pid_t writer = fork();
if (writer == 0)
{
printf("Writer process created, reexecuting the example with -w argument\n");
execlp(argv[0], argv[0], argv[1], "-w", NULL);
}
pid_t reader = fork();
if (reader == 0)
{
printf("Reader process created, reexecuting the example with -r argument\n");
execlp(argv[0], argv[0], argv[1], "-r", NULL);
}
wait(NULL);
wait(NULL);
unlink(FIFO_PATH);
}
else if (argc == 3 && strcmp(argv[2], "-r") == 0)
{
reader();
}
else if (argc == 3 && strcmp(argv[2], "-w") == 0)
{
writer();
}
}
This example reads a string from the keyboard in one process, passes it via the named pipe to a second process, and shows it on the screen.
That's basically it for the FIFOs. In the next article we'll learn about file locking. Stay tuned!
Subscribe to my newsletter
Read articles from Mladen Drmac directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Mladen Drmac
Mladen Drmac
I am a software developer with experience in .NET, SQL, APIs, and to a lesser extent JavaScript and TypeScript. Recently I've started studying concepts and technologies related to distributed systems, containerization, event processing, event streaming, and NoSQL, and decided to document my studying in this form.