Nginx服務器在使用Master-Worker模型時,會涉及到三類通信:Linux系統與Nginx通信,Master進程與Worker進程通信,Worker進程間通信,也採用了三種不同的通信機制。
Linux信號
Linux 系統與Nginx是通過信號進行通信的,例如在Linux命令行敲下 ./nginx -s stop ,實際系統會新開一個Master進程,該進程負責向原Master發送信號,發送完信號該進程就掛了,原Master進程接收到信號後執行相應的操作。
1.信號定義與註冊
Nginx定義了一個ngx_signal_t結構體用於描述接收到信號的行爲
typedef struct { //signals
int signo; //需要處理的信號
char *signame; //信號對應的字符串名稱
char *name; //這個信號對應着的Nginx命令
void (*handler)(int signo); //收到signo信號後就會回調handler方法
} ngx_signal_t;
Nginx在啓動時會調用ngx_init_signals函數,該函數通過sigaction系統調用初始化所有信號,並註冊相應的信號處理函數。完成所有初始化工作後,Master進程調用ngx_master_process_cycle函數進入自身事件循環,負責監聽信號,一旦收到信號後,執行對應信號處理函數。
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;
}
2.信號發送
執行 ./nginx -s stop 命令將會開啓新的Master進程,該進程根據-s參數知道用戶要給Nginx發送信號,通過ngx_get_options()函數解析出所要發送的信號,然後將執行ngx_signal_process()函數,該函數首先獲取原Master進程的pid,然後給原Master進程發送具體的信號,完成通信。
case 's':
if (*p) {
ngx_signal = (char *) p;
} else if (argv[++i]) {
ngx_signal = argv[i];
} else {
ngx_log_stderr(0, "option \"-s\" requires parameter");
return NGX_ERROR;
}
if (ngx_strcmp(ngx_signal, "stop") == 0
|| ngx_strcmp(ngx_signal, "quit") == 0
|| ngx_strcmp(ngx_signal, "reopen") == 0
/*
reload實際上是執行reload的nginx進程向原master+worker中的master進程發送reload信號,源master收到後,啓動新的worker進程,同時向源worker
進程發送quit信號,等他們處理完已有的數據信息後,退出,這樣就只有新的worker進程運行。見ngx_signal_handler
*/
|| ngx_strcmp(ngx_signal, "reload") == 0)
{
ngx_process = NGX_PROCESS_SIGNALLER;
goto next;
}
/*
該函數作用:
讀取ngx_core_module模塊的配置結構ngx_core_conf_t;
根據配置結構找到其工作進程文件,如"/usr/local/nginx/logs/nginx.pid"(該文件保存nginx進程ID,即pid);
打開該文件,讀取pid;
調用ngx_os_signal_process()發送信號;
*/
ngx_int_t ngx_signal_process(ngx_cycle_t *cycle, char *sig)
{
...
//打開存放master進程的文件NGX_PID_PATH
file.name = ccf->pid;
file.log = cycle->log;
file.fd = ngx_open_file(file.name.data, NGX_FILE_RDONLY,
NGX_FILE_OPEN, NGX_FILE_DEFAULT_ACCESS);
if (file.fd == NGX_INVALID_FILE) {
ngx_log_error(NGX_LOG_ERR, cycle->log, ngx_errno,
ngx_open_file_n " \"%s\" failed", file.name.data);
return 1;
}
n = ngx_read_file(&file, buf, NGX_INT64_LEN + 2, 0);
if (ngx_close_file(file.fd) == NGX_FILE_ERROR) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
ngx_close_file_n " \"%s\" failed", file.name.data);
}
if (n == NGX_ERROR) {
return 1;
}
while (n-- && (buf[n] == CR || buf[n] == LF)) { /* void */ }
pid = ngx_atoi(buf, ++n); //master進程id
if (pid == NGX_ERROR) {
ngx_log_error(NGX_LOG_ERR, cycle->log, 0,
"invalid PID number \"%*s\" in \"%s\"",
n, buf, file.name.data);
return 1;
}
return ngx_os_signal_process(cycle, sig, pid);
}
Channel
Nginx中利用socketpair()函數,創建一對相互連接的socket,實現父子進程的通信,即Master進程與Worker進程通信,在Nginx中,將這種通信定義爲頻道——channel。
1.channel定義
typedef struct {
ngx_uint_t command; //對端將要做得命令 取值爲NGX_CMD_OPEN_CHANNEL等
ngx_pid_t pid; //當前的子進程id 進程ID,一般是發送命令方的進程ID
ngx_int_t slot; //在全局進程表中的位置 表示發送命令方在ngx_processes進程數組間的序號
ngx_fd_t fd; //傳遞的fd 通信的套接字句柄
} ngx_channel_t;
2.channel註冊
Master進程通過fork()函數創建子進程,在fork之前首先調用socketpair()創建一對關聯的套接字,用於父子進程間的通信,子進程也將會獲得該套接字,在子進程初始化的時候,將套接字加入epoll等待父進程的消息,並註冊消息處理函數。父進程通過ngx_write_channel()發送消息,子進程通過ngx_read_channel()讀取消息。
ngx_pid_t ngx_spawn_process(ngx_cycle_t *cycle, ngx_spawn_proc_pt proc, void *data,
char *name, ngx_int_t respawn) //respawn取值爲NGX_PROCESS_RESPAWN等,或者爲進程在ngx_processes[]中的序號
{
...
if (socketpair(AF_UNIX, SOCK_STREAM, 0, ngx_processes[s].channel) == -1) //在ngx_worker_process_init中添加到事件集
{
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
"socketpair() failed while spawning \"%s\"", name);
return NGX_INVALID_PID;
}
/* 設置master的channel[0](即寫端口),channel[1](即讀端口)均爲非阻塞方式 */
if (ngx_nonblocking(ngx_processes[s].channel[0]) == -1) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
ngx_nonblocking_n " failed while spawning \"%s\"",
name);
ngx_close_channel(ngx_processes[s].channel, cycle->log);
return NGX_INVALID_PID;
}
if (ngx_nonblocking(ngx_processes[s].channel[1]) == -1) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, ngx_errno,
ngx_nonblocking_n " failed while spawning \"%s\"",
name);
ngx_close_channel(ngx_processes[s].channel, cycle->log);
return NGX_INVALID_PID;
}
...
pid = fork();
}
static void ngx_worker_process_init(ngx_cycle_t *cycle, ngx_int_t worker)
{
//調用epoll add 把ngx_chanel 加入epoll 中
if (ngx_add_channel_event(cycle, ngx_channel, NGX_READ_EVENT,
ngx_channel_handler) //在ngx_spawn_process中賦值
== NGX_ERROR)
{
/* fatal */
exit(2);
}
}
共享內存
Nginx中各Worker進程是通過共享內存進行通信的,共享內存是Linux下提供的最基本的進程間通信方式,它通過mmap和shmget系統調用在內存中創建了一塊連續的線性地址空間,而通過munmap或者shmdt系統調用可以釋放這塊內存。使用共享內存的好處是當多個進程使用同一塊共享內存時,在任何一個進程修改了共享內存中的內容後,其他進程通過訪問這段共享內存都能夠得到修改後的內容。
Nginx定義了ngx_shm_t結構體,用於描述一塊共享內存
typedef struct {
u_char *addr; //共享內存起始地址
size_t size; //共享內存空間大小
ngx_str_t name; //這塊共享內存的名稱
ngx_log_t *log; //shm.log = cycle->log; 記錄日誌的ngx_log_t對象
ngx_uint_t exists; /* unsigned exists:1; */ //表示共享內存是否已經分配過的標誌位,爲1時表示已經存在
} ngx_shm_t;
操作ngx_shm_t結構體的方法有兩個:ngx_shm_alloc(基於mmap實現)用於分配新的共享內存,而ngx_shm_free(基於munmap實現)用於釋放已經存在的共享內存。共享內存由Master進程創建,Worker進程共享。Nginx 解決驚羣問題,是通過設置互斥鎖,只有擁有互斥鎖的工作進程才能擔負與客戶建立連接的任務,這個互斥鎖就放於共享內存中。另外,ngin統計連接數,這個全局變量也放於共享內存中。