執行人: Jordymalone
解說影片
Andrewtangtang
可以解釋一下你認為是麼原因導致引入 CMWQ 後可以提升吞吐量與降低延遲嗎?
otischung
除了模組效能本身提升之外,是否能夠測量其所佔用的資源是否有降低?例如 CPU thread 數量、記憶體用量等資訊?
leowu0411
目前專案使用 per-CPU (bound) workqueue,這樣做的優點為可以享有 cache locality;但如果伺服器請求量起伏很大,此模式可能在尖峰期間為每顆 CPU 生成多個 kworker;負載回落後,這些 kworker 變成 idle,是否會造成暫時的資源浪費?
另外,未來若服務需要執行長時間或 CPU-intensive 的 work item,是否更改為 WQ_UNBOUND 由核心從全域的 kworker 池排程能夠為伺服器帶來更大的吞吐量?
想詢問基於伺服器目前的服務性質,哪一種設定更加合適?
Max042004
在 http_server_daemon
函式接收客戶端連線,以及 http_server_worker
接收客戶端封包,不會在連續未接受到連線或封包時主動釋出 CPU 資源,這樣會不會造成在非尖峰時期的 CPU 忙等。
依據 ktcp 作業規範,提交 CMWQ 以外的 pull request
完成所有的作業要求
執行 pre-commit.hook 會出現以上錯誤,說明 cppcheck 回報這條 suppression 是多餘的,因此觸發了 unmatchedSuppression
警告。
參考 lab0-c 的 Commit 0180045,@komark06,Cppcheck manual
目前是先修改 pre-commit.hook
如下
一次性地抑制所有可能出現的 unmatchedSuppression 警告。
目前不清楚 cppcheck 從哪個版本有做相關的更動
是否要以這改動提交 PR?
已提交 pull request
編譯 khttpd 專案
出現以下 warning
TODO: 待釐清
已提交 pull request
掛載 khttpd 模組並指定 Port,若不指定預設就是 8081
執行 wget localhost:8081
可正常下載並獲得 index.html 的檔案,裡面有 Hello World!!! 表示成功
但透過 dmesg 觀察會發現有以下 error
TODO: 待釐清
在導入 CMWQ 之前先使用 htstress 這個 Benchmark Tool 來測試效果。
導入前:
khttpd 最初的設計思路是為每個連線都生成獨立的一條 kthread。
在模組初始化時,
可以看到我們會使用 kthread_run
啟動一條 daemon thread 來扮演 server 的角色去負責接收連線。同時他會去執行 http_server_daemon
這個函式,可以看到以下:
kthread_run
是個巨集,詳情可參閱 /include/linux/kthread.h
當有 client 被 accept 進來時,一樣透過 kthread_run
生成一條獨立的 thread 執行 http_server_worker
去處理這個 client 的要求。
參考 kecho 的實作,我們先在 http_server.h
中加入:
新增 httpd_service
結構的用意是,我們可以使用鏈結串列來去追蹤 work item 的狀況。
is_stopped
用來記錄是否停止head
用來記錄 work item接著,修改 main.c
中模組載入時初始化的部分,
這邊的 alloc_workqueue 若設定 WQ_UNBOUND 不知道會不會比較好。
透過 alloc_workqueue
建立 workqueue,將後續所有 HTTP 請求封裝為 work item,並交由 kworker 於背景處理。
而主要改動都在 http_server.c
中,新增了 create_work
和 free_work
函式,前者的用途是每當有新連線 accept 成功時,通過 kmalloc
分配一塊 http_request
結構,然後用 INIT_WORK(work->khttpd_work, http_server_worker);
將他的成員 khttpd_work
初始化為一個 work item,綁定 http_server_worker
函式,一旦 kworker 拿到這個 work item 就會自動呼叫 http_server_worker
這個函式。
後者 free_work
則是在模組卸載時走訪鏈結串列上還沒被釋放的 http_request
。
同時我們針對 http_server_daemon
做以下修改:
初始化一個全域的 daemon_list,紀錄尚未釋放的 http_request
,在迴圈當中,每次有新連線 accept 進來,將接收到的 socket 透過 create_work
封裝成 struct http_request
,再使用 queue_work 交由系統的 CMWQ 排程。
同時也對 http_server_worker
做適度的修改,詳情請見對應的 Commit。
導入後:
從上述兩組數據可以看出,在相同的測試條件下,導入 CMWQ 之前和之後:
指標 | 導入前 | 導入後 |
---|---|---|
總耗時(秒) | 13.001 | 7.907 |
吞吐量(req/sec) | 15383.6 | 25293.244 |
執行 rmmod 會出現以下訊息,但不影響移除模組
待處理
Directory listing 是一種伺服器功能,用於在使用者請求目錄而非檔案時,列出該目錄下的檔案清單。
以目前的 khttpd 專案,只會於 http_server_response
回傳是否 keep-alive 以及由程式所定義的網頁狀態巨集。因此要實作檔案存取的功能,我們需要修改 response 回傳的資訊,先在 http_request
結構中添加 dir_context
,
修改 http_server_response
:
並新增了 handle_directory
,詳細程式碼可參考作業要求
iterate_dir
做了甚麼?
具體來說,iterate_dir(fp, &request->dir_context);
會讓核心依序走訪 fp
所指向的目錄,並一筆一筆地把每個檔案或子目錄拿出來交給我們事先註冊好的 callback 函式 tracedir
處理,但這個回呼函式在被呼叫時,只會收到指向 dir_context
的指標。然而,為了要完成任務,我們的回呼函式顯然需要取得完整的 http_request
結構。
為了解決這個問題,我們便利用了 container_of
這個核心巨集。因為我們在設計上已經將 dir_context
放入 http_request
之中,container_of
就能夠根據這個已知的成員指標,反向計算出其所屬的 http_request
結構的起始位址。
新增 tracedir
函式:
觸發此函式為每看到一筆就呼叫一次,假設目前 /home/jordan/khttpd/
目錄中有 5 個檔案,就會呼叫 tracedir
5 次,並將他們動態格式化為 HTML 並送回 client 端。
使用 wget 下載 index.html,可以成功看到該目錄下的檔案
但此時發現下載的進度條,會卡住沒有動作,發現是 handle_directory
中回傳並沒有加上 Content length 所導致,若沒有定義 Content-length 會使接收端難以得知訊息是否傳輸完畢,因此可以看到下方圖片中的左上角是會持續處於載入的狀態。
後面引入動態計算字數?
根據作業要求,後續實作 chunked transfer 的機制。
經過上述修改,即可用以下結果取代原先回傳的 "Hello World"
字串:
但透過 filp_open
寫死的路徑若不對就會出錯,缺乏彈性。
我採用的方式是透過 module_param
供使用者去指定特定路徑。
首先在 main.c
定義了 default 的路徑,若使用者沒特別指定就會採用這個預設值
從掛載模組得到的參數會傳送給 http_server_daemon
,以確保在執行時能讀取到正確的值,因此在 main.c 中透過 extern
來取得 daemon_list
,並把 doc_root
的值賦給他
main.c
:
httpd_service
:
最後,將 handle_directory
中使用路徑的地方,改成從 daemon_list
讀取。
即可使用以下命令指定特定目錄:
問題 1:
用瀏覽器訪問 localhost:8081
,若在執行 rmmod 之前並未把瀏覽器關掉,會導致核心 crashed。
問題 2:
用瀏覽器訪問 localhost:8081
,會發現它仍然在轉圈,是因為使用 keep-alive ?還是因為我沒定義 Content length?
在開啟檔案前,要先判斷目前存取的路徑是目錄還是檔案,參考 include/uapi/linux/stat.h,可以看到有 S_ISDIR
和 S_ISREG
這兩個巨集,前者用來判斷是否為目錄,後者則是判斷是否為一般文件。因此我們要先取得檔案的屬性,例如檔案大小與類型,才能進一步透過巨集去判斷。
Linux 核心以 struct inode
來記錄管理這些屬性,這邊我們主要會使用:
i_mode
: 描述檔案型別和權限的位元欄位摘錄自 linux/fs.h
註解敘述會將大部分 read_only 和常存取的欄位放在 struct inode
的開頭,用意是什麼?
實作的部分,於 http_server_response
新增判斷邏輯,會根據當前的 path_info
去取得其 inode
資料,並透過前面提到的巨集來決定要處理目錄還是一般文件。原作業要求是把處理的方式都寫在 handle_directory
,為了方便維護及可讀性,我新增了專門處理一般文件的函式 handle_file
。
handle_file
:
目前的實作雖採用 HTTP/1.1 協議,但在傳送目錄列表等動態內容時,因無法預先得知 Content-Length,只能在回應結束後強制關閉連線,以告知客戶端傳輸完成。此舉破壞了 HTTP/1.1 的 Keep-Alive 機制,導致每個請求都需重新建立 TCP 連線,降低了傳輸效率。
為解決此問題,我們將引入 Chunked Transfer Encoding。此機制允許伺服器以分塊方式串流傳輸資料,無需在標頭中宣告 Content-Length,從而能完整支援 Keep-Alive 長連線,顯著提升效能。
在實作 Transfer-Encoding: chunked 機制時,需要注意的幾個要點:
Transfer-Encoding: chunked
互斥,絕不能同時存在長度\r\n數據\r\n
的格式,其中長度值要使用十六進位的文字字串。0\r\n\r\n
的零長度分塊來結束回應,否則客戶端會一直等待。以下為範例:
引入之後,可以看到 client 端已不會因為一直等待 server 傳輸內容而轉圈圈了。
目前的程式碼只會回傳 "text/plain",等同於告訴瀏覽器: 「這份位元流請當作一般純文字來顯示」。如果用戶端實際拿到的是 PDF 或 JPEG,結果要不是畫面亂碼,就是瀏覽器為了保護使用者而強制下載檔案。這顯然無法滿足現代 Web 服務對多媒體與互動腳本的需求。因此,只要我們想讓瀏覽器直接在頁面內渲染各種格式 (圖片、CSS、影片、字型…),就必須在 HTTP 回應頭裡正確填入 Content-Type。
Content-Type
的值遵循 MIME 規格,所有官方媒體類型都是由 IANA 所維護並登錄,且持續在新增,可參考 Media Types。
參考作業示範來建立 MIME 表格對應不同類型的檔案,新增了 mime_type.c
和 mime_type.h
檔案,主要透過函式 get_mime_type_from_path
去掃描 MIME 表格,若有匹配及回傳對應的 Content-type。
目前 get_mime_type_from_path
以 while 迴圈對 mime_types[] 做線性掃描,時間複雜度為 (N = 表格條目數)。當副檔名種類或併發請求量增加時,這段 查找可能成為瓶頸。後續可將副檔名映射轉成 hash table (如 rhashtable),把查找成本壓到平均 。
接著修改 handle_file
中關於 Content-type 的處理:
即可於網頁中瀏覽不同類型的檔案,
PDF:
JPG: