カーネルモジュール作成によるlinuxカーネル開発入門 - 第二回 一定時間後に処理をする(タイマー)

はじめに

本記事は第一回の続きです。前回までの記事を既に見た上で開発環境ができていることを前提としています。

前回書いたコードはモジュールをロードした時とアンロードしたときだけ動いていました。今回は、ある時点から一定時間後に所定の処理をする方法を学びましょう。そのためにカーネル内のタイマー機能を使います。タイマー機能はカーネル内のいたるところで使われています。

今回書くソースはすべてelkdatのソースディレクトリ以下のdev/module/timer以下に配置するものとします。

一番簡単な例

モジュールロードから10秒後にメッセージを出してみましょう。次のようなソースになります。

#include <linux/module.h>
#include <linux/timer.h>

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Satoru Takeuchi <satoru.takeuchi@gmail.com>");
MODULE_DESCRIPTION("timer kernel module: oneshot timer");
 
struct timer_list mytimer;
 
#define MYTIMER_TIMEOUT_SECS    10

static void mytimer_fn(unsigned long arg)
{ 
        printk(KERN_ALERT "10 secs passed.\n");
}

static int mymodule_init(void)
{
        init_timer(&mytimer);
        mytimer.expires = jiffies + MYTIMER_TIMEOUT_SECS*HZ;
        mytimer.data = 0;
        mytimer.function = mytimer_fn;
        add_timer(&mytimer);

        return 0;
}

static void mymodule_exit(void)
{ 
        del_timer(&mytimer);
}

module_init(mymodule_init);
module_exit(mymodule_exit);

まず覚えて欲しいのは次のことです。

  • タイマーを使いたいときはlinux/timer.hをインクルードする
  • struct timer_listという構造体が1つのタイマーを指す。各フィールドの意味は次の通り
フィールド名 意味
expires タイマーが動作する時刻。設定した値の意味は後述
function タイマーが動作したときに呼ばれる処理
data functionが呼ばれたときの引数(後述)
  • init_timer()によってタイマーを初期化(ここではモジュールロード時)
  • add_timer()によってタイマーが動作するようにカーネルに登録
  • del_timer()によってタイマーが今後動作しないようにカーネルから登録解除(ここではモジュールアンロード時)

続いてstruct timer_list.expiresに設定した値の意味を説明します。まず、jiffiesはカーネル起動直後からの経過時間を指します。この値は一秒間にHZという数だけ増加します1。つまり、mytimer.expires = jiffies + MYTIMER_TIMEOUT_SECS(=10)*HZは「mytimerというタイマーを現在時刻から10秒後に動作させる」という意味になります。

Makefileは次の通りです。

.PHONY: all clean

obj-m := timer1.o

all:
        make -C ../../../output M=$(PWD) modules

clean:
        make -C ../../../linux M=$(PWD) clean

カーネルモジュールをビルドした上で、VMに配置し、VMにログインしてrootになります。

$ make
...
$ make install
...
$ make login
...
vagrant@packer-qemu:~$ sudo su
root@packer-qemu:/home/vagrant# 

作成したカーネルモジュールをロードし、直後にカーネルログを確認します。

root@packer-qemu:/home/vagrant# insmod /vagrant/timer1.ko && dmesg | tail
...
[  275.906429] random: crng init done

このカーネルモジュールからは何も出力されていません。続いて、10秒以上経過してから再度ログを確認してみましょう。

root@packer-qemu:/home/vagrant# dmesg | tail
...
[  275.906429] random: crng init done
[16480.610243] 10 secs passed.

(モジュールロード時から)10秒経過したことを示すメッセージが出力されました。うまく動いていたようです。

最後にモジュールをアンロードしてVMから抜けます。

root@packer-qemu:/home/vagrant# rmmod timer1
root@packer-qemu:/home/vagrant# exit
...
vagrant@packer-qemu:~$ exit
...
$ 

カーネルに登録したタイマーの処理が呼ばれるのは一度だけということに注意してください。モジュールのロードからどれだけの時間が経過してもカーネルログに出力されるメッセージは一行だけということからそれが確認できます。定期的に所定の処理をしたいような場合については後述します。

カーネルモジュールのビルド、VMへの配置、VMへのログイン、VM上でrootになる手順は毎回ほぼ同じなので、これ以降は省略します。Makefileの中身についても同じく省略します。

タイマーの登録を解除しなければどうなるか

前回はお行儀よく、ロード時にカーネルに登録したタイマーを必ずアンロード時に登録解除していました。では登録解除しなければ、アンロード時に何もしなければどうなるのでしょうか。ソースは次の通りです。

...
static void timer_module_exit(void)
{
        /* do nothing */
}
...

timer_module_exit()の内容以外はtimer1.cと同じです。

ロードしてみましょう。

root@packer-qemu:/home/vagrant# insmod /vagrant/timer2.ko
root@packer-qemu:/home/vagrant# 

10秒後以降にカーネルログを見ます。

root@packer-qemu:/home/vagrant# dmesg | tail
...
[17595.694822] 10 secs passed.

メッセージが出ました。アンロードします。

root@packer-qemu:/home/vagrant# rmmod timer2

何も問題はありませんでした。では、次のようにカーネルモジュールをロードした直後にアンロードする場合はどうでしょうか。

root@packer-qemu:/home/vagrant# insmod /vagrant/timer2.ko && rmmod timer2
root@packer-qemu:/home/vagrant# 

何も起きない…ように見えますが10秒待ってからVMを操作しようとしてください。何も反応が無いはずです。

それもそのはず、タイマーの登録解除処理を省いたおかげで(せいで?)みなさんがお使いのカーネルは無事パニックしました。おめでとうございます。カーネル内のコードでは1つのバグがシステム全体にとっての致命傷になることがこれでお分かりいただけたと思います。

さて、このままでは困るので、まずは復旧します。VMsshで接続している端末はもう戻ってきませんので閉じてしまって、別の端末から次のコマンドを実行します。

$ cd elkdat/
$ vagrant reload 
...
$ vagrant ssh -c "sudo grub-reboot ktest"
...
$ vagrant reload
...
$ 

パニックが起きるまでの流れは次の通りです。

  1. カーネルモジュールをロードしてカーネルにmytimerを登録
  2. カーネルモジュールをアンロードして、当該モジュールのメモリをすべて開放
  3. 10秒後に、解放済みのアドレスに存在するデータ(タイマー)を操作しようとしたためパニック

このようなことを起こさないようにするために、add_timer()とdel_timer()は必ず組にして使いましょう。

定期的に処理を実行する

カーネルモジュールのロードから10秒後だけでなく、10秒毎にメッセージを出してみましょう。ソースは次の通りです。

#include <linux/module.h>
#include <linux/timer.h>

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Satoru Takeuchi <satoru.takeuchi@gmail.com>");
MODULE_DESCRIPTION("timer kernel module: periodic timer");

struct timer_list mytimer;

#define MYTIMER_TIMEOUT_SECS    10

static void mytimer_fn(unsigned long arg)
{
        printk(KERN_ALERT "10 secs passed.\n");
        mod_timer(&mytimer, jiffies + MYTIMER_TIMEOUT_SECS*HZ);
}

static int mymodule_init(void)
{
        init_timer(&mytimer);
        mytimer.expires = jiffies + MYTIMER_TIMEOUT_SECS*HZ;
        mytimer.data = 0;
        mytimer.function = mytimer_fn;
        add_timer(&mytimer);

        return 0;
}

static void mymodule_exit(void)
{
        del_timer(&mytimer);
}

module_init(mymodule_init);
module_exit(mymodule_exit);

ほとんどtimer1.cと同じです。違いはタイマーの処理(mytimer_fn())においてメッセージを出力した直後にmod_timer()という関数を追加していることです。この関数は、第一引数で指定したタイマーを再度カーネルに登録します。

ここではタイマーの処理において、呼び出し時点から10秒後に再度タイマーを動作させるように登録しています。これによって、10秒毎にmytimer_fn()を呼び出しています。カーネル内で定期的に処理を実行したい場合はこのようにします。

ロードして、10秒後以降にカーネルログを見ます。

root@packer-qemu:/home/vagrant# insmod /vagrant/timer3.ko
root@packer-qemu:/home/vagrant# dmesg | tail
...
[   40.130824] 10 secs passed.

メッセージが出力されています。1分ほど経過してから再度ログを見ましょう。

root@packer-qemu:/home/vagrant# dmesg | tail
...
[   40.130824] 10 secs passed.
[   50.407771] 10 secs passed.
[   60.657174] 10 secs passed.
[   70.898123] 10 secs passed.
[   81.137445] 10 secs passed.
[   91.377037] 10 secs passed.
root@packer-qemu:/home/vagrant# 

正しく10秒ごとにメッセージがメッセージが出力されていることがわかります。

モジュールをアンロードします。それから10秒以上経ってから再度ログを見ます。

root@packer-qemu:/home/vagrant# rmmod timer3
root@packer-qemu:/home/vagrant# dmesg
...
[   81.137445] 10 secs passed.
[   91.377037] 10 secs passed.
root@packer-qemu:/home/vagrant# 

アンロード以降はメッセージが出力されていないことがわかります。タイマーの登録解除ができている証拠です。

複数のタイマーを動作させる

ここでは2つのタイマーを動作させる例を示します。2つとも動作時には同じ関数を呼び出します。このようなときに役に立つのがstruct timer_list.dataです。前述のように、このフィールドはタイマー動作時に呼ばれる関数(struct timer_list.function)の第一引数に渡されます。このフィールドには各タイマーに関係する構造体へのポインタを格納することが多いです。具体的にどのように利用するかはこれから述べます。

では、2つのタイマーを動作させる例を示します。仕様は次の通り。

  • 一つ目のタイマー。名前は"foo"。2秒に1回動作し、動作するごとに自分の名前と呼び出し間隔を出力
  • 二つ目のタイマー。名前は"bar"。3秒に1回動作し、動作するごとに自分の名前と呼び出し間隔を出力

ソースは次の通りです。今までの例に比べて若干長くて複雑です。

#include <linux/module.h>
#include <linux/timer.h>

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Satoru Takeuchi <satoru.takeuchi@gmail.com>");
MODULE_DESCRIPTION("timer kernel module: pass arg");

struct mytimer_data { 
        char *name;
        int interval;
        struct timer_list timer;
};

struct mytimer_data data[2] = {
        {
                .name = "foo",
                .interval = 2,
        },
        {
                .name = "bar",
                .interval = 3,
        },
};
 
#define MYTIMER_TIMEOUT_SECS    10

static void mytimer_fn(unsigned long arg)
{
        struct mytimer_data *data = (struct mytimer_data *)arg;

        printk(KERN_ALERT "%s: %d secs passed.\n",
               data->name, data->interval);

        mod_timer(&data->timer, jiffies + data->interval*HZ);
}

static int mymodule_init(void)
{
        int i;

        for (i = 0; i < 2; i++) {
                struct mytimer_data *d = &data[i];
                init_timer(&d->timer);
                d->timer.function = mytimer_fn;
                d->timer.expires = jiffies + d->interval*HZ;
                d->timer.data = (unsigned long)d;
                add_timer(&d->timer);
        }

        return 0;
}

static void mymodule_exit(void)
{
        int i;

        for (i = 0; i < 2; i++) {
                del_timer(&data[i].timer);
        }
}

module_init(mymodule_init);
module_exit(mymodule_exit);

ソースのポイントは次の通りです。

  • それぞれのタイマーに関する情報をstruct timer_list.dataに格納
  • mytimer_fn()呼び出し時に上記の情報を取り出し、処理をする(当該関数を呼び出したタイマーによって挙動を変える)

動かしてみましょう。ロードして、しばらくしてからカーネルログを見ます。

root@packer-qemu:/home/vagrant# insmod /vagrant/timer4.ko
root@packer-qemu:/home/vagrant# dmesg | tail
[ 3916.676466] foo: 2 secs passed.
[ 3918.532436] bar: 3 secs passed.
[ 3918.692439] foo: 2 secs passed.
[ 3920.708419] foo: 2 secs passed.
[ 3921.604404] bar: 3 secs passed.
[ 3922.724395] foo: 2 secs passed.
[ 3924.676374] bar: 3 secs passed.
[ 3924.740372] foo: 2 secs passed.
[ 3926.756355] foo: 2 secs passed.
[ 3927.748342] bar: 3 secs passed.
root@packer-qemu:/home/vagrant# 

タイマーfooが2秒ごとに、同barが3秒ごとに呼び出されていることがわかります。

最後に(コマンドログは示しませんが)、モジュールをアンロードして、その後にこのモジュールからのメッセージが出力されないことを確認しておきましょう。これにて今回はおしまい。

演習問題

  • タイマーを実行するたびに待ち時間を変える。たとえばモジュールロード時から10秒後、15秒後、25秒後、30秒後…など
  • timer4.cを変更して、それぞれのタイマーの呼び出し時に通算呼び出し回数を出力。呼び出し回数を示すカウンタは各タイマで独立させる

おわりに

今回使ったソースはexample/module/timer以下に置いています。必要があれば参照してください。次回はユーザプロセスとカーネルインターフェイスについて扱います。


  1. /boot/config-<カーネルバージョン>というファイルのCONFIG_HZという値に、当該カーネルにおけるHZの値が記載されています。筆者の環境では250なので、jiffiesは一秒間に250増加します。