宏观与微观角度看epoll+nonblock

宏观角度可以叫做全异步

微观角度还是同步 IO(数据到达后得到系统通知,然后同步执行 recv 取回数据,没有 iowait)

真正的异步 IO(AIO)应像 Windows IOCP 一样,传入文件句柄,缓存区,尺寸等参数和一个函数指针,当os真正完成了 IO 操作,再执行对应的函数

  1. 实际上 socket ,epoll 已经是最高效的模型了,虽然比 AIO 多一次 recv 系统调用,但总体来看没有任何 IO 等待,效率很高

  2. epoll 天然 reactor 模型,程序实现更容易。AIO 如 windows 的 IOCP,异步回调的方式,开发难度很高

为什么还是需要 AIO

文件句柄跟 socket 完全不同,它总是处于可读状态,不能 epoll+nonblock 实现异步化

在 epoll 的全异步 Server 中,读写文件必须得用 AIO

AIO 的几种实现方案

gcc AIO

gcc 遵循 posix 标准实现了 AIO

头文件为<aio.h>,支持 FreeBSD/Linux

通过阻塞 IO + 用户线程池实现

主要函数 aio_read/aio_write/aio_return

优点缺点
支持平台多,兼容性好,无需依赖第三方库,阻塞 IO 可以利用到操作系统的 PageCache一些 bug 和陷阱,一直未解决

Linux Native Aio

os kernel 提供的 AIO

头文件 <linux/aio_abi.h>

真正的 AIO,完全非阻塞异步,而不是用阻塞 IO 和线程池模拟

主要系统调用 io_submit/io_setup/io_getevents

优点缺点
os提供,读写操作可以直接投递到硬件,不会浪费 CPU仅支持 Linux,必须使用 DirectIO,无法利os的 PageCache
对于写文件来说 native aio 作用不大,应为本身写文件就是先写到 PageCache 上,直接返回,没有 IO 等待

Libeio

libev 作者开发的 AIO 实现,与 gcc aio 类似也是使用阻塞 IO + 线程池实现的

与 gcc aio 不同之处,代码更简洁 三方库,代码需要依赖 libeio

总结

程序读写文件很大,随机性强,PageCache 命中率低,可以选择 Native AIO,降低 CPU 使用率

读写文件很小,而且是固定的一些文件,PageCache 的命中率高,可以选择 gcc aio 或 libeio


跑 mysql,CPU 的 iowait 很大非常常见

对于以 IO 为最大瓶颈的db,native AIO 几乎不二的选择,仅仅依靠多线程,显然无法解决磁盘和网络的问题

AIO Ring 应用户态进程内存缓存区,内核共享,并报告 IO完成情况

用户态进程也可以直接从 AIO Ring 检查异步 IO 完成情况,避免系统调用开销


AIO 与 epoll

用 AIO 时,需阻塞的系统调用 io_getevents 获取已经完成的 IO 事件,所以有 2 种方式:

  1. 多线程,用专门的线程调用 io_getevents(MySQL5.5)
  2. 单线程程序,可以通过 epoll 来使用 AIO(需要系统调用 eventfd ,kernel 2.6.22)

eventfd 是 Linux-native aio 其中的一个 API,用来生成 file descriptors,可为应用程序提供更高效 “等待/通知” 的事件机制

和 pipe 作用相似,但比 pipe 更好,一是只用一个 file descriptor(pipe 要用两个),节省内核资源; 二是eventfd 缓冲区管理要简单得多,pipe 需要不定长的缓冲区,而 eventfd 全部缓冲只有定长 8 bytes

nginx 0.8 加入native aio 支持

linux 下有两种 aio,glibc 实现的 aio比较烂,在用户空间用 pthread 进行模拟。还有内核实现的 aio,nginx 用的后一种,native aio 只支持 direct io

native aio 的优点就是能够同时提交多个 io 请求给内核,然后直接由内核的 io 调度算法去处理这些请求 (direct io),这样的话,内核就有可能执行一些合并,优化

io_setup、io_cancel、io_destroy、io_getevents、 io_submit

libaio简单的封装了上面的几个系统调用,而 nginx直接使用 syscall 来调用系统调用