终于可以写比较有意思的地方了,Nginx之所以性能很高的原因应该采用事件驱动的模式处理连接,而不是采用多线程或者多进程的方式应该算是很重要的原因吧。
这篇文章先讲Nginx的事件驱动的初始化吧,其实底层只不过是用I/O复用的方式来实现的。首先我们应该知道两个非常重要的模块:ngx_events_module模块与ngx_event_core_module模块,ngx_events_module模块式core类型的模块,ngx_event_core_module为event类型的模块。
嗯,接下来从Nginx的启动开始说起:
我们知道在启动时候会调用所有core模块的create_conf函数,并会解析配置文件,调用命令的set回调函数,还会调用core模块的init_conf函数,以及调用所有模块的init_module(其实只有ngx_event_core_module模块有该函数)函数,而且最后在所有的wokre进程中还会调用所有模块的ngx_worker_process_init函数。
因为ngx_events_module模块没有create_conf函数,所以这里就没事件模块什么事情了,然后是配置文件,我们这里可以看一个比较典型的配置
events { use epoll; #epoll是多路复用IO(I/O Multiplexing)中的一种方式,但是仅用于linux2.6以上内核,可以大大提高nginx的性能 worker_connections 1024;#单个后台worker process进程的最大并发链接数 }
嗯,解析到了events命令,那么会调用ngx_events_module模块的commands的的回调函数ngx_events_block:
//当解析到events的配置项的时候,回执行以下回调函数 static char * ngx_events_block(ngx_conf_t *cf, ngx_command_t *cmd, void *conf) { char *rv; void ***ctx; //上下文 ngx_uint_t i; ngx_conf_t pcf; //配置信息 ngx_event_module_t *m; /* count the number of the event modules and set up their indices */ //计算event模块数量,并且记录 ,相当于是更新event模块的分类索引 ngx_event_max_module = 0; for (i = 0; ngx_modules[i]; i++) { if (ngx_modules[i]->type != NGX_EVENT_MODULE) { continue; } ngx_modules[i]->ctx_index = ngx_event_max_module++; //记录当前事件模块的事件模块分类索引 } //为上下文指针分配内存空间 ctx = ngx_pcalloc(cf->pool, sizeof(void *)); if (ctx == NULL) { return NGX_CONF_ERROR; } //为每一个event模块分配空间,用来保存相应模块的配置 *ctx = ngx_pcalloc(cf->pool, ngx_event_max_module * sizeof(void *)); if (*ctx == NULL) { return NGX_CONF_ERROR; } *(void **) conf = ctx; //遍历每一个事件模块,并为他们创建配置 for (i = 0; ngx_modules[i]; i++) { //如果不是事件模块,那么直接跳过 if (ngx_modules[i]->type != NGX_EVENT_MODULE) { continue; } m = ngx_modules[i]->ctx; //用m来指向当前模块的上下文,在这里说白了就是指向具体的事件模块 //循环调用每个模块的creat_conf钩子函数,用于创建配置结构,这里保存的位置该模块的事件模块的分类索引 if (m->create_conf) { (*ctx)[ngx_modules[i]->ctx_index] = m->create_conf(cf->cycle); //创建该模块的配置 if ((*ctx)[ngx_modules[i]->ctx_index] == NULL) { return NGX_CONF_ERROR; } } } pcf = *cf; cf->ctx = ctx; cf->module_type = NGX_EVENT_MODULE; cf->cmd_type = NGX_EVENT_CONF; //由于events是一个block指令,events域下还可以配置很多其他指令, //比如之前提过的use等,现在开始解析events block中的指令,完成初始化工作。 rv = ngx_conf_parse(cf, NULL); //这里用于解析配置文件中events{}这个block的解析 *cf = pcf; if (rv != NGX_CONF_OK) return rv; for (i = 0; ngx_modules[i]; i++) { if (ngx_modules[i]->type != NGX_EVENT_MODULE) { continue; } //m用来存储模块的上下文 m = ngx_modules[i]->ctx; //循环执行每个event模块的init_conf函数,初始化配置结构 if (m->init_conf) { rv = m->init_conf(cf->cycle, (*ctx)[ngx_modules[i]->ctx_index]); if (rv != NGX_CONF_OK) { return rv; } } } return NGX_CONF_OK; }
该函数基本的意思上面的注释基本也已经说清楚了,函数首先为每个event类型的模块分配模块的类型索引号,然后调用每个时间模块的create_conf函数创建配置,接下来就要开始解析events命令里面的命令了,这里就说一个吧,use
epoll命令,该命令的set回调函数为ngx_event_use函数,该命令主要是用来告诉Nginx,该用哪一种事件模型,一般情况下都是使用epoll的,因为它在linux下的效率是最高的。ngx_event_use函数还是很简单的,说白了就是根据use命令的参数,在所有事件模块中找到相对应的,然后将其的模块号保存下来就可以了,表示以后就用这个事件模块了。然后还要调用所有事件模块的init_conf函数来初始化配置。
接下来还要调用所有事件模块的init_conf函数。这里我们就选两个比较典型的模块来说吧,分别是:ngx_event_core_module与ngx_epoll_module,首先来看ngx_event_core_module模块的init_conf函数:
//创建epoll,说白了就是试一下是否有epoll模块 fd = epoll_create(100); if (fd != -1) { close(fd); module = &ngx_epoll_module; } else if (ngx_errno != NGX_ENOSYS) { module = &ngx_epoll_module; }
如上代码用来判断当前是否有epoll,如果有的话就用module来指向ngx_epoll_module模块,在后面会用到,接下来的代码是:
if (module == NULL) { for (i = 0; ngx_modules[i]; i++) { if (ngx_modules[i]->type != NGX_EVENT_MODULE) { continue; } event_module = ngx_modules[i]->ctx; //这里是要略去ngx_event_core_module模块 if (ngx_strcmp(event_module->name->data, event_core_name.data) == 0) { continue; } module = ngx_modules[i]; break; } } if (module == NULL) { ngx_log_error(NGX_LOG_EMERG, cycle->log, 0, "no events module found"); return NGX_CONF_ERROR; } ngx_conf_init_uint_value(ecf->connections, DEFAULT_CONNECTIONS); cycle->connection_n = ecf->connections; ngx_conf_init_uint_value(ecf->use, module->ctx_index); event_module = module->ctx; ngx_conf_init_ptr_value(ecf->name, event_module->name->data); ngx_conf_init_value(ecf->multi_accept, 0); ngx_conf_init_value(ecf->accept_mutex, 1); ngx_conf_init_msec_value(ecf->accept_mutex_delay, 500);
根据前面的代码,看module是否为空,如果为空的话,那么还要找到一个可用的事件模块,并用该模块来初始化一些信息。这部分代码就不难看出为什么默认使用的是epoll模块了。接下来就是epoll模块的init_conf函数,好吧,还是不要看了。基本就是一些初始化。
然后就是在ngx_events_module模块的init_conf函数了,好吧,基本也可以无视它。
接下来进入所有模块的init_module函数,前面已经说过只有ngx_event_core_module模块有这个函数,该函数的名字是ngx_event_module_init,其实本身还是很简单的,中间比较重要的是用共享内存来实现互斥信号量,这个就留到以后说Nginx实现锁的内容部分讲吧。
最后就是在每个worker进程中都会调用所有模块的ngx_worker_process_init函数,ngx_events_module模块没有这个函数,epoll模块也没有,嗯ngx_event_core_module模块有(函数的名字是ngx_event_process_init),我们来看看(这部分代码还是很重要的):
for (m = 0; ngx_modules[m]; m++) { if (ngx_modules[m]->type != NGX_EVENT_MODULE) { continue; //遍历全局的ngx_modules数组,如果不是事件模块,那么直接跳过 } if (ngx_modules[m]->ctx_index != ecf->use) { //如果当前这个事件模块不是要用的,那么也跳过,貌似默认的是epoll continue; } module = ngx_modules[m]->ctx; //获取该模块的上下文,说白了就是获取该模块具体对应的模块,event,http等 /*调用具体事件模块的init函数。 由于Nginx实现了很多的事件模块,比如:epoll,poll,select, kqueue,aio (这些模块位于src/event/modules目录中)等等,所以Nginx对事件模块进行 了一层抽象,方便在不同的系统上使用不同的事件模型,也便于扩展新的事件 模型。从此过后,将把注意力主要集中在epoll上。 此处的init回调,其实就是调用了ngx_epoll_init函数。module->actions结构 封装了epoll的所有接口函数。Nginx就是通过actions结构将epoll注册到事件 抽象层中。actions的类型是ngx_event_actions_t,位于src/event/ngx_event.h */ if (module->actions.init(cycle, ngx_timer_resolution) != NGX_OK) { /* fatal */ exit(2); } break; }
首先看上面的代码,注释也已经说的比较清楚了,就是找到实际要用的事件模块(默认用epoll),然后调用该事件模块的init函数,其实epoll模块的init函数也是比较简单的,有兴趣的可以自己去看看,主要内容就是调用epoll_create函数创建epoll,然后再用ngx_event_actions(定义在Ngx_event.c中)变量指向epoll模块的action,这样也就完成于epoll模块的贴合,以后可以直接用epoll模块具体实现的函数了。
然后接着看ngx_event_process_init函数的代码:
/*创建一个connection数组,维护所有的connection; 本过程已经是在worker进程中了,所以是每个worker都有自己的 connection数组。 同样每一个worker进程也有自己的cycle */ cycle->connections = ngx_alloc(sizeof(ngx_connection_t) * cycle->connection_n, cycle->log); if (cycle->connections == NULL) { return NGX_ERROR; } c = cycle->connections; //指向当前worker进程的cycle的connection数组 //创建读事件数组 cycle->read_events = ngx_alloc(sizeof(ngx_event_t) * cycle->connection_n, cycle->log); if (cycle->read_events == NULL) { return NGX_ERROR; } rev = cycle->read_events; //指向当前worker进程的cycle的读取事件数组 for (i = 0; i < cycle->connection_n; i++) { rev[i].closed = 1; rev[i].instance = 1; #if (NGX_THREADS) rev[i].lock = &c[i].lock; rev[i].own_lock = &c[i].lock; #endif } //创建写事件的数组 cycle->write_events = ngx_alloc(sizeof(ngx_event_t) * cycle->connection_n, cycle->log); if (cycle->write_events == NULL) { return NGX_ERROR; } wev = cycle->write_events; //同样是指向当前worker进程的cycle的写事件的数组 for (i = 0; i < cycle->connection_n; i++) { wev[i].closed = 1; #if (NGX_THREADS) wev[i].lock = &c[i].lock; wev[i].own_lock = &c[i].lock; #endif } i = cycle->connection_n; next = NULL; /*初始化整个connection数组,connection数组使用得很是巧妙, 这里类似于一个链表的结构 能够快速的获取释放一个连接结构。下一篇画个图来详细看看 这个connection。 */ do { i--; c[i].data = next; //这里用于将connection串成一个链,好管理 c[i].read = &cycle->read_events[i]; //为当前的connection赋读事件 c[i].write = &cycle->write_events[i]; //为当前的connection赋写事件 c[i].fd = (ngx_socket_t) -1; next = &c[i]; #if (NGX_THREADS) c[i].lock = 0; #endif } while (i); cycle->free_connections = next; //初始化当前worker进程的free_connections域 cycle->free_connection_n = cycle->connection_n; //因为刚刚开始,所以可用的connection为总数
这一长串代码用来初始化connection,为他们分配内存,并用链表的方式组织它们,以及初始化他们的读写事件,具体的内容上面的注释也已经说的很清楚了。
ls = cycle->listening.elts; //监听套接字是在master进程那里继承过来的,已经初始化好了 for (i = 0; i < cycle->listening.nelts; i++) { //为当前监听套接字的文件描述符分配一个connection,函数返回值c是当前监听套接字关联的connection c = ngx_get_connection(ls[i].fd, cycle->log); if (c == NULL) { return NGX_ERROR; } c->log = &ls[i].log; c->listening = &ls[i]; //当前连接的监听端口 ls[i].connection = c; //当前监听端口的connection rev = c->read; //rev指向当前connection的读事件 rev->log = c->log; rev->accept = 1; //表示当前的读事件是监听端口的accept事件,可以用于epoll区分是一般的读事件还是监听对口的accept事件 if (!(ngx_event_flags & NGX_USE_IOCP_EVENT)) { if (ls[i].previous) { /* * delete the old accept events that were bound to * the old cycle read events array */ old = ls[i].previous->connection; if (ngx_del_event(old->read, NGX_READ_EVENT, NGX_CLOSE_EVENT) == NGX_ERROR) { return NGX_ERROR; } old->fd = (ngx_socket_t) -1; } } /*注册监听套接口读事件的回调函数ngx_event_accept*/ rev->handler = ngx_event_accept; //说白了就是从监听套接字来获取连接的socket /*使用了accept_mutex,暂时不将监听套接字放入epoll中 而是等到worker抢到accept互斥体后,再放入epoll,避免 惊群的发生。 */ if (ngx_use_accept_mutex) { continue; } if (ngx_event_flags & NGX_USE_RTSIG_EVENT) { if (ngx_add_conn(c) == NGX_ERROR) { //加入当前监听套接字所关联的connection return NGX_ERROR; } } else { /*没有使用accept互斥体,那么就在此处将监听套接字放入 epoll中。 */ if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) { return NGX_ERROR; } } }
上面的代码是用来初始化监听数组,在当前worker进程中为这些监听分配connection,并初始化他们读事件的处理函数式ngx_event_accept,也就是说用这个函数来处理监听接收的socket,这里需要注意的是,如果开启了使用互斥量,那么这里将不会立刻将监听的connection放入到epoll中,而是会等到以后拿到互斥量的时候在放入,这主要是用于防止惊群。
好了到上面为止,事件的初始化基本就弄完了。