本文系轉載》》》》》》》》》》》》》》》》
編者按:高可用架構分享及傳播在架構領域具有典型意義的文章,本文由陳科在高可用架構群分享。轉載請注明來自高可用架構公眾號「ArchNotes」。
導讀:很多工程師及架構師都希望了解及掌握高性能服務器開發(fā),閱讀優(yōu)秀源代碼是一種有效的方式,nginx 是業(yè)界知名的高性能 Web 服務器實現(xiàn),如何有效的閱讀及理解 nginx?本文用圖解的方式幫助大家來更好的閱讀及理解 nginx 關鍵環(huán)節(jié)的實現(xiàn)。
圖一:nginx 啟動及內(nèi)存申請過程分析
任何程序都離不開啟動和配置解析。ngx 的代碼離不開 ngx_cycle_s 和 ngx_pool_s 這兩個核心數(shù)據(jù)結構,所以我們在啟動之前先來分析下。
內(nèi)存申請過程分為 3 步
假如申請的內(nèi)存小于當前塊剩余的空間,則直接在當前塊中分配。
假如當前塊空間不足,則調(diào)用 ngx_palloc_block 分配一個新塊然后把新塊鏈接到 d.next 中,然后分配數(shù)據(jù)。
假如申請的大小大于當前塊的最大值,則直接調(diào)用 ngx_palloc_large 分配一個大塊,并且鏈接到 pool→large 鏈表中
內(nèi)存分配過程圖解如下
(圖片來自網(wǎng)絡)
為了更好理解上面的圖,可以參看文末附 2 的幾個數(shù)據(jù)結構:ngx_pool_s 及?ngx_cycle_s。
知道了這兩個核心數(shù)據(jù)結構之后,我們正式進入 main 函數(shù),main 函數(shù)執(zhí)行過程如下
調(diào)用 ngx_get_options() 解析命令參數(shù);
調(diào)用 ngx_time_init() 初始化并更新時間,如全局變量ngx_cached_time;
調(diào)用 ngx_log_init() 初始化日志,如初始化全局變量 ngx_prefix,打開日志文件 ngx_log_file.fd;
清零全局變量 ngx_cycle,并為 ngx_cycle.pool 創(chuàng)建大小為 1024B 的內(nèi)存池;
調(diào)用 ngx_save_argv() 保存命令行參數(shù)至全局變量 ngx_os_argv、ngx_argc、ngx_argv 中;
調(diào)用 ngx_process_options() 初始化 ngx_cycle 的 prefix, conf_prefix, conf_file, conf_param 等字段;
調(diào)用 ngx_os_init() 初始化系統(tǒng)相關變量,如內(nèi)存頁面大小 ngx_pagesize , ngx_cacheline_size , 最大連接數(shù) ngx_max_sockets 等;
調(diào)用 ngx_crc32_table_init() 初始化 CRC 表 ( 后續(xù)的 CRC 校驗通過查表進行,效率高 );
調(diào)用 ngx_add_inherited_sockets() 繼承 sockets:
解析環(huán)境變量 NGINX_VAR = "NGINX" 中的 sockets,并保存至 ngx_cycle.listening 數(shù)組;
設置 ngx_inherited = 1;
調(diào)用 ngx_set_inherited_sockets() 逐一對 ngx_cycle.listening 數(shù)組中的 sockets 進行設置;
初始化每個 module 的 index,并計算 ngx_max_module;
調(diào)用 ngx_init_cycle() 進行初始化;
該初始化主要對 ngx_cycle 結構進行;
若有信號,則進入 ngx_signal_process() 處理;
調(diào)用 ngx_init_signals() 初始化信號;主要完成信號處理程序的注冊;
若無繼承 sockets,且設置了守護進程標識,則調(diào)用 ngx_daemon() 創(chuàng)建守護進程;
調(diào)用 ngx_create_pidfile() 創(chuàng)建進程記錄文件;( 非 NGX_PROCESS_MASTER = 1 進程,不創(chuàng)建該文件 )
進入進程主循環(huán);
若為 NGX_PROCESS_SINGLE=1模式,則調(diào)用 ngx_single_process_cycle() 進入進程循環(huán);
否則為 master-worker 模式,調(diào)用 ngx_master_process_cycle() 進入進程循環(huán);
在 main 函數(shù)執(zhí)行過程中,有一個非常重要的函數(shù) ngx_init_cycle,這個階段做了什么呢?下面分析 ngx_init_cycle,初始化過程:
更新 timezone 和 time
創(chuàng)建內(nèi)存池
給 cycle 指針分配內(nèi)存
保存安裝路徑,配置文件,啟動參數(shù)等
初始化打開文件句柄
初始化共享內(nèi)存
初始化連接隊列
保存 hostname
調(diào)用各 NGX_CORE_MODULE 的 create_conf 方法
解析配置文件
調(diào)用各NGX_CORE_MODULE的init_conf方法
打開新的文件句柄
創(chuàng)建共享內(nèi)存
處理監(jiān)聽socket
創(chuàng)建socket進行監(jiān)聽
調(diào)用各模塊的init_module
圖二:master 進程工作原理及工作工程
以下過程都在ngx_master_process_cycle 函數(shù)中進行,啟動過程:
暫時阻塞所有 ngx 需要處理的信號
設置進程名稱
啟動工作進程
啟動cache管理進程
進入循環(huán)開始處理相關信號
master 進程工作過程
設置 work 進程退出等待時間
掛起,等待新的信號來臨
更新時間
如果有 worker 進程因為 SIGCHLD 信號退出了,則重啟 worker 進程
master 進程退出。如果所有 worker 進程都退出了,并且收到 SIGTERM 信號或 SIGINT 信號或 SIGQUIT 信號等,master 進程開始處理退出
處理SIGTERM信號
處理SIGQUIT信號,并且關閉socket
處理SIGHUP信號
平滑升級,重啟worker進程
不是平滑升級,需要重新讀取配置
處理重啟 10處理SIGUSR1信號 重新打開所有文件 11處理SIGUSR2信號 熱代碼替換,執(zhí)行新的程序 12處理SIGWINCH信號,不再處理任何請求
圖三:worker 進程工作原理
啟動通過執(zhí)行 ngx_start_worker_processes 函數(shù):
先在 ngx_processes 數(shù)組中找坑位if (ngx_processes[s].pid == -1) {break;}
進程相關結構初始化工作
創(chuàng)建管道 ( socketpair )
設置管道為非阻塞模式
設置管道為異步模式
設置異步 I/O 的所有者
如果 exec 執(zhí)行的時候本 fd 不傳遞給 exec 創(chuàng)建的進程
fork 創(chuàng)建子進程。創(chuàng)建成功后,子進程執(zhí)行相關邏輯:proc(cycle, data)。
設置 ngx_processes[s] 相關屬性
通知子進程新進程創(chuàng)建完畢?ngx_pass_open_channel(cycle, &ch;);
接下來是 ngx_worker_process_cycle worker 進程邏輯
ngx_worker_process_init
初始化環(huán)境變量
設置進程優(yōu)先級
設置文件句柄數(shù)量限制
設置 core_file 文件
用戶組設置
cpu 親和度設置
設定工作目錄
設置隨機種子數(shù)
初始化監(jiān)聽狀態(tài)
調(diào)用各模塊的init_process方法進行初始化
關閉別人的fd[1],保留別人的fd[1]用于互相通信。自己的fd[1]接收master進程的消息。
監(jiān)聽channel讀事件
進程模式
處理管道信號。這個過程由 ngx_channel_handler 完成,這部分具體實現(xiàn)在管道事件中講解。
線程模式
ngx_worker_thread_cycle 是一個線程的循環(huán):死循環(huán)中除了處理退出信號。主要進行ngx_event_thread_process_posted工作,這塊具體內(nèi)容在后面講事件模型的時候再展開。
處理相關信號
master 和 worker 通信原理為:
Nginx 事件機制介紹
先看幾個主要方法
ngx_add_channel_event?主要是把事件注冊到事件池中,并且添加事件 handler,具體要結合后面的事件機制來展開。
ngx_write_channel?主要是將數(shù)據(jù)寫入到 pipe 中:
n = sendmsg(s, &msg;, 0);
Top of Form
Bottom of Form
ngx_read_channel?從 pipe 中讀取數(shù)據(jù):n = recvmsg(s, &msg;, 0);
接下來分析事件模塊工作流程
ngx_event模塊結構
ngx_events_module 的數(shù)據(jù)結構如下:
ngx_module_t ngx_events_module = {
NGX_MODULE_V1,
&ngx;_events_module_ctx, /* module context */
ngx_events_commands, /* module directives */
NGX_CORE_MODULE, /* module type */
NULL, /* init master */
NULL, /* init module */
NULL, /* init process */
NULL, /* init thread */
NULL, /* exit thread */
NULL, /* exit process */
NULL, /* exit master */
NGX_MODULE_V1_PADDING
};
ngx_event 模塊初始化
static ngx_command_t ngx_events_commands[] = {
{
ngx_string("events") ,
NGX_MAIN_CONF|NGX_CONF_BLOCK|NGX_CONF_NOARGS ,
ngx_events_block, 0, 0, NULL
},
ngx_null_command
};
通過 ngx_events_commands 數(shù)組可以知道,event 模塊初始化函數(shù)為 ngx_events_block,該函數(shù)工作內(nèi)容如下:
創(chuàng)建模塊 context 結構
調(diào)用所有 NGX_EVENT_MODULE 模塊的 create_conf
解析 event 配置
調(diào)用所有 NGX_EVENT_MODULE 模塊的 init_conf
ngx_core_event模塊初始化
ngx_core_event_module 是在 ngx_cycle_init 的時候初始化的:
for (i = 0; ngx_modules[i]; i++) {
if (ngx_modules[i]->init_module){
if (ngx_modules[i]->init_module(cycle) != NGX_OK){ /* fatal */
exit(1);
}
}
}
我們先來看下 ngx_core_event_module 的結構:
ngx_module_t ngx_event_core_module = {
NGX_MODULE_V1,
&ngx;_event_core_module_ctx, /* module context */
ngx_event_core_commands, /* module directives */
NGX_EVENT_MODULE, /* module type */
NULL, /* init master */
ngx_event_module_init, /* init module */
ngx_event_process_init, /* init process */
NULL, /* init thread */
NULL, /* exit thread */
NULL, /* exit process */
NULL, /* exit master */ NGX_MODULE_V1_PADDING
};
ngx_event_module_init 實現(xiàn)了初始化過程,該過程分以下幾個步驟:
連接數(shù)校驗
初始化互斥鎖
事件進程初始化
在工作線程初始化的時候,將會調(diào)用 ngx_event_process_init:
for (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_process_init 該過程分以下幾步:
設置 ngx_accept_mutex_held
初始化定時器
初始化真正的事件引擎(linux 中為 epoll)
初始化連接池
添加 accept 事件
ngx_process_events_and_timers 事件處理開始工作
工作流程如下:
ngx_trylock_accept_mutex 當獲取到標志位后才進行 accept 事件注冊。
ngx_process_events 處理事件
釋放 accept_mutex 鎖
處理定時器事件
ngx_event_process_posted 處理 posted 隊列的事件
ngx 定時器實現(xiàn)
ngx 的定時器利用了紅黑樹的實現(xiàn)
ngx 驚群處理
accept_mutex 解決了驚群問題,雖然linux的新內(nèi)核已經(jīng)解決了這個問題,但是ngx 是為了兼容。
整體原理圖:
Nginx 配置解析
再補充一下配置解析,Nginx 配置解析最大的亮點是用一個三級指針和 ctx 關聯(lián)了起來,然后每個模塊關注各自的配置專注解析和初始化就行了。
配置文件解析
ngx 在 main 函數(shù)執(zhí)行的時候會調(diào)用 ngx_init_cycle,在這個過程中,會進行初始化的幾個步驟:
create_conf?針對 core_module 類型的模塊,將會調(diào)用 create_conf 方法:
并且把根據(jù)模塊號存入了 cycle→conf_ctx 中。這個過程主要是進行配置數(shù)據(jù)結構的初始化。以epoll模塊為例:
ngx_conf_parse 解析配置文件
這個函數(shù)一共有以下幾個過程:
ngx_conf_read_token?這個過程主要進行配置配置的解析工作,解析完成的一個配置結構為:
struct ngx_conf_s {
char????*name;
ngx_array_t?????*args;
ngx_cycle_t????*cycle;
ngx_pool_t????*pool;
ngx_pool_t????*temp_pool;
ngx_conf_file_t ????*conf_file;
ngx_log_t????*log;
void ????*ctx;
ngx_uint_t????module_type;
ngx_uint_t????cmd_type;
ngx_conf_handler_pt ? handler;
char????*handler_conf;
};
ngx_conf_handler 進行配置的處理
cmd→set,以 ngx_http 模塊為例
rv = ngx_conf_parse(cf, NULL) ; 在初始化完 http 的上下文之后,繼續(xù)進行內(nèi)部的解析邏輯。這樣就會調(diào)用到 ngx_conf_handler 的下面部分邏輯:
init_conf階段
core 模塊將會按照配置項的值在這個階段進行初始化。ngx 的配置架構如下:
整體架構
serv_conf 結構
loc_conf 結構
附1:Nginx 主要數(shù)據(jù)結構
我們可以參考 ngx_connection_s 結構體,在 ngx_connection_s 中保存了鏈表的指針:ngx_queue_t queue
6 . ngx_hash_t
ngx 的 hash 表沒有鏈表,如果找不到則往右繼續(xù)查找空閑的 bucket。總的初始化 ngx_hash_init 流程即為:
預估需要的桶數(shù)量
搜索需要的桶數(shù)量
分配桶內(nèi)存
初始化每一個 ngx_hash_elt_t
ngx 對內(nèi)存非???,假設了 hash 表不會占用太多的數(shù)據(jù)和空間,所以采用了這樣的方式。
附2:內(nèi)存分配的數(shù)據(jù)結構
ngx_pool_s是 ngx 的內(nèi)存池,每個工作線程都會持有一個,我們來看它的結構:
struct ngx_pool_s {
ngx_pool_data_t d ; // 數(shù)據(jù)塊
size_t max ; // 小塊內(nèi)存的最大值
ngx_pool_t *current ; // 指向當前內(nèi)存池
ngx_chain_t *chain ;
ngx_pool_large_t *large; // 分配大塊內(nèi)存用,即超過max的內(nèi)存請求
ngx_pool_cleanup_t *cleanup ; // 掛載一些內(nèi)存池釋放的時候,同時釋放的資源
ngx_log_t *log;
} ;
ngx_pool_data_t 數(shù)據(jù)結構:
typedef struct {
u_char *last ; // 當前數(shù)據(jù)塊分配結束位置
u_char *end ; // 數(shù)據(jù)塊結束位置
ngx_pool_t *next ; // 鏈接到下一個內(nèi)存池
ngx_uint_t failed ; // 統(tǒng)計該內(nèi)存池不能滿足分配請求的次數(shù)
} ngx_pool_data_t ;
然后我們結合 ngx_palloc 方法來看一下內(nèi)存池的分配原理:
void * ngx_palloc (ngx_pool_t *pool, size_t size) {
u_char *m; ngx_pool_t *p ;
if (size <= pool->max) {
p = pool->current ;
do {
m = ngx_align_ptr(p->d.last, NGX_ALIGNMENT) ;
if ((size_t) (p->d.end - m) >= size) {
p->d.last = m + size ;
return m ;
}
p = p->d.next ;
} while (p) ;
return ngx_palloc_block(pool, size) ;
}
return ngx_palloc_large(pool, size) ;
}
ngx_cycle_s每個工作進程都會維護一個:
struct ngx_cycle_s {
void ? ?****conf_ctx ; ?// 配置上下文數(shù)組(含所有模塊)
ngx_pool_t ? ?*pool ; ?// 內(nèi)存池
ngx_log_t ? ?*log ; // 日志
ngx_log_t ? ?new_log ;
ngx_connection_t ? ?**files ; // 連接文件
ngx_connection_t ? ?*free_connections ; ?// 空閑連接
ngx_uint_t ? free_connection_n ; // 空閑連接個數(shù)
ngx_queue_t ? ?reusable_connections_queue ; ?// 再利用連接隊列
ngx_array_t ? listening ; // 監(jiān)聽數(shù)組
ngx_array_t ? pathes ; // 路徑數(shù)組
ngx_list_t ? ?open_files ; // 打開文件鏈表
ngx_list_t ? ?shared_memory ; // 共享內(nèi)存鏈表
ngx_uint_t ? ?connection_n ; ?// 連接個數(shù)
ngx_uint_t ? ?iles_n ; // 打開文件個數(shù)
ngx_connection_t ? ?*connections ; // 連接
ngx_event_t ? *read_events ; // 讀事件
ngx_event_t ? *write_events ; // 寫事件
ngx_cycle_t ? ?*old_cycle; ? ? //old cycle指針
ngx_str_t ? ?conf_file; ? ? //配置文件
ngx_str_t ? ?conf_param; ? ?//配置參數(shù)
ngx_str_t ? ?conf_prefix; ? //配置前綴
ngx_str_t ? ?prefix; ? ? ? ?//前綴
ngx_str_t ? ?lock_file; ? ? //鎖文件
ngx_str_t ? ?hostname; ? ? ?//主機名
};
附3:Nginx 內(nèi)存管理 & 內(nèi)存對齊
內(nèi)存的申請最終調(diào)用的是 malloc 函數(shù),ngx_calloc 則在調(diào)用 ngx_alloc 后,使用 memset 來填 0。假如自己開發(fā)NGX模塊,不要直接使用 ngx_malloc/ngx_calloc,可以使用 ngx_palloc 否則還需要自己管理內(nèi)存的釋放。在 ngx_http_create_request 的時候會創(chuàng)建 request 級別的 pool:
pool = ngx_create_pool(cscf->request_pool_size, c->log) ;
if (pool == NULL) {
return NULL;
}
r = ngx_pcalloc(pool, sizeof(ngx_http_request_t));
if (r == NULL) {
ngx_destroy_pool(pool) ;
return NULL ;
}
r->pool = pool ;
在 ngx_http_free_request 釋放 request 的時候會調(diào)用?ngx_destroy_pool ( pool ) 釋放連接。內(nèi)存對齊,首先在創(chuàng)建 pool 的時候對齊:p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log) 。ngx_memalign(返回基于一個指定 alignment 的大小為 size 的內(nèi)存空間,且其地址為 alignment 的整數(shù)倍,alignment 為 2 的冪。)最終通過:posix_memalign 或 memalign 來申請。
數(shù) 據(jù)的對齊 ( alignment ) 是指數(shù)據(jù)的地址和由硬件條件決定的內(nèi)存塊大小之間的關系。一個變量的地址是它大小的倍數(shù)的時候,這就叫做自然對齊 ( naturally aligned )。例如,對于一個 32bit 的變量,如果它的地址是 4 的倍數(shù),-- 就是說,如果地址的低兩位是 0,那么這就是自然對齊了。所以,如果一個類型的大小是 2n 個字節(jié),那么它的地址中,至少低 n 位是 0。對齊的規(guī)則是由硬件引起 的。一些體系的計算機在數(shù)據(jù)對齊這方面有著很嚴格的要求。在一些系統(tǒng)上,一個不對齊的數(shù)據(jù)的載入可能會引起進程的陷入。在另外一些系統(tǒng),對不對齊的數(shù)據(jù)的 訪問是安全的,但卻會引起性能的下降。在編寫可移植的代碼的時候,對齊的問題是必須避免的,所有的類型都該自然對齊。
預對齊內(nèi)存的分配在大多數(shù)情況下,編譯器和 C 庫透明地幫你處理對齊問題。POSIX 標明了通過 malloc( ), calloc( ), 和 realloc( ) 返回的地址對于任何的C類型來說都是對齊的。在 Linux 中,這些函數(shù)返回的地址在 32 位系統(tǒng)是以 8 字節(jié)為邊界對齊,在 64 位系統(tǒng)是以 16 字節(jié)為邊界對齊 的。有時候,對于更大的邊界,例如頁面,程序員需要動態(tài)的對齊。雖然動機是多種多樣的,但最常見的是直接塊 I/O 的緩存的對齊或者其它的軟件對硬件的交 互,因此,POSIX 1003.1d 提供一個叫做 posix_memalign( ) 的函數(shù)。
調(diào)用 posix_memalign( ) 成功時會返回 size 字節(jié)的動態(tài)內(nèi)存,并且這塊內(nèi)存的地址是 alignment 的倍數(shù)。參數(shù) alignment 必須是 2 的冪,還是 void 指針的大小的倍數(shù)。返回的內(nèi)存塊的地址放在了 memptr 里面,函數(shù)返回值是 0.
指針對齊:#define ngx_align_ptr(p, a) (u_char *) (((uintptr_t) (p) + ((uintptr_t) a - 1)) & ~((uintptr_t) a - 1))
例如:計算宏 ngx_align (1, 64) = 64,只要輸入d < 64,則結果總是 64,如果輸入 d = 65,則結果為 128,以此類推。
進行內(nèi)存池管理的時候,對于小于64字節(jié)的內(nèi)存,給分配64字節(jié),使之總是cpu二級緩存讀寫行的大小倍數(shù),從而有利cpu二級緩存取速度和效率。
由于公眾號文章篇幅關系,以上就是陳科分享的 nginx 源碼分析前半部分,關注本公眾號可收到后半部分內(nèi)容。
本文策劃鄧啟明,編輯王杰,審校 Tim Yang,如需第一時間獲取高可用架構分享文章,請關注以下公眾號。轉載請注明來自高可用架構 「ArchNotes」微信公眾號及包含以下二維碼。