系统中能够随机访问特定大小数据片的设备被称作块设备,这些数据片就称作块。最常见的块设备是硬盘。注意,它们都是以安装文件系统的方式使用的——这也是块设备通常的访问方式。
另一种基本的设备类型是字符设备。字符设备按照字符流的方式被有序访问,像串口和键盘就属于字符设备。
这两种设备的根本区别在于它们是否可以被随机访问。内核管理块设备比字符设备复杂的多,有一个专门的子系统来管理块设备和对块设备的请求,这一部分在内核中被称作块I/O层。
我们在应用层对文件的读写访问,比如我们读写一个 1.txt,这个文件实际是存储在块设备上的,那么是谁完成了文件读写到具体块设备扇区的读写转换呢?是文件系统!我们内核中支持 vfat,ext2,ext3,yaffs2,jffs2 等非常多种文件系统,然而为了统一多种文件系统,内核抽象出了VFS虚拟文件系统,实际的文件系统向上为VFS提供统一的接口。比如一个 write 操作大体的操作流程如下图所示:

整个块设备的框架:

一、基本概念

块设备中最小的可寻址单元是扇区,扇区大小一般是2的整数倍,而最常见的大小是512字节。内核执行的所有磁盘操作都是按照块进行的,由于扇区是块设备的最小可寻址单元,所以块要比扇区2的整数倍,所以通常是 512 字节 、1k 、2k 。

和磁盘相关的其它术语还有——簇、柱面、磁头等,这些术语都和具体的块设备相关,一般情况下用户空间的软件都用不到这些概念。

二、缓冲区和缓冲区头

当一个块被调入内存时(也就是说,在读入后或等待写出时),它要存储在一个缓冲区中,每个缓冲区与一个块对应,它相当于是磁盘块在内存中的表示。由于内核在处理数据时需要一些相关的控制信息,比如属于哪一个块设备、块对应于哪个缓冲区等,所以内一个缓冲区都有一个对应的描述符,该描述符用 buffer_head 结构体表示,被称作缓冲区头。

struct buffer_head {
	unsigned long b_state;		/* 缓冲区状态标志 */
	struct buffer_head *b_this_page;/* 页面中的缓冲区 */
	struct page *b_page;		/* 存储缓冲区的页面 */
	sector_t b_blocknr;		/* 逻辑块号 */
	size_t b_size;			/* 块大小 */
	char *b_data;			/* 页面中的缓冲区 */
	struct block_device *b_bdev;    <span style="font-family: Arial, Helvetica, sans-serif;">/* 所属的块设备 */</span>
	bh_end_io_t *b_end_io;		/* I/O 完成方法 */
 	void *b_private;		/* reserved for b_end_io */
	struct list_head b_assoc_buffers; /* associated with another mapping */
	struct address_space *b_assoc_map;	/* mapping this buffer is
						   associated with */
	atomic_t b_count;		/* 引用计数 */
    与缓冲区对应的磁盘物理块由 b_blockbr 索引,该值是 b_bdev 所指明的块设备中的逻辑块号。 

    与缓冲区对应的内存物理页由 b_page 表示,另外,b_data 直接指向相应的块(它位于 b_page),块大小由 b_size 表示。

    缓冲区头的目的在于描述磁盘和物理内存缓冲区之间的映射关系。
    缓冲区头仅仅能描述一个缓冲区,当作为所有I/0操作的容器使用时,就会变成对多个 buffer_head 结构体进行操作,势必会造成不必要的负担和空间浪费,因此引入了一个灵活且轻量级的容器——bio 结构体。

三、bio 结构体

    每一个块 I/O 请求都通过一个 bio 结构体表示。每一个请求包含一个或者多个块,这些块存储在 bio_vec 结构体数组中,这些结构体描述了每一个片段在物理页面中的实际位置,并像向量一样被组织在一起。I/O 操作的第一个片段由 b_io_vec 结构体所指向,其他的片段在其后依次放置,共有 bi_vcnt 个片段。当块 I/O 层开始执行请求,需要使用各个片段时,bi_idx 会不断更新,从而总指向当前的片段。

struct bio {
//该bio结构所要传输的第一个(512字节)扇区:磁盘的位置
    sector_t bi_sector;
    struct bio *bi_next;     //请求链表
    struct block_device *bi_bdev;    //相关的块设备
    unsigned long bi_flags;    //状态和命令标志
    unsigned long bi_rw;     //读写
    unsigned short bi_vcnt;    //bio_vesc偏移的个数
    unsigned short bi_idx;     //bi_io_vec的当前索引
    unsigned short bi_phys_segments;    //结合后的片段数目
    unsigned short bi_hw_segments;    //重映射后的片段数目
    unsigned int bi_size; <span style="white-space:pre">	</span>      //I/O计数
    unsigned int bi_hw_front_size;    //第一个可合并的段大小;
    unsigned int bi_hw_back_size;    //最后一个可合并的段大小
    unsigned int bi_max_vecs;     //bio_vecs数目上限
    struct bio_vec *bi_io_vec;    //bio_vec链表:内存的位置
    bio_end_io_t *bi_end_io;//I/O完成方法
    atomic_t bi_cnt; //使用计数
    void *bi_private; //拥有者的私有方法
    bio_destructor_t *bi_destructor;//销毁方法
 
struct bio_vec {
	struct page	*bv_page;  // 这个缓冲区所在的物理页面
	unsigned int	bv_len;    // 这个缓冲区的大小
	unsigned int	bv_offset; // 缓冲区在页面的偏移
 四、请求队列 
     块设备将对它们的I/O操作请求保存在请求队列中,该队列由 request_queue 结构体表示。只要请求队列不为空,队列对应的块设备驱动程序就会从队列头获取请求,然后将其送入对应的块设备上去,请求队列中的每一项都是一个单独的请求,由 request结构体表示。 
     队列中的请求由结构体 request 表示,因为一个请求可能要操作多个连续的磁盘块,所以每个请求可以由多个 bio 结构体组成(每个 bio 结构体本身就可以描述多个块)。 
     磁盘寻址是整个计算机中最慢的操作之一,每一次寻址——定位硬盘磁头到特定块上的某个位置。为了优化寻址操作,内核会在提交 I/O 请求之前,先执行名为合并与排序的预操作。在内核中负责提交 I/O 请求的子系统被称作 I/O 调度程序。 
     I/O 调度程序通过两种方法来减少磁盘的寻址时间:合并与排序,合并指将两个或多个请求结合成一个新请求。如果之前队列中已经存在一个请求,它访问的磁盘扇区和当前请求访问的磁盘扇区相邻,比如同一个文件中早些时候被读取的数据区,那么这两个请求就可以合并成为一个单个对多个相邻磁盘扇区操作的新请求。和并请求可以明显减少对系统的开销和磁盘的寻址次数。 
     如果队列中没有可以合并的扇区,此时就无法将当前请求与其他请求合并,当然,可以将其插入到请求队列的尾部。 
     排序是指整个队列请求将按照扇区增长的方向有序排列,目的不仅是为了缩短单独一次的请求寻址时间,更重要优化在于通过保持磁头以直线方向移动,缩短了所有请求的磁盘寻址时间。该排列算法类似于电梯调度,所以 I/O 调度程序被称为电梯调度。 
    int minors;
/* 描述被磁盘使用的设备号的成员.一个驱动器必须使用最少一个次编号.
 *如果你的驱动会是可分区的,但是(并且大部分应当是),你要分配一个次编号给每个可能 的分区.次编号的一个普通的值是 16, 
 *它允许"全磁盘"设备盒 15 个分区. 一些磁盘驱动使用 64 个次编号给每个设备.
    char disk_name[32]; //应当被设置为磁盘驱动器名子的成员. 它出现在 /proc/partitions 和 sysfs.
    struct hd_struct **part; /* [indexed by minor] */
    struct block_device_operations *fops;// 设备操作集合.
    struct request_queue *queue;//被内核用来管理这个设备的 I/O 请求的结构;
    void *private_data;//块驱动可使用这个成员作为一个指向它们自己内部数据的指针.
    sector_t capacity;
// 这个驱动器的容量,以512-字节扇区来计.sector_t类型可以是64位宽.驱动不应当直接设置这个成员;相反,传递扇区数目给set_capacity.
    int flags;
// 一套标志(很少使用),描述驱动器的状态.如果你的设备有可移出的介质,
// 你应当设置GENHD_FL_REMOVABLE.CD-ROM驱动器可设置 GENHD_FL_CD. 
// 如果, 由于某些原因, 你不需要分区信息出现在 /proc/partitions, 设置 GENHD_FL_SUPPRESS_PARTITIONS_INFO.
    struct device *driverfs_dev; // FIXME: remove
    struct device dev;
    struct kobject *holder_dir;
    struct kobject *slave_dir;
    struct timer_rand_state *random;
    int policy;
    atomic_t sync_io; /* RAID */
    unsigned long stamp;
    int in_flight;
    #ifdef CONFIG_SMP
    struct disk_stats *dkstats;
    #else
    struct disk_stats dkstats;
    #endif
    struct work_struct async_notify;
 七、内核 ll_rw_block 函数调用 
 
分析ll_rw_block
        for (i = 0; i < nr; i++) {
            struct buffer_head *bh = bhs[i];
            submit_bh(rw, bh);
                struct bio *bio; // 使用bh来构造bio (block input/output)
                submit_bio(rw, bio);
                    // 通用的构造请求: 使用bio来构造请求(request)
                    generic_make_request(bio);
                        __generic_make_request(bio);
                            request_queue_t *q = bdev_get_queue(bio->bi_bdev); // 找到队列  
                            // 调用队列的"构造请求函数"
                            ret = q->make_request_fn(q, bio);
                                    // 默认的函数是__make_request
                                    __make_request
                                        // 先尝试合并
                                        elv_merge(q, &req, bio);
                                        // 如果合并不成,使用bio构造请求
                                        init_request_from_bio(req, bio);
                                        // 把请求放入队列
                                        add_request(q, req);
                                        // 执行队列
                                        __generic_unplug_device(q);
                                                // 调用队列的"处理函数"
                                                q->request_fn(q);
Linux三大驱动类型包括字符驱动、块驱动和网络驱动。 块设备是针对存储设备的,比如 SD卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等; 块驱动和字符驱动的区别如下: 1、字符设备是以字节为单位进行数据传输的,不需要缓冲; 2、块设备只能以块为单位进行读写访问,块是linux虚拟文件系统(VFS)基本的数据传输单位,块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,根据回写机制条件将缓冲区中的数 所以块设备子系统,上承文件系统,下承具体的储存设备子系统,对于下层的诸多设备进行统一的抽象,以向上提供统一的块设备,起作用如下:就块设备本身来说,可以分为三层,本专题是针对这三层进行学习,了解其基本的工作原理块设备通常是以数据块大小(如512字节)为单位,能随机访问的设备,典型的块设备是系统中的储存设备,例如:硬盘、闪储、U盘等。块设备按块进行划分,具体块大小由具体设备决定,通常为扇区(512字节)的整数倍。设备自身定义的访问数据块本书称之为物理数据块,块设备中文件系统还会以若干个物理数据块为单位划分逻辑数 目录一、正确理解块设备驱动的概念1、块设备和字符设备的差异2、块设备驱动的特点3、块设备相关的几个单位二、块设备驱动框架简介1、块设备驱动框图2、重点结构体三、块设备驱动案例分析1、块设备驱动案例演示2、块设备驱动简单分析3、源码分析 一、正确理解块设备驱动的概念 1、块设备和字符设备的差异 (1)块和字符是两种不同的访问设备的策略,而非指具体的设备 (2)同一个设备可以同时支持块和字符两种访问策略 (3)设备本身的物理特性决定了哪一种访问策略更适合 (4)块设备本身驱动层支持缓冲区,而字符设备驱动层没有缓 LINUX 驱动针对的对象是存储器和外设,而不是针对cpu内核。字符设备块设备网络设备字符设备指那些必须以串行顺序依次进行访问的设备,如触摸屏、鼠标等。块设备可以按任意顺序进行访问,以块为单位进行操作,如硬盘、eMMC等。字符设备和块设备的驱动设计有很大的差异,但对于用户(应用程序)而言,它们都使用文件系统的接口(open()、write()、read()、close())来进行访问和操作。在这里学习块设备驱动的基本架构,以及构建一个块设备驱动的基本步骤,最后通过内存模拟了一个块设备,并构建了其块设备驱动。 扇区(block)是驱动可以操作的最小单位,是磁盘级别的。一个磁盘扇区(sector)512个字节(现在有4K的了),扇区(sector)是磁盘的最小存储单位,  块(block)是文件系统层的,mkfs时可以设置块的大小.磁盘块(block)应该是类似FAT的簇大小的概念,是操作系统中分配磁盘容量的最小单位. 块(block)是数据存储的最小单位!   文件系统一般按照 Cluste * 要传输的第一个扇区 */ 4 struct bio * bi_next;/* 下一个 bio */ 5 struct block_device * bi_bdev;/* 状态、命令等 */ 7 unsigned long bi_rw;/* 低位表示 READ/WRITE,高位表示优先级*/ 8 struct bvec_iter bi_iter;/* 迭代器,标明数据要操作的块设备的位置 */ 9 unsigned short bi_vcnt;... 块设备驱动是Linux 三大驱动类型之一。块设备驱动要远比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统,本文重点学习一下块设备相关驱动概念,不涉及到具体的存储设备。 1. 什么是块设备? ​ 块设备是针对存储设备的,比如 SD 卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。因此块设备驱动其实就是这些存储设备驱动,块设备驱动相比字符设备 驱动的主要区别如下: ①、块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基 5.10.1.正确理解块设备驱动的概念 本节着重讲块设备驱动和字符设备驱动的差异,并且讲了扇区、块、页等块设备驱动中重要搞的概念。 5.10.2.块设备驱动框架简介 本节讲述块设备驱动的整体框架,先打通上下脉络再后面分析的时候就不会迷失。 5.10.3.块设备驱动案例分析1 本节开始块设备驱动案 read() 调用一个适当的 VFS 函数,将文件描述符和文件内的偏移量传递给它。 虚拟文件系统位于块设备处理体系结构的上层,提供一个通用的文件系统模型,Linux 支持的所有系统均采用该模型。 VFS 函数确定所请求的数据是否已经存在,如有必要,它决定如何执行 read 操作。 有时候没有... 1. 块设备概念块设备是指只能以块为单位进行访问的设备,块的大小一般是512个字节的整数倍。常见的块设备包括硬件,SD卡,光盘等。 2. 块设备驱动的系统架构 2.1 系统架构---VFS VFS是对各种具体文件系统的一种封装,用户程序访问文件提供统一的接口。 2.2 系统架构---Cache 当用户发起文件访问请求的时候,首先回到Disk Cache中寻址文件