カーネルモジュール作成によるlinuxカーネル開発入門 - 第三回 デバッグ用インターフェース

はじめに

本記事は第二回の続きです。前回までの記事を既に見ていることが前提です。

今回は、今後凝ったカーネルモジュールを作るにあたって必要になってくる、デバッグに有用なdebugfsというファイルシステムについて学びます。debugfsはカーネルとユーザとの間で簡単に情報をやりとりするためのファイルシステムです。linuxにはprocfs, sysfsという、このような使い方ができる他のファイルシステムもあります。しかし、前者は原則としてプロセスに関連する情報だけを扱うというルールがあること1、および、後者は扱いかたが少々難しい上に1ファイルにつき1つの値しかやりとりできないという制限があることより、おいそれと使えません。

debugfsは通常/sys/kernel/debugというディレクトリ以下にマウントされています。その下のファイルを読み書きすることによって、カーネルの情報をユーザプロセスから参照したり、ユーザプロセスからカーネルに情報を渡したりできます。これまでに使ってきたprintk()とは、

  • ユーザの望むタイミングで情報をやりとりできる
  • カーネルから情報を受取るだけでなく、ユーザから情報を渡せる

という違いがあります。

今回は、前回作成したタイマー処理を拡張して、ユーザプロセスから

  • タイマーの残り時間を参照する
  • タイマーの残り時間を変更する

ということをやります。

タイマーの残り時間を変更する

以下のようなモジュールを作ります。

  • モジュールロードの1000秒後に1000秒経過したことを示すメッセージを表示する
  • /sys/kernel/debug/mytimer_remain_msecsというファイルの読み出しによって、タイマーの残り時間(ミリ秒単位)を表示する
  • タイマー動作後に当該ファイルを読み出すと0を表示する

ソースを示します。

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

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Satoru Takeuchi <satoru.takeuchi@gmail.com>");
MODULE_DESCRIPTION("A simple example of debugfs");

static struct dentry *testfile;
static char testbuf[128];

struct timer_list mytimer;

#define MYTIMER_TIMEOUT_SECS    ((unsigned long)1000)

static void mytimer_fn(unsigned long arg)
{
        printk(KERN_ALERT "%lu secs passed.\n", MYTIMER_TIMEOUT_SECS);
}

static ssize_t mytimer_remain_msecs_read(struct file *f, char __user *buf, size_t len, loff_t *ppos)
{
        unsigned long diff_msecs, now = jiffies;

        if (time_after(mytimer.expires, now))
                diff_msecs = (mytimer.expires - now) * 1000 / HZ;
        else
                diff_msecs = 0;

        snprintf(testbuf, sizeof(testbuf), "%lu\n", diff_msecs);
        return simple_read_from_buffer(buf, len, ppos, testbuf, strlen(testbuf));
}

static struct file_operations test_fops = {
        .owner = THIS_MODULE,
        .read = mytimer_remain_msecs_read,
};

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);

        testfile = debugfs_create_file("mytimer_remain_msecs", 0400, NULL, NULL, &test_fops);
        if (!testfile)
                return -ENOMEM;

        return 0;
}

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

module_init(mymodule_init);
module_exit(mymodule_exit);

注目すべき点は次の通りです。

  • mytimer_remain_msecs_read()によってタイマーの残り時間を読み出す
  • time_after(a,b)は、時刻aが時刻bより先であれば1を、そうでなければ0を返す。ここでの時刻はunsigned long型なので、単純に"a - b > 0"という比較をしても常に1を返してしまう
  • simple_read_from_buffer()によって、testbufの内容をユーザ空間のバッファであるbufに渡している2
  • test_fops.readが、/sys/kernel/debug/mytimer_remain_msecs読み出し時に呼ばれるハンドラ
  • debugfs_create_file()によって/sys/kernel/debug/mytimer_remain_msecsを作成する(パーミッションは0400)
  • debugfs_remove()によって/sys/kernel/debug/mytimer_remain_msecsを削除する

VM上でモジュールロード前後に/sys/kernel/debug以下のファイルをリストしてみましょう。

# ls /sys/kernel/debug
acpi  cleancache  dma_buf  dynamic_debug  fault_around_bytes  gpio     kprobes  pinctrl  pwm  regmap     sched_features  split_huge_pages  suspend_stats  usb           wakeup_sources  zswap
bdi   clk         dri      extfrag        frontswap           iosf_sb  mce      pm_qos   ras  regulator  sleep_time      sunrpc            tracing        virtio-ports  x86
# insmod /vagrant/debugfs1.ko 
# ls /sys/kernel/debug
acpi  cleancache  dma_buf  dynamic_debug  fault_around_bytes  gpio     kprobes  mytimer_remain_msecs  pm_qos  ras     regulator       sleep_time        sunrpc         tracing  virtio-ports    x86
bdi   clk         dri      extfrag        frontswap           iosf_sb  mce      pinctrl               pwm     regmap  sched_features  split_huge_pages  suspend_stats  usb      wakeup_sources  zswap
# 

ロード前には存在しなかった/sys/kernel/debug/mytimer_remain_msecsがロード後にはできていることがわかります。

では、定期的に、たとえば一秒ごとにこのファイルを読み出してみましょう。

# for ((i=0;i<10;i++)) ; do cat /sys/kernel/debug/mytimer_remain_msecs ; sleep 1; done
839888
838888
837884
836884
835884
834880
833880
832880
831876
830876
# 

一秒ごとに約一秒(1000ミリ秒)づつカウントダウンしていることがわかります。

モジュールアンロード時にファイルが消えるかを確認します。

# rmmod debugfs1
# ls /sys/kernel/debug 
acpi  cleancache  dma_buf  dynamic_debug  fault_around_bytes  gpio     kprobes  pinctrl  pwm  regmap     sched_features  split_huge_pages  suspend_stats  usb           wakeup_sources  zswap
bdi   clk         dri      extfrag        frontswap           iosf_sb  mce      pm_qos   ras  regulator  sleep_time      sunrpc            tracing        virtio-ports  x86
# 

正しく消えたようです。

モジュール固有のディレクトリを作成する

さきほどは自作モジュール用のファイルを/sys/kernel/debug直下に作成しました。実はこれは新規ファイルを増やすに従って他のモジュール用のファイルと名前が重なる可能性が高まること、および、当該ディレクトリ以下にやたらとファイルが出来てしまって見通しが悪くなることによって、あまり褒められた方法ではありません。このようなことを避けるために、モジュールごとに/sys/kernel/debug以下に専用のディレクトリを作成するのが望ましいです。

さきほどのプログラムを一部変更して、タイマーの残り時間を/sys/kernel/debug/mytimer/remain_msecsから得るようにしましょう。

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

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Satoru Takeuchi <satoru.takeuchi@gmail.com>");
MODULE_DESCRIPTION("A simple example of debugfs");

static struct dentry *topdir;
static struct dentry *testfile;
static char testbuf[128];

struct timer_list mytimer;

#define MYTIMER_TIMEOUT_SECS    ((unsigned long)1000)

static void mytimer_fn(unsigned long arg)
{
        printk(KERN_ALERT "%lu secs passed.\n", MYTIMER_TIMEOUT_SECS);
}

static ssize_t mytimer_remain_msecs_read(struct file *f, char __user *buf, size_t len, loff_t *ppos)
{
        unsigned long diff_msecs, now = jiffies;

        if (time_after(mytimer.expires, now))
                diff_msecs = (mytimer.expires - now) * 1000 / HZ;
        else
                diff_msecs = 0;

        snprintf(testbuf, sizeof(testbuf), "%lu\n", diff_msecs);
        return simple_read_from_buffer(buf, len, ppos, testbuf, strlen(testbuf));
}

static struct file_operations test_fops = {
        .owner = THIS_MODULE,
        .read = mytimer_remain_msecs_read,
};

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);

        topdir = debugfs_create_dir("mytimer", NULL);
        if (!topdir)
                return -ENOMEM;
        testfile = debugfs_create_file("remain_msecs", 0400, topdir, NULL, &test_fops);
        if (!testfile)
                return -ENOMEM;

        return 0;
}

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

module_init(mymodule_init);
module_exit(mymodule_exit);

さきほどのソースとの差分で注目すべき点は次の通りです。

  • debugfs_create_dir()によって/sys/kernel/debug以下にmytimerというディレクトリを作成する
  • debugfs_create_file()の第3引数に、ディレクトリに対応するstruct dentry *型のデータを指定すると、当該ディレクトリ以下にファイルを作成する。この場合は/sys/kernel/debug/mytimer以下にremain_msecsというファイルを作成する
  • debugfs_remove_recursive()によって、指定したディレクトリ以下のファイルを再帰的に削除する

さきほどと同様、ロード後に所定のファイルができているかを確認します。

# insmod /vagrant/debugfs2.ko 
# ls /sys/kernel/debug 
acpi  cleancache  dma_buf  dynamic_debug  fault_around_bytes  gpio     kprobes  mytimer  pm_qos  ras     regulator       sleep_time        sunrpc         tracing  virtio-ports    x86
bdi   clk         dri      extfrag        frontswap           iosf_sb  mce      pinctrl  pwm     regmap  sched_features  split_huge_pages  suspend_stats  usb      wakeup_sources  zswap
# ls /sys/kernel/debug/mytimer 
remain_msecs
# 

うまくいっているようです。

また一秒ごとにタイマーの残り時間を見てみましょう。

# for ((i=0;i<10;i++)) ; do cat /sys/kernel/debug/mytimer/remain_msecs ; sleep 1; done
639008
638004
637004
636004
635000
634000
633000
632000
630996
629996
# 

成功です。

モジュールをアンロードして、このモジュールが作成したディレクトリが消えていることを確認します。

# rmmod debugfs2 
# ls /sys/kernel/debug
acpi  cleancache  dma_buf  dynamic_debug  fault_around_bytes  gpio     kprobes  pinctrl  pwm  regmap     sched_features  split_huge_pages  suspend_stats  usb           wakeup_sources  zswap
bdi   clk         dri      extfrag        frontswap           iosf_sb  mce      pm_qos   ras  regulator  sleep_time      sunrpc            tracing        virtio-ports  x86
# 

これも成功です。

タイマーの残り時間を変更する

これまではdebugfsを読み取りだけに使ってきましたが、今度は書き込みも試してみましょう。タイマーの残り時間を/sys/kernel/debug/mytimer/remain_msecsに書き込んだ値に設定し直します。ソースは次の通りです。

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

MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Satoru Takeuchi <satoru.takeuchi@gmail.com>");
MODULE_DESCRIPTION("A simple example of debugfs");

static struct dentry *topdir;
static struct dentry *testfile;
static char testbuf[128];

struct timer_list mytimer;

static unsigned long mytimer_timeout_msecs = 1000 * 1000;

static void mytimer_fn(unsigned long arg)
{
        printk(KERN_ALERT "%lu secs passed.\n", mytimer_timeout_msecs / 1000);
}

static ssize_t mytimer_remain_msecs_read(struct file *f, char __user *buf, size_t len, loff_t *ppos)
{
        unsigned long diff_msecs, now = jiffies;

        if (time_after(mytimer.expires, now))
                diff_msecs = (mytimer.expires - now) * 1000 / HZ;
        else
                diff_msecs = 0;

        snprintf(testbuf, sizeof(testbuf), "%lu\n", diff_msecs);
        return simple_read_from_buffer(buf, len, ppos, testbuf, strlen(testbuf));
}

static ssize_t mytimer_remain_msecs_write(struct file *f, const char __user *buf, size_t len, loff_t *ppos)
{
        ssize_t ret;

        ret = simple_write_to_buffer(testbuf, sizeof(testbuf), ppos, buf, len);
        if (ret < 0)
                return ret;
        sscanf(testbuf, "%20lu", &mytimer_timeout_msecs);
        mod_timer(&mytimer, jiffies + mytimer_timeout_msecs * HZ / 1000);
        return ret;
}

static struct file_operations test_fops = {
        .owner = THIS_MODULE,
        .read = mytimer_remain_msecs_read,
        .write = mytimer_remain_msecs_write,
};

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

        topdir = debugfs_create_dir("mytimer", NULL);
        if (!topdir)
                return -ENOMEM;
        testfile = debugfs_create_file("remain_msecs", 0600, topdir, NULL, &test_fops);
        if (!testfile)
                return -ENOMEM;

        return 0;
}

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

module_init(mymodule_init);
module_exit(mymodule_exit);

次の点に注目してください。

  • mytimer_timeout_msecsがタイマーの待ち時間を示す(ミリ秒単位)
  • mytimer_remain_msecs_write()によって、ユーザ空間のバッファであるbufから、カーネル空間のバッファtestbufにデータを書き込む
  • 上記で書き込んだデータをもとにタイマーの待ち時間を再設定

排他制御という観点からは本当はこのコードは少しまずいのですが、ここでは気にしないことにします。排他制御については後の回で扱う予定です。

では動作確認です。ファイルができているかどうかの確認は省略して、定期的にタイマーの残り時間が減少しているかを見ます。

# for ((i=0;i<10;i++)) ; do cat /sys/kernel/debug/mytimer/remain_msecs ; sleep 1 ; done
975616
974616
973616
972612
971612
970612
969612
968608
967608
966608
# 

ここまではOK。では、タイマーの残り時間を10秒に変更した上で、またカウントダウンの様子を眺めましょう。

# echo 10000 > /sys/kernel/debug/mytimer/remain_msecs 
# for ((i=0;i<10;i++)) ; do cat /sys/kernel/debug/mytimer/remain_msecs ; sleep 1 ; done
7832
6828
5828
4828
3828
2824
1824
824
0
0
# 

うまくいったようです。10秒経過したことを示すメッセージが出力されたかも確認します。

# dmesg | tail -1
[39528.736116] 10 secs passed.
# 

OKです。最後にモジュールをアンロードしておきましょう。実行例の表示は省略します。

ここまでで今回はおしまいです。

演習問題

  • タイマーを2つ作成して、それぞれの残り時間を/sys/kernel/debug/mytimer/{1,2}/remain_msecsによって読み書きできるようにする

おわりに

今回使ったソースはexample/module/debugfs以下に置いています。次回カーネル内の代表的なデータ構造であるリストについて述べます。

参考資料

  • カーネルソースのinclude/linux/debugfs.h: debugfs_create_*によって様々な型の単一のデータを扱うファイルを簡単に作成できる
  • カーネルソースのDocumentation/filesystems/debugfs.txt: debugfsの登場背景、用途、および使い方などについて述べられている

  1. 歴史的な事情によりプロセスに関係ないファイルも一部存在します

  2. 単純にmemcpy()によってデータをコピーしてしまうと、bufの一部が物理メモリ上に存在しない場合に問題となる。これについての詳細は後の回で述べる予定