Module 03 · Processes and threads · Lesson 6 of 7
The process control block
Every field the kernel keeps per process and thread: identity, state, saved registers, scheduling data, memory and file pointers, credentials and accounting. Linux's task_struct, PID limits, and reading it from /proc and ps.
Why it matters
When ps shows a worker stuck in state D, when a container hits "too many open files", when a pod dies of its PID limit, or when a kill lands on a reused PID, the answer is in the record the kernel keeps per process. Every field in it can be read with ps or under /proc, and knowing which field answers which question is most of process-level debugging.
The idea
The process control block (PCB) is the kernel's record for one process: everything it needs to stop the process, run something else, and later resume it exactly where it was. The Processes lesson drew it as a box; this lesson opens the box. On Linux the structure is task_struct, defined in include/linux/sched.h, several kilobytes per task before counting the structures it points to.
What the record holds
| Field group | What it holds | Where you read it |
|---|---|---|
| Identity and state | PID, parent PID, thread group ID; running, sleeping (S), uninterruptible (D), stopped (T), zombie (Z) | ps -o pid,ppid,stat; /proc/PID/status |
| CPU context and scheduling | Saved registers, kernel stack pointer; nice value, virtual runtime, affinity | ps -o ni,psr; /proc/PID/sched |
| Memory | Pointer to the memory descriptor: page-table root, mapped regions, resident size | /proc/PID/maps; VmRSS in status |
| Files | Open file table, current and root directory | /proc/PID/fd; FDSize in status |
| Credentials and signals | User and group IDs, capabilities, pending and blocked signals, handlers | Uid, CapEff, SigPnd in status |
| Accounting and containment | CPU time, faults, switch counts, limits, namespaces, cgroup | /proc/PID/stat, limits, ns, cgroup |
One per thread, not one per process
Linux has no separate thread structure. Every thread is a task_struct; a process is a thread group whose members share the memory descriptor, open file table, signal handlers and credentials. The leader's PID is the thread group ID (Tgid), which ps shows as the PID; ps -L and /proc/PID/task/ list the individual threads. clone creates a thread with flags that say "share the mm, the files and the signal handlers", so its record is mostly pointers to shared structures plus what is truly its own: registers, a 16 KB kernel stack, state and scheduling data.
The process table and PID limits
The kernel reaches every task through a PID lookup structure and a list of all tasks, together the process table. PIDs come from a counter that wraps at /proc/sys/kernel/pid_max: 32,768 on older defaults, 4,194,304 where systemd raises it. kernel.threads-max caps tasks system-wide, ulimit -u per user and cgroup pids.max per container (the pod PID limit in Kubernetes); a thread leak hits one of these as fork: Resource temporarily unavailable with memory to spare. A zombie is a task_struct kept after exit, holding only the PID and exit status until the parent calls wait. Because PIDs wrap, a stored PID can come to name a different process; pidfd_open gives a handle that cannot.
What a context switch saves
The Context switching lesson covers the switch itself; what it saves is a narrow slice of the PCB. On entry to the kernel the user registers are pushed onto the task's kernel stack; the switch stores the kernel stack pointer and callee-saved registers in thread_struct, and floating-point state only if the task used it. The scheduler then compares the two tasks' mm pointers: another thread of the same process shares the descriptor, so page tables and TLB are left alone; a different process means loading a new page-table root.
Reading it
- 1
/proc/PID/status
State, Tgid, PPid, Uid, VmRSS, Threads, FDSize, SigPnd, CapEff and the voluntary and nonvoluntary switch counts. Start here. - 2
/proc/PID/maps and smaps
Every mapped region with permissions and backing file, straight from the memory descriptor; smaps adds resident size per mapping. - 3
/proc/PID/fd
One symlink per open descriptor: a path, a socket or pipe inode, or an epoll instance. Counting the entries is the descriptor-leak check. - 4
ps -o
ps -o pid,ppid,stat,ni,psr,rss,nlwp,wchan,comm prints PCB fields as columns; wchan names the kernel function a sleeping task waits in, the first clue for a D state.
Trade-offs
A rich record costs memory on every task: a task_struct plus its kernel stack is around 20 KB, so 10,000 threads hold roughly 200 MB of kernel memory before their user stacks. Accounting in the PCB is what makes top, cgroup limits and the out-of-memory killer's choice possible, at the cost of counters updated on every tick and fault. The failure modes are operational: zombies piling up under a parent that never calls wait, PID exhaustion from a thread leak, a kill landing on a recycled PID, and a D state hiding a hung NFS mount.
In an interview
Use the record to explain what a symptom means: "The STAT column comes straight from the task_struct; D means uninterruptible sleep inside a system call, almost always I/O, so I would read wchan and check the storage before blaming the application." Or on thread leaks: "Threads are task_structs sharing one memory descriptor, so a leak shows up as a climbing Threads count in /proc/PID/status before RSS moves."
Check yourself
ps shows a PID in state Z for hours with RSS 0, and such entries multiply daily. What is it, and what does it cost?