1. 程式人生 > >塊裝置核心引數max_segments和max_sectors_kb解析

塊裝置核心引數max_segments和max_sectors_kb解析

linux塊裝置在處理io時會受到一些引數(裝置的queue limits引數,以下簡稱limits引數)的影響,比如一個請求中允許的最大扇區數,最大segment數等。這些引數可以在/sys/block//queue/下檢視,塊裝置在初始化時會設定預設值。這裡主要分析max_segments和max_sectors_kb。

1. 基本概念

1.1 段的概念
首先就需要了解一下什麼是段(segment)。
一個段就是一個記憶體頁或者記憶體頁的一部分,它們包含磁碟上物理相鄰的資料塊。
磁碟的每個io操作就是磁碟與一些RAM單元之間相互傳送一些相鄰扇區的內容。大多數情況下,磁碟控制器採用DMA方式傳送資料,塊裝置驅動程式只要向磁碟控制器傳送適當的命令就可以觸發一次資料傳送,一旦完成資料傳送,磁碟控制器就會發出一箇中斷通知塊裝置驅動程式。
DMA方式傳送的是磁碟上相鄰的扇區資料,雖然也允許傳送不相鄰的扇區資料,但是這個比較低效,因為磁頭移動慢。
老的磁碟控制器僅僅支援“簡單”的DMA方式:磁碟必須與RAM中連續的記憶體單元傳送資料。但是新的磁碟控制器支援分散聚集(scatter-gather)DMA方式,磁碟可以與一些非連續的記憶體區域相互傳送資料。
為了支援分散聚集DMA方式,塊裝置驅動程式必須能夠處理稱為段的資料儲存單元,一次分散聚集DMA可以傳送幾個段。
如果不同的段在RAM中相應的頁框正好是連續的並且在磁碟上相應的資料也是相鄰的,那麼通用塊層就可以進行合併,這種合併方式產生的更大的記憶體區域就稱為物理段。

1.2 各引數含義
max_segments表示裝置能夠允許的最大段的數目。
max_sectors_kb表示裝置允許的最大請求大小。
max_hw_sectors_kb表示單個請求所能處理的最大KB(硬約束) 

以一塊sata磁碟為例:
cat /sys/block/sda/queue/max_sectors_kb
512
一個bio中bio_vec 最大為128個,每個ve_len為4096。128 * 4KB = 512KB,與max_sectors_kb吻合。

2. limits引數的設定及影響

2.1 limits預設值的設定
一般的塊裝置在初始化時會呼叫blk_queue_make_request設定make_request回撥函式,與此同時,還會呼叫blk_set_default_limits設定裝置queue的限制的預設值,預設的值如下。

enum blk_default_limits {
 BLK_MAX_SEGMENTS	= 128,
 BLK_SAFE_MAX_SECTORS	= 255,
 BLK_DEF_MAX_SECTORS	= 1024,
 BLK_MAX_SEGMENT_SIZE	= 65536,
 BLK_SEG_BOUNDARY_MASK	= 0xFFFFFFFFUL,
};

2.2 DIRECT IO中max_segments和max_sectors_kb
使用direct io時,跳過檔案系統的cache,io直接下發到塊裝置。以write為例,io path如下:
sys_write() –> vfs_write() –> do_sync_write() –> blkdev_aio_write() –> __generic_file_aio_write() –> generic_file_direct_write() –> blkdev_direct_IO() –> __blockdev_direct_IO() –> submit_page_section() –> dio_send_cur_page() –> dio_bio_submit() –> submit_bio() –> generic_make_request()

在dio_send_cur_page 中,會嘗試將連續的請求合併成一個bio,然後返回,如果合併失敗或者當前請求與已有的bio不連續,就直接dio_bio_submit提交請求。
其中dio_bio_add_page() –> bio_add_page() –> __bio_add_page() 是將當前請求合併到bio中。
1)max_segments
在 __bio_add_page有如下一段程式碼,會判斷bio中的物理段數目是否超過裝置的max_segments,如果超過就返回0表示add page失敗,從而會呼叫dio_bio_submit 提交請求。

 while (bio->bi_phys_segments >= queue_max_segments(q)) {
  if (retried_segments)
   return 0;
  retried_segments = 1;
  blk_recount_segments(q, bio);
 }

2)max_sectors_kb
同樣是在__bio_add_page中,

 if (((bio->bi_size + len) >> 9) > max_sectors)
  return 0;

其中max_sectors就是裝置的max_sectors_kb*2,即是當該bio的大小超過max_sectors_kb時,就返回0表示add page失敗,這樣就會呼叫dio_bio_submit提交請求。

2.3 Device Mapper裝置的limits引數
device mapper裝置(簡稱dm裝置)在剛開始建立初始化時會先設定成預設值,然後再會根據其底層的裝置和預設值,兩者取最小值來設定dm裝置的限制值。其流程如下:
dm_create() –> alloc_dev() –> dm_init_md_queue() –> blk_queue_make_request()設定預設值
然後在dm_calculate_queue_limits中先設定成預設,然後會最終呼叫blk_stack_limits,取底層裝置和dm裝置的這些引數的最小值來設定dm的引數。
do_resume() –> dm_swap_table() –> dm_calculate_queue_limits() –> ti->type->iterate_devices() –>dm_set_device_limits() –> bdev_stack_limits() –> blk_stack_limits()

不過在不同的核心中還有些不同,上面描述的是3.2.54核心的流程,在新核心(3.10.11)中,設定成預設的流程也一樣,不過在dm_calculate_queue_limits中呼叫了blk_set_stacking_limits(),這個函式會把limits引數設定成極大值,這樣在後續呼叫blk_stack_limits的時候,也是取兩者最小值,不過這個時候因為dm的limits引數是極大值,所以會使用底層裝置的limits引數的值來設定dm的limits引數。
這樣做的好處在於可以繼承底層裝置的限制,底層裝置都能處理這些limits引數,在它們之上的dm裝置同樣也可以。
這裡附上blk_set_stacking_limits的程式碼

void blk_set_stacking_limits(struct queue_limits *lim)
{
 blk_set_default_limits(lim);
 /* Inherit limits from component devices */
 lim->discard_zeroes_data = 1;
 lim->max_segments = USHRT_MAX;
 lim->max_hw_sectors = UINT_MAX;
 lim->max_sectors = UINT_MAX;
 lim->max_write_same_sectors = UINT_MAX;
}

2.4 軟raid裝置中的limits引數
軟raid的建立過程參考http://blog.csdn.net/liumangxiong/article/details/12405231
簡單來說,就是分配裝置,初始化資料結構,註冊請求處理函式,根據不同的raid級別再進行一些必要的初始化。
呼叫關係如下:
md_init() –> md_probe() –> md_alloc() –> blk_queue_make_request()
之前已經說到在blk_queue_make_request中會註冊make_request_fn,並且會設定limits引數的預設值。
do_md_run –> md_run() –> mddev->pers->run(mddev) 對應raid1中的run() –> 會遍歷raid中的裝置,分別呼叫disk_stack_limits() –> bdev_stack_limits() –> blk_stack_limits()
這裡也是會根據md裝置和其底層的成員裝置的queue->limits引數,兩者取最小值。

與device mapper裝置類似,在不同核心下limits引數設定還有些不同。
上面描述的是3.2.54核心下的流程。
在3.10.11核心下,md_alloc中在呼叫blk_queue_make_request之後還會呼叫blk_set_stacking_limits將md裝置的limits引數設定為極大值。
這樣就能在blk_stack_limits中取最小值時繼承底層裝置的limits引數。

在使用fio在這兩個核心下對軟raid1測試的時候,就會發現區別,當fio使用大於127KB的塊大小時,在3.2.54核心使用iostat就會看到實際落到md裝置的io大小小於fio下發的大小,在3.10.11就沒有這個問題。

另外,需要提到的是在3.10.11核心中也沒有《systemtap分析軟raid io拆分問題》中max_segments設定為1的問題,因為新核心的raid1中的run()函式中已經取消掉這段程式碼

 if (rdev->bdev->bd_disk->queue->merge_bvec_fn) {
   blk_queue_max_segments(mddev->queue, 1);
   blk_queue_segment_boundary(mddev->queue,
         PAGE_CACHE_SIZE - 1);
  }

這樣就不會將max_segments設定成1了。

3. 參考資料
1)《深入理解linux核心》
2)《linux裝置驅動程式》
3)軟raid程式碼分析http://blog.csdn.net/liumangxiong/article/details/12405231
4)https://www.kernel.org/doc/Documentation/block/queue-sysfs.txt
5)3.2.54及3.10.11核心原始碼

轉自:http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=22954220&id=4813537