詳解 Linux ネットワーク - NAPI 編 (前編)

執筆者 : 西村 大助


1. はじめに

本稿では、Linux におけるネットワーク受信処理で利用されている、NAPI(New API)と呼ばれる仕組みについて、実装レベルで解説したいと思います。

2. Linux カーネルのおさらい

NAPI について解説する前に、前提として Linux カーネルについていくつかおさらいします。

2.1 実行コンテキスト

Linux には実行コンテキストとして、大きく以下の2つのコンテキストがあります1

  • プロセスコンテキスト
    プロセス(本稿では、ユーザプロセスだけでなく、カーネルスレッドも含むものとします)を実行するコンテキスト。
    ハードウェアからの割り込みにより、後述する割り込みコンテキストに移行する他、スケジューラにより他プロセスに処理が切り替えられる可能性もある。
  • 割り込み(IRQ)コンテキスト
    特定のプロセスには結びつかず、任意のプロセスからハードウェア割り込みを契機に実行され、割り込みの処理を行うコンテキスト。
    プロセスが切り替わるわけではなく、元の(割り込まれた)プロセスのカーネルスタック上で実行される。割り込み処理完了後、元のコンテキスト に戻る。

さらに、割り込みコンテキストはハードウェア割り込みコンテキストとソフトウェア割り込みコンテキストに分けられます。

  • ハードウェア割り込みコンテキスト
    ハードウェアからの割り込みに対する応答処理などを行う。
    割り込みは禁止されるため、システムの応答性を維持するため、できるだけ実行時間は短くする必要がある。
    割り込みに対する実質的な処理(例えば、本稿で説明する NIC からの割り込みに対するパケットの受信処理)は時間がかかるため、後述するソフトウェア割り込みコンテキストで実行する。
  • ソフトウェア割り込みコンテキスト
    ハードウェア割り込みコンテキストから、割り込み前の元のコンテキストに戻る前に実行されるコンテキストで、ハードウェアからの割り込みに対する実質的な処理を行う。
    詳細については後述するが、処理に時間がかかるなどしてソフトウェア割り込みの処理がソフトウェア割り込みコンテキスト内で完了しなかった場合、ksoftirqd で(プロセスコンテキスト2で)ソフトウェア割り込み処理を行う。

上記の事を簡単にまとめると以下の図のような感じになります。

f:id:va_nishimura:20220121174653p:plain

2.2 ソフトウェア割り込み

本節では、前述のソフトウェア割り込みについてもう少し詳細に解説します。

ソフトウェア割り込みには以下のような種類3があり、それぞれに専用のハンドラが定義されています。

(include/linux/interrupt.h)

    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)になっています。

(kernel/softirq.c)

    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.15(後に、v2.14 にバックポートされた)で、全く「新しく」はありませんが、それ以前は、パケットを受信する度に割り込みを上げてその割り込み処理により受信処理を行っていました。この方法では、ネットワークの負荷が軽い場合はパケットを高速に処理できるというメリットもありますが、ネットワークの負荷が上がると CPU が割り込み処理により高負荷となり、システムの応答性が悪くなるという問題がありました。

NAPI では、パケットの到着の通知(割り込み)とパケットの受信処理を分離して、

  • パケットが到着するとソフトウェア割り込みを raise し、受信処理はソフトウェア割り込み処理として行う。
  • 受信処理中にパケットを受信しても、通知(ソフトウェア割り込みを raise)しない。
  • 受信処理では NIC のキューを polling する(なので通知する必要がない)。

というふうに、割り込みによる通知と polling のハイブリッドな実装とすることで上記の問題を解決しています。

3.2 関連データ構造

  • softnet_data 構造体
    NAPI も含め、ネットワークの送受信に関し、CPU 毎に管理すべき様々なデータを保持するデータ構造(当然、per cpuなデータとして定義されている)で、後述する napi_struct 構造体をリスト化するための list_head 構造体(poll_list)などをメンバに持つ5

(net/core/dev.c)

    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);

(include/linux/netdevice.h)

   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 するハンドラなどをメンバに持つ。

(include/linux/netdevice.h)

    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);
    ...

これらのデータ構造の関係を大まかに図示すると以下のようになります。

f:id:va_nishimura:20220125233703p:plain

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() を実行します。

(net/core/dev.c)

   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. 参考資料



  1. 他にも例外処理のコンテキストもありますが、本稿では触れないため割愛します。

  2. ソフトウェア割り込み処理を行うという意味では、ksoftirqd もソフトウェア割り込みコンテキストと言えますが、本稿ではプロセスコンテキストとしています。

  3. 本稿では、v5.12 のソースコードを前提に説明します。

  4. ksoftirqd の処理の実体も __do_softirq() です。

  5. 本稿で触れるのは poll_list のみです。

  6. napi_schedule_irqoff() の中で、前述の通り、当該 napi_struct が処理中だった場合はソフトウェア割り込みを raise しないよう制御されています。

  7. https://www.dpdk.org/