FIFO Open Semantics Blocking/Nonblocking open(), O_RDWR, Edge Cases

 

๐Ÿ”“ FIFO Open Semantics
Blocking/Nonblocking open(), O_RDWR, Edge Cases
Part 7 of 9
FIFO Semantics
Topic
open() behavior
Level
Intermediate+

FIFO open() โ€” A Rendezvous Point

Opening a FIFO is special โ€” it acts as a rendezvous between the reader and writer. The kernel makes both processes wait at the open() call until both ends are connected. This provides implicit synchronization.

Understanding the exact rules for blocking vs nonblocking FIFO opens is important for avoiding race conditions and unexpected program hangs.

Key Concepts

blocking open() O_NONBLOCK ENXIO O_RDWR rendezvous EOF on FIFO re-open FIFO

๐Ÿ“‹ FIFO open() Blocking Rules โ€” Complete Reference
Flags No matching open on other end Matching open exists
O_RDONLY (blocking) Blocks until a writer opens Returns immediately
O_WRONLY (blocking) Blocks until a reader opens Returns immediately
O_RDONLY | O_NONBLOCK Returns immediately (fd valid) Returns immediately
O_WRONLY | O_NONBLOCK Fails: errno = ENXIO Returns immediately
O_RDWR Returns immediately (single process holds both ends) Returns immediately
Important: O_RDWR on a FIFO is not specified by POSIX but works on Linux. It means the single process holds both the read and write ends โ€” useful for preventing EOF when the last writer closes (see below).

๐Ÿ’ป Example 1: Demonstrate Blocking open() Behavior
/* fifo_open_demo.c โ€” shows blocking open() */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <time.h>

#define FIFO_PATH "/tmp/ep_blocking_fifo"

int main(void)
{
    mkfifo(FIFO_PATH, 0666);

    pid_t pid = fork();

    if (pid == 0) {
        /* Child: waits 2 seconds, then opens as writer */
        printf("[Child] Sleeping 2 seconds before opening as writer...\n");
        sleep(2);

        printf("[Child] Opening FIFO as writer (O_WRONLY)...\n");
        int fd = open(FIFO_PATH, O_WRONLY);
        printf("[Child] Writer opened! Sending data...\n");

        const char *msg = "Sent from child after 2-second delay";
        write(fd, msg, strlen(msg));
        close(fd);
        _exit(0);

    } else {
        /* Parent: opens as reader โ€” will BLOCK for ~2 seconds */
        printf("[Parent] Opening FIFO as reader (O_RDONLY)...\n");
        printf("[Parent] Blocking now โ€” waiting for writer...\n");

        time_t t1 = time(NULL);
        int fd = open(FIFO_PATH, O_RDONLY);   /* BLOCKS HERE ~2 seconds */
        time_t t2 = time(NULL);

        printf("[Parent] Reader opened after ~%ld second(s) wait!\n", t2 - t1);

        char buf[128];
        ssize_t n = read(fd, buf, sizeof(buf) - 1);
        buf[n] = '\0';
        printf("[Parent] Received: \"%s\"\n", buf);

        close(fd);
        wait(NULL);
    }

    unlink(FIFO_PATH);
    return 0;
}
/* Output:
   [Parent] Opening FIFO as reader (O_RDONLY)...
   [Parent] Blocking now โ€” waiting for writer...
   [Child] Sleeping 2 seconds before opening as writer...
   [Child] Opening FIFO as writer (O_WRONLY)...
   [Child] Writer opened! Sending data...
   [Parent] Reader opened after ~2 second(s) wait!
   [Parent] Received: "Sent from child after 2-second delay"
*/

๐Ÿ’ป Example 2: Nonblocking open() on FIFO
/* fifo_nonblock_open.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/stat.h>

#define FIFO_PATH "/tmp/ep_nonblock_fifo"

int main(void)
{
    mkfifo(FIFO_PATH, 0666);

    printf("=== Test 1: O_RDONLY | O_NONBLOCK (no writer) ===\n");
    int fd_r = open(FIFO_PATH, O_RDONLY | O_NONBLOCK);
    if (fd_r == -1)
        printf("Reader open failed: %s\n", strerror(errno));
    else
        printf("Reader opened immediately (fd=%d) โ€” no blocking even without writer!\n", fd_r);

    printf("\n=== Test 2: O_WRONLY | O_NONBLOCK (no reader) ===\n");
    /* NOTE: We now have fd_r open as reader, so writer should succeed */
    /* Let's first test without a reader: close fd_r temporarily */
    /* Actually since we have fd_r open, writer will succeed */
    int fd_w = open(FIFO_PATH, O_WRONLY | O_NONBLOCK);
    if (fd_w == -1) {
        if (errno == ENXIO)
            printf("Writer open failed with ENXIO (no reader present)\n");
        else
            printf("Writer open failed: %s\n", strerror(errno));
    } else {
        printf("Writer opened (fd=%d) โ€” reader was present\n", fd_w);
    }

    printf("\n=== Test 3: Send/receive data using nonblocking fds ===\n");
    if (fd_r != -1 && fd_w != -1) {
        const char *msg = "Nonblocking FIFO test";
        write(fd_w, msg, strlen(msg));

        char buf[64];
        ssize_t n = read(fd_r, buf, sizeof(buf) - 1);
        if (n > 0) {
            buf[n] = '\0';
            printf("Read from FIFO: \"%s\"\n", buf);
        }
    }

    if (fd_r != -1) close(fd_r);
    if (fd_w != -1) close(fd_w);
    unlink(FIFO_PATH);
    return 0;
}

๐Ÿ”š EOF on a FIFO

Just like anonymous pipes, a FIFO reader sees EOF (read returns 0) when all write-end descriptors are closed. But FIFOs have an extra nuance: the FIFO can be re-opened by a new writer after EOF, unlike anonymous pipes which are destroyed.

Example 3: FIFO EOF and re-open behavior

/* fifo_eof_reopen.c โ€” demonstrate FIFO can be used again after EOF */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>

#define FIFO_PATH "/tmp/ep_reopen_fifo"
#define BUF_SIZE 128

int main(void)
{
    mkfifo(FIFO_PATH, 0666);

    pid_t pid = fork();

    if (pid == 0) {
        /* ===== CHILD: writer, writes twice ===== */
        printf("[Writer] First write session\n");
        int fd = open(FIFO_PATH, O_WRONLY);
        write(fd, "First message", 13);
        close(fd);  /* Close โ†’ reader sees EOF */
        printf("[Writer] Closed. Sleeping 1 second...\n");
        sleep(1);

        printf("[Writer] Second write session (FIFO re-opened)\n");
        fd = open(FIFO_PATH, O_WRONLY);
        write(fd, "Second message", 14);
        close(fd);
        _exit(0);

    } else {
        /* ===== PARENT: reader, reads in a loop ===== */
        char buf[BUF_SIZE];
        ssize_t n;
        int session = 1;

        /* Session 1 */
        int fd = open(FIFO_PATH, O_RDONLY);
        while ((n = read(fd, buf, BUF_SIZE - 1)) > 0) {
            buf[n] = '\0';
            printf("[Reader] Session %d: \"%s\"\n", session, buf);
        }
        printf("[Reader] Session %d EOF received\n", session++);
        close(fd);

        /* FIFO still exists! Re-open for session 2 */
        fd = open(FIFO_PATH, O_RDONLY);
        while ((n = read(fd, buf, BUF_SIZE - 1)) > 0) {
            buf[n] = '\0';
            printf("[Reader] Session %d: \"%s\"\n", session, buf);
        }
        printf("[Reader] Session %d EOF received\n", session);
        close(fd);

        wait(NULL);
    }

    unlink(FIFO_PATH);
    return 0;
}
/* Output:
   [Writer] First write session
   [Reader] Session 1: "First message"
   [Reader] Session 1 EOF received
   [Writer] Closed. Sleeping 1 second...
   [Writer] Second write session (FIFO re-opened)
   [Reader] Session 2: "Second message"
   [Reader] Session 2 EOF received
*/
Key insight: After EOF on a FIFO, the FIFO file still exists in the filesystem. Both sides can re-open it for a new “session”. This is fundamentally different from anonymous pipes which are destroyed after all fds are closed.

๐Ÿ”€ O_RDWR Trick โ€” Prevent EOF in a Server

A server process that reads from a FIFO will see EOF when the last client (writer) closes. To prevent this, a server can open the FIFO with O_RDWR โ€” this keeps a write-end reference open permanently, so EOF is never triggered unexpectedly.

/* fifo_server_rdwr.c โ€” server that survives client disconnects */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

#define FIFO_PATH "/tmp/ep_server_fifo"
#define MAX_CLIENTS 10

int main(void)
{
    mkfifo(FIFO_PATH, 0666);

    printf("[Server] Waiting for clients...\n");

    /* O_RDWR: opens both ends โ€” server holds write end open
     * This means: server won't see EOF even when no clients are connected.
     * The server stays alive between client connections. */
    int fd = open(FIFO_PATH, O_RDWR);
    if (fd == -1) {
        perror("open O_RDWR");
        return 1;
    }

    char buf[256];
    ssize_t n;
    int msg_count = 0;

    while (msg_count < MAX_CLIENTS) {
        n = read(fd, buf, sizeof(buf) - 1);
        if (n == 0) {
            /* Without O_RDWR this would terminate the server
             * With O_RDWR: this shouldn't happen (server holds write end) */
            printf("[Server] EOF received (unexpected with O_RDWR)\n");
            break;
        }
        if (n > 0) {
            buf[n] = '\0';
            printf("[Server] Message %d: \"%s\"\n", ++msg_count, buf);
        }
    }

    close(fd);
    unlink(FIFO_PATH);
    return 0;
}

๐ŸŽฏ Interview Questions โ€” FIFO Open Semantics
Q1. What happens when two processes both open a FIFO simultaneously?
A: They unblock each other. If process A is waiting in open(O_RDONLY) and process B calls open(O_WRONLY), both open() calls complete and return. This is the “rendezvous” behavior.
Q2. What is ENXIO in the context of FIFOs?
A: ENXIO is returned when a process tries to open a FIFO for writing (O_WRONLY) with O_NONBLOCK flag, but no process currently has the FIFO open for reading.
Q3. Can a FIFO be used multiple times (after the first writer closes)?
A: Yes. The FIFO file persists in the filesystem after the first writer closes. New writers can open and write to it again. This enables a server to serve multiple sequential clients.
Q4. What is the purpose of opening a FIFO with O_RDWR?
A: A server process can open a FIFO O_RDWR to keep an implicit write-end reference. This prevents the server from seeing a spurious EOF when the last client closes their write end โ€” the server’s own write reference keeps the write-end count above zero.
Q5. How does a FIFO’s EOF behavior differ from a regular file?
A: A regular file’s EOF means you’ve reached the end of data that exists in the file. A FIFO’s read() returning 0 means all write-end descriptors are closed โ€” there is no current writer. More data could arrive later if a new writer opens the FIFO.

Leave a Reply

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