# 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, ¶m, 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 專案開發的學員,在其開發紀錄提出你的疑惑和建議。
> 在此彙整你的認知和對比你的產出。