Linuxカーネルで一回だけ実行する関数を作る

この記事はLinux Advent Calendar 2017の9日目の記事です。 なんとなくlib/を見ていたらonce.cなんてファイルを見つけて、一度だけ実行したいという時に使う関数を見つけたのでその機能についてのきじになります。

使い方

まず使い方をザクっと見てみましょう。使用するのはDO_ONCEマクロです。 実装はこんな感じです。

サンプルコード

#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/debugfs.h>
#include <asm/uaccess.h>
#include <linux/err.h>
#include <linux/once.h>

MODULE_DESCRIPTION("once test");
MODULE_AUTHOR("masami256");
MODULE_LICENSE("GPL");

static struct dentry *once_test_file;

static void once_test_run_once(int *count)
{
    (*count)++;
    pr_info("%s called\n", __func__);
}

static ssize_t once_test_write(struct file *filp, const char __user *buf, size_t len, loff_t *ppos)
{
    static int called_cnt = 0;

    DO_ONCE(once_test_run_once, &called_cnt);
    pr_info("called_cnt: %d\n", called_cnt);
    return strnlen_user(buf, 8);
}

static struct file_operations once_test_fops = {
    .owner = THIS_MODULE,
    .write = once_test_write,
};

static int once_test_init(void)
{
    once_test_file = debugfs_create_file("once_test", 0200,
                   NULL, NULL,
                   &once_test_fops);

    if (IS_ERR(once_test_file)) {
        WARN_ON(1);
        return PTR_ERR(once_test_file);
    }

    pr_info("setup done.\n");
    return 0;
}

static void once_test_cleanup(void)
{
    pr_info("cleanup\n");
    debugfs_remove(once_test_file);
}

module_init(once_test_init);
module_exit(once_test_cleanup);

使い方的には/sys/kernel/debug/once_testというファイルに何か書き込むとpr_info()で変数の値を表示します。ここで、初回に限りcalled_cnt変数の値をインクリメントしています。

実行結果

実行するとこうなります。想像通りですね( ´∀`)bグッ!

[  998.207119] once_test: setup done.
[ 1004.934061] once_test: once_test_run_once called
[ 1004.934069] once_test: called_cnt: 1
[ 1015.924961] once_test: called_cnt: 1

DO_ONCEの実装

一度だけ実行したい関数と、その関数用の引数を任意の数だけ受け取ります。先のサンプルコードは1個しか引数を受け取ってませんが。

        #define DO_ONCE(func, ...)                           \
   ({                                     \
       bool ___ret = false;                       \
       static bool ___done = false;                  \
       static struct static_key ___once_key = STATIC_KEY_INIT_TRUE; \
       if (static_key_true(&___once_key)) {                 \
           unsigned long ___flags;                    \
           ___ret = __do_once_start(&___done, &___flags);       \
           if (unlikely(___ret)) {                  \
               func(__VA_ARGS__);               \
               __do_once_done(&___done, &___once_key,       \
                          &___flags);           \
           }                          \
       }                              \
       ___ret;                              \
   })

実行したかのチェックは単純なstatic変数のdoneです。で、doneがfalseならまだ未実行なので引数で渡された関数を実行します。funcの実行前後で_do_once_start()__do_once_done()の呼び出しがあります。 do_once_start()のほうはfuncの実行前にロックを取るだけです。__do_once_done()のほうは多少の処理があります。こちらは後ほど。

WARN*ONCEマクロとの違い

自分は一度だけ実行ってことで思い浮かぶのはWARN*ONCE系のマクロだったりします。DO_ONCEマクロはそれらとは違っています。WARN*ONCE系のマクロの実装、カウンタのクリアに関する実装はこちらに書いたので興味のある方は読んでみてくださいm( )m

qiita.com

一度だけというところをチェックする変数の置き場所もWARN*ONCE系マクロとは違っています。WARN*ONCE系マクロの場合、チェックに使用する変数は.dataセクションのstart_onceとend_onceの間に変数が置かれますが、DO_ONCEの場合は単なるstatic変数です。あと、チェックの変数が、doneとonce_keyの2種類があります。doneの方はdo_once_start()でロックを取る時にというか、ロックを取ったあとに値をチェックして、trueだったら即ロックを解放します。その場合、do_once_start()はfalseを返すのでfuncの実行はありません。funcを実行した場合は、do_once_done()でdoneをtrueに変えます。_once_keyのほうは__do_once_done()で使用します。

__do_once_done()の処理

この関数の処理は3つあります。1つはdone変数の値をtrueに変える。2つ目はdo_once_start()で取ったロックの解放です。そして3つ目が__once_keyの処理です。と言っても難しいことはなくて、static_key構造体のenabled変数の値を1から0にするだけです。

___once_keyはこのように初期化されていました。

static struct static_key ___once_key = STATIC_KEY_INIT_TRUE;

これはinclude/linux/jump_label.hを見るとこのようになっています。

#define STATIC_KEY_INIT_TRUE                    \
   { .enabled = { 1 },                    \
     { .entries = (void *)JUMP_TYPE_TRUE } }

そして、値を変えているのはlib/once.cのこの部分です。

static_key_slow_dec(work->key);

static_key_slow_dec()はinclude/linux/jump_label.hにある関数で、enabledメンバ変数の値を減らしているだけです。

static inline void static_key_slow_dec(struct static_key *key)
{
    STATIC_KEY_CHECK_USE();
    atomic_dec(&key->enabled);
}

と、やっていることは簡単です。ただ、do_once_done()で_once_keyの値を変えるのではなくて、値の変更をする関数をワークキューに突っ込んで、スケジューラによってワーカーが実行されたら値を変えるというようになってます。