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

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

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

本連載について

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

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

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

今回の内容

古い(?)方法で、ユーザがデバイスドライバにアクセスできるようにする。

前回、モジュールがロード/アンロード(insmod/rmmod)されるときのハンドラだけを用意したハローワールド的なものを作成しました。今回は、実際のプログラムやシェルからopen/closeして、値をread/write出来るようにします。
そのために、open/close/read/writeといったシステムコール用の処理を実装します。また、ユーザはデバイスファイル(/dev/XXX)として本デバイスドライバにアクセスするため、カーネルへのデバイス登録を行います。今回はデバイスを決め打ちで静的に登録します。この方法は古く、今は推奨されていないようなのですが、内容を理解するためにこのステップを挟みます。

ビルド用Makefile

1ファイルだけなので、以下のようなMakefileを用意します。myDeviceDriver.cというソースコードからMyDeviceModule.koを作成します。

CFILES = myDeviceDriver.c

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

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

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

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 <asm/current.h>
#include <asm/uaccess.h>

#define DRIVER_NAME "MyDevice_NAME"
#define DRIVER_MAJOR 63

/* 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");
    /* ★ カーネルに、本ドライバを登録する */
    register_chrdev(DRIVER_MAJOR, DRIVER_NAME, &s_myDevice_fops);
    return 0;
}

/* アンロード(rmmod)時に呼ばれる関数 */
static void myDevice_exit(void)
{
    printk("myDevice_exit\n");
    unregister_chrdev(DRIVER_MAJOR, DRIVER_NAME);
}

module_init(myDevice_init);
module_exit(myDevice_exit);

システムコール用ハンドラの定義

ユーザからのシステムコール(open,close,read,write)に対応するハンドラ関数を定義します。今回はひとまず、ログを出したり、固定値を返すだけにします。これらの関数をs_myDevice_fopsに格納します。

カーネルにドライバを登録する

モジュールがロードされるタイミング(つまり、myDevice_initの中)で、register_chrdev関数によって、本デバイスドライバをキャラクターデバイスとしてカーネルに登録します。「このデバイスドライバのメジャー番号はDRIVER_MAJOR(63)で、名前はDRIVER_NAME("MyDevice_NAME")だよ。各システムコールに対応するハンドラのテーブルはs_myDevice_fopsに入っているよ」と、教えてあげます。ここで指定するメジャー番号は、本デバイスを特定するのに使われる非常に重要な番号となります。

ちなみに、メジャー番号60~63、120~127、240~254はローカルの実験用にreservedされている番号のようです。そのため、今回は63を使用しました。

動作確認

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

下記コマンドでビルドとロードを行います。

make
sudo insmod MyDeviceModule.ko

その後、登録されているデバイスリストを確認してみます。すると、コード内で登録したとおり、Character devicesの所に、メジャー番号63として本デバイス("MyDevice_NAME")が登録されていることが分かります。

cat /proc/devices
Character devices:
  1 mem
省略
 63 MyDevice_NAME

ユーザがアクセスできるようにデバイスファイルを作る

通常、ユーザはデバイスドライバに対してデバイスファイルを使用してアクセスします。そのデバイスファイルを用意してあげます。そのためにmknodコマンドを使用します。第1引数がデバイスファイルの名前になります。これは何でもOKです。第2引数はデバイスの種別です。今回はキャラクタデバイスなので、cを設定します。第3引数が作成するデバイスファイルに対応するデバイスのメジャー番号になります。これは、先ほど作成したデバイスドライバの番号と一致する必要があります。今回は63だったので63を指定します。第4引数はマイナー番号となります。同じデバイス用のデバイスファイルを何個も作る場合に、区別するために使われます。ひとまず1を入れておきます。

mknod/dev/myDeviceを作成した後、誰でもアクセスできるように、アクセス権を変更しておきます。

sudo mknod /dev/myDevice c 63 1
sudo chmod 666 /dev/myDevice

ls -la /dev
crw-rw-rw-  1 root root    63,   1 Dec 17 23:08 myDevice

シェルから読み書きしてみる

動作確認は、今回作成した/dev/myDeviceに対してopenして、read/writeしてcloseするようなCコードを書いてもいいのですが、簡単のためシェルから確認します。

まず、下記コマンドでwriteの確認をします

echo "a" > /dev/myDevice
dmesg

コマンド発行後、dmesgでログを見ると、実装したwrite用の関数が呼ばれていることが分かると思います。2回呼ばれているのは'a'と'\0'のためだと思います。また、open,closeも自動的に呼ばれています。

[11974.888831] myDevice_open
[11974.888934] myDevice_write
[11974.888944] myDevice_write
[11974.888968] myDevice_close

続いて、下記コマンドでreadの確認をします。

cat /dev/myDevice

このコマンドを打つと、コンソール上に延々と'A'が出力されると思いますので、適当にCtrl-cで停止してください。これは、myDevice_read関数が常に値を返すためです。実際には、読み出すべき値がなくなったら0を返すようにします。

終了処理

使い終わったら下記コマンドで、デバイスドライバのアンロードとデバイスファイルの削除を行います。デバイスドライバなので、使い終わるということは実際のユースケースではないと思うのですが、デバッグ時などには使うと思います。なお、デバイスファイル(/dev/myDevice)は一度作れば、そのあとはいくらデバイスドライバをrmmod,insmodしても同じメジャー番号である限りは、カーネルがいい感じに新しくロードしたデバイスドライバを呼んでくれるようです。

sudo rmmod MyDeviceModule
sudo rm /dev/myDevice