FreeBSDのパケットディスパッチについて

探検隊での発表や、FreeBSD/i386のパケット受信フロー - かーねる・う゛いえむにっきの補足。

まず、話の前提のおさらいから。

  • マルチコア環境でネットワーク受信処理をスケールさせるには、NICに届く沢山のパケットを複数のコアへばらまいて並列に処理させる必要がある
  • ランダムにばらまくよりも、同一フローに属するパケットは同一コアへ割り当てる方がデータローカリティが高くなりキャッシュミスが減るので性能が上がると考えられている

複数コアへばらまく仕組みとして、二通りの方式がある。

  • RSS: ハードウェアで行う。パケットヘッダのハッシュ値を計算してハッシュテーブルを引き、割り当てられたCPUへ割り込みを行う。受信キューはCPU毎に独立している。
  • RPS(Linux): ソフトウェアで行う。NICからの割り込みは常に特定のCPUへ入るが、受信処理を行うCPU上でパケットヘッダのハッシュ値を計算してハッシュテーブルを引き、割り当てられたCPU用のキューへパケットを積んでそのCPUへCPU間割り込みを行う。

つまり、あるOSが複数コアへパケットをばらまく為の仕組みを持っているか見るには、RSSに対応しているか、或いはRPS相当の機能を実装しているのかについて見てみる必要がある(これの前提として、プロトコルスタックの構造がSMPに最適化されている必要がある)。

FreeBSDの受信フローを以下の図に示す:


ハードウェア割り込みハンドラはtaskqueue_enqueueを行ってすぐに復帰するようで、実際の割り込み処理は割り込みスレッドで行われる。
いくつかの関数を経由するが、最終的にパケットのディスパッチはnetisr_dispatch_srcで行われるようだ。
この関数はLinuxでいうとnetif_receive_skbに相当する。

この関数では、2つの事をしている。

  1. netisr_select_cpuidで送り先のcpuidを取得
  2. netisr_queue_workstreamnetisr_queue_internalを呼んで送り先CPUのキューにパケットを積むか、netisr_proto[proto].np_handlerを呼んでその場でプロトコル処理を行う

どんな方法で送り先CPUを決めているのかとnetisr_select_cpuidを見てみると、どうもNETISR_POLICY_CPU、NETISR_POLICY_FLOW、NETISR_POLICY_SOURCEの3つのポリシーがあるようだ。
IPのポリシーを見ると、NETISR_POLICY_FLOWである事がわかる。
NETISR_POLICY_FLOWの中ではif文によって条件が分岐するが、IPについてはnpp->np_m2flowに関数ポインタが代入されているようには見えない。
よってif (!(m->m_flags & M_FLOWID) && npp->np_m2flow != NULL)は成立しない。
次、if (m->m_flags & M_FLOWID)を見てみる。
どこで設定されているかをみていくと、このM_FLOWIDというフラグはRSS対応NICのドライバで有効にし、同時にm_pkthdr.flowidを設定している事がわかる。
igbのコードでは、キュー番号(≒CPU番号)を直接フローIDとして代入している。
つまり、RSS対応NICでは常に同じキューに積まれているパケットは同じコアが処理するという事だと考えられる。

一方、このifが成立しない場合、NETISR_POLICY_SOURCEにフォールスルーするが、ここではsource % nws_countでcpuidを決めているsourceはnetisr_dispatchまで遡ってみると0であるから、常に0になってしまう。
という事は、LinuxのRPSのようにソフトでパケットヘッダのハッシュ値を計算してディスパッチ先を決める機構を持たないようだ。
やろうと思えばここにその機能を追加するだけで簡単に実現できそうにも思えるが。

この先の処理は、まず送り先が自分宛かどうかで分岐する。
自分宛てではない場合はnetisr_queue_internalで他CPUのNWSスレッドのキューにパケットを積む。
自分宛でNWSスレッドが既に実行中な場合はnetisr_queue_workstreamでやはりキューにパケットを積む。
自分宛でかつNWSスレッドが実行されていない場合はnetisr_proto[proto].np_handlerを呼び出してその場でプロトコルスタックを実行する。
これらの処理はCPU間でアクセス競合を起こす可能性がある為、スレッドごとのロック(NWS_LOCK)で保護している。