# 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, ¶m, 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, ¶m, 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"` 字串:

但透過 `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 傳輸內容而轉圈圈了。


#### 使用 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:

JPG:

### 使用 Ftrace 觀察 khttpd 並改良
### 支援 HTTP 壓縮,善用 Linux 核心的 crypto API
### 引入 timer 機制以主動關閉逾時連線
### 以 RCU 結合自訂 lock‑free 資料結構,在並行環境釋放系統資源
## TODO: 利用核心的 TLS 機制來實作 HTTPS
> 善用 [Kernel TLS offload](https://docs.kernel.org/networking/tls-offload.html)