在Linux上真的没有异步块I/O吗?

56
考虑一个CPU繁忙但还具有高性能I/O要求的应用程序。
我正在比较Linux文件I/O和Windows,我看不出epoll如何帮助Linux程序。内核会告诉我文件描述符“准备好读取”,但我仍然需要调用阻塞的read()来获取数据,如果我要读取数兆字节,那么很明显会被阻塞。
在Windows上,我可以创建设置为OVERLAPPED的文件句柄,然后使用非阻塞I/O,当I/O完成时得到通知并使用该完成函数中的数据。我不需要花费任何应用级墙钟时间等待数据,这意味着我可以精确地将我的线程数量调整为我的核心数量,并获得100%有效的CPU利用率。
如果我必须在Linux上模拟异步I/O,那么我必须分配一些线程来执行此操作,这些线程将花费一点时间执行CPU任务,大部分时间则用于I/O阻塞,而且在与这些线程进行通信时还会产生开销。因此,我要么超额订阅或未充分利用我的CPU核心。
我看过mmap() + madvise()(WILLNEED)作为“穷人的异步I/O”,但它仍然不能完全实现异步I/O,因为我无法在完成时收到通知 - 我必须“猜测”,如果我猜错了,我将被阻塞在内存访问上,等待数据从磁盘中读取。
Linux似乎在io_submit中具有异步I/O的开端,而且似乎还具有用户空间POSIX aio实现,但它已经存在一段时间了,我不知道是否有人会为这些系统保证关键的高性能应用程序。
Windows模型大致如下: 1. 发出异步操作。 2. 将异步操作绑定到特定的I/O完成端口。 3. 等待该端口上的操作完成。 4. 当I/O完成时,正在等待端口的线程解除阻塞,并返回对挂起的I/O操作的引用。步骤1/2通常作为单个步骤完成。步骤3/4通常使用一组工作线程完成,而不是发出I/O的同一线程(可能不同)。此模型与boost::asio提供的模型有些相似,但boost::asio实际上没有提供异步块状(磁盘)I/O。
与Linux中的epoll的区别在于,在第4步中还没有进行任何I/O —— 它将步骤1推迟到步骤4之后进行,“反向”的方式,如果您已经知道所需功能,则“反向”。
由于我编写过大量嵌入式、桌面和服务器操作系统的程序,我可以说这种异步 I/O 模型对于某些类型的程序非常自然。它也具有非常高的吞吐量和低的开销。我认为这是 Linux I/O 模型在 API 层面上仍然存在的一个真正缺陷之一。

1
我完全不了解MS Windows模型,所以无法进行比较,但我想指出的是,如果您正在使用任何形式的select/poll/epoll/kqueue,那么当您收到文件描述符准备好的通知时,跟进阻塞的read/write将非常不寻常。在这种情况下,您几乎肯定希望进行非阻塞的readwrite - Celada
4
select()是为套接字发明的,与recv()和send()系统调用一起使用,如果select()将它们作为就绪返回,则保证不会阻塞--但缺点是I/O量无法保证。你可能只得到几个字节。问题在于,这种模型不能做出反应。非阻塞 I/O 不高效,因为它要求内核预取 "一定数量" 的数据,而内核不知道你需要多少数据。如果你只需要一页,而内核获取了一兆字节,你就输了。如果你需要一兆字节,而内核只获取了一页,你也会输。 - Jon Watte
可能是Linux磁盘文件AIO的重复问题。 - J-16 SDiZ
@JonWatte 我相信你会发布一个修改后的读取并要求一定数量字节的命令,然后在缓冲区被填满时收到通知。 - LtWorf
@LtWorf:那是异步的“read()”模型。我正在寻找异步(而不是非阻塞)的“recv()”模型。 - Jon Watte
通过使用自适应大小的线程池,您将不会有很多阻塞线程,只有少量的线程在执行实际工作。 - peterh
4个回答

81

(2020) 如果你使用的是5.1或以上版本的Linux内核,你可以使用io_uring接口进行类似文件的I/O操作,并获得出色的异步操作。

与现有的libaio/KAIO接口相比,io_uring具有以下优势:

  • 在进行缓冲I/O时保留异步行为(而不仅仅是直接I/O时)
  • 更易于使用(特别是在使用liburing辅助库时)
  • 可以选择以轮询方式工作(但您需要更高的权限才能启用此模式)
  • 每个I/O的簿记空间开销较小
  • 由于用户空间/内核系统调用模式切换较少而具有较低的CPU开销(这些天由于spectre/meltdown mitigations的影响而非常重要)
  • 文件描述符和缓冲区可以预先注册以节省映射/取消映射时间
  • 更快(可以实现更高的聚合吞吐量,I/O具有较低的延迟)
  • “链接模式”可以表达I/O之间的依赖关系(>=5.3内核)
  • 可以与基于套接字的I/O一起使用(>=5.3支持recvmsg()/sendmsg(),请参见提到单词support的消息io_uring.c's git history
  • 支持尝试取消排队的I/O(>=5.5)
  • 可以请求始终从异步上下文中执行I/O,而不仅仅是默认情况下在内联提交路径触发阻塞时回退到将I/O传递给异步上下文(>=5.6内核)
  • 支持执行异步操作超出read/write(例如fsync(>=5.1),fallocate(>=5.6),splice(>=5.7)等)
  • 更高的开发动力
  • 每次星星没有完美对齐时都不会变成阻塞状态

相比于glibc的POSIX AIO,io_uring具有以下优点:

使用 io_uring 进行高效的 IO 文档详细介绍了 io_uring 的优点和用法。io_uring 的新特性 文档描述了在 5.2-5.5 内核之间添加的新功能,而io_uring 的快速增长 LWN 文章则描述了每个 5.1-5.5 内核中可用的功能,并展望了 5.6 中将会有什么(也请参见LWN 的 io_uring 列表文章)。此外,io_uring 作者 Jens Axboe 在 2019 年底发布了通过 io_uring 进行更快 IO 的内核示例视频演示幻灯片),并在 2022 年中期发布了io_uring 的新特性内核示例视频演示幻灯片)。最后,io_uring 教程大师介绍了 io_uring 的用法。

< p > io_uring 社区可以通过 io_uring邮件列表 联系,io_uring邮件列表档案 显示了2021年初的日常流量。

关于“支持recv()read()的部分I/O”的问题:一个补丁已经进入了5.3内核,可以自动重试io_uring短读取, 另一个提交进入了5.4内核,当处理没有设置REQ_F_NOWAIT标志的"常规"文件时,调整了行为以仅自动处理短读取(看起来你可以通过IOCB_NOWAIT或通过使用O_NONBLOCK打开文件来请求REQ_F_NOWAIT)。因此,你也可以从io_uring获得recv()风格的“短”I/O行为。

使用io_uring的软件/项目

虽然界面还很年轻(第一版于2019年5月发布),但一些开源软件正在“野外”使用io_uring


看起来非常棒,现在只需要主流的“可用”发行版也使用5.1内核,这取决于哪个发行版,可能会在2020年至2022年之间(Ubuntu和Fedora目前为5.0,Debian为4.19,SuSE为4.12)。展望未来,这将是一个很好的补充。 - Damon
Fedora(https://fedoraproject.org/wiki/Kernel_Vanilla_Repositories)和Ubuntu(https://wiki.ubuntu.com/Kernel/MainlineBuilds)都提供扩展仓库,其中包含**vanilla**内核,最高版本为5.3(但显然启用任何额外的仓库都存在风险等)。SUSE似乎也有类似的东西(https://software.opensuse.org/package/kernel-vanilla),但不清楚里面有什么。我猜Ubuntu 20.10可能会有一个合适的内核,而Fedora会定期重新制作内核,因此Fedora 30很可能会获得5.1之后的内核(这意味着Fedora 31肯定会有一些东西)。 - Anon

18

彼得·张间接指出的真正答案基于io_setup()和io_submit()。具体来说,彼得所提到的"aio_"函数是基于线程的glibc用户级仿真的一部分,这不是一种有效的实现方式。真正的答案在:

io_submit(2)
io_setup(2)
io_cancel(2)
io_destroy(2)
io_getevents(2)
请注意,日期为2012-08的man页中指出,此实现尚未发展到可以替换glibc用户空间模拟的程度:

http://man7.org/linux/man-pages/man7/aio.7.html

这个实现还没有发展到使用内核系统调用完全重新实现POSIX AIO实现的程度。

因此,根据我所能找到的最新内核文档,Linux仍然没有成熟的基于内核的异步I/O模型。即使我假设文档中记录的模型已经成熟,它仍然不支持类似recv()vs read()的部分I/O操作。


在Linux内核中,哲学是没有什么是“稳定”和成熟的,因为社区总是准备在必要时更改其API。但是像http://man7.org man页面这样发布到用户空间的任何内容,内核都必须遵循并且永远不会破坏用户空间。这个声明来自Linus本人。但是如果你现在说内核(在这里阅读:http://lxr.free-electrons.com/source/include/linux/syscalls.h line 474)有系统调用API,那么我们可以始终使用syscall()直接调用内核,而不管io_submit()(及其系列)API是否已经在glibc中实现。 - Peter Teoh
2
我对你的回答有问题,因为你推荐“aio_xxx()”函数API作为“异步I/O API”。然而,该API实际上并不是由Linux系统调用支持,而是使用glibc中的用户空间线程实现的。至少根据手册是这样说的。因此,“aio_xxx()”函数不是问题的答案——它们是问题的原因。正确的答案是以“io_setup()”开头的函数。 - Jon Watte
通过 aio_xxx(),我一直在指的是内核 API。因此,当然没有针对它的“Linux 系统调用”标准,也没有任何 manpages(即官方 Linux API),严格来说,这些只适用于用户空间。总的来说,所有内核 API 都不会列入任何“标准 API”列表,因为人们经常说(例如在邮件列表中 https://lkml.org/lkml/2006/6/14/164),内核 API 是相当流动的,但更改也不容易被批准。 - Peter Teoh

3

如下所述:

http://code.google.com/p/kernel/wiki/AIOUserGuide

而在这里:

http://www.ibm.com/developerworks/library/l-async/

Linux在内核级别提供了异步块I/O,API如下:

aio_read    Request an asynchronous read operation
aio_error   Check the status of an asynchronous request
aio_return  Get the return status of a completed asynchronous request
aio_write   Request an asynchronous operation
aio_suspend Suspend the calling process until one or more asynchronous requests have completed (or failed)
aio_cancel  Cancel an asynchronous I/O request
lio_listio  Initiate a list of I/O operations

如果您问这些API的用户是谁,那就是内核本身 - 这里只展示了一个很小的子集:

./drivers/net/tun.c (for network tunnelling):
static ssize_t tun_chr_aio_read(struct kiocb *iocb, const struct iovec *iv,

./drivers/usb/gadget/inode.c:
ep_aio_read(struct kiocb *iocb, const struct iovec *iov,

./net/socket.c (general socket programming):
static ssize_t sock_aio_read(struct kiocb *iocb, const struct iovec *iov,

./mm/filemap.c (mmap of files):
generic_file_aio_read(struct kiocb *iocb, const struct iovec *iov,

./mm/shmem.c:
static ssize_t shmem_file_aio_read(struct kiocb *iocb,

在用户空间级别,也有io_submit()等API(来自glibc),但以下文章提供了一种不使用glibc的替代方法:

等等。

http://www.fsl.cs.sunysb.edu/~vass/linux-aio.txt

它直接实现了诸如io_setup()之类的API函数作为直接系统调用(绕过glibc依赖),应存在通过相同的“__NR_io_setup”签名进行内核映射。在搜索内核源代码时,请访问以下网址:http://lxr.free-electrons.com/source/include/linux/syscalls.h#L474(该URL适用于最新版本3.13),您将看到这些io_*() API在内核中的直接实现。
474 asmlinkage long sys_io_setup(unsigned nr_reqs, aio_context_t __user *ctx);
475 asmlinkage long sys_io_destroy(aio_context_t ctx);
476 asmlinkage long sys_io_getevents(aio_context_t ctx_id,
481 asmlinkage long sys_io_submit(aio_context_t, long,
483 asmlinkage long sys_io_cancel(aio_context_t ctx_id, struct iocb __user *iocb,

较新版本的glibc应该使得使用"syscall()"调用sys_io_setup()变得不必要,但如果你正在使用这些"sys_io_setup()"能力的较新内核而没有最新版本的glibc,则始终可以自己进行这些调用。当然,还有其他用户空间选项可用于异步I/O(例如,使用信号?):

http://personal.denison.edu/~bressoud/cs375-s13/supplements/linux_altIO.pdf

或者说:

什么是POSIX异步I/O(AIO)的状态?

"io_submit"和相关功能在glibc中仍然不可用(请参阅io_submit手册页),我已经在我的Ubuntu 14.04上进行了验证,但此API仅适用于Linux。

其他类似的库,如libuv、libev和libevent也是异步API:

http://nikhilm.github.io/uvbook/filesystem.html#reading-writing-files

http://software.schmorp.de/pkg/libev.html

http://libevent.org/

所有这些API都旨在可移植到BSD、Linux、MacOSX甚至Windows。
就性能而言,我没有看到任何数字,但我怀疑libuv可能是最快的,因为它很轻量级?

https://ghc.haskell.org/trac/ghc/ticket/8400


谢谢你的回答。看起来aio_suspend()不能与事件或选择式I/O混合使用。例如:没有recv()的aio版本吗? - Jon Watte
不是真的。如果你在谷歌上搜索“异步接收”(不带引号),你会得到很多答案,比如这个网址:http://www.wangafu.net/~nickm/libevent-book/01_intro.html。其中提供了一个非阻塞recv()的例子,通过使用“fnctl()”。但是它强调现在程序会快速进入循环,占用所有CPU周期。select()和libevent API是提出的解决上述问题的替代方案。 - Peter Teoh
1
我非常熟悉非阻塞套接字I/O。非阻塞与异步不同。而read也不同于recv。我正在寻找的是一种通过相同的“排队请求,稍后获取有关已完成排队请求的通知”机制将所有I/O推送的方法,例如Windows上的I/O完成端口。就像我在问题中所述的那样;-) - Jon Watte
1
以上列表中缺少的一个函数是io_getevents()。有了这个补充,原语集开始看起来更像我所期望的了。现在,如果有一个来自某个强大的用户级应用程序的清晰代码示例,那将让我的一天! - Jon Watte
3
实际上,经过更多阅读,"The current Linux POSIX AIO implementation is provided in user space by glibc."并不是一个准确的答案。正确的答案基于io_setup()和io_submit()函数。 - Jon Watte
用户空间实现的 aio 性能不佳,但对于简单的用途来说通常足够好。为了获得更好的结果,您可以使用 aio_init(仅限于 glibc)增加线程数,并且要在 poll/ppoll/epoll 线程中获得完成通知,您可以使用基于信号的通知和 signalfd 或通过信号处理程序中的原子变量处理调用中断。尽管是用户空间实现,但它确实是异步的。 - nfries88

1

对于网络套接字 I/O,当它“就绪”时,它不会阻塞。这就是“O_NONBLOCK”和“就绪”的含义。

对于磁盘 I/O,我们有 posix aiolinux aiosendfile 等相关工具。


1
posix aio在glibc中使用线程在用户空间中实现,因此它不是真正的AIO。Linux AIO(io_submit)是我想了解更多的内容,但我没有看到任何人实际上将其用于任何事情,这对我来说意味着那里可能存在问题。sendfile()与异步基于磁盘的I/O无关。如果您的答案实际上有助于解决问题,我很乐意接受它 - 但请注意,我已经在我的问题中提到了io_submit。 - Jon Watte
7
Linux AIO 并不是没有使用过……例如,InnoDB (MySQL) 使用 io_submit - J-16 SDiZ
此外,当涉及到网络套接字时,您应该注意read()和recv()之间的区别。io_submit()似乎支持read()语义,而不是recv()语义。如果您使用recv()和适当的select()或事件通知,则永远不需要为套接字使用O_NONBLOCK。 - Jon Watte

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接