2018/10/17

デバイスドライバを書いてみよう04 Linuxドライバとアプリケーションを連携しよう

前回でドライバの基本形である最小のLinuxモジュールを書いたが、今後はどのような実装が必要だろうか?

  1. デバイスとドライバの通信
  2. アプリケーションとドライバのやり取り

が必要だろう。

1は難易度も高いし、実際のデバイスを用意するなど敷居が高いので、とりあえず2から説明する。
今回は、アプリケーションがカーネルのシステムコールを介してドライバとデータをやり取りするようなものを実装したい。



前知識


デバイスと一言で言っても、実はいくつかの種類に分けられる。
主に下記の分類となるが、それによってドライバの書き方も異なる。

  • キャラクタ型デバイス
  • ブロック型デバイス
  • その他

キャラクタ型デバイスは1文字ずつデータをやり取りするデバイスで、ドライバもその仕様に合わせた実装となる。一般的なデバイスはこちらが多い。
ブロック型デバイスとは、データをブロック単位でやり取りする。ストレージなどがこれに当たる。
その他、ネットワーク型デバイスとか擬似デバイスなどあるが、それぞれ特殊な実装となる。

IoTなどで作成・仕様されるドライバはほとんどがキャラクタ型となる。ここではキャラクタ型を説明する。(他の型のドライバは書いたことないし)


アプリケーションとドライバのやり取り


以前「アプリケーションとドライバはOSのシステムコールを介してデータをやり取りする」と言った。
OSのシステムコールは抽象化されており、基本的に4つのシステムコールを使用してデータをやり取りする。open/closeとread/writeだ。
大体のプログラマなら、openでデバイスを使用可能にし、readやwriteを使って読み書きし、終了時にはcloseする、と予想がつくに違いない。
それぞれにハンドラが用意されているので、これをドライバ内に実装する形となる。

また「アプリケーションはデバイススペシャルファイルを介してドライバにアクセスする」とも言った。
アプリケーションはこのファイルをopenし、read/writeし、そしてcloseする。
今回は、実際にこのスペシャルファイルも作成する。


実装


面倒だけど、今回もディレクトリを切ろう。
$ cd ~/drivers
$ mkdir readwrite
$ cd readwrite

ここを今回の作業ディレクトリとする。

コードは下記のようなものとなる。

readwrite.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>

MODULE_LICENSE("Dual MIT/GPL");
MODULE_DESCRIPTION("ReadWrite Driver");

#define DRIVER_NAME "ReadWriteDriver"
#define DRIVER_MAJOR 60

static int mod_open(struct inode *inode, struct file *file)
{
 printk(KERN_ALERT "driver opened\n");
 return 0;
}

static int mod_close(struct inode *inode, struct file *file)
{
 printk(KERN_ALERT "driver closed\n");
 return 0;
}

static const int LENGTH = 256;
static char sBuffer[256];

static ssize_t mod_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
 printk(KERN_ALERT " read\n");
 if(count > LENGTH) count = LENGTH;
 if(copy_to_user(buf, sBuffer, count) != 0) return -EFAULT;
 return 1;
}

static ssize_t mod_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
 printk(KERN_ALERT " writed\n");
 if(count > LENGTH) count = LENGTH;
 if(copy_from_user(sBuffer, buf, count) != 0) return -EFAULT;
 return 1;
}

struct file_operations fops = {
 .open    = mod_open,
 .release = mod_close,
 .read    = mod_read,
 .write   = mod_write,
};

static int mod_init(void)
{
 printk(KERN_ALERT "driver loaded\n");
 register_chrdev(DRIVER_MAJOR, DRIVER_NAME, &fops);

 return 0;
}

static void mod_exit(void)
{
 printk(KERN_ALERT "driver unloaded\n");
 unregister_chrdev(DRIVER_MAJOR, DRIVER_NAME);
}

module_init(mod_init);
module_exit(mod_exit);

Makefile
obj-m := readwrite.o

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

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

構成の説明


まずは簡単に全体を説明する。
関数構成は、前回のinit,exitに加えてopen/close, read/writeのハンドラが実装されている。
上部にopenなどの新規のハンドラが、下部にinit/exitのハンドラがある。
init/exitの指定は前回と変わらない。open/close, read/writeのハンドラの指定は下記のようになされている。

  • 「struct file_operations」構造体「fops」に、openなどの各ハンドラを指定する。
  • その「fops」を、mod_init内で「register_chrdev」関数を使用して登録する。
  • mod_exit内では「unregister_chrdev」関数を使用して登録解除する。

今回はinit/exit内でregister_chrdev/unregister_chrdev関数を使用してキャラクタデバイスを登録/登録解除している。
登録の際に、デバイスドライバのMajor番号、デバイス名文字列も一緒に指定している。
Major番号とデバイス名は上部でdefineされている。

デバイスドライバは登録の際にMajor番号を指定する。
のちにアプリケーションからアクセスする際に、この番号でどのドライバとやり取りをするかを指定するのだが、この番号は予約されていたりするのできちんとした番号の選択が必要となる。
仕様を見ると60-63は「ローカル/実験的な目的で使用」とされているので、今回はデバイスのMajor番号は60とした。
このMajor番号が重複すると大変困る。そのため、厳密に予約されていたりするのだが、こんな仕様では新しいドライバが重複するのは目に見えている。今回は静的に番号指定したが、空いている番号を動的に割り当てる方法もあり、重複しないよう一応気にした構造になっている。

これだけでもopen/close, read/writeを動かすのには十分で、printkで動作チェックもできるが、実際にread/writeで値を書き換えてみたいので、若干の機能を追加した。

機能の説明


今回のプログラムは、writeで書き込んだ文字列を保持して、readでその文字列を返す、というものになっている。
文字は途中に定義されている

static char sBuffer[256];

に保持される。

各関数を説明すると、open/closeでは大したことはやっていない。
read/writeで文字列の保存、読み出しをやっているのだが、まずwriteからみてみよう。

static ssize_t mod_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
 printk(KERN_ALERT " writed\n");
 if(count > LENGTH) count = LENGTH;
 if(copy_from_user(sBuffer, buf, count) != 0) return -EFAULT;
 return 1;
}

mod_write内では「copy_from_user」関数を使用して、sBufferに書き込んでいる。
mod_writeの引数に、ユーザ空間から送られてきた文字列、およびその長さが指定されているので、これをsBufferに書き込むのだが、安全なコピーのため用意されているのがこのcopy_from_user関数となる。

static ssize_t mod_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
 printk(KERN_ALERT " read\n");
 if(count > LENGTH) count = LENGTH;
 if(copy_to_user(buf, sBuffer, count) != 0) return -EFAULT;
 return 1;
}

また、mod_read内では逆に「copy_to_user」関数を使用して、sBufferから読み出されている。
mod_readの引数にはwriteと対照的に、ユーザ空間に送るためのバッファが指定されているので、これにsBufferの内容を書き込む。この際にcopy_to_user関数を使用する。

1文字だけのやり取りだったら、get_userやput_userと言った関数もある。ユーザ空間とデータのやり取りは特殊なので、これらを使った方が無難だろう。


ファイル名がreadwriteに変わったので、Makefile内のファイル指定も変えとこう。
ビルドしてinsmodしといてね。
$ make
$ sudo insmod readwrite.ko

モジュールのロードとスペシャルファイルの作成


アプリケーションからアクセスするために、スペシャルファイルを作成しよう。作成にはmknodコマンドを使用する。
$ sudo mknod /dev/readwrite c 60 1
$ sudo chmod 666 /dev/readwrite

一行目でスペシャルファイルを作成しているのだが、キャラクタデバイスのMajor番号60で登録している。
最後の1はMinor番号というもので、デバイスドライバはMajor番号で指定するが、その中での機能の切り替えはMinor番号で行う。
同じデバイスドライバで複数のデバイスを扱うこともあるため、このような番号が用意されている。
今回は特に切り替えはないので、1を指定した。

あとはパーミッションを変更しておく。
パーミッションはデバイスを扱うユーザを制限するのに有効なのだが、今回は特に制限がないので、擬似デバイス/dev/randomなどと同じ666にでもしておこう。

アプリケーションの実装


今回はユーザ空間からドライバにアクセスするためのアプリケーションの実装も必要だ。
下に記載する。

test.c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

int main()
{
 int fd;
 char buff[256];

 if ((fd = open("/dev/readwrite", O_RDWR)) < 0) perror("open");

 if (write(fd, "feaewfa\0", 8) < 0) perror("write");

 if (read(fd, buff, 8) < 0) perror("read");
 printf("%s\n", buff);

 if (close(fd) != 0) perror("close");

 return 0;
}

さっき作ったスペシャルファイルをopenしてwriteしてreadしてcloseしている。
文字列はなんでもいいが、printする関係上最後に終端文字を入れている。

コンパイルして実行してみよう。
$ gcc test.c
$ ./a.out

writeした文字列が出力されるはず。
これでアプリケーションからの連携もできた。


後片付けとしては、rmmodしてスペシャルファイルをrmコマンドなどで削除すればいい。
$ sudo rmmod readwrite
$ sudo rm /dev/readwrite

問題・補足


今回はデータのバッファにstatic変数を用いたが、ドライバは複数のプログラムからアクセスされるため、staticで保存バッファを用意するのはあまりよくない、という問題もある。
readもwriteも引数にfile構造体を取っているが、これが各アクセス毎に一意となるので、このfile構造体にデータをくっつけるといい。
file構造体はprivate_dataというメンバを持っているため、これにデータをくっつけたらアクセスが重なっても問題ない。

今回はこれ以上プログラムが複雑になるのを避けたかったので、staticで説明したが、実際にはfile構造体にデータをくっつけて欲しい。
open/closeで領域を確保/解放して、read/writeで使う、と言った形がいい。


今回は、アプリケーションからドライバへのアクセスを行なった。
ここまでの知識で擬似デバイスは作ることができる。

次回はデバイスとドライバの通信を説明したい。あと上記の問題・補足についても、問題ないものにしたい。

0 件のコメント:

コメントを投稿