はじめに
Networkから届いたPacketは、NICからApplicationへ直接渡されるわけではありません。
NICがEthernet Frameを受信すると、Packet DataはDMAによってMain Memoryへ転送されます。その後、Linux KernelがNAPIやSoftIRQを使ってPacketを処理し、Ethernet、IP、TCP / UDPを経てSocket Receive Bufferへ格納します。最後にApplicationがrecv()やread()でDataを読み取ります。
この流れを理解すると、RX Drop、Interrupt偏り、SoftIRQ負荷、Socket Buffer不足などが発生したときに、どのLayerで問題が起きているのかを切り分けやすくなります。
本記事では、NICからApplicationまで、実際にPacketが処理される順番に沿ってLinuxのPacket受信処理を解説します。
Packet受信の全体像

大きく見ると、「NICで受信する」「Main Memoryへ転送する」「Kernel Network StackでProtocolを処理する」「Socketへ格納する」「Applicationが読み取る」という流れになります。
ここからは、この①〜⑧の順番に沿って、それぞれの処理を見ていきます。
① NICがPacketを受信する
Packet受信の入口となるのがNICです。
NICはNetworkからSignalを受け取り、Ethernet Frameとして復元します。Frame Check Sequenceなどを確認し、自身が受信すべきFrameであれば受信処理へ進めます。
通常はDestination MAC Addressなどを基準に受信対象を判断しますが、Promiscuous ModeやMulticast設定によって受信対象は変わります。
また、NICによってはChecksum検証、VLAN Tag処理、Receive Side Scaling(RSS)などをHardwareで処理できます。利用される機能はNIC、Driver、設定によって異なります。
② RX Descriptorを参照する
NICがPacketを受信すると、受信したPacket DataをMain Memory上のどこへ書き込むかを判断する必要があります。そのために使われるのがRX Ringです。
RX Ringには、Packet Dataそのものが並んでいるわけではありません。RX RingにはRX Descriptorと呼ばれる、受信Bufferを管理するための情報が並んでいます。
RX Descriptorには、主に「Packet Dataを書き込むMemory Bufferはどこにあるか」を示す情報や、受信状態などが格納されます。
この準備を行うのがDevice Driverです。DriverはPacketが到着する前にMain Memory上へ受信Bufferを用意し、そのBufferをNICが利用できるよう、対応する情報をRX Descriptorへ設定しておきます。
そのため、Packet受信時には次のような関係になります。

Driverは受信処理が完了したDescriptorに受信Bufferを再び割り当て、NICが次のPacket受信に利用できる状態へ戻します。RX DescriptorはRing状のQueueとして循環的に管理されるため、RX Ringと呼ばれます。
DriverによるBufferの補充が追いつかず、NICが利用可能なDescriptorがなくなると、新しいPacketを受信Bufferへ格納できず、NIC側でDropが発生する可能性があります。
RX Ringを大きくするとBurst Trafficを吸収しやすくなる一方で、使用するMemoryが増えたり、QueueにPacketが滞留することでLatencyが増えたりする可能性があります。
③ DMAで受信BufferへPacket Dataを転送する
NICがDescriptorから受信Bufferを確認すると、そのBufferへPacket Dataを書き込みます。ここで利用されるのがDMAです。
DMAはDirect Memory Accessの略で、DeviceがCPUによるByte単位のCopy処理を介さず、Main Memoryとの間でDataを転送する仕組みです。
CPUがPacket Dataを一つずつMemoryへCopyするのではなく、NIC自身がDMAを使って指定された受信BufferへDataを書き込みます。
CPU側では、DriverがDescriptorや受信Bufferの準備を行い、PacketがMemoryへ到着した後のProtocol処理を担当します。
NICが受信Bufferへの書き込みを完了すると、DescriptorへPacket Length、Checksum結果、VLAN情報などを記録し、受信完了を示す状態へ更新します。
Driverは後続の受信処理でDescriptorの状態を確認し、どのBufferへ新しいPacketが到着したのかを識別します。
④ InterruptでCPUへ通知する
Packet DataがMain Memoryへ転送されると、次はKernel側でそのPacketを処理する必要があります。NICは受信処理が必要になったことを、必要に応じてInterruptによってCPUへ通知します。
現在のNICではMSI-Xなどを使い、複数のRX QueueごとにInterrupt Vectorを割り当てられる場合があります。
ただし、Packet一個ごとに重いInterrupt処理を行うと、高Traffic時には大量のInterruptが発生し、CPUを圧迫します。
そのためLinuxでは、Interrupt Handlerで最小限の処理を行い、実際のPacket受信処理の多くをNAPIへ移します。また、NICのInterrupt Moderation機能によって、複数の受信EventをまとめてCPUへ通知する場合もあります。
⑤ NAPI / NET_RX_SOFTIRQで受信処理する
Interruptをきっかけに、LinuxはNAPIによるPacket受信処理を開始します。
NAPIはLinuxのNetwork Driverで利用される仕組みで、InterruptとPollingを組み合わせることで、高Traffic時のInterrupt負荷を抑えます。
Trafficが少ないときはInterruptが受信処理の入口になります。一方、Packetが連続して到着している場合は、NAPI PollによってRX Queueから複数のPacketをまとめて処理し、PacketごとにInterrupt処理を繰り返すことを避けます。

NAPI Pollは主にNET_RX_SOFTIRQの文脈で実行されます。
DriverはRX Ring上の受信完了済みDescriptorを確認し、対応するMemory Buffer上のPacket DataをLinux KernelのNetwork Stackへ渡します。
Driverは処理済みDescriptorに受信Bufferを再び割り当て、NICが次のPacket受信に利用できる状態へ戻します。
NAPIには、一度のPollで処理できる量に上限があります。これは一つのNICやRX QueueがCPUを長時間独占することを防ぎ、他の処理との公平性を保つためです。
SoftIRQ処理をその場ですべて処理できない場合、残りの処理がksoftirqd Kernel Threadへ回されることがあります。
高負荷時に特定CPUのNET_RX SoftIRQやksoftirqdのCPU使用率が高くなっている場合は、RX Queue、IRQ Affinity、RSS、Packet Rateなどを確認します。
⑥ Ethernet → IP → TCP / UDPを処理する
DriverからKernel Network Stackへ渡されたPacketは、Protocol Headerを順番に解析されます。

まずEthernet LayerではEtherTypeなどを確認し、IPv4、IPv6、ARPなど、どの上位Protocolへ渡すかを判断します。
IP LayerではDestination IP AddressやHeaderなどを確認します。Local Host宛てのPacketであれば、Protocol Numberに応じてTCPやUDPなどの上位Protocolへ渡します。
TCP LayerではTCP Headerを解析し、Port Number、Flags、Sequence Number、ACK、Windowなどをもとに受信処理を行います。
UDPの場合はUDP Headerを確認し、Destination Portなどをもとに受信処理を行います。
なお、Netfilter、Bridge、VLAN、Network Namespaceなどを利用している構成では、Packetが追加のHookや仮想Interfaceを通過します。本記事では物理Interfaceへ届いたLocal Host宛てPacketの基本経路に絞っています。
⑦ Socket Receive Bufferへ格納する
TCPやUDPまで処理されたDataは、最終的にApplicationが利用しているSocketへ渡されます。
TCPではSource / DestinationのIP AddressやPort Number、Connection状態などから対象Socketを特定します。

TCP Segmentが順番通りに届くとは限りません。順番が入れ替わって到着したSegmentは必要に応じてKernel内で保持され、Sequence Numberをもとに受信Dataの順序が管理されます。
Applicationへ連続したByte Streamとして渡せるDataはSocket Receive BufferへQueueされ、Applicationがrecv()やread()を呼び出すまでKernel内に保持されます。
Applicationによる読み取りが遅くReceive Bufferの空きが減ると、TCPは相手へ通知するReceive Windowを小さくします。
Bufferに空きがなくなると、Receive Windowとして0を通知するZero Windowになる場合があります。
一方、Socketへ関連付けられないUDP Datagramや、Buffer不足などによって格納できないDataはDropされ、各種Counterへ現れることがあります。
⑧ ApplicationがDataを受け取る
Socket Receive BufferへDataが格納されると、最後にApplicationがDataを読み取ります。
ApplicationがBlocking Modeでrecv()やread()を実行し、Dataを待っている場合、KernelはProcessをSleepさせます。
DataがSocketへ届くとProcessはWake Up対象となり、SchedulerによってCPUへ割り当てられた後、System Callの処理が再開します。
通常のSocket受信では、Kernel内のSocket Receive BufferにあるDataがUser Space側のBufferへCopyされ、Applicationから利用できるようになります。
epollなどを利用するApplicationでは、まずSocketがReadableになったことを検知し、その後recv()などを呼び出してDataを読み取ります。
そのため、NICへPacketが到着してからApplicationが実際に処理を開始するまでには、Kernel内のPacket処理だけでなく、ProcessのScheduling待ちが生じる場合もあります。
LinuxでPacket受信を確認する
Packet受信で問題が発生した場合も、NIC → RX Ring → Interrupt → SoftIRQ → Socket → Applicationという受信経路に沿って確認すると、どこで処理が滞っているのかを切り分けやすくなります。
| 確認箇所 | コマンド | 主に確認できること |
|---|---|---|
| NIC / Interface | ip -s link show dev enp1s0 | RX/TX Packet数、Bytes、Error、Dropなど |
| NIC設定 | ethtool enp1s0 | Link Up/Down、Speed、Duplex、Auto-negotiationなど |
| NIC統計 | ethtool -S enp1s0 | NIC / Driver固有の受信統計、Drop、Errorなど |
| RX Ring | ethtool -g enp1s0 | RX / TX Ringの現在値・最大値 |
| Interrupt | cat /proc/interrupts | NICのHardware Interrupt回数、処理CPUなど |
| SoftIRQ / NAPI | cat /proc/softirqs | NET_RXなどNetwork受信に関するSoftIRQの実行状況 |
| Network Traffic | sar -n DEV 1 | InterfaceごとのPacket数、Bytes、Traffic量 |
| Network Error | sar -n EDEV 1 | InterfaceごとのError、Drop、FIFO Errorなど |
| Socket / TCP | ss -ntmi | Recv-Q / Send-Q、Socket Memory、RTT、cwndなどTCP内部情報 |
| Packet | tcpdump -i enp1s0 -nn | Kernelまで到達したPacketの送受信内容 |
例えばNIC固有のDrop Counterが増えている場合は、NICやRX Ring付近でPacketを処理しきれていない可能性があります。
NICでは正常に受信できている一方で、特定CPUのNET_RX SoftIRQが高くなっている場合は、Kernel側のPacket処理が追いついていない可能性があります。
さらにSocket Receive BufferへDataが溜まっている場合は、Application側の読み取りが追いついていない可能性があります。
NIC固有Counterの名前はDriverによって異なります。そのため、一つのCounterだけを見るのではなく、Traffic量、CPU使用率、Interrupt、SoftIRQ、Socket状態などを組み合わせて確認することが重要です。
また、tcpdumpで確認できる位置とNIC Hardware Counterで確認できる位置は異なります。NIC側でPacketがDropした場合、そのPacketはKernelまで到達していないため、tcpdumpでは確認できないことがあります。
まとめ
LinuxのPacket受信では、まずNICがEthernet Frameを受信します。NICはRX Descriptorが示す受信BufferへDMAでPacket Dataを転送し、必要に応じてInterruptによってCPUへ受信処理を通知します。
その後、NAPI / NET_RX_SOFTIRQによって受信済みPacketが処理され、Ethernet、IP、TCP / UDPの順にKernel Network Stackを通ります。
最終的にDataはSocket Receive Bufferへ格納され、Applicationがrecv()やread()で読み取ります。
Packet受信のトラブルを調査するときも、NIC → RX Ring → Interrupt → SoftIRQ → Protocol Stack → Socket → Applicationという経路に沿って確認することで、どのLayerで処理が滞っているのかを切り分けやすくなります。
Linuxにおけるネットワークの仕組みについては、以下の書籍にまとめています。興味がありましたら、ぜひ読んでみてください。
Linuxのネットワークの仕組みを
図解で理解したい方へ
Socket・TCP・ルーティング・NIC 現場で役立つ通信処理の基礎知識など Linuxにおけるネットワークの仕組みを図解を中心にわかりやすく解説しています。
Kindleで書籍を見る →


コメント