The Anatomy of a Process
Everything a process "is" lives in one kernel record — identity, parent, state, address space, descriptor table, credentials, accounting and scheduling data — and every tool from ps to top to /proc is a view of those fields.
The problem
Identity and lineage
Every process has a PID, an integer unique among live processes, assigned at creation and recycled after death. It also records its parent PID — the process that forked it — so processes form a tree. On a Linux system the root of the tree is PID 1 (systemd or another init), started by the kernel at boot; everything else descends from it by fork. pstree draws the tree; the shell that launched your server is its parent, and the server’s worker processes are its children.
Lineage has consequences. A parent can wait() on its children and read their exit statuses; nobody else can. When a parent dies before its children, the children are orphaned and re-parented to PID 1 (or to a designated "subreaper" on Linux), which is what reaps them. Signals like SIGHUP on terminal close propagate along the tree via process groups and sessions, which is why closing a terminal kills the jobs started in it unless they were detached with nohup or setsid.
- PID, PPID, process group id, session id — identity and who gets your signals.
- On Linux PIDs are namespaced: a container’s PID 1 is some other number on the host — Process Isolation: One Kernel, Many PID 1s.
The process control block
The kernel’s per-process record is conventionally called the process control block (PCB); in Linux it is struct task_struct, several kilobytes in size; in Windows it is the EPROCESS object. It is the process. When the scheduler switches away from a process it saves the CPU registers into this record; when it switches back it restores them from here. Everything else about the process is either in the record or reachable from a pointer in it.
Conceptually the record has these groups of fields. The list is what a process *is*, and it is the checklist for questions like "what does a fork copy?" (all of it, lazily) and "what does a thread share?" (the address space and descriptor table, not the registers or scheduling state).
- Identity: PID, PPID, group, session; command name.
- State: running / ready / blocked / stopped / zombie, and what it is blocked on — Process States.
- Saved CPU context: registers, instruction pointer, stack pointer, flags — restored on the next context switch.
- Address space: a pointer to the memory descriptor (
mm_struct) — page table root, list of mappings, heap bounds — The Virtual Address Space. - Resources: the descriptor table, current directory, root directory, signal handlers, pending signals, timers, resource limits (
ulimit). - Security context: real/effective/saved uid and gid, supplementary groups; on Linux capabilities, seccomp filters, namespaces, LSM labels.
- Accounting: user and system CPU time, page faults, context switches, I/O bytes, start time.
- Scheduling: policy (normal, real-time, idle), nice value or priority, virtual runtime, the CPU it last ran on, allowed CPUs — The Scheduling Problem.
Descriptors and security context
The descriptor table is a per-process array; the integer you get back from open() or socket() is an index into it, and the entry points to a kernel object (an open file description with its offset and flags, which in turn points to an inode, a socket, a pipe…). Descriptors 0, 1 and 2 are just the conventional first three entries, wired by the parent before exec to a terminal, a file or a pipe. The table has a limit (ulimit -n, often 1024 by default, raisable to hundreds of thousands), and exhausting it is one of the most common production failures: every accept() and open() fails with EMFILE while the process otherwise looks healthy. See File Descriptors.
The security context is what the kernel consults on every permission check. The effective uid decides whether open("/etc/shadow") succeeds; the effective gid and supplementary groups decide group permissions. On Linux, root’s power is further split into ~40 capabilities (CAP_NET_BIND_SERVICE to bind ports below 1024, CAP_SYS_PTRACE to debug other processes, CAP_NET_RAW for raw sockets) so a process can hold one power without holding all of them — containers drop most of them by default. A process cannot raise its own privilege; it can only drop it, or exec a setuid binary that the file system has marked as running with its owner’s identity.
Accounting and scheduling information
The kernel charges CPU time to the process that was running when the tick fired (or, with precise accounting, measures it at every switch) and splits it into user and system time. time ./server prints exactly those two numbers plus wall-clock; a program with high system time is spending its life in syscalls, a program with high user time is computing, and a program whose wall-clock greatly exceeds both is waiting. Involuntary versus voluntary context-switch counts tell you whether it is being preempted or blocking itself.
Scheduling fields are the scheduler’s input. The nice value (−20 to 19 on Unix-style systems) weights how much CPU a process gets under contention; the policy says whether it is a normal time-shared task, a real-time task that preempts everything, or a background task that only runs when nothing else wants to. The CPU affinity mask says which cores it may use — the reason a process can be "at 100%" on a 32-core box and still be the bottleneck is that 100% means one core.
Looking at it: `ps`, `top` and `/proc`
On Linux every field above is readable through /proc/<pid>/: status (identity, state, uids, capabilities, memory summary, thread count, context switches), stat (the raw scheduler numbers), maps (the address space), fd/ (a symlink per open descriptor), limits, cgroup, sched. ps and top are just programs that read these files and format them; there is nothing they know that you cannot read yourself.
$ cat /proc/1304/status Name: server State: S (sleeping) Pid: 1304 PPid: 1201 Uid: 1000 1000 1000 1000 ← real, effective, saved, fs Gid: 1000 1000 1000 1000 FDSize: 256 Threads: 9 VmPeak: 1053120 kB VmRSS: 88412 kB CapEff: 0000000000000000 ← no capabilities: an ordinary user voluntary_ctxt_switches: 48213 nonvoluntary_ctxt_switches: 117 $ ls -l /proc/1304/fd | head -5 0 -> /dev/null 1 -> /var/log/server.log 2 -> /var/log/server.log 3 -> socket:[184227] ← the listening socket 4 -> anon_inode:[eventpoll] ← the epoll instance
Key points
- A process is its kernel record: identity, state, saved registers, address space pointer, descriptor table, credentials, accounting, scheduling data.
- Processes form a tree by parent PID; orphans are re-parented to PID 1, which reaps them.
- The descriptor table is per-process, indexed by small integers, and has a limit whose exhaustion (
EMFILE) is a classic outage. - The security context (uid/gid, Linux capabilities) is checked on every syscall; privilege can only be dropped, never raised, except through setuid executables.
- User vs system CPU time and voluntary vs involuntary switches tell you whether a process is computing, syscalling, waiting or being preempted.
psandtopare formatted views of/proc; read/proc/<pid>/statusandfd/directly when debugging.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why keep all this in one kernel-owned record instead of in the process’s own memory?
Because the process must not be able to change its own uid, its own scheduling priority, or its own limits, and because the scheduler needs the record when the process is not running and its memory may be swapped out.
▸Why a per-process descriptor table rather than global handles?
Isolation: descriptor 5 in your process cannot name another process’s socket, and inheritance across fork gives a clean way to hand resources to children.
▸Why split root into capabilities?
A web server needs to bind port 80, not to load kernel modules; with capabilities it can hold exactly one power and a compromise gains exactly that.
The process table
| PID | PPID | S | USER | %CPU | RSS | FDs | COMMAND |
|---|---|---|---|---|---|---|---|
| 1 | 0 | S | root | 1.0 | 12.0 MB | 42 | /sbin/init |
| 812 | 1 | S | root | 0.0 | 8.0 MB | 8 | sshd -D |
| 1204 | 1 | R | postgres | 30.0 | 243.0 MB | 31 | postgres: writer |
| 2231 | 1 | S | www | 0.0 | 21.0 MB | 1024 | nginx: worker |
| 3410 | 812 | S | dan | 1.0 | 5.0 MB | 12 | -zsh |
| 4102 | 3410 | S | dan | 1.0 | 94.0 MB | 512 | ./server |
| 4180 | 3410 | R | dan | 76.0 | 413.0 MB | 64 | node build.js |
rax 0x3b8afada · rflags 0xf4e5e754
How it fails
What the failure looks like from inside real software.
accept: too many open files— the descriptor table is full because something leaks descriptors;ls /proc/<pid>/fd | wc -lclimbs towardulimit -n.- A process cannot bind port 443 as a non-root user: it lacks
CAP_NET_BIND_SERVICE; the fix is a capability grant or a higher port behind a proxy, not running as root. - A container’s process cannot
ptraceor change the clock although it "is root" inside: the capability was dropped by the runtime. kill -9of a parent leaves its children running as orphans under PID 1, serving traffic the deploy system no longer knows about.- High system time with low user time: the process spends its CPU in syscalls, typically tiny reads/writes or a polling loop, not in your code.