ネットワークスタックの概要

このページの目次

アプリケーションでの 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 作業の上限です。予算を使い果たした後に継続してポーリングする戦略を採用する場合、キューがたまたま空になったとしても、次のラウンドで予算不足を返す際に完了し、割り込みを復元する必要があります。

スループットとレイテンシのトレードオフ

バッチ処理の増加、ハードウェア割り込みマージ、またはポーリングを行うことで、通知コストを分散できます。しかし、最初のパケットの待機時間が長くなったり、ビジーなキューが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 を調整したり、すべてのオフロードを無効にしたりしてはいけません。詳細は分割オフロードを参照してください。

6. RSS、RPS、RFS、XPS のそれぞれの選択

メカニズム選択位置目標
RSSNIC RX キュー選択ハードウェアによる受信負荷分散
RPSskb が後続処理に入る CPUソフトウェアによる処理分散、CPU 間通知を発生させる可能性あり
RFSフローとアプリケーションの実行位置を組み合わせプロトコル処理とアプリケーションの局所性を向上
XPSTX キュー選択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 リング満杯として解釈するのではなく、ドライバカウント、ソケット統計、または制御されたトレーシングを参照してください。