Named pipes (FIFOs)

Photo by Drew Sal on Unsplash

Named pipes (FIFOs)

IPC in Unix - part 3

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 reading
  • O_WRONLY for writing
  • O_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!

Did you find this article valuable?

Support Mladen Drmac by becoming a sponsor. Any amount is appreciated!