Chapter 73: IPC: Unnamed Pipes (pipe()
) for Parent-Child Communication
Chapter Objectives
By the end of this chapter, you will be able to:
- Understand the fundamental principles of Inter-Process Communication (IPC) and the role of pipes.
- Implement unnamed pipes in C using the
pipe()
system call to establish a communication channel. - Manage file descriptors and data flow between a parent and child process after a
fork()
. - Develop robust applications that use pipes for unidirectional data transfer.
- Debug common issues related to pipe implementation, such as deadlocks and improper file descriptor management.
- Use pipes in conjunction with
dup2()
andexec()
to build simple shell-like pipelines.
Introduction
In any modern multitasking operating system, processes rarely exist in complete isolation. They often need to cooperate, share data, and signal one another to accomplish complex tasks. This coordination is achieved through Inter-Process Communication (IPC), a set of mechanisms that allow processes to exchange information. IPC is the invisible plumbing that enables the sophisticated features we take for granted, from the simple act of copying and pasting text between applications to the complex data streams managed by web servers. In the world of embedded Linux, where resource-constrained systems must perform specialized tasks efficiently, robust IPC is not just a convenience—it is a necessity. It allows a developer to design modular, resilient systems where dedicated processes handle specific tasks like sensor data acquisition, network communication, or user interface management, all while communicating seamlessly.
mindmap root((Linux IPC)) Unnamed Pipes ::icon(fa fa-water) Unidirectional For related processes (parent-child) Stream-oriented Kernel-managed (Chapter Focus) Named Pipes (FIFOs) ::icon(fa fa-folder-open) Have a filesystem entry For unrelated processes Also stream-oriented Shared Memory ::icon(fa fa-memory) Fastest method Processes map same memory region Requires synchronization (semaphores) Message Queues ::icon(fa fa-envelope) Structured messages Kernel manages queue Processes read/write messages Sockets ::icon(fa fa-network-wired) For network communication Can be used on same machine (UNIX domain) Flexible and powerful Signals ::icon(fa fa-bell) Simple notifications Asynchronous Limited data transfer
This chapter introduces one of the oldest and most fundamental IPC mechanisms in the UNIX and Linux world: the unnamed pipe. A pipe provides a simple, unidirectional channel for a stream of data to flow from one process to another. Its power lies in its simplicity and integration with the Linux file I/O model. When you type a command like ls -l | grep '.c'
into your shell, you are using a pipe to direct the output of the ls
command to become the input of the grep
command. This elegant concept is made possible by the pipe()
system call. We will explore how to create and manage these communication channels within the context of parent and child processes, a foundational pattern in Linux system programming. Using the Raspberry Pi 5 as our development platform, you will learn to build C programs that leverage pipes to create efficient, modular applications, mastering a skill that is essential for any serious embedded Linux developer.
Technical Background
At its core, an unnamed pipe is a kernel-managed buffer that connects two file descriptors. It is a “pipe” in the most literal sense: what is written into one end can be read from the other. This mechanism is inherently simple and secure because it exists only within the kernel’s memory space and is accessible only to the process that created it and its descendants. Because they have no name in the filesystem, they cannot be accessed by unrelated processes, which is why they are primarily used for communication between a parent and its child.
Creating a Pipe: The pipe()
System Call
The journey begins with the pipe()
system call, which is defined in <unistd.h>
:
int pipe(int pipefd[2]);
This function asks the kernel to create a new pipe object. If successful, it returns 0 and populates a two-integer array, which we’ve called pipefd
, with two new file descriptors. If an error occurs, it returns -1 and sets the errno
variable. These two file descriptors are the endpoints of our communication channel:
pipefd[0]
is the read end of the pipe. Any data written to the other end can be read from this file descriptor.pipefd[1]
is the write end of the pipe. Any data written to this file descriptor is buffered by the kernel and becomes available for reading at the other end.
It is a crucial and unchangeable rule that data flows in only one direction: from pipefd[1]
to pipefd[0]
. Think of it as a one-way water pipe; you can put water in at the source (pipefd[1]
) and it will come out at the destination (pipefd[0]
), but not the other way around.
The Pipe and the Fork: Establishing Communication
A pipe created within a single process is not particularly useful. Its true power is unlocked when combined with the fork()
system call. When a process calls fork()
, the child process inherits a complete copy of the parent’s file descriptor table. This means that after the fork, both the parent and the child process have file descriptors pointing to the same underlying pipe object in the kernel. Both processes now hold handles to both the read end and the write end.
This is where a critical design pattern emerges. To establish a clear, unidirectional flow of communication, each process must close the end of the pipe it does not intend to use. For example, if the parent wants to send data to the child:
- The Parent will write to the pipe, so it closes its read end (
pipefd[0]
). It will usepipefd[1]
to send data. - The Child will read from the pipe, so it closes its write end (
pipefd[1]
). It will usepipefd[0]
to receive data.
By closing the unused ends, we create a clean, one-way channel from the parent’s pipefd[1]
to the child’s pipefd[0]
. This cleanup is not just good practice; it is essential for correct program behavior.
graph TD subgraph "Process A (Initial State)" A1(Start) --> A2["<b>pipe()</b> System Call"]; style A1 fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff style A2 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff end A2 --> A3{"Kernel creates pipe object<br>Returns pipefd[0] and pipefd[1]"}; style A3 fill:#8b5cf6,stroke:#8b5cf6,stroke-width:1px,color:#ffffff A3 --> B1["<b>fork()</b> System Call"]; style B1 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff subgraph "After fork()" direction LR B1 --> P1["<b>Parent Process</b><br>Inherits pipefd[0], pipefd[1]"]; B1 --> C1["<b>Child Process</b><br>Inherits pipefd[0], pipefd[1]"]; style P1 fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff style C1 fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff end P1 --> P2{"Parent wants to <b>WRITE</b><br>Closes its READ end: <i>close(pipefd[0])</i>"}; C1 --> C2{"Child wants to <b>READ</b><br>Closes its WRITE end: <i>close(pipefd[1])</i>"}; style P2 fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff style C2 fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff subgraph "Established Communication Channel" P3["Parent writes data using <b>write(pipefd[1], ...)</b>"] C3["Child reads data using <b>read(pipefd[0], ...)</b>"] style P3 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style C3 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff end P2 --> P3 P3 --> KERNEL_PIPE KERNEL_PIPE --> C3 C2 --> C3 C3 --> E1(Data Received) style E1 fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff subgraph "Kernel Space" KERNEL_PIPE[("Pipe Buffer<br><i>(One-Way Data Flow)</i>")] style KERNEL_PIPE fill:#8b5cf6,stroke:#8b5cf6,stroke-width:4px,color:#ffffff,stroke-dasharray: 5 5 end
Reading, Writing, and Blocking Behavior
Once the pipe is established, the processes can use the standard read()
and write()
system calls on the file descriptors, just as they would with a file. However, the behavior of these calls on a pipe has some special characteristics.
The pipe is backed by a kernel buffer of a fixed size. On modern Linux systems, this size is typically 65,536 bytes (64 KiB), but you can query the system-guaranteed minimum atomic write size using PIPE_BUF
from <limits.h>
.
sequenceDiagram actor User participant P as Parent (ls -l) participant K as Kernel Pipe participant C as Child (wc -l) User->>P: Executes ./pipeline activate P P->>K: write("file1...") activate K Note over K,C: Child's read() is blocking,<br>waiting for data. K-->>C: Data available deactivate K activate C C->>C: Processes "file1..." P->>K: write("file2...") activate K K-->>C: Data available deactivate K C->>C: Processes "file2..." P->>P: Finishes listing files P->>K: close(write_end) note over P,K: Signals End-of-File (EOF) activate K K-->>C: read() returns 0 (EOF) deactivate K C->>User: Prints final count deactivate C P->>User: Parent exits deactivate P
- Writing to a Pipe (
write()
): When a process callswrite()
on the write end of the pipe, the kernel copies the data into its internal buffer. If the write is small enough (less than or equal toPIPE_BUF
), the kernel guarantees that the write operation is atomic. This means the data from this singlewrite()
call will not be interleaved with data from any other process writing to the same pipe. If the pipe’s buffer is full, thewrite()
call will block; the process will be put to sleep until some other process reads enough data from the pipe to make space. - Reading from a Pipe (
read()
): When a process callsread()
on the read end of the pipe, it retrieves data from the kernel’s buffer. If the buffer is empty, theread()
call will block until some data is written to the pipe. If the write end of the pipe has been closed by all processes that hold it, aread()
call on an empty pipe will not block. Instead, it will immediately return 0, which is the standard end-of-file (EOF) indication. This is why closing the write end in the reading process is so important—it’s the only way for the reader to know when there is no more data to come.

Pipe Lifecycle and the SIGPIPE
Signal
The kernel is responsible for managing the pipe object’s lifecycle. It keeps a reference count of how many file descriptors (across all processes) point to the pipe. The pipe is only destroyed when this reference count drops to zero—that is, when every file descriptor connected to it has been closed.
This leads to an important and sometimes surprising behavior related to the SIGPIPE
signal. What happens if a process tries to write to a pipe whose read end has been closed by all processes? For instance, if the child process, which was supposed to be reading the data, terminates or closes its read descriptor prematurely. In this case, there is no longer a reader. The kernel cannot simply buffer the data forever.
graph TD A["Process calls <b>write()</b> on a pipe"] --> B{"Is there a reader?<br>(Is pipe's read-end open by any process?)"}; style A fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff style B fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff B -- Yes --> C{Is the pipe buffer full?}; style C fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff C -- No --> D[Write succeeds.<br>Data is copied to buffer.]; style D fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff C -- Yes --> E[Process <b>BLOCKS</b>.<br>Waits for a reader to make space.]; style E fill:#eab308,stroke:#eab308,stroke-width:1px,color:#1f2937 B -- No --> F["<b>No Reader Exists!</b><br>All read-ends of the pipe are closed."]; style F fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff F --> G{Is SIGPIPE handled or ignored by the process?}; style G fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff G -- No --> H["Kernel sends <b>SIGPIPE</b> signal.<br>Default action: Terminate Process."]; style H fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff G -- Yes --> I["<b>write()</b> fails.<br>Returns -1, errno is set to EPIPE."]; style I fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff
Instead of letting the write()
call block indefinitely or return an error, the kernel sends the SIGPIPE
signal to the writing process. The default action for SIGPIPE
is to terminate the process. This is often the desired behavior; if the consumer of your data has disappeared, it usually indicates a fatal error condition. However, programs can choose to ignore this signal or install a custom signal handler for it. If SIGPIPE
is handled or ignored, the write()
call will fail, returning -1, and errno
will be set to EPIPE
.
Tip: The
ls | head -n 1
command is a great example ofSIGPIPE
in action. Thels
command might produce a long list of files, writing them all to the pipe. Thehead
command, however, only reads the first line and then exits. When it exits, it closes its input file descriptor, which is the read end of the pipe. The next timels
tries to write to the pipe, it receives aSIGPIPE
and is terminated by the kernel. This is an efficient way to stop producer processes once they are no longer needed.
Redirecting Standard I/O with dup2()
Pipes become even more powerful when combined with I/O redirection. The dup2()
system call allows a process to duplicate a file descriptor, making the new descriptor number an exact copy of an old one.
int dup2(int oldfd, int newfd);
This call forces newfd
to refer to the same open file description as oldfd
. If newfd
was already open, it is silently closed first. This is the mechanism shells use to implement pipelines. To make a child process read its input from a pipe, you can use dup2()
to copy the pipe’s read descriptor (pipefd[0]
) onto the standard input descriptor (STDIN_FILENO
, which is always 0). Similarly, to make a process write its output to a pipe, you can copy the pipe’s write descriptor (pipefd[1]
) onto the standard output descriptor (STDOUT_FILENO
, which is always 1).
After the dup2()
call, any library function or system call that writes to standard output (like printf()
) will now write directly into the pipe. This allows us to connect existing programs that know nothing about pipes, which is the essence of the UNIX philosophy of building small, single-purpose tools that work together. A process can re-wire its own standard streams and then use execvp()
to load and run a new program, which will inherit these redirected streams, completely unaware that its input is coming from a pipe instead of a terminal.
Practical Examples
Let’s move from theory to practice. The following examples are designed to be compiled and run on your Raspberry Pi 5. You will need the GCC compiler, which is standard on Raspberry Pi OS.
Example 1: Simple Parent-to-Child Communication
This first example demonstrates the fundamental pattern. A parent process will create a pipe, fork a child, and then send a simple string message to the child, which reads the message and prints it to the console.
File: simple_pipe.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>
int main() {
int pipefd[2];
pid_t cpid;
char buf;
const char *message = "Hello from your parent!";
// Create the pipe.
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
// Fork a child process.
cpid = fork();
if (cpid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0) { // Child process reads from pipe
printf("Child: I am the child process.\n");
// The child will only read, so it closes the write end of the pipe.
// This is a critical step!
close(pipefd[1]);
printf("Child: Waiting to read from the pipe...\n");
// Read from the pipe one character at a time and print to stdout.
while (read(pipefd[0], &buf, 1) > 0) {
write(STDOUT_FILENO, &buf, 1);
}
write(STDOUT_FILENO, "\n", 1);
printf("Child: Reached EOF. Closing read end and exiting.\n");
// Close the read end of the pipe.
close(pipefd[0]);
exit(EXIT_SUCCESS);
} else { // Parent process writes to pipe
printf("Parent: I am the parent process.\n");
// The parent will only write, so it closes the read end of the pipe.
close(pipefd[0]);
printf("Parent: Writing message to the pipe...\n");
// Write the message to the pipe.
write(pipefd[1], message, strlen(message));
// After writing, close the write end. This sends an EOF to the reader.
printf("Parent: Closing write end of the pipe.\n");
close(pipefd[1]);
// Wait for the child process to terminate.
wait(NULL);
printf("Parent: Child has terminated. Exiting.\n");
exit(EXIT_SUCCESS);
}
}
Build and Run Steps:
1. Save the code above as simple_pipe.c
on your Raspberry Pi 5.
2. Open a terminal and compile the program:
gcc -o simple_pipe simple_pipe.c -Wall
The -Wall
flag enables all compiler warnings, which is a good practice.
3. Run the executable:
./simple_pipe
Expected Output:
The exact order of the first few lines may vary due to the scheduler, but the overall flow will be consistent.
Parent: I am the parent process.
Parent: Writing message to the pipe...
Parent: Closing write end of the pipe.
Child: I am the child process.
Child: Waiting to read from the pipe...
Hello from your parent!
Child: Reached EOF. Closing read end and exiting.
Parent: Child has terminated. Exiting.
Code Explanation:
pipe(pipefd)
: We create the pipe.pipefd[0]
is for reading,pipefd[1]
for writing.fork()
: We create the child process. The code following theif/else
block executes in two separate processes.- Child Process (
cpid == 0
):close(pipefd[1])
: The child’s first action is to close the write end. It will only be a reader. This is crucial because it ensures that when the parent closes its write end, the child will see an EOF. If the child keptpipefd[1]
open, it would never detect the end of the data stream.while (read(pipefd[0], &buf, 1) > 0)
: The child enters a loop, reading one byte at a time from the pipe.read()
will block until data is available. It returns the number of bytes read, or 0 on EOF, or -1 on error.close(pipefd[0])
: Once the loop finishes (on EOF), the child closes its read descriptor and exits.
- Parent Process (
cpid > 0
):close(pipefd[0])
: The parent closes its read end, as it will only be a writer.write(pipefd[1], ...)
: The parent writes the entire message string into the pipe.close(pipefd[1])
: This is the most important step for the parent. By closing the write end, it signals “end of file” to the reading process. The child’sread()
call, which was blocking, will now return 0, causing its loop to terminate.wait(NULL)
: The parent waits for the child to finish, which is good practice to prevent zombie processes.
Example 2: Emulating a Shell Pipeline (ls | wc -l
)
This more advanced example demonstrates how to redirect standard I/O to create a pipeline, just like the shell does. The parent process will execute ls -l
, and its standard output will be piped to the child process, which will execute wc -l
on that input.
graph TD subgraph "Main Process" A1(Start) --> A2["Create Pipe: <b>pipe()</b>"]; A2 --> A3["Create Child: <b>fork()</b>"]; style A1 fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff style A2 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff style A3 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff end A3 --> PARENT; A3 --> CHILD; subgraph "Parent Process (becomes ls -l)" PARENT[Parent] --> P1["Close unused READ end<br><i>close(pipefd[0])</i>"]; P1 --> P2["Redirect STDOUT to pipe's WRITE end<br><b>dup2(pipefd[1], STDOUT_FILENO)</b>"]; P2 --> P3["Close original WRITE end<br><i>close(pipefd[1])</i>"]; P3 --> P4["Replace process with 'ls -l'<br><b>execlp('ls', ...)</b>"]; P4 --> P_OUT(["ls -l output"]); style PARENT fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff style P1 fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff style P2 fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff style P3 fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff style P4 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff end subgraph "Child Process (becomes wc -l)" CHILD[Child] --> C1["Close unused WRITE end<br><i>close(pipefd[1])</i>"]; C1 --> C2["Redirect STDIN to pipe's READ end<br><b>dup2(pipefd[0], STDIN_FILENO)</b>"]; C2 --> C3["Close original READ end<br><i>close(pipefd[0])</i>"]; C3 --> C4["Replace process with 'wc -l'<br><b>execlp('wc', ...)</b>"]; C_IN(["wc -l input"]) --> C4; style CHILD fill:#1e3a8a,stroke:#1e3a8a,stroke-width:2px,color:#ffffff style C1 fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff style C2 fill:#f59e0b,stroke:#f59e0b,stroke-width:1px,color:#ffffff style C3 fill:#ef4444,stroke:#ef4444,stroke-width:1px,color:#ffffff style C4 fill:#0d9488,stroke:#0d9488,stroke-width:1px,color:#ffffff end subgraph "Kernel Pipe" PIPE[("Data Stream")] style PIPE fill:#8b5cf6,stroke:#8b5cf6,stroke-width:4px,color:#ffffff,stroke-dasharray: 5 5 end P_OUT --> PIPE --> C_IN; C4 --> FINAL_OUTPUT(Final Output to Console); style FINAL_OUTPUT fill:#10b981,stroke:#10b981,stroke-width:2px,color:#ffffff
File: pipeline.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main() {
int pipefd[2];
pid_t cpid;
if (pipe(pipefd) == -1) {
perror("pipe");
exit(EXIT_FAILURE);
}
cpid = fork();
if (cpid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0) { // Child process: executes "wc -l"
// Close the write end of the pipe, as it's not needed.
close(pipefd[1]);
// Redirect standard input to be the read end of the pipe.
// After this, any read from STDIN_FILENO will read from the pipe.
if (dup2(pipefd[0], STDIN_FILENO) == -1) {
perror("dup2");
exit(EXIT_FAILURE);
}
// The original pipe descriptor is no longer needed.
close(pipefd[0]);
// Execute "wc -l". This program reads from its standard input.
execlp("wc", "wc", "-l", NULL);
// execlp only returns on error.
perror("execlp wc");
exit(EXIT_FAILURE);
} else { // Parent process: executes "ls -l"
// Close the read end of the pipe, as it's not needed.
close(pipefd[0]);
// Redirect standard output to be the write end of the pipe.
// After this, any write to STDOUT_FILENO will write to the pipe.
if (dup2(pipefd[1], STDOUT_FILENO) == -1) {
perror("dup2");
exit(EXIT_FAILURE);
}
// The original pipe descriptor is no longer needed.
close(pipefd[1]);
// Execute "ls -l". Its output now goes into the pipe.
execlp("ls", "ls", "-l", NULL);
// execlp only returns on error.
perror("execlp ls");
exit(EXIT_FAILURE);
}
// The parent doesn't reach here unless execlp fails.
// In a more robust shell, you'd wait for the child here.
return 0;
}
Build and Run Steps:
1. Save the code as pipeline.c
.
2. Compile it:
gcc -o pipeline pipeline.c -Wall
3. Run it:
./pipeline
Expected Output:
The program will print a single number, which is the number of files and directories in your current working directory (including .
and ..
), followed by a newline. For example:
15
This is the exact same output you would get from running ls -l | wc -l
in your shell.
Code Explanation:
- Child Process (
wc -l
):close(pipefd[1])
: Closes the unused write end.dup2(pipefd[0], STDIN_FILENO)
: This is the key step. It duplicates the pipe’s read descriptor (pipefd[0]
) onto the standard input descriptor (STDIN_FILENO
). Now, file descriptor 0 points to the read end of the pipe.close(pipefd[0])
: Afterdup2
, we have two descriptors pointing to the read end (pipefd[0]
andSTDIN_FILENO
). We no longer need the original, so we close it.execlp("wc", "wc", "-l", NULL)
: We replace the child process’s image with thewc
program.wc
naturally reads from its standard input, which we have just cleverly rewired to be our pipe. It has no idea it’s not reading from a terminal.
- Parent Process (
ls -l
):close(pipefd[0])
: Closes the unused read end.dup2(pipefd[1], STDOUT_FILENO)
: Duplicates the pipe’s write descriptor (pipefd[1]
) onto the standard output descriptor (STDOUT_FILENO
). Now, file descriptor 1 points to the write end of the pipe.close(pipefd[1])
: Closes the original write descriptor.execlp("ls", "ls", "-l", NULL)
: Replaces the parent process withls -l
.ls
writes its output to standard output, which we have rewired to our pipe. Its output flows directly to the waitingwc
process.
Warning: In this simplified example, the parent process calls
exec
. A real shell would fork twice—once for each command in the pipeline—and the main shell process would wait for both children to complete.
Common Mistakes & Troubleshooting
Implementing pipes seems straightforward, but small logical errors can lead to baffling behavior. Here are some of the most common pitfalls.
Exercises
- Child Data Transformation: Modify the
simple_pipe.c
example. The parent should send a lowercase string to the child. The child process should read the string, convert it to uppercase, and then print it to the console. This reinforces the concept of the child processing data received from the pipe. - Two-Way Communication: Create a program that uses two pipes to establish bidirectional communication. The parent should send a number to the child. The child should read the number, multiply it by 5, and send the result back to the parent on the second pipe. The parent should then read the result and print it. Be careful to avoid the deadlock scenario described above.
- File Transfer and Checksum: Write a program where the parent process reads a text file (e.g.,
/etc/hostname
) and sends its contents through a pipe to a child process. The child process should read all the data from the pipe and calculate a simple checksum (e.g., the sum of all byte values). The child should then print the calculated checksum to the console. - Pipeline with
grep
: Implement a program that emulates the pipelinecat /etc/os-release | grep PRETTY_NAME
. The parent process should executecat
with the specified file, piping its output to the child. The child process should executegrep
with the specified pattern, reading from the pipe. This exercise solidifies the use ofdup2
andexeclp
. - Three-Stage Pipeline: As a challenge, create a three-stage pipeline that emulates
ls -l | grep '^-' | wc -l
(which counts only the regular files in a directory). This will require the main parent process to fork two children. The first child (ls -l
) will pipe its output to the second child (grep '^-'
), which will in turn pipe its output to a third process (which could be the original parent or a third child) that runswc -l
. This requires careful management of two separate pipes and multiple processes.
Summary
- Unnamed Pipes provide a simple, unidirectional IPC channel for a stream of bytes between related processes.
- The
pipe()
system call creates a pipe and returns two file descriptors: a read end and a write end. - Pipes are most commonly used with
fork()
, where the parent and child processes inherit the pipe’s file descriptors. - Crucial Cleanup: To establish a working channel, the writing process must close its read descriptor, and the reading process must close its write descriptor.
- Closing the write end of a pipe signals an End-of-File (EOF) to the reader. The
read()
call will then return 0. - Writing to a pipe whose read end has been closed triggers the
SIGPIPE
signal, which by default terminates the writing process. - The
dup2()
system call is a powerful tool for redirecting a process’s standard input or output to a pipe, enabling the creation of shell-like pipelines withexec()
.
Further Reading
- Linux man pages: The definitive source. On your terminal, run
man 2 pipe
,man 2 fork
,man 2 dup2
, andman 2 wait
. - Advanced Programming in the UNIX Environment by W. Richard Stevens and Stephen A. Rago. Chapters on process control and IPC are the gold standard for this topic.
- The Linux Programming Interface by Michael Kerrisk. An exhaustive and modern reference for Linux system programming, with excellent chapters on pipes and process creation.
- Beej’s Guide to Unix Interprocess Communication: An accessible, friendly guide that covers pipes and other IPC mechanisms. (https://beej.us/guide/bgipc/)
- LWN.net: An excellent source for in-depth articles on kernel development and system programming topics. Search their archives for articles on pipes and IPC. (https://lwn.net/)
- GNU C Library (glibc) Manual: The official documentation for the C library functions, including detailed descriptions of pipe-related functions and their behavior. (https://www.gnu.org/software/libc/manual/)