From Wire to Userland: The Full Lifecycle of a Packet Inside Linux
Understanding the full receive path — from interrupt affinity and NAPI quotas to Netfilter hook ordering and the TCP fast-path conditions — is what separates diagnosing a throughput regression from guessing. Every hook point is a linked-list traversal with a cost, and the fast-path bypass is fragile: one out-of-order segment collapses it.
DMA writes incoming packets into pre-allocated ring buffers shared between the NIC and kernel. Modern multi-queue NICs use MSI-X to deliver per-queue interrupts to specific CPUs, avoiding the interrupt storm that would paralyze a 10G NIC at line rate. The hard interrupt handler does almost nothing: it masks the queue interrupt and schedules NAPI, deferring real work to the NET_RX_SOFTIRQ softirq.
Inside the softirq, the poll function allocates sk_buffs from per-CPU caches, attempts GRO merging for consecutive same-flow TCP segments, and hands the packet up through the protocol stack. Netfilter's PRE_ROUTING and LOCAL_IN hooks run iptables rules and conntrack tracking; route cache turns microsecond lookups into nanosecond hits for subsequent packets in the same flow.
At the transport layer, established TCP sockets take a fast path that skips all out-of-order and retransmission logic when the sequence number matches exactly. The data is queued directly onto the socket's receive queue, and tcp_data_ready wakes every process blocked on read or epoll in one shot.
The entire receive path is a chain of budgeted, deferrable work units: DMA is asynchronous, the hard irq does almost nothing, NAPI polls under a time/packet budget, and even softirq backlog spills to ksoftirqd. The design assumes overload is normal and builds in back-pressure at every stage.
Netfilter hook ordering explains why eBPF and XDP are placed before the stack: every iptables rule is a linked-list traversal inside a hook, and PRE_ROUTING runs on all inbound packets regardless of final destination. Moving drop decisions earlier avoids wasting CPU on packets that will be discarded anyway.
The TCP fast path is a binary cliff, not a gradient. A single missing or out-of-order segment collapses the optimized path into the full slow path with SACK processing and retransmission logic, which is why packet loss matters far more than latency for throughput.
Per-CPU data structures appear at every layer — softnet_data poll_list, sk_buff allocation caches, NAPI instances pinned to queues pinned to CPUs. The architecture is fundamentally a partitioned pipeline, and breaking CPU affinity (e.g., by misconfiguring IRQ smp_affinity) forces cache misses and lock contention across the whole path.