ProcessesPIDPPIDPCBtask_structprocess table

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.

ConceptualLinuxUnix-style
▶ InteractiveInterview question
Progress

The problem

The kernel juggles hundreds of processes and must be able to answer, for any of them and in nanoseconds: who owns it, what may it access, which files has it open, is it runnable, how much CPU has it used, and where are its registers? Where is that information, and what is in it?

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

Conceptual

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

Linux

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`

Linux

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.

A trimmed `/proc/<pid>/status`
$ 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.
  • ps and top are formatted views of /proc; read /proc/<pid>/status and fd/ 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

The process table
What `ps` / `top` shows is a projection of each process control block (PCB) the kernel keeps.
PIDPPIDSUSER%CPURSSFDsCOMMAND
10Sroot1.012.0 MB42/sbin/init
8121Sroot0.08.0 MB8sshd -D
12041Rpostgres30.0243.0 MB31postgres: writer
22311Swww0.021.0 MB1024nginx: worker
3410812Sdan1.05.0 MB12-zsh
41023410Sdan1.094.0 MB512./server
41803410Rdan76.0413.0 MB64node build.js
R running/runnable · S interruptible sleep (waiting for an event) · D uninterruptible sleep (usually disk I/O). Click a row to open its PCB.
PCB · PID 4102Conceptual
Identity
pid 4102 · ppid 3410 · uid dan · state sleeping
Address space
page-table root 0x1020000 · RSS 94.0 MB · text/data/heap/stack + libs
Registers (saved at last switch)
rip 0x37bc245f · rsp 0xccd0153f
rax 0x3b8afada · rflags 0xf4e5e754
Open descriptors
0 tty · 1 tty · 2 tty · 3 socket:LISTEN … (512 total)
Signals
mask: SIGPIPE · pending: none
Scheduling
class CFS nice 0 · last CPU 2 · 1% recent
1/24 · tickSimulated

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 -l climbs toward ulimit -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 ptrace or change the clock although it "is root" inside: the capability was dropped by the runtime.
  • kill -9 of 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.