nginx進程間的通信
進程間消息傳遞
共享內存
共享內存還是Linux下提供的最主要的進程間通信方式,它通過mmap和shmget系統調用在內存中創建了一塊連續的線性地址空間,而通過munmap或者shmdt系統調用可以釋放這塊內存。使用共享內存的優點是當多個進程使用同一塊共享內存時,在不論什么一個進程改動了共享內存中的內容后,其它進程通過訪問這段共享內存都可以得到改動后的內容。Nginx定義了ngx_shm_t結構體。用於描寫敘述一塊共享內存,
typedef struct{
//指向共享內存的事實上地址
u_char* addr;
//共享內存的長度
size_t size;
//這塊共享內存的名稱
ngx_str_t name;
//記錄日志的ngx_log_t對象
ngx_lot_t* log;
//表示共享內存是否已經分配過的標志位。為1時表示已經存在
ngx_uint_t exists;
} ngx_shm_t;
操作ngx_shm_t結構體的方法有兩個:ngx_shm_alloc(基於mmap實現)用於分配新的共享內存。而ngx_shm_free(基於munmap實現)用於釋放已經存在的共享內存。
Nginx各進程間共享數據的主要方式就是使用共享內存。通常是由master進程創建。在master進程fork出子進程后,全部的進程開始使用這塊內存中的數據。
Nginx頻道
ngx_channel_t頻道是Nginx master進程與worker進程之間通信的經常使用工具。它是使用本機套接字實現的。socketpair方法,用於創建父子進程間使用的套接字。
int socketpair ( int d, int type, int protocol, int sv[2] );
通常在父子進程之間通信前。會先調用socketpair創建一組套接字,在調用fork方法創建出子進程后。將會在父進程中關閉sv[1]套接字,子進程關閉sv[0]套接字。
ngx_channel_t頻道結構體是Nginx定義的master父進程和worker子進程間通信的消息格式。
例如以下所看到的:
typedef struct{
//傳遞的TCP消息中的命令
ngx_uint_t command;
//進程ID。通常是發送命令方的進程ID
ngx_pid_t pid;
//表示發送命令方在ngx_processes進程數組間的序號
ngx_int_t slot;
//通信的套接字句柄
ngx_fd_t fd;
} ngx_channel_t;
這個消息的格式之所以如此簡單。是由於Nginx僅用這個頻道同步master進程與work進程間的狀態。這針對command成員已經定義的命令就能夠快拿出來,例如以下所看到的:
//打開頻道,使用頻道這樣的方式通信前必須發送的命令
#define NGX_CMD_OPEN_CHANNEL 1
//關閉已經打開的頻道,實際上也就是關閉套接字
#define NGX_CMD_CLOSE_CHANNEL 2
//要求接收方正常地退出進程
#define NGX_CMD_QUIT 3
//要求接收方強制結束進程
#define NGX_CMD_TERMINATE 4
//要求接收方又一次打開進程已經打開過的文件
#define NGX_CMD_REOPEN 5
master進程正是通過socketpair產生的套接字發送命令的,即每次要派生一個進程之前都會調用socketpair方法。在Nginx派生子進程的ngx_spawn_proces方法中,會首先派生基於TCP的套接字。
Nginx封裝了4個方法: ngx_write_channel,ngx_write_channel, ngx_write_channel和ngx_close_channel。
用於發送消息的ngx_write_channel方法。
ngx_int_t ngx_write_channel(ngx_socket_t s, ngx_channel_t* ch, size_t size, ngx_log_t*log);
這里的s參數是要使用的TCP套接字。ch參數是ngx_channel_t類型的消息,size參數是ngx_channel_t結構體的大小,log參數是日志對象。
讀取消息的方法ngx_read_channel
ngx_int_t ngx_read_channel(ngx_socket_t s, ngx_channel_t* ch, size_t size, ngx_log_t* log);
worker進程使用ngx_add_channel_event方法把接受頻道消息的套接字加入到epoll中,當接收到父進程消息時子進程會通過epoll的事件回調對應的handler方法來處理這個頻道消息。
ngx_int_t ngx_add_channel_event(ngx_cycle_t* cycle, ngx_fd_t fd, ngx_int_t event,ngx_event_handler_pt handler);
cycle參數是每一個nginx進程必須具備的ngx_cycle_t核心結構體;fd參數是上面說過的須要接受消息的套接字。
event參數是須要檢測的事件類型。這里必定是EPOLLIN;handler參數指向的方法就是用於讀取消息的方法。
void ngx_close_channel(ngx_fd_t* fd, ngx_lot_t* log);
參數fd就是上面說過的套接字數組。
信號
Nginx定義了一個ngx_signal_t結構體用於描寫敘述接收到信號的行為:typedef struct{
//須要處理的信號
int signo;
//信號相應的字符串名稱
char* siname;
//這個信號相應着的Nginx命令
char* name;
//收到signo信號后就會回調handler方法
void (*handler)(int signo);
} ngx_signal_t;
還定義了一個數組signals用來定義進程將會處理的全部信號,比如:
ngx_signal_t signals[] = {
{
ngx_signal_value(NGX_RECOFIGURE_SIGNAL),
“SIG” ngx_value(NGX_RECONFIGURE_SIGNAL),
“reload”,
ngx_signal_handler
},
…
}
在定義了signals數組后。ngx_init_signals方法會初始化signals數組中全部的信號,ngx_init_signals事實上是調用了sigaction方法注冊信號的回調方法。
ngx_int_t ngx_init_signals(nx_log_t* log)
{
ngx_signal_t* sig;
struct signaction sa;
//遍歷signals數組。處理每個ngx_signal_t類型的結構體
for(sig = signals; sig->signo != 0; sig++){
ngx_memzero(&sa, sizeof(struct, sigaction));
//設置信號的處理方法為handler方法
sa.sa_handler = sig->handler;
//將sa中的為所有設置為0
sigemptyset(&sa.sa_mask);
//注冊信號的回調方法
if(sigaction(sig->signo, &sa, NULL) == -1){
ngx_log_error(NGX_LOG_EMERG, log, ngx_errno,
“sigaction(%s) failed”, sig->signame);
return NGX_ERROR;
}
}
return NGX_OK;
}
這樣進程就能夠處理信號了。
對信號設置並生是在fork()函數調用之前進行的,所以工作金曾等都能受此作用。當然,普通情況下,我們不會向工作進程等子進程發送控制信息。而主要想監控進程父進程發送,父進程收到信號做對應處理后,在依據情況看是否把信號再通知到其它全部子進程。
進程同步
進程同步主要使用了原子操作,信號量和文件鎖實現。當中基於原子操作能夠實現自旋鎖。基於原子操作、信號量以及文件鎖,Nginx在更高層次上封裝了一個相互排斥鎖,,是用來方便。原子操作
可以運行原子操作的原子變量僅僅有整型,包含無符號整型ngx_atomic_uint_t和有符號整型ngx_atomic_t,這兩種類型都使用了volatilekeyword告訴C編譯器不要做優化。
Nginx提供兩個方法來使用原子操作來改動、獲取整型變量:
ngx_atomic_cmp_set和ngx_atomic_fetch_add。
這兩個方法都能夠用來改動原子變量的值,而ngx_atomic_cmp_set方法同一時候還能夠比較原子變量的值。
static ngx_inline ngx_atomic_uint ngx_atomic_cmp_set(ngx_atomic_t* lock, ngx_atomic_uint_t olc, ngx_atomic_uint_t set)
ngx_atomic_cmp_set方法會將old參數與原子變量lock的值做比較,假設他們相等,則將lock設為參數set,同一時候方法返回1;假設它們不相等,則不作不論什么改動,返回0。
static ngx_inline ngx_atomic_int_t ngx_atomic_fetch_add(ngx_atomic_t* value,ngx_atomic_int_t add)
ngx_atomic_fetch_add方法會把原子變量value的值加上參數add,同一時候翻譯value的值。
自旋鎖
基於原子操作,Nginx實現了一個自旋鎖。自旋鎖是一種非睡眠鎖,也就是說,某進程假設試圖獲取自旋鎖。當發現鎖已經被其它進程獲取時,那么不會使得當前進程進入睡眠狀態,而是始終保持在可運行狀態,每當內核調度到這個進程運行時就持續檢查能否夠獲取鎖。在拿不到鎖時。這個進程的代碼將會一直在自旋鎖代碼出運行。知道其它進程釋放了鎖且當前進程獲取到了鎖后,代碼才會繼續向下運行。
可見自旋鎖主要是為了多處理器操作系統而設置的,它要解決的共享資源保護場景就是進程使用鎖的時間很短。大部分Nginx的worker進程最好都不要進入睡眠狀態,由於它很繁忙,在這個進程的epoll上可能會有十萬甚至百萬的TCP連接等等待着處理,進程一旦睡眠后必須等待其它時間的喚醒,這中間及其頻繁的進程切換帶來的負載消耗可能無法讓用戶接受。
以下介紹基於原子操作的自旋鎖方法ngx_spinlock是怎樣實現的。
它有3個參數。當中lock參數就是原子變量表達的鎖。當lock值為0時,表示鎖是被釋放的,而lock值不為0時則表示鎖已經被某個進程持有了;value參數表示希望當鎖沒有被不論什么進程持有時。把lock值設為value表示當前進程持有了鎖;第三個參數spin表示在多處理器系統內,當ngx_spinlock方法沒有拿到鎖時。當前進程在內核的一次調度中,該方法等待其它處理器釋放鎖的時間。
以下看一下它的源代碼:
/*
* Copyright (C) Igor Sysoev
* Copyright (C) Nginx, Inc.
*/
#include <ngx_config.h>
#include <ngx_core.h>
//函數:基於原子操作的自旋鎖方法ngx_spinlock的實現
//參數解釋:
//lock:原子變量表達的鎖
//value:標志位。鎖是否被某一進程占用
//spin:在多處理器系統內,當ngx_spinlock方法沒有拿到鎖時,當前進程在內核的一次調度中該方法等待其它處理器釋放鎖的時間
void
ngx_spinlock(ngx_atomic_t *lock, ngx_atomic_int_t value, ngx_uint_t spin)
{
#if (NGX_HAVE_ATOMIC_OPS)//支持原子操作
ngx_uint_t i, n;
//一直處於循環中,直到獲取到鎖
for ( ;; ) {
//lock為0表示沒有其它進程持有鎖,這時將lock值設置為value參數表示當前進程持有了鎖
if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {
return;
}
//假設是多處理器系統
if (ngx_ncpu > 1) {
/*
在多處理器下。當發現鎖被其它進程占用時,當前進程並非立馬讓出正在使用的CPU處理器,而是等待一段時間,看看其它處理器上的進程是否會釋放鎖,這會降低進程間切換的次數。
*/
for (n = 1; n < spin; n <<= 1) {
//隨着等待的次數越來越多,實際去檢查鎖的間隔時間越來越大
for (i = 0; i < n; i++) {
/*
ngx_cpu_pause是很多架構體系中專門為了自旋鎖而提供的指令,它會告訴CPU如今處於自旋鎖等待狀態,通常一個CPU會將自己置於節能狀態,降低功耗。可是當前進程並沒有讓出正在使用的處理器。
*/
ngx_cpu_pause();//
}
/*
檢查鎖是否被釋放了,假設lock值為0且釋放了鎖后。就把它的值設為value,當前進程持有鎖成功並返回
*/
if (*lock == 0 && ngx_atomic_cmp_set(lock, 0, value)) {
return;
}
}
}
/*
` 當前進程讓出處理器,但仍然處於可運行狀態,使得處理器優先調度其它可運行狀態的進程,這樣。在進程被內核再次調度時,在for循環代碼中能夠期望其它進程釋放鎖。
*/
ngx_sched_yield();
}
#else
#if (NGX_THREADS)
#error ngx_spinlock() or ngx_atomic_cmp_set() are not defined !
#endif
#endif
}
釋放鎖時須要Nginx模塊通過ngx_atomic_cmp_set方法將原子變量設為0。
信號量
Nginx僅把信號量作為簡單的相互排斥鎖來使用,使用信號量作為相互排斥鎖有可能導致進程睡眠。不做具體解釋。文件鎖
文件鎖是一種文件讀寫機制,在不論什么特定的時間僅僅同意一個進程訪問一個文件。利用這樣的機制可以使讀寫單個文件的過程變得更安全。不做具體解釋。
Nginx實現的相互排斥鎖
基於原子操作、信號量以及文件鎖,Nginx在更高層次封裝了一個相互排斥鎖。使用起來非常方便,很多Nginx模塊也僅僅接受使用它。以下介紹的是操作這個相互排斥鎖的5中方法:ngx_shmtx_create 初始化相互排斥鎖
ngx_shmtx_destory 銷毀相互排斥鎖
ngx_shmtx_trylock 無堵塞地試圖獲取相互排斥鎖,返回1表示獲取相互排斥鎖成功。返回0表示獲取相互排斥鎖失敗
ngx_shmtx_lock 以堵塞進程的方式獲取相互排斥鎖。在方法返回時就已經持有了相互排斥鎖了
ngx_shmtx_unlock 釋放相互排斥鎖
獲取相互排斥鎖時既能夠使用不會堵塞進程的ngx_shmtx_trylock方法,也能夠使用ngx_shmtx_lock方法告訴Nginx必須持有相互排斥鎖后才干繼續向下運行代碼。它們都通過操作ngx_shmtx_t類型的結構來實現相互排斥結構,以下來看一下ngx_shmtx_t有哪些成員。
typedef struct{
#if ( NGX_HAVE_ATOMIC_OPS)
//原子變量鎖
ngx_atomic_t* lock;
#if (NGX_HAVE_POSIX_SEM)
//semaphore為1 時表示獲取鎖將可能使用到的信號量
ngx_uint_t semaphonre;
//sem就是信號量鎖
sem_t sem;
#endif;
#else
//使用文件鎖時fd表示使用的文件句柄
ngx_fd_t fd;
//name表示文件名稱
u_char* name;
#endif
/*自旋次數。表示在自旋狀態下等待其它處理器結果中釋放的時間。由文件鎖實現,spin沒有不論什么意義*/
ngx_uint_t spin;
} ngx_shmtx_t;
ngx_shmtx_t結構涉及兩個宏:NGX_HAVE_ATOMIC_OPS、NGX_HVE_POIX_SEM。這兩個宏相應着相互排斥鎖的3種不同實現。
第1種實現:當不支持原子操作時,會使用文件鎖來實現ngx_hmtx_t相互排斥鎖,這時它僅有fd和name成員。
這兩個成員使用上面介紹的文件鎖來提供堵塞、非堵塞的相互排斥鎖。
第2種實現,支持原子操作卻又不支持信號量。
第3種實現,在支持原子操作的同一時候,操作系統也支持信號量。
后兩種實現的唯一差別是ngx_shmtx_lock方法執行時的效果,也就是說,支持信號量僅僅會影響堵塞進程的ngx_shmtx_lock方法持有鎖的方式。當不支持信號量時,ngx_shmtx_lock取鎖與上面介紹的自旋鎖是一致的,而支持信號量后,ngx_shmtx_lock將在spin指定的一段時間內自旋等待其它處理器釋放鎖,假設達到spin上限還沒有獲取到鎖,那么將會使用sem_wait使得當前進程進入睡眠狀態,等其它進程時回訪了鎖內核后,才會喚醒這個進程。當然。在實際過程中。ngx_shmtx_lock方法執行一段時間后,假設其它進程始終不放棄鎖,那么當前進程將有可能強制性地獲取到這把鎖。這也是出於Nginx不宜使用堵塞進程的睡眠鎖方面的考慮。
