天天看點

Linux 塊裝置驅動分析(二)塊IO請求的處理過程電梯排序

Linux 塊裝置驅動分析(一)

Linux 塊裝置驅動分析(二)

Linux 塊裝置驅動分析(三)

塊IO請求的處理過程

頁高速緩存(page cache)是Linux核心實作磁盤緩存,它主要用來減少對磁盤的I/O操作。具體地講,是通過把磁盤中的資料緩存到實體記憶體中,把對磁盤的通路變為對實體記憶體的通路。

如讀取一個檔案,首先會先檢查你讀的那一部分檔案資料是否在高速緩存中,如果在,則放棄通路磁盤,而直接從記憶體中讀取,如果不在,則去磁盤中讀取。

對于寫操作,應用如果是以非SYNC方式寫的話,寫的資料也隻是進記憶體,然後由核心幫忙在适當的時機回寫進硬碟。

記憶體的頁最終是要轉化為硬碟裡面真實要讀寫的位置的。在Linux裡面,用于描述硬碟裡面要真實操作的位置與page cache頁的映射關系的資料結構是bio,定義如下:

struct bio {
	struct bio		*bi_next;	
	struct block_device	*bi_bdev;     //塊裝置
	struct bvec_iter	bi_iter;      //磁盤位置資訊等
	unsigned short		bi_vcnt;	  //bio_vec數組的大小
	struct bio_vec		*bi_io_vec;	  //bio_vec數組
	
	......

};

struct bvec_iter {
	sector_t		bi_sector;	     //塊I/O操作在磁盤上的起始扇區
	unsigned int	bi_size;	     //還沒有傳輸的位元組數

	unsigned int	bi_idx;		     //bio_vec數組的目前索引

	unsigned int    bi_bvec_done;	//bi_io_vec[bi_idx]完成的位元組數
}

struct bio_vec {
	struct page		*bv_page; //該bio_vec對應的page描述符
	unsigned int	bv_len;   //資料大小
	unsigned int	bv_offset; //資料在頁面中的偏移
};
           

每個bio對應的硬碟裡面一塊連續的位置 ,可能對應着page cache的多頁,或者一頁,是以它裡面會有一個bio_vec數組。

Linux 塊裝置驅動分析(二)塊IO請求的處理過程電梯排序

檔案系統構造好bio之後,調用通用塊層提供的submit_bio函數,向通用塊層送出I/O請求。bio進入通用塊層後,将會由I/O排程層進行合并等操作,所謂I/O請求合并就是将程序内或者程序間産生的在實體位址上連續的多個IO請求合并成單個IO請求一并處理,進而提升IO請求的處理效率。

比如有bio1(對應磁盤A的第100-101塊,寫操作)和bio2(對應磁盤A的第102-103塊,寫操作),那麼這兩個bio是可以合并為一個request。

Linux 塊裝置驅動分析(二)塊IO請求的處理過程電梯排序

submit_bio函數定義如下:

blk_qc_t submit_bio(struct bio *bio)
{
	
	......

	//generic_make_request函數會調用request_queue的make_request_fn成員函數,制造請求
	return generic_make_request(bio)
}
           

塊裝置有個請求隊列:

struct gendisk {
	
	......
	struct request_queue *queue; //請求隊列
	......
};


struct request_queue {
	......
	struct elevator_queue	*elevator;     // I/O電梯排程器
	......
	request_fn_proc		*request_fn;       //處理request的回調函數
	make_request_fn		*make_request_fn;  //make_request_fn回調函數
	......
};
           

顯然,請求隊列的make_request_fn回調函數是由塊裝置驅動設定的。一般,驅動程式會調用blk_init_queue函數申請并初始化一個請求隊列,其make_request_fn回調函數會設定成blk_queue_bio。我們就分析這個函數:

static blk_qc_t blk_queue_bio(struct request_queue *q, struct bio *bio)
{
	struct blk_plug *plug;
	int el_ret, where = ELEVATOR_INSERT_SORT;
	struct request *req;
	unsigned int request_count = 0;

	
	
	if (!blk_queue_nomerges(q)) {

		//嘗試與程序本地的plug隊列裡的request合并
		if (blk_attempt_plug_merge(q, bio, &request_count, NULL))
			return BLK_QC_T_NONE;
	} else
		request_count = blk_plug_queued_count(q); 

	/* request_count儲存着程序本地的plug隊列的request數目 */

	spin_lock_irq(q->queue_lock);

	/* 不能合并到plug隊列裡的request,則嘗試合并到I/O電梯排程器(request_queue->elevator_queue)
     的排程隊列
   */
	el_ret = elv_merge(q, &req, bio);
	......

get_rq:
	//都不能合并的話,産生一個新的request
	req = get_request(q, bio->bi_opf, bio, GFP_NOIO);
	......

	//通過bio初始化這個新的request
	init_request_from_bio(req, bio);

	......

	plug = current->plug;
	if (plug) {

		if (!request_count)
			trace_block_plug(q);
		else {
			/* 如果程序本地的plug隊列的request數目達到BLK_MAX_REQUEST_COUNT(16)個
         則把程序本地的plug隊列的request洩洪到I/O電梯排程器的排程隊列
       */
			if (request_count >= BLK_MAX_REQUEST_COUNT) {
				blk_flush_plug_list(plug, false); //洩洪
				trace_block_plug(q);
			}
		}
		//把新的request插入到程序本地的plug隊列
		list_add_tail(&req->queuelist, &plug->list);
		blk_account_io_start(req, true);
	} else {
		......
	}

	return BLK_QC_T_NONE;
}
           

來個整體的流程圖:

Linux 塊裝置驅動分析(二)塊IO請求的處理過程電梯排序

電梯排序

當各個程序本地的plug list裡面的request滿了,就要洩洪,以排山倒海之勢進入的,不是最終的裝置驅動,而是I/O電梯排程器。

Linux 塊裝置驅動分析(二)塊IO請求的處理過程電梯排序

進入排程器進行電梯排程,其實目的有:

  1. 進一步的合并request
  2. 把request對硬碟的通路變得順序化

在請求隊列裡有一成員elevator:

struct request_queue {
	......
	struct elevator_queue	*elevator; //IO排程器
	......
};

struct elevator_queue
{
	struct elevator_type *type; 
	void *elevator_data;
	......
	DECLARE_HASHTABLE(hash, ELV_HASH_BITS);
};

struct elevator_type
{
	......
	struct elevator_ops ops; //排程器的操作集
	......
};

struct elevator_ops
{
	......
	//排程器出口函數,用于将排程器内的IO請求派發給裝置驅動
	elevator_dispatch_fn *elevator_dispatch_fn;

	//排程器入口函數,用于向排程器添加IO請求
	elevator_add_req_fn *elevator_add_req_fn;
	......
};
           

核心提供了3個I/O電梯排程器,Noop、Deadline和CFQ排程器,預設的排程器是CFQ。CFQ排程器為系統内的所有任務配置設定均勻的I/O帶寬, 提供一個公平的工作環境, 在多媒體應用中, 能保證音、 視訊及時從磁盤中讀取資料。

可以通過類似如下的指令, 改變一個裝置的排程器:

echo SCHEDULER > /sys/block/DEVICE/queue/scheduler
           

接下來就來分析blk_flush_plug_list函數:

void blk_flush_plug_list(struct blk_plug *plug, bool from_schedule)
{
	struct request_queue *q;
	unsigned long flags;
	struct request *rq;
	LIST_HEAD(list);
	unsigned int depth;

	......

	list_splice_init(&plug->list, &list);

	//對蓄流連結清單中的requset進行排序,以扇區大小進行排序
	list_sort(NULL, &list, plug_rq_cmp);

	......

	local_irq_save(flags);
	//循環的取出蓄流連結清單中的requset
	while (!list_empty(&list)) {
		rq = list_entry_rq(list.next);
		list_del_init(&rq->queuelist);
	
		......

		//向排程器中添加IO請求
		if (rq->cmd_flags & (REQ_PREFLUSH | REQ_FUA))
			__elv_add_request(q, rq, ELEVATOR_INSERT_FLUSH); 
		else
			__elv_add_request(q, rq, ELEVATOR_INSERT_SORT_MERGE);

		depth++;
	}

	......
	if (q)
		//調用request_queue->request_fn函數處理請求
		queue_unplugged(q, depth, from_schedule); 

	local_irq_restore(flags);
}
           

__elv_add_request:

void __elv_add_request(struct request_queue *q, struct request *rq, int where)
{

	
	rq->q = q;

	......

	switch (where) {
	
	......

	case ELEVATOR_INSERT_SORT_MERGE:
		//嘗試與排程隊列中的request合并,不能合并則落入ELEVATOR_INSERT_SORT分支
		if (elv_attempt_insert_merge(q, rq))  
			break;
	case ELEVATOR_INSERT_SORT:
		......
		if (rq_mergeable(rq)) { 
			elv_rqhash_add(q, rq);
			if (!q->last_merge)
				q->last_merge = rq;
		}

		//調用elevator_ops->elevator_add_req_fn函數将新來的request插入到排程隊列合适的位置
		q->elevator->type->ops.elevator_add_req_fn(q, rq);
		break;

	......
}
           

request_queue的request_fn成員函數由驅動程式提供,該函數的主要工作是,從請求隊列中提取出請求,然後進行硬體上的資料傳輸,傳輸完資料後,調用__blk_end_request_all函數報告請求處理完成。下面給出一個request_fn示例:

static void do_request(struct request_queue *q)
{
	struct request *req;
	struct bio *bio;
	//從request_queue提取request
	while ((req = blk_fetch_request(q)) != NULL) 
	{
		
		if(req->cmd_type != REQ_TYPE_FS)	//請求類型不是來自fs
		{
			printk(KERN_ALERT"Skip non-fs request\n");
			__blk_end_request_all(req, -EIO); //報告出錯
			break;
		}

		__rq_for_each_bio(bio, req)	//周遊request的bio
			ramblk_xfer_bio(bio);  //處理單個bio
		
		//報告請求處理完成
		__blk_end_request_all(req, 0);
		
	}
}

           

提取請求的函數為blk_fetch_request:

struct request *blk_fetch_request(struct request_queue *q)
{
	struct request *rq;

	/* 傳回下一個要處理的請求(由I/O排程器決定),如果沒有請求則傳回NULL 
     request_queue->queue_head連結清單不空,則從該連結清單取出request,如果空,則會
     調用elevator_ops->elevator_dispatch_fn函數,讓I/O排程器派發request到queue_head連結清單
   */
	rq = blk_peek_request(q);
	if (rq)
		blk_start_request(rq); //啟動請求,将請求從請求隊列中移除
	return rq;
}
           

總結下blk_flush_plug_list函數的處理流程:

Linux 塊裝置驅動分析(二)塊IO請求的處理過程電梯排序

繼續閱讀