Nginx的高效得益于它的事件驱动机制,整个事件驱动机制基本框架就是linux下的select,poll,epoll这几个IO多路复用模式,但是nginx绝不单单只是使用它们这么简单,今天以epoll模式为例,从nginx最开始的listenfd的监听的过程来说明nginx是怎么实现的事件驱动。
首先需要说明的是,整个事件模型(event)是一个模块(module),module在nginx中是一个很重要的概念,这也是nginx牛B的地方之一,模块带来的好处就是非常非常棒的水平扩展能力,在nginx上做二次开发基本也是模块的编写,下面我们就来分析下event module:
先来认识下ngx_module_t这个结构体
struct ngx_module_s {
ngx_uint_t ctx_index;
/*分类的模块计数器
nginx模块可以分为四种:core、event、http和mail
每个模块都会各自计数,ctx_index就是每个模块在其所属类组的计数*/
ngx_uint_t index;
/*一个模块计数器,按照每个模块在ngx_modules[]数组中的声明顺序,从0开始依次给每个模块赋值*/
ngx_uint_t spare0;
ngx_uint_t spare1;
ngx_uint_t spare2;
ngx_uint_t spare3;
ngx_uint_t version; //nginx模块版本
void *ctx;
/*模块的上下文,不同种类的模块有不同的上下文,因此实现了四种结构体*/
ngx_command_t *commands;
/*命令定义地址
模块的指令集
每一个指令在源码中对应着一个ngx_command_t结构变量*/
ngx_uint_t type; //模块类型,用于区分core event http和mail
ngx_int_t (*init_master)(ngx_log_t *log); //初始化master时执行
ngx_int_t (*init_module)(ngx_cycle_t *cycle); //初始化module时执行
ngx_int_t (*init_process)(ngx_cycle_t *cycle); //初始化process时执行
ngx_int_t (*init_thread)(ngx_cycle_t *cycle); //初始化thread时执行
void (*exit_thread)(ngx_cycle_t *cycle); //退出thread时执行
void (*exit_process)(ngx_cycle_t *cycle); //退出process时执行
void (*exit_master)(ngx_cycle_t *cycle); //退出master时执行
//空闲的钩子函数
uintptr_t spare_hook0;
uintptr_t spare_hook1;
uintptr_t spare_hook2;
uintptr_t spare_hook3;
uintptr_t spare_hook4;
uintptr_t spare_hook5;
uintptr_t spare_hook6;
uintptr_t spare_hook7;
};
typedef struct ngx_module_s ngx_module_t;
这里面最重要的就是那几个回调函数(init_master到exit_master之间),比如init_master任务,在ngx_module.c中有个全局变量:
ngx_module_t *ngx_modules[] = {
&ngx_core_module,
&ngx_errlog_module,
&ngx_conf_module,
&ngx_dso_module,
&ngx_conf_extend_module,
&ngx_syslog_module,
...
}
在ngx_worker_process_init中就会从这里面去调用各种module的init_master方法:
for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->init_process) {
if (ngx_modules[i]->init_process(cycle) == NGX_ERROR) {
/* fatal */
exit(2);
}
}
}
那么如何判定到底采用哪一种IO复用方式呢?
在ngx_event.c中有如下声明:
extern ngx_module_t ngx_kqueue_module;
extern ngx_module_t ngx_eventport_module;
extern ngx_module_t ngx_devpoll_module;
extern ngx_module_t ngx_epoll_module;
extern ngx_module_t ngx_rtsig_module;
extern ngx_module_t ngx_select_module;
这些声明以及配置文件中的相应部分设定用来确定你用哪一个IO复用的模块,那么事件是如何注册的呢?
关于事件注册的最重要的结构体如下:
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;
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 (*process_changes)(ngx_cycle_t *cycle, ngx_uint_t nowait);
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为例,来看下epoll的ngx_module_t是怎么实现的:
ngx_event_module_t ngx_epoll_module_ctx = {
&epoll_name,
ngx_epoll_create_conf, /* create configuration */
ngx_epoll_init_conf, /* init configuration */
{
ngx_epoll_add_event, /* add an event */
ngx_epoll_del_event, /* delete an event */
ngx_epoll_add_event, /* enable an event */
ngx_epoll_del_event, /* disable an event */
ngx_epoll_add_connection, /* add an connection */
ngx_epoll_del_connection, /* delete an connection */
NULL, /* process the changes */
ngx_epoll_process_events, /* process the events */
ngx_epoll_init, /* init the events */
ngx_epoll_done, /* done the events */
}
};
那么,调用到这些注册的方法的过程是怎样的呢?我们以ngx_epoll_add_event为例,在ngx_worker_process_init的listenfd注册到epoll的过程如下:
还记得刚刚说到的init_master那个回调吗,我们来看看epoll中的ngx_module_t
ngx_module_t ngx_event_core_module = {
NGX_MODULE_V1,
&ngx_event_core_module_ctx, /* module context */
ngx_event_core_commands, /* module directives */
NGX_EVENT_MODULE, /* module type */
NULL, /* init master */
ngx_event_module_init, /* init module */
ngx_event_process_init, /* init process */
NULL, /* init thread */
NULL, /* exit thread */
NULL, /* exit process */
NULL, /* exit master */
NGX_MODULE_V1_PADDING
};
可以看到在之前说明的过程中,会调用调用ngx_event_process_init函数,在这个函数中
if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
return NGX_ERROR;
}
至此就完成了listenfd注册到epoll的过程,在这里要说明下这个rev,说到rev就要说明一下nginx中连接(connection)的概念,nginx中注册到epoll中的epoll_event里的data部分是指向一个连接的,nginx中是提前分配好了一个连接池,每次需要连接的时候就从连接池里拿一个出来,连接池的东西我们后面再细讲,那么rev是怎么跟连接关联起来的呢,我们来看下rev的结构体:
struct ngx_event_s {
void *data;//关注这个指针,这个指针通常都是指向一个连接
.
.
.
ngx_event_handler_pt handler;//最需要关注的是这个handler,这个handler就是这个event被调用是所调用的函数
.
.
.
};
这是个简化的结构体,我只写出了要关注的部分,再来看下connection的结构体
struct ngx_connection_s {
//连接未使用时候,data域充当连接链表中的next指针.
//当连接被使用时候,data域的意义由模块而定.
void *data;
//连接对应的读事件,这个read指针就指向了刚刚代码中的rev
ngx_event_t *read;
//连接对应的写事件
ngx_event_t *write;
//套接字句柄
ngx_socket_t fd;
//直接接收网络字节流的方法
ngx_recv_pt recv;
//直接放松网络字节流的方法
ngx_send_pt send;
//以ngx_chain链表为参数,接收网络字节流的方法
ngx_recv_chain_pt recv_chain;
//以ngx_chain链表为参数,发送网络字节流的方法
ngx_send_chain_pt send_chain;
//这个链接对应的listening_t监听对象.
//此链接由ngx_listening_t监听的事件建立
ngx_listening_t *listening;
//这个连接已经发送出去的字节数
off_t sent;
//记录日志
ngx_log_t *log;
//在accept一个新连接的时候,会创建一个内存池,而这个连接结束时候,会销毁一个内存池.
//这里所说的连接是成功建立的tcp连接.内存池的大小由pool_size决定
//所有的ngx_connect_t结构体都是预分配的
ngx_pool_t *pool;
//连接客户端的结构体
struct sockaddr *sockaddr;
//连接客户端的结构体长度
socklen_t socklen;
//连接客户端的ip(字符串形式)
ngx_str_t addr_text;
#if (NGX_SSL)
ngx_ssl_connection_t *ssl;
#endif
//本机中监听端口对应的socketaddr结构体
//也就是listen监听对象中的socketaddr成员
struct sockaddr *local_sockaddr;
//用于接收和缓存客户端发来的字符流
ngx_buf_t *buffer;
//该字段表示将该连接以双向链表形式添加到cycle结构体中的
//reusable_connections_queen双向链表中,表示可以重用的连接.
ngx_queue_t queue;
//连接使用次数,每次建立一条来自客户端的连接,
//或者建立一条与后端服务器的连接,number+1
ngx_atomic_uint_t number;
//处理请求的次数
ngx_uint_t requests;
//
unsigned buffered:8;
//日志级别
unsigned log_error:3; /* ngx_connection_log_error_e */
//不期待字符流结束
unsigned unexpected_eof:1;
//连接超时
unsigned timedout:1;
//连接处理过程中出现错误
unsigned error:1;
//标识此链接已经销毁,内存池,套接字等都不可用
unsigned destroyed:1;
//连接处于空闲状态
unsigned idle:1;
//连接可以重用
unsigned reusable:1;
//连接关闭
unsigned close:1;
//正在将文件中的数据法网另一端
unsigned sendfile:1;
//连接中发送缓冲区的数据高于低潮,才发送数据.
//与ngx_handle_write_event方法中的lowat相对应
unsigned sndlowat:1;
//使用tcp的nodely特性
unsigned tcp_nodelay:2; /* ngx_connection_tcp_nodelay_e */
//使用tcp的nopush特性
unsigned tcp_nopush:2; /* ngx_connection_tcp_nopush_e */
。
。
。
}
也就是说,connection中有read指针指向了read事件rev,同事rev也有指针指向了对应的connection,这样就达到了数据相互关联的作用,至此,整个事件的注册流程就大致清晰了。