第18章块设备驱动程序除了字符设备、网络设备外,Linux系统中还有块设备。字符设备和块设备在内核中的结构有很大的不同,总体来说,块设备要比字符设备复杂很多。块设备主要包含磁盘设备、SD卡等,这些设备是Linux系统中不可缺少的存储设备。计算机中都需要这样的设备来存储数据,所以学会块设备驱动程序的写法是非常重要的。18.1块设备简介本节对块设备的相关概念进行了简要的分析。理解这些概念对写块设备驱动程序具有十分重要的意义。18.1.1块设备总体概述Linux内核中,I/O设备大致分为两类:块设备和字符设备。块设备将信息存储在固定大小的块中,每个块都有自己的地址。数据块的大小通常在512字节到4K字节之间。块设备的基本特征是每个块都能独立于其它块而读写。磁盘就是最常见的块设备。在Linux内核中,块设备与内核其他模块的关系如图所示:虚拟文件系统通用块层I/O调度器块设备驱动程序磁盘/硬盘等磁盘文件系统系统调用预期算法(Anticipatory)最后期限算法(Deadline)完全公平算法(CFQ)Noop算法(NoOperation)通用磁盘(gendisk)请求结构体(request)块设备IO(bio)分区表(hd_struct)请求队列request_queue...18.1.2块设备的结构在写块设备驱动程序之前,了解典型块设备的结构是非常重要的。图显示的是磁盘的一个盘面,一些重要的概念将在下面讲述。10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%10%扇区磁道18.2块设备驱动程序的架构相对于字符设备来说,块设备的驱动程序架构要稍微复杂一些,其中涉及到很多重要的概念。对这些概念的理解是编写驱动程序的前提,本节将对块设备的整体架构进行详细讲解。18.2.1块设备加载过程在块设备的模块加载函数中,需要完成的一些重要工作,这些工作涉及到的一些重要概念,将在后面的小节中进行讲解,本节的目的是为了给出一个整体的概念。块设备驱动加载模块中需要完成的工作如下图所示:分配磁盘alloc_disk()注册设备register_blkdev()(可选)使用请求队列blk_alloc_queue()磁盘gendisk属性设置不使用请求队列blk_init_queue()激活磁盘add_disk()18.2.2块设备卸载过程在块设备驱动的卸载模块中完成与模块加载函数相反的工作。(1)使用del_gendisk()函数删除gendisk设备,并使用put_disk()函数删除对gendisk设备的引用。(2)使用blk_cleanup_queue()函数清除请求队列,并释放请求队列所占用的资源。(3)如果在模块加载函数中使用了register_blkdev()注册设备,那么需要在模块卸载函数中使用unregister_blkdev()函数注销块设备,并释放对块设备的引用。18.3通用块层通用块层是块设备驱动的核心部分,这部分主要包含块设备驱动程序的通用代码部分。本节将介绍通用块层的主要函数和数据结构。18.3.1通用块层通用块层是一个内核组件,它处理来自系统其他组件发出的块设备请求。换句话说,通用块层包含了块设备操作的一些通用函数和数据结构。图是块设备加载函数中用到的一些重要数据结构,如通用磁盘结构gendisk、请求队列结构request_queue、请求结构request、块设备I/O操作结构bio、块设备操作结构block_device_operations等。这些结构将在下面的几小节详细简述。分配磁盘alloc_disk()注册设备register_blkdev()(可选)使用请求队列blk_alloc_queue()磁盘gendisk属性设置不使用请求队列blk_init_queue()激活磁盘add_disk()gendiskblock_device_operationsrequest_queuegendiskrequestbio18.3.2alloc_disk()函数对应的gendisk结构体现实生活中有许多具体的物理块设备,例如磁盘、光盘等。不同的物理块设备其结构是不一样的,为了将这些块设备公用属性在内核中统一,内核开发者定义了一个gendisk结构体来描述磁盘。gendisk是generaldisk的简称,一般称为通用磁盘。18.3.3块设备的注册和注销为了使内核知道块设备的存在,需要使用块设备注册函数。在不使用块设备时,也需要注销块设备。块设备的注册和注销如下所述:1.注册块设备函数register_blkdev()2.注销块设备函数unregister_blkdev()18.3.4请求队列简单的讲,一个块设备的请求队列就是包含块设备I/O请求的一个队列。这个队列使用链表线性的排列。请求队列中存储未完成的块设备I/O请求,并不是所有的I/O块请求都可以顺利的加入请求队列中。请求队列中定义了自己能处理的块设备请求限制。这些限制包括:请求的最大尺寸、一个请求能够包含的独立段数、硬盘扇区大小等。18.3.5设置gendisk属性中的block_device_operations结构体在块设备中有一个和字符设备中file_operations对应的结构体block_device_operations。其也是一个对块设备操作的函数集合。下面对这个结构体的主要成员进行分析。1.打开和释放函数2.I/O控制函数3.介质改变函数4.使介质有效函数5.获得驱动器信息的函数6.模块指针18.4不使用请求队列的块设备驱动这里,有两个原因需要向读者介绍不使用请求队列的块设备驱动程序。第一个原因是,希望尽快的向读者展现一个完整的块设备驱动程序;第二个原因是,不使用请求队列的块设备驱动程序相对来说,比较简单。18.4.1不使用请求队列的块设备驱动程序的组成块设备函数驱动程序主要有一个加载函数、卸载函数和一个自定义的请求处理函数组成。本节将写一个虚拟的块设备驱动程序Virtual_blkdev。这个驱动程序在内存中开辟了一个8M的内存空间来模拟实际的物理块设备。这个块设备驱动程序代码比较简单,但功能却非常强大。对实际物理设备的操作命令同样可以应用在Virtual_blkdev这个块设备上,例如mkdir、mkesfs等命令。18.4.2宏定义和全局变量Virtual_blkdev块设备驱动中定义了一些重要的宏和全局指针,包括主设备号、设备名、设备的大小等。18.4.3加载函数Virtual_blkdev设备的加载函数主要完成分配磁盘、初始化请求队列、设置磁盘属性和激活磁盘的工作。18.4.4卸载函数Virtual_blkdev设备的卸载函数中主要完成与设备加载函数中相反的工作:(1)使用del_gendisk()函数删除gendisk设备。(2)使用put_disk()函数清楚gendisk的引用计数。(3)使用blk_cleanup_queue()函数清除请求队列。18.4.5自定义请求处理函数内核将I/O读写请求放入请求结构request中,并连接到请求队列request_queue中。因为Virtual_blkdev设备是一个基于内存的设备,可以随机读取数据,并不需要复杂的I/O调度(I/O调度的作用是对请求结构request进行排序,最大限度的提高读写速率)。所以当请求到来时,将直接使用blk_init_queue()函数中注册的请求处理函数Virtual_blkdev_do_request()函数,对请求进行实际的操作。这里的操作就是将数据赋值的Virtual_blkdev设备或者从Virtual_blkdev设备中读取数据。18.4.6驱动的测试为了了解Virtual_blkdev这个块设备的特性,需要对其进行各方面的测试,这些测试如下所述。1.编译Virtual_blkdev.c文件2.加载模块文件3.lsmod查看模块4.创建块设备文件5.在该设备上创建ext2文件系统6.挂载文件系统7.测试文件系统8.卸载和移除设备模块18.5I/O调度器Linux内核中,I/O调度器涉及到很多复杂的数据结构,而结构之间的关系又非常复杂。要精通这些知识,远非一章一节知识所能够达到。但本节力图给读者一个清晰的概念,随着内核的升级,这些概念可能有所细微的变化,但是其主要的原理是基本不会变化的。在详细讲解I/O调度器之前,需要知道数据是怎样从内存到达磁盘的。18.5.1数据从内存到磁盘的过程内存是一个线性的结构,Linux系统将内存分为页。一页最大可以是64K,但是目前主流的系统页的大小都是4K。现在假设数据存储在内存的相邻几页中,希望将这些数据写到磁盘上。那么每一页的数据会被先封装为一个段,用bio_vec表示。多个页会被封装成多个段,这些段被组成以一个bio_vec为元素的数组,这个数组用bio_io_vec表示。18.5.2块I/O请求(bio)数据从内存到磁盘或者从磁盘到内存的过程,叫做I/O操作。内核使用一个核心数据结构bio来描述I/O操作。1.bio结构体bio结构体包含一个块设备完成一次I/O操作所需要的一切信息。2.bio_vec结构体bio中的段用bio_vec结构体来表示。3.bio结构体的相关宏为了程序的可移植性,在写驱动程序时,不应该直接的操作bio结构和bi_io_vec数组,而应该使用内核开发者提供的一系列宏。由于在驱动中会使用这些宏,这里对其主要的宏进行介绍。18.5.3请求结构(request)几个连续的页面会组成一个bio结构,几个相邻的bio结构就会组成一个请求结构request。这样当磁盘在接收一个与request对应的命令,就不需要大幅度的移动磁头,这样就节省了I/O操作的时间。18.5.4请求队列(request_queue)每个块设备驱动程序都维护着自己的请求队列request_queue,其包含设备将要处理的请求链表。请求队列主要用来连接对同一个块设备的多个request请求结构。同时请求队列中的一些字段还保存了块设备所支持的请求类型信息、请求的个数、段的大小、硬件扇区数等与设备相关的信息。总之,内核负责对请求队列的正确配置,使请求队列不会给块设备发送一个不能处理的请求。18.5.5请求队列、请求结构、bio等之间的关系可能读者对请求队列request_queue、请求结构request、bio、bio_vec、gendisk等结构的关系还并不清楚,除了建议读者查阅内核源码外,认真查看图也是不错的方法。requestrq_diskqueuelistbiobio_tailrequest块设备gendiskqueuegendiskrequest_queuerequest_queuequeue_headrequestrequestrq_diskqueuelistbiobio_tailbiobi_nextbiobi_idxbio_io_vecbiobi_nextbiobi_idxbio_io_vecbiobi_nextbi_idxbio_io_vecbiobiobi_nextbi_idxbio_io_vecbiobio_vecpagebio_vecbv_lenbv_offsetbio_vecpagebio_vecbv_lenbv_offsetbio_vecpagebio_vecbv_lenbv_offsetbio_vecpagebio_vecbv_lenbv_offsetbiopagebv_lenbv_offset。。。bio_vec4K4K4K4K...4K4K4K内存...4K18.5.6四种调度算法对于像磁盘这样的块设备来说,是不能随机访问数据的。在访问实际的扇区数据以前,磁盘控制器必须花费很多时间来寻找扇区的位置,如果两个请求写操作在磁盘中的位置相离很远,那么写操作的大部分时间将花在寻找扇区上。所以内核需要提供一些