Skip to content

03-IPC (Inter-Process Communication)

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:

  1. Virtual-to-Physical Translation: Translates virtual addresses using per-process page tables
  2. Access Control: Prevents access to physical memory not owned by the current process
  3. 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:

  1. Create Segment: Call shmget() to allocate kernel-managed shared memory
  2. Attach Segment: Both processes call shmat() to map it to their virtual address spaces
  3. Same Physical Location: Now both virtual addresses point to the same physical memory
// Process A and B both execute this
int 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.

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 data

Read Blocking:

read(fd[0], buffer, 1024); // Pipe is empty
// Read process blocks
// Unblocked when writer sends data

Related 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 buffer

Pipes trade performance for automatic synchronization and buffering.

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 destroyed
close(fd[0]);
close(fd[1]);
// Pipe is gone from kernel

Message Queues:

int msqid = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
fork();
// ... parent and child use message queue
exit(0); // Parent exits
// Message queue remains in kernel!
// Another process can still access it
msgrcv(msqid, ...); // Works, data still there

Message 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 message1
read(fd[0], buf); // Always gets message2
// No way to skip message1 and get message2

Message 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 first
msgrcv(msqid, &buf, MAXSIZE, 2); // Gets msg2 (type 2)
msgrcv(msqid, &buf, MAXSIZE, 1); // Gets msg1 (type 1)

Comparison Table:

FeaturePipesMessage Queues
Data FormatByte streamStructured messages
BoundariesMust encode manuallyAutomatic
Access OrderStrict FIFOBy type/priority
PersistenceTransientPersistent
Kernel IDFile descriptorsSystem V IPC ID
Related ProcsRequiredAny process
Unread DataLost if reader diesPreserved 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

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 descriptors

C1 (First Child) Execution:

dup2(fd[1], 1); // Redirect stdout to pipe
close(fd[0]); // Don't need to read
close(fd[1]); // Close duplicate descriptor
printf("Hello\n"); // Goes to pipe, not console!
exit(0); // Process ends

After dup2(), any output to stdout (fd=1) actually goes to the pipe.

C2 (Second Child) Execution:

dup2(fd[0], 0); // Redirect stdin to pipe
close(fd[0]); // Close original descriptor
char message[6];
int len = read(0, message, 6); // Read from pipe (fd=0)
write(1, message, len); // Write to stdout
exit(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 write
close(fd[0]); // Parent won't read
waitpid(pid, NULL, 0); // Wait for C2 to finish
printf("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:

Hello
Parent done…

The “Hello” will always appear before “Parent done” because of the waitpid() synchronization point.

Key Lessons:

  1. dup2() redirects I/O: Elegant way to redirect stdin/stdout
  2. Close unused descriptors: Essential for proper pipe EOF handling
  3. Process coordination: Parent ensures C2 finishes before continuing
  4. FIFO behavior: Data flows in order through the pipe

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 pipe
write(fd[1], "Hello\n", 6); // Parent writes immediately
close(fd[1]); // Parent closes write end
fork(); // Now create child

Why 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 data

Timing 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:

AspectProblem 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 bufferingReal-time (synchronous)Buffered (parent waits?)
Timing guaranteeChild may block waiting for dataData always available
Pipe buffer useContinuous flowAccumulated then read

Why Close Write End Before Fork?

This is essential:

close(fd[1]); // MUST close before fork
fork();

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() call

Memory 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:

Hello
Parent 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 pipe
pipe(fd2); // Create C1→C2 pipe
// After first fork() to create C1:
close(fd1[0]); // Parent won't read from fd1
close(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 fd2

Why 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 correctly

Execution 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 order

Output Order Guarantee:

Hello
Parent 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:

  1. Close descriptors it won’t use
  2. Ensure proper EOF signaling
  3. 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:

Terminal window
$ 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
|→ exit

Step-by-Step Execution:

Step 1: Create Pipe

int fd[2];
pipe(fd);
// fd[0] = read end
// fd[1] = write end

Step 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/ls runs
  • 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 write

Critical: 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/wc runs
  • 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 read
waitpid(pid, NULL, 0); // Wait for wc to finish
printf("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:

Terminal window
$ ./pipeline
12 24 256
My 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:

AspectProblem 6 (Manual Messages)Problem 7 (Real Commands)
Data FormatFixed message sizesStream of data
ProcessingCustom C codeExisting Unix utilities
FlexibilityLimited to program logicCan chain any commands
Real-WorldUncommon in practiceUsed 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 │ ──→ Counts
file3\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:

Terminal window
$ 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 console

Each program is independent, but they’re seamlessly connected via pipes.

Critical Implementation Details:

  1. dup2() before execl(): Must redirect before execution
  2. Close unused ends: Essential for EOF signaling
  3. Parent’s role: Create infrastructure, then get out of the way
  4. Synchronization: Only parent needs to know when wc finishes

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:

AspectProblem 7 (ls | wc)Problem 8 (file → wc)
SourceAnother command (ls)A file (file.txt)
ParentCreates infrastructure, waitsActively feeds data
I/O MethodPipe onlyFile + Pipe
CommandImplicit (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 pipe
int fd2 = open("file.txt", O_RDONLY); // Open input file
char 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 wc

Why Read Byte-by-Byte?

While inefficient, reading one byte at a time:

  1. Simplicity: Demonstrates the concept clearly
  2. Works: Pipe buffers data, so performance difference is minimal
  3. 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 18

Timing & Buffering:

Without the pipe, the sequence would be:

Parent: write 1 byte → wc can't process partial data
Parent: write 1 byte → wc still waiting
...
Parent: close write end → wc sees EOF, processes buffer

Pipe allows wc to start processing once enough data arrives.

File Descriptor States:

Initial (Parent perspective):

fd 0: stdin
fd 1: stdout
fd 2: stderr
fd[0]: pipe read
fd[1]: pipe write
fd2: file.txt read

After fork():

Parent: Child (becomes /bin/wc):
fd 0: stdin fd 0: pipe read (redirected)
fd 1: stdout fd 1: stdout
fd 2: stderr fd 2: stderr
fd[0]: closed fd[0]: closed (already dup2'd)
fd[1]: write fd[1]: closed
fd2: 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 wc

Why 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 stdin
Stdin is redirected to pipe fd[0]
Parent has fd[1] still open
Even though parent closed the loop
wc doesn't see EOF because fd[1] is still open somewhere
wc blocks forever waiting for more data

Full 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 ends

Expected Output:

Assuming file.txt contains 3 lines:

3 3 18

Three lines, three words, eighteen characters total.

Key Learning Points:

  1. Parent as Data Source: Parent can feed any data to child via pipe
  2. File to Command: Demonstrates how < redirection works internally
  3. Byte-by-Byte Transfer: Inefficient but correct and educational
  4. EOF Signaling: Closing write end is essential for child to know when data ends
  5. Pipe Buffering: Allows asynchronous communication between parent and child

Real-World Equivalent:

Terminal window
$ wc < file.txt
# This C program implements this command line

Problem 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.txt
Characters: 10234
Words: 1353
Lines: 286
bash%

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.txt
10234 1353 286
bash%

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:

ProblemSourceSinkParent Role
7lswcPassive observer
8filewcData feeder
9filewcData 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 output

Key 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: stderr

Step 2: Parent Waits for Data

close(fd[1]); // Parent won't write
dup2(fd[0], 0); // Parent's stdin ← pipe
int 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: 3

Data 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 ends

Example Run:

Assuming file.txt contains:

hello world
this is test
final line

Actual wc output:

Terminal window
$ wc < file.txt
3 7 40

Program output:

Characters: 40
Words: 7
Lines: 3

Why This Pattern Matters:

This demonstrates:

  1. Bidirectional Communication: Parent both launches child and reads its output
  2. Data Parsing: Converting raw command output to structured data
  3. Formatting: Transforming one output format to another
  4. File Descriptors: Multiple redirections in a single process

Real-World Use Cases:

Terminal window
# 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 -l

Key Differences from Simple Redirection:

AspectShell |This Program
Who parsesShell (simple)Application (complex)
Output useHuman readableProgrammatic
Data flowLinearParent ← Child ← File
ComplexityHighModerate

Important Details:

  1. dup2(fd[0], 0): Parent now reads from pipe via stdin
  2. scanf(): Parses integers from parent’s stdin (the pipe)
  3. printf(): Parent outputs formatted results
  4. 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 this
if (pid == 0) { ... }
// Later:
waitpid(pid, NULL, 0); // Ensure child is reaped

Problem 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-main
Instructor Solution

Expected Output:

Main entered
v: <6>
Main done

Elaboration (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 pipe
  • fds[1]: write end of pipe

Both processes start with open references to both ends.

Process Communication Flow:

  1. Parent writes integer 6 to pipe via fds[1]
  2. Child waits (blocks) on read() from fds[0] until data arrives
  3. When parent writes, child’s read() unblocks with the value 6
  4. 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-main
Instructor Solution

Expected Output:

Str: <Message>, g: 2
Str: <AA>, g: 3

Elaboration (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 0
pg = 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:

  1. Parent & Child both increment g: (*pg)++ → g = 2

    • Happens before fork (parent executes)
    • After fork, child sees g = 1 initially, but parent already incremented
  2. 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”
  3. 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)
  4. Parent resumes: Wakes from busy-wait

    • Prints: “Str: , g: 3” (sees child’s modifications)

Critical Synchronization Points:

// Child waits for parent
while (*pi == 0); // Busy-wait for pi = 1
// Parent waits for child
while (*pi == 1); // Busy-wait for pi = 0

These 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 uses shmget(), 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.txt

As 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:

  1. Creates a new process for wc
  2. Redirects stdin (fd=0) to read from file.txt
  3. Redirects stdout (fd=1) to write to out.txt
  4. 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.txt
close(fd);
fd = open("out.txt", O_WRONLY);
dup2(fd, 1); // stdout → out.txt
close(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: stderr

Now when /bin/wc runs:

wc reads from stdin (fd=0) → reads from file.txt
wc writes to stdout (fd=1) → writes to out.txt
wc writes errors to stderr (fd=2) → still console

Data Flow:

file.txt
/bin/wc (reads from stdin/fd=0)
Processes content (counts lines, words, chars)
Writes to stdout (fd=1)
out.txt

Key Difference: Dual Redirection

Compare with Problem 8 (input only):

// Problem 8: Only input redirection
int fd = open("file.txt", O_RDONLY);
dup2(fd, 0);
close(fd);
execl("/bin/wc", "wc", NULL);
// Output goes to console

With Problem 11 (dual redirection):

// Problem 11: Input AND output redirection
int fd = open("file.txt", O_RDONLY);
dup2(fd, 0);
close(fd);
fd = open("out.txt", O_WRONLY); // Added!
dup2(fd, 1); // Redirect stdout too
close(fd);
execl("/bin/wc", "wc", NULL);
// Input from file, output to file

Important: 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 again
dup2(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 exits

Comparison: Manual vs. Shell Syntax

Terminal window
# 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 one
line two
line three

The 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:

  1. The shell redirects file descriptors before exec()
  2. The executed program doesn’t know about the redirection
  3. All I/O goes through the redirected descriptors
  4. 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/wc

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, 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, chars

Key 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 EOF

Why 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 normally

File 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] closed

After 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 wait

Data 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 exits

Concurrency:

Notice that /bin/cat and /bin/wc run concurrently:

Time 0-5ms: cat reading file, wc waiting for data
Time 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-10
Time 50ms: cat finishes, closes write end
Time 51ms: wc finishes processing all data, outputs result

Without pipes, the sequence would be:

Time 0-10ms: cat reads entire file
Time 10ms: cat finishes
Time 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:

AspectProblem 7Problem 12
First cmd/bin/ls (implicit)/bin/cat file.txt
Second cmd/bin/wc/bin/wc
FocusBasic pipelineFile reading
PatternShell-likeFile + pipe combo

Real-World Shell Equivalent:

Terminal window
$ 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:

  1. Create pipe before forking
  2. First child writes to pipe, executes first command
  3. Parent closes write end (critical!)
  4. Second child reads from pipe, executes second command
  5. Parent closes read end, waits for second child
  6. 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:

PatternProblem 6 (Chain)Problem 13 (Proxy)
Data pathC1 → C2C1 → Parent → C2
ArchitectureLinear pipelineHub-and-spoke
Use caseDirect data flowParent processes data
ComplexitySimplerMore 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 C1
if (fork() == 0) {
write(fd1[1], "Hello\n", 6); // C1 sends data
exit(0); // C1 exits
}
// Main creates C2 AFTER getting C1's data
int len = read(fd1[0], message, 6); // Parent receives from C1
write(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 proceeds

The 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 fd1
close(fd1[0]); // Won't read fd1 again
close(fd1[1]); // Won't write fd1 (never opened write end in parent)
// After writing to fd2, before second fork
close(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 fork
close(fd2[0]); // Parent won't read fd2

Why This Pattern?

Proxy pattern is useful when:

  1. Parent must process/validate data before forwarding
  2. Parent needs to log intermediate values
  3. Parent implements business logic between I/O
  4. Parent coordinates multiple producers/consumers

Example Enhancement:

// Parent could transform the data:
int len = read(fd1[0], message, 6);
// Process/validate/transform
char transformed[6];
transform(message, transformed, len);
// Forward modified data
write(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 implicitly

Key Difference: read() Blocking

int len = read(fd1[0], message, 6);

This call blocks parent until:

  1. C1 writes data to fd1[1], OR
  2. 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:

Hello
Parent done…

Lesson:

The proxy pattern shows how pipes enable complex process coordination:

  1. Pipes provide automatic blocking for synchronization
  2. Parent can implement logic between data transfers
  3. Decouples producer (C1) from consumer (C2)
  4. 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% sum
The sum of the numbers in the array: 6
bash%

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/add
1 2 3 4
10
bash%

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 fd1
close(fd2[1]); // Parent won't write to fd2

Parent 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 result

Without closing fd1[1]:

Child: reads from stdin
Child: gets "1 2 3 4 "
Child: waits for more data
Parent: still has fd1[1] open
Child: doesn't see EOF
DEADLOCK!

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 0

Contrast: Unidirectional vs. Bidirectional

Problem 8 (Parent → Child):

Parent sends file contents to child
Child receives and processes
Child outputs to console
Parent doesn't read from child

Problem 14 (Bidirectional):

Parent sends data to child
Child processes and returns result
Parent reads child's output
Parent displays result

Why 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: plaintext
Child: ciphertext
// Query:
Parent: SQL query
Child: result set

Edge Cases:

Buffer overflow:

char answer[20] = {'\0'};
read(fd2[0], answer, 20); // Could overflow if child sends >20 bytes

Better:

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:

  1. Request-Response pattern: Parent asks, child answers
  2. Service abstraction: External programs as services
  3. Data transformation: Parent controls workflow
  4. 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_nums
3 7 1 6 2
bash%

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% ./sum
The sum of the 5 random numbers is 19
bash%

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 */