Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Fork, a small red daemon, holds a fork. Beside it stands a smaller copy of itself, just created. Tags give their numbers: 500 for the parent and 501 for the child.

Processes

A process is a program in execution, together with everything the kernel keeps for it: its memory, its open files, its identity for permission checks, and its place in the scheduler. The kernel represents each process by a record, struct task_struct, and identifies it by a number, the process ID or pid.

In this chapter, we will follow a process through its whole life: how it is created, how it comes to run a different program, how it ends, and how the process that created it learns the result. We will read the kernel’s record of a process field by field and find the same information under /proc. Then we will write procfacts, which measures each step on a running kernel, and proctable, a model of the kernel’s process table.

This chapter covers

  • what happens when a shell runs a command, step by step
  • struct task_struct: the kernel’s record of a process
  • process states, and how to read them from /proc
  • fork: what the child copies, what it shares, and what is new
  • exec: what is replaced and what survives
  • exit and wait, and the integer that carries the result
  • zombies, orphans, and the process that adopts them
  • how the kernel chooses a pid
  • threads, and the difference between a process and a task

A shell runs a command

When we type ls in a shell, 4 system calls do the work, 2 in the shell and 2 in a new process.

  1. The shell calls fork. The kernel creates a second process that is a copy of the shell: the same program, the same memory contents, the same open files. The new process is the child, and the shell is its parent. fork returns in both processes. In the parent it returns the pid of the child, and in the child it returns 0, which is how each of them knows which one it is.
  2. The child calls exec with the path of the ls program. The kernel discards the memory of the child, loads ls in its place, and starts it. The pid does not change. It is the same process running a different program.
  3. ls does its work and calls exit with a status, 0 for success.
  4. Meanwhile the parent has called wait, which puts it to sleep until a child ends. wait now returns the pid of the child and its status, and the shell prints the next prompt.
A shell runs ls, in 2 columns with time running downward. The parent, pid 500, reads the line and calls fork, which returns the child's pid. The child, pid 501, is a copy of the shell in which fork returns 0. The parent calls wait and sleeps. The child calls exec and its program is replaced by ls, which runs and writes to descriptor 1, then calls exit and becomes a zombie. wait returns in the parent with the pid and status of the child, and the shell prints the next prompt.
Figure 3.1 The 4 calls behind a command: fork and wait in the parent, exec and exit in the child.

Creating a process and loading a program are separate calls, and that is deliberate. Between fork and exec the child is still running the shell’s code, with the shell’s descriptors. The shell uses that interval to prepare the environment of the new program: it redirects descriptors with dup2, changes the working directory, or adjusts signal handling. Then it calls exec. The new program starts in the prepared environment and contains no code for the preparation. The redirections of chapter 2 are performed in this interval, and chapter 4 builds a shell that does it.

The kernel’s record of a process

Every process has a struct task_struct. It is one of the largest structures in the kernel, about 700 lines in include/linux/sched.h. These are the fields that this chapter and the following chapters use, as they appear in Linux 5.15.

struct task_struct {
        unsigned int __state;                  /* running, sleeping, stopped, ... */
        struct sched_entity se;                /* scheduling data, chapter 6 */
        struct mm_struct *mm;                  /* the address space, chapter 7 */
        int exit_state;                        /* EXIT_ZOMBIE after exit */
        int exit_code;                         /* the status for the parent */
        pid_t pid;                             /* the number of this task */
        pid_t tgid;                            /* the number of its process */
        struct task_struct *real_parent;       /* the process that created it */
        struct list_head children;             /* its children */
        struct list_head sibling;              /* its place in the parent's list */
        const struct cred *cred;               /* user and group, capabilities */
        char comm[TASK_COMM_LEN];              /* the name of the program */
        struct files_struct *files;            /* the descriptor table, chapter 2 */
        struct nsproxy *nsproxy;               /* its namespaces, chapter 11 */
        struct signal_struct *signal;          /* signal state, chapter 5 */
        struct sighand_struct *sighand;        /* signal handlers, chapter 5 */
        struct css_set *cgroups;               /* its control groups, chapter 12 */
        /* ... */
};
FieldMeaning
pidThe number by which system calls and /proc name the task.
real_parentThe process that is told when this one ends and that collects its status.
children, siblingThe children of a process form a linked list. children is the head of the list in the parent, and sibling links a child into it.
__stateWhether the task is runnable, sleeping or stopped.
exit_state, exit_codeSet when the task ends, and kept until the parent has read them.
commThe name of the program, at most 15 characters. ps and /proc/<pid>/comm show it.
credThe user and group the process runs as. The kernel checks these when the process opens a file or sends a signal.
mm, files, signal, nsproxy, cgroupsPointers to the other resources of the process. Each is the subject of a later chapter.
struct task_struct has the fields pid and tgid, __state, exit_state, exit_code, real_parent, children and comm, and pointers to other structures: mm to struct mm_struct, the address space of chapter 7; files to struct files_struct, the descriptor table of chapter 2; signal and sighand to the signal state of chapter 5; se, the struct sched_entity of chapter 6; nsproxy to the namespaces of chapter 11; and cgroups to the control groups of chapter 12.
Figure 3.2 A process is a struct task_struct and the structures it points to.

The resources are separate structures reached through pointers, and this is what makes fork and threads possible with the same code. A new task can receive a copy of a resource or a pointer to the same one, resource by resource.

Reading the record from /proc

The kernel publishes the record of every process as text files in /proc/<pid>. This is a sleep started from a shell with pid 565533:

$ ps -o pid,ppid,stat,comm
    PID    PPID STAT COMMAND
 565533  565532 S    sh
 565534  565533 S    sleep
$ grep -E 'Name|State|Tgid|Pid|PPid|Threads' /proc/565534/status
Name:	sleep
State:	S (sleeping)
Tgid:	565534
Pid:	565534
PPid:	565533
Threads:	1
The lines of /proc/pid/status explained. Name is the program name. State S means sleeping, waiting for an event. Tgid and Pid are the process number and the thread number, equal for a process with a single thread. PPid is the parent. Threads is the number of tasks that share the address space.
Figure 3.3 The status file of the sleep process, line by line.

Name is comm, Pid and Tgid are pid and tgid, and PPid is the pid of real_parent. State is a letter derived from __state and exit_state.

LetterStateMeaning
RrunningThe task is on a CPU or waiting in the run queue for one.
SsleepingThe task waits for an event, such as input or a timer, and a signal can wake it.
Duninterruptible sleepThe task waits for an event, usually disk I/O, and signals are held back until it arrives.
TstoppedThe task was stopped by a signal such as SIGSTOP and continues on SIGCONT.
ZzombieThe task has ended, and its parent has not yet collected its status.

Most processes on a system are in state S at any moment. A process in state R uses CPU time, and chapter 6 describes how the kernel divides the CPUs among them.

fork

fork creates a new struct task_struct and decides, for each resource of the parent, whether the child gets a copy.

ResourceIn the child
pidnew
parentthe process that called fork
memorya copy, made lazily: the pages are shared until one side writes (chapter 7)
descriptor tablea copy; the entries refer to the same open files (chapter 2)
signal handlersa copy
pending signalsnone
working directory, user and groupthe same
namespaces and control groupthe same
What fork gives the child, resource by resource. The pid is new: 500 in the parent, 501 in the child. The memory is the same pages, shared until one side writes. The descriptor table is a copy that refers to the same open files. The signal handlers are a copy and independent afterwards. The working directory is copied.
Figure 3.4 The parent and the child directly after fork.

A way to remember the table: the child starts as a photograph of the parent, taken at the moment of the call. Everything that is true of the parent at that moment is true of the child, with 2 differences that let them tell each other apart: the pid, and the return value of fork. From then on they are separate processes, and a change in one is invisible to the other, except through the open files they still share.

The copy of the memory is what makes fork look expensive and why it is cheap in practice. The kernel copies the page table and leaves the pages themselves shared and read-only. A page is copied only when one of the processes writes to it. A child that calls exec immediately, the usual case, discards the address space before it has written to more than a few pages.

On Linux, fork is a special case of a more general call, clone, which takes a flag for each resource: with the flag, the child shares the parent’s structure, and without it the child gets a copy. fork is clone with no sharing flags.

exec

exec keeps the task_struct and replaces what the process runs. The kernel reads the header of the program file, which is in the ELF format on Linux, to learn which parts of the file to map into memory and at which address execution starts. It builds a new address space, places the arguments and the environment variables on a new stack, and sets the processor to the entry point.

Kept across execReplaced or reset
pid, parent, childrenthe address space: code, data, heap and stack
open descriptors without the close-on-exec flagdescriptors with the flag are closed
working directory, user and groupsignal handlers return to the default action
ignored signals stay ignoredcomm becomes the name of the new program
namespaces and control group

The table explains why the preparation between fork and exec works. Everything the shell arranges in that interval is in the left column.

exit and wait

A process ends by calling exit with a status, or because a signal ends it. In both cases the kernel releases its memory and closes its descriptors at once. The task_struct remains, with exit_state set to EXIT_ZOMBIE and the result in exit_code, because the parent has not yet asked for the result. A process in this condition is a zombie. It uses no memory and no CPU. It occupies a task_struct and a pid.

The parent collects the result with wait or waitpid, and the kernel then frees the task_struct. This is called reaping the child. wait delivers both kinds of ending in a single integer.

The integer that wait returns. For a process ended by exit(7), bits 15 to 8 hold the exit status 7 and bits 6 to 0, the signal number, are 0. For a process ended by SIGTERM, bits 15 to 8 are 0 and bits 6 to 0 hold the signal number 15. Bit 7 records whether a core dump was written.
Figure 3.5 The layout of the status that wait returns.

The exit status has 8 bits. A program that calls exit(300) is reported with status 44, the low 8 bits of 300. A shell folds the integer into its own $?: the exit status itself, or 128 + n for a process ended by signal n, so a process ended by SIGTERM, signal 15, appears as 143.

A parent that does not call wait leaves a zombie for each child that ends. The zombies disappear when the parent itself ends, for the reason given next.

A zombie cannot be ended with kill. It has already ended, and only its parent can remove it, by calling wait.

Orphans

A zombie can be reaped only by its parent, so every process needs a living parent. When a process ends before its children, the children are orphans, and the kernel gives them a new parent at once. The new parent is process 1, the first process of the system, unless a closer ancestor has asked for the role: a process can declare itself a subreaper with prctl(PR_SET_CHILD_SUBREAPER), and it then adopts the orphans among its descendants. Process 1 and every subreaper call wait in a loop for exactly this purpose.

The life of a process, top to bottom. Fork gives the parent a child with a new pid. The child runs; exec replaces its program and keeps the pid, the parent and the descriptors. After exit or a signal the process is a zombie: its memory and descriptors are released and its table entry keeps the exit status. When the parent calls wait, the entry is removed and the pid can be given out again.
Figure 3.6 The task_struct of a process, from fork to wait.

Processes and threads

task_struct has 2 numbers, pid and tgid, because the kernel schedules tasks, and a process can consist of several. A thread is a task that shares the address space, the descriptor table and the signal handlers of another task. It is created by clone with the sharing flags set, where fork sets none.

All threads of a process have the same tgid, the thread group ID, and each has its own pid. The number that getpid returns, and that we call the pid of a process, is the tgid. The number of each thread is visible under /proc/<pid>/task, and the Threads line of /proc/<pid>/status counts them. The sleep above has 1. A Go program always has several, because the Go runtime starts threads of its own to run goroutines.

Measuring processes

procfacts starts child processes with the standard libraries, os/exec in Go and std::process in Rust. Both perform the creation of the child and the exec in a single call, since a program with several threads cannot safely run arbitrary code between fork and exec. The Rust version uses the nix crate with the signal feature to send a signal.

$ cargo new procfacts
$ cargo add -p procfacts nix --features signal

field reads a field of /proc/<pid>/stat, a single line with the same information as status in a fixed order. Field 3 is the state letter and field 4 is the parent’s pid.

Listing 3.1 The imports of procfacts, in Go. code/go/procfacts/main.go

import (
	"errors"
	"fmt"
	"log"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"syscall"
	"time"
)

Listing 3.2 The imports of procfacts, in Rust. code/rust/procfacts/src/main.rs

use std::fs;
use std::io;
use std::os::unix::process::ExitStatusExt;
use std::path::Path;
use std::process::{self, Command};
use std::thread;
use std::time::Duration;

use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;

Listing 3.3 Reading a field of a process's record, in Go. code/go/procfacts/main.go

// field returns one field of /proc/<pid>/stat, counted from 1. The second
// field, the command name, is in parentheses and may contain spaces, so the
// fields after it are counted from the closing parenthesis.
func field(pid, n int) (string, error) {
	stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))

	if err != nil {
		return "", err
	}

	end := strings.LastIndexByte(string(stat), ')')
	rest := strings.Fields(string(stat[end+1:]))

	return rest[n-3], nil
}

// output runs a shell command and returns what it printed, without the final
// newline.
func output(script string) (string, error) {
	text, err := exec.Command("sh", "-c", script).Output()

	return strings.TrimSpace(string(text)), err
}

Listing 3.4 Reading a field of a process's record, in Rust. code/rust/procfacts/src/main.rs

/// Returns one field of `/proc/<pid>/stat`, counted from 1. The second field,
/// the command name, is in parentheses and may contain spaces, so the fields
/// after it are counted from the closing parenthesis.
fn field(pid: u32, n: usize) -> io::Result<String> {
    let stat = fs::read_to_string(format!("/proc/{pid}/stat"))?;
    let rest = stat.rsplit_once(')').map_or("", |(_, rest)| rest);

    rest.split_whitespace()
        .nth(n - 3)
        .map(str::to_owned)
        .ok_or_else(|| io::ErrorKind::InvalidData.into())
}

/// Runs a shell command and returns what it printed, without the final
/// newline.
fn output(script: &str) -> io::Result<String> {
    let output = Command::new("sh").args(["-c", script]).output()?;

    Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
}

parent runs a shell that prints $PPID, the pid of its parent, and compares it with the pid of the program.

Listing 3.5 The parent of a child, in Go. code/go/procfacts/main.go

// parent asks a child for the number of its parent process.
func parent() error {
	text, err := output("echo $PPID")

	if err != nil {
		return err
	}

	fmt.Printf("the child's parent is this process: %v\n", text == strconv.Itoa(os.Getpid()))

	return nil
}

Listing 3.6 The parent of a child, in Rust. code/rust/procfacts/src/main.rs

/// Asks a child for the number of its parent process.
fn parent() -> io::Result<()> {
    let text = output("echo $PPID")?;

    println!(
        "the child's parent is this process: {}",
        text == process::id().to_string()
    );

    Ok(())
}

status runs a script and reports how it ended. The program calls it with exit 7, with exit 300, and with a script that sends itself SIGTERM.

Listing 3.7 Exit status and signal, in Go. code/go/procfacts/main.go

// status runs a script and reports how it terminated.
func status(script string) error {
	err := exec.Command("sh", "-c", script).Run()

	var exit *exec.ExitError

	if !errors.As(err, &exit) {
		return fmt.Errorf("%s: %v", script, err)
	}

	wait, ok := exit.Sys().(syscall.WaitStatus)

	switch {
	case ok && wait.Signaled():
		fmt.Printf("%s: killed by signal %d\n", script, wait.Signal())
	default:
		fmt.Printf("%s: exited with status %d\n", script, exit.ExitCode())
	}

	return nil
}

Listing 3.8 Exit status and signal, in Rust. code/rust/procfacts/src/main.rs

/// Runs a script and reports how it terminated.
fn status(script: &str) -> io::Result<()> {
    let status = Command::new("sh").args(["-c", script]).status()?;

    match (status.signal(), status.code()) {
        (Some(signal), _) => println!("{script}: killed by signal {signal}"),
        (None, Some(code)) => println!("{script}: exited with status {code}"),
        (None, None) => println!("{script}: {status}"),
    }

    Ok(())
}

execKeepsPid runs a shell that prints its pid, replaces itself with another shell by exec, and prints the pid again.

Listing 3.9 The pid before and after exec, in Go. code/go/procfacts/main.go

// execKeepsPid prints the process number of a shell before and after the
// shell replaces its program with exec.
func execKeepsPid() error {
	text, err := output(`echo $$; exec sh -c 'echo $$'`)

	if err != nil {
		return err
	}

	pids := strings.Fields(text)

	fmt.Printf("pid before and after exec: equal %v\n", len(pids) == 2 && pids[0] == pids[1])

	return nil
}

Listing 3.10 The pid before and after exec, in Rust. code/rust/procfacts/src/main.rs

/// Prints the process number of a shell before and after the shell replaces
/// its program with exec.
fn exec_keeps_pid() -> io::Result<()> {
    let text = output("echo $$; exec sh -c 'echo $$'")?;
    let pids: Vec<&str> = text.split_whitespace().collect();

    println!(
        "pid before and after exec: equal {}",
        pids.len() == 2 && pids[0] == pids[1]
    );

    Ok(())
}

zombie starts true, which exits immediately, and polls the state field until the child has ended. Then it waits for the child and checks whether the /proc entry still exists.

Listing 3.11 A child between exit and wait, in Go. code/go/procfacts/main.go

// zombie starts a child that exits at once, and reads its state before and
// after waiting for it.
func zombie() error {
	child := exec.Command("true")

	if err := child.Start(); err != nil {
		return err
	}

	pid := child.Process.Pid
	state, err := field(pid, 3)

	for err == nil && state != "Z" {
		time.Sleep(time.Millisecond)
		state, err = field(pid, 3)
	}

	if err != nil {
		return err
	}

	if err := child.Wait(); err != nil {
		return err
	}

	_, err = os.Stat(fmt.Sprintf("/proc/%d", pid))

	fmt.Printf("exited child: state %s before wait, /proc entry present after wait: %v\n", state, err == nil)

	return nil
}

Listing 3.12 A child between exit and wait, in Rust. code/rust/procfacts/src/main.rs

/// Starts a child that exits at once, and reads its state before and after
/// waiting for it.
fn zombie() -> io::Result<()> {
    let mut child = Command::new("true").spawn()?;
    let pid = child.id();

    let mut state = field(pid, 3)?;

    while state != "Z" {
        thread::sleep(Duration::from_millis(1));
        state = field(pid, 3)?;
    }

    child.wait()?;

    let present = Path::new(&format!("/proc/{pid}")).exists();
    println!("exited child: state {state} before wait, /proc entry present after wait: {present}");

    Ok(())
}

orphan runs a shell that starts sleep 30 in the background, prints the pid of the sleep, and exits. The sleep is then an orphan. The program reads the pid of its new parent and the name of that process.

Listing 3.13 The new parent of an orphan, in Go. code/go/procfacts/main.go

// orphan starts a shell that starts a background sleep and exits. The sleep
// outlives its parent, and the kernel gives it a new one.
func orphan() error {
	text, err := output("sleep 30 >/dev/null 2>&1 & echo $!")

	if err != nil {
		return err
	}

	pid, err := strconv.Atoi(text)

	if err != nil {
		return err
	}

	defer syscall.Kill(pid, syscall.SIGKILL)

	adopter, err := field(pid, 4)

	if err != nil {
		return err
	}

	name, err := os.ReadFile("/proc/" + adopter + "/comm")

	if err != nil {
		return err
	}

	fmt.Printf("orphan adopted by: %s\n", strings.TrimSpace(string(name)))

	return nil
}

Listing 3.14 The new parent of an orphan, in Rust. code/rust/procfacts/src/main.rs

/// Starts a shell that starts a background sleep and exits. The sleep
/// outlives its parent, and the kernel gives it a new one.
fn orphan() -> io::Result<()> {
    let text = output("sleep 30 >/dev/null 2>&1 & echo $!")?;

    let pid: u32 = text
        .parse()
        .map_err(|_| io::Error::from(io::ErrorKind::InvalidData))?;

    let adopter = field(pid, 4);
    kill(Pid::from_raw(pid as i32), Signal::SIGKILL)?;

    let name = fs::read_to_string(format!("/proc/{}/comm", adopter?))?;
    println!("orphan adopted by: {}", name.trim());

    Ok(())
}
the child's parent is this process: true
exit 7: exited with status 7
exit 300: exited with status 44
kill -TERM $$: killed by signal 15
pid before and after exec: equal true
exited child: state Z before wait, /proc entry present after wait: false
orphan adopted by: systemd
OutputConclusion
the child’s parent is this processreal_parent of a child is the process that created it.
exit 7: status 7The status passed to exit reaches the parent.
exit 300: status 44Only the low 8 bits are kept: 300 - 256 = 44.
kill -TERM $$: signal 15A process ended by a signal has no exit status. The parent receives the signal number.
the pid is equal before and after execexec replaces the program inside the same task_struct.
state Z before wait, no entry afterA process that has ended is a zombie until its parent reaps it.
the orphan is adopted by systemdThe kernel gave the orphan a new parent.

The process named systemd in the last line is the service manager of the user’s session, which has registered as a subreaper. On a system without one, the line would name process 1.

Modelling the process table

proctable is a model of the part of the kernel that this chapter describes: the table of processes, the allocation of pids, the zombie state, and the adoption of orphans.

Low-level design: proctable

TypeKernel structureFieldsInvariant
processstruct task_structparent, state, statusstate is running or zombie; status is meaningful for a zombie
tablethe kernel’s table of pidsprocs, last, pidMaxevery parent except that of process 1 is a pid in procs
OperationBehaviourError
allocate()returns the first free pid after last, wrapping at pidMaxEAGAIN when every pid is in use
fork(parent)adds a running child of parentESRCH when parent does not exist
exit(pid, status)marks pid a zombie, stores status, gives its children to process 1ESRCH
wait(parent)removes a zombie child and returns its pid and statusECHILD when parent has no children

The table maps a pid to an entry. last remembers the pid allocated most recently.

Listing 3.15 The imports of proctable, in Go. code/go/proctable/main.go

import (
	"errors"
	"fmt"
	"log"
	"slices"
	"strings"
)

Listing 3.16 The imports of proctable, in Rust. code/rust/proctable/src/main.rs

use std::collections::BTreeMap;
use std::fmt;

Listing 3.17 The process table, in Go. code/go/proctable/main.go

// The errors carry the names of the kernel's error codes for the same cases.
var (
	errAgain   = errors.New("EAGAIN: no process number is free")
	errNoChild = errors.New("ECHILD: the caller has no child to wait for")
	errSearch  = errors.New("ESRCH: no such process")
)

// initPid is the process that adopts orphans.
const initPid = 1

type state byte

const (
	running state = 'R'
	zombie  state = 'Z'
)

// A process is one entry of the table.
type process struct {
	parent int
	state  state
	status int
}

// A table maps process numbers to entries. last is the number given out most
// recently, and pidMax is the largest number allowed.
type table struct {
	procs  map[int]*process
	last   int
	pidMax int
}

func newTable(pidMax int) *table {
	return &table{
		procs:  map[int]*process{initPid: {state: running}},
		last:   initPid,
		pidMax: pidMax,
	}
}

Listing 3.18 The process table, in Rust. code/rust/proctable/src/main.rs

/// The errors carry the names of the kernel's error codes for the same cases.
#[derive(Debug)]
enum Error {
    Again,
    NoChild,
    Search,
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Error::Again => "EAGAIN: no process number is free",
            Error::NoChild => "ECHILD: the caller has no child to wait for",
            Error::Search => "ESRCH: no such process",
        })
    }
}

impl std::error::Error for Error {}

/// The process that adopts orphans.
const INIT_PID: u32 = 1;

#[derive(Clone, Copy, PartialEq)]
enum State {
    Running,
    Zombie,
}

/// One entry of the table.
struct Process {
    parent: u32,
    state: State,
    status: i32,
}

/// Maps process numbers to entries. `last` is the number given out most
/// recently, and `pid_max` is the largest number allowed.
struct Table {
    procs: BTreeMap<u32, Process>,
    last: u32,
    pid_max: u32,
}

impl Table {
    fn new(pid_max: u32) -> Table {
        let init = Process {
            parent: 0,
            state: State::Running,
            status: 0,
        };

        Table {
            procs: BTreeMap::from([(INIT_PID, init)]),
            last: INIT_PID,
            pid_max,
        }
    }
}

allocate searches upward from last and wraps around at pidMax. A pid that has been freed is therefore reused only after all higher numbers have been tried. The delay protects programs that hold a pid: a process that stored the pid of another, to send it a signal later, would otherwise reach an unrelated new process if the number were reused quickly. /proc/sys/kernel/pid_max sets the limit, 4194304 on this machine.

Listing 3.19 Allocating a pid and creating a child, in Go. code/go/proctable/main.go

// allocate returns the first free number after last, wrapping around from
// pidMax to the number after initPid.
func (t *table) allocate() (int, error) {
	pid := t.last

	for range t.pidMax {
		pid++

		if pid > t.pidMax {
			pid = initPid + 1
		}

		if _, used := t.procs[pid]; !used {
			t.last = pid

			return pid, nil
		}
	}

	return 0, errAgain
}

// fork creates a child of parent and returns the child's number.
func (t *table) fork(parent int) (int, error) {
	if _, ok := t.procs[parent]; !ok {
		return 0, errSearch
	}

	pid, err := t.allocate()

	if err != nil {
		return 0, err
	}

	t.procs[pid] = &process{parent: parent, state: running}

	return pid, nil
}

Listing 3.20 Allocating a pid and creating a child, in Rust. code/rust/proctable/src/main.rs

impl Table {
    /// Returns the first free number after `last`, wrapping around from
    /// `pid_max` to the number after `INIT_PID`.
    fn allocate(&mut self) -> Result<u32, Error> {
        let mut pid = self.last;

        for _ in 0..self.pid_max {
            pid += 1;

            if pid > self.pid_max {
                pid = INIT_PID + 1;
            }

            if !self.procs.contains_key(&pid) {
                self.last = pid;

                return Ok(pid);
            }
        }

        Err(Error::Again)
    }

    /// Creates a child of `parent` and returns the child's number.
    fn fork(&mut self, parent: u32) -> Result<u32, Error> {
        if !self.procs.contains_key(&parent) {
            return Err(Error::Search);
        }

        let pid = self.allocate()?;

        let child = Process {
            parent,
            state: State::Running,
            status: 0,
        };

        self.procs.insert(pid, child);

        Ok(pid)
    }
}

exit keeps the entry and changes its state. It then gives every child of the ended process to process 1.

Listing 3.21 Ending a process, in Go. code/go/proctable/main.go

// exit ends a process. The entry stays in the table as a zombie that holds
// the exit status, and the children of the process become children of init.
func (t *table) exit(pid, status int) error {
	p, ok := t.procs[pid]

	if !ok {
		return errSearch
	}

	p.state, p.status = zombie, status

	for _, child := range t.procs {
		if child.parent == pid {
			child.parent = initPid
		}
	}

	return nil
}

Listing 3.22 Ending a process, in Rust. code/rust/proctable/src/main.rs

impl Table {
    /// Ends a process. The entry stays in the table as a zombie that holds
    /// the exit status, and the children of the process become children of
    /// init.
    fn exit(&mut self, pid: u32, status: i32) -> Result<(), Error> {
        let process = self.procs.get_mut(&pid).ok_or(Error::Search)?;

        process.state = State::Zombie;
        process.status = status;

        for child in self.procs.values_mut() {
            if child.parent == pid {
                child.parent = INIT_PID;
            }
        }

        Ok(())
    }
}

wait has 3 outcomes. A zombie child exists, and it is removed and returned. Children exist and none has ended, where the kernel would put the caller to sleep. Or the caller has no children, which is the error ECHILD.

Listing 3.23 Collecting a child, in Go. code/go/proctable/main.go

// wait removes a zombie child of parent from the table and returns its number
// and exit status. ok is false when parent has children and none of them has
// exited, the case in which the kernel suspends the caller.
func (t *table) wait(parent int) (pid, status int, ok bool, err error) {
	children := 0

	for _, pid := range t.pids() {
		child := t.procs[pid]

		if child.parent != parent {
			continue
		}

		children++

		if child.state == zombie {
			delete(t.procs, pid)

			return pid, child.status, true, nil
		}
	}

	if children == 0 {
		return 0, 0, false, errNoChild
	}

	return 0, 0, false, nil
}

// pids returns the process numbers in use, in increasing order.
func (t *table) pids() []int {
	pids := make([]int, 0, len(t.procs))

	for pid := range t.procs {
		pids = append(pids, pid)
	}

	slices.Sort(pids)

	return pids
}

Listing 3.24 Collecting a child, in Rust. code/rust/proctable/src/main.rs

impl Table {
    /// Removes a zombie child of `parent` from the table and returns its
    /// number and exit status. The result is `None` when `parent` has
    /// children and none of them has exited, the case in which the kernel
    /// suspends the caller.
    fn wait(&mut self, parent: u32) -> Result<Option<(u32, i32)>, Error> {
        let mut children = self
            .procs
            .iter()
            .filter(|(_, child)| child.parent == parent)
            .peekable();

        if children.peek().is_none() {
            return Err(Error::NoChild);
        }

        let Some((&pid, child)) = children.find(|(_, child)| child.state == State::Zombie) else {
            return Ok(None);
        };

        let status = child.status;
        self.procs.remove(&pid);

        Ok(Some((pid, status)))
    }
}

The program applies a sequence of calls to a table with pidMax set to 6 and prints the table after each call. An entry is printed as pid(parent)state.

Listing 3.25 The trace, in Go. code/go/proctable/main.go

// String renders the table as pid(parent)state for each entry.
func (t *table) String() string {
	var cells []string

	for _, pid := range t.pids() {
		p := t.procs[pid]
		cells = append(cells, fmt.Sprintf("%d(%d)%c", pid, p.parent, p.state))
	}

	return strings.Join(cells, " ")
}

// A call is one step of the trace: an operation, the process it applies to,
// and for exit the status.
type call struct {
	op     string
	pid    int
	status int
}

// apply performs a call and describes its result.
func (t *table) apply(c call) (string, error) {
	switch c.op {
	case "fork":
		child, err := t.fork(c.pid)

		return fmt.Sprintf("fork by %d: pid %d", c.pid, child), err

	case "exit":
		return fmt.Sprintf("exit of %d, status %d", c.pid, c.status), t.exit(c.pid, c.status)

	default:
		child, status, ok, err := t.wait(c.pid)

		if !ok {
			return fmt.Sprintf("wait by %d: would block", c.pid), err
		}

		return fmt.Sprintf("wait by %d: pid %d, status %d", c.pid, child, status), nil
	}
}

func main() {
	t := newTable(6)

	calls := []call{
		{op: "fork", pid: 1},
		{op: "fork", pid: 2},
		{op: "fork", pid: 3},
		{op: "wait", pid: 2},
		{op: "exit", pid: 3},
		{op: "wait", pid: 2},
		{op: "fork", pid: 2},
		{op: "exit", pid: 4, status: 7},
		{op: "wait", pid: 1},
		{op: "fork", pid: 2},
		{op: "fork", pid: 2},
	}

	fmt.Printf("%-30s %s\n", "start", t)

	for _, c := range calls {
		result, err := t.apply(c)

		if err != nil {
			log.Fatal(err)
		}

		fmt.Printf("%-30s %s\n", result, t)
	}
}

Listing 3.26 The trace, in Rust. code/rust/proctable/src/main.rs

/// Renders the table as pid(parent)state for each entry.
impl fmt::Display for Table {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let cells: Vec<String> = self
            .procs
            .iter()
            .map(|(pid, process)| {
                let state = match process.state {
                    State::Running => 'R',
                    State::Zombie => 'Z',
                };

                format!("{pid}({}){state}", process.parent)
            })
            .collect();

        f.write_str(&cells.join(" "))
    }
}

/// One step of the trace.
enum Call {
    Fork(u32),
    Exit(u32, i32),
    Wait(u32),
}

impl Table {
    /// Performs a call and describes its result.
    fn apply(&mut self, call: &Call) -> Result<String, Error> {
        match *call {
            Call::Fork(parent) => {
                let child = self.fork(parent)?;

                Ok(format!("fork by {parent}: pid {child}"))
            }
            Call::Exit(pid, status) => {
                self.exit(pid, status)?;

                Ok(format!("exit of {pid}, status {status}"))
            }
            Call::Wait(parent) => match self.wait(parent)? {
                Some((child, status)) => {
                    Ok(format!("wait by {parent}: pid {child}, status {status}"))
                }
                None => Ok(format!("wait by {parent}: would block")),
            },
        }
    }
}

fn main() -> Result<(), Error> {
    let mut table = Table::new(6);

    let calls = [
        Call::Fork(1),
        Call::Fork(2),
        Call::Fork(3),
        Call::Wait(2),
        Call::Exit(3, 0),
        Call::Wait(2),
        Call::Fork(2),
        Call::Exit(4, 7),
        Call::Wait(1),
        Call::Fork(2),
        Call::Fork(2),
    ];

    println!("{:<30} {table}", "start");

    for call in &calls {
        let result = table.apply(call)?;
        println!("{result:<30} {table}");
    }

    Ok(())
}
start                          1(0)R
fork by 1: pid 2               1(0)R 2(1)R
fork by 2: pid 3               1(0)R 2(1)R 3(2)R
fork by 3: pid 4               1(0)R 2(1)R 3(2)R 4(3)R
wait by 2: would block         1(0)R 2(1)R 3(2)R 4(3)R
exit of 3, status 0            1(0)R 2(1)R 3(2)Z 4(1)R
wait by 2: pid 3, status 0     1(0)R 2(1)R 4(1)R
fork by 2: pid 5               1(0)R 2(1)R 4(1)R 5(2)R
exit of 4, status 7            1(0)R 2(1)R 4(1)Z 5(2)R
wait by 1: pid 4, status 7     1(0)R 2(1)R 5(2)R
fork by 2: pid 6               1(0)R 2(1)R 5(2)R 6(2)R
fork by 2: pid 3               1(0)R 2(1)R 3(2)R 5(2)R 6(2)R
LineWhat it shows
wait by 2: would blockProcess 2 has a child, 3, which is still running.
exit of 3Entry 3 becomes Z and stays. Its child 4 now has parent 1.
wait by 2: pid 3The zombie is removed and its status is returned.
fork by 2: pid 5Pid 3 is free, and the search continues upward from last, which is 4.
wait by 1: pid 4, status 7Process 1 reaps the orphan it adopted.
fork by 2: pid 3After pid 6 the search wraps around. Pid 2 is in use, and 3 is the first free number.
Before pid 3 exits, the processes form a chain: 1 is the parent of 2, 2 of 3, and 3 of 4. After pid 3 exits it is a zombie child of 2, and the kernel has made pid 1, init, the parent of 4.
Figure 3.7 The table before and after the line exit of 3.

Correspondence with the kernel source. kernel/fork.c contains copy_process, which builds the new task_struct and calls a copy_ function for each resource (copy_mm, copy_files, copy_sighand, and others), each of which copies or shares according to the clone flags. alloc_pid in kernel/pid.c allocates numbers with idr_alloc_cyclic, the cyclic search that allocate models. do_exit in kernel/exit.c sets EXIT_ZOMBIE and calls forget_original_parent, which chooses the new parent of each child with find_new_reaper. The kernel keeps a list of children in each task, where proctable scans the whole table.

Inspecting processes on a live system

CommandShows
ps -o pid,ppid,stat,commpid, parent, state and name of the processes of the terminal
ps -ef --forest or pstree -pevery process, drawn as a tree of parents and children
cat /proc/<pid>/statusthe record of a process: state, parent, threads, memory, user
ls /proc/<pid>/taskthe threads of a process
cat /proc/sys/kernel/pid_maxthe largest pid
ps -eo stat,pid,ppid,comm | grep '^Z'the zombies of the system, with the parent that has not reaped them

A zombie cannot be removed with kill, since it has already ended. The last command gives the pid of the parent, and the remedy is there: the parent has to call wait, or end.

The complete programs

Listing 3.27 The complete procfacts, in Go. code/go/procfacts/main.go

// Command procfacts measures the life cycle of a process: who its parent is,
// how its termination is reported, what exec keeps, what remains of a child
// that has exited and has not been waited for, and who becomes the parent of
// an orphan.
//
// Run it from code/go with:
//
//	go run ./procfacts
package main

import (
	"errors"
	"fmt"
	"log"
	"os"
	"os/exec"
	"strconv"
	"strings"
	"syscall"
	"time"
)


// field returns one field of /proc/<pid>/stat, counted from 1. The second
// field, the command name, is in parentheses and may contain spaces, so the
// fields after it are counted from the closing parenthesis.
func field(pid, n int) (string, error) {
	stat, err := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid))

	if err != nil {
		return "", err
	}

	end := strings.LastIndexByte(string(stat), ')')
	rest := strings.Fields(string(stat[end+1:]))

	return rest[n-3], nil
}

// output runs a shell command and returns what it printed, without the final
// newline.
func output(script string) (string, error) {
	text, err := exec.Command("sh", "-c", script).Output()

	return strings.TrimSpace(string(text)), err
}


// parent asks a child for the number of its parent process.
func parent() error {
	text, err := output("echo $PPID")

	if err != nil {
		return err
	}

	fmt.Printf("the child's parent is this process: %v\n", text == strconv.Itoa(os.Getpid()))

	return nil
}


// status runs a script and reports how it terminated.
func status(script string) error {
	err := exec.Command("sh", "-c", script).Run()

	var exit *exec.ExitError

	if !errors.As(err, &exit) {
		return fmt.Errorf("%s: %v", script, err)
	}

	wait, ok := exit.Sys().(syscall.WaitStatus)

	switch {
	case ok && wait.Signaled():
		fmt.Printf("%s: killed by signal %d\n", script, wait.Signal())
	default:
		fmt.Printf("%s: exited with status %d\n", script, exit.ExitCode())
	}

	return nil
}


// execKeepsPid prints the process number of a shell before and after the
// shell replaces its program with exec.
func execKeepsPid() error {
	text, err := output(`echo $$; exec sh -c 'echo $$'`)

	if err != nil {
		return err
	}

	pids := strings.Fields(text)

	fmt.Printf("pid before and after exec: equal %v\n", len(pids) == 2 && pids[0] == pids[1])

	return nil
}


// zombie starts a child that exits at once, and reads its state before and
// after waiting for it.
func zombie() error {
	child := exec.Command("true")

	if err := child.Start(); err != nil {
		return err
	}

	pid := child.Process.Pid
	state, err := field(pid, 3)

	for err == nil && state != "Z" {
		time.Sleep(time.Millisecond)
		state, err = field(pid, 3)
	}

	if err != nil {
		return err
	}

	if err := child.Wait(); err != nil {
		return err
	}

	_, err = os.Stat(fmt.Sprintf("/proc/%d", pid))

	fmt.Printf("exited child: state %s before wait, /proc entry present after wait: %v\n", state, err == nil)

	return nil
}


// orphan starts a shell that starts a background sleep and exits. The sleep
// outlives its parent, and the kernel gives it a new one.
func orphan() error {
	text, err := output("sleep 30 >/dev/null 2>&1 & echo $!")

	if err != nil {
		return err
	}

	pid, err := strconv.Atoi(text)

	if err != nil {
		return err
	}

	defer syscall.Kill(pid, syscall.SIGKILL)

	adopter, err := field(pid, 4)

	if err != nil {
		return err
	}

	name, err := os.ReadFile("/proc/" + adopter + "/comm")

	if err != nil {
		return err
	}

	fmt.Printf("orphan adopted by: %s\n", strings.TrimSpace(string(name)))

	return nil
}


func main() {
	experiments := []func() error{
		parent,
		func() error { return status("exit 7") },
		func() error { return status("exit 300") },
		func() error { return status("kill -TERM $$") },
		execKeepsPid,
		zombie,
		orphan,
	}

	for _, experiment := range experiments {
		if err := experiment(); err != nil {
			log.Fatal(err)
		}
	}
}

Listing 3.28 The complete procfacts, in Rust. code/rust/procfacts/src/main.rs

//! Measures the life cycle of a process: who its parent is, how its
//! termination is reported, what exec keeps, what remains of a child that has
//! exited and has not been waited for, and who becomes the parent of an
//! orphan.
//!
//! Run it from code/rust with:
//!
//! ```text
//! cargo run -p procfacts
//! ```

use std::fs;
use std::io;
use std::os::unix::process::ExitStatusExt;
use std::path::Path;
use std::process::{self, Command};
use std::thread;
use std::time::Duration;

use nix::sys::signal::{Signal, kill};
use nix::unistd::Pid;

/// Returns one field of `/proc/<pid>/stat`, counted from 1. The second field,
/// the command name, is in parentheses and may contain spaces, so the fields
/// after it are counted from the closing parenthesis.
fn field(pid: u32, n: usize) -> io::Result<String> {
    let stat = fs::read_to_string(format!("/proc/{pid}/stat"))?;
    let rest = stat.rsplit_once(')').map_or("", |(_, rest)| rest);

    rest.split_whitespace()
        .nth(n - 3)
        .map(str::to_owned)
        .ok_or_else(|| io::ErrorKind::InvalidData.into())
}

/// Runs a shell command and returns what it printed, without the final
/// newline.
fn output(script: &str) -> io::Result<String> {
    let output = Command::new("sh").args(["-c", script]).output()?;

    Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned())
}

/// Asks a child for the number of its parent process.
fn parent() -> io::Result<()> {
    let text = output("echo $PPID")?;

    println!(
        "the child's parent is this process: {}",
        text == process::id().to_string()
    );

    Ok(())
}

/// Runs a script and reports how it terminated.
fn status(script: &str) -> io::Result<()> {
    let status = Command::new("sh").args(["-c", script]).status()?;

    match (status.signal(), status.code()) {
        (Some(signal), _) => println!("{script}: killed by signal {signal}"),
        (None, Some(code)) => println!("{script}: exited with status {code}"),
        (None, None) => println!("{script}: {status}"),
    }

    Ok(())
}

/// Prints the process number of a shell before and after the shell replaces
/// its program with exec.
fn exec_keeps_pid() -> io::Result<()> {
    let text = output("echo $$; exec sh -c 'echo $$'")?;
    let pids: Vec<&str> = text.split_whitespace().collect();

    println!(
        "pid before and after exec: equal {}",
        pids.len() == 2 && pids[0] == pids[1]
    );

    Ok(())
}

/// Starts a child that exits at once, and reads its state before and after
/// waiting for it.
fn zombie() -> io::Result<()> {
    let mut child = Command::new("true").spawn()?;
    let pid = child.id();

    let mut state = field(pid, 3)?;

    while state != "Z" {
        thread::sleep(Duration::from_millis(1));
        state = field(pid, 3)?;
    }

    child.wait()?;

    let present = Path::new(&format!("/proc/{pid}")).exists();
    println!("exited child: state {state} before wait, /proc entry present after wait: {present}");

    Ok(())
}

/// Starts a shell that starts a background sleep and exits. The sleep
/// outlives its parent, and the kernel gives it a new one.
fn orphan() -> io::Result<()> {
    let text = output("sleep 30 >/dev/null 2>&1 & echo $!")?;

    let pid: u32 = text
        .parse()
        .map_err(|_| io::Error::from(io::ErrorKind::InvalidData))?;

    let adopter = field(pid, 4);
    kill(Pid::from_raw(pid as i32), Signal::SIGKILL)?;

    let name = fs::read_to_string(format!("/proc/{}/comm", adopter?))?;
    println!("orphan adopted by: {}", name.trim());

    Ok(())
}

fn main() -> io::Result<()> {
    parent()?;
    status("exit 7")?;
    status("exit 300")?;
    status("kill -TERM $$")?;
    exec_keeps_pid()?;
    zombie()?;
    orphan()
}

Listing 3.29 The complete proctable, in Go. code/go/proctable/main.go

// Command proctable models the kernel's process table: process numbers handed
// out in a cycle, a parent for every process, a zombie state between exit and
// wait, and a new parent for orphans. It applies a sequence of calls and
// prints the table after each.
//
// Run it from code/go with:
//
//	go run ./proctable
package main

import (
	"errors"
	"fmt"
	"log"
	"slices"
	"strings"
)


// The errors carry the names of the kernel's error codes for the same cases.
var (
	errAgain   = errors.New("EAGAIN: no process number is free")
	errNoChild = errors.New("ECHILD: the caller has no child to wait for")
	errSearch  = errors.New("ESRCH: no such process")
)

// initPid is the process that adopts orphans.
const initPid = 1

type state byte

const (
	running state = 'R'
	zombie  state = 'Z'
)

// A process is one entry of the table.
type process struct {
	parent int
	state  state
	status int
}

// A table maps process numbers to entries. last is the number given out most
// recently, and pidMax is the largest number allowed.
type table struct {
	procs  map[int]*process
	last   int
	pidMax int
}

func newTable(pidMax int) *table {
	return &table{
		procs:  map[int]*process{initPid: {state: running}},
		last:   initPid,
		pidMax: pidMax,
	}
}


// allocate returns the first free number after last, wrapping around from
// pidMax to the number after initPid.
func (t *table) allocate() (int, error) {
	pid := t.last

	for range t.pidMax {
		pid++

		if pid > t.pidMax {
			pid = initPid + 1
		}

		if _, used := t.procs[pid]; !used {
			t.last = pid

			return pid, nil
		}
	}

	return 0, errAgain
}

// fork creates a child of parent and returns the child's number.
func (t *table) fork(parent int) (int, error) {
	if _, ok := t.procs[parent]; !ok {
		return 0, errSearch
	}

	pid, err := t.allocate()

	if err != nil {
		return 0, err
	}

	t.procs[pid] = &process{parent: parent, state: running}

	return pid, nil
}


// exit ends a process. The entry stays in the table as a zombie that holds
// the exit status, and the children of the process become children of init.
func (t *table) exit(pid, status int) error {
	p, ok := t.procs[pid]

	if !ok {
		return errSearch
	}

	p.state, p.status = zombie, status

	for _, child := range t.procs {
		if child.parent == pid {
			child.parent = initPid
		}
	}

	return nil
}


// wait removes a zombie child of parent from the table and returns its number
// and exit status. ok is false when parent has children and none of them has
// exited, the case in which the kernel suspends the caller.
func (t *table) wait(parent int) (pid, status int, ok bool, err error) {
	children := 0

	for _, pid := range t.pids() {
		child := t.procs[pid]

		if child.parent != parent {
			continue
		}

		children++

		if child.state == zombie {
			delete(t.procs, pid)

			return pid, child.status, true, nil
		}
	}

	if children == 0 {
		return 0, 0, false, errNoChild
	}

	return 0, 0, false, nil
}

// pids returns the process numbers in use, in increasing order.
func (t *table) pids() []int {
	pids := make([]int, 0, len(t.procs))

	for pid := range t.procs {
		pids = append(pids, pid)
	}

	slices.Sort(pids)

	return pids
}


// String renders the table as pid(parent)state for each entry.
func (t *table) String() string {
	var cells []string

	for _, pid := range t.pids() {
		p := t.procs[pid]
		cells = append(cells, fmt.Sprintf("%d(%d)%c", pid, p.parent, p.state))
	}

	return strings.Join(cells, " ")
}

// A call is one step of the trace: an operation, the process it applies to,
// and for exit the status.
type call struct {
	op     string
	pid    int
	status int
}

// apply performs a call and describes its result.
func (t *table) apply(c call) (string, error) {
	switch c.op {
	case "fork":
		child, err := t.fork(c.pid)

		return fmt.Sprintf("fork by %d: pid %d", c.pid, child), err

	case "exit":
		return fmt.Sprintf("exit of %d, status %d", c.pid, c.status), t.exit(c.pid, c.status)

	default:
		child, status, ok, err := t.wait(c.pid)

		if !ok {
			return fmt.Sprintf("wait by %d: would block", c.pid), err
		}

		return fmt.Sprintf("wait by %d: pid %d, status %d", c.pid, child, status), nil
	}
}

func main() {
	t := newTable(6)

	calls := []call{
		{op: "fork", pid: 1},
		{op: "fork", pid: 2},
		{op: "fork", pid: 3},
		{op: "wait", pid: 2},
		{op: "exit", pid: 3},
		{op: "wait", pid: 2},
		{op: "fork", pid: 2},
		{op: "exit", pid: 4, status: 7},
		{op: "wait", pid: 1},
		{op: "fork", pid: 2},
		{op: "fork", pid: 2},
	}

	fmt.Printf("%-30s %s\n", "start", t)

	for _, c := range calls {
		result, err := t.apply(c)

		if err != nil {
			log.Fatal(err)
		}

		fmt.Printf("%-30s %s\n", result, t)
	}
}

Listing 3.30 The complete proctable, in Rust. code/rust/proctable/src/main.rs

//! Models the kernel's process table: process numbers handed out in a cycle,
//! a parent for every process, a zombie state between exit and wait, and a
//! new parent for orphans. It applies a sequence of calls and prints the
//! table after each.
//!
//! Run it from code/rust with:
//!
//! ```text
//! cargo run -p proctable
//! ```

use std::collections::BTreeMap;
use std::fmt;

/// The errors carry the names of the kernel's error codes for the same cases.
#[derive(Debug)]
enum Error {
    Again,
    NoChild,
    Search,
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Error::Again => "EAGAIN: no process number is free",
            Error::NoChild => "ECHILD: the caller has no child to wait for",
            Error::Search => "ESRCH: no such process",
        })
    }
}

impl std::error::Error for Error {}

/// The process that adopts orphans.
const INIT_PID: u32 = 1;

#[derive(Clone, Copy, PartialEq)]
enum State {
    Running,
    Zombie,
}

/// One entry of the table.
struct Process {
    parent: u32,
    state: State,
    status: i32,
}

/// Maps process numbers to entries. `last` is the number given out most
/// recently, and `pid_max` is the largest number allowed.
struct Table {
    procs: BTreeMap<u32, Process>,
    last: u32,
    pid_max: u32,
}

impl Table {
    fn new(pid_max: u32) -> Table {
        let init = Process {
            parent: 0,
            state: State::Running,
            status: 0,
        };

        Table {
            procs: BTreeMap::from([(INIT_PID, init)]),
            last: INIT_PID,
            pid_max,
        }
    }
}

impl Table {
    /// Returns the first free number after `last`, wrapping around from
    /// `pid_max` to the number after `INIT_PID`.
    fn allocate(&mut self) -> Result<u32, Error> {
        let mut pid = self.last;

        for _ in 0..self.pid_max {
            pid += 1;

            if pid > self.pid_max {
                pid = INIT_PID + 1;
            }

            if !self.procs.contains_key(&pid) {
                self.last = pid;

                return Ok(pid);
            }
        }

        Err(Error::Again)
    }

    /// Creates a child of `parent` and returns the child's number.
    fn fork(&mut self, parent: u32) -> Result<u32, Error> {
        if !self.procs.contains_key(&parent) {
            return Err(Error::Search);
        }

        let pid = self.allocate()?;

        let child = Process {
            parent,
            state: State::Running,
            status: 0,
        };

        self.procs.insert(pid, child);

        Ok(pid)
    }
}

impl Table {
    /// Ends a process. The entry stays in the table as a zombie that holds
    /// the exit status, and the children of the process become children of
    /// init.
    fn exit(&mut self, pid: u32, status: i32) -> Result<(), Error> {
        let process = self.procs.get_mut(&pid).ok_or(Error::Search)?;

        process.state = State::Zombie;
        process.status = status;

        for child in self.procs.values_mut() {
            if child.parent == pid {
                child.parent = INIT_PID;
            }
        }

        Ok(())
    }
}

impl Table {
    /// Removes a zombie child of `parent` from the table and returns its
    /// number and exit status. The result is `None` when `parent` has
    /// children and none of them has exited, the case in which the kernel
    /// suspends the caller.
    fn wait(&mut self, parent: u32) -> Result<Option<(u32, i32)>, Error> {
        let mut children = self
            .procs
            .iter()
            .filter(|(_, child)| child.parent == parent)
            .peekable();

        if children.peek().is_none() {
            return Err(Error::NoChild);
        }

        let Some((&pid, child)) = children.find(|(_, child)| child.state == State::Zombie) else {
            return Ok(None);
        };

        let status = child.status;
        self.procs.remove(&pid);

        Ok(Some((pid, status)))
    }
}

/// Renders the table as pid(parent)state for each entry.
impl fmt::Display for Table {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let cells: Vec<String> = self
            .procs
            .iter()
            .map(|(pid, process)| {
                let state = match process.state {
                    State::Running => 'R',
                    State::Zombie => 'Z',
                };

                format!("{pid}({}){state}", process.parent)
            })
            .collect();

        f.write_str(&cells.join(" "))
    }
}

/// One step of the trace.
enum Call {
    Fork(u32),
    Exit(u32, i32),
    Wait(u32),
}

impl Table {
    /// Performs a call and describes its result.
    fn apply(&mut self, call: &Call) -> Result<String, Error> {
        match *call {
            Call::Fork(parent) => {
                let child = self.fork(parent)?;

                Ok(format!("fork by {parent}: pid {child}"))
            }
            Call::Exit(pid, status) => {
                self.exit(pid, status)?;

                Ok(format!("exit of {pid}, status {status}"))
            }
            Call::Wait(parent) => match self.wait(parent)? {
                Some((child, status)) => {
                    Ok(format!("wait by {parent}: pid {child}, status {status}"))
                }
                None => Ok(format!("wait by {parent}: would block")),
            },
        }
    }
}

fn main() -> Result<(), Error> {
    let mut table = Table::new(6);

    let calls = [
        Call::Fork(1),
        Call::Fork(2),
        Call::Fork(3),
        Call::Wait(2),
        Call::Exit(3, 0),
        Call::Wait(2),
        Call::Fork(2),
        Call::Exit(4, 7),
        Call::Wait(1),
        Call::Fork(2),
        Call::Fork(2),
    ];

    println!("{:<30} {table}", "start");

    for call in &calls {
        let result = table.apply(call)?;
        println!("{result:<30} {table}");
    }

    Ok(())
}

Summary

  • A process is a struct task_struct and the resources it points to: an address space, a descriptor table, signal state, namespaces and control groups.
  • fork creates a child with a new pid and copies of the parent’s resources. Memory is copied lazily. exec replaces the program and keeps the task_struct.
  • The code that runs between fork and exec prepares the environment of the new program.
  • A process ends with an 8-bit exit status or by a signal, and wait reports which in a single integer.
  • A process that has ended is a zombie until its parent reaps it with wait.
  • An orphan is adopted by the nearest subreaper among its ancestors, or by process 1.
  • Pids are allocated in a cycle, so a freed pid is reused as late as possible.
  • A thread is a task that shares the resources of another. The pid of a process is the thread group ID of its tasks.

References