6回目: ioctlの実装
本連載について
組み込み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回目: 作成したデバイスドライバを起動時にロードする
本記事に登場するソースコード全体
今回の内容
前回まで、基本的なシステムコール(open, close, read, write)の実装方法の解説を行いました。また、それらを使い、実際にラズパイのGPIO用デバイスドライバを作ってみました。
今回は追加で、ioctlというシステムコールを実装してみようと思います。
ioctlとは
#include <sys/ioctl.h>
int ioctl(int d, int request, ...);
上記で定義されるシステムコールです。第1引数にはopenで取得したファイルディスクリプタ(fd)を入れます。第2引数はリクエストと呼ばれていますが、ようはコマンドです。第3引数(可変長)はパラメータになります。このioctlを使うことで、ドライバ側で自由にインターフェイスを追加できます。
今まで、cdevを使用してデバイスドライバを作成してきました。これによって、カーネルからはキャラクタ型のデバイスドライバとして認識されています。そのため、read, writeでユーザアプリケーションとやり取りするときにはキャラクタ(char)型を使用していました。簡単なやり取りだけならこれでもできるのですが、実際にデバイスを制御するときにはこれでは不十分です。例えば、SPIの通信速度設定だったり、I2Cのスレーブアドレス設定には数字を指定する必要がありますが、これらはread,writeだけではできません。そういったときに、デバイスドライバ側でインターフェイスを追加します。そのためにioctlを使用します。
ノート
上述したように、ioctlは各デバイスドライバで独自にコマンドとパラメータを定義しています。そのため、ioctlを使用する際に確認すべき仕様(ヘッダ)はioctl.hではなく、各デバイスドライバが用意しているヘッダ(あるいはソースコードも)になります。今回は自分でデバイスドライバを作ってみるので、そこらへんの感覚が分かると思います。
ioctlシステムコールハンドラの実装
ヘッダの定義
繰り返しになりますが、ioctlではデバイスドライバ側で独自にコマンドとパラメータを定義します。コマンドはint型の数字、パラメータは、通常は構造体になります。(パラメータはなしでもOK)。
これらの定義を書いたヘッダを作ります。ユーザが使うときにも参照するので、別ファイルとします。
#ifndef MY_DEVICE_DRIVER_H_
#define MY_DEVICE_DRIVER_H_
#include <linux/ioctl.h>
/*** ioctl用パラメータ(第3引数)の定義 ***/
struct mydevice_values {
int val1;
int val2;
};
/*** ioctl用コマンド(request, 第2引数)の定義 ***/
/* このデバイスドライバで使用するIOCTL用コマンドのタイプ。なんでもいいが、'M'にしてみる */
#define MYDEVICE_IOC_TYPE 'M'
/* デバドラに値を設定するコマンド。パラメータはmydevice_values型 */
#define MYDEVICE_SET_VALUES _IOW(MYDEVICE_IOC_TYPE, 1, struct mydevice_values)
/* デバドラから値を取得するコマンド。パラメータはmydevice_values型 */
#define MYDEVICE_GET_VALUES _IOR(MYDEVICE_IOC_TYPE, 2, struct mydevice_values)
#endif /* MY_DEVICE_DRIVER_H_ */
とりあえず今回は、値を2つ(val1とval2)を読み書きするコマンドを作ります。特にこれに意味はありません。コマンド(リクエスト)名は、MYDEVICE_SET_VALUES
とMYDEVICE_GET_VALUES
にします。このコマンドは数字直打ちにしてもロジック上は動きます。例えば、以下のようにしても大丈夫です。
#define MYDEVICE_SET_VALUES 3 // 数字に特に意味はない。このドライバ内でユニークなら何でもOK
#define MYDEVICE_GET_VALUES 4 // 数字に特に意味はない。このドライバ内でユニークなら何でもOK
実際は、「パラメータが、ReadなのかWriteなのか、Read/Writeなのか」、「種別」、「ユニーク番号」、「パラメータサイズ」といった情報から、_IO
、_IOW
、_IOR
、_IOWR
マクロを使用して生成するのが一般的なようです。今回は、デバイスドライバ名が"myDeviceDriver"なので、種別は'M'としてみました。
パラメータの型として、値を2つ持つ構造体struct mydevice_values
も定義しておきます。
デバイスドライバの実装
デバイスドライバ側のコードで必要になるのは、ioctlが呼ばれたときに使うハンドラ関数と、struct file_operations
テーブルへの登録だけです。関係するところだけ抜粋したコードが下記になります。
#include "myDeviceDriver.h"
/* ioctlテスト用に値を保持する変数 */
static struct mydevice_values stored_values;
/* ioctl時に呼ばれる関数 */
static long mydevice_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
printk("mydevice_ioctl\n");
switch (cmd) {
case MYDEVICE_SET_VALUES:
printk("MYDEVICE_SET_VALUES\n");
if (copy_from_user(&stored_values, (void __user *)arg, sizeof(stored_values))) {
return -EFAULT;
}
break;
case MYDEVICE_GET_VALUES:
printk("MYDEVICE_GET_VALUES\n");
if (copy_to_user((void __user *)arg, &stored_values, sizeof(stored_values))) {
return -EFAULT;
}
break;
default:
printk(KERN_WARNING "unsupported command %d\n", cmd);
return -EFAULT;
}
return 0;
}
/* 各種システムコールに対応するハンドラテーブル */
static struct file_operations mydevice_fops = {
.open = mydevice_open,
.release = mydevice_close,
.read = mydevice_read,
.write = mydevice_write,
.unlocked_ioctl = mydevice_ioctl,
.compat_ioctl = mydevice_ioctl, // for 32-bit App
};
コマンドとパラメータはヘッダファイルで定義しているので、インクルードしています。また、今回はお試しで値をSET, GETするというコマンドを作ったので、保持するためのstatic変数を用意しました。本当の実装では、file構造体内のprivate_dataを使うなどして、ちゃんと管理してください。
mydevice_ioctl
関数がioctlで呼ばれる処理です。見てわかるように、コマンド(リクエスト)に対してのswitch-case文があるだけです。パラメータへのポインタはargに入っているので、適当にキャストして読み書きします。これはread,writeと同じです。最後に、struct file_operations
テーブルに登録します。ここでは記載していませんが、このテーブルがカーネルロード時にcdev_init, cdev_add
で登録されます。
ノート
本記事の内容は、「Linuxデバイスドライバプログラミング (平田 豊)」の内容に沿っています。
ioctlハンドラの登録のために、struct file_operations
に関数を登録します。本では、.ioctl
メンバを使用して登録していました。しかし、現在では廃止されています。代わりに、.unlocked_ioctl
と.compat_ioctl
を使用します。2つある理由は、64-bit環境における32-bitアプリケーション対応のためのようです。詳細はこちらで説明されていました。
本だと、32-bit/64-bit環境の両対応のために、パディングを入れるなどの対応方法も紹介されていました。おそらく、こういったことをしないで済むように.compat_ioctl
が追加されたのだと思います。たぶん。
ユーザプログラムからioctlを呼んでみる
以下のようなテストプログラムで、実装したデバイスドライバのioctlを呼んでみます。
MYDEVICE_SET_VALUESで値({1, 2})を設定します。その後、MYDEVICE_GET_VALUESで値を取得してみます。
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <sys/ioctl.h>
#include "myDeviceDriver.h"
int main()
{
int fd;
struct mydevice_values values_set;
struct mydevice_values values_get;
values_set.val1 = 1;
values_set.val2 = 2;
if ((fd = open("/dev/mydevice0", O_RDWR)) < 0) perror("open");
if (ioctl(fd, MYDEVICE_SET_VALUES, &values_set) < 0) perror("ioctl_set");
if (ioctl(fd, MYDEVICE_GET_VALUES, &values_get) < 0) perror("ioctl_get");
printf("val1 = %d, val2 = %d\n", values_get.val1, values_get.val2);
if (close(fd) != 0) perror("close");
return 0;
}
実行する
まず、デバイスドライバのビルドとロードをします。その後、テストプログラムのビルドと実行をします。
make
sudo insmod MyDeviceModule.ko
gcc test.c
./a.out
val1 = 1, val2 = 2
すると、このように、設定した値が読めていることが分かります。
おわりに
デバイスドライバを自分で書かないユーザの立場であっても、何らかのデバイスを触るときにはioctlは必ず必要になります。いつもごちゃごちゃしていてよくわからなかったのですが、自分で実装してみることで、よく理解できました。