Nginx源码阅读笔记-事件处理模块

2019-01-31
4分钟阅读时长

大概做高性能服务器的,都绕不开事件处理模块来,一般一个事件模块,会分为以下几部分:

  • 如何定义一个描述事件的数据结构。
  • 如何在事件模块中支持定时器。
  • 如果需要支持多平台,事件模块需要考虑如何统一以及区分各平台的具体实现。

下面就这三部分展开Nginx事件处理模块的分析。

ngx_event_t

描述事件的数据结构,一般至少需要以下几部分数据:

  • 用于保存用户相关的数据。
  • 用于保存事件触发之后的回调函数。
  • 用于表示事件状态、类型的数据。

nginx中,描述事件采用的数据结构是ngx_event_t中,其内部成员就是按照前面的三部分来划分了。

  • void *data:事件相关的数据。
  • ngx_event_handler_pt handler:事件被触发时的回调函数。
  • 第三类数据,ngx_event_t中划分的比较仔细:
    • unsigned write:1:可写标志位
    • unsigned active:1:活跃标志位
    • unsigned disabled:1:禁用标志位
    • unsigned eof:1:为1表示字节流已经结束
    • unsigned error:1:处理事件出错
    • unsigned timedout:1:事件超时
    • unsigned timer_set:1:为1表示这是一个超时事件
    • unsigned deferred_accept:1:为1表示需要延迟接收TCP连接
  • 除了以上三部分,还有其他一些重要的数据:
    • ngx_rbtree_node_t timer:红黑树节点,用于实现定时器的,下面讨论定时器再展开。
    • ngx_queue_t queue:延迟队列,如果事件不在轮询循环中直接处理,而是之后被处理,就放在这个队列中。

总体来看,event这个结构体为了涵盖所有可能的事件,做的大而全,不只是用来描述一般的IO事件,还包括了定时器事件,还包括了接收连接相关的数据。

定时器的实现

Nginx内部使用红黑树来实现定时器,目的在于能够快速的查询到哪些定时器超时了。不同的事件结构中,这部分实现采用的数据结构不一样,libevent、libuv采用的是最小堆,redis比较挫,这部分采用的是链表。

在一个事件循环中,因为既要考虑到一般的IO事件,又要考虑到定时器事件,所以都会以一个最近被触发的定时器来做为查询IO事件被触发的时间,即以下的伪代码:

查询最近将被触发的定时器超时时间返回t
t做为epoll_wait之类的查询IO事件的超时时间,即最长等待t时间看有没有IO事件被触发
遍历定时器,查询已经超时的定时器进行回调处理

从这里可以看出,“迅速查询到距离当前最近被触发的定时器时间”以及“迅速查询到当前哪些定时器超时”,是这个定时器模块速度的关键。

由于红黑树、最小堆这种平衡数据结构,每次查询都排除掉当前一半的元素,可以做到时间复杂度O(logn),所以就常用来实现定时器了。

事件模块的实现

由于nginx需要跑在多个平台下面,而不同平台使用的事件机制又不一样,比如linux是epoll,bsd是kqueue等,需要实现事件模块的时候,既需要统一事件模块的共性部分,又需要区分不同平台的差异部分。

这看上去又是一个面向对象的设计问题了:基类负责实现共性的部分,子类具体再来实现各平台相关的部分。

前面分析libuv的时候提到过,libuv多使用宏来模拟C++中的继承,不是很认可这个代码风格,来看看nginx类似场景的实现。

nginx中,将事件相关的操作函数统一放在结构体ngx_event_actions_t中,可以把这部分类比于子类需要实现的函数接口:

typedef struct {
  ngx_int_t  (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
  ngx_int_t  (*del)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);

  ngx_int_t  (*enable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
  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);

  ngx_int_t  (*notify)(ngx_event_handler_pt handler);

  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;

前面在分析到nginx如何解析配置的时候提到过,nginx中的配置是分层次的,event模块做为一个顶层的core模块,内部又有子模块,而这里的事件模块就是event模块中的子模块:

typedef struct {
  ngx_str_t              *name;

  void                 *(*create_conf)(ngx_cycle_t *cycle);
  char                 *(*init_conf)(ngx_cycle_t *cycle, void *conf);

  ngx_event_actions_t     actions;
} ngx_event_module_t;

在具体实现中,每个平台的事件模块创建自己的ngx_event_module_t结构,在create_conf、init_conf中完成对事件模块的初始化,然后填充模块的actions结构体。

最后,具体调用actions结构体中的函数,封装到宏里面,毕竟虽然有多平台的实现,最后也只能用上一个而已:

#define ngx_process_events   ngx_event_actions.process_events
#define ngx_done_events      ngx_event_actions.done

#define ngx_add_event        ngx_event_actions.add
#define ngx_del_event        ngx_event_actions.del
#define ngx_add_conn         ngx_event_actions.add_conn
#define ngx_del_conn         ngx_event_actions.del_conn

#define ngx_notify           ngx_event_actions.notify

而前面提到的事件处理部分共性的地方,全都放在函数ngx_process_events_and_timers里,那个函数里面再通过宏ngx_process_events调用具体事件模块的处理函数。

这里有个细节,其实前面的分析也提到过,nginx的事件模块里,不一定在检查到事件触发之后就会被马上调用回调函数来处理,而是可能放在一个post队列中,在轮询完所有事件之后再进行回调:

if (flags & NGX_POST_EVENTS) {
		// 有NGX_POST_EVENTS标志位的情况,将accept事件放到ngx_posted_accept_events队列中
		// 等待后续被回调
		queue = rev->accept ? &ngx_posted_accept_events
												: &ngx_posted_events;

		ngx_post_event(rev, queue);

} else {
		// 否则直接处理
		rev->handler(rev);
}