03 - IPC

Problem 1: Shared Memory for Data Structures

Why is it necessary for two processes to explicitly allocate shared memory to share a data structure such as an array?

Student Solution

This is necessary because two separate processes do not have access to each other’s memory space. Thus, in order to share a data structure, such as an array, they need a memory space that is accessible to both processes. That is why they must explicitly allocate shared memory.

Elaboration (by LLM):

This is a fundamental concept in process isolation and inter-process communication.

Memory Space Isolation:

Each process has its own isolated virtual address space. When one process writes to an address, another process writing to the same address affects different physical memory. This is a core feature of modern operating systems.

Why Separate Memory?

  1. Protection: One process cannot corrupt another’s memory
  2. Security: Private data is inaccessible to other processes
  3. Stability: Buggy process doesn’t crash others
  4. Portability: Programs can assume exclusive memory access

Consequences:

If process A creates an array:

// Process A
int arr[100];
arr[0] = 42;

Process B cannot see or access arr[0]. The variable address in A’s space doesn’t exist in B’s address space.

Solution: Shared Memory

To share data, processes must explicitly allocate memory that both can access:

// Both processes see the SAME memory region
int *shared_arr = get_shared_memory(400);  // 100 * sizeof(int)
shared_arr[0] = 42;  // Both processes see this

Shared Memory Mechanics:

Different virtual addresses map to same physical memory region:

Process A's Virtual Space    Physical RAM         Process B's Virtual Space
0x5000 (shared_arr) ──────→ [Shared Memory] ←─── 0x6000 (shared_arr)

Allocation Methods:

  1. shmget/shmat (System V shared memory)
  2. mmap (memory mapping)
  3. Pipes (implicit shared kernel buffer)
  4. Message Queues (kernel-managed)

Key Insight:

Memory protection is a feature, not a bug. Explicit allocation forces programmers to be intentional about sharing, preventing accidental data corruption and enforcing clean IPC design.

Problem 2: Pipes

What’s a pipe? What is it used for?

Student Solution

A pipe is a unidirectional message-passing link between processes. It is used to share data between two processes without allocating space for shared memory. This is done by assigning one process to be a read process and one to be a write process, where the write sends messages through the pipe and to the read process, where it is read.

Elaboration (by LLM):

Pipes are one of the oldest and simplest IPC mechanisms in Unix.

What is a Pipe?

A pipe is a virtual communication channel that the kernel manages:

Writer Process          Kernel           Reader Process
     |                  |                    |
  write()  ────→  [Pipe Buffer]  ────→  read()
     |                  |                    |
  fd[1]                 |                 fd[0]

How Pipes Work:

  1. Creation: pipe(fd) creates two file descriptors

    • fd[0] = read end (input)
    • fd[1] = write end (output)
  2. Writing: Process writes data to fd[1]

    write(fd[1], "Hello", 5);
  3. Reading: Process reads data from fd[0]

    read(fd[0], buffer, 5);
  4. Blocking: Reader blocks if pipe is empty, writer blocks if full

Key Characteristics:

FeatureBehavior
DirectionUnidirectional (one-way)
ReadersOne (single reader)
WritersOne (single writer)
PersistenceLives only while processes exist
Buffer SizeSmall (typically 4-64 KB)
Message BoundariesNo (byte stream)
BlockingYes (reader waits if empty)

Advantages:

  • Very simple to use
  • No explicit allocation (kernel-managed)
  • Automatic cleanup
  • Efficient (kernel buffer)

Limitations:

  • Only one reader, one writer
  • Unidirectional only
  • No persistence (tied to process lifetime)
  • No message structure (byte stream)
  • Cannot select which data to read

Problem 3: Message Queues

What’s a message queue? How does it differ from a pipe?

LLM Solution

A message queue is a kernel-managed data structure that allows processes to exchange messages asynchronously. Unlike pipes, message queues are: bidirectional (not one-way), persistent (not tied to process lifetime), support multiple readers/writers, and allow messages to be tagged with priority.

Elaboration:

While pipes and message queues both enable inter-process communication, they have fundamentally different characteristics:

Message Queue Fundamentals:

A message queue is a kernel-managed FIFO (first-in-first-out) buffer that holds discrete messages. Each message has:

  • Data payload: The actual content being sent
  • Message type: An integer tag for categorization
  • Priority: Optional priority level

Key Differences:

AspectPipeMessage Queue
DirectionUnidirectional (one-way)Bidirectional (any direction)
Readers/WritersExactly 2 (one reader, one writer)Multiple readers and writers
PersistenceExists only while processes runPersists until explicitly deleted
Message StructureByte stream (no boundaries)Discrete messages with type
BufferingSmall kernel buffer (~4-64KB)Larger, configurable buffer
SelectionMust read sequentiallyCan select by message type
BlockingReader blocks if emptyCan be non-blocking

Pipe Characteristics:

Writer1 ---> [Pipe Buffer] ---> Reader1

Characteristics:
- Linear flow: write -> read
- Byte-stream: no message boundaries
- Small buffer (often ~4KB)
- Tightly coupled: reader must exist

Message Queue Characteristics:

Process1 \
         |-> [Message Queue] <-- Process3 (reader)
Process2 /
          (reads message type 5)

Characteristics:
- Multiple senders and receivers
- Messages tagged with type
- Larger, persistent buffer
- Loosely coupled: sender doesn't need receiver

Message Structure Example:

Each message in a queue contains:

struct message {
    long msg_type;      // Message type (1, 2, 3, ...)
    char msg_text[256]; // Payload data
};

A reader can request messages of a specific type:

// Receive only messages with type 5
msgrcv(msqid, &msg, sizeof(msg), 5, 0);

Comparison Examples:

Scenario 1: Multiple Writers

Pipe: Cannot have multiple writers safely (race condition)

// Process A and B both writing to same pipe = DANGER!
write(fd[1], "A's data", ...);
write(fd[1], "B's data", ...);  // Bytes interleave!

Message Queue: Designed for multiple writers

// Process A
msg_a.msg_type = 1;
msgsnd(msqid, &msg_a, len, 0);
 
// Process B
msg_b.msg_type = 2;
msgsnd(msqid, &msg_b, len, 0);
 
// Both messages preserved as discrete units

Scenario 2: Selective Reading

Pipe: Must read sequentially in order

read(fd[0], buffer, 100);  // Get first message (whatever it is)
read(fd[0], buffer, 100);  // Get second message
// No way to skip messages

Message Queue: Can select by type

// Skip to message type 5, ignore others
msgrcv(msqid, &msg, len, 5, 0);

Scenario 3: Process Lifetime

Pipe: Dies when processes die

// Process A and B communicate via pipe
// A exits -> pipe disappears
// B cannot recover the pipe

Message Queue: Persists independently

// Message queue created with unique ID
// Process A sends message
// Process A exits
// Process B starts later
// Process B can still retrieve messages from queue

When to Use Each:

Use Pipes When:

  • Simple one-way communication
  • Parent-child processes
  • Shell pipeline (ls | wc)
  • Need simple, lightweight IPC
  • Exactly 2 processes involved

Use Message Queues When:

  • Multiple processes need to communicate
  • Need selective message reading
  • Messages need persistence
  • Complex server-client relationships
  • Different message types for different purposes

System Calls:

Message Queue Operations:

#include <sys/msg.h>
 
// Create/get message queue
int msqid = msgget(key, flags);
 
// Send message
msgsnd(msqid, &msg, size, flags);
 
// Receive message (optionally by type)
msgrcv(msqid, &msg, size, msg_type, flags);
 
// Control queue (delete, permissions, etc.)
msgctl(msqid, command, &buf);

Pipe Operations:

#include <unistd.h>
 
// Create pipe
pipe(fd);
 
// Read/write (standard I/O)
read(fd[0], buf, size);
write(fd[1], buf, size);

Problem 4: Piping Child Process Output to Input

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);
    }
 
    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);
    }
 
    close(fd[0]);
    waitpid(pid, NULL, 0);
    printf("Parent done…\n");
    return 0;
}

Elaboration (by LLM):

This code demonstrates the classic Unix pipeline pattern: connecting process output to process input via a pipe.

Data Flow:

C1: printf() → stdout (redirected)  ───→  [Pipe]  ───→  C2: stdin (redirected) → read()
                                                              ↓
                                                         Prints to screen

Execution Sequence:

  1. Create Pipe: pipe(fd) creates fd[0] (read) and fd[1] (write)

  2. Fork C1 and redirect its stdout:

    dup2(fd[1], 1);  // stdout now goes to pipe write end

    When C1 calls printf("Hello\n"), the output goes to the pipe instead of the screen.

  3. Parent closes write end: close(fd[1]) in parent

    • Only C1 can write to pipe
    • Ensures C2 gets EOF when C1 closes
  4. Fork C2 and redirect its stdin:

    dup2(fd[0], 0);  // stdin now comes from pipe read end

    When C2 calls read(0, message, 6), it reads from the pipe.

  5. C2 reads and prints: The message “Hello\n” passes through pipe to screen

  6. Parent waits: Only waits for C2 (potential issue: C1 not explicitly reaped)

Key Mechanism: dup2()

dup2(fd[1], 1) makes file descriptor 1 (stdout) point to the pipe:

Before: fd[1] → [Pipe Write End]
        fd 1 → [Terminal]

After:  fd[1] → [Pipe Write End]
        fd 1 → [Pipe Write End]  (both point to same place)

Now any write to fd 1 goes to the pipe.

Message Journey:

C1: printf("Hello\n")
    ├─ Goes to fd 1 (stdout)
    ├─ fd 1 redirected to fd[1] (pipe write)
    ├─ "Hello\n" enters pipe buffer
    ├─ C1 exits, closes fd[1]
    └─ Pipe EOF detected

C2: read(fd[0], message, 6)
    ├─ Reads from fd[0] (pipe read)
    ├─ Gets "Hello\n" from pipe
    ├─ Writes to fd 1 (stdout → screen)
    └─ Output: "Hello"

Suggestions / Fixes:

  • Prefer _exit(0) in children after fork() when not using exec() to avoid stdio duplicate flushing: /* child */ _exit(0);
  • The parent currently waits for only one child; call wait()/waitpid() for all children (or loop on wait(NULL)) to avoid zombies.
  • Add error checking for pipe(), fork(), and dup2() return values.

Problem 5: Parent Sends Message to Child

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);
    }
 
    close(fd[0]);
    waitpid(pid, NULL, 0);
    printf("Parent done…\n");
    return 0;
}

Elaboration (by LLM):

This code reverses the typical parent-child relationship: the parent writes first, then creates the child to read.

Execution Sequence:

1. Parent: Create pipe
   pipe(fd) → fd[0] (read), fd[1] (write)

2. Parent: Write data BEFORE forking
   write(fd[1], "Hello\n", 6)
   └─ "Hello\n" written to pipe buffer immediately

3. Parent: Close write end
   close(fd[1])
   └─ Signal: no more writes coming

4. Parent: Fork child
   fork() → Child inherits open fd[0]

5. Child: Read from pipe
   read(fd[0], message, 6)
   └─ Gets "Hello\n" (already in buffer, no blocking)

6. Child: Write to stdout
   write(1, message, len) → Outputs: "Hello"

7. Parent: Clean up and wait
   close(fd[0])  (parent not reading)
   waitpid(pid, NULL, 0)

Data Timeline:

Parent writes             Parent forks            Child reads
"Hello\n"                  child                   from buffer
   |                        |                        |
   v                        v                        v
┌─────────────────────────────────────┐
│  Pipe Buffer     │  "Hello\n"
│  (ready to read) │
└─────────────────────────────────────┘

Key Difference from Problem 4:

In Problem 4, C1 writes via stdout redirection after fork. In Problem 5, parent writes directly via write() before fork.

Why No Blocking?

The write happens before fork:

  • Data is already in pipe buffer when child is created
  • Child’s read() returns immediately with data
  • No waiting required

Pipe Decouple Timing:

This demonstrates that pipes decouple the timing of writes and reads:

// Parent writes once
write(fd[1], "Hello", 5);
close(fd[1]);
 
// Can create multiple children to read same data
for (int i = 0; i < 3; i++) {
    if (fork() == 0) {
        char buf[6];
        read(fd[0], buf, 5);  // Each child gets same message
    }
}

Typical Output:

Hello
Parent done…

(Order may vary due to process scheduling)

Suggestions / Fixes:

  • Check return values for pipe(), write(), and fork(); handle errors where appropriate.
  • Consider closing unused file descriptors explicitly in both parent and child to avoid accidental fd inheritance or blocking.

Problem 6: Parent Relays Message Between Two Children

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);
    }
 
    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);
    }
 
    close(fd2[0]);
    waitpid(pid, NULL, 0);
 
    printf("Parent done…\n");
    return 0;
}

Elaboration (by LLM):

This code chains two pipes together, creating a multi-stage pipeline where parent sends to C1, C1 relays to C2, and C2 outputs.

Pipeline Architecture:

Parent → [fd1] → C1 → [fd2] → C2 → Output
"Hello"  pipe1      relay      pipe2   screen

Stage 1: Parent Creates fd1 and Sends

int fd1[2];
pipe(fd1);                    // Create pipe 1
write(fd1[1], "Hello\n", 6);  // Write message
close(fd1[1]);                // Close write end (done sending)

Stage 2: C1 Created (Relay)

if (fork() == 0) {  // C1
    close(fd2[0]);           // Not reading from fd2
    char message[6];
    int len = read(fd1[0], message, 6);   // Read from fd1
    write(fd2[1], message, len);          // Write to fd2
    exit(0);
}

C1:

  • Reads from pipe 1 (receives “Hello\n” from parent)
  • Writes to pipe 2 (forwards “Hello\n” to C2)
  • Acts as intermediary

Stage 3: Parent Cleanup and C2 Creation

close(fd1[0]);  // Parent not reading from fd1
close(fd2[1]);  // Parent not writing to fd2
 
if ((pid = fork()) == 0) {  // C2
    char message[6];
    int len = read(fd2[0], message, 6);  // Read from fd2
    write(1, message, len);              // Write to stdout
    exit(0);
}

C2:

  • Reads from pipe 2 (gets “Hello\n” from C1)
  • Writes to stdout (screen)
  • Final destination

File Descriptor Management:

Tracking which fd’s each process needs:

Parent:
  fd1[0] - Close (not reading)     ❌
  fd1[1] - Close (done writing)    ❌
  fd2[0] - Close (not reading)     ❌
  fd2[1] - Close (not writing)     ❌

C1 (relay):
  fd1[0] - Keep (reads from parent) ✅
  fd1[1] - Close (not needed)       ❌
  fd2[0] - Close (not needed)       ❌
  fd2[1] - Keep (writes to C2)      ✅

C2 (final):
  fd2[0] - Keep (reads from C1)     ✅
  fd2[1] - Close (not needed)       ❌
  fd1[*] - Inherited (not needed)   ❌

Execution Timeline:

Time 0: Parent writes "Hello\n" to fd1
        Parent closes fd1[1] (EOF signal)
        Parent creates both pipes before forking

Time 1: C1 created
        C1: read(fd1[0]) → Gets "Hello\n" (data ready)
        C1: write(fd2[1]) → Sends to fd2
        C1: exit()

Time 2: C2 created
        C2: read(fd2[0]) → Gets "Hello\n" from C1
        C2: write(1) → Output to screen
        C2: exit()

Time 3: Parent: waitpid() returns
        Parent: Print "Parent done…\n"

Why This Pattern?

Chaining pipes allows:

  1. Data Transformation: Each stage modifies data

    Input → [Upper] → [Reverse] → [Compress] → Output
    
  2. Pipeline Pattern: Classic Unix approach

    cat file.txt | grep pattern | wc -l
  3. Process Networks: Complex data flows

    Source → Stage1 → Stage2 → Stage3 → Sink
    

Typical Output:

Hello
Parent done…

Potential Issue:

The parent only waits for C2 (waitpid(pid, NULL, 0)), leaving C1 as a zombie. Better approach:

pid_t pid1 = fork();  // Save C1's PID
if (pid1 == 0) { /* C1 */ }
 
pid_t pid2 = fork();  // Save C2's PID
if (pid2 == 0) { /* C2 */ }
 
waitpid(pid1, NULL, 0);  // Reap both
waitpid(pid2, NULL, 0);

Suggestions / Fixes:

  • Parent should wait()/waitpid() for both children instead of only one (waitpid(pid, ...)) to avoid leaving a zombie process.
  • Close unused file descriptors in the parent and children as soon as they’re not needed to avoid accidental blocking.

Problem 7: Piping /bin/ls to /bin/wc

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);
    }
 
    close(fd[0]);
    waitpid(pid, NULL, 0);
 
    printf("My program exiting...\n");
    return 0;
}

Elaboration (by LLM):

This code implements the classic Unix pipeline ls | wc by connecting two processes via a pipe.

Process Steps:

  1. Create Pipe: pipe(fd) creates fd[0] (read) and fd[1] (write)
  2. Fork /bin/ls: Redirects stdout to pipe write end via dup2(fd[1], 1)
  3. Fork /bin/wc: Redirects stdin to pipe read end via dup2(fd[0], 0)
  4. Parent waits for /bin/wc to finish

Data Flow:

/bin/ls output  →  Pipe buffer  →  /bin/wc input
[file listing]      [buffered]       [counts lines/words/chars]

Key Synchronization:

  • Parent closes write end after forking /bin/ls
  • This ensures /bin/wc gets EOF when /bin/ls finishes writing
  • /bin/wc blocks on read until data arrives from /bin/ls

Equivalent Shell Command:

ls | wc

Note:

Parent only waits for /bin/wc, leaving /bin/ls as potential zombie. Better to save both PIDs and wait for both.

Suggestions / Fixes:

  • The parent should wait()/waitpid() for both child processes (both /bin/ls and /bin/wc) to properly reap them.
  • Add error checking for pipe(), fork(), dup2(), and execl() return values in production code.

Problem 8: Count Characters Using /bin/wc

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);
    }
 
    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;
}

Elaboration (by LLM):

This problem reverses typical data flow: parent reads from file and sends data character-by-character to /bin/wc via a pipe.

Data Flow:

Parent reads file  →  Writes to pipe  →  Child /bin/wc reads
One char at time      Buffers in pipe       Counts all data

Execution Steps:

  1. Child redirects stdin to pipe read end, then execs /bin/wc
  2. Parent reads file character by character: read(fd2, &ch, 1)
  3. Parent sends each char to pipe: write(fd[1], &ch, 1)
  4. When file ends, parent closes write end (sends EOF signal)
  5. /bin/wc detects EOF, processes accumulated data, outputs count

Pipe Buffering:

Even though data is written one character at a time, the pipe buffers it and /bin/wc reads it efficiently as chunks become available.

Inefficiency Note:

Character-by-character transfer causes many system calls. Better to use larger buffers:

char buffer[4096];
int n;
while ((n = read(fd2, buffer, sizeof(buffer))) > 0)
    write(fd[1], buffer, n);

Process Timeline:

Parent iteratively reads chars → writes to pipe → /bin/wc accumulates in buffer → parent closes write end (EOF) → /bin/wc processes and outputs

Why This Pattern?

Reuses /bin/wc for counting instead of reimplementing. Demonstrates how pipes provide clean separation of concerns.

Suggestions / Fixes:

  • Parent should wait()/waitpid() for the child running /bin/wc to avoid zombies and ensure child finished processing input.
  • Add error checks for open(), pipe(), fork(), and dup2() and handle failures gracefully.
  • Use buffered reads instead of one character at a time for efficiency.

Problem 9: Implement Counter Program Using /bin/wc

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 fd2 = open("file.txt", O_RDONLY);
        dup2(fd2, 0);
        close(fd2);
        // Child. Execl "/bin/wc < file.txt > pipe"
        execl("/bin/wc", "wc", NULL);
    }
 
    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;
}

Elaboration (by LLM):

This problem uses two-way communication: child reads from file, executes /bin/wc to process it, and outputs via pipe; parent reads reformatted output.

Complete Data Path:

file.txt (on disk)
    ↓
Child redirects stdin to file, stdout to pipe
    ↓
Child execs /bin/wc
    ↓
/bin/wc reads from stdin (file via redirection)
Counts: lines, words, characters
Writes to stdout (redirected to pipe)
    ↓
Pipe buffer: "286 1353 10234\n"
    ↓
Parent's stdin (redirected to pipe via dup2)
    ↓
Parent's scanf reads: lines=286, words=1353, chars=10234
    ↓
Parent reformats and prints

Key Redirection Pattern:

Child:

int fd2 = open("file.txt", O_RDONLY);
dup2(fd2, 0);          // stdin from file
dup2(fd[1], 1);        // stdout to pipe
execl("/bin/wc", "wc", NULL);

Parent:

dup2(fd[0], 0);        // stdin from pipe
scanf("%d %d %d", &numLines, &numWords, &numChars);

Parent’s scanf() reads from pipe instead of keyboard!

Output Transformation:

/bin/wc output: 286 1353 10234

Program reformats as:

Characters: 10234
Words: 1353
Lines: 286

Issues in Original Code:

  1. Hard-coded “file.txt” - inflexible
  2. No error checking
  3. No waitpid() - child becomes zombie
  4. Doesn’t validate scanf() return value

Suggestions / Fixes:

  • Avoid hard-coding "file.txt"; use argv[1] as the input filename so the program handles any filename passed on the command line.
  • When the parent dup2(fd[0], 0) to read the child’s output, be explicit about closing/reverting stdin if the program needs it elsewhere.
  • Add error checking for open(), dup2(), fork(), and execl().
  • Add waitpid(pid, NULL, 0) to avoid zombie process.

Problem 10: Analyzing Code Output

What would be the output of the following code segments (a) and (b) when run in a Linux system. Explain briefly.

(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[1], &v, sizeof(int));
        printf("v: <%d>\n", v);
    } else {
        write(fds[0], &l, sizeof(int));
        waitpid(pid);
        printf("Process 2 done.\n");
    }
}
LLM Solution

This code has a bug: the pipe endpoints are reversed. fds[0] is the read end and fds[1] is the write end, but the code writes to fds[0] (should be fds[1]) and reads from fds[1] (should be fds[0]). With the bug, the program will hang indefinitely waiting for data on the wrong end of the pipe.

Elaboration:

The Bug:

The fundamental issue is a misunderstanding of pipe file descriptors:

int fds[2];
pipe(fds);
 
// After pipe() call:
fds[0] = read end
fds[1] = write end

Code’s Mistakes:

// WRONG: Parent writes to read end (fds[0])
write(fds[0], &l, sizeof(int));    // ← Should be fds[1]
 
// WRONG: Child reads from write end (fds[1])
read(fds[1], &v, sizeof(int));     // ← Should be fds[0]

What Actually Happens (With Bug):

Execution Flow:

1. write(1, "Main entered\n");    -> Prints "Main entered"

2. pipe(fds) creates:
   fds[0] = read end
   fds[1] = write end

3. fork() creates child

4. Parent tries: write(fds[0], &l, sizeof(int))
   Problem: Writing to read end = FAILS or unexpected behavior
   (Pipe has write end at fds[1], not fds[0])

5. Child tries: read(fds[1], &v, sizeof(int))
   Problem: Reading from write end = BLOCKS FOREVER
   (No data available, waiting for something that won't come)

6. Parent calls waitpid(pid) -> BLOCKS
   (Waiting for child that's blocked on read)

7. DEADLOCK - Both processes hang forever

Output (if it runs at all):

Main entered
[Program hangs - requires Ctrl+C to kill]

Corrected Code:

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));   // ← FIX: Read from fds[0]
        printf("v: <%d>\n", v);
    } else {
        write(fds[1], &l, sizeof(int));  // ← FIX: Write to fds[1]
        waitpid(pid, NULL, 0);
        printf("Process 2 done.\n");
    }
}

Corrected Output:

Main entered
v: <6>
Process 2 done.

Why This Matters:

This is a classic mistake that demonstrates:

  1. Pipe Semantics: fds[0] = read, fds[1] = write (always)
  2. Deadlock Risk: Mishandling pipes can cause processes to hang indefinitely
  3. File Descriptor Direction: You cannot write to a read-only FD or read from a write-only FD

Lesson:

Always remember the pipe convention:

int fd[2];
pipe(fd);
 
fd[0] -> read end   (use with read())
fd[1] -> write end  (use with write())

(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 = (int *)pSm + sizeof(int);
    pStr = (char *)(pSm + 2*sizeof(int));
    *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);
    }
}
LLM Solution

This code has multiple bugs in pointer arithmetic and synchronization. The pointer pg calculation adds sizeof(int) integers instead of bytes, *pg++ increments the pointer instead of the value, and busy-waiting on shared memory is fragile. With corrections, the output is: Str: <Message>, g: 3 (child) then Str: <AA>, g: 4 (parent).

Elaboration:

Bug 1: Pointer Arithmetic Error

pg = (int *)pSm + sizeof(int);  // WRONG!

This line intends to skip one int and point to the second int in shared memory, but pointer arithmetic in C automatically multiplies by the type size:

(int *)pSm + sizeof(int)
= (int *)pSm + 4         (on 32-bit systems)
= offset by 4 * sizeof(int) = 16 bytes
= way past where intended!

Correct Version:

pg = (int *)(pSm + sizeof(int));  // CORRECT
// Or simpler:
pg = (int *)pSm + 1;  // Skip 1 int worth of space

Bug 2: Increment Operator Precedence

*pg++;  // WRONG!

This increments the pointer, not the value it points to:

*pg++;
// Equivalent to: *(pg++)
// Which means: dereference pg, THEN increment pg
// Not: increment the value that pg points to

Correct Version:

(*pg)++;  // CORRECT - increment the dereferenced value

Bug 3: Busy-Waiting (Fragile Synchronization)

while (*pi == 0);   // WRONG!
while (*pi == 1);   // WRONG!

Busy-waiting has multiple problems:

  • Wastes CPU: Spins in tight loop consuming 100% CPU
  • Memory Barriers: No guarantee child sees parent’s writes (compiler/CPU optimization)
  • Not Portable: Behavior undefined without volatile

Better Approach:

volatile int *pi;  // Tell compiler not to optimize reads
 
// Or use proper synchronization:
sem_wait(semaphore);  // Wait for signal
sem_post(semaphore);  // Signal other process

Corrected Code:

main() {
    char *pSm = NULL;
    int *pi;
    char *pStr;
    int *pg;
 
    pSm = GetSharedMemory(1024);
    memset(pSm, 0, 1024);
 
    pi = (int *)pSm;                              // Points to first int
    pg = (int *)(pSm + sizeof(int));              // Points to second int (FIXED)
    pStr = (char *)(pSm + 2*sizeof(int));         // Points to string area
 
    (*pg)++;  // Increment g to 1 (FIXED)
 
    if (fork() == 0) {
        // Child
        while (*pi == 0);  // Wait for parent signal
        printf("Str: <%s>, g: %d\n", pStr, *pg);
        (*pg)++;  // Increment g to 3 (FIXED)
        strcpy(pStr, "AA");
        *pi = 0;  // Signal parent
    } else {
        // Parent
        strcpy(pStr, "Message");
        (*pg)++;  // Increment g to 2 (FIXED)
        *pi = 1;  // Signal child
        while (*pi == 1);  // Wait for child signal
        printf("Str: <%s>, g: %d\n", pStr, *pg);
    }
}

Memory Layout (Corrected):

pSm (base address)
  |--[int: pi]-----------|  (offset 0, size 4 bytes)
  |--[int: pg]-----------|  (offset 4, size 4 bytes)  <- Was pointing way off!
  |--[string: pStr]------|  (offset 8, variable size)
  |                        |
  |__ rest of shared mem __|

Execution Trace (With Corrections):

1. Parent: (*pg)++;  -> *pg = 1

2. Parent: strcpy(pStr, "Message");
   pStr now: "Message\0"

3. Parent: (*pg)++;  -> *pg = 2

4. Parent: *pi = 1;  -> Signal child

5. Child: while (*pi == 0);  -> Exits loop (pi is 1)

6. Child: printf("Str: <%s>, g: %d\n", pStr, *pg);
   OUTPUT: "Str: <Message>, g: 2"

7. Child: (*pg)++;  -> *pg = 3

8. Child: strcpy(pStr, "AA");
   pStr now: "AA\0"

9. Child: *pi = 0;  -> Signal parent

10. Parent: while (*pi == 1);  -> Exits loop (pi is 0)

11. Parent: printf("Str: <%s>, g: %d\n", pStr, *pg);
    OUTPUT: "Str: <AA>, g: 3"

Corrected Output:

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

Key Lessons:

  1. Pointer Arithmetic: (int *)p + n adds n * sizeof(int) bytes, not n bytes
  2. Operator Precedence: *p++(*p)++
  3. Shared Memory Synchronization: Use proper mechanisms (semaphores, mutexes), not busy-waiting
  4. Volatile: Use volatile keyword for shared variables modified by other processes

Problem 11: Pipe /bin/wc Input and Output

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);
    }
 
    waitpid(pid, NULL, 0);
    printf("DONE\n");
    return 0;
}

Elaboration (by LLM):

This code implements file redirection for both input and output: wc < file.txt > out.txt

I/O Redirection Concept:

file.txt  ──dup2──→  stdin (fd 0)  ──execl──→  /bin/wc
                                          ↓
                                    Count & process
                                          ↓
out.txt   ──dup2──←  stdout (fd 1)

Child Process Setup:

// Redirect stdin
int fd = open("file.txt", O_RDONLY);
dup2(fd, 0);    // fd 0 (stdin) reads from file
close(fd);
 
// Redirect stdout
fd = open("out.txt", O_WRONLY);
dup2(fd, 1);    // fd 1 (stdout) writes to file
close(fd);
 
// Execute with redirected I/O
execl("/bin/wc", "wc", NULL);

How dup2() Works:

dup2(old_fd, new_fd) copies file descriptor:

  • Both old_fd and new_fd point to same file
  • Subsequent reads/writes to new_fd go to original file
  • Original fd can be closed to free descriptor

Execution Flow:

Child:
  1. Open file.txt for reading
  2. dup2(fd, 0) - stdin now reads from file.txt
  3. Open out.txt for writing
  4. dup2(fd, 1) - stdout now writes to out.txt
  5. execl("/bin/wc", "wc", NULL)

/bin/wc (inherits redirected descriptors):
  read(0, ...) reads from file.txt
  printf() writes to out.txt
  Counts: lines, words, characters

Parent:
  waitpid() waits for child
  prints "DONE\n"

Equivalent Shell Command:

/bin/wc < file.txt > out.txt

Example:

If file.txt contains: “Hello World\nFoo Bar”

After execution, out.txt contains: ” 2 4 21”

Issues in Original Code:

  1. Missing O_CREAT flag: If out.txt doesn’t exist, open() fails
  2. Missing O_TRUNC flag: If out.txt exists, data isn’t overwritten
  3. No error checking: open() or dup2() failures ignored
  4. Reuses variable fd: Confusing but works

Suggestions / Fixes:

  • When opening out.txt use O_WRONLY | O_CREAT | O_TRUNC and a mode argument, e.g. open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644) so the file is created/truncated as expected.
  • Add error checks for open() and dup2() and handle failures.
  • Use separate variables for clarity: fd_in and fd_out.

Problem 12: Pipe /bin/cat to /bin/wc

Implement a program that implements the following command by forking two child processes and tying the stand 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);
    }
 
    close(fd[0]);
    waitpid(pid, NULL, 0);
    printf("DONE\n");
    return 0;
}

Elaboration (by LLM):

This code implements the classic Unix pipeline cat file.txt | wc with two child processes connected by a pipe.

Pipeline Architecture:

cat file.txt         Pipe              wc
[reads file]  ─────→  [buffer]  ─────→  [counts]
  fd[1]                                 fd[0]

Process Creation Order:

  1. Create Pipe before forking any children

  2. Fork and exec /bin/cat:

    • Redirect stdout to pipe write end
    • Pass “file.txt” as argument
    • /bin/cat reads file and outputs to pipe
  3. Parent closes write end (only /bin/cat writes)

  4. Fork and exec /bin/wc:

    • Redirect stdin to pipe read end
    • /bin/wc reads from pipe
    • Outputs count to stdout (screen)
  5. Parent waits for wc to finish

File Descriptor Flow:

/bin/cat writes:
  stdout (fd 1) ─dup2─→ Pipe write end (fd[1])
            ↓
        file.txt content flows through pipe
            ↓
/bin/wc reads:
  stdin (fd 0) ─dup2─→ Pipe read end (fd[0])

Execution Timeline:

Time 1: Parent creates pipe
        Parent forks /bin/cat
        /bin/cat opens file.txt
        /bin/cat execs /bin/cat (inherits redirected fd 1)

Time 2: /bin/cat reads file.txt line by line
        /bin/cat writes to stdout (redirected to pipe)
        Each line goes into pipe buffer

Time 3: Parent closes pipe write end
        Parent forks /bin/wc
        /bin/wc execs /bin/wc (inherits redirected fd 0)

Time 4: /bin/wc reads from stdin (redirected from pipe)
        /bin/wc accumulates data in buffer
        /bin/cat finishes reading file
        /bin/cat exits
        Pipe write end closes (EOF signal)

Time 5: /bin/wc detects EOF
        /bin/wc processes accumulated data
        /bin/wc outputs: "<lines> <words> <chars>"
        /bin/wc exits

Time 6: Parent's waitpid() returns
        Parent prints "DONE\n"

Automatic Synchronization:

The pipe naturally synchronizes producer and consumer:

  • If /bin/cat is fast, data fills pipe buffer, /bin/cat blocks
  • If /bin/wc is fast, pipe empties, /bin/wc blocks on read
  • Flow control happens automatically

Data Quantity:

Example: file.txt has 100 lines, 500 words, 3000 characters

/bin/cat outputs all to pipe
/bin/wc reads from pipe and outputs:
  100 500 3000

Equivalent Shell Command:

cat file.txt | wc

This program achieves the same result by explicitly managing fork, exec, and pipes.

Why Both Processes?

Reuses existing tools rather than reimplementing:

  • /bin/cat: reads files (don’t reimplement)
  • /bin/wc: counts content (don’t reimplement)
  • Pipe: connects them (standard Unix pattern)

Issue in Original Code:

Parent only waits for wc via waitpid(pid, NULL, 0), leaving /bin/cat as zombie. Better:

pid_t pid1 = fork();  // /bin/cat
if (pid1 == 0) { ... }
 
pid_t pid2 = fork();  // /bin/wc
if (pid2 == 0) { ... }
 
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);

Suggestions / Fixes:

  • Parent should wait() for both children (/bin/cat and /bin/wc) to properly reap them.
  • Close unused fds in both parent and children as soon as they are not needed.
  • Add error checking for all system calls.

Problem 13: Parent as Proxy Between Two Children

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);
    }
 
    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);
    }
 
    close(fd2[0]);
    waitpid(pid, NULL, 0);
 
    printf("Parent done…\n");
    return 0;
}

Elaboration (by LLM):

This code demonstrates a relay pattern: C1 sends message to parent via pipe 1, parent reads and forwards to C2 via pipe 2.

Data Flow:

C1 writes        Parent relays       C2 outputs
"Hello\n"   ────→  receives & sends    ────→  "Hello\n"
 [fd1[1]]        (active relay)       [fd2[0]→stdout]

Key Difference from Earlier Problems:

Parent is active intermediary, not just creator/manager:

Problem 6:   Parent creates pipes for C1→C2
Problem 13:  Parent actively relays data between children

Execution Sequence:

1. Parent creates and writes to pipe 1:

int fd1[2];
pipe(fd1);
write(fd1[1], "Hello\n", 6);
close(fd1[1]);

2. Parent creates pipe 2:

int fd2[2];
pipe(fd2);

3. Parent forks C1:

if (fork() == 0) {
    // C1 relays from fd1 to fd2
    close(fd2[0]);
    char message[6];
    int len = read(fd1[0], message, 6);
    write(fd2[1], message, len);
    exit(0);
}

C1: reads from pipe1 (from parent), writes to pipe2 (to C2)

4. Parent cleanup:

close(fd1[0]);
close(fd2[1]);

5. Parent forks C2:

if ((pid = fork()) == 0) {
    char message[6];
    int len = read(fd2[0], message, 6);
    write(1, message, len);
    exit(0);
}

C2: reads from pipe2 (from C1), writes to stdout

6. Parent waits:

close(fd2[0]);
waitpid(pid, NULL, 0);
printf("Parent done…\n");

Complete Data Path:

Parent writes to fd1[1]:
  "Hello\n" ────→ [fd1 buffer]
                      ↓
C1 reads from fd1[0]:
  Gets "Hello\n"
  Writes to fd2[1]:
  "Hello\n" ────→ [fd2 buffer]
                      ↓
C2 reads from fd2[0]:
  Gets "Hello\n"
  Writes to stdout:
  Output: "Hello"

Timeline:

Time 0: Parent creates fd1, writes "Hello\n", closes fd1[1]
Time 1: Parent creates fd2
Time 2: Parent forks C1
        C1: reads from fd1 (data waiting, no block)
        C1: writes to fd2
        C1: exits
Time 3: Parent closes fd1[0], fd2[1]
Time 4: Parent forks C2
        C2: reads from fd2 (data from C1)
        C2: writes to stdout → "Hello"
        C2: exits
Time 5: Parent waits, then prints "Parent done…"

Why Three Stages?

Demonstrates multi-stage pipeline:

Source ──→ Stage1 ──→ Stage2 ──→ Output
  C1      pipe1     pipe2     C2

Real-world example:

cat file | grep pattern | wc -l

Synchronization Without Explicit Code:

No explicit synchronization needed:

  • Pipes block automatically
  • C1 blocks on read until parent writes
  • C2 blocks on read until C1 writes
  • EOF signals close the loop

Typical Output:

Hello
Parent done…

Issues in Original Code:

  1. Zombie Risk: Parent only waits for C2, C1 not explicitly reaped
  2. No Error Checking: System calls unvalidated
  3. Unclear C1 Logic: Code doesn’t show relay pattern clearly

Better Approach:

pid_t pid1 = fork();  // Save C1's PID
if (pid1 == 0) { /* C1 */ }
 
pid_t pid2 = fork();  // Save C2's PID
if (pid2 == 0) { /* C2 */ }
 
waitpid(pid1, NULL, 0);  // Reap both
waitpid(pid2, NULL, 0);

Suggestions / Fixes:

  • Wait for all children (C1 and C2) to ensure they’re reaped and avoid zombies.
  • Close any remaining unused file descriptors in parent and children promptly to prevent accidental blocking.
  • Add error checking for pipe(), fork(), read(), write().
  • Clarify C1’s logic to show it reads from fd1 and writes to fd2.

Problem 14: Compute Sum Using /bin/add

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.

LLM Solution

Create two pipes: P1 for parent→child (to send numbers), P2 for child→parent (to receive sum). Fork a child that redirects stdin to P1’s read end, stdout to P2’s write end, then execs /bin/add. Parent writes numbers to P1, reads sum from P2, and prints formatted result.

Elaboration:

This problem requires coordinating two pipes with stdin/stdout redirection:

Design Overview:

Parent Process                Child Process (/bin/add)
    |
    | Write numbers to P1
    |---write--> [P1] ---read---> stdin redirect
    |                              |
    |                         /bin/add processes
    |                              |
    | Read sum from P2         stdout redirect
    |<--read--- [P2] <--write---   |
    |
    Print formatted output

Step-by-Step Implementation:

1. Create Two Pipes:

int p1[2];  // For sending numbers to /bin/add
int p2[2];  // For receiving sum from /bin/add
 
pipe(p1);
pipe(p2);

2. Fork Child Process:

pid_t pid = fork();
 
if (pid == 0) {
    // Child: will run /bin/add

3. Child: Redirect I/O

Child must:

  • Redirect stdin (fd 0) to read end of P1
  • Redirect stdout (fd 1) to write end of P2
  • Close unused file descriptors
if (pid == 0) {
    // Redirect stdin to P1 read end
    dup2(p1[0], 0);
 
    // Redirect stdout to P2 write end
    dup2(p2[1], 1);
 
    // Close unused pipes
    close(p1[0]);
    close(p1[1]);
    close(p2[0]);
    close(p2[1]);
 
    // Execute /bin/add
    execl("/bin/add", "add", NULL);
    perror("execl failed");
    exit(1);
}

4. Parent: Close Unused Ends

// Parent doesn't read from P1, doesn't write to P2
close(p1[0]);  // Not using P1 read end
close(p2[1]);  // Not using P2 write end

5. Parent: Send Numbers

int nums[] = {1, 2, 3};
char buffer[256];
 
// Format numbers as space-separated string
int len = 0;
for (int i = 0; i < 3; i++) {
    len += sprintf(buffer + len, "%d ", nums[i]);
}
buffer[len-1] = '\n';  // Replace last space with newline
 
// Write to P1's write end
write(p1[1], buffer, len);
close(p1[1]);  // Signal end of input

6. Parent: Read Sum

char result[64];
int n = read(p2[0], result, sizeof(result) - 1);
result[n] = '\0';
 
// Parse and display
int sum;
sscanf(result, "%d", &sum);
printf("The sum of the numbers in the array: %d\n", sum);

7. Parent: Wait for Child

waitpid(pid, NULL, 0);

Complete Solution:

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>
 
int main(int argc, char *argv[]) {
    int p1[2], p2[2];
    pipe(p1);  // Parent to child
    pipe(p2);  // Child to parent
 
    pid_t pid = fork();
 
    if (pid == 0) {
        // Child: /bin/add
        dup2(p1[0], 0);   // stdin from P1
        dup2(p2[1], 1);   // stdout to P2
        close(p1[0]);
        close(p1[1]);
        close(p2[0]);
        close(p2[1]);
        execl("/bin/add", "add", NULL);
        perror("execl");
        exit(1);
    }
 
    // Parent
    close(p1[0]);
    close(p2[1]);
 
    // Send numbers to /bin/add
    int nums[] = {1, 2, 3};
    char buffer[256];
    int len = 0;
 
    for (int i = 0; i < 3; i++) {
        len += sprintf(buffer + len, "%d ", nums[i]);
    }
    buffer[len-1] = '\n';  // Replace last space
 
    write(p1[1], buffer, strlen(buffer));
    close(p1[1]);  // Signal EOF to /bin/add
 
    // Read sum from /bin/add
    char result[64];
    int n = read(p2[0], result, sizeof(result) - 1);
    result[n] = '\0';
 
    int sum;
    sscanf(result, "%d", &sum);
    printf("The sum of the numbers in the array: %d\n", sum);
 
    close(p2[0]);
    waitpid(pid, NULL, 0);
 
    return 0;
}

Key Points:

  1. P1 Direction: Parent writes, child reads

    • Parent closes P1 read end (p1[0])
    • Child closes P1 write end (p1[1])
  2. P2 Direction: Child writes, parent reads

    • Child closes P2 read end (p2[0])
    • Parent closes P2 write end (p2[1])
  3. EOF Signal: Close write end of P1 after sending numbers

    • /bin/add detects EOF and terminates
  4. Input Format: /bin/add expects space-separated integers

    • Format as: "1 2 3\n"
  5. Output Parsing: /bin/add outputs just the sum

    • Parse with sscanf()

Execution Flow:

Parent: Write "1 2 3\n" to P1
Child: Redirects stdin to P1
Child: Execs /bin/add

/bin/add:
  Reads stdin: "1 2 3\n"
  Computes: 1 + 2 + 3 = 6
  Writes stdout (redirected to P2): "6\n"

Parent: Reads "6\n" from P2
Parent: Prints: "The sum of the numbers in the array: 6"

Problem 15: Sum Random Numbers

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[0]);
        execl("./rand_nums", "rand_nums", NULL);
    }
 
    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;
}

Elaboration (by LLM):

This code combines child process output with parent input processing: child generates random numbers, parent reads and sums them.

Data Flow:

Child Process (/bin/rand_nums)    Pipe         Parent Process
printf("3 7 1 6 2")               (buffer)     Parent's stdin
(output to pipe via fd 1)  ─────→  ────────    (redirected)
                                                    ↓
                                              scanf() reads
                                              Sums numbers
                                              Prints result

Key Mechanism:

Parent’s stdin is redirected to pipe via dup2(fd[0], 0), so scanf() automatically reads from pipe without explicit read() calls.

Step-by-Step Execution:

1. Create Pipe:

int fd[2];
pipe(fd);

2. Fork Child:

if (fork() == 0) {
    dup2(fd[1], 1);    // Child's stdout goes to pipe
    close(fd[0]);
    execl("./rand_nums", "rand_nums", NULL);
}

3. Parent closes write end:

close(fd[1]);  // Only child writes

4. Parent redirects stdin:

dup2(fd[0], 0);  // Parent's stdin comes from pipe
close(fd[0]);

Now parent’s stdin is the pipe!

5. Parent reads via scanf():

int sum = 0;
for (int i = 0; i < 5; i++) {
    int num;
    scanf("%d", &num);  // Reads from stdin (which is pipe)
    sum += num;
}

6. Parent prints result:

printf("The sum of the 5 random numbers is %d\n", sum);

Process Timeline:

Time 0: Parent creates pipe
        Parent forks child

Time 1: Child execs /bin/rand_nums
        /bin/rand_nums generates 5 random numbers
        /bin/rand_nums outputs: "3 7 1 6 2\n"
        Output goes to pipe via fd 1
        /bin/rand_nums exits

Time 2: Parent redirects stdin to pipe
        Parent calls scanf() iteration 1:
          Reads from stdin (pipe): 3
          sum = 3
        Parent calls scanf() iteration 2:
          Reads from stdin (pipe): 7
          sum = 10
        Parent calls scanf() iteration 3:
          Reads from stdin (pipe): 1
          sum = 11
        Parent calls scanf() iteration 4:
          Reads from stdin (pipe): 6
          sum = 17
        Parent calls scanf() iteration 5:
          Reads from stdin (pipe): 2
          sum = 19

Time 3: Parent prints:
        "The sum of the 5 random numbers is 19"

Why Redirect stdin?

Instead of explicit read():

// Without redirect: must use read()
char buffer[100];
read(fd[0], buffer, sizeof(buffer));
sscanf(buffer, "%d %d %d...", ...);
 
// With redirect: scanf() handles it
dup2(fd[0], 0);  // stdin from pipe
scanf("%d", &num);  // Reads from stdin automatically

Using standard I/O (scanf) is cleaner than manual read().

Blocking Behavior:

Child generates:  3   → writes to pipe
                        ↓
Parent scanf() tries to read next number
                   → (child hasn't generated yet)
Parent blocks on read()
                   ← Child generates: 7
Parent scanf() reads:  7 ← unblocks

Pipe naturally synchronizes producer (child) and consumer (parent).

Data Format:

/bin/rand_nums outputs space-separated integers: 3 7 1 6 2

scanf(“%d”, &num) automatically:

  • Skips whitespace
  • Reads next integer
  • Perfect for pipe input

Typical Output:

If /bin/rand_nums generates: 3 7 1 6 2

Parent output:

The sum of the 5 random numbers is 19

Comparison with Problem 8:

AspectProblem 8Problem 15
DirectionParent→ChildChild→Parent
Child/bin/wc (processes)/bin/rand_nums (generates)
Parent RoleFeeds dataReads data
I/O Methodwrite() by charscanf() from stdin

Issues in Original Code:

  1. No waitpid(): Child becomes zombie
  2. No error checking: System calls unvalidated
  3. Hardcoded count: Assumes exactly 5 numbers
  4. Missing headers: Needs #include <sys/wait.h>

Suggestions / Fixes:

  • Parent should wait()/waitpid() for the child to ensure the child has terminated and to avoid zombies.
  • Add error checking for pipe(), fork(), and dup2() and handle failures.
  • Validate scanf() return value to confirm successful reads.
  • Add required headers: #include <unistd.h>, #include <stdio.h>, and #include <sys/wait.h>.