shimada-kの日記

ソフトウェア・エンジニアのブログです

マイナー番号で複数のデバイスファイルを管理するコードのメモ

今までデバイスドライバを書く時はマイナー番号は0にしていました。なので「マイナー番号はメジャー番号の1つ下の階層の番号」ぐらいの理解でした。
#複数のデバイスファイルを扱う要件がなかったので

しかし、1つのドライバコードで複数のデバイスファイルを扱う要件が出たので、マイナー番号を使ってどのデバイスファイルにアクセスされているのかドライバ側で区別するコードを書いてみましたので、その時のメモです。

シンプルなキャラクタデバイスを作成するカーネルモジュールです。

環境:Linux debian 3.0.0-1-amd64 [debian wheezy]

minor_test.c

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/proc_fs.h>	/* alloc_chrdev_region */
#include <linux/cdev.h>		/* cdev_init */
#include <asm/uaccess.h>	/* copy_from_user, copy_to_user */

#define MODNAME "minor_test"

#define MINOR_COUNT 4		/* 接続するマイナー番号数 */
#define MAX_BUFLEN 64		/* バッファサイズ */

static dev_t dev_id;		/* デバイス番号 */
static struct cdev c_dev;	/* キャラクタデバイス構造体 */

/* 内部バッファの構造体 */
struct cdev_buffer{
	int buflen;
	unsigned char buf[MAX_BUFLEN];
};

static struct cdev_buffer cbuf[MINOR_COUNT]; 

/* open(2) */
static int cdev_open(struct inode *inode, struct file *filp) 
{
	printk(KERN_INFO "cdev_open[minor#%d]\n", MINOR(inode->i_rdev));

	filp->private_data = &inode->i_rdev;	/* マイナー番号を参照するためにアドレスをメモ */

	return 0;
}

/* close(2) */
static int cdev_release(struct inode *inode, struct file *filp)
{
	printk(KERN_INFO "cdev_release\n");
	return 0;
}

/* read(2) */
static ssize_t cdev_read(struct file *filp, char *buf, size_t count, loff_t *offset)
{
	int len, index;
	dev_t minor_no = *((int *)filp->private_data);

	index = MINOR(minor_no);

	if((len = cbuf[index].buflen) == 0){
		/* 何も記録されてなかたらreturn */
		return 0;
	}

	printk(KERN_INFO "/dev/minor%d is read\n", index);

	if(copy_to_user(buf, cbuf[index].buf, len)){
		printk(KERN_WARNING "copy_to_user failed\n");
		return -EFAULT;
	}

	*offset += len;

	/* 読み出したのでクリア */
	cbuf[index].buflen = 0;

	return len;
}

/* write(2) */
static ssize_t cdev_write(struct file *filp, const char *buf, size_t count, loff_t *offset)
{
	int index;
	dev_t minor_no = *((int *)filp->private_data);

	index = MINOR(minor_no);

	printk(KERN_INFO "/dev/minor%d is written\n", index);

	if(count >= MAX_BUFLEN){
		return -EFAULT;
	}

	if(copy_from_user(cbuf[index].buf, buf, count)){
		printk(KERN_WARNING "copy_from_user failed\n");
		return -EFAULT;
	}

	cbuf[index].buf[count] = '\0';

	*offset += count;

	cbuf[index].buflen = count;

	return count;
}

static struct file_operations minor_test_fops ={
	.owner			= THIS_MODULE,
	.open			= cdev_open,
	.release		= cdev_release,
	.read			= cdev_read,
	.write			= cdev_write,
	.unlocked_ioctl	= NULL,
};

/* モジュール初期化関数 */
static int __init minor_test_module_init(void)
{
	int ret;

	/* キャラクタデバイス番号の動的取得 */
	ret = alloc_chrdev_region(&dev_id, 0, MINOR_COUNT, MODNAME);

	if(ret < 0){
		printk(KERN_WARNING "alloc_chrdev_region failed\n");
		return ret;
	}

	/* キャラクタデバイス初期化 */
	cdev_init(&c_dev, &minor_test_fops);
	c_dev.owner = THIS_MODULE;

	/* キャラクタデバイスの登録 */
	ret = cdev_add(&c_dev, dev_id, MINOR_COUNT);

	if(ret < 0){
		printk(KERN_WARNING "cdev_add failed\n");
		return ret;
	}

	return 0;
}

/* モジュール解放関数 */
static void __exit minor_test_module_exit(void)
{
	/* キャラクタデバイス削除 */
	cdev_del(&c_dev);

	/* デバイス番号の解放 */
	unregister_chrdev_region(dev_id, MINOR_COUNT);
}


module_init(minor_test_module_init);
module_exit(minor_test_module_exit);

MODULE_DESCRIPTION(MODNAME);
MODULE_LICENSE("GPL2");

モジュールをコンパイルした後はmknod(1)でデバイスファイルを作成する必要があります。今回マイナー番号は4つ作成しますのでスクリプトを用意してrootで実行します

mknod.sh

#!/bin/sh

MINOR_COUNT=3
MAJOR=$(awk "\$2==\"minor_test\" {print \$1}" /proc/devices)

echo "$MAJOR"

rm -f /dev/minor?

for i in `seq 0 1 "$MINOR_COUNT"`
do
    mknod /dev/minor"$i" c "$MAJOR" "$i"
done

実行すると/dev以下にminor0~minor3まで4つのデバイスファイルができます。

f:id:shimada-k:20111106030114j:image

マイナー番号はcdev_open()の第1引数のinode構造体のi_rdevメンバに格納されています(ただ、dev_t型なのでMINORマクロで整数型に変換する必要があります)。なのでcdev_open()の中でグローバル変数にマイナー番号を記録すれば、マイナー番号ごとに固有のメモリ空間を持てそうです。

しかし、この方法だと

  1. open(2)
  2. read(2) or write(2)
  3. close(2)

の1連の処理がデバイスごとに一ヶ所にまとまっている。という前提がいります。デバイスファイルが1つだけだったり、単発のデータしかやりとりしないなどの場合は問題にならないかもしれません。

例えば複数のデバイスファイルが存在して

  1. 最初に全てopen(2)
  2. read(2) or write(2)の繰り返し
  3. 最後に全てclose(2)

というロジックには非対応。ということです。

このようなロジックに対応させるためには、cdev_open()だけでなく、cdev_read()/cdev_write()の中でも「現在どのデバイスファイルに対する入出力なのか」を区別する必要があります。

そこで、file構造体のprivate_dataというメンバを使いました。このメンバは「ファイルシステムやデバイスファイルで自由に使っていいよ」というメンバ変数です(たぶん)。*1

966         /* needed for tty driver, and maybe others */
967         void                    *private_data;

cdev_open()時にinode->i_rdevのアドレスをfilp->private_dataにコピーすることでcdev_read()/cdev_write()でもマイナー番号を参照できるようにしています。

デバイスファイルにcat(1)/echo(1)を実行してデバイスファイルごとに固有のメモリ空間を参照できているか確認です。

f:id:shimada-k:20111106033601j:image

できているようです。これでコード1つでデバイスファイルを別にすることで、システム空間←→ユーザ空間で関連あるデータをやりとりできますので、スマートにデバイスドライバが書けそうです。おそらく既存のデバイスドライバも同じような手法で複数のデバイスファイルを管理していると思われます。

*1:file構造体はカーネルツリーのinclude/linux/fs.hで定義されています。ちなみにinode構造体にもi_privateという(void *)型のメンバ変数があります。