首页 > 代码库 > 【转】写一个块设备驱动(1)

【转】写一个块设备驱动(1)

原文地址:写一个块设备驱动

  一直对块设备驱动似懂非懂,这次发现了这个介绍块设备驱动很好的系列,打算把这套东西弄懂,一起跟着作者学习一遍

  作者写这个系列的初衷如下,我觉得很好,网上搜到的大部分都是介绍一些玄乎的东西,看完似懂非懂的~

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

 

概述

  在开始赵磊的教程之前,先对块IO子系统进行一个概述

  块设备(Block Device)是支持以固定长度的块为单位读写数据的存储设备的统称。块设备通常是支持随机访问和寻道的硬件设备,如磁盘、软盘、CDROM、内存区域等,或者是基于其他块设备之上的逻辑设备,如分区、MD、Device Mapper等。

  Linux内核中负责提交对块设备IO请求的子系统被称为块IO子系统,也被称为Linux块层。块IO子系统可以被分为下面三层:

  技术分享

 

  通用块层为各种类型的块设备建立了一个统一的模型,它主要的工作是接收上层发出的磁盘请求,并最终发出IO请求。该层隐藏了底层硬件块设备的特性,为块设备提供了一个通用的抽象视图。

  IO调度层:接收通用块层发出的IO请求,缓存请求并试图合并相邻的请求(如果请求在磁盘上面是相邻的),并根据设置好的算法,回调驱动层提供的请求处理函数,以处理具体的IO请求。

  块设备驱动层:具体的IO处理交给块设备驱动层来完成,视块设备的不同。对于大多数逻辑块设备,块设备驱动可能是一个纯粹的软件层,并不需要直接和硬件打交道,只是机械地重定向IO。对于SCSI块设备,其块设备驱动即为SCSI磁盘驱动,为SCSI子系统的高层驱动,从而将块IO子系统和SCSI子系统联系了起来。

  块IO子系统的一般IO处理流程是:上层调用通用块层提供的接口向块IO子系统提交IO请求,这些请求首先被放入IO调度层的调度队列,经过合并和排序,最终将转换后的IO请求派发到具体的块设备的等待队列,由后者的驱动进一步处理。这个过程涉及两种形式的IO请求:一种是通用块层的IO请求,即上层提交的IO请求,在Linux内核中以bio结构描述;另一种是块设备驱动层的IO请求,即经过IO调度层转换后的IO请求,在Linux内核中以request描述。

  IO简单来讲,就是将数据从磁盘读入内存或者从内存写入磁盘。但是,为了提升系统性能,块IO子系统采用了聚散IO(scatter/gather IO)这样一种机制:将对磁盘上连续,但内存中不连续的的数据访问由单次操作即可完成。也就是说,在单次操作中,从磁盘上的连续扇区中的数据读取到几个物理上不连续的内存空间或者将物理上不连续的内存空间的数据写入磁盘的连续扇区。前者叫分散读,后者叫聚集写。

  上层向通用块层提交的IO请求是基于聚散IO的,它包含多个“请求段(segment)”,一个“请求段”是一段连续的内存区域,其中包含了和其他“请求段”处于连续扇区的数据。

 

  如果说SCSI磁盘驱动是连接块IO子系统和SCSI子系统之间的桥梁,那么也可以这样认为:块设备是联系块IO子系统和文件系统之间的纽带。bio表示上层发给通用块层的请求,称为通用块层请求,它关注的是请求的应用层面,即读取(或写入)哪个块设备,读取(或写入)多少字节的数据,读取(或写入)到哪个目标缓冲区等、request表示通用块层为底层块设备驱动准备的请求,称作块设备驱动层IO请求,或块设备驱动请求,它关注的是请求的实施层面,即构造哪种类型的SCSI命令。

  一个块设备驱动层请求可能包含多个通用块层请求,也就是说,一次SCSI命令可以服务多个上层请求,这就是所谓的请求合并。在Linux内核实现中,请求合并就是将多个bio链入到同一个request。此外,块IO子系统还涉及不同的请求队列,包括IO调度队列和派发队列。IO调度队列是块IO子系统用于对通用块层请求进行合并和排序的队列。派发队列是针对块设备驱动的,即块IO子系统严格按照队列顺序提交块设备驱动层请求给块设备驱动处理。一般来说,每个块设备都有一个派发队列,IO子系统又为它内部维护了一个IO调度队列,不同的块设备可以采用不同的IO调度算法。

  通用块层请求到达块IO子系统时,首先在IO调度队列中进行合并和排序,变成为块设备驱动层的请求。之后块设备驱动层请求按照特定的算法被转移到派发队列,从而被提交到块设备驱动。在Linux内核中,IO调度队列和派发队列都反应在request_queue结构中。

   

第一章

本章的目的用尽可能最简单的方法写出一个能用的块设备驱动。
所谓的能用,是指我们可以对这个驱动生成的块设备进行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.oKDIR := /lib/modules/$(shell uname -r)/buildPWD := $(shell pwd)default:    $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modulesclean:    $(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

现在这个块设备的使用情况是
# 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

【转】写一个块设备驱动(1)