Pipes & Signals
Connecting processes with pipes and handling interrupts with signals
Quick check before you start: In the shell,
ls | wc -lcounts the number of files. How does the shell connect those two programs? If you are not sure, read on.Practice this topic: Pipes & Signals skill drill
After this lesson, you will be able to:
- Create a pipe with
pipe(fd)and explain which end is read vs. write - Use
dup2to redirect stdin/stdout through a pipe - Build
ls | wc -lin C - Write a basic signal handler for SIGINT
Creating a Pipe
A pipe is a one-way communication channel between two processes. The pipe() system call creates it:
int fd[2];
pipe(fd);
After this call:
fd[0]is the read endfd[1]is the write end
Data written to fd[1] comes out of fd[0]. Think of it as a physical pipe — water goes in one end and comes out the other.
Connecting Two Processes
The pattern for piping one program’s output into another:
- Create the pipe
- Fork
- In the child: close the read end, redirect stdout to the write end
- In the parent: close the write end, redirect stdin to the read end
int fd[2];
pipe(fd);
pid_t pid = fork();
if (pid == 0) {
// Child: will write to pipe
close(fd[0]); // close read end
dup2(fd[1], STDOUT_FILENO); // stdout → pipe write end
close(fd[1]); // close original write fd
execlp("ls", "ls", NULL);
perror("execlp");
exit(EXIT_FAILURE);
}
dup2: Redirecting File Descriptors
dup2(old_fd, new_fd) makes new_fd point to the same place as old_fd. After dup2(fd[1], STDOUT_FILENO), anything the child writes to stdout goes into the pipe instead of the terminal.
Building ls | wc -l in C
Here is the complete program:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void) {
int fd[2];
pipe(fd);
pid_t pid1 = fork();
if (pid1 == 0) {
// Child 1: ls (writes to pipe)
close(fd[0]);
dup2(fd[1], STDOUT_FILENO);
close(fd[1]);
execlp("ls", "ls", NULL);
perror("ls");
exit(EXIT_FAILURE);
}
pid_t pid2 = fork();
if (pid2 == 0) {
// Child 2: wc -l (reads from pipe)
close(fd[1]);
dup2(fd[0], STDIN_FILENO);
close(fd[0]);
execlp("wc", "wc", "-l", NULL);
perror("wc");
exit(EXIT_FAILURE);
}
// Parent: close both ends and wait
close(fd[0]);
close(fd[1]);
wait(NULL);
wait(NULL);
return 0;
}
The parent closes both pipe ends because it does not use the pipe. Then it waits for both children. Closing unused pipe ends is essential — if the read end stays open in a process that is not reading, the writer may block forever.
Closing the Right End
Rule: each process closes the pipe end it does not use. If you forget:
- Leaving the write end open in the reader means the reader never sees EOF
- Leaving the read end open in the writer wastes a file descriptor
Signals
A signal is an asynchronous notification sent to a process. The most common one is SIGINT — sent when you press Ctrl+C.
By default, SIGINT terminates the program. You can install a custom handler:
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
void handle_sigint(int sig) {
(void)sig; // suppress unused warning
const char msg[] = "\nCaught SIGINT — cleaning up\n";
write(STDOUT_FILENO, msg, sizeof(msg) - 1);
// free resources, close files, etc.
_exit(0);
}
int main(void) {
signal(SIGINT, handle_sigint);
printf("Running... press Ctrl+C to stop\n");
while (1) {
// work
}
return 0;
}
When the user presses Ctrl+C, the kernel delivers SIGINT to the process. Instead of dying, the process runs handle_sigint, which can clean up resources before exiting.
Why write() instead of printf()? A signal can arrive while printf() is in the middle of executing and holding an internal lock on stdout. If the handler calls printf() too, it tries to acquire the same lock, causing a deadlock. The write() system call is async-signal-safe — it does not use internal locks, so it is always safe to call from a handler. Similarly, _exit() is safe where exit() is not, because exit() flushes stdio buffers and runs atexit handlers that may themselves use non-signal-safe functions.
Common signals:
| Signal | Default | Triggered by |
|---|---|---|
| SIGINT | Terminate | Ctrl+C |
| SIGTERM | Terminate | kill command |
| SIGSEGV | Core dump | Invalid memory access |
| SIGPIPE | Terminate | Writing to a broken pipe |
| SIGKILL | Terminate | kill -9 (cannot be caught or ignored) |
pipe(fd), which file descriptor does the reading process close?fd[0] to read, so it closes fd[1] (the write end). If it kept the write end open, the pipe would never signal EOF because there would still be a potential writer.
What Comes Next
You have covered the full systems programming toolkit: processes, pipes, and signals. In the final lesson, you will step back and see how everything in this course connects — from Java to C, from the shell to the kernel.