# Linux 核心專題: 高度並行的核心模式網頁伺服器 > 執行人: Jordymalone > [解說影片](https://youtu.be/Ac0eOj7EWF0) ### Reviewed by `Andrewtangtang` 可以解釋一下你認為是麼原因導致引入 CMWQ 後可以提升吞吐量與降低延遲嗎? ### Reviewed by [`otischung`](https://github.com/otischung) 除了模組效能本身提升之外,是否能夠測量其所佔用的資源是否有降低?例如 CPU thread 數量、記憶體用量等資訊? ### Reviewed by `leowu0411` 目前專案使用 per-CPU (bound) workqueue,這樣做的優點為可以享有 cache locality;但如果伺服器請求量起伏很大,此模式可能在尖峰期間為每顆 CPU 生成多個 kworker;負載回落後,這些 kworker 變成 idle,是否會造成暫時的資源浪費? 另外,未來若服務需要執行長時間或 CPU-intensive 的 work item,是否更改為 WQ_UNBOUND 由核心從全域的 kworker 池排程能夠為伺服器帶來更大的吞吐量? 想詢問基於伺服器目前的服務性質,哪一種設定更加合適? ### Reviewed by `Max042004` 在 `http_server_daemon` 函式接收客戶端連線,以及 `http_server_worker` 接收客戶端封包,不會在連續未接受到連線或封包時主動釋出 CPU 資源,這樣會不會造成在非尖峰時期的 CPU 忙等。 ## TODO: 進行 ktcp > 依據 [ktcp 作業規範](https://hackmd.io/@sysprog/linux2025-ktcp),提交 CMWQ 以外的 pull request > 完成所有的作業要求 ### 自我檢查清單 - [ ] 研讀〈Linux 核心設計: RCU 同步機制〉並測試相關 Linux 核心模組以理解其用法 * [筆記](https://hackmd.io/PcvOarpOTP22Y6Ve2AlCQA) - [ ] 如何測試網頁伺服器的效能,針對多核處理器場景調整 - [ ] 如何利用 Ftrace 找出 khttpd 核心模組的效能瓶頸,該如何設計相關實驗學習。搭配閱讀《Demystifying the Linux CPU Scheduler》第 6 章 - [ ] 解釋 drop-tcp-socket 核心模組運作原理。TIME-WAIT sockets 又是什麼? * [筆記](https://hackmd.io/KwW96cOiRfScZCTjOeNjGA) - [ ] 參照〈測試 Linux 核心的虛擬化環境〉和〈建構 User-Mode Linux 的實驗環境〉,在原生的 Linux 系統中,利用 UML 或 virtme 建構虛擬化執行環境,搭配 GDB 追蹤 khttpd 核心模組 ### Cppcheck 問題 ```bash $ cppcheck --version Cppcheck 2.13.0 $ git commit -a http_server.h:-1:0: information: Unmatched suppression: unusedStructMember [unmatchedSuppression] ^ nofile:0:0: information: Active checkers: 132/592 (use --checkers-report=<filename> to see details) [checkersReport] Fail to pass static analysis. ``` 執行 pre-commit.hook 會出現以上錯誤,說明 cppcheck 回報這條 suppression 是多餘的,因此觸發了 `unmatchedSuppression` 警告。 > 參考 lab0-c 的 [Commit 0180045](https://github.com/sysprog21/lab0-c/commit/0180045dba46fd4571f48ff10faed7d1872a8ad8),[@komark06](https://hackmd.io/@komark06/SyCUIEYpj#Cppcheck-%E5%9B%9E%E5%A0%B1%E9%8C%AF%E8%AA%A4),[Cppcheck manual](https://cppcheck.sourceforge.io/manual.pdf) 目前是先修改 `pre-commit.hook` 如下 ```diff +CPPCHECK_unmatched= +for f in *.c *.h; do + CPPCHECK_unmatched="$CPPCHECK_unmatched --suppress=unmatchedSuppression:$f" +done # http-parser was taken from Node.js, and we don't tend to validate it. CPPCHECK_suppresses="--suppress=missingIncludeSystem \ --suppress=unusedFunction:http_parser.c \ @@ -8,7 +12,7 @@ CPPCHECK_suppresses="--suppress=missingIncludeSystem \ --suppress=unknownMacro:http_server.c \ --suppress=unusedStructMember:http_parser.h \ --suppress=unusedStructMember:http_server.h" -CPPCHECK_OPTS="-I. --enable=all --error-exitcode=1 --force $CPPCHECK_suppresses" +CPPCHECK_OPTS="-I. --enable=all --error-exitcode=1 --force $CPPCHECK_suppresses $CPPCHECK_unmatched" ``` 一次性地抑制所有可能出現的 unmatchedSuppression 警告。 :::info 目前不清楚 cppcheck 從哪個版本有做相關的更動 是否要以這改動提交 PR? > 已提交 [pull request](https://github.com/sysprog21/khttpd/pull/14) ::: ### 實驗環境 ```bash $ gcc --version gcc (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0 $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 39 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 8 On-line CPU(s) list: 0-7 Vendor ID: GenuineIntel Model name: Intel(R) Xeon(R) CPU E3-1245 v5 @ 3.50GHz CPU family: 6 Model: 94 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 3 CPU(s) scaling MHz: 92% CPU max MHz: 3900.0000 CPU min MHz: 800.0000 BogoMIPS: 6999.82 ``` ### 測試原始專案 編譯 khttpd 專案 ```bash $ make ``` 出現以下 warning :::spoiler ``` CC [M] /home/jordan/linux2025/khttpd/http_parser.o /home/jordan/linux2025/khttpd/http_parser.c: In function ‘http_parser_execute’: /home/jordan/linux2025/khttpd/http_parser.c:1105:16: warning: this statement may fall through [-Wimplicit-fallthrough=] 1105 | if (parser->method == HTTP_SOURCE) { | ^ /home/jordan/linux2025/khttpd/http_parser.c:1110:11: note: here 1110 | default: | ^~~~~~~ /home/jordan/linux2025/khttpd/http_parser.c:1539:23: warning: this statement may fall through [-Wimplicit-fallthrough=] 1539 | h_state = h_content_length_num; | ~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~ /home/jordan/linux2025/khttpd/http_parser.c:1542:13: note: here 1542 | case h_content_length_num: | ^~~~ /home/jordan/linux2025/khttpd/http_parser.c:1854:31: warning: this statement may fall through [-Wimplicit-fallthrough=] 1854 | parser->upgrade = 1; | ~~~~~~~~~~~~~~~~^~~ /home/jordan/linux2025/khttpd/http_parser.c:1857:13: note: here 1857 | case 1: | ^~~~ /home/jordan/linux2025/khttpd/http_parser.c:1401:12: warning: this statement may fall through [-Wimplicit-fallthrough=] 1401 | if (ch == LF) { | ^ /home/jordan/linux2025/khttpd/http_parser.c:1408:7: note: here 1408 | case s_header_value_start: | ^~~~ /home/jordan/linux2025/khttpd/http_parser.c: In function ‘parse_url_char’: /home/jordan/linux2025/khttpd/http_parser.c:548:10: warning: this statement may fall through [-Wimplicit-fallthrough=] 548 | if (ch == '@') { | ^ /home/jordan/linux2025/khttpd/http_parser.c:553:5: note: here 553 | case s_req_server_start: | ^~~~ /home/jordan/linux2025/khttpd/http_parser.c: In function ‘http_parser_parse_url’: /home/jordan/linux2025/khttpd/http_parser.c:2460:18: warning: this statement may fall through [-Wimplicit-fallthrough=] 2460 | found_at = 1; | ~~~~~~~~~^~~ /home/jordan/linux2025/khttpd/http_parser.c:2463:7: note: here 2463 | case s_req_server: | ^~~~ /home/jordan/linux2025/khttpd/http_parser.c: In function ‘http_parse_host_char’: /home/jordan/linux2025/khttpd/http_parser.c:436:59: warning: this statement may fall through [-Wimplicit-fallthrough=] 436 | #define IS_HOST_CHAR(c) (IS_ALPHANUM(c) || (c) == '.' || (c) == '-') | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~ /home/jordan/linux2025/khttpd/http_parser.c:2279:11: note: in expansion of macro ‘IS_HOST_CHAR’ 2279 | if (IS_HOST_CHAR(ch)) { | ^~~~~~~~~~~~ /home/jordan/linux2025/khttpd/http_parser.c:2284:5: note: here 2284 | case s_http_host_v6_end: | ^~~~ /home/jordan/linux2025/khttpd/http_parser.c:2292:10: warning: this statement may fall through [-Wimplicit-fallthrough=] 2292 | if (ch == ']') { | ^ /home/jordan/linux2025/khttpd/http_parser.c:2297:5: note: here 2297 | case s_http_host_v6_start: | ^~~~ /home/jordan/linux2025/khttpd/http_parser.c:2308:10: warning: this statement may fall through [-Wimplicit-fallthrough=] 2308 | if (ch == ']') { | ^ /home/jordan/linux2025/khttpd/http_parser.c:2313:5: note: here 2313 | case s_http_host_v6_zone_start: | ^~~~ ``` ::: > ~~TODO: 待釐清~~ > 已提交 [pull request](https://github.com/sysprog21/khttpd/commit/3413cb3d8ce68c177064ec80e436f86f6e1c8d46) 掛載 khttpd 模組並指定 Port,若不指定預設就是 8081 ```bash $ sudo insmod khttpd.ko port=<Port number> ``` 執行 `wget localhost:8081` ```bash $ wget localhost:8081 --2025-05-24 16:20:58-- http://localhost:8081/ Resolving localhost (localhost)... 127.0.0.1 Connecting to localhost (localhost)|127.0.0.1|:8081... connected. HTTP request sent, awaiting response... 200 OK Length: 12 [text/plain] Saving to: ‘index.html’ index.html 100%[==================================================================================================================================>] 12 --.-KB/s in 0s 2025-05-24 16:20:58 (7.74 MB/s) - ‘index.html’ saved [12/12] ``` 可正常下載並獲得 index.html 的檔案,裡面有 Hello World!!! 表示成功 但透過 dmesg 觀察會發現有以下 error ``` [ 7326.973496] khttpd: http server worker Begin [ 7326.973519] khttpd: requested_url = / [ 7326.974314] khttpd: recv error: -104 [ 7326.974327] khttpd: http server worker Stop ``` > TODO: 待釐清 ### 導入 CMWQ 改寫 khttpd > 參考 [作業描述](https://hackmd.io/@sysprog/linux2025-ktcp/%2F%40sysprog%2Flinux2025-ktcp-c#Concurrency-Managed-Workqueue-CMWQ) > [Commit e70dd99](https://github.com/sysprog21/khttpd/commit/e70dd99344436247ea13b15162f231b56d00fe47) 在導入 CMWQ 之前先使用 htstress 這個 Benchmark Tool 來測試效果。 導入前: ```bash $ ./htstress http://localhost:8081 -t 3 -c 20 -n 200000 requests: 200000 good requests: 200000 [100%] bad requests: 0 [0%] socket errors: 0 [0%] seconds: 13.001 requests/sec: 15383.569 ``` khttpd 最初的設計思路是為每個連線都生成獨立的一條 kthread。 在模組初始化時, ```c static int __init khttpd_init(void) { int err = open_listen_socket(port, backlog, &listen_socket); ... http_server = kthread_run(http_server_daemon, &param, KBUILD_MODNAME); ... return 0; } ``` 可以看到我們會使用 `kthread_run` 啟動一條 daemon thread 來扮演 server 的角色去負責接收連線。同時他會去執行 `http_server_daemon` 這個函式,可以看到以下: :::info `kthread_run` 是個巨集,詳情可參閱 [/include/linux/kthread.h](https://elixir.bootlin.com/linux/v6.12/source/include/linux/kthread.h#L42) ::: ```c int http_server_daemon(void *arg) { ... while (!kthread_should_stop()) { int err = kernel_accept(param->listen_socket, &socket, 0); ... worker = kthread_run(http_server_worker, socket, KBUILD_MODNAME); ... return 0; } ``` 當有 client 被 accept 進來時,一樣透過 `kthread_run` 生成一條獨立的 thread 執行 `http_server_worker` 去處理這個 client 的要求。 參考 [kecho](https://github.com/sysprog21/kecho) 的實作,我們先在 `http_server.h` 中加入: ```diff +#define MODULE_NAME "khttpd" #include <net/sock.h> +#include <linux/workqueue.h> +#include <linux/list.h> struct http_server_param { struct socket *listen_socket; }; +struct httpd_service { + bool is_stopped; + struct list_head head; +}; ``` 新增 `httpd_service` 結構的用意是,我們可以使用鏈結串列來去追蹤 work item 的狀況。 * `is_stopped` 用來記錄是否停止 * `head` 用來記錄 work item 接著,修改 `main.c` 中模組載入時初始化的部分, ```diff static int __init khttpd_init(void) { ... param.listen_socket = listen_socket; + khttpd_wq = alloc_workqueue(MODULE_NAME, 0, 0); + if (!khttpd_wq) { + pr_err("can't create workqueue\n"); + close_listen_socket(listen_socket); + return -ENOMEM; + } http_server = kthread_run(http_server_daemon, &param, KBUILD_MODNAME); ... return 0; } ``` :::info 這邊的 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` 做以下修改: ```diff int http_server_daemon(void *arg) { struct socket *socket; - struct task_struct *worker; struct http_server_param *param = (struct http_server_param *) arg; + struct work_struct *work; ... + INIT_LIST_HEAD(&daemon_list.head); while (!kthread_should_stop()) { ... - worker = kthread_run(http_server_worker, socket, KBUILD_MODNAME); - if (IS_ERR(worker)) { - pr_err("can't create more worker process\n"); + work = create_work(socket); + if (!work) { + pr_err("can't create work for socket\n"); + kernel_sock_shutdown(socket, SHUT_RDWR); + sock_release(socket); continue; } + queue_work(khttpd_wq, work); } + daemon_list.is_stopped = true; + free_work(); + pr_info("http server daemon stopped\n"); + return 0; } ``` 初始化一個全域的 daemon_list,紀錄尚未釋放的 `http_request`,在迴圈當中,每次有新連線 accept 進來,將接收到的 socket 透過 `create_work` 封裝成 `struct http_request`,再使用 queue_work 交由系統的 CMWQ 排程。 同時也對 `http_server_worker` 做適度的修改,詳情請見對應的 Commit。 導入後: ```bash $ ./htstress http://localhost:8081 -t 3 -c 20 -n 200000 requests: 200000 good requests: 200000 [100%] bad requests: 0 [0%] socket errors: 0 [0%] seconds: 7.907 requests/sec: 25293.244 ``` 從上述兩組數據可以看出,在相同的測試條件下,導入 CMWQ 之前和之後: | 指標 | 導入前 | 導入後 | | ------------ | -------- | -------- | | 總耗時(秒) | 13.001 | 7.907 | | 吞吐量(req/sec) | 15383.6 | 25293.244 | * 總耗時下降 * 由 13.001s 降到 7.907 s,降低了約 39.2 %。 * 吞吐量提升 * 從約 15.4 k req/s 提升到約 25.3 k req/s,增幅約 64.3 %。 執行 rmmod 會出現以下訊息,但不影響移除模組 :::spoiler ``` [ 2206.623504] BUG: kernel NULL pointer dereference, address: 0000000000000068 [ 2206.623511] #PF: supervisor read access in kernel mode [ 2206.623514] #PF: error_code(0x0000) - not-present page [ 2206.623516] PGD 0 P4D 0 [ 2206.623520] Oops: Oops: 0000 [#5] PREEMPT SMP PTI [ 2206.623525] CPU: 4 UID: 0 PID: 319077 Comm: khttpd Tainted: G S D OE 6.11.0-26-generic #26~24.04.1-Ubuntu [ 2206.623531] Tainted: [S]=CPU_OUT_OF_SPEC, [D]=DIE, [O]=OOT_MODULE, [E]=UNSIGNED_MODULE [ 2206.623533] Hardware name: ASUSTeK COMPUTER INC. ESC500 G4/P10S WS, BIOS 0801 09/02/2016 [ 2206.623535] RIP: 0010:kernel_sock_shutdown+0xa/0x20 ... [ 2206.623832] note: khttpd[319077] exited with irqs disabled [ 2206.623873] khttpd: module unloaded ``` ::: > 待處理 ### 基本 directory listing 功能 > [Commit 5dac8d7](https://github.com/sysprog21/khttpd/commit/5dac8d7cfcaceae837c389f00a75eef238217dec) Directory listing 是一種伺服器功能,用於在使用者請求目錄而非檔案時,列出該目錄下的檔案清單。 以目前的 khttpd 專案,只會於 `http_server_response` 回傳是否 keep-alive 以及由程式所定義的網頁狀態巨集。因此要實作檔案存取的功能,我們需要修改 response 回傳的資訊,先在 `http_request` 結構中添加 [`dir_context`](https://elixir.bootlin.com/linux/v6.12/source/include/linux/fs.h#L2004), ```diff struct http_request { int complete; struct list_head node; // for list management struct work_struct khttpd_work; // workitem for workqueue + struct dir_context dir_context; }; ``` 修改 `http_server_response`: ```c static int http_server_response(struct http_request *request, int keep_alive) { int ret = handle_directory(request); if (!ret) { pr_info("handle_directory failed\n"); return -1; } return 0; } ``` 並新增了 `handle_directory`,詳細程式碼可參考[作業要求](https://hackmd.io/@sysprog/linux2025-ktcp/%2F%40sysprog%2Flinux2025-ktcp-c#%E5%AF%A6%E4%BD%9C-directory-listing-%E5%8A%9F%E8%83%BD) ```c static bool handle_directory(struct http_request *request) { struct file *fp; char buf[SEND_BUFFER_SIZE] = {0}; ... fp = filp_open("/home/jordan/khttpd/", O_RDONLY | O_DIRECTORY, 0); if (IS_ERR(fp)) { pr_info("Open file failed"); return false; } iterate_dir(fp, &request->dir_context); ... return true; } ``` `iterate_dir` 做了甚麼? > 出自 [/fs/readdir.c](https://elixir.bootlin.com/linux/v6.12/source/fs/readdir.c#L85) 具體來說,`iterate_dir(fp, &request->dir_context);` 會讓核心依序走訪 `fp` 所指向的目錄,並一筆一筆地把每個檔案或子目錄拿出來交給我們事先註冊好的 callback 函式 `tracedir` 處理,但這個回呼函式在被呼叫時,只會收到指向 `dir_context` 的指標。然而,為了要完成任務,我們的回呼函式顯然需要取得完整的 `http_request` 結構。 為了解決這個問題,我們便利用了 `container_of` 這個核心巨集。因為我們在設計上已經將 `dir_context` 放入 `http_request` 之中,`container_of` 就能夠根據這個已知的成員指標,反向計算出其所屬的 `http_request` 結構的起始位址。 新增 `tracedir` 函式: ```c // callback for 'iterate_dir', trace entry. static bool tracedir(struct dir_context *dir_context, const char *name, int namelen, loff_t offset, u64 ino, unsigned int d_type) { if (strcmp(name, ".") && strcmp(name, "..")) { struct http_request *request = container_of(dir_context, struct http_request, dir_context); char buf[SEND_BUFFER_SIZE] = {0}; snprintf(buf, SEND_BUFFER_SIZE, "<tr><td><a href=\"%s\">%s</a></td></tr>\r\n", name, name); http_server_send(request->socket, buf, strlen(buf)); } return true; } ``` 觸發此函式為每看到一筆就呼叫一次,假設目前 `/home/jordan/khttpd/` 目錄中有 5 個檔案,就會呼叫 `tracedir` 5 次,並將他們動態格式化為 HTML 並送回 client 端。 使用 wget 下載 index.html,可以成功看到該目錄下的檔案 ```bash $ wget localhost:8081 --2025-05-24 15:30:33-- http://localhost:8081/ Resolving localhost (localhost)... 127.0.0.1 Connecting to localhost (localhost)|127.0.0.1|:8081... connected. HTTP request sent, awaiting response... 200 OK Length: unspecified [text/html] Saving to: ‘index.html’ index.html [ <=> ] 2.08K --.-KB/s ``` 但此時發現下載的進度條,會卡住沒有動作,發現是 `handle_directory` 中回傳並沒有加上 Content length 所導致,若沒有定義 Content-length 會使接收端難以得知訊息是否傳輸完畢,因此可以看到下方圖片中的左上角是會持續處於載入的狀態。 :::info 後面引入動態計算字數? > 根據作業要求,後續實作 chunked transfer 的機制。 ::: 經過上述修改,即可用以下結果取代原先回傳的 `"Hello World"` 字串: ![image](https://hackmd.io/_uploads/rJyy1jJrxg.png =250x) 但透過 `filp_open` 寫死的路徑若不對就會出錯,缺乏彈性。 #### 掛載模組時指定目錄 > [Commit 481a573](https://github.com/sysprog21/khttpd/commit/481a57387aa2db21b9e2df94185468667f5ff8b3) > 參考 [The Linux Kernel Module Programming Guide](https://sysprog21.github.io/lkmpg/#passing-command-line-arguments-to-a-module) 我採用的方式是透過 `module_param` 供使用者去指定特定路徑。 首先在 `main.c` 定義了 default 的路徑,若使用者沒特別指定就會採用這個預設值 ```c #define DEFAULT_DOC_ROOT "/home/jordan871130/jserv/khttpd/" static char *doc_root = DEFAULT_DOC_ROOT; // Document Root Directory module_param(doc_root, charp, 0444); // user readable MODULE_PARM_DESC(doc_root, "Document root directory"); ``` 從掛載模組得到的參數會傳送給 `http_server_daemon`,以確保在執行時能讀取到正確的值,因此在 main.c 中透過 `extern` 來取得 `daemon_list`,並把 `doc_root` 的值賦給他 `main.c`: ```diff +extern struct httpd_service daemon_list; ``` `httpd_service`: ```diff struct httpd_service { bool is_stopped; + char *root_path; struct list_head head; }; ``` 最後,將 `handle_directory` 中使用路徑的地方,改成從 `daemon_list` 讀取。 ```diff struct http_request { struct socket *socket; @@ -129,14 +129,14 @@ static bool handle_directory(struct http_request *request) "</style></head><body><table>\r\n"); http_server_send(request->socket, buf, strlen(buf)); - fp = filp_open("/home/jordan871130/jserv/khttpd/", O_RDONLY | O_DIRECTORY, - 0); + fp = filp_open(daemon_list.root_path, O_RDONLY | O_DIRECTORY, 0); if (IS_ERR(fp)) { pr_info("Open file failed"); return false; } ... ``` 即可使用以下命令指定特定目錄: ```bash $ sudo insmod khttpd.ko doc_root="/home/jordan871130/" ``` :::info 問題 1: 用瀏覽器訪問 `localhost:8081`,若在執行 rmmod 之前並未把瀏覽器關掉,會導致核心 crashed。 問題 2: 用瀏覽器訪問 `localhost:8081`,會發現它仍然在轉圈,是因為使用 keep-alive ?還是因為我沒定義 Content length? ::: #### 根據指定路徑開啟檔案內容 > [Add file serving and fix directory browsing](https://github.com/Jordymalone/khttpd/commit/169321aa76505ae347ba665bfe6838568eedba5c) 在開啟檔案前,要先判斷目前存取的路徑是目錄還是檔案,參考 [include/uapi/linux/stat.h](https://elixir.bootlin.com/linux/v6.12/source/include/uapi/linux/stat.h#L23),可以看到有 `S_ISDIR` 和 `S_ISREG` 這兩個巨集,前者用來判斷是否為目錄,後者則是判斷是否為一般文件。因此我們要先取得檔案的屬性,例如檔案大小與類型,才能進一步透過巨集去判斷。 Linux 核心以 `struct inode` 來記錄管理這些屬性,這邊我們主要會使用: * `i_mode`: 描述檔案型別和權限的位元欄位 > 摘錄自 [linux/fs.h](https://github.com/torvalds/linux/blob/master/include/linux/fs.h#L674) ```c /* * Keep mostly read-only and often accessed (especially for * the RCU path lookup and 'stat' data) fields at the beginning * of the 'struct inode' */ struct inode { umode_t i_mode; ... } ``` :::info 註解敘述會將大部分 read_only 和常存取的欄位放在 `struct inode` 的開頭,用意是什麼? ::: 實作的部分,於 `http_server_response` 新增判斷邏輯,會根據當前的 `path_info` 去取得其 `inode` 資料,並透過前面提到的巨集來決定要處理目錄還是一般文件。原作業要求是把處理的方式都寫在 `handle_directory`,為了方便維護及可讀性,我新增了專門處理一般文件的函式 `handle_file`。 `handle_file`: ```c static int handle_file(struct http_request *request, const char *path) { struct file *fp; char *file_buf = NULL; loff_t file_size; int ret; fp = filp_open(path, O_RDONLY, 0); if (IS_ERR(fp)) { send_http_header(request->socket, 404, "Not Found", "text/plain", 14, "Close"); http_server_send(request->socket, "404 Not Found.", 14); return -ENOENT; } file_size = i_size_read(file_inode(fp)); file_buf = kmalloc(file_size, GFP_KERNEL); if (!file_buf) { send_http_header(request->socket, 500, "Internal Server Error", "text/plain", 21, "Close"); http_server_send(request->socket, "Internal Server Error", 21); filp_close(fp, NULL); return -ENOMEM; } ret = kernel_read(fp, file_buf, file_size, &fp->f_pos); if (ret < 0) { send_http_header(request->socket, 500, "Internal Server Error", "text/plain", 18, "Close"); http_server_send(request->socket, "File read error.", 18); kfree(file_buf); filp_close(fp, NULL); return ret; } send_http_header(request->socket, 200, "OK", "text/plain", file_size, "Close"); http_server_send(request->socket, file_buf, file_size); kfree(file_buf); filp_close(fp, NULL); return 0; } ``` #### 使用 Chunked transfer encoding 送出目錄資料 > [Implement chunked transfer encoding](https://github.com/sysprog21/khttpd/commit/a5b7fcce284a93d8bfe37e7a0e6a0a63bc6c7cf8) 目前的實作雖採用 HTTP/1.1 協議,但在傳送目錄列表等動態內容時,因無法預先得知 Content-Length,只能在回應結束後強制關閉連線,以告知客戶端傳輸完成。此舉破壞了 HTTP/1.1 的 Keep-Alive 機制,導致每個請求都需重新建立 TCP 連線,降低了傳輸效率。 為解決此問題,我們將引入 Chunked Transfer Encoding。此機制允許伺服器以分塊方式串流傳輸資料,無需在標頭中宣告 Content-Length,從而能完整支援 Keep-Alive 長連線,顯著提升效能。 在實作 Transfer-Encoding: chunked 機制時,需要注意的幾個要點: * 必須省略 Content-length,這個標頭與 `Transfer-Encoding: chunked` 互斥,絕不能同時存在 * 遵循標準分塊格式: 每個分塊都必須是 `長度\r\n數據\r\n` 的格式,其中長度值要使用十六進位的文字字串。 * 發送終止分塊: 所有數據都發送完畢後,必須發送一個格式為 `0\r\n\r\n` 的零長度分塊來結束回應,否則客戶端會一直等待。 以下為範例: ```bash HTTP/1.1 200 OK Content-Type: text/plain Transfer-Encoding: chunked 7\r\n Mozilla\r\n 9\r\n Developer\r\n 7\r\n Network\r\n 0\r\n \r\n ``` 引入之後,可以看到 client 端已不會因為一直等待 server 傳輸內容而轉圈圈了。 ![image](https://hackmd.io/_uploads/rkGtys1Sxx.png =350x) ![image](https://hackmd.io/_uploads/B1Ki1sJrgg.png) #### 使用 MIME 處理不同類型的檔案 > [Add MIME type lookup for static files](https://github.com/Jordymalone/khttpd/commit/6b1d76cb8789d1d4128a68f7820633ef9fbc915d) 目前的程式碼只會回傳 "text/plain",等同於告訴瀏覽器: 「這份位元流請當作一般純文字來顯示」。如果用戶端實際拿到的是 PDF 或 JPEG,結果要不是畫面亂碼,就是瀏覽器為了保護使用者而強制下載檔案。這顯然無法滿足現代 Web 服務對多媒體與互動腳本的需求。因此,只要我們想讓瀏覽器直接在頁面內渲染各種格式 (圖片、CSS、影片、字型...),就必須在 HTTP 回應頭裡正確填入 Content-Type。 `Content-Type` 的值遵循 MIME 規格,所有官方媒體類型都是由 IANA 所維護並登錄,且持續在新增,可參考 [Media Types](https://www.iana.org/assignments/media-types/media-types.xhtml)。 參考作業示範來建立 MIME 表格對應不同類型的檔案,新增了 `mime_type.c` 和 `mime_type.h` 檔案,主要透過函式 `get_mime_type_from_path` 去掃描 MIME 表格,若有匹配及回傳對應的 Content-type。 ```c const char *get_mime_type_from_path(const char *path) { const char *dot = strrchr(path, '.'); const char *ret_type = "text/plain"; int index = 0; if (!dot || dot == path) { pr_info("khttpd: No extension found, defaulting to %s\n", ret_type); return ret_type; } while (mime_types[index].type) { if (strcmp(mime_types[index].type, dot) == 0) { ret_type = mime_types[index].string; break; } index++; } return ret_type; } ``` :::info 目前 `get_mime_type_from_path` 以 while 迴圈對 mime_types[] 做線性掃描,時間複雜度為 $O(N)$ (N = 表格條目數)。當副檔名種類或併發請求量增加時,這段 $O(N)$ 查找可能成為瓶頸。後續可將副檔名映射轉成 hash table (如 rhashtable),把查找成本壓到平均 $O(1)$。 ::: 接著修改 `handle_file` 中關於 Content-type 的處理: ```diff @@ -196,8 +198,9 @@ static int handle_file(struct http_request *request, const char *path) filp_close(fp, NULL); return ret; ,,} + content_type = get_mime_type_from_path(path); - send_http_header(request->socket, 200, "OK", "text/plain", file_size, + send_http_header(request->socket, 200, "OK", content_type, file_size, "Close"); http_server_send(request->socket, file_buf, file_size); ``` 即可於網頁中瀏覽不同類型的檔案, PDF: ![image](https://hackmd.io/_uploads/Syq8JokHex.png) JPG: ![image](https://hackmd.io/_uploads/BkjPJjkHll.png) ### 使用 Ftrace 觀察 khttpd 並改良 ### 支援 HTTP 壓縮,善用 Linux 核心的 crypto API ### 引入 timer 機制以主動關閉逾時連線 ### 以 RCU 結合自訂 lock‑free 資料結構,在並行環境釋放系統資源 ## TODO: 利用核心的 TLS 機制來實作 HTTPS > 善用 [Kernel TLS offload](https://docs.kernel.org/networking/tls-offload.html)