Linux 驱动之块设备 (一)

1、引言

块设备的驱动比字符设备的难,这是因为块设备的驱动和内核的联系进一步增大,但是同时块设备的访问的几个基本结构和字符还是有相似之处的。

有一句话必须记住:对于存储设备(硬盘~~带有机械的操作)而言,调整读写的顺序作用巨大,因为读写连续的扇区比分离的扇区快。

但是同时:SD卡和U盘这类设备没有机械上的限制,所以像上面说的进行连续扇区的调整显得就没有必要了


2、块设备概念

块设备(blockdevice)

--- 是一种具有一定结构的随机存取设备,对这种设备的读写是按进行的,他使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性读到缓冲区。

字符设备(Character device)

---是一个顺序的数据流设备,对这种设备的读写是按字符进行的,而且这些字符是连续地形成一个数据流。他不具备缓冲区,所以对这种设备的读写是实时的

 

扇区(Sectors):任何块设备硬件对数据处理的基本单位。通常,1个扇区的大小为512byte。(对设备而言)

块  (Blocks):由Linux制定对内核或文件系统等数据处理的基本单位。通常,1个块由1个或多个扇区组成。(对Linux操作系统而言)

段(Segments):由若干个相邻的块组成。是Linux内存管理机制中一个内存页或者内存页的一部分。

页、段、块、扇区之间的关系图如下:


3、怎么建立一个可用的硬盘这类块设备

为了建立一个可用的块设备,我们需要做......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;

   }

这里的alloc_disk函数是在内核中实现的,它后面的参数1代表的是使用次设备号的数量,这个数量是不能被修改的。

还有别忘了在卸载模块的代码中也加一个行清理函数:

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。


strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME); //宏定义simp_blkdev
        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); //宏定义(16*1024*1024),实际上就是这个结构体。

看到这里,是不是有种身陷茫茫大海的无助感?并且一波未平,一波又起,在搞定这4个问号的同时,居然又引入了simp_blkdev_do_request函数!
为了理清思路,我们把目前为止涉及到的代码整理出来:
#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()。

在说等待队列之前先要明确几个概念:

①用户希望对硬盘数据做的事情叫做请求,这个请求和IO请求是一样的,所以IO请求来自于上层
每一个IO请求对应内核中的一个bio结构

IO调度算法可以将连续的bio(也就是用户的对硬盘数据的相邻簇的请求)合并成一个request
多个request就是一个请求队列,这个请求队列的作用就是驱动程序响应用户的需求的队列。


下面先说一下硬盘这类带有机械的存储设备的驱动。

这类驱动中用户的IO请求对应于硬盘上的簇可能是连续的,可能是不连续的,连续的当然好,如果要是不连续的,那么IO调度器就会对这些BIO进行排序(例如老谢说的电梯调度算法),合并成一个request,然后再接收请求,再合并成一个request,多个request之后那么我们的请求队列就形成了,然后就可以向驱动程序提交了。

请求队列具体分析先不讲,放到下一篇博客分析首先我们看看究竟把块设备的数据以什么方式放在内存中。
我们使用最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()函数为:

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


编码到此结束,然后我们试试这个程序:
首先编译:
# make
#
加载模块
# 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
#

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。