3回目: システムコールハンドラとドライバの登録(動的な方法)
本連載について
組み込み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回目: 作成したデバイスドライバを起動時にロードする
本記事に登場するソースコード全体
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オブジェクトをカーネルに登録をします。
#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", cdev_err);
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_fops0
とs_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_init
とmydevice_exit
だけなので、そこだけ抜粋します。 それぞれクラス登録と削除の処理が追加されています。
/* デバイスドライバのクラスオブジェクト */
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
を使用しています。