Long Story of Block - DISCARD

Concept

introduction to DISCARD

DISCARD 的概念其实来自 SSD 设备。我们知道由于 flash 存储介质的特性,SSD 设备中的一个 block 只支持 write、erase 操作,而不支持 overwrite 操作。对于一个已经被 write 过的 block,如果需要向这个 block 写入新的数据,就必须先对该 block 执行 erase 操作,之后再将新的数据写入这个 block

正是由于这种特性,SSD 设备必须尽可能及时地对已经 write 过的、但是用户不需要了的 block 执行 erase 操作,以作备用,不然等到 free block 用完了才执行 erase 操作,为时晚矣

但是 SSD 设备并不知道哪些 block 是需要的,哪些是不需要的,这些信息只有上层的用户也就是文件系统知道。例如文件系统对一个文件执行删除或 truncate 操作时,那些被删除的数据占用的 block 就是不再需要的,文件系统必须通过某种方式将这些信息告诉底层的 SSD 设备

block 层通过 DISCARD request 的方式来传递这些信息,文件系统将这些不再需要了的 sector range 封装为一个 DISCARD request,设备驱动在接收到这个 DISCARD request 时,同时也就接收到了需要执行 erase 操作的 sector range

DISCARD request

实际上 block layer 支持的 DISCARD request 只是一个统称,其可以细分为以下三种 request

REQ_OP_DISCARD
REQ_OP_WRITE_ZEROES
REQ_OP_WRITE_SAME

这是因为不同的协议实现有各自不同的命令来实现 DISCARD 操作,这些不同的命令实现的效果又存在细微的差别

DISCARD

ATA 实现有 TRIM 命令,对应于 DISCARD,也就是封装了需要执行 erase 操作的 sector range,设备接收到该 request 时就会对指定的 sector range 执行 erase 操作

WRITE_ZERO

nvme 实现有 deallocate 命令,类似于 TRIM 命令,对应于 DISCARD;同时还实现有 write zero 命令,对应于 WRITE_ZERO

WRITE_ZERO 与 DISCARD 都是告诉底层设备哪些 sector range 是不需要了的,设备可以对这些 sector range 执行 erase 操作,但是区别在于,DISCARD 返回后从这些 sector range 读到的值是 undefined 的,而 WRITE_ZERO 返回后,会确保从这些 sector range 读到的值是 0

The Write Zeroes command is used to set a range of logical blocks to zero. After successful completion of this command, the value returned by subsequent reads of logical blocks in this range shall be zeroes until a write occurs to this LBA range.
-- NVMe Spec

WRITE_SAME

SCSI 实现有 UNMAP 命令,类似于 TRIM 命令,相当于 DISCARD;此外还实现有 write same 命令,相当于 WRITE_SAME

WRITE_SAME 命令的本意是对指定的 sector range 重复执行写操作,写入的数据由传入的一个 block 大小的 buffer 指定,这样 sector range 中的每个 block 都会被写入同样的内容

WRITE_SAME 命令中有一个 UNMAP bit,当这个 bit 被置位时,这个 WRITE_SAME 命令实际上相当于是执行 UNMAP 命令,同时输入的 block 大小的 buffer 的内容必须全为 0,此时会对指定的 sector range 执行 erase 操作,同时当 WRITE_SAME 命令返回后,会确保从这些 sector range 读到的值是 0

The WRITE SAME(10) command (see table 236) requests that the device server transfer a single logical block from the Data-Out

Buffer and for each LBA in the specified range of LBAs:
a) perform a write operation using the contents of that logical block; or
b) perform an unmap operation.

-- SCSI spec

SCSI supports the WRITE SAME commands to write a LBA sized buffer to many LBAs.

  • If the UNMAP bit is set WRITE SAME ask the device to unmap the blocks covered
  • Buffer must be all zeros for the UNMAP bit to work.
  • Future reads from the LBAs must return all zeros

DISCARD 和 WRTE_ZERO request 都是没有 payload 的,即所有 bio 的 bio_vec 数组都是空的,此时这些 bio 只是描述对应的 sector range

但是 WRTE_SAME request 是有 payload 的,此时每个 bio 内有且只有一个 bio_vec,同时所有 bio 的 bio_vec 都指向同一个 page,这个 page 实际上就是之前介绍的被重复写入的“block 大小的 buffer”

physical segment of DISCARD

introduction of physical segment of DISCARD

有些 IO controller 支持在单个 DISCARD request 中同时对多个非连续的 sector range 执行 discard 操作,因而每个 request 需要维护一个字段描述该 request 中 sector range 的数量

DISCARD request 实际上复用了 @nr_phys_segments 字段来描述该 request 包含的 sector range 的数量,对应地也复用了 @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;
    ...
};
struct bio {
    /* Number of segments in this BIO after
     * physical address coalescing is performed.
     */
    unsigned int        bi_phys_segments;
    ...
}

bio calculation

DISCARD bio 不带有 payload,其 bio_vec 数组为空,此时一个 bio 就只是描述一段需要执行 discard 操作的 sector range;一个 bio 只能描述一段 sector range,因而其 @bi_phys_segments 字段的值只能为 1

bio->bi_phys_segments 字段的值在 bio split 路径中初始化,其初始值为 1

blk_mq_make_request
    blk_queue_split
        blk_bio_discard_split
            *nsegs = 1

request calculation

在 bio 封装为 request 的过程中,DISCARD request 的 @nr_phys_segments 字段被初始化为 1

blk_mq_make_request
    blk_mq_bio_to_request
        blk_init_request_from_bio
            blk_rq_bio_prep
                rq->nr_phys_segments = 1

前面介绍过,有些 IO controller 支持在单个 DISCARD request 中同时对多个非连续的 sector range 执行 discard 操作,@limits.max_discard_segments 参数就描述了这一限制,即单个 DISCARD request 可以包含的 sector range 数量的上限;如果该参数为 1,则说明单个 DISCARD request 中只能包含一段连续的 sector range

@limits.max_discard_segments 参数的不同,直接导致了 request 合并(包括 bio 与 request 合并、requests 之间的合并)过程中行为的差异

@max_discard_segments > 1

首先介绍 @max_discard_segments 大于 1 时的行为,此时一个 request 可以包含多个非连续的 sector range,同时不会对相邻的两个 bio 的 sector range 进行合并,而无论这两个 bio 描述的 sector range 是否连续,此时 req->nr_phys_segments 的值就等同于该 request 中包含的 bio 的数量

为什么不把 sector range 连续的两个 bio 合并为一个 sector range 呢?

我想是因为 DISCARD IO 与普通的 READ/WRITE IO 存在差异。普通的 READ/WRITE IO 可能存在比较多的小 IO,将其中物理地址连续的小 IO 合并为一个 physical segment 对于性能提升是有益的;而 DISCARD IO 在下发下来的时候,通常就已经是一个 sector 地址连续的单个的大 IO,此时再尝试将多个连续的 sector range 合并为一个 sector range,可能受益不大

此外当 @limits.max_discard_segments 参数大于 1 时,IO controller 本身就支持单个 DISCARD request 中包含多个非连续的 sector range,由于 DISCARD IO 通常都是 sector 地址连续的大 IO,因而即使将一个 bio 就视为一个独立的 sector range,一个 DISCARD request 中包含的 bio 的数量可能都小于 @limits.max_discard_segments 参数的值,因而这个时候执行 sector range 的合并操作,的确受益不大

bio & request merge

此时 request 与 bio 合并过程中,req->nr_phys_segments 的值总是直接加 1,而无论新合入的 bio 描述的 sector range 是否与之前的 sector range 相连续

blk_mq_bio_list_merge
    bio_attempt_discard_merge
        req->nr_phys_segments += 1

同时当 @limits.max_discard_segments 参数大于 1 时,对于 DISCARD request 来说,是不存在 requests 之间的合并的

@max_discard_segments == 1

而当 @max_discard_segments 参数等于 1 时,其行为就复杂很多。此时一个 request 仍然可以包含多个 bio,但是这些 bio 的 sector range 必须是连续的,即多个 bio 仍然组成一个连续的 sector range,也就是说这个时候是会对相邻的两个 bio 的 sector range 进行合并操作的

为什么会存在这种差异呢?我认为主要是 @max_discard_segments 参数等于 1 时,一个 DISCARD request 只能包含一个连续的 sector range,相当于是 "sector range" 资源比较匮乏,这个时候将 sector 地址连续的多个 bio 合并为一个连续的 sector range,相当于是一种优化

bio & request merge

此时 request 与 bio 合并过程中,@req->nr_phys_segments 字段会加上合并的 @bio->bi_phys_segments 的值,由于 DISCARD bio 的 @bi_phys_segments 字段的值均为 1,因而 @req->nr_phys_segments 字段的值实际上也就是加 1

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

因而需要注意的是,无论 @max_discard_segments 参数是否大于 1,request->nr_phys_segments 的值实际上都会随着 request 中包含的 bio 数量的增加而增加,因而严格意义上 @req->nr_phys_segments 并不是描述 request 中包含的非连续 sector range 的数量

requests merge

值得一提的是,当 @max_discard_segments 参数等于 1 时,bio 与 request 的合并走的是 normal READ/WRITE request 合并的路径,也就是 front/back merge 的路径,这也就直接导致了会走到 requests 合并的路径

在 4.19 版本内核中,两个 DISCARD requests 合并过程中,合并后的 @req->nr_phys_segments 会减 1,这也就直接导致了合并后 @req->nr_phys_segments 的值比 request 中包含的 bio 的数量小 1

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

device driver calculation

需要注意的是,以上描述的 @req->nr_phys_segments 的计算规则都是 block layer 的行为

而 nvme/virtio-blk driver 在处理 DISCARD request 时,会期待 @req->nr_phys_segments 的值与 request 中包含的 bio 数量相等

virtio_queue_rq
    virtblk_setup_discard_write_zeroes
        segments = blk_rq_nr_discard_segments(req)
        range = kmalloc_array(segments, ...)
        __rq_for_each_bio(bio, req) {
            range[n].sector = ;
            ...
        }
上一篇:内存模型与同步原语 - 2 内存模型


下一篇:元心科技加入,龙蜥社区迎来国内领先的智能移动OS厂商