Unix identifies each process by a PID, an integer unique among running processes. Beyond the OS context tracked generically, Unix also tracks working directory and umask.
Operating system security applies these process boundaries to protection rings and reference monitors.
Refer stopped, zombie, and orphan for the Unix-specific additions to the generic process state model.
Process Fundamentals
Kernel Data Structures
Process Table and PCB
The kernel keeps 1 PCB per process. The process table is the collection of these PCBs. In Linux a PCB is a task_struct.
Process Memory Space
Each PCB points to the address space of its process. The address space is divided into regions, from low to high addresses:
- Text
The program’s machine code. Read-only and executable, shared between processes running the same program. - Initialized data
Global and static variables with a non-zero initial value. - BSS
Global and static variables initialized to zero. Not stored in the executable file, the kernel zero-fills them on load. - Heap
Dynamically allocated memory, e.g. frommalloc(). Grows upward toward higher addresses. - Memory-mapped region
Shared libraries and files mapped withmmap(), between the heap and the stack. - Stack
Stack frames holding local variables, arguments, and return addresses of each function call. Grows downward toward lower addresses. - Command line arguments and environment
Theargvandenvpstrings, placed at the top of the user address space.
The kernel is mapped into the upper part of the address space, but is not accessible from user mode.
PCB Contexts
The PCB preserves the state needed to stop 1 process and resume it later. Its 3 main contexts are:
- Process context
PID, parent PID, process state, scheduling priority, accounting data, and open-file references. - Hardware context
Program counter, stack pointer, general-purpose registers, and processor status word. - Memory-management context
Page-table base address, segment mappings, memory limits, and protection information.
Multiprogramming works because the scheduler saves the running process context in its PCB, then restores another process context. Each process therefore appears to own a processor and address space.
Process Lifecycle
Process Creation
Fork
#include <unistd.h>
int fork(void);
fork() creates a new child process that is a duplicate of the calling process’s executable image, same code, same address space layout.
- Data in the child is a copy of the parent’s, not shared.
- The child differs from the parent only in its PID, its PPID, and the return value of
fork(). - Return value of
fork()is 0 in child, positive in parent, negative on error.
Both parent and child continue executing the instructions after fork(). Since each has an independent memory copy, modifying a variable in one process does not affect the other’s copy.
Fork Implementation
Simplified steps:
- Create the address space of the child process.
- Allocate a new PID.
- Create kernel process data structures, e.g. a process table entry.
- Copy kernel environment from the parent, e.g. scheduling priority.
- Initialize child process context: PID, PPID, zero CPU time.
- Copy memory regions (program, data, stack) from the parent.
- Acquire shared resources: open files, current working directory.
- Initialize the hardware context by copying registers from the parent.
- Add the child to the scheduler’s ready queue.
Linux additionally provides clone(), a more general primitive than fork() that allows partial resource sharing between parent and child, used to implement threads.
Exec
fork() alone only duplicates the calling process. To run a different program, use the exec() family, a group of 6 related system calls sharing a naming convention.
l
Arguments passed as a list, e.g.execl().v
Arguments passed as a vector (array), e.g.execv().p
Searches thePATHenvironment variable for the executable, e.g.execlp().e
Takes an explicitenvparray instead of inheriting the caller’s environment, e.g.execle().
#include <unistd.h>
int execl(const char *path, const char *arg0, ..., const char *argN, NULL);
path
Location of the executable.arg0throughargN
Command line arguments, terminated byNULL.
execl() replaces the current process image with a new one. Code and data are replaced, but the PID and other state such as open files are preserved.
Every Unix executable is invoked as if main() were called:
int main(int argc, char *argv[], char *envp[])
argc
Number of command line arguments, including the program name.argv
NULL-terminated array of C strings,argv[0]is the program name.envp
NULL-terminated array ofkey=valuestrings, the environment.
Fork and Exec Together
fork() call creates 2 continuations. The return value tells
each continuation which path it is running.
pid_t pid = fork();
if (pid < 0) {
// fork failed, no child created
handle_error();
} else if (pid == 0) {
// child process
execl(path, arg0, ..., NULL);
// only reached if exec fails
exit(1);
} else {
// parent process, pid is the child's PID
wait(&status);
}
Termination and Synchronization
Exit
#include <stdlib.h>
void exit(int status);
Non-zero status indicates an error or abnormal termination.
_exit() (the system call) performs immediate termination:
- closes all open file descriptors
- reparents any children to
init - sends
SIGCHLDto the parent.
exit() (the C library function) flushes and closes open C stdio streams, calls registered exit handlers, then invokes _exit(). Never returns.
Returning from main() implicitly calls exit().
Wait
#include <sys/types.h>
#include <sys/wait.h>
int wait(int *status);
Blocks the calling (parent) process until at least 1 child terminates. Returns the PID of the terminated child. Cleans up the remaining child resources not released by exit().
The status parameter, passed by address, stores the child’s exit status. Pass NULL if not needed.
If there are no children, returns immediately with -1 and sets errno to ECHILD.
Variants:
waitpid()
Wait for a specific child process.waitid()
Wait for any child process to change status.
Signals
int kill(pid_t pid, int sig);
Sends a signal to a process, the Unix analog of a hardware interrupt, routed through the kernel.
SIGSTOP
Stop the process.SIGTERM
Request termination. Can be caught, blocked, ignored. Lets the process clean up before exiting.SIGKILL
Kill the process. Cannot be caught, blocked, or ignored. The kernel terminates the process immediately.SIGCONT
Restart a stopped process.SIGCHLD
Sent by the kernel to the parent when a child terminates, stops, or continues. Ignored by default. A parent can install a handler that callswait()on receiving it, reaping terminated children without blocking. Children that are never reaped remain zombies.
Command Execution
Building a Shell
A console application that runs utilities such as someutil is a shell. It reads a command line, then uses fork() and exec() to run the utility in a child process.
Shell Loop
loop forever
print prompt
line = read_line()
if line is empty
continue
args = split(line)
if args[0] == "exit"
break
pid = fork()
if pid < 0
print "fork failed"
else if pid == 0
execvp(args[0], args)
print "command not found"
exit(1)
else
waitpid(pid, &status, 0)
Command Trace
Command trace for running someutil with the arguments -v and data.txt:
$ someutil -v data.txt
- The shell reads the line and splits it into
args = ["someutil", "-v", "data.txt", NULL]. - The shell calls
fork(). The child is a copy of the shell. - The child calls
execvp("someutil", args). It searchesPATHand replaces its image withsomeutil. - The parent shell calls
waitpid()and blocks untilsomeutilterminates. - The shell collects the exit status, prints the prompt, and reads the next command.
Failure Modes
Process Misbehavior
Fork Bomb
A process that calls fork() in an infinite loop is a fork bomb. Each child continues the same loop, so the number of processes doubles every iteration.
while true
fork()
- After iterations there are processes.
- The process table fills, so no new process can be created.
- Memory and CPU time are consumed by the running copies, so the system stops responding.
Limiting the number of processes per user (ulimit -u) contains this.
Fork Trace and Zombies
pid_t first = fork();
if (first == 0) {
fork();
printf("Hi!\n");
exit(0);
}
sleep(1);
printf("Hey!\n");
fork();
printf("Hello!!\n");
Solution Reveal Hide
The process tree has 4 processes:
P0
├── P1
│ └── P3
└── P2P0is the original process.P1is the first child.P3is created byP1.P2is created byP0after the sleep.
Hi! is printed 2 times. Hey! is printed 1 time. Hello!! is printed 2 times. The Hi! output precedes Hey! because the original parent sleeps. The 2 Hello!! lines have no fixed order.
P1 terminates while P0 sleeps. The kernel retains P1’s PCB and exit status until P0 calls wait() or waitpid(). P1 is a zombie during that interval. Adding waitpid() for every child, or installing a SIGCHLD handler that reaps children, prevents unreaped zombies.