What Will You Learn?
This tutorial covers the very first IPC (Inter-Process Communication) mechanism in UNIX history โ Pipes. Pipes have been in UNIX since the early 1970s (Third Edition UNIX). You will learn:
- What a pipe is and how it works internally
- Key properties of pipes: byte stream, unidirectional, PIPE_BUF atomicity, capacity
- How the shell uses pipes (the
ls | wc -lmechanism) - Difference between pipes and FIFOs
๐ท๏ธ Key Terms
A pipe is a kernel-maintained buffer that lets two processes communicate. Data written by one process can be read by another. Think of it like a water pipe โ water (data) flows in one direction only.
Every Linux user has seen pipes in the shell:
$ ls | wc -l
Here the shell:
- Creates a pipe (two file descriptors: read end + write end)
- Forks two child processes: one runs
ls, one runswc - Connects
ls‘s stdout to the write end of the pipe (fd 1) - Connects
wc‘s stdin to the read end of the pipe (fd 0)
The two processes don’t even know a pipe exists. They just write to stdout and read from stdin as usual. The shell sets everything up.
The name comes from physical plumbing. Just like water flows through a pipe from one place to another, data flows from one process to another โ in one direction only.
2a. A Pipe is a Byte Stream
A pipe has no concept of messages or boundaries. It is just a continuous stream of bytes. This means:
- Writer writes 100 bytes โ Reader can read them as 10 reads of 10 bytes each, or 1 read of 100 bytes. Both work.
- There are no “message separators” built in.
- You cannot use lseek() on a pipe โ no random access.
- Bytes come out in the exact order they were written (FIFO order).
2b. Pipes are Unidirectional
Data can only flow in one direction. One end is for writing (fd[1]), the other is for reading (fd[0]). You cannot write to the read end or read from the write end.
socketpair() to create UNIX domain socket pairs instead.2c. Reading Behavior
Two important rules when reading from a pipe:
- Empty pipe: If you try to read from an empty pipe,
read()blocks until some data is written. - Write end closed: If all processes that have the write end open have closed it, then
read()returns 0 (EOF) after all remaining data is consumed.
2d. Writes up to PIPE_BUF are Atomic
If multiple processes write to the same pipe, the kernel guarantees that writes of up to PIPE_BUF bytes will not be interleaved with other writes. This is called atomic write.
| System | PIPE_BUF Value |
|---|---|
| Linux | 4096 bytes |
| FreeBSD 6.0 | 512 bytes |
| Solaris 8 | 5120 bytes |
| Tru64 5.1 | 4096 bytes |
| POSIX minimum | 512 bytes (_POSIX_PIPE_BUF) |
If Process A writes 100 bytes and Process B writes 200 bytes to the same pipe, and both writes are < PIPE_BUF (4096), then the reader will see either A’s 100 bytes or B’s 200 bytes โ never a mix of A’s and B’s data. Without atomicity, bytes from A and B could get shuffled together, corrupting data.
If you write more than PIPE_BUF bytes:
- The kernel may split the write into smaller pieces
- Other writers can interleave their data between your pieces
- This is fine for a single writer, but dangerous for multiple writers
2e. Pipe Capacity (Kernel Buffer Size)
A pipe is a buffer in kernel memory. It has a finite size:
- Linux < 2.6.11: capacity = system page size (4096 bytes on x86)
- Linux โฅ 2.6.11: capacity = 65,536 bytes (64 KB)
If the pipe is full, write() blocks until the reader removes some data.
From Linux 2.6.35, you can change pipe capacity using:
/* Set pipe capacity to at least 'size' bytes */
fcntl(fd, F_SETPIPE_SZ, size);
/* Get current pipe capacity */
int actual_size = fcntl(fd, F_GETPIPE_SZ);
/proc/sys/fs/pipe-max-size (default: 1,048,576 = 1 MB). Root/CAP_SYS_RESOURCE can override this.This example shows that a pipe is a byte stream: the reader can read in smaller chunks than the writer wrote.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(void)
{
int pfd[2]; /* pfd[0] = read end, pfd[1] = write end */
char buf[10]; /* reader reads only 10 bytes at a time */
ssize_t n;
/* Step 1: Create the pipe */
if (pipe(pfd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
/* Step 2: Write 30 bytes in one shot */
const char *msg = "Hello from pipe! This is 30B!";
printf("[writer] Writing %zu bytes to pipe\n", strlen(msg));
write(pfd[1], msg, strlen(msg));
/* Step 3: Close write end so reader gets EOF eventually */
close(pfd[1]);
/* Step 4: Read 10 bytes at a time - shows byte stream behavior */
int chunk = 1;
while ((n = read(pfd[0], buf, sizeof(buf) - 1)) > 0) {
buf[n] = '\0';
printf("[reader] chunk %d: read %zd bytes: \"%s\"\n", chunk++, n, buf);
}
/* n == 0 means EOF (write end was closed) */
printf("[reader] EOF reached (write end closed)\n");
close(pfd[0]);
return 0;
}
/*
Expected output:
[writer] Writing 29 bytes to pipe
[reader] chunk 1: read 9 bytes: "Hello fro"
[reader] chunk 2: read 9 bytes: "m pipe! T"
[reader] chunk 3: read 9 bytes: "his is 30"
[reader] chunk 4: read 2 bytes: "B!"
[reader] EOF reached (write end closed)
*/
Key observations:
- Writer wrote 29 bytes in one
write()call - Reader read them in 10-byte chunks โ pipe doesn’t care about message boundaries
- When write end is closed,
read()returns 0 (EOF)
This example shows how to programmatically get the PIPE_BUF value and the actual pipe capacity on your system.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <limits.h> /* for PIPE_BUF */
#include <fcntl.h> /* for F_GETPIPE_SZ */
int main(void)
{
int pfd[2];
/* Show compile-time PIPE_BUF constant */
printf("PIPE_BUF (compile-time constant) = %d bytes\n", PIPE_BUF);
/* Create pipe and check runtime capacity */
if (pipe(pfd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
/* F_GETPIPE_SZ: Linux-specific, get actual allocated pipe size */
int pipe_sz = fcntl(pfd[0], F_GETPIPE_SZ);
if (pipe_sz == -1) {
perror("fcntl F_GETPIPE_SZ");
} else {
printf("Actual pipe capacity (F_GETPIPE_SZ) = %d bytes\n", pipe_sz);
}
/* Try to set a larger pipe size */
int desired = 131072; /* 128 KB */
int result = fcntl(pfd[1], F_SETPIPE_SZ, desired);
if (result == -1) {
perror("fcntl F_SETPIPE_SZ");
} else {
printf("Requested %d bytes, kernel allocated %d bytes\n",
desired, fcntl(pfd[0], F_GETPIPE_SZ));
}
/* fpathconf: POSIX way to get PIPE_BUF for this fd */
long pbuf = fpathconf(pfd[0], _PC_PIPE_BUF);
printf("fpathconf PIPE_BUF for this pipe = %ld bytes\n", pbuf);
close(pfd[0]);
close(pfd[1]);
return 0;
}
/*
Sample output on Linux:
PIPE_BUF (compile-time constant) = 4096 bytes
Actual pipe capacity (F_GETPIPE_SZ) = 65536 bytes
Requested 131072 bytes, kernel allocated 131072 bytes
fpathconf PIPE_BUF for this pipe = 4096 bytes
*/
- PIPE_BUF (4096): The threshold for atomic writes. Writes โค 4096 bytes are guaranteed not to be interleaved.
- Pipe capacity (65536): The total buffer size. The pipe blocks writes when full.
- These are two different things! PIPE_BUF is about atomicity; pipe capacity is about how much data can be buffered.
A FIFO (First In, First Out) is also called a named pipe. It has all the same properties as a regular pipe, but with one major difference:
| Feature | Anonymous Pipe | FIFO (Named Pipe) |
|---|---|---|
| Created with | pipe() | mkfifo() or mknod() |
| Visible in filesystem | No | Yes (has a path like /tmp/myfifo) |
| Between related processes? | Must be related (parent-child) | Any two processes on the same system |
| Lifetime | Ends when both fds closed | Persists until explicitly deleted |
| Direction | Unidirectional | Unidirectional |
For a regular pipe, both processes need to have inherited the same file descriptors โ which only happens when one process is created by
fork() from another (parent-child relationship). FIFOs don’t have this restriction โ any two unrelated processes can open the same FIFO path.lseek(), and is unidirectional. Data written flows sequentially and can only be read once.read() returns 0 (EOF).fcntl(fd, F_SETPIPE_SZ, size).Continue Learning
Next: Creating pipes with pipe(), using fork() to connect two processes
