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

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

3回目: システムコールハンドラとドライバの登録(動的な方法)

本連載について

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

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

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

今回の内容

前回は、デバイスのメジャー番号を決め打ちで静的に設定して、カーネルに登録するという方法を試しました。しかし、この方法は現在では推奨されていないようです。
今回は、これを動的に設定する方法を行います。また、udevの仕組みを利用して、デバイスファイルを自動で作ってみます。

こまごました修正

  • カーネルモジュールのライセンスを示す必要があるようです。これがないと、ビルド時にwarningが出ます。本に倣い、以下のようなライセンス設定をします。
    • MODULE_LICENSE("Dual BSD/GPL");
    • (BSDとのデュアルライセンスになっています。Linuxだから、強制的に全部GPLになるのではないかな? と思うのですが、カーネルモジュールだからBSDでもいいのかな? よくわからないです。。。)
  • 変数や関数のつけ方として、個人的にはローワーキャメルケースが好きです。Cの場合は、先頭にモジュール名+_をつけてます。ただ、Linuxカーネルのソースコードを見ていると、スネークケースが主流っぽいので合わせます。
  • デフォルトだとCのバージョンがC89でしたが、あまりにも不便なのでC99を使用するようにMakefileを修正しました。これからは、下記Makefileを使用します。
CFILES = myDeviceDriver.c

obj-m := MyDeviceModule.o
MyDeviceModule-objs := $(CFILES:.c=.o)

ccflags-y += -std=gnu99 -Wall -Wno-declaration-after-statement

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) modules
clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(shell pwd) clean

動的にメジャー番号を割り当てて、カーネルに登録する

デバイスドライバのソースコード

open/close/read/writeといったシステムコールハンドラ関数は前回と同じです。重要なのは、mydevice_init関数内での登録処理になります。

  • (1) alloc_chrdev_region関数によって空いているメジャー番号を動的に取得します。その時、本デバイスドライバが使うマイナー番号に関する情報も設定します。今回は試しにマイナー番号0と1を使うようにしてみました。つまり、デバイスが2つ接続され得ると想定します。
  • (3) cdev_init関数によって、cdevオブジェクトを初期化します。具体的には、システムコールハンドラ(open/close/read/write)のテーブルを登録します。
  • (4) cdev_add関数を使用して、3.で初期化したcdevオブジェクトをカーネルに登録をします。
myDeviceDriver.c
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/sched.h>
#include <linux/device.h>
#include <asm/current.h>
#include <asm/uaccess.h>

MODULE_LICENSE("Dual BSD/GPL");

/* /proc/devices等で表示されるデバイス名 */
#define DRIVER_NAME "MyDevice"

/* このデバイスドライバで使うマイナー番号の開始番号と個数(=デバイス数) */
static const unsigned int MINOR_BASE = 0;
static const unsigned int MINOR_NUM  = 2;   /* マイナー番号は 0 ~ 1 */

/* このデバイスドライバのメジャー番号(動的に決める) */
static unsigned int mydevice_major;

/* キャラクタデバイスのオブジェクト */
static struct cdev mydevice_cdev;

/* open時に呼ばれる関数 */
static int mydevice_open(struct inode *inode, struct file *file)
{
    printk("mydevice_open");
    return 0;
}

/* close時に呼ばれる関数 */
static int mydevice_close(struct inode *inode, struct file *file)
{
    printk("mydevice_close");
    return 0;
}

/* read時に呼ばれる関数 */
static ssize_t mydevice_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
    printk("mydevice_read");
    buf[0] = 'A';
    return 1;
}

/* write時に呼ばれる関数 */
static ssize_t mydevice_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
    printk("mydevice_write");
    return 1;
}

/* 各種システムコールに対応するハンドラテーブル */
struct file_operations s_mydevice_fops = {
    .open    = mydevice_open,
    .release = mydevice_close,
    .read    = mydevice_read,
    .write   = mydevice_write,
};

/* ロード(insmod)時に呼ばれる関数 */
static int mydevice_init(void)
{
    printk("mydevice_init\n");

    int alloc_ret = 0;
    int cdev_err = 0;
    dev_t dev;

    /* 1. 空いているメジャー番号を確保する */
    alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DRIVER_NAME);
    if (alloc_ret != 0) {
        printk(KERN_ERR  "alloc_chrdev_region = %d\n", alloc_ret);
        return -1;
    }

    /* 2. 取得したdev( = メジャー番号 + マイナー番号)からメジャー番号を取得して保持しておく */
    mydevice_major = MAJOR(dev);
    dev = MKDEV(mydevice_major, MINOR_BASE);    /* 不要? */

    /* 3. cdev構造体の初期化とシステムコールハンドラテーブルの登録 */
    cdev_init(&mydevice_cdev, &s_mydevice_fops);
    mydevice_cdev.owner = THIS_MODULE;

    /* 4. このデバイスドライバ(cdev)をカーネルに登録する */
    cdev_err = cdev_add(&mydevice_cdev, dev, MINOR_NUM);
    if (cdev_err != 0) {
        printk(KERN_ERR  "cdev_add = %d\n", alloc_ret);
        unregister_chrdev_region(dev, MINOR_NUM);
        return -1;
    }

    return 0;
}

/* アンロード(rmmod)時に呼ばれる関数 */
static void mydevice_exit(void)
{
    printk("mydevice_exit\n");

    dev_t dev = MKDEV(mydevice_major, MINOR_BASE);

    /* 5. このデバイスドライバ(cdev)をカーネルから取り除く */
    cdev_del(&mydevice_cdev);

    /* 6. このデバイスドライバで使用していたメジャー番号の登録を取り除く */
    unregister_chrdev_region(dev, MINOR_NUM);
}

module_init(mydevice_init);
module_exit(mydevice_exit);

ビルドしてロードしてみる

ビルドとロードは前回と同じです。デバイスファイルの作成ですが、メジャー番号が動的に決まるので、いちいち調べるのが面倒なので、下記のようなコマンドでまとめてしまいます。ちなみに、ラズパイ環境だと、メジャー番号は242になりました。

make
sudo insmod  MyDeviceModule.ko
sudo mknod --mode=666 /dev/mydevice0 c `grep MyDevice /proc/devices | awk '{print $1;}'` 0
sudo mknod --mode=666 /dev/mydevice1 c `grep MyDevice /proc/devices | awk '{print $1;}'` 1
sudo mknod --mode=666 /dev/mydevice2 c `grep MyDevice /proc/devices | awk '{print $1;}'` 2

実験的に/dev/mydevice0、/dev/mydevice1、/dev/mydevice2を作ってみました。cdev_addするときに、デバイス数は2個だと指定しています。そのため、/dev/mydevice2を作ることはできるのですが、openしたりアクセスしようとすると、cat: /dev/mydevice2: No such device or addresといったエラーが出ます。

終了処理

静的な場合と同じく、下記で削除します。

sudo rmmod MyDeviceModule
sudo rm /dev/mydevice0
sudo rm /dev/mydevice1
sudo rm /dev/mydevice2

ノート: マイナー番号ごとに処理を変えたい場合

例えば、マイナー番号0(/dev/mydevice0)とマイナー番号1(/dev/mydevice1)で処理を変えたい場合があります。read/writeハンドラ関数内で、マイナー番号を使ってswitch-caseで処理を分けてもいいのですが、登録するハンドラテーブルを分けることでも実現できます。具体的には、上記のコード上で、s_mydevice_fopsを処理を分けたいマイナー数分用意する(例えば、s_mydevice_fops0s_mydevice_fops1)。struct cdev mydevice_cdev;を配列にして、3, 4, 5の処理内容(それぞれ、cdev_init()cdev_add()cdev_del())で、別々の設定をすることで、対応できます。

udev対応して自動的にデバイスファイル(/dev/XXX)を作る

上述の方法で、メジャー番号を動的に割り当てたデバイスドライバのロードが出来るようになりました。しかし、デバイスファイルを手動で作る必要があり、面倒です。
Linuxにはudevという仕組みがあります。ドライバロード時に、/sys/class/にクラス登録をすると、udevdというデーモンがそれを検出して自動的にデバイスファイルを作ってくれます。
実装上は、「ドライバロード時に、/sys/class/にクラス登録をする」という処理が追加で必要になります。

ノート

実際にはudevはプラグ&プレイ機能に使われるようです。デバイスドライバモジュール(.ko)を特別な場所(/lib/modules/)に置いておくと、デバイスが検出されたときに、自動的に対応するデバイスドライバがロードされるようです。そのため、insmodも本来は不要です。

デバイスドライバのソースコード

変更があるのは、mydevice_initmydevice_exitだけなので、そこだけ抜粋します。 それぞれクラス登録と削除の処理が追加されています。

myDeviceDriver.c(一部省略)
/* デバイスドライバのクラスオブジェクト */
static struct class *mydevice_class = NULL;

/* ロード(insmod)時に呼ばれる関数 */
static int mydevice_init(void)
{
    printk("mydevice_init\n");

    int alloc_ret = 0;
    int cdev_err = 0;
    dev_t dev;

    /* 1. 空いているメジャー番号を確保する */
    alloc_ret = alloc_chrdev_region(&dev, MINOR_BASE, MINOR_NUM, DRIVER_NAME);
    if (alloc_ret != 0) {
        printk(KERN_ERR  "alloc_chrdev_region = %d\n", alloc_ret);
        return -1;
    }

    /* 2. 取得したdev( = メジャー番号 + マイナー番号)からメジャー番号を取得して保持しておく */
    mydevice_major = MAJOR(dev);
    dev = MKDEV(mydevice_major, MINOR_BASE);    /* 不要? */

    /* 3. cdev構造体の初期化とシステムコールハンドラテーブルの登録 */
    cdev_init(&mydevice_cdev, &s_mydevice_fops);
    mydevice_cdev.owner = THIS_MODULE;

    /* 4. このデバイスドライバ(cdev)をカーネルに登録する */
    cdev_err = cdev_add(&mydevice_cdev, dev, MINOR_NUM);
    if (cdev_err != 0) {
        printk(KERN_ERR  "cdev_add = %d\n", alloc_ret);
        unregister_chrdev_region(dev, MINOR_NUM);
        return -1;
    }

    /* 5. このデバイスのクラス登録をする(/sys/class/mydevice/ を作る) */
    mydevice_class = class_create(THIS_MODULE, "mydevice");
    if (IS_ERR(mydevice_class)) {
        printk(KERN_ERR  "class_create\n");
        cdev_del(&mydevice_cdev);
        unregister_chrdev_region(dev, MINOR_NUM);
        return -1;
    }

    /* 6. /sys/class/mydevice/mydevice* を作る */
    for (int minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
        device_create(mydevice_class, NULL, MKDEV(mydevice_major, minor), NULL, "mydevice%d", minor);
    }

    return 0;
}

/* アンロード(rmmod)時に呼ばれる関数 */
static void mydevice_exit(void)
{
    printk("mydevice_exit\n");

    dev_t dev = MKDEV(mydevice_major, MINOR_BASE);

    /* 7. /sys/class/mydevice/mydevice* を削除する */
    for (int minor = MINOR_BASE; minor < MINOR_BASE + MINOR_NUM; minor++) {
        device_destroy(mydevice_class, MKDEV(mydevice_major, minor));
    }

    /* 8. このデバイスのクラス登録を取り除く(/sys/class/mydevice/を削除する) */
    class_destroy(mydevice_class);

    /* 9. このデバイスドライバ(cdev)をカーネルから取り除く */
    cdev_del(&mydevice_cdev);

    /* 10. このデバイスドライバで使用していたメジャー番号の登録を取り除く */
    unregister_chrdev_region(dev, MINOR_NUM);
}

module_init(mydevice_init);
module_exit(mydevice_exit);

ルールファイルを追加する

上記コードによって、insmodしたタイミングで/sys/class/mydevice/mydevice0/devが作られます。udevの仕組みによって、/sys/class/mydevice/mydevice0/devの情報を基に自動的にデバイスファイル/dev/mydevice0が作られます。

しかし、デフォルトだと一般ユーザのアクセス権限がない状態になります。 ルールファイルを追加してあげて、アクセス権限を変更するようにします。ルールファイルは/etc/udev/rules.d/にあります。ルールファイル自体は数字から始まって、拡張子が.rulesのファイルなら何でもいいようです。ラズパイだと最初から、/etc/udev/rules.d/99-com.rulesというルールファイルがあるので、そこに追加しても良いです。僕は、一応分けてみたかったので、下記のようにしました。ラズパイだとsudoをつけても/etc/udev/rules.d/に新規ファイルを作れなかったので、sudo -iで一度スーパーユーザモード入ってから操作を行いました。

sudo -i
echo 'KERNEL=="mydevice[0-9]*", GROUP="root", MODE="0666"' >>  /etc/udev/rules.d/81-my.rules
exit

ビルドしてロードしてみる

ビルドしてinsmodしてみます。今回は、insmodするだけで自動的にデバイスファイル(/dev/mydevice0と/dev/mydevice1)が作られています。

make
sudo insmod  MyDeviceModule.ko
ls -a /dev/my*
/dev/mydevice0  /dev/mydevice1

また、/sys/class/mydevice/mydevice0/ディレクトリとその下にデバイス情報を格納したファイルが作られています。udevが上手くいかないときはここも確認してみてください。

cat /sys/class/mydevice/mydevice0/dev
242:0
cat /sys/class/mydevice/mydevice0/uevent
MAJOR=242
MINOR=0
DEVNAME=mydevice0

注意

本記事の内容は、「Linuxデバイスドライバプログラミング (平田 豊)」の内容に沿っています。
クラス作成のために、本ではclass_device_createを使用するようになっています。しかし、class_device_createは現在は廃止されています。本記事では代わりに、device_createを使用しています。