第11章
+---------------------------------------------------+
| 写一个块设备驱动 |
+---------------------------------------------------+
| 作者:赵磊 |
| email: zhaoleidd@hotmail.com |
+---------------------------------------------------+
| 文章版权归原作者所有。 |
| 大家可以自由转载这篇文章,但原版权信息必须保留。 |
| 如需用于商业用途,请务必与原作者联系,若因未取得 |
| 授权而收起的版权争议,由侵权者自行负责。 |
+---------------------------------------------------+
本章中我们仍然为块设备驱动程序使用高端内存做准备工作。
这里要进行的准备工作并不意味着要增加或改变什么功能,
而是要收拾一部分代码,因为它们看起来已经有点复杂了。
有编程经验的读者大概能够意识到,编程时最常做的往往不是输入程序,而是拷贝-粘贴。
这是由于我们在编程时可能会不断地发现设计上的问题,或意识到还可以采用更好的结构,然后当然是实现它。
当然,更理想的情况大概是在一开始规划时就确定一个最佳的结构,以避免将来的更改,
但事实往往会与理想背道而驰,但关键是我们发现这种苗头时要及时纠正,而不是像某些部门一样去得过且过大事化小来掩盖问题。
要知道,酒是越陈越香,而垃圾却是越捂越臭,如果我们无法在最初做出完美的设计,至少我们还拥有纠正的勇气。
这里读者可能已经感觉到了,这里我们将要修改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()不需要关心数据长度是否超出块设备数据块边界的问题,正如领导也不会去管那棵树的死活一样。
本章的代码也同样不做实验,因为我们确实也没什么好做的。
至于能不能通过编译,作者已经试过了,有兴趣的读者大概可以验证一下前一句话是不是真的。
作为支持高端内存的前奏,前一章和本章中做了一些可能让人觉得莫名其妙的改动。
不过到此为止,准备工作已经做得差不多了,我们的程序已经为支持高端内存打下坚实的基础。
下一章将进入正题,我们将实现这一期盼已久的功能。
<未完,待续>
第12章
+---------------------------------------------------+
| 写一个块设备驱动 |
+---------------------------------------------------+
| 作者:赵磊 |
| email: zhaoleidd@hotmail.com |
+---------------------------------------------------+
| 文章版权归原作者所有。 |
| 大家可以自由转载这篇文章,但原版权信息必须保留。 |
| 如需用于商业用途,请务必与原作者联系,若因未取得 |
| 授权而收起的版权争议,由侵权者自行负责。 |
+---------------------------------------------------+
本章中我们将实现对高端内存的支持。
女孩子相处时,和她聊天,逛街,爬山,看电影,下棋中的每一件事情好像都与结婚扯不上太大的关系,
但经过天天年年的日积月累后,女孩子在潜意识中可能已经把你看成了她生活的一部分,
最终的结果显得是那么的自然,甚至连求婚都有些多余了。
学习也很相似,我们认真学习的的每一样知识,努力寻求的每一个答案就其本身而言,
都不能让自己成为专家,但专家却无一不是经历了长时间的认真学习,
努力钻研和细致思考的结果。
正如我们的程序,经历了前几章中的准备工作,离目标功能的距离大概也不算太远了。
而现在我们要做得就是实现它。
首先改动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,
而是靠每一个屁民们的诚实、认真、勤劳、勇敢、创造、奉献与精益求精。
<未完,待续>
阅读(806) | 评论(0) | 转发(0) |