執筆者 : 西村 大助
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_read
1 ファイルで指定され、当該ファイルのデフォルト値は 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
メンバを指定された値で更新していることがわかります。
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 すべきかどうか」の判断に使われていることがわかります。
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。
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()
が呼び出されます。
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()
によってカーネルスレッドを作成しています。
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 されることがわかります。
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 により起床されます。
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必要となりますが、それぞれの仕組みについて知っていれば、より適切なチューニングが行えるようになると思います。
-
似たようなファイルとして、
/proc/sys/net/core/busy_poll
がありますが、こちらは poll(2)/select(2)/epoll_wait(2) 時に効いてくる、NAPI のビジーポールのタイムアウト値を設定出来る物で、本稿では触れていません。↩ -
ここでは詳細は割愛しますが、この延長で、前編でも触れた
napi_poll()
が実行されます。↩ -
本稿での実行例は、kernel:5.13.0-27-generic、NIC:Intel X722(i40e ドライバ)での結果です。↩
-
napi id はカーネル内部で管理されている各 napi_struct に固有の ID です。↩
-
前編では細かく触れていませんが、NIC の割り込みハンドラから呼ばれる
napi_schedule_irqoff()
の延長で呼ばれる関数で、実際に、NET_RX_SOFTIRQ ソフトウェア割り込みを raise する関数でもあります。↩ -
例えば、
isolcpus
を使って専用の CPU で処理させるなど。↩