上一篇分析了master进程的初始化流程,接着来分析work进程的初始化流程。work进程初始化流程包括:

1、work进程创建;

2、ngx_worker_process_init初始化过程

3、master-work进程通信

4、work进程的事件机制

5、work进程负载均衡实现

一、work进程的创建

在master进程的ngx_start_worker_processes函数中,会调用ngx_spawn_process函数开始创建work进程。创建完成后master进程、work进程同时工作,分别执行不同的业务逻辑。work进程用来处理来自客户端的读写事件,或者将客户端的请求反向代理到上游服务器等。函数ngx_spawn_process用来创建一个work进程,而work进程的处理函数为ngx_worker_process_cycle.

ngx_pid_t ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,char *name, ngx_int_t respawn)
{//创建work进程pid = fork();switch (pid){case -1:ngx_close_channel(ngx_processes[s].channel, cycle->log);return NGX_INVALID_PID;case 0://子进程处理逻辑ngx_pid = ngx_getpid();proc(cycle, data);break;default:break;}return pid;
}

二、ngx_worker_process_init初始化过程

ngx_worker_process_cycle函数为work进程的入口函数,在该函数内,会调用ngx_worker_process_init函数,对work进程进行初始化操作。

初始化操作主要是为work进程的运行提供必要的环境。例如设置work进程能够打开socket的最大个数;预先分配连接池空间,当有客户端请求时,直接从连接池中获取一个空闲连接;创建epoll对象,使得能够监听客户端的读写事件等。下面以一张思维导图来概括ngx_worker_process_init初始化过程。

1、函数会对work进程的一些基本环境进行初始化操作。例如:

(1)设置work进程的优先级;

(2)设置work进程最大能打开多少文件;

(3)设置work进程core文件的最大值;

(4)设置work进程绑定到特定的cpu

//设置work进程的优先级if (priority && ccf->priority != 0) {if (setpriority(PRIO_PROCESS, 0, ccf->priority) == -1) {ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,"setpriority(%d) failed", ccf->priority);}}

而像ccf->priority这些要设置的值,都是解析nginx.conf配置后得到,保存到core核心模块的上下文中。

2、接下来work进程将调用各个模块实现的init_process方法,用来初始化所有的模块

//调用所有模块的init_process方法,目前只有ngx_event_core_module模块实现了方法。//方法名为ngx_event_process_initfor (i = 0; ngx_modules[i]; i++) {if (ngx_modules[i]->init_process) {if (ngx_modules[i]->init_process(cycle) == NGX_ERROR) {/* fatal */exit(2);}}}

目前只有ngx_event_core_module模块实现init_process方法,方法名为ngx_event_process_init。函数内部主要创建work事件模型所需要的epoll对象;以及使用红黑树数实现定时器,用来管理超时事件;注册SIGALRM信号,在信号回调中获取系统时间;预先开辟连接池空间,注册客户端连接事件的回调等等。

2.1、创建红黑树实现的定时器

nginx内部使用红黑树实现定时器功能,用来管理所有超时事件。函数ngx_event_timer_init就是用来初始化一颗红黑树。

//初始化红黑树定时器if (ngx_event_timer_init(cycle->log) == NGX_ERROR) {return NGX_ERROR;}

初始化好红黑树定时器后,就可以调用ngx_event_add_timer函数往红黑树中插入一个事件,待事件超时后,就会触发超时回调。

如果不想使用超时事件了,则可以调用ngx_event_del_timer函数将超时事件从红黑树中删除,删除后相应的超时回调就不会被触发。

2.2、work进程会对所有事件模块进行初始化操作。

在编译nginx时,系统会优先使用一种事件模型进行编译,一般linux下事件模型为epoll。在每一个平台只会有一个事件模块编译,例如:epoll, select中的一个。

各个事件模块实现的init方法,就是为了创建相应的事件对象。以epoll为例,init函数将创建epoll对象,同时指定epoll每次最多可以从内核中返回多少个事件。

for (m = 0; ngx_modules[m]; m++) {if (ngx_modules[m]->type != NGX_EVENT_MODULE) {continue;}//使用use配置项指定的事件驱动模块,linux下为epollif (ngx_modules[m]->ctx_index != ecf->use) {continue;}module = ngx_modules[m]->ctx;//调用epoll事件模块的init方法,创建epoll对象与epoll事件方法。//因为目前只有ngx_epoll_module模块的上下文实现了actions方法,init方法为ngx_epoll_initif (module->actions.init(cycle, ngx_timer_resolution) != NGX_OK){exit(2);}break;}

epoll事件模块实现的init方法为ngx_epoll_init。函数内部会创建epoll对象,同时指定epoll每次最多可以从内核中返回多少个事件。

//epoll模块初始化,创建epoll句柄,同时创建epoll队列
static ngx_int_t ngx_epoll_init(ngx_cycle_t *cycle, ngx_msec_t timer)
{//创建epoll对象if (ep == -1) {ep = epoll_create(cycle->connection_n / 2);if (ep == -1) {return NGX_ERROR;}}//创建epoll返回队列,也就是epoll每次从epoll_wait返回时,最多可以返回多少个事件if (nevents < epcf->events){event_list = ngx_alloc(sizeof(struct epoll_event) * epcf->events,cycle->log);}//返回队列元素个数nevents = epcf->events;//保存epoll事件的10个方法ngx_event_actions = ngx_epoll_module_ctx.actions;return NGX_OK;
}

有了epoll对象后,就可以将读写事件注册到epoll中,当客户端连接时将触发读事件回调。

2.3 设置定时器,在定时时间到后,会触发SIGALRM信号。这个定时器是用来避免每次事件循环返回都调用gettimeofday函数,而是定时调用,减少调用系统调用频率

   itv.it_value.tv_sec = ngx_timer_resolution / 1000;itv.it_value.tv_usec = (ngx_timer_resolution % 1000 ) * 1000;setitimer(ITIMER_REAL, &itv, NULL);

定时器的回调为ngx_timer_signal_handler,函数内部只是给变量ngx_event_timer_alarm打上标记

//信号处理函数,用于更新标记。目的是为了减少频繁的系统调用更新时间,而采用信号定时器方式获取
//ngx_event_timer_alarm更新后,表示需要进行一次gettimeofday系统调用,获取时间
void ngx_timer_signal_handler(int signo)
{ngx_event_timer_alarm = 1;
}

这样每次epoll返回时,会根据这个变量决定是否需要进行一次系统调用,从而获取系统时间。ngx_epoll_process_events为ngx_epoll_module模块上下文结构实现的process_events方法

ngx_int_t ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{//等待事件返回events = epoll_wait(ep, event_list, (int) nevents, timer);//更新缓存时间if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {ngx_time_update();}
}

2.4 接下来将会创建连接池空间。每一个连接对象ngx_connection_t,都会关联一个读事件,一个写事件ngx_event_t结构。也就是说,nginx服务器对于每一个客户端连接,都可以处理该客户端的读事件,写事件。nginx通过预先分配一个连接池空间,当有客户端连接时,直接从连接池中获取一个空闲连接。

 //预分配连接池cycle->connections = ngx_alloc(sizeof(ngx_connection_t) * cycle->connection_n, cycle->log);
 //预分配读事件池cycle->read_events = ngx_alloc(sizeof(ngx_event_t) * cycle->connection_n, cycle->log);
 //预先分配写事件池cycle->write_events = ngx_alloc(sizeof(ngx_event_t) * cycle->connection_n,cycle->log);
 //将连接池,读事件池,写事件池一一关联起来do {i--;c[i].data = next;      //data成员组成一个空闲连接池链表c[i].read = &cycle->read_events[i];c[i].write = &cycle->write_events[i];c[i].fd = (ngx_socket_t) -1;next = &c[i];} while (i);

nginx使用数组方式实现了一个连接池链表,使用data成员将各个连接对象ngx_connection_t给连接起来。这样从连接池中获取一个连接后,也相应得到连接对应的读事件,写事件。最后使用cycle->free_connections指针指向空闲连接池链表头;

cycle->free_connections = next; //空闲连接池链表头

以下是可能存在的连接池、读事件池、写事件池的内存布局图

在创建好连接池后,如果有客户端连接,就可以调用ngx_get_connection函数,从连接池中获取一个连接。

2.5 为所有监听socket,从空间连接池中获取一个连接。同时将注册监听socket读事件回调设置为ngx_event_accept。当有客户端连接时,该函数会被调用,用于建立与客户端的连接。

 //对于每一个监听端口,从连接池中取出一个连接对象(也将从读时间,写事件池取出对象,//使得连接,读、写保持一一对应关系),负责监听来自客户端的连接ls = cycle->listening.elts;for (i = 0; i < cycle->listening.nelts; i++) {c = ngx_get_connection(ls[i].fd, cycle->log);c->listening = &ls[i];ls[i].connection = c;rev = c->read;//设置连接回调,当有客户端连接时,将触发回调//这里不需要注册写事件回调,应该在没有建立连接时,nginx不可能向客户端写入数据rev->handler = ngx_event_accept;//如果work进程之间使用枷锁,则暂时不把读事件加入epoll中,在别处加入。这种方式下,只会有一个work进程将所有的监听socket加入到epoll事件管理器中if (ngx_use_accept_mutex) {continue;}//如果work进程之间没有使用枷锁,则把读事件加入epoll中。这种情况下各个work进程将该socket的读连接事件。if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {return NGX_ERROR;} }

如果work进程之间不使用同步,则各个work进程都可以将所有监听socket加入到epoll中。这样当某个监听socket有客户端连接时,各个work将采用抢占方式,最终只会有一个work进程获取该socket的连接权限,处理客户端的连接请求。这容易造成"惊群"现象。在这种方式下,上面函数会将监听socket加入到epoll中。ngx_add_event是一个宏,添加事件的方法为ngx_epoll_add_event。

static ngx_int_t
ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags)
{//data指的是该事件对于的是哪个连接c = ev->data;//活跃事件,则说明之前已经注册过事件,现在则应是修改操作if (e->active) {op = EPOLL_CTL_MOD;events |= prev;}else {op = EPOLL_CTL_ADD;}//注册或者修改事件epoll_ctl(ep, op, c->fd, &ee);return NGX_OK;
}

如果work进程之前使用同步方式,则只会有一个work进程能够将所有监听socket加入到epoll中。其它work进程因为获取不到监听socket,从而不会处理来自客户端的连接。因此,同一时刻只会有一个work进程在监听客户端的连接事件。当然如果已经连接的请求,则不影响,各个work进程正常接收客户端的读写事件。在使用同步方式时,上面函数并不会将监听socket加入到epoll中。那什么时候会将监听socket加入到epoll中呢? 在ngx_enable_accept_events函数中,会将监听socket加入到epoll中。

ngx_worker_process_cycle

---->ngx_process_events_and_timers

---->ngx_trylock_accept_mutex

---->ngx_enable_accept_events

static ngx_int_t ngx_enable_accept_events(ngx_cycle_t *cycle)
{ls = cycle->listening.elts;for (i = 0; i < cycle->listening.nelts; i++) {c = ls[i].connection;ngx_add_event(c->read, NGX_READ_EVENT, 0);}return NGX_OK;
}

3、work进程继承了master进程的进程表。进程表中存放了各个work进程的信息。work进程使用自己的channe[1]管道读取master进程或者其它work进程发来的数据。因此就不需要其它work进程的channel[1]读管道,因此可以关闭。

    for (n = 0; n < ngx_last_process; n++) {//本work进程只会从自己的channel[1]读取来自master进程或者其它work进程的数据,因此可以关闭其它//work进程的读端close(ngx_processes[n].channel[1]);}

work进程自己不需要channel[0]写管道,因此可以关闭写管道。

 //work进程关闭写端,也就是说work进程从channel[1]读取来自master进程或者其它work进程写入管道的数据//work进程不会写数据给master进程,但work进程会写数据给其它的work进程,则可以在进程表中找到相应work进程,//并向channel[0]写入数据if (close(ngx_processes[ngx_process_slot].channel[0]) == -1) {ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,"close() channel failed");}

4、work进程间通信、以及master进程与work进程通信,使用管道来通信。在上面我们已经创建了epoll对象,同时也预先分配了连接池空间。现在可以将管道加入到epoll中监听。这样就将管道与epoll事件关联起来了。 ngx_add_channel_event函数将从连接池中获取一个连接,并加事件加入到epoll中。

//将管道加入到epoll事件
ngx_int_t ngx_add_channel_event(ngx_cycle_t *cycle, ngx_fd_t fd, ngx_int_t event,ngx_event_handler_pt handler)
{ngx_event_t       *ev, *rev, *wev;ngx_connection_t  *c;//从空闲连接队列中获取一个连接对象c = ngx_get_connection(fd, cycle->log);c->pool = cycle->pool;//关联连接对应的读写事件rev = c->read;wev = c->write;rev->channel = 1;wev->channel = 1;ev = (event == NGX_READ_EVENT) ? rev : wev;//设置事件回调ev->handler = handler;//将事件添加到epoll中ngx_add_event(ev, event, 0);return NGX_OK;
}

将读管道注册到epoll中,读管道的回调为ngx_channel_handler。在master进程向管道写入数据时,epoll被触发,从而可以ngx_channel_handler函数被调用。在该函数内可以读取到数据。下面以master-work进程通信的例子来分析ngx_channel_handler函数的使用。

三、master-work进程通信

下面以重新打开文件为例,说明master-work进程的通信过程。当执行./nginx -s reopen时。这个新执行的程序会查找master进程所在的pid文件,进而向master进程发送USR1信号,也就是reopen信号。master进程中,信号函数ngx_signal_handler会被调用,给ngx_reopen打上标记。

void ngx_signal_handler(int signo)
{//master进程中执行switch (signo){case ngx_signal_value(NGX_REOPEN_SIGNAL):      //用户信号,用来reopenngx_reopen = 1;                                  //打上标记action = ", reopening logs";break;}
}

之后master进程的事件循环函数ngx_master_process_cycle,会循环检测是否有reopen标记。如果有,master进程首先将所有已经打开文件缓存中的数据保存到文件中。然后在重新打开所有文件。接着向channe[1]写管道,向work进程写入数据,通知work进程重新打开所有文件。

    //master进程工程流程
void ngx_master_process_cycle(ngx_cycle_t *cycle)
{for ( ;; ) {//收到重新打开所有文件信号if (ngx_reopen) {ngx_reopen = 0;ngx_reopen_files(cycle, ccf->user);ngx_signal_worker_processes(cycle, ngx_signal_value(NGX_REOPEN_SIGNAL));}}
}
void ngx_signal_worker_processes(ngx_cycle_t *cycle, int signo)
{ch.command = NGX_CMD_REOPEN;ngx_write_channel(ngx_processes[i].channel[0],&ch, sizeof(ngx_channel_t), cycle->log)
}  

在上面已经将work进程的读管道channel[1]已经注册到epoll中。这样master进程通过channel[0]向管道写入数据时,epoll会被触发,进而调用读管道的回调ngx_channel_handler。该函数会从管道中读取数据,并将ngx_reopen打上标记。

static void ngx_channel_handler(ngx_event_t *ev)
{for ( ;; ) {//work进程从读管道channel[1]中读取数据,数据内容为ngx_channel_tn = ngx_read_channel(c->fd, &ch, sizeof(ngx_channel_t), ev->log);switch (ch.command) {case NGX_CMD_REOPEN:ngx_reopen = 1;     //work进程给该变量打上标记break;}}
}

work进程的事件循环函数ngx_worker_process_cycle会检测ngx_reopen标记是否为1。如果是,则work进程将所有已经打开文件的缓存写入到文件中保存。然后重新打开文件。

static void ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{for ( ;; ) {//重新打开所有文件if (ngx_reopen) {ngx_reopen = 0;ngx_reopen_files(cycle, -1);}}
}

四、work进程的事件机制

nginx采用epoll来管理来自客户端的读写事件。work进程的事件循环函数中会调用ngx_process_events_and_timers函数,处理来自客户端的网络事件。

static void ngx_worker_process_cycle(ngx_cycle_t *cycle, void *data)
{for ( ;; ) {ngx_process_events_and_timers(cycle);  //处理epoll网络事件}
}

在上面2.4小节中,如果work进程需要使用互斥锁来进行同步,那时候并没有将所有监听socket加入到epoll中。使用互斥所情况下ngx_use_accept_mutex这个标记将为1,这个时候应该将所有监听socket加入到epoll中。当前work进程这个时候如果获取到了锁,则将所有监听socket加入到epoll中。如果work进程没有获取到锁,那应该将所有监听socket从epoll中移除。这样就保证了在同一时候,只会有一个work进程监听来自客户端的连接。函数ngx_trylock_accept_mutex就是来实现这个功能。

//获取互斥锁
ngx_int_t ngx_trylock_accept_mutex(ngx_cycle_t *cycle)
{//获取到锁if (ngx_shmtx_trylock(&ngx_accept_mutex)) {//如果进程已经获取到锁了,则直接返回,不需要把监听fd加入到事件中//什么时候会走入这样逻辑? 进程第一次获取锁成功,并在epoll_wait返回后释放了锁,但没有将//ngx_accept_mutex_held标记清0。这样所有子进程都可以抢占锁,如果还是本进程先获取到锁,会进入//这个逻辑?if (ngx_accept_mutex_held&& ngx_accept_events == 0&& !(ngx_event_flags & NGX_USE_RTSIG_EVENT)){return NGX_OK;}//将所有监听fd加入监听事件中ngx_enable_accept_events(cycle);//标示进程拥有锁ngx_accept_events = 0;ngx_accept_mutex_held = 1;return NGX_OK;}//未获取到锁,但进程拥有了锁,则是不正确的。应该把所有监听连接都关闭,并标示进程没有拥有锁。//什么时候会走入这样逻辑? 进程第一次获取锁成功,并在epoll_wait返回后释放了锁,但没有将//ngx_accept_mutex_held标记清0。这样所有子进程都可以抢占锁,在其他子进程先获取到情况下,会进入//这个逻辑。if (ngx_accept_mutex_held) {if (ngx_disable_accept_events(cycle) == NGX_ERROR) {return NGX_ERROR;}ngx_accept_mutex_held = 0;}return NGX_OK;
}

ngx_enable_accept_events负责将所有监听socket加入到epoll, ngx_disable_accept_events则将所有监听socket从epoll中移除。

接下来执行ngx_process_events,等待事件发生。在有事件时从epoll中返回,或者超时时间到了,即便没有事件也会返回。这是一个宏,实际函数为ngx_epoll_process_events。看下这个函数的处理逻辑。

static ngx_int_t ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{//等待事件返回events = epoll_wait(ep, event_list, (int) nevents, timer);//更新缓存时间if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {ngx_time_update();}//遍历所有已经发生的事件for (i = 0; i < events; i++) {c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1);rev = c->read;revents = event_list[i].events;//如果是读事件且事件时活跃的if ((revents & EPOLLIN) && rev->active) {//标示这个事件要延后处理if (flags & NGX_POST_EVENTS) {//如果要在post队列中延后处理事件,首先要判断它是新连接还是普通事件,从而决定要把//事件放到哪个队列。连接队列:存放来自客户端的连接。//普通队列: 存放已经建立连接的客户端,进行读写事件通信queue = (ngx_event_t **) (rev->accept ?&ngx_posted_accept_events : &ngx_posted_events);//将事件放入相应的延后队列ngx_locked_post_event(rev, queue);} else {//不是延迟事件,则立即调用读事件回调rev->handler(rev);}}wev = c->write;//取出写事件if ((revents & EPOLLOUT) && wev->active) {//将事件添加到延迟队列中if (flags & NGX_POST_EVENTS) {ngx_locked_post_event(wev, &ngx_posted_events);} else {//立即调用写事件的回调wev->handler(wev);}}}return NGX_OK;
}

work进程阻塞在epoll_wait系统调用中。如果有事件发生,则work进程被唤醒。进而判断事件是否需要延迟处理。如果不需要延迟处理,则立即调用事件的处理函数。如果需要延迟处理,则将事件加入到队列中,稍后再对队列中的事件进行处理。nginx使用了两个队列,一个连接队列,存放新客户端的连接。另一个普通队列,存放所有已经建立连接后的客户端。这两个队列中,连接队列的优先级更高。也就是说在同一个时刻,有新客户端的连接,也有老客户的读写事件请求。则优先处理新客户端的连接事件。

 //调用epoll_wait等待事件(void) ngx_process_events(cycle, timer, flags);//计算处理事件消耗多长时间delta = ngx_current_msec - delta;//优先处理连接队列中的所有连接if (ngx_posted_accept_events) {ngx_event_process_posted(cycle, &ngx_posted_accept_events);}//处理完连接队列后,要立即释放互斥锁,避免长事件占用锁,而不能在普通读写事件处理完后才释放锁。//因为有可能普通事件处理的时间很长,导致锁没释放,其它子进程无法处理新的连接if (ngx_accept_mutex_held) {//此处最好也调用ngx_disable_accept_events清除监听socket,而不需要等待循环把监听socket清除//因为释放锁后,其它子进程有可能马上获取到了锁,并把监听socket加入到epoll。//这样导致多个work进程都在监听同一个socketngx_shmtx_unlock(&ngx_accept_mutex);}//消耗了时间,有可能红黑树中有超时事件,则处理红黑树中所有超时事件,并把超时事件从红黑树中删除if (delta) {ngx_event_expire_timers();}//最后才处理普通事件队列中的读写事件if (ngx_posted_events) {ngx_event_process_posted(cycle, &ngx_posted_events);}

而ngx_event_process_posted实际上只是调用事件的处理函数。

//事件post事件队列中的事件,处理完后并将事件从队列中删除
//注意:删除事件并没有真正把ngx_event_t删除,而是将prev,next指针从post队列中断开
void ngx_event_process_posted(ngx_cycle_t *cycle,ngx_thread_volatile ngx_event_t **posted)
{ngx_event_t  *ev;for ( ;; ) {ev = (ngx_event_t *) *posted;ngx_delete_posted_event(ev);ev->handler(ev);}
}

五、work进程间负载均衡

每一个work进程都可以监听来自客户的连接,使用互斥锁方式实现work进程间的同步。在某些情况下,有可能某个work进程处理着大量的客户端连接,处于高负载工作状态。而其他work进程可能只处理着少量的客户端连接,这样就造成负载不均,影响整体性能。那nginx是如何处理work进程之间的负载均衡呢?

在2.4中将所有监听socket加入到epoll时,注册了读事件的回调为ngx_event_accept;在函数中会对ngx_accept_disabled变量进行统计。如果work进程处理的客户端连接数达到了work进程连接的7/8时,说明work进程处于高负载运行状态,此时ngx_accept_disabled将大于0,不能在处理新客户端的连接事件了。

  ngx_accept_disabled = ngx_cycle->connection_n / 8- ngx_cycle->free_connection_n;c = ngx_get_connection(s, ev->log);

那work进程根据这个ngx_accept_disabled这个变量,什么时候确定是否还能允许新客户端连接呢? 在函数ngx_process_events_and_timers中会对这个变量值进行判断。如果该变量值大于0,说明work进程处于高负载运行状态,此时不能处理新客户端连接。函数中当值大于0时,只是把变量值减1操作。并不会尝试去获取锁,也就不会将所有监听socket加入到epoll中。因为没有把监听socket加入到epoll中,因此该work进程也就不会处理新客户端的连接。这样本work进程不参数互斥锁的抢占,其它的work进程可以获取到锁后,处理新客户端的连接,从而实现work进程之间的负载均衡。当然,在变量值递减到0以下时,本work进程还是会再次尝试获取锁操作。

void ngx_process_events_and_timers(ngx_cycle_t *cycle)
{//nginx.conf配置了accept_mutex on, 标示使用锁if (ngx_use_accept_mutex) {//当值为正数时,本work进程不处理新连接事件,而是将值递减1。//此处怀疑有bug,最好调用ngx_disable_accept_events清除监听socketif (ngx_accept_disabled > 0) {ngx_accept_disabled--;} else {//尝试获取锁if (ngx_trylock_accept_mutex(cycle) == NGX_ERROR) {return;}}}
}

到此work进程的初始化操作就全部分析完成了。

nginx启动流程之work初始化相关推荐

  1. 我的Android进阶修炼:安卓启动流程之init(1)

    文章目录 我的Android进阶修炼:安卓启动流程之init(1) 一.前言 二.init进程简介 1.文件位置 2.主要功能 三.init进程源码分析 3.1 main() 源码注解 3.1.1 参 ...

  2. android启动流程之lk,Android系统之LK启动流程分析(一)

    1.前言 LK是Little Kernel的缩写,在Qualcomm平台的Android系统中普遍采用LK作为bootloader,它是一个开源项目,LK是整个系统的引导部分,所以不是独立存在的,但是 ...

  3. Android平台WIFI启动流程之二

    http://blog.sina.com.cn/s/blog_13146f9590101wji1.html [摘要] 本文从用户界面出发,从应用层到硬件适配层,对Android平台wifi启动和关闭的 ...

  4. Linux启动流程之ROM-CODE

    1.从哪里开始? 下图是AM335X核心板和功能框图: AM335X核心板的存储信息如下: AM335X核心板运行linux系统,在这里提出一个问题: 上电后指令从哪里开始执行? DDR or EMM ...

  5. android启动流程之preloader--->lk

    关于异常的基本知识 什么是异常 对于AArch64而言,exception是指cpu的某些异常状态或者一些系统的事件(可能来自外部,也可能来自内部),这些状态或者事件可以导致cpu去执行一些预先设定的 ...

  6. mavenspringboot项目启动流程一:初始化

    启动流程 1.运行主启动类 对项目进行编译.测试 ---- 此时会执行到mven的test生命周期 a. 导入maven依赖jar包 b.加载build配置和profile配置 build 0).设置 ...

  7. (连载)Android系统源码分析--Android系统启动流程之Linux内核

    > **这是一个连载的博文系列,我将持续为大家提供尽可能透彻的Android源码分析 [github连载地址](https://github.com/foxleezh/AOSP/issues/3 ...

  8. (连载)Android 8.0 : 系统启动流程之Linux内核

    这是一个连载的博文系列,我将持续为大家提供尽可能透彻的Android源码分析 github连载地址 前言 Android本质上就是一个基于Linux内核的操作系统,与Ubuntu Linux.Fedo ...

  9. nginx启动初始化过程(二)

    前面提到nginx启动过程中,关键部分是ngx_init_cycle.ngx_init_cycle()函数究竟做了哪些工作.ngx_cycle_t结构定义在src/core/ngx_core.h文件中 ...

  10. ARM64的启动过程之(三):为打开MMU而进行的CPU初始化

    原文地址:http://www.wowotech.net/linux_kenrel/__cpu_setup.html 一.前言 上一节主要描述了为了打开MMU而进行的Translation table ...

最新文章

  1. C#一键显示及杀死占用端口号进程
  2. 【BZOJ-2342】双倍回文 Manacher + 并查集
  3. 热点推荐:程序员路在何方
  4. jvm二:类加载,连接,初始化
  5. mysql 的select语句_MYSQL SELECT语句新手
  6. 跨域产生的原因和解决方法_幼儿语言障碍产生的原因及其解决方法
  7. c++变量的作用域、生存期和可见性
  8. 在webpack和vue-cli上的rem适配终极方案
  9. 如何用Python抓抖音上的小姐姐
  10. 【React自制全家桶】九、Redux入手
  11. dz3 php post 登录,discuz X3用户登录uc_user_login()函数详解
  12. 如何下载react依赖包
  13. 分布式大气监测系统架构介绍及案例解析
  14. 关于sqlldr官方教材上的几个例子ulcase study1-9
  15. 高考水平科测试软件,新高考选课测评app-新高考最好用的软件推荐!
  16. c语言转义字符 xhh,jsj_C语言转义字符和格式控制符参考.pdf
  17. today JavaScript 笔记和案例
  18. 让XueTr卸载不了我们的驱动
  19. Bat脚本 - 详解
  20. Cosplay美图爬取

热门文章

  1. 【UVa11584】划分成回文串
  2. AES简介加密算法介绍
  3. MBTI职业性格测试 测评报告
  4. 被伽卡他卡禁用任务管理器如何解决
  5. Pandas学习-Task05
  6. 上海交通大学学生生存手册
  7. 【手写简易版 vue3】究极长文详细讲解如何实现一个简易版 vue3
  8. android 陀螺仪加速度传感器,如何在Android智能手机中使用加速度传感器...
  9. 计算机没有显示远程桌面连接,连接远程桌面时出现:“这台计算机没有远程桌面客户端访问许可”,怎么处理?...
  10. NHibernate 联合查询,解决方法-通过自动转换成DataTable