:question: 提問清單
依據 ktcp 的指示,持續改進 sysprog21/khttpd 的程式碼,打造出高效且穩定的網頁伺服器。
依據 ktcp 的指示,在 sysprog21/khttpd 的基礎之上,利用 CMWQ 一類的機制,打造出高效且穩定的網頁伺服器,需要處理 lock-free 的資源管理議題 (如 RCU)。
搭配閱讀: 〈RCU Usage In the Linux Kernel: One Decade Later〉
An application can change the IP options on a per-socket basis by calling sys_setsockopt,
which eventually causes the kernel to call setsockopt.
setsockopt sets the new IP options, then uses call_
rcu to asynchronously free the memory storing the old IP
options. Usingcall_rcu
ensures all threads that might
be manipulating the old options will have released their
reference by exiting the RCU critical section.
引入前:
引入 CMWQ:
參考 kecho
的做法, 在 http_server.h
中引入:
在 khttp
中,原本是用 kthread_run
,但在 kecho
中改用 CMWQ
,當有新任務被創建成功時, khttp
是採用 kthread_run
來創建 worker 來處理,但 kecho
是把任務 push 進 CMWQ 中處理
在 main.c
中加入 workqueue
,以便未來處理 request
再來在 http_server.h
中增加新的 struct khttpd_service
,用一個 bool 紀錄是否要停止以及用一個 list_head
來紀錄 work。
接下來,新增 create_work
以及 free_work
,前者的作用是當 listen 到一個新的 request 時,在 kernal kmalloc 分配一段空間給 http_request
結構體,再把 work 放入至 CMWQ 中。
在前面有提到,新增一個結構體 khttpd_service
, 裡面的 bool 是給 http_server_worker
這個函式用的,如果 != is_stopped
,那在 create_work
就會透過 INIT_WORK
這個函式來執行,以下是 INIT_WORK
的定義:
INIT_WORK
可以綁定要執行的函式 http_server_worker,最後將此新建的 work 加入到服務的 worker 鏈結串列中。
引入後:
使用 dmesg
來檢查 kernal 內的錯誤訊息,當 rmmod
時會出現以下錯誤:
TODO: 解決以上 dereference null pointer
錯誤
首先,在 struct http_request
中加入新成員 struct dir_context
加入讀取現行目錄的檔案名稱的函式,如下
可以查閱 include/linux/fs.h 以得知以下結構體的定義。
其中, struct file
, struct dir_context
,iterate_dir funtion
都定義在標頭檔 fs.h
中。
關於 dir_context
結構體, fs.h
中有下列說明
加入自定的 filldir_t
函式,讓 dir_content
的 function pointer
指向它, 這邊要用這種方式的原因是因為 iterate_dir function
的定義是必須傳入一個 pointer to dir_content
Callback function:
為了解決當一遇到函式需要等待,但其他函式又與該等待的函式有關連時,就會使用CallBack Function的時機點來處理。
i.e. 確保程式不會因為 timer 之類的影響執行的順序
對 http_server_response
做修改,只要把 request pass handle_director
就可以了
但這樣做,會出現錯誤,當 insmod
後,會馬上出錯並且馬上 unload module, 但奇怪的是存取 localhost 依然可以取得目錄 content,以下是報錯訊息:
至此,可以顯示 filp_open
開啟的的目錄結構,但只要路徑不對就會出錯,缺乏彈性。
( filp_open
, open file in kernal)
Note:
在一般 user space 時,想要引用外部函式時,使用 #include
就可以引入,但在 keranl 中想要引入外部函式只能使用 EXPORT_SYMBOL
,否則在編譯時會出錯。
掛載模組時,指定要開啟的路徑,使用巨集 module_param_string
新增參數。
module_param_string
可以指定陣列大小(PATH_SIZE
),這樣做的好處是可以設定模組變數的預設值。
以下是更改紀錄:
http_server.h
main.c
這裡使用 extern
的理由是因為 khttpd_service
宣告在 http_server.c
當中,成員當中的 is_stopped
用來告訴 CMWQ 是否要繼續執行。
http_server.c
更改寫死的路徑成 WWWROOT
結果是: 編譯成功,但是需要特別注意使用者輸入必須符合規範,或者針對使用者輸入做後處理,不然容易出現 invalid for parameter WWWROOT
,如下
添加函式來讀取檔案內容,在 kernal space 中開啟檔案使用 filp_open
函式,並且通過前面添加的 WWWROOT
加上 URL 來指定開啟檔案的路徑,並且利用 include/uapi/linux/stat.h 當中定義的 macro 來判斷 filp_open
的類型
新增函式 catstr
,顧名思義將兩個路徑串接起來,使用一個 buffer 來紀錄當前的路徑 pwd
,將 pwd
與 url
串接起來。
修改 tracedir
修改 hadle_directory
,判斷現在開啟的是目錄還是檔案
至此,可以在網頁上看到給定目錄的內容以及檔案內容。
測試 request 效率,因為有 open file 帶來的額外 I/O 開銷,所以 throughput 變少。
HTTP header 是是在請求(request)或回應(response)行(一條訊息的第一行內容)之後傳輸的。協定頭的欄位是以明文的字串格式傳輸,是以冒號分隔的鍵名與鍵值對,以 Enter (CR) 加換行 (LF) 符號序列結尾。
使用 telnet
發送 GET
時,可以看到以下內容:
返回的資訊被 \r\n
隔開,上面是 HTTP header
的內容,下面是 Payload
,也就是 HTML 內容
原本的實作中,每次傳送 header 都要傳入 Content-Length
,而使用 Chunked encoding
就可以不用額外發送 Content-Length
,此方法的好處是當大量資料要傳給 client 時可以不用等整個 response 處理完畢才知道大小。
Note:
macro 中使用 ...
(可變數量參數) 可使 macro 接受不同數量的參數
根據 C11 規格書(6.10.3.1 節),對於可變數量參數的處理方式如下所述:
The identifier
__VA_ARGS__
shall occur only in the replacement list of a function-like macro that uses the ellipsis notation in the parameters.
新增 macro SEND_HTTP_MSG
透過以上 macro 來減少重複的程式碼。
改寫後:
效能:
前面有提到 Content type
是用來表示資源的 media type
,而 MIME 可以標示傳送的資料型態,對於後端來說是可以增加資料正確性的手段。
MIME 是由主要型態(type)、次要型態 (subtype)、參數組成 Type 是廣義分類, subtype 則是資料精確型態,MIME 永遠都有主要型態和次要型態,而後面可以加上參數提供更多細節,如下: type/subtype;parameter=value
在原本的實作中,只會顯示文字檔,只要遇到非文字檔如 .jpg
就無法正確的顯示,所以需要透過提供 MIME 使此類非文字檔正常顯示。
增加標頭檔 mime_type.h
,在此標頭檔中定義不同類型的 MIME,
在增加 MIME 後,在瀏覽器中檢查 http header 可以顯示不同類型的檔案類型。
此函式用來區分檔案名稱及副檔名,再來去定義好的數組中比對是否存在該檔名,若存在就返回對應的 MIME type 。
TODO:
上面的方式有待改進,因為每次收到 request 後都進行 linear search,即便比對數量不多,但勢必會影響效能,可改用 hash table
的方式來改善搜尋速度。
HTTP 採用 請求 request
-回應 response
模式,非 KeepAlive
模式時,每個請求/應答客戶和伺服器都要新建一個連接,完成 之後立即斷開連接(HTTP 爲 stateless 的通訊協定);當使用 Keep-Alive
模式(又稱持久連接、連接重用)時, Keep-Alive
功能使客戶端到伺服器端的連接持續有效,當出現對伺服器的後繼請求時, Keep-Alive
功能避免了建立或者重新建立連接。
保持閒置連線的優點是:
Note:
關閉連線時可以採用 graceful shutdown
,避免尚有資料在傳輸給某個 client 時就馬上被關閉。
以 ftrace 檢視 kHTTPd 運作的細節,檢視 I/O 模型並尋求效能改進空間
先確認目前的系統是否有 ftrace
確認系統內有 ftrace 後,找出 khttpd
內所有可以被追蹤的函式
確認後,參考 鳥哥私房菜,並且參考 《Demystifying the Linux CPU Scheduler》第六章
撰寫 shell script
。
echo function_graph > $TRACE_DIR/current_tracer
是把 function_graph
設成當前的 tracer (其他 tracer 可參考 Ftrace)
echo http_server_worker > $TRACE_DIR/set_graph_function
是把 http_server_worker
設成要 trace 的函式。
節錄部份結果:
可以看到花的大部分時間都是在 iterate_dir
函式中
iterate_dir
函式參考 Jerejere0808 以及 terry23304,為了避免請求時都要執行一次 iterate_dir()
,可以將 response 給快取下來,如果下次有同樣的請求時可以避免在執行一次 iterate_dir()
,並加入 RCU 同步機制,以支援多個端同時讀取存取快取的需求(允許多個 reader (port) 在單一 writer (cache) 更新資料的同時,在不需要 lock 的前提正確讀取資料)。
在 Linux 核心中,RCU 用來保護以下場景:
使用 hashtable
來實作 content cache
增加 content_cache.h
來實作 content cache
在 content_cache.c
中實作 content_cache
的功能
在收到新的 request 後,會先檢查該 request 存在於 hashtable
中,如果存在那就返回,反之插入 hashtable
當中
需要注意的是當插入 hashtable
時,需要保證資料的正確性,避免 reader 讀取錯誤資料,故在存取該結構體時,需要使用到結構體內的成員 lock
。
定義好 hashtable
後,之後在存取之前都會先檢查該 request 的 url 是否存在裡面,如果有便可以把 cache 的回應返回,反之如果不存在就插入 table 內。
加入 content cache 後,重新測試效能,由於每一測試皆是訪問同一個 url,故改善了 requests/sec ,並且隨著 WWWROOT
的改變,由於要讀取的 file 數目不同,request 也會有所改變
request/sec 由 12482.545 上升到了 23482.116 。