Application send success, NIC transmission completion, peer ACK, and business processing completion are distinct moments. Along the transmit/receive path, you must track in-flight packets, skbs, and application byte streams separately; they do not necessarily correspond one-to-one.
Objective
Reference Section
How skbs reference data
Section 1
Receive, NAPI, and transmission completion
Sections 2–4
Offload, queue allocation, and counting
Sections 5–7
1. skbs are descriptors, not fixed contiguous packets
struct sk_buff stores metadata and references actual data buffers. The linear region is described by the logical positions of head, data, tail, and end; some members store offsets in the implementation and are not all directly dereferenceable C pointers.
skb->len is the total data length, data_len is the non-linear part length, and skb_headlen() returns the linear valid length. The header members mainly represent relative positions; access should use appropriate interfaces like ip_hdr() and tcp_hdr(). GSO information and frags are located in structures like skb_shared_info; do not arbitrarily flatten them into a fabricated skb definition.
During packet reception parsing, skb_pull() advances the data pointer; during encapsulation, skb_push() utilizes headroom to prepend headers. However, these operations are only valid if space, linear range, and shared state conditions are met; calling them does not automatically expand capacity or establish copy-on-write semantics.
Operation
What is added
What must still be confirmed before writing
skb_clone
New metadata, sharing underlying data
Data writability and sharing relationships
skb_get
Reference to the same skb
Concurrency access protocols
Reference counting maintains lifetime but does not grant permission for arbitrary concurrent modifications. See skb documentation.
2. How received data reaches the application
DMA writes frames into buffers managed by the driver; completion descriptors inform the driver which data is available for processing. Drivers must also adhere to DMA cache, ordering, and buffer recycling rules; discovering that a packet has been received does not mean the page can be immediately returned to the NIC for overwriting.
Common native XDP runs before constructing standard skbs. Packet socket capture entry points, bridge RX handlers, and VLAN processing belong to different mechanisms and cannot all be drawn as a single RX handler list; the position of generic XDP is also not the same as native XDP.
IP PREROUTING occurs before routing decisions. Local delivery continues through INPUT, while forwarding goes through FORWARD and egress processing. Placing PREROUTING after ip_local_deliver() would directly mislead DNAT and routing analysis.
TCP reception also requires checking connections, sequences, windows, checksums, and states; data is queued into the corresponding receive structure before notifying waiters. sk_data_ready() wake-ups do not mean the application has been scheduled to run, nor that the application has copied or processed all data. MSG_DONTWAIT only changes waiting behavior; it does not give softirqs a special path to automatically copy directly to the current user buffer.
3. NAPI: Processing time via budget allocation
NAPI handles reception and transmission completion via poll methods, often running in softirqs, and also supports threaded NAPI and busy polling. In traditional interrupt-driven modes, interrupts trigger scheduling while keeping relevant device interrupts masked; after appropriate work is done, polling completes and notifications are restored.
Budget: View individual polls and the entire NET_RX cycle separately
The budget in the poll callback limits the RX workload for this round and should not be confused with the netdev_budget for the entire NET_RX cycle as "fixed 300 packets per NIC per time." TX completion cleanup does not simply consume the same RX budget. Calls with a budget of 0 have specific restrictions and cannot perform RX/page-pool/XDP operations or call completion interfaces.
Exhausting the budget usually implies that polling must continue; if processing finishes exactly when the work count equals the budget, it must still be handled according to API conventions to avoid false completion reports or missed subsequent processing. The successful return of napi_complete_done() participates in the decision to restore device interrupts.
NAPI instances, IRQs, and RX/TX queue pairs often have a near one-to-one organizational mapping, but the protocol does not mandate this mapping. One instance can serve multiple queues, and transmission/reception can be separated. See NAPI documentation.
Change the budget and add packets. Pay attention to an empty queue after exactly exhausting the budget: emptiness alone does not prove scheduling ownership was released.
Preparing the visual
Why might polling continue after the queue becomes empty?
Budget bounds RX work in one poll. With the “continue after consuming the full budget” strategy, even an exactly emptied queue completes and restores interrupts only on a later below-budget poll.
{"id":"napi-budget-and-completion","title":"Why might polling continue after the queue becomes empty?","summary":"Budget bounds RX work in one poll. With the “continue after consuming the full budget” strategy, even an exactly emptied queue completes and restores interrupts only on a later below-budget poll.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"budget\" data-i18n=\"budget\"></label><output for=\"budget\" id=\"budget-value\"></output><input id=\"budget\" type=\"range\" min=\"1\" max=\"8\" value=\"4\" step=\"1\"></div><div class=\"control\"><label for=\"arrival\" data-i18n=\"arrival\"></label><output for=\"arrival\" id=\"arrival-value\"></output><input id=\"arrival\" type=\"range\" min=\"1\" max=\"16\" value=\"4\" step=\"1\"></div><div class=\"actions\"><button id=\"inject\" data-i18n=\"inject\"></button><button id=\"poll\" data-i18n=\"poll\"></button></div><p class=\"caption\" data-i18n=\"queue\"></p><div id=\"ring\"></div><div class=\"metrics\"><div><span data-i18n=\"pending\"></span><strong id=\"pending\"></strong></div><div><span data-i18n=\"dropped\"></span><strong id=\"dropped\"></strong></div></div><div class=\"card\"><header data-i18n=\"state\"></header><div id=\"state\" class=\"readout\"></div><code id=\"returned\"></code></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"initial\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"exact\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"completed\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"arrived\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n#ring{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:5px;margin:15px 0}#ring i{height:25px;border:1px dashed var(--rule);border-radius:4px;background:var(--surface)}#ring i.on{border:1px solid var(--accent);background:var(--accent-soft)}.readout{min-height:5.3em}#returned{display:block;min-height:3em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let queued=0,dropped=0,scheduled=false,last=null,message='initial';function draw(){const b=+$('#budget').value;set('budget-value',b);set('arrival-value',$('#arrival').value);$('#ring').replaceChildren(...Array.from({length:32},(_,i)=>{const e=document.createElement('i');e.classList.toggle('on',i<queued);return e;}));set('pending',queued);set('dropped',dropped);set('state',t(scheduled?'scheduled':'idle'));set('returned',last===null?'return —':'return '+last.work+' / budget '+last.budget);$('#poll').disabled=!scheduled;set('explanation',t(message));}$('#budget').oninput=$('#arrival').oninput=draw;$('#inject').onclick=()=>{const n=+$('#arrival').value,accepted=Math.min(n,32-queued);queued+=accepted;dropped+=n-accepted;if(queued)scheduled=true;message='arrived';draw();};$('#poll').onclick=()=>{if(!scheduled)return;const b=+$('#budget').value,work=Math.min(queued,b);queued-=work;last={work,budget:b};scheduled=work===b;message=scheduled?'exact':'completed';draw();};$('#reset').onclick=()=>{queued=dropped=0;scheduled=false;last=null;message='initial';draw();};draw();","audio":false,"strings":{"arrival":"Packets per arrival","arrived":"Packets are retained in the ring; notification schedules processing. Notification count does not replace packet accounting.","budget":"RX budget per poll","completed":"The return is below budget; this model successfully completes NAPI and restores interrupts.","dropped":"Dropped because this ring is full","exact":"This poll returned the full budget and stays scheduled. Even with zero packets remaining, another poll completes it.","heading":"Why might polling continue after the queue becomes empty?","idle":"Complete; IRQ notification enabled","initial":"The queue is empty, awaiting a device notification.","inject":"Deliver this packet batch","intro":"Adjust the RX budget and arrival count, then inject packets or poll. Compare finishing below budget with emptying the queue at exactly the budget.","pending":"Queued packets","poll":"Call poll once","queue":"RX ring: solid slots await processing","reset":"Reset","scheduled":"Scheduled; IRQ masked","scope":"One 32-entry RX queue, budget 1…8, traditional interrupt-triggered masked polling. Uses the documented strategy of returning the full budget and completing on the next poll. Budget=0, TX completions, completion races, multiple queues, and busy polling are omitted. Drops represent only this model’s ring capacity.","scopeLabel":"Model scope and assumptions","state":"Scheduling / interrupt state"}}
Trade-offs between throughput and latency
Increasing batch sizes, hardware interrupt coalescing, or polling can amortize notification costs; however, it may also cause the first packet to wait longer or allow a busy queue to occupy the CPU for longer. Budget exhaustion should be judged in conjunction with softirq time, queue backlog, packet drops, and application latency; you cannot simply assert faster performance just because there are fewer interrupts.
4. Two meanings of transmission completion
Application send success usually indicates that data has been accepted by the socket or queued into the transmit path. TCP is also subject to window, congestion control, and pacing constraints, then constructs an skb, passes through routing, Netfilter, qdisc, and driver transmission.
Completion Point
What can be confirmed based on this
Local TX completion
Typically allows recycling corresponding transmission resources
TCP ACK
Peer protocol stack confirms sequence range
Business reply
Determines processing result based on application protocol
NIC completion does not cross the latter two boundaries. When troubleshooting latency, you must clarify which type of completion you are waiting for.
Network drivers provide device operations via interfaces like net_device_ops; statistics are often exported via interfaces like rtnl_link_stats64. Virtual devices also implement net_device, so a single skb may pass through multiple net_devices; do not interpret "reached net_device" as having reached the physical NIC.
5. Why GRO, GSO, and TSO change packet capture perception
GRO merges compatible packets on the receive side, reducing the number of protocol-processing operations; GSO describes how to segment larger skbs later, which software can complete; TSO is hardware offload for TCP segmentation. GSO does not mean "only hardware segmentation," and GRO does not unconditionally merge any same-connection data into a single packet.
Capture position: Objects before segmentation are not equal to in-flight frames
Therefore, the "large TCP packet" captured locally may just be the skb before segmentation; in-flight traffic is still sent according to the path MTU. When the transmit-side checksum is yet to be completed by hardware, packet captures showing incomplete checksums are not necessarily line errors. Verification must occur at the correct observation point; do not adjust MTU or disable all offloads based solely on local captures. See Segmentation Offloads.
6. RSS, RPS, RFS, XPS: What to choose for each
Mechanism
Selection Point
Goal
RSS
NIC RX queue selection
Hardware distribution of receive load
RPS
CPU for subsequent skb processing
Software distribution of processing; can generate cross-CPU notifications
RFS
Combines flow and application running location
Improves locality for protocol processing and application
XPS
TX queue selection
Reduces transmission contention by mapping based on CPU/RX queue, etc.
RSS often maps via hash and indirection tables and cannot be simply defined as a four-tuple hash modulo the number of queues. Configuration, encapsulation, fragmentation, and NIC capabilities affect which fields participate in the hash; modifying indirection or mapping may also cause traffic migration.
Stable mapping for the same flow aids in-order delivery and cache hits but does not replace TCP's own sequence handling. Increasing the number of queues also increases interrupt, memory, and CPU migration costs. See Network Scaling Mechanisms.
These are cumulative counts from the device perspective; virtual and physical devices may count the same traffic, and GRO/GSO as well as driver definitions affect observation. /proc/softirqs statistics count processing instances, not packet counts; private fields in ethtool -S are not unified NAPI statistics interfaces.
Take the difference between two time points and correlate it with the actual load window. For finer-grained root causes, check driver counters, socket statistics, or controlled tracing, rather than interpreting all rx_dropped as the NIC RX ring being full.