Kernel, Traced
Read the path. Trace the path. Change the path.
Linux, Observed builds a system and watches it from the outside. This series goes inside. Each chapter takes one thing the kernel does — open a file, create a process, pick the next task, fault a page in, send a packet, verify a BPF program — and follows it through the actual source, file and function, from the system-call boundary to the point where the work happens. Then it does two things most kernel write-ups don’t:
- Trace it live. ftrace’s function graph tracer and a few BPF probes, pointed at the exact functions you just read, on a kernel you built. The call graph on your screen is the call graph in the source, with timings.
- Change it. Every chapter ends with a small modification on that path — a BPF program attached at a hook you can now locate precisely, or a kernel patch of a few lines that adds a tracepoint or alters a decision — built, booted in QEMU, and observed. You haven’t understood a path until you’ve altered it and watched the alteration happen.
The kernel is a 30-million-line codebase. The trick is that any one path through it is a few dozen functions, and the paths you care about are the ones your programs exercise every millisecond. Read twelve of them properly and the rest of the tree stops being a wall.
Format of every chapter
- The question — the one-sentence thing we want to know. “When a process calls connect(), where exactly does the kernel decide where the SYN goes?”
- The path — the annotated call chain:
file.c:function()at each step, what it does in one line, and the one data structure it works on. Kernel version pinned (6.12 LTS unless stated); line numbers avoided because they rot, function names kept because they don’t (much). - Trace it — a step-by-step lab with the
function_graphfilter or bpftrace script for that path, and what to look for. - Change it — the patch or BPF program, with the prediction you should test.
- The takeaway — the one design idea the path embodies, stated once.
Prerequisites
Start with chapter 0, which builds the kernel and virtual machine every experiment uses. You can read C and aren’t afraid of a macro. You have the source tree checked out at the tag you’re running, and cscope or an LSP (clangd with compile_commands.json from scripts/clang-tools/gen_compile_commands.py) so you can jump to definitions. That last one is not optional; reading the kernel without jump-to-definition is reading it blind.
The map
Keep one text file, map.txt, throughout: every function you’ve read, one line each — file, name, what it does, which chapter. By chapter 12 it will be the most useful kernel document you own, because it’s the kernel you have walked. There’s a version at /kernel/map as a starting point and a cross-check.
The chapters name the kernel version they were checked against.
Chapters
- 0Set up the machineBuild Linux 6.12 with tracing turned on, boot it under QEMU, and check everything before you start.Draft
- 1A system call, from
syscalltoreturnThe instruction, the entry stub, the dispatch, the work, and the long way back to user mode.Draft - 2open(): the path walkFrom a string to a struct file: namei, the dentry cache, mount crossing, permission checks, and the filesystem's turn.Draft
- 3fork() and exec(): making a processcopy_process line by line, what a new task shares and what it owns, and the moment exec throws it all away.Draft
- 4The scheduler picks the next taskschedule(), the runqueue, EEVDF as completed in 6.12, the context switch, sched_ext, and a prediction you can check.Draft
- 5A page faultFrom the CPU's #PF exception to a mapped page: the fault handler, the VMA lookup, the three kinds of fault, and a patch that makes copy-on-write visible.Draft
- 6The page cache and readaheadread() from the top: the folio lookup, the readahead window, the filesystem's read path, and the block layer underneath.Draft
- 7connect(): where the SYN is bornFrom the connect() system call to the first segment on the wire: every function on the path, the places policy can speak, and a BPF program that moves the destination before the route exists.Draft
- 8A packet out: send() to the NICtcp_sendmsg to ndo_start_xmit: segmentation, the egress BPF hook, netfilter, the qdisc, the driver ring, and one program that drops a flow.Draft
- 9A packet in: NIC to recv()Interrupt, NAPI, XDP, GRO, the protocol demux, the socket queue and the wakeup, with the two earliest places a program can run.Draft
- 10bpf(): the verifierHow a BPF program goes from bytes to running kernel code: the load, the verifier's symbolic execution, the helper checks, the JIT, and a program you fix one line at a time.Draft
- 11LSM hooks and cgroups: where policy attachessecurity_* calls, static-call slots, BPF LSM; css_set, cgroup migration, and how a program attached to a parent runs for every child.Draft
- 12Signals, timers and RCU: three mechanisms every path usesHow a signal is queued and delivered on the way back to user mode; how nanosleep() turns into an hrtimer and a schedule(); and what synchronize_rcu() is actually waiting for.Draft
- ·The mapEvery function read in the series, one line each: file, name, what it does, chapter. The kernel you have walked.