はじめに
こんにちは。(株)東芝 総合研究所の中川と申します。
業務ではネットワークに関する研究開発を行っており、その中でLinuxカーネルのネットワークスタックやデバイスドライバ周辺のコードを触る機会が多くあります。
カーネルレベルでネットワーク機能を開発していると、ユーザ空間から見れば単に「通信できない」なのですが、実際には「NICレベルではパケットが来ているのにIPスタックの前で落ちている」「TCPレベルではコネクションが張れているのにアプリケーションのrecv()まで届かない」といった具合に、原因が複数レイヤにまたがっていることが多くあります。このときに重要になるのが、「どのレイヤで、どのイベントが起きているのか」を段階的に切り分けていくことです。
そこで本記事では、私が日頃の開発・デバッグでよく使っているツールや手法を、何をどのようなレベルで観測することを主目的としているかで以下のように分類し、説明していきます。なお、以下で示したツールや手法の分類は、使用方法によって何を観測するかが変化したり、分類をまたぐことがあるため、厳密な分類ではないことに注意してください。
- ソケット/接続状態:netstat, ss
- パケット:tshark, tcpdump
- インタフェース単位のカウンタ:ip -s link
- ドライバ/NIC:ethtool
- カーネルの処理経路/イベント:systemtap, dropwatch, ftrace
- カーネルコードレベル:print_hex_dump
また、これらのツールや手法をそれぞれ以下の三点で整理します。
- その技術がそもそもどのようなものか(概要)
- Linuxカーネル内部でどのように実装/動作しているか(仕組み)
- 実務上どんなときに使うと便利か(何を見るときに使うか)
なお、本記事はツールの使い方(コマンドオプションやインストール方法)ではなく、「どのレイヤで何が見えるのか」「どういう観点でツールを選ぶのがよいのか」に焦点を当てています。
ソケット/接続状態(netstat, ss)
概要
netstatやssは、カーネルが保持しているソケットの情報をユーザ空間から一覧するためのツールです。どのポートがLISTEN状態にあるのか、どの接続がESTABLISHED/CLOSE-WAIT/TIME-WAITなのか、各ソケットの送受信キューにどれだけデータが溜まっているのか、どのプロセスがそのソケットを所有しているのか、といった情報を確認できます。netstatは比較的古い実装で、ssはiproute2に含まれる新しめの実装です。
仕組み
ここでは、netstatとssがどのようにカーネル内部のソケット情報を取得しているかを説明します。
まずnetstatですが、これは主に/proc/net/tcpや/proc/net/udpといったprocfs上の仮想ファイルをread()し、カーネルがその場で生成したソケット情報をユーザ空間にコピーして表示します。例えば/proc/net/tcpのreadが実行されると、カーネルはreadのシステムコールを受け取り、proc_create_seq()によって登録されたseq_operationsのshow関数であるnet/ipv4/tcp_ipv4のtcp4_seq_show()を実行します。この関数は、struct tcp_sockの情報を文字列化してstruct seq_fileが管理するバッファに書き込みます。その後seq_read()がこのバッファの内容をユーザ空間のバッファにコピーをします。このコピーした内容が、/proc/net/tcpに書かれているように見え、netstatに返されるという流れです。
一方ssは、netlinkというカーネルとユーザ空間の間の通信メカニズムを利用しています。netlinkは「カーネル内の各サブシステムとユーザ空間のツールがメッセージをやり取りするためのソケットの一種」で、ルーティング情報(rtnetlink)やネットフィルタ設定(nfnetlink)などに広く使われています。詳細に説明すると、まずはssではnetlinkでメッセージタイプSOCK_DIAG_BY_FAMILYを送信します。カーネル側ではこれを受信すると、inet_diag_cmd_exact()はコマンドがSOCK_DIAG_BY_FAMILYであると判定すると、netlink_dump_start()を用いてinet_diag_dump()をコールバックとして登録します。inet_diag_dump()はソケット情報を走査・収集し、その結果をnetlinkメッセージとしてユーザ空間に返信するという流れです。
何を見るときに使用するか
ソケット状態の可視化ツールは、アプリケーション視点の問題を切り分ける最初のステップとして使います。例えば、TCPのハンドシェイクが完了しているかどうかを確認する場合、ssの出力で接続がESTABLISHEDになっているか、あるいはSYN-SENTのまま止まっていないかを見ます。また、CLOSE-WAIT状態のソケットが大量に残っている場合は、アプリケーションが接続クローズの片側処理を正しく実装していない可能性を疑えます。
送受信キューの長さは、「どこかが詰まっている」兆候を教えてくれます。送信キューにデータが溜まり続けている場合は、ネットワーク側が遅くてカーネルがなかなかフレームを送り出せていないか、あるいは相手がACKを返しておらず輻輳制御でウィンドウが狭まっているかもしれません。一方、受信キューが溢れている場合は、アプリケーションがrecv()する速度が足りず、カーネル側にデータが溜まっている可能性があります。
さらに、各ソケットがどのプロセスに紐付いているかを見ることで、「想定外のプロセスがポートをバインドしていないか」「古いプロセスがソケットを掴んだまま残っていないか」といった問題も特定できます。
パケットキャプチャ(tshark, tcpdump)
概要
tsharkやtcpdumpは、カーネル空間でパケットをキャプチャし、その内容をL2からL7レベルまで解析するためのツールです。WiresharkのCLI版がtsharkであり、tcpdumpは軽量で、サーバや組込みLinuxでも使いやすいキャプチャツールです。
これらのツールを使うことで、ARPやIPv4/IPv6の基本的なトラフィックから、VLANタグ(802.1Q)、VXLAN/GREのようなトンネルまで、実際に流れているビット列を直接観察できます。
仕組み
パケットキャプチャツールは基本的にlibpcapというライブラリを経由してカーネルのパケットキャプチャ機構を利用します。libpcapはOSごとに異なるカーネルのキャプチャAPIを吸収し、アプリケーションからは統一的なインタフェースに見えるようにしています。
パケットキャプチャツールがlibpcapを用いてsocket(AF_PACKET, SOCK_RAW, ...)を呼び出すと、カーネルはAF_PACKETソケットと呼ばれる「L2フレームを扱うソケット」を作成します。AF_PACKET(Address Family Packet)は、NICドライバが受信したフレームをIPスタックに渡す前の段階でコピーし、ユーザ空間に渡します。具体的には、受信フレームをstruct sk_buffというデータ構造のskbインスタンスとし、これをAF_PACKETを通してユーザ空間に渡しています。skbは、L2/L3/L4の各ヘッダへのポインタ(skb->header)や、データ部分(skb->data)、メタデータ(skb->protocolなど)を保持しています。
また、libpcapがpcap_setfilter()でフィルタを設定すると、カーネルにBPF(Berkeley Packet Filter)プログラムがロードされます。これは「この条件にマッチするパケットだけユーザ空間に渡してほしい」といったフィルタをカーネル側で実行するための仕組みです。つまり、BFPによって条件に合わないフレームはコピーしてユーザ空間に渡されずに破棄されます。これにより、不要なパケットをユーザ空間に上げずに済み、キャプチャのオーバーヘッドを抑えることができます。
何を見るときに使うか
パケットキャプチャは、「本当にパケットが流れているのか」「どのレイヤまで届いているのか」を確認するための基本的な手段です。例えば、TCPの3wayハンドシェイクが期待どおりSYN → SYN/ACK → ACKの順に進んでいるか、あるいはどこかで止まっているのかを確認できます。
IPアドレスやポート番号の設定ミスなども、パケットキャプチャを見ればすぐに分かります。NATの前後のインタフェースでキャプチャし、L3/L4ヘッダの変化を比較すれば、「どこでアドレスが書き換わっているか」「期待どおりのアドレスに変換されているか」が明らかになります。
カーネルの処理経路/イベント(dropwatch, ftrace, systemtap)
dropwatch
概要
dropwatchは、Linuxカーネル内部でパケットが「ドロップされた地点」を関数単位で可視化するツールです。ユーザ空間から見ると単に「パケットが届かない」という現象でも、実際にはNICドライバ、IP層、netfilter(iptables/nftables)、ソケット層など、さまざまな箇所でskbが破棄されている可能性があります。dropwatchは、「どの関数の中でskbが解放されたのか」を集計することで、パケットがどのレイヤで捨てられているかを把握するのに役立ちます。
仕組み
dropwatchは、Linuxカーネルが提供するtracepoint(トレースポイント)を利用します。tracepointは、カーネルのソースコード中に仕込まれた「計測用のフック」であり、特定のイベントが発生したときに、登録されたハンドラが呼ばれる仕組みです。ネットワーク関連ではkfree_skbやkfree_skb_list、net:skb:kfree_skbといったtracepointがあり、skbが解放されるタイミングで発火します。
dropwatchはこのtracepointにフックし、呼び出し元アドレス(return address)や、関数シンボルを収集します。ユーザ空間側のdropwatchコマンドは、netlinkを通じてこの情報を受け取り、「ip_rcv_finish()内で何回dropが起きたか」「tcp_v4_rcv() 内で何回dropが起きたか」といった形で集計結果を表示します。カーネル内部ではスタックトレース全体を取得するのではなく、「どの関数からkfree_skbが呼ばれたか」という単位で情報を圧縮しているため、比較的低いオーバーヘッドでdropの傾向を観測できます。
tracepoint自体は「静的トレースポイント」と呼ばれる仕組みで、あらかじめソースコードに埋め込まれており、動的に挿入されるkprobeなどと比べるとオーバーヘッドや安全性の制御がしやすくなっています。dropwatchはこの既存の仕組みをうまく活用し、ネットワークスタック全域にわたるdropイベントを効率的に集計しています。
何を見るときに使うのか
dropwatchは、「パケットキャプチャツールではパケットが見えているのに、アプリケーションに届かない」といった状況で特に役立ちます。この場合、パケットはカーネル内部のどこかで捨てられているはずですが、その場所が分からないと、ルーティングやnetfilterの設定、ソケットオプション、ドライバの不具合など、調査範囲が非常に広くなってしまいます。
dropwatchを実行すると、どの関数でdropが多発しているかが数値で見えてきます。例えばip_rcv()直後の関数で dropが多いならルーティングやフィルタの問題を疑えますし、tcp_v4_do_rcv()内でdropが多いなら、TCPのステートを調べた方がいいとわかります。
ftrace
概要
ftraceは、Linuxカーネルに組み込まれている関数トレースの仕組みです。指定した関数が呼び出されたときに、その関数自身やその後に呼ばれる関数の情報を、スタックトレースや実行時間付きで記録できます。ネットワークデバッグでは、例えば「このパケットがIRQ発火からソケットに届くまでにどの関数チェーンを通ったか」を調べたり、「どの関数で異常に時間がかかっているか」を見たりするのに有用です。
仕組み
ftraceを利用するには、カーネルコンフィグでCONFIG_FUNCTION_TRACERやCONFIG_FUNCTION_GRAPH_TRACERなどを有効にしておく必要があります。これらが有効な状態でビルドされたカーネルでは、コンパイル時に各関数の先頭にmcountや__fentry__といったプロファイリング用のフックが挿入されます。このフックは、ランタイムで有効化されるとftraceのトランポリンを経由して、トレースのためのハンドラに制御を渡します。
ユーザ空間からは/sys/kernel/debug/tracing/以下のインタフェースを叩いてftraceを制御します。例えば current_tracerにfunctionやfunction_graphを書き込むことでトレーサの種類を設定し、set_ftrace_filterに関数名(例えばnetif_receive_skbやip_rcv)を書き込むと、その関数を起点にトレースが行われるようになります。トレース結果はtraceというファイルにリングバッファ形式で蓄積されます。
functionトレーサは「どの関数が呼ばれたか」の列挙に特化しており、function_graphトレーサは「関数の入口と出口」「ネスト関係」「実行時間」を可視化します。ネットワークスタックの入口となる__netif_receive_skb_coreやdev_queue_xmitを入口に指定すると、IRQハンドラからsoftirq(ソフトウェア割り込み)、ネットワークスタック各層、ソケット層までの関数呼び出しの流れを時系列で追うことができます。
何を見るときに使うか
ftraceは、dropwatch などで「怪しい関数」が特定できたあと、その前後の経路や時間の使い方を詳しく知りたい場面で活躍します。例えば、ある関数でskbが落とされていることが分かっているなら、その関数をftraceの入口にして、どの呼び出し元からそこに到達しているのか、呼び出しの前後でどんな関数が動いているのかを確認できます。
また、性能問題の解析にも向いています。function_graphトレーサを使うと、特定の処理パスにおける関数ごとの実行時間を可視化できるため、「IRQハンドラで時間を使いすぎているのか」「softirqでの処理が重いのか」「ドライバの送信処理がボトルネックなのか」といったことを推測できます。ネットワークスタックは割り込み、ソフト割り込み、ksoftirqdスレッドなどが絡むため、実行コンテキストを正しく理解するのが難しいのですが、ftraceのログを眺めると「どのコンテキストでどの関数が動いているか」がはっきりします。
systemtap
概要
systemtapは、カーネルに動的にプローブを挿入し、任意の関数やトレースポイント、あるいはユーザ空間の関数などをスクリプトベースで観測できるフレームワークです。ftraceやdropwatchがあらかじめ用意された情報をうまく可視化してくれるのに対して、systemtapは「この関数が呼ばれるたびに引数の特定フィールドをログに出したい」といった細かいニーズに応えられるのが特徴です。
仕組み
systemtapを使うときは、まず.stpという拡張子のスクリプトファイルを作成します。このスクリプトでは、probeという構文を使って「どのイベントを観測するか」と「そのとき何をするか」を記述します。例えば、probe kernel.function("netif_receive_skb") { ... } のように書けば、カーネル関数netif_receive_skbが呼ばれるたびにハンドラが実行されます。
systemtapの実行時には、このスクリプトがカーネルモジュール(.ko)にコンパイルされ、insmod相当の操作でカーネルにロードされます。カーネルモジュール内では、kprobe(任意関数への動的フック)やtracepointなどの仕組みを用いて、指定された関数やイベントにプローブが挿入されます。プローブが発火すると、スクリプト内に書いた処理がカーネルコンテキストで実行され、関数引数やstruct sk_buffのフィールドを読み出して、ユーザ空間に送ったり、カウンタをインクリメントしたりできます。
何を見るときに使うか
systemtapは、「既存のトレーサでは情報が足りないが、カーネルコードを直接書き換えるほどではない」という場面で活躍します。例えば、netif_receive_skbに渡されるskbのprotocolフィールドやパケット長、dev->nameだけをログしたいといった場合、systemtapのスクリプトでそれらのフィールドを参照し、テキストで出力することができます。
また、特定ポートや特定プロトコルのパケットだけを観測したい場合にも便利です。スクリプト内で条件分岐を書き、条件に合うパケットだけ情報を出力するようにすれば、ログの量を抑えながら詳細な情報を得られます。dropwatchやftraceでは「どこで落ちたか」「どの関数が呼ばれたか」は分かっても、そのときの引数や内部状態までは見えません。systemtapなら、関数引数や構造体フィールドに直接アクセスできるため、「なぜその条件でdropしたのか」をかなり具体的に理解できます。
ドライバ/NIC(ethtool)
概要
ethtoolは、NICドライバが公開している統計情報や設定値をユーザ空間から取得・変更するためのツールです。リンク速度やデュプレックス設定、オートネゴシエーションの状態、PHY情報、各種オフロード機能の有効・無効に加えて、ドライバ固有の詳細な統計(リングごとのドロップ数やエラー内訳など)を確認できます。
カーネルのnetdev層が提供する標準的な統計よりも一段下の、ドライバ固有の視点からネットワークインタフェースを観察できるのが大きな特徴です。
仕組み
ユーザ空間のethtoolコマンドは、ETHTOOL_GENL_NAMEというファミリを使ってカーネルにリクエストを送り、統計の取得や設定の変更を行います。リクエストは「GET」「SET」「ACT」といった種類に分かれており、必要な情報や操作内容はNetlink属性として構造化されて伝えられます。カーネル側ではインタフェース名からstruct net_deviceを探索し、ドライバが登録しているethtool用のハンドラを呼び出します。ドライバは受け取った属性を解釈して、自身が持つカウンタや設定値を返したり変更したりすることで、ユーザ空間に情報を返します。標準的なrx_packetsやtx_packetsといったカウンタはnetdev層のstruct rtnl_link_stats64に保持されますが、rx_no_bufferやrx_missed_errorsのようなドライバ固有の詳細統計は Netlink経由のethtool APIを通じて初めて見えることが多いです。
何を見るときに使うか
ethtoolは、物理層やドライバレベルの問題を疑うときにまず使用するツールです。例えば、rx_crc_errorsやrx_length_errorsが増えている場合は、ケーブルの品質やコネクタの接触不良、PHYあるいは物理リンク周りの問題を疑えます。rx_no_bufferやrx_missed_errorsのようなカウンタが増加している場合は、NICが受信したフレームをDMAバッファに配置できずに捨てている可能性が高く、リングサイズやNAPIポーリングの設定、IRQの割り当てを見直す必要があるかもしれません。
送信側についても、tx_timeoutの増加はNICへの送信が一定時間内に完了せず、ドライバがリセット処理を行ったことを示します。このような場合、ファームウェアの不具合やPCIe周りの問題など、より低レイヤのトラブルが隠れている可能性があります。
インタフェース単位のカウンタ(ip -s link)
概要
ip -s linkは、カーネルのnetdev層が保持しているインタフェース単位の統計情報を表示するコマンドです。ethtoolがドライバ固有の詳細統計を見せるのに対して、ip -s linkはより汎用的で、すべてのインタフェースに共通する基本的なカウンタ(受信・送信パケット数、バイト数など)を確認するのに適しています。
仕組み
ipコマンドはnetlinkを通じてrtnetlinkサブシステムに問い合わせを行います。RTM_GETLINKというメッセージを送ると、カーネルは各struct net_deviceについて、struct rtnl_link_stats64にまとめられた統計情報を収集し、netlinkメッセージに詰めて返します。このstruct rtnl_link_stats64には、rx_packets, tx_packets, rx_bytes, tx_bytes, rx_errors, tx_errors, rx_dropped, tx_droppedといった標準的なカウンタが含まれています。
これらの統計は、ドライバが netif_rx()などの呼び出しに応じて更新しているものや、netdev層がパケット送受信のたびに加算しているものです。そのため、ethtoolのドライバ固有統計とは異なり、すべてのインタフェースで共通の意味を持つカウンタとして扱えます。
何を見るときに使うか
ip -s linkは、「ざっくりとした傾向」を把握したいときの一次的なチェックに使います。例えば、ある時間間隔でrx_packetsやtx_packetsの増分を確認することで、そのインタフェースがどの程度のトラフィックを処理しているかを把握できます。同時にrx_errorsやrx_droppedの増え方も観察することで、トラフィック量に対してエラーやドロップが異常に多くないかを確認できます。
ethtoolの詳細統計のようなより低レイヤの統計と組み合わせることで、「ドロップがnetdevレイヤの統計に反映されているのか、それともドライバ内部に止まっているのか」を判断できます。まずip -s linkで全体像を掴み、異常がありそうならethtool -Sで掘り下げる、という使い分けがしやすいです。
カーネルコードレベル(print_hex_dumpによるパケット内容の直接出力)
最後に、ツールを使うのではなく、カーネルコードに直接デバッグ用のログを仕込む方法について触れます。ここで扱うprint_hex_dump()は、その代表的な例です。もちろん、これを使うにはソースコードの修正とカーネルの再ビルドが必要ですが、その代わりに「好きな箇所で」「好きなバッファ」を確実に観測できます。
概要
print_hex_dump()は、カーネル内部で利用できるヘルパ関数で、任意のメモリ領域を16進数とASCII表示の両方でカーネルログに出力します。ネットワークデバッグでは、skb->dataやskb_mac_header()が指す領域をダンプすることで、「カーネルがその時点で保持しているパケットバッファの生の内容」を確認できます。
ユーザ空間のキャプチャツールでは見えない中間状態やメタデータを含めて、カーネル視点でパケットを観察できるのが大きな利点です。
仕組み
print_hex_dump()のプロトタイプは概ね次のようになっています。
void print_hex_dump(const char *level, const char *prefix_str, int prefix_type,
int rowsize, int groupsize,
const void *buf, size_t len, bool ascii);
第一引数のlevel はログレベル(例えば KERN_DEBUG)、prefix_strとprefix_typeは各行の先頭に付与する文字列やオフセットの形式を指定します。rowsizeは1行に表示するバイト数、groupsizeは1グループのバイト数、bufとlenはダンプ対象のポインタと長さです。最後のasciiフラグを真にすると、16進表現に加えて印字可能文字のASCII表示も行われます。
ネットワークスタックの任意の箇所に、例えば次のようなコードを挿入すると、その時点でのパケット内容がdmesgなどで確認できるようになります。
print_hex_dump(KERN_DEBUG, "rx packet: ", DUMP_PREFIX_OFFSET,
16, 1, skb->data, skb_headlen(skb), true);
ここでDUMP_PREFIX_OFFSETは行頭にオフセットを表示するための指定です。出力はカーネルログのリングバッファに書き込まれるため、連続した大量のダンプを行うとログが溢れたり、性能に影響を与えたりする点には注意が必要です。必要に応じて、条件分岐やレートリミットを設けておくと安全です。
何を見るときに使うか
print_hex_dump()は、ユーザ空間からは観測しづらい中間状態を確認したいときに特に役立ちます。例えば、ドライバが skbに付加したメタデータ(control bufferやprivateデータ)とペイロードの整合性を確認したい場合などです。
ユーザ空間のキャプチャでは、オフロード機能の影響や、NIC やドライバ内部の処理順序によって、カーネルが見ている内容と完全には一致しないことがあります。こうした場合、実際にカーネルコード中でskb->dataをダンプすることで、「この時点ではVLANタグが既にストリップされている」「チェックサムフィールドはまだ未計算で、別のフラグに情報が入っている」といった細かい事実を確認できます。
もちろん、コードの修正とカーネルの再ビルドが必要になるため、頻繁に使う手法ではありませんが、最後の切り札として覚えておくと心強い方法です。
まとめ
本記事では、Linuxカーネルにおけるネットワークデバッグツールや手法を説明しました。
実際の現場では、これらのツールを単体で使うことはあまりなく、例えば次のような流れで組み合わせて使うことが多いと思います。まずssやip -s link、tcpdump, tsharkで大まかな異常の有無と位置を確認し、次にdropwatchやethtoolを用いて「どのレイヤ・どの関数周辺が怪しいか」を絞り込みます。その上で、必要であれば ftraceやsystemtapを用いて詳細な挙動や性能特性を解析し、最後の手段としてprint_hex_dump()のようなコードレベルの観測に踏み込む、という段階的なアプローチです。
本記事が、ネットワークトラブルに直面した際に「どこから手を付ければ良いか」「どのツールで何が見えるか」を考えるための足がかりになれば幸いです。
参考文献
- kernel source code
- netstat source code
- ss source code
- tshark source code
- tcpdump source code
- systemtap source code
- dropwatch source code
- Better visibility into packet-dropping decisions
- Secrets of the Ftrace function tracer
- Systemtap tutorial
- Netlink interface for ethtool
商標について
- Linuxは、Linus Torvalds 氏の米国およびその他の国における登録商標です。
- その他本記事に掲載の商品、機能等の名称は、それぞれ各社が商標として使用している場合
があります。