algoverse Operating Systems / Processes

Operating Systems · Chapter 1

Processes

A process is a program in execution. It is the unit the operating system uses to isolate programs from one another and to share the processor among them. Each process has its own address space, its own register state, and its own set of kernel-managed resources.

Program and process

A program is a file: machine instructions and initialized data stored in an executable format such as ELF. A process exists only while the program runs. The kernel creates it by building a fresh address space, mapping the executable's sections into that space, and pointing the processor's registers at the first instruction.

One program can back many processes at once. Two terminal windows each running bash are two processes executing the same file. They share nothing except the read-only instructions, which the kernel maps into both address spaces from a single copy in physical memory.

An executable file's sections mapped into a process address space, with the program counter and stack pointer pointing into it Executable file on disk ELF header .text .data symbols, relocations execve() maps the loadable sections into memory Process address space Kernel not accessible in user mode Stack one frame per active call UNMAPPED Shared libraries and memory-mapped files UNMAPPED Heap .data and .bss .text instructions, read-only HIGH ADDRESSES LOW ADDRESSES CPU SP PC Process address space Kernel not accessible in user mode Stack one frame per active call UNMAPPED Shared libraries and memory-mapped files UNMAPPED Heap .data and .bss .text instructions, read-only HIGH ADDRESSES LOW ADDRESSES CPU SP PC Executable file on disk ELF header .text .data symbols, relocations
Loading an executable creates a process. The file's code and data sections are mapped into a new address space; the program counter (PC) points into the code and the stack pointer (SP) points into the stack.

The address space

Every process sees memory as one contiguous range of virtual addresses, laid out in regions with distinct purposes:

These addresses are virtual. Two processes can both use address 0x401000; the memory management unit translates each process's addresses through that process's own page table to different physical frames. This translation is what prevents one process from reading or overwriting another's memory.

Process states

A process moves through a small set of states during its lifetime. The kernel records the current state and changes it in response to scheduling decisions, interrupts, and system calls.

Process state diagram with New, Ready, Running, Blocked, and Terminated states New Ready Running Blocked Terminated admitted scheduled preempted (timer interrupt) waits for I/O or event I/O or event completes exit() New Ready Running Blocked Terminated admitted scheduled preempted (timer interrupt) waits for I/O or event exit() I/O or event completes
The process state machine. Only the Ready and Running states compete for the processor; a Blocked process is ignored by the scheduler until its event arrives.
StateMeaning
ReadyRunnable, waiting in the run queue for a processor.
RunningExecuting instructions on a processor. A machine with N cores has at most N running processes at any instant.
BlockedWaiting for an event: a disk read, a network packet, a lock, or a child's exit.
TerminatedFinished executing; its exit status has not yet been collected by the parent.

The process control block

The kernel stores everything it knows about a process in a process control block (PCB). All PCBs are reachable from the process table. On Linux the PCB is struct task_struct, and the process table is the set of these structures linked together and indexed by process ID.

The process table pointing to one process control block and the six groups of fields it contains Process table 1systemd 412sshd 977bash 1203ls 1204grep … Process control block PID 977 IDENTIFICATION pid 977 parent pid 412 uid 1000 PROCESSOR STATE program counter stack pointer registers, flags MEMORY page table base region list (text, heap, stack) OPEN FILES fd 0, 1, 2 → tty fd 3 → config.yaml current directory SCHEDULING state: Ready priority, nice value CPU time consumed SIGNALS pending signals blocked mask handlers Process table 1systemd 412sshd 977bash 1203ls … Process control block PID 977 IDENTIFICATION pid 977 parent pid 412 uid 1000 PROCESSOR STATE program counter stack pointer registers, flags MEMORY page table base region list (text, heap, …) OPEN FILES fd 0, 1, 2 → tty fd 3 → config current dir SCHEDULING state: Ready priority, nice CPU time used SIGNALS pending signals blocked mask handlers
The process table holds one entry per process. Each entry leads to a process control block, which records the process's identity, saved processor state, memory map, open files, scheduling data, and signal state.

The saved processor state is the field that makes multitasking possible. When a process stops running, its register values are written into its PCB; when it runs again, they are loaded back. The process cannot observe that it was ever stopped.

The context switch

A process runs directly on the processor, in user mode, with no kernel code executing alongside it. The kernel regains control only when one of three events transfers control to it:

The timer interrupt guarantees that the kernel regains control even from a process that never makes a system call. At each timer interrupt the scheduler may decide that the current process has used its time slice and switch to another. This operation is a context switch.

Timeline of a context switch from process A to process B through the kernel Process A USER MODE Kernel KERNEL MODE Process B USER MODE running ready, waiting in the run queue timer interrupt save A schedule restore B registers → PCB A select B to run PCB B → registers return-from-trap ready, waiting in the run queue running TIME Process A USER MODE Kernel KERNEL MODE Process B USER MODE running ready timer interrupt save A registers → PCB A schedule select B to run restore B PCB B → registers return-from-trap ready running TIME FLOWS DOWNWARD
A context switch. The timer interrupt moves the processor into kernel mode; the kernel saves A's registers to A's PCB, selects B, loads B's registers from B's PCB, and returns to user mode inside B.

The direct cost of a context switch is the work shown above: a few hundred instructions to save and restore state. The indirect cost is larger. The incoming process finds the processor caches and the translation lookaside buffer filled with the previous process's data, and runs slowly until they are refilled.

Creating a process: fork, exec, wait

Unix creates processes with two separate system calls. fork() creates a new process; execve() replaces the program running in a process. Separating the two lets the parent configure the child, for example by redirecting its standard output, between the two calls.

fork

fork() creates a child process that is a duplicate of the caller: the same program, the same memory contents, the same open files, and the same position in the code. The call returns twice, once in each process:

The kernel does not copy the parent's memory at fork time. Parent and child share the same physical pages, marked read-only; a page is copied only when one of them writes to it. This technique, copy-on-write, makes fork() fast even for large processes.

exec

execve(path, argv, envp) discards the calling process's address space and loads a new program into it. The process ID, parent, and open file descriptors are kept. On success execve() never returns, because the code that called it no longer exists.

wait

waitpid(pid, &status, 0) blocks the parent until the specified child terminates, then returns the child's exit status and releases the child's last kernel resources.

#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>

int main(void) {
    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");                    /* no child was created */
        return 1;
    }

    if (pid == 0) {                        /* child */
        char *argv[] = { "ls", "-l", NULL };
        execv("/bin/ls", argv);
        perror("execv");                   /* reached only if exec failed */
        _exit(127);
    }

    int status;                             /* parent */
    waitpid(pid, &status, 0);
    if (WIFEXITED(status))
        printf("child %d exited with %d\n", pid, WEXITSTATUS(status));
    return 0;
}
Timeline of a parent shell forking a child, the child executing ls, exiting, and being reaped by waitpid Parent bash, PID 977 Child PID 1203 running blocked in waitpid() running fork() waitpid() returns 1203 bash (copy) ls zombie execve("/bin/ls") exit(0) returns 0 waitpid() returns, exit status 0 TIME Parent bash, PID 977 Child PID 1203 running pid = 1203 blocked in waitpid() running fork() waitpid() returns, status 0 bash (copy) pid = 0 ls zombie execve() exit(0) reaped TIME FLOWS DOWNWARD
The lifecycle of one shell command. fork() duplicates the shell, execve() replaces the copy with ls, and the exited child remains a zombie until the parent's waitpid() collects its status.

Termination, zombies, and orphans

A process terminates by calling exit(), by returning from main, or by receiving a signal it does not handle. The kernel then frees the process's memory and closes its files, but keeps its process table entry, which holds the exit status the parent has not yet read.

A terminated process whose status has not been collected is a zombie. It uses no memory or processor time, but it occupies a process ID and a slot in the process table; ps shows it in state Z. A parent that creates children and never calls wait() accumulates zombies until fork fails because the system has run out of process IDs.

If a parent terminates before its children, the children become orphans. The kernel reassigns each orphan to init (PID 1) or to a designated subreaper process, which calls wait() on it when it exits. Orphans therefore never remain zombies indefinitely.

Review

What is the difference between a program and a process?

A program is an executable file on disk. A process is a running instance of a program, with its own address space, register state, and kernel resources. One program can back many processes at once.

Why can two processes use the same virtual address without interfering?

Each process has its own page table. The memory management unit translates the same virtual address in two processes to two different physical frames.

How does the kernel regain control from a process that never makes a system call?

Through the hardware timer interrupt, which transfers control to the kernel at a fixed interval regardless of what the process is executing.

What does fork() return, and to whom?

The child's process ID to the parent, 0 to the child, and -1 to the parent if no child could be created.

Why does a successful execve() never return?

It replaces the calling process's address space with the new program, so the code that made the call no longer exists.

What is a zombie process, and what removes it?

A terminated process whose exit status has not been collected. It is removed when its parent calls wait() or waitpid(), or, if the parent has exited, when init or a subreaper reaps it.