contributed by < OscarShiang
>
linux2020
http-parse-sample.py
運作機制,以及 eBPF 程式在 Linux 核心內部分析封包的優勢為何?src/http.c
裡頭傳遞檔案內容的實作 (對應到 serve_static
函式),思考在高並行的環境中,檔案系統 I/O 的開銷及改進空間;$ grep -r TODO *
找出 seHTTPd 的待做事項,予以列出並充分闡述具體執行方法,例如涉及動態記憶體管理、長期閒置連線的處理機制等等。因為在典型的 accept
, read
, write
等等系統呼叫會導致程式 blocking,即使目前沒有資料傳入。
但是若直接將這些 socket 的 file descroptor 設定成 non-blocking 卻會讓程式無法分辨什麼時候要呼叫 read
以接收資料,進而導致收發資料的過程中發生錯誤。
為了監聽 file descriptor 的狀態,讓 server 可以在正確的時間點使用系統呼叫進行操作,我們可以使用 select
來監聽多個 file descriptor,以便在有資料傳送時才接收資料,但是 select
使用遍歷的方式檢查 fd_set
中的 file descriptor,且 select
最多只能監聽 1024 個 file descriptor,可以看到在使用 select
的限制。
而其替代方案就是使用監聽的時間複雜度為 且可以監聽更多 file descriptor 的 epoll
來實作。
根據 epoll(7) - Linux manual 的敘述:
The epoll API performs a similar task to poll(2): monitoring multiple
file descriptors to see if I/O is possible on any of them. The epoll
API can be used either as an edge-triggered or a level-triggered
interface and scales well to large numbers of watched file
descriptors.
在 epoll 系列的操作中有兩種工作的模式:
而在 Level-triggered 的工作模式下,只要 file descriptor 處於準備就緒的狀態,epoll
就會被觸發,就可能因此造成 client 傳送了一筆資料,但在 server 端會呼叫多次 read
讀取內容。
而與 server 設計考量較為符合的是第二種工作模式,因為我們使用 epoll
來監聽 file descriptor 狀態的目的在於讓 server 在真正需要讀寫的情況時才呼叫對應的系統呼叫來處理,所以我們關注的是狀態的轉變,只有在 file descriptor 從閒置的狀態轉為準備就緒時才進行操作。
所以在使用 epoll 進行監聽時,我們應該要選擇的工作模式應為 Edge-triggered
在 http_parser.c
中針對不同的動作對應出不同的狀態,並設計 struct
來保存這些狀態:
其實作的目的與我們先前提到的 non-blocking 的讀寫行為有關。
因為在 non-blocking 的行為中,伺服器不會等待到資料寫入結束後才進行其他的處理,且在 server 執行時也可能會遇到連線不穩定或是封包缺失的問題,這就表示 http_parser
需要保存當前解析的狀態,以便在中斷解析後可以回復到之前的狀態繼續解析剩下的資料。
在 Computed goto for efficient dispatch tables 中有提到:因為 switch 還是會有判斷數值的處理,所以在執行效率上 goto
還是會比使用 switch-case
來得好,而且使用 goto
也可以避免使用 switch-case
帶來 branch miss 的損失。
所以我搭配 GNU extension 中提供的 Labels as Values,在 http_parser.c:http_parse_request_line
中的加入 conditions
作為 label address table 讓其可以利用 state
來查表,並直接利用 goto
跳躍到對應的程式碼。
接著將 switch
的部分取代為 goto
接著利用 perf 觀察使用 computed goto 改寫後關於 branch prediction 的表現:
使用 switch-case 的版本:
可以看到 computed goto 版本的伺服器因為在 branch miss 的數量較使用 switch-case 版本少,所以執行的時間得到了改善。
接著因為在宣告 state
與 conditions
時裡面的名稱有高度的重疊,所以改以 X macro 的方式簡化宣告的程式碼
利用 macro 組合 enumerate 的變數與 label 並將 macro 作為變數傳到 state_code
這個 macro 中,展開後就會得到與上方宣告一樣的結果
設計 dispatch()
的 macro 來進行各種情況的程式碼跳躍。
因為將 for 迴圈的功能也實作在 dispatch
中,所以使用 computed goto 進行 dispatch
之前,也要處理字串迭代的更新。
但考慮到在進入 for 迴圈的時候並不會將 pi
遞增,所以為了讓進入迴圈前與進入迴圈後 dispatch
的行爲與原先 for 迴圈的實作一致,我將 pi
作為 macro 參數的參數帶入,讓其可以在兩種不同的情況做不同的處理。
因為在使用 server 的過程中,如果是用戶關閉瀏覽器,結束與伺服器連線時,伺服器可以透過檢查 read 與 write 的回傳判斷用戶是否以結束連線,從而主動關閉連線。
但是在用戶離線時,因為沒有發生讀寫,所以伺服器無法利用系統呼叫的回傳值來判斷,而通訊協定也並沒有針對這樣情況的處理,所以也無從判斷用戶已離線。
因此我們需要使用計時器的方式,在用戶超過一段時間未對伺服器傳送請求時關閉其連線。
在 timer 中使用 proirity queue 的用意在於,因為伺服器需要同時處理多個系統連線的請求,如果在操作的過程中能夠優先挑出即將超時的 request 來處理,就及時關閉超時的 request,並釋放相關的資源,減輕伺服器的負擔。
「及時」是 "in time", 「即時」是 "real time",這兩個詞彙語意不同
為了在執行 seHTTPd 可以具備更多的彈性,讓其可以依據執行時提供的參數更改 port 與 root 的位置,我使用 <getopt.h>
提供的 getopt_long
來分析輸入的參數。
接著建立使用 getopt_long
需要提供的 optstring
與 longopts
。
根據 getopt_long(3) - Linux manual 的說明,進行設定:
接著在 mainloop.c
中使用 do-while 迴圈分析參數,並依據對應的參數進行設定:
並建立參數使用的說明訊息
serve_static
產生 header
的行為在 http.c:serve_static
函式原本的實作中,為了產生 HTTP header,使用如下列的方式來串接字串:
但在 sprintf(3) - Linux manual 中有提到:
Some programs imprudently rely on code such as the following
sprintf(buf, "%s some further text", buf);
to append text to buf.
However, the standards explicitly note that the results are undefined if source and destination buffers overlap when calling sprintf(), snprintf(), vsprintf(), and vsnprintf(). Depending on the version of gcc(1) used, and the compiler options employed, calls such as the above will not produce the expected results.
所以我改用 snprintf
並將 src
指標利用 len
變數來做位移,避免 overlapping 的問題產生。
除了可以避免 src
與 dst
重疊的問題之外,因為有提供寫入長度的上限,所以也不會有 overflow 的情況發生。
因為使用 len
變數記錄了 header
字串的長度,在傳送內容的時候不需要重新呼叫 strlen
來計算字串長度,直接傳送 len
的數值即可。