【図解】LinuxのPacket受信の仕組みを徹底解説|NIC・RX Ring・DMA・NAPI・SoftIRQ・Socketまで

はじめに

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受信の全体像

Linux 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受信時には次のような関係になります。

RX RingとRX Descriptor、受信Bufferの関係

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処理を繰り返すことを避けます。

Linux NAPIとNET_RX_SOFTIRQの受信処理

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からIP、TCP UDPまでのPacket処理

まず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からSocket Receive Bufferまでの流れ

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 / Interfaceip -s link show dev enp1s0RX/TX Packet数、Bytes、Error、Dropなど
NIC設定ethtool enp1s0Link Up/Down、Speed、Duplex、Auto-negotiationなど
NIC統計ethtool -S enp1s0NIC / Driver固有の受信統計、Drop、Errorなど
RX Ringethtool -g enp1s0RX / TX Ringの現在値・最大値
Interruptcat /proc/interruptsNICのHardware Interrupt回数、処理CPUなど
SoftIRQ / NAPIcat /proc/softirqsNET_RXなどNetwork受信に関するSoftIRQの実行状況
Network Trafficsar -n DEV 1InterfaceごとのPacket数、Bytes、Traffic量
Network Errorsar -n EDEV 1InterfaceごとのError、Drop、FIFO Errorなど
Socket / TCPss -ntmiRecv-Q / Send-Q、Socket Memory、RTT、cwndなどTCP内部情報
Packettcpdump -i enp1s0 -nnKernelまで到達した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入門・図解

Linuxのネットワークの仕組みを
図解で理解したい方へ

Socket・TCP・ルーティング・NIC 現場で役立つ通信処理の基礎知識など Linuxにおけるネットワークの仕組みを図解を中心にわかりやすく解説しています。

Kindleで書籍を見る →

コメント