主講人: jserv / 課程討論區: 2024 年系統軟體課程
返回「Linux 核心設計/實作」課程進度表Image Not Showing Possible ReasonsLearn More →
- The image file may be corrupted
- The server hosting the image is unavailable
- The image path is incorrect
- The image format is not supported
kecho
: 執行在 Linux 核心模式的 TCP 伺服器取得 kecho 原始程式碼並編譯:
預期會見到以下:
bench
及 user-echo-server
kecho.ko
及 drop-tcp-socket.ko
接著可進行測試:
參考輸出:
該操作由以下動作組成:
會出現以下輸出:
可輸入任何字元 (記得按下 Enter),然後就會看到 telnet 回應你剛才輸入的字元。
按下 Ctrl
和 ]
組合鍵,之後按下 q
,即可離開 telnet 畫面。接著可以試著在 $ telnet localhost 12345
時不要輸入任何字元,只是等待,會看到以下的 kernel 訊息 (可用 $ dmesg
觀察):
kecho 掛載時可指定 port 號碼: (預設是 port=12345
)
修改或測試 kecho 的過程,可能因為 TIME-WAIT
sockets 持續佔用,導致 rmmod
無法成功,這時可透過給定的 drop-tcp-socket
核心模組來剔除特定的 TCP 連線。請詳細閱讀 kecho 以得知必要的設定和準備工作。
user-echo-server
: 執行於使用者層級的 TCP 伺服器user-echo-server
是 kecho
的使用者層級的實作,可對照功能和比較效能,運用 epoll 系統呼叫,會傾聽 port 12345。
不管是 user-echo-server
抑或 kecho
,都可搭配給定的 bench
程式來分析效能。請詳細閱讀 kecho 以得知必要的設定和準備工作。
seHTTPd 是個高效的 web 伺服器,涵蓋並行處理、I/O 事件模型、epoll, Reactor pattern,和 Web 伺服器在事件驅動架構的考量,可參見 高效 Web 伺服器開發。
預先準備的套件: (eBPF 作為後續分析使用)
取得程式碼和編譯:
預期可見 sehttpd
這個執行檔。接著透過內建的 test suite 來測試:
首先,我們可用「古典」的方法,透過 Apache Benching tool 對 seHTTPd 進行壓力測試。在一個終端機視窗執行以下命令:
切換到網頁瀏覽器,開啟網址 http://127.0.0.1:8081/
應在網頁瀏覽器畫面中見到以下輸出:
Welcome!
If you see this page, the seHTTPd web server is successfully working.
然後在另一個終端機視窗執行以下命令:
參考程式輸出: (數值若跟你的測試結果有顯著出入,實屬正常)
留意到上述幾項:
-k
參數: 表示 "Enable the HTTP KeepAlive feature",也就是在一個 HTTP session 中執行多筆請求-c
參數: 表示 concurrency,即同時要下達請求的數量-n
參數: 表示壓力測試過程中,期望下達的請求總量關於輸出結果,請詳閱 ab - Apache HTTP server benchmarking tool 說明。
ab
無法有效反映出多執行緒的特性 (ab
自身就消耗單核 100% 的運算量),因此我們才會在 khttpd 提供 htstress.c,後者提供 -t
選項,能夠依據測試環境的有效 CPU 個數進行分配。ab - Apache HTTP server benchmarking tool 的實作從今日的 GNU/Linux 或 FreeBSD 來說,算是過時且未能反映系統特性,除了 htstress
,尚可使用 wrk,該專案的訴求是
wrk is a modern HTTP benchmarking tool capable of generating significant load when run on a single multi-core CPU. It combines a multithreaded design with scalable event notification systems such as epoll and kqueue.
另一個可測試 HTTP 伺服器負載的工具是 httperf。
倘若你將 seHTTPd 執行後,不立刻關閉,隨即較長時間的等待和重新用上述 ab
多次測試 (變更 -n
和 -c
參數的指定數值) 後,可能會遇到以下錯誤狀況 (部分)
[ERROR] (src/http.c:253: errno: Resource temporarily unavailable) rc != 0
可用 $ grep -r log_err
搜尋原始程式碼,以得知現有的例外處理機制 (注意: 裡頭存在若干缺失,切勿「舉燭」)。
研讀〈Linux 核心設計: 透過 eBPF 觀察作業系統行為 〉以理解核心動態追蹤機制,後者允許我們使用非侵入式的方式,不用去修改我們的作業系統核心內部,不用去修改我們的應用程式,也不用去修改我們的業務程式碼或者任何系統配置,就可快速高效地精確獲取我們想要的資訊。
在 seHTTPd 原始程式碼的 ebpf 目錄提供簡易的 HTTP 封包分析工具,就是建構在 eBPF 的基礎之上,並透過 IO Visor 提供的工具來運作。
概念示意圖:
使用方式: (預先在另一個終端機視窗執行 $ ./sehttpd
)
注意: 這個工具預設監控 eth0
這個網路介面 (network interface)。倘若你的預設網路介面不是 eth0
,你需要依據 ip
工具的輸出,決定監控哪個網路介面。舉例來說,在某台 GNU/Linux 機器上執行以下命令:
你會見到若干輸出,如果你的環境裡頭已執行 Docker,輸出數量會很可觀,但不用擔心,排除 lo
, tun
, virbr
, docker
, br-
, veth
開頭的輸出,然後就剩下 enp5s0
這樣的網路介面 (端視你的網路硬體而有不同),於是可將上述命令改為:
然後打開網頁瀏覽器,多次存取和刷新 http://127.0.0.1:8081/
網址,然後你應可在上述執行 Python 程式的終端機見到類似以下的輸出:
關於上述程式運作的概況,可參考 Appendix C。
透過 eBPF 追蹤 fibdrv 核心模組的運作機制,可參見 0xff07 的共筆
預習 CS:APP 第 11 章: Network Programming,搭配閱讀:
網頁伺服器流程,參見下圖:
getaddrinfo()
啟用程序,回傳值為 struct addrinfo
的結構,裡面就含有連線所需要的資料,如:IP 位址、 port (通訊埠)、 server 名稱…等等socket()
建立連接,回傳值為 file descriptor
,注意只有建立連結但不會操作系統,也不會往網路上傳送任何內容socket man page
socket() creates an endpoint for communication and returns a file descriptor that refers to that endpoint.
bind()
函式將 socket
與特定的 IP 位址和 port 連接起來(在 kernel space 中進行)listen()
,準備接受來自 client 的請求accept()
將 client 連接connect()
發送 Connection request 等待 server accept
此流程只能用於單一的 server/client 的連線,依照需求適用於小型連線系統,如:路由器內部系統設定
在 CS:APP 第 12 章 (並行程式設計)中,提到接收多個 client 的連線方式,與 khttpd 的方式相近,架構圖:
server 開啟監聽過程,當有任何一個 client 請求連線時,server 就會 fork
行程去處理到對應的 client ,所以在處理的過程中,子行程只要對應 client 就好,不會干涉到其他子行程的運作(Address space
獨立),能完成多 client 的連線需求。
以 server 的角度就是持續的接受連線的請求
取得 kHTTPd 原始程式碼並編譯:
預期會見到執行檔 htstress
和核心模組 khttpd.ko
。接著可進行測試:
參考輸出:
這台電腦的實驗結果顯示,我們的 kHTTPd 每秒可處理超過 20K 個 HTTP 請求。上述實驗透過修改過的 htstress 工具得到,我們可拿來對 http://www.google.com
網址進行測試:
參考輸出:
kHTTPd 掛載時可指定 port 號碼: (預設是 port=8081
)
除了用網頁瀏覽器開啟,也可用 wget
工具:
參考 wget
執行輸出:
得到的 index.html
內容就是 Hello World!
字串。
下方命令可追蹤 kHTTPd 傾聽的 port 狀況:
注意,在多次透過網頁瀏覽器存取 kHTTPd 所建立的連線後,可能在 module unload 時,看到 dmesg
輸出以下:
htstress.c
流程htstress.c
為 client,做為發送給 server 的測試,未傳入參數時可以得到參數的設定模式,如下
對應在 script/test.sh
中的敘述:
-n
: 表示對 server 請求連線的數量-c
: 表示總體對 server 的連線數量-t
: 表示使用多少執行緒main
中主要建立與 server 的連線
getopt_long()
獲得輸入的參數,再透過 swtich
設定對應的變數getaddrinfo
取得多個 addrinfo
結構,裡面含有 server 的 IP 位址start_time()
紀錄時間,使用 gettimeofday()
計算運行時間pthread_create
創立參數所設定的執行數數量,執行 worker()
函式對應到每一個創建 client,發送連線請求給 server再來看到 worker()
函式,與 server 進行連線過程,分別要建立與 server 連線的 client 與 epoll 程序監聽
epoll_event
結構陣列儲存監聽資料,變數名稱為 evts[MAX_EVENT]
(MAX_EVENT 為設定監聽事件數量的最大值)epoll_create
(變數為 efd
)建立總體對 server 的 concurrency(1) 連線不過自從 Linux2.6.8 後 epoll_create 中 size 的引數是被忽略的,建立好後占用一個 fd,使用後必須呼叫 close() 關閉,否則會導致資源的浪費
init_conn()
,並設定 epoll 程序
struct econn ecs[concurrency], *ec
中,進行初始化將 efd(epoll fd) 與 socket(ecs) 傳入 init_conn()
中socket()
建立與 server 的連線,並返回 fd,傳入 ec->fd
中fctrl(ec->fd, F_SETFL, O_NONBLOCK)
將 socket 的 fd 更改為非阻塞式,相比於阻塞式的方式,不會因為讀取不到資料就會停著ec->fd
) 與 server 的 IP 地址連線,因為是 nonblocking 的型式,所以不會等待連線成功的時候才會返回,因此在未連線時會回傳一巨集 EAGAIN
表示未連線,所以將 connect()
在迴圈中執行到連線成功EPOLL_CTL_ADD
巨集加入監聽事件,並將 efd 事件設定為可寫的狀態,使用 EPOLLOUT
連線的初始化完成後,繼續看 worker()
處理 I/O 事件的無限 for-loop
epoll 監聽 :
epoll_wait
輪詢的方式將可用的 fd 儲存至 evts
陣列中htstress.c
中 evts.event
表示事件狀態的巨集:
epoll 的錯誤處理,以 if (evts[n].events & EPOLLERR){ ... }
判斷事件是否為錯誤狀態
SO_ERROR
紀錄錯誤訊息(0 為沒有錯誤的產生),看到宣告方式 if (getsockopt(efd, SOL_SOCKET, SO_ERROR, (void *) &error, &errlen) == 0)
,讀取到 efd
的資料將檢查的結果寫入至 error
變數中ISO/IEC 9899:2011 (P.283) : atomic_fetch function
These operations are atomic read-modify-write operations.
client 傳送數據至 server :
send()
函式開啟要傳送資料的 fd,再來傳送資料(包含傳送的資料與長度,以檔案的 offset 表示),傳送成功後會返回傳送資料的長度write()
,注意到 write
的第一個引數為 fd,這裡使用 2
,參考文章解釋,0 表示 STDIN
標準輸入(鍵盤),1 表示 STDOUT
標準輸出(終端機視窗),2 表示 STDERR
標準錯誤輸出(將錯誤訊息輸出至終端機))EPOLLIN
可讀的狀態,等待 server 傳送資料server 傳送數據至 client :
EPOLLIN
,使用 recv()
得到從 server 傳送來的資料,從 socket 的 fd 獲得,將獲得的資料存入 buffer(inbuf)
中。關閉 client 與 server 連線:
ret = 0
) 時,使用 close()
關閉 client 的 fd(要關閉否則會占用資源),這裡要注意的是在建立 epoll 監聽與 socket 連線,同時都要有對應的 close()
關閉其 fd,不過在 htstress.c
中沒有看到對 epoll 的 fd 進行 close()
的敘述。epoll_create(2) 提到:
When no longer required, the file descriptor returned by epoll_create() should be closed by using close(2). When all file descriptors referring to an epoll instance have been closed, the kernel destroys the instance and releases the associated resources for reuse.
(對應的epoll_create()
要透過close()
將 epoll fd 關閉,不過若 epoll 所監聽所有的 fd 已被關閉,核心就會直接釋放 epoll 的相關資源)
許多 Linux 裝置驅動程式或子系統會透過 kernel threads(簡稱kthread
),在背景執行提供特定服務,然後等待特定 events 的發生。等待的過程中,kthread 會進入 sleep 狀態,當 events 發生時,kthread 會被喚醒執行一些耗時的工作,如此一來,可防止 main thread 被 blocked。
使用示範: kernel-threads.c
kthread_run
巨集在 Linux v5.5 的定義 include/linux/kthread.h :
可見到 kthread_create
成功時直接 wake_up_process
,回傳值為 task_struct
。
下方命令可查閱系統上的 kthread:
預期可見:
PPID 為 2
的都屬於 kthread,而 $ ps auxf
可見樹狀結構。
參考輸出結果:
尋找剛才載入的 khttpd 核心模組:
預期可見以下: