本書ではローダブルモジュール形式の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);
}
}