一、概述
系统能够随机(不按顺序)的访问固定大小数据片的设备,被称为块设备。文件系统定义了块设备数据的格式,IO栈定义了传输块设备数据的流程。IO栈介于文件系统与块设备之间,起到承上启下的作用。IO层对文件数据请求进行转换、合并、排序、调度,然后发往块设备。
二、块设备相关概念
扇区:块设备中最小的可寻址单元是扇区。扇区大小一般是2的整数倍,而最常见的大小是512字节。扇区的大小是设备的物理属性,扇区是所有块设备的基本单元—块设备无法对比它还小的单元进行寻址和操作,不过许多块设备能够一次传输多个扇区。虽然大多数块设备的扇区大小都是512字节。
块索引(sector indices)在32或64位系统上的变量类型为sector_t。
块(block):对于硬件设备来说,扇区是基本的数据传输单元;而对于VFS(虚拟文件系统),块是基本的数据传输单元。例如,当内核访问文件的数据时,它首先从磁盘上读取一个块,这个块有文件的inode,该块对应于磁盘上一个或多个扇区。由于扇区是块设备的最小可寻址单元,所以块不能比扇区还小,只能整数倍于扇区大小。另外内核(对有扇区的硬件设备)还要求块大小是2的整数倍,而且不能超过一页的长度。所以对块大小的最终要求是,必须是扇区大小的2的整数倍,并且要小于页面大小。所以通常块大小是512字节、1K或4K。至于块(block)大小,与具体的硬件设备无关。在块设备上创建文件系统时,管理员可以选择块大小;因此在同一个磁盘上,多个分区可能使用不同的块大小。
三、通用块设备层(generic block IO layer)
通用块设备层(Generic Block Layer)是内核的一个组成部分,它处理系统所有对块设备的请求。
• 将数据存放在高端内存—当CPU访问高端内存的数据时,页就被临时映射到内核的线性地址空间, 然后解除映射。
• 实现零拷贝(zero-copy),即磁盘数据直接拷贝到用户地址空间,而不需要先拷贝到内核地址空间。实际上就是,内核进行I/O数据传输使用的页面被映射到用户进程的地址空间中。
• 管理逻辑卷,如LVM(Logical Volume Manager)和RAID(Redundant Array of Inexpensive Disks)。
• 使用现在的磁盘控制器新特性,如大的磁盘缓存、增强的DMA功能、板上I/O请求调度等。
3.1 缓冲区和缓冲区头(buffer and buffer_head)
当一个块被调入内存时(也就是,在读入或等待写出时),它要存储在一个缓冲区中。
每个块都要有自己的缓冲区(page cache),内核使用内存上的这块缓冲区来保存块数据。当内核从块设备上读取一个块时,就用从硬件上读取的数据填充块缓冲区;同理,当内核向块设备写数据时,就用块缓冲区中的数据写到块设备上。
由于内核在处理这些块数据时,需要一些相关的控制信息(比如块属于哪一个块设备,块对应于哪一个缓冲区等),所以每个缓冲区都有一个对应的描述符。Buffer_head正式扮演这个角色,从缓冲区到块的映射关系,其所描述块的状态(脏,干净,过期等),但是它并不与底层的块驱动程序打交道。具体的数据由bio结构体负责,后面会具体讲。
每个缓冲区都有一个缓冲区头(buffer head),描述类型为buffer_head,这个数据结构里包含了内核处理缓冲区所需要的信息;于是,在每个缓冲区上操作之前,内核都要先检查它的缓冲区头。buffer_head的定义在文件include/linux/buffer_head.h中
- buffer_head是磁盘块的一个抽象,一个buffer_head对应一个磁盘块,buffer_head中保存对应的磁盘号
2. buffer_head把page与磁盘块联系起来,由于page和磁盘块的大小可能不一样,所以一个page可能管理多个buffer_head
这里假设page大小4K,块大小为1K, buffer_head,page和磁盘块关系如下
b_state域表示缓冲区的状态,可以是表1中一种标志或多种标志的组合。合法的标志存放在bh_state_bits枚举中,其定义也在文件include/linux/buffer_head.h中。
3.2 bio结构体
目前内核中块I/O操作的基本容器由bio结构体表示,定义在<linux/bio.h>中。该结构代表了正在现场(活动)的以片段(segment)链表形式组织的块I/O操作。一个片段是一小块连续的内存缓冲区。通过片段来描述缓冲区,即使一个缓冲区分散在内存的多个位置上,bio结构体也能对内核保障I/O操作的执行。像这样的向量I/O就是所谓的聚散I/O。
bio结构体中最重要的几个域是bi_io_ves、bi_vcnt、和bi_idx。
总之,每一个块I/O请求都是通过一个bio结构体表示。每个请求包含一个或多个块,这些块储存在bio_vec结构体数组中。
buffer head用来管理每一个文件块,指向buffer page所包含的buffer;每个buffer包含几个连续的sector。每个块设备传输的request中包含至少一个bio,每个bio都包含几个bio_vec,每个bio_vec都包含一个segment(ulk3中这么称呼),每个segment包含几个连续的buffer。 要写盘时,通过buffer_head为bio结构体赋值,以保证io操作能正确执行。
简言之,buffer_head是用来管理buffer,而bio是用来传输buffer的。bio为通用层的主要数据结构,既描述了磁盘的位置,又描述了内存的位置,是上层内核vfs与下层驱动的连接纽带。
有几个重点:
第一:
一个BIO所请求的数据在块设备中是连续的,对于不连续的数据块需要放到多个BIO中。
第二:
一个BIO所携带的数据大小是有上限的,该上限值由bi_max_vecs间接指定,超过上限的数据块必须放到多个BIO中。
第三:
使用bio_for_each_segment来遍历 bio_vec
第四:
BIO、bi_io_vec、page之间的关系
迭代器 bvec_iter
寻遍整个 bio 发现居然没有携带需要下盘的扇区编号以及当前 bio 的大小,这个很尴尬,但确实如此,相当于 bio 作为一辆汽车,他携带了货物但是没告诉他目的地这不是完蛋了吗?不是,真正的目的地保存在这个 bvec_iter 中,作为一个迭代器,自然他的使命就是用来遍历 bvec,也就是 bio 数据区。那么他好比就是这辆 bio 汽车的货物分拣员,自然我的目的地不必贴到车上,直接告诉分拣员也是可以的,因为后面的事情可不是这辆汽车再做,而是分拣员需要逐个卸货的时候用。一起来看看迭代器长什么样?
struct bvec_iter {
sector_t bi_sector; /* device address in 512 byte sectors */
unsigned int bi_size; /* residual I/O count */
unsigned int bi_idx; /* current index into bvl_vec */
unsigned int bi_bvec_done; /* number of bytes completed in current bvec */
};
1. 块设备的上层抽象 - block_device
block_device是伪文件系统bdevfs中对块设备或设备分区的抽象,它唯一的对应于一个设备号(对分区来说,主设备号相同,次设备号不同)。
它的详细内容如下(include/linux/fs.h):
struct block_device {
dev_t bd_dev; /* 对应底层设备的设备号 */
int bd_openers; /* 该设备同时被多少进程打开 */
struct inode * bd_inode; /* 块设备的inod,可利用bd_dev通过bdget获得 */
struct super_block * bd_super; /* 文件系统的超级块信息 */
struct mutex bd_mutex; /* open/close mutex */
struct list_head bd_inodes;
void * bd_claiming;
void * bd_holder;
int bd_holders;
bool bd_write_holder;
#ifdef CONFIG_SYSFS
struct list_head bd_holder_disks;
#endif
/* 首先block_device既可以是gendisk的抽象,又可以是hd_struct(分区)的抽象
* 当作为分区的抽象时,bd_contains指向了该分区所属的gendisk对应的block_device
* 当作为gendisk的抽象时,bd_contains指向自身的block_device
*/
struct block_device * bd_contains;
unsigned bd_block_size; /* 块的大小 */
struct hd_struct * bd_part; /* 指向分区指针,对于gendisk,指向内置的分区0 */
/* number of times partitions within this device have been opened. */
unsigned bd_part_count; /* 该设备的所有分区同时被打开的次数 */
int bd_invalidated; /* 置1表示内存中的分区信息无效,下次打开设备时需要重新扫描分区表 */
struct gendisk * bd_disk; /* 通用磁盘抽象,当该block_device作为分区抽象时,指向该分区所属的gendisk,当作为gendisk的抽象时,指向自身 */
struct list_head bd_list;
/*
* Private data. You must have bd_claim'ed the block_device
* to use this. NOTE: bd_claim allows an owner to claim
* the same device multiple times, the owner must take special
* care to not mess up bd_private for that case.
*/
unsigned long bd_private;
/* The counter of freeze processes */
int bd_fsfreeze_count;
/* Mutex for freeze */
struct mutex bd_fsfreeze_mutex;
};
2. 通用磁盘描述 - gendisk
gendisk是对通用磁盘的一个描述,与真正的底层物理设备相关联。其详细内容如下(include/linux/genhd.h):
struct gendisk {
/* major, first_minor and minors are input parameters only,
* don't use directly. Use disk_devt() and disk_max_parts().
*/
int major; /* 主设备号 */
int first_minor; /* 第一个次设备号 */
/* 表示分区的个数,分区号从1开始,0表示gendisk本身 */
int minors; /* maximum number of minors, =1 for
* disks that can't be partitioned. */
char disk_name[DISK_NAME_LEN]; /* 磁盘的名称,用于在sysfs和/proc/partitions中表示该磁盘 */
char *(*devnode)(struct gendisk *gd, mode_t *mode);
unsigned int events; /* supported events */
unsigned int async_events; /* async events, subset of all */
/* Array of pointers to partitions indexed by partno.
* Protected with matching bdev lock but stat and other
* non-critical accesses use RCU. Always access through
* helpers.
*/
struct disk_part_tbl __rcu *part_tbl; /* 分区表 */
struct hd_struct part0; /* 用于表示gendisk本身 */
const struct block_device_operations *fops; /* 指向底层具体设备的操作函数,一般由用户驱动程序实现,如ramdisk驱动实现的fops为brd_fops */
struct request_queue *queue; /* 该disk关联的请求队列 */
void *private_data; /* 私有数据,用于提供给用户驱动程序使用 */
int flags;
struct device *driverfs_dev; // FIXME: remove
struct kobject *slave_dir;
struct timer_rand_state *random;
atomic_t sync_io; /* RAID */
struct disk_events *ev;
#ifdef CONFIG_BLK_DEV_INTEGRITY
struct blk_integrity *integrity;
#endif
int node_id;
};
3. 磁盘分区描述 - hd_struct
hd_struct用于描述一个具体的磁盘分区,其详细内容如下(include/linux/genhd.h):
struct hd_struct {
sector_t start_sect; /* 该分区的起始扇区号 */
sector_t nr_sects; /* 该分区的扇区个数,也就是分区容量 */
sector_t alignment_offset;
unsigned int discard_alignment;
struct device __dev;
struct kobject *holder_dir;
int policy, partno; /* 该分区的分区号 */
struct partition_meta_info *info;
#ifdef CONFIG_FAIL_MAKE_REQUEST
int make_it_fail;
#endif
unsigned long stamp;
atomic_t in_flight[2];
#ifdef CONFIG_SMP
struct disk_stats __percpu *dkstats;
#else
struct disk_stats dkstats;
#endif
atomic_t ref;
struct rcu_head rcu_head;
};
4. 三者之间的关系
如果你能读到我这篇文章,说明你应该很熟悉软件开发中的管理模式,下面我就类比软件开发的管理模式来说明三者之间的关系,可能不是十分贴切,但希望对你了解三者的关系能提供一些帮助。
block_device就相当于每个程序猿的档案信息(如姓名、电话、邮件、职位以及leader等等),gendisk相当于一个项目组,而hd_struct相当于项目组中的每一个程序猿。如何解释呢?^_^
想象一下,对于人力管理者(相当于VFS)来说,他其实并不关心底下干活的是哪个程序猿,是高富帅还是矮矬穷,是美女还是帅哥,他只需要知道你的档案信息(block_device)就可以了,因为只要有了你的档案信息,在需要你的时候就随时可以找到你(程序猿就是这么悲催)。
而对于一个项目组(gendisk)来说,里面一个或多个程序猿(hd_struct)。因为项目组至少有一个leader吧,而leader本质上也是一个程序猿(相当于struct hd_struct part0)。当一个项目比较庞大时,可能一个leader会带领多个兄弟(就像一个硬盘管理着多个分区),然而如果是一个迷你项目,可能只需要项目组leader一个人就搞定了(就像一个磁盘不进行分区)。
是不是有那么点意思?^_^
当人力管理需要找某个程序猿(hd_struct或者part0)时,只需要找到他的档案信息(block_device)就可以了,因为二者是一对一的关系,而且根据程序猿找到他的项目组(gendisk)是不是也是一件很容易的事情?同理,一旦找到了项目组(gendisk),那么里面的所有程序猿(hd_struct)是不是也非常明朗了?总之一句话,档案信息(block_device)充当了人力管理(相当于VFS)和项目组成员(gendisk、hd_struct)之间的桥梁。
不知道你有没有看明白,如果还有些混乱直接看下面的图好了:
struct request_queue {
struct list_head queue_head; //待处理请求的链表,请求队列中的请求用链表组织在一起
struct request *last_merge; //指向队列中首先可能合并的请求描述符
struct elevator_queue *elevator;//指向elevator对象的指针(电梯算法)
struct request_list root_rl;///为分配请求描述符所使用的数据结构
request_fn_proc *request_fn;//实现驱动程序的策略例程入口点的方法,由他处理队列中请求
make_request_fn *make_request_fn;//将一个新请求插入请求队列时调用的方法
prep_rq_fn *prep_rq_fn; //该方法把这个处理请求的命令发送给硬件设备
softirq_done_fn *softirq_done_fn;
rq_timed_out_fn *rq_timed_out_fn;
sector_t end_sector;
struct request *boundary_rq;
struct delayed_work delay_work;
struct backing_dev_info backing_dev_info;
/*
* The queue owner gets to use this for whatever they like.
* ll_rw_blk doesn't touch it.
*/
void *queuedata;
spinlock_t __queue_lock; //请求队列锁
spinlock_t *queue_lock; //指向请求队列锁的指针
unsigned long nr_requests;/* 请求队列中允许的最大请求数 */
struct queue_limits limits;//队列的其他限制
};