シンプルブロックデバイスドライバ
ブロックファイルの読み書きは、ファイルのinodeと参照するファイルのオフセット及びパーティション位置から、対応するブロックをデバイスの絶対位置として算出します。カーネル(vfs)はブロック単位で管理して、ブロックは2のべき乗のセクタを有しています。(物理デバイスはセクタ管理で、セクタ数の倍数というのは、無駄にセクタを使わないと言う事と、ビット処理での効率な実装のためゆえの2のべき乗と言うことです。たぶん)
参照ブロックはbioに設定され、submit_io()関数をコールすることで、ブロックデバイス下のDevice.gd->queue->make_request_fnのエレベータと称するコールバック関数が、デバイスのrequest_queにリストされているrequestにリストされます。必要とあれば新規にrequestを作成したり、request間をマージしたりいたします。従ってbioは2重のリスト構造での管理となります。
実デバイスとのやりとりは、カーネル管理下で適時必要とされるタイミングで、Device.gd->queue->request_fnコールバック関数がコールされ、かかるrequest_queueのrequestのbioと実デバイスとのやりとりを行います。下記サンプルは、O'REILLYのLinux Device Driversのシンプルブロックデバイスドライバサンプルです。
sbd_request()で request_queueのrequestのbio単位で、デバイスバッファとやり取りします。req->cmd_type != REQ_TYPE_FSは、requestがファイル参照のリクエストかどうかと言うことです。requestにはデバイスの制御コマンドとしてのコード、シャットダウンコマンド等の他のリクエストも有しているからです。
サンプルでは未処理です。この時__blk_end_request_all()をコールすることで、そのrequestを削除し、かかるセクタ数を更新し、request_queueの先頭に次のrequestが接続されます。故に、関数内の変数reqは、次にrequestということで、改めてblk_fetch_request()をコールすることなく、次にbioを取得できるわけです。
sbd_transfer()では、実デバイスとやり取りをブロック単位で行います。blk_rq_pos(req)はrequestの先頭セクタ位置、blk_rq_cur_sectors()はrequestの先頭のbioのセクタ数、rq_data_dir()はread/writeフラグです。
やり取りが終了すると、__blk_end_request_cur()をコールします。これはrequest先頭(読み書きした)のbioのみを削除します。この時requestの先頭セクタ位置/セクタ数等も更新されます。返り値がfalseの場合、requestにはこれ以上のbioはありません。従って次のrequestのbioがデバイスが読み書きとなり、blk_fetch_request()で改めてrequestを取得することになります。
sbd_transfer()は、dev->dataのセクタ位置をオフセットとして、req->bufferと読み書きします。dev->dataは本サンプルに特化したもので、実際はデバイス依存のデバイスとのやり取りとなります。
サンプルのように、リアルタイム性を向上させるため、request単位でなくbio単位で実デバイスとやり取りするのが一般的のようですが、サンプルのようなメモリとしてのデバイスなら、むしろrequest単位でやり取りした方がいいかと思います。sbd_transfer()の引数を、requestのセクタ数とし、__blk_end_request_cur()でなく__blk_end_request_all()でrequestを削除すればいいはずです。(未検証)
デバイスコールバックの.getgeoは、ioctrlの HDIO_GETGEOでコールすることで、blkdev_ioctl()と、そして disk->fops->getgeo()とすることで、シリンダ数/ヘッド数/セクタ数/先頭位置のstruct hd_geometryが取得でき、fdisk等で無地なデバイスを設定する際のデバイス情報が取得できるように成っています。サンプルはfdiskでパーティションを作ることも可能です。
ここで、シリンダというのが円盤の数と思ってしまいますが、シリンダ数はトラック数で、円盤毎に読み込みヘッドがあるということで、ヘッド数が円盤数となるわけです。(たぶん)
で、このようなハード構造を持たないデバイスにも、シリンダ/ヘッド/セクタを割り当てる必要があります。要はシリンダ×ヘッド×セクタが全セクタ数になればいいわけで、(size & ~0x3f) >> 6で、上位6ビットをシリンダ数、ヘッドを4、セクタを16とします。4×16=2^6ですから、size & ~0x3fが全セクタ数となるわけです。なお、この実装ではsize & 0x3fのセクタ数は無視されることになります。
参照ブロックはbioに設定され、submit_io()関数をコールすることで、ブロックデバイス下のDevice.gd->queue->make_request_fnのエレベータと称するコールバック関数が、デバイスのrequest_queにリストされているrequestにリストされます。必要とあれば新規にrequestを作成したり、request間をマージしたりいたします。従ってbioは2重のリスト構造での管理となります。
実デバイスとのやりとりは、カーネル管理下で適時必要とされるタイミングで、Device.gd->queue->request_fnコールバック関数がコールされ、かかるrequest_queueのrequestのbioと実デバイスとのやりとりを行います。下記サンプルは、O'REILLYのLinux Device Driversのシンプルブロックデバイスドライバサンプルです。
#include <linux/module.h> #include <linux/moduleparam.h> #include <linux/init.h> #include <linux/kernel.h> /* printk() */ #include <linux/fs.h> /* everything... */ #include <linux/errno.h> /* error codes */ #include <linux/types.h> /* size_t */ #include <linux/vmalloc.h> #include <linux/genhd.h> #include <linux/blkdev.h> #include <linux/hdreg.h> MODULE_LICENSE("Dual BSD/GPL"); static char *Version = "1.4"; static int major_num = 0; module_param(major_num, int, 0); static int logical_block_size = 512; module_param(logical_block_size, int, 0); static int nsectors = 1024; /* How big the drive is */ module_param(nsectors, int, 0); #define KERNEL_SECTOR_SIZE 512 static struct request_queue *Queue; static struct sbd_device { unsigned long size; spinlock_t lock; u8 *data; struct gendisk *gd; } Device; static void sbd_transfer(struct sbd_device *dev, sector_t sector, unsigned long nsect, char *buffer, int write) { unsigned long offset = sector * logical_block_size; unsigned long nbytes = nsect * logical_block_size; if ((offset + nbytes) > dev->size) { printk (KERN_NOTICE "sbd: Beyond-end write (%ld %ld)\n", offset, nbytes); return; } if (write) memcpy(dev->data + offset, buffer, nbytes); else memcpy(buffer, dev->data + offset, nbytes); } static void sbd_request(struct request_queue *q) { struct request *req; req = blk_fetch_request(q); while (req != NULL) { if (req == NULL || (req->cmd_type != REQ_TYPE_FS)) { printk (KERN_NOTICE "Skip non-CMD request\n"); __blk_end_request_all(req, -EIO); continue; } sbd_transfer(&Device, blk_rq_pos(req), blk_rq_cur_sectors(req), req->buffer, rq_data_dir(req)); if ( ! __blk_end_request_cur(req, 0) ) { req = blk_fetch_request(q); } } } int sbd_getgeo(struct block_device * block_device, struct hd_geometry * geo) { long size; size = Device.size * (logical_block_size / KERNEL_SECTOR_SIZE); geo->cylinders = (size & ~0x3f) >> 6; geo->heads = 4; geo->sectors = 16; geo->start = 0; return 0; } static struct block_device_operations sbd_ops = { .owner = THIS_MODULE, .getgeo = sbd_getgeo }; static int __init sbd_init(void) { Device.size = nsectors * logical_block_size; spin_lock_init(&Device.lock); Device.data = vmalloc(Device.size); if (Device.data == NULL) return -ENOMEM; Queue = blk_init_queue(sbd_request, &Device.lock); if (Queue == NULL) goto out; blk_queue_logical_block_size(Queue, logical_block_size); major_num = register_blkdev(major_num, "sbd"); if (major_num < 0) { printk(KERN_WARNING "sbd: unable to get major number\n"); goto out; } Device.gd = alloc_disk(16); if (!Device.gd) goto out_unregister; Device.gd->major = major_num; Device.gd->first_minor = 0; Device.gd->fops = &sbd_ops; Device.gd->private_data = &Device; strcpy(Device.gd->disk_name, "sbd0"); set_capacity(Device.gd, nsectors); Device.gd->queue = Queue; add_disk(Device.gd); return 0; out_unregister: unregister_blkdev(major_num, "sbd"); out: vfree(Device.data); return -ENOMEM; } static void __exit sbd_exit(void) { del_gendisk(Device.gd); put_disk(Device.gd); unregister_blkdev(major_num, "sbd"); blk_cleanup_queue(Queue); vfree(Device.data); } module_init(sbd_init); module_exit(sbd_exit);実行結果
[root@localhost lkm]# insmod sbd.ko [root@localhost lkm]# ls -li /dev/sbd* 13999 brw-rw---- 1 root disk 252, 0 10月 30 00:16 /dev/sbd0 [root@localhost lkm]# mkfs.ext2 /dev/sbd0 mke2fs 1.41.14 (22-Dec-2010) Filesystem label= OS type: Linux Block size=1024 (log=0) Fragment size=1024 (log=0) Stride=0 blocks, Stripe width=0 blocks 64 inodes, 512 blocks 25 blocks (4.88%) reserved for the super user First data block=1 Maximum filesystem blocks=524288 1 block group 8192 blocks per group, 8192 fragments per group 64 inodes per group Writing inode tables: done Writing superblocks and filesystem accounting information: done This filesystem will be automatically checked every 33 mounts or 180 days, whichever comes first. Use tune2fs -c or -i to override. [root@localhost lkm]# mount /dev/sbd0 /mnt4 [root@localhost lkm]# echo "hogehoge" > /mnt4/hoge.txt [root@localhost lkm]# ls -l /mnt4 合計 13 -rw-r--r-- 1 root root 9 10月 30 00:22 hoge.txt drwx------ 2 root root 12288 10月 30 00:19 lost+foundblk_init_queue()は肝となる関数で、request_queueが作成され、ブロック読み書き時のデバイス実参照時に、sbd_request()がコールされるように、Device.gd->queueのrequest_fnに設定し、同時にrequest_queueのmake_request_fnに、エレベータ関数のopコールバックも設定されます。エレベータ関数でbioを2重構造でリストされるわけです。
sbd_request()で request_queueのrequestのbio単位で、デバイスバッファとやり取りします。req->cmd_type != REQ_TYPE_FSは、requestがファイル参照のリクエストかどうかと言うことです。requestにはデバイスの制御コマンドとしてのコード、シャットダウンコマンド等の他のリクエストも有しているからです。
サンプルでは未処理です。この時__blk_end_request_all()をコールすることで、そのrequestを削除し、かかるセクタ数を更新し、request_queueの先頭に次のrequestが接続されます。故に、関数内の変数reqは、次にrequestということで、改めてblk_fetch_request()をコールすることなく、次にbioを取得できるわけです。
sbd_transfer()では、実デバイスとやり取りをブロック単位で行います。blk_rq_pos(req)はrequestの先頭セクタ位置、blk_rq_cur_sectors()はrequestの先頭のbioのセクタ数、rq_data_dir()はread/writeフラグです。
やり取りが終了すると、__blk_end_request_cur()をコールします。これはrequest先頭(読み書きした)のbioのみを削除します。この時requestの先頭セクタ位置/セクタ数等も更新されます。返り値がfalseの場合、requestにはこれ以上のbioはありません。従って次のrequestのbioがデバイスが読み書きとなり、blk_fetch_request()で改めてrequestを取得することになります。
sbd_transfer()は、dev->dataのセクタ位置をオフセットとして、req->bufferと読み書きします。dev->dataは本サンプルに特化したもので、実際はデバイス依存のデバイスとのやり取りとなります。
サンプルのように、リアルタイム性を向上させるため、request単位でなくbio単位で実デバイスとやり取りするのが一般的のようですが、サンプルのようなメモリとしてのデバイスなら、むしろrequest単位でやり取りした方がいいかと思います。sbd_transfer()の引数を、requestのセクタ数とし、__blk_end_request_cur()でなく__blk_end_request_all()でrequestを削除すればいいはずです。(未検証)
補足
ブロックデバイスの実デバイスへの読み書きは、request_queueのrequest_fnコールバックが行います。従ってキャラクタデバイスのようなread/writeコールバック関数を有しません。デバイスコールバックの.getgeoは、ioctrlの HDIO_GETGEOでコールすることで、blkdev_ioctl()と、そして disk->fops->getgeo()とすることで、シリンダ数/ヘッド数/セクタ数/先頭位置のstruct hd_geometryが取得でき、fdisk等で無地なデバイスを設定する際のデバイス情報が取得できるように成っています。サンプルはfdiskでパーティションを作ることも可能です。
追記
シリンダ/ヘッド/セクタはHDデバイスの内部構造からくる属性となります。HDデバイスは複数毎の円盤があって、円盤毎に円周としてトラックがあり、トラックを参照ブロック単位のセクタで管理したものです。従って全セクタ数は、シリンダ×ヘッド×セクタということです。ここで、シリンダというのが円盤の数と思ってしまいますが、シリンダ数はトラック数で、円盤毎に読み込みヘッドがあるということで、ヘッド数が円盤数となるわけです。(たぶん)
で、このようなハード構造を持たないデバイスにも、シリンダ/ヘッド/セクタを割り当てる必要があります。要はシリンダ×ヘッド×セクタが全セクタ数になればいいわけで、(size & ~0x3f) >> 6で、上位6ビットをシリンダ数、ヘッドを4、セクタを16とします。4×16=2^6ですから、size & ~0x3fが全セクタ数となるわけです。なお、この実装ではsize & 0x3fのセクタ数は無視されることになります。