Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

19 · Interrupts and Drivers

The question

How does a device get the CPU’s attention, and what does the CPU do about it?

ch18 was about the CPU interrupting itself. A page fault happens because of the instruction that is running: it is caused by that instruction, it is reported at that instruction, and returning re-runs it. Everything about it is attached to the program it happened to.

A device interrupt has none of those properties. It arrives because something outside the processor finished doing something, at a moment decided by that thing, while the processor was in the middle of work with no relationship to it at all. The instruction it lands between is arbitrary. The process it lands on is very probably not the one that asked.

That difference is why device drivers are shaped the way they are, and this chapter asks which of its consequences can be counted.

The material

Getting attention costs someone else’s time

The machine has one path in. A device raises a line, the interrupt controller — on this machine the PLIC, the block that decides which device may interrupt which hart — picks which device is allowed to speak, and the processor takes exactly the trap path ch16 counted: the same thirty-odd register saves, the same page-table switch, the same restoration on the way out.

None of that work is the device’s. It is charged to whichever process was running, which had nothing to do with the I/O and is not consulted. A process that performs no I/O at all still pays for every interrupt that arrives while it holds a core, and it pays in exactly the currency ch16 measured.

So the obvious question is how many of these there are. One kind of device has an answer and the other does not.

A workload that decides its own numbers

intrload.c
int main(void) {
  /* Console: one write of a known number of characters, on one line so the transcript stays
   * readable. Every one of them has to reach a device that takes them one at a time. */
  static char line[SYSFS_INTRLOAD_CHARS + 1];
  for (int i = 0; i < SYSFS_INTRLOAD_CHARS; i++)
    line[i] = '.';
  write(1, line, SYSFS_INTRLOAD_CHARS);
  write(1, "\n", 1);

  /* Disk: a file written and read back, so the request count is decided here and not by a
   * cache's opinion of what is already in memory. */
  for (int i = 0; i < BLOCK; i++)
    block[i] = (char)i;

  int fd = open("intrload.dat", O_CREATE | O_WRONLY);
  for (int i = 0; i < SYSFS_INTRLOAD_BLOCKS; i++)
    write(fd, block, BLOCK);
  close(fd);

  fd = open("intrload.dat", O_RDONLY);
  for (int i = 0; i < SYSFS_INTRLOAD_BLOCKS; i++)
    read(fd, block, BLOCK);
  close(fd);
  unlink("intrload.dat");

Build it into the kernel, boot, and run it:

./run intrload

Two devices, deliberately. The console takes characters one at a time; the disk takes blocks. Both amounts are constants this program chose, so anything that varies between runs is varying for a reason that is not the workload.

The kernel counts interrupts by source, and counts two extra facts about the console driver:

16-interrupt-census.patch
+uartnote(int what, uint64 n)
+{
+  switch (what) {
+  case 0: icensus.tx_wakeups += n; break;
+  case 1: icensus.rx_chars += n; break;
+  case 2: icensus.chars_written += n; break;
+  default: icensus.write_sleeps += n; break;
+  }
+}
+

Three interrupt sources, one countable

Three interrupt sources, and which of them a fixed workload gives a fixed count for.

Three sources. Same workload, repeated: one count came back identical every time and two did not.

What happenedCount
Block operations the workload performed64
Interrupts the disk raised319
Characters the workload asked to be written513
Times it had to stop and wait for the device0
Characters arriving from the keyboard10

Conditions: target xv6 · qemu virt (-machine virt -bios none) · QEMU emulator version 8.2.2 (Debian 1:8.2.2+ds-0ubuntu1.18) · xv6-riscv @ 9e3161a · riscv64-linux-gnu-gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0 · xv6's own CFLAGS · 2026-09-17 · Source: bench/results/interrupts-xv6.json, code hash e20981b7aa1ab1c9.

The disk count is in the table. The console count is not, and its absence is the finding rather than an omission.

A second row is missing too, and it took longer to find. The table used to carry a count of how many characters the writing process handed to the device. It reads like a companion to the row above it and is not one: the counter behind it is incremented for every character the kernel sends, so it counts the boot log, the shell’s prompt and the echo of what was typed as well as the workload’s output. It reported more characters than the workload had asked for, which is what gave it away, and it came back one different between identical runs often enough to fail the check that re-runs this measurement. The same argument that keeps the console’s interrupt count out of the table keeps that row out of it.

Run the identical workload five times and the disk raises the same number of interrupts every time. Run it five times and the console raises a different number every time. Nothing about the workload changed between those runs, so the console’s count is not telling you about the workload.

The reason is in what the two devices are saying. A block device interrupts to say that a request is finished. Requests are discrete, the workload decides how many there are, and each is completed exactly once — so the count follows. A character device interrupts to say it is ready for more. That is not a unit of anything. How many times a transmitter announces its readiness during a burst of output depends on how the output and the announcements interleave, which depends on timing, and timing inside an emulator is a property of the laptop.

This is ch16’s rule again, from a different direction. There the uncountable thing was the timer, and the reason was obviously about elapsed time. Here two devices are doing what looks like the same job, and only one of them is countable — which is a much better demonstration that the question is about what an event means and not about which peripheral raised it.

The zero

One number in the table is zero, and it says more about the emulator than about the driver.

xv6’s console driver is written to be asynchronous. A process writing to the console hands a character to the transmitter if it is idle, and otherwise sleeps, to be woken by the interrupt that says the transmitter has caught up. That machinery — the sleep, the wait channel the sleeper is parked on, the wakeup in the handler — is the reason the driver is split into a part that runs in the process and a part that runs in the interrupt, and it is the standard explanation for why device drivers have two halves.

In this run it never once engaged. Every character was handed over by the writing process itself, and the count of times anybody had to stop and wait is zero.

That is not a fault in the driver or in the measurement. It is the emulator: QEMU’s transmitter is always ready, because there is no serial line and nothing is being clocked out at a fixed rate. The design exists to cope with a device that is slower than the program, and this target does not have one.

So this chapter can show you the structure and cannot show you the reason for it. The split into two halves is visible in the code, the interrupt that would do the waking is counted, and the thing that makes any of it necessary — a device that makes a program wait — is absent from the machine the chapter runs on. ch29 is on hardware where it is not.

What the image has to do with it

One more dependency, because it caught this chapter out and would otherwise catch a reader out.

The disk figure is reproducible, but it is reproducible for a filesystem image. It counts the block operations xv6’s filesystem performs, and how many those are depends on where the blocks are — which depends on what mkfs, the program that builds the image, laid down, which depends on which programs this book has added to xv6. Add another one in a later chapter and the number moves, for a reason that has nothing to do with interrupts.

Nothing in the book’s stamping scheme covers the filesystem image, because until now nothing had needed it to. The result records the image’s digest — a hash of its contents — so the dependency is visible, and CI regenerates the measurement on every push, so a change is reported rather than absorbed.

What we measured

Interrupts raised by the disk for a fixed number of block operations, and what the console driver did with a fixed number of characters. The census prints two counts this book does not record; the result names them rather than quietly dropping them, and problem 19.3 asks the reader to work out which they are before being told.

What this cannot tell you

What an interrupt costs. No duration appears above. The cost of an interrupt is the trap path plus the handler plus whatever the interruption does to the caches and the pipeline of the innocent process that was running — and this target models none of the last part. ch29 prices it.

How often the timer really fires. The census counts timer interrupts and the book does not print the number, for ch16’s reason. The count is steady here over repeated runs and still must not be published: steady on one machine for one short workload is not the same as determined by the workload, and the difference is exactly the mistake this book is trying not to make.

Why the driver is split in two. The structure is here and the pressure that produced it is not, because the emulated transmitter is never busy. A reader who wants to see the asynchronous path do something has to run this on hardware, which is Part V.

Anything about interrupt latency, priority or affinity. The PLIC can be told which core should take which interrupt and at what priority; xv6 uses almost none of that, and none of it shows up in a count. On a machine where a device interrupt can land on the core running your benchmark, those settings are the difference between a measurement and a puzzle, and ch30 has to care.

Problems

Three. The first two are in tests/interrupts_and_drivers/console.c; the third is a Python stub you fill in with an opinion and are graded against the machine.

19.1 — How many characters does the console lose? Given a string of arrivals and drains, and a buffer of xv6’s own size, say how many are dropped.

A handler cannot block, cannot allocate and has nobody to return an error to, so a character arriving at a full buffer is simply gone and nothing anywhere is told. This is why a terminal loses characters when you paste into it, and it is not a bug in anything.

python3 -m pytest tests/interrupts_and_drivers/test_problem_1_lost.py

19.2 — How many interrupts will this cost? Given a number of operations and how many the device reports at once, predict the count — and return “undecidable” for the device where the workload does not determine it.

The last case is the only one that matters. Being able to say this question has no answer is worth more than any number, and it is a skill this book keeps asking for.

python3 -m pytest tests/interrupts_and_drivers/test_problem_2_interrupts.py

19.3 — Which of these counts is a property of the workload? Six counters; mark each reproducible or not, before running anything.

You are not being graded against what this book decided. The test boots xv6 several times and compares the censuses, so a counter you called reproducible had better come back the same every time. Where the experiment cannot settle one — a varying counter that happened not to vary — you are told so rather than failed, because a test that occasionally fails a correct answer is worse than one that occasionally proves less than it hoped to.

python3 -m pytest tests/interrupts_and_drivers/test_problem_3_attribute.py

Where to go next

The RISC-V privileged specification RISC-V International (n.d.) defines the interrupt causes and the rules about when a hart will take one; the PLIC’s own specification defines claiming and completing, which is the handshake devintr performs twice per interrupt.

xv6’s kernel/uart.c and kernel/plic.c Cox et al. (n.d.) are both short enough to read at a sitting. Read uartwrite beside uartintr and find the sleep and the wakeup that this chapter measured zero of — the code is written for a device that makes you wait, and seeing it never wait is the most useful thing this target can show you about it.

ch20 is about the problem this chapter has been carefully stepping around. A handler and a process share a buffer; one of them can start at any instruction of the other; and the counters in this chapter’s own patch are deliberately unlocked, which is a decision that needs defending.

References
  1. RISC-V International. (n.d.). The RISC-V Instruction Set Manual, Volume II: Privileged Architecture. https://riscv.org/technical/specifications/
  2. Cox, R., Kaashoek, M. F., & Morris, R. (n.d.). xv6-riscv. MIT PDOS. https://github.com/mit-pdos/xv6-riscv