先说说为什么要引入块设备驱动程序。
以FLASH为例,如果对flash的读写采用字符设备驱动程序的那一套的话,会产生效率低下的问题。flash是以块为单位进行操作的,假如在flash一个块中,要完成对扇区0和扇区1的改写,如果按照字符设备的方式,需要完成以下步骤:
①读出整个块到buffer
②修改buffer的扇区0
③擦除整个块
④烧写整块
⑤读出整个块到buffer
⑥修改buffer的扇区1
⑦擦除整个块
⑧烧写整块
但如果我们引入一些优化:
①读出整个块到buffer
②修改buffer的扇区0和扇区1
③擦除整个块
④烧写整块
这样我们就能够大大提高效率。因此,块设备驱动程序的基本思想就出来了:先不执行读写,而是放入队列;优化之后再执行。
下面介绍一下块设备驱动程序的框架。
app: open,read,write "1.txt"
--------------------------------------------- 文件的读写
文件系统: vfat, ext2, ext3, yaffs2, jffs2 (把文件的读写转换为扇区的读写)
-----------------ll_rw_block----------------- 扇区的读写
1. 把"读写"放入队列
2. 调用队列的处理函数(优化/调顺序/合并)
块设备驱动程序
---------------------------------------------
硬件: 硬盘,flash
下面开始分析ll_rw_block(low level read/write block)
分析ll_rw_block
for (i = 0; i < nr; i++) {
struct buffer_head *bh = bhs[i];
submit_bh(rw, bh);
struct bio *bio; // 使用bh来构造bio (block input/output)
submit_bio(rw, bio);
// 通用的构造请求: 使用bio来构造请求(request)
generic_make_request(bio);
__generic_make_request(bio);
request_queue_t *q = bdev_get_queue(bio->bi_bdev); // 找到队列
// 调用队列的"构造请求函数"
ret = q->make_request_fn(q, bio);
// 默认的函数是__make_request
__make_request
// 先尝试合并
elv_merge(q, &req, bio);
// 如果合并不成,使用bio构造请求
init_request_from_bio(req, bio);
// 把请求放入队列
add_request(q, req);
// 执行队列
__generic_unplug_device(q);
// 调用队列的"处理函数"
q->request_fn(q);
所以,怎么写块设备驱动程序呢?
1. 分配gendisk: alloc_disk
2. 设置
2.1 分配/设置队列: request_queue_t 和blk_init_queue// 它提供读写能力
2.2 设置gendisk其他信息 // 它提供属性: 比如容量
3. 注册: add_disk
接下来要写一个真正的驱动程序,分配一块内存,用内存来模拟块设备。
第一个程序:
static struct gendisk *ramblock_disk;
static request_queue_t *ramblock_queue;
static int major;
static DEFINE_SPINLOCK(ramblock_lock);//定义自选锁
static struct block_device_operations ramblock_fops = {
.owner = THIS_MODULE,
};
#define RAMBLOCK_SIZE (1024*1024)
static void do_ramblock_request(request_queue_t * q)
{
static int cnt = 0;
printk("do_ramblock_request %d\n", ++cnt);
}
static int ramblock_init(void)
{
/* 1. 分配一个gendisk结构体 */
ramblock_disk = alloc_disk(16); /* 次设备号个数: 分区个数+1 */
/* 2. 设置 */
/* 2.1 分配/设置队列: 提供读写能力 */
ramblock_queue = blk_init_queue(do_ramblock_request, &ramblock_lock);
ramblock_disk->queue = ramblock_queue;
/* 2.2 设置其他属性: 比如容量 */
major = register_blkdev(0, "ramblock"); /* 用cat /proc/devices可以看到块设备 */
ramblock_disk->major = major;
ramblock_disk->first_minor = 0;
sprintf(ramblock_disk->disk_name, "ramblock");
ramblock_disk->fops = &ramblock_fops;
set_capacity(ramblock_disk, RAMBLOCK_SIZE / 512);//单位是扇区,在内核层面默认扇区是521字节
/* 3. 注册 */
add_disk(ramblock_disk);
return 0;
}
static void ramblock_exit(void)
{
unregister_blkdev(major, "ramblock");
del_gendisk(ramblock_disk);
put_disk(ramblock_disk);
blk_cleanup_queue(ramblock_queue);
}
module_init(ramblock_init);
module_exit(ramblock_exit);
MODULE_LICENSE("GPL");
加载驱动之后发现:
ramblock:do_ramblock_request 1
然后就卡住了,无法继续操作。说明确实调用了do_ramblock_request这个函数,但由于我们没有对它进行进一步的处理,所以还需要完善。
static void do_ramblock_request(request_queue_t * q)
{
static int cnt = 0;
struct request *req;
printk("do_ramblock_request %d\n", ++cnt);
while ((req = elv_next_request(q)) != NULL) {
end_request(req, 1);
}
}
以电梯调度算法取出下一个请求,然后结束请求并返回1表示成功。实际上我们什么都没有做,但这样就不会卡住了。它会打印出如下结果:
ramblock:do_ramblock_request 1
unknown partition table
之所以提示不识别的分区表,是因为我们在实际上我们在do_ramblock_request里什么都没有做。我们只是简单粗暴的把请求拿出来,然后就简单把它当成完成了。
进一步优化:
static void do_ramblock_request(request_queue_t * q)
{
static int cnt = 0;
struct request *req;
//printk("do_ramblock_request %d\n", ++cnt);
while ((req = elv_next_request(q)) != NULL) {
/* 数据传输三要素: 源,目的,长度 */
/* 源/目的:当读的时候就是源,写的时候就是目的 */
unsigned long offset = req->sector * 512;//扇区的长度乘以每个扇区大小
/* 目的/源:当读的时候就是目的,写的时候就是源 */
// req->buffer
/* 长度: */
unsigned long len = req->current_nr_sectors * 512;//扇区的长度乘以每个扇区大小
if (rq_data_dir(req) == READ)
{
memcpy(req->buffer, ramblock_buf+offset, len);
}
else
{
memcpy(ramblock_buf+offset, req->buffer, len);
}
end_request(req, 1);
}
其中memcpy(void *dest, const void *src, size_t count)。
测试:
在开发板上:
1. insmod ramblock.ko
2. 格式化: mkdosfs /dev/ramblock
3. 挂接: mount /dev/ramblock /tmp/
4. 读写文件: cd /tmp, 在里面vi文件,比如新建一个1.txt,里面的内容是“hello”
5. cd /; umount /tmp/,卸载设备
6. cat /dev/ramblock > /mnt/ramblock.bin,把文件的内容统统存到/mnt/ramblock.bin,相当于整个磁盘映象。
7. 在PC上查看ramblock.bin
sudo mount -o loop ramblock.bin /mnt,可以把一个普通文件当作块设备文件挂载,输入ls仍然可以看到1.txt。
接下来加入打印语句,可以看到一些有意思的事情:
static void do_ramblock_request(request_queue_t * q)
{
static int r_cnt = 0;
static int w_cnt = 0;
struct request *req;
//printk("do_ramblock_request %d\n", ++cnt);
while ((req = elv_next_request(q)) != NULL) {
/* 数据传输三要素: 源,目的,长度 */
/* 源/目的: */
unsigned long offset = req->sector * 512;
/* 目的/源: */
// req->buffer
/* 长度: */
unsigned long len = req->current_nr_sectors * 512;
if (rq_data_dir(req) == READ)
{
printk("do_ramblock_request read %d\n", ++r_cnt);
memcpy(req->buffer, ramblock_buf+offset, len);
}
else
{
printk("do_ramblock_request write %d\n", ++w_cnt);
memcpy(ramblock_buf+offset, req->buffer, len);
}
end_request(req, 1);
}
}
还是按照上面的步骤测试,当挂载到/tmp之后,我们执行“cp /etc/inittab /tmp”:
do_ramblock_request write 6
do_ramblock_request write 7
do_ramblock_request write 8
再执行sync
do_ramblock_request write 9
do_ramblock_request write 10
其中,打印的时候会有很长的时间间隔,当输入sync时,会完成全部操作。sync是一个系统调用,是同步的意思。
从这里我们可以看出,块设备的读写不会交叉执行,而是会进行优化,放入队列。
接下来进行下一步的优化,实现分区:
static struct block_device_operations ramblock_fops = {
.owner = THIS_MODULE,
.getgeo = ramblock_getgeo,
};
static int ramblock_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{
/* 容量=heads*cylinders*sectors*512 */
geo->heads = 2;//磁头的个数
geo->cylinders = 32;//柱面的个数
geo->sectors = RAMBLOCK_SIZE/2/32/512;
return 0;
}
要注意的是,实际上很多磁盘已经不是这种结构了,但为了兼容,必须设置虚拟的参数,这样才能借助分区工具。
加载驱动之后输入fdisk /dev/ramblock。然后输入n新建一个分区,输入p表示主分区,然后输入1表示第一个主分区。
Command (m for help): p
Disk /dev/ramblock: 1 MB, 1048576 bytes
2 heads, 32 sectors/track, 32 cylinders
Units = cylinders of 64 * 512 = 32768 bytes
Device Boot Start End Blocks Id System
Command (m for help): n
Command action
e extended
p primary partition (1-4)
p
Partition number (1-4): 1
First cylinder (1-32, default 1): 1
Last cylinder or +size or +sizeM or +sizeK (1-32, default 32): 5
Command (m for help): w
The partition table has been altered!
ramblock: ramblock1
这样第一个分区就好了。剩下的分区也这样操作,最后别忘记输入“w”把分区写到分区表中。最后可以看到:
Command (m for help): p
Disk /dev/ramblock: 1 MB, 1048576 bytes
2 heads, 32 sectors/track, 32 cylinders
Units = cylinders of 64 * 512 = 32768 bytes
Device Boot Start End Blocks Id System
/dev/ramblock1 1 5 144 83 Linux
/dev/ramblock2 6 25 640 83 Linux
/dev/ramblock3 26 32 224 83 Linux
如果现在输入”ls /dev/ramblock* -l”,会看到:
brw-rw---- 1 0 0 254, 0 Jan 1 00:45 /dev/ramblock
brw-rw---- 1 0 0 254, 1 Jan 1 00:45 /dev/ramblock1
brw-rw---- 1 0 0 254, 2 Jan 1 00:45 /dev/ramblock2
brw-rw---- 1 0 0 254, 3 Jan 1 00:45 /dev/ramblock3
如果我们想分别格式化的时候就可以直接“fdisk /dev/ramblock2”,还可以直接挂载“mount /dev/ramblock1 /tmp/”。
驱动程序就写到这里,用内存模拟磁盘很简单,可以想象一下,如果块设备也能够像内存一样访问的话,块设备就会非常的简单,但可惜的是,现在还达不到这种水平,硬件的操作太复杂了。但块设备本身的框架并不复杂。