主講人: jserv / 課程討論區: 2025 年系統軟體課程
返回「Linux 核心設計」課程進度表Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
khttpd
程式碼導讀khttpd
核心模組掛載 khttpd
時,會執行函式 khttpd_init
,程式碼如下所示:
khttpd
模組初始化的設定和 kecho
模組相似,但仍然可發現二者不同之處,最明顯在於 khttpd
不使用函式 alloc_workqueue
,而用系統預設的 workqueue ,因此之後可討論二者之間的效能差異,以下主要將 khttpd
分成兩個部份
open_listen
: 建立伺服器並等待連線kthread_run
: 用於建立一個立刻執行的執行緒首先函式 open_listen
的部份,建立 socket 連線的步驟都相同,而這邊有個特別的函式 setsockopt
,以下節錄部份 open_listen
程式碼及 setsockopt
程式碼
這邊要留意判斷 Linux 核心版本,參考 Support Linux v5.8+ (#5) 及 net: remove kernel_setsockopt 發現函式 kernel_setsockopt
在 Linux v5.8 之後已被移除,因此在 khttpd
模組裡有對應不同 Linux 核心版本的實作
接著研究像是 SOL_SOCKET
和 SOL_TCP
這類設定的意義,分別參考 socket(7) - Linux man page 及 tcp(7) — Linux manual page ,以下整理 khttpd
所使用到的設定,關於其中 SO_REUSEADDR
,可對照 What is the meaning of SO_REUSEADDR (setsockopt option) - Linux?
SOL_SOCKET
Setting | Description |
---|---|
SO_REUSEADDR | 在原本的連線結束後,有使用相同 IP 及 Port 的連線要求出現,讓 socket 可直接重新建立連線 |
SO_RCVBUF | 設定 socket receive buffer 可接收的最大數量 |
SO_SNDBUF | 設定 socket send buffer 可送出的最大數量 |
SOL_TCP
Setting | Description |
---|---|
TCP_NODELAY | 關閉 Nagle's algorithm — 參考 Best Practices for TCP Optimization in 2019 |
TCP_CORK | 常搭配 TCP_NODELAY 使用,為了避免不斷送出資料量不多 (小於 MSS) 的封包,使用 TCP_CORK 可將資料匯聚並且一次發送資料量較大的封包 — 參考 Is there any significant difference between TCP_CORK and TCP_NODELAY in this use-case? |
建立 socket 後,使用函式 kthread_run
建立執行緒並執行函式 http_server_daemon
整體程式邏輯類似 kecho
核心模組,首先登記 SIGKILL
及 SIGTERM
,接著使用函式 kthread_should_stop
判斷負責執行函式 http_server_daemon
的執行緒是否應該中止,使用函式 kernel_accept
接受 client 連線要求,成功建立後使用函式 kthread_run
建立新的執行緒並且執行函式 http_server_worker
。
http_server_worker
每條連線都由一個子執行緒負責,該執行緒進入 http_server_worker
後依序完成下列工作:
kthread_should_stop()
檢查是否需要終止http_parser_execute()
解析請求內容設定回呼函式的部份,主要是用來送出回應 client 的資料,以下為相關函式
而呼叫以下函式的時機在於解析整個資料後,可在函式 http_parser_execute
裡找到相關實作。
接著探討整個 khttpd
關鍵的函式 http_parser_execute
,其功能就是將收到的資料進行解讀,並傳送給 client
函式 http_parser_execute
主要是一個很大的迴圈,將讀取到的資料的每個字元進行解讀,這邊特別提到兩種情況,分別是 s_start_req
及 s_message_done
在第 7 行可看到整個函式的使用,第 15 行可看到 s_start_req
的情況,其功能是當一開始進行解析時,會使用第一個字元判斷該要求是屬於那一種的類型,可在第 31 ~ 48 行找到各種的對應
第 57 行可看到 s_message_done
的實作,其功能是解析資料完畢後,要給 client 對應的回應,主要是使用以下的巨集進行上面提過的回呼函式呼叫 (位於第 59 行)
khttpd
和 CS:APP 給定的網站伺服器理解 khttpd 的整體流程後,可將其與 CS:APP 中介紹的 TINY Web 伺服器進行比較。
上圖為 CS:APP 教材提供的伺服器流程。可觀察到:
socket
→ bind
→ listen
→ accept
→ 資料傳輸kernel_recvmsg
、kernel_sendmsg
;TINY Web 則以自寫的 RIO (可靠 I/O) 套件包裝 read
與 write
,簡化阻塞處理與緩衝管理其他主要差別:
khttpd
實作的缺失在函式 http_server_worker
執行迴圈的部份,如下所示
觀察程式碼後發現,用於接收資料的緩衝區 buf
在每次迴圈結束並未清空,殘留內容可能影響下一次解析而產生非預期結果。
為驗證此現象,進行一項簡單測試:先執行 telnet localhost 8081
連入伺服器,依序送出 GET /12345 HTTP/1.1
和 GET / HTTP/1.1
。
雖然可見伺服器正常回應,但是查看核心模組相關的訊息
實測顯示 buf
內容確實會受到前一次輸入影響;雖然此範例未出現錯誤,但無法保證其他情境亦安全。
每送出一個請求時會看到 2 次 buf =
,原因在於 HTTP 以 2 個 \r\n
判斷結束,使用者需按 2 次 Enter 按鍵才形成完整請求。
可在迴圈結束前呼叫 memset
清空 buf
,避免殘留資料干擾下一次解析。
接著可再次嘗試上面的實驗,以下為模組輸出的結果
可很明顯看到參數 buf
已經不會被之前的輸入給影響
printk
的使用在實作之前,先使用 htstress.c
測試原本 server 的效能,這裡使用命令 ./htstress http://localhost:8081 -t 3 -c 20 -n 200000
進行測試
在 http_server.h
新增以下結構
而在 khttpd
裡,最常呼叫的 pr_info
位於函式 http_server_response
,以下為修改過程
這裡將 pr_info
移除,改成使用計算送出次數的方式,可避免每次送出資料前,都要先印出的多餘動作,而其他的部份也是做相同的事
最後輸入命令 ./htstress http://localhost:8081 -t 3 -c 20 -n 200000
並測試:
可見伺服器處理效率有明顯上升,再使用命令 dmesg
查看實際運作狀況,如下所示
下圖將 HTTP 傳輸方式分為 2 種:multiple connections 與 persistent connection。
multiple connections 在伺服器回應後關閉連線;persistent connection 則維持同一 TCP 連線,可於其內處理多個請求。
根據 HTTP 規範 可得:
Connection: keep-alive
Connection: close
khttpd 測試步驟:透過 telnet localhost 8081
連線,輸入以下命令並觀察伺服器回應。
GET / HTTP/1.0
GET / HTTP/1.1
從回應中的 Connection:
欄位可見,khttpd 依 HTTP 版本自動切換連線模式,證實已內建 keep‑alive 支援。
參考 Passing Command Line Arguments to a Module ,發現 kernel 是使用巨集 module_param
傳遞參數,接著可在檔案 main.c
發現該巨集的使用,可得知 khttpd
可讓使用者自己設定 port
及 backlog
接著研究 module_param
的實作,可在 linux/include/linux/moduleparam.h 找到數個定義,將相關定義表示在下方
可分成 param_check_##type
, module_param_cb
及 __MODULE_PARM_TYPE
做討論
param_check_##type
由於 khttpd
的變數是使用 ushort
的型態,因此巨集會被展開成 param_check_ushort
,以下為相關巨集
從註解很明顯可知道 param_check_##type
的目的是要在編譯時期就判斷變數 p
是否真的是 type
型態,方法是藉由回傳 p
判斷函式是否回傳相同型態
module_param_cb
__module_param_call
建立一個型態為 kernel_param
且名稱為 __param_##name
的結構,並告訴編譯器以下資訊
__param
區__alignof__(struct kernel_param)
的大小接著查看結構 kernel_param
的宣告
由以上資訊我們可得到最後 __module_param_call
建立的結構,以變數 port
作為範例,如以下所示
最後使用命令 readelf -r khttpd.ko
查看 __param
的區域,的確有 port
和 backlog
的資料
__MODULE_PARM_TYPE
參考〈Linux 核心模組掛載機制〉可知 __UNIQUE_ID
的功能
__UNIQUE_ID
會根據參數產生一個不重複的名字,其中使用到的技術是利用巨集中的 ##
來將兩個參數合併成一個新的字串__attribute__
關鍵字告訴編譯器,這段訊息
.modinfo
區 (__section(".modinfo")
)__used
)__aligned(1)
)__stringify
的目的是為了把參數轉換成字串形式MODULE_PARAM_PREFIX
由巨集 KBUILD_MODNAME
和 "."
組合而成,簡單來說就只是個字串最後以變數 port
為例,會產生以下巨集
接著使用命令 objdump -s khttpd.ko
查看 .modinfo
的區域
繼續根據〈Linux 核心模組掛載機制〉,使用 strace 追蹤 insmod khttpd.ko
查看位於第 8 行 finit_module
的實作,參考 kernel/module.c 及 finit_module(2) - Linux man page
對應 strace 的結果
fd = 3
param_values = "port=1999"
flag = 0
函式 finit_module
呼叫函式 load_module
,接著繼續分析
從上述程式碼可看到從命令列的輸入參數已經被複製到 mod->arg
,且 mod
的型態為 struct mod
,參考 include/linux/module.h
找到了 args
的宣告,從註解可知道 args
的目的就是儲存 command line 的設定參數
回到 load_module
,發現了函式 parse_args
,從註解可知道是要將 command line 的字串拆解
進到函式 parse_args
,參考 kernel/params.c
函式 parse_args
做了以下:
skip_spaces
將字串的第一個字元如果為空白字元,將空白字元全部移除next_arg
找到下一個 argument ,參考 lib/cmdline.cparse_one
將試著將 argument 加進 module 裡,該函式位於 kernel/params.c最後討論函式 parse_one
注意第 17 行的部份, linux 核心逐步尋找符合的參數,並在第 29 行呼叫函式指標 params[i].ops->set(val, ¶ms[i])
,將輸入的資料複製到模組的資料裡,以下為其結構宣告