Try   HackMD

Linux 核心專題:kHTTPd 改進

執行人: yan112388
專題解說影片

任務簡介

改進 kHTTPd 的並行處理能力,予以量化並有效管理連線。

TODO: 依據第七次作業 開發 kHTTPd

著重 concurrency, workqueue

實驗環境

$ 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 同步機制

簡介

RCU (Read-Copy Update) 是 Linux 核心中的一種資料同步機制,允許多個 reader 在單一 writer 更新資料的同時,得以在不需要 lock 的前提,正確讀取資料。

RCU 適用場景:

  • 頻繁的讀取 (即多個 reader)、資料寫入 (即少量的 updater/writer) 不頻繁的情境
  • 對資料沒有 strong consistency 需求

測試相關 RCU 核心模組

rcu_example 為一個簡易的 RCU 相關 Linux 核心模組,rcu_example 使用 rcu 鏈結串列來實做一個書籍借閱管理系統。

讀取方面的操作 is_borrowed_book print_book()

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 函式

$ make
$ sudo insmod list_rcu_example.ko

使用以下命令來查看核心的輸出訊息,可以看見 test_example 的輸出內容

$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 Not Showing Possible Reasons
  • The image was uploaded to a note which you don't have access to
  • The note which the image was originally uploaded to has been deleted
Learn More →

TIME_WAIT socket 為一個處於 TIME_WAIT 狀態的 socket,以下內容參考 TCP/IP Illustrated, Volume 1 : The Protocols

當 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 種結構體

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) 中寫道

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_writedroptcp_proc_open
    使用者會透過 proc 檔案來指定要關閉的連線

proc 檔案系統待補充

  1. droptcp_proc_releasedroptcp_process
while (*p && p < d->data + d->len) {
...
}

使用 while 循環輸入資料,分析地址後找到對應的 socket

  1. droptcp_dropdroptcp_proc_release
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 作業頁面、kecho 的實作、學員筆記 fatcatorange

詳見 commit a22e28c

CMWQ(Concurrency Managed Workqueue) 是 Linux 核心中的一個機制,用於處理非同步的任務。

image

  • 該機制使用 struct list_head 將要處理的每一個任務連接,依次取出處理,處理結束的再從 queue 中刪除

  • 傳統的 workqueue 需要考慮 CPU 調度的問題,CMWQ 使用 worker-pool ,藉此全幫開發者處理好,可以減少消耗的資源,提高並行性和效率,

於 khttpd 中 http_server.h 新增以下結構體

struct httpd_service {
    bool is_stopped;
    struct list_head head;
};

extern struct httpd_service daemon_list;

修改 http_server.c 中的 http_request 結構,新增鏈結串列節點及 work_struct 結構體

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 ,包含以下內容:

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 進行處理
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

seconds:       20.371
requests/sec:  9818.075

引入 CMWQ 後的 khttpd

seconds:       6.330
requests/sec:  31597.419

由此可見,吞吐量有所提升

TODO: 實作 content cache

確保得以處理大量並行請求,並正確處理資源的釋放

TODO: 檢視其他學員在 kHTTPd 的投入狀況,提出疑惑和建議

課程期末專題找出同樣從事 kHTTPd 專案開發的學員,在其開發紀錄提出你的疑惑和建議。
在此彙整你的認知和對比你的產出。