感觉目前看到介绍 io_uring 的文章还是比较少,大部分都集中在对其原理性的介绍和简单的对官方文档的翻译,真正结合实际的例子还是比较少。本文翻译整理自一篇博客:
io-uring-by-example-part-1-introduction
我也增加了一些自己的理解和其他的参考材料。另外,在 2020 年,C++ 也正式将协程 coroutine 加入标准,我尝试使用 io_uring 和 c++20 协程实现了一个高性能web服务器,并进行了一些性能测试,具体代码会放在这个仓库里面,同时也包含了这篇文档以及所需的 demo 代码:
https://github.com/yunwei37/co-uring-WebServergithub.com/yunwei37/co-uring-WebServer
事实上,只有 I/O 和计算是计算机真正做的两件事。在 Linux 下,对于计算,您可以在进程或线程之间进行选择;对于 I/O,Linux 既有同步 I/O,也称为阻塞 I/O,和异步 I/O。尽管异步 I/O(aio系统调用系列)已经成为 Linux 的一部分有一段历史了,但它们仅适用于直接 I/O 而不适用于缓冲 I/O。对于以缓冲模式打开的文件,aio就像常规的阻塞系统调用一样。这不是一个令人愉快的限制。除此之外,Linux 当前的aio 接口还有很多系统调用开销。
考虑到项目的复杂性,提出一个提供高性能异步 I/O 的 Linux 子系统并不容易,因此对 io_uring 的大肆宣传是绝对合理的。不仅io_uring提供了一个优雅的内核/用户空间接口,它还通过允许一种特殊的轮询模式,完全取消从内核到用户空间获取数据的系统调用,从而提供了卓越的性能。
然而,对于大多数的异步编程完全是另一回事。如果你已经试过在像 C 这样的低级语言中用select/ poll/epoll 异步编程,你会明白我的意思。我们不太擅长异步思考,换句话说,使用线程。线程有一个“从这里开始”、“做 1-2-3 件事”和“从这里结束”的进展。尽管它们被操作系统多次阻塞和启动,但这种错觉对程序员来说是隐藏的,因此它是一个相对简单的心理模型,可以吸收和适应您的需求。但这并不意味着异步编程很难:它通常是程序中的最低层。一旦你编写了一个抽象层出来,你就会很舒服并忙于做你的应用程序真正打算做的事情,你的用户主要关心的事情。
说到抽象,io_uring 确实提供了一个更高级的库 liburing,它实现并隐藏了很多io_uring 需要的模板代码,同时提供了一个更简单的接口供您处理。但是,如果不先了解 io_uring 底层是如何工作的,那么使用 liburing 的乐趣何在?知道了这一点,您也可以更好地使用 liburing:您会了解极端情况,并且可以更好地了解其背后工作的原理。这是一件好事。为此,我们将使用 liburing 构建大多数示例,但我们同时也会使用系统调用接口构建它们。
让我们以同步或阻塞的方式使用 readv() 系统调用,构建一个简单的 cat 等效命令。这将使您熟悉 readv(),它是启用分散/聚集 I/O 的系统调用集的一部分,也称为向量 I/O。如果您熟悉 readv() 工作方式,则可以跳到下一节。
比起 read() 和 write() 将文件描述符、缓冲区及其长度作为参数,readv() 和 writev() 将文件描述符、指向struct iovec结构数组的指针和最后一个表示该数组长度的参数作为参数。现在让我们来看看struct iovec。
- struct iovec {
- void *iov_base; /* 起始地址 */
- size_t iov_len; /* 要传输的字节数 */
- };
函数原型: ssize_t readv(int fd, const struct iovec iov, int iovcnt); ssize_t writev(int fd, const struct iovec iov, int iovcnt);
关于 readv/writev 的性能分析,可以参考 readv/writev分析 - 知乎
每个结构简单地指向一个缓冲区。一个基地址和一个长度。
您可能会问,比起常规 read() 和 write(),使用矢量或分散/收集 I/O 有什么意义。答案是使用 readv() 和 writev() 更自然。例如,使用readv(),您可以填充一个 struct 的许多成员,而无需求助于复制缓冲区或多次调用read(),这两种方法的效率都相对较低。同样的优势适用于writev(). 此外,这些调用是原子的,而多次调用read()和write()不是,如果您出于某种原因碰巧关心它。