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));
上一篇:Long Story of Block - 1 Data Unit


下一篇:NGINX如何配置对网站某个目录允许几个IP访问并拒绝其他所有人。