大家好!我是大聪明-PLUS!
io_uring 最初在 Linux 5.1 中引入,它显著改变了异步 I/O 的方式。与 epoll 或 AIO 等传统机制不同,io_uring 使用共享环形缓冲区在用户空间和内核之间交换请求和结果。这减少了系统调用次数,最大限度地减少了上下文切换,并实现了高吞吐量。在本文中,我们将深入探讨 io_uring 架构,解释其关键扩展,并演示如何基于它构建高性能网络服务。
1. io_uring 的工作原理
io_uring 由两个环形缓冲区组成:提交队列 (SQ) 和完成队列 ( CQ )。应用程序填充提交队列条目 (SQE) 结构并递增 SQ 尾部索引,内核自行处理这些请求,并将结果放入完成队列条目 (CQE),从而更新 CQ 尾部。共享 mmap 缓冲区的存在使应用程序可以避免每次 I/O 请求都进行昂贵的系统调用。
基本系统
-
io_uring_setup()--- 分配环形缓冲区并初始化环。 -
io_uring_enter()--- 通知内核有关新的 SQE,并在必要时在等待 CQE 时进行阻塞。 -
io_uring_register()--- 注册文件、缓冲区和 BPF 程序以加快工作速度。
io_uring 内部使用工作线程,可以在轮询和非轮询模式下运行。6.0 及以上版本引入了委托任务(IORING_SETUP_COOP_TASKRUN)和single‑issuer rings,可以减少多线程环境中的阻塞。
2. 环延伸和标志
有许多标志和扩展可用于微调 io_uring。最有趣的是:
-
IORING_SETUP_SQPOLL--- 内核会创建一个后台线程来独立读取 SQE,从而消除了
io_uring_enter()每个请求都需要进行系统调用的情况。这减少了开销,但需要将环分配给特定的内核(IORING_SETUP_SQ_AFF)。 -
IORING_SETUP_COOP_TASKRUN--- 允许在应用程序上下文中执行已完成的操作,从而减少上下文切换。
-
IORING_REGISTER_BUFFERS并且IORING_REGISTER_FILES- 注册缓冲区和文件减少了每次 I/O 的成本,因为内核不需要提交内存页面并查找文件描述符。
-
多重操作 (
IORING_OP_ACCEPT,IORING_OP_RECV和IOSQE_IO_LINK)允许从单个 SQE 处理多个事件,这对于网络服务器特别有用。 -
零拷贝转发 (函数
send_zc()和IORING_REGISTER_PBUF_RING)允许内核将指向用户缓冲区的指针传递到网络适配器,而无需复制它们。
这些选项的正确组合可以显著加快高负载服务的速度。
3. 带有 liburing 的 echo 服务器示例
让我们看一个简化的回显服务器,它处理新的连接和消息
`#include
#include
#include
#define MAX_EVENTS 1024
int main() {
int srv_fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = { .sin_family = AF_INET, .sin_port = htons(12345), .sin_addr.s_addr = INADDR_ANY };
bind(srv_fd, (struct sockaddr*)&addr, sizeof(addr));
listen(srv_fd, SOMAXCONN);
struct io_uring ring;
io_uring_queue_init(MAX_EVENTS, &ring, 0);
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_multishot_accept(sqe, srv_fd, NULL, NULL, 0);
sqe->user_data = srv_fd;
io_uring_submit(&ring);
while (1) {
struct io_uring_cqe *cqe;
int ret = io_uring_wait_cqe(&ring, &cqe);
if (ret < 0) break;
int fd = cqe->res;
if (cqe->user_data == srv_fd) {
struct io_uring_sqe *sq = io_uring_get_sqe(&ring);
io_uring_prep_recv_multishot(sq, fd, NULL, 0, 0);
sq->user_data = fd;
io_uring_submit(&ring);
} else {
char buf[4096];
int n = recv(fd, buf, sizeof(buf), 0);
if (n <= 0) {
close(fd);
} else {
struct io_uring_sqe *sq2 = io_uring_get_sqe(&ring);
io_uring_prep_send(sq2, fd, buf, n, 0);
io_uring_submit(&ring);
}
}
io_uring_cqe_seen(&ring, cqe);
}
return 0;
}
`
这里我们使用io_uring_prep_multishot_accept()和io_uring_prep_recv_multishot()来重用单个 SQE 来处理多个连接和消息。在实际应用中,您应该注册缓冲区(io_uring_register_buffers())和文件,并处理错误。
4. 优化和最佳实践
为了充分利用 io_uring,请考虑以下准则:
-
资源注册 。用于
io_uring_register_buffers()缓冲区和io_uring_register_files()文件以减少开销。对于网络应用程序来说,这可能很有效io_uring_register_pbuf_ring()。 -
SQPOLL 线程亲和性 。如果启用
IORING_SETUP_SQPOLL,请将 CPU 亲和性设置为IORING_SETUP_SQ_AFF。这将减少处理器间的同步。 -
使用 multishot。无需在每个事件后设置新的 accept/recv ,而是使用 multishot 操作------这样可以节省 SQE。
-
零拷贝转发 。此功能
send_zc()与 io_uring 结合使用SO_ZEROCOPY,允许数据在网络上传输而无需复制;这对于 io_uring 尤其有效。 -
尽量减少系统调用 。在执行一个操作之前发出多个 SQE
io_uring_submit(),读取时使用io_uring_peek_cqe()一次调用迭代所有可用的 CQE。 -
可观察性 。将跟踪功能与
perf、bcc/BPF或集成ftrace,以监控延迟和错过的请求。libbpf 库简化了加载 eBPF 程序以进行 io_uring 监控的过程。
遵循这些做法将允许您的应用程序跨数十个核心扩展并每秒处理数百万个请求。
5. 调试和监控
尽管 API 很简单,但 io_uring 隐藏了许多复杂性。如需诊断,请使用:
-
strace****并 perf trace控制系统调用
io_uring_enter和io_uring_setup。 -
bpftool****并且 bpftrace------允许您编写 BPF 脚本并跟踪延迟,例如入口点
io_uring_queue_async_work。 -
cat /proc/sys/fs/io-uring/*- 检查系统参数,例如最大环尺寸。
-
内核标志 CONFIG_IO_URING_DEBUG- 启用附加消息
dmesg。 -
溢出指示器 。读取 CQE 时
cqe->flags & IORING_CQE_F_MORE,检查是否仍有待处理的事件,并IORING_CQE_F_BUFFER确定已使用了多少缓冲区。
请记住,io_uring 正在不断发展:新内核会带来新的标志和操作码。请定期更新您的内核和 liburing,以获取最新功能。
结果
--- 是 Linux 异步 I/O 开发的重要一步。凭借共享环、丰富的选项和零拷贝支持,它无需编写复杂的内核模块即可开发高性能文件和网络应用程序。然而,要有效地使用它,需要深入了解内核操作、仔细注册资源并进行细致的分析。
对于专业系统开发人员来说,学习 io_uring 是一个在 Linux I/O 中获得显著性能提升并为未来创新做好准备的机会。