Nginx學習之路(八)Nginx中的事件驅動過程詳解-----以listenfd註冊過程為例
Nginx的高效得益於它的事件驅動機制,整個事件驅動機制基本框架就是linux下的select,poll,epoll這幾個IO多路複用模式,但是nginx絕不單單只是使用它們這麼簡單,今天以epoll模式為例,從nginx最開始的listenfd的監聽的過程來說明nginx是怎麼實現的事件驅動。
首先需要說明的是,整個事件模型(event)是一個模組(module),module在nginx中是一個很重要的概念,這也是nginx牛B的地方之一,模組帶來的好處就是非常非常棒的水平擴充套件能力,在nginx上做二次開發基本也是模組的編寫,下面我們就來分析下event module:
先來認識下ngx_module_t這個結構體
struct ngx_module_s { ngx_uint_t ctx_index; /*分類的模組計數器 nginx模組可以分為四種:core、event、http和mail 每個模組都會各自計數,ctx_index就是每個模組在其所屬類組的計數*/ ngx_uint_t index; /*一個模組計數器,按照每個模組在ngx_modules[]陣列中的宣告順序,從0開始依次給每個模組賦值*/ ngx_uint_t spare0; ngx_uint_t spare1; ngx_uint_t spare2; ngx_uint_t spare3; ngx_uint_t version; //nginx模組版本 void *ctx; /*模組的上下文,不同種類的模組有不同的上下文,因此實現了四種結構體*/ ngx_command_t *commands; /*命令定義地址 模組的指令集 每一個指令在原始碼中對應著一個ngx_command_t結構變數*/ ngx_uint_t type; //模組型別,用於區分core event http和mail ngx_int_t (*init_master)(ngx_log_t *log); //初始化master時執行 ngx_int_t (*init_module)(ngx_cycle_t *cycle); //初始化module時執行 ngx_int_t (*init_process)(ngx_cycle_t *cycle); //初始化process時執行 ngx_int_t (*init_thread)(ngx_cycle_t *cycle); //初始化thread時執行 void (*exit_thread)(ngx_cycle_t *cycle); //退出thread時執行 void (*exit_process)(ngx_cycle_t *cycle); //退出process時執行 void (*exit_master)(ngx_cycle_t *cycle); //退出master時執行 //空閒的鉤子函式 uintptr_t spare_hook0; uintptr_t spare_hook1; uintptr_t spare_hook2; uintptr_t spare_hook3; uintptr_t spare_hook4; uintptr_t spare_hook5; uintptr_t spare_hook6; uintptr_t spare_hook7; }; typedef struct ngx_module_s ngx_module_t;
這裡面最重要的就是那幾個回撥函式(init_master到exit_master之間),比如init_master任務,在ngx_module.c中有個全域性變數:
ngx_module_t *ngx_modules[] = {
&ngx_core_module,
&ngx_errlog_module,
&ngx_conf_module,
&ngx_dso_module,
&ngx_conf_extend_module,
&ngx_syslog_module,
...
}
在ngx_worker_process_init中就會從這裡面去呼叫各種module的init_master方法:
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);
}
}
}
那麼如何判定到底採用哪一種IO複用方式呢?
在ngx_event.c中有如下宣告:
extern ngx_module_t ngx_kqueue_module;
extern ngx_module_t ngx_eventport_module;
extern ngx_module_t ngx_devpoll_module;
extern ngx_module_t ngx_epoll_module;
extern ngx_module_t ngx_rtsig_module;
extern ngx_module_t ngx_select_module;
這些宣告以及配置檔案中的相應部分設定用來確定你用哪一個IO複用的模組,那麼事件是如何註冊的呢?
關於事件註冊的最重要的結構體如下:
typedef struct {
ngx_str_t *name;
void *(*create_conf)(ngx_cycle_t *cycle);
char *(*init_conf)(ngx_cycle_t *cycle, void *conf);
ngx_event_actions_t actions;
} ngx_event_module_t;
typedef struct {
ngx_int_t (*add)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
ngx_int_t (*del)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
ngx_int_t (*enable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
ngx_int_t (*disable)(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags);
ngx_int_t (*add_conn)(ngx_connection_t *c);
ngx_int_t (*del_conn)(ngx_connection_t *c, ngx_uint_t flags);
ngx_int_t (*process_changes)(ngx_cycle_t *cycle, ngx_uint_t nowait);
ngx_int_t (*process_events)(ngx_cycle_t *cycle, ngx_msec_t timer,
ngx_uint_t flags);
ngx_int_t (*init)(ngx_cycle_t *cycle, ngx_msec_t timer);
void (*done)(ngx_cycle_t *cycle);
} ngx_event_actions_t;
我們以epoll為例,來看下epoll的ngx_module_t是怎麼實現的:
ngx_event_module_t ngx_epoll_module_ctx = {
&epoll_name,
ngx_epoll_create_conf, /* create configuration */
ngx_epoll_init_conf, /* init configuration */
{
ngx_epoll_add_event, /* add an event */
ngx_epoll_del_event, /* delete an event */
ngx_epoll_add_event, /* enable an event */
ngx_epoll_del_event, /* disable an event */
ngx_epoll_add_connection, /* add an connection */
ngx_epoll_del_connection, /* delete an connection */
NULL, /* process the changes */
ngx_epoll_process_events, /* process the events */
ngx_epoll_init, /* init the events */
ngx_epoll_done, /* done the events */
}
};
那麼,呼叫到這些註冊的方法的過程是怎樣的呢?我們以ngx_epoll_add_event為例,在ngx_worker_process_init的listenfd註冊到epoll的過程如下:
還記得剛剛說到的init_master那個回撥嗎,我們來看看epoll中的ngx_module_t
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_process_init函式,在這個函式中
if (ngx_add_event(rev, NGX_READ_EVENT, 0) == NGX_ERROR) {
return NGX_ERROR;
}
至此就完成了listenfd註冊到epoll的過程,在這裡要說明下這個rev,說到rev就要說明一下nginx中連線(connection)的概念,nginx中註冊到epoll中的epoll_event裡的data部分是指向一個連線的,nginx中是提前分配好了一個連線池,每次需要連線的時候就從連線池裡拿一個出來,連線池的東西我們後面再細講,那麼rev是怎麼跟連線關聯起來的呢,我們來看下rev的結構體:
struct ngx_event_s {
void *data;//關注這個指標,這個指標通常都是指向一個連線
.
.
.
ngx_event_handler_pt handler;//最需要關注的是這個handler,這個handler就是這個event被呼叫是所呼叫的函式
.
.
.
};
這是個簡化的結構體,我只寫出了要關注的部分,再來看下connection的結構體
struct ngx_connection_s {
//連線未使用時候,data域充當連線連結串列中的next指標.
//當連線被使用時候,data域的意義由模組而定.
void *data;
//連線對應的讀事件,這個read指標就指向了剛剛程式碼中的rev
ngx_event_t *read;
//連線對應的寫事件
ngx_event_t *write;
//套接字控制代碼
ngx_socket_t fd;
//直接接收網路位元組流的方法
ngx_recv_pt recv;
//直接放鬆網路位元組流的方法
ngx_send_pt send;
//以ngx_chain連結串列為引數,接收網路位元組流的方法
ngx_recv_chain_pt recv_chain;
//以ngx_chain連結串列為引數,傳送網路位元組流的方法
ngx_send_chain_pt send_chain;
//這個連結對應的listening_t監聽物件.
//此連結由ngx_listening_t監聽的事件建立
ngx_listening_t *listening;
//這個連線已經發送出去的位元組數
off_t sent;
//記錄日誌
ngx_log_t *log;
//在accept一個新連線的時候,會建立一個記憶體池,而這個連線結束時候,會銷燬一個記憶體池.
//這裡所說的連線是成功建立的tcp連線.記憶體池的大小由pool_size決定
//所有的ngx_connect_t結構體都是預分配的
ngx_pool_t *pool;
//連線客戶端的結構體
struct sockaddr *sockaddr;
//連線客戶端的結構體長度
socklen_t socklen;
//連線客戶端的ip(字串形式)
ngx_str_t addr_text;
#if (NGX_SSL)
ngx_ssl_connection_t *ssl;
#endif
//本機中監聽埠對應的socketaddr結構體
//也就是listen監聽物件中的socketaddr成員
struct sockaddr *local_sockaddr;
//用於接收和快取客戶端發來的字元流
ngx_buf_t *buffer;
//該欄位表示將該連線以雙向連結串列形式新增到cycle結構體中的
//reusable_connections_queen雙向連結串列中,表示可以重用的連線.
ngx_queue_t queue;
//連線使用次數,每次建立一條來自客戶端的連線,
//或者建立一條與後端伺服器的連線,number+1
ngx_atomic_uint_t number;
//處理請求的次數
ngx_uint_t requests;
//
unsigned buffered:8;
//日誌級別
unsigned log_error:3; /* ngx_connection_log_error_e */
//不期待字元流結束
unsigned unexpected_eof:1;
//連線超時
unsigned timedout:1;
//連線處理過程中出現錯誤
unsigned error:1;
//標識此連結已經銷燬,記憶體池,套接字等都不可用
unsigned destroyed:1;
//連線處於空閒狀態
unsigned idle:1;
//連線可以重用
unsigned reusable:1;
//連線關閉
unsigned close:1;
//正在將檔案中的資料法網另一端
unsigned sendfile:1;
//連線中傳送緩衝區的資料高於低潮,才傳送資料.
//與ngx_handle_write_event方法中的lowat相對應
unsigned sndlowat:1;
//使用tcp的nodely特性
unsigned tcp_nodelay:2; /* ngx_connection_tcp_nodelay_e */
//使用tcp的nopush特性
unsigned tcp_nopush:2; /* ngx_connection_tcp_nopush_e */
。
。
。
}
也就是說,connection中有read指標指向了read事件rev,同事rev也有指標指向了對應的connection,這樣就達到了資料相互關聯的作用,至此,整個事件的註冊流程就大致清晰了。