Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion LAYOUT.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ linux-kernel-internals/
├── kxray/ # Python: trace, BTF, /proc and dump analysis
│ ├── btf/ # BTF reader: types, fields, offsets, holes, type tags
│ ├── trace/ # ftrace function_graph, function and trace_event parsers
│ ├── proc/ # /proc and /sys snapshot parsers
│ ├── proc/ # /proc and /sys snapshot parsers, and the ABI stability ledger
│ ├── source/ # kernel tree navigation, Kconfig, MAINTAINERS
│ ├── models/ # the shared model everything else renders
│ ├── replay/ # recorded Tier 1 session playback
Expand Down Expand Up @@ -75,6 +75,12 @@ Three of the five ride along in BTF as a `type_tag` record. `__iomem` does not,

`kxray/models.py` holds `TraceLog` under all three parsers. The banner at the top of a trace says how many events the kernel produced and how many survived, and a trace where those differ has holes in it that nothing in the body of the file admits to, so it is read rather than skipped. The per tracer subclasses add only what is actually different, which is the list of things on the lines.

`kxray/proc/` is five readers for four file shapes, because `/proc` has fewer shapes in it than it has files. `keyed.py` reads `Key: value`, which is `meminfo` and a process status file. `percpu.py` reads a label and one count per CPU, which is `interrupts` and `softirqs`. `maps.py` reads one record per line with positional columns. `pidstat.py` reads the single line file, which is its own shape only because of what a command name is allowed to contain. `version.py` reads one line of free text with two useful things in it.

`kxray/proc/stability.py` is the reason the package is shaped that way rather than being five loose functions. Every reader returns something carrying a `Promise` saying what the kernel tree says about the file it read, taken from `Documentation/ABI` and citing the file that makes the claim. That matters because of a number: on 7.2.2 there are 685 files under `Documentation/ABI` and six of them describe a path in `/proc`, and not one of those six is a file anybody reads. `meminfo`, `interrupts`, `maps` and `/proc/<pid>/stat` are all undocumented, which is not the same as unstable and is worth telling a reader before they lean on one. Two paths are worse than undocumented and this project reads both: the closing section of that README names Kconfig, calling out `/proc/config.gz`, and kernel symbols, which is `/proc/kallsyms`, as things that must not under any circumstances be considered stable.

`kxray/proc/pidstat.py` is a file of its own for one reason, and the reason is a real capture. The kernel prints the command name in brackets and does not escape it, so a process whose executable is called `od) d ma` prints as `37 (od) d ma) R 1 0 ...`, and a whitespace split puts the state two fields to the left of where it belongs and reports a process as being in a state that does not exist. `corpora/proc/tier0/odd-comm-stat.txt` is that line off the pinned kernel. The parser takes the first opening bracket and the last closing bracket, which is what `procps` has done for decades, and keeps what the naive split would have said so that a lesson can show both answers rather than assert that the trap is real.

`kxray/layout.py` is the arithmetic that turns a tree of frames into rectangles. It is in `kxray` for the same reason. A widget and an animation of the same trace call it and get the same answer, so the wide box is in the same place in both.

`kxshapes/` is the next step up from that. It is the nine shapes every picture in this book is built out of, held as plain data rather than as drawing: a frame card, a layer band, an object box, a pointer thread, an ops plug, a trace cell, a CPU lane, a context badge and a memory slot. A test asserts there are exactly nine, because a closed set is the point. Each shape works out its own rows, its own labels and its own alt text, and neither renderer is allowed to work any of that out again. It is a package of its own rather than a module inside either renderer, and that is the whole reason it exists. If the arithmetic lived in `kxwidgets` then `kxmanim` would have to redo it, and two renderers doing their own arithmetic are two renderers that can disagree, in the worst possible way, which is that both pictures look fine and one of them is wrong.
Expand Down
80 changes: 76 additions & 4 deletions corpora/BASELINE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@
schema = 1

[totals]
artefacts = 24
lines = 1856
read = 1480
skipped = 207
artefacts = 32
lines = 1982
read = 1604
skipped = 209
unparsed = 0

[[artefact]]
Expand Down Expand Up @@ -105,6 +105,15 @@ read = 34
skipped = 1
unparsed = 0

[[artefact]]
path = "corpora/proc/tier0/interrupts.txt"
reader = "proc-percpu"
lines = 8
found = 7
read = 7
skipped = 1
unparsed = 0

[[artefact]]
path = "corpora/proc/tier0/lockdep-stats-after.txt"
reader = "lockdep-stats"
Expand All @@ -123,6 +132,24 @@ read = 51
skipped = 1
unparsed = 0

[[artefact]]
path = "corpora/proc/tier0/meminfo.txt"
reader = "proc-keyed"
lines = 47
found = 47
read = 47
skipped = 0
unparsed = 0

[[artefact]]
path = "corpora/proc/tier0/odd-comm-stat.txt"
reader = "proc-pidstat"
lines = 1
found = 50
read = 1
skipped = 0
unparsed = 0

[[artefact]]
path = "corpora/proc/tier0/ring-overrun.txt"
reader = "tracefs-stats"
Expand All @@ -132,6 +159,51 @@ read = 6
skipped = 2
unparsed = 0

[[artefact]]
path = "corpora/proc/tier0/self-maps.txt"
reader = "proc-maps"
lines = 7
found = 7
read = 7
skipped = 0
unparsed = 0

[[artefact]]
path = "corpora/proc/tier0/self-stat.txt"
reader = "proc-pidstat"
lines = 1
found = 50
read = 1
skipped = 0
unparsed = 0

[[artefact]]
path = "corpora/proc/tier0/self-status.txt"
reader = "proc-keyed"
lines = 50
found = 50
read = 50
skipped = 0
unparsed = 0

[[artefact]]
path = "corpora/proc/tier0/softirqs.txt"
reader = "proc-percpu"
lines = 11
found = 10
read = 10
skipped = 1
unparsed = 0

[[artefact]]
path = "corpora/proc/tier0/version.txt"
reader = "proc-version"
lines = 1
found = 3
read = 1
skipped = 0
unparsed = 0

[[artefact]]
path = "corpora/traces/handwritten/page-fault.txt"
reader = "function_graph"
Expand Down
89 changes: 89 additions & 0 deletions corpora/proc/tier0/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Tier 0 snapshots of /proc

These are copies of files out of `/proc` and `/sys` on the pinned kernel. A trace is a recording of something happening. These are the opposite: a look at what the kernel was willing to say about itself at one moment, with nothing happening at all.

Every file here is real, off the 7.2.2 built for 32-bit x86 with one processor, running under v86. Every `.meta.toml` says `evidence = true`, so a lesson may cite one. Each also records the file in `/proc` it is a copy of, under `path`, and the level the kernel tree gives that path, under `stability`.

## The thing to read before reading any of them

Almost none of these files are documented.

The kernel keeps its own record of what it promises in `Documentation/ABI`, four directories deep, one per level, defined in `Documentation/ABI/README`. On 7.2.2 that tree has 685 files in it. Six of them describe a path in `/proc`, and those six are `/proc/i8k`, `/proc/diskstats`, `/proc/pid/smaps_rollup` and the three files under `/proc/*/attr`. Not `meminfo`. Not `interrupts`. Not `/proc/<pid>/stat`, which is the file behind every process monitor ever written. Not `maps`.

That is worth sitting with rather than being alarmed by. Those files have had the same shape for many years and will keep it, because changing one would break userspace and that is the rule nobody gets to bend. What is missing is anybody having written down which part of the shape you may lean on. So this project reads them, and `kxray.proc.stability` attaches the answer to every read, and printing one of these objects tells you it is leaning on custom rather than on a promise.

Two paths this project reads are stronger than undocumented and worse. The closing section of `Documentation/ABI/README` names, as things that "should not under any circumstances be considered stable", Kconfig, calling out `/proc/config.gz` by name, and kernel symbols, saying not to rely on "the presence, absence, location, or type of any kernel symbol". The second of those is `/proc/kallsyms`, which is next door in `../handwritten/` and which `kxray.kallsyms` reads anyway. Counting ops tables by name on a machine in front of you is a fine thing to do. Shipping the same code inside a tool is not, and now the ledger says so.

## Taking one again

```
node kxbox/web/headless.js sh 'cat /proc/meminfo'
```

No setup, no tracer, nothing to turn on or put back. These are the cheapest artefacts in the corpus to refresh, and the ones most likely to be different after a kernel bump, which is the point of having them.

## version.txt

One line. The release and the build number are worth pulling out and the rest is not: it holds the user and host that built the kernel, then the whole compiler and linker banner, with brackets inside brackets in it, and nothing anywhere promises its shape.

It is also the cheapest confirmation that the kernel running is the kernel the profile asked for. `PREEMPT` is in there because `kxbox/kernel/pin.toml` asked for `CONFIG_PREEMPT=y`, and if it ever stops being in there, half the claims in the concurrency lessons are about a different machine.

`kxray.proc.version` turns the release into a tuple of numbers, because string comparison says 6.9 is newer than 6.10 and it is not.

## meminfo.txt

All of memory as the kernel accounts for it, on a box given 100 MiB.

The unit says `kB` and means KiB. `MemTotal` is 102308 kB, and 102308 times 1024 is a shade under 100 MiB while 102308 times 1000 is nowhere near it. The kernel has spelled it that way since the beginning and every tool on the machine agrees with it, so the parser keeps the kernel's spelling and multiplies by 1024.

The key list is per config. There is no `HugePages_Total` here and there is a `GPUActive`, and a machine built differently prints a different set. Nothing in `kxray.proc` requires a key to be there, and asking for one that is not raises rather than returning zero.

## interrupts.txt and softirqs.txt

Read these two together or the pair is wasted.

`interrupts.txt` counts the hardware asking for attention. There is one column because this box has one CPU. A laptop prints one column per possible CPU, which is not the same as the number online and not the same as the number in the machine, and the header is the only place that number is already worked out. That is why `kxray.proc.percpu` reads the header and refuses to guess.

The rows underneath the numbered ones are per architecture and per config. Two here, `NMI` and `TLB`. An x86-64 desktop prints around fifteen and an arm64 machine prints a different set again, so there is no list of them anywhere in the code.

`softirqs.txt` counts the work that answering an interrupt did not do itself. Ten vectors, seven of which have never fired on this idle box. The two that have are `TIMER` and `RCU`, which is exactly the pair that `../../traces/tier0/flat-interrupt.txt` catches in the act: `raise_softirq` inside the hardware handler with interrupts off, then `handle_softirqs` four lines later with interrupts back on. That trace is the gap happening once. These two files are the same gap counted since boot.

## self-maps.txt

The whole address space of one process, which was the `cat` that read the file. Seven lines, and four things in them.

`/bin/busybox` appears twice, once `r-xp` and once `rw-p`. A program's text and its data are one file mapped two ways with different permissions, and that is true of every program on every Linux machine rather than being a busybox quirk.

Line three has no name at all. That is anonymous memory, and it is what a first write has to go and find a page for, which is the entire subject of `blueprints/page-fault.md`.

Line three also ends in a space, and that is the trap. The kernel pads every line out to a fixed column before printing the path, and when there is no path the padding is printed anyway. So that line has five whitespace separated fields and every other line has six, and code that reaches for field six works on every maps file it has ever seen until it meets an anonymous mapping. Do not let an editor strip the trailing whitespace from this file.

The two gaps between the mappings are most of the address space. From `0814c000` to `b7f8f000` is about 2.7 GiB of nothing, and a fault anywhere in it is a segmentation fault.

## self-stat.txt and odd-comm-stat.txt

The same file twice, for two processes with different names, and the pair is the point.

`self-stat.txt` is the ordinary case: a process called `cat`. Fifty two fields on one line, which is exactly what Table 1-4 of `Documentation/filesystems/proc.rst` lists. That table is headed "as of 2.6.30-rc7" and still describes 7.2.2 without an error in it, for a file that has no ABI entry at all. Splitting this line on whitespace gives the right answer.

`odd-comm-stat.txt` is the same file for a process whose executable is named `od) d ma`:

```
37 (od) d ma) R 1 0 0 0 -1 4194304 37 0 0 0 0 1 0 0 20 0 1 0 265 ...
```

The kernel prints the command name in brackets and does not escape it. `line.split()` on that gives `37`, `(od)`, `d`, `ma)`, `R`, and every field after the name has slid two places along. The state, field three, comes back as `d`, which is not a state any process is ever in. Nothing raises. All the numbers are still numbers. A monitor reading this would carry on reporting nonsense.

The correct parse is the one `procps` has used for decades and it is not clever: first opening bracket, last closing bracket, and the fields are what is left. `kxray.proc.pidstat` does that, and keeps what the naive split would have said, so a lesson can print the two answers next to each other instead of asking anybody to take the trap on trust.

Getting the capture needed a process with a name like that, and on a busybox rootfs that means a shell script. busybox dispatches on its own `argv[0]` and refuses to run under a name that is not one of its applets, so a copy of `/bin/sleep` called `od) d ma` exits immediately with "applet not found". A script gets its `comm` from the script's own filename, so the name sticks.

One number ties this file to the one above it. `vsize` in `self-stat.txt` is 1298432, and the sizes of the seven mappings in `self-maps.txt` add up to 1298432, because `vsize` is that sum. Two files, two readers, one fact, and a test that checks they still agree.

## self-status.txt

The same process as `self-stat.txt`, printed for a person instead of for a program. Fifty keys, tab separated where meminfo uses spaces, and three of them break the idea that a value is a number.

`Uid` has four values on one line: real, effective, saved and filesystem. `State` has a letter and then the same state spelled out in brackets. `Groups` is empty, and the kernel prints the key and the separator anyway.

So the reader keeps a tuple of words per key and offers a number only when there is exactly one word and it is one. A model with `value: int` on it would have to throw two of those three away and would be wrong about the third.
36 changes: 36 additions & 0 deletions corpora/proc/tier0/interrupts.meta.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
source = "tier0"
evidence = true

describes = "every interrupt the machine has taken since boot, one column per CPU"
path = "/proc/interrupts"
stability = "undocumented"

kernel = "7.2.2"
arch = "i386"
profile = "A-full"
uniprocessor = true
preempt = true
captured = "2026-09-05"
tier = 0

command = "cat /proc/interrupts"
setup = []

# The column count is the fact worth taking away. There is one column here because the box has one
# CPU. A laptop prints one per possible CPU, which is not the same as the number online and not the
# same as the number the hardware has. Only the header knows, which is why kxray.proc.percpu reads
# the header and refuses to guess.
#
# The rows underneath the numbered ones are per architecture and per config. Two here, NMI and TLB.
# An x86-64 desktop prints around fifteen and an arm64 machine prints a different set. There is no
# list of them in kxray for that reason.
#
# Read this next to softirqs.txt. This file counts the hardware asking. That one counts the work the
# answering deferred.
timings_are_real = false

unparsed_lines = 0
cpus = ["CPU0"]
rows = 7
hardware_rows = 5
named_rows = ["NMI", "TLB"]
8 changes: 8 additions & 0 deletions corpora/proc/tier0/interrupts.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CPU0
0: 316 XT-PIC timer
1: 10 XT-PIC i8042
2: 0 XT-PIC cascade
4: 436 XT-PIC ttyS0
12: 115 XT-PIC i8042
NMI: 0 Non-maskable interrupts
TLB: 0 TLB shootdowns
34 changes: 34 additions & 0 deletions corpora/proc/tier0/meminfo.meta.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
source = "tier0"
evidence = true

describes = "the whole of memory as the kernel accounts for it, on a box given 100 MiB"
path = "/proc/meminfo"
stability = "undocumented"

kernel = "7.2.2"
arch = "i386"
profile = "A-full"
uniprocessor = true
preempt = true
captured = "2026-09-05"
tier = 0

command = "cat /proc/meminfo"
setup = []

# Two things this file is here to show.
#
# The unit says kB and means KiB. MemTotal is 102308 kB on a machine given 100 MiB, and 102308
# times 1024 is a shade under 100 MiB while 102308 times 1000 is nowhere near it. The kernel has
# spelled it that way since the beginning and every tool on the machine agrees, so kxray keeps the
# kernel's spelling and does the multiplication by 1024.
#
# The key list is per config. There is no HugePages_Total here and there is a GPUActive, and a
# machine with transparent huge pages configured differently prints a different set again. Nothing
# in kxray.proc requires a key to be present.
timings_are_real = false

unparsed_lines = 0
keys = 47
mem_total_kb = 102308
mem_total_bytes = 104763392
47 changes: 47 additions & 0 deletions corpora/proc/tier0/meminfo.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
MemTotal: 102308 kB
MemFree: 97572 kB
MemAvailable: 95668 kB
Buffers: 0 kB
Cached: 1204 kB
SwapCached: 0 kB
Active: 1232 kB
Inactive: 0 kB
Active(anon): 1232 kB
Inactive(anon): 0 kB
Active(file): 0 kB
Inactive(file): 0 kB
Unevictable: 0 kB
Mlocked: 0 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 0 kB
Writeback: 0 kB
AnonPages: 48 kB
Mapped: 680 kB
Shmem: 1204 kB
KReclaimable: 0 kB
Slab: 2676 kB
SReclaimable: 0 kB
SUnreclaim: 2676 kB
KernelStack: 224 kB
PageTables: 64 kB
SecPageTables: 0 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 51152 kB
Committed_AS: 1508 kB
VmallocTotal: 917496 kB
VmallocUsed: 12 kB
VmallocChunk: 0 kB
Percpu: 32 kB
AnonHugePages: 0 kB
ShmemHugePages: 0 kB
ShmemPmdMapped: 0 kB
FileHugePages: 0 kB
FilePmdMapped: 0 kB
Balloon: 0 kB
GPUActive: 0 kB
GPUReclaim: 0 kB
DirectMap4k: 8192 kB
DirectMap4M: 106496 kB
Loading
Loading