圖一:nginx 啟動及內存申請過程分析
任何程序都離不開啟動和配置解析。ngx 的代碼離不開 ngx_cycle_s 和 ngx_pool_s 這兩個核心數據結構,所以我們在啟動之前先來分析下。
內存申請過程分為 3 步
- 假如申請的內存小於當前塊剩餘的空間,則直接在當前塊中分配。
- 假如當前塊空間不足,則調用 ngx_palloc_block 分配一個新塊然後把新塊鏈接到 d.next 中,然後分配數據。
- 假如申請的大小大於當前塊的最大值,則直接調用 ngx_palloc_large 分配一個大塊,並且鏈接到 pool→large 鏈表中
內存分配過程圖解如下
為了更好理解上面的圖,可以參看文末附 2 的幾個數據結構:ngx_pool_s 及 ngx_cycle_s。
知道了這兩個核心數據結構之後,我們正式進入 main 函數,main 函數執行過程如下
- 調用 ngx_get_options 解析命令參數;
- 調用 ngx_time_init 初始化並更新時間,如全局變量ngx_cached_time;
- 調用 ngx_log_init 初始化日誌,如初始化全局變量 ngx_prefix,打開日誌文件 ngx_log_file.fd;
- 清零全局變量 ngx_cycle,併為 ngx_cycle.pool 創建大小為 1024B 的內存池;
- 調用 ngx_save_argv 保存命令行參數至全局變量 ngx_os_argv、ngx_argc、ngx_argv 中;
- 調用 ngx_process_options 初始化 ngx_cycle 的 prefix, conf_prefix, conf_file, conf_param 等字段;
- 調用 ngx_os_init 初始化系統相關變量,如內存頁面大小 ngx_pagesize , ngx_cacheline_size , 最大連接數 ngx_max_sockets 等;
- 調用 ngx_crc32_table_init 初始化 CRC 表 ( 後續的 CRC 校驗通過查表進行,效率高 );
- 調用 ngx_add_inherited_sockets 繼承 sockets:
- 解析環境變量 NGINX_VAR = "NGINX" 中的 sockets,並保存至 ngx_cycle.listening 數組;
- 設置 ngx_inherited = 1;
- 調用 ngx_set_inherited_sockets 逐一對 ngx_cycle.listening 數組中的 sockets 進行設置;
- 初始化每個 module 的 index,並計算 ngx_max_module;
- 調用 ngx_init_cycle 進行初始化;
- 該初始化主要對 ngx_cycle 結構進行;
- 若有信號,則進入 ngx_signal_process 處理;
- 調用 ngx_init_signals 初始化信號;主要完成信號處理程序的註冊;
- 若無繼承 sockets,且設置了守護進程標識,則調用 ngx_daemon 創建守護進程;
- 調用 ngx_create_pidfile 創建進程記錄文件;( 非 NGX_PROCESS_MASTER = 1 進程,不創建該文件 )
- 進入進程主循環;
- 若為 NGX_PROCESS_SINGLE=1模式,則調用 ngx_single_process_cycle 進入進程循環;
- 否則為 master-worker 模式,調用 ngx_master_process_cycle 進入進程循環;
在 main 函數執行過程中,有一個非常重要的函數 ngx_init_cycle,這個階段做了什麼呢?下面分析 ngx_init_cycle,初始化過程:
- 更新 timezone 和 time
- 創建內存池
- 給 cycle 指針分配內存
- 保存安裝路徑,配置文件,啟動參數等
- 初始化打開文件句柄
- 初始化共享內存
- 初始化連接隊列
- 保存 hostname
- 調用各 NGX_CORE_MODULE 的 create_conf 方法
- 解析配置文件
- 調用各NGX_CORE_MODULE的init_conf方法
- 打開新的文件句柄
- 創建共享內存
- 處理監聽socket
- 創建socket進行監聽
- 調用各模塊的init_module
圖二:master 進程工作原理及工作工程
以下過程都在ngx_master_process_cycle 函數中進行,啟動過程:
- 暫時阻塞所有 ngx 需要處理的信號
- 設置進程名稱
- 啟動工作進程
- 啟動cache管理進程
- 進入循環開始處理相關信號
master 進程工作過程
- 設置 work 進程退出等待時間
- 掛起,等待新的信號來臨
- 更新時間
- 如果有 worker 進程因為 SIGCHLD 信號退出了,則重啟 worker 進程
- master 進程退出。如果所有 worker 進程都退出了,並且收到 SIGTERM 信號或 SIGINT 信號或 SIGQUIT 信號等,master 進程開始處理退出
- 處理SIGTERM信號
- 處理SIGQUIT信號,並且關閉socket
- 處理SIGHUP信號
- 平滑升級,重啟worker進程
- 不是平滑升級,需要重新讀取配置
- 處理重啟 10處理SIGUSR1信號 重新打開所有文件 11處理SIGUSR2信號 熱代碼替換,執行新的程序 12處理SIGWINCH信號,不再處理任何請求
圖三:worker 進程工作原理
啟動通過執行 ngx_start_worker_processes 函數:
- 先在 ngx_processes 數組中找坑位if (ngx_processes[s].pid == -1) {break;}
- 進程相關結構初始化工作
- 創建管道 ( socketpair )
- 設置管道為非阻塞模式
- 設置管道為異步模式
- 設置異步 I/O 的所有者
- 如果 exec 執行的時候本 fd 不傳遞給 exec 創建的進程
- fork 創建子進程。創建成功後,子進程執行相關邏輯:proc(cycle, data)。
- 設置 ngx_processes[s] 相關屬性
- 通知子進程新進程創建完畢 ngx_pass_open_channel(cycle, &ch);
接下來是 ngx_worker_process_cycle worker 進程邏輯
- ngx_worker_process_init
- 初始化環境變量
- 設置進程優先級
- 設置文件句柄數量限制
- 設置 core_file 文件
- 用戶組設置
- cpu 親和度設置
- 設定工作目錄
- 設置隨機種子數
- 初始化監聽狀態
- 調用各模塊的init_process方法進行初始化
- 關閉別人的fd[1],保留別人的fd[1]用於互相通信。自己的fd[1]接收master進程的消息。
- 監聽channel讀事件
- 進程模式
- 處理管道信號。這個過程由 ngx_channel_handler 完成,這部分具體實現在管道事件中講解。
- 線程模式
- ngx_worker_thread_cycle 是一個線程的循環:死循環中除了處理退出信號。主要進行ngx_event_thread_process_posted工作,這塊具體內容在後面講事件模型的時候再展開。
- 處理相關信號
master 和 worker 通信原理為:
Nginx 事件機制介紹
先看幾個主要方法
- ngx_add_channel_event 主要是把事件註冊到事件池中,並且添加事件 handler,具體要結合後面的事件機制來展開。
- ngx_write_channel 主要是將數據寫入到 pipe 中:
n = sendmsg(s, &msg, 0);
Top of Form
Bottom of Form
- ngx_read_channel 從 pipe 中讀取數據:n = recvmsg(s, &msg, 0);
接下來分析事件模塊工作流程
ngx_event模塊結構
ngx_events_module 的數據結構如下:
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 */
, /* init master */
, /* init module */
, /* init process */
, /* init thread */
, /* exit thread */
, /* exit process */
, /* 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,
},
ngx__command
};
通過 ngx_events_commands 數組可以知道,event 模塊初始化函數為 ngx_events_block,該函數工作內容如下:
- 創建模塊 context 結構
- 調用所有 NGX_EVENT_MODULE 模塊的 create_conf
- 解析 event 配置
- 調用所有 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 */
, /* init master */
ngx_event_module_init, /* init module */
ngx_event_process_init, /* init process */
, /* exit master */ NGX_MODULE_V1_PADDING
};
ngx_event_module_init 實現了初始化過程,該過程分以下幾個步驟:
- 連接數校驗
- 初始化互斥鎖
事件進程初始化
在工作線程初始化的時候,將會調用 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 定時器實現
ngx 的定時器利用了紅黑樹的實現
ngx 驚群處理
accept_mutex 解決了驚群問題,雖然linux的新內核已經解決了這個問題,但是ngx 是為了兼容。
整體原理圖:
Nginx 配置解析
再補充一下配置解析,Nginx 配置解析最大的亮點是用一個三級指針和 ctx 關聯了起來,然後每個模塊關注各自的配置專注解析和初始化就行了。
配置文件解析
ngx 在 main 函數執行的時候會調用 ngx_init_cycle,在這個過程中,會進行初始化的幾個步驟:
- create_conf 針對 core_module 類型的模塊,將會調用 create_conf 方法:
並且把根據模塊號存入了 cycle→conf_ctx 中。這個過程主要是進行配置數據結構的初始化。以epoll模塊為例:
- ngx_conf_parse 解析配置文件
這個函數一共有以下幾個過程:
- 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, ) ; 在初始化完 http 的上下文之後,繼續進行內部的解析邏輯。這樣就會調用到 ngx_conf_handler 的下面部分邏輯:
- init_conf階段
core 模塊將會按照配置項的值在這個階段進行初始化。ngx 的配置架構如下:
整體架構
serv_conf 結構
loc_conf 結構
附1:Nginx 主要數據結構
我們可以參考 ngx_connection_s 結構體,在 ngx_connection_s 中保存了鏈表的指針:ngx_queue_t queue
6 . ngx_hash_t
ngx 的 hash 表沒有鏈表,如果找不到則往右繼續查找空閒的 bucket。總的初始化 ngx_hash_init 流程即為:
- 預估需要的桶數量
- 搜索需要的桶數量
- 分配桶內存
- 初始化每一個 ngx_hash_elt_t
ngx 對內存非常扣,假設了 hash 表不會佔用太多的數據和空間,所以採用了這樣的方式。
附2:內存分配的數據結構
ngx_pool_s是 ngx 的內存池,每個工作線程都會持有一個,我們來看它的結構:
struct ngx_pool_s {
ngx_pool_data_t d ; // 數據塊
size_t max ; // 小塊內存的最大值
ngx_pool_t *current ; // 指向當前內存池
ngx_chain_t *chain ;
ngx_pool_large_t *large; // 分配大塊內存用,即超過max的內存請求
ngx_pool_cleanup_t *cleanup ; // 掛載一些內存池釋放的時候,同時釋放的資源
ngx_log_t *log;
} ;
ngx_pool_data_t 數據結構:
typedef struct {
u_char *last ; // 當前數據塊分配結束位置
u_char *end ; // 數據塊結束位置
ngx_pool_t *next ; // 鏈接到下一個內存池
ngx_uint_t failed ; // 統計該內存池不能滿足分配請求的次數
} ngx_pool_data_t ;
然後我們結合 ngx_palloc 方法來看一下內存池的分配原理:
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 ; // 配置上下文數組(含所有模塊)
ngx_pool_t *pool ; // 內存池
ngx_log_t *log ; // 日誌
ngx_log_t new_log ;
ngx_connection_t **files ; // 連接文件
ngx_connection_t *free_connections ; // 空閒連接
ngx_uint_t free_connection_n ; // 空閒連接個數
ngx_queue_t reusable_connections_queue ; // 再利用連接隊列
ngx_array_t listening ; // 監聽數組
ngx_array_t pathes ; // 路徑數組
ngx_list_t open_files ; // 打開文件鏈表
ngx_list_t shared_memory ; // 共享內存鏈表
ngx_uint_t connection_n ; // 連接個數
ngx_uint_t iles_n ; // 打開文件個數
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; //配置參數
ngx_str_t conf_prefix; //配置前綴
ngx_str_t prefix; //前綴
ngx_str_t lock_file; //鎖文件
ngx_str_t hostname; //主機名
};
附3:Nginx 內存管理 & 內存對齊
內存的申請最終調用的是 malloc 函數,ngx_calloc 則在調用 ngx_alloc 後,使用 memset 來填 0。假如自己開發NGX模塊,不要直接使用 ngx_malloc/ngx_calloc,可以使用 ngx_palloc 否則還需要自己管理內存的釋放。在 ngx_http_create_request 的時候會創建 request 級別的 pool:
pool = ngx_create_pool(cscf->request_pool_size, c->log) ;
if (pool == ) {
return ;
}
r = ngx_pcalloc(pool, sizeof(ngx_http_request_t));
if (r == ) {
ngx_destroy_pool(pool) ;
r->pool = pool ;
在 ngx_http_free_request 釋放 request 的時候會調用 ngx_destroy_pool ( pool ) 釋放連接。內存對齊,首先在創建 pool 的時候對齊:p = ngx_memalign(NGX_POOL_ALIGNMENT, size, log) 。ngx_memalign(返回基於一個指定 alignment 的大小為 size 的內存空間,且其地址為 alignment 的整數倍,alignment 為 2 的冪。)最終通過:posix_memalign 或 memalign 來申請。
數 據的對齊 ( alignment ) 是指數據的地址和由硬件條件決定的內存塊大小之間的關係。一個變量的地址是它大小的倍數的時候,這就叫做自然對齊 ( naturally aligned )。
例如,對於一個 32bit 的變量,如果它的地址是 4 的倍數,-- 就是說,如果地址的低兩位是 0,那麼這就是自然對齊了。
所以,如果一個類型的大小是 2n 個字節,那麼它的地址中,至少低 n 位是 0。對齊的規則是由硬件引起 的。一些體系的計算機在數據對齊這方面有著很嚴格的要求。在一些系統上,一個不對齊的數據的載入可能會引起進程的陷入。在另外一些系統,對不對齊的數據的 訪問是安全的,但卻會引起性能的下降。在編寫可移植的代碼的時候,對齊的問題是必須避免的,所有的類型都該自然對齊。
預對齊內存的分配在大多數情況下,編譯器和 C 庫透明地幫你處理對齊問題。POSIX 標明瞭通過 malloc, calloc, 和 realloc 返回的地址對於任何的C類型來說都是對齊的。在 Linux 中,這些函數返回的地址在 32 位系統是以 8 字節為邊界對齊,在 64 位系統是以 16 字節為邊界對齊 的。有時候,對於更大的邊界,例如頁面,程序員需要動態的對齊。雖然動機是多種多樣的,但最常見的是直接塊 I/O 的緩存的對齊或者其它的軟件對硬件的交 互,因此,POSIX 1003.1d 提供一個叫做 posix_memalign 的函數。
調用 posix_memalign 成功時會返回 size 字節的動態內存,並且這塊內存的地址是 alignment 的倍數。參數 alignment 必須是 2 的冪,還是 void 指針的大小的倍數。返回的內存塊的地址放在了 memptr 裡面,函數返回值是 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,以此類推。
進行內存池管理的時候,對於小於64字節的內存,給分配64字節,使之總是cpu二級緩存讀寫行的大小倍數,從而有利cpu二級緩存取速度和效率。
閱讀更多 Java高級架構技術 的文章