lighttpd1.4.18程式碼分析(六)--處理連線fd的流程
阿新 • • 發佈:2018-12-27
現在開始講解lighttpd如何處理連線fd.
在第四節,已經講解了如何處理伺服器負責監聽連線的fd,處理連線fd的流程在前半部分與監聽fd大體相同:在通過監聽fd接收一個新的連線之後,伺服器將這個fd加入到事件處理器中, 設定它所感興趣的IO事件型別, 當IO狀態發生變化時, 事件處理器獲取被觸發的fd,回撥函式,事件等,然後通過已經註冊的回撥函式進行處理.
這裡的區別就在於回撥函式的不同上.對於監聽fd而言, 該回調函式是network.c檔案中的network_server_handle_fdevent函式,這個在第四節中已經做了分析;與之對應的,連線fd的回撥函式是connections.c檔案中的connection_handle_fdevent函式.
// 這個函式是處理接受連結的函式, 與network_server_handle_fdevent對應handler_t connection_handle_fdevent(void*s, void*context, int revents) {
server *srv = (server *)s;
connection *con = context;
// 新增到server的joblist中 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
*/
#ifdef USE_LINUX_SIGIO
if (srv->ev->in_sigio ==1) {
log_error_write(srv, __FILE__, __LINE__, "sd",
"connection closed: poll() -> HUP", con->fd);
} else {
connection_set_state(srv, con, CON_STATE_ERROR);
}
#else
connection_set_state(srv, con, CON_STATE_ERROR);
#endif
}
} elseif (revents & FDEVENT_ERR) {
#ifndef USE_LINUX_SIGIO
log_error_write(srv, __FILE__, __LINE__, "sd",
"connection closed: poll() -> ERR", con->fd);
#endif
connection_set_state(srv, con, CON_STATE_ERROR);
} else {
log_error_write(srv, __FILE__, __LINE__, "sd",
"connection closed: poll() -> ???", revents);
}
}
// 如果連線的狀態是READ, 那麼處理read狀態if (con->state == CON_STATE_READ ||
con->state == CON_STATE_READ_POST) {
connection_handle_read_state(srv, con);
}
// 如果連線狀態是WRITE並卻寫緩衝區佇列中不為空 並且該連線是可寫的, 就去處理寫狀態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.");
} elseif (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));
}
// 如果還有資料, 列印錯誤log, 並且讀入資料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,也就是儲存連線相關資料的結構體中, 有一個叫state的成員, 顧名思義, 這個成員儲存的是一個連線的狀態,這裡所說的狀態與前面提到的IO狀態是不同, IO狀態是用於表示一個fd可讀/可寫/出錯等, 而這個狀態更多的是與協議相關的部分.它是一個列舉型別:
/* the order of the items should be the same as they are processed
* read before write as we use this later */
typedef enum {
CON_STATE_CONNECT, // 連線 CON_STATE_REQUEST_START, // 開始獲取請求 CON_STATE_READ, // 處理讀 CON_STATE_REQUEST_END, // 請求結束 CON_STATE_READ_POST, // 處理讀,但是是POST過來的資料 CON_STATE_HANDLE_REQUEST, // 處理請求 CON_STATE_RESPONSE_START, // 開始回覆 CON_STATE_WRITE, // 處理寫 CON_STATE_RESPONSE_END, // 回覆結束 CON_STATE_ERROR, // 出錯 CON_STATE_CLOSE // 連線關閉} connection_state_t;
為什麼需要這些狀態?因為lighttpd中採用了所謂"狀態機"去處理連線,而這些狀態就是狀態機中的各種不同狀態.
在lighttpd的官方文件中, 對其使用的狀態機有一篇文件,在這裡:
http://redmine.lighttpd.net/wiki/lighttpd/Docs:InternalHTTPStates
我覺得裡面的這幅圖非常的直觀,學習過編譯原理的人一看就可以知道這是狀態機的轉換圖:
現在回到本章的主題中, lighttpd如何處理連線fd.
前面給出的處理連線fd的回撥函式,最開始地方有一段程式碼:
joblist_append(srv, con);
這個函式將一個連線connection結構體放入到joblist中, 後面的部分根據不同的情況設定connection中的state欄位,呼叫的是connection_set_state函式.
現在回到server.c函式中, 第四節中已經結合處理監聽fd的流程講解了這段函式:
// 輪詢FDif ((n = fdevent_poll(srv->ev, 1000)) >0) {
/* n is the number of events */int revents;
int fd_ndx;
fd_ndx =-1;
do {
fdevent_handler handler;
void*context;
handler_t r;
// 獲得處理這些事件的函式指標 fd等
// 獲得下一個fd在fdarray中的索引 fd_ndx = fdevent_event_next_fdndx (srv->ev, fd_ndx);
// 獲得這個fd要處理的事件型別 revents = fdevent_event_get_revent (srv->ev, fd_ndx);
// 獲取fd fd = fdevent_event_get_fd (srv->ev, fd_ndx);
// 獲取回撥函式 handler = fdevent_get_handler(srv->ev, fd);
// 獲取處理相關的context(對server是server_socket指標, 對client是connection指標) context = fdevent_get_context(srv->ev, fd);
/* connection_handle_fdevent needs a joblist_append */// 進行處理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:
/* should never happen */
SEGFAULT();
break;
default:
log_error_write(srv, __FILE__, __LINE__, "d", r);
break;
}
} while (--n >0);
} elseif (n <0&& errno != EINTR) {
log_error_write(srv, __FILE__, __LINE__, "ss",
"fdevent_poll failed:",
strerror(errno));
}
在server.c檔案中, 緊跟著這段程式碼的是:
// 處理joblist中的連線for (ndx =0; ndx < srv->joblist->used; ndx++) {
connection *con = srv->joblist->ptr[ndx];
handler_t r;
connection_state_machine(srv, con);
switch(r = plugins_call_handle_joblist(srv, con)) {
case HANDLER_FINISHED:
case HANDLER_GO_ON:
break;
default:
log_error_write(srv, __FILE__, __LINE__, "d", r);
break;
}
con->in_joblist =0;
}
簡單的說, 這段程式碼是一個迴圈, 從joblist中依次取出已經放在這裡的connection指標, 再呼叫connection_state_machine函式進行處理.
connection_state_machine函式是一個非常重要的函式, 它就是處理連線fd的狀態機, 在後面將詳細分析這個函式.
現在回顧一下lighttpd處理連線fd的大體框架:前面的流程與處理監聽fd大體相同,在與處理連線fd相關的回撥函式中, 首先將需要處理的fd相關的connection加入到joblist中, 設定它的state, 後面再輪詢joblist, 進入狀態機進行處理.
這個過程可能值得商榷, 比如為什麼在回撥函式中需要將一個connection指標加入到joblist中, 後面再一個迴圈輪詢joblist中的connection,這樣不是顯得效率低下嗎?我們需要注意的是,前面提到的IO事件狀態和connection中的成員state是不同的!第一個輪詢過程(IO事件處理器的輪詢)是根據哪些fd的IO發生了變化被觸發而去呼叫回撥函式, 而後面的迴圈(joblist的輪詢)中的connection則不一定都是IO發生變化的!
打一個比方, 一個連線到來, 此時我們把它放入到事件處理器中, 當它可讀時被觸發, 也就是在第一個輪詢中被觸發;如果在處理的時候出了問題, 不能繼續, 此時我們只需要儲存它當前的state欄位, 在下一次操作中, 由於它的IO沒有發生變化, 那麼將不會在第一個輪詢也就是IO事件處理器中被處理, 而只會在輪詢joblist時被處理, 只需要它的state欄位是正確的, 放到狀態機處理函式中就可以繼續下去.
在第四節,已經講解了如何處理伺服器負責監聽連線的fd,處理連線fd的流程在前半部分與監聽fd大體相同:在通過監聽fd接收一個新的連線之後,伺服器將這個fd加入到事件處理器中, 設定它所感興趣的IO事件型別, 當IO狀態發生變化時, 事件處理器獲取被觸發的fd,回撥函式,事件等,然後通過已經註冊的回撥函式進行處理.
這裡的區別就在於回撥函式的不同上.對於監聽fd而言, 該回調函式是network.c檔案中的network_server_handle_fdevent函式,這個在第四節中已經做了分析;與之對應的,連線fd的回撥函式是connections.c檔案中的connection_handle_fdevent函式.
//
server *srv = (server *)s;
connection *con = context;
// 新增到server的joblist中 joblist_append(srv, con);
// 可讀if (revents & FDEVENT_IN) {
con->is_readable
}
// 可寫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
if (con->state == CON_STATE_CLOSE) {
con->close_timeout_ts =0;
} else {
/* sigio reports the wrong event here
*
* there was no HUP at all
*/
#ifdef USE_LINUX_SIGIO
if (srv->ev->in_sigio ==1) {
log_error_write(srv, __FILE__, __LINE__, "sd",
"connection closed: poll() -> HUP", con->fd);
} else {
connection_set_state(srv, con, CON_STATE_ERROR);
}
#else
connection_set_state(srv, con, CON_STATE_ERROR);
#endif
}
} elseif (revents & FDEVENT_ERR) {
#ifndef USE_LINUX_SIGIO
log_error_write(srv, __FILE__, __LINE__, "sd",
"connection closed: poll() -> ERR", con->fd);
#endif
connection_set_state(srv, con, CON_STATE_ERROR);
} else {
log_error_write(srv, __FILE__, __LINE__, "sd",
"connection closed: poll() -> ???", revents);
}
}
// 如果連線的狀態是READ, 那麼處理read狀態if (con->state == CON_STATE_READ ||
con->state == CON_STATE_READ_POST) {
connection_handle_read_state(srv, con);
}
// 如果連線狀態是WRITE並卻寫緩衝區佇列中不為空 並且該連線是可寫的, 就去處理寫狀態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.");
} elseif (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));
}
// 如果還有資料, 列印錯誤log, 並且讀入資料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,也就是儲存連線相關資料的結構體中, 有一個叫state的成員, 顧名思義, 這個成員儲存的是一個連線的狀態,這裡所說的狀態與前面提到的IO狀態是不同, IO狀態是用於表示一個fd可讀/可寫/出錯等, 而這個狀態更多的是與協議相關的部分.它是一個列舉型別:
/* the order of the items should be the same as they are processed
* read before write as we use this later */
typedef enum {
CON_STATE_CONNECT, // 連線 CON_STATE_REQUEST_START, // 開始獲取請求 CON_STATE_READ, // 處理讀 CON_STATE_REQUEST_END, // 請求結束 CON_STATE_READ_POST, // 處理讀,但是是POST過來的資料 CON_STATE_HANDLE_REQUEST, // 處理請求 CON_STATE_RESPONSE_START, // 開始回覆 CON_STATE_WRITE, // 處理寫 CON_STATE_RESPONSE_END, // 回覆結束 CON_STATE_ERROR, // 出錯 CON_STATE_CLOSE // 連線關閉} connection_state_t;
為什麼需要這些狀態?因為lighttpd中採用了所謂"狀態機"去處理連線,而這些狀態就是狀態機中的各種不同狀態.
在lighttpd的官方文件中, 對其使用的狀態機有一篇文件,在這裡:
http://redmine.lighttpd.net/wiki/lighttpd/Docs:InternalHTTPStates
我覺得裡面的這幅圖非常的直觀,學習過編譯原理的人一看就可以知道這是狀態機的轉換圖:
現在回到本章的主題中, lighttpd如何處理連線fd.
前面給出的處理連線fd的回撥函式,最開始地方有一段程式碼:
joblist_append(srv, con);
這個函式將一個連線connection結構體放入到joblist中, 後面的部分根據不同的情況設定connection中的state欄位,呼叫的是connection_set_state函式.
現在回到server.c函式中, 第四節中已經結合處理監聽fd的流程講解了這段函式:
// 輪詢FDif ((n = fdevent_poll(srv->ev, 1000)) >0) {
/* n is the number of events */int revents;
int fd_ndx;
fd_ndx =-1;
do {
fdevent_handler handler;
void*context;
handler_t r;
// 獲得處理這些事件的函式指標 fd等
// 獲得下一個fd在fdarray中的索引 fd_ndx = fdevent_event_next_fdndx (srv->ev, fd_ndx);
// 獲得這個fd要處理的事件型別 revents = fdevent_event_get_revent (srv->ev, fd_ndx);
// 獲取fd fd = fdevent_event_get_fd (srv->ev, fd_ndx);
// 獲取回撥函式 handler = fdevent_get_handler(srv->ev, fd);
// 獲取處理相關的context(對server是server_socket指標, 對client是connection指標) context = fdevent_get_context(srv->ev, fd);
/* connection_handle_fdevent needs a joblist_append */// 進行處理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:
/* should never happen */
SEGFAULT();
break;
default:
log_error_write(srv, __FILE__, __LINE__, "d", r);
break;
}
} while (--n >0);
} elseif (n <0&& errno != EINTR) {
log_error_write(srv, __FILE__, __LINE__, "ss",
"fdevent_poll failed:",
strerror(errno));
}
在server.c檔案中, 緊跟著這段程式碼的是:
// 處理joblist中的連線for (ndx =0; ndx < srv->joblist->used; ndx++) {
connection *con = srv->joblist->ptr[ndx];
handler_t r;
connection_state_machine(srv, con);
switch(r = plugins_call_handle_joblist(srv, con)) {
case HANDLER_FINISHED:
case HANDLER_GO_ON:
break;
default:
log_error_write(srv, __FILE__, __LINE__, "d", r);
break;
}
con->in_joblist =0;
}
簡單的說, 這段程式碼是一個迴圈, 從joblist中依次取出已經放在這裡的connection指標, 再呼叫connection_state_machine函式進行處理.
connection_state_machine函式是一個非常重要的函式, 它就是處理連線fd的狀態機, 在後面將詳細分析這個函式.
現在回顧一下lighttpd處理連線fd的大體框架:前面的流程與處理監聽fd大體相同,在與處理連線fd相關的回撥函式中, 首先將需要處理的fd相關的connection加入到joblist中, 設定它的state, 後面再輪詢joblist, 進入狀態機進行處理.
這個過程可能值得商榷, 比如為什麼在回撥函式中需要將一個connection指標加入到joblist中, 後面再一個迴圈輪詢joblist中的connection,這樣不是顯得效率低下嗎?我們需要注意的是,前面提到的IO事件狀態和connection中的成員state是不同的!第一個輪詢過程(IO事件處理器的輪詢)是根據哪些fd的IO發生了變化被觸發而去呼叫回撥函式, 而後面的迴圈(joblist的輪詢)中的connection則不一定都是IO發生變化的!
打一個比方, 一個連線到來, 此時我們把它放入到事件處理器中, 當它可讀時被觸發, 也就是在第一個輪詢中被觸發;如果在處理的時候出了問題, 不能繼續, 此時我們只需要儲存它當前的state欄位, 在下一次操作中, 由於它的IO沒有發生變化, 那麼將不會在第一個輪詢也就是IO事件處理器中被處理, 而只會在輪詢joblist時被處理, 只需要它的state欄位是正確的, 放到狀態機處理函式中就可以繼續下去.