callout(9)のcallout_reset()の実装を調べてみる

[NetBSD Advent Calendar 2023](https://qiita.com/advent-calendar/2023/netbsd) 13日目の記事です。今日は[callout(9)](https://man.netbsd.org/callout.9)に呼び出してほしい関数を設定する `callout_reset()` の実装を見てゆこうと思います。

## callout_reset()

`callout_init()` でcalloutに必要な構造体( `callout_t` )の初期化を行った後、実際に呼び出しほしい関数を `callout_reset()` で設定します。例えば、カーネルモジュールサンプルの `executor.c` では以下のような関数呼び出しとなっています。

```c
callout_reset(&sc, mstohz(1000), callout_example, NULL);
```

[callout_init()](https://qiita.com/furandon_pig/items/1ace0c1f81834d2e659a)の解説を踏まえると、 `callout_reset()` の内部で行われる処理がが何となく想像できそうです。さっそく `callout_reset()` の実装を見てみます。

`callout_reset()` は `/usr/src/sys/kern/kern_timeout.c` で以下のように定義されています。やはり `callout_impl_t` のメンバ変数 `c_func` に引数で渡した関数を設定しています。

```c
377 /*
378  * callout_reset:
379  *
380  *  Reset a callout structure with a new function and argument, and
381  *  schedule it to run.
382  */
383 void
384 callout_reset(callout_t *cs, int to_ticks, void (*func)(void *), void *arg)
385 {
386     callout_impl_t *c = (callout_impl_t *)cs;
387     kmutex_t *lock;
388
389     KASSERT(c->c_magic == CALLOUT_MAGIC);
390     KASSERT(func != NULL);
391
392     lock = callout_lock(c);
393     c->c_func = func;
394     c->c_arg = arg;
395     callout_schedule_locked(c, lock, to_ticks);
396 }
```

`callout_reset()` 内で指定した関数が発火するまでの時間を設定しているのだろうと思っていましたが、これは `callout_reset()` から呼び出している `callout_schedule_locked()` の中で設定しているようです。

`callout_schedule_locked()` を見てみます。関数の頭の方で `callout_impl_t` の `c_flags` から `CALLOUT_FIRED` と `CALLOUT_INVOKING` のフラグが落とされています。呼び出し元の関数は `callout_reset()` なので、calloutが発火していない&呼び出されていない状態にするのは納得できます。

```c
329 static void
330 callout_schedule_locked(callout_impl_t *c, kmutex_t *lock, int to_ticks)
331 {
...
338     /* Initialize the time here, it won't change. */
339     occ = c->c_cpu;
340     c->c_flags &= ~(CALLOUT_FIRED | CALLOUT_INVOKING);
...
359     cc = curcpu()->ci_data.cpu_callout;
360     if ((c->c_flags & CALLOUT_BOUND) != 0 || cc == occ ||
361         !mutex_tryenter(cc->cc_lock)) {
362         /* Leave on existing CPU. */
363         c->c_time = to_ticks + occ->cc_ticks;
364         c->c_flags |= CALLOUT_PENDING;
365         CIRCQ_INSERT(&c->c_list, &occ->cc_todo);
366     } else {
```

340行目の時点で、 `c_flags` に設定されている値は `CALLOUT_BOUND` のみとなります。これは今回参照している[executor.c](https://qiita.com/furandon_pig/items/83c77c44ac1082535511)では `callout_init(&sc, 0)` となっており、(第2引数の) `u_int flags` が `0` であるためです。

```c
277 void
278 callout_init(callout_t *cs, u_int flags)
279 {
...
288     if (__predict_true((flags & CALLOUT_MPSAFE) != 0 && cc != NULL)) {
289         c->c_flags = flags;
290         c->c_cpu = cc;
291         return;
292     }
293     c->c_flags = flags | CALLOUT_BOUND;
```

そして363行目にて、この時点での `callout_impl_t->cc_ticks` と(引数として渡された) `int to_ticks` を加算した値を `callout_impl_t->c_time` に設定しています。その後、 `c_flags` に `CALLOUT_PENDING` フラグを立ててから `struct callout_cpu->cc_todo` に `CIRCQ_INSERT()` で `struct callout_cpu` の `cc_todo` メンバ変数に `c_list` を追加します( `CIRCQ_INSERT(elem, list)` というマクロになっています)。

`callout_reset()` での関数と関数が発火するまでを指定する処理は以上になります。後は定期的にcallout周りのリスト( `CIRCQ_INSERT()` で操作しているリスト)を辿って関数を呼び出す(イメージ的には「コールバックする」という表現がより適切かもしれません)処理を担うものがあるはずですが、今回はそこまで追いきれませんでした。

```c
329 static void
330 callout_schedule_locked(callout_impl_t *c, kmutex_t *lock, int to_ticks)
331 {
...
338     /* Initialize the time here, it won't change. */
339     occ = c->c_cpu;
340     c->c_flags &= ~(CALLOUT_FIRED | CALLOUT_INVOKING);
...
359     cc = curcpu()->ci_data.cpu_callout;
360     if ((c->c_flags & CALLOUT_BOUND) != 0 || cc == occ ||
361         !mutex_tryenter(cc->cc_lock)) {
362         /* Leave on existing CPU. */
363         c->c_time = to_ticks + occ->cc_ticks;
364         c->c_flags |= CALLOUT_PENDING;
365         CIRCQ_INSERT(&c->c_list, &occ->cc_todo);
366     } else {
```

## まとめ

`callout_reset()` で呼び出してほしい関数と関数呼び出しまでの時間を指定する処理を追いかけてみました。登録した関数はリストで管理されているという所までは把握できたのですが、そのリストを誰が辿って関数呼び出し(関数発火)の条件判定と関数コールを行っているかまでは追いかけきれませんでした。