contributed by < chiacyu
>
從文章的描述可以看到作者主要提到幾個問題
CPU
核之間互相搬移任務透過 CMWQ 希望能夠作到除了能兼容原先的實做之外還做了一些修改包括
利用 kecho
裡面的 bench
來做一下測試:以下是 user-echo-server.c
的結果
kecho
的結果如下
可以看到除了在 kernel space
執行外, CMWQ也帶來時粉顯著的
首先可以先看還未引入 cmwq
時的執行效果
首先需要新增幾個資料結構來進行後續操作
透過 khttp
資料結構來紀錄
socket
位址list
結構為鏈結串列之節點khttp_work
針對 workqueue
的 單一 worker
透過 khttp_server_service
來紀錄
is_stopped
來紀錄整個 server
目前的狀態worker
來作為紀錄 worker
鏈結串列的首部節點透過 create_work()
當不同的客戶端進行連線的時候,新增一個 thread
透過 list_add
並將其串到 daemon.worker
節點的後方
接著需要 free_work()
來將所有資源釋放,透過 list_for_each_entry_safe
走訪每一個 struct khttp *work
kernel_sock_shutdown
來關閉該 work
所監聽的 socket
flush_work
將 work_struct
清空sock_release
將已經關閉的 socket
釋放接著來看如何處理每一個客戶端的連線處理
透過 http_server_worker()
來處理每個客戶端的連線
work
裡面找到目標 thread
socket
daemon.is_stopped
若服務已停止則關閉該 socket
並釋放 buf
透過 http_server_daemon()
來啟動 server daemon
INIT_LIST_HEAD
將 daemon.worker
節點初始化之後新增的客戶端可以透過 list_add()
加入鏈結串列中kthread_should_stop()
判斷執行緒是否在執行中,若是尚未結束則透過 kernel_accept
建立新的連線socket
後透過 create_work
來建立新的執行緒處理新的連線queue_work()
啟動 workqueue
最後我們需要將 server
註冊進 Linux 系統模組中
在 khttpd_init()
的時候
open_listen_socket()
來監聽目標 socket
客戶可以透過該 socket
來建立連線alloc_workqueue()
來創造並啟動 workqueue
修改完之後的效能表現如下
關於 RCU 的相關資訊可以查看 Linux 核心設計: RCU 同步機制 跟 What is RCU, Fundamentally? 最適合 RCU 的場景為, 「讀取很頻繁,寫入較少,且嚴格要求資料一致性」, 因此初步引入 RCU 來管理客戶鍊結串列
在 create_work()
中使用 list_add_rcu()
來加入新的客戶端
在 free_work()
中 使用 list_for_each_entry_rcu()
來走訪鍊結串列並將其一一釋放
但執行結果卻不如預期,因此需要好好運用 ftrace
等工具來進行分析
ftrace
是 Linux kernel 提供的追蹤機制,相關的內容可以參考 Debugging the kernel using Ftrace - part 1 跟 Debugging the kernel using Ftrace - part 2 還有 "Demystifying the Linux CPU Scheduler" 的第六章也可以看到相關的敘述
首先看看目前的系統是否有提供 ftrace
的功能
如果看到下列內容代表 ftrace
在該版本中可以使用
接著可以到 /sys/kernel/debug/tracing
印出以下內容
ftrace 的使用方式是透過 ehco
寫入來進行互動,可以先查看 available_filter_functions
的內容,其中紀錄了目前 ftrace
可以追蹤的函式。
但是在需要先將 khttp.ko
透過 註冊進核心模組,之後就可以看到
我們可以撰寫一個 shellscript
來設定 ftrace
max_graph_depth
可以設定測量函式的深度current_tracer
會紀錄使用的量測項目,這邊設定為 function_graph
set_graph_function
則設定欲觀察的程式,在此為 http_server_worker
執行完之後可以來看看 trace
裡面的內容
接著可以將 max_graph_depth
的數字增加來看看結果
可以看到在 __tcp_push_pending_frames
花了最久的時間。
keep-Alive
功能在測試之前需要先充分的了解 HTTP request 的格式, 詳細資料可以參考 HTTP Messages。 HTTP 的 request 可以分成三的部份
GET
, POST
等等URL
形式因此我們在掛載 khttp
之後輸入 telnet localhost 8081
, 分別輸入 GET / HTTP/1.0
跟 GET / HTTP/1.1
可以看到目前的 khttp
目前有提供 Keep Alive
的功能
timer
主動中斷超時連線由於目前的 khttp
沒有提供 timer
的機制來中斷連線,這個部份可以參考 sehttpd 的實作方式。
sehttpd
是透過一個 priority queue
的方式來管理所有連線。其中 priority queue
的結構是一個 min heap
其中透過 prio_queue_min()
取出最接近 deadline
的連線。
原本 sehttpd
裡面更新時間的方法為透過 gettimeofday()
的方式來獲取目前系統的時間,再轉換成 ms
的單位。
很遺憾的是在 kernel space
並沒有辦法直接使用 gettimeofday()
需要透過別的方式得到目前的系統時間。 這邊使用 ktime_get_real(), 在透過 ktime_to_ms() 轉換成 ms
的格式。
接著在 http_server_daemon()
裡面透過 timer_init()
將 timer
初始化,接著透過 handle_expired_timers()
來找出所有超過截止時間的連線,並一一將其釋放。
在測試的時候遇到一個問題,就是 http_server_daemon()
會停留在 kernel_accept()
的部份而不會回到迴圈的開始,發現的原因是透過 dmesg
查看時並沒有看到 "wait time = %d\n"
持續被輸出,且超過時間的客戶連線也沒有順利被關閉。 翻找資料的時候看到 Risheng1128 同學的報告才知道需要將 socket
改成 non-blocking 的方式。 詳細可以看這個 commit
在 kernel_accept 的頁面中可以看到,int kernel_accept(struct socket * sock, struct socket ** newsock, int flags)
函式需要透過三個參數,第一個參數為目前監聽的 socket
, 第二個為要建立的新連線的 socket
, 最後一個則為 flag
來設定 socket
的相關屬性。
flags must be SOCK_CLOEXEC, SOCK_NONBLOCK or 0. If it fails, newsock is guaranteed to be NULL. Returns 0 or an error.
所以需要把第三個參數內容改成 SOCK_NONBLOCK
接著透過 ./htstress -n 10000 http://localhost:8081/
來進行測試可以從 dmesg
中看到 timer
如預期的運作。
在 kernel space
有提供 int iterate_dir(struct file *file, struct dir_context *ctx)
函式可以使用。 關於 int iterate_dir() 的定義需要輸入兩個參數,分別是 struct file *file
與 struct dir_context *ctx
。
在 kernel space
裡面要開啟檔案需要透過不同的函式,這邊透過 filp_open(const char *filename, int flags, umode_t mode) 來回傳一個 struct file
的指針。
在這邊先指定打開 "/"
root的檔案位置。再來可以看看
struct dir_context *
的結構。透過 typedef int (*filldir_t)(struct dir_context *, const char *, int, loff_t, u64, unsigned);
來定義 callback function
. 這邊先定義出 printdir()
來作為 callback function
。當 iterate_dir()
被執行的時候會呼叫 printdir()
。
執行出來的結果為下圖,可以看到成功印出 root
裡面的檔案內容,接著要把內容轉換成 http
的資料格式。
Http
response 的資料格式可以參考 http response。修改完成程式碼之後可以透過瀏覽器測試。
打開瀏覽器在 URL
中輸入 http://localhost:8081
如果成功可以看到畫面如下:
但目前還沒有辦法實踐回應功能,來試著引入 WWWROOT
功能來達成。透過 #define DEFAULT_ROOT "/"
來定義預設的檔案位置,再來可以透過 module_param
巨集來在 insmod
的時候定義 WWWROOT
個變數。詳細的使用方法可以看 The Linux Kernel Module Programming Guide : 4.5 Passing Command Line Arguments to a Module
這邊在 khttp_server_service
裡面新增一個 char *root
來儲存 WWWROOT
的內容。這邊先將 struct khttp_server_service daemon
宣告為 extern
。 接下來在 khttpd_init()
中將 WWWROOT
的內容指派給 daemon.root
。之後在 list_directory_info()
可以取得 WWWROOT
的內容。
接著當使用者在點擊資料夾的過程會透過 request_url
來改變目標位置。原本預設的 request_url
是 /
。當點擊 home
這個資料夾時 request_url
會變成 /home
。 再來還需要判斷開啟的檔案內容是資料夾還是一般檔案。可以透過 inode
來判斷檔案的屬性。其中 inode
的結構可以參考 fs.h
可以透過巨集 S_ISREG(m)
, S_ISDIR(m)
來判斷檔案的類型,其中要填入的參數則是 imode
, 因此可以判定當 S_ISDIR(m)
為真時表示目前開啟的檔案為目錄格式。
先新增一個 inode
的結構來取得 struct file *fp
的 inode
內容。再來對 inode
中的 i_mode
元素進行判斷。
如果打開的檔案是 regular file
的話需要把檔案的內容讀取進 buffer
再回傳,在 kernel space
讀取檔案需要透過 kernel_read
相關的說明可以看 fs.h。
之後打開網頁瀏覽器之後就可以就可以透過點擊資料夾來進行互動,當讀到文字檔的時候也可以看到文字檔的內容呈現在瀏覽器上。