JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Polling vs Interrupts, Device Trees, and User Transfer

Guide 4 taught your driver to talk to hardware; guide 3 taught it to answer when hardware interrupts. Now the closing questions of the rung: when is answering every interrupt the wrong move, how does the kernel even know what hardware exists before any driver runs, and how does a driver hand bytes across the user-kernel wall without opening a hole an attacker walks through?

When interrupts stop helping

Guide 3 sold you on interrupts for a good reason: instead of the CPU wastefully asking "are you done yet?" over and over, the device taps the CPU on the shoulder only when something actually happened. For a keyboard that fires a few dozen times a second, this is wonderful — the CPU does real work in between and pays the cost of an interrupt only when a key is genuinely pressed. The whole appeal is that interrupts are cheap when events are rare. Hold onto that qualifier, because the entire tension of this guide lives inside it.

Now point that same mechanism at a 10-gigabit network card under a flood of small packets. Each packet that arrives raises an interrupt. An interrupt is not free: the CPU must stop what it is doing, save its registers, switch into interrupt context, jump through the interrupt table, run the handler, and restore everything on the way out. At a few million packets per second, the machine spends all its time entering and leaving interrupt handlers and never reaches the user code that was supposed to process the packets. Throughput collapses; under enough load it can drop to zero. This pathology has a name — an interrupt storm, or receive livelock — and it is the moment the very mechanism that made the device responsive becomes the thing strangling it.

The escape is the very thing interrupts were invented to replace: polling. In a pure polling design the driver disables the device's interrupt and instead loops, repeatedly reading a status register to ask "is there a packet? another? another?" and draining as many as it finds in one pass. Each packet now costs one cheap register read instead of a full interrupt entry-and-exit, so under heavy load polling is dramatically more efficient — the CPU amortizes one trip into the driver across a whole batch of packets. The catch is the mirror image of the interrupt's catch: when the device is idle, a polling loop is pure waste, a CPU spinning at full power asking a quiet card the same question millions of times a second for nothing.

NAPI: take the best of both

So interrupts win when traffic is light and polling wins when traffic is heavy — and a real network card sees both, often within the same second. The kernel's answer is not to pick one but to switch between them dynamically. That hybrid is NAPI ("New API"), the receive model every modern Linux network driver uses. The idea is one sentence long: use an interrupt to discover that work has arrived, then switch to polling to drain it, then switch back to interrupts when the work runs out.

Walk the cycle. The card sits with interrupts enabled, costing nothing while idle. A packet arrives and raises an interrupt; the handler does almost nothing in its top half — it disables this card's receive interrupt and schedules a poll. From then on the kernel polls the card, draining packets in batches, with interrupts off so a torrent of arrivals raises zero further interrupts. The poll function is handed a budget — say 64 packets — so one device cannot monopolize the CPU; if it hits the budget with more waiting, it yields and is polled again soon. Crucially, when a poll comes back having found fewer packets than the budget, the queue has drained: the driver re-enables the interrupt and stops polling, sliding back to the cheap idle state. Light load behaves like interrupts; heavy load behaves like polling; the transition is automatic.

How the kernel learns what hardware exists

Step back to a question we have quietly skipped. Guide 1 said the driver model matches a driver to a device by reading the device's id during bus enumeration. That works because a self-describing bus like PCI or USB lets the kernel walk it and ask each slot, "who are you?" — the device answers with a vendor and device id, the kernel finds the matching driver, done. On a PC this is the whole story. But it quietly assumes the hardware can introduce itself.

On the embedded boards that run most of the world's devices — phones, routers, your car's dashboard — that assumption breaks. A chip soldered onto a board often sits on a simple bus with no enumeration at all: there is no protocol to ask a memory-mapped sensor "who are you and what address do you live at?" The hardware is simply there, at a fixed physical address the board designer chose, and nothing on the chip will ever tell software about it. If the kernel cannot discover this hardware, how can it possibly drive it without hard-coding every board's wiring into the kernel itself?

The answer is the device tree: a data file, separate from the kernel, that describes the hardware the kernel cannot discover. It is a tree of nodes — one per device — where each node names a compatible driver string (like "vendor,uart-v2"), the physical address of the device's registers, which interrupt line it raises, its clock, and so on. The bootloader loads this description into memory and hands its address to the kernel at boot. The kernel reads the tree, and for each node it finds a driver whose "compatible" string matches, then calls that driver's probe with the address and interrupt taken straight from the node. It is the same probe-on-match dance from guide 1 — but the match data comes from a file someone wrote, not from hardware that introduced itself.

Carrying bytes across the wall

Now the last piece, and the one with the sharpest teeth. A character driver's read() and write() must move data between a user program's buffer and the kernel. From the earliest rungs you know a pointer is just an address. So when a user calls read(fd, buf, n), the kernel handler receives the address buf and the length n — and the obvious, catastrophically wrong move is to treat buf like any kernel pointer and just write to it with memcpy() or a plain *p = value. Do that and you have built a security hole the size of the machine.

Why is a plain dereference of a user pointer a disaster? Because that pointer crossed a trust boundary — it came from an untrusted user program, and the kernel must assume it is hostile. The address could be a lie: it might point into kernel memory the process was never allowed to touch. If the kernel naively dereferences it, the kernel — running at full privilege — would happily read a secret out of its own space and copy it back to the attacker, or overwrite kernel data with attacker-chosen bytes. A user pointer is a claim, not a guarantee, and the entire security of the boundary rests on the kernel never trusting that claim blindly.

This is why the kernel never dereferences a user pointer directly. It uses two dedicated helpers — copy_to_user() and copy_from_user() — that do the transfer safely. copy_from_user(dst_in_kernel, src_user_ptr, n) pulls n bytes from a user address into the kernel; copy_to_user(dst_user_ptr, src_in_kernel, n) pushes the other way. Before touching a single byte, each one checks that the user address range genuinely lies inside this process's permitted user-space region, refusing any pointer that reaches into the kernel. And because a user page might be swapped out or simply not mapped, these functions are written to catch a fault on the access and return an error count rather than crashing the kernel — they turn a wild access into a clean failure.

ssize_t my_read(struct file *f, char *user_buf, size_t n, loff_t *off)
{
    char kbuf[256];
    size_t len = fill_from_device(kbuf, min(n, sizeof kbuf));

    /* WRONG: *user_buf = ... ; or memcpy(user_buf, kbuf, len);
       trusts an untrusted address, at full kernel privilege. */

    /* RIGHT: validates the range, faults safely, never trusts buf. */
    if (copy_to_user(user_buf, kbuf, len) != 0)
        return -EFAULT;          /* some bytes could not be copied */
    return len;                  /* report how many bytes we gave */
}
A character device read(). The user gave us user_buf; we never dereference it directly. copy_to_user() validates the range, handles a page fault without crashing, and returns the count it could not copy — nonzero means we hand back -EFAULT.

Closing the rung

Stand back and see the whole rung as one shape. A driver is kernel code packaged as a loadable module, bound to hardware by the driver model (guide 1). It presents one of three faces — character, block, or network — through a table of function pointers (guide 2). It answers hardware promptly with a fast top half and a deferred bottom half (guide 3). It moves bytes to and from the chip through memory-mapped I/O and DMA (guide 4). And in this guide it learned to choose between polling and interrupts — settling on NAPI when event rates swing wildly — to learn what hardware exists from a device tree when the bus cannot introduce it, and to carry data across the user wall with copy_to_user() and copy_from_user() instead of trusting a pointer it did not create.

If one theme runs through all five guides, it is that the kernel earns its power by giving up its safety net. There is no process to crash, no operating system above to clean up, no garbage collector, and no shell to catch you — every reflex this course built (check every return, own every resource, validate every boundary, defer the heavy work, distrust every user-supplied pointer) becomes load-bearing here in a way it never was in user space. That is not a reason to fear kernel programming; it is the reason it is satisfying. You are writing the layer everything else stands on, and now you understand, end to end, how a few hundred lines of careful C let a quiet block of silicon become a device the whole system can use.