Linux Block IO IV

内核使用大量的缓存、异步、优化机制来提高IO性能,这一点Block IO子系统也不例外。以最常见的buffered IO为例,所有需要写入磁盘的数据起初都是存在Page Cache中,由文件系统来管理,显然,这样的IO是基于task_struct的,在当前版本中,内核使用task_struct->buffer_head来管理task_struct提交的IO请求。然后,借助write_back机制,在合适的时机将存储了task_struct IO数据的脏页下刷到磁盘,即write_back线程调用Block IO子系统提供的submit_bio()接口。然而,这样的bio也不是直接转换为CDB直接落盘,Block IO子系统也有自己的异步、优化机制,这就是本文要讨论的重点,具体的,IO路径中Block层路段有以下几个重要的话题:

  1. pluging /unpluging 机制
  2. IO Scheduler的优化策略
  3. Block层与SCSI层衔接

pluging/unpluging机制使通用块层的request批量下发到Blk-core层,并进一步下发到底层设备。这个机制说起来也简单,FS下发的bio描述了一个IO的源头与目的地,Block层想要对其进行优化,上层提交一次request就发送一次CDB显然是不行的。而要优化,无非两种前提:池(缓存)使用空间为优化提供机会,异步使用时间为优化提供机会。这里,数据已经在内核空间的主存中,再缓存一次显然是浪费资源,所以,为了优化,Block层使用了异步机制为优化创造条件,也就是pluging/unpluging机制。

具体地,每个设备都有自己的水位线:BLK_MAX_REQUEST_COUNT,表示该设备一次性可以处理的request数量,为了最大限度的提高硬件性能,同时为IO调度器提供优化的机会,绝大多数意欲下发到底层设备的request(IO barrier request除外)都会先进入到task_struct->plug->list缓存起来,待request数量足够的时候,一次性蓄在其中的request加入到elv中优化,再转变为CDB下发到设备,而这掌控蓄放的塞子plug,就由下发线程负责,每下发一个request,都会检查是否达到了警戒水位,如此,便实现了异步下发。对下发的bio/request来说,核心函数是blk_queue_bio().

pluging

submit_bio()	//blk-core.c
	generic_make_request(struct bio * bio)       //static私有方法
		bio_list_init()  //同一时刻只允许进程有一个active
		do{q->make_request_fn() }while(bio)	//blk_queue_bio()
			where = ELEVATOR_INSERT_SORT
			get_request()			//failed to merge, get a new request
			req=init_request_from_bio()		//根据bio初始化request
			if plug:						//塞子塞住的情况下
			list_add_tail(&req->queuelist, &plug->list)	//add request to plug->list

unpluging

submit_bio()	//blk-core.c
	generic_make_request(struct bio * bio)       //static私有方法
		bio_list_init()  //同一时刻只允许进程有一个active
		do{q->make_request_fn() }while(bio)	//blk_queue_bio()
			where = ELEVATOR_INSERT_SORT
			get_request()			//failed to merge, get a new request
			req=init_request_from_bio()		//根据bio初始化request
			if unplug
			add_acct_request(q, req, where)
				__elv_add_request(q, rq, where)
					ELEVATOR_INSERT_SORT
					elv_rqhash_add(q, rq)
					q->elevator->type->ops.elevator_add_req_fn(q, rq);  //noop_add_request
					list_add_tail(&rq->queuelist, &nd->queue);  //noop,nd->queue只是一个list_head
			__blk_run_queue()
				__blk_run_queue_uncond()
					q->request_fn(q)							//回调scsi_request_fn()
						blk_peek_request(q);
							__elv_next_request
								return list_entry_rq(q->queue_head.next)
								or elevator_dispatch_fn //noop_dispatch,.=>request_queue
							q->prep_rq_fn(q, rq)				//回调scsi_prep_fn() 
								cmd = scsi_get_cmd_from_req(sdev, req)
									cmd = scsi_get_command()
										cmd = __scsi_get_command()
											cmd = scsi_host_alloc_command()
												cmd = kmem_cache_zalloc()
										INIT_DELAYED_WORK()
										list_add_tail(&cmd->list, &dev->cmd_list)
									req->special = cmd  //双向引用
									cmd->request = req
									cmd->cmnd = req->cmd  
								scsi_setup_cmnd()
									scsi_setup_fs_cmnd()
										scsi_cmd_to_driver(cmd)->init_command(cmd);	//回调sd_init_command()
											sd_setup_discard_cmnd()			//根据request->cmd_flags 4选1
											sd_setup_write_same_cmnd()
											sd_setup_flush_cmnd()
											sd_setup_read_write_cmnd()		//针对普通读写设置scsi_cmnd->cmnd
						cmd = req->special
						cmd->scsi_done = scsi_done;
						scsi_dispatch_cmd(cmd)
							host->hostt->queuecommand(host, cmd)

IO Scheduler的优化策略

在一个IO的生命周期中,IO 调度器的作用主要体现在以下几个3个方面:

  1. 合并bio到现有请求队列已有的request
  2. 合并request到现有请求队列已有的request
  3. 对不能合并的request在其添加时进行重排等优化处理

下述接口是blk-core层为不同IO 调度器提供的接口,需要其根据需求来实现,本文不讨论这些IO调度器具体的实现,重点讨论这些接口含义以及在IO生命周期中的调用时机。

对于一个IO调度队列来说,其在内核中最能体现自己价值的方式就是对elevator_ops的实现。

typefielddescription
elevator_merge_fn*elevator_merge_fn查找request_queue中可以和传入bio合并的request,如果可以合并,返回值取ELEVATOR_FRONT_MERGE或ELEVATOR_BACK_MERGE,并将相应的request通过输入参数带出。如果不可合并,返回值为ELEVATOR_NO_MERGE
elevator_merged_fn*elevator_merged_fn在调度器有请求被合并时调用
elevator_merge_req_fn*elevator_merge_req_fn两个request被合并时调用
elevator_allow_merge_fn*判定bio可以被安全合并到现有request时调用
elevator_bio_merged_fn*elevator_bio_merged_fn
elevator_dispatch_fn*elevator_dispatch_fn*将准备好的request们移到派发队列request_queue
elevator_add_req_fn*elevator_add_req_fn向调度器中添加一个新请求时调用
elevator_activate_req_fn*elevator_activate_req_fn在块设备驱动首次看到一个请求时被调用,IO调度器可以用这个回调来确定请求执行从什么时候开始
elevator_deactivate_req_fn*elevator_deactivate_req_fn在块设备决定要延迟一个请求,把它重新排入队列时调用
elevator_completed_fn*elevator_completed_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
elevator_exit_icq_fn*elevator_exit_icq_fn
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为队列分配elv-specific的空间,是elv的私有数据
elevator_exit_fn*elevator_exit_fn
elevator_registered_fn*elevator_registered_fn

在IO过程中,主要的调用点如下:

submit_bio()	//blk-core.c
	generic_make_request(struct bio * bio)       //static私有方法
		bio_list_init()  //同一时刻只允许进程有一个active
		do{q->make_request_fn() }while(bio)	//blk_queue_bio()
			elv_merge() 			//attempt to merge bio to an existed request
			elv_bio_merged()        //elevator_bio_merged_fn();
			attempt_back_merge()
			elv_merged_request()    //elevator_merged_fn(); //attempt to merge request to an existed one
submit_bio()	//blk-core.c
	generic_make_request(struct bio * bio)       //static私有方法
		bio_list_init()  //同一时刻只允许进程有一个active
		do{q->make_request_fn() }while(bio)	//blk_queue_bio()
			where = ELEVATOR_INSERT_SORT
			if unplug
			add_acct_request(q, req, where)
				__elv_add_request(q, rq, where)
					ELEVATOR_INSERT_SORT
					elv_rqhash_add(q, rq)
					q->elevator->type->ops.elevator_add_req_fn(q, rq);  //noop_add_request
					list_add_tail(&rq->queuelist, &nd->queue);  //noop,nd->queue只是一个list_head
			__blk_run_queue()
				__blk_run_queue_uncond()
					q->request_fn(q)
						blk_peek_request(q);
							__elv_next_request
								return list_entry_rq(q->queue_head.next)
								or elevator_dispatch_fn //noop_dispatch,.=>request_queue

与SCSI 子系统接口

submit_bio()	//blk-core.c
	generic_make_request(struct bio * bio)       //static私有方法
		bio_list_init()  //同一时刻只允许进程有一个active
		do{q->make_request_fn() }while(bio)	//blk_queue_bio()
			__blk_run_queue()
				__blk_run_queue_uncond()
					q->request_fn(q)
						blk_peek_request(q);
							__elv_next_request
								return list_entry_rq(q->queue_head.next)
								or elevator_dispatch_fn //noop_dispatch,.=>request_queue
							q->prep_rq_fn(q, rq)	//scsi_prep_fn() or 
								cmd = scsi_get_cmd_from_req(sdev, req)
									cmd = scsi_get_command()
										cmd = __scsi_get_command()
											cmd = scsi_host_alloc_command()
												cmd = kmem_cache_zalloc()
										INIT_DELAYED_WORK()
										list_add_tail(&cmd->list, &dev->cmd_list)
									req->special = cmd  //双向引用
									cmd->request = req
									cmd->cmnd = req->cmd  
						cmd = req->special
						cmd->scsi_done = scsi_done;
						scsi_dispatch_cmd(cmd)
							host->hostt->queuecommand(host, cmd)

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.