Processes, memory and signals
fork and exec, process states and zombies, virtual memory and page faults, copy-on-write, what RSS really measures, the OOM killer, and how signals reach a process.
On this page
A process is the kernel's unit of "a running program": some memory, some open files, a place in the scheduler's queue, and a number. This page follows one from birth to death, then looks at the memory it thinks it owns, and at the messages the kernel uses to interrupt it.
The map
Read this first when short on time. Every branch is a section below.
A process: a program with its own address space and a number
A process is the kernel's record of one running program. The kernel keeps a structure for it (the task_struct) that holds everything the program owns or is owed.
- An address space. The memory the program can see, described by page tables. No other process can see it.
- A file descriptor table. The open files, sockets and pipes, numbered from 0.
- Credentials. User and group IDs, capabilities.
- A signal table. Which signals are blocked, which have handlers.
- A place in the scheduler. Its state, priority, how much CPU it has used.
- Numbers. Its PID, its parent's PID, its process group and session.
The PID is just an integer, handed out in increasing order and wrapped at pid_max (32768 by default, often raised). Every process has a parent, so all processes form one tree under PID 1. pstree draws it.
A thread is a process that shares its address space, file table and signal handlers with another. Linux does not have a separate object for threads. It creates them with clone and a set of flags saying what to share. Each thread has its own PID internally (called a TID in user space), its own stack and registers, and the group of them shares one thread group ID, which is what getpid returns. ps -T or ls /proc/PID/task shows them.
States
At any moment a process is in one state, shown as a letter in ps and top.
stateDiagram-v2 [*] --> R: fork R --> S: waits for a lock, a socket, a timer S --> R: woken up R --> D: waits for disk or NFS D --> R: I/O done R --> T: SIGSTOP, Ctrl-Z, debugger T --> R: SIGCONT R --> Z: exit Z --> [*]: parent calls wait
| Letter | State | Meaning |
|---|---|---|
R | Running or runnable | On a CPU or in a run queue waiting for one. |
S | Interruptible sleep | Waiting for something: a read, a lock, a timer. Signals wake it. |
D | Uninterruptible sleep | Waiting on disk or a stuck network filesystem. Cannot be killed until the I/O returns. |
T | Stopped | Paused by SIGSTOP or a debugger. Resumes on SIGCONT. |
Z | Zombie | Exited, but the parent has not collected the exit status yet. |
I | Idle | An idle kernel thread. Not counted in the load average. |
ps -eo pid,ppid,stat,wchan,cmdis the fastest way to see what every process is waiting on. Thewchancolumn names the kernel function it is sleeping in.- A pile of
Dprocesses on a server almost always means a disk or an NFS mount is stuck.kill -9does nothing to them. - htop shows threads as separate rows when you press
H; a Java process with 200 threads suddenly makes sense.
- PID
- Process ID. A small integer, unique while the process exists, reused later.
- PPID
- Parent process ID. The process that created this one.
- Thread group
- A set of threads sharing one address space. Its ID is what user space calls the PID.
fork and exec: how every process is born
Linux makes a new process in two steps that are deliberately separate: fork copies the current process, and exec replaces the copy's program with a new one. Almost every process on the machine was created this way by its parent.
sequenceDiagram
autonumber
participant Sh as Shell (PID 500)
participant K as Kernel
participant C as Child (PID 501)
Sh->>K: fork()
K->>C: create a copy of the shell
K-->>Sh: returns 501
K-->>C: returns 0
C->>K: execve("/bin/ls", argv, envp)
K->>C: throw away the shell image, load ls
Sh->>K: wait4(501)
Note over Sh: sleeping in S
C->>K: exit_group(0)
K-->>Sh: wait returns, status 0
ls. Between steps 4 and 5 the child is still a copy of bash; it could set up redirections before exec. That gap is what makes the two-step design useful.fork
fork creates a child that is a near-exact copy of the parent: same code, same memory contents, same open file descriptors, same signal handlers. It returns twice. In the parent it returns the child's PID; in the child it returns 0. That is how the code after it knows which one it is.
Copying a whole address space would be slow, so the kernel cheats with copy-on-write: both processes share the same physical pages, marked read-only, and a page is copied only when one of them writes to it. A fork of a 1 GB process copies page tables, not gigabytes. The details are in the page faults section below.
Open file descriptors are shared too, and they share the file offset. Two processes writing to the same descriptor after a fork will interleave correctly rather than overwrite each other, which is why redirecting a shell pipeline's output works.
exec
execve throws away the current program and loads a new one into the same process. The PID stays. Open file descriptors stay unless they were opened with O_CLOEXEC. Signal handlers are reset to default. The memory is gone: new code, new stack, new heap.
The kernel reads the file, checks the first bytes to pick a loader (ELF for binaries, #! for scripts, which runs the interpreter with the script as its argument), maps the segments into memory, and jumps to the entry point. For a dynamically linked program the entry point is actually the dynamic loader, which maps libc and the other libraries before calling main.
The reason for two calls instead of one is the gap between them. The child can change the environment before it loads the program: redirect standard output to a file, close descriptors, change directory, drop privileges, join a container namespace. Then exec. The new program starts with everything already arranged.
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <stdio.h>
int main(void) {
pid_t pid = fork();
if (pid == 0) {
// child: redirect stdout to a file, then become ls
int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, 1); // fd 1 now points at out.txt
close(fd);
execlp("ls", "ls", "-l", (char *) NULL);
perror("execlp"); // only reached if exec failed
_exit(127);
}
// parent: wait for the child and read its exit status
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status))
printf("ls exited with %d\n", WEXITSTATUS(status));
return 0;
}
That is exactly what the shell does for ls -l > out.txt. A pipeline is the same trick with a pipe's two ends dup'ed onto fd 1 of one child and fd 0 of the next.
wait, zombies and orphans
When a process exits, its memory and files are released at once, but its entry in the process table stays until the parent asks for the exit status with wait. Until then it is a zombie: a PID and an exit code, nothing else. Zombies take no memory to speak of, but they take a PID, and a parent that forks forever and never waits will run the table dry.
If the parent dies first, the child is an orphan and the kernel reparents it to PID 1, or to the nearest ancestor that declared itself a subreaper with prctl. PID 1 waits for its adopted children, which is how zombies eventually clear. This is also why a container whose PID 1 is a plain application can fill with zombies: the app never calls wait for children it did not know it had. tini and docker run --init exist to be a proper PID 1.
- Every shell command is a fork, some setup, and an exec.
strace -f bash -c lsshows all three. - Android's Zygote is a process with the runtime and framework already loaded; every app is a fork of it, so app startup skips the loading. The app launch article shows that fork.
- Redis forks to write its snapshot: the child walks a frozen copy of the data while the parent keeps serving, and copy-on-write keeps the cost to the pages that change.
posix_spawncombines fork and exec in one call and is faster for large processes, because it avoids copying the page tables at all.
A zombie cannot be killed; it is already dead. Sending signals to it does nothing. The fix is to make the parent call wait, or to kill the parent so PID 1 adopts and reaps the zombie. Check the parent's PID with ps -o ppid= -p ZOMBIE_PID.
- fork
- Create a child process that is a copy of the caller. Returns the child's PID to the parent and 0 to the child.
- exec
- Replace the current process's program with another one. Same PID, new memory.
- Zombie
- A process that has exited but whose parent has not yet read its exit status.
- Subreaper
- A process that has asked to adopt orphaned descendants instead of PID 1 doing it.
The scheduler: who runs next
There are more runnable tasks than CPUs, so the scheduler picks. It keeps one run queue per CPU and tries to give every runnable task a fair share of time, weighted by priority.
The fair scheduler was CFS for many years, and since kernel 6.6 it is EEVDF. The idea behind both: track how much CPU each task has had, and run the one that is furthest behind its fair share. A task that just woke up after sleeping is behind, so it runs almost at once, which is what makes interactive programs feel responsive. A task that has been computing for a while is ahead, so it waits. There is no fixed time slice; the scheduler aims for a target latency and divides it among the runnable tasks.
nice shifts a task's weight. It runs from -20 (most CPU) to 19 (least). Each step is roughly 10 percent more or less CPU when the machine is busy, and it does nothing at all when the machine is idle. Real-time classes (SCHED_FIFO, SCHED_RR) sit above all of that: a runnable real-time task always beats a normal one, which is useful for audio and dangerous for anything with a bug.
The load average in uptime and top is the number of tasks that are runnable or in D state, averaged over 1, 5 and 15 minutes. On a 4-CPU machine a load of 4 means every CPU has one thing to do; 8 means tasks are queueing. Because D counts, a machine waiting on a dead NFS server can show a huge load with idle CPUs.
nice -n 10 make -jkeeps a big build from making the desktop stutter.renicechanges a running process.- Kubernetes CPU requests become cgroup weights, which the scheduler uses the same way as nice, but for groups of processes. The cgroups section has the details.
perf schedand/proc/PID/schedstatshow how long a task waited in the queue versus ran.
- Run queue
- The per-CPU list of tasks ready to run.
- nice
- A priority hint from -20 to 19. Higher is nicer, meaning less CPU.
- Load average
- Runnable plus uninterruptible tasks, smoothed over 1, 5 and 15 minutes.
Virtual memory: every process thinks it has the whole machine
A process never sees physical RAM addresses. It sees a private, contiguous address space of its own, and the CPU translates every access through the process's page tables. That indirection is what gives isolation, sharing and lazy allocation all at once.
On x86-64 the usable space is 48 bits, 128 TB for user space and another 128 TB for the kernel at the top. Inside it, a process's memory is laid out in regions. cat /proc/self/maps lists them.
| Region | Contents | Backed by |
|---|---|---|
| text | The program's machine code, read-only | The executable file, shared between all processes running it |
| data and bss | Global variables, initialised and zeroed | The file (data) or fresh zero pages (bss) |
| heap | malloc's small allocations | Anonymous pages, grown with brk |
| mmap area | Shared libraries, big allocations, mapped files | Files or anonymous pages, placed by mmap |
| stack | Local variables and return addresses, grows down | Anonymous pages, extended on demand up to ulimit -s |
| vDSO | Kernel-provided code for fast time calls | One page shared with the kernel |
The bases of the heap, stack and mmap area are randomised at every exec (ASLR) so an attacker cannot guess where anything is.
Page tables
Memory is managed in pages, 4 KB each. The page tables map each virtual page number to a physical one, plus permission bits: readable, writable, executable, present, accessed, dirty. With 48-bit addresses a flat table would be enormous, so it is a tree, four levels deep (five on machines that enable it). Most of the tree is empty and never allocated.
flowchart TD
V["Virtual address from the program"] --> T{"TLB has it?"}
T -- yes --> P["Physical address, done in a cycle"]
T -- no --> W["MMU walks four levels of page tables"]
W --> E{"Entry present?"}
E -- yes --> C["Cache it in the TLB, continue"]
E -- no --> F["Page fault: trap into the kernel"]
F --> H["Kernel maps a page or kills the process"]
The MMU in the CPU does the translation in hardware. Walking four levels for every access would be far too slow, so the TLB, a small cache of recent translations, catches nearly all of them. A context switch to another process changes the page tables, which is part of why context switches are expensive: the TLB entries for the old process are no longer valid.
Because the mapping is per process, the same physical page can appear in many address spaces. libc is loaded once into RAM and mapped into every process on the machine. A file mapped with MAP_SHARED is one set of pages that every mapper sees, which is the basis for shared memory in the IPC article. MAP_PRIVATE gives a copy-on-write view instead.
pmap PIDor/proc/PID/mapsshows every region with its permissions and backing file. It is how you find out what a mystery 2 GB of virtual size actually is.- Databases such as Postgres and Oracle ask for huge pages (2 MB) for their buffer pools, so a 64 GB pool needs tens of thousands of TLB entries instead of millions.
- Transparent huge pages do the same automatically, and Redis and MongoDB documentation both recommend turning them off because the background compaction causes latency spikes.
- Page
- The unit of memory mapping, 4 KB on x86-64 and most ARM systems.
- Page table
- The per-process tree that maps virtual pages to physical ones with permission bits.
- MMU
- Memory management unit. The CPU hardware that walks the page tables.
- TLB
- Translation lookaside buffer. A cache of recent page table lookups.
- ASLR
- Address space layout randomisation. Random base addresses per exec.
Page faults, copy-on-write and swap
A page fault is what happens when a program touches a virtual page that has no valid entry in its page table. The CPU traps into the kernel, and the kernel decides what that access meant. Most page faults are normal and expected; they are how the kernel avoids doing work until it must.
flowchart TD
A["Page fault on address X"] --> B{"Mapped region?"}
B -- no --> S["SIGSEGV: segmentation fault"]
B -- yes --> C{"Why missing?"}
C -- "never touched" --> M1["Minor: hand out a zeroed page"]
C -- "in the page cache" --> M2["Minor: map the cached page"]
M1 ~~~ M3
M2 ~~~ M3
C -- "write to a COW page" --> M3["Minor: copy the page, make it writable"]
M3 ~~~ MJ
C -- "on disk or swap" --> MJ["Major: read it in, sleep meanwhile"]
- Minor fault. The data is already in memory, or does not exist yet. The kernel allocates a zero page, or maps a page cache page, or copies a shared page. Microseconds.
- Major fault. The data has to come from disk: a code page of a program not yet read, a file page that was dropped from the cache, or an anonymous page that went to swap. The process sleeps while the I/O happens. Milliseconds on a hard disk, tens of microseconds on NVMe.
- Segmentation fault. The address is not in any region, or the access violates the region's permissions (writing to text, executing the stack). The kernel sends
SIGSEGV.
Demand paging
When a program starts, the kernel does not read the whole binary in. It maps the file and lets page faults pull in the pages that actually run. When malloc asks for 100 MB, the kernel adds 100 MB to the address space and hands out nothing; the pages appear one by one as the program writes to them. This is why a process's virtual size and its resident size differ so much, and why malloc almost never fails even on a machine that could not honour every allocation.
Copy-on-write
After a fork, parent and child point at the same physical pages, and the kernel marks every writable page read-only in both page tables. When either side writes, the CPU faults, the kernel copies just that one page, gives the writer the copy, and makes it writable. Pages nobody writes are never copied. The same mechanism serves MAP_PRIVATE file mappings and the zero page: every untouched page of a fresh allocation maps to one shared page of zeros until it is written.
Swap
When RAM is short, the kernel reclaims pages. File-backed pages that are clean (unchanged since they were read) are simply dropped; they can be read again from the file. Dirty file pages are written back first. Anonymous pages, meaning heap and stack, have no file behind them, so the only way to reclaim them is to write them to the swap area on disk. Touching a swapped page later is a major fault.
vm.swappiness (0 to 200, default 60) tunes how readily the kernel swaps anonymous pages rather than dropping file pages. A machine with no swap at all cannot reclaim anonymous memory, so under pressure it goes straight from "dropping the page cache" to the OOM killer. Some swap, even a small amount, gives the kernel room to move rarely used pages out.
vmstat 1showssiandso, swap in and out per second. Anything sustained above zero means the machine is thrashing.perf stat -e page-faults,major-faultsaround a command tells you how much of its startup was reading code from disk.- zram on Android, Chrome OS and Fedora is swap into a compressed region of RAM, which is faster than a disk and roughly doubles usable memory for compressible data.
- Redis snapshots rely on copy-on-write; a heavily written dataset during a save can nearly double memory use as pages get copied.
- Page fault
- A trap raised when a virtual page has no valid mapping. Minor if no disk I/O is needed, major if it is.
- Demand paging
- Mapping memory lazily, one page at a time, when first touched.
- Copy-on-write
- Sharing a page read-only and copying it only when someone writes.
- Anonymous memory
- Pages with no file behind them: heap, stack, private allocations. Only swap can hold them out of RAM.
Allocation and measurement: what a process really uses
malloc is a user space allocator that gets big regions from the kernel and hands out small pieces. Because the kernel is lazy about backing pages, the numbers that describe memory use are less obvious than they look.
How malloc gets memory
glibc's allocator uses two system calls. For small requests it grows the heap with brk and carves pieces out of it. For large ones, 128 KB and up by default, it calls mmap for a private anonymous region and returns the whole thing to the kernel on free. Small freed pieces are kept for reuse and rarely given back, because the heap can only shrink from the top. A program whose heap grew to 2 GB and then freed most of it can still show 2 GB resident. Alternative allocators like jemalloc and tcmalloc are better at returning memory and at avoiding lock contention between threads.
Overcommit
Because pages are only backed when touched, the kernel usually promises more memory than it has. This is overcommit, controlled by vm.overcommit_memory: 0 refuses only obviously absurd requests (the default), 1 never refuses, 2 keeps total commitments under a fixed budget of swap plus a share of RAM. Overcommit is what lets a 20 GB process fork on a machine with 24 GB, since the child will touch very little. It is also why running out of memory is discovered at page fault time, in the middle of some unrelated write, and not at malloc.
The numbers
| Metric | Counts | Good for |
|---|---|---|
| VSZ (virtual size) | Every mapped byte, touched or not, including libraries and reserved regions | Almost nothing. A Java process with a 4 GB VSZ may be using 200 MB. |
| RSS (resident set size) | Pages actually in RAM for this process, including shared library pages counted in full | A quick view of one process. Overstates the total when many processes share libraries. |
| PSS (proportional set size) | RSS, but each shared page divided by the number of sharers | Summing across processes to get a true total. In /proc/PID/smaps_rollup. |
| USS (unique set size) | Pages only this process has | How much would be freed if it exited. |
At the machine level, free -h is the summary, and its columns confuse everyone once. used is application memory. buff/cache is the page cache and kernel buffers, which the kernel will give up the moment an application needs it. free is memory nobody has touched, and on a healthy busy server it is small, because unused RAM is wasted RAM. The column that matters is available: an estimate of how much a new program could take without swapping. /proc/meminfo is where all of it comes from.
- "Linux ate my RAM" is the classic complaint from someone reading the
freecolumn instead ofavailable. - Java's
-Xmxcaps the heap, but RSS is heap plus metaspace, thread stacks, JIT code and native buffers. Container limits that equal-Xmxget the process OOM-killed. - Redis, Facebook and FreeBSD use jemalloc for its fragmentation behaviour and per-thread arenas.
- smem prints PSS and USS per process, the honest way to rank memory users on a host running many similar processes.
- brk
- The system call that moves the end of the heap. glibc uses it for small allocations.
- Overcommit
- The kernel granting more virtual memory than physical memory plus swap, betting most of it is never touched.
- Page cache
- File contents kept in RAM by the kernel after a read or before a write. Shown as
buff/cache.
The OOM killer: what happens when memory runs out
When the kernel needs a page and has nothing left to reclaim, no cache to drop and no swap space to write to, it kills a process to get memory back. That is the out-of-memory killer. It is a last resort, and it is loud.
flowchart TD
A["Page fault needs a free page"] --> B["Reclaim: drop clean cache, write dirty pages, swap out"]
B --> C{"Got one?"}
C -- yes --> D["Carry on"]
C -- no --> E["Score every process: memory used, adjusted by oom_score_adj"]
E --> F["SIGKILL the highest score"]
F --> G["Log to dmesg: Out of memory: Killed process ..."]
The score is mostly the process's RSS plus swap use, so the biggest process is the usual victim. /proc/PID/oom_score_adj shifts it, from -1000 (never kill this) to 1000 (kill this first). systemd sets it for services with OOMScoreAdjust=, and sshd sets its own to -1000 so you can still log in. The kill is SIGKILL, so the process gets no chance to clean up, and the only evidence is a block in dmesg listing every process's score and the one chosen.
With cgroups, the same thing happens inside one container when it hits its memory.max, even if the host has plenty free. The kernel kills a process inside the cgroup. Docker reports it as exit code 137 (128 plus signal 9) and Kubernetes as OOMKilled. This is the most common way a container dies in production, and the fix is usually a limit that matches what the program really uses, measured with RSS over time rather than guessed.
- Kubernetes sets
oom_score_adjper pod from its quality of service class, so best-effort pods die before guaranteed ones. - systemd-oomd and earlyoom are user space daemons that act before the kernel does, using pressure stall information to kill early while the machine is still responsive.
dmesg -T | grep -i "killed process"is the first command to run when a service disappeared with no log line.
Before the OOM killer fires, a machine under memory pressure spends a long time thrashing: dropping and rereading cache, swapping in and out. It looks like the disk is slow and the CPU is idle. vmstat's si/so and /proc/pressure/memory tell you it is memory. A machine with no swap skips the thrashing and goes straight to killing, which is sometimes the better behaviour for a server.
- OOM killer
- The kernel routine that kills a process when there is no memory left to reclaim.
- oom_score_adj
- A per-process adjustment from -1000 to 1000 that makes it less or more likely to be chosen.
- Exit code 137
- 128 plus 9: the process was killed by SIGKILL, in a container almost always by the OOM killer.
Signals: asynchronous messages to a process
A signal is a small integer delivered to a process to tell it something happened: the user pressed Ctrl-C, a child exited, it touched bad memory, someone asked it to stop. It carries no data beyond the number. It interrupts whatever the process was doing.
| Signal | Number | Default action | Sent when |
|---|---|---|---|
SIGHUP | 1 | Terminate | Terminal closed. Daemons reuse it to mean "reload config". |
SIGINT | 2 | Terminate | Ctrl-C in the terminal. |
SIGQUIT | 3 | Core dump | Ctrl-\ in the terminal. |
SIGKILL | 9 | Terminate | kill -9, the OOM killer. Cannot be caught, blocked or ignored. |
SIGSEGV | 11 | Core dump | Bad memory access. |
SIGPIPE | 13 | Terminate | Writing to a pipe or socket nobody reads. |
SIGALRM | 14 | Terminate | A timer set with alarm expired. |
SIGTERM | 15 | Terminate | Plain kill, docker stop, systemd stop. The polite request. |
SIGCHLD | 17 | Ignore | A child exited or stopped. |
SIGCONT | 18 | Continue | Resume a stopped process. fg and bg. |
SIGSTOP | 19 | Stop | Pause. Cannot be caught. SIGTSTP (20) is the Ctrl-Z version and can be. |
SIGUSR1, SIGUSR2 | 10, 12 | Terminate | Whatever the program decides. |
Each signal has a default action: terminate, terminate with a core dump, ignore, stop, or continue. A process can change that for most signals by installing a handler, a function the kernel will call, or by setting the signal to be ignored. SIGKILL and SIGSTOP are the exceptions: the kernel handles them and the process gets no say, which is what makes kill -9 reliable.
How a signal is delivered
sequenceDiagram autonumber participant S as Sender (kill, terminal, kernel) participant K as Kernel participant T as Target process S->>K: kill(pid, SIGTERM) K->>K: permission check, set the pending bit K->>T: wake it if sleeping in S Note over T: next return to user mode K->>T: run the handler on the user stack T->>T: handler sets a flag, returns K->>T: resume where it was interrupted
- Something calls
kill, or the kernel raises the signal itself (a fault, a child exit, a terminal key). - The kernel checks permission (same user, or root) and marks the signal pending on the target. If the target is asleep in
S, it is woken; a process inDis not. - The next time the target returns to user mode, from a system call or a context switch, the kernel notices the pending signal.
- If the action is default, the kernel does it: terminates, stops, or ignores. If there is a handler, the kernel sets up a frame on the process's stack and jumps to the handler.
- The handler runs in the process, as ordinary user code. When it returns, the kernel restores the interrupted state and the process carries on, or, if it was in a slow system call, that call returns
EINTRunless the handler was installed withSA_RESTART.
Standard signals are not queued. If the same signal is sent five times before the process gets to handle it, the handler runs once. Real-time signals (SIGRTMIN and up) do queue, and can carry a value.
Writing a handler
The handler can run between any two instructions of the program, including in the middle of malloc or printf. Calling those from a handler can deadlock or corrupt the heap. Only async-signal-safe functions are allowed: write, _exit, signal, and a fixed list of others. The safe pattern is to set a flag and return.
#include <signal.h>
#include <unistd.h>
static volatile sig_atomic_t stop = 0;
static void on_term(int sig) {
stop = 1; // nothing else: no printf, no malloc
}
int main(void) {
struct sigaction sa = {0};
sa.sa_handler = on_term;
sa.sa_flags = SA_RESTART; // restart interrupted reads instead of EINTR
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGINT, &sa, NULL);
while (!stop) {
// do work, check the flag between units
}
// clean up: flush, close, remove the pid file
return 0;
}
Servers that would rather not be interrupted at all use signalfd, which turns signals into bytes readable from a file descriptor, so they can be handled in the normal event loop next to sockets. In a shell script, trap 'cleanup' TERM INT does the same job.
When a process is killed by a signal, the shell reports it as exit code 128 plus the signal number: 130 for Ctrl-C, 137 for SIGKILL, 143 for SIGTERM. That is how you tell "exited with an error" from "was killed".
docker stopsendsSIGTERM, waits 10 seconds, thenSIGKILL. Kubernetes does the same with a 30-second grace period. A server that ignoresSIGTERMalways loses in-flight requests.- nginx reloads its configuration on
SIGHUPand shuts down gracefully onSIGQUIT;nginx -s reloadis justkill -HUPon the master's PID. - PID 1 in a container gets special treatment: the kernel ignores a signal sent to PID 1 unless PID 1 has a handler for it. A Node or Python app running as PID 1 without a handler cannot be stopped with
SIGTERMat all, which is whydocker stoptakes 10 seconds on such containers.tinifixes it. - Go converts
SIGSEGVinto a panic with a stack trace, andSIGPIPEon stdout into a clean exit.
kill -9 should be the second thing you try, not the first. The process gets no chance to flush buffers, remove lock files, or tell its peers goodbye, so it can leave a database in recovery or a stale .pid file that stops the next start. Send SIGTERM, wait, then escalate.
- Signal
- A numbered asynchronous notification delivered to a process by the kernel.
- Handler
- A function the process registers with
sigactionto run when a signal arrives. - Pending
- Sent but not yet delivered, because the process has not returned to user mode or has the signal blocked.
- Async-signal-safe
- A function that is safe to call from a handler because it cannot be mid-way through in the interrupted code.
Recap
- A process is a
task_struct: address space, file table, credentials, signal table, scheduler state and a PID. Threads are processes that share the first three. - States: R runnable, S sleeping, D waiting on I/O and unkillable, T stopped, Z zombie.
forkcopies a process and returns twice;execreplaces its program and keeps the PID and open files. The gap between them is where redirections and namespaces are set up.- A zombie is an exited child nobody has
waited for. Orphans are adopted by PID 1 or a subreaper. - The scheduler runs the task furthest behind its fair share.
niceshifts the share; load average counts runnable plus D tasks. - Every process has a private virtual address space; page tables map it to RAM and the TLB caches the mapping. The same physical page can appear in many processes.
- Page faults are normal. Minor ones map a page that is already in memory; major ones wait for disk. Allocation is lazy: pages appear on first touch.
- Copy-on-write makes
forkcheap and shares pages until someone writes. - Only anonymous memory needs swap; file pages can just be dropped and reread.
mallocis user space code overbrkandmmap. VSZ is nearly meaningless, RSS overcounts shared pages, PSS is the honest number, andavailableis the column to read infree.- The OOM killer fires when reclaim fails, picks by score, sends SIGKILL, and writes to
dmesg. In a container it fires at the cgroup limit: exit code 137. - Signals are numbered notifications with default actions. Handlers run on the way back to user mode and may only call async-signal-safe functions. SIGKILL and SIGSTOP cannot be caught.
- SIGTERM first, SIGKILL later. PID 1 ignores signals it has no handler for.
Questions
Try answering each one out loud before opening it. Lead with the one-line answer, then a couple of points, then one extra detail.
What is the difference between a process and a thread on Linux?
Both are tasks created with clone; a thread is one that shares its address space, file descriptor table and signal handlers with its creator, while a process gets copies.
- Each thread has its own stack, registers and kernel task ID;
getpidreturns the shared thread group ID. - The scheduler sees only tasks; it does not care which are threads.
ls /proc/PID/task lists a process's threads, and htop shows them with H.
Explain fork and exec, and why they are two calls rather than one.
fork duplicates the calling process and returns the child's PID to the parent and 0 to the child; exec replaces the calling process's program with a new one, keeping the PID and open descriptors. Keeping them separate lets the child set up redirections, close files, change directory or enter namespaces before the new program starts.
- The shell implements
ls > out.txtas fork, open anddup2in the child, then exec. - Copy-on-write makes the fork cheap even for a large process.
posix_spawn exists for the common case where you want both at once with no setup, and it avoids copying page tables entirely.
What is a zombie process, and how do you get rid of one?
A process that has exited but whose parent has not called wait to collect its exit status; it holds only a PID and an exit code. You cannot kill it because it is already dead. Make the parent call wait, or kill the parent so PID 1 adopts and reaps it.
- Zombies matter because they hold PIDs, and
pid_maxis finite. - An orphan is different: its parent died, so PID 1 or a subreaper adopts it.
A container whose PID 1 is a plain application accumulates zombies because it never reaps; that is what tini and docker run --init are for.
What does state D mean, and why can't you kill a process in it?
Uninterruptible sleep: the process is waiting on I/O inside the kernel, typically disk or a network filesystem, and the kernel will not deliver signals until that operation returns.
- It is a design choice so a driver is never interrupted half way through a device operation.
- A pile of
Dprocesses with idle CPUs and a high load average usually means a stuck disk or NFS mount.
D tasks count in the load average, which is why load can be 50 on a machine doing nothing.
How does the scheduler decide who runs next, and what does nice change?
It keeps a run queue per CPU and runs the task that has received the least CPU relative to its fair share; nice changes the weight that defines the share, from -20 to 19, about 10 percent per step.
- A task that just woke is behind, so it runs almost immediately; that is what makes interactive programs responsive.
- Real-time classes always beat normal tasks and are dangerous with bugs.
The scheduler was CFS for years and is EEVDF since kernel 6.6; both are built on the same fair-share idea.
What is virtual memory, and what does it buy?
Each process sees a private address space that the CPU translates through per-process page tables to physical RAM; the indirection gives isolation, sharing of the same physical page in many processes, and lazy allocation.
- Only the kernel can edit page tables, so a process cannot map another's memory.
- libc is in RAM once and mapped into every process; a file mapped
MAP_SHAREDis shared memory.
On x86-64 a process has 128 TB of virtual space, nearly all of it unmapped, so page tables are a sparse four-level tree rather than a flat array.
What is the TLB, and why do context switches hurt it?
A small hardware cache of recent virtual-to-physical translations, so most accesses avoid the four-level page table walk; a context switch changes page tables, so the old process's entries become useless and the new process starts with misses.
- Huge pages (2 MB) cover more memory per TLB entry, which is why databases use them for buffer pools.
- Address space IDs let newer CPUs keep some entries across switches.
Threads of one process share page tables, so switching between them does not flush the TLB.
Minor versus major page fault?
A minor fault is resolved from memory: a first-touch zero page, a page cache hit, or a copy-on-write copy. A major fault needs disk: reading a code page, a dropped file page, or a swapped-out page, and the process sleeps while it happens.
- Minor faults are microseconds and completely normal; a program touching fresh
mallocmemory takes one per page. - Major faults are what make a cold start slow and a thrashing machine crawl.
perf stat -e page-faults,major-faults around a command splits the two.
How does copy-on-write make fork cheap?
After fork, parent and child share every physical page, marked read-only in both page tables; a write from either side faults, the kernel copies that one page for the writer, and pages nobody writes are never copied.
- The fork itself copies only the page tables.
- The same trick backs
MAP_PRIVATEmappings and the shared zero page for untouched allocations.
Redis relies on it for snapshots, and pays in extra memory when the parent writes heavily during the save.
Why does malloc almost never fail, and where does the failure show up instead?
Because the kernel overcommits: it extends the address space without backing pages, and pages are only allocated on first write. So the shortage appears as a page fault the kernel cannot satisfy, in the middle of some later write, and ends with the OOM killer rather than a NULL from malloc.
vm.overcommit_memory=2turns this off and makesmallocfail honestly, at the cost of refusing many workable allocations.- This is also why a large process can fork on a machine without room for two copies.
Checking malloc's return value is still right; it just does not protect you from running out of memory.
RSS, PSS, VSZ: which one do you trust?
VSZ counts every mapped byte and is nearly meaningless; RSS counts resident pages but charges shared library pages to every process in full; PSS divides shared pages among their sharers and is the number that sums correctly across a machine.
- For one process in isolation RSS is fine; for ranking many similar processes use PSS or USS.
/proc/PID/smaps_rolluphas all of them;smemprints them per process.
In free, read available, not free: the page cache is reclaimable and the kernel deliberately keeps RAM full.
Why does a machine with no swap behave differently under memory pressure?
Only anonymous memory (heap, stack) needs swap to be reclaimed; without swap the kernel can only drop file pages, so once the page cache is gone it goes straight to the OOM killer instead of paging rarely used heap out.
- With swap, the same machine thrashes first: slow but alive, sometimes for a long time.
- A small swap lets idle anonymous pages leave RAM without enabling heavy thrashing.
Android and Fedora use zram, swap into compressed RAM, to get the benefit without a disk.
How does the OOM killer choose its victim, and how do containers change the picture?
It scores each process mostly by RSS plus swap, shifts the score by oom_score_adj, and sends SIGKILL to the highest; inside a cgroup with memory.max the same logic runs when that group alone hits its limit, even if the host has memory free.
- Docker reports the result as exit code 137 and Kubernetes as OOMKilled.
- Kubernetes sets
oom_score_adjfrom the pod's QoS class, so best-effort pods die first.
The victim is not necessarily the process whose allocation failed; it is whoever scores highest.
When is a signal actually delivered?
Not when it is sent: the kernel marks it pending and runs the handler or default action the next time the process returns to user mode, from a system call or a context switch. A process sleeping in S is woken for it; one in D is not.
- Standard signals do not queue: five SIGTERMs before delivery run the handler once.
- A slow system call interrupted by a handler returns EINTR unless SA_RESTART was set.
SIGKILL and SIGSTOP never reach the process at all; the kernel acts on them itself.
What can you safely do inside a signal handler?
Only call async-signal-safe functions, such as write and _exit, and set a flag of type volatile sig_atomic_t; the handler may have interrupted malloc or printf half way, so calling them again can deadlock or corrupt state.
- The usual pattern is to set a flag and let the main loop act on it.
signalfdor a self-pipe turns signals into file descriptor events for an event loop.
Runtimes like Go and Java install their own handlers and hand you a channel or a hook instead.
Why does docker stop sometimes take exactly 10 seconds?
Docker sends SIGTERM, waits 10 seconds, then SIGKILL; if the container's PID 1 has no SIGTERM handler, the kernel ignores the signal for PID 1, so nothing happens until the SIGKILL.
- PID 1 gets that special treatment so an init process is not killed by a stray signal.
- Fix it with a proper handler, or run under
tiniordocker run --init, which forwards signals and reaps zombies.
The exit code afterwards is 137, the same as an OOM kill, so check dmesg before blaming memory.
What does exit code 143 mean, and 130?
128 plus the signal number: 143 is SIGTERM (15) and 130 is SIGINT (2), so the process was killed by a signal rather than exiting on its own.
- 137 is SIGKILL, from
kill -9or the OOM killer. - 139 is SIGSEGV, a crash.
A program can also return those numbers deliberately, so the convention is strong but not proof.