03-IPC (Inter-Process Communication)
Problem 1: Shared Memory Allocation
Section titled “Problem 1: Shared Memory Allocation”Why is it necessary for two processes to explicitly allocate shared memory to share a data structure such as an array?
Instructor Solution
It is necessary because, by default, modern operating systems enforce Process Isolation. Each process operates within its own Private Virtual Address Space, meaning the same memory address in Process A and Process B points to different physical locations. Without explicitly allocating Shared Memory:
- Hardware Protection: The CPU’s Memory Management Unit (MMU) prevents one process from accessing another’s memory to ensure security and stability.
- Independent Page Tables: Each process has its own set of page tables; therefore, changes made to an array in Process A are physically invisible to Process B.
Elaboration (by LLM)
Process isolation is a cornerstone of operating system security and stability:
The Problem Without Shared Memory:
Each process operates in its own virtual address space, isolated from all others:
- Process A’s virtual address 0x1000 maps to physical address 0x4000 (via page table)
- Process B’s virtual address 0x1000 maps to physical address 0x5000 (different page table)
- Even though both use the same virtual address, they access different physical memory
- Modifications in Process A are completely invisible to Process B
Memory Management Unit (MMU) Role:
The MMU hardware enforces this isolation:
- Virtual-to-Physical Translation: Translates virtual addresses using per-process page tables
- Access Control: Prevents access to physical memory not owned by the current process
- Privilege Checking: Ensures user-mode processes cannot access kernel memory
Process A: write to address 0x1000 ↓ (MMU translation) Actually writes to physical 0x4000
Process B: read from address 0x1000 ↓ (different page table) Actually reads from physical 0x5000 (different data!)Why Isolation is Critical:
- Security: One process cannot steal another’s data
- Stability: A crash in Process A doesn’t corrupt Process B’s memory
- Privacy: User applications cannot access kernel data
- Fairness: Processes cannot interfere with each other
Explicit Shared Memory Allocation:
To share data, processes must explicitly allocate a shared memory segment:
- Create Segment: Call
shmget()to allocate kernel-managed shared memory - Attach Segment: Both processes call
shmat()to map it to their virtual address spaces - Same Physical Location: Now both virtual addresses point to the same physical memory
// Process A and B both execute thisint shmid = shmget(IPC_PRIVATE, 1024, IPC_CREAT | 0666);char *shared_data = (char *)shmat(shmid, NULL, 0);
// Now both processes can read/write the same data*shared_data = 'X'; // Visible to both!Key Point:
Process isolation is enforced by hardware (MMU) and kernel page table management. Sharing requires explicit kernel involvement to coordinate virtual address mappings.
Problem 2: Pipe Mechanisms
Section titled “Problem 2: Pipe Mechanisms”What’s a pipe? What is it used for?
Instructor Solution
A pipe is a unidirectional communication channel between two related processes that acts as a temporary data buffer managed by the operating system kernel.
- Mechanism: It follows a First-In, First-Out (FIFO) structure.
- Buffer: If the pipe fills up, the writing process is blocked; if the pipe is empty, the reading process is blocked until data arrives.
Elaboration (by LLM)
Pipes are one of the oldest and most fundamental IPC mechanisms in Unix systems:
Basic Pipe Structure:
A pipe is essentially a kernel-managed circular buffer:
Writer Process Pipe Buffer Reader Process | (Circular FIFO) | v v[Write data] →→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→ [Read data] fd[1] kernel memory fd[0]The pipe has two file descriptors:
fd[0]: Read end (where data exits)fd[1]: Write end (where data enters)
FIFO Ordering:
Data flows in strict order:
write(fd[1], "Hello", 5);write(fd[1], "World", 5);read(fd[0], buf, 5); // Gets "Hello" (first data in)read(fd[0], buf, 5); // Gets "World" (second data in)Blocking Behavior:
Write Blocking:
// Pipe has limited buffer (typically 64KB on Linux)write(fd[1], large_data, 1MB); // Kernel buffer fills up // Write process blocked// Unblocked when reader drains dataRead Blocking:
read(fd[0], buffer, 1024); // Pipe is empty // Read process blocks// Unblocked when writer sends dataRelated Processes Requirement:
Pipes work only between related processes (parent-child or processes created from same parent):
- Pipes are created before
fork() - Child inherits parent’s file descriptors
- Unrelated processes cannot use pipes directly (use named pipes/FIFOs instead)
Pipe Cleanup:
When a pipe is no longer needed:
- Both ends must be closed
- Unused descriptors must be explicitly closed in parent/child
- If write end closes, reader gets EOF
- If read end closes, writer gets SIGPIPE signal
Pipe vs. Memory Performance:
Shared Memory: Direct memory access (very fast, ~nanoseconds)Pipe: Kernel copy (slower, ~microseconds) - Write side must copy to kernel buffer - Read side must copy from kernel bufferPipes trade performance for automatic synchronization and buffering.
Problem 3: Message Queues vs. Pipes
Section titled “Problem 3: Message Queues vs. Pipes”What’s a message queue? How does it differ from a pipe?
Instructor Solution
A message queue is a linked list of messages stored within the kernel. It allows processes to exchange data in the form of discrete, structured “packets” or messages rather than a continuous byte stream. It differs from a pipe in three key ways:
- Structure: Pipes are byte streams with no internal boundaries; message queues handle formatted blocks of data with specific types or priorities.
- Persistence: Pipes are transient and vanish when processes close them; message queues can be persistent, remaining in the kernel even after the creating process exits.
- Access: Pipes are strictly First-In, First-Out (FIFO); message queues allow processes to retrieve messages out of order based on a message type or priority.
Elaboration (by LLM)
Message queues provide a more sophisticated communication mechanism than pipes:
Structural Differences:
Pipes (Byte Stream):
Writer: "Hello" "World" "Test" ↓ (merged into single stream)Pipe buffer: H e l l o W o r l d T e s t ↓Reader: Must know where boundaries are First read(5) → "Hello" Second read(5) → "World" Third read(4) → "Test"Message Queues (Structured Messages):
Writer: {type: 1, data: "Hello"} {type: 2, data: "World"} {type: 1, data: "Test"} ↓ (stored as discrete messages)Kernel Queue: ┌─────────────────────┐ │ type: 1, data: "Hello" │ ├─────────────────────┤ │ type: 2, data: "World" │ ├─────────────────────┤ │ type: 1, data: "Test" │ └─────────────────────┘ ↓Reader: Can extract by type msgrcv(..., type=1) → "Hello" msgrcv(..., type=1) → "Test"Key Structural Benefit:
Message boundaries are preserved automatically. No need to encode length or delimiters.
Persistence Differences:
Pipes:
int fd[2];pipe(fd);fork();// ... parent and child use the pipe// When both processes exit, pipe is destroyedclose(fd[0]);close(fd[1]);// Pipe is gone from kernelMessage Queues:
int msqid = msgget(IPC_PRIVATE, IPC_CREAT | 0666);fork();// ... parent and child use message queueexit(0); // Parent exits// Message queue remains in kernel!// Another process can still access itmsgrcv(msqid, ...); // Works, data still thereMessage queues persist until explicitly removed (msgctl(..., IPC_RMID)) or system reboot.
Access Pattern Differences:
Pipes (Strict FIFO):
write(fd[1], message1);write(fd[1], message2);read(fd[0], buf); // Always gets message1read(fd[0], buf); // Always gets message2// No way to skip message1 and get message2Message Queues (Priority/Type-Based):
struct Message { long mtype; // Message type char data[256];};
msg1.mtype = 1;msg1.data = "Hello";send(msqid, &msg1);
msg2.mtype = 2;msg2.data = "Urgent";send(msqid, &msg2);
// Retrieve high-priority message firstmsgrcv(msqid, &buf, MAXSIZE, 2); // Gets msg2 (type 2)msgrcv(msqid, &buf, MAXSIZE, 1); // Gets msg1 (type 1)Comparison Table:
| Feature | Pipes | Message Queues |
|---|---|---|
| Data Format | Byte stream | Structured messages |
| Boundaries | Must encode manually | Automatic |
| Access Order | Strict FIFO | By type/priority |
| Persistence | Transient | Persistent |
| Kernel ID | File descriptors | System V IPC ID |
| Related Procs | Required | Any process |
| Unread Data | Lost if reader dies | Preserved in kernel |
When to Use Each:
- Pipes: Simple producer-consumer, shell pipelines, related processes
- Message Queues: Complex multi-message handling, persistent storage, priority-based processing
Problem 4: Connecting Two Child Processes
Section titled “Problem 4: Connecting Two Child Processes”Assume you want to create 2 child processes, C1 and C2. You want the standard output of C1 to go to the standard input of C2. Write a very simple code to create child processes C1, C2 and tie them up using a pipe. C1 will then send the message “Hello\n” to its standard output, which C2 will read from its standard input and print it on the screen. After both children terminate, the parent prints “Parent done…\n” on the screen and terminates.
You may assume the existence of a system call pipe(int fd[2]) that creates a pipe and returns 2 descriptors. fd[0] will be the read end of the pipe, fd[1] will be the write end of the pipe. Also assume the existence of another system call dup2(int fd1, int fd2), which makes fd2 same as fd1, i.e., copies the contents of fd1 over fd2. Use fork() system call to create a child process.
Instructor Solution
#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <sys/wait.h>
int main(int argc, char *argv[]) { int fd[2]; pipe(fd); // From C1 to C2
if (fork() == 0) { // C1 dup2(fd[1], 1); close(fd[0]); close(fd[1]); printf("Hello\n"); exit(0); } // end-if
close(fd[1]); pid_t pid;
if ((pid = fork()) == 0) { // C2 dup2(fd[0], 0); close(fd[0]); char message[6]; int len = read(0, message, 6); // Read hello write(1, message, len); exit(0); } // end-if
close(fd[0]); waitpid(pid, NULL, 0); printf("Parent done…\n"); return 0;} /* end-main */Elaboration (by LLM)
This program demonstrates the power of pipes combined with file descriptor redirection:
Understanding dup2() Function:
dup2(int oldfd, int newfd) makes file descriptor newfd refer to the same open file as oldfd:
Before dup2(fd[1], 1):File Descriptor Table: 0 → stdin 1 → stdout (console) fd[1] → pipe write end
After dup2(fd[1], 1):File Descriptor Table: 0 → stdin 1 → pipe write end (stdout now points to pipe!) fd[1] → pipe write end (same as 1 now)Process Execution Flow:
Parent Creates Infrastructure:
1. pipe(fd) creates pipe fd[0] = read end fd[1] = write end
2. First fork() for C1 Child gets copy of all file descriptors
3. Second fork() for C2 Gets copy of pipe file descriptorsC1 (First Child) Execution:
dup2(fd[1], 1); // Redirect stdout to pipeclose(fd[0]); // Don't need to readclose(fd[1]); // Close duplicate descriptorprintf("Hello\n"); // Goes to pipe, not console!exit(0); // Process endsAfter dup2(), any output to stdout (fd=1) actually goes to the pipe.
C2 (Second Child) Execution:
dup2(fd[0], 0); // Redirect stdin to pipeclose(fd[0]); // Close original descriptorchar message[6];int len = read(0, message, 6); // Read from pipe (fd=0)write(1, message, len); // Write to stdoutexit(0);Now stdin (fd=0) comes from the pipe, so read(0, ...) reads from C1’s output.
Parent Cleanup:
close(fd[1]); // Parent won't writeclose(fd[0]); // Parent won't readwaitpid(pid, NULL, 0); // Wait for C2 to finishprintf("Parent done…\n");Why Close Unused Descriptors?
This is critical:
Pipe Lifecycle: - If C1 closes fd[1] (write end), C2 knows when C1 is done - If C2 closes fd[0] (read end), C1 gets SIGPIPE if writing - If parent doesn't close fd[1], C2 never sees EOF (waits forever!)Data Flow Timeline:
Time 0: Parent creates pipe, forks C1 and C2
Time 1: C1 redirects stdout → pipe C1: printf("Hello\n") → data written to pipe buffer
Time 2: C2 redirects stdin ← pipe C2: read(0, message, 6) → blocks until data arrives → data is available from C1 → reads "Hello\n"
Time 3: C2 writes to stdout Console shows: "Hello"
Time 4: C1 exits, closes fd[1] C2 sees EOF, exits
Time 5: Parent's waitpid() returns Parent: printf("Parent done…\n")Output Guarantee:
HelloParent done…The “Hello” will always appear before “Parent done” because of the waitpid() synchronization point.
Key Lessons:
- dup2() redirects I/O: Elegant way to redirect stdin/stdout
- Close unused descriptors: Essential for proper pipe EOF handling
- Process coordination: Parent ensures C2 finishes before continuing
- FIFO behavior: Data flows in order through the pipe
Problem 5: Sending Data Between Processes
Section titled “Problem 5: Sending Data Between Processes”Implement a program that creates a child process and sends the message “Hello\n” to the child process over a pipe. The child must read the message coming from the pipe, print it out and then terminate. Make sure parent P waits for the child to terminate before printing “Parent done…\n” and terminating itself.
You may assume the existence of a system call pipe(int fd[2]) that creates a pipe and returns 2 descriptors. fd[0] will be the read end of the pipe, fd[1] will be the write end of the pipe. You can use waitpid(int pid) system call to wait for the termination of a process with “pid”. Use fork() system call to create a child process.
Instructor Solution
#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <sys/wait.h>
int main(int argc, char *argv[]) { int fd[2]; pipe(fd); // From Parent to C1 write(fd[1], "Hello\n", 6); close(fd[1]);
pid_t pid; if ((pid = fork()) == 0) { // C1 char message[6]; int len = read(fd[0], message, 6); // Read hello write(1, message, len); // Send hello to the screen exit(0); } // end-if
close(fd[0]); waitpid(pid, NULL, 0); printf("Parent done…\n"); return 0;} /* end-main */Elaboration (by LLM)
This program demonstrates a key communication pattern: parent writes first, then child reads.
Execution Order Differences:
Unlike Problem 4, the parent writes before creating the child:
pipe(fd); // Create pipewrite(fd[1], "Hello\n", 6); // Parent writes immediatelyclose(fd[1]); // Parent closes write endfork(); // Now create childWhy Write Before Fork?
The data is already in the kernel’s pipe buffer:
After write():Pipe buffer: [H][e][l][l][o][\n] ^data is here
After fork():Child inherits fd[0] (read end)Child can immediately read buffered dataTiming Guarantee:
Child doesn't need to wait for parent to write(data is already there)
vs. Problem 4:C2 must wait for C1's printf() to complete(data arrives asynchronously)Execution Timeline:
Time 0: Parent creates pipe
Time 1: Parent writes "Hello\n" to pipe buffer Pipe buffer now contains: "Hello\n"
Time 2: Parent closes pipe write end (fd[1]) This signals "no more data coming"
Time 3: Parent creates child via fork() Child inherits fd[0] (pipe read end) Child immediately has access to buffered data
Time 4: Parent closes fd[0] (local copy) Parent doesn't need to read
Time 5: Child reads 6 bytes from pipe read(fd[0], message, 6) → gets "Hello\n" Data was already buffered, no blocking
Time 6: Child writes to stdout Console: "Hello"
Time 7: Child exits
Time 8: Parent's waitpid() returns Parent: printf("Parent done…\n")Key Differences from Problem 4:
| Aspect | Problem 4 (Pipe Between Siblings) | Problem 5 (Parent→Child) |
|---|---|---|
| Who writes? | C1 (child process) | Parent (before fork) |
| Who reads? | C2 (child process) | C1 (child process) |
| Data buffering | Real-time (synchronous) | Buffered (parent waits?) |
| Timing guarantee | Child may block waiting for data | Data always available |
| Pipe buffer use | Continuous flow | Accumulated then read |
Why Close Write End Before Fork?
This is essential:
close(fd[1]); // MUST close before forkfork();Reason:
- If parent doesn’t close
fd[1], child inherits it - Child’s
read()would block waiting for EOF - EOF only arrives when ALL write ends are closed
- But parent still has
fd[1]open! - Child would wait forever
With proper close:
After parent closes fd[1]:- Only open write end references: ZERO- Child's read() sees EOF immediately when pipe empties- Child can complete the read() callMemory Diagram:
Parent: Pipe creation: fd[2] = {read_end, write_end}
After fork(): ┌─ Parent ────────────────┐ │ fd[0] → (closed) │ │ fd[1] → (closed) │ │ Other descriptors: 0,1,2│ └─────────────────────────┘
┌─ Child ─────────────────┐ │ fd[0] → read pipe │ │ fd[1] → (closed by exec)│ │ Other descriptors: 0,1,2│ └─────────────────────────┘Output:
HelloParent done…The order is guaranteed because parent waits for child to finish via waitpid().
Problem 6: Multi-Stage Process Communication
Section titled “Problem 6: Multi-Stage Process Communication”Consider implementing a program P that creates two child processes, C1 and C2 with the following constraints: P creates 2 children and sends “Hello\n” to C1, which receives this message and sends it over to C2, which simply prints it on the screen. To achieve this, you must create two pipes, one between P and C1 and another between C1 and C2.
You may assume the existence of a system call pipe(int fd[2]) that creates a pipe and returns 2 descriptors. fd[0] will be the read end of the pipe, fd[1] will be the write end of the pipe. Make sure parent P waits for all children to terminate before printing “Parent done…\n” and terminating itself. You can use waitpid(int pid) system call to wait for the termination of a process with “pid”. Use fork() system call to create a child process.
Instructor Solution
#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <sys/wait.h>
int main(int argc, char *argv[]) { int fd1[2]; pipe(fd1); // From Parent to C1 write(fd1[1], "Hello\n", 6); close(fd1[1]);
int fd2[2]; pipe(fd2); // From C1 to C2
if (fork() == 0) { // C1 close(fd2[0]); char message[6]; int len = read(fd1[0], message, 6); // Read hello write(fd2[1], message, len); // Send hello to C2 exit(0); } // end-if
close(fd1[0]); close(fd2[1]);
pid_t pid; if ((pid = fork()) == 0) { // C2 char message[6]; int len = read(fd2[0], message, 6); // Read hello write(1, message, len); exit(0); } // end-if
close(fd2[0]); waitpid(pid, NULL, 0); printf("Parent done…\n"); return 0;} /* end-main */Elaboration (by LLM)
This is a multi-stage pipeline where data flows through two pipes: Parent → C1 → C2 → stdout.
Pipeline Architecture:
Parent C1 C2 Console | | | | |-- Write "Hello"----|→ (fd1 pipe) →| | | | | |→ Read | | | | | |→ Process | | | | | |→ Write ---|→ (fd2 pipe) →| | | | | | |→ Read | | | | | | |→ Write --|→ Hello | |--- Wait C2 done --|---|-----------|---|---------| | | | | | | | | |-- Done message | | | | | | | | | | | | |Resource Management: Critical Detail
Proper file descriptor cleanup is essential:
int fd1[2], fd2[2];pipe(fd1); // Create parent→C1 pipepipe(fd2); // Create C1→C2 pipe
// After first fork() to create C1:close(fd1[0]); // Parent won't read from fd1close(fd2[1]); // Parent won't write to fd2
// C1 closes:close(fd2[0]); // C1 won't read from fd2 (only writes)
// After second fork() to create C2:close(fd2[0]); // Parent won't read fd2Why These Closes Matter:
If parent keeps fd2[1] open:- C2's read(fd2[0], ...) blocks waiting for EOF- EOF won't arrive until parent also closes fd2[1]- But parent won't close it (it only waits)- DEADLOCK!
Proper closing ensures EOF signals propagate correctlyExecution Flow:
Time 0: Parent creates fd1, writes "Hello\n", closes fd1[1] Parent creates fd2
Time 1: Parent forks C1 C1 inherits: fd1[0] (read from parent) fd2[1] (write to C2) Parent closes fd1[0] (won't read) Parent closes fd2[1] (won't write)
Time 2: C1 reads from fd1[0] → Gets "Hello\n" (buffered from parent)
Time 3: C1 writes to fd2[1] → Data goes to C2 via pipe
Time 4: C1 exits Closes fd1[0], fd2[1]
Time 5: Parent forks C2 C2 inherits fd2[0] (read from C1) Parent closes fd2[0] (won't read)
Time 6: C2 reads from fd2[0] → Gets "Hello\n" (written by C1)
Time 7: C2 writes to stdout (fd=1) Console shows: "Hello"
Time 8: C2 exits
Time 9: Parent's waitpid() returns Parent prints "Parent done…\n"Data Flow Sequencing:
┌─ Parent ──────────────────────┐│ Writes "Hello" to fd1[1] ││ Closes fd1[1] ││ (now EOF will be signaled) │└───────────────────┬────────────┘ │ ↓ (fd1 pipe) ┌─ C1 ──────────────────┐ │ Reads from fd1[0] │ │ (gets "Hello") │ │ Writes to fd2[1] │ │ (sends "Hello") │ └────────┬──────────────┘ │ ↓ (fd2 pipe) ┌─ C2 ──────────────────┐ │ Reads from fd2[0] │ │ (gets "Hello") │ │ Writes to stdout │ │ Displays "Hello" │ └────────────────────────┘File Descriptor Table Evolution:
Initially (Parent): fd1[0], fd1[1], fd2[0], fd2[1], + standard 0,1,2
After Parent closes: Only: fd1[1] (write), fd2[0] (read) inherited by children Actually: Parent only waits, C1 has fd1[0],fd2[1] C2 has fd2[0]
After C1 closes fd2[0]: C1 can't read fd2 (which is fine, C1 only writes) Ensures C1 doesn't accidentally block on reading
After C2 starts: C2 has fd2[0] (read) Parent has nothing (closed all pipe ends)Synchronization Points:
1. Parent writes to fd1, closes write end → C1 can read immediately (data buffered)
2. C1 reads, writes to fd2 → C2 can read C1's output
3. Parent waits for C2 (waitpid) → Ensures C2 finishes before "Parent done" prints → Guarantees output orderOutput Order Guarantee:
HelloParent done…The “Hello” always comes first because:
- C2 must complete before waitpid() returns
- Only after waitpid() does parent print “Parent done”
Key Learning:
Multi-stage pipelines require careful descriptor management. Each process must:
- Close descriptors it won’t use
- Ensure proper EOF signaling
- Coordinate through parent’s synchronization
Problem 7: Executing Multiple Commands in Sequence
Section titled “Problem 7: Executing Multiple Commands in Sequence”Write a simple code that forks two child processes “/bin/ls” and “/bin/wc” and connects the standard output of “/bin/ls” to the standard input of “/bin/wc” using a pipe. Your program then waits until both processes terminate and finally prints “My program exiting…” on the screen.
You may assume the existence of a system call pipe(int fd[2]) that creates a pipe and returns 2 descriptors. fd[0] will be the read end of the pipe, fd[1] will be the write end of the pipe. Also assume the existence of another system call dup2(int fd1, int fd2), which makes fd2 same as fd1, i.e., copies the contents of fd1 over fd2. You can use waitpid(int pid) system call to wait for the termination of a process with “pid”. Use fork() system call to create a child process.
Instructor Solution
#include <unistd.h>#include <stdio.h>#include <sys/wait.h>
/* /bin/ls | /bin/wc */int main(int argc, char *argv[]) { int fd[2]; pipe(fd);
if (fork() == 0) { // fork a child and load /bin/ls dup2(fd[1], 1); close(fd[0]); close(fd[1]); execl("/bin/ls", "ls", NULL); }
close(fd[1]); pid_t pid;
if ((pid = fork()) == 0) { dup2(fd[0], 0); close(fd[0]); execl("/bin/wc", "wc", NULL); } // end-if
close(fd[0]); waitpid(pid, NULL, 0); printf("My program exiting...\n"); return 0;} /* end-main */Elaboration (by LLM)
This program demonstrates the classic shell pipeline implementation: ls | wc.
How Unix Shells Implement Pipelines:
When you type ls | wc, the shell actually:
$ ls | wc
# Shell internally does (roughly):if fork() == 0: # Child 1: ls dup2(pipe_write_end, stdout) exec("/bin/ls")else: # Parent: wc if fork() == 0: dup2(pipe_read_end, stdin) exec("/bin/wc")This C program mirrors that behavior exactly.
Program Architecture:
Parent (Shell Simulator) | |→ fork() → Child 1: /bin/ls (stdout redirected to pipe) |→ fork() → Child 2: /bin/wc (stdin redirected from pipe) |→ wait for Child 2 |→ exitStep-by-Step Execution:
Step 1: Create Pipe
int fd[2];pipe(fd);// fd[0] = read end// fd[1] = write endStep 2: Fork Child 1 (ls)
if (fork() == 0) { // In child dup2(fd[1], 1); // stdout → pipe write end close(fd[0]); // Don't need to read from pipe close(fd[1]); // Close original descriptor execl("/bin/ls", "ls", NULL); // Replace with /bin/ls}After dup2() and execl():
/bin/lsruns- Its stdout (fd=1) points to the pipe
- Directory listing goes into pipe buffer
Step 3: Parent Closes Write End
close(fd[1]); // Parent won't writeCritical: If parent doesn’t close, Child 2’s read will block waiting for EOF.
Step 4: Fork Child 2 (wc)
if ((pid = fork()) == 0) { dup2(fd[0], 0); // stdin ← pipe read end close(fd[0]); // Close original descriptor execl("/bin/wc", "wc", NULL); // Replace with /bin/wc}After dup2() and execl():
/bin/wcruns- Its stdin (fd=0) points to the pipe
- Reads directory listing from Child 1
- Counts lines, words, characters
- Outputs result to stdout (console)
Step 5: Parent Waits and Exits
close(fd[0]); // Parent also won't readwaitpid(pid, NULL, 0); // Wait for wc to finishprintf("My program exiting...\n");Data Flow Timeline:
Time 0: Parent creates pipe Parent forks Child 1 (ls)
Time 1: Child 1 redirects stdout to pipe Parent closes write end of pipe Parent forks Child 2 (wc)
Time 2: Child 1 executes /bin/ls /bin/ls queries directory /bin/ls writes to stdout (actually pipe) Directory entries flow into pipe buffer
Time 3: Child 2 executes /bin/wc /bin/wc reads from stdin (actually pipe) /bin/wc counts lines, words, chars /bin/wc writes results to stdout (console)
Time 4: Both children complete Parent's waitpid() returns Parent prints "My program exiting..."Sample Output:
$ ./pipeline 12 24 256My program exiting...The first line is from wc counting ls output.
The second line is from the parent process.
Key Differences from Manual Data Passing:
| Aspect | Problem 6 (Manual Messages) | Problem 7 (Real Commands) |
|---|---|---|
| Data Format | Fixed message sizes | Stream of data |
| Processing | Custom C code | Existing Unix utilities |
| Flexibility | Limited to program logic | Can chain any commands |
| Real-World | Uncommon in practice | Used constantly in shell |
Buffering Behavior:
The pipe buffers data between ls and wc:
/bin/ls writes: /bin/wc reads:file1\n ────→ │ F │file2\n ────→ │ I │ ──→ Countsfile3\n ────→ │ L │ │ E │ │ S │ └───┘Pipe acts as a FIFO buffer, allowing ls and wc to run at different speeds without coordination.
Real-World Usage:
This is how shell commands work:
$ cat huge_file.txt | grep pattern | sort | uniq | wc
# Internally:fork() → /bin/cat pipe() → dup2 to /bin/grep pipe() → dup2 to /bin/sort pipe() → dup2 to /bin/uniq pipe() → dup2 to /bin/wc wc outputs to consoleEach program is independent, but they’re seamlessly connected via pipes.
Critical Implementation Details:
- dup2() before execl(): Must redirect before execution
- Close unused ends: Essential for EOF signaling
- Parent’s role: Create infrastructure, then get out of the way
- Synchronization: Only parent needs to know when wc finishes
Problem 8: File Input to Command Pipeline
Section titled “Problem 8: File Input to Command Pipeline”Assume you are asked to implement a program to count the number of characters, words and lines in a text file named “file.txt”. You know that there is a Unix system utility program called wordcount located at “/bin/wc” and you would like to make use of its services as follows: You want to create a child process that will execute “/bin/wc” and have the child receive the input data from the parent over a pipe. The parent process (your program) will read in the file “file.txt” one character at a time and send the character to “/bin/wc” over the pipe.
Implement the above-described program. Recall that by default “/bin/wc” reads its data from the standard input. So after your program creates a pipe and a child process, it must manipulate the child’s standard input descriptor to point to the read-end of the pipe using dup2(int fd1, int fd2) system call, which makes fd2 same as fd1, i.e., copies the contents of fd1 over fd2. Recall that you create a new child using fork(), load a new executable using execv(char *execFilename, char *args[]), and create a pipe using pipe(int fd[2]), which creates a pipe where fd[0] will be the read end of the pipe, fd[1] will be the write end of the pipe.
Instructor Solution
#include <unistd.h>#include <stdio.h>#include <fcntl.h> // For open()
int main(int argc, char *argv[]) { int fd[2]; pipe(fd);
if (fork() == 0) { dup2(fd[0], 0); close(fd[0]); close(fd[1]); // Child. Execl "/bin/wc < pipe" execl("/bin/wc", "wc", NULL); } // end-if
close(fd[0]); int fd2 = open("file.txt", O_RDONLY); char ch;
while (read(fd2, &ch, 1) > 0) write(fd[1], &ch, 1);
close(fd2); close(fd[1]); return 0;} /* end-main */Elaboration (by LLM)
This program demonstrates feeding file contents to a command via a pipe, implementing wc < file.txt without shell redirection.
Program Architecture:
file.txt ↓ (read by parent) ↓Parent process ├─ Reads file.txt byte-by-byte └─ Writes each byte to pipe ↓ ↓ (pipe) ↓ /bin/wc ↓ stdin ↓ Count lines, words, chars ↓ stdout (console)Key Difference from Problem 7:
| Aspect | Problem 7 (ls | wc) | Problem 8 (file → wc) |
|---|---|---|
| Source | Another command (ls) | A file (file.txt) |
| Parent | Creates infrastructure, waits | Actively feeds data |
| I/O Method | Pipe only | File + Pipe |
| Command | Implicit (ls is source) | Explicit (wc is sink) |
Execution Flow:
Setup Phase:
int fd[2];pipe(fd); // Create pipe: fd[0]=read, fd[1]=write
if (fork() == 0) { // Child process: /bin/wc dup2(fd[0], 0); // stdin ← pipe read end close(fd[0]); // Close duplicate close(fd[1]); // Close write end (child won't write) execl("/bin/wc", "wc", NULL); // At this point, /bin/wc replaces the child process}Parent Phase (Data Feeding):
close(fd[0]); // Parent won't read from pipeint fd2 = open("file.txt", O_RDONLY); // Open input filechar ch;
while (read(fd2, &ch, 1) > 0) // Read one byte at a time write(fd[1], &ch, 1); // Write to pipe
close(fd2);close(fd[1]); // Close write end → signals EOF to wcWhy Read Byte-by-Byte?
While inefficient, reading one byte at a time:
- Simplicity: Demonstrates the concept clearly
- Works: Pipe buffers data, so performance difference is minimal
- Educational: Shows that pipes work with any granularity
More Efficient Version:
char buffer[4096];int bytes;while ((bytes = read(fd2, buffer, sizeof(buffer))) > 0) write(fd[1], buffer, bytes);But the solution provided is correct and pedagogically valuable.
Data Flow:
file.txt contents: "line1\nline2\nline3\n"
Parent reads: 'l' → writes to pipe 'i' → writes to pipe 'n' → writes to pipe 'e' → writes to pipe '1' → writes to pipe '\n' → writes to pipe ... (continues)
Pipe buffer accumulates data: ┌──────────────────────┐ │ line1 │ │ line2 │ ← /bin/wc reads from here │ line3 │ └──────────────────────┘
/bin/wc reads entire buffer, counts: 3 lines 3 words 18 characters
Output: 3 3 18Timing & Buffering:
Without the pipe, the sequence would be:
Parent: write 1 byte → wc can't process partial dataParent: write 1 byte → wc still waiting...Parent: close write end → wc sees EOF, processes bufferPipe allows wc to start processing once enough data arrives.
File Descriptor States:
Initial (Parent perspective):
fd 0: stdinfd 1: stdoutfd 2: stderrfd[0]: pipe readfd[1]: pipe writefd2: file.txt readAfter fork():
Parent: Child (becomes /bin/wc):fd 0: stdin fd 0: pipe read (redirected)fd 1: stdout fd 1: stdoutfd 2: stderr fd 2: stderrfd[0]: closed fd[0]: closed (already dup2'd)fd[1]: write fd[1]: closedfd2: file read (fd2 not inherited in exec)Parent Data Transfer Loop:
while (read(fd2, &ch, 1) > 0) // Returns 0 at EOF write(fd[1], &ch, 1);
// After loop exits (file EOF reached):close(fd2);close(fd[1]); // CRITICAL: signals EOF to wcWhy closing fd[1] is critical:
- When parent closes write end
- /bin/wc’s read() sees EOF
- wc knows no more data is coming
- wc proceeds to output results
Without the close:
/bin/wc reads from stdinStdin is redirected to pipe fd[0]Parent has fd[1] still openEven though parent closed the loopwc doesn't see EOF because fd[1] is still open somewherewc blocks forever waiting for more dataFull Program Flow:
Time 0: Main creates pipe Main forks child Child redirects stdin ← pipe Child executes /bin/wc
Time 1: Main closes pipe read end Main opens file.txt
Time 2: Main reads first byte from file Main writes byte to pipe /bin/wc's stdin receives data
Time 3: Main continues reading file Multiple bytes accumulate in pipe buffer /bin/wc can now start reading and processing
Time 4: Main finishes reading file Main closes file descriptor Main closes pipe write end → SIGNALS EOF
Time 5: /bin/wc's read() returns 0 (EOF) /bin/wc completes its count /bin/wc outputs: "3 3 18\n" /bin/wc exits
Time 6: Main returns 0 Program endsExpected Output:
Assuming file.txt contains 3 lines:
3 3 18Three lines, three words, eighteen characters total.
Key Learning Points:
- Parent as Data Source: Parent can feed any data to child via pipe
- File to Command: Demonstrates how
<redirection works internally - Byte-by-Byte Transfer: Inefficient but correct and educational
- EOF Signaling: Closing write end is essential for child to know when data ends
- Pipe Buffering: Allows asynchronous communication between parent and child
Real-World Equivalent:
$ wc < file.txt
# This C program implements this command lineProblem 9: File-Based Counter with Reformatted Output
Section titled “Problem 9: File-Based Counter with Reformatted Output”Assume that you are asked to implement a program (Counter.c) that takes 1 argument, a filename, and counts the number of characters, words and lines in the file and prints them on the screen on separate lines. A sample output of your program is given below:
bash% Counter a.txtCharacters: 10234Words: 1353Lines: 286bash%Instead of implementing the program yourself, you decide to make use of the existing system utility /bin/wc, which counts the number of characters, words and lines fed in at its standard input and prints them out on the screen all on the same line. A sample output of /bin/wc on the same input file a.txt is given below (notice the input redirection):
bash% /bin/wc < file.txt10234 1353 286bash%Implement this program as follows: Your program must create a child process to run “/bin/wc”. You must manipulate the child process’s file descriptors so that “/bin/wc” reads its input from the file passed to your program as the first argument at the command line. Your program (the parent) and the child “/bin/wc” will communicate using a pipe. That is, the child process will send out its result over the pipe to your program. Your program will then read the message from the pipe and print out the result on the screen as required.
Recall that you create a new child using fork(), start a new executable using execv(char *execFilename, char *args[]), create a pipe using pipe(int fd[2]) and copy the contents of fd1 to the contents of fd2 using dup2(int fd1, int fd2) system calls.
Instructor Solution
#include <unistd.h>#include <stdio.h>#include <fcntl.h> // For open()
int main(int argc, char *argv[]) { int fd[2]; pipe(fd);
if (fork() == 0) { dup2(fd[1], 1); close(fd[0]); close(fd[1]); int fd = open("file.txt", O_RDONLY); dup2(fd, 0); close(fd); // Child. Execl "/bin/wc < file.txt > pipe" execl("/bin/wc", "wc", NULL); } // end-if
close(fd[1]); dup2(fd[0], 0); int numLines, numWords, numChars; scanf("%d %d %d", &numLines, &numWords, &numChars); printf("Characters: %d\n", numChars); printf("Words: %d\n", numWords); printf("Lines: %d\n", numLines); return 0;} /* end-main */Elaboration (by LLM)
This is the most complex IPC pattern yet: bidirectional communication where the parent both orchestrates the child and parses its output.
Comparison with Previous Problems:
| Problem | Source | Sink | Parent Role |
|---|---|---|---|
| 7 | ls | wc | Passive observer |
| 8 | file | wc | Data feeder |
| 9 | file | wc | Data feeder + parser |
Program Architecture:
Parent Process │ ├─ Create pipe │ ├─ Fork child │ │ │ └─ Child: │ ├─ Redirect stdin ← file.txt │ ├─ Redirect stdout → pipe │ └─ Execute /bin/wc │ └─ wc outputs: "<lines> <words> <chars>\n" │ ├─ Close write end │ ├─ Redirect parent's stdin ← pipe │ ├─ Parse output with scanf() │ └─ Print formatted outputKey Insight: Two Redirections in Child
The child performs TWO dup2() operations:
dup2(fd[1], 1); // stdout → pipe (output redirect)int fd = open("file.txt", O_RDONLY);dup2(fd, 0); // stdin ← file (input redirect)This effectively executes: /bin/wc < file.txt > pipe
Execution Steps:
Step 1: Child Setup
if (fork() == 0) { // Child process dup2(fd[1], 1); // Connect stdout to pipe write end close(fd[0]); // Won't read from pipe close(fd[1]); // Close duplicate
int fd = open("file.txt", O_RDONLY); dup2(fd, 0); // Connect stdin to file close(fd); // Close duplicate
execl("/bin/wc", "wc", NULL);}File Descriptor Mapping (Child at exec time):
Before exec: After exec (inside /bin/wc):fd 0: file fd 0: file (stdin for wc)fd 1: pipe fd 1: pipe (stdout for wc)fd 2: stderr fd 2: stderrStep 2: Parent Waits for Data
close(fd[1]); // Parent won't writedup2(fd[0], 0); // Parent's stdin ← pipeint numLines, numWords, numChars;scanf("%d %d %d", &numLines, &numWords, &numChars);The parent redirects its own stdin to read from the pipe!
Why Redirect Parent’s stdin?
Alternatively:
// Without dup2:char buffer[256];read(fd[0], buffer, sizeof(buffer));sscanf(buffer, "%d %d %d", &numLines, &numWords, &numChars);
// With dup2:dup2(fd[0], 0);scanf("%d %d %d", &numLines, &numWords, &numChars);Both work, but dup2() is cleaner and matches the elegance of Unix redirection.
Data Flow:
file.txt: line1 line2 line3
↓ (child reads via stdin)
/bin/wc processes: Counts: 3 lines, 3 words, 18 chars Outputs: " 3 3 18\n"
↓ (written to pipe via stdout)
Pipe buffer: " 3 3 18\n"
↓ (parent reads via stdin via dup2)
Parent scanf(): Parses: numLines=3, numWords=3, numChars=18
↓ (reformats)
Parent printf(): Characters: 18 Words: 3 Lines: 3Data Format Transformation:
From wc (standard format): 3 3 18 ^lines ^words ^chars (order: lines, words, chars)
To our format: Characters: 18 Words: 3 Lines: 3 (order: chars, words, lines + labels)Timing & Synchronization:
Time 0: Parent creates pipe, forks child
Time 1: Child redirects stdin ← file.txt Child redirects stdout → pipe Child executes /bin/wc
Time 2: /bin/wc reads from file /bin/wc counts contents /bin/wc outputs to pipe
Time 3: Parent closes pipe write end Parent redirects stdin ← pipe Parent calls scanf() scanf() blocks until data available
Time 4: /bin/wc finishes, outputs counts Pipe receives: " 3 3 18\n"
Time 5: scanf() reads from stdin (pipe) Parses three integers scanf() returns
Time 6: Parent printf() outputs formatted results Console shows: Characters: 18 Words: 3 Lines: 3
Time 7: Parent returns 0 Program endsExample Run:
Assuming file.txt contains:
hello worldthis is testfinal lineActual wc output:
$ wc < file.txt 3 7 40Program output:
Characters: 40Words: 7Lines: 3Why This Pattern Matters:
This demonstrates:
- Bidirectional Communication: Parent both launches child and reads its output
- Data Parsing: Converting raw command output to structured data
- Formatting: Transforming one output format to another
- File Descriptors: Multiple redirections in a single process
Real-World Use Cases:
# Parent might count files and display nicely:$ find . -type f | wc -l
# Or parse system info:$ df | awk ...
# Or monitor logs:$ tail -f logfile | grep ERROR | wc -lKey Differences from Simple Redirection:
| Aspect | Shell | | This Program |
|---|---|---|
| Who parses | Shell (simple) | Application (complex) |
| Output use | Human readable | Programmatic |
| Data flow | Linear | Parent ← Child ← File |
| Complexity | High | Moderate |
Important Details:
- dup2(fd[0], 0): Parent now reads from pipe via stdin
- scanf(): Parses integers from parent’s stdin (the pipe)
- printf(): Parent outputs formatted results
- No waitpid(): Not needed; scanf() implicitly waits for child data
Actually, for robustness, a waitpid() at the end would be good practice:
pid_t pid = fork(); // Should save thisif (pid == 0) { ... }// Later:waitpid(pid, NULL, 0); // Ensure child is reapedProblem 10: Pipe and Shared Memory Output Analysis
Section titled “Problem 10: Pipe and Shared Memory Output Analysis”What would be the output of the following code segments (a) and (b) when run in a Linux system. Explain briefly.
Code Segment (a):
main() { pid_t pid; int fds[2]; int l = 6; write(1, "Main entered\n"); pipe(fds);
if ((pid = fork()) == 0) { int v = 0; read(fds[0], &v, sizeof(int)); printf("v: <%d>\n", v); } else { write(fds[1], &l, sizeof(int)); waitpid(pid); printf("Main done\n"); } // end-else
} // end-mainInstructor Solution
Expected Output:
Main enteredv: <6>Main doneElaboration (by LLM)
This demonstrates inter-process communication via pipes, showing how a parent writes data to a child process.
Pipe Creation and Fork Timing:
The pipe is created before the fork, so both parent and child inherit file descriptors to the same pipe:
fds[0]: read end of pipefds[1]: write end of pipe
Both processes start with open references to both ends.
Process Communication Flow:
- Parent writes integer 6 to pipe via
fds[1] - Child waits (blocks) on
read()fromfds[0]until data arrives - When parent writes, child’s
read()unblocks with the value 6 - Parent calls
waitpid()to ensure child completes before printing “Main done”
Execution Timeline:
Main process: Child process: write(1, ...) pipe(fds) fork() | int v = 0 | read(fds[0], ...) → BLOCKS write(fds[1], ...) | ← unblocks, v = 6 | printf("v: <6>") waitpid(pid) exits | printf("Main done")Why This Works:
- The pipe persists across the
fork(), maintaining the connection read()blocks the child until data is available- The parent writes the integer (4 bytes) to the pipe
- Child receives the exact same 4 bytes in the same order
- Parent synchronizes with
waitpid()to maintain correct output order
Common Mistakes:
- Forgetting to close unused file descriptors (both parent and child have both ends open)
- Not calling
waitpid()(could print “Main done” before child finishes) - Assuming pipe can communicate without blocking (pipes are synchronous)
Code Segment (b):
main() { char *pSm = NULL; int *pi; char *pStr; int *pg;
// Allocate 1024 bytes shared memory pSm = GetSharedMemory(1024); memset(pSm, 0, 1024); pi = (int *)pSm; pg = pi + 1; pStr = (char *)(pg + 1);
(*pg)++;
if (fork() == 0) { while (*pi == 0); printf("Str: <%s>, g: %d\n", pStr, *pg); (*pg)++; strcpy(pStr, "AA"); *pi = 0; } else { strcpy(pStr, "Message"); (*pg)++; *pi = 1; while (*pi == 1); printf("Str: <%s>, g: %d\n", pStr, *pg); } // end-else} // end-mainInstructor Solution
Expected Output:
Str: <Message>, g: 2Str: <AA>, g: 3Elaboration (by LLM)
This demonstrates synchronization and coordination between parent and child processes using shared memory and busy-waiting (spinlock).
Shared Memory Layout:
pSm (base): [ pi (int) ][ pg (int) ][ pStr (string) ]...Offset: 0 8 16
Variables in shared memory: pi → pointer to integer at offset 0 (synchronization flag) pg → pointer to integer at offset 8 (counter, initially 1) pStr → pointer to string at offset 16 (message buffer)Pointer Arithmetic:
pi = (int *)pSm; // Points to offset 0pg = pi + 1; // Points to offset 8 (pi + 4 bytes = +8)pStr = (char *)(pg + 1); // Points to offset 16 (pg + 4 bytes = +12, then cast)Execution Sequence:
-
Parent & Child both increment g:
(*pg)++→ g = 2- Happens before fork (parent executes)
- After fork, child sees g = 1 initially, but parent already incremented
-
Parent:
- Writes “Message” to shared string
- Increments g → g = 2
- Sets pi = 1 (signal to child: ready)
- Busy-waits until child sets pi = 0
- Prints: “Str:
, g: 2”
-
Child:
- Busy-waits until pi == 1 (parent’s signal)
- Prints: “Str:
, g: 2” (sees parent’s message) - Increments g → g = 3
- Overwrites string with “AA”
- Sets pi = 0 (signal to parent: done)
-
Parent resumes: Wakes from busy-wait
- Prints: “Str:
, g: 3” (sees child’s modifications)
- Prints: “Str:
Critical Synchronization Points:
// Child waits for parentwhile (*pi == 0); // Busy-wait for pi = 1
// Parent waits for childwhile (*pi == 1); // Busy-wait for pi = 0These spinlocks ensure sequential execution: child doesn’t read until parent writes, and parent doesn’t proceed until child finishes.
Why Shared Memory Instead of Pipes?
- Allows bidirectional communication without separate pipe structures
- Permits sharing complex data structures (strings, arrays, records)
- Synchronization is explicit (via flag variables), not implicit
- More flexible than pipes for general-purpose IPC
Potential Issues:
- Busy-waiting wastes CPU: Could use semaphores or condition variables instead
- Race condition on g: Both processes modify g, but operations are atomic (integer increment)
- Non-portable:
GetSharedMemory()is not standard; typically usesshmget(),shmat(), or memory-mapped files
Problem 11: File Redirection with fork and execv
Section titled “Problem 11: File Redirection with fork and execv”Implement a program that forks a child which will execute “/bin/wc” as follows:
bash% /bin/wc < file.txt > out.txtAs you can see, “/bin/wc” takes its standard input from file.txt & sends its standard output to out.txt. To implement this program, you can use the following system calls: fork() creates a child process, execv(char *execFilename, char *args[]) loads an executable on top of an existing process, dup2(int fd1, int fd2) copies the contents of fd1 to the contents of fd2, open(char *filename, int mode) opens a file, where mode can be O_RDONLY, O_WRONLY. Your program must wait until “/bin/wc” terminates and then prints the message “DONE\n” before terminating. You can use waitpid(int pid) system call to wait for the termination of a process with “pid”.
Instructor Solution
#include <unistd.h>#include <stdio.h>#include <fcntl.h> // For open()#include <sys/wait.h>
/* /usr/bin/wc < file.txt > out.txt */int main(int argc, char *argv[]) { pid_t pid;
if ((pid = fork()) == 0) { int fd = open("file.txt", O_RDONLY); dup2(fd, 0); close(fd); fd = open("out.txt", O_WRONLY); dup2(fd, 1); close(fd); // /bin/wc < file.txt > out.txt execl("/bin/wc", "wc", NULL); } // end-if
waitpid(pid, NULL, 0); printf("DONE\n"); return 0;} /* end-main */Elaboration (by LLM)
This program demonstrates dual input/output redirection: simultaneously redirecting stdin from a file and stdout to a file.
Implementing Shell Redirection:
When you type wc < file.txt > out.txt, the shell:
- Creates a new process for
wc - Redirects stdin (fd=0) to read from file.txt
- Redirects stdout (fd=1) to write to out.txt
- Executes
/bin/wc
This C program directly implements that behavior.
File Descriptor Redirection Sequence:
int fd = open("file.txt", O_RDONLY);dup2(fd, 0); // stdin ← file.txtclose(fd);
fd = open("out.txt", O_WRONLY);dup2(fd, 1); // stdout → out.txtclose(fd);
execl("/bin/wc", "wc", NULL);File Descriptor State at Each Step:
Before open("file.txt"): fd 0: stdin (console) fd 1: stdout (console) fd 2: stderr (console)
After open("file.txt") [fd=3]: fd 0: stdin fd 1: stdout fd 2: stderr fd 3: file.txt (read)
After dup2(fd, 0) [fd=3 → 0]: fd 0: file.txt (now stdin reads from file) fd 1: stdout fd 2: stderr fd 3: file.txt (duplicate reference)
After close(fd) [close 3]: fd 0: file.txt fd 1: stdout fd 2: stderr
After open("out.txt", O_WRONLY) [fd=3]: fd 0: file.txt fd 1: stdout fd 2: stderr fd 3: out.txt (write)
After dup2(fd, 1) [fd=3 → 1]: fd 0: file.txt (stdin) fd 1: out.txt (now stdout writes to file) fd 2: stderr fd 3: out.txt (duplicate reference)
After close(fd) [close 3]: fd 0: file.txt fd 1: out.txt fd 2: stderrNow when /bin/wc runs:
wc reads from stdin (fd=0) → reads from file.txtwc writes to stdout (fd=1) → writes to out.txtwc writes errors to stderr (fd=2) → still consoleData Flow:
file.txt ↓/bin/wc (reads from stdin/fd=0) ↓Processes content (counts lines, words, chars) ↓Writes to stdout (fd=1) ↓out.txtKey Difference: Dual Redirection
Compare with Problem 8 (input only):
// Problem 8: Only input redirectionint fd = open("file.txt", O_RDONLY);dup2(fd, 0);close(fd);execl("/bin/wc", "wc", NULL);// Output goes to consoleWith Problem 11 (dual redirection):
// Problem 11: Input AND output redirectionint fd = open("file.txt", O_RDONLY);dup2(fd, 0);close(fd);fd = open("out.txt", O_WRONLY); // Added!dup2(fd, 1); // Redirect stdout tooclose(fd);execl("/bin/wc", "wc", NULL);// Input from file, output to fileImportant: Must Reuse fd Variable
Note the code reuses fd for both files:
int fd = open("file.txt", O_RDONLY);dup2(fd, 0);close(fd); // Close this descriptor
fd = open("out.txt", O_WRONLY); // Reuse variable, get fd=3 againdup2(fd, 1);close(fd);Why? Because the kernel recycles file descriptor numbers. After closing fd=3, the next open() returns fd=3 again.
Process Execution Timeline:
Time 0: Parent forks child
Time 1: Child opens file.txt Child redirects stdin to file.txt Child opens out.txt (for writing) Child redirects stdout to out.txt
Time 2: Child executes /bin/wc /bin/wc starts running
Time 3: /bin/wc reads from stdin (actually file.txt) Processes lines, words, characters /bin/wc writes to stdout (actually out.txt)
Time 4: /bin/wc finishes Child process terminates
Time 5: Parent's waitpid() returns Parent prints "DONE\n" Parent exitsComparison: Manual vs. Shell Syntax
# Shell command$ wc < file.txt > out.txt
# What happens internally (this C program does it manually)fork() stdin ← file.txt stdout → out.txt exec(/bin/wc)File Mode Consideration:
The code opens out.txt with O_WRONLY but not O_CREAT | O_TRUNC:
fd = open("out.txt", O_WRONLY); // File must already exist!Better practice:
fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);This creates the file if it doesn’t exist and truncates it if it does.
Real-World Output:
Assuming file.txt contains:
line oneline twoline threeThe out.txt will contain:
3 6 22(3 lines, 6 words, 22 characters)
Key Lesson:
This program shows how operating systems implement shell I/O redirection:
- The shell redirects file descriptors before exec()
- The executed program doesn’t know about the redirection
- All I/O goes through the redirected descriptors
- The program remains completely unaware of file names
Problem 12: Pipeline of Two Commands (cat | wc)
Section titled “Problem 12: Pipeline of Two Commands (cat | wc)”Implement a program that implements the following command by forking two child processes and tying the standard output of the first child with the standard input of the second with a pipe. You program must wait until the second process terminates and then prints “ALL DONE\n” before terminating itself.
bash% /bin/cat file.txt | /bin/wcTo implement this program, you can use the following system calls: fork() creates a child process, execv(char *execFilename, char *args[]) loads an executable on top of an existing process, pipe(int fd[2]), which creates a pipe where fd[0] will be the read end of the pipe, fd[1] will be the write end of the pipe. Your program must wait until “/bin/wc” terminates and prints “DONE\n” before terminating. You can use waitpid(int pid) system call to wait for the termination of a process with “pid”.
Instructor Solution
#include <unistd.h>#include <stdio.h>#include <fcntl.h> // For open()#include <sys/wait.h>
/* /bin/cat file.txt | /usr/bin/wc */int main(int argc, char *argv[]) { int fd[2]; pipe(fd);
if (fork() == 0) { // fork a child and load /bin/cat dup2(fd[1], 1); close(fd[0]); close(fd[1]); execl("/bin/cat", "cat", "file.txt", NULL); }
close(fd[1]); pid_t pid;
if ((pid = fork()) == 0) { dup2(fd[0], 0); close(fd[0]); execl("/bin/wc", "wc", NULL); } // end-if
close(fd[0]); waitpid(pid, NULL, 0); printf("DONE\n"); return 0;} /* end-main */Elaboration (by LLM)
This is the final form of the command pipeline pattern: connecting two independent external commands via a pipe. This is exactly how Unix shells implement pipelines.
The Complete Pipeline Pattern:
Parent ├─ Child 1 (/bin/cat) │ ├─ Input: file.txt │ ├─ Output: pipe (fd[1]) │ └─ Process: read and output file contents │ ├─ Pipe (buffer) │ └─ Child 2 (/bin/wc) ├─ Input: pipe (fd[0]) ├─ Output: stdout (console) └─ Process: count lines, words, charsKey Pattern: Parent Closes Write End
if (fork() == 0) { dup2(fd[1], 1); // Child 1 writes to pipe close(fd[0]); // Child 1 won't read close(fd[1]); // Close duplicate execl("/bin/cat", "cat", "file.txt", NULL);}
close(fd[1]); // PARENT MUST CLOSE WRITE END! // Otherwise Child 2 never sees EOFWhy Parent Must Close fd[1]:
If parent doesn’t close the write end:
Child 1 (/bin/cat): ├─ Has write end (fd[1]) ├─ Writes file contents └─ Eventually exits (closes fd[1])
Child 2 (/bin/wc): ├─ Reads from pipe ├─ Blocks waiting for more data ├─ Checks: "Is write end closed?" ├─ Sees: Parent still has fd[1] open ├─ Thinks: "More data coming, keep waiting..." └─ DEADLOCK!With parent closing fd[1]:
Child 1 (/bin/cat): ├─ Has write end (fd[1]) ├─ Writes file contents └─ Eventually exits (closes fd[1])
Child 2 (/bin/wc): ├─ Reads from pipe ├─ Checks: "Is write end closed?" ├─ Sees: All write ends are closed ├─ Receives EOF signal ├─ Stops reading ├─ Processes buffered data ├─ Outputs results └─ Exits normallyFile Descriptor Management:
After pipe() and before any fork():
Parent: fd[0] (read), fd[1] (write)After first fork() (Child 1 - cat):
Parent: fd[0] (read), fd[1] (write)
Child 1: ├─ Inherits fd[0], fd[1] ├─ dup2(fd[1], 1): stdout → pipe ├─ close(fd[0]): won't read ├─ close(fd[1]): close original ├─ Now: fd 0,1,2 available, fd 3+ unused └─ exec(/bin/cat)Parent closes write end:
Parent: close(fd[1]) Now: fd[0] (read only), fd[1] closedAfter second fork() (Child 2 - wc):
Parent: fd[0] (read), fd[1] (closed)
Child 2: ├─ Inherits fd[0], fd[1] ├─ dup2(fd[0], 0): stdin ← pipe ├─ close(fd[0]): close original ├─ Now: all write ends are closed │ (Child 1's closed + Parent's closed) ├─ EOF will be generated when pipe empties └─ exec(/bin/wc)Parent closes read end:
Parent: close(fd[0]) Now: nothing to do, just waitData Flow Timeline:
Time 0: Parent creates pipe Parent forks Child 1 (cat)
Time 1: Child 1 redirects stdout to pipe Parent closes pipe write end Parent forks Child 2 (wc)
Time 2: Child 1 executes /bin/cat /bin/cat opens file.txt /bin/cat reads file contents /bin/cat outputs to stdout (pipe) Data flows: file.txt → pipe buffer
Time 3: Child 2 executes /bin/wc /bin/wc reads from stdin (pipe) /bin/wc starts counting as data arrives CPU-I/O overlap: cat reading while wc processing
Time 4: Child 1 finishes reading file /bin/cat exits Write end of pipe closed Pipe sends EOF to Child 2
Time 5: Child 2 sees EOF /bin/wc finishes counting /bin/wc outputs results to console /bin/wc exits
Time 6: Parent's waitpid() returns Parent prints "DONE\n" Parent exitsConcurrency:
Notice that /bin/cat and /bin/wc run concurrently:
Time 0-5ms: cat reading file, wc waiting for dataTime 5-50ms: cat outputs lines 1-10 to pipe wc reads and processes lines 1-5 cat continues outputting lines 11-20 wc continues processing lines 6-10Time 50ms: cat finishes, closes write endTime 51ms: wc finishes processing all data, outputs resultWithout pipes, the sequence would be:
Time 0-10ms: cat reads entire fileTime 10ms: cat finishesTime 10-15ms: wc counts (cat is done, waiting)Pipes enable efficient overlap of I/O and processing.
Comparison: Problem 7 vs Problem 12
Both implement ls | wc, but:
| Aspect | Problem 7 | Problem 12 |
|---|---|---|
| First cmd | /bin/ls (implicit) | /bin/cat file.txt |
| Second cmd | /bin/wc | /bin/wc |
| Focus | Basic pipeline | File reading |
| Pattern | Shell-like | File + pipe combo |
Real-World Shell Equivalent:
$ cat file.txt | wc
# What the shell does internally:# (Exactly what this C program does)Error Handling Not Shown:
The code omits error checking for clarity:
// Robust version:if (fork() == 0) { if (dup2(fd[1], 1) < 0) perror("dup2"); close(fd[0]); close(fd[1]); if (execl("/bin/cat", "cat", "file.txt", NULL) < 0) perror("execl");}Always check return values in production code.
Key Lesson:
This demonstrates the complete pipeline implementation:
- Create pipe before forking
- First child writes to pipe, executes first command
- Parent closes write end (critical!)
- Second child reads from pipe, executes second command
- Parent closes read end, waits for second child
- Both commands run concurrently, sharing data via pipe
Problem 13: Process Proxy with Bidirectional Pipes
Section titled “Problem 13: Process Proxy with Bidirectional Pipes”Consider implementing a program P that creates two child processes, C1 and C2 with the following constraints: P creates 2 children C1 and C2 and acts as a proxy between the two: That is, C1 sends a message “Hello\n” to P, which receives this message and sends it over to C2, which simply prints it on the screen. To achieve this, you must create two pipes, one between P and C1 and another between P and C2.
You may assume the existence of a system call pipe(int fd[2]) that creates a pipe and returns 2 descriptors. fd[0] will be the read end of the pipe, fd[1] will be the write end of the pipe. Make sure parent P waits for all children to terminate before printing “Parent done…\n” and terminating itself. You can use waitpid(int pid) system call to wait for the termination of a process with “pid”. Use fork() system call to create a child process.
Instructor Solution
#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <sys/wait.h>
int main(int argc, char *argv[]) { int fd1[2]; pipe(fd1); // From C1 to Parent
if (fork() == 0) { // C1 write(fd1[1], "Hello\n", 6); exit(0); } // end-if
int fd2[2]; pipe(fd2); // From Parent to C2 char message[6]; int len = read(fd1[0], message, 6); // Message from C1 write(fd2[1], message, len); // Message to C2 close(fd1[0]); close(fd1[1]); close(fd2[1]);
pid_t pid; if ((pid = fork()) == 0) { // C2 char message[6]; int len = read(fd2[0], message, 6); // Read hello write(1, message, len); close(fd2[0]); exit(0); } // end-if
close(fd2[0]); waitpid(pid, NULL, 0); printf("Parent done…\n"); return 0;} /* end-main */Elaboration (by LLM)
This program demonstrates a proxy pattern where the parent acts as a middleman between two child processes. Compare with Problem 6:
| Pattern | Problem 6 (Chain) | Problem 13 (Proxy) |
|---|---|---|
| Data path | C1 → C2 | C1 → Parent → C2 |
| Architecture | Linear pipeline | Hub-and-spoke |
| Use case | Direct data flow | Parent processes data |
| Complexity | Simpler | More powerful |
Proxy Architecture:
C1 (Producer) Parent (Proxy) C2 (Consumer) | | | |─── write(fd1) ──────┤ | | | | | read(fd1[0]) | | | | | [could process here] | | | | | write(fd2[1]) ────────────────┤ | | read(fd2) | | | EXIT | [display] waitpid() ←─────────────── EXIT | printf("done")Execution Flow:
// Main creates C1if (fork() == 0) { write(fd1[1], "Hello\n", 6); // C1 sends data exit(0); // C1 exits}
// Main creates C2 AFTER getting C1's dataint len = read(fd1[0], message, 6); // Parent receives from C1write(fd2[1], message, len); // Parent forwards to C2
if ((pid = fork()) == 0) { read(fd2[0], message, 6); // C2 receives from parent write(1, message, len); // C2 displays exit(0);}Sequential Execution:
Unlike Problem 6 (concurrent), this is sequential:
C1 writes → Parent blocks reading ↓Parent reads, then creates C2 ↓C2 reads → Parent blocked reading ↓C2 writes output ↓Parent proceedsThe read() calls act as synchronization points.
Data Flow with Synchronization:
Time 0: Parent forks C1 C1: write(fd1[1], "Hello\n", 6) Parent: blocked on read(fd1[0], ...)
Time 1: C1 finishes write C1: exit(0) Parent: read() completes, gets "Hello\n"
Time 2: Parent creates fd2 Parent: write(fd2[1], message, len) Parent forks C2 C2: blocked on read(fd2[0], ...)
Time 3: Parent closes fd2[1] C2: read() completes, gets "Hello\n" C2: write(1, message, len) Console: "Hello" C2: exit(0)
Time 4: Parent: waitpid() returns Parent: printf("Parent done…\n")Pipe Closure Pattern:
// After reading from fd1close(fd1[0]); // Won't read fd1 againclose(fd1[1]); // Won't write fd1 (never opened write end in parent)
// After writing to fd2, before second forkclose(fd2[1]); // Parent won't use write end anymore// But parent still has fd2[0] open (inherited by C2)// C2 can read from it
// After second forkclose(fd2[0]); // Parent won't read fd2Why This Pattern?
Proxy pattern is useful when:
- Parent must process/validate data before forwarding
- Parent needs to log intermediate values
- Parent implements business logic between I/O
- Parent coordinates multiple producers/consumers
Example Enhancement:
// Parent could transform the data:int len = read(fd1[0], message, 6);
// Process/validate/transformchar transformed[6];transform(message, transformed, len);
// Forward modified datawrite(fd2[1], transformed, len);Comparison: Sequential vs. Concurrent
Problem 6 (Concurrent):
- C1 and C2 run simultaneously
- Parent acts as pipe connector
- Both read/write independently
- Potential race conditions
Problem 13 (Sequential):
- C1 runs, parent waits for its data
- Parent processes/forwards to C2
- Parent controls timing
- Easy to synchronize
File Descriptor Lifecycle:
After pipe(fd1), pipe(fd2): Parent: 0(stdin), 1(stdout), 2(stderr), fd1[0], fd1[1], fd2[0], fd2[1]
After first fork() → C1: C1 inherits all parent descriptors C1: write(fd1[1], ...) C1: exit(0) → closes all descriptors
After read(fd1[0], ...): Data transferred from C1 Parent closes fd1[0] and fd1[1]
After fork() → C2: C2 inherits parent descriptors C2: read(fd2[0], ...) C2: write(1, ...) → outputs to console C2: exit(0) → closes all descriptors
Parent cleanup: close(fd2[0]) and close(fd2[1]) already done implicitlyKey Difference: read() Blocking
int len = read(fd1[0], message, 6);This call blocks parent until:
- C1 writes data to fd1[1], OR
- C1 exits and all write ends are closed (EOF)
This provides natural synchronization without explicit locks.
Real-World Applications:
- Request-Response: Child sends request, parent responds
- Filter: Parent filters/transforms data between children
- Aggregator: Parent collects from multiple sources, sends to destination
- Router: Parent routes data based on content
Output:
HelloParent done…Lesson:
The proxy pattern shows how pipes enable complex process coordination:
- Pipes provide automatic blocking for synchronization
- Parent can implement logic between data transfers
- Decouples producer (C1) from consumer (C2)
- Parent becomes the control point for the system
Problem 14: Two-Way Pipe Communication with External Program
Section titled “Problem 14: Two-Way Pipe Communication with External Program”Assume that you are asked to implement a program (sum.c) that computes the sum of an array of integers stored in the following array: int nums[]. Your program needs to compute the sum and prints it on the screen. Assuming that int nums[] = {1, 2, 3}, here is a sample run of your program:
bash% sumThe sum of the numbers in the array: 6bash%Instead of implementing the program yourself, you decide to make use of a system utility /bin/add, which computes the sum of a stream of integers fed in at its standard input and prints the sum on the screen. A sample output of /bin/add is given below, where the integers 1 2 3 4 are input by the user from the keyboard, and /bin/add prints their sum 10 on the standard output on the next line.
bash% /bin/add1 2 3 410bash%Implement this program as follows: Create a child process to run “/bin/add” and create two pipes P1 and P2. P1 will be used by your program to send the numbers in “nums” array to /bin/add and P2 will be used by /bin/add to send your program the sum of these numbers. Make sure that you connect standard input of /bin/add to the “read end” of P1 and connect standard output of /bin/add to the “write end” of P2.
Recall that you create a new child using fork(), start a new executable using execv(char *execFilename, char *args[]), create a pipe using pipe(int fd[2]) and copy the contents of fd1 to the contents of fd2 using dup2(int fd1, int fd2) system calls.
Instructor Solution
#include <unistd.h>#include <stdio.h>#include <stdlib.h>#include <string.h>
int main(int argc, char *argv[]) { int fd1[2]; pipe(fd1); // From Parent to child int fd2[2]; pipe(fd2); // From child to parent
if (fork() == 0) { // Child dup2(fd1[0], 0); dup2(fd2[1], 1); close(fd1[0]); close(fd1[1]); close(fd2[0]); close(fd2[1]); execl("./add", "add", NULL); } // end-if
close(fd1[0]); close(fd2[1]); int nums[] = {1, 2, 3, 4};
for (int i = 0; i < sizeof(nums) / sizeof(int); i++) { char buffer[20]; sprintf(buffer, "%d ", nums[i]); write(fd1[1], buffer, strlen(buffer)); }
close(fd1[1]); char answer[20] = {'\0'}; read(fd2[0], answer, 20); printf("The sum of the numbers in the array: %s", answer); close(fd2[0]); return 0;} /* end-main */Elaboration (by LLM)
This is the bidirectional pipe pattern: parent and child communicate in both directions. The external program (./add) remains unaware of pipes—it simply reads from stdin and writes to stdout.
Two-Pipe Architecture:
Parent Child (/bin/add) | | fd1[1] (write) ──────pipe1──────→ stdin (0) | (process) | fd2[0] (read) ←──────pipe2←────── stdout (1) |File Descriptor Setup in Child:
if (fork() == 0) { // Child inherits: fd1[0], fd1[1], fd2[0], fd2[1]
dup2(fd1[0], 0); // stdin ← parent's pipe1 (read end) dup2(fd2[1], 1); // stdout → parent's pipe2 (write end)
close(fd1[0]); // Close originals (now duplicated as 0,1) close(fd1[1]); // Don't need to write pipe1 close(fd2[0]); // Don't need to read pipe2 close(fd2[1]); // Close original (now duplicated as 1)
execl("./add", "add", NULL);}File Descriptor State:
Child at exec time:
fd 0: stdin ← pipe1[0] (reads from parent)fd 1: stdout → pipe2[1] (writes to parent)fd 2: stderr (console)When /bin/add reads from stdin, it reads parent’s data.
When /bin/add writes to stdout, it writes to parent’s pipe.
Parent File Descriptor Management:
close(fd1[0]); // Parent won't read from fd1close(fd2[1]); // Parent won't write to fd2Parent only:
- Writes to fd1[1] (send data to child)
- Reads from fd2[0] (receive data from child)
Data Flow:
Parent creates arrays: int nums[] = {1, 2, 3, 4}
Parent sends data: write(fd1[1], "1 ", 2) write(fd1[1], "2 ", 2) write(fd1[1], "3 ", 2) write(fd1[1], "4 ", 2) ↓ pipe1 buffer ↓Child (/bin/add) receives: Reads from stdin (fd=0) Accumulates: "1 2 3 4 " Parses integers: 1, 2, 3, 4 Computes sum: 10 Writes to stdout (fd=1) Output: "10\n" ↓ pipe2 buffer ↓Parent receives: read(fd2[0], answer, 20) Gets: "10\n" Displays: "The sum of the numbers in the array: 10"String Formatting:
Notice parent formats numbers as strings:
for (int i = 0; i < sizeof(nums) / sizeof(int); i++) { char buffer[20]; sprintf(buffer, "%d ", nums[i]); // Convert int to string write(fd1[1], buffer, strlen(buffer)); // Send as text}The child program (./add) expects text input (like keyboard input), so parent must convert:
1(int) →"1 "(string) → pipe → child reads as string
Close and EOF Signaling:
close(fd1[1]); // CRITICAL! // After writing all numbers, close write end // Child sees EOF on stdin // Child knows no more data coming // Child can process and output resultWithout closing fd1[1]:
Child: reads from stdinChild: gets "1 2 3 4 "Child: waits for more dataParent: still has fd1[1] openChild: doesn't see EOFDEADLOCK!Process Execution Timeline:
Time 0: Parent forks child
Time 1: Child redirects stdin ← pipe1, stdout → pipe2 Parent closes unused ends: fd1[0], fd2[1]
Time 2: Child executes /bin/add /bin/add starts, reads from stdin
Time 3: Parent writes integers to fd1[1] 1 2 3 4
Time 4: /bin/add reads from stdin (pipe1) Receives: "1 2 3 4 " Parses and sums: 10
Time 5: Parent closes fd1[1] /bin/add sees EOF on stdin Knows no more input coming
Time 6: /bin/add outputs: "10\n" to stdout (pipe2)
Time 7: Parent reads from fd2[0] Gets: "10\n"
Time 8: Parent printf() Output: "The sum of the numbers in the array: 10"
Time 9: /bin/add exits Parent closes fd2[0] Parent returns 0Contrast: Unidirectional vs. Bidirectional
Problem 8 (Parent → Child):
Parent sends file contents to childChild receives and processesChild outputs to consoleParent doesn't read from childProblem 14 (Bidirectional):
Parent sends data to childChild processes and returns resultParent reads child's outputParent displays resultWhy Two Pipes?
One pipe is unidirectional:
- Can’t send data in both directions
- Problem: Child output goes to console, not parent
Two pipes enable:
- Parent → Child: send input
- Child → Parent: receive output
- Parent can process child’s result
Real-World Applications:
// Calculator:Parent: "2 + 3"Child: "5"
// Translator:Parent: "hello"Child: "hola"
// Encryption:Parent: plaintextChild: ciphertext
// Query:Parent: SQL queryChild: result setEdge Cases:
Buffer overflow:
char answer[20] = {'\0'};read(fd2[0], answer, 20); // Could overflow if child sends >20 bytesBetter:
char answer[20];int n = read(fd2[0], answer, sizeof(answer)-1);if (n > 0) answer[n] = '\0';Blocking reads:
If child crashes without writing:
read(fd2[0], answer, 20); // Blocks forever!Would need timeout or signal handling.
Key Lesson:
Bidirectional pipe communication enables:
- Request-Response pattern: Parent asks, child answers
- Service abstraction: External programs as services
- Data transformation: Parent controls workflow
- Decoupling: Child doesn’t know about parent
This is the foundation of many Unix tools and server architectures.
Problem 15: Reading Output from External Program
Section titled “Problem 15: Reading Output from External Program”Assume that there is a system utility program “/bin/rand_nums” that prints 5 random integers on the screen. Here is a sample run of this program:
bash% /bin/rand_nums3 7 1 6 2bash%You are asked to implement a program called “sum.c” that will run “/bin/rand_nums”, get the 5 numbers generated, compute their sum and print the sum on the screen. To make this possible, you will need to create a child process to run “/bin/rand_nums”, create a pipe for the child process to send the generated numbers to your program over the pipe. Make sure that you connect descriptor 1 of “/bin/rand_nums” to the “write end” of the pipe so that the numbers are sent to your program over the pipe. Also connect your descriptor 0 to the “read end” of the pipe so that you can use “scanf” to read the numbers as if they are coming from the keyboard.
Here is a sample run of your program:
bash% ./sumThe sum of the 5 random numbers is 19bash%Recall that you create a new child using fork(), start a new executable using execl(char *execFilename, char *arg1, ...), create a pipe using pipe(int fd[2]) and copy the contents of fd1 to the contents of fd2 using dup2(int fd1, int fd2) system calls.
Instructor Solution
#include <stdio.h>#include <unistd.h>
int main(int argc, char *argv[]) { int fd[2]; pipe(fd); // From child to parent
if (fork() == 0) { // Child dup2(fd[1], 1); close(fd[1]); close(fd[0]); execl("./rand_nums", "rand_nums", NULL); } // end-if
close(fd[1]); dup2(fd[0], 0); close(fd[0]); int sum = 0;
for (int i = 0; i < 5; i++) { int num; scanf("%d", &num); sum += num; }
printf("The sum of the 5 random numbers is %d\n", sum); return 0;} /* end-main */