ネットワークスタックのカーネル内データパス
パケットがNICからアプリへ届くまでカーネル内で何が起きるか。sk_buffのゼロコピー、NAPIの割り込み緩和、GRO/TSOのオフロードまで、Linuxネットワークの速さの正体を原理から掴めます。
- 1.パケットはsk_buffという制御構造で表現され、本体データは触らずヘッダのポインタ(head/data/tail/end)だけ動かすことで、各層をまたぐコピーを避ける。
- 2.NAPIは「1パケット1割り込み」をやめ、最初の割り込みで以後を無効化しポーリングで一括処理する割り込み緩和。高負荷ほどバッチ効率が上がる。
- 3.GRO(受信)とGSO/TSO(送信)は複数の小パケットを1つの大セグメントに束ねて層をまたがせ、per-packetの固定コストを劇的に削る。
カーネル内データパスとは何か
アプリが recv() で受け取る1バイトは、NIC(ネットワークインターフェースカード)に電気信号として届いてから、ドライバ・プロトコル処理・ソケットバッファという何段もの層を通り抜けてきています。このNICとソケットの間を流れる経路がカーネル内データパスです。ここでの設計判断は「1パケットあたりに固定でかかるコスト(割り込み・関数呼び出し・コピー・ロック)を、どうやってパケット数で割り算して薄めるか」に集約されます。本記事は、その薄め方の中核である sk_buff・NAPI・オフロード を内部から解剖します。
sk_buff:パケットを表す中心構造
カーネルが扱うパケットは1つ残らず struct sk_buff(通称 skb)で表現されます。skbは2つの領域に分かれます。
- メタデータ(skb本体):プロトコル種別、入力デバイス、チェックサム状態、各層ヘッダへのポインタなどの制御情報。
- データバッファ:実際のパケットの中身(イーサネットヘッダ+IPヘッダ+TCPヘッダ+ペイロード)。
データバッファには4つのポインタが付随します。head(確保領域の先頭)、data(現在の有効データ先頭)、tail(有効データ末尾)、end(確保領域の末尾)です。head と data の間の隙間をヘッドルーム、tail と end の間をテールルームと呼びます。
[ headroom ][===== 有効データ =====][ tailroom ]
^head ^data ^tail ^end
この構造が効くのは層をまたぐ瞬間です。送信時、TCP層がヘッダを足すには skb_push() で data をヘッドルーム側へ戻すだけ、IP層・イーサネット層も同様に data を前へ動かすだけで自分のヘッダ領域を確保できます。受信時は逆に skb_pull() で data を後ろへ進め、処理済みヘッダを論理的に剥がします。ペイロード本体は一度も移動しません。これがカーネル内ゼロコピーの第一の柱です。
ドライバが受信バッファを確保する時点で、後段が skb_push できるようあらかじめヘッドルームを空けておくのが定石です。スタックを下りながら各層がヘッダを足す送信経路でも、ヘッドルームが足りないと再確保とコピーが発生します。NET_SKB_PAD などの定数で確保される余白は、この「後で足すヘッダの置き場」を保証するための設計です。
さらに大きなペイロードは、線形バッファに収まらないぶんを skb_shared_info 配下のページフラグメント配列で持ちます。skbの**複製(clone)**では、メタデータだけ別に持ちデータバッファは参照カウントで共有します。tcpdumpがパケットを覗くときも、本体をコピーせずcloneで枝分かれさせるため安価です。
ソケットからNICまでの処理経路
送信はスタックを下る動きです。send() のデータはまずソケットの送信バッファに積まれ、TCP層がセグメント分割・シーケンス番号付与・チェックサム準備を行い、IP層がルーティングとフラグメント判断、近隣探索(ARP)でL2アドレスを解決し、最後にqdisc(キューイング規律)を経てドライバの送信リングへ渡ります。
受信はスタックを上る逆向きの動きですが、ここに割り込みが絡みます。素朴な実装では「パケット1個到着 → 割り込み1回 → 全処理」ですが、これでは高負荷で割り込みが嵐になります。そこでLinuxは処理を二段に割ります。
- 上半分(ハードウェア割り込み):NICが「届いた」と知らせる。ここでは最小限——後述のNAPIをスケジュールするだけ。
- 下半分(ソフト割り込み, softirq):
NET_RX_SOFTIRQの文脈で、実際のプロトコル処理(GRO・IP・TCP)をまとめて回す。
この上半分/下半分の二段構えは割り込み処理の二段構え(上半分/下半分)で詳説した設計そのもので、ネットワーク受信はその最大の応用例です。割り込みそのものの仕組みは割り込みと入出力(I/O)を土台にしています。
NAPI:割り込み緩和の原理
NAPI(New API)は「1パケット1割り込み」をやめる仕組みです。流れはこうです。
- パケット到着でハード割り込みが入ると、ドライバはそのデバイスの受信割り込みを無効化し、NAPIをソフト割り込みのポーリングリストに登録する。
- softirq文脈で
poll()が呼ばれ、受信リングから一度に最大 budget 個(既定64など)のパケットを引き抜いてスタックへ流す。 - リングが空になれば割り込みを再び有効化して通常モードへ戻る。空にならず budget を使い切れば、割り込みは無効のまま次のpollサイクルで続きを処理する。
低負荷: 割込→無効化→poll(数個)→空→割込有効化 …割込ありモード
高負荷: 割込→無効化→poll(budget)→まだ有る→poll…→割込はずっと無効
ここが核心です。負荷が高いほど1回の割り込みで処理できるパケットが増え、割り込み1回あたりのコストがパケット数で割り算されて薄まります。低負荷では割り込み駆動で低遅延、高負荷では自動的にポーリング駆動へ移って高スループット——この自己調整が、固定閾値の割り込みコアレッシングにはない強みです。budget で1デバイスの処理量に上限を設けるのは、1枚のNICがCPUを占有して他デバイスやプロセスを飢餓させないスケジューリング上の公平性のためでもあります。
「NAPIは何を緩和するのか」を問われたら、答えは割り込み回数です。コピーを消すのはゼロコピー、システムコールを消すのは別の話。NAPIは受信割り込みを一時的に止めてポーリングに切り替え、per-packetの割り込みオーバーヘッドをバッチ化で償却します。「高負荷ほど効く」理由まで言えると満点です。
GRO/GSO/TSO:オフロードでpacketを束ねる
per-packetの固定コストはプロトコル処理自体にもあります。IPルーティング探索、TCP状態更新、各層の関数呼び出し——これらは1パケットごとに発生します。そこで複数の小パケットを1つの大セグメントとして層に通すのがオフロードの発想です。
| 名称 | 方向 | 束ねる場所 | 効果 |
|---|---|---|---|
| GRO | 受信 | ドライバ/NAPI poll内(ソフトウェア) | 連続セグメントを1つの大skbに合体させ、上位層を1回だけ通す |
| GSO | 送信 | スタック内(ソフトウェア、ドライバ直前で分割) | 大セグメントのまま下り、最後にMTUへ分割。分割を遅延させる |
| TSO | 送信 | NICハードウェア | 大セグメントをNICが自動でMTUごとに分割。CPUは分割に一切関与しない |
**GRO(Generic Receive Offload)**は受信側で働きます。NAPIのpoll中、同じフローに属し連続する複数のTCPセグメントを見つけたら、ペイロードをページフラグメントとして繋ぎ1つの巨大skbに合体させます。IP層・TCP層はこの大skbを1回処理するだけで済み、何十個分ものヘッダ処理・ルーティング探索が1回に圧縮されます。合体できる条件(同一フロー・連続シーケンス・互換オプション)を満たさなければ束ねず、正しさは保たれます。
**送信側のGSO(Generic Segmentation Offload)**は逆方向の遅延戦略です。TCP層は最初からMTUサイズに刻まず、大きな1セグメントのままスタックを下らせ、分割をドライバ直前まで先延ばしします。層を通る回数が減るぶんCPUが軽くなります。
TSO(TCP Segmentation Offload)はGSOの分割作業すらNICハードウェアへ丸投げしたものです。カーネルは64KB級の大セグメントとテンプレートヘッダを渡すだけで、NICがMTUごとのパケットに切り分け、各パケットのシーケンス番号とチェックサムを補完します。CPUはセグメント分割から完全に解放されます。チェックサム計算自体をNICに任せるチェックサムオフロードも併用され、skbの ip_summed フィールドが「計算済み/NIC任せ」の状態を運びます。
受信のGROは「小さく届いたものを大きくまとめてから上げる」、送信のTSO/GSOは「大きいまま下ろして最後に小さく割る」。どちらも層をまたぐ回数=per-packetコストの発生回数を減らす点で同一思想です。GROで束ねたものをそのまま転送するルータ/ブリッジでは、送信時にGSOで割り直されるため、受信GRO→送信GSOが綺麗に対になります。
GROや割り込みコアレッシングはスループット最適化と引き換えに、束ねる待ち時間ぶんの遅延を足します。レイテンシ最重視のワークロード(高頻度取引、一部のRPC)では、あえてGROを切る・割り込みコアレッシングを弱める調整が効くことがあります。さらにTSO/GROにはNICドライバ実装のバグでまれにコーナーケースが残ることがあり、原因不明のTCP不調の切り分けで ethtool -K によるオフロード無効化が定石の一手になります。
まとめ
カーネル内データパスの全設計は「per-packetの固定コストをいかに薄めるか」という一点に貫かれています。sk_buffはhead/data/tailのポインタ操作だけで層をまたぎ、cloneは参照カウントで本体を共有してコピーを避けます。NAPIは最初の割り込み以後を無効化してポーリングへ移り、割り込み回数をバッチで償却——高負荷ほど効く自己調整です。GRO/GSO/TSOは小パケットを大セグメントに束ね、層をまたぐ回数そのものを削ります。土台にあるのは割り込みと入出力(I/O)と上半分/下半分の二段構え。この経路が腑に落ちると、高性能I/Oモデル(epoll・io_uring)の内部が「カーネルが終えた仕事をアプリへ渡す最後の窓口」だと一本の線で見えてきます。
OS Article
ネットワークスタックのカーネル内データパスを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
Linuxカーネル
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
NAPIは「1パケット1割り込み」をやめ、最初の割り込みで以後を無効化しポーリングで一括処理する割り込み緩和。高負荷ほどバッチ効率が上がる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「Linuxカーネル / ネットワーク」に近いか確認する。
- 強みである「パケットはsk_buffという制御構造で表現され、本体データは触らずヘッダのポインタ(head/data/tail/end)だけ動かすことで、各層をまたぐコピーを避ける。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。