Nginx中的文件异步I/O采用的不是glibc库提供的基于多线程实现的异步I/O,而是由Linux内核实现的。Linux内核提供了5个系统调用来完成文件操作的异步I/O功能,列举如下:
方法名 | 参数含义 | 执行意义 |
---|---|---|
int io_setup(unsigned nr_events, aio_context_t *ctxp) | nr_events表示需要可以处理的事件的最小个数,ctxp是文件异步I/O的上下文描述符指针 | 初始化文件异步I/O的上下文,返回0表示成功。 |
int io_destory(aio_context_t ctx) | ctx是文件异步I/O的上下文描述符 | 销毁文件异步I/O的上下文,返回0表示成功。 |
int io_submit(aio_context_t ctx, long nr, struct iocb *cbp[]) | ctx是文件异步I/O的上下文描述符,nr是一次提交的事件个数,cbp是提交的事件数组中的首个元素地址 | 提交文件异步I/O操作。返回值表示成功提交的事件个数 |
int io_cancel(aio_context_t ctx, struct iocb *iocb, struct io_event *result) | ctx表示文件异步I/O的上下文描述符,iocb是要取消的异步I/O操作,而result表示这个操作的执行结果 | 取消之前使用io_submit提交的一个文件异步I/O操作。返回0表示成功。 |
int io_getevents(aio_context_t ctx, long min_nr,long_nr, struct io_event *events, struct timespec *timeout) | ctx表示文件异步I/O的上下文描述符,获取的已完成事件个数范围是[min_nr,nr],events是执行完成的事件数组,timeout是超时时间,也就是获取min_nr个时间前的等待时间。 | 从已完成的文件异步I/O操作队列中读取操作。 |
文件异步I/O中有一个核心结构体–struct iocb,其定义如下:
1 | struct iocb { |
上述的struct iocb结构体中,aio_flags和aio_read两个结构体成员正是Nginx中将文件异步I/O、eventfd以及epoll机制结合起来一起使用的关键,三者的结合也使得Nginx中的文件异步I/O同网络事件的处理一样高效。另外有一点需要说明的就是Nginx中目前只使用了异步I/O中的读操作,即struct iocb结构体中的成员aio_lio_opcode的值为IO_CMD_PREAD,因为文件的异步I/O不支持缓存操作,而正常写文件的操作往往是写入内存中就返回,而如果使用异步I/O方式写入的话反而会使得速度下降。
struct iocb用在提交和取消异步I/O事件中,而通过io_getevents获取已经完成的I/O事件时则用到的是另一个十分重要的结构体—struct io_event,其定义如下:
1 | struct io_event { |
在Nginx中,主要用到的字段就是data和res。其中data中保存的是文件异步I/O事件对象,res就是保存异步I/O的结果。
在简单了解了Linux内核提供的异步I/O系统调用及其在Nginx中涉及到的相关知识后,再来讲述下eventfd系统调用,因为这是Nginx将异步I/O事件集成到epoll中的一个桥梁。为什么这么说呢?通过上面对异步I/O结构体struct iocb结构体的分析,我们知道,当成员aio_flags设置为IOCB_FLAG_RESFD时,表明使用eventfd句柄来进行异步I/O事件的完成通知。正是这个eventfd,让其可以使用epoll机制来对其进行监控,从而间接对异步I/O事件完成进行监控,保证了事件驱动模块对网络事件和文件异步I/O事件的处理接口保持一致。
eventfd系统调用原型如下:
1 | int eventfd(unsigned int initval, int flags); |
eventfd系统调用常用与进程之间通信或者用于内核与应用程序之间通信。在Nginx中正是利用了内核会在异步I/O事件完成时通过eventfd通知Nginx来完成对异步I/O事件的间接监控。
在简单介绍完Linux内核提供的异步I/O接口以及eventfd系统调用后,接下来开始分析文件异步I/O事件是如何在ngx_epoll_module中实现的。下面是涉及到的主要全局变量,可以分为两部分:
1)、系统调用相关:
int ngx_eventfd = -1; 这个是用于通知异步I/O事件完成的描述符,在Nginx中它会赋值给struct iocb结构体中的aio_resfd成员,也是epoll监控的描述符。
aio_context_t ngx_aio_ctx = 0; 这个就是异步I/O接口会使用到的异步I/O上下文,并且需要经过io_setup初始化后才能使用。
2)、与网络事件处理兼容相关。
static ngx_event_t ngx_eventfd_event; 这个就是eventfd描述符对应的读事件对象。因为文件异步I/O事件完成后,内核通知应用程序eventfd有可读事件(EPOLLIN)发生。然后应用程序就会调用读事件回调函数进行处理。
static ngx_connection_t ngx_eventfd_conn; 这个就是eventfd描述符对应的连接对象。
为什么要称它们是与网络事件处理兼容呢?回想下Nginx在处理网络事件的时候会为socket获取一个连接对象,然后设置连接对象ngx_connection_t的fd成员为socket描述符,接着设置连接的读事件和写事件,并设置对应的事件回调函数,最后将读/写事件(或整个连接)加入到epoll中监控。对应地处理文件异步I/O事件时,首先是让I/O事件的完成通知用eventfd来完成,然后设置eventfd的读事件及其处理函数,再用一个连接对象来保存eventfd和读事件,并将eventfd加入到epoll监控。这样就保证了Nginx内核可以像处理网络事件一样处理文件异步I/O事件。但是Nginx内核处理文件异步I/O事件又有其特别的地方。因为当epoll中监控到eventfd有读事件完成时,只可以说明Linux内核通知Nginx有文件异步I/O事件完成了,此时Nginx还并不知道有哪些或有几个异步I/O事件完成了,可以这么理解,eventfd仅仅是Linux内核用来通知Nginx有异步I/O事件完成了。那Nginx又是如何获取完成的异步I/O事件的呢,这就是eventfd描述符关联的读事件回调函数所需要完成的工作了,这个后面进行详细说明。
现假设有一个模块需要读取磁盘中的文件,那么如果Nginx启动了文件异步I/O处理的话,那么这个读盘的操作会被Nginx作为一个异步I/O事件来处理。
因为要将这个读盘事件以异步I/O方式来处理,那么首先就需要初始化一个异步I/O上下文,在Nginx中代码如下:
1 | static void |
通过调用ngx_epoll_aio_init方法,Nginx就将异步I/O以eventfd为桥梁与epoll结合起来了。
初始化完异步I/O上下文后,模块就可以提交文件异步I/O事件了。在此之前需要再了解下Nginx封装的一个异步I/O事件的对象,如下:
1 | struct ngx_event_aio_s { |
那Nginx中是如何处理异步I/O事件的提交的呢?其代码实现如下:
1 | ssize_t |
在模块提交了文件异步I/O事件后,在事件完成之后,Linux就会触发eventfd的读事件来告诉Nginx异步I/O事件完成了,我们知道,当epoll监控到eventfd有事件发生时,在ngx_epoll_process_events()函数中会通过epoll_wait取出该事件,然后通过struct epoll_event结构体中的data.ptr成员获取eventfd对应的连接对象(在上面有介绍),并调用连接对象中的读事件处理函数ngx_epoll_eventfd_handler(),而Nginx正是通过这个读事件处理函数来获取真正完成的文件异步I/O事件,这个读事件处理函数正是在ngx_epoll_aio_init()函数中进行注册的。该函数的实现如下:
1 | static void |
一般来说业务模块在文件异步I/O事件完成后都需要在进行一些和业务相关的处理,那Nginx又是怎么实现的呢?当然是通过注册回调函数的方法来实现的。在通过ngx_epoll_eventfd_handler()回调函数获取到已经完成的文件异步I/O事件并加入到ngx_posted_events队列中后,执行ngx_posted_events队列中的事件时就会回调异步I/O事件完成的回调函数ngx_file_aio_event_handler(在ngx_file_aio_read注册),然后在该函数中调用业务模块(提交文件异步I/O事件的模块)实现的回调方法,这个方法一般都是在业务模块提交异步I/O事件前注册到上面介绍的ngx_event_aio_t的handler成员中。其中异步I/O事件完成后的回调函数实现如下:
1 | static void |
到这里Nginx中设计到的文件异步I/O、eventfd和epoll的配合使用就介绍完了。
参考文章:
1、《深入理解Nginx 模块开发与架构解析》