# 概述

当用户空间通过 syscall 发起读 / 写命令时,如果页缓存(page cache)中没有需要的数据,就需要实际从存储介质中读取 / 写入。Linux 为了兼容各式各样的底层存储设备,抽象出了通用块设备层来作为一个统一的接口向上层(文件系统)提供高效的读 / 写服务。在 Linux 中,一次 IO 请求可以用如下简化的示意图表示。

linux 设备 IO 路径

Linux 块层通过 struct biostruct request 这两种抽象,把上层(如文件系统、页缓存)发起的 IO 请求,与下层(各种驱动 / 硬件)实际能够高效处理的 IO 单元进行了完美解耦和桥接,从而实现了高性能、高兼容性的通用存储框架。本篇文章主要聚焦于这两个结构:

  • 上层文件系统将自己的需求包装成 struct bio ,表达自己要读写位于何处的多少数据
  • 块层在 struct bio 的基础上进一步将 IO 请求处理成 struct request 结构,并下发给设备驱动处理

在实际情况中,bio 和 request 不一定是一一对应。多个 bio 可以合并到一个 request,也可能一个 bio 拆成多个 request(如跨越设备最大 IO 限制时)。块层在 submit_bio 到驱动 queue_rq 这段过程中,负责 bio→request 的组织、合并、限速和调度,极大提升了系统 IO 性能和硬件利用率。

接下来我们详细介绍一下这部分内容。

# bio 层

struct bio 是高层(文件系统 / 页缓存 / RAID/dm 等)到块层的通用 IO 请求描述,主要包括数据页(bio_vec)、IO 方向、起始扇区等,表达 “我想从这里读 / 写这些数据”。

# 关键数据结构

struct bio 的定义位于 include/linux/blk_types.h ,其字段详细含义如下:

/*
 * main unit of I/O for the block layer and lower layers (ie drivers and
 * stacking drivers)
 */
struct bio {
	struct bio		*bi_next;	/* 用于将多个 bio 连接成一个链表,通常用于 request queue 的合并和调度 */
	struct block_device	*bi_bdev; /* bio 要操作的设备的指针(block_device 表示一个 “已打开的块设备 / 分区” 的实例) */
	blk_opf_t		bi_opf;		/* 用于表明这个 bio 是做什么操作的
                                 * 低 8 位代表操作类型,参考下面的 REQ_OP_*
						 		 * 高 24 位代表请求的 flag, 参考下面的 __REQ_*   */
	unsigned short		bi_flags;	/* bio 本身的 flag,参考下面的 BIO_* */
	unsigned short		bi_ioprio;  /* IO 优先级,低层可以据此做调度优化 */
	blk_status_t		bi_status;  /* bio 的执行结果,记录 IO 操作的返回 / 完成状态 */
    /* 
     * 代表 bio chain 中还有几个 bio 没有处理完,默认值为 1。
	 * 对某一个 bio 每 split 产生一个新的 bio,__bi_remaining 加 1。
	 * chain 类型的 bio 的回调函数为 bio_chain_endio,
	 * bio_chain_endio->bio_endio->bio_remaining_done 将 __bi_remaining 减 1 
	 * 后判断 __bi_remaining 是否为 0,不为 0 说明 bio 并没有处理完(目前只是完成了 chain 中一个 bio ),
	 * bio_endio 直接返回,只有当 __bi_remaining 为 0 时,bio_endio 才会正在地执行 
	 */
	atomic_t		__bi_remaining; 
	struct bvec_iter	bi_iter;    /* 描述当前 bio 的遍历状态
                                     *(存储器端起始 sector、还剩多少个字节需要读写;)
                                     *(内存端当前操作的 vector 是哪个、当前 vector 中已经完成了多少个字节)
                                     */
	blk_qc_t		bi_cookie;
	bio_end_io_t		*bi_end_io; /* bio 完成后的回调函数指针,IO 操作完成(无论成功或失败)时调用 */
	void			*bi_private;    /* 各功能模块的私有指针,用于实现特殊功能 */
    /* ...(省略部分根据内核配置才启用的成员) */
	unsigned short		bi_vcnt;	/* bio 中当前 vector 数量,不能大于 bio->bi_max_vecs 
	                                 * bio->bi_io_vec [bio->bi_vcnt] 得到的 vector 是空闲可用的  
	                                 */
	/* 
	* 从 bi_max_vecs 成员开始的所有字段,在调用 bio_reset () 这个函数时,都会被保留,不会被重置或覆盖。 
	*/
    
	unsigned short		bi_max_vecs;	/* bio 中 vector 的最大值,分配 bio 时设置 */
	atomic_t		__bi_cnt;	/* bio 的引用计数,防止 bio 被提前释放 */
	/* 
	 * 当 bio 中的 vector 数量小于等于 BIO_INLINE_VECS(值为 4)时,
	 * bio->bi_io_vec 指向 bio 内嵌的 bio->bi_inline_vecs, 
	 * 否则指向按需分配的 vector 地址。
	 * 不管是 bio->bi_inline_vecs 还是按需分配的 vector,这些 vector 在虚拟地址上是连续的,
	 * 本质上就是一个元素为 struct bio_vec 的数组,bio->bi_io_vec 指向数组第一个元素,
	 * 将地址(bio->bi_io_vec)+ 1 即可得到下一个元素 
	 */
    struct bio_vec		*bi_io_vec;	
	/*
	 * 表明 bio 归属于哪个内存池(bio_set),用于 bio 释放回收。
	 * bio 结构体在内核中会频繁的申请、释放,为了提升性能、避免内存碎片,
	 * 内核用 slab 管理 bio 内存,slab 中存放相同大小的 object(object 就是 struct bio),
	 * 这些 objcect 就构成了内存池。需要 bio 时,从内存池申请,释放时返回给内存池。
	 */
	struct bio_set		*bi_pool;
	/*
	 * bio 内嵌了 BIO_INLINE_VECS(值为 4)个 vector(struct bio_vec)。
	 * 若 bio 需要的 vector 数量小于等于 BIO_INLINE_VECS,
	 * 可直接用这里预定义的 bi_inline_vecs, 就不需要额外申请了,
	 * 但是该字段必须位于 struct bio 的结尾处。
	 */
	struct bio_vec		bi_inline_vecs[];
};

struct bio 中的 bi_opf 字段的可选值及解释如下:

/**
 * enum req_op - bio 和 request 结构体都通用的操作类型。
 * 用 8 个比特(位)来编码操作类型,剩下的 24 位用于标志位(flags)。
 *
 * 操作码的最低有效位(最低一位)表示数据传输方向:
 *
 *   - 如果最低位被置位(1),表示数据 “写入” 到设备(TO the device)
 *   - 如果最低位未被置位(0),表示数据 “读取” 自设备(FROM the device)
 *
 * 如果某个操作并不涉及数据传输(如 FLUSH),那么最低位没有意义。
 */
enum req_op {
	/* 从设备读取扇区 */
	REQ_OP_READ		= (__force blk_opf_t)0,
	/* 向设备写入扇区 */
	REQ_OP_WRITE		= (__force blk_opf_t)1,
	/* 刷新易失性写缓存 */
	REQ_OP_FLUSH		= (__force blk_opf_t)2,
	/* 丢弃扇区 */
	REQ_OP_DISCARD		= (__force blk_opf_t)3,
	/* 安全擦除扇区(比 discard 更彻底) */
	REQ_OP_SECURE_ERASE	= (__force blk_opf_t)5,
	/* 向当前分区写指针写入数据 */
	REQ_OP_ZONE_APPEND	= (__force blk_opf_t)7,
	/* 将扇区用 0 填充多次 */
	REQ_OP_WRITE_ZEROES	= (__force blk_opf_t)9,
	/* 管理分区状态(打开) */
	REQ_OP_ZONE_OPEN	= (__force blk_opf_t)10,
	/* 管理分区状态(关闭) */
	REQ_OP_ZONE_CLOSE	= (__force blk_opf_t)11,
	/* 管理分区状态(已满) */
	REQ_OP_ZONE_FINISH	= (__force blk_opf_t)12,
	/* 重置一个分区写指针 */
	REQ_OP_ZONE_RESET	= (__force blk_opf_t)13,
	/* 重置设备上所有分区 */
	REQ_OP_ZONE_RESET_ALL	= (__force blk_opf_t)15,
	/* 驱动私有请求 */
	REQ_OP_DRV_IN		= (__force blk_opf_t)34,
	REQ_OP_DRV_OUT		= (__force blk_opf_t)35,
	REQ_OP_LAST		= (__force blk_opf_t)36,
};
enum req_flag_bits {
	__REQ_FAILFAST_DEV =	/* 设备层错误时不重试(fail fast,快速失败) */
		REQ_OP_BITS,
	__REQ_FAILFAST_TRANSPORT, /* 传输错误不重试 */
	__REQ_FAILFAST_DRIVER,	/* 驱动错误不重试 */
	__REQ_SYNC,		/* 请求是同步的(如同步写 / 读) */
	__REQ_META,		/* 元数据 IO 请求 */
	__REQ_PRIO,		/* 提升 IO 优先级(如 CFQ 调度器下)*/
	__REQ_NOMERGE,		/* 此请求不允许和其他请求合并 */
	__REQ_IDLE,		/* 预期后续还有更多 IO */
	__REQ_INTEGRITY,	/* IO 中包含数据完整性 payload */
	__REQ_FUA,		/* 强制单元访问(Flush on Write,写完必须落盘) */
	__REQ_PREFLUSH,		/* 请求缓存刷盘 */
	__REQ_RAHEAD,		/* 预读(可随时失败) */
	__REQ_BACKGROUND,	/* 后台 IO */
	__REQ_NOWAIT,           /* 遇到阻塞直接返回,用于高并发 */
	__REQ_POLLED,		/* 调用方使用 bio_poll 主动轮询完成 */
	__REQ_ALLOC_CACHE,	/* 可用时从缓存分配 IO */
	__REQ_SWAP,		/* 交换分区 IO */
	__REQ_DRV,		/* 驱动自用标志 */
	__REQ_FS_PRIVATE,	/* 文件系统自用标志 */
	/*
	 * Command specific flags, keep last:
	 */
	/* for REQ_OP_WRITE_ZEROES: */
	__REQ_NOUNMAP,		/* 全零化时不释放块 */
	__REQ_NR_BITS,		/* 标志位数量上限 */
};

内核中定义的 bio flags 包括如下几种:

/*
 * bio flags
 */
enum {
	BIO_PAGE_PINNED,	/* 在 bio_release_pages () 里需要解除页面的 “钉住” 状态。通常用于确保 bio 释放时正确释放页面引用 */
	BIO_CLONED,		    /* 该 bio 是 “克隆” 的,不拥有数据。即页面 /vector 不是它自己分配的,释放时不能主动释放数据。 */
	BIO_BOUNCED,		/* 该 bio 是 “bounce” bio(转跳用),比如内存无法 DMA 时临时搬运用的 bio */
	BIO_QUIET,		    /* 该 bio 设置为 “静默” 模式。IO 错误等不会在日志中打印或报告 */
	BIO_CHAIN,		    /* 该 bio 是链式 bio(被链表 / 递归合并),bi_remaining 计数生效 */
	BIO_REFFED,		    /* bio 的引用计数已被提升(加过一次引用),用于防止提前释放 */
	BIO_BPS_THROTTLED,	/* 该 bio 已经执行过 BPS(带宽)限制,不要再限速了 */
	BIO_TRACE_COMPLETION,	/* bio_endio () 应追踪 / 记录该 bio 的最终完成。用于 trace / 调试 */
	BIO_CGROUP_ACCT,	/* 该 bio 已经被计入了 cgroup IO 统计 */
	BIO_QOS_THROTTLED,	/* bio 经过 rq_qos 的限速路径(request queue 的 QoS 调度) */
	BIO_QOS_MERGED,		/* bio 经过 rq_qos 的合并路径 */
	BIO_REMAPPED,       /* 该 bio 已被重新映射(如 device mapper 层分区 / 重定向)*/
	BIO_ZONE_WRITE_LOCKED,	/* 拥有分区存储设备(zoned device)的 zone 写锁。确保对该 zone 的并发写入安全 */
	BIO_FLAG_LAST
};

struct bio 遵循 “需求与执行” 分离的原则。 bio->bi_io_vec 描述 “需求”(即 io 会涉及到哪些 vector),其类型为 struct bio_vecbio->bi_iter 描述执行情况(即 bio 的处理进度,当前在处理哪个 vector、当前 vector 中已经完成了多少个字节),其类型为 struct bvec_iter

struct bio_vecstruct bvec_iter 的定义均位于 include/linux/bvec.h

struct bvec_iter {
	sector_t		bi_sector;	/* 设备扇区地址(以 512 字节为单位)。表示本次 IO 操作当前要处理的 “设备起始扇区号” */
	unsigned int		bi_size;	/* 剩余未处理的 IO 大小(字节)。表示 bio 剩余还有多少字节要处理,随着分段或分片会递减 */
	unsigned int		bi_idx;		/* 当前正在处理的 bio_vec 的下标 */
	unsigned int            bi_bvec_done;	/* 当前 bio_vec(bi_idx 指向的那个)中已完成的字节数。 */
} __packed __aligned(4);
/**
 * struct bio_vec - 一段连续的物理内存地址区间
 * @bv_page:   指向与该地址区间关联的第一个内存页(struct page 指针)
 * @bv_len:    该地址区间的总字节数
 * @bv_offset: 该地址区间在 @bv_page 起始处的偏移(字节为单位)
 *
 * 如果满足 n * PAGE_SIZE < bv_offset + bv_len(就是存在数据跨页的情况),则对一个 bvec 有:
 *
 *   nth_page (@bv_page, n) == @bv_page + n
 *
 * 通常每个 bio_vec 只指向一段页内或跨页起始段,内核会通过 page_is_mergeable () 检查能否合并多个 bio_vec
 */
struct bio_vec {
	struct page	*bv_page;
	unsigned int	bv_len;
	unsigned int	bv_offset;
};

struct bio 的主要成员关系如下:

bio主要成员关系

# 执行流程

当上层模块(如文件系统)有对块设备的 IO 需求时,会封装一个 bio 结构体,并调用 submit_bio() 将该 IO 请求下发到块层处理。

# submit_bio

/**
 * submit_bio - 向块设备层提交一个 bio 进行 I/O 操作
 * @bio: 用于描述本次 I/O 的 struct bio 结构体指针
 *
 * submit_bio () 用于向块设备提交 I/O 请求。它接收一个已经完全设置好的 struct bio,
 * 该 bio 描述了需要进行的 I/O 操作。bio 会被发送到其 bi_bdev 字段指定的设备上。
 *
 * 本次请求的成功 / 失败状态,以及完成的通知,都会通过 bio->bi_end_io () 回调函数
 * 以异步方式传递,在 bi_end_io () 被调用之前,调用者【不得】修改或访问该 bio。
 */
void submit_bio(struct bio *bio)
{
	if (bio_op(bio) == REQ_OP_READ) {
		task_io_account_read(bio->bi_iter.bi_size);
		count_vm_events(PGPGIN, bio_sectors(bio));
	} else if (bio_op(bio) == REQ_OP_WRITE) {
		count_vm_events(PGPGOUT, bio_sectors(bio));
	}
	bio_set_ioprio(bio);
	submit_bio_noacct(bio);
}
EXPORT_SYMBOL(submit_bio);

# submit_bio_noacct

noacctnoaccounting 代表不进行统计,比如 IO 计数、内存统计、进程的 io_account、页缓存计数等,这些只需要在上层做一次即可。

/**
 * submit_bio_noacct - 重新提交一个 bio 到块设备层进行 I/O 操作
 * @bio:  描述了内存和设备上的位置的 bio
 *
 * 这是 submit_bio () 的一个更底层的版本,仅应被 “分层块设备驱动”(stacking block drivers)
 * 用于将 I/O 重新提交给更底层的驱动。所有文件系统以及其他块层上层用户,都应该使用 submit_bio ()。
 */
void submit_bio_noacct(struct bio *bio)
{
	struct block_device *bdev = bio->bi_bdev;
	struct request_queue *q = bdev_get_queue(bdev);
	blk_status_t status = BLK_STS_IOERR;
	might_sleep();
	/*
	 * 对一个带有 REQ_NOWAIT 标志的请求,如果队列不支持 NOWIAT 则返回 -EOPNOTSUPP
	 */
	if ((bio->bi_opf & REQ_NOWAIT) && !bdev_nowait(bdev))
		goto not_supported;
    /* 检查一些可能会出错的场景 */
	if (should_fail_bio(bio))
		goto end_io;
	bio_check_ro(bio); /* 检查是否是在写一个只读设备 */
	if (!bio_flagged(bio, BIO_REMAPPED)) {
		if (unlikely(bio_check_eod(bio)))
			goto end_io;
		if (bdev->bd_partno && unlikely(blk_partition_remap(bio)))
			goto end_io;
	}
	/*
	 * 及早过滤掉 flush 类型的 bio,这样不支持 flush 的基于 bio 的驱动就不必关心这些 bio 了。
	 * op_is_flush () 用于检测 bi_opf 是否包含 REQ_PREFLUSH 或 REQ_FUA 标志,代表该 bio 要求刷写设备的易失性写缓存。
	 */
	if (op_is_flush(bio->bi_opf)) {
        /* 如果 bio_op 既不是 REQ_OP_WRITE,也不是 REQ_OP_ZONE_APPEND,则打印警告,并提前结束 IO。
         * 这是因为 flush/fua 语义只适用于写入操作,对其它操作类型设置 flush 是非法用法。
         */
		if (WARN_ON_ONCE(bio_op(bio) != REQ_OP_WRITE &&
				 bio_op(bio) != REQ_OP_ZONE_APPEND))
			goto end_io;
        /* 如果底层设备(队列)没有写缓存(即不需要 flush),则直接忽略 flush/fua */
		if (!test_bit(QUEUE_FLAG_WC, &q->queue_flags)) {
			bio->bi_opf &= ~(REQ_PREFLUSH | REQ_FUA);
			if (!bio_sectors(bio)) {
				status = BLK_STS_OK;
				goto end_io;
			}
		}
	}
    /* 如果队列没有设置 POLL 特性(即不支持轮询完成),就把 REQ_POLLED 标志清掉(即该 bio 不能用轮询方式等待完成)。 */
	if (!test_bit(QUEUE_FLAG_POLL, &q->queue_flags))
		bio_clear_polled(bio);
	switch (bio_op(bio)) {
	case REQ_OP_READ:
	case REQ_OP_WRITE:
		break;
	case REQ_OP_FLUSH:
		/*
		 * REQ_OP_FLUSH 不能直接通过 bio 提交,它只能由 flush 状态机在 struct request 层面合成出来
		 */
		goto not_supported;
	case REQ_OP_DISCARD:
		if (!bdev_max_discard_sectors(bdev))
			goto not_supported;
		break;
	case REQ_OP_SECURE_ERASE:
		if (!bdev_max_secure_erase_sectors(bdev))
			goto not_supported;
		break;
	case REQ_OP_ZONE_APPEND:
		status = blk_check_zone_append(q, bio);
		if (status != BLK_STS_OK)
			goto end_io;
		break;
	case REQ_OP_WRITE_ZEROES:
		if (!q->limits.max_write_zeroes_sectors)
			goto not_supported;
		break;
	case REQ_OP_ZONE_RESET:
	case REQ_OP_ZONE_OPEN:
	case REQ_OP_ZONE_CLOSE:
	case REQ_OP_ZONE_FINISH:
		if (!bdev_is_zoned(bio->bi_bdev))
			goto not_supported;
		break;
	case REQ_OP_ZONE_RESET_ALL:
		if (!bdev_is_zoned(bio->bi_bdev) || !blk_queue_zone_resetall(q))
			goto not_supported;
		break;
	case REQ_OP_DRV_IN:
	case REQ_OP_DRV_OUT:
		/* 驱动私有操作(REQ_OP_DRV_IN/OUT)只允许用于直通(passthrough)请求。*/
		fallthrough;
	default:
		goto not_supported;
	}
    /* 如果队列有 blk-throttle 规则(限速 / 限带宽),会先在这里 “拦截”,由 blk-throtl 机制调度后再继续提交。 */
	if (blk_throtl_bio(bio))
		return;
    /* 如果不需要限速,直接调用 submit_bio_noacct_nocheck (bio) 下发到驱动,不再进行请求类型的二次检查。 */
	submit_bio_noacct_nocheck(bio);
	return;
not_supported:
	status = BLK_STS_NOTSUPP;
end_io:
	bio->bi_status = status;
	bio_endio(bio);
}
EXPORT_SYMBOL(submit_bio_noacct);

# submit_bio_noacct_nocheck

void submit_bio_noacct_nocheck(struct bio *bio)
{
	/* cgroup 的 IO 信息统计相关操作 */
    blk_cgroup_bio_start(bio);
	blkcg_bio_issue_init(bio);
    /* 检查 bio 是否已经设置了 “需要完成时追踪” 的标志(BIO_TRACE_COMPLETION)。 */
	if (!bio_flagged(bio, BIO_TRACE_COMPLETION)) {
		trace_block_bio_queue(bio);
		/*
		 * 既然已经对 bio 的入队操作进行了 trace(跟踪 / 记录),就需要对其完成(completion)也做 trace。
		 */
		bio_set_flag(bio, BIO_TRACE_COMPLETION);
	}
	/*
     * 我们希望同一时刻只激活一个 ->submit_bio(块设备递交)过程,
     * 否则在有堆叠块设备(如 device-mapper, LVM, MD)时会造成栈空间消耗问题。
     * 用 current->bio_list 来收集在一个 ->submit_bio 调用期间被递交的所有 bio,
     * 等该 submit_bio 方法返回后,再统一处理这些 bio。
	 */
	if (current->bio_list) // 当前进程的 bio_list 已经存在,即处于一个递归 / 嵌套的 submit_bio 调用过程中
		bio_list_add(&current->bio_list[0], bio); // 先把 bio 添加到 current->bio_list 中,不马上递交
	else if (!bio->bi_bdev->bd_has_submit_bio) // 如果目标 block_device 没有实现 bd_has_submit_bio (即没有自定义递交函数)
		__submit_bio_noacct_mq(bio); // 调用基于 MQ(多队列块层)的递交函数
	else
		__submit_bio_noacct(bio);  // 调用设备专用的 submit_bio 递交函数
}

# __submit_bio_noacct

块设备提交 I/O(bio)过程中, submit_bio() 可能导致递归调用自身,递归过程中可能又生成新的 bio(例如多层设备映射、加密、压缩、RAID 等),为避免递归导致栈爆炸,Linux 块层设计了两级 bio_list 机制

递归部分需要结合着 submit_bio_noacct_nocheck 一起来看。

/*
 * 这个函数中的循环可能有些不太直观,因此需要做一些解释:
 *
 *  - 在进入循环之前,bio->bi_next 是 NULL(调用者都会确保这一点),
 *    所以我们最开始的 bio 链表中只有一个 bio。
 *
 *  - 我们假装它是从一个更长的链表中取出来的,因此我们将 bio_list 指向
 *    bio_list_on_stack,从而初始化用于添加新 bio 的 bio_list。
 *    ->submit_bio () 的确可能会通过递归调用 submit_bio_noacct 添加更多的 bio。
 *    如果确实添加了新的 bio,我们会在 bio_list 中发现非 NULL 的值,
 *    然后从循环顶端重新进入。
 *
 *  - 这个时候我们是真的从 bio_list 的顶部取出一个 bio(不再是假装),
 *    所以我们从 bio_list 中移除该 bio,并再次调用 ->submit_bio () 处理它。
 *
 * bio_list_on_stack [0] 保存的是当前这次 ->submit_bio 提交过程中生成的 bio。
 * bio_list_on_stack [1] 保存的是在当前这次 ->submit_bio 之前提交、但尚未处理的 bio。
 */
static void __submit_bio_noacct(struct bio *bio)
{
	struct bio_list bio_list_on_stack[2];
  /* bio->bi_next 应为 NULL,确保初始 bio 独立 */
	BUG_ON(bio->bi_next);
	bio_list_init(&bio_list_on_stack[0]);  // 初始化 bio_list_on_stack [0]
	current->bio_list = bio_list_on_stack; // 绑定当前进程的 current->bio_list,供递归提交中挂载新 bio
	do {
		struct request_queue *q = bdev_get_queue(bio->bi_bdev); // 获取 bio 目标块设备的 request_queue
		/*
		 * 准备两个临时链表:
		 *  lower:指向更底层设备的 bio
		 *  same :仍针对当前层设备的 bio
		 */
		struct bio_list lower, same;
		/*
		 * 为所有下层的 requests 创建一个新的 bio_list
		 */
		bio_list_on_stack[1] = bio_list_on_stack[0]; // 旧的 bio_list_on_stack [0] 备份到 bio_list_on_stack [1]
		bio_list_init(&bio_list_on_stack[0]); // 重新初始化 bio_list_on_stack [0],收集本次递归中派生的新 bio
    /*
     * 递归提交当前 bio
		 * 递归过程中若产生新的 bio,会挂入 bio_list_on_stack [0]
		 */
		__submit_bio(bio);
		/* 分类新生成的 bio */
		bio_list_init(&lower);
		bio_list_init(&same);
    /*
     * 遍历本层新生成 bio,按目标设备队列划分:
     *  与当前 q 相同:放入 same
     *  不相同:放入 lower
     */
		while ((bio = bio_list_pop(&bio_list_on_stack[0])) != NULL)
			if (q == bdev_get_queue(bio->bi_bdev))
				bio_list_add(&same, bio);
			else
				bio_list_add(&lower, bio);
		/*
		 * 组合待提交队列,优先级顺序为:
		 * 1. 指向更底层的 bio
		 * 2. 本设备队列的 bio
		 * 3. 上层递归残留的 bio 队列
		 */
		bio_list_merge(&bio_list_on_stack[0], &lower);
		bio_list_merge(&bio_list_on_stack[0], &same);
		bio_list_merge(&bio_list_on_stack[0], &bio_list_on_stack[1]);
	} while ((bio = bio_list_pop(&bio_list_on_stack[0]))); // 不断从 bio_list_on_stack [0] 弹出 bio 提交
	current->bio_list = NULL;
}

# __submit_bio_noacct_mq

提交 bio 请求,但不记录统计信息(noacct),并且针对多队列(mq)块设备子系统。

static void __submit_bio_noacct_mq(struct bio *bio)
{
	struct bio_list bio_list[2] = { };
  /* 取出当前进程的待提交 bio 列表 */
	current->bio_list = bio_list;
	do {
		__submit_bio(bio);
	} while ((bio = bio_list_pop(&bio_list[0]))); /* 从列表中取出一个 bio, bio_list [0] 是主 bio 列表 */
	current->bio_list = NULL;
}

# __submit_bio

static void __submit_bio(struct bio *bio)
{
    /* 检查和准备 bio 的加密上下文(如果有开启 inline 加密) */
	if (unlikely(!blk_crypto_bio_prep(&bio)))
		return;
    /* 检查目标块设备是否实现了自己的递交函数 */
	if (!bio->bi_bdev->bd_has_submit_bio) {
		blk_mq_submit_bio(bio); /* 默认的实现(现代磁盘、普通驱动大多都用这套) */
	} else if (likely(bio_queue_enter(bio) == 0)) { /* device-mapper、md/raid、zram、加密等 stacking driver,进入这里 */
		struct gendisk *disk = bio->bi_bdev->bd_disk;
		/* 拿到 gendisk,调用其驱动注册的 submit_bio 方法递交 bio */
		disk->fops->submit_bio(bio);
		blk_queue_exit(disk->queue); /* 递交结束后,blk_queue_exit () 释放队列资源。 */
	}
}

至此,bio 层的主要操作已经完成,下面就开始进入到 request 层。

# request 层

struct request 是块层到设备驱动的请求单元,除了包含 bio 信息,还包括调度策略标志、优先级、调度合并结果、设备资源限制(如分段数、tag)、最终完成回调等,是能被驱动 / 硬件直接下发的任务描述。驱动只认识 request,不直接处理 bio。

# 关键数据结构

在本节中,“请求” 在绝大多数情况下都是指 request 这个内核结构。

struct request 是块层(block layer)调度和驱动处理的核心 I/O 单元,每个要发往硬件的 I/O 都必须包装为 request。

/*
 * 尽量将被引用的字段放在同一缓存行中
 *
 * 如果改变了该结构体,需要确保同步更新 blk_rq_init (), 尤其是 blk_mq_rq_ctx_init () 以处理额外的字段。
 */
struct request {
    /* 该 request 所属的请求队列(request_queue)。决定该 request 要投递到哪个块设备 */
	struct request_queue *q;
	/* 多队列(blk-mq)下该 request 的上下文信息,标识本 request 属于哪个 software queue(通常和当前 CPU 绑定)*/
    struct blk_mq_ctx *mq_ctx;
    /* 多队列 blk-mq 下的硬件队列上下文,决定本 request 实际发往哪个硬件队列 */
	struct blk_mq_hw_ctx *mq_hctx;
	blk_opf_t cmd_flags;   /* request 的操作码和常用标志,和 bio->bio_opf 相同,但是属于 request 级别 */
	req_flags_t rq_flags;  /* request 的各种状态标志 */
	int tag;               /* 本 request 在队列中的唯一标识符。用于硬件队列和 IO 调度的 tracking */
	int internal_tag;      /* 专用于内部管理的 tag,一般在 request pool 管理 / 特殊调度时使用 */
	unsigned int timeout;  /* 超时时间,request 最长存活时间,通常用于超时重试和调度 */
	/* 如下两个字段仅限内部使用,【不能】直接访问 */
	unsigned int __data_len;	/* 该 request 总的数据长度(字节) */
	sector_t __sector;		    /* 当前处理到的扇区号,表示请求进度  */
	/* 挂在本 request 上的第一个 bio,request 可能由多个 bio 合并而成,这里指向链表头 */
    struct bio *bio;
    /* 指向 bio 链表的末尾,用于快速追加新 bio */
	struct bio *biotail;
	union {
		struct list_head queuelist; /* 一个双向链表节点,用于将 request 加入到各种 “队列” 中 */
		struct request *rq_next;    /* 一个单链表指针,用于一些特殊场景,例如 request 合并 */
	};
	struct block_device *part; /* request 操作的目标块设备 / 分区 */
#ifdef CONFIG_BLK_RQ_ALLOC_TIME
	/* 第一个 bio 开始分配到该 request 的时间 */
	u64 alloc_time_ns;
#endif
	/* 该 request 被分配到这次 IO 的时间戳 */
	u64 start_time_ns;
	/* I/O 被提交到硬件的时间戳   */
	u64 io_start_time_ns;
    #ifdef CONFIG_BLK_WBT
        unsigned short wbt_flags;
    #endif
	/*
	 * 用于块设备的统计,记录 request 最初的扇区数,值与 blk_rq_sectors (rq) 相同,在完成时不会被清零
	 */
	unsigned short stats_sectors;
	/*
	 * 在物理地址合并(coalescing)之后,scatter-gather DMA 地址 + 长度对的数量。
	 *
	 * 在块设备做 DMA 传输时,数据不一定连续,可能分散在不同的内存物理页面上。
	 * Scatter-gather DMA 机制允许一次传输描述多个(物理地址,长度)对,称为 “SG 列表”。
	 * 这样设备可以一次性高效读写多个不连续内存段
	 */
	unsigned short nr_phys_segments;
	/* ...(省略一些可选字段) */
	unsigned short ioprio; /* request 的 I/O 优先级 */
	enum mq_rq_state state; /* 当前 request 的状态 */
	atomic_t ref;           /* 引用计数,防止 request 被提前释放 */
	unsigned long deadline; /* 该 request 的调度 deadline,IO scheduler 用来判断超时顺序。 */
	/*
	 * hash 字段(hlist_node)只在调度器内部使用,当 request 进入 dispatch 列表后就会被移除。
	 * ipi_list 字段只用于将 request 加入软中断(softirq)完成队列,
	 * 这发生在 request 已经从 hash 中移除(甚至 dispatch 列表中也移除)很久之后。
	 */
	union {
		struct hlist_node hash;
		struct llist_node ipi_list;
	};
	/*
	 * rb_node 字段仅用于 io scheduler 内部,当 request 被移到 dispatch 队列时就会被移除。
	 * special_vec 字段只有在设置了 RQF_SPECIAL_PAYLOAD 标志时才能使用,而且带有该标志的 request
	 * 不能被插入到 IO 调度器中。
	 */
	union {
		struct rb_node rb_node;	/* 用于将 request 加入调度器的红黑树结构,用于调度排序、查找 */
		struct bio_vec special_vec; /* 特殊 payload 时用(如 SCSI sense 数据),只有 RQF_SPECIAL_PAYLOAD 标志时可用 */
	};
	/* 提前预留了三个指针给 IO schedulers 使用,如果需要更多的话,需要动态分配 */
	struct {
		struct io_cq		*icq;
		void			*priv[2];
	} elv;
    /* flush 请求相关的数据,保存原本的 end_io 回调和序号 */
	struct {
		unsigned int		seq;
		rq_end_io_fn		*saved_end_io;
	} flush; 
	u64 fifo_time; /* FIFO 调度时的入队时间(用于排序) */
	/* 完成回调 */
	rq_end_io_fn *end_io; /* 完成回调函数。request 完成时调用 */
	void *end_io_data; /* 完成回调的数据(context),随 request 传递给 end_io */
};

dispatch 队列就是等待下发给块设备驱动的 request 列表,它位于调度器和硬件之间,是高性能块层的关键缓冲区。当调度器决定该 request “可以发给硬件了”,把 request 从调度队列移出,放进 dispatch 队列。

# 执行流程

# blk_mq_submit_bio

解释几个机制:

  • plug 队列机制(plug queue):
    • blk_plug 是一个进程私有的 “临时请求缓冲区”,用于收集短时间内产生的多个请求,之后一起发给设备,提高合并和排序效率(减少寻道、提升吞吐)。
    • plug 队列上的 request 还没有直接进入硬件队列。
  • 请求合并(request merge):
    • 根据程序的局部性原理,对于块设备的访问容易出现聚集性;对于块设备来说,寻址是慢的,但是顺序读是快的,所以可以通过将多个读写请求合并的方法来减少 IO 数量,提升顺序 IO 性能。
    • 所以如果当前 request 能与 plug 队列中已有的 request 合并,就直接合并。
  • IO 调度器(IO scheduler):
    • 如果硬件队列挂载了 IO 调度器,则请求可能被排队、排序或合并后再发到硬件队列。
  • 多队列(blk-mq):
    • 支持多核并发,每个 CPU 都有自己的 hardware queue,提高并发度。
/**
 * blk_mq_submit_bio - 创建并发送 request 到块设备
 * @bio: bio 指针
 *
 * 该函数会根据 @q 和 @bio 构建出一个 request 结构体,并发送到设备。 
 * 但这个 request 可能不会被直接排队到硬件队列中,如果:
 * * 该 request 能与其它 request 合并
 * * 我们希望把 request 放到 plug 队列, 以便未来可能合并
 * * 该队列上启用了 IO 调度器
 *
 * 如果 bio 有错误,或创建 request 时出错,则不会入队。
 */
void blk_mq_submit_bio(struct bio *bio)
{
    // 获取 bio 目标块设备的请求队列指针
	struct request_queue *q = bdev_get_queue(bio->bi_bdev);
	struct blk_plug *plug = blk_mq_plug(bio);
	const int is_sync = op_is_sync(bio->bi_opf);
	struct blk_mq_hw_ctx *hctx;
	struct request *rq = NULL; // 用于复用或者新建的 request 指针
	unsigned int nr_segs = 1; // 记录 bio 会被分割成多少个 segment(bio_vec)
	blk_status_t ret;
	
    /*
     * 如果 bio 指向的数据页面不在设备支持的 DMA 区域(如高端内存),
     * 就需要搬运(bounce)到低端内存,否则后续 IO 无法被设备访问。
     * 返回 bounce 后的新 bio(可能和原 bio 相同)。
     */
	bio = blk_queue_bounce(bio, q);
	if (plug) { // 如果 plug 存在,尝试复用 plug 的 cached request(有助于减少频繁分配)
		rq = rq_list_peek(&plug->cached_rq);
		if (rq && rq->q != q) // 如果 plug 中的 request 队列与本次 bio 目标设备不同,则不能复用,置 rq 为 NULL。
			rq = NULL;
	}
	if (rq) {
        // 如果 bio 超出设备支持的限制,需要拆分为多个 bio
		if (unlikely(bio_may_exceed_limits(bio, &q->limits))) {
			bio = __bio_split_to_limits(bio, &q->limits, &nr_segs);
			if (!bio)
				return;
		}
        // 数据完整性扩展准备(如 T10 DIF/DIX),失败直接返回
		if (!bio_integrity_prep(bio))
			return;
        // 如果能与已有请求合并,则直接合并,不需新建 request
		if (blk_mq_attempt_bio_merge(q, bio, nr_segs))
			return;
        // 使用 plug 缓存的 request 进行 IO
		if (blk_mq_use_cached_rq(rq, plug, bio))
			goto done;
        // 增加队列引用计数(因为要新建 request 了)
		percpu_ref_get(&q->q_usage_counter);
	} else {
        // 没有可复用 request(或不在 plug 队列),正常流程
		if (unlikely(bio_queue_enter(bio)))
			return; // 队列不可用,直接返回
        //bio 是否超设备限制,如果超则拆分
		if (unlikely(bio_may_exceed_limits(bio, &q->limits))) {
			bio = __bio_split_to_limits(bio, &q->limits, &nr_segs);
			if (!bio)
				goto fail;
		}
		if (!bio_integrity_prep(bio))
			goto fail;
	}
	
    // 获取一个新 request 对象
	rq = blk_mq_get_new_requests(q, plug, bio, nr_segs);
	if (unlikely(!rq)) {
fail:
		blk_queue_exit(q);
		return;
	}
done:
	trace_block_getrq(bio); // 跟踪 IO 获取 request 的事件,用于 trace/ftrace/perf
	rq_qos_track(q, rq, bio);
    // 把 bio 里的所有 IO 数据 / 元数据都转移到 request 结构体中,request 就可以被设备识别和处理。
	blk_mq_bio_to_request(rq, bio, nr_segs); 
	
    // 如果 request 需要 inline 加密,这里分配加密 keyslot,失败时结束 bio。
	ret = blk_crypto_rq_get_keyslot(rq);
	if (ret != BLK_STS_OK) {
		bio->bi_status = ret;
		bio_endio(bio);
		blk_mq_free_request(rq);
		return;
	}
    // 如果 bio 是 flush 类型并且插入 flush 队列成功,则后续会专门处理,无需下发到普通硬件队列
	if (op_is_flush(bio->bi_opf) && blk_insert_flush(rq))
		return;
    /*
     * 如果当前进程开启了 plug,则将 request 放入 plug 队列,不立即发送到硬件。
     * plug 队列会在适当时机(比如 plug 结束、同步写、队列满等)批量下发。
     */
	if (plug) {
		blk_add_rq_to_plug(plug, rq);
		return;
	}
    /*
     * 根据队列是否启用 IO 调度器(如 bfq、deadline),或队列繁忙、同步 / 异步等条件:
     * 有调度器或队列繁忙:插入队列等待调度,并唤醒硬件队列处理
     * 否则:直接尝试发往硬件(无须调度,优先直发,提高低延迟场景性能)
     */
	hctx = rq->mq_hctx;
	if ((rq->rq_flags & RQF_USE_SCHED) ||
	    (hctx->dispatch_busy && (q->nr_hw_queues == 1 || !is_sync))) {
		blk_mq_insert_request(rq, 0);
		blk_mq_run_hw_queue(hctx, true);
	} else {
		blk_mq_run_dispatch_ops(q, blk_mq_try_issue_directly(hctx, rq));
	}
}

# blk_mq_try_issue_directly

/**
 * blk_mq_try_issue_directly - 尝试直接将 request 发送到设备驱动
 * @hctx: 关联硬件队列的指针 (每个硬件队列一个 hctx)
 * @rq:   要发送的 request 指针
 *
 * 如果设备当前有足够资源可以接受新 request,就直接将 request 发送给驱动。
 * 否则,把它插入 hctx->dispatch 队列,将来重试。插入该队列的 request 具有更高优先级。
 */
static void blk_mq_try_issue_directly(struct blk_mq_hw_ctx *hctx,
		struct request *rq)
{
	blk_status_t ret;
    /* 如果硬件队列被暂停或整个请求队列进入暂停状态,则不能下发 request */
	if (blk_mq_hctx_stopped(hctx) || blk_queue_quiesced(rq->q)) {
		blk_mq_insert_request(rq, 0); // 将 request 插入 dispatch 队列,等待后续恢复后统一下发
		return;
	}
    /* 当前 request 需要调度器处理(RQF_USE_SCHED),或者无法获得 IO budget 或 tag */
	if ((rq->rq_flags & RQF_USE_SCHED) || !blk_mq_get_budget_and_tag(rq)) {
		blk_mq_insert_request(rq, 0); // 插入 dispatch 队列
		blk_mq_run_hw_queue(hctx, rq->cmd_flags & REQ_NOWAIT); // 唤醒硬件队列调度 (有机会优先处理)
		return;
	}
	ret = __blk_mq_issue_directly(hctx, rq, true); // 直接调用驱动发出 request
	switch (ret) {
	case BLK_STS_OK: //request 成功下发,驱动会异步处理和完成
		break;
	case BLK_STS_RESOURCE: 
	case BLK_STS_DEV_RESOURCE: // 设备或驱动暂时资源不足
		blk_mq_request_bypass_insert(rq, 0); // 将 request 插入 bypass 队列(高优先级)
		blk_mq_run_hw_queue(hctx, false);
		break;
	default:
		blk_mq_end_request(rq, ret); // 直接结束 request,回调 end_io,设置错误状态
		break;
	}
}

# __blk_mq_issue_directly

static blk_status_t __blk_mq_issue_directly(struct blk_mq_hw_ctx *hctx,
					    struct request *rq, bool last)
{
	struct request_queue *q = rq->q;
	struct blk_mq_queue_data bd = {
		.rq = rq,
		.last = last,
	};
	blk_status_t ret;
	/*
     * 如果(queue_rq 返回)OK,说明请求已成功提交,不用做其他事。
     * 如果是错误(error,比如请求本身有严重问题),调用者会直接终止这个请求(kill it)。
     * 其它类型的错误(比如繁忙 busy),就把这个请求加回我们的待处理队列(dispatch 队列),
     * 这样以后还能再试(这和我们以前的处理方式一样)。
	 */
	ret = q->mq_ops->queue_rq(hctx, &bd); // 将 request 交给块设备驱动
	switch (ret) {
	case BLK_STS_OK:
		blk_mq_update_dispatch_busy(hctx, false); // 更新硬件队列的 busy 状态 (not busy)
		break;
	case BLK_STS_RESOURCE:
	case BLK_STS_DEV_RESOURCE:
		blk_mq_update_dispatch_busy(hctx, true);  // 更新硬件队列的 busy 状态 (busy)
		__blk_mq_requeue_request(rq);
		break;
	default:
		blk_mq_update_dispatch_busy(hctx, false);
		break;
	}
	return ret;
}

queue_rq 是一个函数指针,定义在 struct blk_mq_ops 里:

struct blk_mq_ops {
	...
	blk_status_t (*queue_rq)(struct blk_mq_hw_ctx *hctx,
				 const struct blk_mq_queue_data *bd);
	...
};
  • 它是块层(blk-mq)向具体块设备驱动程序(比如 NVMe、SCSI、virtio-blk、loop、zram、dm、md 等)下发 request 的 “驱动回调函数”

  • 每个支持 blk-mq 的块设备驱动,都必须实现自己的 queue_rq 并在注册驱动时填入 struct blk_mq_ops 结构体。

# 参考

  • https://blog.csdn.net/geshifei/article/details/119959905

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Gality 微信支付

微信支付

Gality 支付宝

支付宝