转自:http://bbs.chinaunix.net/thread-2017377-1-1.html
本章的目的用尽可能最简单的方法写出一个能用的块设备驱动。
所谓的能用,是指我们可以对这个驱动生成的块设备进行mkfs,mount和读写文件。
为了尽可能简单,这个驱动的规模不是1000行,也不是500行,而是100行以内。
这里插一句,我们不打算在这里介绍如何写模块,理由是介绍的文章已经满天飞舞了。
如果你能看得懂、并且成功地编译、运行了这段代码,我们认为你已经达到了本教程的入学资格,
当然,如果你不幸的卡在这段代码中,那么请等到搞定它以后再往下看:
mod.c:
#include <linux/module.h>
static int __init init_base(void)
{
printk("----Hello. World----\n");
return 0;
}
static void __exit exit_base(void)
{
printk("----Bye----\n");
}
module_init(init_base);
module_exit(exit_base);
MODULE_LICENSE ("GPL");
MODULE_AUTHOR("Zhao Lei");
MODULE_DESCRIPTION("For test");
Makefile:
obj-m := mod.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) clean
rm -rf Module.markers modules.order Module.symvers
好了,这里我们假定你已经搞定上面的最简单的模块了,懂得什么是看模块,以及简单模块的编写、编译、加载和卸载。
还有就是,什么是块设备,什么是块设备驱动,这个也请自行google吧,因为我们已经迫不及待要写完程序下课。
为了建立一个可用的块设备,我们需要做......1件事情:
1:用add_disk()函数向系统中添加这个块设备
添加一个全局的
static struct gendisk *simp_blkdev_disk;
然后申明模块的入口和出口:
module_init(simp_blkdev_init);
module_exit(simp_blkdev_exit);
然后在入口处添加这个设备、出口处私房这个设备:
static int __init simp_blkdev_init(void)
{
add_disk(simp_blkdev_disk);
return 0;
}
static void __exit simp_blkdev_exit(void)
{
del_gendisk(simp_blkdev_disk);
}
当然,在添加设备之前我们需要申请这个设备的资源,这用到了alloc_disk()函数,因此模块入口函数simp_blkdev_init(void)应该是:
static int __init simp_blkdev_init(void)
{
simp_blkdev_disk = alloc_disk(1);
if (!simp_blkdev_disk) {
ret = -ENOMEM;
goto err_alloc_disk;
}
add_disk(simp_blkdev_disk);
return 0;
err_alloc_disk:
return ret;
}
还有别忘了在卸载模块的代码中也加一个行清理函数:
put_disk(simp_blkdev_disk);
还有就是,设备有关的属性也是需要设置的,因此在alloc_disk()和add_disk()之间我们需要:
strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME);
simp_blkdev_disk->major = ?1;
simp_blkdev_disk->first_minor = 0;
simp_blkdev_disk->fops = ?2;
simp_blkdev_disk->queue = ?3;
set_capacity(simp_blkdev_disk, ?4);
SIMP_BLKDEV_DISKNAME其实是这个块设备的名称,为了绅士一些,我们把它定义成宏了:
#define SIMP_BLKDEV_DISKNAME "simp_blkdev"
这里又引出了4个问号。(天哪,是不是有种受骗的感觉,像是陪老婆去做头发)
第1个问号:
每个设备需要对应的主、从驱动号。
我们的设备当然也需要,但很明显我不是脑科医生,因此跟写linux的那帮疯子不熟,得不到预先为我保留的设备号。
还有一种方法是使用动态分配的设备号,但在这一章中我们希望尽可能做得简单,因此也不采用这种方法。
那么我们采用的是:抢别人的设备号。
我们手头没有AK47,因此不敢干的太轰轰烈烈,而偷偷摸摸的事情倒是可以考虑的。
柿子要捡软的捏,而我们试图找出一个不怎么用得上的设备,然后抢他的ID。
打开linux/include/linux/major.h,把所有的设备一个个看下来,我们觉得最胜任被抢设备号的家伙非COMPAQ_SMART2_XXX莫属。
第一因为它不强势,基本不会被用到,因此也不会造成冲突;第二因为它有钱,从COMPAQ_SMART2_MAJOR到COMPAQ_SMART2_MAJOR7有那8个之多的设备号可以被抢,不过瘾的话还有它妹妹:COMPAQ_CISS_MAJOR~COMPAQ_CISS_MAJOR7。
为了让抢劫显得绅士一些,我们在外面又定义一个宏:
#define SIMP_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR
然后在?1的位置填上SIMP_BLKDEV_DEVICEMAJOR。
第2个问号:
gendisk结构需要设置fops指针,虽然我们用不到,但该设还是要设的。
好吧,就设个空得给它:
在全局部分添加:
struct block_device_operations simp_blkdev_fops = {
.owner = THIS_MODULE,
};
然后把?2的位置填上&simp_blkdev_fops。
第3个问号:
这个比较麻烦一些。
首先介绍请求队列的概念。对大多数块设备来说,系统会把对块设备的访问需求用bio和bio_vec表示,然后提交给通用块层。
通用块层为了减少块设备在寻道时损失的时间,使用I/O调度器对这些访问需求进行排序,以尽可能提高块设备效率。
关于I/O调度器在本章中不打算进行深入的讲解,但我们必须知道的是:
1:I/O调度器把排序后的访问需求通过request_queue结构传递给块设备驱动程序处理
2:我们的驱动程序需要设置一个request_queue结构
申请request_queue结构的函数是blk_init_queue(),而调用blk_init_queue()函数时需要传入一个函数的地址,这个函数担负着处理对块设备数据的请求。
因此我们需要做的就是:
1:实现一个static void simp_blkdev_do_request(struct request_queue *q)函数。
2:加入一个全局变量,指向块设备需要的请求队列:
static struct request_queue *simp_blkdev_queue;
3:在加载模块时用simp_blkdev_do_request()函数的地址作参数调用blk_init_queue()初始化一个请求队列:
simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
if (!simp_blkdev_queue) {
ret = -ENOMEM;
goto err_init_queue;
}
4:卸载模块时把simp_blkdev_queue还回去:
blk_cleanup_queue(simp_blkdev_queue);
5:在?3的位置填上simp_blkdev_queue。
第4个问号:
这个还好,比前面的简单多了,这里需要设置块设备的大小。
块设备的大小使用扇区作为单位设置,而扇区的大小默认是512字节。
当然,在把字节为单位的大小转换为以扇区为单位时,我们需要除以512,或者右移9位可能更快一些。
同样,我们试图把这一步也做得绅士一些,因此使用宏定义了块设备的大小,目前我们定为16M:
#define SIMP_BLKDEV_BYTES (16*1024*1024)
然后在?4的位置填上SIMP_BLKDEV_BYTES>>9。
看到这里,是不是有种身陷茫茫大海的无助感?并且一波未平,一波又起,在搞定这4个问号的同时,居然又引入了simp_blkdev_do_request函数!
当然,如果在身陷茫茫波涛中时你认为到处都是海,因此绝望,那么恭喜你可以不必挨到65岁再退休;
反之,如果你认为到处都是没有三聚氰胺鲜鱼,并且随便哪个方向都是岸时,那么也恭喜你,你可以活着回来继续享受身为纳税人的荣誉。
为了理清思路,我们把目前为止涉及到的代码整理出来:
#define SIMP_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR
#define SIMP_BLKDEV_DISKNAME "simp_blkdev"
#define SIMP_BLKDEV_BYTES (16*1024*1024)
static struct request_queue *simp_blkdev_queue;
static struct gendisk *simp_blkdev_disk;
static void simp_blkdev_do_request(struct request_queue *q);
struct block_device_operations simp_blkdev_fops = {
.owner = THIS_MODULE,
};
static int __init simp_blkdev_init(void)
{
int ret;
simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
if (!simp_blkdev_queue) {
ret = -ENOMEM;
goto err_init_queue;
}
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;
}
static void __exit simp_blkdev_exit(void)
{
del_gendisk(simp_blkdev_disk);
put_disk(simp_blkdev_disk);
blk_cleanup_queue(simp_blkdev_queue);
}
module_init(simp_blkdev_init);
module_exit(simp_blkdev_exit);
剩下部分的不多了,真的不多了。请相信我,因为我不在质监局上班。
我写的文章诚实可靠,并且不拿你纳税的钱。
我们还有一个最重要的函数需要实现,就是负责处理块设备请求的simp_blkdev_do_request()。
首先我们看看究竟把块设备的数据以什么方式放在内存中。
毕竟这是在第1章,因此我们将使用最simple的方式实现,也就是,数组。
我们在全局代码中定义:
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];
对驱动程序来说,这个数组看起来大了一些,如果不幸被懂行的人看到,将100%遭到最无情、最严重的鄙视。
而我们却从极少数公仆那里学到了最有效的应对之策,那就是:无视他,然后把他定为成“不明真相的群众”。
然后我们着手实现simp_blkdev_do_request。
这里介绍elv_next_request()函数,原型是:
struct request *elv_next_request(struct request_queue *q);
用来从一个请求队列中拿出一条请求(其实严格来说,拿出的可能是请求中的一段)。
随后的处理请求本质上是根据rq_data_dir(req)返回的该请求的方向(读/写),把块设备中的数据装入req->buffer、或是把req->buffer中的数据写入块设备。
刚才已经提及了与request结构相关的rq_data_dir()宏和.buffer成员,其他几个相关的结构成员和函数是:
request.sector:请求的开始磁道
request.current_nr_sectors:请求磁道数
end_request():结束一个请求,第2个参数表示请求处理结果,成功时设定为1,失败时设置为0或者错误号。
因此我们的simp_blkdev_do_request()函数为:
static void simp_blkdev_do_request(struct request_queue *q)
{
struct request *req;
while ((req = elv_next_request(q)) != NULL) {
if ((req->sector + req->current_nr_sectors) << 9
> SIMP_BLKDEV_BYTES) {
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": bad request: block=%llu, count=%u\n",
(unsigned long long)req->sector,
req->current_nr_sectors);
end_request(req, 0);
continue;
}
switch (rq_data_dir(req)) {
case READ:
memcpy(req->buffer,
simp_blkdev_data + (req->sector << 9),
req->current_nr_sectors << 9);
end_request(req, 1);
break;
case WRITE:
memcpy(simp_blkdev_data + (req->sector << 9),
req->buffer, req->current_nr_sectors << 9);
end_request(req, 1);
break;
default:
/* No default because rq_data_dir(req) is 1 bit */
break;
}
}
}
函数使用elv_next_request()遍历struct request_queue *q中使用struct request *req表示的每一段,首先判断这个请求是否超过了我们的块设备的最大容量,
然后根据请求的方向rq_data_dir(req)进行相应的请求处理。由于我们使用的是指简单的数组,因此请求处理仅仅是2条memcpy。
memcpy中也牵涉到了扇区号到线性地址的转换操作,我想对坚持到这里的读者来说,这个操作应该不需要进一步解释了。
编码到此结束,然后我们试试这个程序:
首先编译:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step1 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686‘
CC [M] /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.o
Building modules, stage 2.
MODPOST
CC /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.mod.o
LD [M] /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686‘
#
加载模块
# insmod simp_blkdev.ko
#
用lsmod看看。
这里我们注意到,该模块的Used by为0,因为它既没有被其他模块使用,也没有被mount。
# lsmod
Module Size Used by
simp_blkdev 16784008 0
...
#
如果当前系统支持udev,在调用add_disk()函数时即插即用机制会自动为我们在/dev/目录下建立设备文件。
设备文件的名称为我们在gendisk.disk_name中设置的simp_blkdev,主、从设备号也是我们在程序中设定的72和0。
如果当前系统不支持udev,那么很不幸,你需要自己用mknod /dev/simp_blkdev b 72 0来创建设备文件了。
# ls -l /dev/simp_blkdev
brw-r----- 1 root disk 72, 0 11-10 18:13 /dev/simp_blkdev
#
在块设备中创建文件系统,这里我们创建常用的ext3。
当然,作为通用的块设备,创建其他类型的文件系统也没问题。
# mkfs.ext3 /dev/simp_blkdev
mke2fs 1.39 (29-May-2006)
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
4096 inodes, 16384 blocks
819 blocks (5.00%) reserved for the super user
First data block=1
Maximum filesystem blocks=16777216
2 block groups
8192 blocks per group, 8192 fragments per group
2048 inodes per group
Superblock backups stored on blocks:
8193
Writing inode tables: done
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done
This filesystem will be automatically checked every 38 mounts or
180 days, whichever comes first. Use tune2fs -c or -i to override.
#
如果这是第一次使用,建议创建一个目录用来mount这个设备中的文件系统。
当然,这不是必需的。如果你对mount之类的用法很熟,你完全能够自己决定在这里干什么,甚至把这个设备mount成root。
# mkdir -p /mnt/temp1
#
把建立好文件系统的块设备mount到刚才建立的目录中
# mount /dev/simp_blkdev /mnt/temp1
#
看看现在的mount表
# mount
...
/dev/simp_blkdev on /mnt/temp1 type ext3 (rw)
#
看看现在的模块引用计数,从刚才的0变成1了,
原因是我们mount了。
# lsmod
Module Size Used by
simp_blkdev 16784008 1
...
#
看看文件系统的内容,有个mkfs时自动建立的lost+found目录。
# ls /mnt/temp1
lost+found
#
随便拷点东西进去
# cp /etc/init.d/* /mnt/temp1
#
再看看
# ls /mnt/temp1
acpid conman functions irqbalance mdmpd NetworkManagerDispatcher rdisc sendmail winbind
anacron cpuspeed gpm kdump messagebus nfs readahead_early setroubleshoot wpa_supplicant
apmd crond haldaemon killall microcode_ctl nfslock readahead_later single xfs
atd cups halt krb524 multipathd nscd restorecond smartd xinetd
auditd cups-config-daemon hidd kudzu netconsole ntpd rhnsd smb ypbind
autofs dhcdbd ip6tables lost+found netfs pand rpcgssd sshd yum-updatesd
avahi-daemon dund ipmi lvm2-monitor netplugd pcscd rpcidmapd syslog
avahi-dnsconfd firstboot iptables mcstrans network portmap rpcsvcgssd vmware
bluetooth frecord irda mdmonitor NetworkManager psacct saslauthd vncserver
#
现在这个块设备的使用情况是
# df
文件系统 1K-块 已用 可用 已用% 挂载点
...
/dev/simp_blkdev 15863 1440 13604 10% /mnt/temp1
#
再全删了玩玩
# rm -rf /mnt/temp1/*
#
看看删完了没有
# ls /mnt/temp1
#
好了,大概玩够了,我们把文件系统umount掉
# umount /mnt/temp1
#
模块的引用计数应该还原成0了吧
# lsmod
Module Size Used by
simp_blkdev 16784008 0
...
#
最后一步,移除模块
# rmmod simp_blkdev
#
这是这部教程的第1章,不好意思的是,内容比预期还是难了一些。
当初还有一种考虑是在本章中仅仅实现一个写了就丢的块设备驱动,也就是说,对这个块设备的操作只能到mkfs这一部,而不能继续mount,因为刚才写的数据全被扔了。
或者更简单些,仅仅写一个hello world的模块。
但最后还是写成了现在这样没,因为我觉得拿出一个真正可用的块设备驱动程序对读者来说更有成就感。
无论如何,本章是一个开始,而你,已经跨入了学习块设备驱动教室的大门,或者通俗来说,上了贼船。
而在后续的章节中,我们将陆续完善对这个程序,通过追加或者强化这个程序,来学习与块设备有关、或与块设备无关但与linux有关的方方面面。
总之,我希望通过这部教程,起码让读者学到有用的知识,或者更进一步,引导读者对linux的兴趣,甚至领悟学习一切科学所需要的钻研精神。
作为第一章的结尾,引用我在另一篇文章中的序言:
谨以此文向读者示范什么叫做严谨的研究。
呼唤踏实的治学态度,反对浮躁的论坛风气。
块设备驱动程序:
#include <linux/module.h> #include <linux/errno.h> #include <linux/interrupt.h> #include <linux/mm.h> #include <linux/fs.h> #include <linux/kernel.h> #include <linux/timer.h> #include <linux/genhd.h> #include <linux/hdreg.h> #include <linux/ioport.h> #include <linux/init.h> #include <linux/wait.h> #include <linux/blkdev.h> #include <linux/blkpg.h> #include <linux/delay.h> #include <linux/io.h> #include <asm/system.h> #include <asm/uaccess.h> #include <asm/dma.h> static DEFINE_SPINLOCK(block_lock); static unsigned int major; #define RAMBLOCK_SIZE (1024 * 1024) static unsigned char *ramblock_buffer; static struct gendisk *ramblock_disk; static struct request_queue *ramblock_queue; static struct block_device_operations ramblock_ops = { .owner = THIS_MODULE, }; static void do_ramblock_irq(struct request_queue *q) { struct request *req; req = blk_fetch_request(q); // printk("do_ramblock_irq\n"); while(req) { unsigned int offset = blk_rq_pos(req); unsigned int len = blk_rq_cur_sectors(req); if (offset + len > get_capacity(req->rq_disk)) return; if(rq_data_dir(req) == READ) memcpy(req->buffer, ramblock_buffer + offset, len); else memcpy(ramblock_buffer + offset, req->buffer, len); /* wrap up, 0 = success, -errno = fail */ if (!__blk_end_request_cur(req, 0)) req = blk_fetch_request(q); } } static int __init my_block_init(void) { /*分配一个gendisk结构体*/ ramblock_disk = alloc_disk(16); if(IS_ERR(ramblock_disk)) { printk(KERN_ALERT"alloc_disk error\n"); return -EINVAL; } /*设置*/ /*设置/分配一个请求队列,提供读写功能*/ ramblock_queue = blk_init_queue(do_ramblock_irq, &block_lock); if(IS_ERR(ramblock_queue)) { printk(KERN_ALERT"blk_init_queue error\n"); goto ramblock_queue_err; } ramblock_disk->queue = ramblock_queue; major = register_blkdev(0, "ramblock"); ramblock_disk->major = major; ramblock_disk->first_minor = 0; sprintf(ramblock_disk->disk_name, "ramblock"); ramblock_disk->fops = &ramblock_ops; set_capacity(ramblock_disk, RAMBLOCK_SIZE / 512); /*硬件相关操作*/ ramblock_buffer = kzalloc(RAMBLOCK_SIZE, GFP_KERNEL); if(!ramblock_buffer) { printk(KERN_ALERT"ramblock_buffer kmalloc error\n"); goto ramblock_buffer_err; } /*注册*/ add_disk(ramblock_disk); return 0; ramblock_buffer_err: blk_cleanup_queue(ramblock_queue); ramblock_queue_err: del_gendisk(ramblock_disk); return -EINVAL; } static void __exit my_block_exit(void) { unregister_blkdev(major, "ramblock"); del_gendisk(ramblock_disk); put_disk(ramblock_disk); blk_cleanup_queue(ramblock_queue); kfree(ramblock_buffer); } module_init(my_block_init); module_exit(my_block_exit); MODULE_LICENSE("GPL");
块设备驱动程序
1~9章块驱动详解 来自:http://bbs.chinaunix.net/thread-2017377-1-1.html
第1章 +---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | 网名:OstrichFly、飞翔的鸵鸟 | | email: [email protected] | +---------------------------------------------------+ | 文章版权归原作者所有。 | | 大家可以自由转载这篇文章,但原版权信息必须保留。 | | 如需用于商业用途,请务必与原作者联系,若因未取得 | | 授权而收起的版权争议,由侵权者自行负责。 | +---------------------------------------------------+ 同样是读书,读小说可以行云流水,读完后心情舒畅,意犹未尽;读电脑书却举步艰难,读完后目光呆滞,也是意犹未尽,只不过未尽的是痛苦的回忆。 研究证明,痛苦的记忆比快乐的更难忘记,因此电脑书中的内容比小说记得持久。 而这套教程的目的是要打破这种状况,以至于读者在忘记小说内容忘记本文。 在这套教程中,我们通过写一个建立在内存中的块设备驱动,来学习linux内核和相关设备驱动知识。 选择写块设备驱动的原因是: 1:容易上手 2:可以牵连出更多的内核知识 3:像本文这样的块设备驱动教程不多,所以需要一个 好吧,扯淡到此结束,我们开始写了。 本章的目的用尽可能最简单的方法写出一个能用的块设备驱动。 所谓的能用,是指我们可以对这个驱动生成的块设备进行mkfs,mount和读写文件。 为了尽可能简单,这个驱动的规模不是1000行,也不是500行,而是100行以内。 这里插一句,我们不打算在这里介绍如何写模块,理由是介绍的文章已经满天飞舞了。 如果你能看得懂、并且成功地编译、运行了这段代码,我们认为你已经达到了本教程的入学资格, 当然,如果你不幸的卡在这段代码中,那么请等到搞定它以后再往下看: mod.c: #include <linux/module.h> static int __init init_base(void) { printk("----Hello. World----\n"); return 0; } static void __exit exit_base(void) { printk("----Bye----\n"); } module_init(init_base); module_exit(exit_base); MODULE_LICENSE ("GPL"); MODULE_AUTHOR("Zhao Lei"); MODULE_DESCRIPTION("For test"); Makefile: obj-m := mod.o KDIR := /lib/modules/$(shell uname -r)/build PWD := $(shell pwd) default: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules clean: $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) clean rm -rf Module.markers modules.order Module.symvers 好了,这里我们假定你已经搞定上面的最简单的模块了,懂得什么是看模块,以及简单模块的编写、编译、加载和卸载。 还有就是,什么是块设备,什么是块设备驱动,这个也请自行google吧,因为我们已经迫不及待要写完程序下课。 为了建立一个可用的块设备,我们需要做......1件事情: 1:用add_disk()函数向系统中添加这个块设备 添加一个全局的 static struct gendisk *simp_blkdev_disk; 然后申明模块的入口和出口: module_init(simp_blkdev_init); module_exit(simp_blkdev_exit); 然后在入口处添加这个设备、出口处私房这个设备: static int __init simp_blkdev_init(void) { add_disk(simp_blkdev_disk); return 0; } static void __exit simp_blkdev_exit(void) { del_gendisk(simp_blkdev_disk); } 当然,在添加设备之前我们需要申请这个设备的资源,这用到了alloc_disk()函数,因此模块入口函数simp_blkdev_init(void)应该是: static int __init simp_blkdev_init(void) { simp_blkdev_disk = alloc_disk(1); if (!simp_blkdev_disk) { ret = -ENOMEM; goto err_alloc_disk; } add_disk(simp_blkdev_disk); return 0; err_alloc_disk: return ret; } 还有别忘了在卸载模块的代码中也加一个行清理函数: put_disk(simp_blkdev_disk); 还有就是,设备有关的属性也是需要设置的,因此在alloc_disk()和add_disk()之间我们需要: strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME); simp_blkdev_disk->major = ?1; simp_blkdev_disk->first_minor = 0; simp_blkdev_disk->fops = ?2; simp_blkdev_disk->queue = ?3; set_capacity(simp_blkdev_disk, ?4); SIMP_BLKDEV_DISKNAME其实是这个块设备的名称,为了绅士一些,我们把它定义成宏了: #define SIMP_BLKDEV_DISKNAME "simp_blkdev" 这里又引出了4个问号。(天哪,是不是有种受骗的感觉,像是陪老婆去做头发) 第1个问号: 每个设备需要对应的主、从驱动号。 我们的设备当然也需要,但很明显我不是脑科医生,因此跟写linux的那帮疯子不熟,得不到预先为我保留的设备号。 还有一种方法是使用动态分配的设备号,但在这一章中我们希望尽可能做得简单,因此也不采用这种方法。 那么我们采用的是:抢别人的设备号。 我们手头没有AK47,因此不敢干的太轰轰烈烈,而偷偷摸摸的事情倒是可以考虑的。 柿子要捡软的捏,而我们试图找出一个不怎么用得上的设备,然后抢他的ID。 打开linux/include/linux/major.h,把所有的设备一个个看下来,我们觉得最胜任被抢设备号的家伙非COMPAQ_SMART2_XXX莫属。 第一因为它不强势,基本不会被用到,因此也不会造成冲突;第二因为它有钱,从COMPAQ_SMART2_MAJOR到COMPAQ_SMART2_MAJOR7有那8个之多的设备号可以被抢,不过瘾的话还有它妹妹:COMPAQ_CISS_MAJOR~COMPAQ_CISS_MAJOR7。 为了让抢劫显得绅士一些,我们在外面又定义一个宏: #define SIMP_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR 然后在?1的位置填上SIMP_BLKDEV_DEVICEMAJOR。 第2个问号: gendisk结构需要设置fops指针,虽然我们用不到,但该设还是要设的。 好吧,就设个空得给它: 在全局部分添加: struct block_device_operations simp_blkdev_fops = { .owner = THIS_MODULE, }; 然后把?2的位置填上&simp_blkdev_fops。 第3个问号: 这个比较麻烦一些。 首先介绍请求队列的概念。对大多数块设备来说,系统会把对块设备的访问需求用bio和bio_vec表示,然后提交给通用块层。 通用块层为了减少块设备在寻道时损失的时间,使用I/O调度器对这些访问需求进行排序,以尽可能提高块设备效率。 关于I/O调度器在本章中不打算进行深入的讲解,但我们必须知道的是: 1:I/O调度器把排序后的访问需求通过request_queue结构传递给块设备驱动程序处理 2:我们的驱动程序需要设置一个request_queue结构 申请request_queue结构的函数是blk_init_queue(),而调用blk_init_queue()函数时需要传入一个函数的地址,这个函数担负着处理对块设备数据的请求。 因此我们需要做的就是: 1:实现一个static void simp_blkdev_do_request(struct request_queue *q)函数。 2:加入一个全局变量,指向块设备需要的请求队列: static struct request_queue *simp_blkdev_queue; 3:在加载模块时用simp_blkdev_do_request()函数的地址作参数调用blk_init_queue()初始化一个请求队列: simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL); if (!simp_blkdev_queue) { ret = -ENOMEM; goto err_init_queue; } 4:卸载模块时把simp_blkdev_queue还回去: blk_cleanup_queue(simp_blkdev_queue); 5:在?3的位置填上simp_blkdev_queue。 第4个问号: 这个还好,比前面的简单多了,这里需要设置块设备的大小。 块设备的大小使用扇区作为单位设置,而扇区的大小默认是512字节。 当然,在把字节为单位的大小转换为以扇区为单位时,我们需要除以512,或者右移9位可能更快一些。 同样,我们试图把这一步也做得绅士一些,因此使用宏定义了块设备的大小,目前我们定为16M: #define SIMP_BLKDEV_BYTES (16*1024*1024) 然后在?4的位置填上SIMP_BLKDEV_BYTES>>9。 看到这里,是不是有种身陷茫茫大海的无助感?并且一波未平,一波又起,在搞定这4个问号的同时,居然又引入了simp_blkdev_do_request函数! 当然,如果在身陷茫茫波涛中时你认为到处都是海,因此绝望,那么恭喜你可以不必挨到65岁再退休; 反之,如果你认为到处都是没有三聚氰胺鲜鱼,并且随便哪个方向都是岸时,那么也恭喜你,你可以活着回来继续享受身为纳税人的荣誉。 为了理清思路,我们把目前为止涉及到的代码整理出来: #define SIMP_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR #define SIMP_BLKDEV_DISKNAME "simp_blkdev" #define SIMP_BLKDEV_BYTES (16*1024*1024) static struct request_queue *simp_blkdev_queue; static struct gendisk *simp_blkdev_disk; static void simp_blkdev_do_request(struct request_queue *q); struct block_device_operations simp_blkdev_fops = { .owner = THIS_MODULE, }; static int __init simp_blkdev_init(void) { int ret; simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL); if (!simp_blkdev_queue) { ret = -ENOMEM; goto err_init_queue; } 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; } static void __exit simp_blkdev_exit(void) { del_gendisk(simp_blkdev_disk); put_disk(simp_blkdev_disk); blk_cleanup_queue(simp_blkdev_queue); } module_init(simp_blkdev_init); module_exit(simp_blkdev_exit); 剩下部分的不多了,真的不多了。请相信我,因为我不在质监局上班。 我写的文章诚实可靠,并且不拿你纳税的钱。 我们还有一个最重要的函数需要实现,就是负责处理块设备请求的simp_blkdev_do_request()。 首先我们看看究竟把块设备的数据以什么方式放在内存中。 毕竟这是在第1章,因此我们将使用最simple的方式实现,也就是,数组。 我们在全局代码中定义: unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES]; 对驱动程序来说,这个数组看起来大了一些,如果不幸被懂行的人看到,将100%遭到最无情、最严重的鄙视。 而我们却从极少数公仆那里学到了最有效的应对之策,那就是:无视他,然后把他定为成“不明真相的群众”。 然后我们着手实现simp_blkdev_do_request。 这里介绍elv_next_request()函数,原型是: struct request *elv_next_request(struct request_queue *q); 用来从一个请求队列中拿出一条请求(其实严格来说,拿出的可能是请求中的一段)。 随后的处理请求本质上是根据rq_data_dir(req)返回的该请求的方向(读/写),把块设备中的数据装入req->buffer、或是把req->buffer中的数据写入块设备。 刚才已经提及了与request结构相关的rq_data_dir()宏和.buffer成员,其他几个相关的结构成员和函数是: request.sector:请求的开始磁道 request.current_nr_sectors:请求磁道数 end_request():结束一个请求,第2个参数表示请求处理结果,成功时设定为1,失败时设置为0或者错误号。 因此我们的simp_blkdev_do_request()函数为: static void simp_blkdev_do_request(struct request_queue *q) { struct request *req; while ((req = elv_next_request(q)) != NULL) { if ((req->sector + req->current_nr_sectors) << 9 > SIMP_BLKDEV_BYTES) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": bad request: block=%llu, count=%u\n", (unsigned long long)req->sector, req->current_nr_sectors); end_request(req, 0); continue; } switch (rq_data_dir(req)) { case READ: memcpy(req->buffer, simp_blkdev_data + (req->sector << 9), req->current_nr_sectors << 9); end_request(req, 1); break; case WRITE: memcpy(simp_blkdev_data + (req->sector << 9), req->buffer, req->current_nr_sectors << 9); end_request(req, 1); break; default: /* No default because rq_data_dir(req) is 1 bit */ break; } } } 函数使用elv_next_request()遍历struct request_queue *q中使用struct request *req表示的每一段,首先判断这个请求是否超过了我们的块设备的最大容量, 然后根据请求的方向rq_data_dir(req)进行相应的请求处理。由于我们使用的是指简单的数组,因此请求处理仅仅是2条memcpy。 memcpy中也牵涉到了扇区号到线性地址的转换操作,我想对坚持到这里的读者来说,这个操作应该不需要进一步解释了。 编码到此结束,然后我们试试这个程序: 首先编译: # make make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step1 modules make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686‘ CC [M] /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.o Building modules, stage 2. MODPOST CC /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.mod.o LD [M] /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.ko make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686‘ # 加载模块 # insmod simp_blkdev.ko # 用lsmod看看。 这里我们注意到,该模块的Used by为0,因为它既没有被其他模块使用,也没有被mount。 # lsmod Module Size Used by simp_blkdev 16784008 0 ... # 如果当前系统支持udev,在调用add_disk()函数时即插即用机制会自动为我们在/dev/目录下建立设备文件。 设备文件的名称为我们在gendisk.disk_name中设置的simp_blkdev,主、从设备号也是我们在程序中设定的72和0。 如果当前系统不支持udev,那么很不幸,你需要自己用mknod /dev/simp_blkdev b 72 0来创建设备文件了。 # ls -l /dev/simp_blkdev brw-r----- 1 root disk 72, 0 11-10 18:13 /dev/simp_blkdev # 在块设备中创建文件系统,这里我们创建常用的ext3。 当然,作为通用的块设备,创建其他类型的文件系统也没问题。 # mkfs.ext3 /dev/simp_blkdev mke2fs 1.39 (29-May-2006) Filesystem label= OS type: Linux Block size=1024 (log=0) Fragment size=1024 (log=0) 4096 inodes, 16384 blocks 819 blocks (5.00%) reserved for the super user First data block=1 Maximum filesystem blocks=16777216 2 block groups 8192 blocks per group, 8192 fragments per group 2048 inodes per group Superblock backups stored on blocks: 8193 Writing inode tables: done Creating journal (1024 blocks): done Writing superblocks and filesystem accounting information: done This filesystem will be automatically checked every 38 mounts or 180 days, whichever comes first. Use tune2fs -c or -i to override. # 如果这是第一次使用,建议创建一个目录用来mount这个设备中的文件系统。 当然,这不是必需的。如果你对mount之类的用法很熟,你完全能够自己决定在这里干什么,甚至把这个设备mount成root。 # mkdir -p /mnt/temp1 # 把建立好文件系统的块设备mount到刚才建立的目录中 # mount /dev/simp_blkdev /mnt/temp1 # 看看现在的mount表 # mount ... /dev/simp_blkdev on /mnt/temp1 type ext3 (rw) # 看看现在的模块引用计数,从刚才的0变成1了, 原因是我们mount了。 # lsmod Module Size Used by simp_blkdev 16784008 1 ... # 看看文件系统的内容,有个mkfs时自动建立的lost+found目录。 # ls /mnt/temp1 lost+found # 随便拷点东西进去 # cp /etc/init.d/* /mnt/temp1 # 再看看 # ls /mnt/temp1 acpid conman functions irqbalance mdmpd NetworkManagerDispatcher rdisc sendmail winbind anacron cpuspeed gpm kdump messagebus nfs readahead_early setroubleshoot wpa_supplicant apmd crond haldaemon killall microcode_ctl nfslock readahead_later single xfs atd cups halt krb524 multipathd nscd restorecond smartd xinetd auditd cups-config-daemon hidd kudzu netconsole ntpd rhnsd smb ypbind autofs dhcdbd ip6tables lost+found netfs pand rpcgssd sshd yum-updatesd avahi-daemon dund ipmi lvm2-monitor netplugd pcscd rpcidmapd syslog avahi-dnsconfd firstboot iptables mcstrans network portmap rpcsvcgssd vmware bluetooth frecord irda mdmonitor NetworkManager psacct saslauthd vncserver # 现在这个块设备的使用情况是 # df 文件系统 1K-块 已用 可用 已用% 挂载点 ... /dev/simp_blkdev 15863 1440 13604 10% /mnt/temp1 # 再全删了玩玩 # rm -rf /mnt/temp1/* # 看看删完了没有 # ls /mnt/temp1 # 好了,大概玩够了,我们把文件系统umount掉 # umount /mnt/temp1 # 模块的引用计数应该还原成0了吧 # lsmod Module Size Used by simp_blkdev 16784008 0 ... # 最后一步,移除模块 # rmmod simp_blkdev # 这是这部教程的第1章,不好意思的是,内容比预期还是难了一些。 当初还有一种考虑是在本章中仅仅实现一个写了就丢的块设备驱动,也就是说,对这个块设备的操作只能到mkfs这一部,而不能继续mount,因为刚才写的数据全被扔了。 或者更简单些,仅仅写一个hello world的模块。 但最后还是写成了现在这样没,因为我觉得拿出一个真正可用的块设备驱动程序对读者来说更有成就感。 无论如何,本章是一个开始,而你,已经跨入了学习块设备驱动教室的大门,或者通俗来说,上了贼船。 而在后续的章节中,我们将陆续完善对这个程序,通过追加或者强化这个程序,来学习与块设备有关、或与块设备无关但与linux有关的方方面面。 总之,我希望通过这部教程,起码让读者学到有用的知识,或者更进一步,引导读者对linux的兴趣,甚至领悟学习一切科学所需要的钻研精神。 作为第一章的结尾,引用我在另一篇文章中的序言: 谨以此文向读者示范什么叫做严谨的研究。 呼唤踏实的治学态度,反对浮躁的论坛风气。 --OstrichFly <未完,待续> 第2章 +---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | email: [email protected] | +---------------------------------------------------+ | 文章版权归原作者所有。 | | 大家可以自由转载这篇文章,但原版权信息必须保留。 | | 如需用于商业用途,请务必与原作者联系,若因未取得 | | 授权而收起的版权争议,由侵权者自行负责。 | +---------------------------------------------------+ 上一章不但实现了一个最简单的块设备驱动程序,而且可能也成功地吓退了不少准备继续看下去的读者。 因为第一章看起来好像太难了。 不过读者也不要过于埋怨作者,因为大多数情况下第一次都不是什么好的体验...... 对于坚持到这里的读者,这一章中,我们准备了一些简单的内容来犒劳大家。 关于块设备与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,我们完成简单的一章,并且用事实说明了作者并没有在开头撒谎。 当然,作者也会力图让接下来的章节同样比小说易读。 <未完,待续> 第3章 +---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | email: [email protected] | +---------------------------------------------------+ | 文章版权归原作者所有。 | | 大家可以自由转载这篇文章,但原版权信息必须保留。 | | 如需用于商业用途,请务必与原作者联系,若因未取得 | | 授权而收起的版权争议,由侵权者自行负责。 | +---------------------------------------------------+ 上一章中我们讨论了mm的衣服问题,并成功地为她换上了一件轻如鸿毛、关键是薄如蝉翼的新衣服。 而这一章中,我们打算稍稍再前进一步,也就是:给她脱光。 目的是更加符合我们的审美观、并且能够更加深入地了解该mm(喜欢制服皮草的读者除外)。 付出的代价是这一章的内容要稍稍复杂一些。 虽然noop调度器确实已经很简单了,简单到比我们的驱动程序还简单,在2.6.27中的120行代码量已经充分说明了这个问题。 但显而易见的是,不管它多简单,只要它存在,我们就把它看成累赘。 这里我们不打算再次去反复磨嘴皮子论证不使用I/O调度器能给我们的驱动程序带来什么样的好处、面临的困难、以及如何与国际接轨的诸多事宜, 毕竟现在不是在讨论汽油降价,而我们也不是中石油。我们更关心的是实实在在地做一些对驱动程序有益的事情。 不过I/O调度器这层遮体衣服倒也不是这么容易脱掉的,因为实际上我们还使用了它捆绑的另一个功能,就是请求队列。 因此我们在前两章中的程序才如此简单。 从细节上来说,请求队列request_queue中有个make_request_fn成员变量,我们看它的定义: struct request_queue { ... make_request_fn *make_request_fn; ... } 它实际上是: typedef int (make_request_fn) (struct request_queue *q, struct bio *bio); 也就是一个函数的指针。 如果上面这段话让读者感到莫名其妙,那么请搬个板凳坐下,Let‘s Begin the Story。 对通用块层的访问,比如请求读某个块设备上的一段数据,通常是准备一个bio,然后调用generic_make_request()函数来实现的。 调用者是幸运的,因为他往往不需要去关心generic_make_request()函数如何做的,只需要知道这个神奇的函数会为他搞定所有的问题就OK了。 而我们却没有这么幸运,因为对一个块设备驱动的设计者来说,如果不知道generic_make_request()函数的内部情况,很可能会让驱动的使用者得不到安全感。 了解generic_make_request()内部的有效方法还是RTFSC,但这里会给出一些提示。 我们可以在generic_make_request()中找到__generic_make_request(bio)这么一句, 然后在__generic_make_request()函数中找到ret = q->make_request_fn(q, bio)这么一行。 偷懒省略掉解开谜题的所有关键步骤后,这里可以得出一个作者相信但读者不一定相信的正确结论: generic_make_request()最终是通过调用request_queue.make_request_fn函数完成bio所描述的请求处理的。 Story到此结束,现在我们可以解释刚才为什么列出那段莫名其妙的数据结构的意图了。 对于块设备驱动来说,正是request_queue.make_request_fn函数负责处理这个块设备上的所有请求。 也就是说,只要我们实现了request_queue.make_request_fn,那么块设备驱动的Primary Mission就接近完成了。 在本章中,我们要做的就是: 1:让request_queue.make_request_fn指向我们设计的make_request函数 2:把我们设计的make_request函数写出来 如果读者现在已经意气风发地拿起键盘跃跃欲试了,作者一定会假装谦虚地问读者一个问题: 你的钻研精神遇到城管了? 如果这句话问得读者莫名其妙的话,作者将补充另一个问题: 前两章中明显没有实现make_request函数,那时的驱动程序倒是如何工作的? 然后就是清清嗓子自问自答。 前两章确实没有用到make_request函数,但当我们使用blk_init_queue()获得request_queue时, 万能的系统知道我们搞IT的都低收入,因此救济了我们一个,这就是大名鼎鼎的__make_request()函数。 request_queue.make_request_fn指向了__make_request()函数,因此对块设备的所有请求被导向了__make_request()函数中。 __make_request()函数不是吃素的,马上喊上了他的兄弟,也就是I/O调度器来帮忙,结果就是bio请求被I/O调度器处理了。 同时,__make_request()自身也没闲着,它把bio这条咸鱼嗅了嗅,舔了舔,然后放到嘴里嚼了嚼,把鱼刺鱼鳞剔掉, 然后情意绵绵地通过do_request函数(也就是blk_init_queue的第一个参数)喂到驱动程序作者的口中。 这就解释了前两章中我们如何通过simp_blkdev_do_request()函数处理块设备请求的。 我们理解__make_request()函数本意不错,它把bio这条咸鱼嚼成request_queue喂给do_request函数,能让我们的到如下好处: 1:request.buffer不在高端内存 这意味着我们不需要考虑映射高端内存到虚存的情况 2:request.buffer的内存是连续的 因此我们不需要考虑request.buffer对应的内存地址是否分成几段的问题 这些好处看起来都很自然,正如某些行政不作为的“有关部门”认为老百姓纳税养他们也自然, 但不久我们就会看到不很自然的情况。 如果读者是mm,或许会认为一个摔锅把咸鱼嚼好了含情脉脉地喂过来是一件很浪漫的事情(也希望这位读者与作者联系), 但对于大多数男性IT工作者来说,除非取向问题,否则...... 因此现在我们宁可把__make_request()函数一脚踢飞,然后自己去嚼bio这条咸鱼。 当然,踢飞__make_request()函数也意味着摆脱了I/O调度器的处理。 踢飞__make_request()很容易,使用blk_alloc_queue()函数代替blk_init_queue()函数来获取request_queue就行了。 也就是说,我们把原先的 simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL); 改成了 simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL); 这样。 至于嚼人家口水渣的simp_blkdev_do_request()函数,我们也一并扔掉: 把simp_blkdev_do_request()函数从头到尾删掉。 同时,由于现在要脱光,所以上一章中我们费好大劲换上的那件薄内衣也不需要了, 也就是把上一章中增加的elevator_init()这部分的函数也删了,也就是删掉如下部分: 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); 到这里我们已经成功地让__make_request()升空了,但要自己嚼bio,还需要添加一些东西: 首先给request_queue指定我们自己的bio处理函数,这是通过blk_queue_make_request()函数实现的,把这面这行加在blk_alloc_queue()之后: blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request); 然后实现我们自己的simp_blkdev_make_request()函数, 然后编译。 如果按照上述的描述修改出的代码让读者感到信心不足,我们在此列出修改过的simp_blkdev_init()函数: static int __init simp_blkdev_init(void) { int ret; simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL); if (!simp_blkdev_queue) { ret = -ENOMEM; goto err_alloc_queue; } blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request); 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_alloc_queue: return ret; } 这里还把err_init_queue也改成了err_alloc_queue,希望读者不要打算就这一点进行提问。 正如本章开头所述,这一章的内容可能要复杂一些,而现在看来似乎已经做到了。 而现在的进度大概是......一半! 不过值得安慰的是,余下的内容只有我们的simp_blkdev_make_request()函数了。 首先给出函数原型: static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio); 该函数用来处理一个bio请求。 函数接受struct request_queue *q和struct bio *bio作为参数,与请求有关的信息在bio参数中, 而struct request_queue *q并没有经过__make_request()的处理,这也意味着我们不能用前几章那种方式使用q。 因此这里我们关注的是:bio。 关于bio和bio_vec的格式我们仍然不打算在这里做过多的解释,理由同样是因为我们要避免与google出的一大堆文章撞衫。 这里我们只说一句话: bio对应块设备上一段连续空间的请求,bio中包含的多个bio_vec用来指出这个请求对应的每段内存。 因此simp_blkdev_make_request()本质上是在一个循环中搞定bio中的每个bio_vec。 这个神奇的循环是这样的: dsk_mem = simp_blkdev_data + (bio->bi_sector << 9); bio_for_each_segment(bvec, bio, i) { void *iovec_mem; switch (bio_rw(bio)) { case READ: case READA: iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset; memcpy(iovec_mem, dsk_mem, bvec->bv_len); kunmap(bvec->bv_page); break; case WRITE: iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset; memcpy(dsk_mem, iovec_mem, bvec->bv_len); kunmap(bvec->bv_page); break; default: printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": unknown value of bio_rw: %lu\n", bio_rw(bio)); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif return 0; } dsk_mem += bvec->bv_len; } bio请求的块设备起始扇区和扇区数存储在bio.bi_sector和bio.bi_size中, 我们首先通过bio.bi_sector获得这个bio请求在我们的块设备内存中的起始部分位置,存入dsk_mem。 然后遍历bio中的每个bio_vec,这里我们使用了系统提供的bio_for_each_segment宏。 循环中的代码看上去有些眼熟,无非是根据请求的类型作相应的处理。READA意味着预读,精心设计的预读请求可以提高I/O效率, 这有点像内存中的prefetch(),我们同样不在这里做更详细的介绍,因为这本身就能写一整篇文章,对于我们的基于内存的块设备驱动, 只要按照READ请求同样处理就OK了。 在很眼熟的memcpy前后,我们发现了kmap和kunmap这两个新面孔。 这也证明了咸鱼要比烂肉难啃的道理。 bio_vec中的内存地址是使用page *描述的,这也意味着内存页面有可能处于高端内存中而无法直接访问。 这种情况下,常规的处理方法是用kmap映射到非线性映射区域进行访问,当然,访问完后要记得把映射的区域还回去, 不要仗着你内存大就不还,实际上在i386结构中,你内存越大可用的非线性映射区域越紧张。 关于高端内存的细节也请自行google,反正在我的印象中intel总是有事没事就弄些硬件限制给程序员找麻烦以帮助程序员的就业。 所幸的是逐渐流行的64位机的限制应该不那么容易突破了,至少我这么认为。 switch中的default用来处理其它情况,而我们的处理却很简单,抛出一条错误信息,然后调用bio_endio()告诉上层这个bio错了。 不过这个万恶的bio_endio()函数在2.6.24中改了,如果我们的驱动程序是内核的一部分,那么我们只要同步更新调用bio_endio()的语句就行了, 但现在的情况显然不是,而我们又希望这个驱动程序能够同时适应2.6.24之前和之后的内核,因此这里使用条件编译来比较内核版本。 同时,由于使用到了LINUX_VERSION_CODE和KERNEL_VERSION宏,因此还需要增加#include <linux/version.h>。 循环的最后把这一轮循环中完成处理的字节数加到dsk_mem中,这样dsk_mem指向在下一个bio_vec对应的块设备中的数据。 读者或许开始耐不住性子想这一章怎么还不结束了,是的,马上就结束,不过我们还要在循环的前后加上一丁点: 1:循环之前的变量声明: struct bio_vec *bvec; int i; void *dsk_mem; 2:循环之前检测访问请求是否超越了块设备限制: if ((bio->bi_sector << 9) + bio->bi_size > SIMP_BLKDEV_BYTES) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": bad request: block=%llu, count=%u\n", (unsigned long long)bio->bi_sector, bio->bi_size); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif return 0; } 3:循环之后结束这个bio,并返回成功: #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, bio->bi_size, 0); #else bio_endio(bio, 0); #endif return 0; bio_endio用于返回这个对bio请求的处理结果,在2.6.24之后的内核中,第一个参数是被处理的bio指针,第二个参数成功时为0,失败时为-ERRNO。 在2.6.24之前的内核中,中间还多了个unsigned int bytes_done,用于返回搞定了的字节数。 现在可以长长地舒一口气了,我们完工了。 还是附上simp_blkdev_make_request()的完成代码: static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio) { struct bio_vec *bvec; int i; void *dsk_mem; if ((bio->bi_sector << 9) + bio->bi_size > SIMP_BLKDEV_BYTES) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": bad request: block=%llu, count=%u\n", (unsigned long long)bio->bi_sector, bio->bi_size); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif return 0; } dsk_mem = simp_blkdev_data + (bio->bi_sector << 9); bio_for_each_segment(bvec, bio, i) { void *iovec_mem; switch (bio_rw(bio)) { case READ: case READA: iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset; memcpy(iovec_mem, dsk_mem, bvec->bv_len); kunmap(bvec->bv_page); break; case WRITE: iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset; memcpy(dsk_mem, iovec_mem, bvec->bv_len); kunmap(bvec->bv_page); break; default: printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": unknown value of bio_rw: %lu\n", bio_rw(bio)); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif return 0; } dsk_mem += bvec->bv_len; } #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, bio->bi_size, 0); #else bio_endio(bio, 0); #endif return 0; } 读者可以直接用本章的simp_blkdev_make_request()函数替换掉上一章的simp_blkdev_do_request()函数, 然后用本章的simp_blkdev_init()函数替换掉上一章的同名函数,再在文件头部增加#include <linux/version.h>, 就得到了本章的最终代码。 在结束本章之前,我们还是试验一下: 首先还是编译和加载: # make make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step3 modules make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686‘ CC [M] /root/test/simp_blkdev/simp_blkdev_step3/simp_blkdev.o Building modules, stage 2. MODPOST CC /root/test/simp_blkdev/simp_blkdev_step3/simp_blkdev.mod.o LD [M] /root/test/simp_blkdev/simp_blkdev_step3/simp_blkdev.ko make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686‘ # insmod simp_blkdev.ko # 然后使用上一章中的方法看看sysfs中的这个设备的信息: # ls /sys/block/simp_blkdev dev holders range removable size slaves stat subsystem uevent # 我们发现我们的驱动程序在sysfs目录中的queue子目录不见了。 这并不奇怪,否则就要抓狂了。 本章中我们实现自己的make_request函数来处理bio,以此摆脱了I/O调度器和通用的__make_request()对bio的处理。 由于我们的块设备中的数据都是存在于内存中,不牵涉到DMA操作、并且不需要寻道,因此这应该是最适合这种形态的块设备的处理方式。 在linux中类似的驱动程序大多使用了本章中的处理方式,但对大多数基于物理磁盘的块设备驱动来说,使用适合的I/O调度器更能提高性能。 同时,__make_request()中包含的回弹机制对需要进行DMA操作的块设备驱动来说,也能提供不错帮助。 虽然说量变产生质变,通常质变比量变要复杂得多。 同理,相比前一章,把mm衣服脱光也比让她换一件薄一些的衣服要困难得多。 不过无论如何,我们总算连哄带骗地让mm脱下来了,而付出了满头大汗的代价: 本章内容的复杂度相比前一章大大加深了。 如果本章的内容不幸使读者感觉头部体积有所增加的话,作为弥补,我们将宣布一个好消息: 因为根据惯例,随后的1、2章将会出现一些轻松的内容让读者得到充分休息。 <未完,待续> 第4章 +---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | email: [email protected] | +---------------------------------------------------+ | 文章版权归原作者所有。 | | 大家可以自由转载这篇文章,但原版权信息必须保留。 | | 如需用于商业用途,请务必与原作者联系,若因未取得 | | 授权而收起的版权争议,由侵权者自行负责。 | +---------------------------------------------------+ 上一章结束时说过,本章会准备一些不需要动脑子的内容,现在我们开始履行诺言。 看上去简单的事情实际上往往会被弄得很复杂,比如取消公仆们的招待费用问题; 看上去复杂的事情真正做起来也可能很简单,比如本章中要让我们的块设备支持分区操作。 谈到分区,不懂电脑的人想到了去找“专家”帮忙;电脑入门者想到了“高手”这个名词; 渐入佳境者想到了fdisk;资深级玩家想到了dm;红点玩家想到了隐藏的系统恢复区; 程序员想到了分区表;病毒制造者想到了把分区表清空...... 作为块设备驱动程序的设计者,我们似乎需要想的比他们更多一些, 我们大概需要在驱动程序开始识别块设备时访问设备上的分区表,读出里面的数据进行分析, 找出这个块设备中包含哪一类的分区(奇怪吧,但真相是分区表确实有很多种,只是我们经常遇到的大概只有ibm类型罢了)、 几个分区,每个分区在块设备上的区域等信息,再在驱动程序中对每个分区进行注册、创建其管理信息...... 读到这里,正在系鞋带准备溜之大吉的同学们请稍等片刻听我说完, 虽然实际上作者也鼓励同学们多作尝试,甚至是这种无谓的尝试,但本章中的做法却比上述的内容简单得多。 因为这一回linux居然帮了我们的忙,并且不是I/O调度器的那种倒忙。 打开linux代码,我们会在fs/partitions/目录中发现一些文件,这些友好的文件将会默默无闻地帮我们的大忙。 而我们需要做的居然如此简单,还记得alloc_disk()函数吗? 我们一直用1作参数来调用它的,但现在,我们换成64,这意味着设定块设备最大支持63个分区。 然后......不要问然后,因为已经做完了。 当然,如果要让代码看起来漂亮一些的话,我们可以考虑用一个宏来定义最大分区数。 也就是,在文件的头部增加: /* usable partitions is SIMP_BLKDEV_MAXPARTITIONS - 1 */ #define SIMP_BLKDEV_MAXPARTITIONS (64) 然后把 simp_blkdev_disk = alloc_disk(1); 改成 simp_blkdev_disk = alloc_disk(SIMP_BLKDEV_MAXPARTITIONS); 好了,真的改好了。 上一章那样改得太多看起来会让读者不爽,那么这里改得太少,是不是也同样不爽? 大概有关部门深信老百姓接受不了有害物质含量过少的食品,因此制定了食品中三聚氰胺含量的标准。 于是,今后我们大概会制定出一系列标准,比如插入多深才能叫强奸什么的。 为了达到所谓的标准,我们破例补充介绍一下alloc_disk()函数: 这个函数的原型为: struct gendisk *alloc_disk(int minors); 用于申请一个gendisk结构,并做好一些初始化工作。 minors用于指定这个设备使用的次设备号数量,因为第一个次设备号已经用于表示整个块设备了, 因此余下的minors-1个设备号用于表示块设备中的分区,这就限制了这个块设备中的最大可访问分区数。 我们注意“最大可访问分区数”这个词: “最大”虽然指的是上限,但并不意味这是唯一的上限。 极端情况下如果这个块设备只有2个磁道,那么无论minors多大,块设备本身充其量也只能建立2个分区。 这时再谈minors值能到达多少简直就是扯淡,就像腐败不根除,建多少经济适用房都是白搭一样。 “可访问”指的是通过驱动程序可以访问的分区数量,这是因为我们只有那么多次设备号。 但这个数字并不妨碍用户在块设备上面建多少个区。比如我们把minors设定为4,那么最大可访问的分区数量是3, 足够变态的用户完全可以在块设备上建立几十个分区,只不过结果是只能使用前3个分区而已。 现在我们可以试试这个程序了。 与以往相同的是,我们编译和加载这个模块: # make make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step04 modules make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686‘ CC [M] /root/test/simp_blkdev/simp_blkdev_step04/simp_blkdev.o Building modules, stage 2. MODPOST CC /root/test/simp_blkdev/simp_blkdev_step04/simp_blkdev.mod.o LD [M] /root/test/simp_blkdev/simp_blkdev_step04/simp_blkdev.ko make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686‘ # insmod simp_blkdev.ko # 与以往不同的是,这一次加载完模块后,我们并不直接在块设备上创建文件系统,而是进行分区: # fdisk /dev/simp_blkdev Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel Building a new DOS disklabel. Changes will remain in memory only, until you decide to write them. After that, of course, the previous content won‘t be recoverable. Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite) Command (m for help): 关于fdisk我们不打算在这里介绍,因为我们试图让这篇文档看起来专家一些。 使用n命令创建第一个主分区: Command (m for help): n Command action e extended p primary partition (1-4) p Partition number (1-4): 1 First cylinder (1-2, default 1): 1 Last cylinder or +size or +sizeM or +sizeK (1-2, default 2): 1 Command (m for help): 如果细心一些的话,在这里可以看出一个小麻烦,就是:这块磁盘一共只有2个磁道。 因此,我们只好指定第一个分区仅占用1个磁道。毕竟,还要为第2个分区留一些空间。 然后建立第二个分区: Command (m for help): n Command action e extended p primary partition (1-4) p Partition number (1-4): 2 First cylinder (2-2, default 2): 2 Command (m for help): 这一步中由于只剩下1个磁道,fdisk便不再问我们Last cylinder,而是自作主张地把最后一个磁道分配给新的分区。 这时我们的分区情况是: Command (m for help): p Disk /dev/simp_blkdev: 16 MB, 16777216 bytes 255 heads, 63 sectors/track, 2 cylinders Units = cylinders of 16065 * 512 = 8225280 bytes Device Boot Start End Blocks Id System /dev/simp_blkdev1 1 1 8001 83 Linux /dev/simp_blkdev2 2 2 8032+ 83 Linux Command (m for help): 写入分区,退出fdisk: Command (m for help): w The partition table has been altered! Calling ioctl() to re-read partition table. Syncing disks. # 然后我们在这两个分区中创建文件系统 # mkfs.ext3 /dev/simp_blkdev1 mke2fs 1.39 (29-May-2006) Filesystem label= OS type: Linux Block size=1024 (log=0) Fragment size=1024 (log=0) 2000 inodes, 8000 blocks 400 blocks (5.00%) reserved for the super user First data block=1 Maximum filesystem blocks=8388608 1 block group 8192 blocks per group, 8192 fragments per group 2000 inodes per group Writing inode tables: done Creating journal (1024 blocks): done Writing superblocks and filesystem accounting information: done This filesystem will be automatically checked every 27 mounts or 180 days, whichever comes first. Use tune2fs -c or -i to override. # mkfs.ext3 /dev/simp_blkdev2 mke2fs 1.39 (29-May-2006) Filesystem label= OS type: Linux Block size=1024 (log=0) Fragment size=1024 (log=0) 2008 inodes, 8032 blocks 401 blocks (4.99%) reserved for the super user First data block=1 Maximum filesystem blocks=8388608 1 block group 8192 blocks per group, 8192 fragments per group 2008 inodes per group Writing inode tables: done Creating journal (1024 blocks): done Writing superblocks and filesystem accounting information: done This filesystem will be automatically checked every 23 mounts or 180 days, whichever comes first. Use tune2fs -c or -i to override. # 然后mount设两个设备: # mount /dev/simp_blkdev1 /mnt/temp1 # mount /dev/simp_blkdev2 /mnt/temp2 # 看看结果: # mount /dev/hda1 on / type ext3 (rw) proc on /proc type proc (rw) sysfs on /sys type sysfs (rw) devpts on /dev/pts type devpts (rw,gid=5,mode=620) tmpfs on /dev/shm type tmpfs (rw) none on /proc/sys/fs/binfmt_misc type binfmt_misc (rw) /dev/simp_blkdev1 on /mnt/temp1 type ext3 (rw) /dev/simp_blkdev2 on /mnt/temp2 type ext3 (rw) # 然后读/写: # cp /etc/init.d/* /mnt/temp1/ # cp /etc/passwd /mnt/temp2 # ls /mnt/temp1/ NetworkManager avahi-dnsconfd dund ipmi lost+found netfs portmap rpcsvcgssd vncserver NetworkManagerDispatcher bluetooth firstboot iptables lvm2-monitor netplugd psacct saslauthd winbind acpid capi functions irda mcstrans network rdisc sendmail wpa_supplicant anacron conman gpm irqbalance mdmonitor nfs readahead_early setroubleshoot xfs apmd cpuspeed haldaemon isdn mdmpd nfslock readahead_later single ypbind atd crond halt kdump messagebus nscd restorecond smartd yum-updatesd auditd cups hidd killall microcode_ctl ntpd rhnsd sshd autofs cups-config-daemon hplip krb524 multipathd pand rpcgssd syslog avahi-daemon dhcdbd ip6tables kudzu netconsole pcscd rpcidmapd vmware-tools # ls /mnt/temp2 lost+found passwd # 收尾工作: # umount /dev/temp1 # umount /dev/temp2 # rmmod simp_blkdev # 看起来本章应该结束了,但为了耽误大家更多的时间,我们来回忆一下刚才出现的小麻烦。 我们发现这块磁盘只有2个磁道,由于分区是以磁道为边界的,因此最大只能创建2个分区。 不过谢天谢地,好歹我们能够证明我们的程序是支持“多个”分区的......尽管只有2个。 那么为什么系统会认为我们的块设备只有2个磁道呢?其实这不怪系统,因为我们根本没有告诉系统我们的磁盘究竟有多少个磁道。 因此系统只好去猜、猜、猜,结果就猜成2个磁道了。 好吧,说的细节一些,传统的磁盘使用8个位表示盘面数、6个位表示每磁道扇区数、10个位表示磁道数,因此盘面、每磁道扇区、磁道的最大数值分别为255、63和1023。 这也是传说中启动操作系统时的1024柱面(磁道)和硬盘容量8G限制的根源。 现代磁盘采用线性寻址方式突破了这一限制,从本质上说,如果你的机器还没生锈,那么你的硬盘无论是内部结构还是访问方式都与常识中的盘面、每磁道扇区、磁道无关。 但为了与原先的理解兼容,对于现代磁盘,我们在访问时还是假设它具有传统的结构。目前比较通用的假设是:所有磁盘具有最大数目的(也就是恒定的)盘面和每磁道扇区数,而磁盘大小与磁道数与成正比。 因此,对于一块80G的硬盘,根据假设,这块磁盘的盘面和每磁道扇区数肯定是255和63,磁道数为:80*1024*1024*1024/512(字节每扇区)/255(盘面数)/63(每磁道扇区数)=10043(小数部分看作不完整的磁道被丢弃)。 话归原题,在驱动程序中我们指定了磁盘大小为16M,共包含16*1024*1024/512=32768个扇区。假设这块磁盘具有最大盘面和每磁道扇区数后,它的磁道数就是:32768/255/63=2。 我们看起开应该很happy,因为系统太看得起我们了,竟然把我们的块设备看成现代磁盘进行磁道数的换算处理。 不过我们也可能unhappy,因为这造成磁盘最大只能被分成2个区。(至于为什么分区以磁道作为边界,可以想象一下磁盘的结构) 但我们的磁盘只有区区16M啊,所以最好还是告诉系统我们的磁盘没有那么多的盘面数和每磁道扇区数,这将让磁道数来得多一些。 在下一章中,我们打算搞定这个问题。 <未完,待续> 第5章 +---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | email: [email protected] | +---------------------------------------------------+ | 文章版权归原作者所有。 | | 大家可以自由转载这篇文章,但原版权信息必须保留。 | | 如需用于商业用途,请务必与原作者联系,若因未取得 | | 授权而收起的版权争议,由侵权者自行负责。 | +---------------------------------------------------+ 既然上一章结束时我们已经预告了本章的内容, 那么本章中我们就让这个块设备有能力告知操作系统它的“物理结构”。 当然,对于基于内存的块设备来说,什么样的物理结构并不重要, 这就如同从酒吧带mm回家时不需要打听她的姓名一样。 但如果不幸遇到的是兼职,并且带她去不入流的招待所时, 建议最好还是先串供一下姓名、生日和职业等信息, 以便JJ查房时可以伪装成情侣。 同样,如果要实现的是真实的物理块设备驱动, 那么返回设备的物理结构时大概不能这么随意。 对于块设备驱动程序而言,我们现在需要关注那条目前只有一行的struct block_device_operations simp_blkdev_fops结构。 到目前为止,它存在的目的仅仅是因为它必须存在,但马上我们将发现它存在的另一个目的:为块设备驱动添加获得块设备物理结构的接口。 对于具有极强钻研精神的极品读者来说,大概在第一章中就会自己去看struct block_device_operations结构,然后将发现这个结构其实还挺复杂: struct block_device_operations { int (*open) (struct block_device *, fmode_t); int (*release) (struct gendisk *, fmode_t); int (*locked_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long); int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long); int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long); int (*direct_access) (struct block_device *, sector_t, void **, unsigned long *); int (*media_changed) (struct gendisk *); int (*revalidate_disk) (struct gendisk *); int (*getgeo)(struct block_device *, struct hd_geometry *); struct module *owner; }; 在前几章中,我们邂逅过其中的owner成员变量,它用于存储这个结构的所有者,也就是我们的模块,因此我们做了如下的赋值: .owner = THIS_MODULE, 而这一章中,我们将与它的同胞妹妹------getgeo也亲密接触一下。 我们要做的是: 1:在block_device_operations中增加getgeo成员变量初值的设定,指向我们的“获得块设备物理结构”函数。 2:实现我们的“获得块设备物理结构”函数。 第一步很简单,我们暂且为“获得块设备物理结构”函数取个名字叫simp_blkdev_getgeo()吧,也避免了在下文中把这么一大堆汉字拷来拷去。 在simp_blkdev_fops中添加.getgeo指向simp_blkdev_getgeo,也就是把simp_blkdev_fops结构改成这个样子: struct block_device_operations simp_blkdev_fops = { .owner = THIS_MODULE, .getgeo = simp_blkdev_getgeo, }; 第二步难一些,但也难不到哪去,在代码中的struct block_device_operations simp_blkdev_fops这行之前找个空点的场子,把如下函数插进去: static int simp_blkdev_getgeo(struct block_device *bdev, struct hd_geometry *geo) { /* * capacity heads sectors cylinders * 0~16M 1 1 0~32768 * 16M~512M 1 32 1024~32768 * 512M~16G 32 32 1024~32768 * 16G~... 255 63 2088~... */ if (SIMP_BLKDEV_BYTES < 16 * 1024 * 1024) { geo->heads = 1; geo->sectors = 1; } else if (SIMP_BLKDEV_BYTES < 512 * 1024 * 1024) { geo->heads = 1; geo->sectors = 32; } else if (SIMP_BLKDEV_BYTES < 16ULL * 1024 * 1024 * 1024) { geo->heads = 32; geo->sectors = 32; } else { geo->heads = 255; geo->sectors = 63; } geo->cylinders = SIMP_BLKDEV_BYTES>>9/geo->heads/geo->sectors; return 0; } 因为这里我们用到了struct hd_geometry结构,所以还要增加一行#include <linux/hdreg.h>。 这个函数的目的,是选择适当的物理结构信息装入struct hd_geometry *geo结构。 当然,为了克服上一章中只能分成2个区的问题,我们应该尽可能增加磁道的数量。 希望读者不要理解成分几个区就需要几个磁道,这意味着一个磁道一个区,也意味着每个区必须一般大小。 由于分区总是以磁道为边界,尽可能增加磁道的数量不仅仅是为了让块设备容纳更多的分区, 更重要的是让分区的实际大小更接近于分区时的指定值,也就是提高实际做出的分区容量的精度。 不过对于设置的物理结构值,还存在一个限制,就是struct hd_geometry中的数值上限。 我们看struct hd_geometry的内容: struct hd_geometry { unsigned char heads; unsigned char sectors; unsigned short cylinders; unsigned long start; }; unsigned char的磁头数和每磁道扇区数决定了其255的上限,同样,unsigned short的磁道数决定了其65535的上限。 这还不算,但在前一章中,我们知道对于现代硬盘,磁头数和每磁道扇区数通常取的值是255和63, 再组合上这里的65535的磁道数上限,hd_geometry能够表示的最大块设备容量是255*63*65535*512/1024/1024/1024=502G。 显然目前linux支持的最大硬盘容量大于502G,那么对于这类块设备,内核是如何通过hd_geometry结构表示其物理结构的呢? 诀窍不在内核,而在于用户态程序如fdisk等通过内核调用获得hd_geometry结构后, 会舍弃hd_geometry.cylinders内容,取而代之的是直接通过hd_geometry中的磁头数和每磁道扇区数以及硬盘大小去计算磁道数。 因此对于超过502G的硬盘,由于用户程序得出的磁道数与hd_geometry.cylinders无关,所以我们往往在fdisk中能看到这块硬盘的磁道数大于65535。 刚才扯远了,现在言归正题,我们决定让这个函数对于任何尺寸的块设备,总是试图返回比较漂亮的物理结构。 漂亮意味着返回的物理结构既要保证拥有足够多的磁道,也要保证磁头数和每磁道扇区数不超过255和63,同时最好使用程序员看起来比较顺眼的数字, 如:1、2、4、8、16、32、64等。 当然,我们也希望找到某个One Shot公式适用于所有大小的块设备,但很遗憾目前作者没找到,因此采用了分段计算的方法: 首先考虑容量很小的块设备: 即使磁头数和每磁道扇区数都是1,磁道数也不够多时,我们会将磁头数和每磁道扇区数都固定为1,以使磁道数尽可能多,以提高分区的精度。 因此磁道数随块设备容量而上升。 虽然我们已经知道了磁道数其实可以超过unsigned short的65535上限,但在这里却没有必要,因此我们要给磁道数设置一个上限。 因为不想让上限超过65535,同时还希望上限也是一个程序员喜欢的数字,因此这里选择了32768。 当然,当磁道数超过32768时,已经意味着块设备容量不那么小了,也就没有必要使用这种情况中如此苛刻的磁头数和每磁道扇区数了。 简单来说,当块设备容量小于1个磁头、每磁道1扇区和32768个磁道对应的容量--也就是16M时,我们将按照这种情况处理。 然后假设块设备容量已经大于16M了: 我们希望保证块设备包含足够多的磁道,这里我们认为1024个磁道应该不少了。 磁道的最小值发生在块设备容量为16M的时候,这时使用1024作为磁道数,可以计算出磁头数*每磁道扇区数=32。 这里暂且把磁头数和每磁道扇区数固定为1和32,而让磁道数随着块设备容量的增大而增加。 同时,我们还是磁道的上限设置成32768,这时的块设备容量为512M。 总结来说,当块设备容量在16M和512M之间时,我们把磁头数和每磁道扇区数固定为1和32。 然后对于容量大于512M的块设备: 与上述处理相似,当块设备容量在512M和16G之间时,我们把磁头数和每磁道扇区数固定为32和32。 最后的一种情况: 块设备已经足够大了,大到即使我们使用磁头数和每磁道扇区数的上限, 也能获得足够多的磁道数。这时把磁头数和每磁道扇区数固定为255和63。 至于磁道数就算出多少是多少了,即使超过unsigned short的上限也无所谓,反正用不着。 随着这个函数解说到此结束,我们对代码的修改也结束了。 现在开始试验: 编译和加载: # make make -C /lib/modules/2.6.27.4/build SUBDIRS=/mnt/host_test/simp_blkdev/simp_blkdev_step05 modules make[1]: Entering directory `/mnt/ltt-kernel‘ CC [M] /mnt/host_test/simp_blkdev/simp_blkdev_step05/simp_blkdev.o Building modules, stage 2. MODPOST 1 modules CC /mnt/host_test/simp_blkdev/simp_blkdev_step05/simp_blkdev.mod.o LD [M] /mnt/host_test/simp_blkdev/simp_blkdev_step05/simp_blkdev.ko make[1]: Leaving directory `/mnt/ltt-kernel‘ # insmod simp_blkdev.ko # 用fdisk打开设备文件 # fdisk /dev/simp_blkdev Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel Building a new DOS disklabel. Changes will remain in memory only, until you decide to write them. After that, of course, the previous content won‘t be recoverable. Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite) Command (m for help): 看看设备的物理结构: Command (m for help): p Disk /dev/simp_blkdev: 16 MB, 16777216 bytes 1 heads, 32 sectors/track, 1024 cylinders Units = cylinders of 32 * 512 = 16384 bytes Device Boot Start End Blocks Id System Command (m for help): 我们发现,现在的设备有1个磁头、32扇区每磁道、1024个磁道。 这是符合代码中的处理的。 本章的内容也不是太难,连同上一章,我们已经休息2章了。 聪明的读者可能已经猜到作者打算说什么了。 不错,下一章会有一个surprise。 <未完,待续> 第6章 +---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | email: [email][email protected][/email] | +---------------------------------------------------+ | 文章版权归原作者所有。 | | 大家可以自由转载这篇文章,但原版权信息必须保留。 | | 如需用于商业用途,请务必与原作者联系,若因未取得 | | 授权而收起的版权争议,由侵权者自行负责。 | +---------------------------------------------------+ 经历了内容极为简单的前两章的休息,现在大家一定感到精神百倍了。 作为已经坚持到现在的读者,对接下去将要面临的内容大概应该能够猜得八九不离十了, 具体的内容猜不出来也无妨,但一定将是具有增加颅压功效的。 与物理块设备驱动程序的区别在于,我们的驱动程序使用内存来存储块设备中的数据。 到目前为止,我们一直都是使用这样一个静态数组来担负这一功能的: unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES]; 如果读者懂得一些模块的知识,或者现在赶紧去临时抱佛脚google一些模块知识, 应该知道模块其实是加载在非线性映射区域的。 详细来说,在加载模块时,根据模块的ELF信息(天哪,又要去google elf了),确定这个模块所需的静态内存大小。 这些内存用来容纳模块的二进制代码,以及静态变量。然后申请容纳这些数据的一大堆页面。 当然,这些页面并不是连续的,而代码和变量却不可能神奇到即使被切成一块一块的也能正常工作。 因此需要在非线性映射区域中找到一块连续的地址(现在有要去google非线性映射区域了),用来将刚才申请到的一块一块的内存页映射到这个地址段中。 最后模块被请到这段区域中,然后执行模块的初始化函数...... 现在看我们这个模块中的simp_blkdev_data变量,如果不是现在刻意关注,这个变量看起来显得那么得普通。 正如其它的一些名字原先也是那么的普通,但由于一些突发的事件受到大家的热烈关注, 比如一段视频让我们熟悉了kappa和陆佳妮,比如呼吸税让我们认识了蒋有绪。 现在我们开始关注simp_blkdev_data变量了,导火索是刚才介绍的非线性映射区域。 模块之所以被加载到非线性映射区域,是因为很难在线性映射区域中得到加载模块所需的连续的内存。 但使用非线性映射区域也并非只赚不赔的生意,至少在i386结构中,非线性映射区域实在是太小了。 在物理内存大于896M的i386系统中,整个非线性映射区域不会超过128M。 相反如果物理内存小于896M(不知道该算是幸运还是不幸),非线性映射区域反而会稍微大一些,这种情况我想我们可以不用讨论了,毕竟不能为了加载一个模块去拔内存。 因此我们的结论是:非线性映射区域是很紧张的资源,我们要节约使用。 而像我们现在这个模块中的simp_blkdev_data却是个如假包换的反面典型,居然上来就吃掉了16M!这还是因为我们没有把SIMP_BLKDEV_BYTES定义得更大。 现在我们开始列举simp_blkdev_data的种种罪行: 1:剩余的非线性映射区域较小时导致模块加载失败 2:模块加载后占用了大量的非线性映射区域,导致其它模块加载失败。 3:模块加载后占用了大量的非线性映射区域,影响系统的正常运行。 这是因为不光模块,系统本身的很多功能也依赖非线性映射区域空间。 对于这样的害群之马,我们难道还有留下他的理由吗? 本章的内容虽然麻烦一些,但想到能够一了百了地清除这个体大膘肥的simp_blkdev_data,倒也相当值得。 也希望今后能够看到在对贪官的处理上,能够也拿出这样的魄力和勇气。 现在在清除simp_blkdev_data的问题上,已经不存在什么悬念了,接下来我们需要关注的是将simp_blkdev_data碎尸万段后,拿出一个更恰当方法来代替它。 首先,我们决定不用静态声明的数组,而改用动态申请的内存。 其次,使用类似vmalloc()的函数可以动态申请大段内存,但其实这段内存占用的还是非线性映射区域,就好像用一个比较隐蔽的贪官来代替下马的贪官,我们不会愚蠢在这种地步。 剩下的,就是在线性映射区域申请很多个页的内存,然后自己去管理。这个方法一了百了地解决了使用大段非线性映射区域的问题,而唯一的问题是由于需要自己管理申请到的页面,使程序复杂了不少。 但为了整个系统的利益,这难道不是我们该做的吗? 申请一个内存页是很容易的,这里我们将采用所有容易的方法中最容易的那个: __get_free_page函数,原型是: unsigned long __get_free_page(gfp_t gfp_mask); 这个函数用来申请一个页面的内存。gfp_mask包含一些对申请内存时的指定,比如,要在DMA区域中啦、必须清零等。 我们这里倒是使用最常见的__get_free_page(GFP_KERNEL)就可以了。 通过__get_free_page申请到了一大堆内存页,新的问题来了,在读写块设备时,我们得到是块设备的偏移,如何快速地通过偏移找到对应的内存页呢? 最简单的方法是建立一个数组,用来存放偏移到内存的映射,数组中的每项对应一个一个页: 数组定义如下: void *simp_blkdev_data[(SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) / PAGE_SIZE]; PAGE_SIZE是系统中每个页的大小,对i386来说,通常是4K,那堆加PAGE_SIZE减1的代码是考虑到SIMP_BLKDEV_BYTES不是PAGE_SIZE的整数倍时要让末尾的空间也能访问。 然后申请内存的代码大概是: for (i=0; i < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) / PAGE_SIZE; i++) { p = (void *)__get_free_page(GFP_KERNEL); simp_blkdev_data[i] = p; } 通过块设备偏移得到内存中的数据地址的代码大概是: mem_addr = simp_blkdev_data[dev_addr/PAGE_SIZE] + dev_addr % PAGE_SIZE; 这种方法实现起来还是比较简单的,但缺点也不是没有:存放各个页面地址的数组虽然其体积比原先那个直接存放数据的数组已经缩小了很多, 但本身毕竟还是在非线性映射区域中。如果块设备大小为16M,在i386上,需要4096个页面,数组大小16K,这不算太大。 但如果某个疯子打算建立一个2G的虚拟磁盘,数组大小将达到2M,这就不算小了。 或者我们可以不用数组,而用链表来存储偏移到内存页的映射关系,这样可以回避掉数组存在的问题,但在链表中查找指定元素却不是一般的费时, 毕竟我们不希望用户觉得这是个软盘。 接下来作者不打断继续卖关子了,我们最终选择使用的是传说中的基树。 关于linux中基树细节的文档不多,特别是中文文档更少,更糟的是我们这篇文档也不打算作详细的介绍(因为作者建议去RTFSC)。 但总的来说,相对于二叉平衡树的红黑树来说,基树是一个n叉(一般为64叉)非平衡树,n叉减少了搜索的深度,非平衡减少了复杂的平衡操作。 当然,这两个特点也不是仅仅带来优点,但在这里我们就视而不见了,毕竟我们已经选择了基树,因此护短也是自认而然的事情,正如公仆护着王细牛一样。 从功能上来说,基树好像是为我们量身定做的一样,好用至极。 (其实我们也可以考虑选择红黑树和哈希表来实现这个功能,感兴趣的读者可以了解一下) 接下来的代码中,我们将要用到基树种的如下函数: void INIT_RADIX_TREE((struct radix_tree_root *root, gfp_t mask); 用来初始化一个基树的结构,root是基树结构指针,mask是基树内部申请内存时使用的标志。 int radix_tree_insert(struct radix_tree_root *root, unsigned long index, void *item); 用来往基树中插入一个指针,index是指针的索引,item是指针,将来可以通过index从基树中快速获得这个指针的值。 void *radix_tree_delete(struct radix_tree_root *root, unsigned long index); 用来根据索引从基树中删除一个指针,index是指针的索引。 void *radix_tree_lookup(struct radix_tree_root *root, unsigned long index); 用来根据索引从基树中查找对应的指针,index是指针的索引。 其实基树的功能不仅限于此,比如,还可以给指针设定标志,详情还是请去读linux/lib/radix-tree.c 现在开始改造我们的代码: 首先删除那个无耻的数组: unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES]; 然后引入它的替代者--一个基树结构: static struct radix_tree_root simp_blkdev_data; 然后增加两个函数,用来申请和释放块设备的内存: 申请内存的函数如下: int alloc_diskmem(void) { int ret; int i; void *p; INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL); for (i = 0; i < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) >> PAGE_SHIFT; i++) { p = (void *)__get_free_page(GFP_KERNEL); if (!p) { ret = -ENOMEM; goto err_alloc; } ret = radix_tree_insert(&simp_blkdev_data, i, p); if (IS_ERR_VALUE(ret)) goto err_radix_tree_insert; } return 0; err_radix_tree_insert: free_page((unsigned long)p); err_alloc: free_diskmem(); return ret; } 先初始化基树结构,然后申请需要的每一个页面,按照每页面的次序作为索引,将指针插入基树。 代码中的“>> PAGE_SHIFT”与“/ PAGE_SIZE”作用相同, if (不明白为什么要这样) do_google(); 释放内存的函数如下: void free_diskmem(void) { int i; void *p; for (i = 0; i < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) >> PAGE_SHIFT; i++) { p = radix_tree_lookup(&simp_blkdev_data, i); radix_tree_delete(&simp_blkdev_data, i); /* free NULL is safe */ free_page((unsigned long)p); } } 遍历每一个索引,得到页面的指针,释放页面,然后从基树中释放这个指针。 由于alloc_diskmem()函数在中途失败时需要释放申请过的页面,因此我们把free_diskmem()函数设计成能够释放建立了一半的基树的形式。 对于只建立了一半的基树而言,有一部分索引对应的指针还没来得及插入基树,对于不存在的索引,radix_tree_delete()函数会返回NULL,幸运的是free_page()函数能够忽略传入的NULL指针。 因为alloc_diskmem()函数需要调用free_diskmem()函数,在代码中需要把free_diskmem()函数写在alloc_diskmem()前面,或者在文件头添加函数的声明。 然后在模块的初始化和释放函数中添加对alloc_diskmem()和free_diskmem()的调用, 也就是改成这个样子: static int __init simp_blkdev_init(void) { int ret; simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL); if (!simp_blkdev_queue) { ret = -ENOMEM; goto err_alloc_queue; } blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request); simp_blkdev_disk = alloc_disk(SIMP_BLKDEV_MAXPARTITIONS); if (!simp_blkdev_disk) { ret = -ENOMEM; goto err_alloc_disk; } ret = alloc_diskmem(); if (IS_ERR_VALUE(ret)) goto err_alloc_diskmem; 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_diskmem: put_disk(simp_blkdev_disk); err_alloc_disk: blk_cleanup_queue(simp_blkdev_queue); err_alloc_queue: return ret; } static void __exit simp_blkdev_exit(void) { del_gendisk(simp_blkdev_disk); free_diskmem(); put_disk(simp_blkdev_disk); blk_cleanup_queue(simp_blkdev_queue); } 最麻烦的放在最后: 我们需要修改simp_blkdev_make_request()函数,让它适应新的数据结构。 原先的实现中,对于一个bio_vec,我们找到对应的内存中数据的起点,直接传送bvec->bv_len个字节就大功告成了,比如,读块设备时就是: memcpy(iovec_mem, dsk_mem, bvec->bv_len); 但现在由于容纳数据的每个页面地址是不连续的,因此可能出现bio_vec中的数据跨越页面边界的情况。 也就是说,一个bio_vec中的数据的前半段在一个页面中,后半段在另一个页面中。 虽然这两个页面对应的块设备地址连续,但在内存中的地址不一定连续,因此像原先那样简单使用memcpy看样子是解决不了问题了。 实际上,虽然bio_vec可能跨越页面边界,但它无论如何也不可能跨越2个以上的页面。 这是因为bio_vec本身对应的数据最大长度只有一个页面。 因此如果希望做最简单的实现,只要在代码中做一个条件判断就OK了: if (没有跨越页面) { 1个memcpy搞定 } else { /* 肯定是跨越2个页面了 */ 2个memcpy搞定 } 但为了表现出物理设备一次传送1个扇区数据的处理方式(这种情况下一个bio_vec可能会跨越2个以上的扇区),我们让代码支持2个以上页面的情况。 首先列出修改后的simp_blkdev_make_request()函数: static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio) { struct bio_vec *bvec; int i; unsigned long long dsk_offset; if ((bio->bi_sector << 9) + bio->bi_size > SIMP_BLKDEV_BYTES) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": bad request: block=%llu, count=%u\n", (unsigned long long)bio->bi_sector, bio->bi_size); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif return 0; } dsk_offset = bio->bi_sector << 9; bio_for_each_segment(bvec, bio, i) { unsigned int count_done, count_current; void *iovec_mem; void *dsk_mem; iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset; count_done = 0; while (count_done < bvec->bv_len) { count_current = min(bvec->bv_len - count_done, (unsigned int)(PAGE_SIZE - ((dsk_offset + count_done) & ~PAGE_MASK))); dsk_mem = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) >> PAGE_SHIFT); if (!dsk_mem) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": search memory failed: %llu\n", (dsk_offset + count_done) >> PAGE_SHIFT); kunmap(bvec->bv_page); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif return 0; } dsk_mem += (dsk_offset + count_done) & ~PAGE_MASK; switch (bio_rw(bio)) { case READ: case READA: memcpy(iovec_mem + count_done, dsk_mem, count_current); break; case WRITE: memcpy(dsk_mem, iovec_mem + count_done, count_current); break; default: printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": unknown value of bio_rw: %lu\n", bio_rw(bio)); kunmap(bvec->bv_page); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif return 0; } count_done += count_current; } kunmap(bvec->bv_page); dsk_offset += bvec->bv_len; } #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, bio->bi_size, 0); #else bio_endio(bio, 0); #endif return 0; } 看样子长了一些,但不要被吓着了,因为读的时候我们可以对代码做一些简化: 1:去掉乱七八糟的出错处理 2:无视每行80字符限制 3:把比特运算改成等价但更易读的乘除运算 4:无视碍眼的类型转换 5:假设内核版本大于2.6.24,以去掉判断版本的宏 就会变成这样了: static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio) { struct bio_vec *bvec; int i; unsigned long long dsk_offset; dsk_offset = bio->bi_sector * 512; bio_for_each_segment(bvec, bio, i) { unsigned int count_done, count_current; void *iovec_mem; void *dsk_mem; iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset; count_done = 0; while (count_done < bvec->bv_len) { count_current = min(bvec->bv_len - count_done, PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE); dsk_mem = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) / PAGE_SIZE); dsk_mem += (dsk_offset + count_done) % PAGE_SIZE; switch (bio_rw(bio)) { case READ: case READA: memcpy(iovec_mem + count_done, dsk_mem, count_current); break; case WRITE: memcpy(dsk_mem, iovec_mem + count_done, count_current); break; } count_done += count_current; } kunmap(bvec->bv_page); dsk_offset += bvec->bv_len; } bio_endio(bio, 0); return 0; } 是不是清楚多了? dsk_offset用来存储当前要处理的数据在块设备上的偏移,初始值是bio->bi_sector * 512,也就是起始扇区对应的偏移,也是第一个bio_vec对应的块设备偏移。 每处理完成一个bio_vec时,dsk_offset值会被更新:dsk_offset += bvec->bv_len,以指向将要处理的数据在块设备上的偏移。 在bio_for_each_segment()中代码的起始和末尾,执行kmap和kunmap开映射当前这个bio_vec的内存,这个知识在前面的章节中已经提到了, 这个处理的结果是iovec_mem指向当前的bio_vec中的缓冲区。 现在在kmap和kunmap之间的代码的功能已经很明确了,就是完成块设备上偏移为dsk_offset、长度为bvec->bv_len的数据与iovec_mem地址之间的传送。 假设不考虑bio_vec跨越页面边界的情况,这段代码应该十分写意: dsk_mem = radix_tree_lookup(&simp_blkdev_data, dsk_offset / PAGE_SIZE) + dsk_offset % PAGE_SIZE; switch (bio_rw(bio)) { case READ: case READA: memcpy(iovec_mem, dsk_mem, bvec->bv_len); break; case WRITE: memcpy(dsk_mem, iovec_mem, bvec->bv_len); break; } 首先使用dsk_offset / PAGE_SIZE、也就是块设备偏移在内存中数据所位于的页面次序作为索引,查找该页的内存起始地址, 然后加上块设备偏移在该页内的偏移、也就是dsk_offset % PAGE_SIZE, 就得到了内存中数据的地址,然后就是简单的数据传送。 关于块设备偏移到内存地址的转换,我们举个例子: 假使模块加载时我们分配的第1个页面的地址为0xd0000000,用于存放块设备偏移为0~4095的数据 第2个页面的地址为0xd1000000,用于存放块设备偏移为4096~8191的数据 第3个页面的地址为0xc8000000,用于存放块设备偏移为8192~12287的数据 第4个页面的地址为0xe2000000,用于存放块设备偏移为12288~16383的数据 对于块设备偏移为9000的数据,首先通过9000 / PAGE_SIZE确定它位于第3个页面中, 然后使用radix_tree_lookup(&simp_blkdev_data, 3)将查找出0xc8000000这个地址。 这是第3个页面的起始地址,这个地址的数据在块设备中的偏移是8192, 因此我们还要加上块设备偏移在页内的偏移量,也就是9000 % PAGE_SIZE = 808, 得到的才是块设备偏移为9000的数据在内存中的数据地址。 当然,假设终归是假设,往往大多数情况下是自欺欺人的,就好像彩迷总喜欢跟女友说如果中了500万,就要怎么怎么对她好一样。 现在回到残酷的现实,我们还是要去考虑bio_vec跨越页面边界的情况。 这意味着对于一个bio_vec,我们将有可能传送多次。 为了记录前几次中已经完成的数据量,我们引入了一个新的变量,叫做count_done。 在进行bio_vec内的第一次传送前,count_done的值是0,每完成一次传送,count_done将加上这次完成的数据量。 当count_done == bvec->bv_len时,就是大功告成的之日。 接着就是边界的判断。 当前块设备偏移所在的内存页中,块设备偏移所在位置到页头的距离为: offset % PAGE_SIZE 块设备偏移所在位置到页尾的距离为: PAGE_SIZE - offset % PAGE_SIZE 这个距离也就是不超越页边界时所能传送的数据的最大值。 因此在bio_vec内的每一次中,我们使用 count_current = min(bvec->bv_len - count_done, PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE); 来确定这一次传送的数据量。 bvec->bv_len - count_done指的是余下需要传送的数据总量, PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE指的是从当前块设备偏移开始、不超越页边界时所能传送的数据的最大值。 如果bvec->bv_len - count_done > PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE,说明这一次将传送从当前块设备偏移到其所在内存页的页尾之间的数据, 余下的数据位于后续的页面中,将在接下来的循环中搞定, 如果bvec->bv_len - count_done <= PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE,那么可喜可贺,这将是当前bio_vec的最后一次传送,完成后就可以回家洗澡了。 结合以上的说明,我想应该不难看懂simp_blkdev_make_request()的代码了,而我们的程序也已经大功告成。 现在总结一下修改的位置: 1:把unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];换成static struct radix_tree_root simp_blkdev_data; 2:把本文中的free_diskmem()和alloc_diskmem()函数添加到代码中,虽然没有特别意义,但建议插在紧邻simp_blkdev_init()之前的位置。 但有特别意义的是free_diskmem()和alloc_diskmem()的顺序,如果读者到这里还打算提问是什么顺序,作者可要哭了。 3:把simp_blkdev_make_request()、simp_blkdev_init()和simp_blkdev_exit()函数替换成文中的代码。 注意不要企图使用简化过的simp_blkdev_make_request()函数,否则造成的后果:从程序编译失败到读者被若干美女轮奸,作者都概不负责。 从常理分析,在修改完程序后,我们都将试验一次修改的效果。 这次也不例外,因为审判彭宇的王法官也是这么推断的。 首先证明我们的模块至今为止仍然经得起编译、能够加载: # make make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step06 modules make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686‘ CC [M] /root/test/simp_blkdev/simp_blkdev_step06/simp_blkdev.o Building modules, stage 2. MODPOST CC /root/test/simp_blkdev/simp_blkdev_step06/simp_blkdev.mod.o LD [M] /root/test/simp_blkdev/simp_blkdev_step06/simp_blkdev.ko make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686‘ # insmod simp_blkdev.ko # 看看模块的加载时分配的非线性映射区域大小: # lsmod Module Size Used by simp_blkdev 8212 0 ... # 如果这个Size一栏的数字没有引起读者的足够重视的话,我们拿修改前的模块来对比一下: # lsmod Module Size Used by simp_blkdev 16784392 0 看出区别了没? 如果本章到这里还不结束的话,估计读者要开始闪人了。 好的,我们马上就结束,希望在这之前闪掉的读者不要太多。 由于还没有来得及闪掉而看到这段话的读者们,作者相信你们具有相当的毅力。 学习是需要毅力的,这时的作者同样也需要毅力来坚持完成这本教程。 最后还是希望读者坚持,坚持看完所有的章节,坚持在遇到每一个不明白的问题时都努力寻求答案, 坚持在发现自己感兴趣的内容时能够深入地去了解、探寻、思考。 <未完,待续> 第7章 +---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | email: [email protected] | +---------------------------------------------------+ | 文章版权归原作者所有。 | | 大家可以自由转载这篇文章,但原版权信息必须保留。 | | 如需用于商业用途,请务必与原作者联系,若因未取得 | | 授权而收起的版权争议,由侵权者自行负责。 | +---------------------------------------------------+ 上一章中我们对驱动程序做了很大的修改,单独分配每一页的内存,然后使用基树来进行管理。 这使得驱动程序占用的非线性映射区域大大减少,让它看起来朝优秀的代码又接近了一些。 因为优秀的代码是相似的,糟糕的代码却各有各的糟糕之处。 本章中我们将讨论一些细枝末节的问题,算是对上一章中内容的巩固,也是为后面的章节作一些铺垫。 首先聊一聊低端内存、高端内存和非线性映射区域的问题: 在i386结构中,由于任务使用32位寄存器表示地址,这造成每个任务的最大寻址范围是4G。 无论任务对应的是用户程序还是内核代码,都逃脱不了这个限制。 让问题更糟糕的是,普通的linux内核又将4G的地址划分为2个部分,前3G让用户空间程序使用,后1G由内核本身使用。 这又将内核实际使用的空间压缩了4倍。 不过linux采用这样的方案倒也不是由于开发者脑瘫,因为这样一来,内核可以与用户进程共用同一个页表, 因而在进行用户态和内核态的切换时不必刷新页表,提高了系统的效率。 而带来的麻烦就是内核只有1G的地址范围可用。 其实也有一个相当出名的4G+4G的patch,就是采用上述相反的方法,让内核与用户进程使用独立的地址空间,其优缺点也正好与现在的实现相反。 但这毕竟不是标准内核的情况,对大多数系统而言,我们不得不接受内核只有1G的地址范围可用的现实。 然后我们再来看内核如何使用这1G的地址范围。 作为内核,当然需要有能力访问到所有的物理内存,而在保护模式下,内存需要通过页表映射到一个虚拟地址上,再进行访问。 虽然内核可以在访问任何物理内存时都采用映射->访问->取消映射的方法,但这很可能将任意一台机器彻底变成386的速度。 因此,内核一般把尽可能多的物理内存事先映射到它的地址空间中去,这里的“尽可能多”指的是896M。 原因是内核手头只有1G的地址空间,而其中的128M还需要留作非线性映射空间。 这样一来,内核地址空间中的3G~3G+896M便映射了0~896M范围的物理内存。 这个映射关系在启动系统时完成,并且在系统启动后不会改变。 物理内存中0~896M的这段空间是幸运的,因为它们在内核空间中有固定的住所, 这也使它们能够方便、快速地被访问。相对896M以上的物理内存,它们地址是比较低的, 正因为此,我们通常把这部分内存区域叫做低端内存。 但地址高于896M的物理内存就没这么幸运了。 由于它们没有在启动时被固定映射到内核空间的地址空间中,我们需要在访问之前对它们进行映射。 但映射到哪里呢?幸好内核没有把整个1G的地址空间都用作映射上面所说的低端内存,好歹还留下128M。 其实这128M还是全都能用,在其开头和结尾处还有一些区域拿去干别的事情了(希望读者去详细了解一下), 所以我们可以用这剩下的接近128M的区域来映射高于896M的物理内存。 明显可以看出这时是僧多粥少,所以这部分区域最好应该节约使用。 但希望读者不要把访问高于896M的物理内存的问题想得过于严重,因为一般来说,内核会倾向于把这部分内存分配给用户进程使用,而这是不需要占用内核空间地址的。 其实非线性映射区域还有另一个作用,就是用来作连续地址的映射。 内核采用伙伴系统管理内存,这使得内核程序可以一次申请2的n次幂个页面。 但如果n比较大时,申请失败的风险也会随之增加。正如桑拿时遇到双胞胎的机会很少、遇到三胞胎的机会更少一样, 获得地址连续的空闲页面的机会总是随着连续地址长度的增加而减少。 另外,即使能够幸运地得到地址连续的空闲页面,可能产生的浪费问题也是不能回避的。 比如我们需要申请地址连续513K的内存,从伙伴系统中申请时,由于只能选择申请2的n次幂个页面,因此我们不得不去申请1M内存。 不过这两个问题倒是都能够通过使用非线性映射区域来解决。 我们可以从伙伴系统中申请多个小段的内存,然后把它们映射到非线性映射区域中的连续区域中访问。 内核中与此相关的函数有vmalloc、vmap等。 其实80前的作者很羡慕80后和90后的新一代,不仅因为可以在上中学时谈恋爱, 还因为随着64位系统的流行,上面这些与32位系统如影随形的问题都将不复存在。 关于64位系统中的内存区域问题就留给有兴趣的读者去钻研了。 然后我们再谈谈linux中的伙伴系统。 伙伴系统总是分配出2的n次幂个连续页面,并且首地址以其长度为单位对齐。 这增大了将回收的页与其它空白页合并的可能性,也就是减少了内存碎片。 我们的块设备驱动程序需要从伙伴系统中获得所需的内存。 目前的做法是每次获得1个页面,也就是分配页面时,把2的n次幂中的n指定为0。 这样做的好处是只要系统中存在空闲的页面,不管空闲的页面是否连续,分配总是能成功。 但坏处是增加了造就页面碎片的几率。 当系统中没有单独的空闲页面时,伙伴系统就不得不把原先连续的空闲页面拆开,再把其中的1个页面返回给我们的程序。 同时,在伙伴系统中需要使用额外的内存来管理每一组连续的空闲页面,因此增大页面碎片也意味着需要更多的内存来管理这些碎片。 这还不算,如果系统中的空闲页面都以碎片方式存在,那么真正到了需要分配连续页面的时候,即使存在空闲的内存,也会因为这些内存不连续而导致分配失败。 除了对系统的影响以外,对我们的驱动程序本身而言,由于使用了基树来管理每一段内存,将内存段定义得越短,意味着需要管理更多的段数,也意味着更大的基树结构和更慢的操作。 因此我们打算增加单次从伙伴系统中获得连续内存的长度,比如,每次分配2个、4个、或者8个甚至64个页,来避免上述的问题。 每次分配更大的连续页面很明显拥有不少优势,但其劣势也同样明显: 当系统中内存碎片较多时,吃亏的就是咱们的驱动程序了。原本分很多次一点一点去系统讨要,最终可以要到足够的内存,但像现在这样子狮子大开口,却反而要不到了。 还有就是如果系统中原先就存在不少碎片,原先的分配方式倒是可以把碎片都利用起来,而现在这种挑肥捡瘦的分配会同样无视那些更小的不连续页面,反而可能企图去拆散那些更大的连续页面。 折中的做法大概就是选择每次分配一块不大不小的连续的页,暂且我们选择每次分配连续的4个页。 现在开始修改代码: 为简单起见,我们了以下的4个宏: #define SIMP_BLKDEV_DATASEGORDER (2) #define SIMP_BLKDEV_DATASEGSHIFT (PAGE_SHIFT + SIMP_BLKDEV_DATASEGORDER) #define SIMP_BLKDEV_DATASEGSIZE (PAGE_SIZE << SIMP_BLKDEV_DATASEGORDER) #define SIMP_BLKDEV_DATASEGMASK (~(SIMP_BLKDEV_DATASEGSIZE-1)) SIMP_BLKDEV_DATASEGORDER表示我们从伙伴系统中申请内存时使用的order值,把这个值设置为2时,每次将从伙伴系统中申请连续的4个页面。 我们暂且把这样的连续页面叫做内存段,这样一来,在i386结构中,每个内存段的大小为16K,假设块设备大小还是16M,那么经历了本章的修改后, 驱动程序所使用的内存段数量将从原先的4096个减少为现在的1024个。 SIMP_BLKDEV_DATASEGSHIFT是在偏移量和内存段之间相互转换时使用的移位值,类似于页面处理中的PAGE_SHIFT。这里就不做更详细地介绍了,毕竟这不是C语言教程。 SIMP_BLKDEV_DATASEGSIZE是以字节为单位的内存段的长度,在i386和SIMP_BLKDEV_DATASEGORDER=2时它的值是16384。 SIMP_BLKDEV_DATASEGMASK是内存段的屏蔽位,类似于页面处理中的PAGE_MASK。 其实对于功能而言,我们只需要SIMP_BLKDEV_DATASEGORDER和SIMP_BLKDEV_DATASEGSIZE就足够了,其它的宏用于快速的乘除和取模等计算。 如果读者对此感到有些迷茫的话,建议最好还是搞明白,因为在linux内核的世界中这一类的位操作将随处可见。 然后要改的是申请和释放内存代码。 原先我们使用的是__get_free_page()和free_page()函数,这一对函数用来申请和释放一个页面。 这显然不能满足现在的要求,我们改用它们的大哥:__get_free_pages()和free_pages()。 它们的原型是: unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order); void free_pages(unsigned long addr, unsigned int order); 可以注意到与__get_free_page()和free_page()函数相比,他们多了个order参数,正是用于指定返回2的多少次幂个连续的页。 因此原先的free_diskmem()和alloc_diskmem()函数将改成以下这样: void free_diskmem(void) { int i; void *p; for (i = 0; i < (SIMP_BLKDEV_BYTES + SIMP_BLKDEV_DATASEGSIZE - 1) >> SIMP_BLKDEV_DATASEGSHIFT; i++) { p = radix_tree_lookup(&simp_blkdev_data, i); radix_tree_delete(&simp_blkdev_data, i); /* free NULL is safe */ free_pages((unsigned long)p, SIMP_BLKDEV_DATASEGORDER); } } int alloc_diskmem(void) { int ret; int i; void *p; INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL); for (i = 0; i < (SIMP_BLKDEV_BYTES + SIMP_BLKDEV_DATASEGSIZE - 1) >> SIMP_BLKDEV_DATASEGSHIFT; i++) { p = (void *)__get_free_pages(GFP_KERNEL, SIMP_BLKDEV_DATASEGORDER); if (!p) { ret = -ENOMEM; goto err_alloc; } ret = radix_tree_insert(&simp_blkdev_data, i, p); if (IS_ERR_VALUE(ret)) goto err_radix_tree_insert; } return 0; err_radix_tree_insert: free_pages((unsigned long)p, SIMP_BLKDEV_DATASEGORDER); err_alloc: free_diskmem(); return ret; } 除了用__get_free_pages()和free_pages()代替了原先的__get_free_page()和free_page()函数以外, 还使用刚刚定义的那几个宏代替了原先的PAGE宏。 这样一来,所需内存段数的计算方法也完成了修改。 剩下的就是使用内存段的simp_blkdev_make_request()代码。 实际上,我们只要用刚才定义的SIMP_BLKDEV_DATASEGSIZE、SIMP_BLKDEV_DATASEGMASK和SIMP_BLKDEV_DATASEGSHIFT替换原先代码中的PAGE_SIZE、PAGE_MASK和PAGE_SHIFT就大功告成了, 当然,这个结论是作者是经过充分检查和实验后才得出的,希望不要误认为编程时可以大大咧咧地随心所欲。作为程序员,严谨的态度永远都是需要的。 现在,我们的simp_blkdev_make_request()函数变成了这样: static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio) { struct bio_vec *bvec; int i; unsigned long long dsk_offset; if ((bio->bi_sector << 9) + bio->bi_size > SIMP_BLKDEV_BYTES) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": bad request: block=%llu, count=%u\n", (unsigned long long)bio->bi_sector, bio->bi_size); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif return 0; } dsk_offset = bio->bi_sector << 9; bio_for_each_segment(bvec, bio, i) { unsigned int count_done, count_current; void *iovec_mem; void *dsk_mem; iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset; count_done = 0; while (count_done < bvec->bv_len) { count_current = min(bvec->bv_len - count_done, (unsigned int)(SIMP_BLKDEV_DATASEGSIZE - ((dsk_offset + count_done) & ~SIMP_BLKDEV_DATASEGMASK))); dsk_mem = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) >> SIMP_BLKDEV_DATASEGSHIFT); if (!dsk_mem) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": search memory failed: %llu\n", (dsk_offset + count_done) >> SIMP_BLKDEV_DATASEGSHIFT); kunmap(bvec->bv_page); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif return 0; } dsk_mem += (dsk_offset + count_done) & ~SIMP_BLKDEV_DATASEGMASK; switch (bio_rw(bio)) { case READ: case READA: memcpy(iovec_mem + count_done, dsk_mem, count_current); break; case WRITE: memcpy(dsk_mem, iovec_mem + count_done, count_current); break; default: printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": unknown value of bio_rw: %lu\n", bio_rw(bio)); kunmap(bvec->bv_page); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif return 0; } count_done += count_current; } kunmap(bvec->bv_page); dsk_offset += bvec->bv_len; } #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, bio->bi_size, 0); #else bio_endio(bio, 0); #endif return 0; } 本章的到这里就完成了,接下去我们还是打算试验一下效果。 其实这个实验不太好做,因为linux本身也会随时分配和释放页面,这会影响我们看到的结果。 如果读者看到的现象与预期不同,这也属于预期。 不过为了降低试验受到linux自身活动影响的可能性,建议试验开始之前尽可能关闭系统中的服务、不要同时做其它的操作、不要在xwindows中做。 然后我们开始试验: 先编译模块: # make make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step07 modules make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686‘ CC [M] /root/test/simp_blkdev/simp_blkdev_step07/simp_blkdev.o Building modules, stage 2. MODPOST CC /root/test/simp_blkdev/simp_blkdev_step07/simp_blkdev.mod.o LD [M] /root/test/simp_blkdev/simp_blkdev_step07/simp_blkdev.ko make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686‘ # 现在看看伙伴系统的情况: # cat /proc/buddyinfo Node 0, zone DMA 288 63 34 0 0 0 0 1 1 1 0 Node 0, zone Normal 9955 1605 24 1 0 1 1 0 0 0 1 Node 0, zone HighMem 2036 544 13 6 2 1 1 0 0 0 0 # 加载模块后再看看伙伴系统的情况: # insmod simp_blkdev.ko # cat /proc/buddyinfo Node 0, zone DMA 337 140 1 1 1 0 0 0 1 0 0 Node 0, zone Normal 27888 8859 18 0 0 1 0 0 1 0 0 Node 0, zone HighMem 1583 544 13 6 2 1 1 0 0 0 0 # 释放模块后再看看伙伴系统的情况: # rmmod simp_blkdev # cat /proc/buddyinfo Node 0, zone DMA 337 140 35 0 0 0 0 1 1 1 0 Node 0, zone Normal 27888 8860 632 7 0 1 1 0 0 0 1 Node 0, zone HighMem 1583 544 13 6 2 1 1 0 0 0 0 # 首先补充说明一下伙伴系统对每种类型的内存区域分别管理,这在伙伴系统中称之为zone。 在i386中,常见的zone有DMA、Normal和HighMem,分别对应0~16M、16~896M和896M以上的物理内存。 DMA zone的特点是老式ISA设备只能使用这段区域进行DMA操作。 Normal zone的特点它被固定映射在内核的地址空间中,我们可以直接使用指针访问这段内存。(不难看出,DMA zone也有这个性质) HighMem zone的特点它没有以上两种zone的特点。 其实我们在上文中讲述的低端内存区域是这里的DMA和Normal zone,而高端内存区域是这里的HighMem zone。 /proc/buddyinfo用于显示伙伴系统的各个zone中剩余的各个order的内存段个数。 我们的模块目前使用低端内存来存储数据,而一般情况下系统会尽可能保留DMA zone的空域内存不被分配出去, 因此我们主要关注/proc/buddyinfo中的Normal行。 行中的各列中的数字表示伙伴系统的这一区域中每个order的剩余内存数量。 比如: Node 0, zone Normal 9955 1605 24 1 0 1 1 0 0 0 1 这一行表示Normal zone中剩余9955个独立的内存页、1605个连续2个页的内存、24连续4个页的内存等。 由于我们现在每次申请4个页的内存,因此最关注的Normal行的第3列。 首先看模块加载前,Normal行的第3列数字是24,表示系统中剩余24个连续4页的内存区域。 然后我们看模块加载之后的情况,Normal行的第3列从24变为了18,减少了6个连续4页的内存区域。 这说明我们的程序只用掉了6个连续4页的内存区域------明显不可能。 因为作为模块编者,我们很清楚程序需要使用1024个连续4页的内存区域。 继续看这一行的后面,原先处在最末尾的1便成了0。 我们可以数出来最末尾的数字对应order为10的连续页面,也就是连续4M的页面,原来是空闲的,而现在被拆散用掉了。 但即使它被用掉了,也不够我们的的16M空间,数字的分析变得越来越复杂,是坚持下去还是就此停止? 这一次我们决定停止,因为真相是现在进行的模块加载前后的剩余内存对比确实产生不了什么结论。 详细解释一下,其实我们可以看出在模块加载之前,Normal区域中order>=2的全部空闲内存加起来也不够这个模块使用。 甚至加上DMA区域中order>=2的全部空闲内存也不够。 虽然剩余的order<2的一大堆页面凑起来倒是足够,但谁让我们的模块挑食,只要order=2的页面呢。 因此这时候系统会试图释放出空闲内存。比如:释放一些块设备缓冲页面,或者将用户进程的内存转移到swap中,以获得更多的空闲内存。 很幸运,系统通过释放内存操作拿到了足够的空闲内存使我们的模块得以顺利加载, 但同时由于额外增加出的空闲内存使我们对比模块加载前后的内存差别失去了意义。 其实细心一些的话,刚才的对比中,我们还是能够得到一些结论的,比如, 我们可以注意到模块加载后order为0和1的两个数字的暴增,这就是系统释放页面的证明。 详细来说,系统释放出的页面既包含order<2的,也包含order>=2的,但由于其中order>=2的页面多半被我们的程序拿走了, 这就造成模块加载后的空闲页面中大量出现order<2的页面。 既然我们没有从模块加载前后的空闲内存变化中拿到什么有意义的结论, 我们不妨换条路走,去看看模块释放前后空闲内存的变化情况: 首先还是看Normal区域: order为0和1的页面数目基本没有变化,这容易解释,因为我们释放出的都是order=2的连续页面。 order=2的连续页面从18增加到632,增加了614个。这应该是模块卸载时所释放的内存的一部分。 由于这个模块在卸载时,会释放1024个order=2的连续页面,那么我们还要继续找出模块释放的内存中其他部分的行踪。 也就是1024-614=410个order=2的连续页到哪去了。 回顾上文中的伙伴系统说明,伙伴系统会适时地合并连续页面,那么我们假设一部分模块释放出的页面被合并成更大order的连续页面了。 让我们计算一下order>2的页面的增加情况: order=3的页面增加了7个,order=6的页面增加了1个,order=8的页面减少了1个,order=10的页面增加了1个。 这分别相当于order=2的页面增加14个、增加16、减少64个、增加256个,综合起来就是增加222个。 这就又找到了一部分,剩下的行踪不明的页面还有410-222=188个。 我们继续追查,现在DMA zone区域。 我们的程序所使用的是低端内存,其实也包含0~16M之间的DMA zone。 刚才我们说过,系统会尽可能不把DMA区域的内存分配出去,以保证真正到必须使用这部分内存时,能够拿得出来。 但“尽可能”不代表“绝对不”,如果出现内存不足的情况,DMA zone的空闲内存也很难幸免。 但刚才我们的试验中,已经遇到了Normal区域内存不足情况,这时把DMA zone中的公主们拿去充当Normal zone的军妓也是必然的了。 因此我们继续计算模块释放后DMA区域的内存变化。在DMA区域: order=2的页面增加了34个,order=3的页面减少了1个,order=4的页面减少了1个,order=7的页面增加了1个,order=9的页面增加了1个。 这分别相当于order=2的页面增加34个、减少2、减少4个、增加32个,增加128个,综合起来就是增加188个。 数字刚好吻合,我们就找到了模块释放出的全部页面的行踪。 这也验证了本章中改动的功能符合预期。 然后我们再一次加载和卸载模块,同时查看伙伴系统中空闲内存的变化: # insmod simp_blkdev.ko # cat /proc/buddyinfo Node 0, zone DMA 336 141 0 0 0 1 1 0 1 0 0 Node 0, zone Normal 27781 8866 0 1 0 1 0 0 1 0 0 Node 0, zone HighMem 1459 544 13 6 2 1 1 0 0 0 0 # # rmmod simp_blkdev # cat /proc/buddyinfo Node 0, zone DMA 336 141 35 0 0 0 0 1 1 1 0 Node 0, zone Normal 27781 8867 633 7 0 1 1 0 0 0 1 Node 0, zone HighMem 1459 544 13 6 2 1 1 0 0 0 0 # 我们可以发现这一次模块加载前后的内存变化情况与上一轮有些不同,而分析工作就留给有兴趣的读者了。 本章对代码的改动量不大,主要说明一下与我们程序中出现的linux内存管理知识。 其实上一章的改动中已经涉及到了这部分知识,只是因为那时的重点不在这个方面,并且作者也不希望在同一章中加入过多的内容, 因此在本章中做个补足。 同时,本章中的说明也给后续章节中将要涉及到的内容做个准备,这样读者在将来也可以惬意一些。 不过在开始写这一章时,作者曾反复考虑该不该这样组织本章, 正如我们曾经说过的,希望读者在遇到不明白的地方时主动去探索教程之外更多的知识, 而不是仅仅读完这个教程本身。 本教程的目的是牵引出通过实现一个块设备驱动程序来牵引出相关的linux的各个知识点, 让读者们以此为契机,通过寻求疑问的答案、通过学习更细节的知识来提高自己的能力。 因此教程中对于不少涉及到的知识点仅仅给出简单的介绍,因为读者完全有能力通过google了解更详细的内容, 这也是作者建议的看书方法。 不过本章是个例外,因为作者最终认为对这些知识的介绍对于这部教程的整体性是有帮助的。 但这里的介绍其实仍然只属于皮毛,因此还是希望读者进一步了解教程以外的更多知识。 <未完,待续> 第8章 +---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | email: [email protected] | +---------------------------------------------------+ | 文章版权归原作者所有。 | | 大家可以自由转载这篇文章,但原版权信息必须保留。 | | 如需用于商业用途,请务必与原作者联系,若因未取得 | | 授权而收起的版权争议,由侵权者自行负责。 | +---------------------------------------------------+ 本章的目的是让读者继续休息,因此决定仍然搞一些简单的东西。 比如:给我们的驱动程序模块加上模块参数,这样在加载模块时,可以通过参数设定块设备的大小。 给我们模块加参数的工作不难,这牵涉到1个宏: module_param_named(name, value, type, perm) name是参数的名称 value是参数在模块中对应的变量 type是参数的类型 perm是参数的权限 如,在模块中添加 int disk_size = 1024; module_param_named(size, disk_size, int, S_IRUGO); 可以给模块加上名称为"size"的参数,如果在加载模块是使用insmod thismodule size=100,那么在模块代码中disk_size的值就是100。 相反,如果加载模块时没有指定参数,那么模块代码中disk_size的值仍是默认的1024。 S_IRUGO指定了这个参数的值在模块加载以后可以被所有人通过/sys/module/[module_name]/parameters/看到,但无法修改。 好了,有关module_param_named就介绍到这里,细节可以google或者看linux/include/linux/moduleparam.h。 然后我们就要给这个模块加个参数,用来在加载时指定块设备的大小。 参数的名字都已经想好了,就叫size吧,类型嘛,32位无符号整数最大能设定到4G,而我们的野心看起来可能更大一些, 为了让这个模块支持4G以上的虚拟磁盘(当然是内存足够的情况下),我们打算使用64位无符号整型。这样能够设定的最大值为16777216T,应该够了吧。 然后我们试图找出module_param_named的参数中与unsigned long long对应的type来。 结果是:google了,没找到;看linux/include/linux/moduleparam.h了,还是没找到。 结论是:目前的linux(2.6.28)还不支持unsigned long long类型的模块参数。 更新一些的内核中会不会有是将来的事,尽快搞定这一章的功能却是现在面临的问题。 然后我们就开始找解决方案: 1:给内核打个补丁,看样子不错,但至少今天之类完成不了我们的程序了 并且这样一来,我们的程序只能在今后的内核中运行,而失去对旧版linux的兼容性。 2:指定设置磁盘大小的单位为M。这样可设置的最大的数字就成了4G*1M,也就是4096T。 这个主意看似不错。而且看样子10年内机器的内存应该到不了这个容量。 3:用字符串来指定大小 这倒是可以解决所有问题,并且我们可以支持16M、1G之类的设定,让我们的程序看起来比较花哨。 缺点应该是我们需要在程序中自己去解析传入的字符串了,幸运的是,实际的解析代码比想象的容易一些。 因此,我们采用第3个方案,向模块中添加一个名称为size、类型为字符串的参数,并且支持解析以K,M,G,T为单位的设定。 第1步: 向程序中添加以下参数申明。 static char *simp_blkdev_param_size = "16M"; module_param_named(size, simp_blkdev_param_size, charp, S_IRUGO); char *simp_blkdev_param_size用于存储设定的磁盘大小,我们把磁盘大小的默认值指定为16M。 目前我们不允许用户在模块加载后改变磁盘大小,将来嘛,有可能增加这一功能,看起来很眩。 第2步: 原来的程序使用 #define SIMP_BLKDEV_BYTES (16*1024*1024) 定义磁盘大小,而现在我们不需要这一行了。 同时,我们需要一个unsigned long long变量来存储用户设定的磁盘大小,因此我们增加这个变量: static unsigned long long simp_blkdev_bytes; 然后把程序中所有使用SIMP_BLKDEV_BYTES的位置换成使用simp_blkdev_bytes变量。 第3步: 在模块加载时对模块参数进行解析,设置simp_blkdev_bytes变量的值。 我们增加一个函数进行解析工作: int getparam(void) { char unit; char tailc; if (sscanf(simp_blkdev_param_size, "%llu%c%c", &simp_blkdev_bytes, &unit, &tailc) != 2) { return -EINVAL; } if (!simp_blkdev_bytes) return -EINVAL; switch (unit) { case ‘g‘: case ‘G‘: simp_blkdev_bytes <<= 30; break; case ‘m‘: case ‘M‘: simp_blkdev_bytes <<= 20; break; case ‘k‘: case ‘K‘: simp_blkdev_bytes <<= 10; break; case ‘b‘: case ‘B‘: break; default: return -EINVAL; } /* make simp_blkdev_bytes fits sector‘s size */ simp_blkdev_bytes = (simp_blkdev_bytes + (1<<9) - 1) & ~((1ULL<<9) - 1); return 0; } 然后在simp_blkdev_init()中调用这个函数: ret = getparam(); if (IS_ERR_VALUE(ret)) goto err_getparam; 当然,err_getparam的位置读者应该能猜出来了。 这样一来,工作大概就完成了,让我们看看结果: 使用默认值: # insmod simp_blkdev.ko # fdisk /dev/simp_blkdev Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel Building a new DOS disklabel. Changes will remain in memory only, until you decide to write them. After that, of course, the previous content won‘t be recoverable. Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite) Command (m for help): p Disk /dev/simp_blkdev: 16 MB, 16777216 bytes 1 heads, 32 sectors/track, 1024 cylinders Units = cylinders of 32 * 512 = 16384 bytes Device Boot Start End Blocks Id System Command (m for help): q # 设定成20M: # rmmod simp_blkdev # insmod simp_blkdev.ko size=20M # fdisk /dev/simp_blkdev Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel Building a new DOS disklabel. Changes will remain in memory only, until you decide to write them. After that, of course, the previous content won‘t be recoverable. The number of cylinders for this disk is set to 1280. There is nothing wrong with that, but this is larger than 1024, and could in certain setups cause problems with: 1) software that runs at boot time (e.g., old versions of LILO) 2) booting and partitioning software from other OSs (e.g., DOS FDISK, OS/2 FDISK) Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite) Command (m for help): p Disk /dev/simp_blkdev: 20 MB, 20971520 bytes 1 heads, 32 sectors/track, 1280 cylinders Units = cylinders of 32 * 512 = 16384 bytes Device Boot Start End Blocks Id System Command (m for help): q # 变态一下,还是设定成20M,但用k作单位: # rmmod simp_blkdev # insmod simp_blkdev.ko size=20480k # fdisk /dev/simp_blkdev Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel Building a new DOS disklabel. Changes will remain in memory only, until you decide to write them. After that, of course, the previous content won‘t be recoverable. The number of cylinders for this disk is set to 1280. There is nothing wrong with that, but this is larger than 1024, and could in certain setups cause problems with: 1) software that runs at boot time (e.g., old versions of LILO) 2) booting and partitioning software from other OSs (e.g., DOS FDISK, OS/2 FDISK) Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite) Command (m for help): p Disk /dev/simp_blkdev: 20 MB, 20971520 bytes 1 heads, 32 sectors/track, 1280 cylinders Units = cylinders of 32 * 512 = 16384 bytes Device Boot Start End Blocks Id System Command (m for help): q # 看样子结果不错。 这一章中基本上没有提到什么比较晦涩的知识,而且看样子通过这一章的学习,大家也应该休息好了。 如果读者现在感觉到精神百倍,那么这一章的目的应该就达到了。 <未完,待续> 第9章 +---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | email: [email protected] | +---------------------------------------------------+ | 文章版权归原作者所有。 | | 大家可以自由转载这篇文章,但原版权信息必须保留。 | | 如需用于商业用途,请务必与原作者联系,若因未取得 | | 授权而收起的版权争议,由侵权者自行负责。 | +---------------------------------------------------+ 在本章中我们来讨论一下这个驱动程序的数据安全, 因为最近的一些事情让作者愈发地感觉到数据泄漏对当事人来说是麻烦的。 我们开门见山的解释一下数据安全问题: 内核常常会向用户态传递数据,而作为内核程序的开发者,我们必须意识到不能把包含意料内容之外的数据随便透露给用户态, 因为如果这些数据不巧被别有用心者利用,就会带来不少麻烦。 比如陈冠希就犯了这样的错误。新余市出国考察团也没有在陈冠希身上吸取教训,把单据也不当回事儿。 单据对于考察团而言并不是什么重要的玩意儿,但一旦落到“别有用心”的人手中被加以利用,就不得不当一回事了。 由此我们发现了单据的商业价值。 今后在旅游公司干过的员工拿着手头攒到的大量单据,可能会比KIRA更有前途。 因此公务员确实属于高风险职业,加薪也是情理当中的了。 对于内核而言,其中的数据也是如此。 即使一些数据对内核而言没有价值,但也不能随意地向用户态传递,因为这段内存中可能不巧包含了不能随意让用户获取的数据, 比如用户A使用linux整理他女友的裸照文件,裸照的数据很可能存在于用户A的进程的虚存中,也可能还存在于文件缓存中, A的进程结束后,系统回收了进程的内存,这时内存中的数据被系统认定为无效数据,但系统并没有清空这段数据。 A打开的文件的缓存也类似,缓存被系统回收后,内存中的数据并没有被清除。 随后用户B使用了我们的块设备驱动程序。驱动程序初始化时需要获取足够的内存以存储块设备中的数据, 系统很可能将用户A使用过的那段包含裸照数据的内存分配给我们的块设备驱动程序。 这时如果用户B老老实实分区、创建文件系统、写入文件,这当然没事, 但如果用户B别有用心的上来就直接去读块设备中的数据,那么他可能很幸运的看到不该看的东西。 因此我们咬牙切齿,嫉妒心促使我们修改这个块设备驱动,我们都没遇到的好事儿,也决不允许用户B遇到。 修改的方法很简单,我们申请内存时使用了__get_free_pages()函数, 这个函数的第一个参数是gfp_mask,原先我们传递的是GFP_KERNEL,表示用于内核中的一般情况。 现在我们只要向gfp_mask中添加__GFP_ZERO标志,以提示需要申请清0后的内存。 这样驱动程序加载后,块设备中数据的初始值全为0,这就避免了上文中提到的安全问题。 详细来说,就是把alloc_diskmem()函数中的 p = (void *)__get_free_pages(GFP_KERNEL | __GFP_ZERO, 这一行改成 p = (void *)__get_free_pages(GFP_KERNEL, 安全方面的改动已经完成了,但为了避免读者认为本章偷工减料,我们再多改一些代码。 块设备中每扇区的数据长度为512字节,我们在驱动程序经常遇到与此相关的转换。 为了快速运算,我们经常用到9这个常数,比如: 乘以512就是左移9、除以512就是右移9、除以512的余数就是& ((1ULL<<9) - 1)、 向上对齐到512的倍数就是加上(1<<9) - 1再& ~((1ULL<<9) - 1)。 不过现在我们决定通过定义几个宏来吧这些操作写得好看一些。 先定义: #define SIMP_BLKDEV_SECTORSHIFT (9) #define SIMP_BLKDEV_SECTORSIZE (1ULL<<SIMP_BLKDEV_SECTORSHIFT) #define SIMP_BLKDEV_SECTORMASK (~(SIMP_BLKDEV_SECTORSIZE-1)) 然后使用这几个宏来进行扇区相关的转换工作。 详细来说,就是把simp_blkdev_make_request()函数中的: if ((bio->bi_sector << 9) + bio->bi_size > simp_blkdev_bytes) { 改成 if ((bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT) + bio->bi_size > simp_blkdev_bytes) { dsk_offset = bio->bi_sector << 9; 改成 dsk_offset = bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT; 把simp_blkdev_getgeo()函数中的: geo->cylinders = simp_blkdev_bytes>>9/geo->heads/geo->sectors; 改成 geo->cylinders = simp_blkdev_bytes >> SIMP_BLKDEV_SECTORSHIFT / geo->heads / geo->sectors; 把getparam()函数中的: simp_blkdev_bytes = (simp_blkdev_bytes + (1<<9) - 1) & ~((1ULL<<9) - 1); 改成 simp_blkdev_bytes = (simp_blkdev_bytes + SIMP_BLKDEV_SECTORSIZE - 1) & SIMP_BLKDEV_SECTORMASK; 把simp_blkdev_init()函数中的: set_capacity(simp_blkdev_disk, simp_blkdev_bytes>>9); 改成 set_capacity(simp_blkdev_disk, simp_blkdev_bytes >> SIMP_BLKDEV_SECTORSHIFT); 如果运气不算太背的话,程序应该是能够运行的,让我们试试: # make make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step09 modules make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686‘ CC [M] /root/test/simp_blkdev/simp_blkdev_step09/simp_blkdev.o Building modules, stage 2. MODPOST CC /root/test/simp_blkdev/simp_blkdev_step09/simp_blkdev.mod.o LD [M] /root/test/simp_blkdev/simp_blkdev_step09/simp_blkdev.ko make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686‘ # insmod simp_blkdev.ko # 看一看驱动程序刚刚加载时里面的数据: # hexdump /dev/simp_blkdev -vn512 0000000 0000 0000 0000 0000 0000 0000 0000 0000 0000010 0000 0000 0000 0000 0000 0000 0000 0000 0000020 0000 0000 0000 0000 0000 0000 0000 0000 0000030 0000 0000 0000 0000 0000 0000 0000 0000 0000040 0000 0000 0000 0000 0000 0000 0000 0000 0000050 0000 0000 0000 0000 0000 0000 0000 0000 0000060 0000 0000 0000 0000 0000 0000 0000 0000 0000070 0000 0000 0000 0000 0000 0000 0000 0000 0000080 0000 0000 0000 0000 0000 0000 0000 0000 0000090 0000 0000 0000 0000 0000 0000 0000 0000 00000a0 0000 0000 0000 0000 0000 0000 0000 0000 00000b0 0000 0000 0000 0000 0000 0000 0000 0000 00000c0 0000 0000 0000 0000 0000 0000 0000 0000 00000d0 0000 0000 0000 0000 0000 0000 0000 0000 00000e0 0000 0000 0000 0000 0000 0000 0000 0000 00000f0 0000 0000 0000 0000 0000 0000 0000 0000 0000100 0000 0000 0000 0000 0000 0000 0000 0000 0000110 0000 0000 0000 0000 0000 0000 0000 0000 0000120 0000 0000 0000 0000 0000 0000 0000 0000 0000130 0000 0000 0000 0000 0000 0000 0000 0000 0000140 0000 0000 0000 0000 0000 0000 0000 0000 0000150 0000 0000 0000 0000 0000 0000 0000 0000 0000160 0000 0000 0000 0000 0000 0000 0000 0000 0000170 0000 0000 0000 0000 0000 0000 0000 0000 0000180 0000 0000 0000 0000 0000 0000 0000 0000 0000190 0000 0000 0000 0000 0000 0000 0000 0000 00001a0 0000 0000 0000 0000 0000 0000 0000 0000 00001b0 0000 0000 0000 0000 0000 0000 0000 0000 00001c0 0000 0000 0000 0000 0000 0000 0000 0000 00001d0 0000 0000 0000 0000 0000 0000 0000 0000 00001e0 0000 0000 0000 0000 0000 0000 0000 0000 00001f0 0000 0000 0000 0000 0000 0000 0000 0000 0000200 # 对比一下修改前的效果: # hexdump /dev/simp_blkdev -vn512 0000000 f300 0800 1200 0000 b804 1200 0000 0500 0000010 501a 6930 1806 246a bf0a 7700 256a bf0b 0000020 1f80 256b bf0b 47a0 266b bf0b 0ff0 246a 0000030 bf0a 1708 ffff 00ff 5028 256b bf0b 00a8 0000040 ffff 00ff 04b8 ffff 00ff 10c8 256b bf0b 0000050 00e8 246a bf0a 0229 ffff 00ff 1339 ffff 0000060 00ff 0059 246a bf0a 1669 ffff 00ff 12a9 0000070 256b bf0b 02c9 ffff 00ff 12d9 246a bf0a 0000080 215a ffff 00ff 302c 256b bf0b 03ac ffff 0000090 00ff 10cc 256b bf0b 03ec 246a bf0a 522d 00000a0 256b bf0b 32bd 2318 266b bf0c 2700 266c 00000b0 bf0c 2730 276c bf0c 1f60 276c bf0d 3580 00000c0 276d bf0d 1bc0 286d bf0d 05e0 286d bf0e 00000d0 04f0 ffff 00ff 07f5 276c bf0d 0186 ffff 00000e0 00ff 1596 276c bf0d 01b6 ffff 00ff 15e6 00000f0 266b bf0c 0708 266b bf0c 0018 ffff 00ff 0000100 0428 ffff 00ff 1038 266c bf0c 0058 ffff 0000110 00ff 3088 ffff 00ff 1219 266c bf0c 0239 0000120 ffff 00ff 1249 276c bf0d 0689 276c bf0d 0000130 02b9 266b bf0c 031c ffff 00ff 103c 266c 0000140 bf0c 035c 276c bf0d 039c ffff 00ff 20ac 0000150 276d bf0d 03dc 286d bf0d 03ec 266b bf0c 0000160 022d 266c bf0c 223d 276c bf0d 12ad 276d 0000170 bf0d 12cd 286d bf0e 02fd 2b18 286d bf0e 0000180 4400 296e bf0e 1450 296e bf0f 4470 2a6e 0000190 bf0f 14c0 2a6f bf0f 04e0 2a6f bf10 04f0 00001a0 ffff 00ff 2005 286d bf0e 1035 ffff 00ff 00001b0 5055 296e bf0f 0ab5 ffff 00ff 30c5 286d 00001c0 bf0e 1006 ffff 00ff 1426 286d bf0e 0946 00001d0 ffff 00ff 1056 296e bf0f 0176 ffff 00ff 00001e0 1186 296e bf0f 14a6 2a6e bf0f 05c6 ffff 00001f0 00ff 16d6 2a6f bf10 05f6 286d bf0e 0007 0000200 # 本章到此结束,读者是不是感觉我们的教程越来越简单了?
1~9章驱动详解