執筆者 : 西村 大助
1. はじめに
本稿では、Linux におけるネットワーク受信処理で利用されている、NAPI(New API)と呼ばれる仕組みについて、実装レベルで解説したいと思います。
2. Linux カーネルのおさらい
NAPI について解説する前に、前提として Linux カーネルについていくつかおさらいします。
2.1 実行コンテキスト
Linux には実行コンテキストとして、大きく以下の2つのコンテキストがあります1。
- プロセスコンテキスト
プロセス(本稿では、ユーザプロセスだけでなく、カーネルスレッドも含むものとします)を実行するコンテキスト。
ハードウェアからの割り込みにより、後述する割り込みコンテキストに移行する他、スケジューラにより他プロセスに処理が切り替えられる可能性もある。 - 割り込み(IRQ)コンテキスト
特定のプロセスには結びつかず、任意のプロセスからハードウェア割り込みを契機に実行され、割り込みの処理を行うコンテキスト。
プロセスが切り替わるわけではなく、元の(割り込まれた)プロセスのカーネルスタック上で実行される。割り込み処理完了後、元のコンテキスト に戻る。
さらに、割り込みコンテキストはハードウェア割り込みコンテキストとソフトウェア割り込みコンテキストに分けられます。
- ハードウェア割り込みコンテキスト
ハードウェアからの割り込みに対する応答処理などを行う。
割り込みは禁止されるため、システムの応答性を維持するため、できるだけ実行時間は短くする必要がある。
割り込みに対する実質的な処理(例えば、本稿で説明する NIC からの割り込みに対するパケットの受信処理)は時間がかかるため、後述するソフトウェア割り込みコンテキストで実行する。 - ソフトウェア割り込みコンテキスト
ハードウェア割り込みコンテキストから、割り込み前の元のコンテキストに戻る前に実行されるコンテキストで、ハードウェアからの割り込みに対する実質的な処理を行う。
詳細については後述するが、処理に時間がかかるなどしてソフトウェア割り込みの処理がソフトウェア割り込みコンテキスト内で完了しなかった場合、ksoftirqd で(プロセスコンテキスト2で)ソフトウェア割り込み処理を行う。
上記の事を簡単にまとめると以下の図のような感じになります。
2.2 ソフトウェア割り込み
本節では、前述のソフトウェア割り込みについてもう少し詳細に解説します。
ソフトウェア割り込みには以下のような種類3があり、それぞれに専用のハンドラが定義されています。
531 /* PLEASE, avoid to allocate new softirqs, if you need not _really_ high 532 frequency threaded job scheduling. For almost all the purposes 533 tasklets are more than enough. F.e. all serial device BHs et 534 al. should be converted to tasklets, not to softirqs. 535 */ 536 537 enum 538 { 539 HI_SOFTIRQ=0, 540 TIMER_SOFTIRQ, 541 NET_TX_SOFTIRQ, 542 NET_RX_SOFTIRQ, 543 BLOCK_SOFTIRQ, 544 IRQ_POLL_SOFTIRQ, 545 TASKLET_SOFTIRQ, 546 SCHED_SOFTIRQ, 547 HRTIMER_SOFTIRQ, 548 RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */ 549 550 NR_SOFTIRQS 551 };
また、値が小さい物ほど優先度が高くなっており、先に実行されます。
このことについて、ソフトウェア割り込み処理の実体である __do_softirq()
4 を見て確認してみましょう。
__do_softirq()
のメインの処理は以下のようなループ(L.333-L.355)になっています。
266 #define MAX_SOFTIRQ_TIME msecs_to_jiffies(2) 267 #define MAX_SOFTIRQ_RESTART 10 ... 302 asmlinkage __visible void __softirq_entry __do_softirq(void) 303 { 304 unsigned long end = jiffies + MAX_SOFTIRQ_TIME; 305 unsigned long old_flags = current->flags; 306 int max_restart = MAX_SOFTIRQ_RESTART; ... 319 pending = local_softirq_pending(); 320 321 __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET); 322 in_hardirq = lockdep_softirq_start(); 323 account_softirq_enter(current); 324 325 restart: 326 /* Reset the pending bitmask before enabling irqs */ 327 set_softirq_pending(0); 328 329 local_irq_enable(); 330 331 h = softirq_vec; 332 333 while ((softirq_bit = ffs(pending))) { 334 unsigned int vec_nr; 335 int prev_count; 336 337 h += softirq_bit - 1; 338 ... 344 trace_softirq_entry(vec_nr); 345 h->action(h); 346 trace_softirq_exit(vec_nr); ... 353 h++; 354 pending >>= softirq_bit; 355 } 356 357 if (__this_cpu_read(ksoftirqd) == current) 358 rcu_softirq_qs(); 359 local_irq_disable(); 360 361 pending = local_softirq_pending(); 362 if (pending) { 363 if (time_before(jiffies, end) && !need_resched() && 364 --max_restart) 365 goto restart; 366 367 wakeup_softirqd(); 368 }
ソフトウェア割り込みが raise されると、上記の pending
の当該ソフトウェア割り込みのビットが立てられますが ffs(find first set) なため小さいものから検出されループに入り、
前述の通り、各ソフトウェア割り込み毎に定義されている専用のハンドラ(action())が L.345 で呼び出されることになります。
全てのソフトウェア割り込みの処理が終わった後(ループを抜けた後)、(例えばループ中に、新たにソフトウェア割り込みが raise される等して)まだ未処理のソフトウェア割り込みがあれば(L.362)、goto restart
により L.325 に戻るか、wakeup_softirqd()
により ksoftirqd を起床し残りのソフトウェア割り込み処理を ksoftirqd に委譲しますが、この ksoftirqd に処理を委譲する条件として、
__do_softirq()
の処理開始から 2 ms 経過している- 10 回以上、
goto restart
により L.325 に戻っている
がある通り、ソフトウェア割り込みが立て込んでいるなどして、ソフトウェア割り込み処理に時間がかかった場合、ソフトウェア割り込みの処理はソフトウェア割り込みコンテキストではなく ksoftirqd によるプロセスコンテキストで実行されるようになることがわかります。
3. NAPI とは
3.1 概要
NAPI とは New API の略で、パケットの受信処理で利用されている仕組みです。
"New" とは言っていますが、そもそも登場したのが v2.5(後に、v2.4 にバックポートされた)で、全く「新しく」はありませんが、それ以前は、パケットを受信する度に割り込みを上げてその割り込み処理により受信処理を行っていました。この方法では、ネットワークの負荷が軽い場合はパケットを高速に処理できるというメリットもありますが、ネットワークの負荷が上がると CPU が割り込み処理により高負荷となり、システムの応答性が悪くなるという問題がありました。
NAPI では、パケットの到着の通知(割り込み)とパケットの受信処理を分離して、
- パケットが到着するとソフトウェア割り込みを raise し、受信処理はソフトウェア割り込み処理として行う。
- 受信処理中にパケットを受信しても、通知(ソフトウェア割り込みを raise)しない。
- 受信処理では NIC のキューを polling する(なので通知する必要がない)。
というふうに、割り込みによる通知と polling のハイブリッドな実装とすることで上記の問題を解決しています。
3.2 関連データ構造
- softnet_data 構造体
NAPI も含め、ネットワークの送受信に関し、CPU 毎に管理すべき様々なデータを保持するデータ構造(当然、per cpuなデータとして定義されている)で、後述する napi_struct 構造体をリスト化するための list_head 構造体(poll_list
)などをメンバに持つ5。
399 /* 400 * Device drivers call our routines to queue packets here. We empty the 401 * queue in the local softnet handler. 402 */ 403 404 DEFINE_PER_CPU_ALIGNED(struct softnet_data, softnet_data); 405 EXPORT_PER_CPU_SYMBOL(softnet_data);
3228 /* 3229 * Incoming packets are placed on per-CPU queues 3230 */ 3231 struct softnet_data { 3232 struct list_head poll_list; ...
- napi_struct 構造体
NAPI の仕組みの中心となるデータ構造で、基本的に、ネットワークデバイス(やそのキュー)毎に管理されており、前述のsoftnet_data.poll_list
に繋ぐための list_head 構造体や、polling するハンドラなどをメンバに持つ。
321 /* 322 * Structure for NAPI scheduling similar to tasklet but with weighting 323 */ 324 struct napi_struct { 325 /* The poll_list must only be managed by the entity which 326 * changes the state of the NAPI_STATE_SCHED bit. This means 327 * whoever atomically sets that bit can add this napi_struct 328 * to the per-CPU poll_list, and whoever clears that bit 329 * can remove from the list right before clearing the bit. 330 */ 331 struct list_head poll_list; ... 337 int (*poll)(struct napi_struct *, int); ...
これらのデータ構造の関係を大まかに図示すると以下のようになります。
3.3 処理の流れ
パケットを受信すると、まずハードウェア割り込みが発生し、NIC ドライバの割り込みハンドラが実行されます。
ixgbe ドライバを例に、割り込みハンドラを見てみましょう。
ixgbe ドライバの割り込みハンドラである ixgbe_msix_clean_rings()
は以下のようになっており、napi_schedule_irqoff()
により自身の管理する napi_struct を当該 CPU の softnet_data.poll_list
に繋ぎ、NET_RX_SOFTIRQ ソフトウェア割り込みを raise します6。
(drivers/net/ethernet/intel/ixgbe/ixgbe_main.c)
3124 static irqreturn_t ixgbe_msix_clean_rings(int irq, void *data) 3125 { 3126 struct ixgbe_q_vector *q_vector = data; 3127 3128 /* EIAM disabled interrupts (on this vector) for us */ 3129 3130 if (q_vector->rx.ring || q_vector->tx.ring) 3131 napi_schedule_irqoff(&q_vector->napi); 3132 3133 return IRQ_HANDLED; 3134 }
NET_RX_SOFTIRQ ソフトウェア割り込みのハンドラは net_rx_action()
であり、以下の通り、softnet_data.poll_list
に繋がっている全ての napi_struct に対して、napi_poll()
を実行します。
7044 static __latent_entropy void net_rx_action(struct softirq_action *h) 7045 { 7046 struct softnet_data *sd = this_cpu_ptr(&softnet_data); ... 7053 local_irq_disable(); 7054 list_splice_init(&sd->poll_list, &list); 7055 local_irq_enable(); 7056 7057 for (;;) { 7058 struct napi_struct *n; 7059 7060 if (list_empty(&list)) { 7061 if (!sd_has_rps_ipi_waiting(sd) && list_empty(&repoll)) 7062 return; 7063 break; 7064 } 7065 7066 n = list_first_entry(&list, struct napi_struct, poll_list); 7067 budget -= napi_poll(n, &repoll); ... 7077 } 7078 } ...
napi_poll()
はその延長で、引数で渡された napi_struct の poll() メンバ(ixgbeドライバの場合は ixgbe_poll()
)を実行しますが、そこで NIC のキューを polling して(ixgbeドライバの場合は下記の L.3177~L.3187)、最終的には、__netif_receive_skb_core()
といった汎用関数により受信処理(例えば、netfilter によるフィルタ処理や、IP といった上位のプロトコルの処理)が行われていくことになります。
(drivers/net/ethernet/intel/ixgbe/ixgbe_main.c)
3136 /** 3137 * ixgbe_poll - NAPI Rx polling callback 3138 * @napi: structure for representing this polling device 3139 * @budget: how many packets driver is allowed to clean 3140 * 3141 * This function is used for legacy and MSI, NAPI mode 3142 **/ 3143 int ixgbe_poll(struct napi_struct *napi, int budget) 3144 { 3145 struct ixgbe_q_vector *q_vector = 3146 container_of(napi, struct ixgbe_q_vector, napi); 3147 struct ixgbe_adapter *adapter = q_vector->adapter; 3148 struct ixgbe_ring *ring; 3149 int per_ring_budget, work_done = 0; 3150 bool clean_complete = true; ... 3177 ixgbe_for_each_ring(ring, q_vector->rx) { 3178 int cleaned = ring->xsk_pool ? 3179 ixgbe_clean_rx_irq_zc(q_vector, ring, 3180 per_ring_budget) : 3181 ixgbe_clean_rx_irq(q_vector, ring, 3182 per_ring_budget); 3183 3184 work_done += cleaned; 3185 if (cleaned >= per_ring_budget) 3186 clean_complete = false; 3187 } ...
4. NAPI の問題点
NAPI ではソフトウェア割り込みとして受信処理を行うため、
- ソフトウェア割り込み処理自体、ハードウェア割り込み処理の完了後に実行される
- より優先度の高いソフトウェア割り込みがあればそちらが優先される
- ソフトウェア割り込み全体の処理時間次第で、ksoftirqd に処理が委譲されると、スケジューラにより CPU が奪われる可能性もある
といったように、受信(ハードウェア割り込み)から実際に処理されるまでにタイムラグがあり、性能(特にレイテンシ)に影響が出てくる場合があります。
非常に性能にシビアな環境では DPDK7 を使って常に NIC を polling するといった方法も考えられますが、DPDK ではアプリケーションを独自で作る必要があるなどハードルが高いため、後編では、Busy Poll Socket と kthread NAPI polling という Linux kernel に組み込まれている、ソフトウェア割り込み以外で NIC を polling するための仕組みについて解説したいと思います。
5. 参考資料
- Understanding Linux Network Internals
古い本ですが今でもとても参考になります。
- 他にも例外処理のコンテキストもありますが、本稿では触れないため割愛します。↩
- ソフトウェア割り込み処理を行うという意味では、ksoftirqd もソフトウェア割り込みコンテキストと言えますが、本稿ではプロセスコンテキストとしています。↩
- 本稿では、v5.12 のソースコードを前提に説明します。↩
-
ksoftirqd の処理の実体も
__do_softirq()
です。↩ -
本稿で触れるのは
poll_list
のみです。↩ -
napi_schedule_irqoff()
の中で、前述の通り、当該 napi_struct が処理中だった場合はソフトウェア割り込みを raise しないよう制御されています。↩ - https://www.dpdk.org/↩