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.
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.
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.
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.
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.
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.
perf stat -e irq_vectors:local_timer,net:netif_receive_skb,kmem:kmem_cache_alloc -a sleep 1The 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.