The Runtime Theory
SystemArchitecturenetwork

What happens when a packet arrives?

A step-by-step walk from the NIC's DMA ring through interrupt, softirq, GRO, protocol demux, and the socket buffer that finally wakes your blocked read.

The Runtime Theory Team1 min read05 steps

trace spine

  1. 01 NIC DMA into the ring buffer
  2. 02 Interrupt and NAPI poll
  3. 03 GRO and protocol demux
  4. 04 Socket queue and wakeup
  5. 05 recvfrom copies to user space

Somewhere, a NIC just received a frame. Everything between the wire and your read() is a choreographed handoff through hardware queues and kernel softirq context. This is the receive path — one of the most heavily optimized pieces of the entire operating system.

trace stepHardware

The NIC's DMA engine writes the frame into a pre-allocated ring buffer in RAM — memory the driver registered at ifconfig up time. The CPU has not touched the packet. At 10 Gbps a 1500-byte frame arrives every 1.2 µs, so the ring has to be hundreds of entries deep or frames get dropped at the hardware level.

trace stepKernel

The NIC raises an interrupt (MSI-X vector). The driver's IRQ handler does the absolute minimum — it marks the queue as needing service and schedules NAPI polling instead of processing the packet inline. The real work runs in softirq context on whichever CPU received the interrupt. Under sustained load, NAPI switches to polling mode entirely: the CPU drains the ring in a loop, and the NIC's interrupts go quiet. This is the famous interrupt-to-polling transition, and it's why a busy server's receive path can run at line rate without drowning in IRQ overhead.

trace stepKernel

The softirq path processes each skb: GRO (Generic Receive Offload) merges consecutive segments of the same flow into one larger skb before the stack sees them — a 10-packet burst becomes one 14.6 KB object, cutting per-packet overhead ~10×. Then protocol demux: Ethernet type → IPv4 → TCP/UDP, where the packet's 4-tuple is hashed to find the owning socket. Hash is computed once, in software; some NICs (RSS) do the hashing in hardware and steer the packet straight to the right CPU's queue.

trace stepKernel

The payload is appended to the socket's receive queue (sk_backlog), and the kernel checks if a thread is blocked in recvfrom/read on this socket. If so — and if the socket is alone in the epoll set — the kernel wakes it directly (wakeup on the same CPU, avoiding the scheduler's remote-wakeup path). If the app uses epoll, the socket is added to the ready list and the epoll waiter is woken instead.

trace stepApplication

The application's recvfrom syscall copies bytes from the kernel's skb into the user buffer. With TCP that's a stream copy (kernel tracks the byte offset); with UDP it's a datagram copy. The syscall itself is ~1–2 µs; the copy of a 1500-byte payload is another ~0.5–1 µs.

bash
perf stat -e irq_vectors:local_timer,net:netif_receive_skb,kmem:kmem_cache_alloc -a sleep 1

The receive path is the rare trace where hardware does half the work (DMA, hashing, coalescing) and the kernel the rest (demux, queuing, wakeup). Every stage exists to defer work: defer the copy to the app, defer processing to softirq, defer interrupts to polling. What the machine actually does is delay everything as long as possible, then do it in the fewest possible CPU cycles.