Pipes
A pipe is a one-way byte channel with two ends, each a file descriptor. What one process writes at fds[1] another reads at fds[0], in order, with no message boundaries. It is the mechanism behind the shell's |, and the simplest way for a parent to hand data to a child.
A pipe in one process
pipe fills an array with the read end and the write end. The same process can use both, which is the smallest test of the mechanism. read returns the number of bytes actually read, and write the number written, so both results are compared.
#include <stdio.h>
#include <unistd.h>
int main(void)
{
int fds[2];
if (pipe(fds) != 0) {
perror("pipe");
return 1;
}
const char *message = "hello pipe";
if (write(fds[1], message, 10) != 10) {
return 1;
}
char buffer[16] = {0};
ssize_t got = read(fds[0], buffer, sizeof buffer - 1);
printf("read %zd: %s\n", got, buffer);
close(fds[0]);
close(fds[1]);
return 0;
}
gcc -Wall -Wextra pipe.c -o pipe
./pipe
read 10: hello pipe
Ten bytes went in and ten came out. The buffer reserves one byte for the terminator that read does not add, which is why the count is passed to printf separately. Closing both ends releases the descriptors, the habit from lesson 55.
Parent writes, child reads
After fork both processes hold both ends, so each one closes the end it does not need. The parent closes the read end and writes; the child closes the write end and reads until the parent closes its side, which is the end-of-file for the reader. Closing the unused ends is what makes the protocol work.
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
int fds[2];
if (pipe(fds) != 0) {
return 1;
}
pid_t pid = fork();
if (pid == 0) {
close(fds[1]);
char buffer[16] = {0};
ssize_t got = read(fds[0], buffer, sizeof buffer - 1);
printf("child read %zd: %s\n", got, buffer);
close(fds[0]);
_exit(0);
}
close(fds[0]);
const char *message = "from parent";
write(fds[1], message, strlen(message));
close(fds[1]);
waitpid(pid, NULL, 0);
return 0;
}
gcc -Wall -Wextra forkpipe.c -o forkpipe
./forkpipe
child read 11: from parent
The child's read returned eleven bytes because the parent wrote a strlen of eleven characters and closed. A reader on a pipe does not get a zero terminator, so the child's buffer was initialised to zero before the read. The wait keeps the parent alive until the child has printed.
Redirecting stdout
A child that execs a program can send that program's output into a pipe instead of the terminal. dup2 makes the pipe's write end the new descriptor 1, and the exec'd program writes to stdout without knowing that it is a pipe.
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>
int main(void)
{
int fds[2];
if (pipe(fds) != 0) {
return 1;
}
pid_t pid = fork();
if (pid == 0) {
close(fds[0]);
dup2(fds[1], STDOUT_FILENO);
close(fds[1]);
execlp("echo", "echo", "through the pipe", (char *) NULL);
_exit(1);
}
close(fds[1]);
char buffer[64] = {0};
ssize_t got = read(fds[0], buffer, sizeof buffer - 1);
printf("parent read %zd: %s", got, buffer);
close(fds[0]);
waitpid(pid, NULL, 0);
return 0;
}
gcc -Wall -Wextra dup2.c -o dup2
./dup2
parent read 17: through the pipe
Seventeen bytes is "through the pipe" plus the newline that echo writes. The child duplicated the pipe onto descriptor 1 and then closed the original, so only one descriptor refers to the write end; the parent sees end-of-file when the child exits. This is the shell pipeline in miniature.
A read that never ends
A read on a pipe returns as soon as some data is available and waits while the pipe is empty and at least one write end is open. The program below keeps both ends open in the same process and reads an empty pipe, so the read waits forever. The shell's timeout command stops it after two seconds.
#include <stdio.h>
#include <unistd.h>
int main(void)
{
int fds[2];
if (pipe(fds) != 0) {
return 1;
}
char buffer[4];
ssize_t got = read(fds[0], buffer, sizeof buffer);
printf("read %zd\n", got);
return 0;
}
gcc -Wall -Wextra block.c -o block
timeout 2 ./block; echo "status: $?"
status: 124
Status 124 is timeout's own code, meaning it killed the program. The fix in the two-process programs was to close the write end before reading: the reader then gets 0, the end-of-file, and stops. In one process the write end has to be closed before the read, and no data can arrive afterwards, which is why a pipe is a channel between processes rather than a buffer inside one.
Exercises
- Send two messages through one pipe and read them with two reads; check that the bytes arrive in order.
- Run /bin/cat in a child with its stdin connected to a pipe and its stdout to the terminal, then write a line from the parent.
- Use poll or select to wait for the pipe with a timeout of one second instead of blocking.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
Which descriptor end of a pipe does a process read from?
Answer
fds[0], the read endpipefills the array with the read end first and the write end second, and data flows one way. -
Why close the unused end of a pipe after
fork?Answer
The reader must see end-of-file when the writer closes
An open write end keeps the reader waiting, so each process closes the end it does not use.
-
What does
dup2(fds[1], STDOUT_FILENO)do beforeexec?Answer
It makes the pipe the child's standard output
The duplicated descriptor becomes stdout, so the new program writes into the pipe without knowing it.
-
When does a read on an empty pipe return zero?
Answer
When all write ends have been closed
Zero is the end-of-file, and it appears only after the last write descriptor is closed.
-
What does this program print?
#include <stdio.h> #include <unistd.h> int main(void){int f[2];pipe(f);write(f[1],"hello pipe",10);char b[16]={0};ssize_t n=read(f[0],b,15);printf("%zd %s\n",n,b);close(f[0]);close(f[1]);return 0;}Answer
10 hello pipe
Ten bytes are written and read, and the buffer is zeroed so the string ends there.
-
What does this program print?
#include <stdio.h> #include <string.h> #include <sys/wait.h> #include <unistd.h> int main(void){int f[2];pipe(f);pid_t p=fork();if(p==0){close(f[1]);char b[16]={0};ssize_t n=read(f[0],b,15);printf("child read %zd: %s\n",n,b);close(f[0]);_exit(0);}close(f[0]);const char*m="from parent";write(f[1],m,strlen(m));close(f[1]);waitpid(p,0,0);return 0;}Answer
child read 11: from parent
The parent writes the eleven characters of the message and closes, so the child reads eleven bytes.
-
What does this program print?
#include <stdio.h> #include <sys/wait.h> #include <unistd.h> int main(void){int f[2];pipe(f);pid_t p=fork();if(p==0){close(f[0]);dup2(f[1],STDOUT_FILENO);close(f[1]);execlp("echo","echo","through the pipe",(char*)NULL);_exit(1);}close(f[1]);char b[64]={0};ssize_t n=read(f[0],b,63);printf("%zd %s",n,b);close(f[0]);waitpid(p,0,0);return 0;}Answer
17 through the pipe
echowrites the fifteen characters plus a space and newline, seventeen bytes in total. -
What happens to a read on a pipe with both ends open and no data?
Answer
It blocks until
timeoutkills the programWith a write end still open the reader waits, and the shell reports the killed process with status 124.
-
What happens if the writer forgets to close its end?
Answer
The reader keeps waiting for more data
The reader cannot see end-of-file while any write descriptor remains open, so it stays blocked.
-
Does
readadd a terminating zero to the buffer?Answer
No, the caller must zero or terminate it
readreturns a byte count and writes only those bytes, so the examples initialise the buffer before printing.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev