摘要:內(nèi)核代表進(jìn)程來執(zhí)行信號處理器函數(shù),當(dāng)處理器返回時,主程序會在處理器被中斷的位置恢復(fù)執(zhí)行。進(jìn)程信號掩碼內(nèi)核會為每個進(jìn)程維護(hù)一個信號掩碼。這個競態(tài)條件發(fā)生在主程序和信號處理器對同一個被解除信號的競爭關(guān)系。
運(yùn)營研發(fā)團(tuán)隊 季偉濱
一、前言眾所周如,Nginx是多進(jìn)程架構(gòu)。有1個master進(jìn)程和N個worker進(jìn)程,一般N等于cpu的核數(shù)。另外, 和文件緩存相關(guān),還有cache manager和cache loader進(jìn)程。
master進(jìn)程并不處理網(wǎng)絡(luò)請求,網(wǎng)絡(luò)請求是由worker進(jìn)程來處理,而master進(jìn)程負(fù)責(zé)管理這些worker進(jìn)程。比如當(dāng)一個worker進(jìn)程意外掛掉了,他負(fù)責(zé)拉起新的worker進(jìn)程,又比如通知所有的worker進(jìn)程平滑的退出等等。本篇wiki將簡單分析下master進(jìn)程是如何做管理工作的。
二、nginx進(jìn)程模式在開始講解master進(jìn)程之前,我們需要首先知道,其實Nginx除了生產(chǎn)模式(多進(jìn)程+daemon)之外,還有其他的進(jìn)程模式,雖然這些模式一般都是為了研發(fā)&調(diào)試使用。
非daemon模式以非daemon模式啟動的nginx進(jìn)程并不會立刻退出。其實在終端執(zhí)行非bash內(nèi)置命令,終端進(jìn)程會fork一個子進(jìn)程,然后exec執(zhí)行我們的nginx bin。然后終端進(jìn)程本身會進(jìn)入睡眠態(tài),等待著子進(jìn)程的結(jié)束。在nginx的配置文件中,配置【daemon off;】即可讓進(jìn)程模式切換到前臺模式。
下圖展示了一個測試?yán)樱瑢orker的個數(shù)設(shè)置為1,開啟非daemon模式,開啟2個終端pts/0和pts/1。在pts/1上執(zhí)行nginx,然后在pts/0上看進(jìn)程的狀態(tài),可以看到終端進(jìn)程進(jìn)入了阻塞態(tài)(睡眠態(tài))。這種情況下啟動的master進(jìn)程,它的父進(jìn)程是當(dāng)前的終端進(jìn)程(/bin/bash),隨著終端的退出(比如ctrl+c),所有nginx進(jìn)程都會退出。
single模式nginx可以以單進(jìn)程的形式對外提供完整的服務(wù)。這里進(jìn)程可以是daemon,也可以不是daemon進(jìn)程,都沒有關(guān)系。在nginx的配置文件中,配置【master_process off;】即可讓進(jìn)程模式切換到單進(jìn)程模式。這時你會看到,只有一個進(jìn)程在對外服務(wù)。
生產(chǎn)模式(多進(jìn)程+daemon)想像一下一般我們是怎么啟動nginx的,我在自己的vm上把Nginx安裝到了/home/xiaoju/nginx-jiweibin,所以啟動命令一般是這樣:
/home/xiaoju/nginx-jiweibin/sbin/nginx
然后,ps -ef|grep nginx就會發(fā)現(xiàn)啟動好了master和worker進(jìn)程,像下面這樣(warn是由于我修改worker_processes為1,但未修改worker_cpu_affinity,可以忽略)
這里和非daemon模式的一個很大區(qū)別是啟動程序(終端進(jìn)程的子進(jìn)程)會立刻退出,并被終端進(jìn)程這個父進(jìn)程回收。同時會產(chǎn)生master這種daemon進(jìn)程,可以看到master進(jìn)程的父進(jìn)程id是1,也就是init或systemd進(jìn)程。這樣,隨著終端的退出,master進(jìn)程仍然可以繼續(xù)服務(wù),因為master進(jìn)程已經(jīng)和啟動nginx命令的終端shell進(jìn)程無關(guān)了。
啟動nginx命令,是如何生成daemon進(jìn)程并退出的呢?答案很簡單,同樣是fork系統(tǒng)調(diào)用。它會復(fù)制一個和當(dāng)前啟動進(jìn)程具有相同代碼段、數(shù)據(jù)段、堆和棧、fd等信息的子進(jìn)程(盡管cow技術(shù)使得復(fù)制發(fā)生在需要分離那一刻),參見圖-1。
圖1-生產(chǎn)模式Nginx進(jìn)程啟動示意圖
master進(jìn)程被fork后,繼續(xù)執(zhí)行ngx_master_process_cycle函數(shù)。這個函數(shù)主要進(jìn)行如下操作:
1、設(shè)置進(jìn)程的初始信號掩碼,屏蔽相關(guān)信號
2、fork子進(jìn)程,包括worker進(jìn)程和cache manager進(jìn)程、cache loader進(jìn)程
3、進(jìn)入主循環(huán),通過sigsuspend系統(tǒng)調(diào)用,等待著信號的到來。一旦信號到來,會進(jìn)入信號處理程序。信號處理程序執(zhí)行之后,程序執(zhí)行流程會判斷各種狀態(tài)位,來執(zhí)行不同的操作。
圖2- ngx_master_process_cycle執(zhí)行流程示意圖
master進(jìn)程的主循環(huán)里面,一直通過等待各種信號事件,來處理不同的指令。這里先普及信號的一些知識,有了這些知識的鋪墊再看master相關(guān)代碼會更加從容一些(如果對信號比較熟悉,可以略過這一節(jié))。
標(biāo)準(zhǔn)信號和實時信號信號分為標(biāo)準(zhǔn)信號(不可靠信號)和實時信號(可靠信號),標(biāo)準(zhǔn)信號是從1-31,實時信號是從32-64。一般我們熟知的信號比如,SIGINT,SIGQUIT,SIGKILL等等都是標(biāo)準(zhǔn)信號。master進(jìn)程監(jiān)聽的信號也是標(biāo)準(zhǔn)信號。標(biāo)準(zhǔn)信號和實時信號有一個區(qū)別就是:標(biāo)準(zhǔn)信號,是基于位的標(biāo)記,假設(shè)在阻塞等待的時候,多個相同的信號到來,最終解除阻塞時,只會傳遞一次信號,無法統(tǒng)計等待期間信號的計數(shù)。而實時信號是通過隊列來實現(xiàn),所以,假設(shè)在阻塞等待的時候,多個相同的信號到來,最終解除阻塞的時候,會傳遞多次信號。
信號處理器信號處理器是指當(dāng)捕獲指定信號時(傳遞給進(jìn)程)時將會調(diào)用的一個函數(shù)。信號處理器程序可能隨時打斷進(jìn)程的主程序流程。內(nèi)核代表進(jìn)程來執(zhí)行信號處理器函數(shù),當(dāng)處理器返回時,主程序會在處理器被中斷的位置恢復(fù)執(zhí)行。(主程序在執(zhí)行某一個系統(tǒng)調(diào)用的時候,有可能被信號打斷,當(dāng)信號處理器返回時,可以通過參數(shù)控制是否重啟這個系統(tǒng)調(diào)用)。
信號處理器函數(shù)的原型是:void (* sighandler_t)(int);入?yún)⑹?-31的標(biāo)準(zhǔn)信號的編號。比如SIGHUP的編號是1,SIGINT的編號是2。
通過sigaction調(diào)用可以對某一個信號安裝信號處理器。函數(shù)原型是:int sigaction(int sig,const struct sigaction act,struct sigaction oldact); sig表示想要監(jiān)聽的信號。act是監(jiān)聽的動作對象,這里包含信號處理器的函數(shù)指針,oldact是指之前的信號處理器信息。見下面的結(jié)構(gòu)體定義:
struct sigaction{ void (*sa_handler)(int); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); }
sa_hander就是我們的信號處理器函數(shù)指針。除了捕獲信號外,進(jìn)程對信號的處理還可以有忽略該信號(使用SIG_IGN常量)和執(zhí)行缺省操作(使用SIG_DFL常量)。這里需要注意,SIGKILL信號和SIGSTOP信號不能被捕獲、阻塞、忽略的。
sa_mask是一組信號,在sa_handler執(zhí)行期間,會將這組信號加入到進(jìn)程信號掩碼中(進(jìn)程信號掩碼見下面描述),對于在sa_mask中的信號,會保持阻塞。
sa_flags包含一些可以改變處理器行為的標(biāo)記位,比如SA_NODEFER表示執(zhí)行信號處理器時不自動將該信號加入到信號掩碼 SA_RESTART表示自動重啟被信號處理器中斷的系統(tǒng)調(diào)用。
sa_restorer僅內(nèi)部使用,應(yīng)用程序很少使用。
發(fā)送信號一般我們給某個進(jìn)程發(fā)送信號,可以使用kill這個shell命令。比如kill -9 pid,就是發(fā)送SIGKILL信號。kill -INT pid,就可以發(fā)送SIGINT信號給進(jìn)程。與shell命令類似,可以使用kill系統(tǒng)調(diào)用來向進(jìn)程發(fā)送信號。
函數(shù)原型是:(注意,這里發(fā)送的一般都是標(biāo)準(zhǔn)信號,實時信號使用sigqueue系統(tǒng)調(diào)用來發(fā)送)。
int kill(pit_t pid, int sig);
另外,子進(jìn)程退出,會自動給父進(jìn)程發(fā)送SIGCHLD信號,父進(jìn)程可以監(jiān)聽這一信號來滿足相應(yīng)的子進(jìn)程管理,如自動拉起新的子進(jìn)程。
進(jìn)程信號掩碼內(nèi)核會為每個進(jìn)程維護(hù)一個信號掩碼。信號掩碼包含一組信號,對于掩碼中的信號,內(nèi)核會阻塞其對進(jìn)程的傳遞。信號被阻塞后,對信號的傳遞會延后,直到信號從掩碼中移除。
假設(shè)通過sigaction函數(shù)安裝信號處理器時不指定SA_NODEFER這個flag,那么執(zhí)行信號處理器時,會自動將捕獲到的信號加入到信號掩碼,也就是在處理某一個信號時,不會被相同的信號中斷。
通過sigprocmask系統(tǒng)調(diào)用,可以顯式的向信號掩碼中添加或移除信號。函數(shù)原型是:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how可以使下面3種:
SIG_BLOCK:將set指向的信號集內(nèi)的信號添加到信號掩碼中。即信號掩碼是當(dāng)前值和set的并集。
SIG_UNBLOCK:將set指向的信號集內(nèi)的信號從信號掩碼中移除。
SIG_SETMASK:將信號掩碼賦值為set指向的信號集。
等待信號在應(yīng)用開發(fā)中,可能需要存在這種業(yè)務(wù)場景:進(jìn)程需要首先屏蔽所有的信號,等相應(yīng)工作已經(jīng)做完之后,解除阻塞,然后一直等待著信號的到來(在阻塞期間有可能并沒有信號的到來)。信號一旦到來,再次恢復(fù)對信號的阻塞。
linux編程中,可以使用int pause(void)系統(tǒng)調(diào)用來等待信號的到來,該調(diào)用會掛起進(jìn)程,直到信號到來中斷該調(diào)用。基于這個調(diào)用,對于上面的場景可以編寫下面的偽代碼:
struct sigaction sa; sigset_t initMask,prevMask; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sa.sa_handler = handler; sigaction(SIGXXX,&sa,NULL); //1-安裝信號處理器 sigemptyset(&initMask); sigaddset(&initMask,xxx); sigaddset(&initMask,yyy); .... sigprocmask(SIG_BLOCK,&initMask,&prevMask); //2-設(shè)置進(jìn)程信號掩碼,屏蔽相關(guān)信號 do_something() //3-這段邏輯不會被信號所打擾 sigprocmask(SIG_SETMASK,&prevMask,NULL); //4-解除阻塞 pause(); //5-等待信號 sigprocmask(SIG_BLOCK,&initMask,&prevMask); //6-再次設(shè)置掩碼,阻塞信號的傳遞 do_something2(); //7-這里一般需要監(jiān)控一些全局標(biāo)記位是否已經(jīng)改變,全局標(biāo)記位在信號處理器中被設(shè)置
想想上面的代碼會有什么問題?假設(shè)某一個信號,在上面的4之后,5之前到來,也就是解除阻塞之后,等待信號調(diào)用之前到來,信號會被信號處理器所處理,并且pause調(diào)用會一直陷入阻塞,除非有第二個信號的到來。這和我們的預(yù)期是不符的。這個問題本質(zhì)是,解除阻塞和等待信號這2步操作不是原子的,出現(xiàn)了競態(tài)條件。這個競態(tài)條件發(fā)生在主程序和信號處理器對同一個被解除信號的競爭關(guān)系。
要避免這個問題,可以通過sigsuspend調(diào)用來等待信號。函數(shù)原型是:
int sigsuspend(const sigset_t *mask);
它接收一個掩碼參數(shù)mask,用mask替換進(jìn)程的信號掩碼,然后掛起進(jìn)程的執(zhí)行,直到捕獲到信號,恢復(fù)進(jìn)程信號掩碼為調(diào)用前的值,然后調(diào)用信號處理器,一旦信號處理器返回,sigsuspend將返回-1,并將errno置為EINTR
五、基于信號的事件架構(gòu)master進(jìn)程啟動之后,就會處于掛起狀態(tài)。它等待著信號的到來,并處理相應(yīng)的事件,如此往復(fù)。本節(jié)讓我們看下nginx是如何基于信號構(gòu)建事件監(jiān)聽框架的。
安裝信號處理器在nginx.c中的main函數(shù)里面,初始化進(jìn)程fork master進(jìn)程之前,就已經(jīng)通過調(diào)用ngx_init_signals函數(shù)安裝好了信號處理器,接下來fork的master以及work進(jìn)程都會繼承這個信號處理器。讓我們看下源代碼:
/* @src/core/nginx.c */ int ngx_cdecl main(int argc, char *const *argv) { .... cycle = ngx_init_cycle(&init_cycle); ... if (ngx_init_signals(cycle->log) != NGX_OK) { //安裝信號處理器 return 1; } if (!ngx_inherited && ccf->daemon) { if (ngx_daemon(cycle->log) != NGX_OK) { //fork master進(jìn)程 return 1; } ngx_daemonized = 1; } ... } /* @src/os/unix/ngx_process.c */ typedef struct { int signo; char *signame; char *name; void (*handler)(int signo); } ngx_signal_t; ngx_signal_t signals[] = { { ngx_signal_value(NGX_RECONFIGURE_SIGNAL), "SIG" ngx_value(NGX_RECONFIGURE_SIGNAL), "reload", ngx_signal_handler }, ... { SIGCHLD, "SIGCHLD", "", ngx_signal_handler }, { SIGSYS, "SIGSYS, SIG_IGN", "", SIG_IGN }, { SIGPIPE, "SIGPIPE, SIG_IGN", "", SIG_IGN }, { 0, NULL, "", NULL } }; ngx_int_t ngx_init_signals(ngx_log_t *log) { ngx_signal_t *sig; struct sigaction sa; for (sig = signals; sig->signo != 0; sig++) { ngx_memzero(&sa, sizeof(struct sigaction)); sa.sa_handler = sig->handler; sigemptyset(&sa.sa_mask); if (sigaction(sig->signo, &sa, NULL) == -1) { #if (NGX_VALGRIND) ngx_log_error(NGX_LOG_ALERT, log, ngx_errno, "sigaction(%s) failed, ignored", sig->signame); #else ngx_log_error(NGX_LOG_EMERG, log, ngx_errno, "sigaction(%s) failed", sig->signame); return NGX_ERROR; #endif } } return NGX_OK; }
全局變量signals是ngx_signal_t的數(shù)組,包含了nginx進(jìn)程(master進(jìn)程和worker進(jìn)程)監(jiān)聽的所有的信號。
ngx_signal_t有4個字段,signo表示信號的編號,signame表示信號的描述字符串,name在nginx -s時使用,用來作為向nginx master進(jìn)程發(fā)送信號的快捷方式,例如nginx -s reload相當(dāng)于向master進(jìn)程發(fā)送一個SIGHUP信號。handler字段表示信號處理器函數(shù)指針。
下面是針對不同的信號安裝的信號處理器列表:
通過上表,可以看到,在nginx中,只要捕獲的信號,信號處理器都是ngx_signal_handler。ngx_signal_handler的實現(xiàn)細(xì)節(jié)將在后面進(jìn)行介紹。
設(shè)置進(jìn)程信號掩碼在ngx_master_process_cycle函數(shù)里面,fork子進(jìn)程之前,master進(jìn)程通過sigprocmask系統(tǒng)調(diào)用,設(shè)置了進(jìn)程的初始信號掩碼,用來阻塞相關(guān)信號。
而對于fork之后的worker進(jìn)程,子進(jìn)程會繼承信號掩碼,不過在worker進(jìn)程初始化的時候,對信號掩碼又進(jìn)行了重置,所以worker進(jìn)程可以并不阻塞信號的傳遞。
void ngx_master_process_cycle(ngx_cycle_t *cycle) { ... sigset_t set; ... sigemptyset(&set); sigaddset(&set, SIGCHLD); sigaddset(&set, SIGALRM); sigaddset(&set, SIGIO); sigaddset(&set, SIGINT); sigaddset(&set, ngx_signal_value(NGX_RECONFIGURE_SIGNAL)); sigaddset(&set, ngx_signal_value(NGX_REOPEN_SIGNAL)); sigaddset(&set, ngx_signal_value(NGX_NOACCEPT_SIGNAL)); sigaddset(&set, ngx_signal_value(NGX_TERMINATE_SIGNAL)); sigaddset(&set, ngx_signal_value(NGX_SHUTDOWN_SIGNAL)); sigaddset(&set, ngx_signal_value(NGX_CHANGEBIN_SIGNAL)); if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, "sigprocmask() failed"); } ...掛起進(jìn)程
當(dāng)做完上面2項準(zhǔn)備工作后,就會進(jìn)入主循環(huán)。在主循環(huán)里面,master進(jìn)程通過sigsuspend系統(tǒng)調(diào)用,等待著信號的到來,在等待的過程中,進(jìn)程一直處于掛起狀態(tài)(S狀態(tài))。至此,master進(jìn)程基于信號的整體事件監(jiān)聽框架講解完成,關(guān)于信號到來之后的邏輯,我們在下一節(jié)討論。
void ngx_master_process_cycle(ngx_cycle_t *cycle) { .... if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, "sigprocmask() failed"); } sigemptyset(&set); //重置信號集合,作為后續(xù)sigsuspend入?yún)ⅲ试S任何信號傳遞 ... ngx_start_worker_processes(cycle, ccf->worker_processes, NGX_PROCESS_RESPAWN); //fork worker進(jìn)程 ngx_start_cache_manager_processes(cycle, 0); //fork cache相關(guān)進(jìn)程 ... for ( ;; ) { ... sigsuspend(&set); //掛起進(jìn)程,等待信號 ... //后續(xù)處理邏輯 } } //end of ngx_master_process_cycle六、主循環(huán) 進(jìn)程數(shù)據(jù)結(jié)構(gòu)
在展開說明之前,我們需要了解下,nginx對進(jìn)程的抽象的數(shù)據(jù)結(jié)構(gòu)。
ngx_int_t ngx_last_process; //ngx_processes數(shù)組中有意義(當(dāng)前有效或曾經(jīng)有效)的進(jìn)程,最大的下標(biāo)+1(下標(biāo)從0開始計算) ngx_process_t ngx_processes[NGX_MAX_PROCESSES]; //所有的子進(jìn)程數(shù)組,NGX_MAX_PROCESSES為1024,也就是nginx子進(jìn)程不能超過1024個。 typedef struct { ngx_pid_t pid; //進(jìn)程pid int status; //進(jìn)程狀態(tài),waitpid調(diào)用獲取 ngx_socket_t channel[2]; //基于匿名socket的進(jìn)程之間通信的管道,由socketpair創(chuàng)建,并通過fork復(fù)制給子進(jìn)程。但一般是單向通信,channel[0]只用來寫,channel[1]只用來讀。 ngx_spawn_proc_pt proc; //子進(jìn)程的循環(huán)方法,比如worker進(jìn)程是ngx_worker_process_cycle void *data; //fork子進(jìn)程后,會執(zhí)行proc(cycle,data) char *name; //進(jìn)程名稱 unsigned respawn:1; //為1時表示受master管理的子進(jìn)程,死掉可以復(fù)活 unsigned just_spawn:1; //為1時表示剛剛新fork的子進(jìn)程,在重新加載配置文件時,會使用到 unsigned detached:1; //為1時表示游離的新的子進(jìn)程,一般用在升級binary時,會fork一個新的master子進(jìn)程,這時新master進(jìn)程是detached,不受原來的master進(jìn)程管理 unsigned exiting:1; //為1時表示正在主動退出,一般收到SIGQUIT或SIGTERM信號后,會置該值為1,區(qū)別于子進(jìn)程的異常被動退出 unsigned exited:1; //為1時表示進(jìn)程已退出,并通過waitpid系統(tǒng)調(diào)用回收 } ngx_process_t;
比如我只啟動了一個worker進(jìn)程,gdb master進(jìn)程,ngx_processes和ngx_last_process的結(jié)果如圖3所示:
圖3-gdb單worker進(jìn)程下ngx_processes和ngx_last_process的結(jié)果
全局標(biāo)記上面我們提到ngx_signal_handler這個函數(shù),它是nginx為捕獲的信號安裝的通用信號處理器。它都干了什么呢?很簡單,它只是用來標(biāo)記對應(yīng)的全局標(biāo)記位為1,這些標(biāo)記位,后續(xù)的主循環(huán)里會使用到,根據(jù)不同的標(biāo)記位,執(zhí)行不同的邏輯。
master進(jìn)程對應(yīng)的信號與全局標(biāo)記位的對應(yīng)關(guān)系如下表:
對于SIGCHLD信號,情況有些復(fù)雜,ngx_signal_handler還會額外多做一件事,那就是調(diào)用ngx_process_get_status函數(shù)去做子進(jìn)程的回收。在ngx_process_get_status內(nèi)部,會使用waitpid系統(tǒng)調(diào)用獲取子進(jìn)程的退出狀態(tài),并回收子進(jìn)程,避免產(chǎn)生僵尸進(jìn)程。同時,會更新ngx_processes數(shù)組中相應(yīng)的退出進(jìn)程的exited為1,表示進(jìn)程已退出,并被父進(jìn)程回收。
現(xiàn)在考慮一個問題:假設(shè)在進(jìn)程屏蔽信號并且進(jìn)行各種標(biāo)記位的邏輯處理期間(下面會講標(biāo)記位的邏輯流程),同時有多個子進(jìn)程退出,會產(chǎn)生多個SIGCHLD信號。但由于SIGCHLD信號是標(biāo)準(zhǔn)信號(非可靠信號),當(dāng)sigsuspend等待信號時,只會被傳遞一個SIGCHLD信號。那么這樣是否有問題呢?答案是否定的,因為ngx_process_get_status這里是循環(huán)的調(diào)用waitpid,所以在一個信號處理器的邏輯流程里面,會回收盡可能多的退出的子進(jìn)程,并且更新ngx_processes中相應(yīng)進(jìn)程的exited標(biāo)記位,因此不會存在漏掉的問題。
static void ngx_process_get_status(void) { ... for ( ;; ) { pid = waitpid(-1, &status, WNOHANG); if (pid == 0) { return; } if (pid == -1) { err = ngx_errno; if (err == NGX_EINTR) { continue; } if (err == NGX_ECHILD && one) { return; } ... return; } ... for (i = 0; i < ngx_last_process; i++) { if (ngx_processes[i].pid == pid) { ngx_processes[i].status = status; ngx_processes[i].exited = 1; process = ngx_processes[i].name; break; } } ... } }
邏輯流程
主循環(huán),針對不同的全局標(biāo)記,執(zhí)行不同action的整體邏輯流程見圖4:
圖4-主循環(huán)邏輯流程
上面的流程圖,總體還是比較復(fù)雜的,根據(jù)具體的場景去分析會更加清晰一些。在此之前,下面先就圖上一些需要描述的給予解釋說明:
1、臨時變量live,它表示是否仍有存活的子進(jìn)程。只有當(dāng)ngx_processes中所有的子進(jìn)程的exited標(biāo)記位都為1時,live才等于0。而master進(jìn)程退出的條件是【!live && (ngx_terminate || ngx_quit)】,即所有的子進(jìn)程都已退出,并且接收到SIGTERM、SIGINT或者SIGQUIT信號時,master進(jìn)程才會正常退出(通過SIGKILL信號殺死m(xù)aster一般在異常情況下使用,這里不算)。
2、在循環(huán)的一開始,會判斷delay是否大于0,這個delay其實只和ngx_terminate即強(qiáng)制退出的場景有關(guān)系。在后面會詳細(xì)講解。
3、ngx_terminate、ngx_quit、ngx_reopen這3種標(biāo)記,master進(jìn)程都會通過上面提到的socket channel向子進(jìn)程進(jìn)行廣播。如果寫socket失敗,會執(zhí)行kill系統(tǒng)調(diào)用向子進(jìn)程發(fā)送信號。而其他的case,master會直接執(zhí)行kill系統(tǒng)調(diào)用向子進(jìn)程發(fā)送信號,比如發(fā)送SIGKILL。關(guān)于socket channel,后續(xù)會進(jìn)行講解。
4、除了和信號直接映射的標(biāo)記位,我們看到,流程圖中還有ngx_noaccepting和ngx_restart這2個全局標(biāo)記位以及ngx_new_binary這個全局變量。ngx_noaccepting表示當(dāng)前master下的所有的worker進(jìn)程正在退出或已退出,不再對外服務(wù)。ngx_restart表示需要重新啟動worker子進(jìn)程,ngx_new_binary表示升級binary時新的master進(jìn)程的pid,這3個都和升級binary有關(guān)系。
socket channelnginx中進(jìn)程之間通信的方式有多種,socket channel是其中之一。這種方式,不如共享內(nèi)存使用的廣泛,目前主要被使用在master進(jìn)程廣播消息到子進(jìn)程,這里面的消息包括下面5種:
#define NGX_CMD_OPEN_CHANNEL 1 //新建或者發(fā)布一個通信管道 #define NGX_CMD_CLOSE_CHANNEL 2 //關(guān)閉一個通信管道 #define NGX_CMD_QUIT 3 //平滑退出 #define NGX_CMD_TERMINATE 4 //強(qiáng)制退出 #define NGX_CMD_REOPEN 5 //重新打開文件
master進(jìn)程在創(chuàng)建子進(jìn)程的時候,fork調(diào)用之前,會在ngx_processes中選擇空閑的ngx_process_t,這個空閑的ngx_process_t的下標(biāo)為s(s不超過1023)。然后通過socketpair調(diào)用創(chuàng)建一對匿名socket,相對應(yīng)的fd存儲在ngx_process_t的channel中。并且把s賦值給全局變量ngx_process_slot,把channel[1]賦值給全局變量ngx_channel。
ngx_pid_t ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,char *name, ngx_int_t respawn) { ...//尋找空閑的ngx_process_t,下標(biāo)為s if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1) //創(chuàng)建匿名socket channel { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, "socketpair() failed while spawning "%s"", name); return NGX_INVALID_PID; } ... ngx_channel = ngx_processes[s].channel[1]; ... ngx_process_slot = s; pid = fork(); //fork調(diào)用,子進(jìn)程繼承socket channel ...
fork之后,子進(jìn)程繼承了這對socket。因為他們共享了相同的系統(tǒng)級打開文件,這時master進(jìn)程寫channel[0],子進(jìn)程就可以通過channel[1]讀取到數(shù)據(jù),master進(jìn)程寫channel[1],子進(jìn)程就可以通過channel[0]讀取到數(shù)據(jù)。子進(jìn)程向master通信也是如此。這樣在fork N個子進(jìn)程之后,實際上會建立N個socket channel,如圖5所示。
圖5-master和子進(jìn)程通過socket channel通信原理
在nginx中,對于socket channel的使用,總是使用channel[0]作為數(shù)據(jù)的發(fā)送端,channel[1]作為數(shù)據(jù)的接收端。并且master進(jìn)程和子進(jìn)程的通信是單向的,因此在后續(xù)子進(jìn)程初始化時關(guān)閉了channel[0],只保留channel[1]即ngx_channel。同時將ngx_channel的讀事件添加到整個nginx高效的事件框架中(關(guān)于事件框架這里限于篇幅不多談),最終實現(xiàn)了master進(jìn)程向子進(jìn)程消息的同步。
了解到這里,其實socket channel已經(jīng)差不多了。但是還不是它的全部,nginx源碼中還提供了通過socket channel進(jìn)行子進(jìn)程之間互相通信的機(jī)制。不過目前來看,沒有實際的使用。
讓我們先思考一個問題:如果要實現(xiàn)worker之間的通信,難點(diǎn)在于什么?答案不難想到,master進(jìn)程fork子進(jìn)程是有順序的,fork最后一個worker和master進(jìn)程一樣,知道所有的worker進(jìn)程的channel[0],因此它可以像master一樣和其他的worker通信。但是第一個worker就很糟糕了,它只知道自己的channel[0](而且還是被關(guān)閉了),也就是第一個worker無法主動向任意其他的woker進(jìn)程通信。在圖6中可以看到,對于第二個worker進(jìn)程,僅僅知道第一個worker的channel[0],因此僅僅可以和第一個worker進(jìn)行通信。
圖6-第二個worker進(jìn)程的channel示意圖
nginx是怎么解決這個問題的呢?簡單來講, nginx使用了進(jìn)程間傳遞文件描述符的技術(shù)。關(guān)于進(jìn)程間傳遞文件描述符,這里關(guān)鍵的系統(tǒng)調(diào)用涉及到2個,socketpair和sendmsg,這里不細(xì)講,有興趣的可以參考下這篇文章:https://pureage.info/2015/03/...。
master在每次fork新的worker的時候,都會通過ngx_pass_open_channel函數(shù)將新創(chuàng)建進(jìn)程的pid以及的socket channel寫端channel[0]傳遞給所有之前創(chuàng)建的worker。上面提到的NGX_CMD_OPEN_CHANNEL就是用來做這件事的。worker進(jìn)程收到這個消息后,會解析消息的pid和fd,存儲到ngx_processes中相應(yīng)slot下的ngx_process_t中。
這里channel[1]并沒有被傳遞給子進(jìn)程,因為channel[1]是接收端,每一個socket channel的channe[1]都唯一對應(yīng)一個子進(jìn)程,worker A持有worker B的channel[1],并沒有任何意義。因此在子進(jìn)程初始化時,會將之前worker進(jìn)程創(chuàng)建的channel[1]全部關(guān)閉掉,只保留的自己的channel[1]。最終,如圖7所示,每一個worker持有自己的channel的channel[1],持有著其他worker對應(yīng)channel的channel[0]。而master則持有者所有的worker對應(yīng)channel的channel[0]和channel[1](為什么這里master仍然保留著所有channel的channe[1],沒有想明白為什么,也許是為了在未來監(jiān)聽worker進(jìn)程的消息)。
圖7-socket channel最終示意圖
這里進(jìn)程退出包含多種場景:
1、worker進(jìn)程異常退出
2、系統(tǒng)管理員使用nginx -s stop或者nginx -s quit讓進(jìn)程全部退出
3、系統(tǒng)管理員使用信號SIGINT,SIGTERM,SIGQUIT等讓進(jìn)程全部退出
4、升級binary期間,新master進(jìn)程退出(當(dāng)發(fā)現(xiàn)重啟的nginx有問題之后,可能會殺死新master進(jìn)程)
對于場景1,master進(jìn)程需要重新拉起新的worker進(jìn)程。對于場景2和3,master進(jìn)程需要等到所有的子進(jìn)程退出后再退出(避免出現(xiàn)孤兒進(jìn)程)。對于場景4,本小節(jié)先不介紹,在后面會介紹binary升級。下面我們了解下master進(jìn)程是如何實現(xiàn)前三個場景的。
處理子進(jìn)程退出子進(jìn)程退出時,發(fā)送SIGCHLD信號給父進(jìn)程,被信號處理器處理,會更新ngx_reap全局標(biāo)記位,并且使用waitpid收集所有的子進(jìn)程,設(shè)置ngx_processes中對應(yīng)slot下的ngx_process_t中的exited為1。然后,在主循環(huán)中使用ngx_reap_children函數(shù),對子進(jìn)程退出進(jìn)行處理。這個函數(shù)非常重要,是理解進(jìn)程退出的關(guān)鍵。
圖8-ngx_reap_children函數(shù)流程圖
通過上圖,可以看到ngx_reap_children函數(shù)的整體執(zhí)行流程。它遍歷ngx_processes數(shù)組里有效(pid不等于-1)的worker進(jìn)程:
一、如果子進(jìn)程的exited標(biāo)志位為1(即已退出并被master回收)
1、如果子進(jìn)程是游離進(jìn)程(detached為1)
1.1、如果退出的子進(jìn)程是新master進(jìn)程(升級binary時會fork一個新的master進(jìn)程),會將舊的pid文件恢復(fù),即恢復(fù)使用當(dāng)前的master來服務(wù)【場景4】
(1)如果當(dāng)前master進(jìn)程已經(jīng)將它下面的worker都?xì)⒌袅耍╪gx_noaccepting為1),這時會修改全局標(biāo)記位ngx_restart為1,然后跳到步驟1.c。在外層的主循環(huán)里,檢測到這個標(biāo)記位,master進(jìn)程便會重新fork worker進(jìn)程
(2)如果當(dāng)前的master進(jìn)程還沒有殺死他的子進(jìn)程,直接跳到步驟1.c
1.2、如果退出的子進(jìn)程是其他進(jìn)程,直接跳到步驟1.c(實際上這種case不存在,因為目前看,所有的detached的進(jìn)程都是新master進(jìn)程。detached只有在升級binary時才使用到)
2、如果子進(jìn)程不是游離進(jìn)程(detached為0),通過socket channel通知其他的worker進(jìn)程N(yùn)GX_CMD_CLOSE_CHANNEL指令,管道需要關(guān)閉(我要死了,以后不用給我打電話了)
2.1、如果子進(jìn)程是需要復(fù)活的(進(jìn)程標(biāo)記respawn為1,并沒有收到過相關(guān)退出信號),那么fork新的worker進(jìn)程取代死掉的子進(jìn)程,并通過socket channel通知其他的worker進(jìn)程N(yùn)GX_CMD_OPEN_CHANNEL指令,新的worker已啟動,請記錄好新啟動進(jìn)程的pid和channel[0](大家好,我是新worker xxx,這是我的電話,有事隨時call me),同時置live為1,表示還有存活的子進(jìn)程,master進(jìn)程不可退出。然后繼續(xù)遍歷下一個進(jìn)程【場景1】
2.2、如果不需要復(fù)活,直接跳到步驟1.c【場景2+場景3】
3、對于退出的進(jìn)程,置ngx_process_t中的pid為-1,繼續(xù)遍歷下一個進(jìn)程
二、如果子進(jìn)程exited標(biāo)志為0,即沒有退出
1、如果子進(jìn)程是非游離進(jìn)程,那么更新live為1,然后繼續(xù)遍歷下一個進(jìn)程。live為1表示還有存活的子進(jìn)程,master進(jìn)程不可退出(對這里的判斷條件ngx_processes[i].exiting || !ngx_processes[i].detached存疑,大部分worker都是非游離,游離的進(jìn)程只有升級 binary時的新master進(jìn)程,但是新master退出時,并不會修改exiting為1,所以個人覺得這里的ngx_processes[i].exiting的判斷沒有必要,只需要判斷是否游離進(jìn)程即可)
2、如果子進(jìn)程是游離進(jìn)程,那么忽略,遍歷下一個進(jìn)程。也就是說,master并不會因為游離子進(jìn)程沒有退出,而停止退出的步伐。(在這種case下,游離進(jìn)程就像別人家的孩子一樣,master不再關(guān)心)
最終,ngx_reap_children會妥善的處理好各種場景的子進(jìn)程退出,并且返回live的值。即告訴主循環(huán),當(dāng)前是否仍有存活的子進(jìn)程存在。在主循環(huán)里,當(dāng)!live && (ngx_terminate || ngx_quit)條件滿足時,master進(jìn)程就會做相應(yīng)的進(jìn)程退出工作(刪除pid文件,調(diào)用每一個模塊的exit_master函數(shù),關(guān)閉監(jiān)聽的socket,釋放內(nèi)存池)。
觸發(fā)子進(jìn)程退出對于場景2和場景3,當(dāng)master進(jìn)程收到SIGTERM或者SIGQUIT信號時,會在信號處理器中設(shè)置ngx_terminate或ngx_quit全局標(biāo)記。當(dāng)主循環(huán)檢測到這2種標(biāo)記時,會通過socket channel向所有的子進(jìn)程廣播消息,傳遞的指令分別是:NGX_CMD_TERMINATE或NGX_CMD_QUIT。子進(jìn)程通過事件框架檢測到該消息后,同樣會設(shè)置ngx_terminate或者ngx_quit標(biāo)記位為1(注意這里是子進(jìn)程的全局變量)。子進(jìn)程的主循環(huán)里檢測到ngx_terminate時,會立即做進(jìn)程退出工作(調(diào)用每一個模塊的exit_process函數(shù),釋放內(nèi)存池),而檢測到ngx_quit時,情況會稍微復(fù)雜些,需要釋放連接,關(guān)閉監(jiān)聽socket,并且會等待所有請求以及定時事件都被妥善的處理完之后,才會做進(jìn)程退出工作。
這里可能會有一個隱藏的問題:進(jìn)程的退出可能沒法被一次waitpid全部收集到,有可能有漏網(wǎng)之魚還沒有退出,需要等到下次的suspend才能收集到。如果按照上面的邏輯,可能存在重復(fù)給子進(jìn)程發(fā)送退出指令的問題。nginx比較嚴(yán)謹(jǐn),針對這個問題有自己的處理方式:
ngx_quit:一旦給某一個worker進(jìn)程發(fā)送了退出指令(強(qiáng)制退出或平滑退出),會記錄該進(jìn)程的exiting為1,表示這個進(jìn)程正在退出。以后,如果還要再給該進(jìn)程發(fā)送退出NGX_CMD_QUIT指令,一旦發(fā)現(xiàn)這個標(biāo)記位為1,那么就忽略。這樣就可以保證一次平滑退出,針對每一個worker只通知一次,不重復(fù)通知。
ngx_terminate:和ngx_quit略有不同,它不依賴exiting標(biāo)記位,而是通過sigio的臨時變量(不是SIGIO信號)來緩解這個問題。在向worker進(jìn)程廣播NGX_CMD_TERMINATE之前,會置sigio為worker進(jìn)程數(shù)+2(2個cache進(jìn)程),每次信號到來(假設(shè)每次到來的信號都是SIGCHLD,并且只wait了一個子進(jìn)程退出),sigio會減一。直到sigio為0,又會重新廣播NGX_CMD_TERMINATE給worker進(jìn)程。sigio大于0的期間,master是不會重復(fù)給worker發(fā)送指令的。(這里只是緩解,并沒有完全屏蔽掉重復(fù)發(fā)指令的問題,至于為什么沒有像ngx_quit一樣處理,不是很明白這么設(shè)計的原因)
ngx_terminate的timeout機(jī)制還記得上面提到的delay嗎?這個變量只有在ngx_terminate為1時才大于0,那么它是用來干什么的?實際上,它用來在進(jìn)程強(qiáng)制退出時做倒計時使用。
master進(jìn)程為了保證所有的子進(jìn)程最終都會退出,會給子進(jìn)程一定的時間,如果那時候仍有子進(jìn)程沒有退出,會直接使用SIGKILL信號殺死所有子進(jìn)程。
當(dāng)最開始master進(jìn)程處理ngx_terminate(第一次收到SIGTERM或者SIGINT信號)時,會將delay從0改為50ms。在下一個主循環(huán)的開始將設(shè)置一個時間為50ms的定時器。然后等待信號的到來。這時,子進(jìn)程可能會陸續(xù)退出產(chǎn)生SIGCHLD信號。理想的情況下,這一個sigsuspend信號處理周期里面,將全部的子進(jìn)程進(jìn)行回收,那么master進(jìn)程就可以立刻全身而退了,如圖9所示:
圖9-理想退出情況
當(dāng)然,糟糕的情況總是會發(fā)生,這期間沒有任何SIGCHLD信號產(chǎn)生,直到50ms到了產(chǎn)生SIGALRM信號,SIGALRM產(chǎn)生后,會將sigio重置為0,并將delay翻倍,設(shè)置一個新的定時器。當(dāng)下個sigsuspend周期進(jìn)來的時候,由于sigio為0,master進(jìn)程會再次向worker進(jìn)程廣播NGX_CMD_TERMINATE消息(催促worker進(jìn)程盡快退出)。如此往復(fù),直到所有的子進(jìn)程都退出,或者delay超過1000ms之后,master直接通過SIGKILL殺死子進(jìn)程。
圖10-糟糕的退出場景timeout機(jī)制
nginx支持在不停止服務(wù)的情況下,重新加載配置文件并生效。通過nginx -s reload即可。通過前面可以看到,nginx -s reload實際上是向master進(jìn)程發(fā)送SIGHUP信號,信號處理器會置ngx_reconfigure為1。
當(dāng)主循環(huán)檢測到ngx_reconfigure為1時,首先調(diào)用ngx_init_cycle函數(shù)構(gòu)造一個新的生命周期cycle對象,重新加載配置文件。然后根據(jù)新的配置里設(shè)定的worker_processes啟動新的worker進(jìn)程。然后sleep 100ms來等待著子進(jìn)程的啟動和初始化,更新live為1,最后,通過socket channel向舊的worker進(jìn)程發(fā)送NGX_CMD_QUIT消息,讓舊的worker優(yōu)雅退出。
if (ngx_reconfigure) { ngx_reconfigure = 0; if (ngx_new_binary) { ngx_start_worker_processes(cycle, ccf->worker_processes, NGX_PROCESS_RESPAWN); ngx_start_cache_manager_processes(cycle, 0); ngx_noaccepting = 0; continue; } ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "reconfiguring"); cycle = ngx_init_cycle(cycle); if (cycle == NULL) { cycle = (ngx_cycle_t *) ngx_cycle; continue; } ngx_cycle = cycle; ccf = (ngx_core_conf_t *) ngx_get_conf(cycle->conf_ctx, ngx_core_module); ngx_start_worker_processes(cycle, ccf->worker_processes, //fork新的worker進(jìn)程 NGX_PROCESS_JUST_RESPAWN); ngx_start_cache_manager_processes(cycle, 1); /* allow new processes to start */ ngx_msleep(100); live = 1; ngx_signal_worker_processes(cycle, //讓舊的worker進(jìn)程退出 ngx_signal_value(NGX_SHUTDOWN_SIGNAL)); }
可以看到,nginx并沒有讓舊的worker進(jìn)程重新reload配置文件,而是通過新進(jìn)程替換舊進(jìn)程的方式來完成了配置文件的重新加載。
對于master進(jìn)程來說,如何區(qū)分新的worker進(jìn)程和舊的worker進(jìn)程呢?在fork新的worker時,傳入的flag是NGX_PROCESS_JUST_RESPAWN,傳入這個標(biāo)記之后,fork的子進(jìn)程的just_spawn和respawn2個標(biāo)記會被置為1。而舊的worker在fork時傳入的flag是NGX_PROCESS_RESPAWN,它只會將respawn標(biāo)記置為1。因此,在通過socket channel發(fā)送NGX_CMD_QUIT命令時,如果發(fā)現(xiàn)子進(jìn)程的just_spawn標(biāo)記為1,那么就會忽略該命令(要不然新的worker進(jìn)程也會被無辜?xì)⑺懒耍缓骿ust_spwan標(biāo)記會恢復(fù)為0(不然未來reload時,就無法區(qū)分新舊worker了)。
細(xì)心的同學(xué)還可以看到,在上面還有一個當(dāng)ngx_new_binary為真時的邏輯分支,它竟然直接使用舊的配置文件,fork新的子進(jìn)程就continue了。對于這段代碼我得理解是這樣:
ngx_new_binary上面提到過,是升級binary時的新master進(jìn)程的pid,這個場景應(yīng)該是正在升級binary過程中,舊的master進(jìn)程還沒有推出。如果這時通過nginx -s reload去重新加載配置文件,只會給新的master進(jìn)程發(fā)送SIGHUP信號(因為這時的pid文件記錄的新master進(jìn)程的pid),因此走到這個邏輯分支,說明是手動使用kill -HUP發(fā)送給舊的master進(jìn)程的,對于升級中這個中間過程,舊的master進(jìn)程并沒有重新加載最新的配置文件,因為沒有必要,舊的master和舊worker進(jìn)行最終的歸宿是被殺死,所以這里就簡單的fork了下,其實這里我覺得舊master進(jìn)程忽略這個信號也未嘗不可。
重新打開文件在日志切分場景,重新打開文件這個feature非常有用。線上nginx服務(wù)產(chǎn)生的日志量是巨大的,隨著時間的累積,會產(chǎn)生超大文件,對于排查問題非常不方便。
所以日志切割很有必要,那么日志是如何切割的?直接mv nginx.log nginx.log.xxx,然后再新建一個nginx.log空文件,這樣可行嗎?答案當(dāng)然是否。這涉及到fd,打開文件表和inode的概念。在這里簡單描述下:
見圖11(引用網(wǎng)絡(luò)圖片),fd是進(jìn)程級別的,fd會指向一個系統(tǒng)級的打開文件表中的一個表項。這個表項如果指代的是磁盤文件的話,會有一個指向磁盤inode節(jié)點(diǎn)的指針,并且這里還會存儲文件偏移量等信息。磁盤文件是通過inode進(jìn)行管理的,inode里會存儲著文件的user、group、權(quán)限、時間戳、硬鏈接以及指向數(shù)據(jù)塊的指針。進(jìn)程通過fd寫文件,最終寫到的是inode節(jié)點(diǎn)對應(yīng)的數(shù)據(jù)區(qū)域。如果我們通過mv命令對文件進(jìn)行了重命名,實際上該fd與inode之間的映射鏈路并不會受到影響,也就是最終仍然向同一塊數(shù)據(jù)區(qū)域?qū)憯?shù)據(jù),最終表現(xiàn)就是,nginx.log.xxx中日志仍然會源源不斷的產(chǎn)生。而新建的nginx.log空文件,它對應(yīng)的是另外的inode節(jié)點(diǎn),和fd毫無關(guān)系,因此,nginx.log不會有日志產(chǎn)生的。
圖11-fd、打開文件表、inode關(guān)系(引用網(wǎng)絡(luò)圖片)
那么我們一般要怎么切割日志呢?實際上,上面的操作做對了一半,mv是沒有問題的,接下來解決內(nèi)存中fd映射到新的inode節(jié)點(diǎn)就可以搞定了。所以這就是重新打開文件發(fā)揮作用的時候了。
向master進(jìn)程發(fā)送SIGUSR1信號,在信號處理器里會置ngx_reopen全局標(biāo)記為1。當(dāng)主循環(huán)檢測到ngx_reopen為1時,會調(diào)用ngx_reopen_files函數(shù)重新打開文件,生成新的fd,然后關(guān)閉舊的fd。然后通過socket channel向所有worker進(jìn)程廣播NGX_CMD_REOPEN指令,worker進(jìn)程針對NGX_CMD_REOPEN指令也采取和master一樣的動作。
對于日志分割場景,重新打開之后的日志數(shù)據(jù)就可以在新的nginx.log中看到了,而nginx.log.xxx也不再會有數(shù)據(jù)寫入,因為相應(yīng)的fd都已close。
升級binarynginx支持不停止服務(wù)的情況下,平滑升級nginx binary程序。一般的操作步驟是:
- 1、先向master進(jìn)程發(fā)送SIGUSR2信號,產(chǎn)生新的master和新的worker進(jìn)程。(注意這時同時存在2個master+worker集群) - 2、向舊的master進(jìn)程發(fā)送SIGWINCH信號,這樣舊的worker進(jìn)程就會全部退出。 - 3、新的集群如果服務(wù)正常的話,就可以向舊的master進(jìn)程發(fā)送SIGQUIT信號,讓它退出。
master進(jìn)程收到SIGUSR2信號后,信號處理器會置ngx_change_binary為1。主循環(huán)檢測到該標(biāo)記位后,會調(diào)用ngx_exec_new_binary函數(shù)產(chǎn)生一個新的master進(jìn)程,并且將新master進(jìn)程的pid賦值給ngx_new_binary。
讓我們看下ngx_exec_new_binary如何產(chǎn)生新master進(jìn)程的。首先會構(gòu)建一個ngx_exec_ctx_t類型的臨時變量ctx,ngx_exec_ctx_t結(jié)構(gòu)體如下:
``
typedef struct {
char *path; //binary路徑 char *name; //新進(jìn)程名稱 char *const *argv; //參數(shù) char *const *envp; //環(huán)境變量
} ngx_exec_ctx_t;
``
如圖12所示,所示將ctx.path置為啟動master進(jìn)程的nginx程序路徑,比如"/home/xiaoju/nginx-jiweibin/sbin/nginx",ctx.name置為"new binary process",ctx.argv置為nginx main函數(shù)執(zhí)行時傳入的參數(shù)集合。對于環(huán)境變量,除了繼承當(dāng)前master進(jìn)程的環(huán)境變量外,會構(gòu)造一個名為NGINX的環(huán)境變量,它的取值是所有監(jiān)聽的socket對應(yīng)fd按";"分割,例如:NGINX="8;9;10;..."。這個環(huán)境變量很關(guān)鍵,下面會提到它的作用。
圖12-ngx_exec_ctx_t ctx示意圖
構(gòu)造完ctx后,將pid文件重命名,后面加上".old"后綴。然后調(diào)用ngx_execute函數(shù)。這個函數(shù)內(nèi)部會通過ngx_spawn_process函數(shù)fork一個新的子進(jìn)程,該進(jìn)程的標(biāo)記detached為1,表示是游離進(jìn)程。該子進(jìn)程一旦啟動后,會執(zhí)行ngx_execute_proc函數(shù),這里會執(zhí)行execve系統(tǒng)調(diào)用,重新執(zhí)行ctx.path,即exec nginx程序。這樣,新的master進(jìn)程就通過fork+execve2個系統(tǒng)調(diào)用啟動起來了。隨后,新master進(jìn)程會啟動新的的worker進(jìn)程。
ngx_pid_t ngx_execute(ngx_cycle_t *cycle, ngx_exec_ctx_t *ctx) { return ngx_spawn_process(cycle, ngx_execute_proc, ctx, ctx->name, //fork 新的子進(jìn)程 NGX_PROCESS_DETACHED); } static void ngx_execute_proc(ngx_cycle_t *cycle, void *data) //fork新的mast { ngx_exec_ctx_t *ctx = data; if (execve(ctx->path, ctx->argv, ctx->envp) == -1) { ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno, "execve() failed while executing %s "%s"", ctx->name, ctx->path); } exit(1); }
其實這里是有一個問題要解決的:舊的master進(jìn)程對于80,8080這種監(jiān)聽端口已經(jīng)bind并且listen了,如果新的master進(jìn)程進(jìn)行同樣的bind操作,會產(chǎn)生類似這種錯誤:nginx: [emerg] bind() to 0.0.0.0:8080 failed (98: Address already in use)。所以,master進(jìn)程是如何做到監(jiān)聽這些端口的呢?
讓我們先了解exec(execve是exec系列系統(tǒng)調(diào)用的一種)這個系統(tǒng)調(diào)用,它并不改變進(jìn)程的pid,但是它會用新的程序(這里還是nginx)替換現(xiàn)有進(jìn)程的代碼段,數(shù)據(jù)段,BSS,堆,棧。比如ngx_processes這個全局變量,它處于BSS段,在exec之后,這個數(shù)據(jù)會清空,新的master不會通過ngx_processes數(shù)組引用到舊的worker進(jìn)程。同理,存儲著所有監(jiān)聽的數(shù)據(jù)結(jié)構(gòu)cycle.listening由于在進(jìn)程的堆上,同樣也會清空。但fd比較特殊,對于進(jìn)程創(chuàng)建的fd,exec之后仍然有效(除非設(shè)置了FD_CLOEXEC標(biāo)記,nginx的打開的相關(guān)文件都設(shè)置了這個標(biāo)記,但監(jiān)聽socket對應(yīng)的fd沒有設(shè)置)。所以舊的master打開了某一個80端口的fd假設(shè)是9,那么在新的master進(jìn)程,仍然可以繼續(xù)使用這個fd。所以問題就變成了,如何讓新的master進(jìn)程知道這些fd的存在,并重新構(gòu)建cycle.listening數(shù)組?
這就用到了上面提到的NGINX這個環(huán)境變量,它將所有的fd通過NGINX傳遞給新master進(jìn)程,新master進(jìn)程看到這個環(huán)境變量后,就可以根據(jù)它的值,重新構(gòu)建cycle.listening數(shù)組啦。代碼如下:
static ngx_int_t ngx_add_inherited_sockets(ngx_cycle_t *cycle) { u_char *p, *v, *inherited; ngx_int_t s; ngx_listening_t *ls; inherited = (u_char *) getenv(NGINX_VAR); if (inherited == NULL) { return NGX_OK; } ngx_log_error(NGX_LOG_NOTICE, cycle->log, 0, "using inherited sockets from "%s"", inherited); if (ngx_array_init(&cycle->listening, cycle->pool, 10, sizeof(ngx_listening_t)) != NGX_OK) { return NGX_ERROR; } for (p = inherited, v = p; *p; p++) { if (*p == ":" || *p == ";") { s = ngx_atoi(v, p - v); if (s == NGX_ERROR) { ngx_log_error(NGX_LOG_EMERG, cycle->log, 0, "invalid socket number "%s" in " NGINX_VAR " environment variable, ignoring the rest" " of the variable", v); break; } v = p + 1; ls = ngx_array_push(&cycle->listening); if (ls == NULL) { return NGX_ERROR; } ngx_memzero(ls, sizeof(ngx_listening_t)); ls->fd = (ngx_socket_t) s; } } ngx_inherited = 1; return ngx_set_inherited_sockets(cycle); }
這里還有一個需要知道的細(xì)節(jié),舊master進(jìn)程fork子進(jìn)程并exec nginx程序之后,并不會像上面的daemon模式一樣,再fork一個子進(jìn)程作為master,因為這個子進(jìn)程不屬于任何終端,不會隨著終端退出而退出,因此這個exec之后的子進(jìn)程就是新master進(jìn)程,那么nginx程序是如何區(qū)分這2種啟動模式的呢?同樣也是基于NGINX這個環(huán)境變量,如上面代碼所示,如果存在這個環(huán)境變量,ngx_inherited會被置為1,當(dāng)nginx檢測到這個標(biāo)記位為1時,就不會再fork子進(jìn)程作為master了,而是本身就是master進(jìn)程。
當(dāng)舊的master進(jìn)程收到SIGWINCH信號,信號處理器會置ngx_noaccept為1。當(dāng)主循環(huán)檢測到這個標(biāo)記時,會置ngx_noaccepting為1,表示舊的master進(jìn)程下的worker進(jìn)程陸續(xù)都會退出,不再對外服務(wù)了。然后通過socket channel通知所有的worker進(jìn)程N(yùn)GX_CMD_QUIT指令,worker進(jìn)程收到該指令,會優(yōu)雅的退出(注意,這里的worker進(jìn)程是指舊master進(jìn)程管理的worker進(jìn)程,為什么通知不到新的worker進(jìn)程,大家可以想下為什么)。
最后,當(dāng)新的worker進(jìn)程服務(wù)正常之后,可以放心的殺死舊的master進(jìn)程了。為什么不通過SIGQUIT一步殺死舊的master+worker呢?之所以不這么做,是為了可以隨時回滾。當(dāng)我們發(fā)現(xiàn)新的binary有問題時,如果舊的master進(jìn)程被我干掉了,我們還要使用backup的舊的binary再啟動,這個切換時間一旦過長,會造成比較嚴(yán)重的影響,可能更糟糕的情況是你根本沒有對舊的binary進(jìn)程備份,這樣就需要回滾代碼,重新編譯,安裝。整個回滾的時間會更加不可控。所以,當(dāng)我們再升級binary時,一般都要留著舊master進(jìn)程,因為它可以按照舊的binary隨時重啟worker進(jìn)程。
還記得上面講到子進(jìn)程退出的邏輯嗎,新的master進(jìn)程是舊master進(jìn)程的child,當(dāng)新master進(jìn)程退出,并且ngx_noaccepting為1,即舊master進(jìn)程已經(jīng)殺了了它的worker(不包括新master,因為它是detached),那么會置ngx_restart為1,當(dāng)主循環(huán)檢測到這個全局標(biāo)記位,會再次啟動worker進(jìn)程,讓舊的binary恢復(fù)工作。
if (ngx_restart) { ngx_restart = 0; ngx_start_worker_processes(cycle, ccf->worker_processes, NGX_PROCESS_RESPAWN); ngx_start_cache_manager_processes(cycle, 0); live = 1; }七、總結(jié)
本篇wiki分析了master進(jìn)程啟動,基于信號的事件循環(huán)架構(gòu),基于各種標(biāo)記位的相應(yīng)進(jìn)程的管理,包括進(jìn)程退出,配置文件變更,重新打開文件,升級binary以及master和worker通信的一種方式之一:socket channel。希望大家有所收獲。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/40136.html
摘要:而對于堆內(nèi)存,通常需要程序員進(jìn)行管理。二內(nèi)存池管理說明本部分使用的版本為具體源碼參見文件實現(xiàn)使用流程內(nèi)存池的使用較為簡單可以分為步,調(diào)用函數(shù)獲取指針。將內(nèi)存塊按照的整數(shù)次冪進(jìn)行劃分最小為最大為。 運(yùn)營研發(fā)團(tuán)隊 施洪寶 一. 概述 應(yīng)用程序的內(nèi)存可以簡單分為堆內(nèi)存,棧內(nèi)存。對于棧內(nèi)存而言,在函數(shù)編譯時,編譯器會插入移動棧當(dāng)前指針位置的代碼,實現(xiàn)棧空間的自管理。而對于堆內(nèi)存,通常需要程序...
摘要:的部分是基于以及協(xié)議的。例如父進(jìn)程向中寫入子進(jìn)程從中讀取子進(jìn)程向中寫入父進(jìn)程從中讀取。默認(rèn)使用對進(jìn)程進(jìn)行分配交給對應(yīng)的線程進(jìn)行監(jiān)聽線程收到某個進(jìn)程的數(shù)據(jù)后會進(jìn)行處理值得注意的是這個線程可能并不是發(fā)送請求的那個線程。 作者:施洪寶 一. 基礎(chǔ)知識 1.1 swoole swoole是面向生產(chǎn)環(huán)境的php異步網(wǎng)絡(luò)通信引擎, php開發(fā)人員可以利用swoole開發(fā)出高性能的server服務(wù)。...
摘要:在中,用戶主要通過配置文件的塊來控制和調(diào)節(jié)事件模塊的參數(shù)。中,事件會使用結(jié)構(gòu)體來表示。初始化定時器,該定時器就是一顆紅黑樹,根據(jù)時間對事件進(jìn)行排序。 運(yùn)營研發(fā)團(tuán)隊 譚淼 一、nginx模塊介紹 高并發(fā)是nginx最大的優(yōu)勢之一,而高并發(fā)的原因就是nginx強(qiáng)大的事件模塊。本文將重點(diǎn)介紹nginx是如果利用Linux系統(tǒng)的epoll來完成高并發(fā)的。 首先介紹nginx的模塊,nginx...
摘要:可以通過等方式按照協(xié)議通信。上述都需要發(fā)送結(jié)束包。函數(shù)所需的變量在進(jìn)入該函數(shù)之前認(rèn)為已經(jīng)初始化完成。和都有自己的,且互不干涉,后續(xù)發(fā)送的序列號以此為基準(zhǔn)。 運(yùn)營研發(fā)團(tuán)隊 施洪寶 一. FastCGI協(xié)議簡介 1.1 簡介 FastCGI(Fast Common Gateway Interface, 快速通用網(wǎng)關(guān)接口)是一種通信協(xié)議。可以通過Unix Domain Socket, Na...
閱讀 3093·2023-04-26 00:53
閱讀 3544·2021-11-19 09:58
閱讀 1705·2021-09-29 09:35
閱讀 3302·2021-09-28 09:46
閱讀 3875·2021-09-22 15:38
閱讀 2700·2019-08-30 15:55
閱讀 3022·2019-08-23 14:10
閱讀 3837·2019-08-22 18:17