callout(9)に登録した関数のリストは誰がどのタイミングで処理しているか調べてみる 

[NetBSD Advent Calendar 2023](https://qiita.com/advent-calendar/2023/netbsd) 14日目の記事です。今日は[callout(9)のcallout_reset()の実装を調べてみる](https://qiita.com/furandon_pig/items/d72c1a8070e0f56647a1)で調べきれなかった、calloutに登録した関数について、関数呼び出し(関数発火)の条件判定と関数コールを行っている箇所を調べてみようと思います。

## calloutに登録した関数のリストを操作している箇所

まずはcalloutに登録した関数のリストを操作している箇所を探してみます。 `callout_softclock()` という関数の中で `struct callout_cpu->cc_todo` のリストを順に処理するような実装になっています。

```c
687 /*
688  * callout_softclock:
689  *
690  *  Soft interrupt handler, scheduled above if there is work to
691  *  be done.  Callouts are made in soft interrupt context.
692  */
693 static void
694 callout_softclock(void *v)
695 {
696     callout_impl_t *c;
697     struct callout_cpu *cc;
698     void (*func)(void *);
699     void *arg;
700     int mpsafe, count, ticks, delta;
701     lwp_t *l;
702
703     l = curlwp;
704     KASSERT(l->l_cpu == curcpu());
705     cc = l->l_cpu->ci_data.cpu_callout;
706
707     mutex_spin_enter(cc->cc_lock);
708     cc->cc_lwp = l;
709     while (!CIRCQ_EMPTY(&cc->cc_todo)) {
710         c = CIRCQ_FIRST(&cc->cc_todo);
```

さらに、 `callout_impl_t->c_flags` に `CALLOUT_FIRED` あるいは `CALLOUT_INVOKING` のビットが立っていた場合に `callout_impl_t->c_func` に設定された関数を呼び出す、という処理になっています。 `callout_softclock()` の関数名(と関数コメント)を見ると、どうやらソフトウェア割り込みでこの関数が呼ばれるようです。

```c
728         c->c_flags = (c->c_flags & ~CALLOUT_PENDING) |
729             (CALLOUT_FIRED | CALLOUT_INVOKING);
730         mpsafe = (c->c_flags & CALLOUT_MPSAFE);
731         func = c->c_func;
732         arg = c->c_arg;
733         cc->cc_active = c;
734
735         mutex_spin_exit(cc->cc_lock);
736         KASSERT(func != NULL);
737         if (__predict_false(!mpsafe)) {
738             KERNEL_LOCK(1, NULL);
739             (*func)(arg);
740             KERNEL_UNLOCK_ONE(NULL);
741         } else
742             (*func)(arg);
743         mutex_spin_enter(cc->cc_lock);
```

`callout_softclock()` が参照されている箇所を見ると以下のようになっています。249行目のif文のelseブロック中で `softint_establich()` 関数を使用して `callout_softclock()` を登録しています。

```c
236 void
237 callout_init_cpu(struct cpu_info *ci)
238 {
...
242     if ((cc = ci->ci_data.cpu_callout) == NULL) {
...
248     } else {
249         /* Boot CPU, one time only. */
250         callout_sih = softint_establish(SOFTINT_CLOCK | SOFTINT_MPSAFE,
251             callout_softclock, NULL);
252         if (callout_sih == NULL)
253             panic("callout_init_cpu (2)");
254     }
```

[softint_establish(9)](https://man.netbsd.org/softint.9)を見ると以下のように説明されています。 `flags` に指定したソフトウェア割り込みが発生すると、 `func` に指定した関数が呼び出されるという挙動になっています。 `callout_init_cpu()` 内の `softint_establish()` ではフラグとして `SOFTINT_CLOCK` と `SOFTINT_MPSAFE` が指定されており、クロックまたはMPSAFE(カーネルのグローバルロック発生時？)の割り込み時に関数が呼び出される指定になっています。

```
     softint_establish(flags, func, arg)

              Register a software interrupt.  The flags value must contain one
              of the following constants, specifying the priority level for
              the soft interrupt:

              SOFTINT_CLOCK, SOFTINT_BIO, SOFTINT_NET, SOFTINT_SERIAL
...
              The constant func specifies the function to call when the soft
              interrupt is executed.  The argument arg will be passed to this
              function.
```

そして `callout_init_cpt()` は `/usr/src/sys/kern/kern_cpu.c` で定義されている以下の関数から呼ばれています。 `mi_cpu_attach()` はマシン非依存(Machine Independent)な関数となっています。そのため、 `callout_init_cpu()` は各CPUアーキテクチャにおいてCPUがされた際に `SOFTINT_CLOCK` または `SOFTINT_MPSAFE` による割り込み時に `struct callout_cpu->cc_todo` のリストが処理されるという挙動になります。

```c
150 int
151 mi_cpu_attach(struct cpu_info *ci)
152 {
...
194     callout_init_cpu(ci);
```

## まとめ

リストとして管理されている、calloutに登録した関数がどのタイミングで処理されるかを調べてみました。てっきりカーネルスレッド等でループしながら処理しているのかなと思っていましたが、実際はソフトウェア割り込みを起因にしてリストを処理している実装となっていました。