绑定完请刷新页面
取消
刷新

分享好友

×
取消 复制
[原创] 写一个块设备驱动(14)
2020-06-03 10:32:01

第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。
因为这个操作将迫使驱动程序吃掉全部可能获得的物理内存,并且在吃光后那么一丁点内存之前不会发生错误,
这也意味着走到出错处理这一步的时候,系统已经几乎无可救药了。其实在此之前系统就会一次进行:
释放缓存、试图把所有的用户进程的内存换出、杀死全部能够杀死的进程等操作。
而我们的驱动程序由于被看作是内核的一部分,却不会被停止,而是在继续不停的吃掉通过上述方式释放出的可怜的内存。
试想,一个已经走到这一步的系统还有什么继续运行的可能呢?

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

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

<未完,待续>


文章来源CU社区:[原创] 写一个块设备驱动

分享好友

分享这个小栈给你的朋友们,一起进步吧。

内核源码
创建时间:2020-05-18 13:36:55
内核源码精华帖内容汇总
展开
订阅须知

• 所有用户可根据关注领域订阅专区或所有专区

• 付费订阅:虚拟交易,一经交易不退款;若特殊情况,可3日内客服咨询

• 专区发布评论属默认订阅所评论专区(除付费小栈外)

技术专家

查看更多
  • 飘絮絮絮丶
    专家
戳我,来吐槽~