NetBSD Advent Calendar 2023 13日目の記事です。今日はcallout(9)に呼び出してほしい関数を設定する callout_reset()
の実装を見てゆこうと思います。
callout_reset()
callout_init()
でcalloutに必要な構造体( callout_t
)の初期化を行った後、実際に呼び出しほしい関数を callout_reset()
で設定します。例えば、カーネルモジュールサンプルの executor.c
では以下のような関数呼び出しとなっています。
callout_reset(&sc, mstohz(1000), callout_example, NULL);
callout_init()の解説を踏まえると、 callout_reset()
の内部で行われる処理がが何となく想像できそうです。さっそく callout_reset()
の実装を見てみます。
callout_reset()
は /usr/src/sys/kern/kern_timeout.c
で以下のように定義されています。やはり callout_impl_t
のメンバ変数 c_func
に引数で渡した関数を設定しています。
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が発火していない&呼び出されていない状態にするのは納得できます。
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では callout_init(&sc, 0)
となっており、(第2引数の) u_int flags
が 0
であるためです。
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()
で操作しているリスト)を辿って関数を呼び出す(イメージ的には「コールバックする」という表現がより適切かもしれません)処理を担うものがあるはずですが、今回はそこまで追いきれませんでした。
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()
で呼び出してほしい関数と関数呼び出しまでの時間を指定する処理を追いかけてみました。登録した関数はリストで管理されているという所までは把握できたのですが、そのリストを誰が辿って関数呼び出し(関数発火)の条件判定と関数コールを行っているかまでは追いかけきれませんでした。