免费注册 查看新帖 |

ChinaUnix.net

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 235961 | 回复: 65

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

论坛徽章:
0
发表于 2008-11-14 17:46 |显示全部楼层
第1章

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

同样是读书,读小说可以行云流水,读完后心情舒畅,意犹未尽;读电脑书却举步艰难,读完后目光呆滞,也是意犹未尽,只不过未尽的是痛苦的回忆。
研究证明,痛苦的记忆比快乐的更难忘记,因此电脑书中的内容比小说记得持久。
而这套教程的目的是要打破这种状况,以至于读者在忘记小说内容忘记本文。

在这套教程中,我们通过写一个建立在内存中的块设备驱动,来学习linux内核和相关设备驱动知识。
选择写块设备驱动的原因是:
1:容易上手
2:可以牵连出更多的内核知识
3:像本文这样的块设备驱动教程不多,所以需要一个

好吧,扯淡到此结束,我们开始写了。

本章的目的用尽可能最简单的方法写出一个能用的块设备驱动。
所谓的能用,是指我们可以对这个驱动生成的块设备进行mkfs,mount和读写文件。
为了尽可能简单,这个驱动的规模不是1000行,也不是500行,而是100行以内。

这里插一句,我们不打算在这里介绍如何写模块,理由是介绍的文章已经满天飞舞了。
如果你能看得懂、并且成功地编译、运行了这段代码,我们认为你已经达到了本教程的入学资格,
当然,如果你不幸的卡在这段代码中,那么请等到搞定它以后再往下看:
mod.c:
#include <linux/module.h>

static int __init init_base(void)
{
        printk("----Hello. World----\n");
        return 0;
}

static void __exit exit_base(void)
{
        printk("----Bye----\n");
}

module_init(init_base);
module_exit(exit_base);

MODULE_LICENSE ("GPL");
MODULE_AUTHOR("Zhao Lei");
MODULE_DESCRIPTION("For test");

Makefile:
obj-m := mod.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
        $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules
clean:
        $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) clean
        rm -rf Module.markers modules.order Module.symvers


好了,这里我们假定你已经搞定上面的最简单的模块了,懂得什么是看模块,以及简单模块的编写、编译、加载和卸载。
还有就是,什么是块设备,什么是块设备驱动,这个也请自行google吧,因为我们已经迫不及待要写完程序下课。

为了建立一个可用的块设备,我们需要做......1件事情:
1:用add_disk()函数向系统中添加这个块设备
   添加一个全局的
   static struct gendisk *simp_blkdev_disk;
   然后申明模块的入口和出口:
   module_init(simp_blkdev_init);
   module_exit(simp_blkdev_exit);
   然后在入口处添加这个设备、出口处私房这个设备:
   static int __init simp_blkdev_init(void)
   {
           add_disk(simp_blkdev_disk);
        return 0;
   }
   static void __exit simp_blkdev_exit(void)
   {
           del_gendisk(simp_blkdev_disk);
   }

当然,在添加设备之前我们需要申请这个设备的资源,这用到了alloc_disk()函数,因此模块入口函数simp_blkdev_init(void)应该是:
   static int __init simp_blkdev_init(void)
   {
        simp_blkdev_disk = alloc_disk(1);
        if (!simp_blkdev_disk) {
                ret = -ENOMEM;
                goto err_alloc_disk;
        }

           add_disk(simp_blkdev_disk);

        return 0;

   err_alloc_disk:
        return ret;
   }
还有别忘了在卸载模块的代码中也加一个行清理函数:
  put_disk(simp_blkdev_disk);

还有就是,设备有关的属性也是需要设置的,因此在alloc_disk()和add_disk()之间我们需要:
        strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME);
        simp_blkdev_disk->major = ?1;
        simp_blkdev_disk->first_minor = 0;
        simp_blkdev_disk->fops = ?2;
        simp_blkdev_disk->queue = ?3;
        set_capacity(simp_blkdev_disk, ?4);

SIMP_BLKDEV_DISKNAME其实是这个块设备的名称,为了绅士一些,我们把它定义成宏了:
#define SIMP_BLKDEV_DISKNAME        "simp_blkdev"

这里又引出了4个问号。(天哪,是不是有种受骗的感觉,像是陪老婆去做头发)
第1个问号:
  每个设备需要对应的主、从驱动号。
  我们的设备当然也需要,但很明显我不是脑科医生,因此跟写linux的那帮疯子不熟,得不到预先为我保留的设备号。
  还有一种方法是使用动态分配的设备号,但在这一章中我们希望尽可能做得简单,因此也不采用这种方法。
  那么我们采用的是:抢别人的设备号。
  我们手头没有AK47,因此不敢干的太轰轰烈烈,而偷偷摸摸的事情倒是可以考虑的。
  柿子要捡软的捏,而我们试图找出一个不怎么用得上的设备,然后抢他的ID。
  打开linux/include/linux/major.h,把所有的设备一个个看下来,我们觉得最胜任被抢设备号的家伙非COMPAQ_SMART2_XXX莫属。
  第一因为它不强势,基本不会被用到,因此也不会造成冲突;第二因为它有钱,从COMPAQ_SMART2_MAJOR到COMPAQ_SMART2_MAJOR7有那8个之多的设备号可以被抢,不过瘾的话还有它妹妹:COMPAQ_CISS_MAJOR~COMPAQ_CISS_MAJOR7。
  为了让抢劫显得绅士一些,我们在外面又定义一个宏:
  #define SIMP_BLKDEV_DEVICEMAJOR        COMPAQ_SMART2_MAJOR
  然后在?1的位置填上SIMP_BLKDEV_DEVICEMAJOR。
第2个问号:
  gendisk结构需要设置fops指针,虽然我们用不到,但该设还是要设的。
  好吧,就设个空得给它:
  在全局部分添加:
  struct block_device_operations simp_blkdev_fops = {
          .owner                = THIS_MODULE,
  };
  然后把?2的位置填上&simp_blkdev_fops。
第3个问号:
  这个比较麻烦一些。
  首先介绍请求队列的概念。对大多数块设备来说,系统会把对块设备的访问需求用bio和bio_vec表示,然后提交给通用块层。
  通用块层为了减少块设备在寻道时损失的时间,使用I/O调度器对这些访问需求进行排序,以尽可能提高块设备效率。
  关于I/O调度器在本章中不打算进行深入的讲解,但我们必须知道的是:
  1:I/O调度器把排序后的访问需求通过request_queue结构传递给块设备驱动程序处理
  2:我们的驱动程序需要设置一个request_queue结构
  申请request_queue结构的函数是blk_init_queue(),而调用blk_init_queue()函数时需要传入一个函数的地址,这个函数担负着处理对块设备数据的请求。
  因此我们需要做的就是:
  1:实现一个static void simp_blkdev_do_request(struct request_queue *q)函数。
  2:加入一个全局变量,指向块设备需要的请求队列:
     static struct request_queue *simp_blkdev_queue;
  3:在加载模块时用simp_blkdev_do_request()函数的地址作参数调用blk_init_queue()初始化一个请求队列:
     simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
     if (!simp_blkdev_queue) {
             ret = -ENOMEM;
             goto err_init_queue;
     }
  4:卸载模块时把simp_blkdev_queue还回去:
     blk_cleanup_queue(simp_blkdev_queue);
  5:在?3的位置填上simp_blkdev_queue。
第4个问号:
  这个还好,比前面的简单多了,这里需要设置块设备的大小。
  块设备的大小使用扇区作为单位设置,而扇区的大小默认是512字节。
  当然,在把字节为单位的大小转换为以扇区为单位时,我们需要除以512,或者右移9位可能更快一些。
  同样,我们试图把这一步也做得绅士一些,因此使用宏定义了块设备的大小,目前我们定为16M:
  #define SIMP_BLKDEV_BYTES        (16*1024*1024)
  然后在?4的位置填上SIMP_BLKDEV_BYTES>>9。

看到这里,是不是有种身陷茫茫大海的无助感?并且一波未平,一波又起,在搞定这4个问号的同时,居然又引入了simp_blkdev_do_request函数!
当然,如果在身陷茫茫波涛中时你认为到处都是海,因此绝望,那么恭喜你可以不必挨到65岁再退休;
反之,如果你认为到处都是没有三聚氰胺鲜鱼,并且随便哪个方向都是岸时,那么也恭喜你,你可以活着回来继续享受身为纳税人的荣誉。

为了理清思路,我们把目前为止涉及到的代码整理出来:
#define SIMP_BLKDEV_DEVICEMAJOR        COMPAQ_SMART2_MAJOR
#define SIMP_BLKDEV_DISKNAME        "simp_blkdev"
#define SIMP_BLKDEV_BYTES        (16*1024*1024)

static struct request_queue *simp_blkdev_queue;
static struct gendisk *simp_blkdev_disk;

static void simp_blkdev_do_request(struct request_queue *q);

struct block_device_operations simp_blkdev_fops = {
        .owner                = THIS_MODULE,
};

static int __init simp_blkdev_init(void)
{
        int ret;

        simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
        if (!simp_blkdev_queue) {
                ret = -ENOMEM;
                goto err_init_queue;
        }

        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_init_queue:
        return ret;
}

static void __exit simp_blkdev_exit(void)
{
        del_gendisk(simp_blkdev_disk);
        put_disk(simp_blkdev_disk);
        blk_cleanup_queue(simp_blkdev_queue);
}

module_init(simp_blkdev_init);
module_exit(simp_blkdev_exit);

剩下部分的不多了,真的不多了。请相信我,因为我不在质监局上班。
我写的文章诚实可靠,并且不拿你纳税的钱。

我们还有一个最重要的函数需要实现,就是负责处理块设备请求的simp_blkdev_do_request()。

首先我们看看究竟把块设备的数据以什么方式放在内存中。
毕竟这是在第1章,因此我们将使用最simple的方式实现,也就是,数组。
我们在全局代码中定义:
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];
对驱动程序来说,这个数组看起来大了一些,如果不幸被懂行的人看到,将100%遭到最无情、最严重的鄙视。
而我们却从极少数公仆那里学到了最有效的应对之策,那就是:无视他,然后把他定为成“不明真相的群众”。

然后我们着手实现simp_blkdev_do_request。
这里介绍elv_next_request()函数,原型是:
struct request *elv_next_request(struct request_queue *q);
用来从一个请求队列中拿出一条请求(其实严格来说,拿出的可能是请求中的一段)。
随后的处理请求本质上是根据rq_data_dir(req)返回的该请求的方向(读/写),把块设备中的数据装入req->buffer、或是把req->buffer中的数据写入块设备。
刚才已经提及了与request结构相关的rq_data_dir()宏和.buffer成员,其他几个相关的结构成员和函数是:
request.sector:请求的开始磁道
request.current_nr_sectors:请求磁道数
end_request():结束一个请求,第2个参数表示请求处理结果,成功时设定为1,失败时设置为0或者错误号。
因此我们的simp_blkdev_do_request()函数为:
static void simp_blkdev_do_request(struct request_queue *q)
{
        struct request *req;
        while ((req = elv_next_request(q)) != NULL) {
                if ((req->sector + req->current_nr_sectors) << 9
                        > SIMP_BLKDEV_BYTES) {
                        printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                ": bad request: block=%llu, count=%u\n",
                                (unsigned long long)req->sector,
                                req->current_nr_sectors);
                        end_request(req, 0);
                        continue;
                }

                switch (rq_data_dir(req)) {
                case READ:
                        memcpy(req->buffer,
                                simp_blkdev_data + (req->sector << 9),
                                req->current_nr_sectors << 9);
                        end_request(req, 1);
                        break;
                case WRITE:
                        memcpy(simp_blkdev_data + (req->sector << 9),
                                req->buffer, req->current_nr_sectors << 9);
                        end_request(req, 1);
                        break;
                default:
                        /* No default because rq_data_dir(req) is 1 bit */
                        break;
                }
        }
}
函数使用elv_next_request()遍历struct request_queue *q中使用struct request *req表示的每一段,首先判断这个请求是否超过了我们的块设备的最大容量,
然后根据请求的方向rq_data_dir(req)进行相应的请求处理。由于我们使用的是指简单的数组,因此请求处理仅仅是2条memcpy。
memcpy中也牵涉到了扇区号到线性地址的转换操作,我想对坚持到这里的读者来说,这个操作应该不需要进一步解释了。

编码到此结束,然后我们试试这个程序:
首先编译:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step1 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
  CC [M]  /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.o
  Building modules, stage 2.
  MODPOST
  CC      /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.mod.o
  LD [M]  /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
#
加载模块
# insmod simp_blkdev.ko
#
用lsmod看看。
这里我们注意到,该模块的Used by为0,因为它既没有被其他模块使用,也没有被mount。
# lsmod
Module                  Size  Used by
simp_blkdev         16784008  0
...
#
如果当前系统支持udev,在调用add_disk()函数时即插即用机制会自动为我们在/dev/目录下建立设备文件。
设备文件的名称为我们在gendisk.disk_name中设置的simp_blkdev,主、从设备号也是我们在程序中设定的72和0。
如果当前系统不支持udev,那么很不幸,你需要自己用mknod /dev/simp_blkdev  b 72 0来创建设备文件了。
# ls -l /dev/simp_blkdev
brw-r----- 1 root disk 72, 0 11-10 18:13 /dev/simp_blkdev
#
在块设备中创建文件系统,这里我们创建常用的ext3。
当然,作为通用的块设备,创建其他类型的文件系统也没问题。
# mkfs.ext3 /dev/simp_blkdev
mke2fs 1.39 (29-May-2006)
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
4096 inodes, 16384 blocks
819 blocks (5.00%) reserved for the super user
First data block=1
Maximum filesystem blocks=16777216
2 block groups
8192 blocks per group, 8192 fragments per group
2048 inodes per group
Superblock backups stored on blocks:
        8193

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

This filesystem will be automatically checked every 38 mounts or
180 days, whichever comes first.  Use tune2fs -c or -i to override.
#
如果这是第一次使用,建议创建一个目录用来mount这个设备中的文件系统。
当然,这不是必需的。如果你对mount之类的用法很熟,你完全能够自己决定在这里干什么,甚至把这个设备mount成root。
# mkdir -p /mnt/temp1
#
把建立好文件系统的块设备mount到刚才建立的目录中
# mount /dev/simp_blkdev /mnt/temp1
#
看看现在的mount表
# mount
...
/dev/simp_blkdev on /mnt/temp1 type ext3 (rw)
#
看看现在的模块引用计数,从刚才的0变成1了,
原因是我们mount了。
# lsmod
Module                  Size  Used by
simp_blkdev         16784008  1
...
#
看看文件系统的内容,有个mkfs时自动建立的lost+found目录。
# ls /mnt/temp1
lost+found
#
随便拷点东西进去
# cp /etc/init.d/* /mnt/temp1
#
再看看
# ls /mnt/temp1
acpid           conman              functions  irqbalance    mdmpd           NetworkManagerDispatcher  rdisc            sendmail        winbind
anacron         cpuspeed            gpm        kdump         messagebus      nfs                       readahead_early  setroubleshoot  wpa_supplicant
apmd            crond               haldaemon  killall       microcode_ctl   nfslock                   readahead_later  single          xfs
atd             cups                halt       krb524        multipathd      nscd                      restorecond      smartd          xinetd
auditd          cups-config-daemon  hidd       kudzu         netconsole      ntpd                      rhnsd            smb             ypbind
autofs          dhcdbd              ip6tables  lost+found    netfs           pand                      rpcgssd          sshd            yum-updatesd
avahi-daemon    dund                ipmi       lvm2-monitor  netplugd        pcscd                     rpcidmapd        syslog
avahi-dnsconfd  firstboot           iptables   mcstrans      network         portmap                   rpcsvcgssd       vmware
bluetooth       frecord             irda       mdmonitor     NetworkManager  psacct                    saslauthd        vncserver
#
现在这个块设备的使用情况是
# df
文件系统               1K-块        已用     可用 已用% 挂载点
...
/dev/simp_blkdev         15863      1440     13604  10% /mnt/temp1
#
再全删了玩玩
# rm -rf /mnt/temp1/*
#
看看删完了没有
# ls /mnt/temp1
#
好了,大概玩够了,我们把文件系统umount掉
# umount /mnt/temp1
#
模块的引用计数应该还原成0了吧
# lsmod
Module                  Size  Used by
simp_blkdev         16784008  0
...
#
最后一步,移除模块
# rmmod simp_blkdev
#

这是这部教程的第1章,不好意思的是,内容比预期还是难了一些。
当初还有一种考虑是在本章中仅仅实现一个写了就丢的块设备驱动,也就是说,对这个块设备的操作只能到mkfs这一部,而不能继续mount,因为刚才写的数据全被扔了。
或者更简单些,仅仅写一个hello world的模块。
但最后还是写成了现在这样没,因为我觉得拿出一个真正可用的块设备驱动程序对读者来说更有成就感。

无论如何,本章是一个开始,而你,已经跨入了学习块设备驱动教室的大门,或者通俗来说,上了贼船。
而在后续的章节中,我们将陆续完善对这个程序,通过追加或者强化这个程序,来学习与块设备有关、或与块设备无关但与linux有关的方方面面。
总之,我希望通过这部教程,起码让读者学到有用的知识,或者更进一步,引导读者对linux的兴趣,甚至领悟学习一切科学所需要的钻研精神。

作为第一章的结尾,引用我在另一篇文章中的序言:
谨以此文向读者示范什么叫做严谨的研究。
呼唤踏实的治学态度,反对浮躁的论坛风气。
--OstrichFly

<未完,待续>

评分

参与人数 2可用积分 +60 收起 理由
cjaizss + 30 我很赞同
bitmilong + 30 精品文章

查看全部评分

论坛徽章:
0
发表于 2008-11-14 17:47 |显示全部楼层

第2章

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

上一章不但实现了一个最简单的块设备驱动程序,而且可能也成功地吓退了不少准备继续看下去的读者。
因为第一章看起来好像太难了。
不过读者也不要过于埋怨作者,因为大多数情况下第一次都不是什么好的体验......

对于坚持到这里的读者,这一章中,我们准备了一些简单的内容来犒劳大家。

关于块设备与I/O调度器的关系,我们在上一章中已经有所提及。
I/O调度器可以通过合并请求、重排块设备操作顺序等方式提高块设备访问的顺序。
就好像吃街边的大排档,如果点一个冷门的品种,可能会等更长的时间,
而如果点的恰好与旁边桌子上刚点的相同,那么会很快上来,因为厨师八成索性一起炒了。
然而I/O调度器和块设备的情况却有一些微妙的区别,大概可以类比成人家点了个西红柿鸡蛋汤你接着就点了个西红柿炒蛋。
聪明的厨师一定会先做你的菜,因为随后可以直接往锅里加水煮汤,可怜比你先来的人喝的却是你的刷锅水。
两个菜一锅煮表现在块设备上可以类比成先后访问块设备的同一个位置,这倒是与I/O调度器无关,有空学习linux缓存策略时可以想想这种情况。

一个女孩子换了好多件衣服问我漂不漂亮,而我的评价只要一眼就能拿出来。
对方总觉得衣服要牌子好、面料好、搭配合理、要符合个人的气质、要有文化,而我的标准却简单的多:越薄越好。
所谓臭气相投,我写的块设备驱动程序对I/O调度器的要求大概也是如此。
究其原因倒不是因为块设备驱动程序好色,而是这个所谓块设备中的数据都是在内存中的。
这也意味着我们的“块设备”读写迅速、并且不存在磁盘之类设备通常面临的寻道时间。
因此对这个“块设备”而言,一个复杂的I/O调度器不但发挥不了丝毫作用,反而其本身将白白耗掉不少内存和CPU。
同样的情况还出现在固态硬盘、U盘、记忆棒之类驱动中。将来固态硬盘流行之时,大概就是I/O调度器消亡之日了。

这里我们试图给我们的块设备驱动选择一个最简单的I/O调度器。
目前linux中包含anticipatory、cfq、deadline和noop这4个I/O调度器。
2.6.18之前的linux默认使用anticipatory,而之后的默认使用cfq。
关于这4个调度器的原理和特性我们不打算在这里介绍,原因是相关的介绍满网都是。
但我们还是不能避免在这里提及一下noop调度器,因为我们马上要用到它。
noop顾名思义,是一个基本上不干事的调度器。它基本不对请求进行什么附加的处理,仅仅假惺惺地告诉通用块设备层:我处理完了。
但与吃空饷的公仆不同,noop的存在还是有不少进步意义的。至少我们现在就需要一个不要没事添乱的I/O调度器。

选择一个指定的I/O调度器需要这个函数:
int elevator_init(struct request_queue *q, char *name);
q是请求队列的指针,name是需要设定的I/O调度器的名称。
如果name为NULL,那么内核会首先尝试选择启动参数"elevator="中指定的调度器,
不成功的话就去选择编译内核时指定的默认调度器,
如果运气太背还是不成功,就去选择"noop"调度器。
不要问我怎么知道的,一切皆在RTFSC(Read the F**ing Source Code --Linus Torvalds)。

对于我们的代码,就是在simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL)后面加上:
elevator_init(simp_blkdev_queue, "noop");

但问题是在blk_init_queue()函数中系统已经帮我们申请一个了,因此这里我们需要费点周折,把老的那个送回去。
所以我们的代码应该是:
simp_blkdev_init()函数开头处:
elevator_t *old_e;
blk_init_queue()函数之后:
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);

为方便阅读并提高本文在google磁盘中的占用率,我们给出修改后的整个simp_blkdev_init()函数:
static int __init simp_blkdev_init(void)
{
        int ret;
        elevator_t *old_e;

        simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
        if (!simp_blkdev_queue) {
                ret = -ENOMEM;
                goto err_init_queue;
        }

        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);

        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_init_queue:
        return ret;
}

本章的改动很小,我们现在测试一下这段代码:
首先我们像原先那样编译模块并加载:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step2 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
  CC [M]  /root/test/simp_blkdev/simp_blkdev_step2/simp_blkdev.o
  Building modules, stage 2.
  MODPOST
  CC      /root/test/simp_blkdev/simp_blkdev_step2/simp_blkdev.mod.o
  LD [M]  /root/test/simp_blkdev/simp_blkdev_step2/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
# insmod simp_blkdev.ko
#
然后看一看咱们的这个块设备现在使用的I/O调度器:
# cat /sys/block/simp_blkdev/queue/scheduler
[noop] anticipatory deadline cfq
#
看样子是成功了。

哦,上一章中忘了看老程序的调度器信息了,这里补上老程序的情况:
# cat /sys/block/simp_blkdev/queue/scheduler
noop anticipatory deadline [cfq]
#

OK,我们完成简单的一章,并且用事实说明了作者并没有在开头撒谎。
当然,作者也会力图让接下来的章节同样比小说易读。

<未完,待续>

论坛徽章:
0
发表于 2008-11-17 08:55 |显示全部楼层
原帖由 Godbach 于 2008-11-14 18:11 发表
顶一下。
中文内核邮件列表中有个Zhao Lei是不是LZ啊

有个叫zhaolei@cn。fujitsu。c0m的是我

论坛徽章:
0
发表于 2008-11-17 09:00 |显示全部楼层
原帖由 duanius 于 2008-11-15 11:22 发表
感谢lz  只搞过字符设备和网络设备驱动    这回有机会看看了
不过不知道是否直接可以看 还是要把ldd相关章节干掉才能阅读

建议的读法:
直接读,不懂的地方直接去google。

比如文章中用到的一些函数都没有给出详细说明,毕竟我不想写成函数说明书。
如果希望深入研究的话,很多文章中没有提到的内容还是需要了解的。
不过有了google咱们还怕什么?google不到,还有源码不是?

谢谢支持!

论坛徽章:
0
发表于 2008-11-17 18:47 |显示全部楼层

第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章将会出现一些轻松的内容让读者得到充分休息。

<未完,待续>

论坛徽章:
0
发表于 2008-11-18 21:19 |显示全部楼层
原帖由 platinum 于 2008-11-15 10:26 发表

Mail List 里面那个 zhao lei 是 ShadowStar,是 ipp2p 模块修改者
http://linux.chinaunix.net/bbs/thread-1038871-1-1.html

我是Zhaolei,但不是ShadowStar。
因为我不懂ipp2p。

论坛徽章:
0
发表于 2008-11-18 21:20 |显示全部楼层

第4章

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

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

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

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

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

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

而我们需要做的居然如此简单,还记得alloc_disk()函数吗?
我们一直用1作参数来调用它的,但现在,我们换成64,这意味着设定块设备最大支持63个分区。
然后......不要问然后,因为已经做完了。
当然,如果要让代码看起来漂亮一些的话,我们可以考虑用一个宏来定义最大分区数。
也就是,在文件的头部增加:
/* usable partitions is SIMP_BLKDEV_MAXPARTITIONS - 1 */
#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啊,所以最好还是告诉系统我们的磁盘没有那么多的盘面数和每磁道扇区数,这将让磁道数来得多一些。

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

<未完,待续>

论坛徽章:
0
发表于 2008-11-18 21:21 |显示全部楼层

第5章

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

既然上一章结束时我们已经预告了本章的内容,
那么本章中我们就让这个块设备有能力告知操作系统它的“物理结构”。

当然,对于基于内存的块设备来说,什么样的物理结构并不重要,
这就如同从酒吧带mm回家时不需要打听她的姓名一样。
但如果不幸遇到的是兼职,并且带她去不入流的招待所时,
建议最好还是先串供一下姓名、生日和职业等信息,
以便JJ查房时可以伪装成情侣。
同样,如果要实现的是真实的物理块设备驱动,
那么返回设备的物理结构时大概不能这么随意。

对于块设备驱动程序而言,我们现在需要关注那条目前只有一行的struct block_device_operations simp_blkdev_fops结构。
到目前为止,它存在的目的仅仅是因为它必须存在,但马上我们将发现它存在的另一个目的:为块设备驱动添加获得块设备物理结构的接口。

对于具有极强钻研精神的极品读者来说,大概在第一章中就会自己去看struct block_device_operations结构,然后将发现这个结构其实还挺复杂:
struct block_device_operations {
        int (*open) (struct block_device *, fmode_t);
        int (*release) (struct gendisk *, fmode_t);
        int (*locked_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
        int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
        int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
        int (*direct_access) (struct block_device *, sector_t,
                                                void **, unsigned long *);
        int (*media_changed) (struct gendisk *);
        int (*revalidate_disk) (struct gendisk *);
        int (*getgeo)(struct block_device *, struct hd_geometry *);
        struct module *owner;
};
在前几章中,我们邂逅过其中的owner成员变量,它用于存储这个结构的所有者,也就是我们的模块,因此我们做了如下的赋值:
.owner                = THIS_MODULE,
而这一章中,我们将与它的同胞妹妹------getgeo也亲密接触一下。

我们要做的是:
1:在block_device_operations中增加getgeo成员变量初值的设定,指向我们的“获得块设备物理结构”函数。
2:实现我们的“获得块设备物理结构”函数。

第一步很简单,我们暂且为“获得块设备物理结构”函数取个名字叫simp_blkdev_getgeo()吧,也避免了在下文中把这么一大堆汉字拷来拷去。
在simp_blkdev_fops中添加.getgeo指向simp_blkdev_getgeo,也就是把simp_blkdev_fops结构改成这个样子:
struct block_device_operations simp_blkdev_fops = {
        .owner                = THIS_MODULE,
        .getgeo                = simp_blkdev_getgeo,
};

第二步难一些,但也难不到哪去,在代码中的struct block_device_operations simp_blkdev_fops这行之前找个空点的场子,把如下函数插进去:
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>>9/geo->heads/geo->sectors;

        return 0;
}
因为这里我们用到了struct hd_geometry结构,所以还要增加一行#include <linux/hdreg.h>。

这个函数的目的,是选择适当的物理结构信息装入struct hd_geometry *geo结构。
当然,为了克服上一章中只能分成2个区的问题,我们应该尽可能增加磁道的数量。
希望读者不要理解成分几个区就需要几个磁道,这意味着一个磁道一个区,也意味着每个区必须一般大小。
由于分区总是以磁道为边界,尽可能增加磁道的数量不仅仅是为了让块设备容纳更多的分区,
更重要的是让分区的实际大小更接近于分区时的指定值,也就是提高实际做出的分区容量的精度。

不过对于设置的物理结构值,还存在一个限制,就是struct hd_geometry中的数值上限。
我们看struct hd_geometry的内容:
struct hd_geometry {
        unsigned char heads;
        unsigned char sectors;
        unsigned short cylinders;
        unsigned long start;
};
unsigned char的磁头数和每磁道扇区数决定了其255的上限,同样,unsigned short的磁道数决定了其65535的上限。
这还不算,但在前一章中,我们知道对于现代硬盘,磁头数和每磁道扇区数通常取的值是255和63,
再组合上这里的65535的磁道数上限,hd_geometry能够表示的最大块设备容量是255*63*65535*512/1024/1024/1024=502G。
显然目前linux支持的最大硬盘容量大于502G,那么对于这类块设备,内核是如何通过hd_geometry结构表示其物理结构的呢?
诀窍不在内核,而在于用户态程序如fdisk等通过内核调用获得hd_geometry结构后,
会舍弃hd_geometry.cylinders内容,取而代之的是直接通过hd_geometry中的磁头数和每磁道扇区数以及硬盘大小去计算磁道数。
因此对于超过502G的硬盘,由于用户程序得出的磁道数与hd_geometry.cylinders无关,所以我们往往在fdisk中能看到这块硬盘的磁道数大于65535。

刚才扯远了,现在言归正题,我们决定让这个函数对于任何尺寸的块设备,总是试图返回比较漂亮的物理结构。
漂亮意味着返回的物理结构既要保证拥有足够多的磁道,也要保证磁头数和每磁道扇区数不超过255和63,同时最好使用程序员看起来比较顺眼的数字,
如:1、2、4、8、16、32、64等。
当然,我们也希望找到某个One Shot公式适用于所有大小的块设备,但很遗憾目前作者没找到,因此采用了分段计算的方法:
首先考虑容量很小的块设备:
  即使磁头数和每磁道扇区数都是1,磁道数也不够多时,我们会将磁头数和每磁道扇区数都固定为1,以使磁道数尽可能多,以提高分区的精度。
  因此磁道数随块设备容量而上升。
  虽然我们已经知道了磁道数其实可以超过unsigned short的65535上限,但在这里却没有必要,因此我们要给磁道数设置一个上限。
  因为不想让上限超过65535,同时还希望上限也是一个程序员喜欢的数字,因此这里选择了32768。
  当然,当磁道数超过32768时,已经意味着块设备容量不那么小了,也就没有必要使用这种情况中如此苛刻的磁头数和每磁道扇区数了。
  简单来说,当块设备容量小于1个磁头、每磁道1扇区和32768个磁道对应的容量--也就是16M时,我们将按照这种情况处理。
然后假设块设备容量已经大于16M了:
  我们希望保证块设备包含足够多的磁道,这里我们认为1024个磁道应该不少了。
  磁道的最小值发生在块设备容量为16M的时候,这时使用1024作为磁道数,可以计算出磁头数*每磁道扇区数=32。
  这里暂且把磁头数和每磁道扇区数固定为1和32,而让磁道数随着块设备容量的增大而增加。
  同时,我们还是磁道的上限设置成32768,这时的块设备容量为512M。
  总结来说,当块设备容量在16M和512M之间时,我们把磁头数和每磁道扇区数固定为1和32。
然后对于容量大于512M的块设备:
  与上述处理相似,当块设备容量在512M和16G之间时,我们把磁头数和每磁道扇区数固定为32和32。
最后的一种情况:
  块设备已经足够大了,大到即使我们使用磁头数和每磁道扇区数的上限,
  也能获得足够多的磁道数。这时把磁头数和每磁道扇区数固定为255和63。
  至于磁道数就算出多少是多少了,即使超过unsigned short的上限也无所谓,反正用不着。

随着这个函数解说到此结束,我们对代码的修改也结束了。

现在开始试验:
编译和加载:
# make
make -C /lib/modules/2.6.27.4/build SUBDIRS=/mnt/host_test/simp_blkdev/simp_blkdev_step05 modules
make[1]: Entering directory `/mnt/ltt-kernel'
  CC [M]  /mnt/host_test/simp_blkdev/simp_blkdev_step05/simp_blkdev.o
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /mnt/host_test/simp_blkdev/simp_blkdev_step05/simp_blkdev.mod.o
  LD [M]  /mnt/host_test/simp_blkdev/simp_blkdev_step05/simp_blkdev.ko
make[1]: Leaving directory `/mnt/ltt-kernel'
# insmod simp_blkdev.ko
#
用fdisk打开设备文件
# 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):
看看设备的物理结构:
Command (m for help): p

Disk /dev/simp_blkdev: 16 MB, 16777216 bytes
1 heads, 32 sectors/track, 1024 cylinders
Units = cylinders of 32 * 512 = 16384 bytes

           Device Boot      Start         End      Blocks   Id  System

Command (m for help):
我们发现,现在的设备有1个磁头、32扇区每磁道、1024个磁道。
这是符合代码中的处理的。

本章的内容也不是太难,连同上一章,我们已经休息2章了。
聪明的读者可能已经猜到作者打算说什么了。
不错,下一章会有一个surprise。

<未完,待续>

论坛徽章:
0
发表于 2008-11-18 21:25 |显示全部楼层
第6章可能要等一段时间了,因为准备讲述动态申请内存存储设备数据,并用基树来管理,更关键的理由是:
我打算先把英雄无敌的一个战役打完,还有老婆在催我画墙上的那棵树,都画了几个月了,还只有树根......

论坛徽章:
0
发表于 2008-11-25 21:29 |显示全部楼层

第6章

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

经历了内容极为简单的前两章的休息,现在大家一定感到精神百倍了。
作为已经坚持到现在的读者,对接下去将要面临的内容大概应该能够猜得八九不离十了,
具体的内容猜不出来也无妨,但一定将是具有增加颅压功效的。

与物理块设备驱动程序的区别在于,我们的驱动程序使用内存来存储块设备中的数据。
到目前为止,我们一直都是使用这样一个静态数组来担负这一功能的:
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];

如果读者懂得一些模块的知识,或者现在赶紧去临时抱佛脚google一些模块知识,
应该知道模块其实是加载在非线性映射区域的。
详细来说,在加载模块时,根据模块的ELF信息(天哪,又要去google elf了),确定这个模块所需的静态内存大小。
这些内存用来容纳模块的二进制代码,以及静态变量。然后申请容纳这些数据的一大堆页面。
当然,这些页面并不是连续的,而代码和变量却不可能神奇到即使被切成一块一块的也能正常工作。
因此需要在非线性映射区域中找到一块连续的地址(现在有要去google非线性映射区域了),用来将刚才申请到的一块一块的内存页映射到这个地址段中。
最后模块被请到这段区域中,然后执行模块的初始化函数......

现在看我们这个模块中的simp_blkdev_data变量,如果不是现在刻意关注,这个变量看起来显得那么得普通。
正如其它的一些名字原先也是那么的普通,但由于一些突发的事件受到大家的热烈关注,
比如一段视频让我们熟悉了kappa和陆佳妮,比如呼吸税让我们认识了蒋有绪。

现在我们开始关注simp_blkdev_data变量了,导火索是刚才介绍的非线性映射区域。
模块之所以被加载到非线性映射区域,是因为很难在线性映射区域中得到加载模块所需的连续的内存。
但使用非线性映射区域也并非只赚不赔的生意,至少在i386结构中,非线性映射区域实在是太小了。
在物理内存大于896M的i386系统中,整个非线性映射区域不会超过128M。
相反如果物理内存小于896M(不知道该算是幸运还是不幸),非线性映射区域反而会稍微大一些,这种情况我想我们可以不用讨论了,毕竟不能为了加载一个模块去拔内存。
因此我们的结论是:非线性映射区域是很紧张的资源,我们要节约使用。
而像我们现在这个模块中的simp_blkdev_data却是个如假包换的反面典型,居然上来就吃掉了16M!这还是因为我们没有把SIMP_BLKDEV_BYTES定义得更大。

现在我们开始列举simp_blkdev_data的种种罪行:
1:剩余的非线性映射区域较小时导致模块加载失败
2:模块加载后占用了大量的非线性映射区域,导致其它模块加载失败。
3:模块加载后占用了大量的非线性映射区域,影响系统的正常运行。
   这是因为不光模块,系统本身的很多功能也依赖非线性映射区域空间。
对于这样的害群之马,我们难道还有留下他的理由吗?
本章的内容虽然麻烦一些,但想到能够一了百了地清除这个体大膘肥的simp_blkdev_data,倒也相当值得。
也希望今后能够看到在对贪官的处理上,能够也拿出这样的魄力和勇气。

现在在清除simp_blkdev_data的问题上,已经不存在什么悬念了,接下来我们需要关注的是将simp_blkdev_data碎尸万段后,拿出一个更恰当方法来代替它。
首先,我们决定不用静态声明的数组,而改用动态申请的内存。
其次,使用类似vmalloc()的函数可以动态申请大段内存,但其实这段内存占用的还是非线性映射区域,就好像用一个比较隐蔽的贪官来代替下马的贪官,我们不会愚蠢在这种地步。
剩下的,就是在线性映射区域申请很多个页的内存,然后自己去管理。这个方法一了百了地解决了使用大段非线性映射区域的问题,而唯一的问题是由于需要自己管理申请到的页面,使程序复杂了不少。
但为了整个系统的利益,这难道不是我们该做的吗?

申请一个内存页是很容易的,这里我们将采用所有容易的方法中最容易的那个:
__get_free_page函数,原型是:
unsigned long __get_free_page(gfp_t gfp_mask);
这个函数用来申请一个页面的内存。gfp_mask包含一些对申请内存时的指定,比如,要在DMA区域中啦、必须清零等。
我们这里倒是使用最常见的__get_free_page(GFP_KERNEL)就可以了。

通过__get_free_page申请到了一大堆内存页,新的问题来了,在读写块设备时,我们得到是块设备的偏移,如何快速地通过偏移找到对应的内存页呢?

最简单的方法是建立一个数组,用来存放偏移到内存的映射,数组中的每项对应一个一个页:
数组定义如下:
void *simp_blkdev_data[(SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) / PAGE_SIZE];
PAGE_SIZE是系统中每个页的大小,对i386来说,通常是4K,那堆加PAGE_SIZE减1的代码是考虑到SIMP_BLKDEV_BYTES不是PAGE_SIZE的整数倍时要让末尾的空间也能访问。
然后申请内存的代码大概是:
for (i=0; i < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) / PAGE_SIZE; i++) {
        p = (void *)__get_free_page(GFP_KERNEL);
        simp_blkdev_data[i] = p;
}
通过块设备偏移得到内存中的数据地址的代码大概是:
mem_addr = simp_blkdev_data[dev_addr/PAGE_SIZE] + dev_addr % PAGE_SIZE;
这种方法实现起来还是比较简单的,但缺点也不是没有:存放各个页面地址的数组虽然其体积比原先那个直接存放数据的数组已经缩小了很多,
但本身毕竟还是在非线性映射区域中。如果块设备大小为16M,在i386上,需要4096个页面,数组大小16K,这不算太大。
但如果某个疯子打算建立一个2G的虚拟磁盘,数组大小将达到2M,这就不算小了。

或者我们可以不用数组,而用链表来存储偏移到内存页的映射关系,这样可以回避掉数组存在的问题,但在链表中查找指定元素却不是一般的费时,
毕竟我们不希望用户觉得这是个软盘。

接下来作者不打断继续卖关子了,我们最终选择使用的是传说中的基树。
关于linux中基树细节的文档不多,特别是中文文档更少,更糟的是我们这篇文档也不打算作详细的介绍(因为作者建议去RTFSC)。
但总的来说,相对于二叉平衡树的红黑树来说,基树是一个n叉(一般为64叉)非平衡树,n叉减少了搜索的深度,非平衡减少了复杂的平衡操作。
当然,这两个特点也不是仅仅带来优点,但在这里我们就视而不见了,毕竟我们已经选择了基树,因此护短也是自认而然的事情,正如公仆护着王细牛一样。
从功能上来说,基树好像是为我们量身定做的一样,好用至极。
(其实我们也可以考虑选择红黑树和哈希表来实现这个功能,感兴趣的读者可以了解一下)

接下来的代码中,我们将要用到基树种的如下函数:
  void INIT_RADIX_TREE((struct radix_tree_root *root, gfp_t mask);
  用来初始化一个基树的结构,root是基树结构指针,mask是基树内部申请内存时使用的标志。

  int radix_tree_insert(struct radix_tree_root *root, unsigned long index, void *item);
  用来往基树中插入一个指针,index是指针的索引,item是指针,将来可以通过index从基树中快速获得这个指针的值。

  void *radix_tree_delete(struct radix_tree_root *root, unsigned long index);
  用来根据索引从基树中删除一个指针,index是指针的索引。

  void *radix_tree_lookup(struct radix_tree_root *root, unsigned long index);
  用来根据索引从基树中查找对应的指针,index是指针的索引。
其实基树的功能不仅限于此,比如,还可以给指针设定标志,详情还是请去读linux/lib/radix-tree.c

现在开始改造我们的代码:
首先删除那个无耻的数组:
unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];
然后引入它的替代者--一个基树结构:
static struct radix_tree_root simp_blkdev_data;

然后增加两个函数,用来申请和释放块设备的内存:

申请内存的函数如下:
int alloc_diskmem(void)
{
        int ret;
        int i;
        void *p;

        INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL);

        for (i = 0; i < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) >> PAGE_SHIFT;
                i++) {
                p = (void *)__get_free_page(GFP_KERNEL);
                if (!p) {
                        ret = -ENOMEM;
                        goto err_alloc;
                }

                ret = radix_tree_insert(&simp_blkdev_data, i, p);
                if (IS_ERR_VALUE(ret))
                        goto err_radix_tree_insert;
        }
        return 0;

err_radix_tree_insert:
        free_page((unsigned long)p);
err_alloc:
        free_diskmem();
        return ret;
}
先初始化基树结构,然后申请需要的每一个页面,按照每页面的次序作为索引,将指针插入基树。
代码中的“>> PAGE_SHIFT”与“/ PAGE_SIZE”作用相同,
if (不明白为什么要这样)
        do_google();

释放内存的函数如下:
void free_diskmem(void)
{
        int i;
        void *p;

        for (i = 0; i < (SIMP_BLKDEV_BYTES + PAGE_SIZE - 1) >> PAGE_SHIFT;
                i++) {
                p = radix_tree_lookup(&simp_blkdev_data, i);
                radix_tree_delete(&simp_blkdev_data, i);
                /* free NULL is safe */
                free_page((unsigned long)p);
        }
}
遍历每一个索引,得到页面的指针,释放页面,然后从基树中释放这个指针。
由于alloc_diskmem()函数在中途失败时需要释放申请过的页面,因此我们把free_diskmem()函数设计成能够释放建立了一半的基树的形式。
对于只建立了一半的基树而言,有一部分索引对应的指针还没来得及插入基树,对于不存在的索引,radix_tree_delete()函数会返回NULL,幸运的是free_page()函数能够忽略传入的NULL指针。

因为alloc_diskmem()函数需要调用free_diskmem()函数,在代码中需要把free_diskmem()函数写在alloc_diskmem()前面,或者在文件头添加函数的声明。

然后在模块的初始化和释放函数中添加对alloc_diskmem()和free_diskmem()的调用,
也就是改成这个样子:
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(SIMP_BLKDEV_MAXPARTITIONS);
        if (!simp_blkdev_disk) {
                ret = -ENOMEM;
                goto err_alloc_disk;
        }

        ret = alloc_diskmem();
        if (IS_ERR_VALUE(ret))
                goto err_alloc_diskmem;

        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_diskmem:
        put_disk(simp_blkdev_disk);
err_alloc_disk:
        blk_cleanup_queue(simp_blkdev_queue);
err_alloc_queue:
        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);
}

最麻烦的放在最后:
我们需要修改simp_blkdev_make_request()函数,让它适应新的数据结构。

原先的实现中,对于一个bio_vec,我们找到对应的内存中数据的起点,直接传送bvec->bv_len个字节就大功告成了,比如,读块设备时就是:
memcpy(iovec_mem, dsk_mem, bvec->bv_len);
但现在由于容纳数据的每个页面地址是不连续的,因此可能出现bio_vec中的数据跨越页面边界的情况。
也就是说,一个bio_vec中的数据的前半段在一个页面中,后半段在另一个页面中。
虽然这两个页面对应的块设备地址连续,但在内存中的地址不一定连续,因此像原先那样简单使用memcpy看样子是解决不了问题了。

实际上,虽然bio_vec可能跨越页面边界,但它无论如何也不可能跨越2个以上的页面。
这是因为bio_vec本身对应的数据最大长度只有一个页面。
因此如果希望做最简单的实现,只要在代码中做一个条件判断就OK了:
if (没有跨越页面) {
        1个memcpy搞定
} else {
        /* 肯定是跨越2个页面了 */
        2个memcpy搞定
}

但为了表现出物理设备一次传送1个扇区数据的处理方式(这种情况下一个bio_vec可能会跨越2个以上的扇区),我们让代码支持2个以上页面的情况。
首先列出修改后的simp_blkdev_make_request()函数:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
        struct bio_vec *bvec;
        int i;
        unsigned long long dsk_offset;

        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_offset = bio->bi_sector << 9;

        bio_for_each_segment(bvec, bio, i) {
                unsigned int count_done, count_current;
                void *iovec_mem;
                void *dsk_mem;

                iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;

                count_done = 0;
                while (count_done < bvec->bv_len) {
                        count_current = min(bvec->bv_len - count_done,
                                (unsigned int)(PAGE_SIZE
                                - ((dsk_offset + count_done) & ~PAGE_MASK)));

                        dsk_mem = radix_tree_lookup(&simp_blkdev_data,
                                (dsk_offset + count_done) >> PAGE_SHIFT);
                        if (!dsk_mem) {
                                printk(KERN_ERR SIMP_BLKDEV_DISKNAME
                                        ": search memory failed: %llu\n",
                                        (dsk_offset + count_done)
                                        >> PAGE_SHIFT);
                                kunmap(bvec->bv_page);
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
                                bio_endio(bio, 0, -EIO);
#else
                                bio_endio(bio, -EIO);
#endif
                                return 0;
                        }
                        dsk_mem += (dsk_offset + count_done) & ~PAGE_MASK;

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

                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;
}

看样子长了一些,但不要被吓着了,因为读的时候我们可以对代码做一些简化:
1:去掉乱七八糟的出错处理
2:无视每行80字符限制
3:把比特运算改成等价但更易读的乘除运算
4:无视碍眼的类型转换
5:假设内核版本大于2.6.24,以去掉判断版本的宏
就会变成这样了:
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
        struct bio_vec *bvec;
        int i;
        unsigned long long dsk_offset;

        dsk_offset = bio->bi_sector * 512;

        bio_for_each_segment(bvec, bio, i) {
                unsigned int count_done, count_current;
                void *iovec_mem;
                void *dsk_mem;

                iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;

                count_done = 0;
                while (count_done < bvec->bv_len) {
                        count_current = min(bvec->bv_len - count_done, PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE);

                        dsk_mem = radix_tree_lookup(&simp_blkdev_data, (dsk_offset + count_done) / PAGE_SIZE);
                        dsk_mem += (dsk_offset + count_done) % PAGE_SIZE;

                        switch (bio_rw(bio)) {
                        case READ:
                        case READA:
                                memcpy(iovec_mem + count_done, dsk_mem, count_current);
                                break;
                        case WRITE:
                                memcpy(dsk_mem, iovec_mem + count_done, count_current);
                                break;
                        }
                        count_done += count_current;
                }

                kunmap(bvec->bv_page);
                dsk_offset += bvec->bv_len;
        }

        bio_endio(bio, 0);
        return 0;
}
是不是清楚多了?

dsk_offset用来存储当前要处理的数据在块设备上的偏移,初始值是bio->bi_sector * 512,也就是起始扇区对应的偏移,也是第一个bio_vec对应的块设备偏移。
每处理完成一个bio_vec时,dsk_offset值会被更新:dsk_offset += bvec->bv_len,以指向将要处理的数据在块设备上的偏移。

在bio_for_each_segment()中代码的起始和末尾,执行kmap和kunmap开映射当前这个bio_vec的内存,这个知识在前面的章节中已经提到了,
这个处理的结果是iovec_mem指向当前的bio_vec中的缓冲区。

现在在kmap和kunmap之间的代码的功能已经很明确了,就是完成块设备上偏移为dsk_offset、长度为bvec->bv_len的数据与iovec_mem地址之间的传送。
假设不考虑bio_vec跨越页面边界的情况,这段代码应该十分写意:
dsk_mem = radix_tree_lookup(&simp_blkdev_data, dsk_offset / PAGE_SIZE) + dsk_offset % PAGE_SIZE;
switch (bio_rw(bio)) {
case READ:
case READA:
        memcpy(iovec_mem, dsk_mem, bvec->bv_len);
        break;
case WRITE:
        memcpy(dsk_mem, iovec_mem, bvec->bv_len);
        break;
}
首先使用dsk_offset / PAGE_SIZE、也就是块设备偏移在内存中数据所位于的页面次序作为索引,查找该页的内存起始地址,
然后加上块设备偏移在该页内的偏移、也就是dsk_offset % PAGE_SIZE,
就得到了内存中数据的地址,然后就是简单的数据传送。
关于块设备偏移到内存地址的转换,我们举个例子:
假使模块加载时我们分配的第1个页面的地址为0xd0000000,用于存放块设备偏移为0~4095的数据
第2个页面的地址为0xd1000000,用于存放块设备偏移为4096~8191的数据
第3个页面的地址为0xc8000000,用于存放块设备偏移为8192~12287的数据
第4个页面的地址为0xe2000000,用于存放块设备偏移为12288~16383的数据
对于块设备偏移为9000的数据,首先通过9000 / PAGE_SIZE确定它位于第3个页面中,
然后使用radix_tree_lookup(&simp_blkdev_data, 3)将查找出0xc8000000这个地址。
这是第3个页面的起始地址,这个地址的数据在块设备中的偏移是8192,
因此我们还要加上块设备偏移在页内的偏移量,也就是9000 % PAGE_SIZE = 808,
得到的才是块设备偏移为9000的数据在内存中的数据地址。

当然,假设终归是假设,往往大多数情况下是自欺欺人的,就好像彩迷总喜欢跟女友说如果中了500万,就要怎么怎么对她好一样。
现在回到残酷的现实,我们还是要去考虑bio_vec跨越页面边界的情况。
这意味着对于一个bio_vec,我们将有可能传送多次。
为了记录前几次中已经完成的数据量,我们引入了一个新的变量,叫做count_done。
在进行bio_vec内的第一次传送前,count_done的值是0,每完成一次传送,count_done将加上这次完成的数据量。
当count_done == bvec->bv_len时,就是大功告成的之日。

接着就是边界的判断。
当前块设备偏移所在的内存页中,块设备偏移所在位置到页头的距离为:
offset % PAGE_SIZE
块设备偏移所在位置到页尾的距离为:
PAGE_SIZE - offset % PAGE_SIZE
这个距离也就是不超越页边界时所能传送的数据的最大值。

因此在bio_vec内的每一次中,我们使用
count_current = min(bvec->bv_len - count_done, PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE);
来确定这一次传送的数据量。
bvec->bv_len - count_done指的是余下需要传送的数据总量,
PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE指的是从当前块设备偏移开始、不超越页边界时所能传送的数据的最大值。
如果bvec->bv_len - count_done > PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE,说明这一次将传送从当前块设备偏移到其所在内存页的页尾之间的数据,
余下的数据位于后续的页面中,将在接下来的循环中搞定,
如果bvec->bv_len - count_done <= PAGE_SIZE - (dsk_offset + count_done) % PAGE_SIZE,那么可喜可贺,这将是当前bio_vec的最后一次传送,完成后就可以回家洗澡了。

结合以上的说明,我想应该不难看懂simp_blkdev_make_request()的代码了,而我们的程序也已经大功告成。

现在总结一下修改的位置:
1:把unsigned char simp_blkdev_data[SIMP_BLKDEV_BYTES];换成static struct radix_tree_root simp_blkdev_data;
2:把本文中的free_diskmem()和alloc_diskmem()函数添加到代码中,虽然没有特别意义,但建议插在紧邻simp_blkdev_init()之前的位置。
   但有特别意义的是free_diskmem()和alloc_diskmem()的顺序,如果读者到这里还打算提问是什么顺序,作者可要哭了。
3:把simp_blkdev_make_request()、simp_blkdev_init()和simp_blkdev_exit()函数替换成文中的代码。
   注意不要企图使用简化过的simp_blkdev_make_request()函数,否则造成的后果:从程序编译失败到读者被若干美女轮奸,作者都概不负责。

从常理分析,在修改完程序后,我们都将试验一次修改的效果。
这次也不例外,因为审判彭宇的王法官也是这么推断的。

首先证明我们的模块至今为止仍然经得起编译、能够加载:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step06 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
  CC [M]  /root/test/simp_blkdev/simp_blkdev_step06/simp_blkdev.o
  Building modules, stage 2.
  MODPOST
  CC      /root/test/simp_blkdev/simp_blkdev_step06/simp_blkdev.mod.o
  LD [M]  /root/test/simp_blkdev/simp_blkdev_step06/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
# insmod simp_blkdev.ko
#

看看模块的加载时分配的非线性映射区域大小:
# lsmod
Module                  Size  Used by
simp_blkdev             8212  0
...
#
如果这个Size一栏的数字没有引起读者的足够重视的话,我们拿修改前的模块来对比一下:
# lsmod
Module                  Size  Used by
simp_blkdev         16784392  0
看出区别了没?

如果本章到这里还不结束的话,估计读者要开始闪人了。
好的,我们马上就结束,希望在这之前闪掉的读者不要太多。
由于还没有来得及闪掉而看到这段话的读者们,作者相信你们具有相当的毅力。
学习是需要毅力的,这时的作者同样也需要毅力来坚持完成这本教程。

最后还是希望读者坚持,坚持看完所有的章节,坚持在遇到每一个不明白的问题时都努力寻求答案,
坚持在发现自己感兴趣的内容时能够深入地去了解、探寻、思考。

<未完,待续>

[[i] 本帖最后由 OstrichFly 于 2008-11-26 09:31 编辑 [/i]]
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

基于案例的 SQL 优化实战训练营

讲师:中电福富特级专家梁敬彬,参与本次课程培训,你将收获:
1. 能编写出较为高效的 SQL;
2. 能解决70%以上的数据库常见优化问题;
3. 能得到老师提供的高效的相关工具和解决方案;
4. 能举一反三,收获不仅仅是 SQL 优化。
现在购票享受8.8折优惠!
----------------------------------------
优惠时间:2019年3月20日前

大会官网>>
  

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

清除 Cookies - ChinaUnix - Archiver - WAP - TOP