Loading learning content...
In the previous page, we explored the conceptual architecture of anonymous pipes—their history, internal structure, and role in Unix philosophy. Now we turn to the practical interface that brings these concepts to life: the pipe() system call.
Every anonymous pipe in existence was created through this single system call (or its variants). Understanding its precise behavior, edge cases, and proper usage patterns is essential for writing correct pipe-based programs. A single misunderstanding about file descriptor handling can lead to deadlocks, resource leaks, or silent data loss.
This page provides an exhaustive treatment of pipe(), covering everything from its basic signature to advanced patterns used in production systems.
By the end of this page, you will master the pipe() system call signature, understand the meaning of each file descriptor, handle all possible error conditions, know when to use pipe2() for flags, and write robust pipe management code that avoids common pitfalls.
The pipe() system call is remarkably simple in its signature, yet powerful in its implications:
#include <unistd.h>
int pipe(int pipefd[2]);
Parameters:
pipefd — An array of two integers where the kernel will store the new file descriptorsReturn Value:
0 on success-1 on failure, with errno set to indicate the errorSide Effects:
pipefd[0] receives the read end file descriptorpipefd[1] receives the write end file descriptor123456789101112131415161718192021222324252627282930313233343536373839
#include <unistd.h>#include <stdio.h>#include <stdlib.h> int main(void) { int pipefd[2]; // Array to receive file descriptors // Create the pipe // After this call: // pipefd[0] = read end (data exits here) // pipefd[1] = write end (data enters here) if (pipe(pipefd) == -1) { perror("pipe"); exit(EXIT_FAILURE); } printf("Pipe created successfully!"); printf(" Read end: fd %d", pipefd[0]); printf(" Write end: fd %d", pipefd[1]); // Typical output (actual values vary): // Pipe created successfully! // Read end: fd 3 // Write end: fd 4 // The numbers 3 and 4 indicate: // - 0, 1, 2 are already in use (stdin, stdout, stderr) // - The kernel assigned the next available descriptors // IMPORTANT: Always close both ends when done! close(pipefd[0]); close(pipefd[1]); return 0;}Remembering which index is which can be tricky. Think of it as: 0 = output (you read output from the pipe), 1 = input (you write input to the pipe). Alternatively: 0 is like stdout's recipient, 1 is like stdin's source.
File Descriptor Allocation:
The kernel allocates the lowest available file descriptor numbers for the pipe. Since file descriptors 0, 1, and 2 are typically occupied by stdin, stdout, and stderr, pipes usually start at fd 3.
This behavior has important implications:
Understanding the relationship between file descriptors and the underlying pipe is crucial. Multiple file descriptors can reference the same pipe end, and the kernel tracks references to determine when to signal EOF or SIGPIPE.
Reference Counting:
The kernel maintains separate reference counts for:
These counts change through:
pipe() — Creates one reference to each endfork() — Duplicates all references (count doubles)dup() / dup2() — Creates additional references to same endclose() — Decrements the appropriate reference count┌────────────────────────────────────────────────────────────────────┐│ PIPE REFERENCE COUNTING MECHANICS │└────────────────────────────────────────────────────────────────────┘ KERNEL PIPE OBJECT ┌─────────────────────────┐ │ readers_count = N │ │ writers_count = M │ │ buffer[...] │ └─────────────────────────┘ SCENARIO 1: After pipe() in single process───────────────────────────────────────────── Process A: fd[0] ──────────► readers = 1 fd[1] ──────────► writers = 1 SCENARIO 2: After fork()───────────────────────────────────────────── Parent: Child: fd[0] ─────┐ fd[0] ─────┐ ├──► readers = 2 │ fd[1] ─────┤ fd[1] ─────┤ └──► writers = 2 │ SCENARIO 3: After closing unused ends (proper pattern)───────────────────────────────────────────── Parent (writer): Child (reader): fd[0] closed fd[0] open ──► readers = 1 fd[1] open ────────────► writers = 1 fd[1] closed SCENARIO 4: When writer_count drops to 0───────────────────────────────────────────── Parent closes fd[1]: Child reads: read() returns 0 (EOF) Remaining data is available, then EOF signaled SCENARIO 5: When reader_count drops to 0───────────────────────────────────────────── Child closes fd[0]: Parent writes: write() raises SIGPIPE (or returns -1, EPIPE if SIGPIPE is ignored)Critical Behaviors:
EOF Signaling (writers = 0): When all write-end references are closed:
read() returns 0SIGPIPE / EPIPE (readers = 0): When all read-end references are closed:
SIGPIPE signalwrite() returns -1 with errno = EPIPEDeadlock Risk: If both ends remain open in a single-threaded process:
This is why closing unused ends after fork is essential.
After fork(), each process should immediately close the pipe end it doesn't use. A writer closes fd[0]; a reader closes fd[1]. Failure to do this prevents EOF detection and can cause deadlocks. This is the most common pipe programming mistake.
The pipe() system call can fail for several reasons. Proper error handling is essential for robust programs. When pipe() returns -1, errno indicates the specific failure:
EMFILE (Too many open files):
The calling process has reached its limit on open file descriptors. This limit is per-process and configurable via ulimit -n or the RLIMIT_NOFILE resource limit.
ENFILE (File table overflow): The system-wide limit on open files has been reached. This is a more severe condition affecting all processes on the system.
EFAULT (Bad address):
The pipefd array pointer is invalid—points to memory the process cannot access. This typically indicates a programming bug.
ENOMEM (Insufficient kernel memory): The kernel couldn't allocate memory for the pipe structure. Rare on modern systems but possible under extreme load.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <errno.h>#include <string.h> /** * Creates a pipe with comprehensive error handling. * * @param pipefd Array to receive file descriptors * @return 0 on success, -1 on failure (with errno set) */int create_pipe_robust(int pipefd[2]) { if (pipefd == NULL) { errno = EFAULT; return -1; } if (pipe(pipefd) == -1) { // Detailed error handling based on errno switch (errno) { case EMFILE: fprintf(stderr, "Error: Process file descriptor limit reached."); fprintf(stderr, "Consider: ulimit -n <higher_value>"); break; case ENFILE: fprintf(stderr, "Error: System file table full."); fprintf(stderr, "This is a system-wide condition."); break; case EFAULT: fprintf(stderr, "Error: Invalid memory address for pipefd."); break; case ENOMEM: fprintf(stderr, "Error: Kernel memory exhausted."); break; default: fprintf(stderr, "Error: pipe() failed: %s", strerror(errno)); break; } return -1; } return 0;} /** * Example: Checking available file descriptors before pipe creation */#include <sys/resource.h> int can_create_pipe(void) { struct rlimit rl; if (getrlimit(RLIMIT_NOFILE, &rl) == -1) { return -1; // Can't determine } // Count open file descriptors (simplified approach) // In production, would use /proc/self/fd or similar int open_count = 0; for (int fd = 0; fd < (int)rl.rlim_cur; fd++) { if (fcntl(fd, F_GETFD) != -1) { open_count++; } } // Need at least 2 more descriptors for pipe if ((rl.rlim_cur - open_count) < 2) { fprintf(stderr, "Warning: Only %lu descriptors available", rl.rlim_cur - open_count); return 0; // Cannot safely create pipe } return 1; // Safe to create pipe}| errno | Meaning | Common Cause | Recovery Strategy |
|---|---|---|---|
EMFILE | Process limit reached | Too many pipes/files open | Close unused FDs, increase ulimit |
ENFILE | System limit reached | System under heavy load | Wait and retry, reduce system load |
EFAULT | Invalid address | NULL or bad pointer | Fix the bug—validate pointer |
ENOMEM | Out of memory | Kernel memory exhausted | Free resources, add memory |
EMFILE is by far the most common pipe() failure in production. Prevent it through disciplined FD management: close file descriptors as soon as you're done with them, especially in loops that create pipes. Use tools like 'lsof -p <pid>' to audit open descriptors.
Linux and some other systems provide an enhanced version of pipe() called pipe2(), which accepts flags to set properties atomically at creation time:
#define _GNU_SOURCE // Required for pipe2 on glibc
#include <unistd.h>
#include <fcntl.h>
int pipe2(int pipefd[2], int flags);
Available Flags:
O_CLOEXEC — Set close-on-exec flag for both descriptorsO_NONBLOCK — Set non-blocking mode for both descriptorsO_DIRECT (Linux 3.4+) — Packet mode (data boundaries preserved)Why pipe2() Matters:
Setting flags after pipe() requires separate fcntl() calls, creating race conditions. Between pipe() and fcntl():
pipe2() sets flags atomically, eliminating these races.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
#define _GNU_SOURCE#include <unistd.h>#include <fcntl.h>#include <stdio.h>#include <stdlib.h>#include <errno.h> /** * Compare: pipe() + fcntl() vs pipe2() */ // OLD WAY: Race condition vulnerableint create_pipe_old(int pipefd[2]) { if (pipe(pipefd) == -1) { return -1; } // RACE WINDOW: Another thread could fork() here, // inheriting the pipe WITHOUT O_CLOEXEC set! // Set close-on-exec on both descriptors if (fcntl(pipefd[0], F_SETFD, FD_CLOEXEC) == -1) { close(pipefd[0]); close(pipefd[1]); return -1; } // Another race window between these two fcntl calls... if (fcntl(pipefd[1], F_SETFD, FD_CLOEXEC) == -1) { close(pipefd[0]); close(pipefd[1]); return -1; } return 0;} // NEW WAY: Atomic, race-freeint create_pipe_new(int pipefd[2]) { // O_CLOEXEC is set atomically—no race window if (pipe2(pipefd, O_CLOEXEC) == -1) { return -1; } return 0;} // Non-blocking pipe for async I/O patternsint create_nonblocking_pipe(int pipefd[2]) { if (pipe2(pipefd, O_CLOEXEC | O_NONBLOCK) == -1) { return -1; } return 0;} // Packet mode pipe (preserves message boundaries)// Writes must not exceed PIPE_BUF; reads get whole packetsint create_packet_pipe(int pipefd[2]) {#ifdef O_DIRECT if (pipe2(pipefd, O_CLOEXEC | O_DIRECT) == -1) { return -1; } return 0;#else errno = ENOSYS; return -1;#endif} int main(void) { int pipefd[2]; if (create_pipe_new(pipefd) == 0) { printf("Created pipe with O_CLOEXEC: fd %d, %d", pipefd[0], pipefd[1]); close(pipefd[0]); close(pipefd[1]); } return 0;}The O_CLOEXEC Flag:
Perhaps the most important flag is O_CLOEXEC. When set, the file descriptor automatically closes when the process calls exec(). This prevents accidental file descriptor leakage to child programs:
Without O_CLOEXEC:
With O_CLOEXEC:
The O_NONBLOCK Flag:
Normally, read() on an empty pipe blocks until data arrives. With O_NONBLOCK:
This enables event-driven and async I/O patterns where blocking would be unacceptable.
pipe2() is Linux-specific (and adopted by some BSDs). For portable code, you must use pipe() + fcntl(), accepting the race condition or using synchronization. Always check for ENOSYS in case pipe2() isn't available.
Let's synthesize everything into a complete, production-ready pattern for creating and using pipes between parent and child processes. This pattern addresses all the pitfalls we've discussed: proper error handling, correct FD closing, and clean resource management.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
#define _GNU_SOURCE#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/wait.h>#include <errno.h> /** * Complete pipe pattern: Parent writes, Child reads * * This demonstrates the canonical pipe usage pattern with * proper error handling and resource cleanup. */int main(void) { int pipefd[2]; pid_t pid; const char *message = "Hello from parent process!"; // ═══════════════════════════════════════════════════════════════ // STEP 1: Create the pipe BEFORE forking // ═══════════════════════════════════════════════════════════════ // The pipe must exist before fork() so both processes inherit it. if (pipe(pipefd) == -1) { perror("pipe"); exit(EXIT_FAILURE); } printf("[Parent] Pipe created: read_fd=%d, write_fd=%d", pipefd[0], pipefd[1]); // ═══════════════════════════════════════════════════════════════ // STEP 2: Fork to create child process // ═══════════════════════════════════════════════════════════════ // Both parent and child now have copies of both file descriptors. pid = fork(); if (pid == -1) { perror("fork"); // Clean up pipe before exiting close(pipefd[0]); close(pipefd[1]); exit(EXIT_FAILURE); } // ═══════════════════════════════════════════════════════════════ // CHILD PROCESS: The reader // ═══════════════════════════════════════════════════════════════ if (pid == 0) { char buffer[256]; ssize_t bytes_read; // CRITICAL: Close the write end in the child! // This is essential for EOF detection. If the write end // remains open in the child, read() will never return 0. close(pipefd[1]); printf("[Child] Reading from pipe..."); // Read all available data while ((bytes_read = read(pipefd[0], buffer, sizeof(buffer) - 1)) > 0) { buffer[bytes_read] = '\0'; // Null-terminate printf("[Child] Received %zd bytes: %s", bytes_read, buffer); } if (bytes_read == -1) { perror("[Child] read"); close(pipefd[0]); exit(EXIT_FAILURE); } // bytes_read == 0 means EOF (parent closed write end) printf("[Child] EOF reached, exiting."); close(pipefd[0]); exit(EXIT_SUCCESS); } // ═══════════════════════════════════════════════════════════════ // PARENT PROCESS: The writer // ═══════════════════════════════════════════════════════════════ // CRITICAL: Close the read end in the parent! // This allows SIGPIPE to work correctly if child exits early. close(pipefd[0]); printf("[Parent] Writing to pipe..."); // Write the message ssize_t bytes_written = write(pipefd[1], message, strlen(message)); if (bytes_written == -1) { perror("[Parent] write"); close(pipefd[1]); exit(EXIT_FAILURE); } printf("[Parent] Wrote %zd bytes to pipe.", bytes_written); // CRITICAL: Close write end to signal EOF to child! // Without this, child's read() will block forever. close(pipefd[1]); printf("[Parent] Closed write end (signaling EOF)."); // Wait for child to finish int status; if (waitpid(pid, &status, 0) == -1) { perror("[Parent] waitpid"); exit(EXIT_FAILURE); } if (WIFEXITED(status)) { printf("[Parent] Child exited with status %d.", WEXITSTATUS(status)); } return 0;}Expected Output:
[Parent] Pipe created: read_fd=3, write_fd=4
[Child] Reading from pipe...
[Parent] Writing to pipe...
[Parent] Wrote 28 bytes to pipe.
[Parent] Closed write end (signaling EOF).
[Child] Received 28 bytes: Hello from parent process!
[Child] EOF reached, exiting.
[Parent] Child exited with status 0.
Critical Observations:
Pipe before fork — The pipe must be created before fork() so both processes inherit the file descriptors.
Close unused ends immediately — Right after fork(), each process closes the end it won't use. This is not optional—it's essential for correct behavior.
Writer closes to signal EOF — When the parent finishes writing, closing pipefd[1] causes the child's read() to return 0 (EOF). Without this, the child blocks forever.
Handle partial writes — Our example writes a small message that fits atomically. For larger writes, loop until all data is written.
Wait for child — Parent should wait() to collect child's exit status and avoid zombie processes.
Since anonymous pipes are unidirectional, bidirectional communication between parent and child requires two pipes—one for each direction. This pattern is essential for request-response protocols or interactive communication.
┌─────────────────────────────────────────────────────────────────────┐│ BIDIRECTIONAL PIPE COMMUNICATION │└─────────────────────────────────────────────────────────────────────┘ ┌─────────────────────┐ ┌─────────────────────┐ │ PARENT PROCESS │ │ CHILD PROCESS │ │ │ │ │ │ pipe1: parent→child│ │ pipe1: parent→child│ │ write_fd ─────────┼────── Pipe 1 ───────┼→ read_fd │ │ read_fd (closed) │ (requests) │ write_fd (closed) │ │ │ │ │ │ pipe2: child→parent│ │ pipe2: child→parent│ │ read_fd ←─────────┼────── Pipe 2 ───────┼─ write_fd │ │ write_fd (closed) │ (responses) │ read_fd (closed) │ │ │ │ │ └─────────────────────┘ └─────────────────────┘ DATA FLOW: 1. Parent writes request to pipe1[1] ────────────► 2. ◄──────────────── Child reads from pipe1[0] 3. ◄──────────────── Child writes response to pipe2[1] 4. Parent reads response from pipe2[0] ◄───────── FILE DESCRIPTOR MANAGEMENT: After fork(), each process closes TWO descriptors: Parent: Child: close(pipe1[0]) // Pipe1 read close(pipe1[1]) // Pipe1 write close(pipe2[1]) // Pipe2 write close(pipe2[0]) // Pipe2 read keep(pipe1[1]) // Pipe1 write keep(pipe1[0]) // Pipe1 read keep(pipe2[0]) // Pipe2 read keep(pipe2[1]) // Pipe2 write12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879
#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include <sys/wait.h> /** * Bidirectional communication using two pipes. * Parent sends a number, child returns its square. */int main(void) { int parent_to_child[2]; // Pipe 1: parent writes, child reads int child_to_parent[2]; // Pipe 2: child writes, parent reads pid_t pid; // Create both pipes if (pipe(parent_to_child) == -1 || pipe(child_to_parent) == -1) { perror("pipe"); exit(EXIT_FAILURE); } pid = fork(); if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); } if (pid == 0) { // ═══════════════════════════════════════════════════ // CHILD PROCESS // ═══════════════════════════════════════════════════ // Close unused ends close(parent_to_child[1]); // Don't write to Pipe 1 close(child_to_parent[0]); // Don't read from Pipe 2 int number; while (read(parent_to_child[0], &number, sizeof(number)) > 0) { int result = number * number; printf("[Child] Received %d, sending back %d", number, result); write(child_to_parent[1], &result, sizeof(result)); } // Cleanup close(parent_to_child[0]); close(child_to_parent[1]); exit(EXIT_SUCCESS); } // ═══════════════════════════════════════════════════ // PARENT PROCESS // ═══════════════════════════════════════════════════ // Close unused ends close(parent_to_child[0]); // Don't read from Pipe 1 close(child_to_parent[1]); // Don't write to Pipe 2 // Send numbers, receive squares for (int i = 1; i <= 5; i++) { int result; write(parent_to_child[1], &i, sizeof(i)); read(child_to_parent[0], &result, sizeof(result)); printf("[Parent] Sent %d, got back %d", i, result); } // Cleanup and wait close(parent_to_child[1]); // Signal EOF to child close(child_to_parent[0]); waitpid(pid, NULL, 0); printf("[Parent] Done."); return 0;}While two pipes work for bidirectional communication, the complexity increases significantly. For complex request-response protocols, consider Unix domain sockets (socketpair()), which provide bidirectional communication through a single API call.
Pipe programming is deceptively simple on the surface but harbors subtle bugs that can be difficult to diagnose. Here are the most common pitfalls and how to identify and fix them:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
#include <unistd.h>#include <stdio.h>#include <fcntl.h>#include <errno.h>#include <signal.h> /** * Debugging techniques for pipe problems */ // Technique 1: Print file descriptor statevoid debug_print_fd_state(int fd, const char *name) { int flags = fcntl(fd, F_GETFL); if (flags == -1) { printf("%s (fd %d): CLOSED or INVALID", name, fd); } else { printf("%s (fd %d): OPEN, flags=0x%x", name, fd, flags); if (flags & O_NONBLOCK) printf(" NONBLOCK"); if (flags & O_RDONLY) printf(" RDONLY"); if (flags & O_WRONLY) printf(" WRONLY"); printf(""); }} // Technique 2: Trace all pipe operations#define TRACE_WRITE(fd, buf, len) \ do { \ ssize_t _ret = write(fd, buf, len); \ fprintf(stderr, "[TRACE] write(fd=%d, len=%zu) = %zd", \ fd, (size_t)len, _ret); \ if (_ret == -1) fprintf(stderr, "[TRACE] errno: %s", strerror(errno)); \ } while(0) // Technique 3: Set up SIGPIPE handling for debuggingvolatile sig_atomic_t sigpipe_received = 0; void sigpipe_handler(int sig) { sigpipe_received = 1;} void setup_sigpipe_debugging(void) { struct sigaction sa; sa.sa_handler = sigpipe_handler; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGPIPE, &sa, NULL);} // Technique 4: Check if pipe is readable/writable#include <poll.h> int check_pipe_state(int fd) { struct pollfd pfd = { .fd = fd, .events = POLLIN | POLLOUT }; if (poll(&pfd, 1, 0) == -1) { return -1; // Error } if (pfd.revents & POLLIN) printf("fd %d: data available", fd); if (pfd.revents & POLLOUT) printf("fd %d: writable", fd); if (pfd.revents & POLLHUP) printf("fd %d: write end closed", fd); if (pfd.revents & POLLERR) printf("fd %d: error condition", fd); return 0;}When pipe programs hang or behave unexpectedly, strace is invaluable: 'strace -f -e trace=read,write,close,pipe,dup2 ./program'. The -f flag follows forks, showing you exactly which process is blocked on which system call.
We've built a comprehensive understanding of the pipe() system call—the gateway to pipe-based inter-process communication. Let's consolidate the key points:
What's Next:
Now that we understand how to create pipes, the next page explores their fundamental characteristic: unidirectional communication. We'll examine why pipes flow in one direction only, how this influences system design, and the patterns that emerge from this constraint.
You now have deep mastery of the pipe() system call—its signature, semantics, error handling, and the pipe2() variant. Combined with the conceptual foundation from the previous page, you're ready to explore the implications of unidirectional data flow.