写一个块设备驱动3,4(转)

第3章

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

上一章中我们讨论了mm的衣服问题,并成功地为她换上了一件轻如鸿毛、关键是薄如蝉翼的新衣服。
而这一章中,我们打算稍稍再前进一步,也就是:给她脱光。
目的是更加符合我们的审美观、并且能够更加深入地了解该mm(喜欢制服皮草的读者除外)。
付出的代价是这一章的内容要稍稍复杂一些。

虽然noop调度器确实已经很简单了,简单到比我们的驱动程序还简单,在2.6.27中的120行代码量已经充分说明了这个问题。
但显而易见的是,不管它多简单,只要它存在,我们就把它看成累赘。
这里我们不打算再次去反复磨嘴皮子论证不使用I/O调度器能给我们的驱动程序带来什么样的好处、面临的困难、以及如何与国际接轨的诸多事宜,
毕竟现在不是在讨论汽油降价,而我们也不是中石油。我们更关心的是实实在在地做一些对驱动程序有益的事情。

不过I/O调度器这层遮体衣服倒也不是这么容易脱掉的,因为实际上我们还使用了它捆绑的另一个功能,就是请求队列。
因此我们在前两章中的程序才如此简单。
从细节上来说,请求队列request_queue中有个make_request_fn成员变量,我们看它的定义:
struct request_queue
{
...
make_request_fn???????? *make_request_fn;
...
}
它实际上是:
typedef int (make_request_fn) (struct request_queue *q, struct bio *bio);
也就是一个函数的指针。

如果上面这段话让读者感到莫名其妙,那么请搬个板凳坐下,Let's Begin the Story。

对通用块层的访问,比如请求读某个块设备上的一段数据,通常是准备一个bio,然后调用generic_make_request()函数来实现的。
调用者是幸运的,因为他往往不需要去关心generic_make_request()函数如何做的,只需要知道这个神奇的函数会为他搞定所有的问题就OK了。
而我们却没有这么幸运,因为对一个块设备驱动的设计者来说,如果不知道generic_make_request()函数的内部情况,很可能会让驱动的使用者得不到安全感。

了解generic_make_request()内部的有效方法还是RTFSC,但这里会给出一些提示。
我们可以在generic_make_request()中找到__generic_make_request(bio)这么一句,
然后在__generic_make_request()函数中找到ret = q->make_request_fn(q, bio)这么一行。
偷懒省略掉解开谜题的所有关键步骤后,这里可以得出一个作者相信但读者不一定相信的正确结论:
generic_make_request()最终是通过调用request_queue.make_request_fn函数完成bio所描述的请求处理的。

Story到此结束,现在我们可以解释刚才为什么列出那段莫名其妙的数据结构的意图了。
对于块设备驱动来说,正是request_queue.make_request_fn函数负责处理这个块设备上的所有请求。
也就是说,只要我们实现了request_queue.make_request_fn,那么块设备驱动的Primary Mission就接近完成了。
在本章中,我们要做的就是:
1:让request_queue.make_request_fn指向我们设计的make_request函数
2:把我们设计的make_request函数写出来

如果读者现在已经意气风发地拿起键盘跃跃欲试了,作者一定会假装谦虚地问读者一个问题:
你的钻研精神遇到城管了?
如果这句话问得读者莫名其妙的话,作者将补充另一个问题:
前两章中明显没有实现make_request函数,那时的驱动程序倒是如何工作的?
然后就是清清嗓子自问自答。

前两章确实没有用到make_request函数,但当我们使用blk_init_queue()获得request_queue时,
万能的系统知道我们搞IT的都低收入,因此救济了我们一个,这就是大名鼎鼎的__make_request()函数。
request_queue.make_request_fn指向了__make_request()函数,因此对块设备的所有请求被导向了__make_request()函数中。

__make_request()函数不是吃素的,马上喊上了他的兄弟,也就是I/O调度器来帮忙,结果就是bio请求被I/O调度器处理了。
同时,__make_request()自身也没闲着,它把bio这条咸鱼嗅了嗅,舔了舔,然后放到嘴里嚼了嚼,把鱼刺鱼鳞剔掉,
然后情意绵绵地通过do_request函数(也就是blk_init_queue的第一个参数)喂到驱动程序作者的口中。
这就解释了前两章中我们如何通过simp_blkdev_do_request()函数处理块设备请求的。

我们理解__make_request()函数本意不错,它把bio这条咸鱼嚼成request_queue喂给do_request函数,能让我们的到如下好处:
1:request.buffer不在高端内存
这意味着我们不需要考虑映射高端内存到虚存的情况
2:request.buffer的内存是连续的
因此我们不需要考虑request.buffer对应的内存地址是否分成几段的问题
这些好处看起来都很自然,正如某些行政不作为的“有关部门”认为老百姓纳税养他们也自然,
但不久我们就会看到不很自然的情况。

如果读者是mm,或许会认为一个摔锅把咸鱼嚼好了含情脉脉地喂过来是一件很浪漫的事情(也希望这位读者与作者联系),
但对于大多数男性IT工作者来说,除非取向问题,否则......
因此现在我们宁可把__make_request()函数一脚踢飞,然后自己去嚼bio这条咸鱼。
当然,踢飞__make_request()函数也意味着摆脱了I/O调度器的处理。

踢飞__make_request()很容易,使用blk_alloc_queue()函数代替blk_init_queue()函数来获取request_queue就行了。
也就是说,我们把原先的
simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
改成了
simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL);
这样。

至于嚼人家口水渣的simp_blkdev_do_request()函数,我们也一并扔掉:
把simp_blkdev_do_request()函数从头到尾删掉。

同时,由于现在要脱光,所以上一章中我们费好大劲换上的那件薄内衣也不需要了,
也就是把上一章中增加的elevator_init()这部分的函数也删了,也就是删掉如下部分:
old_e = simp_blkdev_queue->elevator;
if (IS_ERR_VALUE(elevator_init(simp_blkdev_queue, "noop")))
printk(KERN_WARNING "Switch elevator failed, using default\n");
else
elevator_exit(old_e);

到这里我们已经成功地让__make_request()升空了,但要自己嚼bio,还需要添加一些东西:
首先给request_queue指定我们自己的bio处理函数,这是通过blk_queue_make_request()函数实现的,把这面这行加在blk_alloc_queue()之后:
blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request);
然后实现我们自己的simp_blkdev_make_request()函数,
然后编译。

如果按照上述的描述修改出的代码让读者感到信心不足,我们在此列出修改过的simp_blkdev_init()函数:
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(1);
if (!simp_blkdev_disk) {
ret = -ENOMEM;
goto err_alloc_disk;
}

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_disk:
blk_cleanup_queue(simp_blkdev_queue);
err_alloc_queue:
return ret;
}
这里还把err_init_queue也改成了err_alloc_queue,希望读者不要打算就这一点进行提问。

正如本章开头所述,这一章的内容可能要复杂一些,而现在看来似乎已经做到了。
而现在的进度大概是......一半!
不过值得安慰的是,余下的内容只有我们的simp_blkdev_make_request()函数了。

首先给出函数原型:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio);
该函数用来处理一个bio请求。
函数接受struct request_queue *q和struct bio *bio作为参数,与请求有关的信息在bio参数中,
而struct request_queue *q并没有经过__make_request()的处理,这也意味着我们不能用前几章那种方式使用q。
因此这里我们关注的是:bio。

关于bio和bio_vec的格式我们仍然不打算在这里做过多的解释,理由同样是因为我们要避免与google出的一大堆文章撞衫。
这里我们只说一句话:
bio对应块设备上一段连续空间的请求,bio中包含的多个bio_vec用来指出这个请求对应的每段内存。
因此simp_blkdev_make_request()本质上是在一个循环中搞定bio中的每个bio_vec。

这个神奇的循环是这样的:
dsk_mem = simp_blkdev_data + (bio->bi_sector << 9);

bio_for_each_segment(bvec, bio, i) {
void *iovec_mem;

switch (bio_rw(bio)) {
case READ:
case READA:
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(iovec_mem, dsk_mem, bvec->bv_len);
kunmap(bvec->bv_page);
break;
case WRITE:
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(dsk_mem, iovec_mem, bvec->bv_len);
kunmap(bvec->bv_page);
break;
default:
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": unknown value of bio_rw: %lu\n",
bio_rw(bio));
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
dsk_mem += bvec->bv_len;
}
bio请求的块设备起始扇区和扇区数存储在bio.bi_sector和bio.bi_size中,
我们首先通过bio.bi_sector获得这个bio请求在我们的块设备内存中的起始部分位置,存入dsk_mem。
然后遍历bio中的每个bio_vec,这里我们使用了系统提供的bio_for_each_segment宏。

循环中的代码看上去有些眼熟,无非是根据请求的类型作相应的处理。READA意味着预读,精心设计的预读请求可以提高I/O效率,
这有点像内存中的prefetch(),我们同样不在这里做更详细的介绍,因为这本身就能写一整篇文章,对于我们的基于内存的块设备驱动,
只要按照READ请求同样处理就OK了。

在很眼熟的memcpy前后,我们发现了kmap和kunmap这两个新面孔。
这也证明了咸鱼要比烂肉难啃的道理。
bio_vec中的内存地址是使用page *描述的,这也意味着内存页面有可能处于高端内存中而无法直接访问。
这种情况下,常规的处理方法是用kmap映射到非线性映射区域进行访问,当然,访问完后要记得把映射的区域还回去,
不要仗着你内存大就不还,实际上在i386结构中,你内存越大可用的非线性映射区域越紧张。
关于高端内存的细节也请自行google,反正在我的印象中intel总是有事没事就弄些硬件限制给程序员找麻烦以帮助程序员的就业。
所幸的是逐渐流行的64位机的限制应该不那么容易突破了,至少我这么认为。

switch中的default用来处理其它情况,而我们的处理却很简单,抛出一条错误信息,然后调用bio_endio()告诉上层这个bio错了。
不过这个万恶的bio_endio()函数在2.6.24中改了,如果我们的驱动程序是内核的一部分,那么我们只要同步更新调用bio_endio()的语句就行了,
但现在的情况显然不是,而我们又希望这个驱动程序能够同时适应2.6.24之前和之后的内核,因此这里使用条件编译来比较内核版本。
同时,由于使用到了LINUX_VERSION_CODE和KERNEL_VERSION宏,因此还需要增加#include <linux/version.h>。

循环的最后把这一轮循环中完成处理的字节数加到dsk_mem中,这样dsk_mem指向在下一个bio_vec对应的块设备中的数据。

读者或许开始耐不住性子想这一章怎么还不结束了,是的,马上就结束,不过我们还要在循环的前后加上一丁点:
1:循环之前的变量声明:
struct bio_vec *bvec;
int i;
void *dsk_mem;
2:循环之前检测访问请求是否超越了块设备限制:
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;
}
3:循环之后结束这个bio,并返回成功:
#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_endio用于返回这个对bio请求的处理结果,在2.6.24之后的内核中,第一个参数是被处理的bio指针,第二个参数成功时为0,失败时为-ERRNO。
在2.6.24之前的内核中,中间还多了个unsigned int bytes_done,用于返回搞定了的字节数。

现在可以长长地舒一口气了,我们完工了。
还是附上simp_blkdev_make_request()的完成代码:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
struct bio_vec *bvec;
int i;
void *dsk_mem;

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_mem = simp_blkdev_data + (bio->bi_sector << 9);

bio_for_each_segment(bvec, bio, i) {
void *iovec_mem;

switch (bio_rw(bio)) {
case READ:
case READA:
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(iovec_mem, dsk_mem, bvec->bv_len);
kunmap(bvec->bv_page);
break;
case WRITE:
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(dsk_mem, iovec_mem, bvec->bv_len);
kunmap(bvec->bv_page);
break;
default:
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": unknown value of bio_rw: %lu\n",
bio_rw(bio));
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
dsk_mem += 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;
}

读者可以直接用本章的simp_blkdev_make_request()函数替换掉上一章的simp_blkdev_do_request()函数,
然后用本章的simp_blkdev_init()函数替换掉上一章的同名函数,再在文件头部增加#include <linux/version.h>,
就得到了本章的最终代码。

在结束本章之前,我们还是试验一下:
首先还是编译和加载:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step3 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
CC [M]? /root/test/simp_blkdev/simp_blkdev_step3/simp_blkdev.o
Building modules, stage 2.
MODPOST
CC????? /root/test/simp_blkdev/simp_blkdev_step3/simp_blkdev.mod.o
LD [M]? /root/test/simp_blkdev/simp_blkdev_step3/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
# insmod simp_blkdev.ko
#
然后使用上一章中的方法看看sysfs中的这个设备的信息:
# ls /sys/block/simp_blkdev
dev? holders? range? removable? size? slaves? stat? subsystem? uevent
#
我们发现我们的驱动程序在sysfs目录中的queue子目录不见了。
这并不奇怪,否则就要抓狂了。

本章中我们实现自己的make_request函数来处理bio,以此摆脱了I/O调度器和通用的__make_request()对bio的处理。
由于我们的块设备中的数据都是存在于内存中,不牵涉到DMA操作、并且不需要寻道,因此这应该是最适合这种形态的块设备的处理方式。
在linux中类似的驱动程序大多使用了本章中的处理方式,但对大多数基于物理磁盘的块设备驱动来说,使用适合的I/O调度器更能提高性能。
同时,__make_request()中包含的回弹机制对需要进行DMA操作的块设备驱动来说,也能提供不错帮助。

虽然说量变产生质变,通常质变比量变要复杂得多。
同理,相比前一章,把mm衣服脱光也比让她换一件薄一些的衣服要困难得多。
不过无论如何,我们总算连哄带骗地让mm脱下来了,而付出了满头大汗的代价:
本章内容的复杂度相比前一章大大加深了。

如果本章的内容不幸使读者感觉头部体积有所增加的话,作为弥补,我们将宣布一个好消息:
因为根据惯例,随后的1、2章将会出现一些轻松的内容让读者得到充分休息。

<未完,待续>
第4章

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

上一章结束时说过,本章会准备一些不需要动脑子的内容,现在我们开始履行诺言。

看上去简单的事情实际上往往会被弄得很复杂,比如取消公仆们的招待费用问题;
看上去复杂的事情真正做起来也可能很简单,比如本章中要让我们的块设备支持分区操作。

谈到分区,不懂电脑的人想到了去找“专家”帮忙;电脑入门者想到了“高手”这个名词;
渐入佳境者想到了fdisk;资深级玩家想到了dm;红点玩家想到了隐藏的系统恢复区;
程序员想到了分区表;病毒制造者想到了把分区表清空......

作为块设备驱动程序的设计者,我们似乎需要想的比他们更多一些,
我们大概需要在驱动程序开始识别块设备时访问设备上的分区表,读出里面的数据进行分析,
找出这个块设备中包含哪一类的分区(奇怪吧,但真相是分区表确实有很多种,只是我们经常遇到的大概只有ibm类型罢了)、
几个分区,每个分区在块设备上的区域等信息,再在驱动程序中对每个分区进行注册、创建其管理信息......
读到这里,正在系鞋带准备溜之大吉的同学们请稍等片刻听我说完,
虽然实际上作者也鼓励同学们多作尝试,甚至是这种无谓的尝试,但本章中的做法却比上述的内容简单得多。
因为这一回linux居然帮了我们的忙,并且不是I/O调度器的那种倒忙。

打开linux代码,我们会在fs/partitions/目录中发现一些文件,这些友好的文件将会默默无闻地帮我们的大忙。

而我们需要做的居然如此简单,还记得alloc_disk()函数吗?
我们一直用1作参数来调用它的,但现在,我们换成64,这意味着设定块设备最大支持63个分区。
然后......不要问然后,因为已经做完了。
当然,如果要让代码看起来漂亮一些的话,我们可以考虑用一个宏来定义最大分区数。
也就是,在文件的头部增加:

#define SIMP_BLKDEV_MAXPARTITIONS????? (64)

然后把
simp_blkdev_disk = alloc_disk(1);
改成
simp_blkdev_disk = alloc_disk(SIMP_BLKDEV_MAXPARTITIONS);

好了,真的改好了。
上一章那样改得太多看起来会让读者不爽,那么这里改得太少,是不是也同样不爽?
大概有关部门深信老百姓接受不了有害物质含量过少的食品,因此制定了食品中三聚氰胺含量的标准。
于是,今后我们大概会制定出一系列标准,比如插入多深才能叫强奸什么的。

为了达到所谓的标准,我们破例补充介绍一下alloc_disk()函数:
这个函数的原型为:
struct gendisk *alloc_disk(int minors);
用于申请一个gendisk结构,并做好一些初始化工作。
minors用于指定这个设备使用的次设备号数量,因为第一个次设备号已经用于表示整个块设备了,
因此余下的minors-1个设备号用于表示块设备中的分区,这就限制了这个块设备中的最大可访问分区数。
我们注意“最大可访问分区数”这个词:
“最大”虽然指的是上限,但并不意味这是唯一的上限。
极端情况下如果这个块设备只有2个磁道,那么无论minors多大,块设备本身充其量也只能建立2个分区。
这时再谈minors值能到达多少简直就是扯淡,就像腐败不根除,建多少经济适用房都是白搭一样。
“可访问”指的是通过驱动程序可以访问的分区数量,这是因为我们只有那么多次设备号。
但这个数字并不妨碍用户在块设备上面建多少个区。比如我们把minors设定为4,那么最大可访问的分区数量是3,
足够变态的用户完全可以在块设备上建立几十个分区,只不过结果是只能使用前3个分区而已。

现在我们可以试试这个程序了。
与以往相同的是,我们编译和加载这个模块:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step04 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
CC [M]? /root/test/simp_blkdev/simp_blkdev_step04/simp_blkdev.o
Building modules, stage 2.
MODPOST
CC????? /root/test/simp_blkdev/simp_blkdev_step04/simp_blkdev.mod.o
LD [M]? /root/test/simp_blkdev/simp_blkdev_step04/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
# insmod simp_blkdev.ko
#
与以往不同的是,这一次加载完模块后,我们并不直接在块设备上创建文件系统,而是进行分区:
# 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):

关于fdisk我们不打算在这里介绍,因为我们试图让这篇文档看起来专家一些。
使用n命令创建第一个主分区:
Command (m for help): n
Command action
e?? extended
p?? primary partition (1-4)
p
Partition number (1-4): 1
First cylinder (1-2, default 1): 1
Last cylinder or +size or +sizeM or +sizeK (1-2, default 2): 1

Command (m for help):
如果细心一些的话,在这里可以看出一个小麻烦,就是:这块磁盘一共只有2个磁道。
因此,我们只好指定第一个分区仅占用1个磁道。毕竟,还要为第2个分区留一些空间。
然后建立第二个分区:
Command (m for help): n
Command action
e?? extended
p?? primary partition (1-4)
p
Partition number (1-4): 2
First cylinder (2-2, default 2): 2

Command (m for help):
这一步中由于只剩下1个磁道,fdisk便不再问我们Last cylinder,而是自作主张地把最后一个磁道分配给新的分区。
这时我们的分区情况是:
Command (m for help): p

Disk /dev/simp_blkdev: 16 MB, 16777216 bytes
255 heads, 63 sectors/track, 2 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes

Device Boot????? Start???????? End????? Blocks?? Id? System
/dev/simp_blkdev1?????????????? 1?????????? 1??????? 8001?? 83? Linux
/dev/simp_blkdev2?????????????? 2?????????? 2??????? 8032+? 83? Linux

Command (m for help):
写入分区,退出fdisk:
Command (m for help): w
The partition table has been altered!

Calling ioctl() to re-read partition table.
Syncing disks.
#

然后我们在这两个分区中创建文件系统
# mkfs.ext3 /dev/simp_blkdev1
mke2fs 1.39 (29-May-2006)
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
2000 inodes, 8000 blocks
400 blocks (5.00%) reserved for the super user
First data block=1
Maximum filesystem blocks=8388608
1 block group
8192 blocks per group, 8192 fragments per group
2000 inodes per group

Writing inode tables: done
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done

This filesystem will be automatically checked every 27 mounts or
180 days, whichever comes first.? Use tune2fs -c or -i to override.
# mkfs.ext3 /dev/simp_blkdev2
mke2fs 1.39 (29-May-2006)
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
2008 inodes, 8032 blocks
401 blocks (4.99%) reserved for the super user
First data block=1
Maximum filesystem blocks=8388608
1 block group
8192 blocks per group, 8192 fragments per group
2008 inodes per group

Writing inode tables: done
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done

This filesystem will be automatically checked every 23 mounts or
180 days, whichever comes first.? Use tune2fs -c or -i to override.
#
然后mount设两个设备:
# mount /dev/simp_blkdev1 /mnt/temp1
# mount /dev/simp_blkdev2 /mnt/temp2
#
看看结果:
# mount
/dev/hda1 on / type ext3 (rw)
proc on /proc type proc (rw)
sysfs on /sys type sysfs (rw)
devpts on /dev/pts type devpts (rw,gid=5,mode=620)
tmpfs on /dev/shm type tmpfs (rw)
none on /proc/sys/fs/binfmt_misc type binfmt_misc (rw)
/dev/simp_blkdev1 on /mnt/temp1 type ext3 (rw)
/dev/simp_blkdev2 on /mnt/temp2 type ext3 (rw)
#
然后读/写:
# cp /etc/init.d/* /mnt/temp1/
# cp /etc/passwd /mnt/temp2
# ls /mnt/temp1/
NetworkManager??????????? avahi-dnsconfd????? dund?????? ipmi??????? lost+found???? netfs???? portmap????????? rpcsvcgssd????? vncserver
NetworkManagerDispatcher ? bluetooth?????????? firstboot? iptables??? lvm2-monitor?? netplugd? psacct?????????? saslauthd?????? winbind
acpid???????????????????? capi??????????????? functions? irda??????? mcstrans?????? network?? rdisc??????????? sendmail??????? wpa_supplicant
anacron?????????????????? conman????????????? gpm??????? irqbalance? mdmonitor????? nfs?????? readahead_early? setroubleshoot? xfs
apmd????????????????????? cpuspeed??????????? haldaemon? isdn??????? mdmpd????????? nfslock?? readahead_later? single????????? ypbind
atd?????????????????????? crond?????????????? halt?????? kdump?????? messagebus???? nscd????? restorecond????? smartd????????? yum-updatesd
auditd??????????????????? cups??????????????? hidd?????? killall???? microcode_ctl? ntpd????? rhnsd??????????? sshd
autofs??????????????????? cups-config-daemon? hplip????? krb524????? multipathd???? pand????? rpcgssd????????? syslog
avahi-daemon????????????? dhcdbd????????????? ip6tables? kudzu?????? netconsole???? pcscd???? rpcidmapd??????? vmware-tools
# ls /mnt/temp2
lost+found? passwd
#
收尾工作:
# umount /dev/temp1
# umount /dev/temp2
# rmmod simp_blkdev
#

看起来本章应该结束了,但为了耽误大家更多的时间,我们来回忆一下刚才出现的小麻烦。
我们发现这块磁盘只有2个磁道,由于分区是以磁道为边界的,因此最大只能创建2个分区。
不过谢天谢地,好歹我们能够证明我们的程序是支持“多个”分区的......尽管只有2个。

那么为什么系统会认为我们的块设备只有2个磁道呢?其实这不怪系统,因为我们根本没有告诉系统我们的磁盘究竟有多少个磁道。
因此系统只好去猜、猜、猜,结果就猜成2个磁道了。
好吧,说的细节一些,传统的磁盘使用8个位表示盘面数、6个位表示每磁道扇区数、10个位表示磁道数,因此盘面、每磁道扇区、磁道的最大数值分别为255、63和1023。
这也是传说中启动操作系统时的1024柱面(磁道)和硬盘容量8G限制的根源。
现代磁盘采用线性寻址方式突破了这一限制,从本质上说,如果你的机器还没生锈,那么你的硬盘无论是内部结构还是访问方式都与常识中的盘面、每磁道扇区、磁道无关。
但为了与原先的理解兼容,对于现代磁盘,我们在访问时还是假设它具有传统的结构。目前比较通用的假设是:所有磁盘具有最大数目的(也就是恒定的)盘面和每磁道扇区数,而磁盘大小与磁道数与成正比。
因此,对于一块80G的硬盘,根据假设,这块磁盘的盘面和每磁道扇区数肯定是255和63,磁道数为:80*1024*1024*1024/512(字节每扇区)/255(盘面数)/63(每磁道扇区数)=10043(小数部分看作不完整的磁道被丢弃)。
话归原题,在驱动程序中我们指定了磁盘大小为16M,共包含16*1024*1024/512=32768个扇区。假设这块磁盘具有最大盘面和每磁道扇区数后,它的磁道数就是:32768/255/63=2。

我们看起开应该很happy,因为系统太看得起我们了,竟然把我们的块设备看成现代磁盘进行磁道数的换算处理。
不过我们也可能unhappy,因为这造成磁盘最大只能被分成2个区。(至于为什么分区以磁道作为边界,可以想象一下磁盘的结构)
但我们的磁盘只有区区16M啊,所以最好还是告诉系统我们的磁盘没有那么多的盘面数和每磁道扇区数,这将让磁道数来得多一些。

在下一章中,我们打算搞定这个问题。

<未完,待续>
<script type="text/javascript" id="wumiiRelatedItems"> </script>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值