## Linux 核心專題: 高效網頁伺服器 > 執行人: yqt2000 > [解說影片](https://youtu.be/IrM6YIt7Z-0) ## 任務說明 依據 [ktcp](https://hackmd.io/@sysprog/linux2024-ktcp) 作業規範,開發高效網頁伺服器 (針對靜態)。 ## TODO: 回答「自我檢查清單」的所有問題 > 需要附上對應的參考資料和必要的程式碼,以第一手材料 (包含自己設計的實驗) 為佳 - [ ] 研讀〈[Linux 核心設計: RCU 同步機制](https://hackmd.io/@sysprog/linux-rcu)〉並測試相關 Linux 核心模組以理解其用法 - [ ] 如何測試網頁伺服器的效能,針對多核處理器場景調整 - [ ] 如何利用 [Ftrace](https://docs.kernel.org/trace/ftrace.html) 找出 `khttpd` 核心模組的效能瓶頸,該如何設計相關實驗學習。搭配閱讀《Demystifying the Linux CPU Scheduler》第 6 章 - [ ] 解釋 `drop-tcp-socket` 核心模組運作原理。`TIME-WAIT` sockets 又是什麼? > 待整理 [eric88525](https://hackmd.io/@eric88525/linux2022-ktcp#drop-tcp-socket-%E6%A0%B8%E5%BF%83%E6%A8%A1%E7%B5%84%E9%81%8B%E4%BD%9C%E5%8E%9F%E7%90%86), [zoanana990](https://hackmd.io/@zoanana990/linux2022-ktcp#-%E8%87%AA%E6%88%91%E6%AA%A2%E6%9F%A5%E6%B8%85%E5%96%AE) 筆記內容 - [ ] 研讀 [透過 eBPF 觀察作業系統行為](https://hackmd.io/@sysprog/linux-ebpf),如何用 eBPF 測量 kthread / CMWQ 關鍵操作的執行成本? > 參照 [eBPF 教程](https://github.com/eunomia-bpf/bpf-developer-tutorial) - [ ] 參照〈[測試 Linux 核心的虛擬化環境](https://hackmd.io/@sysprog/linux-virtme)〉和〈[建構 User-Mode Linux 的實驗環境](https://hackmd.io/@sysprog/user-mode-linux-env)〉,在原生的 Linux 系統中,利用 UML 或 `virtme` 建構虛擬化執行環境,搭配 GDB 追蹤 `khttpd` 核心模組 ### 研讀〈[Linux 核心設計: RCU 同步機制](https://hackmd.io/@sysprog/linux-rcu)〉並測試相關 Linux 核心模組以理解其用法 #### 簡記 [Linux 核心設計: 淺談同步機制](https://hackmd.io/@sysprog/linux-sync#seqlock) 隨著電腦硬體逐漸提供 atomic 指令後,mutex 或稱為 lock 的機制被列入作業系統的實作考量: * 需要進入 CS 時, 用 mutex/lock —— 上鎖/解鎖永遠是同一個 thread/process; * 要處理 signalling 時,用 semaphore —— 等待/通知通常是不同的 threads/processes; <s>簡言之,要搶資源時用 mutex,要互相通知時用 semaphore。</s> :::danger 上方說法過於武斷,避免這樣的「簡言之」。 工程人員說話要精準。 ::: mutex 與 semaphore 的差別在於: * process 使用 mutex 時,process 的運作是持有 mutex,執行 CS 來存取資源,然後釋放 mutex * Mutex 就像是資源的一把鎖:解鈴還須繫鈴人 * process 使用 semaphore 時,process 總是發出信號 (signal),或者總是接收信號 (wait),同一個 process 不會先後進行 signal 與 wait * 換言之,process 要不擔任 producer,要不充當 consumer 的角色,不能兩者都是。semaphore 是為了保護 process 的執行同步正確; #### 簡記〈[Linux 核心設計: RCU 同步機制](https://hackmd.io/@sysprog/linux-rcu)〉 RCU 適用於頻繁的讀取 (即多個 reader)、但資料寫入 (即少量的 updater/writer) 卻不頻繁的情境,例如檔案系統,經常需要搜尋特定目錄,但對目錄的修改卻相對少,這就是 RCU 理想應用場景。 RCU 藉由 lock-free 程式設計滿足以下場景的同步需求: * 頻繁的讀取,不頻繁的寫入 * 對資料沒有 strong consistency 需求 即使存取舊的資料,不會影響最終行為的正確,這樣的情境就適合 RCU,對其他網路操作也有相似的考量。 ## 實驗環境 ```shell $ gcc --version gcc (Ubuntu 13.2.0-23ubuntu4) 13.2.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: 11th Gen Intel(R) Core(TM) i5-1135G7 @ 2.40GHz CPU family: 6 Model: 140 Thread(s) per core: 2 Core(s) per socket: 4 Socket(s): 1 Stepping: 1 CPU(s) scaling MHz: 39% CPU max MHz: 4200.0000 CPU min MHz: 400.0000 BogoMIPS: 4838.40 ``` ## TODO: 引入 [Concurrency Managed Workqueue](https://www.kernel.org/doc/html/v4.15/core-api/workqueue.html) (cmwq) > 改寫 kHTTPd,分析效能表現和提出改進方案,可參考 [kecho](https://github.com/sysprog21/kecho) 參考 [YiChianLin](https://hackmd.io/@YiChianLin/linux2022-ktcp#CMWQ-Concurrency-Managed-Workqueue%E4%BB%8B%E7%B4%B9), [oscarshiang](https://hackmd.io/@oscarshiang/linux_kecho#%E4%BD%BF%E7%94%A8-CMWQ-%E7%9A%84%E5%84%AA%E5%8B%A2) ### CMWQ 簡介 CMWQ 是對 Linux 傳統 Workqueue 機制的改進和重新實現。它保持了原有 API 的<s>兼容</s> 性,同時引入了更智能的並行管理和資源利用策略。 :::danger 注意用語! ::: **一般 workqueue:** ![20151217154804828](https://hackmd.io/_uploads/Bkv2eq9IC.jpg) **CMWQ架構圖:** ![YJV2n8h](https://hackmd.io/_uploads/BkJUlc5LR.png) #### CMWQ 與一般 Workqueue 的差別: || CMWQ | 一般workqueue | | -------- | -------- | -------- | | 執行緒管理 | 使用統一的 Worker pools,由所有 workqueue 共享,更有效利用資源 | Multi Thread 模式下每個 CPU 有一個 worker,Single Thread 模式下整個系統只有一個 worker | | 並行處理 | 可以動態調整並行級別,更靈活的進行管理 | Multi Thread 可能因 I/O blocking 導致其他 worker 等待;Single Thread 可能造成死鎖 | | 資源利用 | 通過共享 worker pools 和動態調整,大幅減少資源浪費 | 容易造成系統資源浪費,特別是在 Multi Thread 模式下 | #### CMWQ 設計方式: 引入了 work item 的概念,為一個簡單的結構包含函式指標指向非同步任務運作的函式,執行時建立一個 work item 和(放入) workqueue。而處理這些任務的執行緒為 worker threads 用於一個接一個處理這些任務,而在任務結束後 thread 會變為 idle 狀態,而 worker-pools 就是用來管理這些 threads 兩種 worker-pools 類型: 1. Bound 類型:綁定特定的 CPU,使管理的 worker 執行在指定的 CPU 上執行,而每個 CPU 中會有兩個 worker-pools 一個為高優先級的,另一個給普通優先級的,透過不同的 flags 影響 workqueue 的執行優先度 3. Unbound 類型:thread pool 用於處理不綁定特定 CPU,其 thread pool 是動態變化,透過設定 workqueue 的屬性建立對應的 worker-pools 分離 Workqueue 和 Worker-pools:使用者只需關注將任務放入 queue,不需考慮執行細節。 #### CMWQ 的優勢及為什麼適用於該專案: :::danger 注意用語! * concurrency 是「並行」 * file 是「檔案」,不是「文件」 * 不要參照品質低劣的簡體中文網頁,避免用 ChatGPT,後者已嚴重受簡體中文扭曲 務必詳閱 https://hackmd.io/@sysprog/it-vocabulary 和 https://hackmd.io/@l10n-tw/glossaries ::: * 網頁伺服器需要同時處理大量請求。CMWQ 通過共享 worker pools 和根據負載動態調整執行緒數量,可以滿足高並行的需求和大幅減少一般 workqueue 資源浪費的問題。 * 另外網頁伺服器可能需要處理不同優先級的請求。CMWQ 提供了高優先級和普通優先級的 worker pools,可以更好地處理不同類型的 HTTP 請求。 * 長時間任務處理:對於可能耗時較長的 HTTP 請求(如大檔案傳輸),CMWQ 可以動態建立新執行緒並分配給其他 CPU 執行,避免阻塞其他請求的處理。 * 跨 CPU 執行能力:CMWQ 的 Unbound 類型 worker pools 允許任務在不同 CPU 間切換,這可以提高多核系統上網頁伺服器的整體性能。 #### 使用方式 為了在核心模組中引入 CMWQ,我們會需要使用到 `<linux/workqueue.h>` 中的這些函式: 1. alloc_workqueue : 在初始化模組時用來建立一個 CMWQ 2. destroy_workqueue : 用來釋放 workqueue 3. queue_work : 將 work 放入 workqueue 中排程 4. INIT_WORK : 用以初始化 work ### 引入 CMWQ 至 khttpd 參考了 [kecho](https://github.com/sysprog21/kecho) 的作法,以及 [fatcatorange](https://hackmd.io/@sysprog/HkyVuh0NR#TODO-%E5%BC%95%E5%85%A5-CMWQ-%E6%94%B9%E5%AF%AB-kHTTPd),[YiChianLin](https://hackmd.io/@YiChianLin/linux2022-ktcp#%E5%BC%95%E5%85%A5-CMWQ-%E8%87%B3-khttp) 的筆記 這部份我的 commit: [fde4de](https://github.com/sysprog21/khttpd/commit/fde4de66ceaa93882622a63cb1fc984d22ad7a34), [e593125](https://github.com/sysprog21/khttpd/commit/e5931255ade230198d0cddb23be173f460052eae) :::danger 程式碼註解不該出現中文!總是用美式英語書寫。 改進 git commit message,參照第一次作業的規範。 ::: #### `main.c` 宣告 workqueue ,並在使用 workqueue 相關 API 時 include [workqueue.h](https://elixir.bootlin.com/linux/latest/source/include/linux/workqueue.h) * workqueue_struct 定義於 [workqueue.c](https://elixir.bootlin.com/linux/latest/source/kernel/workqueue.c#L257) 中,裡面有 CMWQ 文件中所提及的 unbound_attrs 可以設定 workqueue 在 unbound 條件下的屬性; * alloc_workqueue 定義於 [workqueue.c](https://elixir.bootlin.com/linux/latest/source/kernel/workqueue.c#L257) 中,初始化一個 CMWQ 並在 flag 的設定為 WQ_UNBOUND 表示不會被特定的 CPU 所限制,使資源不會被閒置,可以透過切換的方式執行未完成的任務 ```diff ... + #include <linux/workqueue.h> ... + struct workqueue_struct *khttpd_wq; ... static int __init khttpd_init(void) { ... param.listen_socket = listen_socket; + khttpd_wq = alloc_workqueue("khttp_wq", WQ_UNBOUND, 0); ... } ``` #### `http_server.h` khttpd_service 採用雙向鏈結串列作為 workqueue 管理 work ```c struct khttpd_service { bool is_stopped; struct list_head head; //workqueue }; extern struct khttpd_service daemon_list; ``` #### `http_server.c` 請求 - 以 http_request 作為其結構體,嵌入 list_head 進行管理,而 work_struct 為真正要作的任務,可被 `INIT_WORK` 函式初始化,去運行客製化的函式( e.g. 此專案中處理請求的函式 `http_server_worker`) ```diff struct http_request { struct socket *socket; enum http_method method; char request_url[128]; int complete; + struct list_head node; + struct work_struct khttpd_work; }; ``` ##### http_server_daemon() * 第 37 行(work = create_work(socket);) 為每一個連線的請求建立一個 work 進行處理 * queue_work() 將 work 放入 CMWQ 中進行排程 :::danger 注意書寫規範: * 使用 lab0 規範的程式碼書寫風格,務必用 clang-format 確認一致 ::: ```diff= + #include <linux/workqueue.h> + struct khttpd_service daemon_list = {.is_stopped = false}; + extern struct workqueue_struct *khttpd_wq; // set up workqueue ... int http_server_daemon(void *arg) { - struct task_struct *worker; ... + // CMWQ + struct work_struct *work; + if (!khttpd_wq) + return -ENOMEM; + // initial workqueue head + INIT_LIST_HEAD(&daemon_list.head); ... // 判斷執行緒是否該被中止 while (!kthread_should_stop()) { int err = kernel_accept(param->listen_socket, &socket, 0); // 接受 client 連線要求 if (err < 0) { // 檢查目前執行緒是否有 signal 發生 if (signal_pending(current)) break; pr_err("kernel_accept() error: %d\n", err); continue; } - // 建立新的執行緒並且執行函式 http_server_worker - worker = kthread_run(http_server_worker, socket, KBUILD_MODNAME); - if (IS_ERR(worker)) { - pr_err("can't create more worker process\n"); - continue; - } + // CMWQ 為每一個連線的請求建立一個 work 進行處理 + if (unlikely(!(work = create_work(socket)))) { + pr_err("can't create work\n"); + continue; + } + // 而建立出來的 work 會由 os 配置 worker 執行, + // 配置後由 khttpd_wq 將每一個 work 用 list_head 的 linked list + // 進行管理, 使用到 queue_work() 將 work 放入 workqueue 中 + queue_work(khttpd_wq, work); + } + daemon_list.is_stopped = true; + // free work and workqueue + free_work(); + destroy_workqueue(khttp_wq); return 0; } ``` :::danger 注意書寫規範: * 程式碼註解不該出現中文,總是用美式英語書寫 ::: ##### `create_work()` & `free_work()` 在 create_work 中,根據傳入的 socket 建立一個 work,為每一個連線請求進行 kernel space (kmalloc) 的動態記憶體配置,並進行初始化,再透過 list_add 加入到 workqueue 當中 ```c // ref : kecho create_work static struct work_struct *create_work(struct socket *sk) { struct http_request *work; if (!(work = kmalloc(sizeof(struct http_request), GFP_KERNEL))) return NULL; work->socket = sk; // 初始化已經建立的 work ,並運行函式 http_server_worker INIT_WORK(&work->khttpd_work, http_server_worker); list_add(&work->node, &daemon_list.head); return &work->khttpd_work; } ``` :::danger 注意書寫規範: * 程式碼註解不該出現中文,總是用美式英語書寫 ::: * free_work():用於釋放掉所建立連線的所配置的記憶體空間,使用 list_for_each_entry_safe 巨集走訪每一個在 list 中所管理的 work * kernel_sock_shutdown(): 斷開 socket 的連線(包含傳送與接收的功能),對應的巨集 SHUT_RDWR 關閉方式 * flush_work():等待目前的的 work 執行完畢 * sock_release(): 根據文件的註解,將 socket 釋放在 stack,也會斷開對應連接的 fd * kfree():釋放掉從 kmalloc 所配置出的記憶體空間 ```c static void free_work(void) { struct http_request *tmp, *target; /* cppcheck-suppress uninitvar */ // list : member list_for_each_entry_safe (target, tmp, &daemon_list.head, node) { kernel_sock_shutdown(target->socket, SHUT_RDWR); flush_work(&target->khttpd_work); sock_release(target->socket); kfree(target); } } ``` ##### http_server_worker() 使用 workqueue 時,程式執行時就是傳入一個 struct work_struct ,因此可透過 container_of 取得請求的 socket ```diff= - static int http_server_worker(void *arg) + static void http_server_worker(struct work_struct *work){ ... - struct socket *socket = (struct socket *) arg; + // http_request->socket, 透過 container_of 找到 http_request 中的 socket + struct socket *socket = + container_of(work, struct http_request, khttpd_work)->socket; ... } ``` :::danger 注意書寫規範: * 程式碼註解不該出現中文,總是用美式英語書寫 ::: ### 引入 CMWQ 之前,khttpd 與其他網路伺服器相比,ab 測試的結果 100000 requests with 500 requests at a time(concurrency) `ab -n 100000 -c 500 -k http://127.0.0.1:8081/` | | Requests per second | Time per request (mean, across all concurrent requests)| | -------- | -------- | -------- | | khttpd | 185278.85 #/sec | 0.005 ms | | kecho | 136125.17 #/sec | 0.007 ms | | cserv | 42542.40 #/sec | 0.024 ms | ### 比較 khttpd 在引入 CMWQ 之後的結果 make check 運行腳本進行測試,htstress 作為客戶端發送請求給伺服器,比較單純使用 kthread_run 和引入 CMWQ 後伺服器處理 200000 筆的請求的表現。 `./htstress http://localhost:8081 -t 3 -c 20 -n 200000` * -n : 表示對 server 請求連線的數量 * -c : 表示總體對 server 的連線數量 * -t : 表示使用多少執行緒 | | 引入 CMWQ 之前| 引入 CMWQ 之後| | -------- | -------- | -------- | |requests:| 200000 | 200000 | | good requests: | 200000 [100%] | 200000 [100%] | | bad requests: | 0 [0%]| 0 [0%] | | socket errors: | 0 [0%] | 0 [0%] | | seconds: | 3.354 | 2.003 | | requests/sec: | 59624.124 | 99860.744 | 可以看到處理 200000 筆請求的整體時間降低了不少,另外每秒可處理的請求個數也提昇了約 1.6倍。 ## TODO: 提供目錄檔案存取功能,提供基本的 [directory listing](https://cwiki.apache.org/confluence/display/httpd/DirectoryListings) 功能 按照[作業 kecho + khttpd 實作 directory listening 的步驟](https://hackmd.io/@sysprog/linux2024-ktcp/%2F%40sysprog%2Flinux2024-ktcp-c#%E5%AF%A6%E4%BD%9C-directory-listing-%E5%8A%9F%E8%83%BD) 以及比照 [fatcatorange同學筆記內容](https://hackmd.io/@sysprog/HkyVuh0NR#TODO-%E6%8F%90%E4%BE%9B%E7%9B%AE%E9%8C%84%E6%AA%94%E6%A1%88%E5%AD%98%E5%8F%96%E5%8A%9F%E8%83%BD) 修正缺失的方法去實作該功能,以下為整理兩者筆記並融合我在實作遇上的問題和解決方法。 ### 修改 `http_server_response` 要加入這個功能,要修改 `http_server_response` ,原本的 `http_server_response` 只會檢查是不是用 get ,是的話回傳一個 HTTP_RESPONSE_200 (代表成功) ,內容是 hello world。 而這個函式被使用在 `http_server.c/http_parser_callback_message_complete()`: ```c static int http_parser_callback_message_complete(http_parser *parser) { struct http_request *request = parser->data; http_server_response(request, http_should_keep_alive(parser)); request->complete = 1; return 0; } ``` 上述函式被綁定在 `http_server.c/http_server_worker()`: ```c struct http_parser_settings setting = { .on_message_begin = http_parser_callback_message_begin, .on_url = http_parser_callback_request_url, .on_header_field = http_parser_callback_header_field, .on_header_value = http_parser_callback_header_value, .on_headers_complete = http_parser_callback_headers_complete, .on_body = http_parser_callback_body, .on_message_complete = http_parser_callback_message_complete}; ``` 當 `http_server_worker()` 執行 `http_parser_execute()` 時,就會根據解析執行對應的函式,以這個例子來說,解析完整個 http 請求時執行 `http_parser_callback_message_complete()` 正式修改 `http_server.c/http_server_response()`: ```diff static int http_server_response(struct http_request *request, int keep_alive) { - char const *response; - pr_info("requested_url = %s\n", request->request_url); - if (request->method != HTTP_GET) - response = keep_alive ? HTTP_RESPONSE_501_KEEPALIVE : HTTP_RESPONSE_501; - else - response = keep_alive ? HTTP_RESPONSE_200_KEEPALIVE_DUMMY - : HTTP_RESPONSE_200_DUMMY; - http_server_send(request->socket, response, strlen(response)); + pr_info("khttpd: requested_url = %s\n", request->request_url); + if (handle_directory(request) == 0) + kernel_sock_shutdown(request->socket, SHUT_RDWR); return 0; } ``` ### 實作目錄檔案存取功能 `http_server.c/handle_directory()` `handle_directory()` 對於不同的 case 做出回應,主要執行以下步驟: 1. 檢查請求屬性是否為 GET ,以及開啟請求的 url 檔案 2. case: IS_ERR - url 檔案無法開啟 => 404 Not Found 3. case: S_ISDIR - url 屬於目錄 => 繼續對迭代目錄 4. case: S_ISREG - url 屬於檔案 => 讀取檔案,印出內容 其中迭代目錄(`tracedir`, `iterate_dir`)為該方法的精隨,於下面內容繼續介紹 ```c static bool handle_directory(struct http_request *request) { struct file *fp; char pwd[BUFFER_SIZE] = {0}; request->dir_context.actor = tracedir; // request is not GET => 501 Not Implemented if (request->method != HTTP_GET) { send_http_header(request->socket, HTTP_STATUS_NOT_IMPLEMENTED, http_status_str(HTTP_STATUS_NOT_IMPLEMENTED), "text/plain", 19, "close"); send_http_content(request->socket, "501 Not Implemented"); return false; } catstr(pwd, daemon_list.path, request->request_url); printk("%s\n", pwd); fp = filp_open(pwd, O_RDONLY, 0); // case : link is not found if (IS_ERR(fp)) { send_http_header(request->socket, HTTP_STATUS_NOT_FOUND, http_status_str(HTTP_STATUS_NOT_FOUND), "text/plain", 14, "close"); send_http_content(request->socket, "404 Not Found"); kernel_sock_shutdown(request->socket, SHUT_RDWR); return false; } //case: link is directory => iterate_dir if (S_ISDIR(fp->f_inode->i_mode)) { char buf[SEND_BUFFER_SIZE] = {0}; snprintf(buf, SEND_BUFFER_SIZE, "HTTP/1.1 200 OK\r\n%s%s%s", "Connection: Keep-Alive\r\n", "Content-Type: text/html\r\n", "Keep-Alive: timeout=5, max=1000\r\n\r\n"); http_server_send(request->socket, buf, strlen(buf)); snprintf(buf, SEND_BUFFER_SIZE, "%s%s%s%s", "<html><head><style>\r\n", "body{font-family: monospace; font-size: 15px;}\r\n", "td {padding: 1.5px 6px;}\r\n", "</style></head><body><table>\r\n"); http_server_send(request->socket, buf, strlen(buf)); iterate_dir(fp, &request->dir_context); snprintf(buf, SEND_BUFFER_SIZE, "</table></body></html>\r\n"); http_server_send(request->socket, buf, strlen(buf)); // case: link is file => readfile } else if (S_ISREG(fp->f_inode->i_mode)) { char *read_data = kmalloc(fp->f_inode->i_size, GFP_KERNEL); int ret = read_file(fp, read_data); send_http_header(request->socket, HTTP_STATUS_OK, http_status_str(HTTP_STATUS_OK), "text/plain", ret, "Close"); http_server_send(request->socket, read_data, ret); kfree(read_data); } kernel_sock_shutdown(request->socket, SHUT_RDWR); filp_close(fp, NULL); return true; } ``` `iterate_dir(struct file *, struct dir_context *);` 定義於 [<linux/fs.h>](https://elixir.bootlin.com/linux/v5.10.180/source/include/linux/fs.h#L3135) 可對目錄`struct file*` 進行迭代並運行 `struct dir_context *` 設置的 callback function。 因此首先須在 `struct http_request` 中加入 `struct dir_context` ```diff= + #include <linux/fs.h> ... + #define SEND_BUFFER_SIZE 256 + #define BUFFER_SIZE 256 ... struct http_request { struct socket *socket; enum http_method method; char request_url[128]; int complete; struct list_head node; struct work_struct khttpd_work; + // struct dir_context, defines in fs.h + struct dir_context dir_context; }; ``` 而 `http_server.c/trace_dir()` 可以回傳給客戶端當前目錄下的資料夾連結或檔案連結,並將其印出顯示在 localhost 的頁面上, 使得使用者可以藉由點擊目錄進入更深層的目錄或存取檔案 ```c 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}; char *des = kmalloc(strlen(request->request_url) + strlen(name) + 2, GFP_KERNEL); if (strcmp(request->request_url, "/") != 0) { strncpy(des, request->request_url, strlen(request->request_url)); strcat(des, "/"); strcat(des, name); } else { strncpy(des, name, strlen(name)); } snprintf(buf, SEND_BUFFER_SIZE, "<tr><td><a href=\"%s\">%s</a></td></tr>\r\n", des, name); pr_info("khttpd: %s\n", buf); http_server_send(request->socket, buf, strlen(buf)); } return 1; } ``` ### 指定開啟目錄的路徑 `main.c` ```diff ... + static char WWWROOT[PATH_LENGTH] = {0}; + module_param_string(WWWROOT, WWWROOT, PATH_LENGTH, 0); ... static int __init khttpd_init(void) { ... + if (!*WWWROOT) // prevent empty input from user + WWWROOT[0] = '/'; + daemon_list.path = WWWROOT; ... } ``` `http_server.h` ```diff struct khttpd_service { bool is_stopped; + char *path; struct list_head head; // workqueue }; ``` 即可透過 module_param ,在載入模組時指定路徑,e.g. ```shell $ sudo insmod khttpd.ko WWWROOT='"/home/oldwustd1/linux2024"' ``` ### 其餘實作細節 `http_server.c` 中的 `send_http_header()` & `send_http_content()` & `catstr()` & `read_file()` ```c static void send_http_header(struct socket *socket, int status, const char *status_msg, char const *type, int length, char const *conn_msg) { char buf[SEND_BUFFER_SIZE] = {0}; snprintf(buf, SEND_BUFFER_SIZE, "HTTP/1.1 %d %s\r\n \ Content-Type: %s\r\n \ Content-Length: %d\r\n \ Connection: %s\r\n\r\n", status, status_msg, type, length, conn_msg); http_server_send(socket, buf, strlen(buf)); } static void send_http_content(struct socket *socket, char const *content) { char buf[SEND_BUFFER_SIZE] = {0}; snprintf(buf, SEND_BUFFER_SIZE, "%s\r\n", content); http_server_send(socket, buf, strlen(buf)); } // concatenate string static void catstr(char *res, char const *first, char const *second) { int first_size = strlen(first); int second_size = strlen(second); memset(res, 0, BUFFER_SIZE); memcpy(res, first, first_size); memcpy(res + first_size, second, second_size); } static inline int read_file(struct file *fp, char *buf) { return kernel_read(fp, buf, fp->f_inode->i_size, 0); } ``` ### 成果展示 ![螢幕快照 2024-06-27 23-57-27](https://hackmd.io/_uploads/S1nWh-o8R.png) ![螢幕快照 2024-06-27 23-58-37](https://hackmd.io/_uploads/SyVSnboLC.png) ## TODO: 引入 timer,讓 kHTTPd 主動關閉逾期的連線 ## TODO: 以 RCU 搭配自行設計的 lock-free 資料結構,在並行環境中得以釋放系統資源 ## 學習 [cserv](https://github.com/sysprog21/cserv) 的 memcache 並在 kHTTPd 重新實作 * 過程中應一併完成以下: * 修正 kHTTPd 的執行時期缺失 * 指出 kHTTPd 實作的缺失 (特別是安全疑慮) 並予以改正 ## TODO: 用你改進的 kHTTPd 和 [cserv](https://github.com/sysprog21/cserv) 進行效能評比 > 解釋行為落差