Linux io_uring 怎样用异步超时限制慢请求
来源:17golang原创
时间:2026-11-08 04:03:34 356浏览 收藏
Linux io_uring 要给一个可能变慢的 I/O 请求设置上限,通常把请求 SQE 标记为 IOSQE_IO_LINK,再紧跟一个 IORING_OP_LINK_TIMEOUT。这样超时针对的是前一个链接请求,而不是把整个完成队列当成一个计时器。请求先完成时,超时 CQE 通常是 -ECANCELED;超时先到时,超时 CQE 是 -ETIME,原请求则会收到取消相关结果。
官方地址:https://docs.kernel.org/
IORING_OP_LINK_TIMEOUT必须通过IOSQE_IO_LINK绑定到前一个 SQE,不能单独当作普通完成队列定时器。- 两个 CQE 要靠各自的
user_data区分,res直接返回负的 errno,不要只看全局errno。 - 超时只解决等待上限,不保证底层磁盘 I/O 已经停止;缓冲区和超时结构体的生命周期仍由应用负责。
触发场景:为什么给单个请求加链接超时
在事件驱动程序里,慢请求最麻烦的地方不是“计时器不会用”,而是计时器到底作用于谁。普通的 IORING_OP_TIMEOUT 是面向完成队列等待者的超时;IORING_OP_LINK_TIMEOUT 则和一个前置 SQE 组成链接,专门限制这一个请求。
因此,适合的场景是“读一个可能卡住的文件描述符”“等待一个可能迟到的 socket 操作”或“给一段链路中的单个操作设置上限”。它不等于强制终止内核已经开始执行的所有工作:对于不可中断的磁盘操作,应用仍要按 CQE 结果决定是否丢弃结果、重试或记录超时。

权限配置:准备 I/O SQE 和超时结构
liburing 把底层 SQE 的字段封装成准备函数。关键约束有三个:先准备实际 I/O 请求,再给它设置 IOSQE_IO_LINK;超时 SQE 要紧跟在它后面;两者必须在同一次 io_uring_submit 中送入内核。
#include
#include
// 为一个读请求准备链接超时;timeout 由调用方持有,不能在未完成前失效。
static int prepare_read_with_timeout(struct io_uring *ring,
int fd,
void *buffer,
unsigned length,
struct __kernel_timespec *timeout) {
struct io_uring_sqe *read_sqe = io_uring_get_sqe(ring);
if (read_sqe == NULL) {
return -ENOSPC; // SQ 环已没有可用的提交项。
}
// 第一个 SQE 是真正的读请求,user_data 用来识别它的 CQE。
io_uring_prep_read(read_sqe, fd, buffer, length, 0);
read_sqe->user_data = 1001;
read_sqe->flags |= IOSQE_IO_LINK;
struct io_uring_sqe *timeout_sqe = io_uring_get_sqe(ring);
if (timeout_sqe == NULL) {
return -ENOSPC; // 超时项拿不到时,不能提交不完整的链路。
}
// 第二个 SQE 只约束前一个请求,不统计整个 CQ 的完成数量。
io_uring_prep_link_timeout(timeout_sqe, timeout, 0);
timeout_sqe->user_data = 1002;
return 0;
}
// 调用方需要在合适的时机提交,并检查 submit 返回值。
static int submit_read_with_timeout(struct io_uring *ring,
int fd,
void *buffer,
unsigned length,
struct __kernel_timespec *timeout) {
int prepared = prepare_read_with_timeout(ring, fd, buffer, length, timeout);
if (prepared
这个片段故意把 timeout 作为调用方参数,而不是在函数内部创建栈变量。普通提交模式下,超时结构至少要保持到提交完成;如果启用了 SQPOLL,还要按当前 liburing/内核文档的要求延长它的有效期。实际读写缓冲区也不能在原请求的 CQE 到来前释放。
流水线阶段:把请求和超时放进同一条链路
链路的静态关系可以概括为“前一个请求 + 一个链接超时”。io_uring_prep_link_timeout 的超时参数可以是相对时间;不指定其他时钟标志时,相关文档使用单调时钟。不要把它写成单独提交的普通 timeout,否则它就不再明确绑定刚才那个 I/O 请求。
#include
// 这个示例只展示调用关系;fd、buffer 和 ring 的创建由上层负责。
int submit_bounded_read(struct io_uring *ring, int fd, char *buffer, unsigned size) {
struct __kernel_timespec timeout = {
.tv_sec = 2, // 最多等待两秒,示例使用相对时间。
.tv_nsec = 0 // 纳秒部分保持为合法范围内的值。
};
// timeout 在请求完成前仍由当前作用域持有,实际异步程序应把它放在更长生命周期对象中。
return submit_read_with_timeout(ring, fd, buffer, size, &timeout);
}
上面的简化函数只用于解释组合关系:如果 ring 使用 SQPOLL,函数返回后栈上的 timeout 可能过早失效,生产代码应把它放到请求对象、堆内存或其他可证明存活的存储里。这个生命周期问题和“是否调用了 submit”是两件不同的事。
门禁规则:按两个 CQE 解释结果
提交后不要只等待一个结果就把请求对象释放。链路通常会产生两个可识别的 CQE:一个来自原始 I/O,一个来自链接超时。用 user_data 区分来源,再根据 cqe->res 判断是正常完成、超时还是取消。
#include
#include
// 读取一条 CQE,并把业务请求和超时请求分开解释。
static int consume_one_cqe(struct io_uring *ring) {
struct io_uring_cqe *cqe = NULL;
int wait_result = io_uring_wait_cqe(ring, &cqe);
if (wait_result user_data == 1001) {
if (cqe->res >= 0) {
printf("read completed: %d bytes\n", cqe->res);
} else if (cqe->res == -ECANCELED || cqe->res == -EINTR) {
printf("read was canceled or interrupted: %d\n", cqe->res);
} else {
printf("read failed: %d\n", cqe->res); // res 已经是负 errno。
}
} else if (cqe->user_data == 1002) {
if (cqe->res == -ETIME) {
printf("link timeout expired\n"); // 超时先到,链路请求被尝试取消。
} else if (cqe->res == -ECANCELED) {
printf("link timeout canceled by request completion\n");
} else {
printf("timeout result: %d\n", cqe->res);
}
}
io_uring_cqe_seen(ring, cqe); // 每个取出的 CQE 都要归还给 CQ 环。
return 0;
}
这里的“门禁”不是再发一个探测请求,而是把状态解释完整:原请求成功时,业务可以消费数据;原请求被取消时,业务不能把缓冲区里的旧内容当成新结果;超时 CQE 为 -ECANCELED 时,说明计时器因前置请求完成而被取消,并不表示业务请求失败。

失败处理:区分超时、取消和底层 I/O 失败
最常见的误判是看到 -ETIME 就认为“原请求已经安全停止”。链接超时会尝试取消前置请求,但不同 I/O 类型的可取消性不同,已经在内核中开始的磁盘操作不一定能瞬间消失。程序应把“本次请求对调用方超时”与“底层工作已经完全终止”分开记录。
| 观察位置 | 典型结果 | 处理建议 |
|---|---|---|
| 原请求 CQE | 非负值 | 按实际字节数或操作结果消费数据 |
| 原请求 CQE | -ECANCELED 或 -EINTR | 丢弃未确认数据,释放请求资源 |
| 超时 CQE | -ETIME | 记录超时并决定重试、降级或返回上层 |
| 超时 CQE | -ECANCELED | 前置请求已完成或链路被取消,不能当成超时 |
如果上层需要重试,必须先确认旧请求的所有 CQE 和缓冲区生命周期,避免同一块内存被旧请求继续写入时又交给新请求。对于不可安全重试的写操作,更应该把超时当成“结果未知”的状态,交给幂等键、日志或业务补偿机制处理,而不是机械地再次提交。
通知复盘:给超时链路留下可追踪记录
线上排查时建议同时记录请求的 user_data、提交时间、超时预算、原请求 CQE 结果和超时 CQE 结果。这样能回答三个不同问题:请求有没有提交、是否先收到超时、内核最终给原请求返回了什么。
request_id=1001 timeout_id=1002 timeout_ms=2000 read_res=-125 timeout_res=-62 state=timed_out request_id=1001 timeout_id=1002 read_res=4096 timeout_res=-125 state=completed
复盘时不要只统计超时数量,还要区分“计时器正常取消”和“计时器真正到期”。前者通常说明请求已经在预算内完成;后者才说明调用方的等待预算被消耗。若超时持续升高,再结合文件系统、网络和队列深度指标定位原因。
相关问题与边界
IORING_OP_TIMEOUT 和 IORING_OP_LINK_TIMEOUT 有什么区别?
前者是面向完成队列等待的普通超时,可以按时间或完成事件数量触发;后者必须链接到前一个请求,作用对象是那条请求链里的操作。
链接超时一定会让磁盘 I/O 立即停止吗?
不一定。它会对链接请求发起取消处理,但底层操作能否及时中止取决于请求类型和当时是否已经开始执行。应用应等待原请求 CQE,再释放缓冲区。
为什么 timeout CQE 是 -ECANCELED?
这通常表示原请求已经完成,计时器因不再需要而被取消;真正到期的 timeout CQE 通常是 -ETIME。
能不能把 timeout 结构体定义在提交函数的栈上?
只有在能证明内核读取它之前栈帧仍有效时才安全。使用 SQPOLL 或其他异步提交方式时,应让结构体的生命周期覆盖实际需要的阶段。
总结一下:先把请求 SQE 标记为 IOSQE_IO_LINK,再紧跟 IORING_OP_LINK_TIMEOUT,两者一起提交;随后按两个 user_data 分别解释 CQE,并把超时预算、取消结果和底层 I/O 是否真正停止分开处理。
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
367 收藏
-
226 收藏
-
294 收藏
-
369 收藏
-
225 收藏
-
104 收藏
-
232 收藏
-
439 收藏
-
479 收藏
-
316 收藏
-
333 收藏
-
451 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习