免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
1234567
最近访问板块 发新帖
楼主: OstrichFly

[原创] 写一个块设备驱动 [复制链接]

论坛徽章:
0
发表于 2009-02-17 20:37 |显示全部楼层

第14章

+---------------------------------------------------+
|                 写一个块设备驱动                  |
+---------------------------------------------------+
| 作者:赵磊                                        |
| email: zhaoleidd@hotmail.com                      |
+---------------------------------------------------+
| 文章版权归原作者所有。                            |
| 大家可以自由转载这篇文章,但原版权信息必须保留。  |
| 如需用于商业用途,请务必与原作者联系,若因未取得  |
| 授权而收起的版权争议,由侵权者自行负责。          |
+---------------------------------------------------+

在本章中我们要做一个比较大的改进,就是实现内存的推迟分配。

这意味着我们并不是在驱动程序加载时就分配用于容纳数据的全部内存,
而是推迟到真正需要用到某块内存时再进行分配。

详细来说,我们将在块设备的某个区域上发生第一次写请求时分配用于容纳被写入数据的内存,
如果读者在之前章节的熏陶下养成了细致的作风和勤于思考的习惯,
应该能发现这里提到的分配内存的时机是第一次写,而不是第一次读写。
现在可能有些读者已经悟出了这样做的道理,让我们无视他们,依然解释一下这样做的目的。
对块设备而言,只要保证读出的数据是最近一次写进的即可。
如果在读数据之前从来没有往块设备的同一块区域中写入数据,那么这时返回任何随机数据都是正确的。
这意味着对于第一次读,我们完全可以返回任意的数据给用户,这时并不需要分配某段内存来存储它。
对真实的物理设备而言,就像我们买回的新硬盘,出厂时盘片中的数据内容是什么都无所谓。
在具体的实现中,我们可以不对用以接收被读出数据的内存进行任何填充,直接告诉上层“已经读好了”,
这样做无疑会更加快速,但这会造成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。
因为这个操作将迫使驱动程序吃掉全部可能获得的物理内存,并且在吃光最后那么一丁点内存之前不会发生错误,
这也意味着走到出错处理这一步的时候,系统已经几乎无可救药了。其实在此之前系统就会一次进行:
释放缓存、试图把所有的用户进程的内存换出、杀死全部能够杀死的进程等操作。
而我们的驱动程序由于被看作是内核的一部分,却不会被停止,而是在继续不停的吃掉通过上述方式释放出的可怜的内存。
试想,一个已经走到这一步的系统还有什么继续运行的可能呢?

因此,我们的程序确实需要改善以解决这个问题,因为世界上总是有一些疯狂的人在想各种办法虐待电脑。
但我们并不打算在本教程中解决它,因为这个教程中的每一章都企图为读者说明一类知识或一种方法,
而不是仅仅为了这个示例性质的程序的功能本身。
所以这一项改善就当作是留给读者的练习了。

本章通过改善块设备驱动程序实现了内存的滞后申请,
其目的在于介绍这种方法,以使它在其他的相似程序中也得以实现。
不过,这并不意味着作者希望读者把这种方法过分引用,
比如引用成平时不学习,考试前临时抱佛脚。

<未完,待续>

评分

参与人数 1可用积分 +15 收起 理由
scutan + 15 精品文章

查看全部评分

论坛徽章:
0
发表于 2009-03-08 20:12 |显示全部楼层
原帖由 scutan 于 2009-2-17 22:28 发表
谢谢分享!

谢谢支持!

论坛徽章:
0
发表于 2009-03-08 20:20 |显示全部楼层
原帖由 weily0000 于 2009-2-18 17:43 发表
最近在写一个设备驱动,希望对每一个bio结构中的数据段的数据进行读出,我参考了前面OstrichFly兄的《写一个块设备驱动》一文
我利用bio_for_each_segment()这个宏
具体代码片段如下:
int j;
void *iovec_ ...

你好,由于只有片断,我不能定位具体的原因,写一些我的想法,看看对你有没有帮助:
1:“而rmmod卸载模块的时候,显示模块在使用中”这是正常的,模块意外退出时,用为没有机会归还引用计数,因此无法卸载。
   这时如果要卸载,大概只能重启了。
2:printk("Data: %s",(char *)dsk_mem);
   我觉得块设备得到的数据不一定是字符串,因此如果这样进行printk的话,倒是很容易产生错误的内存访问。
  不过这大概是你的调试代码吧。

论坛徽章:
0
发表于 2009-03-08 20:25 |显示全部楼层
原帖由 wangch05 于 2009-2-22 11:20 发表
哇,赵磊大哥!太佩服了,虽然还没全看懂,但是感觉写的太好了。
一直都想学习内核,但是感觉不知道从哪入手,一直都没入门的感觉
不知道赵大哥有啥学习的建议不?

人人常说各个领域的知识是相通的,看来学习时遇到的麻烦倒也是相通的啊。
一开始学习不知道下手是正常的,就比如你的初恋......
找本书看看?“Linux内核设计与实现”我倒是觉得挺适合初学的,
并且翻译的不错(相比ULK),连续好多页都找不到一个错误。

[ 本帖最后由 OstrichFly 于 2009-3-9 11:05 编辑 ]

论坛徽章:
0
发表于 2009-03-08 20:30 |显示全部楼层
原帖由 veking 于 2009-2-22 13:09 发表
好东西要收藏,要一字一句的读,这样才能对的起LZ的一片苦心。:wink: 。。。
原帖由 bamv2005 于 2009-2-23 14:17 发表
拜读了楼主的前六章感觉受益匪浅, 行文流畅, 没有废话(google能说清的都跳过了). 刚好过段时间需要做块设备相关的东西, 很适合我, 谢谢楼主
原帖由 jackren317 于 2009-2-25 01:44 发表
,讲解很精辟啊,而且看得出LZ非常幽默~
佩服万分!
原帖由 peimichael 于 2009-2-25 12:31 发表
必须顶一下
先顶再看
看完再顶,呵呵
谢谢楼主
原帖由 xuxd32 于 2009-2-26 11:16 发表
好东西哟,朋友谢谢了
原帖由 piginthetree 于 2009-2-26 23:10 发表
下载了 抽时间慢慢看

谢谢支持!

论坛徽章:
0
发表于 2009-03-08 20:31 |显示全部楼层

第15章(最终章)

+---------------------------------------------------+
|                 写一个块设备驱动                  |
+---------------------------------------------------+
| 作者:赵磊                                        |
| email: zhaoleidd@hotmail.com                      |
+---------------------------------------------------+
| 文章版权归原作者所有。                            |
| 大家可以自由转载这篇文章,但原版权信息必须保留。  |
| 如需用于商业用途,请务必与原作者联系,若因未取得  |
| 授权而收起的版权争议,由侵权者自行负责。          |
+---------------------------------------------------+

在上一章中我们对这个块设备驱动所作的更改使它具备了动态申请内存的能力,
但实际上同时也埋下一个隐患,就是数据访问冲突。

这里我们顺便唠叨一下内核开发中的同步问题。
提到数据访问同步,自然而然会使人想到多进程、多线程、加锁、解锁、
信号量、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 <linux/module.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>
#include <linux/version.h>

/*
* A simple block device driver based on memory
*
* Copyright 2008 -
*        Zhaolei <zhaolei@cn.fujitsu.com>
*
* 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<<SIMP_BLKDEV_SECTORSHIFT)
#define SIMP_BLKDEV_SECTORMASK        (~(SIMP_BLKDEV_SECTORSIZE-1))

/* 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一下,
总能找到自己想了解的知识。

<--全文完,赵磊出品,必属精品-->

评分

参与人数 2可用积分 +45 收起 理由
T-Bagwell + 30 精品文章,再次阅读
scutan + 15 原创内容

查看全部评分

您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

SACC2021中国系统架构师大会

【数字转型 架构重塑】2021年5月20日-22日第十三届中国系统架构师大会将在云端进行网络直播。

大会为期3天的议程,涉及20+专场,近120个主题,完整迁移到线上进行网络直播对会议组织来说绝非易事;但考虑到云端会议的直播形式可以实现全国各地技术爱好者的参与,也使ITPUB作为技术共享交流平台得到更好的普及,我们决定迎难而上。
http://sacc.it168.com/


大会官网>>
  

北京盛拓优讯信息技术有限公司. 版权所有 16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP