第1章
+---------------------------------------------------+
| 写一个块设备驱动 |
+---------------------------------------------------+
| 作者:赵磊 |
| 网名:OstrichFly、飞翔的鸵鸟 |
| email: [email protected] |
+---------------------------------------------------+
| 文章版权归原作者所有。 |
| 大家可以自由转载这篇文章,但原版权信息必须保留。 |
| 如需用于商业用途,请务必与原作者联系,若因未取得 |
| 授权而收起的版权争议,由侵权者自行负责。 |
+---------------------------------------------------+
同样是读书,读小说可以行云流水,读完后心情舒畅,意犹未尽;读电脑书却举步艰难,读完后目光呆滞,也是意犹未尽,只不过未尽的是痛苦的回忆。
研究证明,痛苦的记忆比快乐的更难忘记,因此电脑书中的内容比小说记得持久。
而这套教程的目的是要打破这种状况,以至于读者在忘记小说内容忘记本文。
在这套教程中,我们通过写一个建立在内存中的块设备驱动,来学习linux内核和相关设备驱动知识。
选择写块设备驱动的原因是:
1:容易上手
2:可以牵连出更多的内核知识
3:像本文这样的块设备驱动教程不多,所以需要一个
好吧,扯淡到此结束,我们开始写了。
本章的目的用尽可能最简单的方法写出一个能用的块设备驱动。
所谓的能用,是指我们可以对这个驱动生成的块设备进行mkfs,mount和读写文件。
为了尽可能简单,这个驱动的规模不是1000行,也不是500行,而是100行以内。
这里插一句,我们不打算在这里介绍如何写模块,理由是介绍的文章已经满天飞舞了。
如果你能看得懂、并且成功地编译、运行了这段代码,我们认为你已经达到了本教程的入学资格,
当然,如果你不幸的卡在这段代码中,那么请等到搞定它以后再往下看:
mod.c:
#include
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的兴趣,甚至领悟学习一切科学所需要的钻研精神。
作为第一章的结尾,引用我在另一篇文章中的序言:
谨以此文向读者示范什么叫做严谨的研究。
呼唤踏实的治学态度,反对浮躁的论坛风气。
+---------------------------------------------------+ |
+---------------------------------------------------+ |
+---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | 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啊,所以最好还是告诉系统我们的磁盘没有那么多的盘面数和每磁道扇区数,这将让磁道数来得多一些。 在下一章中,我们打算搞定这个问题。 |
+---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | 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 这个函数的目的,是选择适当的物理结构信息装入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。 |
+---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | 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 看出区别了没? 如果本章到这里还不结束的话,估计读者要开始闪人了。 好的,我们马上就结束,希望在这之前闪掉的读者不要太多。 由于还没有来得及闪掉而看到这段话的读者们,作者相信你们具有相当的毅力。 学习是需要毅力的,这时的作者同样也需要毅力来坚持完成这本教程。 最后还是希望读者坚持,坚持看完所有的章节,坚持在遇到每一个不明白的问题时都努力寻求答案, 坚持在发现自己感兴趣的内容时能够深入地去了解、探寻、思考。 |
+---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | 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了解更详细的内容, 这也是作者建议的看书方法。 不过本章是个例外,因为作者最终认为对这些知识的介绍对于这部教程的整体性是有帮助的。 但这里的介绍其实仍然只属于皮毛,因此还是希望读者进一步了解教程以外的更多知识。 |
+---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | 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 # 看样子结果不错。 这一章中基本上没有提到什么比较晦涩的知识,而且看样子通过这一章的学习,大家也应该休息好了。 如果读者现在感觉到精神百倍,那么这一章的目的应该就达到了。 |
+---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | 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_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 # 本章到此结束,读者是不是感觉我们的教程越来越简单了? |
+---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | email: [email protected] | +---------------------------------------------------+ | 文章版权归原作者所有。 | | 大家可以自由转载这篇文章,但原版权信息必须保留。 | | 如需用于商业用途,请务必与原作者联系,若因未取得 | | 授权而收起的版权争议,由侵权者自行负责。 | +---------------------------------------------------+ 如果你的linux系统是x86平台,并且内存大于896M,那么恭喜你,我们大概可以在这个实验中搞坏你的系统。 反之如果你的系统不符合这些条件,也不用为无法搞坏系统而感到失望,本章的内容同样适合你。 这时作者自然也要申明一下对读者产生的任何损失概不负责, 因为这年头一不小心就可能差点成了被告,比如南京的彭宇和镇江花山湾的小许姑娘。 在实验看到的情况会因为系统的实际状况不同而稍有区别,但我们需要说明的问题倒是相似的。 但希望读者不要把这种相似理解成了ATM机取款17.5万和贪污2.6亿在判决上的那种相似。 首先我们来看看目前系统的内存状况: # cat /proc/meminfo MemTotal: 1552532 kB MemFree: 1529236 kB Buffers: 2716 kB Cached: 10124 kB SwapCached: 0 kB Active: 8608 kB Inactive: 7664 kB HighTotal: 655296 kB HighFree: 640836 kB LowTotal: 897236 kB LowFree: 888400 kB SwapTotal: 522104 kB SwapFree: 522104 kB Dirty: 44 kB Writeback: 0 kB AnonPages: 3440 kB Mapped: 3324 kB Slab: 2916 kB SReclaimable: 888 kB SUnreclaim: 2028 kB PageTables: 272 kB NFS_Unstable: 0 kB Bounce: 0 kB WritebackTmp: 0 kB CommitLimit: 1298368 kB Committed_AS: 10580 kB VmallocTotal: 114680 kB VmallocUsed: 392 kB VmallocChunk: 114288 kB HugePages_Total: 0 HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 4096 kB DirectMap4k: 12288 kB DirectMap4M: 905216 kB # 输出很多,但我们只关心这几行: MemFree: 1529236 kB --这说明系统中有接近1.5G的空闲内存 HighFree: 640836 kB --这说明空闲内存中,处在高端的有600M左右 LowFree: 888400 kB --这说明空闲内存中,处在低端的有800M左右 现在加载上一章完成的模块,我们指定创建800M的块设备: # insmod simp_blkdev.ko size=800M # 成功了,我们再看看内存状况: # cat /proc/meminfo MemFree: 708812 kB HighFree: 640464 kB LowFree: 68348 kB ... # 我们发现高端内存没怎变,低端内存却已经被耗得差不多了。 我们一不做二不休,继续加大块设备的容量,看看极限能到多少: # rmmod simp_blkdev # insmod simp_blkdev.ko size=860M # cat /proc/meminfo MemFree: 651184 kB HighFree: 641972 kB LowFree: 9212 kB ... # 系统居然还没事,这时虽然高端内存还是没怎么变,但低端内存剩下的得已经很可怜了。 然后进一步加大块设备的容量: # rmmod simp_blkdev # insmod simp_blkdev.ko size=870M ... 这里不用再cat /proc/meminfo了,因为系统已经完蛋了。 如果有些读者嗜好独特,对出错信息情有独钟的话,在这里也满足一下: kernel: [ 3588.769050] insmod invoked oom-killer: gfp_mask=0x80d0, order=2, oomkilladj=0 kernel: [ 3588.769516] Pid: 4236, comm: insmod Tainted: G W 2.6.27.4 #53 kernel: [ 3588.769868] [ kernel: [ 3588.771041] [ kernel: [ 3588.771306] [ kernel: [ 3588.771500] [ kernel: [ 3588.771679] [ kernel: [ 3588.771899] [ kernel: [ 3588.772217] [ kernel: [ 3588.772393] [ kernel: [ 3588.772599] [ kernel: [ 3588.772845] [ kernel: [ 3588.773250] [ kernel: [ 3588.773884] ======================= kernel: [ 3588.774237] Mem-Info: kernel: [ 3588.774241] DMA per-cpu: kernel: [ 3588.774404] CPU 0: hi: 0, btch: 1 usd: 0 kernel: [ 3588.774582] Normal per-cpu: kernel: [ 3588.774689] CPU 0: hi: 186, btch: 31 usd: 0 kernel: [ 3588.774870] HighMem per-cpu: kernel: [ 3588.778602] CPU 0: hi: 186, btch: 31 usd: 0 ... 搞坏系统就当是交学费了,但交完学费我们总要学到些东西。 虽然公款出国考察似乎已经斯通见惯,但至少在我们的理解中,学费不是旅游费,更不是家属的旅游费。 我们通过细心观察、周密推理后得出的结论是: 目前的块设备驱动程序会一根筋地使用低端内存,即使系统中低端内存很紧缺的时候, 也会直道把系统搞死却不去动半点的高端内存,这未免也太挑食了, 因此在本章和接下来的几章中,我们将帮助驱动程序戒掉对低端内存的瘾。 相对高端内存而言,低端内存是比较宝贵的,这是因为它不需要影射就能直接被内核访问的特性。 而内核中的不少功能都直接使用低端内存,以保证访问的速度和简便, 但换句话来说,如果低端内存告急,那么系统可能离Panic也不远了。 因此总的来说,对低端内存的使用方法大概应该是:除非有足够理由,否则就别乱占着。 详细来说,就是: 1:不需要使用低端内存的“在内核中不需要映射就能直接访问”这个特性的功能,应该优先使用高端内存 如:分配给用户态进程的内存,和vmalloc的内存 2:需要占用大量内存的功能,并且也可以通过高端内存实现的,应该优先使用高端内存 如:我们的程序 与内存有关的知识我们在以前的章节中已经谈到,因此这里不再重复了, 但需要说明的是在高端内存被映射之前,我们是无法通过指针来指向它的。 因为它不在内核空间的地址范围以内。 虽然如此,我们却无论如何都需要找出一种方法来指定一个没有被映射的高端内存, 这是由于至少在进行映射操作时,我们需要指定去映射谁。 这就像为一群猴子取名的时候,如何来说明是正在给哪只猴子取名一样。 虽然给猴子取名的问题可能比较容易解决,比如我们可以说, 给哪只红屁股的公猴取名叫齐天大圣、给那只瘦瘦的母猴取名叫白晶晶, 但可惜一块高端内存即没有红屁股,又没有胖瘦之分, 它们唯一有的就是地址,因此我们也必须通过地址来指定这段高端内存。 刚才说过,在高端内存被映射之前,他在内核的地址空间中是不存在的, 但虽然如此,它至少存在其物理地址,而我们正是可以通过它的物理地址来指定它。 是的,本质上是这样的,但在linux中,我们还需要再绕那么一丁点: linux在启动阶段为全部物理内存按页为单位建立了的对应的struct page结构,用来管理这些物理内存, 也就是,每个页的物理内存,都有着1对1的struct page结构,而这些struct page结构是位于低端内存中的, 我们只要使用指向某个struct page结构的指针,就能指定物理内存中的一个页。 因此,对于没有被映射到内核空间中的高端内存,我们可以通过对应的struct page结构来指定它。 (如果读者希望了解更详细的知识,可以考虑从virt_to_page函数一路google下去) 我们在这里大肆谈论高端内存的表示方法,因为这是让我们的模块使用高端内存的前提。 我们的驱动程序使用多段内存来存储块设备中的数据。 原先的程序中,我们使用指向这些内存段的指针来指定这些数据的位置,这是没有问题的, 因为当时我们是使用__get_free_pages()来申请内存,__get_free_pages()函数只能用来申请低端内存, 因为这个函数返回的是申请到的内存的指针,而上文中说过,高端内存是不能用这样的指针表示的。 要申请高端内存,明显不能使用这样的函数,因此我们隆重介绍它的代替者出场: struct page *alloc_pages(gfp_t gfp_mask, unsigned int order); 这个函数的参数与__get_free_pages()相同,但区别在于,它返回指向struct page的指针, 这个我们在上文中介绍过的指针赋予了alloc_pages()函数申请高端内存的能力。 其实申请一块高端内存并不难,只要使用__GFP_HIGHMEM参数调用alloc_pages()函数, 就可能返回一块高端内存,之所以说是“可能”,使因为在某些情况下,比如高端内存不够或不存在时,也会但会低端内存充数。 我们的现在的目标是让驱动程序使用高端内存,这需要: 1:让驱动程序申请高端内存 2:让驱动程序使用高端内存 但在这一章中,我们要做的即不是1,也不是2,而是1之前的准备工作。 因为1和2必须一气呵成地改完,而为了让一气呵成的时候不要再面临其他插曲, 我们需要做好充足的准备工作,就像ml前尿尿一样。 对应到程序的修改工作上,我们打算先让程序使用struct page *来指定申请到的内存。 要实现这个目的,我们先要改申请内存的函数,也就是alloc_diskmem()。 刚才我们介绍过alloc_pages(),现在就要用它了: 首先把函数中定义的 void *p; 改成 struct page *page; 因为我们要使用struct page *来指定申请到的内存,而不是地址了。 然后把 p = (void *)__get_free_pages(GFP_KERNEL | __GFP_ZERO, SIMP_BLKDEV_DATASEGORDER); 改成 page = alloc_pages(GFP_KERNEL | __GFP_ZERO, SIMP_BLKDEV_DATASEGORDER); 这一行改动的原因大概已经说得很详细了。 还有那个if(!p)改成if (!page) 然后就是把指针加入基树的那一行: ret = radix_tree_insert(&simp_blkdev_data, i, p); 改成 ret = radix_tree_insert(&simp_blkdev_data, i, page); 由于我们使用了struct page *来指定申请到的内存,因此错误处理部分也要小改一下: free_pages((unsigned long)p, SIMP_BLKDEV_DATASEGORDER); 改成 __free_pages(page, SIMP_BLKDEV_DATASEGORDER); 这里补充介绍一下__free_pages()函数,可能大家已经猜到其作用了, 其实与我们原先使用的free_pages()函数相似,都是用来释放一段内存, 但__free_pages()使用struct page *来指定要释放的内存,这也意味着它能够用来释放高端内存。 大家应该已经发现我们虽然改用alloc_pages()函数来申请内存,但并没有指定__GFP_HIGHMEM参数, 这时申请到的仍然是低端内存,因此避免了在这一章中对访问内存那部分代码的大肆改动。 改动过的alloc_pages()函数是这样的: int alloc_diskmem(void) { int ret; int i; struct page *page; INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL); for (i = 0; i < (simp_blkdev_bytes + SIMP_BLKDEV_DATASEGSIZE - 1) >> SIMP_BLKDEV_DATASEGSHIFT; i++) { page = alloc_pages(GFP_KERNEL | __GFP_ZERO, SIMP_BLKDEV_DATASEGORDER); if (!page) { ret = -ENOMEM; goto err_alloc; } ret = radix_tree_insert(&simp_blkdev_data, i, page); if (IS_ERR_VALUE(ret)) goto err_radix_tree_insert; } return 0; err_radix_tree_insert: __free_pages(page, SIMP_BLKDEV_DATASEGORDER); err_alloc: free_diskmem(); return ret; } 相应的,释放内存用的free_diskmem()函数也需要一些更改, 为了避免有人说作者唐僧,列出修改后的样子应该已经足够了: void free_diskmem(void) { int i; struct page *page; for (i = 0; i < (simp_blkdev_bytes + SIMP_BLKDEV_DATASEGSIZE - 1) >> SIMP_BLKDEV_DATASEGSHIFT; i++) { page = radix_tree_lookup(&simp_blkdev_data, i); radix_tree_delete(&simp_blkdev_data, i); /* free NULL is safe */ __free_pages(page, SIMP_BLKDEV_DATASEGORDER); } } 随后是simp_blkdev_make_request()函数: 首先我们不是把void *dsk_mem改成struct page *dsk_page,而是增加一个 struct page *dsk_page; 变量,因为在访问内存时,我们还是需要用到dsk_mem变量的。 然后是从基数中获取指针的代码,把原先的 dsk_mem = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) >> SIMP_BLKDEV_DATASEGSHIFT); 改成 dsk_page = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) >> SIMP_BLKDEV_DATASEGSHIFT); 虽然看起来没什么太大变化,但我们需要知道,这时基树返回的指针已经不是直接指向数据所在的内存了。 还有那个判断是否从基树中获取成功的 if (!dsk_mem) { 用脚丫子也能想得出应该改成这样: if (!dsk_page) { 还有就是我们需要首先将struct page *dsk_page地址转换成内存的地址后,才能对这块内存进行访问。 这里我们使用了page_address()函数。 这个函数可以获得struct page数据结构所对应内存的地址。 这时可能有读者要问了,如果这个struct page对应的是高端内存,那么如何返回地址呢? 实际上,这种情况下如果高端内存中的页面已经被映射到内核的地址空间,那么函数会返回映射到内核空间中的地址, 而如果没有映射的话,函数将返回0。 对于我们目前的程序而言,由于使用的是低端内存,因此struct page对应的内存总是处于内核地址空间中的。 对应到代码中,我们需要在使用dsk_mem之前,也就是 dsk_mem += (dsk_offset + count_done) & ~SIMP_BLKDEV_DATASEGMASK; 这条语句之前,让dsk_mem指向struct page *dsk_page对应的内存的实际地址。 这是通过如下代码实现的: dsk_mem = page_address(dsk_page); if (!dsk_mem) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": get page's address failed: %p\n", dsk_page); kunmap(bvec->bv_page); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif 总的来说,修改后的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 << SIMP_BLKDEV_SECTORSHIFT) + 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 << SIMP_BLKDEV_SECTORSHIFT; bio_for_each_segment(bvec, bio, i) { unsigned int count_done, count_current; void *iovec_mem; struct page *dsk_page; 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_page = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) >> SIMP_BLKDEV_DATASEGSHIFT); if (!dsk_page) { 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 = page_address(dsk_page); if (!dsk_mem) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": get page's address failed: %p\n", dsk_page); kunmap(bvec->bv_page); #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif } 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; } 通过对这3个函数的更改,代码可以使用struct page *来定位存储块设备数据的内存了。 这也为将来使用高端内存做了一部分准备。 因为本章修改的代码在外部功能上没有发生变动,所以我们就不在这里尝试编译了运行代码了。 不过感兴趣的读者不妨试一试这段代码能不能进行编译和会不会引起死机。 |
+---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | email: [email protected] | +---------------------------------------------------+ | 文章版权归原作者所有。 | | 大家可以自由转载这篇文章,但原版权信息必须保留。 | | 如需用于商业用途,请务必与原作者联系,若因未取得 | | 授权而收起的版权争议,由侵权者自行负责。 | +---------------------------------------------------+ 本章中我们仍然为块设备驱动程序使用高端内存做准备工作。 这里要进行的准备工作并不意味着要增加或改变什么功能, 而是要收拾一部分代码,因为它们看起来已经有点复杂了。 有编程经验的读者大概能够意识到,编程时最常做的往往不是输入程序,而是拷贝-粘贴。 这是由于我们在编程时可能会不断地发现设计上的问题,或意识到还可以采用更好的结构,然后当然是实现它。 当然,更理想的情况大概是在一开始规划时就确定一个最佳的结构,以避免将来的更改, 但事实往往会与理想背道而驰,但关键是我们发现这种苗头时要及时纠正,而不是像某些部门一样去得过且过大事化小来掩盖问题。 要知道,酒是越陈越香,而垃圾却是越捂越臭,如果我们无法在最初做出完美的设计,至少我们还拥有纠正的勇气。 这里读者可能已经感觉到了,这里我们将要修改simp_blkdev_make_request()函数,因为它显得有些大了, 以至于在前几章中对其进行修改时,不得不列出大段的代码来展示修改结果。 不过这不是主要原因,相对于缩短函数长度来说,我们分割函数时可能更加在意的是提高代码的可读性。 其实这里分割simp_blkdev_make_request()也是为了将来实现对高端内存的支持, 因为访问高端内存无疑将牵涉到页面映射问题,而页面映射的处理又牵涉到了这个函数, 因此我们也希望把这部分功能独立出来,以免动戳就改动这个大函数, 也可能是为了作者的偏好,因为作者作者哪怕是改动函数中的一个字符,也会把整个函数从头到尾检查一番, 以确定这次改动不会产生其他影响,这就解释了作者为什么更加偏爱简单一些的函数了。 当然这种偏好也不一定完全是好事,比如前两天选择液晶电视时,作者就趋向于显示器+机顶盒... 对于一直坚持到这一章的读者而言,应该对simp_blkdev_make_request()函数的功能烂熟于心了, 因此我们直接列出修改后的代码: static int simp_blkdev_trans_oneseg(struct page *start_page, unsigned long offset, void *buf, unsigned int len, int dir) { void *dsk_mem; dsk_mem = page_address(start_page); if (!dsk_mem) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": get page's address failed: %p\n", start_page); return -ENOMEM; } dsk_mem += offset; if (!dir) memcpy(buf, dsk_mem, len); else memcpy(dsk_mem, buf, len); return 0; } static int simp_blkdev_trans(unsigned long long dsk_offset, void *buf, unsigned int len, int dir) { unsigned int done_cnt; struct page *this_first_page; unsigned int this_off; unsigned int this_cnt; done_cnt = 0; while (done_cnt < len) { /* iterate each data segment */ this_off = (dsk_offset + done_cnt) & ~SIMP_BLKDEV_DATASEGMASK; this_cnt = min(len - done_cnt, (unsigned int)SIMP_BLKDEV_DATASEGSIZE - this_off); this_first_page = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT); if (!this_first_page) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": search memory failed: %llu\n", (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT); return -ENOENT; } if (IS_ERR_VALUE(simp_blkdev_trans_oneseg(this_first_page, this_off, buf + done_cnt, this_cnt, dir))) return -EIO; done_cnt += this_cnt; } return 0; } static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio) { int dir; unsigned long long dsk_offset; struct bio_vec *bvec; int i; void *iovec_mem; switch (bio_rw(bio)) { case READ: case READA: dir = 0; break; case WRITE: dir = 1; break; default: printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": unknown value of bio_rw: %lu\n", bio_rw(bio)); goto bio_err; } if ((bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT) + 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); goto bio_err; } dsk_offset = bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT; bio_for_each_segment(bvec, bio, i) { iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset; if (!iovec_mem) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": map iovec page failed: %p\n", bvec->bv_page); goto bio_err; } if (IS_ERR_VALUE(simp_blkdev_trans(dsk_offset, iovec_mem, bvec->bv_len, dir))) goto bio_err; 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; bio_err: #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif return 0; } 代码在功能上与原先没什么不同, 我们只是从中抽象出处理块设备与一段连续内存之间数据传输的simp_blkdev_trans()函数, 和同样功能的、但数据长度符合块设备数据块长度限制的simp_blkdev_trans_oneseg()函数。 这样一来,程序的结构就比较明显了: simp_blkdev_make_request()负责决定数据传输方向、检查bio请求是否合法、遍历bio中的每个bvec、映射bvec中的内存页, 然后把剩余的工作扔给simp_blkdev_trans(), 而simp_blkdev_trans()函数通过分割请求数据搞定了数据跨越多个块设备数据块的问题,并且顺便把块设备数据块的第一个page给找了出来, 然后邀请simp_blkdev_trans_oneseg()函数出场。 simp_blkdev_trans_oneseg()函数是幸运的,因为前期的大多数铺垫工作已经做完了,而它只要像领导种树一样装模作样的添上最后一铲土, 就可以迎来开热烈的掌声。实际上,simp_blkdev_trans_oneseg()拿到page指针对应的内存,然后根据给定的数据方向执行指定长度的数据传输。 simp_blkdev_trans_oneseg()不需要关心数据长度是否超出块设备数据块边界的问题,正如领导也不会去管那棵树的死活一样。 本章的代码也同样不做实验,因为我们确实也没什么好做的。 至于能不能通过编译,作者已经试过了,有兴趣的读者大概可以验证一下前一句话是不是真的。 作为支持高端内存的前奏,前一章和本章中做了一些可能让人觉得莫名其妙的改动。 不过到此为止,准备工作已经做得差不多了,我们的程序已经为支持高端内存打下坚实的基础。 下一章将进入正题,我们将实现这一期盼已久的功能。 |
+---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | email: [email protected] | +---------------------------------------------------+ | 文章版权归原作者所有。 | | 大家可以自由转载这篇文章,但原版权信息必须保留。 | | 如需用于商业用途,请务必与原作者联系,若因未取得 | | 授权而收起的版权争议,由侵权者自行负责。 | +---------------------------------------------------+ 本章中我们将实现对高端内存的支持。 女孩子相处时,和她聊天,逛街,爬山,看电影,下棋中的每一件事情好像都与结婚扯不上太大的关系, 但经过天天年年的日积月累后,女孩子在潜意识中可能已经把你看成了她生活的一部分, 最终的结果显得是那么的自然,甚至连求婚都有些多余了。 学习也很相似,我们认真学习的的每一样知识,努力寻求的每一个答案就其本身而言, 都不能让自己成为专家,但专家却无一不是经历了长时间的认真学习, 努力钻研和细致思考的结果。 正如我们的程序,经历了前几章中的准备工作,离目标功能的距离大概也不算太远了。 而现在我们要做得就是实现它。 首先改动alloc_diskmem()函数,给这个函数中申请内存的语句、也就是alloc_pages()的gfp_mask中加上__GFP_HIGHMEM标志, 这使得申请块设备的内存块时,会优先考虑使用高端内存。 修改过的函数如下: int alloc_diskmem(void) { int ret; int i; struct page *page; INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL); for (i = 0; i < (simp_blkdev_bytes + SIMP_BLKDEV_DATASEGSIZE - 1) >> SIMP_BLKDEV_DATASEGSHIFT; i++) { page = alloc_pages(GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM, SIMP_BLKDEV_DATASEGORDER); if (!page) { ret = -ENOMEM; goto err_alloc; } ret = radix_tree_insert(&simp_blkdev_data, i, page); if (IS_ERR_VALUE(ret)) goto err_radix_tree_insert; } return 0; err_radix_tree_insert: __free_pages(page, SIMP_BLKDEV_DATASEGORDER); err_alloc: free_diskmem(); return ret; } 不过事情还没有全部做完,拿到了高端内存,我们还要有能力使用它才行。 这就如同带回一个身材火爆的mm仅仅是个开始,更关键的还在于如何不让人家半小时后怒火冲天摔门而归。 因此我们要继续改造使用内存处的代码,也就是simp_blkdev_trans_oneseg()函数。 在此之前这个函数很简单,由于申请的是低端内存,这就保证了这些内存一直是被映射在内核的地址空间中的。 因此只要使用一个page_address()函数就完成了page指针到内存指针的转换问题。 但对于高端内存就没有这样简单了。 首先,高端内存需要在进行访问之前被映射到非线性映射区域,还要在访问之后解除这个映射以免人家骂我们的程序像公仆欠白条, 我们可以使用kmap()和kunmap()函数解决这个问题。 然后我们还要考虑另一个边界问题,也就是页面边界。 由于我们使用的kmap()函数一次只能映射一个物理页面,当需要访问的数据在块设备的内存块中跨越页面边界时, 我们就需要识别这样的情况,并做出相应的处理,也就是多次调用kmap()和kunmap()函数对依次每个页面进行访问。 我们可以采用与先前章节中处理被访问数据跨越多个块设备内存块相似的方法来应对这种情况。 其实对于这种情况,我们还可以选择另一个方案,就是使用vmap()函数。 我们可以使用它把地址分散的多个物理页面映射到一段地址连续的区域中, 当然对我们正在用作块设备存储空间的这些地址连续的物理页面更没有问题。 但问题在于vmap()函数的内部处理比较复杂,这也意味着vmap()函数需要耗费更多的CPU时间, 并且使用vmap()函数时,我们需要一次性映射相当于内存块长度的所有页面, 但我们往往不会访问全部的这些页面,这意味着另一方面的性能损失。 因此,我们决定选择使用kmap()函数,而让程序自己去处理跨页面的访问问题。 参照以上的思路,我们写出了新的simp_blkdev_trans_oneseg()函数: static int simp_blkdev_trans_oneseg(struct page *start_page, unsigned long offset, void *buf, unsigned int len, int dir) { unsigned int done_cnt; struct page *this_page; unsigned int this_off; unsigned int this_cnt; void *dsk_mem; done_cnt = 0; while (done_cnt < len) { /* iterate each page */ this_page = start_page + ((offset + done_cnt) >> PAGE_SHIFT); this_off = (offset + done_cnt) & ~PAGE_MASK; this_cnt = min(len - done_cnt, (unsigned int)PAGE_SIZE - this_off); dsk_mem = kmap(this_page); if (!dsk_mem) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": map device page failed: %p\n", this_page); return -ENOMEM; } dsk_mem += this_off; if (!dir) memcpy(buf + done_cnt, dsk_mem, this_cnt); else memcpy(dsk_mem, buf + done_cnt, this_cnt); kunmap(this_page); done_cnt += this_cnt; } return 0; } 其核心是使用kmap()函数将内存页面映射到内核空间然后再进行访问, 以实现对高端内存的操作。 到此为止,经历了若干章的问题就这样被解决了。 通过这样的改变,我们至少得到了两个好处: 1:避免了争抢宝贵的低端内存 作为内存消耗大户,霸占低端内存的行为不可容忍, 其理由我们在前些章节中已经论述过。 今后我们的程序至少不会在这一方面被人鄙视了。 2:增加了块设备的最大容量 使用原先的程序,在i386中无论如何也无法建立容量超过896M的块设备, 实际上更小,这是由于低端内存不可能全部拿来放块设备的数据, 而现在的程序可以使用包括高端内存在内的所有空闲内存, 这无疑大大增加了块设备的最大容量。 前些章中没有进行的试验憋到现在终于可以开始了。 首先证明这个程序经过了这么多个章节的折腾后仍然是能编译的: # make make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step12 modules make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686' CC [M] /root/test/simp_blkdev/simp_blkdev_step12/simp_blkdev.o Building modules, stage 2. MODPOST CC /root/test/simp_blkdev/simp_blkdev_step12/simp_blkdev.mod.o LD [M] /root/test/simp_blkdev/simp_blkdev_step12/simp_blkdev.ko make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686' # 然后瞧瞧目前的内存状况: # cat /proc/meminfo ... HighTotal: 1146816 kB HighFree: 509320 kB LowTotal: 896356 kB LowFree: 872612 kB ... # 我们看到高端内存与低端内存分别剩余509M和872M。 然后加载现在的模块,为了让模块吃内存的行为表现得更加显眼一些, 我们使用size参数指定了更大的块设备容量: # insmod simp_blkdev.ko size=500M # 现在看看内存的变化情况: # cat /proc/meminfo ... HighTotal: 1146816 kB HighFree: 1652 kB LowTotal: 896356 kB LowFree: 863696 kB ... # 结果显示模块如我们所料的吃掉了500M左右的高端内存。 虽然低端内存看样子也少了一些,我们却不能用模块本身占用的内存空间来解释这一现象, 因为模块的代码和静态数据占用的内存无论如何也到不了8.9M, 或许我们解释为用作一些文件操作的缓存了,还有就是基树结构占用的内存, 这个结构占用的内存会随着块设备容量的增大而增加,或者我们可以计算一下...... 不过现在我们并不打算对这个小问题做过多的关注,因为这是扯淡, 正如闹得沸沸扬扬的周久耕事件的最后调查结果居然仅仅只是公款买烟。 因此我们不会纠缠在这8.9M的问题中,因为很明显大头是在减少的500多兆高端内存上, 这减少的500M高端内存已经足以证明这几章中的修改结果了。 我们再移除这个模块后看看内存的状况: # rmmod simp_blkdev # cat /proc/meminfo ... HighTotal: 1146816 kB HighFree: 504684 kB LowTotal: 896356 kB LowFree: 868480 kB ... # 刚才被占用的高端内存又回来了, 一切都显得如此的和谐。 作为最后一步的测试,我们做一件本章之前做不到的事情, 就是申请大于896M的内存。 刚才我们看到剩余的低端内存和高端内存总共达到了1.37G, 好吧,我们就申请1.3G: # insmod simp_blkdev.ko size=1300M # 这时我们惊喜地发现系统没有DOWN掉。 再看看这时的内存情况: # cat /proc/meminfo ... HighTotal: 1146816 kB HighFree: 41204 kB LowTotal: 896356 kB LowFree: 48284 kB ... # 高端内存与低端内存中的大头基本上都被吃掉了, 数量上也差不多是1.3G,这符合我们的预期。 老让模块占用着这么多的内存也不是什么好主意, 我们放掉: # rmmod simp_blkdev. # 随着本章的结束,围绕高端内存的讨论也终于修成正果了。 不过我们对这个驱动程序的改进还没有完,因为我们要发扬做精每一样事情的精神, 一个民族的振兴,不是靠对小学生进行填鸭式的政治思想教育,也不是靠官员及家属的出国考察, 更不是靠公仆们身先士卒、前仆后继、以自己的健康为代价大吃大喝以创造9000亿的GDP, 而是靠每一个屁民们的诚实、认真、勤劳、勇敢、创造、奉献与精益求精。 |
+---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | email: [email protected] | +---------------------------------------------------+ | 文章版权归原作者所有。 | | 大家可以自由转载这篇文章,但原版权信息必须保留。 | | 如需用于商业用途,请务必与原作者联系,若因未取得 | | 授权而收起的版权争议,由侵权者自行负责。 | +---------------------------------------------------+ 没有最好的代码,是因为我们总能把代码改得更好。 因此我们现在打算做一个小的性能改进,这次我们准备拿free_diskmem()函数下刀。 本质上说,这个改进的意义不大,这是因为free_diskmem()函数仅仅是在模块卸载时被调用, 而对这种执行次数即少又不在关键路径上的函数来说,最好是尽量让他简单以增加可靠性和可读性, 除非它的耗时已经慢到能让人有所感觉,否则0.01秒和0.00001秒是差不多的,毕竟在现实中尼奥不太可能用我们的程序。 但我们仍然打算继续这一改进,一是为了示范什么是没有意义的改进,二是为了通过这一改进示范使用radix_tree_gang_lookup()函数和page->index的技巧。 首先我们看看原先的free_diskmem()函数: void free_diskmem(void) { int i; struct page *page; for (i = 0; i < (simp_blkdev_bytes + SIMP_BLKDEV_DATASEGSIZE - 1) >> SIMP_BLKDEV_DATASEGSHIFT; i++) { page = radix_tree_lookup(&simp_blkdev_data, i); radix_tree_delete(&simp_blkdev_data, i); /* free NULL is safe */ __free_pages(page, SIMP_BLKDEV_DATASEGORDER); } } 它遍历所有的内存块索引,在基树中找到这个内存块的page指针,然后释放内存,顺带着释放掉基数中的这个节点。 考虑到这个函数不仅会在模块卸载时被调用,也会在模块加载时、申请内存中途掉链子时用来擦屁股,因此也需要考虑内存没有完全申请的情况。 所幸的是这种情况下radix_tree_lookup()函数会返回NULL指针,而radix_tree_delete()和__free_pages()函数都能对NULL指针做出我们最期待的处理:就是什么也不做。 这段代码很小很直接,逻辑简单而清晰,性能也差不到哪里去,完全符合设计要求, 不幸的是我们还是打算做一些没必要的优化,借此还可以顺便读一读基树的内核代码。 首先看radix_tree_lookup()函数,它在基数中查找指定索引对应的指针,为了获得这一指针的值,基本上它需要把基树从上到下找一遍。 而对于free_diskmem()函数而言,我们仅仅是需要遍历基树中的所有节点,使用逐一查找的方法进行遍历未免代价太大了。 就像是我们要给在场的所有同学每人发一个糖果,只需要让他们排好队,每人领一个即可,而不需要按照名单找出每个人再发。 为了实现这一思想,我们跑到linux/lib/radix-tree.c中找函数,找啊找,找到了radix_tree_gang_lookup()函数。 radix_tree_gang_lookup()函数虽然不是我们理想中的遍历函数,但也有了八九不离十的功能。 就像在酒吧里找不到D Cup,带回去个C Cup也总比看A片强。 通过radix_tree_gang_lookup()函数,我们可以一次从基树中获取多个节点的信息: unsigned int radix_tree_gang_lookup(struct radix_tree_root *root, void **results, unsigned long first_index, unsigned int max_items); 具体的参数嘛,RTFSC吧。 这是我们注意到使用这个函数时顾此失彼的一面,虽然我们获得了一组需要释放的指针,但却无法获得这些指针的索引。 而执行释放基树中节点的操作时却恰恰需要使用索引作参数。 然后就是一个技巧了,我们借用page结构的index成员来存储这一索引。 之所以可以这样用,是因为page结构的index成员在该页用作页高速缓存时存储相对文件起始处的以页大小为单位的偏移, 而我们所使用的页面不会被同时用作页高速缓存,因此这里可以借用page.index成员。 按照以上思路,我们写出了修改后的代码: void free_diskmem(void) { unsigned long long next_seg; struct page *seglist[64]; int listcnt; int i; next_seg = 0; do { listcnt = radix_tree_gang_lookup(&simp_blkdev_data, (void **)seglist, next_seg, ARRAY_SIZE(seglist)); for (i = 0; i < listcnt; i++) { next_seg = seglist[i]->index; radix_tree_delete(&simp_blkdev_data, next_seg); __free_pages(seglist[i], SIMP_BLKDEV_DATASEGORDER); } next_seg++; } while (listcnt == ARRAY_SIZE(seglist)); } 当然,alloc_diskmem()函数中也需要加上page->index = i这一行,用于把基树的索引存入page.index,修改后的代码如下: int alloc_diskmem(void) { int ret; int i; struct page *page; INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL); for (i = 0; i < (simp_blkdev_bytes + SIMP_BLKDEV_DATASEGSIZE - 1) >> SIMP_BLKDEV_DATASEGSHIFT; i++) { page = alloc_pages(GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM, SIMP_BLKDEV_DATASEGORDER); if (!page) { ret = -ENOMEM; goto err_alloc; } page->index = i; ret = radix_tree_insert(&simp_blkdev_data, i, page); if (IS_ERR_VALUE(ret)) goto err_radix_tree_insert; } return 0; err_radix_tree_insert: __free_pages(page, SIMP_BLKDEV_DATASEGORDER); err_alloc: free_diskmem(); return ret; } 现在试验一下修改后的代码,先看看能不能编译: # make make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step13 modules make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686' CC [M] /root/test/simp_blkdev/simp_blkdev_step13/simp_blkdev.o Building modules, stage 2. MODPOST CC /root/test/simp_blkdev/simp_blkdev_step13/simp_blkdev.mod.o LD [M] /root/test/simp_blkdev/simp_blkdev_step13/simp_blkdev.ko make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686' # 看看当前系统的内存情况: # cat /proc/meminfo HighTotal: 1146816 kB HighFree: 339144 kB LowTotal: 896356 kB LowFree: 630920 kB ... # 这里显示现在剩余339M高端内存和630M低端内存。 然后加载我们的模块,让它吃掉300M内存: # insmod simp_blkdev.ko size=300M # cat /proc/meminfo HighTotal: 1146816 kB HighFree: 137964 kB LowTotal: 896356 kB LowFree: 523900 kB ... # 正如我们的预期,剩余内存减少300M左右。 然后看看卸载模块后的内存情况: # rmmod simp_blkdev # cat /proc/meminfo HighTotal: 1146816 kB HighFree: 338028 kB LowTotal: 896356 kB LowFree: 631044 kB ... # 我们发现剩余内存增加了300M,这意味着模块已经把吃掉的内存吐回来了, 从而可以推断出我们修改过的free_diskmem()函数基本上是能够工作的。 本章的改动不大,就算是暂作休整,以留住忍耐至今忍无可忍认为无需再忍而开始打包收拾行李准备溜之大吉的读者们。 不过下一章中倒是预备了一个做起来让人比较有成就感的功能。 |
+---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | email: [email protected] | +---------------------------------------------------+ | 文章版权归原作者所有。 | | 大家可以自由转载这篇文章,但原版权信息必须保留。 | | 如需用于商业用途,请务必与原作者联系,若因未取得 | | 授权而收起的版权争议,由侵权者自行负责。 | +---------------------------------------------------+ 在本章中我们要做一个比较大的改进,就是实现内存的推迟分配。 这意味着我们并不是在驱动程序加载时就分配用于容纳数据的全部内存, 而是推迟到真正需要用到某块内存时再进行分配。 详细来说,我们将在块设备的某个区域上发生第一次写请求时分配用于容纳被写入数据的内存, 如果读者在之前章节的熏陶下养成了细致的作风和勤于思考的习惯, 应该能发现这里提到的分配内存的时机是第一次写,而不是第一次读写。 现在可能有些读者已经悟出了这样做的道理,让我们无视他们,依然解释一下这样做的目的。 对块设备而言,只要保证读出的数据是最近一次写进的即可。 如果在读数据之前从来没有往块设备的同一块区域中写入数据,那么这时返回任何随机数据都是正确的。 这意味着对于第一次读,我们完全可以返回任意的数据给用户,这时并不需要分配某段内存来存储它。 对真实的物理设备而言,就像我们买回的新硬盘,出厂时盘片中的数据内容是什么都无所谓。 在具体的实现中,我们可以不对用以接收被读出数据的内存进行任何填充,直接告诉上层“已经读好了”, 这样做无疑会更加快速,但这会造成2个问题: 1:这块内存原先的内容最终将被传送到用户程序中,这将造成数据安全问题 2:违背了真实设备的一个潜特性,就是即使这个设备没有写入任何内容,对同一区域的多次读操作返回的内容相同。 因此,我们将向接收数据的内存中写些什么,最简单的就是用全0填充了。 实现这一功能的优点在于,块设备不需要在一开始加载时就占用全部的内存,这优化了系统资源的使用率。 让我们假设块设备自始至终没有被全部填满时,通过本章的功能,将占用更少的内存。 另外,我们甚至可以创建容量远远大于机器物理内存的块设备,只要在随后的使用中不往这个块设备中写入过多的内容即可。 在linux中,类似的思想被广泛应用。 比如对进程的内存区而言,并不是一开始就为这段内存区申请和映射全部需要的物理内存, 又如在不少文件系统中,也不会给没有写入内容的文件部分分配磁盘的。 现在我们就实现这一功能。 分析代码,我们发现不太容易找到往什么地方加代码。 往往在这种情况下,不如首先看看可以剥掉哪部分不需要的代码, 正如初次跟一个mm时,如果两个人都有些害羞,不知道从哪开始、或者正在期待对方打开局面时, 不如先脱下该脱的东西,然后的事情基本上就比较自然了。 现在的代码中,明显可以砍掉的是在驱动程序加载时用于申请容纳数据的内存的代码, 也就是alloc_diskmem()函数,把它砍了,没错,是全砍了。 还有调用它的代码,在simp_blkdev_init()函数里面的这几行: ret = alloc_diskmem(); if (IS_ERR_VALUE(ret)) goto err_alloc_diskmem; 是的,也砍了。 还没完,既然这个函数的调用都没了,那么调用这个函数失败时的出错处理也没用了,也就是: err_alloc_diskmem: put_disk(simp_blkdev_disk); 这两句,不用犹豫了,砍掉。 经过刚才的大刀阔斧后,我们发现......刚才由于砍上瘾了,不小心多砍了一条语句,就是对基树的初始化语句: INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL); 原来它是在alloc_diskmem()函数里面的,现在alloc_diskmem()函数不在了,我们索性把它放到初始化模块的simp_blkdev_init()函数中, 放到刚才原来调用alloc_diskmem()函数的位置就行了。 (注: 其实这里不添加INIT_RADIX_TREE()宏也行,直接在定义基树结构时顺便初始化掉就行了,也就是把 static struct radix_tree_root simp_blkdev_data; 改成 static struct radix_tree_root simp_blkdev_data = RADIX_TREE_INIT(GFP_KERNEL); 就行了,或者改成让人更加撞墙的形式: static RADIX_TREE(simp_blkdev_data, GFP_KERNEL); 也可以,但我们这里的代码中,依然沿用原先的方式。 ) 这样一来,simp_blkdev_init()函数变成了这个样子: static int __init simp_blkdev_init(void) { int ret; ret = getparam(); if (IS_ERR_VALUE(ret)) goto err_getparam; 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; } INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL); 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 >> SIMP_BLKDEV_SECTORSHIFT); add_disk(simp_blkdev_disk); return 0; err_alloc_disk: blk_cleanup_queue(simp_blkdev_queue); err_alloc_queue: err_getparam: return ret; } 淋漓尽致地大砍一番之后,我们发现下一步的工作清晰多了。 现在在模块加载时,已经不会申请所需的内存,而我们需要做的就是, 在处理块设备读写操作时,添加不存在相应内存时的处理代码。 在程序中,查找基数中的一个内存块是在simp_blkdev_trans()函数内完成的,目前的处理是: this_first_page = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT); if (!this_first_page) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": search memory failed: %llu\n", (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT); return -ENOENT; } 也就是找不到内存块时直接看作错误。 在以前这是正确的,因为所有的内存块都在初始化驱动程序时申请了,因此除非电脑的脑子进水了, 运行错了指令,或者人脑的脑子进水了,编错了代码,否则不会发生这种情况。 但现在情况不同了,这时找不到内存块是正常的,这意味着该位置的数据从未被写入过, 因此我们需要在这里做出合理的动作。 也就是在本章开始时所说的,对于读处理返回全0,对于写处理给块设备的这段空间申请内存,并写入数据。 因此我们把上段代码改成了这个样子: this_first_page = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT); if (!this_first_page) { if (!dir) { memset(buf + done_cnt, 0, this_cnt); goto trans_done; } /* prepare new memory segment for write */ this_first_page = alloc_pages( GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM, SIMP_BLKDEV_DATASEGORDER); if (!this_first_page) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": allocate page failed\n"); return -ENOMEM; } this_first_page->index = (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT; if (IS_ERR_VALUE(radix_tree_insert(&simp_blkdev_data, this_first_page->index, this_first_page))) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": insert page to radix_tree failed" " seg=%lu\n", this_first_page->index); __free_pages(this_first_page, SIMP_BLKDEV_DATASEGORDER); return -EIO; } } 对这段代码的流程几乎不要解释了,因为代码本身就是最好的说明。 唯一要提一下的就是goto trans_done这句话,因为前一条语句实质上已经完成了数据读取, 因此需要直接跳转到该段数据处理完成的位置,也就是函数中的done_cnt += this_cnt语句之前。 说到这里猴急的读者可能已经在done_cnt += this_cnt语句之前添加 trans_done: 这一行了,不错,正是要加这一行。 改过的simp_blkdev_trans()函数变成了这个样子: static int simp_blkdev_trans(unsigned long long dsk_offset, void *buf, unsigned int len, int dir) { unsigned int done_cnt; struct page *this_first_page; unsigned int this_off; unsigned int this_cnt; done_cnt = 0; while (done_cnt < len) { /* iterate each data segment */ this_off = (dsk_offset + done_cnt) & ~SIMP_BLKDEV_DATASEGMASK; this_cnt = min(len - done_cnt, (unsigned int)SIMP_BLKDEV_DATASEGSIZE - this_off); this_first_page = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT); if (!this_first_page) { if (!dir) { memset(buf + done_cnt, 0, this_cnt); goto trans_done; } /* prepare new memory segment for write */ this_first_page = alloc_pages( GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM, SIMP_BLKDEV_DATASEGORDER); if (!this_first_page) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": allocate page failed\n"); return -ENOMEM; } this_first_page->index = (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT; if (IS_ERR_VALUE(radix_tree_insert(&simp_blkdev_data, this_first_page->index, this_first_page))) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": insert page to radix_tree failed" " seg=%lu\n", this_first_page->index); __free_pages(this_first_page, SIMP_BLKDEV_DATASEGORDER); return -EIO; } } if (IS_ERR_VALUE(simp_blkdev_trans_oneseg(this_first_page, this_off, buf + done_cnt, this_cnt, dir))) return -EIO; trans_done: done_cnt += this_cnt; } return 0; } 代码就这样被莫名其妙地改完了,感觉这次的改动比预想的少,并且也比较集中, 这其实还是托了前些章的福,正是在此之前对程序结构的规划调整, 在增加可读性的同时,也给随后的维护带来方便。 处于良好维护下的程序代码结构应该越维护越让人赏心悦目,而不是越维护越混乱不堪。 现在我们来试验一下这次修改的效果: 先编译: # make make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step14 modules make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686' CC [M] /root/test/simp_blkdev/simp_blkdev_step14/simp_blkdev.o Building modules, stage 2. MODPOST CC /root/test/simp_blkdev/simp_blkdev_step14/simp_blkdev.mod.o LD [M] /root/test/simp_blkdev/simp_blkdev_step14/simp_blkdev.ko make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686' # 没发现问题。 然后看看目前的内存状况: # cat /proc/meminfo ... HighTotal: 1146816 kB HighFree: 87920 kB LowTotal: 896356 kB LowFree: 791920 kB ... # 可以看出高端和低端内存分别剩余87M和791M。 然后指定size=50M加载模块后看看内存变化: # insmod simp_blkdev.ko size=50M # cat /proc/meminfo ... HighTotal: 1146816 kB HighFree: 86804 kB LowTotal: 896356 kB LowFree: 791912 kB ... # 在这里我们发现剩余内存的变化不大, 这也证明了这次修改的效果,因为加载模块时不会申请用于存储数据的全部内存。 而在原先的代码中,这一步骤将使机器减少大约50M的剩余空间。 然后我们来验证读取块设备时也不会导致分配内存: # dd if=/dev/simp_blkdev of=/dev/null 102400+0 records in 102400+0 records out 52428800 bytes (52 MB) copied, 0.376118 seconds, 139 MB/s # cat /proc/meminfo ... HighTotal: 1146816 kB HighFree: 85440 kB LowTotal: 896356 kB LowFree: 791888 kB ... # 剩余内存几乎没有变化,这证明了我们的设想。 然后是写设备的情况: # dd if=/dev/zero of=/dev/simp_blkdev dd: writing to `/dev/simp_blkdev': No space left on device 102401+0 records in 102400+0 records out 52428800 bytes (52 MB) copied, 0.542117 seconds, 96.7 MB/s # cat /proc/meminfo ... HighTotal: 1146816 kB HighFree: 34116 kB LowTotal: 896356 kB LowFree: 791516 kB ... # 这时剩余内存终于减少了大约50M, 这意味着驱动程序申请了大约50M的内存用于存储写入的数据。 如果向已写入的位置再次写入数据,理论上不应该造成再一次的分配, 让我们试试: # dd if=/dev/zero of=/dev/simp_blkdev dd: writing to `/dev/simp_blkdev': No space left on device 102401+0 records in 102400+0 records out 52428800 bytes (52 MB) copied, 0.644972 seconds, 81.3 MB/s # cat /proc/meminfo ... HighTotal: 1146816 kB HighFree: 33620 kB LowTotal: 896356 kB LowFree: 791516 kB ... # 结果与预想一致。 现在卸载模块: # rmmod simp_blkdev # cat /proc/meminfo ... HighTotal: 1146816 kB HighFree: 84572 kB LowTotal: 896356 kB LowFree: 791640 kB ... # 我们发现被驱动程序使用的内存被释放回来了。 如果以上的实验没有让读者过瘾的话,我们来继续一个过分一些的, 也就是创建空间远远大于机器物理内存的块设备。 首先我们看看目前的系统内存状况: # cat /proc/meminfo ... HighTotal: 1146816 kB HighFree: 77688 kB LowTotal: 896356 kB LowFree: 783296 kB ... # 机器的总内存是2G,目前剩余的高、低端内存加起来是860M左右。 然后我们加载模块,注意一下size参数的值: # insmod simp_blkdev.ko size=10000G # 命令成功返回,而如果换作原先的代码, 命令出错返回......是不太可能的, 最可能的大概是内核直接panic。 这是因为申请光全部内存的操作将导致申请出错时运行的用于释放内存的代码所需要的内存都无法满足。 无论我们设置多大的块设备容量,模块加载后只要不执行写操作, 驱动程序都不会申请存储数据的内存。而这个测试: # cat /proc/meminfo ... HighTotal: 1146816 kB HighFree: 75208 kB LowTotal: 896356 kB LowFree: 783132 kB ... # 也证明了这一点。 现在我们看看这时的块设备情况: # fdisk -l /dev/simp_blkdev Disk /dev/simp_blkdev: 10737.4 GB, 10737418240000 bytes 255 heads, 63 sectors/track, 1305416 cylinders Units = cylinders of 16065 * 512 = 8225280 bytes Disk /dev/simp_blkdev doesn't contain a valid partition table # 果然是10000G,这可以通过换算10737418240000 bytes得到。 而fdisk显示10737.4 GB是因为它是按照1k=1000字节、1M=1000K、1G=1000M来算的, 这种流氓的算法给硬盘厂商的缺斤少两行为提供了极好的借口。 这里省略fdisk、mkfs、mount、cp等操作, 直接用dd往这个"10000G磁盘"中写入50M的数据: # dd if=/dev/zero of=/dev/simp_blkdev bs=1M count=50 50+0 records in 50+0 records out 52428800 bytes (52 MB) copied, 0.324054 seconds, 162 MB/s # cat /proc/meminfo ... HighTotal: 1146816 kB HighFree: 23512 kB LowTotal: 896356 kB LowFree: 782884 kB ... # 现在的内存情况证明我们的"10000G磁盘"为这些数据申请了50M的内存。 实验差不多了,我们卸载模块: # rmmod simp_blkdev. # 做完以上的实验,读者可能会有一个疑问,如果我们真的向那个"10000G磁盘"中写入了10000G的数据怎么样呢? 回答可能不太如人意,就是系统很可能会panic。 因为这个操作将迫使驱动程序吃掉全部可能获得的物理内存,并且在吃光最后那么一丁点内存之前不会发生错误, 这也意味着走到出错处理这一步的时候,系统已经几乎无可救药了。其实在此之前系统就会一次进行: 释放缓存、试图把所有的用户进程的内存换出、杀死全部能够杀死的进程等操作。 而我们的驱动程序由于被看作是内核的一部分,却不会被停止,而是在继续不停的吃掉通过上述方式释放出的可怜的内存。 试想,一个已经走到这一步的系统还有什么继续运行的可能呢? 因此,我们的程序确实需要改善以解决这个问题,因为世界上总是有一些疯狂的人在想各种办法虐待电脑。 但我们并不打算在本教程中解决它,因为这个教程中的每一章都企图为读者说明一类知识或一种方法, 而不是仅仅为了这个示例性质的程序的功能本身。 所以这一项改善就当作是留给读者的练习了。 本章通过改善块设备驱动程序实现了内存的滞后申请, 其目的在于介绍这种方法,以使它在其他的相似程序中也得以实现。 不过,这并不意味着作者希望读者把这种方法过分引用, 比如引用成平时不学习,考试前临时抱佛脚。 |
+---------------------------------------------------+ | 写一个块设备驱动 | +---------------------------------------------------+ | 作者:赵磊 | | email: [email protected] | +---------------------------------------------------+ | 文章版权归原作者所有。 | | 大家可以自由转载这篇文章,但原版权信息必须保留。 | | 如需用于商业用途,请务必与原作者联系,若因未取得 | | 授权而收起的版权争议,由侵权者自行负责。 | +---------------------------------------------------+ 在上一章中我们对这个块设备驱动所作的更改使它具备了动态申请内存的能力, 但实际上同时也埋下一个隐患,就是数据访问冲突。 这里我们顺便唠叨一下内核开发中的同步问题。 提到数据访问同步,自然而然会使人想到多进程、多线程、加锁、解锁、 信号量、synchronized关键字等东西,然后就很头疼。 对于用户态程序,网上大量的解释数据同步概念和方法的文章给人的印象大概是: 同步很危险,编程要谨慎, 处处有机关,问题很难找。 对于第一次进行多线程时编程的人来说,感觉可能是以下两种: 一种是觉得程序中处处都会有问题,任何一条访问数据的指令都不安全, 恨不得把程序中所有的数据都加上锁,甚至打算给锁本身的数据再加个锁, 另一种是没觉得有什么困难,根本不去理什么都互斥不互斥, 就按原先的来,编出的程序居然也运行得很顺。 然后怀着这两种想法人通过不断的学习和实践掌握了数据同步的知识后认识到, 数据同步其实并不像前一种想法那样危险,也不像后一种想法那样简单。 所幸的是对于不少用户态程序来说,倒是可以不用考虑数据同步问题。 至少当我们刚开始写HelloWorld时不用去理这个麻烦。 而对于内核态代码而言,很不幸,整个儿几乎都相当于用户态的多线程。 其实事情也并非原本就是这么糟的。 在很久很久以前,山是青的,草是绿的,牛奶是能喝的, 见到老人摔跤是敢扶的,作者是纯情的,电脑也是单CPU的。 那时的内核环境很静,很美。除了中断会时不时地捣捣乱,其余的都挺诗意。 代码独个儿在跑,就像是一辆汽车在荒漠上奔驰,因为没有其他妨碍, 几乎可以毫无顾忌地访问数据,而不用考虑什么万恶的访问冲突。 唯一要考虑的从天而降的中断奥特曼,解决的方法倒也不难,禁用了中断看你还能咋的。 然后随着作者的成长,目光从书本转向了美眉,计算机也由单CPU发展成了多CPU。 内核代码的执行环境终于开始热闹起来,由于每个CPU上都在执行任务, 这些任务进入到对应的内核态时会出现多条内核指令流同时执行, 这些指令流对全局数据的访问很明显就牵涉到了同步问题,这是开端。 从那时起编程时要考虑其他CPU上的事情了。 然后随着作者的进一步成长,目光从美眉的脸转向了胸, CPU制造商为了贯彻给程序员找麻烦的精神,搞出了乱序执行。 这一创举惊醒了多年来还在梦中的诸多程序员,原来,程序不是按程序执行的啊。 正如林高官说的:“我是交通部派来的,级别和你们市长一样高,敢跟我斗, 你们这些人算个屁呀!”原来,无职无权的平民百姓就是屁啊。 正当程序员从睡梦中惊醒还没缓过神时,编译器又跟着捣乱, “你CPU都能乱序了,凭什么不让我乱序?” 然后热闹了,好在我们还有mb()、rmb()、wmb()、barrier()这几根救命稻草, 事情倒是没变得太糟。 然后随着作者的进一步成长,目光从美眉的胸转向了臀, 内核也从一开始时被动的为了适应多CPU而不得已半推半就支持多任务并行, 转向了主动掀起裙角管它一个还是几个CPU都去多任务了。 从技术面解释,这就是大名鼎鼎的内核抢占。 内核的程序员从此不仅要考虑其他CPU,好要提妨自个儿的CPU, 因为执行代码的CPU说不定什么时候就莫名其妙的被调度执行别的任务了。 如果以作者的成长历程为主线解释内核的演化还不至于太混乱的话, 我们还可以考虑再介绍一下spin_lock, mutex_lock, preempt_disable, atomic_t和rcu等函数,不过作者忍住了这一冲动,还是让读者去google吧。 然后回到我们的代码,现在的代码是有问题的。 比如simp_blkdev_trans()函数中,假设2个任务同时向块设备的同一区域写数据, 而这块区域在这之前没有被写过,也就是说还没有申请内存,那么如果运气够好的话, 这两个进程可能几乎同时运行到: this_first_page = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT); 这句,很明显这两个任务得到的this_first_page都是NULL,然后它们争先恐后的执行 if (!this_first_page) 判断,从而进入之后的alloc_pages,随后它们都会为这个块设备区域申请内存,并加入基树结构。 如果运气爆发的话,这两个任务radix_tree_insert()的代码中将有机会近乎同时越过 if (slot != NULL) return -EEXIST; 的最后防线,先后将新申请的内存指针赋值给基树结点。 虽然x86的多处理器对同一块内存的写操作是原子的, 这样至少不会因为这两个任务同时赋值基树指针造成指针指向莫名其妙的值, 但这仍然也解决不了我们的问题,后一个赋值操作将覆盖前一个操作的结果, 基数节点最终将指向稍后一点执行赋值操作的任务。 这两个任务最终将运行到radix_tree_insert()函数的结尾,而函数的返回值都是漂亮的0。 剩下的事情扳脚丫子大概也能想出来了,这两个任务都将自欺欺人地认为自己正确而成功地为块设备分配了内存, 而真相是其中一个任务拿走的内存却再也没有机会拿回来了。 至于解决方法嘛,当然是加锁。 只要我们让“查找基数中有没有这个节点”到“分配内存并插入这节点”的过程中没有其他任务的打搅, 就自然的解决了这个问题。 首先定义一个锁,因为是用来锁simp_blkdev_data的, 就放在static struct radix_tree_root simp_blkdev_data;后面吧: DEFINE_MUTEX(simp_blkdev_datalock); /* protects the disk data op */ 然后根据刚才的思想给对simp_blkdev_trans()函数中的simp_blkdev_datalock的操作加锁, 也就是在 this_first_page = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT); 语句之前添加: mutex_lock(&simp_blkdev_datalock); 操作结束后被忘了把锁还回去,否则下次再操作时就成死锁了,因此在 trans_done: 后面加上 mutex_unlock(&simp_blkdev_datalock); 这一行。 完成了吗?细心看看就知道还没完。 simp_blkdev_trans()函数中有一些判断异常的代码,这些代码大多是扔出一条printk就直接return的。 这样可不行,可千万别让它们临走时把锁也顺回去了。 这意味着我们要在simp_blkdev_trans()函数中的3个故障时return的代码前完成锁的释放。 因此simp_blkdev_trans()函数最后就成了这样: static int simp_blkdev_trans(unsigned long long dsk_offset, void *buf, unsigned int len, int dir) { unsigned int done_cnt; struct page *this_first_page; unsigned int this_off; unsigned int this_cnt; done_cnt = 0; while (done_cnt < len) { /* iterate each data segment */ this_off = (dsk_offset + done_cnt) & ~SIMP_BLKDEV_DATASEGMASK; this_cnt = min(len - done_cnt, (unsigned int)SIMP_BLKDEV_DATASEGSIZE - this_off); mutex_lock(&simp_blkdev_datalock); this_first_page = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT); if (!this_first_page) { if (!dir) { memset(buf + done_cnt, 0, this_cnt); goto trans_done; } /* prepare new memory segment for write */ this_first_page = alloc_pages( GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM, SIMP_BLKDEV_DATASEGORDER); if (!this_first_page) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": allocate page failed\n"); mutex_unlock(&simp_blkdev_datalock); return -ENOMEM; } this_first_page->index = (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT; if (IS_ERR_VALUE(radix_tree_insert(&simp_blkdev_data, this_first_page->index, this_first_page))) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": insert page to radix_tree failed" " seg=%lu\n", this_first_page->index); __free_pages(this_first_page, SIMP_BLKDEV_DATASEGORDER); mutex_unlock(&simp_blkdev_datalock); return -EIO; } } if (IS_ERR_VALUE(simp_blkdev_trans_oneseg(this_first_page, this_off, buf + done_cnt, this_cnt, dir))) { mutex_unlock(&simp_blkdev_datalock); return -EIO; } trans_done: mutex_unlock(&simp_blkdev_datalock); done_cnt += this_cnt; } return 0; } 这个函数差不多了。 我们再看看代码中还有什么地方也对simp_blkdev_data进行操作来着,别漏掉了这些小王八蛋。 查找一下代码,我们发现free_diskmem()函数中也进行了操作。 其实从理论上说,这里不加锁是不会产生问题的,因为对内核在执行对块设备设备时, 会锁住这个设备对应的模块(天哪,又是锁,这一章和锁彪上了), 其结果是在simp_blkdev_trans()函数操作simp_blkdev_data的过程中, 该模块无法卸载,从而无法不会运行到free_diskmem()函数。 那么如果同时卸载这个模块呢,回答是也没有问题,英勇的模块锁也会搞掂这种情况。 这一章由于没有进行功能增加,就不列出修改后模块的测试经过了, 不过作为对读者的安慰,我们将列出到目前为止经历了大大小小修改后的全部模块代码。 看到这些代码,我们能历历在目的回忆出读这篇教程到现在为止所经受的全部折磨和苦难。 当然也能感受到坚持到现在所得到的知识和领悟。 对于Linux而言,甚至仅仅对于块设备驱动程序而言,这部教程揭开的也仅仅是冰山一角。 而更多的知识其实离我们很近,在google上,在代码中,在心中。 学习,是要用心,不断地去想,同时要有恒心、耐心、要细心, 人应该越学越谦虚,问题应该越学越多,这大概就是作者通过这部教程最想告诉读者的。 #include #include #include #include /* * A simple block device driver based on memory * * Copyright 2008 - * Zhaolei * * Sample for using: * Create device file (first time only): * Note: If your system have udev, it can create device file for you in time * of lsmod and fdisk automatically. * Otherwise you need to create them yourself by following steps. * mknod /dev/simp_blkdev b 72 0 * mknod /dev/simp_blkdev1 b 72 1 * mknod /dev/simp_blkdev2 b 72 2 * * Create dirs for test (first time only): * mkdir /mnt/temp1/ # first time only * mkdir /mnt/temp2/ # first time only * * Run it: * make * insmod simp_blkdev.ko * # or insmod simp_blkdev.ko size=numK/M/G/T * fdisk /dev/simp_blkdev # create 2 patitions * mkfs.ext3 /dev/simp_blkdev1 * mkfs.ext3 /dev/simp_blkdev2 * mount /dev/simp_blkdev1 /mnt/temp1/ * mount /dev/simp_blkdev2 /mnt/temp2/ * # play in /mnt/temp1/ and /mnt/temp2/ * umount /mnt/temp1/ * umount /mnt/temp2/ * rmmod simp_blkdev.ko * */ #define SIMP_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR #define SIMP_BLKDEV_DISKNAME "simp_blkdev" #define SIMP_BLKDEV_SECTORSHIFT (9) #define SIMP_BLKDEV_SECTORSIZE (1ULL< /* usable partitions is SIMP_BLKDEV_MAXPARTITIONS - 1 */ #define SIMP_BLKDEV_MAXPARTITIONS (64) #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)) static struct request_queue *simp_blkdev_queue; static struct gendisk *simp_blkdev_disk; static struct radix_tree_root simp_blkdev_data; DEFINE_MUTEX(simp_blkdev_datalock); /* protects the disk data op */ static char *simp_blkdev_param_size = "16M"; module_param_named(size, simp_blkdev_param_size, charp, S_IRUGO); static unsigned long long simp_blkdev_bytes; static int simp_blkdev_trans_oneseg(struct page *start_page, unsigned long offset, void *buf, unsigned int len, int dir) { unsigned int done_cnt; struct page *this_page; unsigned int this_off; unsigned int this_cnt; void *dsk_mem; done_cnt = 0; while (done_cnt < len) { /* iterate each page */ this_page = start_page + ((offset + done_cnt) >> PAGE_SHIFT); this_off = (offset + done_cnt) & ~PAGE_MASK; this_cnt = min(len - done_cnt, (unsigned int)PAGE_SIZE - this_off); dsk_mem = kmap(this_page); if (!dsk_mem) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": map device page failed: %p\n", this_page); return -ENOMEM; } dsk_mem += this_off; if (!dir) memcpy(buf + done_cnt, dsk_mem, this_cnt); else memcpy(dsk_mem, buf + done_cnt, this_cnt); kunmap(this_page); done_cnt += this_cnt; } return 0; } static int simp_blkdev_trans(unsigned long long dsk_offset, void *buf, unsigned int len, int dir) { unsigned int done_cnt; struct page *this_first_page; unsigned int this_off; unsigned int this_cnt; done_cnt = 0; while (done_cnt < len) { /* iterate each data segment */ this_off = (dsk_offset + done_cnt) & ~SIMP_BLKDEV_DATASEGMASK; this_cnt = min(len - done_cnt, (unsigned int)SIMP_BLKDEV_DATASEGSIZE - this_off); mutex_lock(&simp_blkdev_datalock); this_first_page = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT); if (!this_first_page) { if (!dir) { memset(buf + done_cnt, 0, this_cnt); goto trans_done; } /* prepare new memory segment for write */ this_first_page = alloc_pages( GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM, SIMP_BLKDEV_DATASEGORDER); if (!this_first_page) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": allocate page failed\n"); mutex_unlock(&simp_blkdev_datalock); return -ENOMEM; } this_first_page->index = (dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT; if (IS_ERR_VALUE(radix_tree_insert(&simp_blkdev_data, this_first_page->index, this_first_page))) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": insert page to radix_tree failed" " seg=%lu\n", this_first_page->index); __free_pages(this_first_page, SIMP_BLKDEV_DATASEGORDER); mutex_unlock(&simp_blkdev_datalock); return -EIO; } } if (IS_ERR_VALUE(simp_blkdev_trans_oneseg(this_first_page, this_off, buf + done_cnt, this_cnt, dir))) { mutex_unlock(&simp_blkdev_datalock); return -EIO; } trans_done: mutex_unlock(&simp_blkdev_datalock); done_cnt += this_cnt; } return 0; } static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio) { int dir; unsigned long long dsk_offset; struct bio_vec *bvec; int i; void *iovec_mem; switch (bio_rw(bio)) { case READ: case READA: dir = 0; break; case WRITE: dir = 1; break; default: printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": unknown value of bio_rw: %lu\n", bio_rw(bio)); goto bio_err; } if ((bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT) + 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); goto bio_err; } dsk_offset = bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT; bio_for_each_segment(bvec, bio, i) { iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset; if (!iovec_mem) { printk(KERN_ERR SIMP_BLKDEV_DISKNAME ": map iovec page failed: %p\n", bvec->bv_page); goto bio_err; } if (IS_ERR_VALUE(simp_blkdev_trans(dsk_offset, iovec_mem, bvec->bv_len, dir))) goto bio_err; 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; bio_err: #if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24) bio_endio(bio, 0, -EIO); #else bio_endio(bio, -EIO); #endif return 0; } 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 >> SIMP_BLKDEV_SECTORSHIFT / geo->heads / geo->sectors; return 0; } struct block_device_operations simp_blkdev_fops = { .owner = THIS_MODULE, .getgeo = simp_blkdev_getgeo, }; void free_diskmem(void) { unsigned long long next_seg; struct page *seglist[64]; int listcnt; int i; next_seg = 0; do { listcnt = radix_tree_gang_lookup(&simp_blkdev_data, (void **)seglist, next_seg, ARRAY_SIZE(seglist)); for (i = 0; i < listcnt; i++) { next_seg = seglist[i]->index; radix_tree_delete(&simp_blkdev_data, next_seg); __free_pages(seglist[i], SIMP_BLKDEV_DATASEGORDER); } next_seg++; } while (listcnt == ARRAY_SIZE(seglist)); } 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 + SIMP_BLKDEV_SECTORSIZE - 1) & SIMP_BLKDEV_SECTORMASK; return 0; } static int __init simp_blkdev_init(void) { int ret; ret = getparam(); if (IS_ERR_VALUE(ret)) goto err_getparam; 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; } INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL); 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 >> SIMP_BLKDEV_SECTORSHIFT); add_disk(simp_blkdev_disk); return 0; err_alloc_disk: blk_cleanup_queue(simp_blkdev_queue); err_alloc_queue: err_getparam: 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); } module_init(simp_blkdev_init); module_exit(simp_blkdev_exit); MODULE_LICENSE("GPL"); 追记:偶然看到刚才的代码首部注释,Copyright后面还是2008年。 大概是从第一章开始一直这样拷贝过来的。 这部教程从2008年11月断断续续的写到了2009年3月,终于功德圆满了。 作为作者写的第一个如此长度篇幅的教程,炸一眼瞟过来,倒也还像个样子, 看来写教程并不是太难高攀的事情,因此如果读者也时不时地有一些写起来的冲动, 就不妨开始吧: ) 本章以块设备驱动程序的代码为例,说明了内核中的同步概念, 当然,在不少情况下,程序员遇到的同步问题比这里的要复杂的多, 内核中也采用了很多方法和技巧来处理同步,了解和学习这些知识, 收获的不仅是数据同步本身的解决方法,更是一种思路, 这对于更一般的程序设计都是有很大帮助的,因此有空时google一下, 总能找到自己想了解的知识。 |