通用块层位于scsi的上层,文件系统的下层,系统主要的内存管理和读写优化都是在这里完成的。DIRECT_IO是跳过这一层的。这一层不是驱动,而是一种机制。其代码位于linux/block文件夹内,是单列出来的。
我们先不看代码,分析一下这一层都需要什么组件。
l 对磁盘的抽象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; };
struct request_queue *q; struct blk_mq_ctx *mq_ctx;
u64 cmd_flags; enum rq_cmd_type_bits cmd_type; unsigned long atomic_flags;
int cpu;
/* the following two fields are internal, NEVER access directly */ unsigned int __data_len; /* total data len */ sector_t __sector; /* sector cursor */
struct bio *bio; struct bio *biotail;
/* * The hash is used inside the scheduler, and killed once the * request reaches the dispatch list. The ipi_list is only used * to queue the request for softirq completion, which is long * after the request has been unhashed (and even removed from * the dispatch list). */ union { struct hlist_node hash; /* merge hash */ struct list_head ipi_list; };
/* * The rb_node is only used inside the io scheduler, requests * are pruned when moved to the dispatch queue. So let the * completion_data share space with the rb_node. */ union { struct rb_node rb_node; /* sort/lookup */ void *completion_data; };
/* * Three pointers are available for the IO schedulers, if they need * more they have to dynamically allocate it. Flush requests are * never put on the IO scheduler. So let the flush fields share * space with the elevator data. */ union { struct { struct io_cq *icq; void *priv[2]; } elv;
struct { unsigned int seq; struct list_head list; rq_end_io_fn *saved_end_io; } flush; };
struct gendisk *rq_disk; struct hd_struct *part; unsigned long start_time; #ifdef CONFIG_BLK_CGROUP struct request_list *rl; /* rl this rq is alloced from */ unsigned long long start_time_ns; unsigned long long io_start_time_ns; /* when passed to hardware */ #endif /* Number of scatter-gather DMA addr+len pairs after * physical address coalescing is performed. */ unsigned short nr_phys_segments; #if defined(CONFIG_BLK_DEV_INTEGRITY) unsigned short nr_integrity_segments; #endif
unsigned short ioprio;
void *special; /* opaque pointer available for LLD use */
int tag; int errors;
/* * when request is used as a packet command carrier */ unsigned char __cmd[BLK_MAX_CDB]; unsigned char *cmd; unsigned short cmd_len;
unsigned int extra_len; /* length of alignment and padding */ unsigned int sense_len; unsigned int resid_len; /* residual count */ void *sense;
unsigned long deadline; struct list_head timeout_list; unsigned int timeout; int retries;
/* * completion callback. */ rq_end_io_fn *end_io; void *end_io_data;
/* for bidi */ struct request *next_rq; }; struct list_head queuelist BI Organization on various internal queues
void *elevator_private I I/O scheduler private data
unsigned char cmd[16] D Driver can use this for setting up a cdb before execution, see blk_queue_prep_rq
unsigned long flags DBI Contains info about data direction, request type, etc.
int rq_status D Request status bits
kdev_t rq_dev DBI Target device
int errors DB Error counts
sector_t sector DBI Target location
unsigned long hard_nr_sectors B Used to keep sector sane
unsigned long nr_sectors DBI Total number of sectors in request
unsigned long hard_nr_sectors B Used to keep nr_sectors sane
unsigned short nr_phys_segments DB Number of physical scatter gather segments in a request
unsigned short nr_hw_segments DB Number of hardware scatter gather segments in a request
unsigned int current_nr_sectors DB Number of sectors in first segment of request
unsigned int hard_cur_sectors B Used to keep current_nr_sectors sane
int tag DB TCQ tag, if assigned
void *special D Free to be used by driver
char *buffer D Map of first segment, also see section on bouncing SECTION
struct completion *waiting D Can be used by driver to get signalled on request completion
struct bio *bio DBI First bio in request
struct bio *biotail DBI Last bio in request
struct request_queue *q DB Request queue this request belongs to
struct request_list *rl B Request list this request came from |
结构体的定义都是和功能相关的。由于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。
request_queue中有挂载的电梯算法的队列,并且还有为电梯算法服务的域。如last_merge表示上次合并的request。利用这个相当于cache,可以首先尝试看能不能与这个合并。因为连续数据的概率很大。
struct request_queue { struct list_head queue_head; struct request *last_merge; struct elevator_queue *elevator; int nr_rqs[2]; /* # allocated [a]sync rqs */ int nr_rqs_elvpriv; /* # allocated rqs w/ elvpriv */ struct request_list root_rl;
request_fn_proc *request_fn; make_request_fn *make_request_fn; prep_rq_fn *prep_rq_fn; unprep_rq_fn *unprep_rq_fn; merge_bvec_fn *merge_bvec_fn; softirq_done_fn *softirq_done_fn; rq_timed_out_fn *rq_timed_out_fn; dma_drain_needed_fn *dma_drain_needed; lld_busy_fn *lld_busy_fn;
struct blk_mq_ops *mq_ops;
unsigned int *mq_map;
/* sw queues */ struct blk_mq_ctx __percpu *queue_ctx; unsigned int nr_queues; struct blk_mq_hw_ctx **queue_hw_ctx; unsigned int nr_hw_queues; sector_t end_sector; struct request *boundary_rq; struct delayed_work delay_work;
struct backing_dev_info backing_dev_info; void *queuedata; unsigned long queue_flags; int id; gfp_t bounce_gfp; __queue_lock; spinlock_t *queue_lock; struct kobject kobj; struct kobject mq_kobj; unsigned long nr_requests; /* Max # of requests */ unsigned int nr_congestion_on; unsigned int nr_congestion_off; unsigned int nr_batching;
unsigned int dma_drain_size; void *dma_drain_buffer; unsigned int dma_pad_mask; unsigned int dma_alignment;
struct blk_queue_tag *queue_tags; struct list_head tag_busy_list;
unsigned int nr_sorted; unsigned int in_flight[2]; unsigned int request_fn_active;
unsigned int rq_timeout; struct timer_list timeout; struct list_head timeout_list; struct list_head icq_list; struct queue_limits limits; unsigned int sg_timeout; unsigned int sg_reserved_size; int node; unsigned int flush_flags; unsigned int flush_not_queueable:1; struct blk_flush_queue *fq;
struct list_head requeue_list; spinlock_t requeue_lock; struct work_struct requeue_work;
struct mutex sysfs_lock;
int bypass_depth; int mq_freeze_depth;
struct rcu_head rcu_head; wait_queue_head_t mq_freeze_wq; struct percpu_ref mq_usage_counter; struct list_head all_q_node;
struct blk_mq_tag_set *tag_set; struct list_head tag_set_list; }; |
这里的第一个元素是queue_head,是linux内核特殊的list定义方式,这种定义法可以把不同的结构体串成一个list,这里的list的第一个元素就是request_queue,后续的都是request。也就是说后面来的新的request都是添加到这个队列中的。
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的极限
struct queue_limits { unsigned long bounce_pfn; unsigned long seg_boundary_mask; unsigned int max_hw_sectors; unsigned int chunk_sectors; unsigned int max_sectors; unsigned int max_segment_size; unsigned int physical_block_size; unsigned int alignment_offset; unsigned int io_min; unsigned int io_opt; unsigned int max_discard_sectors; unsigned int max_write_same_sectors; unsigned int discard_granularity; unsigned int discard_alignment;
unsigned short logical_block_size; unsigned short max_segments; //本队列最多可放的物理segment数,在合并操作前要检查合并前队列的总物理段数+合并的物理段数是否超过这个数 unsigned short max_integrity_segments;
unsigned char misaligned; unsigned char discard_misaligned; unsigned char cluster; unsigned char discard_zeroes_data; unsigned char raid_partial_stripes_expensive; }; |
要实现电梯算法,需要知道电梯相关的元素:
l 每个电梯算法的具体函数作为一个函数表要定义struct elevator_type结构体
l 每个电梯算法都要有自己的队列组织(可以有多个队列),struct elevator_queue
核心的元素是以上两个。定义好了以上两个结构,使用elv_register注册elevator_type,将request_queue的elevator域赋值为定义的elevator_queue即可。如此,系统在处理request_queue调用电梯算法的时候就可以找到算法的数据和函数了。
要了解电梯算法的工作原理,具体的算法可以先略过,找到其框架流程更重要。这个流程函数是blk_queue_bio (struct request_queue *q, struct bio *bio)。一个参数是要插入的request队列,一个参数是传递下来的bio数据。
当然在这个函数之上,作为整个通用块层的提交请求的入口函数是void submit_bio(int rw, struct bio *bio)。
而submit_bio本质上是做一些统计记录之后就调用generic_make_request。generic_make_request的返回值不是使用函数返回值,而是使用bio本身提供的回调函数bio->bi_end_io。
这个函数的流程是:
l 检查bio
n 检查长度是否超过设备的最大sector
n 检查长度是否超出设备的队列长度
n 检查bio是基于分区的还是基于设备的,如果是基于分区的,改为基于设备的
u 再次检查是否超过设备的最大sector
n 检查处理bio的rw域的各种可能取值
n 尝试创建io_context(允许失败)
n 调用throtle接口看是否需要对bio进行限制,需要的话进行限制
l 将bio添加到设备的队列。如果设备队列当前为空,则直接处理该bio,而处理的时候如果发现队列又不为空了(即在处理的过程中有新的bio请求添加),则递归处理队列。
blk_queue_bio
实际的处理函数是blk_queue_bio。这个函数以设备的request_queue和要插入的bio作为参数,并且执行电梯算法。
l 执行bounce操作,就是在开启了bounce情况下,将上层提交来的bio拷贝一份再向下传递(可以支持重传),是否开启bounce,取决于宏CONFIG_BOUNCE
l 检查完整性测试是否可以通过。是否开启该功能取决于宏CONFIG_BLK_DEV_INTEGRITY
l 如果队列允许合并
n 调用blk_attempt_plug_merge。这个函数不是针对全部的request进行搜索合并,而是只针对要插入的bio搜索看有没有可以合并的的request,有的话只将该bio与该request合并。
l 如果队列不允许合并
n 执行电梯算法,执行前要锁定request_queue
由于两种路径都要进行合并,一种是添加的时候查找合并,另一种是电梯合并,而在电梯合并的时候要对队列进行锁定。而老版本的内核只有电梯合并一种路径。接下来将重点讨论电梯合并的情况:
el_ret = elv_merge(q, &req, bio); if (el_ret == ELEVATOR_BACK_MERGE) { if (bio_attempt_back_merge(q, req, bio)) { elv_bio_merged(q, req, bio); if (!attempt_back_merge(q, req)) elv_merged_request(q, req, el_ret); goto out_unlock; } } else if (el_ret == ELEVATOR_FRONT_MERGE) { if (bio_attempt_front_merge(q, req, bio)) { elv_bio_merged(q, req, bio); if (!attempt_front_merge(q, req)) elv_merged_request(q, req, el_ret); goto out_unlock; } } |
由于前置合并和后置合并类似,区别是后置合并要栋req->biotail,而前置合并只需要动bio,在动的方式又是一样的。所以这里只分析后置合并。
值得注意的是,这里进入电梯算法是在发现可以合并的情况下,如果不可以合并(前后都不可以),程序会继续向下执行。(代码为简化版)
req = get_request(q, rw_flags, bio, GFP_NOIO); //获得一个空闲的request结构体 init_request_from_bio(req, bio); //用bio初始化这个结构体 plug = current->plug; if (plug) { //如果现在队列处于plug状态,简单的添加 if (!request_count) trace_block_plug(q); else { if (request_count >= BLK_MAX_REQUEST_COUNT) { blk_flush_plug_list(plug, false); trace_block_plug(q); } } list_add_tail(&req->queuelist, &plug->list); blk_account_io_start(req, true); } else { //如果不是plug状态,就立即执行 spin_lock_irq(q->queue_lock); add_acct_request(q, req, where); //把request添加到队列q __blk_run_queue(q); //启动队列的执行 out_unlock: spin_unlock_irq(q->queue_lock); } |
add_acct_request(q,req, where)这个函数会调用电梯算法的elevator_add_req_fn,将request添加到电梯的队列。
elv_merge (struct request_queue *q,struct request **req, struct bio *bio)
这是电梯算法要执行的第一个函数。其首先尝试和queue->last_merge进行合并计算。如果不成功就hash搜索request_queue进行合并尝试。仍旧搜索不到才调用电梯算法计算。但是,这里很重要的是,这一步仅仅进行合并计算,也就是验证是否能够合并,具体的合并操作在blk_queue_bio函数中会根据elv_merge的返回值调用。
传入的3个参数分别是request队列,作为返回值的标示可以合并的request的,和传入的bio。也就是说如果在q中找到了可以合并bio的request,就将该request通过req传出。
这个函数的最后会调用电梯函数的elevator_merge_fn函数,看看电梯算法有没有合并的建议。电梯算法也只是计算看能不能按照电梯算法的需求合并,并不真正的进行合并。
可合并路径
bio_attempt_back_merge(structrequest_queue *q, struct request *req, struct bio *bio)
传入的参数是队列q,队列中要合并的req和要合并的bio。
l 由于每个request都有能携带的最大sector数。先判断如果合并是否会超出,会的话就返回,拒绝合并
l 计算更新bio中的域:bi_phys_segments。
l 计算bio的完整性检查是否通过(如果需要)
l 判断bio可以合并进req,执行req->nr_phys_segments+= bio-> bi_phys_segments;
l 进行IO数统计
elv_bio_merged (struct request_queue *q,struct request *rq,struct bio *bio)
此函数实际是调用电梯算法的elevator_bio_merged_fn函数。具体的内容执行与具体的电梯算法相关。电梯算法的分析稍后会进入。
我们可以看出,虽然之前有合并的数值计算,但是此处才是真正的合并方法。
attempt_back_merge (struct request_queue*q, struct request *rq)
首先调用电梯算法提供的elevator_latter_req_fn。由于此时rq是之前的bio要合并进入的request,这个函数的作用就是找到q中的下个request,然后将这两个req进行合并。这里的怎么找是电梯算法的具体规定。
但是合并之前可以做很多的检查,例如,现在是back_merge,就需要检查下个request的物理地址是否刚好在rq之后。还需要检查两个req的方向是否一致,所作用的目标设备是否一致。两个bio是否是同一个(有可能发生重传,但是这种情况目的地址就可以过滤掉,其实并不是必须的)。这里的合并参数调用了elv_merge_requests(elevator_merge_req_fn),也是电梯算法的函数。
可以合并就合并两个request的参数。例如sector数目,物理sector的数目等。然后执行真实的合并操作。最后再把已经执行完合并操作的request放入队列。
elv_merged_request
实际调用的是电梯算法的elevator_merged_fn函数。上一步理论上是已经完成了合并。这些看起来重复的步骤其实是给电梯算法提供更多的选择。但是这一步进入的条件是上一步返回0,也就是合并不成功。
比如,如果elevator_latter_req_fn不返回有效的request,这个函数就可以调用,而不用通用的合并框架代码。通用代码的最大缺点是只合并一个next,如果想要一次合并多个,就可以在这里实现,但是这种情况确实很少,因为每个request进来都会调用这个函数,除非新的bio可以导致两个本来不可合并的request相邻,否则一次的合并确实够用。
这里的进入条件并不是说上次attempt_back_merge合并失败才会进入,而是attempt_back_merge发现需要合并并且已经完成了自己的动作,才会进入这里。也就是说进入这里就意味着合并必须要进行了,这里只是在合并需要进行的条件下通知电梯算法,让其做出适当的内部调整。
不可合并路径
__elv_add_request
如果发现不可与已有的request合并,将实际的调用本函数。其插入位置有很多种:
ELEVATOR_INSERT_SORT(默认) 、ELEVATOR_INSERT_FLUSH、ELEVATOR_INSERT_REQUEUE、ELEVATOR_INSERT_FRONT、ELEVATOR_INSERT_BACK、ELEVATOR_INSERT_SORT_MERGE、ELEVATOR_INSERT_SORT、ELEVATOR_INSERT_FLUSH。我们只看第一种,这是大部分bio走的路径。
这一种首先将request的hash合并到电梯算法的哈希表,以让电梯算法可以见到这个请求的存在,然后调用电梯算法的q->elevator->type->ops.elevator_add_req_fn(q, rq); 进行实际的添加。
总结
由上文可以看到各个电梯函数在不同的时刻被调用,并且调用时很多电梯函数可以存在也可以不存在。
struct elevator_ops { elevator_merge_fn *elevator_merge_fn; elevator_merged_fn *elevator_merged_fn; elevator_merge_req_fn *elevator_merge_req_fn; elevator_allow_merge_fn *elevator_allow_merge_fn; elevator_bio_merged_fn *elevator_bio_merged_fn;
elevator_dispatch_fn *elevator_dispatch_fn; elevator_add_req_fn *elevator_add_req_fn; elevator_activate_req_fn *elevator_activate_req_fn; elevator_deactivate_req_fn *elevator_deactivate_req_fn;
elevator_completed_req_fn *elevator_completed_req_fn;
elevator_request_list_fn *elevator_former_req_fn; elevator_request_list_fn *elevator_latter_req_fn;
elevator_init_icq_fn *elevator_init_icq_fn; /* see iocontext.h */ elevator_exit_icq_fn *elevator_exit_icq_fn; /* ditto */
elevator_set_req_fn *elevator_set_req_fn; elevator_put_req_fn *elevator_put_req_fn;
elevator_may_queue_fn *elevator_may_queue_fn;
elevator_init_fn *elevator_init_fn; elevator_exit_fn *elevator_exit_fn; }; |
与判断是否可以合并相关的是elevator_merge_fn、elevator_merged_fn、 elevator_merge_req_fn三个函数。elevator_merge_fn用于判断是否可以合并,是向前合并还是向后合并,elevator_merged_fn是实际的更新request进行合并,如果是实际进行了合并操作,就会继续调用elevator_merged_fn,elevator_merged_fn是在确定了向前合并还是向后合并后调用的回调用来做本电梯算法内部数据的一些调整(根据是向前还是向后)
如果当前的queue正在执行电梯算法,该queue就会处于plug状态。处于该状态的queue不会被真正的发送出去。这也是电梯算法的意义,电梯算法在执行时队列是要被锁定的,自然队列中的request也不能交给下层处理。执行完毕电梯算法后会unplug,队列流水线才可以正常执行。
struct elevator_queue
{
structelevator_type *type;
void*elevator_data;
structkobject kobj;
structmutex sysfs_lock;
unsignedint registered:1;
DECLARE_HASHTABLE(hash,ELV_HASH_BITS);
};
每个电梯算法都有的队列。其中elevator_data存放电梯算法私有的数据,elevator_type存放电梯算法提供的操作。由于每个queue对应一个电梯算法,每个电梯算法对应一个elevator_queue结构体,所以这个结构体的存在就是为了电梯算法服务的。
最后定义了哈希表。这个哈希表是排序过的,以request计算出key,添加的部分是request->hash域。也就是说每个新来加入队列的request请求,其hash域都会被在这里添加。
当电梯算法执行时,电梯算法只需要考虑这个结构体。当添加一个新的request时,会将其首先添加到最后定义的hash标中,然后会调用电梯的elevator_add_req_fn,怎么样组织这些request取决于电梯算法的实现,但是都是组织在elevator_data中。这个结构每个电梯算法都可以自由定义其用途。
也就是说这里的request会被添加两次。hash的那次用于日后的方便检索,而elevator_data的那次用于服务于电梯算法。在电梯算法运行处理时,其处理的对象就是elevator_data中由自己存放的数据。
这部分是性能优化的关键。我们看一个最简单的noop方式。
static struct elevator_type elevator_noop = { .ops = { .elevator_merge_req_fn = noop_merged_requests, .elevator_dispatch_fn = noop_dispatch, .elevator_add_req_fn = noop_add_request, .elevator_former_req_fn = noop_former_request, .elevator_latter_req_fn = noop_latter_request, .elevator_init_fn = noop_init_queue, .elevator_exit_fn = noop_exit_queue, }, .elevator_name = "noop", .elevator_owner = THIS_MODULE, };
static int __init noop_init(void) { return elv_register(&elevator_noop); }
static void __exit noop_exit(void) { elv_unregister(&elevator_noop); } |
可以看出使用方法。一个结构体,然后启动时注册,关闭时解注册即可。
电梯算法有很多操作,这个noop定义的只是一部分。但是也是精简必须的。所以,我们考虑这种算法。
noop_init_queue
noop_init_queue函数定义这种算法如何安排它的request queue队列。内容就是生成elevator_queue结构体,并注册。
struct noop_data是noop算法挂载在电梯结构体上的私有数据,挂载在elevator_data,这个数据仅仅是个list。
struct noop_data{
struct list_head queue;
};
noop_add_request
这一步非常简单,仅仅是将request添加到电梯算法的队列elevator_data(noop_data)中。
noop_latter_request、noop_former_request
上文讲到,这个函数对应的电梯函数发生在bio合并进了request之后,寻找下一个可以跟已经合并的request进行合并的request。前后类似。这步发生的条件是bio可合并且已合并到已有的request。
其返回的next简单的是所请求的request的next。
noop_dispatch
这一步是把noop_data的第一个元素取出来,重新排序加入request_queue队列。排序的方法是sector的顺序。这是处理队列,在IO调度算法的执行结束后,需要实际的执行request。调度算法执行的时候该request不在电梯主程序的控制范围,但是调度算法执行结束该request就通过本函数归还主程序的request_queue队列。
noop_merged_requests
这一步是直接把next_request从上层的request_queue中删除。
static struct elevator_type iosched_deadline = { .ops = { .elevator_merge_fn = deadline_merge, .elevator_merged_fn = deadline_merged_request, .elevator_merge_req_fn = deadline_merged_requests, .elevator_dispatch_fn = deadline_dispatch_requests, .elevator_add_req_fn = deadline_add_request, .elevator_former_req_fn = elv_rb_former_request, .elevator_latter_req_fn = elv_rb_latter_request, .elevator_init_fn = deadline_init_queue, .elevator_exit_fn = deadline_exit_queue, },
.elevator_attrs = deadline_attrs, .elevator_name = "deadline", .elevator_owner = THIS_MODULE, }; |
这种算法也是比较简单的。算法的核心思想是request的sector临近的合并,并且保证不临近的都有一个适当的延时不至于饥饿,是标准的电梯算法。因为磁头移动的距离越短效率越高,但是总是如此移动就可能给远距离的请求带来饥饿。所以既要近距离移动磁头,又要保证远距离的请求不饥饿。要实现这个算法就需要两个结构体,一个rb_tree,一个fifo。rb_tree用来查找seoctor最靠近的request进行合并,而fifo用来拿到该要超时处理的接近饥饿的request。rb_tree中的节点是用request->__sector组织的。
所以,在添加操作时(deadline_add_request)会同时添加到rb_tree和fifo。在处理时也要根据超时检查也要兼顾处理两个结构。而在合并操作时则大部分是使用rb_tree。