主講人: jserv / 課程討論區: 2023 年系統軟體課程
返回「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
,主要執行以下幾件事
khttpd
裡主要用來回傳資料給 clientkthread_should_stop
判斷該執行緒是否該中止http_parser_execute
解讀收到的資料設定 call back function 的部份,主要是用來送出回應 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 對應的回應,主要是使用以下的巨集進行上面提過的 callback function 呼叫 (位於第 59 行)
khttpd
和 CS:APP 給定的網站伺服器大致理解 khttpd
的實作流程後,可以開始 khttpd
和 CS:APP 提到的 TINY web 的比較
上圖是 CS:APP 所提供的 server 的流程架構,從這個流程圖可以得到一些資訊
socket
→ bind
→ listen
→ accept
,接著開始傳輸資料,而不同之處在於兩者使用的 API 不同khttpd
使用 linux 核心的 API 而 TINY web 則是使用自己實作的 RIO 套件尚有以下差異:
khttpd
運行在 kernel space 而 TINY web 是運行在 user spacekhttpd
使用多執行緒的方式處理不同的連線,而 TINY web 則是用單執行緒一個一個處理連線khttpd
實作的缺失在函式 http_server_worker
執行迴圈的部份,如下所示
發現用來讀取資料的參數 buf
在每次的迭代中,最後都沒有將原本的 buf 清空,可能會導致非預期的結果
為了觀察是否會有問題,做了一個小實驗,首先輸入命令 telnet localhost 8081
接著對伺服器輸入不同的要求,分別是 GET /12345 HTTP/1.1
及 GET / HTTP/1.1
雖然可見伺服器正常回應,但是查看核心模組相關的訊息
發現參數 buf
實際上會被之前的輸入影響,雖然在這個範例沒有出現任何的問題,但很難保證這種情況不會出現問題
另外這裡每次送出要求會顯示兩個 buf =
是因為 HTTP 的格式是由兩個 \r\n
作為結束條件,因此需要按兩次 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
查看實際運作狀況,如下所示
參考 Passing Command Line Arguments to a Module ,發現 kernel 是使用巨集 module_param
傳遞參數,接著可以在檔案 main.c
發現該巨集的使用,可以得知 khpptd
可以讓使用者自己設定 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])
,將輸入的資料複製到模組的資料裡,以下為其結構宣告