fork + pipe
Parent-Child IPC
Intermediate
Why Combine pipe() and fork()?
A pipe by itself is only useful within a single process. The real power of pipes comes when combined with fork(). After fork(), the child inherits all open file descriptors from the parent β including both ends of the pipe. This is how parent and child processes can communicate with each other.
The critical rule: after fork(), each process must close the pipe end it doesn’t use. This is not optional β it prevents deadlocks and ensures correct EOF detection.
Key Concepts
read
write
This is the classic example from TLPI β the parent sends a command-line string to the child via a pipe.
/* simple_pipe.c β parent writes, child reads */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define BUF_SIZE 10
int main(int argc, char *argv[])
{
int pfd[2]; /* pipe file descriptors */
char buf[BUF_SIZE];
ssize_t numRead;
if (argc != 2) {
fprintf(stderr, "Usage: %s <string>\n", argv[0]);
return 1;
}
/* Step 1: Create the pipe */
if (pipe(pfd) == -1) {
perror("pipe");
return 1;
}
/* Step 2: Fork a child */
switch (fork()) {
case -1:
perror("fork");
return 1;
case 0: /* ===== CHILD: reads from pipe ===== */
/* Close unused write end */
if (close(pfd[1]) == -1) {
perror("close - child write end");
_exit(1);
}
/* Read data in BUF_SIZE chunks until EOF */
for (;;) {
numRead = read(pfd[0], buf, BUF_SIZE);
if (numRead == -1) {
perror("read");
_exit(1);
}
if (numRead == 0)
break; /* EOF: parent closed write end */
/* Echo data to stdout */
if (write(STDOUT_FILENO, buf, numRead) != numRead) {
fprintf(stderr, "child: partial write\n");
_exit(1);
}
}
write(STDOUT_FILENO, "\n", 1); /* trailing newline */
if (close(pfd[0]) == -1) {
perror("close - child read end");
_exit(1);
}
_exit(EXIT_SUCCESS);
default: /* ===== PARENT: writes to pipe ===== */
/* Close unused read end */
if (close(pfd[0]) == -1) {
perror("close - parent read end");
exit(1);
}
/* Write command-line argument to pipe */
if (write(pfd[1], argv[1], strlen(argv[1])) != (ssize_t)strlen(argv[1])) {
fprintf(stderr, "parent: partial write\n");
exit(1);
}
/* Close write end β child sees EOF after this */
if (close(pfd[1]) == -1) {
perror("close - parent write end");
exit(1);
}
/* Wait for child to finish */
wait(NULL);
exit(EXIT_SUCCESS);
}
}
gcc -o simple_pipe simple_pipe.c
./simple_pipe "Hello from parent to child!"
Hello from parent to child!Here we reverse the direction β the child sends data up to the parent.
/* child_to_parent.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
int pfd[2];
char buf[128];
ssize_t n;
pid_t child_pid;
if (pipe(pfd) == -1) {
perror("pipe");
return 1;
}
child_pid = fork();
if (child_pid == -1) {
perror("fork");
return 1;
}
if (child_pid == 0) {
/* ===== CHILD: writes to pipe ===== */
close(pfd[0]); /* close unused read end */
char *msg = "Message from child process!";
printf("[Child PID=%d] Sending: %s\n", getpid(), msg);
write(pfd[1], msg, strlen(msg));
close(pfd[1]); /* EOF signal to parent */
_exit(0);
} else {
/* ===== PARENT: reads from pipe ===== */
close(pfd[1]); /* close unused write end */
n = read(pfd[0], buf, sizeof(buf) - 1);
buf[n] = '\0';
printf("[Parent PID=%d] Received: %s\n", getpid(), buf);
close(pfd[0]);
waitpid(child_pid, NULL, 0);
}
return 0;
}
/* Output:
[Child PID=12346] Sending: Message from child process!
[Parent PID=12345] Received: Message from child process!
*/
For two-way communication, use two separate pipes β one for each direction. Be careful of deadlocks!
/* bidirectional_pipe.c β parent and child talk to each other */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
int p2c[2]; /* parent β child pipe */
int c2p[2]; /* child β parent pipe */
char buf[128];
ssize_t n;
/* Create both pipes before fork */
if (pipe(p2c) == -1 || pipe(c2p) == -1) {
perror("pipe");
return 1;
}
pid_t pid = fork();
if (pid == -1) { perror("fork"); return 1; }
if (pid == 0) {
/* ===== CHILD ===== */
close(p2c[1]); /* don't write to parentβchild pipe */
close(c2p[0]); /* don't read from childβparent pipe */
/* Read request from parent */
n = read(p2c[0], buf, sizeof(buf) - 1);
buf[n] = '\0';
printf("[Child] Received from parent: \"%s\"\n", buf);
/* Send response back to parent */
char *reply = "PONG from child";
write(c2p[1], reply, strlen(reply));
printf("[Child] Sent reply: \"%s\"\n", reply);
close(p2c[0]);
close(c2p[1]);
_exit(0);
} else {
/* ===== PARENT ===== */
close(p2c[0]); /* don't read from parentβchild pipe */
close(c2p[1]); /* don't write to childβparent pipe */
/* Send a message to child */
char *msg = "PING from parent";
write(p2c[1], msg, strlen(msg));
printf("[Parent] Sent: \"%s\"\n", msg);
close(p2c[1]); /* signal EOF so child can proceed */
/* Read response from child */
n = read(c2p[0], buf, sizeof(buf) - 1);
buf[n] = '\0';
printf("[Parent] Received reply: \"%s\"\n", buf);
close(c2p[0]);
wait(NULL);
}
return 0;
}
/* Output:
[Parent] Sent: "PING from parent"
[Child] Received from parent: "PING from parent"
[Child] Sent reply: "PONG from child"
[Parent] Received reply: "PONG from child"
*/
When a shell runs ls | grep .c, it creates a pipe, then forks two children (siblings). The pipe was created by the parent shell. Both siblings inherit both ends, then each closes the end it doesn’t need.
/* sibling_pipe.c β pipe between two child processes (siblings) */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
int pfd[2];
/* Parent creates pipe BEFORE forking children */
if (pipe(pfd) == -1) {
perror("pipe");
return 1;
}
/* Fork first child β WRITER */
pid_t writer = fork();
if (writer == 0) {
close(pfd[0]); /* writer doesn't read */
char *data = "Data from sibling writer";
write(pfd[1], data, strlen(data));
printf("[Writer child PID=%d] Sent data\n", getpid());
close(pfd[1]);
_exit(0);
}
/* Fork second child β READER */
pid_t reader = fork();
if (reader == 0) {
close(pfd[1]); /* reader doesn't write */
char buf[128];
ssize_t n = read(pfd[0], buf, sizeof(buf) - 1);
buf[n] = '\0';
printf("[Reader child PID=%d] Got: \"%s\"\n", getpid(), buf);
close(pfd[0]);
_exit(0);
}
/* Parent: closes both ends (it doesn't use the pipe) */
close(pfd[0]);
close(pfd[1]);
/* Wait for both children */
waitpid(writer, NULL, 0);
waitpid(reader, NULL, 0);
printf("[Parent] Both children done.\n");
return 0;
}
A: Because fork() copies the parent’s open file descriptors. If you call pipe() after fork(), each process would get its own independent pipe with no shared connection between them.
A: Two reasons: (1) If the reader keeps its write end open, it will never see EOF β it will block forever even after the writer is done. (2) If the writer keeps its read end open, the kernel won’t send SIGPIPE when the reader is gone, and the writer could block indefinitely trying to fill a full pipe.
A: When the parent closes its write end of the pipe,
read() in the child returns 0 (EOF), signaling there is no more data.A: Technically yes, but it creates a race condition β you cannot predict which process will get which data. Normally only one process reads from a pipe.
A: Create two pipes before fork(). One pipe carries data from parent to child, the other from child to parent. Each process closes the ends it doesn’t use in each pipe.
A: The parent calls
wait() (or waitpid()) to wait for the child to terminate, preventing zombie processes and ensuring orderly cleanup.