56

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

 5 years ago
source link: https://www.codedump.info/post/20190131-nginx-event/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

下面就这三部分展开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);
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK