文章目录
nginx 事件驱动框架浅析
Nginx的网络框架集成了各个操作系统的事件框架,这里主要基于linux 的 epoll 讲解。 一个事件处理框架所要解决的问题是如何收集、管理、分发事件。这里所说的事件,主要以网络事件和定时器事件为主,而网络事件中又以TCP网络事件为主。本文主要结合nginx源码解析整个事件驱动框架的机制。
参考书目《深入理解nginx》。
nginx主循环框架 ngx_cycle_s
nginx 有一个全局的对象 ngx_cycle_s, 这个结构体囊括了所有的核心结构体,并控制这个进程的运行。比如 nginx 的配置文件,所有模块,以及连接,事件,内存池,日志等等,都存在 nginx_cycle_s 结构体中。由于这里只介绍 nginx 的事件驱动机制,因此只列出与之相关的一部分字段。
struct ngx_cycle_s {
...
//ngx_listening_t 动态数组,存储需要监听的端口,初始化时会根据数组内容监听
ngx_array_t listening;
//所有的连接
ngx_connection_t *connections;
//所有注册的读事件
ngx_event_t *read_events;
//所有注册的写事件
ngx_event_t *write_events;
...
};
以下是启动流程
nginx事件 ngx_event_s
一个事件驱动框架包括,事件产生者,事件收集分发者,和事件消费者。
有了事件注册分发器,还需要对每一个事件进行封装。事件是注册和分发的实体,网络事件也好,定时器事件,信号事件也好,在 nginx 中都抽象成了结构体 ngx_event_s 。这里列出了结构体一部分的字段和含义。
struct ngx_event_s {
/*data通常指向一个ngx_connection_t连接对象。开启文件异步I/O时,它可能会指向ngx_event_aio_t结构体*/
void *data;
/*是否可写标志位。通常情况下表示对应的TCP连接目前状态是可写的,也就是连接处于可以发送网络包的状态*/
unsigned write:1;
/*是否可接受连接标志位。通常情况下,在ngx_cycle_t中的
listening动态数组中,每一个监听对象ngx_listening_t对应的读事件中的accept标志位才会是1*/
unsigned accept:1;
/*当前事件是否过期标志位。仅用于事件驱动模块,事件消费模块可不用关心。为什么需要这个标志位呢?当开instance标志位来避免处理后面的已经过期的事件。在9.6节中,将详细描述ngx_epoll_module是如何使用instance标志位区分过期事件的,这是一个巧妙的设计方法*/
unsigned instance:1;
/*是否活跃标志位。这个状态对应着事件驱动模块处理方式的不同。例如,在添加事件、删除事件和处理事件时,active标志位的不同都会对应着不同的处理方式。在使用事件时,一般不会直接改变active标志位*/
unsigned active:1;
...
/*最核心的事件处理函数:该回调函数由每个事件消费模块自己实现。
typedef void (*ngx_event_handler_pt)(ngx_event_t *ev);
*/
ngx_event_handler_pt handler;
....
};
nginx 为了提高性能,读写事件都是预先生成的。在启动过程中,会在 ngx_cycle_t 结构体中预先分配好读事件和写事件,存储在 ngx_cycle_t->read_events 和 ngx_cycle_t->write_events 中。事实上,不仅仅是事件,连连接也是预先分配的,每一个连接会自动对应一个读事件和一个写事件。这个对应关系在后面会讲到。
事件添加和删除操作,可以调用事件模块中的 add() 和 del() 来实现,但是并不推荐这样做,因为,事件类型的不同,事件模块使用的add() del() 等操作也不一样。nginx 提供了更简单和简洁的操作接口,会屏蔽掉底层事件的差异,大部分场景使用这2个接口就可以了。
//将读事件注册要事件驱动模块中
ngx_int_t ngx_handle_read_event(ngx_event_t *rev, ngx_uint_t flags);
//将写事件注册要事件驱动模块中
ngx_int_t ngx_handle_write_event(ngx_event_t *wev, size_t lowat);
nginx连接 ngx_connection_s
连接分为2类,一类是被动连接,也就是服务端监听时,accept() 收到的连接,属于服务端连接,nginx 中用 ngx_connection_s 表示。一类是主动连接,就是 connect() 获得的连接,属于客户端连接,用 ngx_peer_connection_s 。连接和事件的关系前文说了,每个连接都会对应一个读事件和写事件。那么连接池如何和读写事件对应起来呢?答案是使用数组。连接池,读事件,写事件由3个大小相同的数组存储,取不同数组的相同下标即可获得一个连接以及对应的读写事件。
nginx事件模块 ngx_event_module_s
事件模块是对不同的操作系统事件驱动框架的抽象。因为不同的系统平台提供的事件驱动API不一样,比如linux 下有 select/poll/epoll,macOS下有 kqueue, windows下是IOCP。ngx_event_module_s 提供了统一的模板,即所有的事件框架都是一个 ngx_event_module_s 类型的变量。
typedef struct {
//事件模块的名称,比如 epoll/select 等
ngx_str_t *name;
//create_conf和init_conf方法的调用可参见图9-3
//在解析配置项前,这个回调方法用于创建存储配置项参数的结构体
void *(*create_conf)(ngx_cycle_t *cycle);
//在解析配置项完成后,init_conf方法会被调用,用以综合处理当前事件模块感兴趣的全部配置项
char *(*init_conf)(ngx_cycle_t cycle, void conf);
//对于不同的事件驱动框架需要实现的10个抽象方法
ngx_event_actions_t actions;
} ngx_event_module_t;
每个事件驱动框架需要自定义对事件的 actions。ngx_event_actions_t 是一组操作集,包括对事件的10种操作。
typedef struct {
/*添加事件方法,它将负责把1个感兴趣的事件添加到操作系统提供的事件驱动机制(如 epoll、kqueue等)中,这样,在事件发生后,将可以在调用下面的process_events时获取这个事件*/
ngx_int_t (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
/*删除事件方法,它将把1个已经存在于事件驱动机制中的事件移除,这样以后即使这个事件发生,调用process_events方法时也无法再获取这个事件*/
ngx_int_t (*del)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
/*启用1个事件,目前事件框架不会调用这个方法,大部分事件驱动模块对于该方法的实现都是与上面的add方法完全一致的*/
ngx_int_t (*enable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
/*禁用1个事件,目前事件框架不会调用这个方法,大部分事件驱动模块对于该方法的实现都是与上面的del方法完全一致的*/
ngx_int_t (*disable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
/*向事件驱动机制中添加一个新的连接*/
ngx_int_t (*add_conn)(ngx_connection_t *c);
/*从事件驱动机制中移除一个连接的读写事件*/
ngx_int_t (*del_conn)(ngx_connection_t *c, ngx_uint_t flags);
/*仅在多线程环境下会被调用。目前Nginx不会以多线程方式运行,忽略*/
ngx_int_t (*notify)(ngx_event_handler_pt handler);
/*在正常的工作循环中,将通过调用process_events方法来处理事件。这个方法仅在ngx_process_events_and_timers方法中调用,它是处理、分发事件的核心*/
ngx_int_t (*process_events)(ngx_cycle_t *cycle, ngx_msec_t timer,
ngx_uint_t flags);
/*初始化事件驱动模块*/
ngx_int_t (*init)(ngx_cycle_t *cycle, ngx_msec_t timer);
/*退出事件驱动模块前调用的方法*/
void (*done)(ngx_cycle_t *cycle);
} ngx_event_actions_t;
epoll事件模块 ngx_epoll_module
了解 epoll 的都知道,epoll 编程中,需要事先生成一个 epoll_fd 的对象,然后在一个循环里里不断调用 epoll_wait() 来获取就绪的事件,并分发到不同的消费者上去处理。而每当有新的事件需要关注时,就继续往 epoll_fd 注册,这个系统生成的 epoll_fd 句柄实际上就是一个事件的收集器和分发器。所有的事件需要通过 epoll_fd 才能注册到底层的网络硬件上,而当网卡或磁盘等硬件接受了事件的发生时,就会回调给操作系统,操作系统的再通过 epoll_wait() 返回给应用层ÿ