块设备驱动程序提供对面向块的设备的访问,这种设备以随机访问的方式传输数据,并且数据总是具有固定大小的块。典型的块设备是磁盘驱动器,也有其它类型的块设备。
块设备和字符设备有很大区别。比如块设备上可以挂载文件系统,字符设备不可以。这是随机访问带来的优势,因为文件系统需要能按块存储数据,同时更需要能随机读写数据。
另外数据经过块设备相比操作字符设备需要多经历一个数据缓冲层,应用程序与块设备传递数据时不同于操作字符设备那样直接打交道,而必须经过一个中间缓冲层来存储数据,然后才可以使用数据。
追根溯源,提高系统整体性能(吞吐量)是其根本原因。系统运行快慢受文件系统访问速度直接影响,而文件系统的访问行为是大量无序的,而且常常会重复地访问请求。无序访问请求会让磁头不断地改变方向(比直线运动费时的多);重复访问又会使得上次读出的数据再被读取(上次的结果被白白浪费了)。
为解决上述两个弊端,内核对块设备访问引入缓冲层。缓冲层的作用一是作为数据缓冲区,存储已取得的数据,以便加快访问速度——如果需要从块设备读取的数据已经在缓冲区中,则使用缓冲区中的数据,避免了耗时的设备操作和I/O操作;二是将对块设备的I/O访问按照访问扇区的位置进行了优化排序,尽量保证访问时磁头做直线移动——这在操作系统中被称为电梯调度算法。
块设备驱动程序的接口要稍微复杂一些,原因有两个:一是因为历史——块驱动程序接口从Linux第一个版本开始就一直存在于每个版本中,并且已经证明很难修改或改进;其二是因为性能,一个慢的字符设备驱动程序虽不受欢迎,但仍可以接受,但一个慢的块驱动程序将影响整个系统的性能。因此,块驱动程序的接口设计经常受到速度要求的影响。
利用一个示例驱动程序(Mysbd)阐述块驱动程序的创建。Mysbd实现一个使用系统内存的块设备,从本质上讲,属于一种RAM磁盘驱动程序,由于块驱动程序复杂性,Mysbd只给出实现块驱动程序的框架。
Mysbd作为一个内存块设备,对其设备结构的定义如下:
一、块设备驱动程序的注册
和字符驱动程序一样,内核使用主设备号来标识块驱动程序,但块主设备号和字符主设备号互不相干。一个主设备号为32的块设备和具有相同主设备号的字符设备可以同时存在,因为它们具有各自独立的主设备号分配空间。
注册和注销块设备驱动程序的函数如下所示:
备注:上述函数中的参数意义和字符设备几乎相同,而且可以通过一样的方式动态赋予主设备号。注册Mysbd的程序代码如下:
register_blkdev()使用block_device_operations结构的指针,该结构是块驱动程序接口,其定义如下:
Mysbd使用的bdops接口定义如下:
注意,block_device_operations接口中没有读或者写操作。所有涉及块设备的I/O通常由系统进行缓冲处理,用户进程不会对这些设备执行直接的I/O操作。在用户模式下对块设备的访问,通常隐含在对文件系统的操作当中,而这些操作能够从I/O缓冲当中获得明显的好处。但是,对块设备的直接I/O访问,比如在创建文件系统时的I/O操作,也要通过Linux的缓冲区缓存。内核为块设备提供了一组单独的读写函数generic_file_read()和generic_file_write(),驱动程序不必理会这些函数。
块驱动程序最终必须提供完成实际块I/O操作的机制。在Linux中,用于这些I/O操作的方法称为request。request方法同时处理读取和写入操作,要复杂一些。
在块设备的注册过程中,必须告诉内核实际的request方法。该方法不在block_device_operations结构中指定(出于历史和性能两方面的考虑),相反,该方法和用于该设备的挂起I/O操作队列关联在一起。默认情况下,对每个主设备号并没有这样一个对应的队列。块设备驱动程序必须通过blk_init_queue初始化这一队列。队列的初始化和清除接口定义如下:
blk_init_queue()函数建立请求队列,并将该驱动程序的request()函数(通过第二个参数传递)关联到队列。在模块的清除阶段,调用blk_cleanup_queue()函数。Mysbd驱动程序使用如下代码初始化它的队列:
blk_init_queue(BLK_DEFAULT_QUEUE(major), Mysbd_request);
每个设备都有一个默认使用的请求队列,必要时,可使用BLK_DEFAULT_QUEUE宏得到该默认队列。这个宏在blk_dev_struct结构形成的全局数组(该数组名为blk_dev)中搜素得到对应的默认队列。blk_dev数组由内核维护,并可通过主设备号索引。blk_dev_struct结构定义如下:
request_queue成员包含了初始化之后的I/O请求队列。还有指向queue_proc函数的指针,当这个指针非空时,就调用这个函数来找到具体设备的请求队列,这是为具有同一主设备号的多种同类设备而设的一个成员。data成员可由驱动程序使用,以便保存一些私有数据,但很少有驱动程序使用该成员。
图9.7描述注册和注销一个块驱动程序模块时所要用的函数及数据结构的关系图。
(1)加载模块时,insmod命令调用init_module()函数,该函数调用register_blkdev()和blk_init_queue()分别进行驱动程序的注册和请求队列的初始化。
(2)register_blkdev()把块驱动程序接口block_device_operations加入blkdevs[]表(数组)中。
(3)blk_init_queue()初始化一个默认的请求队列,将其放入blk_dev[]表中,并将该驱动程序的request()函数关联到该队列。
(4)卸载模块时,rmmod命令调用cleanup_module()函数,该函数调用unregister_blkdev()和blk_cleanup_queue()分别进行驱动程序的注销和请求队列的删除。
二、块设备请求
在内核安排一次数据传输时,它首先在一个表中对该请求队列,以最大化系统性能为原则进行排序。然后,请求队列被传递到驱动程序的request()函数,该函数的原型如下:
void request_fn(request_queue_t *queue);
块设备的读写操作都是由request()函数完成。对于具体的块设备,函数request()不同。所有的读写请求都存储在request结构的链表中。内核定义request结构,其定义如下:
内核定义一个CURRENT宏,它是一个指向当前请求的指针,request()函数利用该宏检查当前的请求。
备注:Mysbd只是一个RAM磁盘,因此,该设备的数据传输只是一个memcpy调用。
块设备驱动程序初始化时,由驱动程序init()完成。为引导内核时调用init(),需要在blk_dev_init()函数中添加Mysbd_init()。
总结:块设备驱动程序初始化的工作主要包括如下几点:
1)检查硬件是否存在。
2)登记主设备号。
3)利用register_blkdev()函数对设备进行注册。
4)将块设备驱动程序的数据容量传递给缓冲区。
5)将request()函数的地址传递给内核。
blk_dev[Mysbd_MAJOR].request_fn = DEVICE_REQUEST;
实际块设备驱动程序比较复杂,关于驱动程序更详细的内容请参考<<Linux设备驱动程序>>。