linux2023
contributed by < xueyang0312 >
kecho
已使用 CMWQ,請陳述其優勢和用法,應重現相關實驗khttpd
核心模組的效能瓶頸,該如何設計相關實驗學習。搭配閱讀《Demystifying the Linux CPU Scheduler》第 6 章drop-tcp-socket
核心模組運作原理。TIME-WAIT
sockets 又是什麼?參照 eBPF 教程
virtme
建構虛擬化執行環境,搭配 GDB 追蹤 khttpd
核心模組HTTP 封包範例:
khttpd
Kernel Module當掛載 khttpd kernel module 時,會執行 khttpd_init
函式,主要會做兩件事情:
open_listen_socket
: 建立 DEFAULT_PORT=8081
server。kthread_run
: 建立一個立刻執行的 kernel thread,等待 client 連線,並服務 client。open_listen_socket
為建立 TCP socket 連線,可以將 setsockopt 函式包裝成類似於系統呼叫 setsockopt
的形式。
在 socket man page 有提到,可以在 SOL_SOCKET 級別上對所有 socket 的屬性進行設定和讀取。
These socket options can be set by using setsockopt(2) and read with getsockopt(2) with the socket level set to SOL_SOCKET for all sockets:
有些特別針對 TCP level 設定,例如:TCP_NODELAY
、TCP_CORK
在額外設定。
SOL_SOCKET
LevelSetting | Description |
---|---|
SO_REUSEADDR | 可以在绑定一個已經被使用過的地址和端口之前允許其重用,即使該地址和端口仍然處於 TIME_WAIT 狀態。這個選項的作用是在程序異常終止或網路問題導致端口没有正常關閉時,能夠更快地重啟程序並繼續使用相同的地址和端口。 |
SO_RCVBUF | Sets or gets the maximum socket receive buffer in bytes. |
SO_SNDBUF | Sets or gets the maximum socket send buffer in bytes. |
SO_REUSEADDR | socketA | socketB | Result |
---|---|---|---|
ON / OFF | 192.168.1.1:21 | 192.168.1.1:21 | ERROR(EADDRINUSE |
ON / OFF | 192.168.1.1:21 | 10.0.1.1:21 | OK |
ON / OFF | 10.0.1.1:21 | 192.168.1.1:21 | OK |
OFF | 192.168.1.1:21 | 0.0.0.0:21 | ERROR(EADDRINUSE) |
OFF | 0.0.0.0:21 | 192.168.1.1:21 | ERROR(EADDRINUSE) |
SOL_TCP
LevelSetting | Description |
---|---|
TCP_NODELAY | Turn off Nagle’s algorithm |
TCP_CORK | 經常搭配 TCP_NODELAY 使用,為了避免不斷送出資料量不多 (小於 MSS) 的封包,使用 TCP_CORK 可以將資料匯聚並且一次發送資料量較大的封包 |
http_server_daemon
建立 socket 完成並等待 client 連線,會建立一個 kernel thread 來執行 http_server_daemon
。
註冊兩個訊號分別為:SIGKILL
、SIGTERM
,使用 while 迴圈判斷是否需要停止 http_server_daemon
的執行緒,當需要停止時,使用函式 kthread_stop
停止執行緒。接著在 while 迴圈中使用 kernel_accept
接受 client 連線要求,並且在成功建立連線後使用 kthread_run
建立新的執行緒,並且執行函式 http_server_worker
。
以下是 kthread_should_stop
在 <linux/kthread.h>
描述
接受 client 連線要求後,都會執行 http_server_worker
這個 worker thread function,主要執行以下事情:
signal
和 初始化 parser。http_parser_execute
,解讀資料。http_parser
structure這個 http_parser
structure 利用 bit field 來節省空間。
Bit-fields 有說明例子:
unsigned int b : 3
has the range 0..7
signed int b : 3
has the range -4..3
int b : 3
注意這裡的 int
是 implement-defined,也就是說它是有號無號的取決於 compiler 所以可能結果為 0..7
or -4..3
在 http_parser.h
宣告 function pointer 和定義 parser callback function
在 Cprogramming.com 提到
A function pointer is a variable that stores the address of a function that can later be called through that function pointer.
也給了以下例子:
Declare a function pointer as though you were declaring a function, except with a name like *foo instead of just foo:
Initializing
You can get the address of a function simply by naming it:
or by prefixing the name of the function with an ampersand:
在設定 callback function 時,有沒有 &
都是可以的
http_server_recv
接收資料http_parser_execute
解讀資料利用 ftrace 來解讀 http_parser_execute
在做什麼事
tracing_on
: 用於開啟或關閉追蹤功能。當 tracing_on 被設置為 1 時,表示追蹤功能已啟用,ftrace 會開始記錄相關的追蹤事件。而當 tracing_on 被設置為 0 時,表示追蹤功能已禁用,ftrace 將停止記錄追蹤事件。set_graph_function
:函數的作用是在函數圖追蹤中設定僅追蹤特定的函數。current_tracer
: function_graph
的話可以打印出函數調用的關係,更加方便理解。max_graph_depth
: This is the max depth it will trace into a function.執行以下 script,主要透過 wget
自動從網絡下載該文件,傳送 HTTP GET Request:
GET Request :
GET / HTTP/1.1
: 這是請求行,表示要求根目錄 (/) 的資源,並使用 HTTP/1.1 版本。User-Agent: wget/1.20.3 (linux-gnu):
這是用戶代理(User-Agent)標頭欄位,用於識別發出請求的客戶端軟體和版本。在這個例子中,使用的是 wget 工具的版本 1.20.3,運行在 Linux 系統(linux-gnu)上。Accept: */*
: 這是 Accept 標頭欄位,用於告訴服務器客戶端所能接受的回應內容類型。這裡的 */*
表示接受任何類型的回應內容。Accept-Encoding: identity
: 這是 Accept-Encoding 標頭欄位,用於告訴服務器客戶端所支援的內容編碼方式。在這個例子中,只接受 "identity" 編碼,表示不進行任何編碼。Host: localhost:8081
: 這是 Host 標頭欄位,指定請求的目標主機和端口。在這個例子中,請求的目標主機是 localhost,端口是 8081。Connection: Keep-Alive
: 這是 Connection 標頭欄位,用於控制連線的行為。在這個例子中,它指定保持連線(Keep-Alive),表示客戶端希望保持和服務器之間的連線,以便在後續請求中重用。結果輸出:
一開始會先初始化 parser
,最初的 state
就是 HTTP_REQUEST,所以 parser->state
為 s_start_req
接著 http_server_recv
等待 client 傳送資料,等到收到資料後,執行 http_parser_execute()
。
在 http_parser.c
裡有一個 macro CURRENT_STATE
代表 p_state,讓人更好理解意思
接著有一個 for loop 來解析封包,以下列出 case 為 s_start_req
:
根據 ch
來 assign parser->method
,然後更新狀態 p_state
為 s_req_method,接著執行 callback function : http_parser_callback_message_begin
是如何執行 callback function ?
將 message_begin
帶入 FOR
,CALLBACK_NOTIFY(message_begin)
會變成如下:
因為在執行 http_parser_execute
時有傳入 settings
當作 argument 之一,所以在第六行會執行在 http_server.c
所定義的 callback function :
到目前為止,p_state
被更改為 s_req_method,並且執行 http_parser_callback_message_begin callback function,下一次 case 則為 s_req_method
method_strings[parser->method]
XX
是 macro name
(num, name, string)
是 macro 的 parameters#
運算符將 string
參數轉換為一個字串。HTTP_METHOD_MAP(XX)
是另一個 macro 名稱
(XX)
是將 XX
macro 作為參數傳遞給 HTTP_METHOD_MAP
macro。XX
macro 將被展開,並在 HTTP_METHOD_MAP
macro 的展開過程中使用。因此,HTTP_METHOD_MAP(XX)
的展開結果將使用 XX
macro,並在每次展開時將 string
參數轉換為字串,以生成一系列的字串常量。
method_strings
陣列將展開為:
所以 method_strings[parser->method]
為 "GET",此時的 ch
為 E
,parser->index
為 1
,要解析完整的 method
直到遇到 ch
為 ‘ ’
且 matcher[parser->index]
為 '\0'
,才會將 p_state
更改為 s_req_spaces_before_url
,在這過程中 p
會往下一格;parser->index
會 ++
接著 CURRENT_STATE()
為 s_req_spaces_before_url
,執行 parse_url_char
函式,將下列 arguments
傳入parse_url_char
函式
CURRENT_STATE()
:s_req_spaces_before_url
ch
: /
將 p_state
更新為 s_req_path
,所以 case 是 s_req_path
,將 CURRENT_STATE
更新為 s_req_http_start
並且執行 http_parser_callback_request_url callback function
到目前為止 ch
為 ' '
,下一次 ch
為 H
並且 CURRENT_STATE
為 s_req_http_start
,逐步分析 HTTP/1.1\r\n
到目前為止已經將 GET / HTTP/1.1\r\n
header
解析完成,接著分析 header_field
總共有 5 個 header_field,分別是:
在這個 case s_header_field_start
中,會根據第一個字元,也就是
ch
= 'U'
, c
= 'u'
ch
= 'A'
, c
= 'a'
ch
= 'A'
, c
= 'a'
ch
= 'H'
, c
= 'h'
ch
= 'C'
, c
= 'c'
去辨識 parser->header_state
,而下一步則是逐步解析後面的字元,所以會將 CURRENT_STATE()
更新為 s_header_field
已最後 Connection: Keep-Alive 為例,已經判斷 parser->header_state
為 h_connection
,且目前 ch
為 :
,所以將 CURRENT_STATE()
更新為 s_header_value_discard_ws
目前 ch
為 ' '
,所以 case s_header_value_discard_ws
第一個 if
成立,下一次 ch
為 K
,會直接 fall through 到 case s_header_value_start,在這個 case 一開始,就會將 CURRENT_STATE()
更新為 s_header_value
,目的是為了解析後續字串
接著在第 50 行,將 parser->header_state
更新為 h_matching_connection_keep_alive
,最終 parser->header_state
會被更新為 h_connection_keep_alive
,會在 case s_header_value_lws
將 parser->flags
新增 F_CONNECTION_KEEP_ALIVE
整個 message 解析完成,最後會到兩個 case,分別是 s_headers_almost_done
、s_headers_done
,在 case s_headers_done
,會在去判斷 http_should_keep_alive(parser)
,且將 CURRENT_STATE()
更新為 s_dead
,執行 message_complete callback function
最後第12行 if
成立,跳離 while loop
整個程式的主要流程是建立 CMWQ → 連線建立後建立 work → workqueue 開始運作 → 釋放所有記憶體。
在 main.c創造一個專屬的 working queue
,型態為 struct workqueue_struct
首先建立 CMWQ 的部份在掛載模組時執行,位於函式 khttpd_init
,以下為修改的部份:
為了有效管理 work ,所有的 work 都會被加到一個鏈結串列,可在 http_server.h
新增以下結構體:
該結構體的作用是充當鏈結串列的首個節點,成員 is_stopped
用以判斷是否有結束連線的訊號發生。
接著新增 khttpd
結構:
在 http_server.c
中的 http_server_daemon
函式新增 create_work()
來新增 work 時機為 server 和 client 建立連線後。
queue_work()
則是將 work
加入到 http_server_wq
,讓內部去處理 request;在最後結束時呼叫 free_work()
來釋放所有的記憶體。
為了方便與原本 kthread_run
做比較,所以新增一個 CMWQ_MODE
debug mode。
函式 create_work 主要流程為建立 work 所需的空間 → 初始化 work → 將 work 加進鏈結串列裡。
當 work 被調度時,會執行 http_server_worker
函式。
在 http_server_worker
函式中,就只有以下地方不同
執行 ./htstress http://localhost:8081 -t 3 -c 20 -n 200000
,以下為執行結果。
Kthread | CMWQ |
---|---|
50521.661 | 130255.568 |
更改為 CMWQ
,整體 throughput (requests/sec) 提升了 2.5 倍。
handle_directory
主要做下列事情
GET
filp_open
來開啟 User 點選的目錄或者檔案iterate_dir
走訪目錄內的所有資料kernel_read
讀取檔案,並且利用 Chunked transfer encoding 送出資料iterate_dir 如何導向到自己定義的 callback function
Callback function:
由於需要透過 socket 回傳資料,但 iterate_dir
參數是固定,所以將 struct dir_context
加入自 http_request
資料結構
所以在 Callback function,透過 container_of
來找到 struct http_request
起始位址,就可以該request 的 socket descripter
如何得知檔案屬性:在 Linux kernel 裡,檔案的屬性由結構 inode 所管理,位於 include/linux/fs.h ,而這裡主要使用到成員 i_mode 及 i_size ,前者主要表示檔案的類型,後者儲存檔案的大小
接著我們可以利用巨集 S_ISDIR
來判斷是否為目錄
若是檔案類型,使用 kernel_read
讀取到 buffer 當中
還未使用 Chunked transfer encoding
送出目錄資料的時候,一開始都必須先定義好 content 長度
在 HTTP 1.1 中提供了 Chunked encoding 的方法,可以將資料分成一個個的 chunk 並且分批發送,如此一來可以避免要在 HTTP header 中傳送 Content-Length: xx
HTTP headers | Transfer-Encoding 有提到例子:
所以我們將 HTTP_RESPONSE_200_XXX 改成 Transfer-Encoding: chunked
在第 14 行當中,就先傳送長度 %x\r\n
,後面在傳送內容
virtme, crash 安裝參照 測試 Linux 核心的虛擬化環境
在宿主環境下,預先編譯好 kernel module,Makefile 中的 kernel 路徑改成如下
啟動 virtme 虛擬環境,需要在啟動虛擬環境的命令中加入 --qemu-opts -qmp tcp:localhost:4444,server,nowait
這樣的參數
到 khttpd 目錄下,載入 module
維持虛擬環境繼續執行的情況下,我們回到宿主系統,使用 QMP 來與現行 QEMU 環境通訊,擷取目前虛擬環境的 kernel dump。
依照以下的步驟產生 kernel dump
準備好含有 debug symbol 的 vmlinux 以及 kernel dump,就可以開始使用 crash 偵錯
dmesg 可以查看造成 crash 的原因,找到 pid 之後,就可以利用 GDB 來 debug。