1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

NetBSD Advent Calendar 2023 16日目の記事です。今日は

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

ここ何日かの記事で紹介してきたcallout(9)は、指定した関数を一定時間経過後に呼び出してもらう機能を提供するものでした。カーネルモジュールサンプルの executor.c から使い方を見てゆき、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

img.png

コードの内容から予想すると、 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_tONCE_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_statusONCE_DONE でない場合に _init_once() 経由で fn ( RUN_ONCE に渡された関数)が呼ばれます。

_init_once() の実装を見てみます。この関数は /usr/src/sys/kern/subr_once.c で定義されています。

今回の説明に必要となる部分を抜粋すると、 once_t->o_statusONCE_RUNNING に変更したのち、指定した関数を実行します。関数の実行が完了した段階で once_t->o_statusONCE_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)の挙動と実装を調べてみました。こうやって見ると、カーネルの機能としては関数だけでなく、便利な動作を実現するマクロもいろいろと提供されていることが分かります。ユーザが必要な機能を実装する際、いくつかのカーネル機能を組み合わせる形になるため、簡単なサンプルコードを皮切りにカーネル機能の調査を進めてゆくと、それぞれの機能の関連と体系的な理解が深まりそうです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?