miscdeviceでLinuxデバイスドライバを作成する

本書ではローダブルモジュール形式の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);
                }
        }
ダウンロード
ドライバのコード
miscdeviceを使用して作成したLinuxデバイスドライバのコード
fop.tgz.tar.gz
GNU tar 1.7 KB