NetBSD Advent Calendar 2018 11日目の記事です。
3日目の記事でNetBSDのカーネルモジュールを紹介していました。今日はそのサンプルをもうちょっとだけ拡張し、 ioctl()
できるようなサンプルを作ってみようと思います。
ioctl(2)って何?
ioctl(2)とはデバイスのパラメータを操作するための機能です。システムコールのシグネチャは以下のようになっています。
#include <sys/ioctl.h>
int
ioctl(int d, unsigned long request, ...);
第1引数のファイルディスクリプタに対し、第2引数でリクエスト内容を指定します。必要なデータがあれば、第3引数以降に指定します。
第2引数のリクエストは、入出力の方向やサイズ、リクエスト種別等をエンコードした値になっています。
使用例としては以下のような形になります。
/usr/src/sys/tests/kernel/t_pty.c:
108 static void
109 condition(int fd)
110 {
...
115 if (ioctl(fd, TIOCSQSIZE, &opt) == -1)
116 err(EXIT_FAILURE, "Couldn't set tty(4) buffer size");
カーネルモジュールにioctl(の受け口)を実装する
3日目の記事で紹介したカーネルモジュールのサンプルは、モジュールのロードとアンロード、そしてread(2)システムコールの例が実装されていました。
該当のコード( /usr/src/sys/modules/examples/readhappy/readhappy.c
)を見ると、以下のような箇所があります。どうやら struct cdevsw.d_ioctl
に ioctl()
の受け口となる関数を指定すれば良さそうです。
/usr/src/sys/modules/examples/readhappy/readhappy.c:
100 static struct cdevsw happy_cdevsw = {
101 .d_open = happy_open,
102 .d_close = happy_close,
103 .d_read = happy_read,
104 .d_write = nowrite,
105 .d_ioctl = noioctl,
106 .d_stop = nostop,
107 .d_tty = notty,
108 .d_poll = nopoll,
109 .d_mmap = nommap,
110 .d_kqfilter = nokqfilter,
111 .d_discard = nodiscard,
112 .d_flag = D_OTHER
113 };
ちなみに、 struct cdevsw
の定義を見ると以下のようになっています。 open()
や close()
への関数ポインタ群をまとめた構造体になっています。
こうやってひも解いて見てみると、ユーザランド側のプログラミングと似通った箇所もあり、NetBSDカーネルとはいえ、思いのほか読み解きやすくも感じられます。
/usr/src/sys/modules/examples/readhappy/sys/conf.h:
82 /*
83 * Character device switch table
84 */
85 struct cdevsw {
86 int (*d_open)(dev_t, int, int, struct lwp *);
87 int (*d_close)(dev_t, int, int, struct lwp *);
88 int (*d_read)(dev_t, struct uio *, int);
89 int (*d_write)(dev_t, struct uio *, int);
90 int (*d_ioctl)(dev_t, u_long, void *, int, struct lwp *);
91 void (*d_stop)(struct tty *, int);
92 struct tty * (*d_tty)(dev_t);
93 int (*d_poll)(dev_t, int, struct lwp *);
94 paddr_t (*d_mmap)(dev_t, off_t, int);
95 int (*d_kqfilter)(dev_t, struct knote *);
96 int (*d_discard)(dev_t, off_t, off_t);
97 int d_flag;
98 };
とりあえず、 struct cdevsw.d_ioctl
に関数ポインタを指定すれば良さそうだという所まで見えてきました。さっそくカーネルモジュールサンプルを流用して ioctl()
を実装してみます。
カーネルモジュールでのioctl実装
まずは雛型を用意する
前回のカーネルモジュールの話で使用した readhappy.c
をもとに ioctl()
を追加してみます。
ディレクトリごと新しいサンプルファイルとして準備します。
# cd /usr/src/sys/modules/examples/
# cp -r readhappy ioctl_sample
# cd ioctl_sample/
# ls -F
CVS/ Makefile readhappy.c
ioctl()の使われ方をソースコードから参照する
いきなり ioctl()
を実装するにしても、どうすれば良いものか分かりません。まずはカーネルソースツリーの中で ioctl()
を使用している箇所を探し、その実装方法を参考にしようと思います。
ちょうど今回のサンプルに応用できそうな例が dev/i2c/i2c.c
にありました。
dev/i2c/i2c.c
85 const struct cdevsw iic_cdevsw = {
...
90 .d_ioctl = iic_ioctl,
...
98 };
...
579 static int
580 iic_ioctl(dev_t dev, u_long cmd, void *data, int flag, lwp_t *l)
581 {
582 struct iic_softc *sc = device_lookup_private(&iic_cd, minor(dev));
583
584 if (sc == NULL)
585 return ENXIO;
586
587 switch (cmd) {
588 case I2C_IOCTL_EXEC:
589 return iic_ioctl_exec(sc, (i2c_ioctl_exec_t *)data, flag);
590 default:
591 return ENODEV;
592 }
593 }
上記の部分だけでも、以下のような内容が分かります。
- 引数
cmd
で指定された値に応じて処理を分けるようだ。- というか、
ioctl()
の第2引数の値がそのまま引数cmd
に渡ってくるようだ。
- というか、
- データは
void *data
として引数で渡されてくるようだ。 他の引数はいったん見なかったことにする
ioctlのリクエスト値を用意する
というわけで、引数 cmd
で処理を分けるための値( ioctl()
の第2引数)を定義します。
とりあえずこんな感じにしてみます。
(よく考えたら引数 cmd
の値に相当する定数って慣習的な命名規則があった気がするけど、参考にした dev/i2c/i2c.c
もそうなってない気がする...)
#include <sys/ioccom.h>
typedef struct happy_ioctl_exec {
int dummy;
} happy_ioctl_exec_t;
#define HAPPY_IOCTL_STRCPY _IOW('I', 0, happy_ioctl_exec_t)
ioctlに対応する関数を用意する
ioctl()
に対応する関数も実装します。今回の例では、渡されてきたデータをカーネルモジュール内のバッファに文字列として格納しています。
static char happy_str[32] = { '\0' };
dev_type_ioctl(happy_ioctl);
static struct cdevsw happy_cdevsw = {
...
.d_ioctl = happy_ioctl,
...
};
...
int
happy_ioctl(dev_t dev, u_long cmd, void *data, int flag, lwp_t *l)
{
if (data == NULL) {
return EINVAL;
}
switch (cmd) {
case HAPPY_IOCTL_STRCPY:
printf("-=> HAPPY_IOCTL_STRCPY\n");
memcpy(happy_str, data, 31);
happy_str[31] = '\0';
default:
return ENODEV;
}
return 0;
}
そして、 read()
に対応する関数も実装します。ユーザランド側で ioctl()
で渡したデータが、 read()
で読み取れるという挙動になるはずです。
int
happy_read(dev_t self __unused, struct uio *uio, int flags __unused)
{
int e;
if ((e = uiomove(happy_str, strlen(happy_str), uio)))
return e;
return 0;
}
ビルドしてみます。これでカーネルモジュールができあがりました。
# make
# compile ioctl_sample/ioctl_sample.o
/usr/src/tooldir.NetBSD-8.0-amd64/bin/x86_64--netbsd-gcc -O2 -g -std=gnu99 -Wall -Wstrict-prototypes -Wmissing-prototypes -Wpointer-arith -Wno-sign-compare -Wsystem-headers -Wno-traditional -Wa,--fatal-warnings -Wreturn-type -Wswitch -Wshadow -Wcast-qual -Wwrite-strings -Wextra -Wno-unused-parameter -Wno-sign-compare -Werror -ffreestanding -fno-strict-aliasing -Wno-pointer-sign -mno-red-zone -mno-mmx -mno-sse -mno-avx -msoft-float -mcmodel=kernel -fno-omit-frame-pointer -I/usr/src/common/include --sysroot=/ -I/usr/src/common/include -nostdinc -I. -I/usr/src/sys/modules/examples/ioctl_sample -isystem /usr/src/sys -isystem /usr/src/sys/arch -isystem /usr/src/sys/../common/include -D_KERNEL -D_LKM -D_MODULE -DSYSCTL_INCLUDE_DESCR -c ioctl_sample.c
/usr/src/tooldir.NetBSD-8.0-amd64/bin/nbctfconvert -L VERSION ioctl_sample.o
# link ioctl_sample/ioctl_sample.kmod
/usr/src/tooldir.NetBSD-8.0-amd64/bin/x86_64--netbsd-gcc --sysroot=/ -Wl,-z,relro -Wl,--warn-shared-textrel -nostdlib -r -Wl,-T,/usr/src/sys/../sys/modules/xldscripts/kmodule,-d -Wl,-Map=ioctl_sample.kmod.map -o ioctl_sample.kmod ioctl_sample.o
/usr/src/tooldir.NetBSD-8.0-amd64/bin/nbctfmerge -t -L VERSION -o ioctl_sample.kmod ioctl_sample.o
ユーザランド側から試してみる
カーネルモジュールのロード
カーネルモジュールをロードしてみます。特に問題なくロードできました。
併せて忘れずに mknod
を実行しておきます。
サンプルプログラム
次に以下のようなサンプルプログラムを用意します。単に /dev/happy
を open()
し、今回実装したカーネルモジュール内の関数が呼ばれるよう、 ioctl()
と read()
を実行しているだけです。
#include <sys/ioctl.h>
#include <sys/ioccom.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <unistd.h>
typedef struct happy_ioctl_exec {
int dummy;
} happy_ioctl_exec_t;
#define HAPPY_IOCTL_STRCPY _IOW('I', 0, happy_ioctl_exec_t)
int main(int argc, char *argv[])
{
char buf[BUFSIZ];
int fd;
fd = open("/dev/happy", O_RDONLY);
if (fd == -1) {
fprintf(stderr, "open() failed.\n");
exit(-1);
}
ioctl(fd, HAPPY_IOCTL_STRCPY, "(^_^)");
read(fd, buf, BUFSIZ);
printf("%s", buf);
close(fd);
return 0;
}
コンパイルして試してみる
コンパイルして実行してみると、ユーザランド側から ioctl()
で指定した文字列が read()
で読み出せています。全部読めてないのが気になりますが...
まとめ
NetBSDのカーネルモジュールサンプルをもとにして、 ioctl()
を処理できるようカーネルモジュール側に実装を追加する手順を紹介しました。カーネルモジュールのload/unloadやread/write/ioctlは、最小限のサンプルプログラムの形で用意しておくと、ちょっとした実験等を行いたい際に役立ちそうです。