π·οΈ Key Terms
The pipe() system call creates a new pipe and returns two file descriptors:
#include <unistd.h>
int pipe(int filedes[2]);
/* Returns: 0 on success, -1 on error */
/* After successful pipe():
filedes[0] = read end (you read from here)
filedes[1] = write end (you write to here)
*/
A pipe within a single process has limited use. The real power comes when we use fork() to create a child process β both parent and child get copies of both file descriptors.
ioctl(fd, FIONREAD, &count);This returns the number of bytes currently in the pipe buffer (not in SUSv3 standard, but works on Linux and many UNIX systems).
The standard way to use a pipe between two processes:
pipe() to create the pipe β you now have fd[0] (read) and fd[1] (write)fork() β now both parent and child have copies of both file descriptorsfd[0]. Write data to fd[1]. Close fd[1] when done.fd[1]. Read data from fd[0]. Gets EOF when parent closes fd[1].If the child keeps
fd[1] (write end) open, it will never see EOF when it reads β because the write end is still “open” (even if no one writes). This causes the child to hang forever waiting for data that never comes.
Rule: Each process should close the end it doesn’t use, immediately after fork().
Classic pipe usage: parent sends a message to child using a pipe.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define MSG "Hello child! Message via pipe.\n"
int main(void)
{
int pfd[2]; /* pfd[0]=read end, pfd[1]=write end */
pid_t pid;
char buf[128];
ssize_t n;
/* Step 1: Create pipe BEFORE fork */
if (pipe(pfd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
/* Step 2: Fork */
pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
/* ===== CHILD PROCESS: Reader ===== */
/* Close write end β child only reads */
close(pfd[1]);
/* Read from pipe until EOF */
while ((n = read(pfd[0], buf, sizeof(buf) - 1)) > 0) {
buf[n] = '\0';
printf("[child PID %d] received: %s", getpid(), buf);
}
if (n == 0) {
printf("[child] Got EOF β parent closed write end\n");
}
close(pfd[0]);
exit(EXIT_SUCCESS);
} else {
/* ===== PARENT PROCESS: Writer ===== */
/* Close read end β parent only writes */
close(pfd[0]);
/* Write message to pipe */
printf("[parent PID %d] sending message...\n", getpid());
write(pfd[1], MSG, strlen(MSG));
/* Close write end β this sends EOF to child */
close(pfd[1]);
/* Wait for child to finish */
wait(NULL);
printf("[parent] child finished, exiting\n");
}
return 0;
}
/*
Compile: gcc -o pipe_basic pipe_basic.c
Run: ./pipe_basic
Expected output:
[parent PID 1234] sending message...
[child PID 1235] received: Hello child! Message via pipe.
[child] Got EOF - parent closed write end
[parent] child finished, exiting
*/
Step-by-step explanation:
- pipe(pfd) β creates pipe before fork, so child inherits both fds
- fork() β duplicates process; child gets same pfd[] values
- Child closes pfd[1] β it only reads, so write end is unnecessary. Important for EOF detection.
- Parent closes pfd[0] β it only writes, read end is unnecessary
- Parent closes pfd[1] after writing β this signals EOF to child
Since a single pipe is unidirectional, to have parent and child talk both ways, you need two pipes.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
/* Two pipes for bidirectional communication:
p2c[]: parent writes to child (parentβchild)
c2p[]: child writes to parent (childβparent)
*/
int main(void)
{
int p2c[2]; /* parent-to-child pipe */
int c2p[2]; /* child-to-parent pipe */
char buf[128];
ssize_t n;
if (pipe(p2c) == -1 || pipe(c2p) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
pid_t pid = fork();
if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); }
if (pid == 0) {
/* ===== CHILD ===== */
/* Child reads from p2c, writes to c2p */
close(p2c[1]); /* don't need write end of p2c */
close(c2p[0]); /* don't need read end of c2p */
/* Read question from parent */
n = read(p2c[0], buf, sizeof(buf) - 1);
buf[n] = '\0';
printf("[child] received question: %s\n", buf);
/* Send answer back to parent via c2p */
const char *answer = "42 is the answer!";
write(c2p[1], answer, strlen(answer));
close(p2c[0]);
close(c2p[1]);
exit(EXIT_SUCCESS);
} else {
/* ===== PARENT ===== */
/* Parent writes to p2c, reads from c2p */
close(p2c[0]); /* don't need read end of p2c */
close(c2p[1]); /* don't need write end of c2p */
/* Ask a question */
const char *question = "What is the meaning of life?";
printf("[parent] sending: %s\n", question);
write(p2c[1], question, strlen(question));
close(p2c[1]); /* done writing */
/* Read the answer */
n = read(c2p[0], buf, sizeof(buf) - 1);
buf[n] = '\0';
printf("[parent] child replied: %s\n", buf);
close(c2p[0]);
wait(NULL);
}
return 0;
}
/*
Expected output:
[parent] sending: What is the meaning of life?
[child] received question: What is the meaning of life?
[parent] child replied: 42 is the answer!
*/
popen() internally use this pattern. In real applications you must be careful of deadlock: if both parent and child try to write to their respective pipes at the same time, and both pipes are full, they both block waiting for the other to read β causing a deadlock.This is exactly what the shell does when you type ls | wc -l. We use dup2() to redirect stdout/stdin to the pipe.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
/* Simulate: ls | wc -l */
int main(void)
{
int pfd[2];
if (pipe(pfd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
/* Fork first child: runs 'ls' */
pid_t pid1 = fork();
if (pid1 == -1) { perror("fork"); exit(EXIT_FAILURE); }
if (pid1 == 0) {
/* Child 1: ls
* Redirect stdout (fd 1) to write end of pipe (pfd[1])
* dup2(old_fd, new_fd): makes new_fd a copy of old_fd
*/
close(pfd[0]); /* close read end, not needed */
dup2(pfd[1], STDOUT_FILENO); /* ls output goes into pipe */
close(pfd[1]); /* original fd no longer needed */
execlp("ls", "ls", NULL); /* run ls */
perror("execlp ls"); /* reached only on error */
exit(EXIT_FAILURE);
}
/* Fork second child: runs 'wc -l' */
pid_t pid2 = fork();
if (pid2 == -1) { perror("fork"); exit(EXIT_FAILURE); }
if (pid2 == 0) {
/* Child 2: wc -l
* Redirect stdin (fd 0) to read end of pipe (pfd[0])
*/
close(pfd[1]); /* close write end, not needed */
dup2(pfd[0], STDIN_FILENO); /* wc reads from pipe */
close(pfd[0]); /* original fd no longer needed */
execlp("wc", "wc", "-l", NULL);
perror("execlp wc");
exit(EXIT_FAILURE);
}
/* Parent: close both ends β it uses neither */
/* IMPORTANT: if parent keeps pfd[1] open, wc will never get EOF! */
close(pfd[0]);
close(pfd[1]);
/* Wait for both children */
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
return 0;
}
/*
Compile: gcc -o ls_wc ls_wc.c
Run: ./ls_wc
Output: (number of files in current directory)
7
*/
Key concepts demonstrated:
- dup2(pfd[1], STDOUT_FILENO) β makes fd 1 (stdout) point to the pipe write end. Now when
lswrites to stdout, data goes into the pipe. - dup2(pfd[0], STDIN_FILENO) β makes fd 0 (stdin) point to the pipe read end.
wc -lreads from stdin which is now the pipe. - Parent closes both ends β critical so
wcgets EOF whenlsfinishes.
| Mistake | Effect | Fix |
|---|---|---|
| Child keeps write end open (pfd[1]) | Child’s read() never returns EOF β hangs forever | Child must close(pfd[1]) immediately after fork |
| Parent keeps pipe fds open after fork | Reader never gets EOF even after all writers close | Parent closes both ends after fork if it won’t use them |
| Writing to pipe whose read end is closed | SIGPIPE signal sent to writer; write() returns EPIPE | Handle SIGPIPE or check errno == EPIPE after write() |
| Not calling waitpid() for children | Zombie processes accumulate | Always wait() or waitpid() for each forked child |
| Using pipe in single process only | Writing to full pipe blocks forever (deadlock) | Use non-blocking I/O or threads for single-process use |
pipe() fills an array of 2 ints. filedes[0] is always the read end; filedes[1] is always the write end. Remember: 0 = read (like stdin = 0), 1 = write (like stdout = 1).dup2(pfd[1], STDOUT_FILENO) makes fd 1 (stdout) point to the pipe’s write end. So when ls calls printf or write(1, ...), the data goes into the pipe, not the terminal. Similarly, dup2(pfd[0], STDIN_FILENO) makes wc‘s stdin come from the pipe read end.signal(SIGPIPE, SIG_IGN) and check errno == EPIPE after write() instead.pipe(pfd);
pid1 = fork();
if (pid1 == 0) {
close(pfd[0]);
dup2(pfd[1], STDOUT_FILENO);
close(pfd[1]);
exec(cmd1);
}
pid2 = fork();
if (pid2 == 0) {
close(pfd[1]);
dup2(pfd[0], STDIN_FILENO);
close(pfd[0]);
exec(cmd2);
}
close(pfd[0]); close(pfd[1]); /* parent closes both */
waitpid(pid1, ...); waitpid(pid2, ...);
fdopen(pfd[0], "r") or fdopen(pfd[1], "w") to get a FILE* stream. However, be careful of stdio buffering β output may not be flushed to the pipe immediately. Use fflush() or set the stream to unbuffered with setbuf(fp, NULL) to avoid hangs.Continue Learning
You’ve learned how to create pipes and connect processes. Next: FIFOs (Named Pipes)
β Part 1: Overview β Part 3: FIFOs (Named Pipes) π Chapter Index
