CSCI 340 - Lecture: Process Termination, IPC, and Pipes (03/26)
This lecture covers fundamental concepts in operating systems process management, including:
- Process termination and exit status handling
- Zombie processes and the process lifecycle
- Parent-child process relationships and reparenting
- Inter-process communication (IPC) mechanisms
- Shared memory and pipes implementation
Process Trees and Signal Handling
Section titled “Process Trees and Signal Handling”Overview
Section titled “Overview”Every process has a parent, creating a hierarchical tree structure of processes in the system. When a signal such as Ctrl+C is delivered to a process, it affects not only that process but also all other processes in the foreground process group.
For example, when you press Ctrl+C to terminate the bash shell, the signal is delivered to bash and to every child process in the foreground, terminating all of them together.
Process Tree Diagram
Section titled “Process Tree Diagram”init (PID 1)├── bash (shell process)│ ├── program1│ └── program2└── other_services └── ...The reason for this hierarchical structure is to handle signal delivery efficiently: when you want to terminate a bash session, you terminate the entire process tree associated with that session.
Key System Calls for Process Management
Section titled “Key System Calls for Process Management”There are four essential system calls you must know for process management:
- fork() - To create a clone of the current process (duplicates the entire process)
- execve() - To load and execute a new program on top of an existing process (overlay execution)
- waitpid() - To wait for the termination of a child process and retrieve its exit status
- exit() - To terminate the current process and return an exit status
These four system calls form the foundation of all process creation, execution, and management in Unix-like operating systems.
Process Termination and Exit Status
Section titled “Process Termination and Exit Status”What Happens During Process Termination
Section titled “What Happens During Process Termination”When a process calls exit():
- The process’s memory space is deallocated (reclaimed by the operating system)
- All open file descriptors and other resources are closed
- However, the Process Control Block (PCB) is not deleted immediately
- The process transitions to the zombie state
The Exit Status
Section titled “The Exit Status”The exit() system call takes an exit status as a parameter. This status is:
- Typically an integer value passed as a parameter to
exit() - By convention:
0indicates successful termination - Non-zero values indicate errors or abnormal conditions
- Stored in the PCB (not lost when memory is freed)
Exit Status Convention
Section titled “Exit Status Convention”- Exit Status 0: Successful execution - no errors occurred
- Non-zero Exit Status: An error occurred during execution
This is how bash scripts work: they check the exit status of commands to determine the next action. For example, in a bash script, you might write:
if command_that_might_fail; then # Command succeeded (exit status 0) echo "Success!"else # Command failed (non-zero exit status) echo "Error!"fiProcess State Transitions
Section titled “Process State Transitions”Running → exit() called → Zombie state → waitpid() called → Process cleaned up ↓ PCB still exists Exit status storedZombie Processes
Section titled “Zombie Processes”What is a Zombie Process?
Section titled “What is a Zombie Process?”A zombie process is a process that has terminated but whose Process Control Block (PCB) still exists in the system. Specifically:
- The process is no longer active or running
- The PCB remains in the process table
- The exit status is stored in the PCB
- The PCB persists until the parent process calls
waitpid()orwait()to retrieve the exit status and clean it up
Why Zombie Processes Exist
Section titled “Why Zombie Processes Exist”The zombie state exists because the operating system must preserve the exit status information until the parent process retrieves it. The parent needs this status to:
- Determine how the child process terminated
- Take appropriate actions based on success or failure
- Implement synchronization between parent and child processes
The PCB cannot be immediately freed because the operating system guarantees that when the parent calls waitpid(), it will be able to retrieve the child’s exit status.
Identifying Zombie Processes
Section titled “Identifying Zombie Processes”Zombie processes appear in the process list with a <defunct> label. You can see this when listing processes with commands like ps. The defunct keyword indicates that a process is in the zombie state.
Zombie Process Example
Section titled “Zombie Process Example”Consider a parent process that creates four child processes:
Parent Process (PID 698) - calls fork() 4 times├── Child 1 (PID 699) → prints message → calls exit(0) → becomes zombie├── Child 2 (PID 700) → prints message → calls exit(1) → becomes zombie├── Child 3 (PID 701) → prints message → calls exit(2) → becomes zombie└── Child 4 (PID 702) → prints message → calls exit(3) → becomes zombie
[ Parent is now waiting for keyboard input (before calling waitpid) ]At this point:
- All 4 children have terminated and are in zombie state
- Their PCBs are stored in the process table with exit statuses 0, 1, 2, 3
- All are listed as “defunct” in the process list
- The parent can call
waitpid()to retrieve each child’s exit status and clean them up
This example demonstrates that multiple zombie processes can exist simultaneously until the parent explicitly waits for them.
Parent-Child Process Relationships
Section titled “Parent-Child Process Relationships”The Parent-Child Relationship
Section titled “The Parent-Child Relationship”Every process except the initial process has a parent:
- The parent process created the child via
fork() - The parent is responsible for calling
wait()orwaitpid()on the child - The child’s exit status is provided to the parent through the
waitpid()system call
Process Reparenting
Section titled “Process Reparenting”When a parent process terminates without waiting for its children, the OS automatically reassigns those children to the init process (PID 1). This process is called reparenting.
Why Reparenting is Important
Section titled “Why Reparenting is Important”Reparenting ensures that:
- No child process is left without a parent
- The
initprocess acts as the “parent of last resort” - Zombie processes can eventually be cleaned up (init will call
waitpid()on them) - The system maintains process tree consistency
- Orphaned processes can complete their execution with a proper parent
The init Process
Section titled “The init Process”The init process (PID 1) is special:
- Started by the kernel at boot time
- Serves as the ancestor of all other user processes
- Periodically calls
waitpid()on all of its children - Ensures that no zombie processes accumulate indefinitely in the system
- Handles clean shutdown of all user processes during system shutdown
Without reparenting and init, orphaned child processes would remain in the zombie state indefinitely, wasting kernel resources.
Inter-Process Communication (IPC)
Section titled “Inter-Process Communication (IPC)”Overview
Section titled “Overview”Inter-Process Communication allows separate processes to exchange data and coordinate their actions. The operating system provides several IPC mechanisms:
- Shared Memory - Direct memory regions accessible by multiple processes
- Pipes - Unidirectional communication channels between related processes
- Message Queues - Queued message passing between arbitrary processes
- Sockets - Network-based communication
- Signals - Asynchronous notifications
This lecture focuses on shared memory and pipes, both of which allow processes running on the same machine to communicate.
Shared Memory Implementation
Section titled “Shared Memory Implementation”Basic Concept
Section titled “Basic Concept”Shared memory allows multiple processes to access the same physical memory region. This is one of the fastest IPC mechanisms because it avoids copying data between processes—the data exists in a single location that multiple processes can access directly.
How Shared Memory Works
Section titled “How Shared Memory Works”Each process in the system has its own address space. Shared memory creates a region that is mapped into multiple process address spaces:
Process A Process B┌──────────────────┐ ┌──────────────────┐│ Address Space A │ │ Address Space B │├──────────────────┤ ├──────────────────┤│ Private Memory │ │ Private Memory │├──────────────────┤ ├──────────────────┤│ │ │ ││ Shared Memory │◄──────►│ Shared Memory ││ (1 KB) │ │ (1 KB) ││ │ │ │├──────────────────┤ ├──────────────────┤│ More Private │ │ More Private │└──────────────────┘ └──────────────────┘ │ ▲ └────────────────────────────┘ Both point to same physical memory regionCreating and Using Shared Memory
Section titled “Creating and Using Shared Memory”The typical workflow for shared memory:
- Create: One process creates the shared memory segment with a specified size and a unique identifier (e.g., 1024 bytes with ID 100)
- Attach: Each process that wants to use it calls attach, which maps the shared memory into its address space
- Use: Processes read from and write to the shared memory
- Detach: When done, processes detach the shared memory
- Delete: When no longer needed, someone must explicitly delete the shared memory segment
Example: Shared Memory Reader-Writer
Section titled “Example: Shared Memory Reader-Writer”A common pattern is a producer-consumer model with shared memory:
- Writer Process: Creates or attaches to shared memory, writes messages into it
- Reader Process(es): Attach to the same shared memory, read messages from it
The message format might include:
- A “ready” flag (e.g., 4 bytes) to indicate if a message is available
- A message buffer (e.g., 1020 bytes) containing the actual data
Reader’s loop:
while (ready_flag is 0): sleep for 50 milliseconds // busy-wait with timeout
// Message is ready, process itread_message()set_ready_flag(0) // Clear for next messageWriter’s operation:
wait_for_user_input()write_message_to_shared_memory()set_ready_flag(1) // Signal to readerKey Characteristics of Shared Memory
Section titled “Key Characteristics of Shared Memory”- Fast: No copying needed—processes access the same physical memory
- Synchronization Required: Multiple processes accessing the same data need coordination (otherwise data corruption can occur)
- Persistence: Data persists until explicitly deleted by administrator or cleanup process
- System Calls: Primary calls are
shmget()(create),shmat()(attach),shmdt()(detach),shmctl()(control) - Same Machine Only: Both processes must be running on the same operating system
Producer-Consumer Pattern
Section titled “Producer-Consumer Pattern”Shared memory enables the producer-consumer pattern:
- Producers write data into the shared memory
- Consumers read data from the shared memory
- Can scale to N producers and M consumers
Basic Concept
Section titled “Basic Concept”A pipe is a unidirectional communication mechanism that allows one process to send data to another process through a byte stream. Pipes are fundamental to Unix process communication.
Pipes characteristics:
- Unidirectional: Data flows in one direction only
- Sequential/FIFO: Data is read in the order it was written (First In, First Out)
- Buffered: The kernel maintains a buffer for pipe data
- Related Processes: Typically used between parent and child processes
Pipe Structure
Section titled “Pipe Structure”Writer Process Reader Process ↓ ↑ │ │ └──────────────[PIPE]◄────────┘ kernel buffer (typically 4KB-64KB)Data enters at the write end and exits at the read end in FIFO order.
Creating Pipes
Section titled “Creating Pipes”The pipe() system call creates a pipe and returns two file descriptors in an array:
int fd[2];pipe(fd);After calling pipe():
fd[0]= file descriptor for the read end of the pipefd[1]= file descriptor for the write end of the pipe
Using Pipes with fork()
Section titled “Using Pipes with fork()”The typical pattern for inter-process communication via pipes:
- Create the pipe: Parent calls
pipe()to create the pipe - Fork: Parent calls
fork()to create a child process - Inherit: Child inherits copies of both file descriptors (pointing to the same pipe)
- Close unnecessary ends:
- Parent closes the read end (
close(fd[0])) if it only writes - Child closes the write end (
close(fd[1])) if it only reads
- Parent closes the read end (
- Communicate: Parent and child use
write()andread()with the remaining file descriptors - Close when done: Each process closes its file descriptors when finished
- Wait: Parent calls
waitpid()to clean up the child process
Message Boundaries and Pipe Data
Section titled “Message Boundaries and Pipe Data”Important: Pipes are byte streams with no message boundaries. This means:
- If the writer sends “hello” and then “world”, the receiver sees “helloworld”
- The receiver has no way to know where one message ends and another begins
- The receiver must implement its own message framing if discrete messages are needed
For example:
Writer sends: "hello" + "world" ↓ [Pipe buffer] ↓Reader reads: "helloworld" (no boundaries preserved)Closing Pipes
Section titled “Closing Pipes”Proper pipe closure is essential for correct process synchronization:
- Closing write end: When a writer closes its write file descriptor, any reader on the pipe sees EOF (end-of-file) when it has read all available data
- Closing read end: When a reader closes its read file descriptor, any writer on the pipe receives a SIGPIPE signal (broken pipe)
- Automatic cleanup: The kernel automatically removes the pipe when the last file descriptor referencing it is closed
Important Pipe Details
Section titled “Important Pipe Details”- Buffering: The kernel maintains a buffer for pipe data (typically 4KB to 64KB depending on the OS)
- Blocking I/O: If a writer writes more data than the buffer can hold, the
write()call blocks until the reader reads some data - Read Blocking: If a reader tries to read from an empty pipe, the
read()call blocks until data is available - Atomicity: Writes smaller than the pipe buffer are guaranteed to be atomic (not interleaved with writes from other processes)
- Related Processes Only: Pipes can only be used between related processes (those with a parent-child relationship via fork)
Pipe Characteristics vs. Message Queues
Section titled “Pipe Characteristics vs. Message Queues”Pipes are simpler and more efficient than message queues, but with trade-offs:
| Characteristic | Pipes | Message Queues |
|---|---|---|
| Message Boundaries | No (byte stream) | Yes (discrete messages) |
| Direction | Unidirectional | Bidirectional |
| Related Processes | Yes (parent-child via fork) | No (any processes) |
| Max Message Size | Pipe buffer (~4-64KB) | OS-defined (often 8KB) |
| Cleanup | Automatic | Explicit required |
Practical Examples and Implementation
Section titled “Practical Examples and Implementation”Pattern 1: Parent Writes, Child Reads
Section titled “Pattern 1: Parent Writes, Child Reads”pipe(fd); // Create pipe, fd[0]=read, fd[1]=writefork(); // Create child
// Parent process:close(fd[0]); // Close read end (not needed)write(fd[1], ...); // Write data to child
// Child process:close(fd[1]); // Close write end (not needed)read(fd[0], ...); // Read data from parentPattern 2: Child Writes, Parent Reads
Section titled “Pattern 2: Child Writes, Parent Reads”pipe(fd); // Create pipefork(); // Create child
// Parent process:close(fd[1]); // Close write end (not needed)read(fd[0], ...); // Read data from child
// Child process:close(fd[0]); // Close read end (not needed)write(fd[1], ...); // Write data to parentPattern 3: Two-Way Communication (Two Pipes Needed)
Section titled “Pattern 3: Two-Way Communication (Two Pipes Needed)”pipe(pipe1); // Parent writes, child readspipe(pipe2); // Child writes, parent readsfork();
// Parent:close(pipe1[0]); close(pipe2[1]);write(pipe1[1], ...); // Send to childread(pipe2[0], ...); // Receive from child
// Child:close(pipe1[1]); close(pipe2[0]);read(pipe1[0], ...); // Receive from parentwrite(pipe2[1], ...); // Send to parentShell Pipeline Example
Section titled “Shell Pipeline Example”When you run a shell command like:
cat file.txt | grep "pattern"The shell internally does:
- Creates a pipe
- Forks twice: one process for
cat, one forgrep - Connects the write end of the pipe to
cat’s stdout - Connects the read end of the pipe to
grep’s stdin - Waits for both processes to complete
Summary and Key Takeaways
Section titled “Summary and Key Takeaways”Process Lifecycle
Section titled “Process Lifecycle”Process Creation → Running → exit() → Zombie → waitpid() → Cleaned Up ↓ Exit status storedProcess Management Concepts
Section titled “Process Management Concepts”- Exit Status: Essential for parent-child synchronization and error reporting
- Parent Responsibility: Parents must call
waitpid()on their children to avoid leaving zombies - Reparenting: Orphaned children are automatically reassigned to
initfor cleanup - Zombie Prevention: A properly written program always waits for its children
IPC Mechanisms
Section titled “IPC Mechanisms”Shared Memory:
- Use when you need fast access to shared data
- Requires explicit synchronization
- For processes on the same machine
- Data persists until explicitly deleted
Pipes:
- Use for unidirectional communication between parent and child
- Simple and automatic cleanup
- Suitable for producer-consumer patterns
- Message boundaries must be handled by application
- Blocking I/O for synchronization
System Call Summary
Section titled “System Call Summary”fork(): Create a child processexecve(): Load a new programexit(): Terminate with exit statuswaitpid(): Wait for child and retrieve exit statuspipe(): Create a unidirectional communication channelshmget(): Create/access shared memoryshmat(): Attach shared memoryshmdt(): Detach shared memory