Linux I/O 演进

Buffered I/O

缓存 I/O 又被称作标准 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。 在 Linux 的缓存 I/O 机制中,这种访问文件的方式是通过两个系统调用实现的:read() 和 write()。调用read()时,如果 操作系统内核地址空间的页缓存( page cache )有数据就读取出该数据并直接返回给应用程序,如果没有就从磁盘读取数据到页缓存。然后再从页缓存拷贝到应用程序的地址空间。调用write()时,数据会先从应用程序的地址空间拷贝到 操作系统内核地址空间的页缓存,然后再写入磁盘。根据Linux的延迟写机制,当数据写到操作系统内核地址空间的页缓存就意味write()完成了,操作系统会定期地将页缓存的数据刷到磁盘上。

Direct I/O

凡是通过直接 I/O 方式进行数据传输,数据均直接在用户地址空间的缓冲区和磁盘之间直接进行传输,中间少了页缓存的支持。操作系统层提供的缓存往往会使应用程序在读写数据的时候获得更好的性能,但是对于某些特殊的应用程序,比如说数据库管理系统这类应用,他们更倾向于选择他们自己的缓存机制,因为数据库管理系统往往比操作系统更了解数据库中存放的数据,数据库管理系统可以提供一种更加有效的缓存机制来提高数据库中数据的存取性能。Java 中目前是没有直接支持 Direct I/O的,只支持Buffer I/O。我们可以通过JNA 来实现其支持

阻塞式 I/O

Linux 内核提供了基于文件描述符的系统调用, 这些描述符指向的可能是存储文件(storage file),也可能是 network sockets,read()/write(),二者称为阻塞式系统调用(blocking system calls),因为程序调用 这些函数时会进入 sleep 状态,然后被调度出去(让出处理器),直到 I/O 操作完成。支持bufferd io和direct io。

  • 如果数据在文件中,并且文件内容已经缓存在 page cache 中,调用会立即返回;
  • 如果数据在另一台机器上,就需要通过网络(例如 TCP)获取,会阻塞一段时间;
  • 如果数据在硬盘上,也会阻塞一段时间。

非阻塞式 I/O

阻塞式之后,出现了一些新的、非阻塞的系统调用,例如 select()、poll() 以及更新的 epoll()。 应用程序在调用这些函数读写时不会阻塞,而是立即返回,返回的是一个 已经 ready 的文件描述符列表。但这种方式存在一个致命缺点:只支持 network sockets 和 pipes —— epoll() 甚至连 storage files 都不支持。支持bufferd io和direct io。

线程池方式(POSIX IO)

对于 storage I/O,经典的解决思路是 thread pool: 主线程将 I/O 分发给 worker 线程,后者代替主线程进行阻塞式读写,主线程不会阻塞。这种方式的问题是线程上下文切换开销可能非常大。支持bufferd io和direct io。

异步 IO(AIO)

异步IO也是非阻塞的IO,随着存储设备越来越快,主线程和 worker 线性之间的上下文切换开销占比越来越高。 现在市场上的一些设备,例如 Intel Optane ,延迟已经低到和上下文切换一个量级(微秒 us)。换个方式描述, 更能让我们感受到这种开销: 上下文每切换一次,我们就少一次 dispatch I/O 的机会。仅支持direct io
因此,Linux 2.6 内核引入了异步 I/O(asynchronous I/O)接口, 方便起见,本文简写为 linux-aio。AIO 原理是很简单的:

  1. 调用 io_setup 创建一个 I/O context 用于提交和收割 I/O 请求。
  2. 创建 1~n 和 I/O 请求,调用 io_submit 提交请求(用户线程继续运行其他任务)。
  3. 内核异步调用DMA执行I/O 请求执行完成,通过 DMA 直接将数据传输到 user buffer(DMA传输完成后,通过interrupt 方式通知内核)。
  4. 用户线程调用 io_getevents 收割已完成的 I/O。
  5. 重新执行第 2 步,或者确认不需要继续执行 AIO,调用 io_destroy 销毁 I/O context。

异步 I/O 框架 io_uring

  1. 统一了 Linux 异步 I/O 框架,io_uring 支持存储文件(direct io)和网络文件(socket),也支持更多的异步系统调用 (accept/openat/stat/...),而非仅限于 read/write 系统调用。
  2. 在设计上是真正的异步 I/O,它在系统调用上下文中就只是将请求放入队列, 不会做其他任何额外的事情,保证了应用永远不会阻塞
  3. 灵活性和可扩展性非常好,虽然 io_uring 与 aio 有一些相似之处,但它的扩展性和架构是革命性的: 它将异步操作的强大能力带给了所有应用(及其开发者),而 不再仅限于是数据库应用这一细分领域。

每个 io_uring 实例都有两个环形队列(ring)提交队列,完成队列,在内核和应用程序之间共享:
两个队列都是单生产者、单消费者,提供无锁接口,内部使用 内存屏障做同步
io_uring 这种请求方式还有一个好处是:原来需要多次系统调用(读或写),现在变成批处理一次提交。

参考:
浅谈Buffer I/O 和 Direct I/O
Linux 文件 I/O 进化史(三):Direct I/O 和 Linux AIO
[译] Linux 异步 I/O 框架 io_uring:基本原理、程序示例与性能压测(2020)
Asynchronous I/O Support in Linux 2.5

posted @   sahara-随笔  阅读(69)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
点击右上角即可分享
微信分享提示