Nginx源碼分析:3張圖看懂啓動及進程工作原理

圖一:nginx 啟動及內存申請過程分析

任何程序都離不開啟動和配置解析。ngx 的代碼離不開 ngx_cycle_s 和 ngx_pool_s 這兩個核心數據結構,所以我們在啟動之前先來分析下。

內存申請過程分為 3 步

  1. 假如申請的內存小於當前塊剩餘的空間,則直接在當前塊中分配。
  2. 假如當前塊空間不足,則調用 ngx_palloc_block 分配一個新塊然後把新塊鏈接到 d.next 中,然後分配數據。
  3. 假如申請的大小大於當前塊的最大值,則直接調用 ngx_palloc_large 分配一個大塊,並且鏈接到 pool→large 鏈表中

內存分配過程圖解如下

Nginx源碼分析:3張圖看懂啟動及進程工作原理

Nginx源碼分析:3張圖看懂啟動及進程工作原理

Nginx源碼分析:3張圖看懂啟動及進程工作原理

為了更好理解上面的圖,可以參看文末附 2 的幾個數據結構:ngx_pool_s 及 ngx_cycle_s。

知道了這兩個核心數據結構之後,我們正式進入 main 函數,main 函數執行過程如下

Nginx源碼分析:3張圖看懂啟動及進程工作原理

  • 調用 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,初始化過程:

  1. 更新 timezone 和 time
  2. 創建內存池
  3. 給 cycle 指針分配內存
  4. 保存安裝路徑,配置文件,啟動參數等
  5. 初始化打開文件句柄
  6. 初始化共享內存
  7. 初始化連接隊列
  8. 保存 hostname
  9. 調用各 NGX_CORE_MODULE 的 create_conf 方法
  10. 解析配置文件
  11. 調用各NGX_CORE_MODULE的init_conf方法
  12. 打開新的文件句柄
  13. 創建共享內存
  14. 處理監聽socket
  15. 創建socket進行監聽
  16. 調用各模塊的init_module

圖二:master 進程工作原理及工作工程

以下過程都在ngx_master_process_cycle 函數中進行,啟動過程:

  1. 暫時阻塞所有 ngx 需要處理的信號
  2. 設置進程名稱
  3. 啟動工作進程
  4. 啟動cache管理進程
  5. 進入循環開始處理相關信號

master 進程工作過程

Nginx源碼分析:3張圖看懂啟動及進程工作原理

  1. 設置 work 進程退出等待時間
  2. 掛起,等待新的信號來臨
  3. 更新時間
  4. 如果有 worker 進程因為 SIGCHLD 信號退出了,則重啟 worker 進程
  5. master 進程退出。如果所有 worker 進程都退出了,並且收到 SIGTERM 信號或 SIGINT 信號或 SIGQUIT 信號等,master 進程開始處理退出
  6. 處理SIGTERM信號
  7. 處理SIGQUIT信號,並且關閉socket
  8. 處理SIGHUP信號
  9. 平滑升級,重啟worker進程
  10. 不是平滑升級,需要重新讀取配置
  11. 處理重啟 10處理SIGUSR1信號 重新打開所有文件 11處理SIGUSR2信號 熱代碼替換,執行新的程序 12處理SIGWINCH信號,不再處理任何請求

圖三:worker 進程工作原理

Nginx源碼分析:3張圖看懂啟動及進程工作原理

啟動通過執行 ngx_start_worker_processes 函數:

  1. 先在 ngx_processes 數組中找坑位if (ngx_processes[s].pid == -1) {break;}
  2. 進程相關結構初始化工作
  3. 創建管道 ( socketpair )
  4. 設置管道為非阻塞模式
  5. 設置管道為異步模式
  6. 設置異步 I/O 的所有者
  7. 如果 exec 執行的時候本 fd 不傳遞給 exec 創建的進程
  8. fork 創建子進程。創建成功後,子進程執行相關邏輯:proc(cycle, data)。
  9. 設置 ngx_processes[s] 相關屬性
  10. 通知子進程新進程創建完畢 ngx_pass_open_channel(cycle, &ch);

接下來是 ngx_worker_process_cycle worker 進程邏輯

  1. ngx_worker_process_init
  2. 初始化環境變量
  3. 設置進程優先級
  4. 設置文件句柄數量限制
  5. 設置 core_file 文件
  6. 用戶組設置
  7. cpu 親和度設置
  8. 設定工作目錄
  9. 設置隨機種子數
  10. 初始化監聽狀態
  11. 調用各模塊的init_process方法進行初始化
  12. 關閉別人的fd[1],保留別人的fd[1]用於互相通信。自己的fd[1]接收master進程的消息。
  13. 監聽channel讀事件
  14. 進程模式
  15. 處理管道信號。這個過程由 ngx_channel_handler 完成,這部分具體實現在管道事件中講解。
  16. 線程模式
  17. ngx_worker_thread_cycle 是一個線程的循環:死循環中除了處理退出信號。主要進行ngx_event_thread_process_posted工作,這塊具體內容在後面講事件模型的時候再展開。
  18. 處理相關信號

master 和 worker 通信原理為:

Nginx源碼分析:3張圖看懂啟動及進程工作原理

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,該函數工作內容如下:

  1. 創建模塊 context 結構
  2. 調用所有 NGX_EVENT_MODULE 模塊的 create_conf
  3. 解析 event 配置
  4. 調用所有 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 實現了初始化過程,該過程分以下幾個步驟:

  1. 連接數校驗
  2. 初始化互斥鎖

事件進程初始化

在工作線程初始化的時候,將會調用 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 該過程分以下幾步:

  1. 設置 ngx_accept_mutex_held
  2. 初始化定時器
  3. 初始化真正的事件引擎(linux 中為 epoll)
  4. 初始化連接池
  5. 添加 accept 事件

ngx_process_events_and_timers 事件處理開始工作

工作流程如下:

  1. ngx_trylock_accept_mutex 當獲取到標誌位後才進行 accept 事件註冊。
  2. ngx_process_events 處理事件
  3. 釋放 accept_mutex 鎖
  4. 處理定時器事件
  5. ngx_event_process_posted 處理 posted 隊列的事件

ngx 定時器實現

ngx 的定時器利用了紅黑樹的實現

ngx 驚群處理

accept_mutex 解決了驚群問題,雖然linux的新內核已經解決了這個問題,但是ngx 是為了兼容。

整體原理圖:

Nginx源碼分析:3張圖看懂啟動及進程工作原理

Nginx 配置解析

再補充一下配置解析,Nginx 配置解析最大的亮點是用一個三級指針和 ctx 關聯了起來,然後每個模塊關注各自的配置專注解析和初始化就行了。

配置文件解析

ngx 在 main 函數執行的時候會調用 ngx_init_cycle,在這個過程中,會進行初始化的幾個步驟:

  • create_conf 針對 core_module 類型的模塊,將會調用 create_conf 方法:
Nginx源碼分析:3張圖看懂啟動及進程工作原理

並且把根據模塊號存入了 cycle→conf_ctx 中。這個過程主要是進行配置數據結構的初始化。以epoll模塊為例:

Nginx源碼分析:3張圖看懂啟動及進程工作原理

  • 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 的下面部分邏輯:

Nginx源碼分析:3張圖看懂啟動及進程工作原理

  • init_conf階段
Nginx源碼分析:3張圖看懂啟動及進程工作原理

core 模塊將會按照配置項的值在這個階段進行初始化。ngx 的配置架構如下:

整體架構

Nginx源碼分析:3張圖看懂啟動及進程工作原理

serv_conf 結構

Nginx源碼分析:3張圖看懂啟動及進程工作原理

loc_conf 結構

Nginx源碼分析:3張圖看懂啟動及進程工作原理

附1:Nginx 主要數據結構

Nginx源碼分析:3張圖看懂啟動及進程工作原理

Nginx源碼分析:3張圖看懂啟動及進程工作原理

我們可以參考 ngx_connection_s 結構體,在 ngx_connection_s 中保存了鏈表的指針:ngx_queue_t queue

Nginx源碼分析:3張圖看懂啟動及進程工作原理

Nginx源碼分析:3張圖看懂啟動及進程工作原理

Nginx源碼分析:3張圖看懂啟動及進程工作原理

6 . ngx_hash_t

ngx 的 hash 表沒有鏈表,如果找不到則往右繼續查找空閒的 bucket。總的初始化 ngx_hash_init 流程即為:

  1. 預估需要的桶數量
  2. 搜索需要的桶數量
  3. 分配桶內存
  4. 初始化每一個 ngx_hash_elt_t

ngx 對內存非常扣,假設了 hash 表不會佔用太多的數據和空間,所以採用了這樣的方式。

Nginx源碼分析:3張圖看懂啟動及進程工作原理

附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二級緩存取速度和效率。


分享到:


相關文章: