カーネルモジュール作成による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以下に置いています。次回はカーネル内の代表的なデータ構造であるリストについて述べます。