NetBSD Advent Calendar 2023 16日目の記事です。今日は
カーネルサンプルのコードで一度だけ実行される関数
ここ何日かの記事で紹介してきたcallout(9)は、指定した関数を一定時間経過後に呼び出してもらう機能を提供するものでした。カーネルモジュールサンプルの executor.c
から使い方を見てゆき、callout(9)の実装まで掘り下げて見てゆきました。
- カーネルモジュールのサンプルからcallout(9)の機能の使い方を見てみる
- callout(9)のcallout_init()の実装を調べてみる
- callout(9)のcallout_reset()の実装を調べてみる
- callout(9)に登録した関数のリストは誰がどのタイミングで処理しているか調べてみる
ところで、callout(9)の使い方を把握するために調べていた executor.c
ですが、よく見ると以下のようなコードがあります。
75 static void
76 callout_example(void *arg) {
77 RUN_ONCE(&ctl, runonce_example);
78 executor_count++;
79 printf("Callout %d\n", executor_count);
80 callout_schedule(&sc, mstohz(1000));
81 }
あらためてカーネルモジュールをロードした時の動作を見ると、"executor once"と出力されているものの、上記のコードと照らし合わせると、その表示を行う printf
が見当たりません。
# modload ./executor.kmod
コードの内容から予想すると、 RUN_ONCE(&ctl, runonce_example);
というマクロ(?)に渡している runonce_example
が怪しいです。実際、 runonce_example()
の中で"executor once"が出力されています。
57 /*
58 * runonce_example : This function should execute only once
59 * It should return 0 as RUN_ONCE calls the function until it returns 0.
60 */
61
62 static int
63 runonce_example(void) {
64 printf("executor once\n");
65 return 0;
66 }
何の気なしに man
を引いてみると、ずばりでRUN_ONCE(9)というマクロが提供されているのでした。
(機能的にはpthread_once(9)と同等の挙動となるようです)
挙動としては名前から想像がつくように、 RUN_ONCE
を挟んで実行した関数は、繰り返し呼び出した場合でも関数が呼ばれるのは最初の一回のみとなります。
RUN_ONCEの実装はどうなっている?
RUN_ONCE
マクロについて、どうやって一回のみの関数呼び出しを実現しているのか調べてみます。もう一度 executor.c
のコードを見てみます。
53 /* Creating a variable that marks whether the function has executed or not */
54 static once_t ctl;
55 static ONCE_DECL(ctl);
...
75 static void
76 callout_example(void *arg) {
77 RUN_ONCE(&ctl, runonce_example);
78 executor_count++;
79 printf("Callout %d\n", executor_count);
80 callout_schedule(&sc, mstohz(1000));
81 }
once_t
や ONCE_DECL
は /usr/src/sys/sys/once.h
で以下のように定義されています。
struct once_t
のメンバ変数、 o_status
で関数が実行完了有無を管理し、 ONCE_DECL
マクロで struct once_t
の内容を初期化する動作になっています。
33 typedef struct {
34 int o_error;
35 uint16_t o_refcnt;
36 uint16_t o_status;
37 #define ONCE_VIRGIN 0
38 #define ONCE_RUNNING 1
39 #define ONCE_DONE 2
40 } once_t;
...
46 #define ONCE_DECL(o) \
47 once_t (o) = { \
48 .o_status = 0, \
49 .o_refcnt = 0, \
50 };
51
52 #define RUN_ONCE(o, fn) \
53 (__predict_true((o)->o_status == ONCE_DONE) ? \
54 ((o)->o_error) : _init_once((o), (fn)))
RUN_ONCE
では、 struct once->o_status
が ONCE_DONE
でない場合に _init_once()
経由で fn
( RUN_ONCE
に渡された関数)が呼ばれます。
_init_once()
の実装を見てみます。この関数は /usr/src/sys/kern/subr_once.c
で定義されています。
今回の説明に必要となる部分を抜粋すると、 once_t->o_status
を ONCE_RUNNING
に変更したのち、指定した関数を実行します。関数の実行が完了した段階で once_t->o_status
を ONCE_DONE
に変更することで、以降は RUN_ONCE
経由で関数を呼び出そうとしても once_t->o_status == ONCE_DONE ? once_t->o_error : /*関数呼び出し*/
のチェックで once_t->o_error
が返されるようになる、という挙動になります。
50 int
51 _init_once(once_t *o, int (*fn)(void))
52 {
...
61 if (o->o_refcnt++ == 0) {
62 o->o_status = ONCE_RUNNING;
63 mutex_exit(&oncemtx);
64 o->o_error = fn();
65 mutex_enter(&oncemtx);
66 o->o_status = ONCE_DONE;
67 cv_broadcast(&oncecv);
68 }
...
まとめ
RUN_ONCE(9)の挙動と実装を調べてみました。こうやって見ると、カーネルの機能としては関数だけでなく、便利な動作を実現するマクロもいろいろと提供されていることが分かります。ユーザが必要な機能を実装する際、いくつかのカーネル機能を組み合わせる形になるため、簡単なサンプルコードを皮切りにカーネル機能の調査を進めてゆくと、それぞれの機能の関連と体系的な理解が深まりそうです。