指定した関数を一回だけ実行するRUN_ONCE(9)マクロの挙動と実装を調べてみる

[NetBSD Advent Calendar 2023](https://qiita.com/advent-calendar/2023/netbsd) 16日目の記事です。今日は

## カーネルサンプルのコードで一度だけ実行される関数

ここ何日かの記事で紹介してきた[callout(9)](https://man.netbsd.org/callout.9)は、指定した関数を一定時間経過後に呼び出してもらう機能を提供するものでした。カーネルモジュールサンプルの `executor.c` から使い方を見てゆき、callout(9)の実装まで掘り下げて見てゆきました。

 * [カーネルモジュールのサンプルからcallout(9)の機能の使い方を見てみる](https://qiita.com/furandon_pig/items/83c77c44ac1082535511)
 * [callout(9)のcallout_init()の実装を調べてみる](https://qiita.com/furandon_pig/items/1ace0c1f81834d2e659a)
 * [callout(9)のcallout_reset()の実装を調べてみる](https://qiita.com/furandon_pig/items/d72c1a8070e0f56647a1)
 * [callout(9)に登録した関数のリストは誰がどのタイミングで処理しているか調べてみる ](https://qiita.com/furandon_pig/items/53f93652c603c3f66831)

ところで、callout(9)の使い方を把握するために調べていた `executor.c` ですが、よく見ると以下のようなコードがあります。

```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` が見当たりません。

```sh
# modload ./executor.kmod
```

![img.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61137/c2ccdf99-9434-7aed-b9f9-01a4b1d76fce.png)

コードの内容から予想すると、 `RUN_ONCE(&ctl, runonce_example);` というマクロ(？)に渡している `runonce_example` が怪しいです。実際、 `runonce_example()` の中で"executor once"が出力されています。

```c
 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)](https://man.netbsd.org/RUN_ONCE.9)というマクロが提供されているのでした。
(機能的には[pthread_once(9)](https://man.netbsd.org/pthread_once.3)と同等の挙動となるようです)

挙動としては名前から想像がつくように、 `RUN_ONCE` を挟んで実行した関数は、繰り返し呼び出した場合でも関数が呼ばれるのは最初の一回のみとなります。

## RUN_ONCEの実装はどうなっている？

`RUN_ONCE` マクロについて、どうやって一回のみの関数呼び出しを実現しているのか調べてみます。もう一度 `executor.c` のコードを見てみます。

```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` の内容を初期化する動作になっています。

```c
 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` が返されるようになる、という挙動になります。

```c
 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)](https://man.netbsd.org/RUN_ONCE.9)の挙動と実装を調べてみました。こうやって見ると、カーネルの機能としては関数だけでなく、便利な動作を実現するマクロもいろいろと提供されていることが分かります。ユーザが必要な機能を実装する際、いくつかのカーネル機能を組み合わせる形になるため、簡単なサンプルコードを皮切りにカーネル機能の調査を進めてゆくと、それぞれの機能の関連と体系的な理解が深まりそうです。