现在的位置: 首页 > 综合 > 正文

Nginx事件驱动初始化

2014年07月04日 ⁄ 综合 ⁄ 共 8047字 ⁄ 字号 评论关闭

终于可以写比较有意思的地方了,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中,而是会等到以后拿到互斥量的时候在放入,这主要是用于防止惊群。


好了到上面为止,事件的初始化基本就弄完了。

抱歉!评论已关闭.