# Linux 核心專題:kHTTPd 改進 > 執行人: yan112388 > [專題解說影片](https://www.youtube.com/watch?v=y8ErJ3ZNSi4) ## 任務簡介 改進 kHTTPd 的並行處理能力,予以量化並有效管理連線。 ## TODO: 依據[第七次作業](https://hackmd.io/@sysprog/linux2024-ktcp) 開發 kHTTPd > 著重 concurrency, workqueue ## 實驗環境 ```shell $ gcc --version gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 Copyright (C) 2021 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE $ 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): 20 On-line CPU(s) list: 0-19 Vendor ID: GenuineIntel Model name: 13th Gen Intel(R) Core(TM) i7-13700H CPU family: 6 Model: 186 Thread(s) per core: 2 Core(s) per socket: 14 Socket(s): 1 Stepping: 2 CPU max MHz: 5000.0000 CPU min MHz: 400.0000 BogoMIPS: 5836.80 ``` ## 研讀 Linux 核心設計: RCU 同步機制 內容參考 [Linux 核心設計: RCU 同步機制](https://hackmd.io/@sysprog/linux-rcu#%E9%BB%9E%E9%A1%8C) ### 簡介 RCU (Read-Copy Update) 是 Linux 核心中的一種資料同步機制,允許多個 reader 在單一 writer 更新資料的同時,得以在不需要 lock 的前提,正確讀取資料。 RCU 適用場景: * 頻繁的讀取 (即多個 reader)、資料寫入 (即少量的 updater/writer) 不頻繁的情境 * 對資料沒有 strong consistency 需求 ### 測試相關 RCU 核心模組 [rcu_example](https://github.com/jinb-park/rcu_example) 為一個簡易的 RCU 相關 Linux 核心模組,`rcu_example` 使用 rcu 鏈結串列來實做一個書籍借閱管理系統。 讀取方面的操作 `is_borrowed_book` `print_book()` ```c static int is_borrowed_book(int id) { struct book *b; /** * reader * * iteration(read) require rcu_read_lock(), rcu_read_unlock() * and use list_for_each_entry_rcu() */ rcu_read_lock(); list_for_each_entry_rcu(b, &books, node) { if(b->id == id) { rcu_read_unlock(); return b->borrow; } } rcu_read_unlock(); pr_err("not exist book\n"); return -1; } ``` * 使用 `rcu_read_lock()` 與 `rcu_read_unlock()`,用來標記 RCU 讀取過程的開始和結束,幫助檢測寬限期是否結束 * `list_for_each_entry_rcu()` 用於走訪 RCU 保護的串列節點 將此程式碼編譯並掛載,將會自動執行 `test_example` 函式 ```shell $ make $ sudo insmod list_rcu_example.ko ``` 使用以下命令來查看核心的輸出訊息,可以看見 `test_example` 的輸出內容 ```shell $dmesg | tail -n 50 .... [75610.718976] id : 0, name : book1, author : jb, borrow : 0, addr : ffff98f4682d8fc0 [75610.719006] id : 1, name : book2, author : jb, borrow : 0, addr : ffff98f4682d9140 [75610.719016] book1 borrow : 0 [75610.719021] book2 borrow : 0 ``` ## `drop-tcp-socket` 核心模組運作原理 與 `TIME-WAIT` sockets 解釋 `drop-tcp-socket` 核心模組運作原理。`TIME-WAIT` sockets 又是什麼? ### `TIME-WAIT` sockets 正常情況,TCP 連線會經過四次揮手過程才會關閉: ![image](https://hackmd.io/_uploads/BJ2tft5I0.png) `TIME_WAIT` socket 為一個處於 TIME_WAIT 狀態的 socket,以下內容參考 [TCP/IP Illustrated, Volume 1 : The Protocols](https://en.wikipedia.org/wiki/TCP/IP_Illustrated): 當 TCP 執行主動關閉並發送最後的 ACK 時,連線必須在 `TIME_WAIT` 狀態停留兩個最大區段壽命(maximum segment lifetime)的時間,因此`TIME_WAIT` 狀態也被稱為 2MSL(maximum segment lifetime,MSL) 等待狀態。 最大區段壽命 maximum segment lifetime * 每個 TCP 實作都必須為最大區段壽命(MSL)選擇一個值,常見的值為 30 秒、1 分鐘或 2 分鐘 * 最大區段壽命(MSL):一個 TCP 分段在網路中存在的最長時間,超過這個時間就會被丟棄 * 因為 TCP 區段是透過 IP 資料包傳輸的,而 IP 資料包有 TTL(Time To Live)欄位限制其生存時間,因此這個時間是有限的 在這段時間內,網路中所有與該連線相關的區段都將消失,進而避免了可能會發生的的衝突: * 延遲區段的誤判: 舊連線中的延遲區段可能在新連線建立後才到達,2MSL(maximum segment lifetime)的等待期確保了舊連線的所有區段都會在網路中消失。如果沒有 `TIME_WAIT`,這些延遲的區段可能被誤認為是新連線的一部分。 * 序列號重疊: TCP 使用序列號來識別和排序封包,如果新舊連線使用相同的四元組(來源 IP 、來源埠號、目的 IP、目的埠號),且序列號恰好重疊,可能導致資料混雜。在 `TIME_WAIT` 期間,相同的四元組不能被用於新的連線。 * ACK 問題: 如果舊連線的 ACK 在新連線建立後到達,可能觸發不必要的重傳,導致網路阻塞。2MSL 的時間能使序列號空間得到充分的更新。 此外,`TIME_WAIT` 狀態還允許重傳最後的 ACK,如果最後的 ACK 丟失,對方會重發 FIN。 一個 TCP 連接進入 `TIME_WAIT` 狀態時,對應的 socket 就成為了 `TIME_WAIT` socket。缺點為,會佔用系統資源,(如佔用了大量的通訊埠),甚至可能引起服務重啟延遲的問題。因此,在設計高效能伺服器時需要考慮 `TIME_WAIT` 的影響。 ### `drop-tcp-socket` 核心模組 提供一個機制,允許在特定條件下強制關閉處於 `TIME-WAIT` 狀態的 TCP 連線,直接釋放 socket 資源。 在 `drop-tcp-socket.c` 中,定義了 3 種結構體 ```c struct droptcp_data { uint32_t len; uint32_t avail; char data[0]; }; struct droptcp_pernet { struct net *net; struct proc_dir_entry *pde; }; struct droptcp_inet { char ipv6 : 1; const char *p; uint16_t port; uint32_t addr[4]; }; ``` * `droptcp_data` 儲存和管理從使用者空間讀取到的資料 * `droptcp_pernet` 儲存每個網路命名空間(network namespace)的特定資料 網路命名空間(network namespace):Linux 核心中一種用來隔離作業系統跟網路相關資源的機制,[NETWORK_NAMESPACES(7)](https://man7.org/linux/man-pages/man7/network_namespaces.7.html) 中寫道 > Network namespaces provide isolation of the system resources associated with networking: network devices, IPv4 and IPv6 protocol stacks, IP routing tables, firewall rules... 每個網路命名空間有自己的網路設備、路由表、防火牆規則等,藉此達到網路虛擬化的目的。 > 待補充細節 * `droptcp_inet` 表達一個 IP 地址和埠號的組合 這些結構體使得模組有助於處理使用者的輸入,管理網路命名空間,並且正確地別和操作 TCP 連線。 整個流程如下: 1. `droptcp_proc_write`、`droptcp_proc_open` 使用者會透過 proc 檔案來指定要關閉的連線 > proc 檔案系統待補充 2. `droptcp_proc_release`、`droptcp_process` ``` while (*p && p < d->data + d->len) { ... } ``` 使用 `while` 循環輸入資料,分析地址後找到對應的 socket 3. `droptcp_drop`、`droptcp_proc_release` ```c if (sk->sk_state == TCP_TIME_WAIT) { inet_twsk_deschedule_put(inet_twsk(sk)); } else { tcp_done(sk); sock_put(sk); } ``` 判斷 socket 是否處於 `TIME-WAIT` 狀態,如果是 `TIME-WAIT` 狀態,就直接釋放。如果是其他狀態,則執行正常的關閉過程。 總結:`drop-tcp-socket` 核心模組結合網路命名空間和 proc 檔案系統來達成目的。 ## 引入 CMWQ 到 khttpd 參考資料: [ktcp](https://hackmd.io/@sysprog/HkyVuh0NR#TODO-%E5%BC%95%E5%85%A5-CMWQ-%E6%94%B9%E5%AF%AB-kHTTPd) 作業頁面、[kecho](https://github.com/sysprog21/kecho/blob/master/echo_server.h) 的實作、學員筆記 [fatcatorange](https://hackmd.io/@sysprog/HkyVuh0NR#TODO-%E5%BC%95%E5%85%A5-CMWQ-%E6%94%B9%E5%AF%AB-kHTTPd) 詳見 [commit a22e28c](https://github.com/yan112388/khttpd/commit/a22e28c1880c2d7a06d246b43c7eec276154c966) CMWQ(Concurrency Managed Workqueue) 是 Linux 核心中的一個機制,用於處理非同步的任務。 ![image](https://hackmd.io/_uploads/SyRL3Oa80.png) * 該機制使用 `struct list_head` 將要處理的每一個任務連接,依次取出處理,處理結束的再從 queue 中刪除 * 傳統的 workqueue 需要考慮 CPU 調度的問題,CMWQ 使用 worker-pool ,藉此全幫開發者處理好,可以減少消耗的資源,提高並行性和效率, 於 khttpd 中 `http_server.h` 新增以下結構體 ```c struct httpd_service { bool is_stopped; struct list_head head; }; extern struct httpd_service daemon_list; ``` 修改 `http_server.c` 中的 `http_request` 結構,新增鏈結串列節點及 `work_struct` 結構體 ```c 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; }; ``` 在 `main.c` 中的 `khttpd_init` ,包含以下內容: ```c khttpd_wq = alloc_workqueue(MODULE_NAME, 0, 0); http_server = kthread_run(http_server_daemon, &param, KBUILD_MODNAME); ``` * 使用 `alloc_workqueue` 建立一個新的 workqueue,為 CMWQ 機制提供基礎的架構 * `kthread_run` 會建立並開啟一個核心執行緒,再執行 `http_server_daemon`,為每一個連線的請求建立一個 work 進行處理 ```diff= int http_server_daemon(void *arg) { ... - 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) { + printk(KERN_ERR ": create work error, connection closed\n"); + kernel_sock_shutdown(socket, SHUT_RDWR); + sock_release(socket); continue; } + queue_work(khttpd_wq, work); ... } ``` * `create_work` 創建新的 work * `queue_work(khttpd_wq, work)` 將 work 放入workqueue 中 主要流程: 建立 CMWQ -> 創建 work -> workqueue 開始運作 -> 待模組卸載,釋放所有記憶體 以下為將時間設定為連續 3 秒,使用 20 個並行連線,且總請求數量為 200,000 的壓力測試結果: 原版 khttpd ```shell seconds: 20.371 requests/sec: 9818.075 ``` 引入 CMWQ 後的 khttpd ``` seconds: 6.330 requests/sec: 31597.419 ``` 由此可見,吞吐量有所提升 ## TODO: 實作 content cache > 確保得以處理大量並行請求,並正確處理資源的釋放 ## TODO: 檢視其他學員在 kHTTPd 的投入狀況,提出疑惑和建議 > 在[課程期末專題](https://hackmd.io/@sysprog/linux2024-projects)找出同樣從事 kHTTPd 專案開發的學員,在其開發紀錄提出你的疑惑和建議。 > 在此彙整你的認知和對比你的產出。