天天看點

Long Story of Block - segment

segment

segment 的概念實際來自 DMA controller,DMA controller 可以實作一段記憶體實體位址區間與一段裝置實體位址區間之間的資料拷貝,segment 就描述 DMA 資料傳輸過程中的一段連續的記憶體空間,也就是說 DMA controller 可以将記憶體中一個 segment 中的資料拷貝到裝置,或将裝置中的資料拷貝到 segment 中

segment 可以是一個 page,也可以是一個 page 的其中一部分,通常存儲一個或多個相鄰的 sector 的資料

Long Story of Block - segment

核心使用 struct bio_vec 來描述 segment 描述符,其中使用 (page, offset_in_page, len) 三元組來描述這一段記憶體區間

struct bio_vec {
    struct page    *bv_page;
    unsigned int    bv_len;
    unsigned int    bv_offset;
};           

一個 bio 可以包含多個 segment,每個 bio 都維護有一個 segment array 即 bio_vec 數組,其中組織了該 bio 包含的所有 segment

struct bio {
    struct bio_vec        *bi_io_vec; /* the actual vec list */
    ...
};           

bio_segments(bio) 用于傳回 bio 中剩餘待處理的 struct bio_vec 的數量,例如 bio 建立的時候 @bi_io_vec[] 數組一共存儲了 X 個 struct bio_vec,在處理了 Y 個 struct bio_vec 之後,bio_segments(bio) 傳回值為 (X-Y)

physical segment

introduction to physical segment

在介紹 physical segment 的概念之前,有必要介紹一下 DMA controller 中 scatter-gather 的概念

支援 scatter-gather 特性的 DMA controller 可以在一次 DMA transfer 中,實作多個非連續的實體記憶體區間到一個連續的裝置位址區間之間的資料傳輸,這裡的每個連續的實體記憶體區間就稱為一個 physical segment

Long Story of Block - segment

那麼這裡的 physical segment 與之前介紹的 segment 有什麼差別呢?

以 bio 為例,實際上 bio 中的一個 bio_vec 就相當于是一個 segment,但是多個 bio_vec 描述的記憶體區間在實體位址上可能是相鄰的,也就是說在執行 DMA transfer 操作的時候,這兩個 bio_vec 描述的記憶體區間實際上可以合并為一個更大的記憶體區間

例如下圖中 bio_vec[1] 和 bio_vec[2] 描述的記憶體區間雖然在虛拟位址上并不連續,但是在實體位址上是連續的,因而這兩個 "segment" 可以合并為一個 physical segment

Long Story of Block - segment

bio 的 @bi_phys_segments 字段就描述該 bio 包含的 physical segment 的數量

struct bio {
    /* Number of segments in this BIO after
     * physical address coalescing is performed.
     */
    unsigned int        bi_phys_segments;
    ...
}           

request 的 @nr_phys_segments 字段描述該 request 包含的 physical segment 的數量,其初始值來自 @bio->bi_phys_segments

struct request {
    /*
     * Number of scatter-gather DMA addr+len pairs after
     * physical address coalescing is performed.
     */
    unsigned short nr_phys_segments;
    ...
};           

physical segment limit

由于 DMA controller 的實體參數可能存在限制,physical segment 在一些參數上可能存在限制,例如一個 request 可以包含的 physical segment 的數量、單個 physical segment 的大小等,request queue 的 @limits 描述了這些限制

max_segments

Maximum number of segments of the device.

由于 DMA controller 自身的限制,單個 request 可以包含的 physical segment 數量可能存在上限,@limits.max_segments 描述了這一限制,預設值為 128,對應

/sys/block/<dev>/queue/max_segments

struct queue_limits {
    unsigned short        max_segments;
    ...
}           

在 bio 與 request 合并,或者兩個 request 合并過程中,需要判斷合并後的 request 包含的 physical segment 的數量不能超過這一上限

example

以 virtio-blk 為例,virtio block config 配置空間的 @seg_max 字段就描述了一個 request 請求可以包含的 physical segment 的數量上限

struct virtio_blk_config {
    /* The maximum number of segments (if VIRTIO_BLK_F_SEG_MAX) */
    __u32 seg_max;
    ...
}           

virtio spec 中對 @seg_max 字段的解釋為

seg_max is the maximum number of segments that can be in a command.

virtio-blk 裝置初始化過程中,就會從 virtio block config 配置空間讀取 @seg_max 的值,并儲存到 @limits.max_segments 中

int virtblk_probe(struct virtio_device *vdev)
{
    /* We can handle whatever the host told us to handle. */
    blk_queue_max_segments(q, sg_elems);
    ...
}           

此外值得注意的是,virtio-blk 中每個 request 後面會有預配置設定好的 scatterlist 數組(驅動在向底層的實體裝置下發 IO 的時候,每個 physical segment 需要初始化一個對應的 scatterlist),數組的大小就是 @limits.max_segments,也就是 physical segment 的數量上限

struct request          struct virtblk_req      sg[]
+-----------------------+-----------------------+-----------+
|                       |                       |           |
+-----------------------+-----------------------+-----------+           
struct virtblk_req {
    ...
    struct scatterlist sg[];
};           

max_segment_size

Maximum segment size of the device.

由于 IO controller 自身的限制,一個 physical segment 的大小可能存在上限,@limits.max_segment_size 描述了這一限制,以位元組為機關,預設值為 BLK_MAX_SEGMENT_SIZE 即 65536 位元組即 64 KB,對應

/sys/block/<dev>/queue/max_segment_size

struct queue_limits {
    unsigned int        max_segment_size;
    ...
}           

在計算一個 bio 或 request 包含的 physical segment 數量的過程中,雖然兩個 bio_vec 描述的記憶體區間在實體位址上連續,因而可以合并為一個 physical segment,但是前提是合并後的 physical segment 的大小不能超過 @max_segment_size 限制

bi_seg_front_size/bi_seg_back_size

@limits.max_segment_size 參數限制了一個 physical segment 的大小上限,在 bio 與 request 合并、或兩個 request 合并的過程中,如果中間可以合并為一個 physical segment,那麼需要確定合并後的 physical segment 的大小沒有超過 @limits.max_segment_size 參數限制

由于 bio 與 request 合并過程中,實際上是 request 的最後一個 bio (back merge) 或第一個 bio (front merge) 與該 bio 合并;兩個 request 合并過程中,實際上是前一個 request 的最後一個 bio 與後一個 request 的第一個 bio 合并;因而實際上都可以抽象為兩個 bio 的合并

之前介紹過,合并過程中需要判斷合并後的 physical segment 的大小沒有超過 @limits.max_segment_size 參數限制,為了加速這一檢查過程,bio 中維護有以下兩個字段

struct bio {
    /*
     * 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;
    ...
}           

這兩個字段隻有 normal READ/WRITE bio 才會設定,其中

  • @bi_seg_front_size 描述了該 bio 中包含的第一個 physical segment 的大小
  • @bi_seg_back_size 描述了該 bio 中包含的最後一個 physical segment 的大小

@bi_seg_front_size、@bi_seg_back_size 在 bio split 路徑中初始化

blk_queue_split
    blk_bio_segment_split
        bio->bi_seg_front_size =
        bio->bi_seg_back_size =           

request merge 過程中,會調用 blk_phys_contig_segment() 檢查,前面一個 request 的最後一個 physical segment 與後面一個 request 的第一個 physical segment 合并過程中,合并後的 physical segment 是否超過 @max_segment_size 限制,這一檢查過程中将前一個 request 的最後一個 bio 的 @bi_seg_back_size 字段,加上後一個 request 的第一個 bio 的 @bi_seg_front_size 字段,就可以快速計算得到合并後的 physical segment 的大小

ll_merge_requests_fn
    blk_phys_contig_segment
            bio *bio = req_prev->biotail, 
            bio *nxt = req_next->bio
            bio->bi_seg_back_size + nxt->bi_seg_front_size > queue_max_segment_size(q)           

physical segment calculation

bio calculation

之前介紹過,bio->bi_phys_segment 字段描述了 bio 中 physical segment 的數量,但是 bio 初始化的時候并不會設定 @bi_phys_segment 字段,後續處理過程中需要調用 blk_recount_segments() 計算 bio 包含的 physical segment 的數量,儲存在 @bi_phys_segment 字段,同時在 bio->bi_flags 字段設定上 BIO_SEG_VALID 标志

request queue 的 entry point 中,在 make_request_fn() 回調函數調用過程中,調用 blk_queue_split() 的時候就會初始化 @bi_phys_segment 字段;如果 bio 不能與任一個 pending request 相合并,在将 bio 封裝為一個新的 request 的過程中,也會初始化 @bi_phys_segment 字段

bio 中包含的 physical segment 數量,即 bio->bi_phys_segment 字段的計算相對簡單,bio 中相鄰的兩個 bio_vec,如果它們各自描述的記憶體區間的實體位址連續,那麼這兩個 bio_vec 就可以視為一個 physical segment,即 bio 内部連續的 bio_vec 可以合并為一個 physical segment

request calculation

request 包含的 physical segment 的數量,即 @request->nr_phys_segments 字段的計算則更為複雜一些,同時計算規則也不那麼“統一”

當 bio 封裝為 request 的時候,@request->nr_phys_segments 字段的初始值自然是來自 @bio->bi_phys_segment

blk_rq_bio_prep
    rq->nr_phys_segments = bio_phys_segments(q, bio)           
requests merge

兩個 request 合并過程中,一個 request 包含的兩個相鄰 bio 可以合并為一個 physical segment

例如一個 request A 與 request B 合并過程中,request A 的最後一個 bio A,以及 request B 的第一個 bio B,兩個 bio 的 sector 位址連續,同時兩個 bio 的 @bi_phys_segments 字段的值分别為 bi_phys_segments_A、bi_phys_segments_B,如果這兩個 bio 描述的記憶體區間的實體位址連續,那麼合并後的 request->nr_phys_segments 的值應該為 (bi_phys_segments_A + bi_phys_segments_B - 1),也就是中間部分合并為了一個 physical segment

ll_merge_requests_fn
    total_phys_segments = req->nr_phys_segments + next->nr_phys_segments
    if (blk_phys_contig_segment()) total_phys_segments--
    req->nr_phys_segments = total_phys_segments           
bio & request merge

詭異的是 bio 與 request 合并過程中,合并後的 @request->nr_phys_segments 的計算則稍有不同,此時 bio 不會與 request 的最後一個 bio 合并為一個 physical segment,即使這兩個 bio 描述的記憶體區間的實體位址連續

例如 bio 與 request 合并過程中,合并後的 @request->nr_phys_segments 隻是合并前 @request->nr_phys_segments 與 @bio->bi_phys_segment 相加的和,而不會考慮 request 的最後一個 bio 的最後一個 bio_vec 能否與 bio 的第一個 bio_vec 合并為一個 physical segment

blk_mq_bio_list_merge
    bio_attempt_back_merge
        ll_back_merge_fn
            ll_new_hw_segment
                req->nr_phys_segments += bio->bi_phys_segments           

值得一提的是,在request 與 bio 合并過程中,不存在新合并的 physical segment,因而也就用不到 @bi_seg_front_size、@bi_seg_back_size 字段

blk_recalc_rq_segments

blk_recalc_rq_segments() 函數用于計算一個 request 中包含的 physical segment 數量即 @request->nr_phys_segments 字段,此時計算規則與 requests merge 時的規則相一緻,即一個 request 包含的兩個相鄰 bio 可以合并為一個 physical segment

例如一個 request 中包含 bio A、bio B,兩個 bio 的 sector 位址連續,同時兩個 bio 的 @bi_phys_segments 字段的值分别為 bi_phys_segments_A、bi_phys_segments_B,如果這兩個 bio 描述的記憶體區間的實體位址連續,那麼此時 request->nr_phys_segments 的值應該為 (bi_phys_segments_A + bi_phys_segments_B - 1),也就是中間部分合并為了一個 physical segment

device driver calculation

在處理 normal READ/WRTE request 時,virtio-blk / nvme 驅動會自己重新計算一遍 request 中包含的 physical segment 的數量,此時計算規則是,兩個相鄰 bio 可以合并為一個 physical segment

注意驅動并不會直接使用 @request->nr_phys_segments,而隻是當驅動計算得到的 nsegs 大于上層 block layer 計算得到的 @request->nr_phys_segments 時,列印 warning 資訊

virtio_queue_rq/nvme_queue_rq
    blk_rq_map_sg
        __blk_bios_map_sg
        WARN_ON(nsegs > blk_rq_nr_phys_segments(rq));