C
Linux
RaspberryPi
kernel
デバイスドライバ

組み込みLinuxデバイスドライバの作り方 (8)

8回目: debugfs用インタフェース

本連載について

組み込みLinuxのデバイスドライバをカーネルモジュールとして開発するためのHowTo記事です。本記事の内容は全てラズパイ(Raspberry Pi)上で動かせます。

本記事に登場するソースコード全体

https://github.com/take-iwiw/DeviceDriverLesson/tree/master/08_01

今回の内容

前回、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に保持します。

myDeviceDriver.c
#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に対して一般ユーザのアクセス権が付与されていないためです。

これらのディレクトリのマウントオプションを変えることで、これは変更できると思います(おそらく起動時に実行されるスクリプトのどこかにある、と思う)。が、深くは追っていませんし、変えるべきではないと思われます。