参考linux API https://www.kernel.org/doc/htmldocs/kernel-api/
1. queue_flag_set_unlocked(QUEUE_FLAG_NOMERGES, ns->queue);
不允许对队列的request进行merge操作
2. blk_queue_virt_boundary(ns->queue, dev->page_size - 1);
to ensure there are no 'holes' in the presented
sg list (all segments in the middle of the list need to be of PAGE_SIZE).Setting virt_boundary_mask to PAGE_SIZE - 1 guarantees we'll never see
such holes
The block layer can reliably guarantee that SG lists won't
contain gaps (page unaligned) if a driver set the queue
virt_boundary.
With this setting the block layer will:
- refuse merges if bios are not aligned to the virtual boundary
- split bios/requests that are not aligned to the virtual boundary
- or, bounce buffer SG_IOs that are not aligned to the virtual boundary
理解是block层保证request bio中的数据是page对齐的,不对齐的话会分成多个request
对磁盘的抽象genhd.c和对分区的抽象:partition-generic.c和partitions目录下的文件
l 上层文件系统会把对文件访问转变为对多个sector的访问,这些sector很可能在内存中是分离的。所以需要一种数据表示方法,用来表示要读写的数据内容。这这个数据结构叫做bio(很奇怪的是这里为什么不直接使用scsi使用的scatterlist?)
l scsi相关
n 新的scsi标准有DIF/DIX的数据保护机制,无论对于读还是写的数据,都需要一个数据完整性的校验,由于在通用块层存储数据的结构体是bio,所以对其进行校验的文件叫做bio-integraty.c。这个文件完成的是与内存相关的设置,真正的算法在blk-integraty.c中定义的一系列钩子函数。不同的硬件会注册不同的计算方法供本层调用。也就是说,这里实际实现的是DIX协议。
n 本层要知道scsi的接口,本层定义了bsg(blockSCSI generic device)的v4接口,在bsg.c和bsg-lib.c
n t10保护的支持算法t0-pi.c,scsi的ioctl:scsi_ioctl.c
l 连接本层各个功能组件的核心程序:blk-core.c,还包括一些辅助文件实现些特定的周边。这一部分包括
n 内核执行这部分代码不是阻塞的,而是使用内核线程完成的,使用的是kblockd,其定义和相关功能位于blk-core.c中
n request的处理(bio只是数据的存储结构,但是一个命令请求不只有数据,还需要有其他控制和状态信息,这些信息和bio一起被组织到request中),但是要注意的是request和bio都只是本层的数据结构,request服务于电梯算法,bio用于盛放用户传进内核的数据
u 将用户数据映射到bio结构体的blk-map.c
u 将request中的bio数据映射到下层(scsi)使用的scatterlist结构体的处理程序blk-merge.c
u request如果超过了一定的时间需要被time out掉,代码在blk-timeout.c
u request请求到的数据在本层需要有缓冲,可以从中提取提交到上层上层所需要的数据,而丢弃或者缓存一部分上层没有要到的数据。这种行为叫做bounce,功能定义在bounce.c中
n 队列(queue)处理(对于块设备的一系列命令,需要队列缓存,并且这一层最重要的,队列中的各个命令有可能可以合并为一个,例如读取连续的数据的两个命令,由于每次存取数据的量越大,越节省时间,所以这一步是提高效率的关键)
u linux的设计者将对queue的插入执行操作单独的提取出来放到blk-exe.c中
u 对队列的属性进行设置的blk-settings.c
u 对队列中的request添加ID(tag),可以通过该tag直接找到该request,实现在blk-tag.c
u 凡是通信管道都要考虑流量控制问题。queue可以有多个来源,如果某个来源瞬间提交了过多的bio,那么其他来源的bio就可能饥饿。防止这种现象发生需要给队列针对某一个来源添加一个阈值,这个阈值的控制在blk-throttle.c
n 电梯算法接口。上一条说的合并多个request的操作,需要有合并的算法,合并的算法有很多,但是核心部分要为这些算法提供调用的接口函数
n 提交请求。当电梯算法被执行完,多个request和其对应的bio被合并,这个bio就需要被提交到下层(scsi的上层)去实际的执行发送。发送完毕还要执行回调。这部分代码也在这里提供。
l 电梯算法:电梯算法在queue上执行合并操作,是性能优化的关键。代码位于elevator.c,deadline-iosched.c,cfq-iosched.c,noop-iosched.c,还有提供优先级的ioprio.c
l 对于IO上下文的处理。IO上下文是在request上层的数据结构,如果说通用块层处理的request级别的数据结构,文件系统就是处理的IO上下文。而文件系统层次包括同步和异步两种数据模式,这里的IO上下文(io_context)主要是用在异步,异步IO在提交IO请求前必须要初始化一个IO上下文,一个IO上下文会包含多个request。通用块层对IO上下文的处理函数放在blk-ioc.c。
l 正常的逻辑是发送了IO命令,命令请求完毕后会调用回调函数。但是,通用块层允许poll操作,就是没有回调函数,请求执行完后需要用户手动查询和处理。这部分代码在blk-iopoll.c
l 对本层命令队列的处理可以有一个CPU,也可以有多个。如果多个,就需要对队列进行特殊的优化,叫mq,相关代码位于blk-mq.c、blk-mq-cpu.c、blk-mq-cpumap.c、blk-mq.h、blk-mq-sysfs.c、blk-mq-tag.c、blk-mq-tag.h中
l 内核处理命令的返回结果,在通用块层不可能是使用硬中断,所以这里的回调使用的是软中断,定义在blk-softirq.c
l 实现sysfs接口,定义在blk-sysfs.c,实现cgroup子系统的blk-cgroup.c
l 其他的辅助功能组件:将内容flush进磁盘的blk-flush.c、辅助函数blk-lib.c、用来解析磁盘信息返回值的cmdline-parser.c、提供ioctl接口的compat_ioctl.c,ioctl.c
l
从以上可以看出,这一部分的关键组件是:request、queue、bio、elevator和磁盘与分区的抽象。
如果要对bio进行数据完整性校验,需要调用bio_integraity_alloc给bio分配对应的空间,之后通过bio_integraty_add_page给bio添加额外的空间,用bio_free就会自动删除掉分配的空间。
具体的计算bip(dif)的算法由具体的驱动提供,驱动调用的是blk_integraty_register来注册自己的计算函数。
在文件系统中,可以通过/sys/block/<bdev>/integraty/目录下的write_generate和read_verify来控制是否执行读写校验。
大部分情况下,数据完整性对于文件系统是透明的,但上层的文件系统仍可以显示的使用DIX。在bio_integraty_enabled为1的情况下,上层调用bio_integrity_prep为bio准备bip。
磁盘设备在注册是可以生成blk_integrity结构体,体面就是存放具体的读写校验函数和tag的大小。
linux的通用快层对磁盘的抽象是gendisk结构体,该层以下的各种设备都是这个结构体的一种。例如scsi磁盘设备scsi_disk就是gendisk的一种。
对于分区的抽象是structpartition。
block_device_operations
对设备进行驱动
对设备进行指令操作的结构体是struct request,连接通用快层和下层设备指令操作的数据结构是bio,bio在request中,被上层识别,也被下层识别。
这部分描述当发现插入了磁盘或者是删除了磁盘时,内核是如何反应的。
BIO是通用块层表达数据的方式,其将用户传递进来的数据转换为bio存储,bio又包含进了request。多个bio可以组成链接,bio中内生提供链表结构。
struct bio { struct bio *bi_next; /*BIO 链表*/ struct block_device *bi_bdev; //文件系统层的块设备抽象 unsigned long bi_flags; /* status, command, etc */ unsigned long bi_rw; /* 标示是读还是写的标志位 */
struct bvec_iter bi_iter; unsigned int bi_phys_segments;
/* * To keep track of the max segment size, we account for the * sizes of the first and last mergeable segments in this bio. */ unsigned int bi_seg_front_size; unsigned int bi_seg_back_size;
atomic_t bi_remaining;
bio_end_io_t *bi_end_io; //BIO全部执行结束的回调函数
void *bi_private; unsigned short bi_vcnt; /* how many bio_vec's */ unsigned short bi_max_vecs; /* max bvl_vecs we can hold */ atomic_t bi_cnt; /* pin count */ struct bio_vec *bi_io_vec; /* the actual vec list */ struct bio_set *bi_pool;
/* * We can inline a number of vecs at the end of the bio, to avoid * double allocations for a small number of bio_vecs. This member * MUST obviously be kept at the very end of the bio. */ struct bio_vec bi_inline_vecs[0]; }; |
内核里一个bio有多个bio_vec,一个bio_vec叫一个segment。由于上层提交来的bio中的bio_vec,所以bio本身也是可以合并的。但是每个queue可以有标志位QUEUE_FLAG_NO_SG_MERGE控制是否允许bio的合并。如此,bio就有了两种统计口径:bi_vcnt表示bio没有经过自身合并的bio_vec数目,bi_phys_segments表示将物理连续的bio_vec算成一个后统计出来的段总数。这里需要注意的是:bio的段总数并不是单个bio的段的数目,而因为bio天生是个链表,所以段的数目总是统计的是链表中段的总数。
BIO_SEG_VALID:bi_phys_segments有了有效值后置这个标志位。
bio_flagged(bio,flag)用于检测bio的bi_flags域是否与flag相等。
request中包含了bio和其他参数,例如表明携带数据总大小的__data_len。用双下划线的域一般是不直接使用,而是要使用辅助函数调用,典型的是blk_rq_bytes(const struct request *rq)函数返回这个值,而
static inline unsigned intblk_rq_sectors(const struct request *rq)
{
returnblk_rq_bytes(rq) >> 9;
}
又可以返回这个request携带的sector的数目。
struct request {
struct list_head queuelist;
union {
struct call_single_data csd;
unsigned long fifo_time;
};
...}
结构体的定义都是和功能相关的。由于bio可以被合并进一个request,所以request要为这种功能提供支持。bio合并进request可以在原bio的前面合并也可能在后面。如果在前面,那么肯定是在最前面,此时直接利用bio本身的链表结构插入到最前面即可。如果在后面,也肯定是在最后面,但是此时没有使用bio本身的链表结构,而是使用了一个额外的域,叫biotail来盛放要合并进入的bio。因为这个域本身的定义就是用来放最后一个bio。向前合并最后一个bio不变,而向后合并最后一个bio要变化。
request中的域分为3类,分别用在3个不同的地方:驱动、通用块层、IO调度。
REQ_FLUSH:表示执行bio前进行fluash。REQ_FUA表示执行bio后进行flush。
QUEUE_FLAG_NO_SG_MERGE:表示是否允许bio本身的bio_vec进行物理合并。
这是通用块层的请求队列,这个队列一个cpu一个。上层的数据请求首先生成bio,然后由bio生成request,然后添加到request_queue,然后request_queue会被执行。这个执行包括很多步骤,最重要的是电梯算法。每个算法都会在全局的request_queue之外生成自己的队列结构体elevator_queue。
Queue的标志
#define QUEUE_FLAG_QUEUED 1 /*uses generic tag queueing */
#define QUEUE_FLAG_STOPPED 2 /*queue is stopped */
#define QUEUE_FLAG_SYNCFULL 3 /*read queue has been filled */
#define QUEUE_FLAG_ASYNCFULL 4 /*write queue has been filled */
#define QUEUE_FLAG_DYING 5 /*queue being torn down */
#define QUEUE_FLAG_BYPASS 6 /*act as dumb FIFO queue */
#define QUEUE_FLAG_BIDI 7 /*queue supports bidi requests */
QUEUE_FLAG_NOMERGES:直接不允许对队列的request进行merge操作
#define QUEUE_FLAG_SAME_COMP 9 /*complete on same CPU-group */
#define QUEUE_FLAG_FAIL_IO 10 /*fake timeout */
#define QUEUE_FLAG_STACKABLE 11 /*supports request stacking */
#define QUEUE_FLAG_NONROT 12 /*non-rotational device (SSD) */
#define QUEUE_FLAG_VIRT QUEUE_FLAG_NONROT /* paravirt device */
#define QUEUE_FLAG_IO_STAT 13 /*do IO stats */
#define QUEUE_FLAG_DISCARD 14 /*supports DISCARD */
#define QUEUE_FLAG_NOXMERGES 15 /*No extended merges */
#define QUEUE_FLAG_ADD_RANDOM 16 /*Contributes to random pool */
#define QUEUE_FLAG_SECDISCARD 17 /*supports SECDISCARD */
#define QUEUE_FLAG_SAME_FORCE 18 /*force complete on same CPU */
#define QUEUE_FLAG_DEAD 19 /*queue tear-down finished */
#define QUEUE_FLAG_INIT_DONE 20 /*queue is initialized */
#define QUEUE_FLAG_NO_SG_MERGE 21 /* don't attempt to merge SG segments*/
#define QUEUE_FLAG_SG_GAPS 22 /*queue doesn't support SG gaps */
#define QUEUE_FLAG_DEFAULT ((1 << QUEUE_FLAG_IO_STAT) | \
(1 << QUEUE_FLAG_STACKABLE) | \
(1 << QUEUE_FLAG_SAME_COMP) | \
(1 << QUEUE_FLAG_ADD_RANDOM))
#define QUEUE_FLAG_MQ_DEFAULT ((1 << QUEUE_FLAG_IO_STAT) | \
(1 << QUEUE_FLAG_SAME_COMP))
queue的极限