8回目: debugfs用インタフェース
本連載について
組み込みLinuxのデバイスドライバをカーネルモジュールとして開発するためのHowTo記事です。本記事の内容は全てラズパイ(Raspberry Pi)上で動かせます。
- 1回目: ビルド環境準備と、簡単なカーネルモジュールの作成
- 2回目: システムコールハンドラとドライバの登録(静的な方法)
- 3回目: システムコールハンドラとドライバの登録(動的な方法)
- 4回目: read/writeの実装とメモリのお話
- 5回目: ラズパイ用のGPIOデバドラの実装
- 6回目: ioctlの実装
- 7回目: procfs用インタフェース
- 8回目: debugfs用インタフェース <--------------------- 今回の内容
- 9回目: 他のカーネルモジュールの関数を呼ぶ / GPIO制御関数を使う
- 10回目: I2Cを使ったデバイスドライバを作る
- 11回目: デバイスツリーにI2Cデバイスを追加する
- 12回目: 作成したデバイスドライバを起動時にロードする
本記事に登場するソースコード全体
今回の内容
前回、procfsを使用してデバイスドライバのデバッグ用インタフェースを作りました。しかし、本来procfsはプロセスに関する情報を配置するところで、デバッグ用に使うことはよくありません。デバッグ用にはdebugfsを使用することが推奨されています。(@rarul さんよりご指摘いただきました。ありがとうございます。)
今回は、前回と同じことを、このdebugfsを使用して実装してみようと思います。具体的には、
/sys/kernel/debug/MyDevice/prm1
にアクセスすることで、デバッグ用のパラメータを読んだり書いたりできるようにします。なお、このパスは環境によって変わる可能性があります。
debugfs用インターフェースの実装
debugfsインターフェースの作り方は、大きく2種類あります。それぞれの方法について記載しようと思います。
方法1: file_operationsを使用する
debugfs用インターフェースを作るには、ドライバがロード(insmod)される所で、debugfs_create_file()
という関数によって、ファイル名とread/write時に呼んでほしい関数を登録するだけです。登録するときには、struct file_operations
テーブルにread/writeハンドラ関数を設定します。これは、前回のprocfs、および、通常のデバイスドライバのハンドラ登録と全く同じです。
しかし、これだとdebugfsアクセス用のファイルが、debugfsのルートディレクトリに作成されてしまいます。当然、そこには他のカーネルモジュールのファイルもあるので、ディレクトリで整理します。debugfs用にディレクトリを作るには、debugfs_create_dir()
を使います。debugfs_create_dir()
の戻り値(entry)を、先ほどのdebugfs_create_file()
の第3引数に入れてあげることで、作成したディレクトリの下にファイルが作られるようになります。
方法2: ヘルパー関数を使用する
デバッグ用にパラメータを読み書きしたいだけなのに、毎回read/write関数を定義するのが面倒な場合があります。read/write操作だけが必要な場合には、ヘルパー関数1つでdebugfsを作ることが出来ます。例えば、32-bitの変数にアクセスする場合には、debugfs_create_u32()
を使用します(定義はunsigned型でしたが、一応負数の入出力もできました)。16進数で読み書きしたい場合には、debugfs_create_x32()
にします。他にも、bool型や、レジスタセット(アドレスと数値)、バイナリデータに使えるblob用の関数も用意されています。今回はひとまず、32bitの数値だけ使ってみます。
終了処理
カーネルをアンロードするときに、作成したdebugfsファイルを削除する必要があります。削除するためには、debugfs_remove()
を使用します。これで作成したファイルを削除できます。が、一つ一つ消していくのは面倒です。何より、作成した時のentry情報を覚えておくのが面倒です。そのために、debugfs_remove_recursive
()が用意されています。この関数に作成したディレクトリのentry (debugfs_create_dir()
の戻り値) を入れることで、その下のファイルも再帰的に削除してくれます。
コード
コードは、以下のようになります。デバッグ用のパラメータとして、debug_prm1
をstaticで持っています。debugfsルートディレクトリの下に作る自分用のディレクトリ名は"MyDevice"にし、entry情報をdebug_entry_dir
に保持します。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/proc_fs.h>
#include <asm/uaccess.h>
#include <linux/debugfs.h>
/*** このデバイスに関する情報 ***/
MODULE_LICENSE("Dual BSD/GPL");
#define DRIVER_NAME "MyDevice" /* /proc/devices等で表示されるデバイス名 */
/* デバッグ用変数 */
struct dentry *debug_entry_dir; /* debugfsディレクトリのエントリ */
static int debug_prm1; /* デバッグ用パラメータ(テスト用) */
static int debug_read_size = 0; /* 1回のopenでreadするバイト数 */
/* /sys/kernel/debug/MyDevice/debug_prm1にアクセスしたときに呼ばれる関数 */
static int mydevice_debug_open(struct inode *inode, struct file *file)
{
printk("mydevice_proc_open\n");
debug_read_size = 4; // 1回につき4Byte readする
return 0;
}
/* /sys/kernel/debug/MyDevice/debug_prm1のread時に呼ばれる関数 */
static ssize_t mydevice_debug_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
printk("mydevice_proc_read\n");
if (debug_read_size > 0) {
/* まだ出力すべきデータがあるとき、*/
/* 保持している整数型の数字(debug_prm1)を文字列で出力する */
int len;
len = sprintf(buf, "%d\n", debug_prm1); // 本当はcopy_to_userすべき
debug_read_size -= 4;
return len;
} else {
return 0;
}
}
/* /sys/kernel/debug/MyDevice/debug_prm1のwrite時に呼ばれる関数 */
static ssize_t mydevice_debug_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
printk("mydevice_proc_write\n");
/* 入力された文字列を整数型の数字(debug_prm1)として保持する */
sscanf(buf, "%d", &debug_prm1); // 本当はcopy_from_userすべき
return count;
}
/* debugfs用のハンドラテーブル */
static struct file_operations debug_debug_prm1_fops = {
.owner = THIS_MODULE,
.open = mydevice_debug_open,
.read = mydevice_debug_read,
.write = mydevice_debug_write,
};
/* ロード(insmod)時に呼ばれる関数 */
static int mydevice_init(void)
{
printk("mydevice_init\n");
/* debufs用ディレクトリを作成する */
debug_entry_dir = debugfs_create_dir(DRIVER_NAME, NULL);
if (debug_entry_dir == NULL) {
printk(KERN_ERR "debugfs_create_dir\n");
return -ENOMEM;
}
/* 方法1: ハンドラテーブル登録方式 */
debugfs_create_file("prm1", S_IRUGO | S_IWUGO, debug_entry_dir, NULL, &debug_debug_prm1_fops);
/* 方法2: ヘルパー関数方式 */
debugfs_create_u32("_prm1", S_IRUGO | S_IWUGO, debug_entry_dir, &debug_prm1);
debugfs_create_x32("_prm1_hex", S_IRUGO | S_IWUGO, debug_entry_dir, &debug_prm1);
return 0;
}
/* アンロード(rmmod)時に呼ばれる関数 */
static void mydevice_exit(void)
{
printk("mydevice_exit\n");
/* debufs用を取り除く(子ファイルも自動的に削除される) */
debugfs_remove_recursive(debug_entry_dir);
}
module_init(mydevice_init);
module_exit(mydevice_exit);
方法1だと、エントリ関数やハンドラテーブルを定義したりする必要がありましたが、方法2だと1行で終わります。当然簡単なのは方法2ですが、パラメータ書き換えと同時に何か処理する必要がある場合には方法1にする必要がありそうです。(例えば、i2cで接続されたデバイスのレジスタ(パラメータ)を変えてみる場合など。)
debugfs経由でパラメータを読み書きしてみる
下記コマンドでビルド、ロードします。
make
sudo insmod MyDeviceModule.ko
sudo ls /sys/kernel/debug/MyDevice
prm1 _prm1 _prm1_hex
すると、/sys/kernel/debug/MyDevice
の下に、prm1
、_prm1
、_prm1_hex
が出来ていることが分かります。prm1
は方法1での実装で、_prm1
と_prm1_hex
は方法2で実装になります。全て同じ変数へのアクセスを行います。(名前はこのように実装したから、prm1となっただけで、特に意味はありません。)
sudo bash -c 'echo 12 > /sys/kernel/debug/MyDevice/prm1'
sudo cat /sys/kernel/debug/MyDevice/prm1
12
sudo cat /sys/kernel/debug/MyDevice/_prm1
12
sudo cat /sys/kernel/debug/MyDevice/_prm1_hex
0x0000000c
sudo bash -c 'echo 13 > /sys/kernel/debug/MyDevice/_prm1'
sudo bash -c 'echo 0x0f > /sys/kernel/debug/MyDevice/_prm1_hex'
その後、catやechoで読み書きができます。全て同じ変数の値を入出力しているので結果は同じになります。16進数の時には自動的に0xがつくようです。
アクセス権について
これは、ラズパイだけかもしれません。
今回、各debugfsファイルを作るときに、アクセス権としてS_IRUGO | S_IWUGO
を指定しました。これによって全ユーザが読み書きできるはずです。しかし、実際には、実行するときにsudo
をつける必要がありました。これは、親ディレクトリである、/sys
及び、/sys/kernel
、/sys/kernel/debug
に対して一般ユーザのアクセス権が付与されていないためです。
これらのディレクトリのマウントオプションを変えることで、これは変更できると思います(おそらく起動時に実行されるスクリプトのどこかにある、と思う)。が、深くは追っていませんし、変えるべきではないと思われます。