Unix Process Management

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. from malloc(). Grows upward toward higher addresses.
  • Memory-mapped region
    Shared libraries and files mapped with mmap(), 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
    The argv and envp strings, 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:

  1. Create the address space of the child process.
  2. Allocate a new PID.
  3. Create kernel process data structures, e.g. a process table entry.
  4. Copy kernel environment from the parent, e.g. scheduling priority.
  5. Initialize child process context: PID, PPID, zero CPU time.
  6. Copy memory regions (program, data, stack) from the parent.
  7. Acquire shared resources: open files, current working directory.
  8. Initialize the hardware context by copying registers from the parent.
  9. 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 the PATH environment variable for the executable, e.g. execlp().
  • e
    Takes an explicit envp array 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.
  • arg0 through argN
    Command line arguments, terminated by NULL.

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 of key=value strings, the environment.

Fork and Exec Together

Fork, exec, and wait A shell calls fork. The call splits execution into a parent path, where it returns the child PID and waits, and a child path, where it returns zero and exec replaces the shell with another program. Shell process fork returns child PID > 0 returns 0 Parent shell waitpid(child) Child copy execvp(args) collects exit status becomes someutil, then exits
One 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 SIGCHLD to 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 calls wait() 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 searches PATH and replaces its image with someutil.
  • The parent shell calls waitpid() and blocks until someutil terminates.
  • 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 nn iterations there are 2n2^n 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
└── P2
  • P0 is the original process.
  • P1 is the first child.
  • P3 is created by P1.
  • P2 is created by P0 after 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.

Written by October 30, 2026 8 min read
Was this helpful?