pipe2() โ€” Linux Extension O_CLOEXEC, O_NONBLOCK, and Nonblocking Pipes

 

๐Ÿ”ง pipe2() โ€” Linux Extension
O_CLOEXEC, O_NONBLOCK, and Nonblocking Pipes
Part 4 of 9
pipe2()
Kernel
2.6.27+
Level
Intermediate

pipe2() โ€” Linux-Specific Enhancement

Linux kernel 2.6.27 introduced pipe2(), a non-standard extension to pipe(). It does everything pipe() does, plus supports an optional flags argument that can atomically set important properties on the file descriptors.

Two flags are supported: O_CLOEXEC (close on exec) and O_NONBLOCK (nonblocking I/O). Both solve race conditions that would otherwise require separate syscalls after pipe().

Key Concepts

pipe2() O_CLOEXEC FD_CLOEXEC O_NONBLOCK EAGAIN fcntl() exec() race condition

โš™๏ธ pipe2() Signature
#define _GNU_SOURCE
#include <unistd.h>
#include <fcntl.h>

int pipe2(int filedes[2], int flags);
/* Returns 0 on success, -1 on error */

/* flags can be:
 *   0            โ€” same as pipe()
 *   O_CLOEXEC    โ€” set FD_CLOEXEC on both fds
 *   O_NONBLOCK   โ€” set O_NONBLOCK on both fds
 *   O_CLOEXEC | O_NONBLOCK โ€” both at once
 */

If flags = 0, pipe2() behaves exactly like pipe(). The benefit is that flags are set atomically at creation time โ€” no race window between pipe() and a subsequent fcntl() call.

๐Ÿ”’ O_CLOEXEC โ€” Close on exec()

By default, file descriptors survive across exec() calls. This means if a process creates a pipe and then calls exec() to replace itself with another program, the new program inherits the pipe descriptors โ€” which is almost never what you want.

FD_CLOEXEC (the close-on-exec flag) tells the kernel to automatically close a file descriptor when exec() is called.

Without vs With O_CLOEXEC

โŒ Without O_CLOEXEC (pipe only)
1. Process A: pipe(fd) โ†’ fd[0]=3, fd[1]=4
2. fork() + exec() โ†’ new program runs
3. fd[3] and fd[4] still open in new program
4. Leaked FDs โ†’ pipe never closes โ†’ bugs

โœ… With O_CLOEXEC (pipe2)
1. Process A: pipe2(fd, O_CLOEXEC)
2. fork() + exec() โ†’ new program runs
3. fd[3] and fd[4] automatically closed
4. No leaks, correct behavior

Example 1: pipe2() with O_CLOEXEC

/* cloexec_demo.c */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(void)
{
    int pfd[2];

    /* Create pipe with close-on-exec flag */
    if (pipe2(pfd, O_CLOEXEC) == -1) {
        perror("pipe2");
        return 1;
    }

    printf("Created pipe: fd[0]=%d, fd[1]=%d\n", pfd[0], pfd[1]);

    /* Verify FD_CLOEXEC is set */
    int flags0 = fcntl(pfd[0], F_GETFD);
    int flags1 = fcntl(pfd[1], F_GETFD);
    printf("fd[0] FD_CLOEXEC set: %s\n", (flags0 & FD_CLOEXEC) ? "YES" : "NO");
    printf("fd[1] FD_CLOEXEC set: %s\n", (flags1 & FD_CLOEXEC) ? "YES" : "NO");

    /* If we exec() here, pfd[0] and pfd[1] would be auto-closed */

    /* Compare: old way needed two extra fcntl() calls */
    int pfd2[2];
    pipe(pfd2);
    /* Old way to set close-on-exec: */
    fcntl(pfd2[0], F_SETFD, FD_CLOEXEC);  /* extra call 1 */
    fcntl(pfd2[1], F_SETFD, FD_CLOEXEC);  /* extra call 2 */
    /* pipe2(O_CLOEXEC) does this atomically! */

    close(pfd[0]); close(pfd[1]);
    close(pfd2[0]); close(pfd2[1]);
    return 0;
}
/* Output:
   Created pipe: fd[0]=3, fd[1]=4
   fd[0] FD_CLOEXEC set: YES
   fd[1] FD_CLOEXEC set: YES
*/
Why atomic matters: Between pipe() and fcntl() there is a brief window where another thread could call fork()+exec() and inherit the unprotected fd. pipe2(O_CLOEXEC) eliminates this race condition.

โšก O_NONBLOCK โ€” Nonblocking Pipe I/O

By default, pipes block: read() on an empty pipe waits, write() on a full pipe waits. With O_NONBLOCK, these operations fail immediately with errno = EAGAIN instead of blocking.

Operation Blocking (default) Nonblocking (O_NONBLOCK)
read() on empty pipe Blocks until data available Returns -1, errno = EAGAIN
write() on full pipe Blocks until space available Returns -1, errno = EAGAIN
read() on closed write end Returns 0 (EOF) Returns 0 (EOF) โ€” same

Example 2: Nonblocking pipe with pipe2()

/* nonblocking_pipe.c */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

int main(void)
{
    int pfd[2];

    /* Create nonblocking pipe */
    if (pipe2(pfd, O_NONBLOCK) == -1) {
        perror("pipe2");
        return 1;
    }

    printf("=== Test 1: Nonblocking read on empty pipe ===\n");
    char buf[64];
    ssize_t n = read(pfd[0], buf, sizeof(buf));
    if (n == -1) {
        if (errno == EAGAIN)
            printf("read() returned EAGAIN (no data yet) โ€” nonblocking works!\n");
        else
            perror("read");
    }

    printf("\n=== Test 2: Write data, then read ===\n");
    const char *msg = "Nonblocking pipe test";
    write(pfd[1], msg, strlen(msg));
    n = read(pfd[0], buf, sizeof(buf) - 1);
    buf[n] = '\0';
    printf("Read after write: \"%s\"\n", buf);

    printf("\n=== Test 3: Fill the pipe to test write blocking ===\n");
    /* Fill up the pipe buffer to test O_NONBLOCK on write */
    int total = 0;
    char fill[4096];
    memset(fill, 'X', sizeof(fill));
    while (1) {
        n = write(pfd[1], fill, sizeof(fill));
        if (n == -1) {
            if (errno == EAGAIN) {
                printf("write() returned EAGAIN โ€” pipe is full (%d bytes written)\n",
                       total);
                break;
            }
            perror("write");
            break;
        }
        total += n;
    }

    close(pfd[0]);
    close(pfd[1]);
    return 0;
}
/* Output:
   === Test 1: Nonblocking read on empty pipe ===
   read() returned EAGAIN (no data yet) โ€” nonblocking works!

   === Test 2: Write data, then read ===
   Read after write: "Nonblocking pipe test"

   === Test 3: Fill the pipe to test write blocking ===
   write() returned EAGAIN โ€” pipe is full (65536 bytes written)
*/

Example 3: Set O_NONBLOCK on existing pipe using fcntl()

/* set_nonblock_fcntl.c โ€” when you can't use pipe2() */
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

/* Helper: make an fd nonblocking */
int set_nonblocking(int fd)
{
    int flags = fcntl(fd, F_GETFL);
    if (flags == -1) return -1;
    return fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main(void)
{
    int pfd[2];
    pipe(pfd);  /* regular pipe */

    /* Make both ends nonblocking */
    set_nonblocking(pfd[0]);
    set_nonblocking(pfd[1]);

    /* Test: read from empty nonblocking pipe */
    char buf[64];
    ssize_t n = read(pfd[0], buf, sizeof(buf));
    if (n == -1 && errno == EAGAIN)
        printf("EAGAIN โ€” empty pipe, nonblocking set via fcntl\n");

    close(pfd[0]);
    close(pfd[1]);
    return 0;
}
When to use O_NONBLOCK: Use it when you want to poll a pipe without blocking โ€” for example in an event loop (with select/poll/epoll) where a blocking read would stall the whole loop.

๐Ÿ“Š pipe() vs pipe2() Comparison
Feature pipe() pipe2(flags)
POSIX standard โœ… Yes โŒ Linux-specific
Available since Always Kernel 2.6.27
O_CLOEXEC support Needs extra fcntl() โœ… Atomic
O_NONBLOCK support Needs extra fcntl() โœ… Atomic
Race-condition safe โŒ (window between pipe+fcntl) โœ… Flags set atomically
Header required <unistd.h> <unistd.h> + <fcntl.h> + #define _GNU_SOURCE

๐ŸŽฏ Interview Questions โ€” pipe2(), CLOEXEC, NONBLOCK
Q1. What is pipe2() and how does it differ from pipe()?
A: pipe2() is a Linux-specific (kernel 2.6.27+) variant of pipe() that accepts a flags argument. It can atomically set O_CLOEXEC (close-on-exec) and/or O_NONBLOCK on the created file descriptors. This avoids the race condition present between pipe() and a subsequent fcntl() call.
Q2. What does O_CLOEXEC do?
A: O_CLOEXEC sets the FD_CLOEXEC flag on a file descriptor. This causes the fd to be automatically closed when the process calls exec(). It prevents fd leaks into child programs that shouldn’t have access to the pipe.
Q3. What is EAGAIN and when does a nonblocking pipe produce it?
A: EAGAIN (or EWOULDBLOCK) is the errno set when a nonblocking I/O operation cannot complete immediately. A nonblocking read() returns EAGAIN when the pipe is empty; a nonblocking write() returns EAGAIN when the pipe is full.
Q4. How do you make an existing pipe nonblocking without pipe2()?
A: Use fcntl(fd, F_GETFL) to read current flags, then fcntl(fd, F_SETFL, flags | O_NONBLOCK) to add O_NONBLOCK. This must be done for each end separately.
Q5. Why is there a race condition between pipe() + fcntl() for CLOEXEC?
A: In a multithreaded program, between the pipe() call and the subsequent fcntl(F_SETFD, FD_CLOEXEC) call, another thread could call fork() + exec(). The new program would inherit the unprotected fd. pipe2(O_CLOEXEC) sets the flag atomically inside the kernel, eliminating this window.

Leave a Reply

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