NetBSD Advent Calendar 2023 12日目の記事です。今日はcallout(9)の機能をもう少し深堀りしてみます。
callout_init()関数
executor.c
では、以下のような形で callout_init()
が呼ばれています。
51 static callout_t sc;
...
96 callout_init(&sc, 0);
callout_init()
は /usr/src/sys/kern/kern_timeout.c
で以下のような関数定義になっています。
277 void
278 callout_init(callout_t *cs, u_int flags)
executor.c
の例では第2引数の u_int flags
は 0
が渡されているため、今回は第1引数の callout_t *cs
のみを見れば良さそうです。 callout_t
は /usr/src/sys/sys/callout.h
で以下のように定義されています。一見すると単なる void*
型の配列なので意味がわからず面食らってしまいますが、コメントを見るとcallout(9)の機能自体はカーネル側で提供するものの、パフォーマンス上の理由から、使用するデータ領域はcalloutを呼び出す側で確保する使い方であるため、このような定義になっているようです。
38 /*
39 * The callout implementation is private to kern_timeout.c yet uses
40 * caller-supplied storage, as lightweight callout operations are
41 * critical to system performance.
42 *
43 * The size of callout_t must remain constant in order to ensure ABI
44 * compatibility for kernel modules: it may become smaller, but must
45 * not grow. If more space is required, rearrange the members of
46 * callout_impl_t.
47 */
48 typedef struct callout {
49 void *_c_store[10];
50 } callout_t;
callout_t
がどのようなデータ型にマップされるかは、 callout_init()
を見るのが良さそうです。実際は callout_impl_t
型にキャストされます。
277 void
278 callout_init(callout_t *cs, u_int flags)
279 {
280 callout_impl_t *c = (callout_impl_t *)cs;
281 struct callout_cpu *cc;
callout_impl_t
型は /usr/src/sys/sys/callout.h
で以下のように定義されています。各メンバ変数の役割を一気に理解するのは大変なので、まずは void (*c_func)(void *)
と int c_time
と struct callout_cpu * volatile c_cpu
を見てみます。それぞれcallout(9)から呼び出してほしい関数と関数が発火するまでの時間となっています。
91 typedef struct callout_impl {
92 struct callout_circq c_list; /* linkage on queue */
93 void (*c_func)(void *); /* function to call */
94 void *c_arg; /* function argument */
95 struct callout_cpu * volatile c_cpu; /* associated CPU */
96 int c_time; /* when callout fires */
97 u_int c_flags; /* state of this entry */
98 u_int c_magic; /* magic number */
99 } callout_impl_t;
100 #define CALLOUT_MAGIC 0x11deeba1
ここまでを踏まえると、 callout_init()
の中身でポイントとなる箇所が見えてきます。 callout_impl_t
の c_func
に NULL
を代入してcalloutされる関数が存在していない状態としておき、 c_cpu
にcalloutと紐付けるCPUを設定します。
(calloutの初期化処理なのに c_time
に 0
とか入れておかなくて大丈夫なの?という疑問はありますが...)
277 void
278 callout_init(callout_t *cs, u_int flags)
279 {
280 callout_impl_t *c = (callout_impl_t *)cs;
281 struct callout_cpu *cc;
282
283 KASSERT((flags & ~CALLOUT_FLAGMASK) == 0);
284
285 cc = curcpu()->ci_data.cpu_callout;
286 c->c_func = NULL;
287 c->c_magic = CALLOUT_MAGIC;
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;
294 c->c_cpu = &callout_cpu0;
295 }
これでとりあえず、 callout_init()
で行われる主要な処理について把握できました。記事的に長くなってので、実際にcallout(9)の設定を行う部分の実装については日を改めて解説したいと思います。
まとめ
callout(9)の初期化を行う callout_init()
の関数の実装について紹介しました。カーネルモジュールサンプルの executor.c
から深掘りしての解説になっていますが、カーネルが提供する機能を理解するための足がかりとしては有効そうです。