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,這樣就達到了數據相互關聯的作用,至此,整個事件的註冊流程就大致清晰了。