上一篇我們探討了lighttpd對監聽socket的處理,這次我們看看連接socket的處理,以及相關超時的處理。
lighttpd和客戶端建立連接的過程:
1.lighttpd檢測監聽socket的IO事件,如果有可讀事件發生,那么表示有新的連接請求,于是調用network.c/network_server_handle_fdevent()來處理連接請求。
2.network_server_handle_fdevent()函數調用connections.c/connection_accept() 接受客戶端的請求,建立連接,得到連接socket的fd,也就是accept函數的返回值。
3.建立連接后,這個連接對應的狀態機狀態被設置為CON_STATE_REQUEST_START,即開始讀取客戶端發過來的request。
4.從connection_accept函數返回到network_server_handle_fdevent()函數的for循環中后,程序調用connection_state_machine()函數,這個函數是根據當前連接的狀態機狀態來設置狀態機的下一個狀態,CON_STATE_REQUEST_START的下一個狀態是CON_STATE_READ,這個狀態表示連接正在讀取客戶端發送的數據。
5.當連接的狀態機被設置成CON_STATE_READ后,在connection_state_machine()函數中有這樣一個switch語句:
~~~
switch (con->state)
{
case CON_STATE_READ_POST:
case CON_STATE_READ:
case CON_STATE_CLOSE:
fdevent_event_add(srv->ev, &(con->fde_ndx), con->fd, FDEVENT_IN);
break;
case CON_STATE_WRITE:
/*
* request write-fdevent only if we really need it
* - if we have data to write
* - if the socket is not writable yet
*/
if (!chunkqueue_is_empty(con->write_queue) && (con->is_writable == 0) &&(con->traffic_limit_reached == 0))
{
fdevent_event_add(srv->ev, &(con->fde_ndx), con->fd, FDEVENT_OUT);
}
else
{
fdevent_event_del(srv->ev, &(con->fde_ndx), con->fd);
}
break;
default:
fdevent_event_del(srv->ev, &(con->fde_ndx), con->fd);
break;
}
~~~
它將狀態處在CON_STATE_READ_POST,CON_STATE_READ和CON_STATE_CLOSE的連接對應的連接socket fd加入到fdevent系統中,并監聽【可讀】事件。將處CON_STATE_WRITE狀態且有數據要寫的連接對應的socket fd加入到fdevent系統中,并監聽【可寫】事件。其他狀態的連接則把對應的fd從fdevent系統中刪除,因為這些連接不會有IO事件發生。
這樣,連接socket fd就被加入到了fdevent系統中,之后等待IO事件的發生,這一部分在上一篇已經說明過了:
~~~
//啟動事件輪詢。底層使用的是IO多路轉接。
if ((n = fdevent_poll(srv->ev, 1000)) > 0)
{
/* n是事件的數量 */
int revents;
int fd_ndx = -1;
/* 逐個處理已經準備好的請求,直到所有的請求處理結束 */
do
{
fdevent_handler handler;
void *context;
handler_t r;
fd_ndx = fdevent_event_next_fdndx(srv->ev, fd_ndx); //獲得發生了 I/O 事件的文件描述符在 fdarray 中的索引
revents = fdevent_event_get_revent(srv->ev, fd_ndx); //獲得該文件描述符上發生的 I/O 事件類型
fd = fdevent_event_get_fd(srv->ev, fd_ndx); //獲得該文件描述符
handler = fdevent_get_handler(srv->ev, fd); //獲得 I/O 事件處理的回調函數
context = fdevent_get_context(srv->ev, fd); //獲得 I/O 事件處理的上下文環境
/*
* connection_handle_fdevent needs a joblist_append
*/
/**
* 調用回調函數進行I/O事件處理,并傳入相關參數
*/
switch (r = (*handler) (srv, context, revents))
{
case HANDLER_FINISHED:
case HANDLER_GO_ON:
case HANDLER_WAIT_FOR_EVENT:
case HANDLER_WAIT_FOR_FD:
break;
case HANDLER_ERROR:
SEGFAULT();
break;
default:
log_error_write(srv, __FILE__, __LINE__, "d", r);
break;
}
}while (--n > 0);
}
else if (n < 0 && errno != EINTR)
{
log_error_write(srv, __FILE__, __LINE__, "ss","fdevent_poll failed:", strerror(errno));
}
~~~
連接fd對應的處理函數是connections.c/connection_handle_fdevent()函數:
~~~
handler_t connection_handle_fdevent(void *s, void *context,int revents)
{
server *srv = (server *) s;
connection *con = context;
//把這個連接加到作業隊列中。
joblist_append(srv, con);
if (revents & FDEVENT_IN)
{
con->is_readable = 1;
}
if (revents & FDEVENT_OUT)
{
con->is_writable = 1;
/*
* we don't need the event twice
*/
}
if (revents & ~(FDEVENT_IN | FDEVENT_OUT))
{
/*
* looks like an error 即可讀又可寫,可能是一個錯誤。
*/
/*
* FIXME: revents = 0x19 still means that we should read from the queue
*/
if (revents & FDEVENT_HUP)
{
if (con->state == CON_STATE_CLOSE)
{
con->close_timeout_ts = 0;
}
else
{
/*
* sigio reports the wrong event here there was no HUP at all
*/
connection_set_state(srv, con, CON_STATE_ERROR);
}
}
else if (revents & FDEVENT_ERR)
{
connection_set_state(srv, con, CON_STATE_ERROR);
}
else
{
log_error_write(srv, __FILE__, __LINE__, "sd","connection closed: poll() -> ???", revents);
}
}
if (con->state == CON_STATE_READ|| con->state == CON_STATE_READ_POST)
{
connection_handle_read_state(srv, con);
//繼續讀取數據,直到數據讀取完畢
}
// 數據的寫回并沒有放給狀態機去處理。
if (con->state == CON_STATE_WRITE&& !chunkqueue_is_empty(con->write_queue) && con->is_writable)
{
if (-1 == connection_handle_write(srv, con))
{
connection_set_state(srv, con, CON_STATE_ERROR);
log_error_write(srv, __FILE__, __LINE__, "ds", con->fd,"handle write failed.");
}
else if (con->state == CON_STATE_WRITE)
{
//寫數據出錯,記錄當前時間,用來判斷連接超時。
con->write_request_ts = srv->cur_ts;
}
}
if (con->state == CON_STATE_CLOSE)
{
/*
* flush the read buffers 清空緩沖區中的數據。
*/
int b;
//獲取緩沖區中數據的字節數
if (ioctl(con->fd, FIONREAD, &b))
{
log_error_write(srv, __FILE__, __LINE__, "ss","ioctl() failed", strerror(errno));
}
if (b > 0)
{
char buf[1024];
log_error_write(srv, __FILE__, __LINE__, "sdd","CLOSE-read()", con->fd, b);
//將緩沖區中的數據讀取后并丟棄,此時連接已經關閉,數據是無用數據。
read(con->fd, buf, sizeof(buf));
}
else
{
/*
* nothing to read 緩沖區中沒有數據。復位連接關閉超時計時。
*/
con->close_timeout_ts = 0;
}
}
return HANDLER_FINISHED;
}
~~~
connection_handle_fdevent()函數根據當前連接fd所發生的IO事件,對connection結構體中的標記變量賦值,如is_writable,is_readable等,并做一些時間的記錄。這些事件所對應的【真正的IO處理則交給狀態機處理】。狀態機根據這些標記變量進行相應的動作處理。
下面的圖簡要的描述了fdevent系統對連接fd和監聽fd的處理:

接下來簡單地看下連接超時的處理。
連接超時有三種:讀數據超時,寫數據超時和關閉超時。
處理超時的代碼在server.c中的main函數woker進程開始部分:
~~~
/* main-loop */
while (!srv_shutdown) { //只要srv_shutdown不為1,工作進程持續執行
int n;
size_t ndx;
time_t min_ts;
/* 處理HUP信號,代碼省略 */
/* 處理ALARM信號 */
if (handle_sig_alarm) {
/* a new second */
#ifdef USE_ALARM
/* reset notification */
handle_sig_alarm = 0;
#endif
/* get current time */
min_ts = time(NULL);
/* 比較服務器記錄的時間和當前時間
* 如果值不一樣,說明已經過了1s
*/
if (min_ts != srv->cur_ts) {
int cs = 0;
connections *conns = srv->conns;
handler_t r;
switch(r = plugins_call_handle_trigger(srv)) { //調用plugins_call_handle_trigger來處理各個模塊的ALARM信號處理函數
case HANDLER_GO_ON:
break;
case HANDLER_ERROR:
log_error_write(srv, __FILE__, __LINE__, "s", "one of the triggers failed");
break;
default:
log_error_write(srv, __FILE__, __LINE__, "d", r);
break;
}
/* trigger waitpid */
srv->cur_ts = min_ts; //更新服務器記錄時間
/* cleanup stat-cache */
stat_cache_trigger_cleanup(srv); //清除緩存,刪除一些比較舊的節點
/**
* check all connections for timeouts
*
*/
for (ndx = 0; ndx < conns->used; ndx++) { //處理超時連接
int changed = 0;
connection *con;
int t_diff;
con = conns->ptr[ndx];
if (con->state == CON_STATE_READ ||
con->state == CON_STATE_READ_POST) { //連接狀態是讀
if (con->request_count == 1) { //處理一個請求
if (srv->cur_ts - con->read_idle_ts > con->conf.max_read_idle) {
/* time - out */
#if 0
log_error_write(srv, __FILE__, __LINE__, "sd",
"connection closed - read-timeout:", con->fd);
#endif
connection_set_state(srv, con, CON_STATE_ERROR); //調用connection_set_state進行狀態機的狀態轉換
changed = 1;
}
} else { //連接同時處理多個請求
if (srv->cur_ts - con->read_idle_ts > con->conf.max_keep_alive_idle) {
/* time - out */
#if 0
log_error_write(srv, __FILE__, __LINE__, "sd",
"connection closed - read-timeout:", con->fd);
#endif
connection_set_state(srv, con, CON_STATE_ERROR);
changed = 1;
}
}
}
if ((con->state == CON_STATE_WRITE) &&
(con->write_request_ts != 0)) { //連接狀態是寫
#if 0
if (srv->cur_ts - con->write_request_ts > 60) {
log_error_write(srv, __FILE__, __LINE__, "sdd",
"connection closed - pre-write-request-timeout:", con->fd, srv->cur_ts - con->write_request_ts);
}
#endif
if (srv->cur_ts - con->write_request_ts > con->conf.max_write_idle) {
/* time - out */
#if 1
log_error_write(srv, __FILE__, __LINE__, "sbsosds",
"NOTE: a request for",
con->request.uri,
"timed out after writing",
con->bytes_written,
"bytes. We waited",
(int)con->conf.max_write_idle,
"seconds. If this a problem increase server.max-write-idle");
#endif
connection_set_state(srv, con, CON_STATE_ERROR);
changed = 1;
}
}
/* we don't like div by zero */
if (0 == (t_diff = srv->cur_ts - con->connection_start)) t_diff = 1;
/* 處理傳輸速度限制
* 如果某一時刻平均傳輸速度達到了用戶設置的最大值,則停止發送數據(con->traffic_limit_reached將被設為1,
* 進入下面if中處理)。只要檢測到平均傳輸速度小于用戶設置的最大值就繼續發送數據,
* 則滿足if的條件,con->traffic_limit_reached設為 0,同時調用狀態機切換函數。
* 代碼省略
* /
}
}
/* 根據當前的資源利用情況禁用或啟用 server sockets 服務,代碼省略 */
/* something else,代碼省略*/
/* 輪詢 I/O 事件的發生,
* 其中等待 I/O 事件發生的超時值為1秒。
* 代碼省略
*/
/* 進行其他處理,之后while一次循環完成 */
}
~~~
為清晰地看超時處理部分的代碼,我把一些無關的代碼略去了。
總結如下:
~~~
while (未收到終止信號)
{
if (收到HUP)
處理HUP信號
if (handle_sig_alarm標識為1)
{
獲取當前時間;
if (當前時間 != 服務器時間)
{
調用各模塊的超時處理函數;
更新服務器記錄時間;
for (每個連接)
處理超時;
}
}
根據當前的資源利用情況禁用或啟用 server sockets 服務;
輪詢 I/O 事件的發生;
對每一個發生的 I/O 事件進行處理;
}
~~~
可以看到,作者通過當前時間和服務器記錄的當前時間來判斷時間是否過了一秒。如果兩個時間不一樣,那么時間就過了一秒,子進程每循環一次都要比較服務器記錄的時間和當前時間,直到兩個時間不一樣為止。
也就是說,作者好像并沒有使用SIGALRM信號來判斷超時,從代碼中我們可以看到,關于SIGALRM的使用,只有在定義了USE_ALARM之后才生效:
~~~
#ifdef USE_ALARM
/* reset notification */
handle_sig_alarm = 0;
#endif
~~~
然而我們查找USE_ALARM,發現事實上該標識沒有被定義(而是被注釋掉了):
~~~
#ifndef __sgi
/* IRIX doesn't like the alarm based time() optimization */
/* #define USE_ALARM */
#endif
~~~
所以handle_sig_alarm一直為1,即作者這里并不使用SIGALRM信號,這樣可以減少很多信號處理,降低程序的復雜度。但是每次循環程序都要輪詢一次,可能會影響效率(實際上效果如何,需要大家試一下哈~)
在處理程序中,lighttpd通過比較read_idle_ts,write_request_ts和當前時間的差值來判斷連接是否讀超時或寫超時。如果這兩個差值分別大于max_read_idle和max_write_idle則表示超時。如果一個連接正在處理多個請求時,讀超時是和max_keep_alive_idle比較。這些上限值在配置中設置。
對于read_idle_ts,在連接進入CON_STATE_REQUEST_START狀態時,記錄了當前時間。如果連接長時間沒有去讀取request請求,則也表示連接超時。當連接開始讀數據時,read_idle_ts記錄開始讀數據的時間。
對于write_request_ts,在處理CON_STATE_WRITE狀態時,有對其賦值的語句。在connection_handle_fdevent函數中也有。事實上,只有在調用connection_handle_write函數出錯并且連接處在CON_STATE_WRITE狀態時,記錄當前時間。
可見,lighttpd對讀和寫的超時處理是不一樣的。對于讀,設定了最長時間,不管讀多少數據,一旦時間超了就算超時。而對于寫,只有在寫出錯的時候才開始計算超時。如果沒有出錯,那么寫數據花再多的時間也不算超時。(可能出現上傳到一半就超時的問題,但是在絕大多數情況下,上傳數據都是很小的,而下載的數據往往很多,因此,這樣處理可以提高效率,如果需要上傳大量數據,可以修改配置中的超時限制)。
fdevent就分析到這里!~
學習內容參考自:
[http://www.cnblogs.com/kernel_hcy/archive/2010/03/22/1691951.html](http://www.cnblogs.com/kernel_hcy/archive/2010/03/22/1691951.html)