Linux 核心專題: 高效網頁伺服器
執行人: yqt2000
解說影片
任務說明
依據 ktcp 作業規範,開發高效網頁伺服器 (針對靜態)。
TODO: 回答「自我檢查清單」的所有問題
需要附上對應的參考資料和必要的程式碼,以第一手材料 (包含自己設計的實驗) 為佳
待整理 eric88525, zoanana990 筆記內容
參照 eBPF 教程
隨著電腦硬體逐漸提供 atomic 指令後,mutex 或稱為 lock 的機制被列入作業系統的實作考量:
- 需要進入 CS 時, 用 mutex/lock —— 上鎖/解鎖永遠是同一個 thread/process;
- 要處理 signalling 時,用 semaphore —— 等待/通知通常是不同的 threads/processes;
簡言之,要搶資源時用 mutex,要互相通知時用 semaphore。
上方說法過於武斷,避免這樣的「簡言之」。
工程人員說話要精準。
mutex 與 semaphore 的差別在於:
- process 使用 mutex 時,process 的運作是持有 mutex,執行 CS 來存取資源,然後釋放 mutex
- process 使用 semaphore 時,process 總是發出信號 (signal),或者總是接收信號 (wait),同一個 process 不會先後進行 signal 與 wait
- 換言之,process 要不擔任 producer,要不充當 consumer 的角色,不能兩者都是。semaphore 是為了保護 process 的執行同步正確;
RCU 適用於頻繁的讀取 (即多個 reader)、但資料寫入 (即少量的 updater/writer) 卻不頻繁的情境,例如檔案系統,經常需要搜尋特定目錄,但對目錄的修改卻相對少,這就是 RCU 理想應用場景。
RCU 藉由 lock-free 程式設計滿足以下場景的同步需求:
- 頻繁的讀取,不頻繁的寫入
- 對資料沒有 strong consistency 需求
即使存取舊的資料,不會影響最終行為的正確,這樣的情境就適合 RCU,對其他網路操作也有相似的考量。
實驗環境
改寫 kHTTPd,分析效能表現和提出改進方案,可參考 kecho
參考 YiChianLin, oscarshiang
CMWQ 簡介
CMWQ 是對 Linux 傳統 Workqueue 機制的改進和重新實現。它保持了原有 API 的兼容 性,同時引入了更智能的並行管理和資源利用策略。
一般 workqueue:
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 →
CMWQ架構圖:
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 →
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 類型:
- Bound 類型:綁定特定的 CPU,使管理的 worker 執行在指定的 CPU 上執行,而每個 CPU 中會有兩個 worker-pools 一個為高優先級的,另一個給普通優先級的,透過不同的 flags 影響 workqueue 的執行優先度
- Unbound 類型:thread pool 用於處理不綁定特定 CPU,其 thread pool 是動態變化,透過設定 workqueue 的屬性建立對應的 worker-pools
分離 Workqueue 和 Worker-pools:使用者只需關注將任務放入 queue,不需考慮執行細節。
CMWQ 的優勢及為什麼適用於該專案:
- 網頁伺服器需要同時處理大量請求。CMWQ 通過共享 worker pools 和根據負載動態調整執行緒數量,可以滿足高並行的需求和大幅減少一般 workqueue 資源浪費的問題。
- 另外網頁伺服器可能需要處理不同優先級的請求。CMWQ 提供了高優先級和普通優先級的 worker pools,可以更好地處理不同類型的 HTTP 請求。
- 長時間任務處理:對於可能耗時較長的 HTTP 請求(如大檔案傳輸),CMWQ 可以動態建立新執行緒並分配給其他 CPU 執行,避免阻塞其他請求的處理。
- 跨 CPU 執行能力:CMWQ 的 Unbound 類型 worker pools 允許任務在不同 CPU 間切換,這可以提高多核系統上網頁伺服器的整體性能。
使用方式
為了在核心模組中引入 CMWQ,我們會需要使用到 <linux/workqueue.h>
中的這些函式:
- alloc_workqueue : 在初始化模組時用來建立一個 CMWQ
- destroy_workqueue : 用來釋放 workqueue
- queue_work : 將 work 放入 workqueue 中排程
- INIT_WORK : 用以初始化 work
引入 CMWQ 至 khttpd
參考了 kecho 的作法,以及 fatcatorange,YiChianLin 的筆記
這部份我的 commit: fde4de, e593125
程式碼註解不該出現中文!總是用美式英語書寫。
改進 git commit message,參照第一次作業的規範。
main.c
宣告 workqueue ,並在使用 workqueue 相關 API 時 include workqueue.h
- workqueue_struct 定義於 workqueue.c 中,裡面有 CMWQ 文件中所提及的 unbound_attrs 可以設定 workqueue 在 unbound 條件下的屬性;
- alloc_workqueue 定義於 workqueue.c 中,初始化一個 CMWQ 並在 flag 的設定為 WQ_UNBOUND 表示不會被特定的 CPU 所限制,使資源不會被閒置,可以透過切換的方式執行未完成的任務
http_server.h
khttpd_service 採用雙向鏈結串列作為 workqueue 管理 work
http_server.c
請求 - 以 http_request 作為其結構體,嵌入 list_head 進行管理,而
work_struct 為真正要作的任務,可被 INIT_WORK
函式初始化,去運行客製化的函式( e.g. 此專案中處理請求的函式 http_server_worker
)
http_server_daemon()
- 第 37 行(work = create_work(socket);) 為每一個連線的請求建立一個 work 進行處理
- queue_work() 將 work 放入 CMWQ 中進行排程
注意書寫規範:
- 使用 lab0 規範的程式碼書寫風格,務必用 clang-format 確認一致
create_work()
& free_work()
在 create_work 中,根據傳入的 socket 建立一個 work,為每一個連線請求進行 kernel space (kmalloc) 的動態記憶體配置,並進行初始化,再透過 list_add 加入到 workqueue 當中
- 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 所配置出的記憶體空間
http_server_worker()
使用 workqueue 時,程式執行時就是傳入一個 struct work_struct ,因此可透過 container_of 取得請求的 socket
引入 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倍。
按照作業 kecho + khttpd 實作 directory listening 的步驟 以及比照 fatcatorange同學筆記內容 修正缺失的方法去實作該功能,以下為整理兩者筆記並融合我在實作遇上的問題和解決方法。
修改 http_server_response
要加入這個功能,要修改 http_server_response
,原本的 http_server_response
只會檢查是不是用 get ,是的話回傳一個 HTTP_RESPONSE_200 (代表成功) ,內容是 hello world。
而這個函式被使用在 http_server.c/http_parser_callback_message_complete()
:
上述函式被綁定在 http_server.c/http_server_worker()
:
當 http_server_worker()
執行 http_parser_execute()
時,就會根據解析執行對應的函式,以這個例子來說,解析完整個 http 請求時執行 http_parser_callback_message_complete()
正式修改 http_server.c/http_server_response()
:
實作目錄檔案存取功能 http_server.c/handle_directory()
handle_directory()
對於不同的 case 做出回應,主要執行以下步驟:
- 檢查請求屬性是否為 GET ,以及開啟請求的 url 檔案
- case: IS_ERR - url 檔案無法開啟 => 404 Not Found
- case: S_ISDIR - url 屬於目錄 => 繼續對迭代目錄
- case: S_ISREG - url 屬於檔案 => 讀取檔案,印出內容
其中迭代目錄(tracedir
, iterate_dir
)為該方法的精隨,於下面內容繼續介紹
static bool handle_directory(struct http_request *request)
{
struct file *fp;
char pwd[BUFFER_SIZE] = {0};
request->dir_context.actor = tracedir;
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);
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;
}
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));
} 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> 可對目錄struct file*
進行迭代並運行 struct dir_context *
設置的 callback function。
因此首先須在 struct http_request
中加入 struct dir_context
而 http_server.c/trace_dir()
可以回傳給客戶端當前目錄下的資料夾連結或檔案連結,並將其印出顯示在 localhost 的頁面上, 使得使用者可以藉由點擊目錄進入更深層的目錄或存取檔案
指定開啟目錄的路徑
main.c
http_server.h
即可透過 module_param ,在載入模組時指定路徑,e.g.
其餘實作細節
http_server.c
中的 send_http_header()
& send_http_content()
& catstr()
& read_file()
成果展示


TODO: 引入 timer,讓 kHTTPd 主動關閉逾期的連線
TODO: 以 RCU 搭配自行設計的 lock-free 資料結構,在並行環境中得以釋放系統資源
學習 cserv 的 memcache 並在 kHTTPd 重新實作
- 過程中應一併完成以下:
- 修正 kHTTPd 的執行時期缺失
- 指出 kHTTPd 實作的缺失 (特別是安全疑慮) 並予以改正
TODO: 用你改進的 kHTTPd 和 cserv 進行效能評比
解釋行為落差