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

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

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

 

內存申請過程分爲 3 步nginx

 

  1. 假如申請的內存小於當前塊剩餘的空間,則直接在當前塊中分配。程序員

  2. 假如當前塊空間不足,則調用 ngx_palloc_block 分配一個新塊而後把新塊連接到 d.next 中,而後分配數據。數組

  3. 假如申請的大小大於當前塊的最大值,則直接調用 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,初始化過程:

 

  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 進程工做過程

 

 

  1. 設置 work 進程退出等待時間

  2. 掛起,等待新的信號來臨

  3. 更新時間

  4. 若是有 worker 進程由於 SIGCHLD 信號退出了,則重啓 worker 進程

  5. master 進程退出。若是全部 worker 進程都退出了,而且收到 SIGTERM 信號或 SIGINT 信號或 SIGQUIT 信號等,master 進程開始處理退出

  6. 處理SIGTERM信號

  7. 處理SIGQUIT信號,而且關閉socket

  8. 處理SIGHUP信號

    1. 平滑升級,重啓worker進程

    2. 不是平滑升級,須要從新讀取配置

  9. 處理重啓 10處理SIGUSR1信號 從新打開全部文件 11處理SIGUSR2信號 熱代碼替換,執行新的程序 12處理SIGWINCH信號,再也不處理任何請求

     

     

圖三:worker 進程工做原理

 

 

啓動經過執行 ngx_start_worker_processes 函數:

 

 

  1. 先在 ngx_processes 數組中找坑位if (ngx_processes[s].pid == -1) {break;}

  2. 進程相關結構初始化工做

    1. 建立管道 ( socketpair )

    2. 設置管道爲非阻塞模式

    3. 設置管道爲異步模式

    4. 設置異步 I/O 的全部者

    5. 若是 exec 執行的時候本 fd 不傳遞給 exec 建立的進程

  3. fork 建立子進程。建立成功後,子進程執行相關邏輯:proc(cycle, data)。

  4. 設置 ngx_processes[s] 相關屬性

  5. 通知子進程新進程建立完畢 ngx_pass_open_channel(cycle, &ch);

 

接下來是 ngx_worker_process_cycle worker 進程邏輯

  1. ngx_worker_process_init

    1. 初始化環境變量

    2. 設置進程優先級

    3. 設置文件句柄數量限制

    4. 設置 core_file 文件

    5. 用戶組設置

    6. cpu 親和度設置

    7. 設定工做目錄

    8. 設置隨機種子數

    9. 初始化監聽狀態

    10. 調用各模塊的init_process方法進行初始化

    11. 關閉別人的fd[1],保留別人的fd[1]用於互相通訊。本身的fd[1]接收master進程的消息。

    12. 監聽channel讀事件

       

  2. 進程模式

    1. 處理管道信號。這個過程由 ngx_channel_handler 完成,這部分具體實如今管道事件中講解。

  3. 線程模式

    1. ngx_worker_thread_cycle 是一個線程的循環:死循環中除了處理退出信號。主要進行ngx_event_thread_process_posted工做,這塊具體內容在後面講事件模型的時候再展開。

       

  4. 處理相關信號

 

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 */ 

        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 數組能夠知道,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 */ 

        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 實現了初始化過程,該過程分如下幾個步驟:

  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 配置解析

再補充一下配置解析,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, NULL) ; 在初始化完 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 流程即爲:

  1. 預估須要的桶數量

  2. 搜索須要的桶數量

  3. 分配桶內存

  4. 初始化每個 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 == 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 的時候會調用 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二級緩存取速度和效率。

 

 

因爲公衆號文章篇幅關係,以上就是陳科分享的 nginx 源碼分析前半部分,關注本公衆號可收到後半部份內容。

相關文章
相關標籤/搜索