本書ではローダブルモジュール形式のLinuxデバイスドライバの作成方法につ いて記載する。ローダブルモジュール形式にすることで、以下の要望を満たす ことができる。
(1) 複数のカーネルに対応したい(カーネルのコードとは独立したい) (2) GPLライセンスを回避したい
(1)について、カーネルの版数が異なったり、カーネルコンフィグレーション が異なったり、適用しているパッチが異なる環境においてもローダブルモジュー ル形式のLinuxデバイスドライバをコンパイルしてロードすることができる。 ただし、カーネルのAPI仕様の差異に留意する必要がある。
(2)について、ローダブルモジュール形式のLinuxデバイスドライバは必ずしも GPLである必要がない(グレーゾーンではあるが)。ただし、カーネル側では ライセンスのチェックをローダブルモジュールのロード時にチェックする機構 があり、GPLでないローダブルモジュールには公開していないAPIが存在する (EXPORT_SYMBOL_GPLで公開されているAPI)。よって、GPLでないローダブル モジュールでも使用可能なAPIで作成する必要がある。
1. 本ドライバの機能
本ドライバの機能仕様は以下の通り。
* カーネル空間のvmalloc領域にデータを保存する * inode単位に領域を分割 * device nodeに対してread/writeでデータの読み書き * O_APPENDで末尾にデータ追加 * miscdeviceで作成 * ローダブルモジュール形式
2. Makefile
通常のMakefileと異なるのはカーネルのソースディレクトリ(.config含む) を参照する点である。
#EXTRA_CFLAGS="-fsyntax-only -Wno-unused-variable -Wno-unused-value" obj-m := fop.o ifndef KERNEL_DIR KERNEL_DIR=/usr/src/linux-headers-`uname -r`/ endif all: ${MAKE} -C ${KERNEL_DIR} M=`pwd` clean: ${MAKE} -C ${KERNEL_DIR} M=`pwd` clean install: cp fop.ko ${TARGET_DIR}
2.1. obj-m
カーネル内部でローダブルモジュール形式のコードを記憶する変数である。 obj-yがカーネル組み込み形式のコードを記憶する変数である。obj-yのコード はカーネルイメージに組み込まれ、obj-mのコードは*.koというオブジェクト となる。なお、以下の記述で複数のコードから成るローダブルモジュールを生 成できる。
obj-m := objname.o objname-y := one.o two.o three.o
2.2. makeの変数M
makeの変数Mにローダブルモジュールのディレクトリを指定する。make -Cオプ ションでカーネルディレクトリを指定せず、カーネルディレクトリに移動して モジュールを作成することも可能である。
$ cd <path-to-linux> $ make M=<path-to-module>
2.3. EXTRA_CFLAGS
CFLAGSはカーネルコンフィグレーションで決定されており、ローダブルモジュー ル側で上書きするのは得策ではない(インクルードパスやコンパイルオプショ ンを全て把握する必要がある)。そこでカーネル内部ではEXTRA_CFLAGSという 変数をローダブルモジュール独自のオプションとして参照している。CFLAGSに 加えたいオプションはEXTRA_CFLAGSに設定すれば良い。
3. モジュールのコード
幾つかのポイントを記載する。
3.1. 初期化処理
MISC_DYNAMIC_MINORを使用すると、device nodeのマイナー番号を動的に設定 してくれるので、miscdeviceを使用。ただし、private_dataの扱いについては 注意した方が良いかもしれない。
static struct file_operations fop_fops = { .owner = THIS_MODULE, .read = fop_read, .write = fop_write, .open = fop_open, .release = fop_close, }; static struct miscdevice fop_dev = { .minor = MISC_DYNAMIC_MINOR, .name = "fop", .fops = &fop_fops, }; static __init int fop_init(void) { return misc_register(&fop_dev); }
3.2. open
inodeに独自定義のバッファ(vmalloc領域)を結びつける。inodeに結びつい たバッファを各ファイルディスクリプタに設定する。inodeはファイル毎に固 有であるため、同じファイルをopenした各プロセスでバッファを共有する。
static int fop_open(struct inode *inode, struct file *file) { struct fop_buffer *buffer; /** Device node is deleted and then new device node which has same path is created and new buffer will be allocated. */ buffer = inode_buffer(inode); if (!buffer) { buffer = fop_alloc_buffer(inode, FOP_BUFFER_SIZE); if (!buffer) return -ENOMEM; } buffer_file(file, buffer); return 0; }
3.3. write
startという変数で書き込み位置の計算。*posはopen後に複数回writeを呼ぶと 更新されていく。O_APPENDのフラグが設定されている場合はデータ末尾から書 き込むようにする。unlikelyマクロは条件が偽の場合にジャンプ命令を発行し ないようにするマクロ(ジャンプ命令はパイプラインがリセットされる等の性 能劣化に繋がる)。copy_from_userはコピー先のアドレスがユーザ空間でない (例えば0xC000000より上のカーネル空間のアドレス)である場合に0でない値 を返す。
static ssize_t fop_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) { struct fop_buffer *buffer = file_buffer(file); ssize_t size, start; mutex_lock(&buffer->mutex); if (*pos != 0) start = *pos; /** open -> write -> write ... case */ else if (file->f_flags & O_APPEND) start = buffer->data_size; /** open(O_APPEND) -> write case */ else start = 0; /** open(not O_APPEND) -> write case */ if (start + count >= buffer->alloc_size) size = buffer->alloc_size - start; else size = count; if (unlikely(copy_from_user(buffer->data + start, buf, size))) { mutex_unlock(&buffer->mutex); return -EFAULT; } if (start != 0) buffer->data_size += size; else buffer->data_size = size; mutex_unlock(&buffer->mutex); *pos += size; return size; }
3.4. バッファの解放について
device nodeが削除された場合にバッファも解放したいのだが、ファイルシス テムとしてドライバを実装しないと検知できない(少なくともフレームワーク として用意されていない)。よってバッファはリストに繋げておき、ローダブ ルモジュールのアンロード時に解放する。
4. 実行例
# insmod fop.ko # grep misc /proc/devices 10 misc # grep fop /proc/misc 57 fop # mknod fop c 10 57 # echo "hello" > fop # echo "world" >> fop # cat fop hello world
シェルスクリプトとしても実行可能(readするだけだから当たり前だが)。
# cat fop #!/bin/sh i=0 while [ ${i} -lt 10 ] ; do echo ${i} i=`expr ${i} + 1` done # sh fop 0 1 2 3 4 5 6 7 8 9
5. デバッグ方法
ここではQEMUに接続したGDBでのデバッグ方法を記載する。 insmodでロードされたアドレスを/proc/modulesで調べる。
# insmod fop.ko # cat /proc/modules fop 1488 0 - Live 0xc8828000
GDBのadd-symbol-fileコマンドでロードされたアドレスとオブジェクトをマッ ピングする。
(gdb) add-symbol-file ../../tp/drv/fop.ko 0xc8828000 (gdb) b fop_open
# cat <device node>を実行したところ、fop_openでブレークした。その際の btコマンドの実行結果は以下の通り。
#1 0xc11cecc0 in misc_open (inode=0xc748c3b8, file=0xc7b08c00) at drivers/char/misc.c:148 #2 0xc10a8f35 in chrdev_open (inode=0xc748c3b8, filp=0xc7b08c00) at fs/char_dev.c:405 #3 0xc10a50f7 in __dentry_open (dentry=0xc7486aa0, mnt=0xc7a0ba80, f=0xc7b08c00, open=<optimized out>,cred=0xc7b08700) at fs/open.c:687 #4 0xc10a5e6a in nameidata_to_filp (nd=<optimized out>) at fs/open.c:790 #5 0xc10af0b2 in finish_open (acc_mode=36, open_flag=32768, nd=0xc7b0bef8) at fs/namei.c:1579 #6 do_last (nd=0xc7b0bef8, path=<optimized out>, open_flag=32768, acc_mode=36, mode=0, pathname=0xc780f000 "file") at fs/namei.c:1742 #7 0xc10af33c in do_filp_open (dfd=-100, pathname=0xc780f000 "file", open_flag=<optimized out>, mode=0, acc_mode=36) at fs/namei.c:1836 #8 0xc10a5ec6 in do_sys_open (dfd=-100, filename=<optimized out>, flags=32768, mode=0) at fs/open.c:886 #9 0xc10a5f7b in sys_open (filename=0xbf99aec4 "file", flags=32768, mode=0) at fs/open.c:907 #10 <signal handler called> #11 0xb7865cf2 in ?? () Cannot access memory at address 0x8040049
6. miscdeviceのfile->private_dataについて
private_dataについてgoogle検索すると各ドライバで自由に使用して良いとい う解釈が多い。
しかし、misc_open関数内の以下の箇所にてmiscdevice構造体へのポインタを 設定している。他の箇所で参照していないようなので、独自データのアドレス を設定しても良さそうではあるが・・・。おとなしくchar_deviceを使った方 がいいのかもしれない。
static int misc_open(struct inode * inode, struct file * file) { <snip> struct miscdevice *c; <snip> if (file->f_op->open) { file->private_data = c; /** Setting miscdevice to private_data */ err=file->f_op->open(inode,file); if (err) { fops_put(file->f_op); file->f_op = fops_get(old_fops); } }