写一个块设备驱动5,6
Posted ztguang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了写一个块设备驱动5,6相关的知识,希望对你有一定的参考价值。
http://blogold.chinaunix.net/u3/108239/showart.php?id=2144628
第5章
+---------------------------------------------------+
| 写一个块设备驱动 |
+---------------------------------------------------+
| 作者:赵磊 |
| email: [email protected] |
+---------------------------------------------------+
| 文章版权归原作者所有。 |
| 大家可以自由转载这篇文章,但原版权信息必须保留。 |
| 如需用于商业用途,请务必与原作者联系,若因未取得 |
| 授权而收起的版权争议,由侵权者自行负责。 |
+---------------------------------------------------+
既然上一章结束时我们已经预告了本章的内容,
那么本章中我们就让这个块设备有能力告知操作系统它的“物理结构”。
当然,对于基于内存的块设备来说,什么样的物理结构并不重要,
这就如同从酒吧带mm回家时不需要打听她的姓名一样。
但如果不幸遇到的是兼职,并且带她去不入流的招待所时,
建议最好还是先串供一下姓名、生日和职业等信息,
以便JJ查房时可以伪装成情侣。
同样,如果要实现的是真实的物理块设备驱动,
那么返回设备的物理结构时大概不能这么随意。
对于块设备驱动程序而言,我们现在需要关注那条目前只有一行的struct block_device_operations simp_blkdev_fops结构。
到目前为止,它存在的目的仅仅是因为它必须存在,但马上我们将发现它存在的另一个目的:为块设备驱动添加获得块设备物理结构的接口。
对于具有极强钻研精神的极品读者来说,大概在第一章中就会自己去看struct block_device_operations结构,然后将发现这个结构其实还挺复杂:
struct block_device_operations {
int (*open) (struct block_device *, fmode_t);
int (*release) (struct gendisk *, fmode_t);
int (*locked_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*direct_access) (struct block_device *, sector_t,
void **, unsigned long *);
int (*media_changed) (struct gendisk *);
int (*revalidate_disk) (struct gendisk *);
int (*getgeo)(struct block_device *, struct hd_geometry *);
struct module *owner;
};
在前几章中,我们邂逅过其中的owner成员变量,它用于存储这个结构的所有者,也就是我们的模块,因此我们做了如下的赋值:
.owner = THIS_MODULE,
而这一章中,我们将与它的同胞妹妹------getgeo也亲密接触一下。
我们要做的是:
1:在block_device_operations中增加getgeo成员变量初值的设定,指向我们的“获得块设备物理结构”函数。
2:实现我们的“获得块设备物理结构”函数。
第一步很简单,我们暂且为“获得块设备物理结构”函数取个名字叫simp_blkdev_getgeo()吧,也避免了在下文中把这么一大堆汉字拷来拷去。
在simp_blkdev_fops中添加.getgeo指向simp_blkdev_getgeo,也就是把simp_blkdev_fops结构改成这个样子:
struct block_device_operations simp_blkdev_fops = {
.owner = THIS_MODULE,
.getgeo = simp_blkdev_getgeo,
};
第二步难一些,但也难不到哪去,在代码中的struct block_device_operations simp_blkdev_fops这行之前找个空点的场子,把如下函数插进去:
static int simp_blkdev_getgeo(struct block_device *bdev,
struct hd_geometry *geo)
{
/*
* capacity heads sectors cylinders
* 0~16M 1 1 0~32768
* 16M~512M 1 32 1024~32768
* 512M~16G 32 32 1024~32768
* 16G~... 255 63 2088~...
*/
if (SIMP_BLKDEV_BYTES < 16 * 1024 * 1024) {
geo->heads = 1;
geo->sectors = 1;
} else if (SIMP_BLKDEV_BYTES < 512 * 1024 * 1024) {
geo->heads = 1;
geo->sectors = 32;
} else if (SIMP_BLKDEV_BYTES < 16ULL * 1024 * 1024 * 1024) {
geo->heads = 32;
geo->sectors = 32;
} else {
geo->heads = 255;
geo->sectors = 63;
}
geo->cylinders = SIMP_BLKDEV_BYTES>>9/geo->heads/geo->sectors;
return 0;
}
因为这里我们用到了struct hd_geometry结构,所以还要增加一行#include。
这个函数的目的,是选择适当的物理结构信息装入struct hd_geometry *geo结构。
当然,为了克服上一章中只能分成2个区的问题,我们应该尽可能增加磁道的数量。
希望读者不要理解成分几个区就需要几个磁道,这意味着一个磁道一个区,也意味着每个区必须一般大小。
由于分区总是以磁道为边界,尽可能增加磁道的数量不仅仅是为了让块设备容纳更多的分区,
更重要的是让分区的实际大小更接近于分区时的指定值,也就是提高实际做出的分区容量的精度。
不过对于设置的物理结构值,还存在一个限制,就是struct hd_geometry中的数值上限。
我们看struct hd_geometry的内容:
struct hd_geometry {
unsigned char heads;
unsigned char sectors;
unsigned short cylinders;
unsigned long start;
};
unsigned char的磁头数和每磁道扇区数决定了其255的上限,同样,unsigned short的磁道数决定了其65535的上限。
这还不算,但在前一章中,我们知道对于现代硬盘,磁头数和每磁道扇区数通常取的值是255和63,
再组合上这里的65535的磁道数上限,hd_geometry能够表示的最大块设备容量是255*63*65535*512/1024/1024/1024=502G。
显然目前linux支持的最大硬盘容量大于502G,那么对于这类块设备,内核是如何通过hd_geometry结构表示其物理结构的呢?
诀窍不在内核,而在于用户态程序如fdisk等通过内核调用获得hd_geometry结构后,
会舍弃hd_geometry.cylinders内容,取而代之的是直接通过hd_geometry中的磁头数和每磁道扇区数以及硬盘大小去计算磁道数。
因此对于超过502G的硬盘,由于用户程序得出的磁道数与hd_geometry.cylinders无关,所以我们往往在fdisk中能看到这块硬盘的磁道数大于65535。
刚才扯远了,现在言归正题,我们决定让这个函数对于任何尺寸的块设备,总是试图返回比较漂亮的物理结构。
漂亮意味着返回的物理结构既要保证拥有足够多的磁道,也要保证磁头数和每磁道扇区数不超过255和63,同时最好使用程序员看起来比较顺眼的数字,
如:1、2、4、8、16、32、64等。
当然,我们也希望找到某个One Shot公式适用于所有大小的块设备,但很遗憾目前作者没找到,因此采用了分段计算的方法:
首先考虑容量很小的块设备:
即使磁头数和每磁道扇区数都是1,磁道数也不够多时,我们会将磁头数和每磁道扇区数都固定为1,以使磁道数尽可能多,以提高分区的精度。
因此磁道数随块设备容量而上升。
虽然我们已经知道了磁道数其实可以超过unsigned short的65535上限,但在这里却没有必要,因此我们要给磁道数设置一个上限。
因为不想让上限超过65535,同时还希望上限也是一个程序员喜欢的数字,因此这里选择了32768。
当然,当磁道数超过32768时,已经意味着块设备容量不那么小了,也就没有必要使用这种情况中如此苛刻的磁头数和每磁道扇区数了。
简单来说,当块设备容量小于1个磁头、每磁道1扇区和32768个磁道对应的容量--也就是16M时,我们将按照这种情况处理。
然后假设块设备容量已经大于16M了:
我们希望保证块设备包含足够多的磁道,这里我们认为1024个磁道应该不少了。
磁道的最小值发生在块设备容量为16M的时候,这时使用1024作为磁道数,可以计算出磁头数*每磁道扇区数=32。
这里暂且把磁头数和每磁道扇区数固定为1和32,而让磁道数随着块设备容量的增大而增加。
同时,我们还是磁道的上限设置成32768,这时的块设备容量为512M。
总结来说,当块设备容量在16M和512M之间时,我们把磁头数和每磁道扇区数固定为1和32。
然后对于容量大于512M的块设备:
与上述处理相似,当块设备容量在512M和16G之间时,我们把磁头数和每磁道扇区数固定为32和32。
最后的一种情况:
块设备已经足够大了,大到即使我们使用磁头数和每磁道扇区数的上限,
也能获得足够多的磁道数。这时把磁头数和每磁道扇区数固定为255和63。
至于磁道数就算出多少是多少了,即使超过unsigned short的上限也无所谓,反正用不着。
随着这个函数解说到此结束,我们对代码的修改也结束了。
现在开始试验:
编译和加载:
# make
make -C /lib/modules/2.6.27.4/build SUBDIRS=/mnt/host_test/simp_blkdev/simp_blkdev_step05 modules
make[1]: Entering directory `/mnt/ltt-kernel‘
CC [M] /mnt/host_test/simp_blkdev/simp_blkdev_step05/simp_blkdev.o
Building modules, stage 2.
MODPOST 1 modules
CC /mnt/host_test/simp_blkdev/simp_blkdev_step05/simp_blkdev.mod.o
LD [M] /mnt/host_test/simp_blkdev/simp_blkdev_step05/simp_blkdev.ko
make[1]: Leaving directory `/mnt/ltt-kernel‘
# insmod simp_blkdev.ko
#
用fdisk打开设备文件
# fdisk /dev/simp_blkdev
Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
Building a new DOS disklabel. Changes will remain in memory only,
until you decide to write them. After that, of course, the previous
content won‘t be recoverable.
Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)
Command (m for help):
看看设备的物理结构:
Command (m for help): p
Disk /dev/simp_blkdev: 16 MB, 16777216 bytes
1 heads, 32 sectors/track, 1024 cylinders
Units = cylinders of 32 * 512 = 16384 bytes
Device Boot Start End Blocks Id System
Command (m for help):
我们发现,现在的设备有1个磁头、32扇区每磁道、1024个磁道。
这是符合代码中的处理的。
本章的内容也不是太难,连同上一章,我们已经休息2章了。
聪明的读者可能已经猜到作者打算说什么了。
不错,下一章会有一个surprise。
<未完,待续>
第6章
+---------------------------------------------------+
| 写一个块设备驱动 |
+---------------------------------------------------+
| 作者:赵磊 |
| email: [email][email protected][/email] |
+---------------------------------------------------+
| 文章版权归原作者所有。 |
| 大家可以自由转载这篇文章,但原版权信息必须保留。 |
| 如需用于商业用途,请务必与原作者联系,若因未取得 |
| 授权而收起的版权争议,由侵权者自行负责。 |
+---------------------------------------------------+
经历了内容极为简单的前两章的休息,现在大家一定感到精神百倍了。
作为已经坚持到现在的读者,对接下去将要面临的内容大概应该能够猜得八九不离十了,
具体的内容猜不出来也无妨,但一定将是具有增加颅压功效的。
与物理块设备驱动程序的区别在于,我们的驱动程序使用内存来存储块设备中的数据。
到目前为止,我们一直都是使用这样一个静态数组来担负这一功能的:
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];
如果读者懂得一些模块的知识,或者现在赶紧去临时抱佛脚google一些模块知识,
应该知道模块其实是加载在非线性映射区域的。
详细来说,在加载模块时,根据模块的ELF信息(天哪,又要去google elf了),确定这个模块所需的静态内存大小。
这些内存用来容纳模块的二进制代码,以及静态变量。然后申请容纳这些数据的一大堆页面。
当然,这些页面并不是连续的,而代码和变量却不可能神奇到即使被切成一块一块的也能正常工作。
因此需要在非线性映射区域中找到一块连续的地址(现在有要去google非线性映射区域了),用来将刚才申请到的一块一块的内存页映射到这个地址段中。
最后模块被请到这段区域中,然后执行模块的初始化函数......
现在看我们这个模块中的simp_blkdev_data变量,如果不是现在刻意关注,这个变量看起来显得那么得普通。
正如其它的一些名字原先也是那么的普通,但由于一些突发的事件受到大家的热烈关注,
比如一段视频让我们熟悉了kappa和陆佳妮,比如呼吸税让我们认识了蒋有绪。
现在我们开始关注simp_blkdev_data变量了,导火索是刚才介绍的非线性映射区域。
模块之所以被加载到非线性映射区域,是因为很难在线性映射区域中得到加载模块所需的连续的内存。
但使用非线性映射区域也并非只赚不赔的生意,至少在i386结构中,非线性映射区域实在是太小了。
在物理内存大于896M的i386系统中,整个非线性映射区域不会超过128M。
相反如果物理内存小于896M(不知道该算是幸运还是不幸),非线性映射区域反而会稍微大一些,这种情况我想我们可以不用讨论了,毕竟不能为了加载一个模块去拔内存。
因此我们的结论是:非线性映射区域是很紧张的资源,我们要节约使用。
而像我们现在这个模块中的simp_blkdev_data却是个如假包换的反面典型,居然上来就吃掉了16M!这还是因为我们没有把SIMP_BLKDEV_BYTES定义得更大。
现在我们开始列举simp_blkdev_data的种种罪行:
1:剩余的非线性映射区域较小时导致模块加载失败
2:模块加载后占用了大量的非线性映射区域,导致其它模块加载失败。
3:模块加载后占用了大量的非线性映射区域,影响系统的正常运行。
这是因为不光模块,系统本身的很多功能也依赖非线性映射区域空间。
对于这样的害群之马,我们难道还有留下他的理由吗?
本章的内容虽然麻烦一些,但想到能够一了百了地清除这个体大膘肥的simp_blkdev_data,倒也相当值得。
也希望今后能够看到在对贪官的处理上,能够也拿出这样的魄力和勇气。
现在在清除simp_blkdev_data的问题上,已经不存在什么悬念了,接下来我们需要关注的是将simp_blkdev_data碎尸万段后,拿出一个更恰当方法来代替它。
首先,我们决定不用静态声明的数组,而改用动态申请的内存。
其次,使用类似vmalloc()的函数可以动态申请大段内存,但其实这段内存占用的还是非线性映射区域,就好像用一个比较隐蔽的贪官来代替下马的贪官,我们不会愚蠢在这种地步。
剩下的,就是在线性映射区域申请很多个页的内存,然后自己去管理。这个方法一了百了地解决了使用大段非线性映射区域的问题,而唯一的问题是由于需要自己管理申请到的页面,使程序复杂了不少。
但为了整个系统的利益,这难道不是我们该做的吗?
申请一个内存页是很容易的,这里我们将采用所有容易的方法中最容易的那个:
__get_free_page函数,原型是:
unsigned long __get_free_page(gfp_t gfp_mask);
这个函数用来申请一个页面的内存。gfp_mask包含一些对申请内存时的指定,比如,要在DMA区域中啦、必须清零等。
我们这里倒是使用最常见的__get_free_page(GFP_KERNEL)就可以了。
通过__get_free_page申请到了一大堆内存页,新的问题来了,在读写块设备时,我们得到是块设备的偏移,如何快速地通过偏移找到对应的内存页呢?
最简单的方法是建立一个数组,用来存放偏移到内存的映射,数组中的每项对应一个一个页:
数组定义如下:
void *simp_blkdev_data[(SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) / PAGE_SIZE];
PAGE_SIZE是系统中每个页的大小,对i386来说,通常是4K,那堆加PAGE_SIZE减1的代码是考虑到SIMP_BLKDEV_BYTES不是PAGE_SIZE的整数倍时要让末尾的空间也能访问。
然后申请内存的代码大概是:
for (i=0; i < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) / PAGE_SIZE; i++) {
p = (void *)__get_free_page(GFP_KERNEL);
simp_blkdev_data[i] = p;
}
通过块设备偏移得到内存中的数据地址的代码大概是:
mem_addr = simp_blkdev_data[dev_addr/PAGE_SIZE] + dev_addr % PAGE_SIZE;
这种方法实现起来还是比较简单的,但缺点也不是没有:存放各个页面地址的数组虽然其体积比原先那个直接存放数据的数组已经缩小了很多,
但本身毕竟还是在非线性映射区域中。如果块设备大小为16M,在i386上,需要4096个页面,数组大小16K,这不算太大。
但如果某个疯子打算建立一个2G的虚拟磁盘,数组大小将达到2M,这就不算小了。
或者我们可以不用数组,而用链表来存储偏移到内存页的映射关系,这样可以回避掉数组存在的问题,但在链表中查找指定元素却不是一般的费时,
毕竟我们不希望用户觉得这是个软盘。
接下来作者不打断继续卖关子了,我们最终选择使用的是传说中的基树。
关于linux中基树细节的文档不多,特别是中文文档更少,更糟的是我们这篇文档也不打算作详细的介绍(因为作者建议去RTFSC)。
但总的来说,相对于二叉平衡树的红黑树来说,基树是一个n叉(一般为64叉)非平衡树,n叉减少了搜索的深度,非平衡减少了复杂的平衡操作。
当然,这两个特点也不是仅仅带来优点,但在这里我们就视而不见了,毕竟我们已经选择了基树,因此护短也是自认而然的事情,正如公仆护着王细牛一样。
从功能上来说,基树好像是为我们量身定做的一样,好用至极。
(其实我们也可以考虑选择红黑树和哈希表来实现这个功能,感兴趣的读者可以了解一下)
接下来的代码中,我们将要用到基树种的如下函数:
void INIT_RADIX_TREE((struct radix_tree_root *root, gfp_t mask);
用来初始化一个基树的结构,root是基树结构指针,mask是基树内部申请内存时使用的标志。
int radix_tree_insert(struct radix_tree_root *root, unsigned long index, void *item);
用来往基树中插入一个指针,index是指针的索引,item是指针,将来可以通过index从基树中快速获得这个指针的值。
void *radix_tree_delete(struct radix_tree_root *root, unsigned long index);
用来根据索引从基树中删除一个指针,index是指针的索引。
void *radix_tree_lookup(struct radix_tree_root *root, unsigned long index);
用来根据索引从基树中查找对应的指针,index是指针的索引。
其实基树的功能不仅限于此,比如,还可以给指针设定标志,详情还是请去读linux/lib/radix-tree.c
现在开始改造我们的代码:
首先删除那个无耻的数组:
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];
然后引入它的替代者--一个基树结构:
static struct radix_tree_root simp_blkdev_data;
然后增加两个函数,用来申请和释放块设备的内存:
申请内存的函数如下:
int alloc_diskmem(void)
{
int ret;
int i;
void *p;
INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL);
for (i = 0; i < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) >> PAGE_SHIFT;
i++) {
p = (void *)__get_free_page(GFP_KERNEL);
if (!p) {
ret = -ENOMEM;
goto err_alloc;
}
ret = radix_tree_insert(&simp_blkdev_data, i, p);
if (IS_ERR_VALUE(ret))
goto err_radix_tree_insert;
}
return 0;
err_radix_tree_insert:
free_page((unsigned long)p);
err_alloc:
free_diskmem();
return ret;
}
先初始化基树结构,然后申请需要的每一个页面,按照每页面的次序作为索引,将指针插入基树。
代码中的“>> PAGE_SHIFT”与“/ PAGE_SIZE”作用相同,
if (不明白为什么要这样)
do_google();
释放内存的函数如下:
void free_diskmem(void)
{
int i;
void *p;
for (i = 0; i < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) >> PAGE_SHIFT;
i++) {
p = radix_tree_lookup(&simp_blkdev_data, i);
radix_tree_delete(&simp_blkdev_data, i);
/* free NULL is safe */
free_page((unsigned long)p);
}
}
遍历每一个索引,得到页面的指针,释放页面,然后从基树中释放这个指针。
由于alloc_diskmem()函数在中途失败时需要释放申请过的页面,因此我们把free_diskmem()函数设计成能够释放建立了一半的基树的形式。
对于只建立了一半的基树而言,有一部分索引对应的指针还没来得及插入基树,对于不存在的索引,radix_tree_delete()函数会返回NULL,幸运的是free_page()函数能够忽略传入的NULL指针。
因为alloc_diskmem()函数需要调用free_diskmem()函数,在代码中需要把free_diskmem()函数写在alloc_diskmem()前面,或者在文件头添加函数的声明。
然后在模块的初始化和释放函数中添加对alloc_diskmem()和free_diskmem()的调用,
也就是改成这个样子:
static int __init simp_blkdev_init(void)
{
int ret;
simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL);
if (!simp_blkdev_queue) {
ret = -ENOMEM;
goto err_alloc_queue;
}
blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request);
simp_blkdev_disk = alloc_disk(SIMP_BLKDEV_MAXPARTITIONS);
if (!simp_blkdev_disk) {
ret = -ENOMEM;
goto err_alloc_disk;
}
ret = alloc_diskmem();
if (IS_ERR_VALUE(ret))
goto err_alloc_diskmem;
strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME);
simp_blkdev_disk->major = SIMP_BLKDEV_DEVICEMAJOR;
simp_blkdev_disk->first_minor = 0;
simp_blkdev_disk->fops = &simp_blkdev_fops;
simp_blkdev_disk->queue = simp_blkdev_queue;
set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9);
add_disk(simp_blkdev_disk);
return 0;
err_alloc_diskmem:
put_disk(simp_blkdev_disk);
err_alloc_disk:
blk_cleanup_queue(simp_blkdev_queue);
err_alloc_queue:
return ret;
}
static void __exit simp_blkdev_exit(void)
{
del_gendisk(simp_blkdev_disk);
free_diskmem();
put_disk(simp_blkdev_disk);
blk_cleanup_queue(simp_blkdev_queue);
}
最麻烦的放在最后:
我们需要修改simp_blkdev_make_request()函数,让它适应新的数据结构。
原先的实现中,对于一个bio_vec,我们找到对应的内存中数据的起点,直接传送bvec->bv_len个字节就大功告成了,比如,读块设备时就是:
memcpy(iovec_mem, dsk_mem, bvec->bv_len);
但现在由于容纳数据的每个页面地址是不连续的,因此可能出现bio_vec中的数据跨越页面边界的情况。
也就是说,一个bio_vec中的数据的前半段在一个页面中,后半段在另一个页面中。
虽然这两个页面对应的块设备地址连续,但在内存中的地址不一定连续,因此像原先那样简单使用memcpy看样子是解决不了问题了。
实际上,虽然bio_vec可能跨越页面边界,但它无论如何也不可能跨越2个以上的页面。
这是因为bio_vec本身对应的数据最大长度只有一个页面。
因此如果希望做最简单的实现,只要在代码中做一个条件判断就OK了:
if (没有跨越页面) {
1个memcpy搞定
} else {
/* 肯定是跨越2个页面了 */
2个memcpy搞定
}
但为了表现出物理设备一次传送1个扇区数据的处理方式(这种情况下一个bio_vec可能会跨越2个以上的扇区),我们让代码支持2个以上页面的情况。
首先列出修改后的simp_blkdev_make_request()函数:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
struct bio_vec *bvec;
int i;
unsigned long long dsk_offset;
if ((bio->bi_sector << 9) + bio->bi_size > SIMP_BLKDEV_BYTES) {
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": bad request: block=%llu, count=%u\n",
(unsigned long long)bio->bi_sector, bio->bi_size);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
dsk_offset = bio->bi_sector << 9;
bio_for_each_segment(bvec, bio, i) {
unsigned int count_done, count_current;
void *iovec_mem;
void *dsk_mem;
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
count_done = 0;
while (count_done < bvec->bv_len) {
count_current = min(bvec->bv_len - count_done,
(unsigned int)(PAGE_SIZE
- ((dsk_offset + count_done) & ~PAGE_MASK)));
dsk_mem = radix_tree_lookup(&simp_blkdev_data,
(dsk_offset + count_done) >> PAGE_SHIFT);
if (!dsk_mem) {
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": search memory failed: %llu\n",
(dsk_offset + count_done)
>> PAGE_SHIFT);
kunmap(bvec->bv_page);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
dsk_mem += (dsk_offset + count_done) & ~PAGE_MASK;
switch (bio_rw(bio)) {
case READ:
case READA:
memcpy(iovec_mem + count_done, dsk_mem,
count_current);
break;
case WRITE:
memcpy(dsk_mem, iovec_mem + count_done,
count_current);
break;
default:
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": unknown value of bio_rw: %lu\n",
bio_rw(bio));
kunmap(bvec->bv_page);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
count_done += count_current;
}
kunmap(bvec->bv_page);
dsk_offset += bvec->bv_len;
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, bio->bi_size, 0);
#else
bio_endio(bio, 0);
#endif
return 0;
}
看样子长了一些,但不要被吓着了,因为读的时候我们可以对代码做一些简化:
1:去掉乱七八糟的出错处理
2:无视每行80字符限制
3:把比特运算改成等价但更易读的乘除运算
4:无视碍眼的类型转换
5:假设内核版本大于2.6.24,以去掉判断版本的宏
就会变成这样了:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
struct bio_vec *bvec;
int i;
unsigned long long dsk_offset;
dsk_offset = bio->bi_sector * 512;
bio_for_each_segment(bvec, bio, i) {
unsigned int count_done, count_current;
void *iovec_mem;
void *dsk_mem;
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
count_done = 0;
while (count_done < bvec->bv_len) {
count_current = min(bvec->bv_len - count_done, PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE);
dsk_mem = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) / PAGE_SIZE);
dsk_mem += (dsk_offset + count_done) % PAGE_SIZE;
switch (bio_rw(bio)) {
case READ:
case READA:
memcpy(iovec_mem + count_done, dsk_mem, count_current);
break;
case WRITE:
memcpy(dsk_mem, iovec_mem + count_done, count_current);
break;
}
count_done += count_current;
}
kunmap(bvec->bv_page);
dsk_offset += bvec->bv_len;
}
bio_endio(bio, 0);
return 0;
}
是不是清楚多了?
dsk_offset用来存储当前要处理的数据在块设备上的偏移,初始值是bio->bi_sector * 512,也就是起始扇区对应的偏移,也是第一个bio_vec对应的块设备偏移。
每处理完成一个bio_vec时,dsk_offset值会被更新:dsk_offset += bvec->bv_len,以指向将要处理的数据在块设备上的偏移。
在bio_for_each_segment()中代码的起始和末尾,执行kmap和kunmap开映射当前这个bio_vec的内存,这个知识在前面的章节中已经提到了,
这个处理的结果是iovec_mem指向当前的bio_vec中的缓冲区。
现在在kmap和kunmap之间的代码的功能已经很明确了,就是完成块设备上偏移为dsk_offset、长度为bvec->bv_len的数据与iovec_mem地址之间的传送。
假设不考虑bio_vec跨越页面边界的情况,这段代码应该十分写意:
dsk_mem = radix_tree_lookup(&simp_blkdev_data, dsk_offset / PAGE_SIZE) + dsk_offset % PAGE_SIZE;
switch (bio_rw(bio)) {
case READ:
case READA:
memcpy(iovec_mem, dsk_mem, bvec->bv_len);
break;
case WRITE:
memcpy(dsk_mem, iovec_mem, bvec->bv_len);
break;
}
首先使用dsk_offset / PAGE_SIZE、也就是块设备偏移在内存中数据所位于的页面次序作为索引,查找该页的内存起始地址,
然后加上块设备偏移在该页内的偏移、也就是dsk_offset % PAGE_SIZE,
就得到了内存中数据的地址,然后就是简单的数据传送。
关于块设备偏移到内存地址的转换,我们举个例子:
假使模块加载时我们分配的第1个页面的地址为0xd0000000,用于存放块设备偏移为0~4095的数据
第2个页面的地址为0xd1000000,用于存放块设备偏移为4096~8191的数据
第3个页面的地址为0xc8000000,用于存放块设备偏移为8192~12287的数据
第4个页面的地址为0xe2000000,用于存放块设备偏移为12288~16383的数据
对于块设备偏移为9000的数据,首先通过9000 / PAGE_SIZE确定它位于第3个页面中,
然后使用radix_tree_lookup(&simp_blkdev_data, 3)将查找出0xc8000000这个地址。
这是第3个页面的起始地址,这个地址的数据在块设备中的偏移是8192,
因此我们还要加上块设备偏移在页内的偏移量,也就是9000 % PAGE_SIZE = 808,
得到的才是块设备偏移为9000的数据在内存中的数据地址。
当然,假设终归是假设,往往大多数情况下是自欺欺人的,就好像彩迷总喜欢跟女友说如果中了500万,就要怎么怎么对她好一样。
现在回到残酷的现实,我们还是要去考虑bio_vec跨越页面边界的情况。
这意味着对于一个bio_vec,我们将有可能传送多次。
为了记录前几次中已经完成的数据量,我们引入了一个新的变量,叫做count_done。
在进行bio_vec内的第一次传送前,count_done的值是0,每完成一次传送,count_done将加上这次完成的数据量。
当count_done == bvec->bv_len时,就是大功告成的之日。
接着就是边界的判断。
当前块设备偏移所在的内存页中,块设备偏移所在位置到页头的距离为:
offset % PAGE_SIZE
块设备偏移所在位置到页尾的距离为:
PAGE_SIZE - offset % PAGE_SIZE
这个距离也就是不超越页边界时所能传送的数据的最大值。
因此在bio_vec内的每一次中,我们使用
count_current = min(bvec->bv_len - count_done, PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE);
来确定这一次传送的数据量。
bvec->bv_len - count_done指的是余下需要传送的数据总量,
PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE指的是从当前块设备偏移开始、不超越页边界时所能传送的数据的最大值。
如果bvec->bv_len - count_done > PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE,说明这一次将传送从当前块设备偏移到其所在内存页的页尾之间的数据,
余下的数据位于后续的页面中,将在接下来的循环中搞定,
如果bvec->bv_len - count_done <= PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE,那么可喜可贺,这将是当前bio_vec的最后一次传送,完成后就可以回家洗澡了。
结合以上的说明,我想应该不难看懂simp_blkdev_make_request()的代码了,而我们的程序也已经大功告成。
现在总结一下修改的位置:
1:把unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];换成static struct radix_tree_root simp_blkdev_data;
2:把本文中的free_diskmem()和alloc_diskmem()函数添加到代码中,虽然没有特别意义,但建议插在紧邻simp_blkdev_init()之前的位置。
但有特别意义的是free_diskmem()和alloc_diskmem()的顺序,如果读者到这里还打算提问是什么顺序,作者可要哭了。
3:把simp_blkdev_make_request()、simp_blkdev_init()和simp_blkdev_exit()函数替换成文中的代码。
注意不要企图使用简化过的simp_blkdev_make_request()函数,否则造成的后果:从程序编译失败到读者被若干美女轮奸,作者都概不负责。
从常理分析,在修改完程序后,我们都将试验一次修改的效果。
这次也不例外,因为审判彭宇的王法官也是这么推断的。
首先证明我们的模块至今为止仍然经得起编译、能够加载:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step06 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686‘
CC [M] /root/test/simp_blkdev/simp_blkdev_step06/simp_blkdev.o
Building modules, stage 2.
MODPOST
CC /root/test/simp_blkdev/simp_blkdev_step06/simp_blkdev.mod.o
LD [M] /root/test/simp_blkdev/simp_blkdev_step06/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686‘
# insmod simp_blkdev.ko
#
看看模块的加载时分配的非线性映射区域大小:
# lsmod
Module Size Used by
simp_blkdev 8212 0
...
#
如果这个Size一栏的数字没有引起读者的足够重视的话,我们拿修改前的模块来对比一下:
# lsmod
Module Size Used by
simp_blkdev 16784392 0
看出区别了没?
如果本章到这里还不结束的话,估计读者要开始闪人了。
好的,我们马上就结束,希望在这之前闪掉的读者不要太多。
由于还没有来得及闪掉而看到这段话的读者们,作者相信你们具有相当的毅力。
学习是需要毅力的,这时的作者同样也需要毅力来坚持完成这本教程。
最后还是希望读者坚持,坚持看完所有的章节,坚持在遇到每一个不明白的问题时都努力寻求答案,
坚持在发现自己感兴趣的内容时能够深入地去了解、探寻、思考。
<未完,待续>
第5章
+---------------------------------------------------+
| 写一个块设备驱动 |
+---------------------------------------------------+
| 作者:赵磊 |
| email: [email protected] |
+---------------------------------------------------+
| 文章版权归原作者所有。 |
| 大家可以自由转载这篇文章,但原版权信息必须保留。 |
| 如需用于商业用途,请务必与原作者联系,若因未取得 |
| 授权而收起的版权争议,由侵权者自行负责。 |
+---------------------------------------------------+
既然上一章结束时我们已经预告了本章的内容,
那么本章中我们就让这个块设备有能力告知操作系统它的“物理结构”。
当然,对于基于内存的块设备来说,什么样的物理结构并不重要,
这就如同从酒吧带mm回家时不需要打听她的姓名一样。
但如果不幸遇到的是兼职,并且带她去不入流的招待所时,
建议最好还是先串供一下姓名、生日和职业等信息,
以便JJ查房时可以伪装成情侣。
同样,如果要实现的是真实的物理块设备驱动,
那么返回设备的物理结构时大概不能这么随意。
对于块设备驱动程序而言,我们现在需要关注那条目前只有一行的struct block_device_operations simp_blkdev_fops结构。
到目前为止,它存在的目的仅仅是因为它必须存在,但马上我们将发现它存在的另一个目的:为块设备驱动添加获得块设备物理结构的接口。
对于具有极强钻研精神的极品读者来说,大概在第一章中就会自己去看struct block_device_operations结构,然后将发现这个结构其实还挺复杂:
struct block_device_operations {
int (*open) (struct block_device *, fmode_t);
int (*release) (struct gendisk *, fmode_t);
int (*locked_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
int (*direct_access) (struct block_device *, sector_t,
void **, unsigned long *);
int (*media_changed) (struct gendisk *);
int (*revalidate_disk) (struct gendisk *);
int (*getgeo)(struct block_device *, struct hd_geometry *);
struct module *owner;
};
在前几章中,我们邂逅过其中的owner成员变量,它用于存储这个结构的所有者,也就是我们的模块,因此我们做了如下的赋值:
.owner = THIS_MODULE,
而这一章中,我们将与它的同胞妹妹------getgeo也亲密接触一下。
我们要做的是:
1:在block_device_operations中增加getgeo成员变量初值的设定,指向我们的“获得块设备物理结构”函数。
2:实现我们的“获得块设备物理结构”函数。
第一步很简单,我们暂且为“获得块设备物理结构”函数取个名字叫simp_blkdev_getgeo()吧,也避免了在下文中把这么一大堆汉字拷来拷去。
在simp_blkdev_fops中添加.getgeo指向simp_blkdev_getgeo,也就是把simp_blkdev_fops结构改成这个样子:
struct block_device_operations simp_blkdev_fops = {
.owner = THIS_MODULE,
.getgeo = simp_blkdev_getgeo,
};
第二步难一些,但也难不到哪去,在代码中的struct block_device_operations simp_blkdev_fops这行之前找个空点的场子,把如下函数插进去:
static int simp_blkdev_getgeo(struct block_device *bdev,
struct hd_geometry *geo)
{
/*
* capacity heads sectors cylinders
* 0~16M 1 1 0~32768
* 16M~512M 1 32 1024~32768
* 512M~16G 32 32 1024~32768
* 16G~... 255 63 2088~...
*/
if (SIMP_BLKDEV_BYTES < 16 * 1024 * 1024) {
geo->heads = 1;
geo->sectors = 1;
} else if (SIMP_BLKDEV_BYTES < 512 * 1024 * 1024) {
geo->heads = 1;
geo->sectors = 32;
} else if (SIMP_BLKDEV_BYTES < 16ULL * 1024 * 1024 * 1024) {
geo->heads = 32;
geo->sectors = 32;
} else {
geo->heads = 255;
geo->sectors = 63;
}
geo->cylinders = SIMP_BLKDEV_BYTES>>9/geo->heads/geo->sectors;
return 0;
}
因为这里我们用到了struct hd_geometry结构,所以还要增加一行#include
这个函数的目的,是选择适当的物理结构信息装入struct hd_geometry *geo结构。
当然,为了克服上一章中只能分成2个区的问题,我们应该尽可能增加磁道的数量。
希望读者不要理解成分几个区就需要几个磁道,这意味着一个磁道一个区,也意味着每个区必须一般大小。
由于分区总是以磁道为边界,尽可能增加磁道的数量不仅仅是为了让块设备容纳更多的分区,
更重要的是让分区的实际大小更接近于分区时的指定值,也就是提高实际做出的分区容量的精度。
不过对于设置的物理结构值,还存在一个限制,就是struct hd_geometry中的数值上限。
我们看struct hd_geometry的内容:
struct hd_geometry {
unsigned char heads;
unsigned char sectors;
unsigned short cylinders;
unsigned long start;
};
unsigned char的磁头数和每磁道扇区数决定了其255的上限,同样,unsigned short的磁道数决定了其65535的上限。
这还不算,但在前一章中,我们知道对于现代硬盘,磁头数和每磁道扇区数通常取的值是255和63,
再组合上这里的65535的磁道数上限,hd_geometry能够表示的最大块设备容量是255*63*65535*512/1024/1024/1024=502G。
显然目前linux支持的最大硬盘容量大于502G,那么对于这类块设备,内核是如何通过hd_geometry结构表示其物理结构的呢?
诀窍不在内核,而在于用户态程序如fdisk等通过内核调用获得hd_geometry结构后,
会舍弃hd_geometry.cylinders内容,取而代之的是直接通过hd_geometry中的磁头数和每磁道扇区数以及硬盘大小去计算磁道数。
因此对于超过502G的硬盘,由于用户程序得出的磁道数与hd_geometry.cylinders无关,所以我们往往在fdisk中能看到这块硬盘的磁道数大于65535。
刚才扯远了,现在言归正题,我们决定让这个函数对于任何尺寸的块设备,总是试图返回比较漂亮的物理结构。
漂亮意味着返回的物理结构既要保证拥有足够多的磁道,也要保证磁头数和每磁道扇区数不超过255和63,同时最好使用程序员看起来比较顺眼的数字,
如:1、2、4、8、16、32、64等。
当然,我们也希望找到某个One Shot公式适用于所有大小的块设备,但很遗憾目前作者没找到,因此采用了分段计算的方法:
首先考虑容量很小的块设备:
即使磁头数和每磁道扇区数都是1,磁道数也不够多时,我们会将磁头数和每磁道扇区数都固定为1,以使磁道数尽可能多,以提高分区的精度。
因此磁道数随块设备容量而上升。
虽然我们已经知道了磁道数其实可以超过unsigned short的65535上限,但在这里却没有必要,因此我们要给磁道数设置一个上限。
因为不想让上限超过65535,同时还希望上限也是一个程序员喜欢的数字,因此这里选择了32768。
当然,当磁道数超过32768时,已经意味着块设备容量不那么小了,也就没有必要使用这种情况中如此苛刻的磁头数和每磁道扇区数了。
简单来说,当块设备容量小于1个磁头、每磁道1扇区和32768个磁道对应的容量--也就是16M时,我们将按照这种情况处理。
然后假设块设备容量已经大于16M了:
我们希望保证块设备包含足够多的磁道,这里我们认为1024个磁道应该不少了。
磁道的最小值发生在块设备容量为16M的时候,这时使用1024作为磁道数,可以计算出磁头数*每磁道扇区数=32。
这里暂且把磁头数和每磁道扇区数固定为1和32,而让磁道数随着块设备容量的增大而增加。
同时,我们还是磁道的上限设置成32768,这时的块设备容量为512M。
总结来说,当块设备容量在16M和512M之间时,我们把磁头数和每磁道扇区数固定为1和32。
然后对于容量大于512M的块设备:
与上述处理相似,当块设备容量在512M和16G之间时,我们把磁头数和每磁道扇区数固定为32和32。
最后的一种情况:
块设备已经足够大了,大到即使我们使用磁头数和每磁道扇区数的上限,
也能获得足够多的磁道数。这时把磁头数和每磁道扇区数固定为255和63。
至于磁道数就算出多少是多少了,即使超过unsigned short的上限也无所谓,反正用不着。
随着这个函数解说到此结束,我们对代码的修改也结束了。
现在开始试验:
编译和加载:
# make
make -C /lib/modules/2.6.27.4/build SUBDIRS=/mnt/host_test/simp_blkdev/simp_blkdev_step05 modules
make[1]: Entering directory `/mnt/ltt-kernel‘
CC [M] /mnt/host_test/simp_blkdev/simp_blkdev_step05/simp_blkdev.o
Building modules, stage 2.
MODPOST 1 modules
CC /mnt/host_test/simp_blkdev/simp_blkdev_step05/simp_blkdev.mod.o
LD [M] /mnt/host_test/simp_blkdev/simp_blkdev_step05/simp_blkdev.ko
make[1]: Leaving directory `/mnt/ltt-kernel‘
# insmod simp_blkdev.ko
#
用fdisk打开设备文件
# fdisk /dev/simp_blkdev
Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
Building a new DOS disklabel. Changes will remain in memory only,
until you decide to write them. After that, of course, the previous
content won‘t be recoverable.
Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)
Command (m for help):
看看设备的物理结构:
Command (m for help): p
Disk /dev/simp_blkdev: 16 MB, 16777216 bytes
1 heads, 32 sectors/track, 1024 cylinders
Units = cylinders of 32 * 512 = 16384 bytes
Device Boot Start End Blocks Id System
Command (m for help):
我们发现,现在的设备有1个磁头、32扇区每磁道、1024个磁道。
这是符合代码中的处理的。
本章的内容也不是太难,连同上一章,我们已经休息2章了。
聪明的读者可能已经猜到作者打算说什么了。
不错,下一章会有一个surprise。
<未完,待续>
第6章
+---------------------------------------------------+
| 写一个块设备驱动 |
+---------------------------------------------------+
| 作者:赵磊 |
| email: [email][email protected][/email] |
+---------------------------------------------------+
| 文章版权归原作者所有。 |
| 大家可以自由转载这篇文章,但原版权信息必须保留。 |
| 如需用于商业用途,请务必与原作者联系,若因未取得 |
| 授权而收起的版权争议,由侵权者自行负责。 |
+---------------------------------------------------+
经历了内容极为简单的前两章的休息,现在大家一定感到精神百倍了。
作为已经坚持到现在的读者,对接下去将要面临的内容大概应该能够猜得八九不离十了,
具体的内容猜不出来也无妨,但一定将是具有增加颅压功效的。
与物理块设备驱动程序的区别在于,我们的驱动程序使用内存来存储块设备中的数据。
到目前为止,我们一直都是使用这样一个静态数组来担负这一功能的:
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];
如果读者懂得一些模块的知识,或者现在赶紧去临时抱佛脚google一些模块知识,
应该知道模块其实是加载在非线性映射区域的。
详细来说,在加载模块时,根据模块的ELF信息(天哪,又要去google elf了),确定这个模块所需的静态内存大小。
这些内存用来容纳模块的二进制代码,以及静态变量。然后申请容纳这些数据的一大堆页面。
当然,这些页面并不是连续的,而代码和变量却不可能神奇到即使被切成一块一块的也能正常工作。
因此需要在非线性映射区域中找到一块连续的地址(现在有要去google非线性映射区域了),用来将刚才申请到的一块一块的内存页映射到这个地址段中。
最后模块被请到这段区域中,然后执行模块的初始化函数......
现在看我们这个模块中的simp_blkdev_data变量,如果不是现在刻意关注,这个变量看起来显得那么得普通。
正如其它的一些名字原先也是那么的普通,但由于一些突发的事件受到大家的热烈关注,
比如一段视频让我们熟悉了kappa和陆佳妮,比如呼吸税让我们认识了蒋有绪。
现在我们开始关注simp_blkdev_data变量了,导火索是刚才介绍的非线性映射区域。
模块之所以被加载到非线性映射区域,是因为很难在线性映射区域中得到加载模块所需的连续的内存。
但使用非线性映射区域也并非只赚不赔的生意,至少在i386结构中,非线性映射区域实在是太小了。
在物理内存大于896M的i386系统中,整个非线性映射区域不会超过128M。
相反如果物理内存小于896M(不知道该算是幸运还是不幸),非线性映射区域反而会稍微大一些,这种情况我想我们可以不用讨论了,毕竟不能为了加载一个模块去拔内存。
因此我们的结论是:非线性映射区域是很紧张的资源,我们要节约使用。
而像我们现在这个模块中的simp_blkdev_data却是个如假包换的反面典型,居然上来就吃掉了16M!这还是因为我们没有把SIMP_BLKDEV_BYTES定义得更大。
现在我们开始列举simp_blkdev_data的种种罪行:
1:剩余的非线性映射区域较小时导致模块加载失败
2:模块加载后占用了大量的非线性映射区域,导致其它模块加载失败。
3:模块加载后占用了大量的非线性映射区域,影响系统的正常运行。
这是因为不光模块,系统本身的很多功能也依赖非线性映射区域空间。
对于这样的害群之马,我们难道还有留下他的理由吗?
本章的内容虽然麻烦一些,但想到能够一了百了地清除这个体大膘肥的simp_blkdev_data,倒也相当值得。
也希望今后能够看到在对贪官的处理上,能够也拿出这样的魄力和勇气。
现在在清除simp_blkdev_data的问题上,已经不存在什么悬念了,接下来我们需要关注的是将simp_blkdev_data碎尸万段后,拿出一个更恰当方法来代替它。
首先,我们决定不用静态声明的数组,而改用动态申请的内存。
其次,使用类似vmalloc()的函数可以动态申请大段内存,但其实这段内存占用的还是非线性映射区域,就好像用一个比较隐蔽的贪官来代替下马的贪官,我们不会愚蠢在这种地步。
剩下的,就是在线性映射区域申请很多个页的内存,然后自己去管理。这个方法一了百了地解决了使用大段非线性映射区域的问题,而唯一的问题是由于需要自己管理申请到的页面,使程序复杂了不少。
但为了整个系统的利益,这难道不是我们该做的吗?
申请一个内存页是很容易的,这里我们将采用所有容易的方法中最容易的那个:
__get_free_page函数,原型是:
unsigned long __get_free_page(gfp_t gfp_mask);
这个函数用来申请一个页面的内存。gfp_mask包含一些对申请内存时的指定,比如,要在DMA区域中啦、必须清零等。
我们这里倒是使用最常见的__get_free_page(GFP_KERNEL)就可以了。
通过__get_free_page申请到了一大堆内存页,新的问题来了,在读写块设备时,我们得到是块设备的偏移,如何快速地通过偏移找到对应的内存页呢?
最简单的方法是建立一个数组,用来存放偏移到内存的映射,数组中的每项对应一个一个页:
数组定义如下:
void *simp_blkdev_data[(SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) / PAGE_SIZE];
PAGE_SIZE是系统中每个页的大小,对i386来说,通常是4K,那堆加PAGE_SIZE减1的代码是考虑到SIMP_BLKDEV_BYTES不是PAGE_SIZE的整数倍时要让末尾的空间也能访问。
然后申请内存的代码大概是:
for (i=0; i < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) / PAGE_SIZE; i++) {
p = (void *)__get_free_page(GFP_KERNEL);
simp_blkdev_data[i] = p;
}
通过块设备偏移得到内存中的数据地址的代码大概是:
mem_addr = simp_blkdev_data[dev_addr/PAGE_SIZE] + dev_addr % PAGE_SIZE;
这种方法实现起来还是比较简单的,但缺点也不是没有:存放各个页面地址的数组虽然其体积比原先那个直接存放数据的数组已经缩小了很多,
但本身毕竟还是在非线性映射区域中。如果块设备大小为16M,在i386上,需要4096个页面,数组大小16K,这不算太大。
但如果某个疯子打算建立一个2G的虚拟磁盘,数组大小将达到2M,这就不算小了。
或者我们可以不用数组,而用链表来存储偏移到内存页的映射关系,这样可以回避掉数组存在的问题,但在链表中查找指定元素却不是一般的费时,
毕竟我们不希望用户觉得这是个软盘。
接下来作者不打断继续卖关子了,我们最终选择使用的是传说中的基树。
关于linux中基树细节的文档不多,特别是中文文档更少,更糟的是我们这篇文档也不打算作详细的介绍(因为作者建议去RTFSC)。
但总的来说,相对于二叉平衡树的红黑树来说,基树是一个n叉(一般为64叉)非平衡树,n叉减少了搜索的深度,非平衡减少了复杂的平衡操作。
当然,这两个特点也不是仅仅带来优点,但在这里我们就视而不见了,毕竟我们已经选择了基树,因此护短也是自认而然的事情,正如公仆护着王细牛一样。
从功能上来说,基树好像是为我们量身定做的一样,好用至极。
(其实我们也可以考虑选择红黑树和哈希表来实现这个功能,感兴趣的读者可以了解一下)
接下来的代码中,我们将要用到基树种的如下函数:
void INIT_RADIX_TREE((struct radix_tree_root *root, gfp_t mask);
用来初始化一个基树的结构,root是基树结构指针,mask是基树内部申请内存时使用的标志。
int radix_tree_insert(struct radix_tree_root *root, unsigned long index, void *item);
用来往基树中插入一个指针,index是指针的索引,item是指针,将来可以通过index从基树中快速获得这个指针的值。
void *radix_tree_delete(struct radix_tree_root *root, unsigned long index);
用来根据索引从基树中删除一个指针,index是指针的索引。
void *radix_tree_lookup(struct radix_tree_root *root, unsigned long index);
用来根据索引从基树中查找对应的指针,index是指针的索引。
其实基树的功能不仅限于此,比如,还可以给指针设定标志,详情还是请去读linux/lib/radix-tree.c
现在开始改造我们的代码:
首先删除那个无耻的数组:
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];
然后引入它的替代者--一个基树结构:
static struct radix_tree_root simp_blkdev_data;
然后增加两个函数,用来申请和释放块设备的内存:
申请内存的函数如下:
int alloc_diskmem(void)
{
int ret;
int i;
void *p;
INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL);
for (i = 0; i < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) >> PAGE_SHIFT;
i++) {
p = (void *)__get_free_page(GFP_KERNEL);
if (!p) {
ret = -ENOMEM;
goto err_alloc;
}
ret = radix_tree_insert(&simp_blkdev_data, i, p);
if (IS_ERR_VALUE(ret))
goto err_radix_tree_insert;
}
return 0;
err_radix_tree_insert:
free_page((unsigned long)p);
err_alloc:
free_diskmem();
return ret;
}
先初始化基树结构,然后申请需要的每一个页面,按照每页面的次序作为索引,将指针插入基树。
代码中的“>> PAGE_SHIFT”与“/ PAGE_SIZE”作用相同,
if (不明白为什么要这样)
do_google();
释放内存的函数如下:
void free_diskmem(void)
{
int i;
void *p;
for (i = 0; i < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) >> PAGE_SHIFT;
i++) {
p = radix_tree_lookup(&simp_blkdev_data, i);
radix_tree_delete(&simp_blkdev_data, i);
/* free NULL is safe */
free_page((unsigned long)p);
}
}
遍历每一个索引,得到页面的指针,释放页面,然后从基树中释放这个指针。
由于alloc_diskmem()函数在中途失败时需要释放申请过的页面,因此我们把free_diskmem()函数设计成能够释放建立了一半的基树的形式。
对于只建立了一半的基树而言,有一部分索引对应的指针还没来得及插入基树,对于不存在的索引,radix_tree_delete()函数会返回NULL,幸运的是free_page()函数能够忽略传入的NULL指针。
因为alloc_diskmem()函数需要调用free_diskmem()函数,在代码中需要把free_diskmem()函数写在alloc_diskmem()前面,或者在文件头添加函数的声明。
然后在模块的初始化和释放函数中添加对alloc_diskmem()和free_diskmem()的调用,
也就是改成这个样子:
static int __init simp_blkdev_init(void)
{
int ret;
simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL);
if (!simp_blkdev_queue) {
ret = -ENOMEM;
goto err_alloc_queue;
}
blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request);
simp_blkdev_disk = alloc_disk(SIMP_BLKDEV_MAXPARTITIONS);
if (!simp_blkdev_disk) {
ret = -ENOMEM;
goto err_alloc_disk;
}
ret = alloc_diskmem();
if (IS_ERR_VALUE(ret))
goto err_alloc_diskmem;
strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME);
simp_blkdev_disk->major = SIMP_BLKDEV_DEVICEMAJOR;
simp_blkdev_disk->first_minor = 0;
simp_blkdev_disk->fops = &simp_blkdev_fops;
simp_blkdev_disk->queue = simp_blkdev_queue;
set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9);
add_disk(simp_blkdev_disk);
return 0;
err_alloc_diskmem:
put_disk(simp_blkdev_disk);
err_alloc_disk:
blk_cleanup_queue(simp_blkdev_queue);
err_alloc_queue:
return ret;
}
static void __exit simp_blkdev_exit(void)
{
del_gendisk(simp_blkdev_disk);
free_diskmem();
put_disk(simp_blkdev_disk);
blk_cleanup_queue(simp_blkdev_queue);
}
最麻烦的放在最后:
我们需要修改simp_blkdev_make_request()函数,让它适应新的数据结构。
原先的实现中,对于一个bio_vec,我们找到对应的内存中数据的起点,直接传送bvec->bv_len个字节就大功告成了,比如,读块设备时就是:
memcpy(iovec_mem, dsk_mem, bvec->bv_len);
但现在由于容纳数据的每个页面地址是不连续的,因此可能出现bio_vec中的数据跨越页面边界的情况。
也就是说,一个bio_vec中的数据的前半段在一个页面中,后半段在另一个页面中。
虽然这两个页面对应的块设备地址连续,但在内存中的地址不一定连续,因此像原先那样简单使用memcpy看样子是解决不了问题了。
实际上,虽然bio_vec可能跨越页面边界,但它无论如何也不可能跨越2个以上的页面。
这是因为bio_vec本身对应的数据最大长度只有一个页面。
因此如果希望做最简单的实现,只要在代码中做一个条件判断就OK了:
if (没有跨越页面) {
1个memcpy搞定
} else {
/* 肯定是跨越2个页面了 */
2个memcpy搞定
}
但为了表现出物理设备一次传送1个扇区数据的处理方式(这种情况下一个bio_vec可能会跨越2个以上的扇区),我们让代码支持2个以上页面的情况。
首先列出修改后的simp_blkdev_make_request()函数:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
struct bio_vec *bvec;
int i;
unsigned long long dsk_offset;
if ((bio->bi_sector << 9) + bio->bi_size > SIMP_BLKDEV_BYTES) {
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": bad request: block=%llu, count=%u\n",
(unsigned long long)bio->bi_sector, bio->bi_size);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
dsk_offset = bio->bi_sector << 9;
bio_for_each_segment(bvec, bio, i) {
unsigned int count_done, count_current;
void *iovec_mem;
void *dsk_mem;
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
count_done = 0;
while (count_done < bvec->bv_len) {
count_current = min(bvec->bv_len - count_done,
(unsigned int)(PAGE_SIZE
- ((dsk_offset + count_done) & ~PAGE_MASK)));
dsk_mem = radix_tree_lookup(&simp_blkdev_data,
(dsk_offset + count_done) >> PAGE_SHIFT);
if (!dsk_mem) {
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": search memory failed: %llu\n",
(dsk_offset + count_done)
>> PAGE_SHIFT);
kunmap(bvec->bv_page);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
dsk_mem += (dsk_offset + count_done) & ~PAGE_MASK;
switch (bio_rw(bio)) {
case READ:
case READA:
memcpy(iovec_mem + count_done, dsk_mem,
count_current);
break;
case WRITE:
memcpy(dsk_mem, iovec_mem + count_done,
count_current);
break;
default:
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": unknown value of bio_rw: %lu\n",
bio_rw(bio));
kunmap(bvec->bv_page);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
count_done += count_current;
}
kunmap(bvec->bv_page);
dsk_offset += bvec->bv_len;
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, bio->bi_size, 0);
#else
bio_endio(bio, 0);
#endif
return 0;
}
看样子长了一些,但不要被吓着了,因为读的时候我们可以对代码做一些简化:
1:去掉乱七八糟的出错处理
2:无视每行80字符限制
3:把比特运算改成等价但更易读的乘除运算
4:无视碍眼的类型转换
5:假设内核版本大于2.6.24,以去掉判断版本的宏
就会变成这样了:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
struct bio_vec *bvec;
int i;
unsigned long long dsk_offset;
dsk_offset = bio->bi_sector * 512;
bio_for_each_segment(bvec, bio, i) {
unsigned int count_done, count_current;
void *iovec_mem;
void *dsk_mem;
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
count_done = 0;
while (count_done < bvec->bv_len) {
count_current = min(bvec->bv_len - count_done, PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE);
dsk_mem = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) / PAGE_SIZE);
dsk_mem += (dsk_offset + count_done) % PAGE_SIZE;
switch (bio_rw(bio)) {
case READ:
case READA:
memcpy(iovec_mem + count_done, dsk_mem, count_current);
break;
case WRITE:
memcpy(dsk_mem, iovec_mem + count_done, count_current);
break;
}
count_done += count_current;
}
kunmap(bvec->bv_page);
dsk_offset += bvec->bv_len;
}
bio_endio(bio, 0);
return 0;
}
是不是清楚多了?
dsk_offset用来存储当前要处理的数据在块设备上的偏移,初始值是bio->bi_sector * 512,也就是起始扇区对应的偏移,也是第一个bio_vec对应的块设备偏移。
每处理完成一个bio_vec时,dsk_offset值会被更新:dsk_offset += bvec->bv_len,以指向将要处理的数据在块设备上的偏移。
在bio_for_each_segment()中代码的起始和末尾,执行kmap和kunmap开映射当前这个bio_vec的内存,这个知识在前面的章节中已经提到了,
这个处理的结果是iovec_mem指向当前的bio_vec中的缓冲区。
现在在kmap和kunmap之间的代码的功能已经很明确了,就是完成块设备上偏移为dsk_offset、长度为bvec->bv_len的数据与iovec_mem地址之间的传送。
假设不考虑bio_vec跨越页面边界的情况,这段代码应该十分写意:
dsk_mem = radix_tree_lookup(&simp_blkdev_data, dsk_offset / PAGE_SIZE) + dsk_offset % PAGE_SIZE;
switch (bio_rw(bio)) {
case READ:
case READA:
memcpy(iovec_mem, dsk_mem, bvec->bv_len);
break;
case WRITE:
memcpy(dsk_mem, iovec_mem, bvec->bv_len);
break;
}
首先使用dsk_offset / PAGE_SIZE、也就是块设备偏移在内存中数据所位于的页面次序作为索引,查找该页的内存起始地址,
然后加上块设备偏移在该页内的偏移、也就是dsk_offset % PAGE_SIZE,
就得到了内存中数据的地址,然后就是简单的数据传送。
关于块设备偏移到内存地址的转换,我们举个例子:
假使模块加载时我们分配的第1个页面的地址为0xd0000000,用于存放块设备偏移为0~4095的数据
第2个页面的地址为0xd1000000,用于存放块设备偏移为4096~8191的数据
第3个页面的地址为0xc8000000,用于存放块设备偏移为8192~12287的数据
第4个页面的地址为0xe2000000,用于存放块设备偏移为12288~16383的数据
对于块设备偏移为9000的数据,首先通过9000 / PAGE_SIZE确定它位于第3个页面中,
然后使用radix_tree_lookup(&simp_blkdev_data, 3)将查找出0xc8000000这个地址。
这是第3个页面的起始地址,这个地址的数据在块设备中的偏移是8192,
因此我们还要加上块设备偏移在页内的偏移量,也就是9000 % PAGE_SIZE = 808,
得到的才是块设备偏移为9000的数据在内存中的数据地址。
当然,假设终归是假设,往往大多数情况下是自欺欺人的,就好像彩迷总喜欢跟女友说如果中了500万,就要怎么怎么对她好一样。
现在回到残酷的现实,我们还是要去考虑bio_vec跨越页面边界的情况。
这意味着对于一个bio_vec,我们将有可能传送多次。
为了记录前几次中已经完成的数据量,我们引入了一个新的变量,叫做count_done。
在进行bio_vec内的第一次传送前,count_done的值是0,每完成一次传送,count_done将加上这次完成的数据量。
当count_done == bvec->bv_len时,就是大功告成的之日。
接着就是边界的判断。
当前块设备偏移所在的内存页中,块设备偏移所在位置到页头的距离为:
offset % PAGE_SIZE
块设备偏移所在位置到页尾的距离为:
PAGE_SIZE - offset % PAGE_SIZE
这个距离也就是不超越页边界时所能传送的数据的最大值。
因此在bio_vec内的每一次中,我们使用
count_current = min(bvec->bv_len - count_done, PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE);
来确定这一次传送的数据量。
bvec->bv_len - count_done指的是余下需要传送的数据总量,
PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE指的是从当前块设备偏移开始、不超越页边界时所能传送的数据的最大值。
如果bvec->bv_len - count_done > PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE,说明这一次将传送从当前块设备偏移到其所在内存页的页尾之间的数据,
余下的数据位于后续的页面中,将在接下来的循环中搞定,
如果bvec->bv_len - count_done <= PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE,那么可喜可贺,这将是当前bio_vec的最后一次传送,完成后就可以回家洗澡了。
结合以上的说明,我想应该不难看懂simp_blkdev_make_request()的代码了,而我们的程序也已经大功告成。
现在总结一下修改的位置:
1:把unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];换成static struct radix_tree_root simp_blkdev_data;
2:把本文中的free_diskmem()和alloc_diskmem()函数添加到代码中,虽然没有特别意义,但建议插在紧邻simp_blkdev_init()之前的位置。
但有特别意义的是free_diskmem()和alloc_diskmem()的顺序,如果读者到这里还打算提问是什么顺序,作者可要哭了。
3:把simp_blkdev_make_request()、simp_blkdev_init()和simp_blkdev_exit()函数替换成文中的代码。
注意不要企图使用简化过的simp_blkdev_make_request()函数,否则造成的后果:从程序编译失败到读者被若干美女轮奸,作者都概不负责。
从常理分析,在修改完程序后,我们都将试验一次修改的效果。
这次也不例外,因为审判彭宇的王法官也是这么推断的。
首先证明我们的模块至今为止仍然经得起编译、能够加载:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step06 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686‘
CC [M] /root/test/simp_blkdev/simp_blkdev_step06/simp_blkdev.o
Building modules, stage 2.
MODPOST
CC /root/test/simp_blkdev/simp_blkdev_step06/simp_blkdev.mod.o
LD [M] /root/test/simp_blkdev/simp_blkdev_step06/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686‘
# insmod simp_blkdev.ko
#
看看模块的加载时分配的非线性映射区域大小:
# lsmod
Module Size Used by
simp_blkdev 8212 0
...
#
如果这个Size一栏的数字没有引起读者的足够重视的话,我们拿修改前的模块来对比一下:
# lsmod
Module Size Used by
simp_blkdev 16784392 0
看出区别了没?
如果本章到这里还不结束的话,估计读者要开始闪人了。
好的,我们马上就结束,希望在这之前闪掉的读者不要太多。
由于还没有来得及闪掉而看到这段话的读者们,作者相信你们具有相当的毅力。
学习是需要毅力的,这时的作者同样也需要毅力来坚持完成这本教程。
最后还是希望读者坚持,坚持看完所有的章节,坚持在遇到每一个不明白的问题时都努力寻求答案,
坚持在发现自己感兴趣的内容时能够深入地去了解、探寻、思考。
<未完,待续>
给主人留下些什么吧!~~
评论热议
以上是关于写一个块设备驱动5,6的主要内容,如果未能解决你的问题,请参考以下文章