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

執筆者 : 西村 大助


1. はじめに

前編では、NAPI とそれに関連するソフトウェア割り込みの仕組みについて解説しました。
本稿では、前回の最後に触れた通り、Linux kernel に組み込まれている、NIC をソフトウェア割り込み以外で polling するための仕組みである、Busy Poll Socket と kthread NAPI polling について解説します。

2. Busy Poll Socket

2.1 概要

使用する socket に setsockopt(2) システムコールを使い、@optname=SO_BUSY_POLL で時間(μ秒単位。以降、「SO_BUSY_POLL 秒」と呼称)を設定することで、SO_BUSY_POLL 秒を上限としてアプリケーション自身に recv(2) システムコール等の延長で NIC を busy polling させる機能です。
man 7 socket にも以下のような記述がある通り、v3.11 で導入されました。

SO_BUSY_POLL (since Linux 3.11)
       Sets  the  approximate  time in microseconds to busy poll on a blocking receive
       when there is no data.  Increasing  this  value  requires  CAP_NET_ADMIN.   The
       default for this option is controlled by the /proc/sys/net/core/busy_read file.

2.2 使い方

SO_BUSY_POLL 秒のデフォルト値は、/proc/sys/net/core/busy_read1 ファイルで指定され、当該ファイルのデフォルト値は 0 となっています。
つまり、Busy Poll Socket の機能を有効にするには、以下のいずれかが必要となります。

  • /proc/sys/net/core/busy_read を 0 より大きくし、システム全体で Busy Poll Socket を有効とする。
  • socket に対し setsockopt(2) システムコールで SO_BUSY_POLL 秒を設定する(元の値より大きくするには管理者権限(CAP_NET_ADMIN)が必要)。

2.3 内部実装

まず、setsockopt(2) システムコールで SO_BUSY_POLL 秒を設定するとどうなるかを見てみると、以下のようになっており2、sk(sock 構造体)の sk_ll_usec メンバを指定された値で更新していることがわかります。

(net/core/sock.c)

   1158 #ifdef CONFIG_NET_RX_BUSY_POLL
   1159         case SO_BUSY_POLL:
   1160                 /* allow unprivileged users to decrease the value */
   1161                 if ((val > sk->sk_ll_usec) && !capable(CAP_NET_ADMIN))
   1162                         ret = -EPERM;
   1163                 else {
   1164                         if (val < 0)
   1165                                 ret = -EINVAL;
   1166                         else
   1167                                 sk->sk_ll_usec = val;
   1168                 }
   1169                 break;

この sk_ll_usec メンバが参照される個所はいくつかありますが、ここではまず sk_can_busy_loop() に着目すると、以下のようになっており、関数名が示す通り、「busy poll すべきかどうか」の判断に使われていることがわかります。

(include/net/busy_poll.h)

     39 static inline bool sk_can_busy_loop(const struct sock *sk)
     40 {
     41         return sk->sk_ll_usec && !signal_pending(current);
     42 }

実際この sk_can_busy_loop() は各プロトコル毎に定義されている受信関数で使用されており、例えば TCP(受信関数は tcp_recvmsg()) の場合、以下のようになっており、sk_can_busy_loop() 等の条件を満たせば sk_busy_loop() により sk(sock 構造体) に対応した napi_struct が busy polling されることになります3

(net/ipv4/tcp.c)

   2543         if (sk_can_busy_loop(sk) &&
   2544             skb_queue_empty_lockless(&sk->sk_receive_queue) &&
   2545             sk->sk_state == TCP_ESTABLISHED)
   2546                 sk_busy_loop(sk, nonblock);

また、sk_ll_usec は、この sk_busy_loop() のループを終了させるかどうかの判断でも参照されており、一定時間経過すればループが終了するようにもなっています。

3. kthread NAPI polling

3.1 概要

LWN でも紹介されていますが、(napi_struct 単位で)専用のカーネルスレッドを作成して NIC を busy polling させるための機能で、v5.12 で導入されました。

3.2 使い方

kthread NAPI polling の有効化・無効化は、以下のように4 sysfs 経由(/sys/class/net/(NIC名)/threaded ファイル)で NIC 単位で行います(カーネルスレッドは、前述の通り napi_struct 単位)。

# cat /sys/class/net/eno1/threaded 
0
# echo 1 >/sys/class/net/eno1/threaded
# cat /sys/class/net/eno1/threaded 
1

有効化すると、"napi/(NIC名)-(napi id)"5 という名前のカーネルスレッドが作成され、それぞれが対応した napi_struct に対して busy polling を行います。

# ps -efww | grep napi
root        2872       2  0 13:15 ?        00:00:00 [napi/eno1-8208]
root        2873       2  0 13:15 ?        00:00:00 [napi/eno1-8207]
root        2874       2  0 13:15 ?        00:00:00 [napi/eno1-8206]
root        2875       2  0 13:15 ?        00:00:00 [napi/eno1-8205]
root        2876       2  0 13:15 ?        00:00:00 [napi/eno1-8204]
root        2877       2  0 13:15 ?        00:00:00 [napi/eno1-8203]
root        2878       2  0 13:15 ?        00:00:00 [napi/eno1-8202]
root        2879       2  0 13:15 ?        00:00:00 [napi/eno1-8201]
root        2880       2  0 13:15 ?        00:00:00 [napi/eno1-8200]
root        2881       2  0 13:15 ?        00:00:00 [napi/eno1-8199]
root        2882       2  0 13:15 ?        00:00:00 [napi/eno1-8198]
root        2883       2  0 13:15 ?        00:00:00 [napi/eno1-8197]
root        2884       2  0 13:15 ?        00:00:00 [napi/eno1-8196]
root        2885       2  0 13:15 ?        00:00:00 [napi/eno1-8195]
root        2886       2  0 13:15 ?        00:00:00 [napi/eno1-8194]
root        2887       2  0 13:15 ?        00:00:00 [napi/eno1-8193]

3.3 内部実装

まず、/sys/class/net/(NIC名)/threaded ファイルで kthread NAPI polling を有効化した時の処理を見てみましょう。

/sys/class/net/(NIC名)/threaded ファイルの write ハンドラは threaded_store() で、最終的に dev_set_threaded() が呼び出されます。

(net/core/net-sysfs.c)

    557 static int modify_napi_threaded(struct net_device *dev, unsigned long val)
    558 {
    559         int ret;
    560 
    561         if (list_empty(&dev->napi_list))
    562                 return -EOPNOTSUPP;
    563 
    564         if (val != 0 && val != 1)
    565                 return -EOPNOTSUPP;
    566 
    567         ret = dev_set_threaded(dev, val);
    568 
    569         return ret;
    570 }
    571 
    572 static ssize_t threaded_store(struct device *dev,
    573                               struct device_attribute *attr,
    574                               const char *buf, size_t len)
    575 {
    576         return netdev_store(dev, attr, buf, len, modify_napi_threaded);
    577 }
    578 static DEVICE_ATTR_RW(threaded);

dev_set_threaded() は以下のようになっており、当該 NIC デバイスに属する全ての napi_struct に対して、napi_kthread_create() によってカーネルスレッドを作成しています。

(net/core/dev.c)

   6751 int dev_set_threaded(struct net_device *dev, bool threaded)
   6752 {
   6753         struct napi_struct *napi;
   6754         int err = 0;
   6755 
   6756         if (dev->threaded == threaded)
   6757                 return 0;
   6758 
   6759         if (threaded) {
   6760                 list_for_each_entry(napi, &dev->napi_list, dev_list) {
   6761                         if (!napi->thread) {
   6762                                 err = napi_kthread_create(napi);
   6763                                 if (err) {
   6764                                         threaded = false;
   6765                                         break;
   6766                                 }
   6767                         }
   6768                 }
   6769         }

napi_kthread_create() からわかる通り、カーネルスレッドの main 処理は napi_threaded_poll() で、最終的に、__napi_poll() による busy loop で busy polling されることがわかります。

(net/core/dev.c)

   7018 static int napi_threaded_poll(void *data)
   7019 {
   7020         struct napi_struct *napi = data;
   7021         void *have;
   7022 
   7023         while (!napi_thread_wait(napi)) {
   7024                 for (;;) {
   7025                         bool repoll = false;
   7026 
   7027                         local_bh_disable();
   7028 
   7029                         have = netpoll_poll_lock(napi);
   7030                         __napi_poll(napi, &repoll);
   7031                         netpoll_poll_unlock(have);
   7032 
   7033                         local_bh_enable();
   7034 
   7035                         if (!repoll)
   7036                                 break;
   7037 
   7038                         cond_resched();
   7039                 }
   7040         }
   7041         return 0;
   7042 }

また、処理すべきパケットが無くなった場合、カーネルスレッドは sleep 状態になりますが、到着したら ____napi_schedule()6 により起床されます。

(net/core/dev.c)

   4294 /* Called with irq disabled */
   4295 static inline void ____napi_schedule(struct softnet_data *sd,
   4296                                      struct napi_struct *napi)
   4297 {
   4298         struct task_struct *thread;
   4299 
   4300         if (test_bit(NAPI_STATE_THREADED, &napi->state)) {
   4301                 /* Paired with smp_mb__before_atomic() in
   4302                  * napi_enable()/dev_set_threaded().
   4303                  * Use READ_ONCE() to guarantee a complete
   4304                  * read on napi->thread. Only call
   4305                  * wake_up_process() when it's not NULL.
   4306                  */
   4307                 thread = READ_ONCE(napi->thread);
   4308                 if (thread) {
   4309                         /* Avoid doing set_bit() if the thread is in
   4310                          * INTERRUPTIBLE state, cause napi_thread_wait()
   4311                          * makes sure to proceed with napi polling
   4312                          * if the thread is explicitly woken from here.
   4313                          */
   4314                         if (READ_ONCE(thread->state) != TASK_INTERRUPTIBLE)
   4315                                 set_bit(NAPI_STATE_SCHED_THREADED, &napi->state);
   4316                         wake_up_process(thread);
   4317                         return;
   4318                 }
   4319         }
   4320 
   4321         list_add_tail(&napi->poll_list, &sd->poll_list);
   4322         __raise_softirq_irqoff(NET_RX_SOFTIRQ);
   4323 }

4. まとめ

前編での、NAPI に関する基本的な仕組みの解説に引き続き、本稿では、より性能を出すための仕組みである Busy Poll Socket と kthread NAPI polling について解説しました。
これらの仕組みに限らず、実際に性能改善を行う際は、ワークロードに応じて他にも様々なチューニング要素が7必要となりますが、それぞれの仕組みについて知っていれば、より適切なチューニングが行えるようになると思います。


  1. 似たようなファイルとして、/proc/sys/net/core/busy_poll がありますが、こちらは poll(2)/select(2)/epoll_wait(2) 時に効いてくる、NAPI のビジーポールのタイムアウト値を設定出来る物で、本稿では触れていません。

  2. 前編に引き続き、本稿でも v5.12 のソースコードを前提に説明します。

  3. ここでは詳細は割愛しますが、この延長で、前編でも触れた napi_poll() が実行されます。

  4. 本稿での実行例は、kernel:5.13.0-27-generic、NIC:Intel X722(i40e ドライバ)での結果です。

  5. napi id はカーネル内部で管理されている各 napi_struct に固有の ID です。

  6. 前編では細かく触れていませんが、NIC の割り込みハンドラから呼ばれる napi_schedule_irqoff() の延長で呼ばれる関数で、実際に、NET_RX_SOFTIRQ ソフトウェア割り込みを raise する関数でもあります。

  7. 例えば、isolcpus を使って専用の CPU で処理させるなど。