第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,
而是靠每一个屁民们的诚实、认真、勤劳、勇敢、创造、奉献与精益求精。

<未完,待续>