文件系统:3. io_uring-sq-poll模式

该文章详细分析了Linux内核5.10版本中`io_sq_thread`线程的实现,包括线程的启动、上下文切换、io_uring上下文的处理流程以及如何根据处理结果决定线程是否让出CPU。文章特别关注了`__io_sq_thread`函数中对SQE的提交、I/O轮询和等待唤醒的逻辑。

Kernel源码笔记目录

sqpoll thread

源码基于5.10

static int io_sq_thread(void *data)
{
	struct cgroup_subsys_state *cur_css = NULL;
	const struct cred *old_cred = NULL;
	struct io_sq_data *sqd = data;
	struct io_ring_ctx *ctx;
	unsigned long start_jiffies;

	// 启动时间
	start_jiffies = jiffies;
	while (!kthread_should_stop()) {
		enum sq_ret ret = 0;
		bool cap_entries;

		// 需要暂停
		if (kthread_should_park()) {
			// 暂停自己
			kthread_parkme();
			// 在unpark之后,有可能kthread需要停止
			if (kthread_should_stop())
				break;
		}

		// 新创建的sq-thread会挂到ctx_new_list列表
		if (unlikely(!list_empty(&sqd->ctx_new_list)))
			// 这个函数里会初始化sqo_wait_entry和它的唤醒函数
			io_sqd_init_new(sqd);

		// list_is_singular是判断一个列表是否只有一个元素
		// 这里cap_entries表示,是否有多个ctx
		cap_entries = !list_is_singular(&sqd->ctx_list);

		// 经过上面的io_sqd_init_new,把io_uring_ctx从ctx_new_list列表移到了ctx_list
		list_for_each_entry(ctx, &sqd->ctx_list, sqd_list) {

			// 当前进程的cred与ctx的cred不一样
			if (current->cred != ctx->creds) {
				// todo: cred相关的后面再看
				if (old_cred)
					// 把当前进程的cred还原成旧的cred
					revert_creds(old_cred);
				// 设置当前进程的cred
				old_cred = override_creds(ctx->creds);
			}
			// 关联blkcg
			io_sq_thread_associate_blkcg(ctx, &cur_css);
#ifdef CONFIG_AUDIT
			// 审计相关
			current->loginuid = ctx->loginuid;
			current->sessionid = ctx->sessionid;
#endif

			// 真正处理ctx上的业务
			ret |= __io_sq_thread(ctx, start_jiffies, cap_entries);

			// 释放当前所占用的mm
			io_sq_thread_drop_mm();
		}

		if (ret & SQT_SPIN) {
			// 需要自旋,就先放弃内存引用,然后让出cpu
			io_run_task_work();
			io_sq_thread_drop_mm();
			cond_resched();
		} else if (ret == SQT_IDLE) {
			// 空闲
			if (kthread_should_park())
				continue;
			// 设置每个entry需要唤醒
			list_for_each_entry(ctx, &sqd->ctx_list, sqd_list)
				io_ring_set_wakeup_flag(ctx);
			// 调度出去
			schedule();
			start_jiffies = jiffies;
			// 清除需要唤醒标志
			list_for_each_entry(ctx, &sqd->ctx_list, sqd_list)
				io_ring_clear_wakeup_flag(ctx);
		}
	}

	io_run_task_work();
	io_sq_thread_drop_mm();

	if (cur_css)
		io_sq_thread_unassociate_blkcg();
	if (old_cred)
		revert_creds(old_cred);

	kthread_parkme();

	return 0;
}

static void io_sqd_init_new(struct io_sq_data *sqd)
{
	struct io_ring_ctx *ctx;

	while (!list_empty(&sqd->ctx_new_list)) {
		ctx = list_first_entry(&sqd->ctx_new_list, struct io_ring_ctx, sqd_list);
		init_wait(&ctx->sqo_wait_entry);
		// 设置sqo_wait的唤醒函数
		ctx->sqo_wait_entry.func = io_sq_wake_function;
		// 把ctx移到sqd->ctx列表
		list_move_tail(&ctx->sqd_list, &sqd->ctx_list);
		// 调用完成量
		complete(&ctx->sq_thread_comp);
	}
}

sqd主流程如下:

  1. 把上下文从new_ctx_list移到ctx_list
  2. 处理ctx上的业务
  3. 根据处理结果,决定sqd是否要让出cpu.

处理业务

static enum sq_ret __io_sq_thread(struct io_ring_ctx *ctx,
				  unsigned long start_jiffies, bool cap_entries)
{
	// 超时时间, sq_thread_idle默认是1秒
	unsigned long timeout = start_jiffies + ctx->sq_thread_idle;
	// 传入的数据
	struct io_sq_data *sqd = ctx->sq_data;
	unsigned int to_submit;
	int ret = 0;

again:
	// poll 列表不为空,则执行poll请求
	if (!list_empty(&ctx->iopoll_list)) {
		unsigned nr_events = 0;

		mutex_lock(&ctx->uring_lock);
		if (!list_empty(&ctx->iopoll_list) && !need_resched())
			// 处理io_poll业务
			io_do_iopoll(ctx, &nr_events, 0);
		mutex_unlock(&ctx->uring_lock);
	}

	// sqe的数量
	to_submit = io_sqring_entries(ctx);

	// 没有提交/忙/需要调度
	if (!to_submit || ret == -EBUSY || need_resched()) {
		// 放弃mm
		io_sq_thread_drop_mm();

		// todo: 这里为啥要自旋
		if (!list_empty(&ctx->iopoll_list) || need_resched() ||
		    (!time_after(jiffies, timeout) && ret != -EBUSY &&
		    !percpu_ref_is_dying(&ctx->refs)))
			return SQT_SPIN;

		// 在sqd->wait上等待
		prepare_to_wait(&sqd->wait, &ctx->sqo_wait_entry,
					TASK_INTERRUPTIBLE);

		// 被唤醒后,如果iopoll_list不为空,则继续处理
		if ((ctx->flags & IORING_SETUP_IOPOLL) &&
		    !list_empty_careful(&ctx->iopoll_list)) {
			finish_wait(&sqd->wait, &ctx->sqo_wait_entry);
			goto again;
		}

		// 睡眠之后要重新获取sqe的数量
		to_submit = io_sqring_entries(ctx);
		// 唤醒后还是没有提交或者当前进程忙,就是空闲状态,返回上层函数后会休眠
		if (!to_submit || ret == -EBUSY)
			return SQT_IDLE;
	}

	// 结束等待
	finish_wait(&sqd->wait, &ctx->sqo_wait_entry);
	// 清除唤醒标志
	io_ring_clear_wakeup_flag(ctx);

	// 如果有多个上下文,且当前ctx的提交数量大于8,则最多只能提交8个
	if (cap_entries && to_submit > 8)
		to_submit = 8;

	mutex_lock(&ctx->uring_lock);

	if (likely(!percpu_ref_is_dying(&ctx->refs) && !ctx->sqo_dead))
		// 真正的提交sqe
		ret = io_submit_sqes(ctx, to_submit);
	mutex_unlock(&ctx->uring_lock);

	// 如果有人在sqo_sq_wait上等待,则唤醒sqo_sq_wait队列
	if (!io_sqring_full(ctx) && wq_has_sleeper(&ctx->sqo_sq_wait))
		wake_up(&ctx->sqo_sq_wait);

	return SQT_DID_WORK;
}
<think>首先,用户的问题是:“我想了解fio工具中--ioengine=io_uring参数的功能和作用请问fioioengineio_uring功能及作用”,并提供了站内引用,我需要参考这些引用。用户的请求基于之前的对话,但我没有看到之前的聊天历史,所以我会假设这是独立的问题。用户提到“用户上一次的问题”,但没有提供上下文,所以我只能基于当前信息。参考站内引用:-[^1]:描述了io_uring中操作的prep和执行函数,例如io_fsync_prep和io_fsync。-[^2]:io_uring是新的异步IO机制,性能提升超150%,只支持directio,而bufferedio是同步的。-[^3]:描述使用liburing封装,如io_uring_queue_init。-[^4]:显示了一个使用io_uring的例子,包括初始化、提交请求和处理完成队列。核心问题是关于fio工具中--ioengine=io_uring参数的功能和作用。fio是一个基准测试工具,用于测试IO性能。--ioengine选项指定IO引擎,这里指定为io_uring。从引用中,我可以整理出io_uring的关键点:-io_uring是一个在Linux内核中的新型异步IO接口,提供了更高的性能和效率。-支持异步操作,包括读写、sync等,但[^2]提到它只支持directio(O_DIRECT),对bufferedio是同步的。-使用时,需要像在引用中那样使用liburing库来初始化、提交请求和处理完成事件。在fio中使用--ioengine=io_uring的作用是:-它启用fio使用io_uring作为底层IO引擎来执行文件操作。-这意味着fio可以利用io_uring的异步特性来高效地处理IO请求。-这有助于在高并发和高IO负载场景下获得更好的性能,因为io_uring减少了系统调用的开销和上下文切换。功能和作用:-功能:当设置--ioengine=io_uring时,fio使用io_uring接口而不是传统的IO引擎如sync、libaio等来处理所有IO操作(如读写、同步)。-作用:-性能提升:由于io_uring的异步性质,fio可以生成和处理更多的IO请求,提高吞吐量和降低延迟。-减少系统开销:io_uring使用环形缓冲区减少系统调用次数,优化了流程。-支持directio:根据[^2],io_uring主要支持O_DIRECT(直接IO),绕过缓存,因此测试的是设备的原始性能。-对bufferedio的限制:io_uring不支持异步bufferedio,所以在fio中使用--ioengine=io_uring测试bufferedIO时,可能不是异步的或效率低下。fio中的具体行为:-在使用io_uring引擎时,fio会初始化一个io_uring实例(类似引用[4]中的代码),创建环形缓冲区。-fio提交IO请求作为SQE(SubmissionQueueEntries),然后等待CQE(CompletionQueueEntries)来处理完成事件。-这允许高度并行的IO测试。回答结构:-系统指令要求:回答使用中文;数学表达式用$...$;独立公式用$$...$$;参考引用末尾加引用标识如[^1];生成相关问题。-用户可见层:结构清晰,逐步解决问题。组织回答:1.介绍--ioengine=io_uring参数在fio中的基本作用。2.解释io_uring是什么,基于引用。3.具体功能和优势。4.潜在的限制。5.如何实际使用。6.基于引用添加细节。引用引用:-在相关地方添加[^1],[^2],[^3],[^4]。生成相关问题:-在最后添加几个相关问题,作为“§§相关问题§§”。最终回答结构:-开头简要说明。-分步骤解释。-列出相关问题。</think> ### fio 工具中 --ioengine=io_uring 参数的功能和作用 在 fio(Flexible I/O Tester)工具中,`--ioengine=io_uring` 参数指定 fio 使用 Linux 内核的 io_uring 接口作为底层 I/O 引擎来处理所有 I/O 操作(如读写、同步等)。io_uring 是一个现代异步 I/O 框架,旨在提供高性能 I/O 处理,通过减少系统调用开销和上下文切换来提高效率。以下我将一步步解释其功能、作用、优势及限制,参考站内引用的内容并添加引用标识。 #### 1. **核心功能:高性能异步 I/O 处理** - **什么是 io_uring?** io_uringLinux 内核提供的一种新型异步 I/O 机制,通过环形缓冲区(ring buffers)管理 I/O 请求的提交和完成。用户态应用程序(如 fio)可以将请求放入提交队列(Submission Queue,SQ),内核异步处理并返回完成结果到完成队列(Completion Queue,CQ)。例如,fsync 操作对应 `io_fsync_prep` 和 `io_fsync` 函数[^1],其他操作类似。 io_uring 使用 liburing 库(如 `io_uring_queue_init`)来简化开发,它封装了系统调用,实现高效初始化[^3]。性能测试显示,io_uring 比传统异步 I/O(如 libaio)快 150% 以上,逼近 SPDK 级别的高性能[^2]。 - **在 fio 中的作用** 当指定 `--ioengine=io_uring` 时,fio 使用 io_uring 引擎执行所有 I/O 操作: - **异步操作处理**:fio 通过初始化 io_uring 实例(如 `io_uring_queue_init`)创建环形缓冲区,然后将 I/O 请求批量提交到 SQ[^4]。这类似于引用中的示例: ```c struct io_uring ring; int ret = io_uring_queue_init(128, &ring, 0); // 初始化缓冲区大小为 128 struct io_uring_sqe *sqe = io_uring_get_sqe(ring); io_uring_prep_accept(sqe, sockfd, addr, addrlen, flags); // 准备操作 io_uring_submit(&ring); // 提交请求 // 等待和完成事件处理 ``` - **降低系统开销**:io_uring 通过批量提交请求减少了系统调用次数和上下文切换,特别适用于高并发场景(如测试数据库或文件系统的 I/O 性能)[^2]。 #### 2. **核心作用:提升性能和效率** - **性能优势**: - **高吞吐量和低延迟**:io_uring 支持高效并行处理,能在单个线程中处理数十万个 I/O 操作,大大提升 fio 测试的 IOPS(每秒 I/O 操作数)和带宽。例如,在 SSD 或 NVMe 设备上,io_uring 引擎的测试结果通常优于传统引擎(如 `libaio`)。 - **减少 CPU 开销**:通过环形缓冲区和异步处理,io_uring 避免了频繁的用户态-内核态切换,降低了 CPU 利用率[^2]。 > 数学表达:对于一个理想场景,假设 I/O 延迟为 $L$ 毫秒,传统同步引擎的开销为 $O(n)$(其中 n 是请求数量),而 io_uring 的开销接近 $O(1)$,这能显著优化性能指标如吞吐量 $\propto \frac{1}{L}$[^2]。 - **测试覆盖范围**: - **支持 Direct I/O**:io_uring 主要适用于 Direct I/O(O_DIRECT),它绕过内核缓存直接访问硬件设备[^2]。这在高并发或低延迟测试中非常有用,因为消除了缓存干扰,测试的是设备的真实性能。 - **对 Buffered I/O 的限制**:io_uring 无法直接支持异步 Buffered I/O,因此 fio 在 `--ioengine=io_uring` 模式下测试 Buffered I/O 时,实际为同步模式(效率下降)。引用指出:“最大的限制无疑是仅支持 direct io。而 O_DIRECT 存在 bypass 缓存和 size 对齐等限制...针对 buffered io,其表现为同步”[^2]。 - **支持的操作类型**:除了读写,io_uring 还支持 fsync、accept、poll 等操作(如引用中的 `io_fsync_prep` 和 `io_uring_prep_accept`)[^1][^4],可用于测试文件同步或网络 I/O。 #### 3. **使用场景和最佳实践** - **何时使用**: - **高并发和低延迟需求**:当测试存储设备(如 SSDs 或云存储)在高负载下的性能时。 - **Linux 内核支持**:io_uring 需要 Linux 5.1+ 内核。使用 fio 命令如下: ```bash fio --name=test --ioengine=io_uring --direct=1 --rw=randread --bs=4k --numjobs=8 # 测试 8 线程的随机读 ``` 这里 `--direct=1` 强制使用 O_DIRECT,避免因 Buffered I/O 导致的性能下降[^2]。 - **liburing 依赖**:fioio_uring 引擎背后使用 liburing 库(如 `io_uring_queue_init`)管理环形缓冲区[^3]。确保系统已安装此库(通过 `apt-get install liburing-dev` 或类似命令)。 - **限制和注意事项**: - **Size 对齐要求**:O_DIRECT 要求 I/O 大小和内存地址对齐(通常为 4K),否则可能失败或性能下降[^2]。 - **缓冲区和队列大小**:初始化时需设置合理的队列大小(如 `io_uring_queue_init(128, &ring, 0)`),大小过小会导致排队延迟,过大则浪费资源[^4]。 - **兼容性**:旧内核或一些文件系统(如 NFS)可能不完全支持 io_uring 的所有功能。 #### 4. **与替代引擎的对比** - **vs libaio**:io_uring 比 libaio 延迟更低,因为 libaio 需要每个请求单独系统调用,而 io_uring 批量处理。 - **vs sync 或 psync**:传统同步引擎效率低,仅适合简单测试,而 io_uring 适用于大规模压力测试[^2]。 - **整体优势**:在 fio 中使用 `--ioengine=io_uring` 能实现接近硬件的性能上限,是存储性能分析的黄金标准。 通过以上分析,fio 的 `--ioengine=io_uring` 功能核心在于利用 io_uring 的高效异步模型提升测试性能,但必须注意其对 Direct I/O 的限制和对齐要求。引用内容提供了详细的技术实现参考,如 io_uring 的 prep/exec 函数和 liburing 的使用[^1][^3][^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值