アプリケーションでの send 成功、NIC での送信完了、相手側からの ACK、ビジネスロジックの処理終了は、それぞれ異なる時点です。送受信パスに沿って、オンラインパケット、skb、アプリケーションのバイトストリーム をそれぞれ追跡する必要があります。これらは必ずしも一対一で対応するわけではありません。
参照目標 参照箇所
データを参照する skb 第1節
受信、NAPI、送信完了 第2~4節
オフロード、キュー割り当て、カウント 第5~7節
1. skb は記述子であり、固定された連続したデータパケットではありません
struct sk_buff はメタデータを格納し、実際のデータバッファを参照します。リニア領域は head/data/tail/end の論理的な位置によって記述されます。これらのメンバーの一部は実装内でオフセットとして保存されており、すべてが直接間接参照可能な C ポインタではありません。
skb メタデータとデータストレージの分離
headroom
リニア有効データ
tailroom
shared info
head
data
tail
end
外部ページフラグメント
skbの総長に含まれる
len = リニア長 + data_len;cloneはデータを共有でき、変更前には書き込み可能であることを確認する必要があります。
skb->len は総データ長、data_len は非リニア部分の長さ、skb_headlen() はリニア有効長を取得します。header メンバーは主に相対位置を示しており、アクセスには適切なインターフェースである ip_hdr() や tcp_hdr() などを使用する必要があります。GSO 情報や frags などは skb_shared_info などの構造体に位置しており、それらを偽造された skb 定義に無理やり展開してはいけません。
受信パケットの解析時には、skb_pull() によって data ポインタを進めることができます。カプセル化時には、skb_push() によって headroom を利用してヘッダーを前に配置します。ただし、これを行うには、スペース、リニア範囲、共有状態の条件を満たしている必要があり、単に呼び出したからといって自動的に拡張されたり、コピーオンライトが確立されたりするわけではありません。
操作 新たに追加されるもの 書き込み前に確認すべきこと
skb_clone 新しいメタデータ、共有される基底データ データの書き込み可能性と共有関係
skb_get 同一 skb への参照 並行アクセスプロトコル
参照カウントは寿命を維持しますが、任意の並行変更権限を付与するものではありません。詳細は skb ドキュメント を参照してください。
2. 受信データがアプリケーションに到達するまでの流れ
ローカル受信:まずパケットを処理し、その後ソケットに渡す
ドライバによる受信完了処理
NAPI がバッファを取得;native XDP は skb より前に実行可能
1
skb とリンク層処理
GRO、VLAN、パケットタップ、ブリッジなどがパスに応じて参加
2
IP 処理とルーティング
PREROUTING 後にローカル受信か転送かを決定
3
ローカルプロトコル配信
INPUT 後に TCP/UDP ソケットを検索し、プロトコル処理を行う
4
アプリケーションが読み取り可能なデータを取得
ウェイクアップは通知に過ぎず、データは recv などのインターフェースによって読み取られる
5
図は一般的なローカル IP パスを示しています。generic XDP、トンネリング、バイパス、オフロードは、通過するノードを変更する可能性があります。
DMA はフレームをドライバが管理するバッファに書き込み、完了ディスクリプタはドライバに対して処理可能なデータがあることを伝えます。ドライバは DMA キャッシュ、順序、バッファ回収の規則に従う必要があり、受信完了を検出しても、すぐにそのページを NIC に再割り当てして上書きすることはできません。
一般的な native XDP は、通常の skb を構築する前に実行されます。packet socket のパケットキャプチャエントリポイント、ブリッジ RX ハンドラ、VLAN 処理は異なるメカニズムであり、すべてを 1 つの RX ハンドラリストとして描画することはできません。generic XDP の位置も native XDP とは一致しません。
IP PREROUTING はルーティング判断の前に実行されます。ローカル配信は引き続き INPUT を経由し、転送の場合は FORWARD と出口処理を経由します。PREROUTING を ip_local_deliver() の後に配置すると、DNAT やルーティング分析を誤解させる原因となります。
TCP 受信では、接続、シーケンス、ウィンドウ、チェックサム、状態のチェックも必要であり、データが対応する受信構造体に並べられた後に待機者に通知されます。sk_data_ready() などのウェイクアップは、アプリケーションがすでにスケジューリングされていることを意味するものではなく、さらにアプリケーションがすべてのデータをコピーまたは処理したことを意味するものでもありません。MSG_DONTWAIT は待機動作を変更するだけで、softirq が現在のユーザーバッファに自動的に直接コピーするための特殊なパスを取得するわけではありません。
3. NAPI:予算配分による処理時間の管理
NAPI は poll メソッドを使用して受信と送信完了を処理し、通常は softirq で実行されますが、スレッド化された NAPI やビジーポーリングなどの方法もサポートしています。従来の割り込み駆動モードでは、割り込みによるスケジューリング後、関連するデバイスの割り込みをマスクし、適切な作業を処理してからポーリングを完了し、通知を復元します。
予算:単一の poll と全体の NET_RX ラウンドを別々に考える
poll コールバックの budget は、このラウンドの RX 作業量を制限するものであり、全体の NET_RX ラウンドの netdev_budget と混同して「各 NIC ごとに毎回固定 300 パケット」と解釈してはいけません。TX 完了のクリーンアップは、同じ RX 予算を単純に消費するわけではありません。budget が 0 の呼び出しには特別な制限があり、RX/page-pool/XDP 操作の実行や完了インターフェースの呼び出しはできません。
予算を使い果たすことは、通常、さらに poll を続ける必要があることを意味します。たまたま処理が完了し、作業数が budget と一致した場合でも、API の規約に従って処理し、完了の誤報や後続処理の漏れを防ぐ必要があります。napi_complete_done() の成功した戻り値は、デバイス割り込みを復元するかどうかの決定に関与します。
NAPI インスタンス、IRQ、RX/TX キューペアは、しばしばほぼ一対一で対応する構成ですが、プロトコルはこのマッピングを強制するものではありません。1 つのインスタンスが複数のキューをサービスでき、送受信を分離することも可能です。詳細は NAPI ドキュメント を参照してください。
poll 予算を調整し、キューにパケットを追加します。「ちょうど予算を使い果たし、かつちょうどキューが空になる」という境界条件に注目してください。キューが空だからといって、必ずしもこのラウンドで NAPI スケジューリングの所有権を解放したことを意味するわけではありません。
図を準備しています 再読み込み
キューが空なのに、なぜまだ poll を続ける可能性があるのか? budget はこの poll における RX 作業の上限です。予算を使い果たした後に継続してポーリングする戦略を採用する場合、キューがたまたま空になったとしても、次のラウンドで予算不足を返す際に完了し、割り込みを復元する必要があります。
{"id":"napi-budget-and-completion","title":"キューが空なのに、なぜまだ poll を続ける可能性があるのか?","summary":"budget はこの poll における RX 作業の上限です。予算を使い果たした後に継続してポーリングする戦略を採用する場合、キューがたまたま空になったとしても、次のラウンドで予算不足を返す際に完了し、割り込みを復元する必要があります。","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":"1回の到着パケット数","arrived":"到着パケットはリングに保存され、通知は処理の手配のみを行う。通知回数はキュー内のパケット数を代替しない。","budget":"今回のpollのRX budget","completed":"戻り値がbudgetより小さい場合、このモデルでNAPIの完了と割り込み復元に成功。","dropped":"このキューが満杯のためドロップ","exact":"今ラウンドで完全なbudgetを返し、スケジューリング維持。残りのパケット数が0でも、完了するために一度さらにpollする。","heading":"キューが空なのに、なぜまだ poll を続ける可能性があるのか?","idle":"完了済み;IRQ通知可能","initial":"キュー空、デバイス通知待ち。","inject":"このバッチのパケットを到着させる","intro":"今ラウンドの RX budget と到着パケット数を調整し、直接注入するか poll します。「予算未満で処理が完了する場合」と「ちょうど予算を使い果たす場合」の最終ラウンドを特に比較します。","pending":"キュー内のパケット数","poll":"1回pollを呼び出す","queue":"RXリング:実色は未処理のパケット","reset":"リセット","scheduled":"スケジューリング維持;IRQマスク","scope":"32項目の単一RXキュー、budget=1…8、従来の割り込みトリガー後マスクモデル。カーネルドキュメントが許可する「完全なbudgetを返し、次のラウンドで完了する」戦略を採用。budget=0、TX完了、完了競合、複数キュー、ビジーポーリングは省略。満杯キューでのパケットドロップは、このモデルのリング容量を示すのみ。","scopeLabel":"モデル範囲と規約","state":"スケジューリング状態 / 割り込み状態"}}
スループットとレイテンシのトレードオフ
バッチ処理の増加、ハードウェア割り込みマージ、またはポーリングを行うことで、通知コストを分散できます。しかし、最初のパケットの待機時間が長くなったり、ビジーなキューがCPUをより長く占有したりする可能性があります。予算の使い果たしは、ソフト割り込み時間、キューバックログ、ドロップ、アプリケーションレイテンシと組み合わせて判断する必要があり、割り込みが少ないからといって即座に「高速」と断定することはできません。
4. 送信完了の2つの意味
アプリケーションでの send 成功は、通常、データがソケットによって受信されたか、送信パスに並べられたことを示します。TCP ではさらにウィンドウ、輻輳制御、パシングの制約を受け、その後 skb が構築され、ルーティング、Netfilter、qdisc、ドライバを経由して送信されます。
完了地点 これに基づいて確認できること
ローカル TX 完了 通常、対応する転送リソースを回収できる
TCP ACK 相手側スタックがシーケンス範囲を確認
ビジネス応答 アプリケーションプロトコルに基づいて処理結果を判断
NIC の完了は、後者の2つの境界を越えていません。レイテンシを調査する際は、どの種類の完了を待っているかを明確にする必要があります。
ネットワークドライバは net_device_ops などのインターフェースを通じてデバイス操作を提供し、統計は通常 rtnl_link_stats64 などのインターフェースを通じてエクスポートされます。仮想デバイスも net_device を実装しているため、ある skb が複数の net_device を通過する可能性があります。「net_device に到達した」ことを物理 NIC に到達したと理解してはいけません。
5. GRO、GSO、TSO がパケットキャプチャの認識をどう変えるか
GRO は受信側で互換性のあるパケットをマージし、プロトコル処理回数を削減します。GSO は後続の大きな skb をどのように分割するかを記述し、ソフトウェアで分割処理が可能です。TSO は TCP 分割のハードウェアオフロードです。GSO は「ハードウェア分割のみ」という意味ではなく、GRO も任意の同一接続データを無条件に1つのパケットに合成するわけではありません。
パケットキャプチャ位置:分割前のオブジェクトはオンラインフレームと等しくない
したがって、ローカルでキャプチャされた「大きな TCP パケット」は、単に分割前の skb である可能性があり、オンラインではパス MTU に従って送信されます。送信側のチェックサムがまだハードウェアによって補完されていない場合、パケットキャプチャでチェックサムが未完成と表示されても、必ずしも回線エラーを意味するわけではありません。正しい観測点で検証する必要があり、ローカルパケットキャプチャのみに基づいて MTU を調整したり、すべてのオフロードを無効にしたりしてはいけません。詳細は分割オフロード を参照してください。
メカニズム 選択位置 目標
RSS NIC RX キュー選択 ハードウェアによる受信負荷分散
RPS skb が後続処理に入る CPU ソフトウェアによる処理分散、CPU 間通知を発生させる可能性あり
RFS フローとアプリケーションの実行位置を組み合わせ プロトコル処理とアプリケーションの局所性を向上
XPS TX キュー選択 CPU/RX キューなどのマッピングに基づき送信競合を削減
RSS は通常、ハッシュとインディレクションテーブルによってマッピングされ、単純に4タプルハッシュをキュー数で割った剰余として規定することはできません。設定、カプセル化、フラグメンテーション、および NIC の能力は、ハッシュに参加するフィールドに影響を与えます。インディレクションやマッピングを変更すると、トラフィックが移行する可能性があります。
同一フローの安定したマッピングは順序保持とキャッシュヒットに有利ですが、TCP 独自のシーケンス処理を代替するものではありません。キュー数が増加すると、割り込み、メモリ、CPU 移行のコストも増加します。詳細はネットワークスケーリングメカニズム を参照してください。
7. 読み取り専用キューとカウントの観察
from pathlib import Path
for dev in sorted ( Path ( ' /sys/class/net' ) . glob ( ' *' ) ) :
names = sorted ( p . name for p in ( dev / ' queues' ) . glob ( ' *' ) )
counters = { }
for key in ( ' rx_packets' , ' tx_packets' , ' rx_dropped' , ' tx_dropped' ) :
try :
counters [ key ] = int ( ( dev / ' statistics' / key ) . read_text ( ) )
except OSError :
pass
print ( dev . name , names , counters )
これらはデバイス視点の累積カウントです。仮想デバイスと物理デバイスは、同じトラフィックをカウントする可能性があります。GRO/GSO およびドライバの定義は、観測に影響を与えます。/proc/softirqs の統計は処理回数を示しており、パケット数ではありません。ethtool -S のプライベートフィールドも、統一された NAPI 統計インターフェースではありません。
2つの時点での値の差を計算し、実際の負荷ウィンドウと対応させます。より細粒度な原因を特定する必要がある場合は、すべての rx_dropped を NIC RX リング満杯として解釈するのではなく、ドライバカウント、ソケット統計、または制御されたトレーシングを参照してください。