第2章
+---------------------------------------------------+
| 写一个块设备驱动 |
+---------------------------------------------------+
| 作者:赵磊 |
| email: |
+---------------------------------------------------+
| 文章版权归原作者所有。 |
| 大家可以自由转载这篇文章,但原版权信息必须保留。 |
| 如需用于商业用途,请务必与原作者联系,若因未取得 |
| 授权而收起的版权争议,由侵权者自行负责。 |
+---------------------------------------------------+
上一章不但实现了一个最简单的块设备驱动程序,而且可能也成功地吓退了不少准备继续看下去的读者。
因为第一章看起来好像太难了。
不过读者也不要过于埋怨作者,因为大多数情况下第一次都不是什么好的体验......
对于坚持到这里的读者,这一章中,我们准备了一些简单的内容来犒劳大家。
关于块设备与I/O调度器的关系,我们在上一章中已经有所提及。
I/O调度器可以通过合并请求、重排块设备操作顺序等方式提高块设备访问的顺序。
就好像吃街边的大排档,如果点一个冷门的品种,可能会等更长的时间,
而如果点的恰好与旁边桌子上刚点的相同,那么会很快上来,因为厨师八成索性一起炒了。
然而I/O调度器和块设备的情况却有一些微妙的区别,大概可以类比成人家点了个西红柿鸡蛋汤你接着就点了个西红柿炒蛋。
聪明的厨师一定会先做你的菜,因为随后可以直接往锅里加水煮汤,可怜比你先来的人喝的却是你的刷锅水。
两个菜一锅煮表现在块设备上可以类比成先后访问块设备的同一个位置,这倒是与I/O调度器无关,有空学习linux缓存策略时可以想想这种情况。
一个女孩子换了好多件衣服问我漂不漂亮,而我的评价只要一眼就能拿出来。
对方总觉得衣服要牌子好、面料好、搭配合理、要符合个人的气质、要有文化,而我的标准却简单的多:越薄越好。
所谓臭气相投,我写的块设备驱动程序对I/O调度器的要求大概也是如此。
究其原因倒不是因为块设备驱动程序好色,而是这个所谓块设备中的数据都是在内存中的。
这也意味着我们的“块设备”读写迅速、并且不存在磁盘之类设备通常面临的寻道时间。
因此对这个“块设备”而言,一个复杂的I/O调度器不但发挥不了丝毫作用,反而其本身将白白耗掉不少内存和CPU。
同样的情况还出现在固态硬盘、U盘、记忆棒之类驱动中。将来固态硬盘流行之时,大概就是I/O调度器消亡之日了。
这里我们试图给我们的块设备驱动选择一个最简单的I/O调度器。
目前linux中包含anticipatory、cfq、deadline和noop这4个I/O调度器。
2.6.18之前的linux默认使用anticipatory,而之后的默认使用cfq。
关于这4个调度器的原理和特性我们不打算在这里介绍,原因是相关的介绍满网都是。
但我们还是不能避免在这里提及一下noop调度器,因为我们马上要用到它。
noop顾名思义,是一个基本上不干事的调度器。它基本不对请求进行什么附加的处理,仅仅假惺惺地告诉通用块设备层:我处理完了。
但与吃空饷的公仆不同,noop的存在还是有不少进步意义的。至少我们现在就需要一个不要没事添乱的I/O调度器。
选择一个指定的I/O调度器需要这个函数:
int elevator_init(struct request_queue *q, char *name);
q是请求队列的指针,name是需要设定的I/O调度器的名称。
如果name为NULL,那么内核会首先尝试选择启动参数"elevator="中指定的调度器,
不成功的话就去选择编译内核时指定的默认调度器,
如果运气太背还是不成功,就去选择"noop"调度器。
不要问我怎么知道的,一切皆在RTFSC(Read the F**ing Source Code --Linus Torvalds)。
对于我们的代码,就是在simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL)后面加上:
elevator_init(simp_blkdev_queue, "noop");
但问题是在blk_init_queue()函数中系统已经帮我们申请一个了,因此这里我们需要费点周折,把老的那个送回去。
所以我们的代码应该是:
simp_blkdev_init()函数开头处:
elevator_t *old_e;
blk_init_queue()函数之后:
old_e = simp_blkdev_queue->elevator;
if (IS_ERR_VALUE(elevator_init(simp_blkdev_queue, "noop")))
printk(KERN_WARNING "Switch elevator failed, using default\n");
else
elevator_exit(old_e);
为方便阅读并提高本文在google磁盘中的占用率,我们给出修改后的整个simp_blkdev_init()函数:
static int __init simp_blkdev_init(void)
{
int ret;
elevator_t *old_e;
simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
if (!simp_blkdev_queue) {
ret = -ENOMEM;
goto err_init_queue;
}
old_e = simp_blkdev_queue->elevator;
if (IS_ERR_VALUE(elevator_init(simp_blkdev_queue, "noop")))
printk(KERN_WARNING "Switch elevator failed, using default\n");
else
elevator_exit(old_e);
simp_blkdev_disk = alloc_disk(1);
if (!simp_blkdev_disk) {
ret = -ENOMEM;
goto err_alloc_disk;
}
strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME);
simp_blkdev_disk->major = SIMP_BLKDEV_DEVICEMAJOR;
simp_blkdev_disk->first_minor = 0;
simp_blkdev_disk->fops = &simp_blkdev_fops;
simp_blkdev_disk->queue = simp_blkdev_queue;
set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9);
add_disk(simp_blkdev_disk);
return 0;
err_alloc_disk:
blk_cleanup_queue(simp_blkdev_queue);
err_init_queue:
return ret;
}
本章的改动很小,我们现在测试一下这段代码:
首先我们像原先那样编译模块并加载:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step2 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
CC [M] /root/test/simp_blkdev/simp_blkdev_step2/simp_blkdev.o
Building modules, stage 2.
MODPOST
CC /root/test/simp_blkdev/simp_blkdev_step2/simp_blkdev.mod.o
LD [M] /root/test/simp_blkdev/simp_blkdev_step2/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
# insmod simp_blkdev.ko
#
然后看一看咱们的这个块设备现在使用的I/O调度器:
# cat /sys/block/simp_blkdev/queue/scheduler
[noop] anticipatory deadline cfq
#
看样子是成功了。
哦,上一章中忘了看老程序的调度器信息了,这里补上老程序的情况:
# cat /sys/block/simp_blkdev/queue/scheduler
noop anticipatory deadline [cfq]
#
OK,我们完成简单的一章,并且用事实说明了作者并没有在开头撒谎。
当然,作者也会力图让接下来的章节同样比小说易读。