主講人: jserv / 課程討論區: 2025 年系統軟體課程
返回「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 bytecode,而是透過 Cilium, bcc 或 bpftrace 等工具提供一個 eBPF 的 high-level 封裝並使用這些工具撰寫 eBPF program ,透過編譯後才得到 eBPF bytecode 。在 Linux kernel 來說可以先透過 C 語言撰寫再透過 LLVM 將 C code 編譯為 eBPF bytecode 。
在 eBPF program 載入 Linux 核心前,需要二個步驟來確保它可以被掛載到 hook point 上。
eBPF Maps 作為一個非常重要的元件,提供整個系統共享資源的能力,不管是核心或使用者層級當中的 eBPF 程式都可透過系統呼叫存取 eBPF Maps ,而 Maps 由很多不同的資料結構組成,包括 hash table, LRU, Ring Buffer 等等。
eBPF 不能直接呼叫隨意的核心函式,否則會使 eBPF 程式和特定版本的核心過度關聯而喪失相容性,Linux 核心因此特別提供一組 API 稱為 helper functions 給 eBPF 程式,例如有以下幾種功能
eBPF program 由 tail calls 和 functions calls 組成。 function calls 使 eBPF 程式可定義並呼叫函式,Tail calls 則是可以呼叫並執行另外一個 eBPF 程式,被呼叫的程式會取代目前的 context 繼續執行,和 execve()
系統呼叫的概念類似。
bcc Python Developer Tutorial
由於我們不直接撰寫 eBPF bytecode ,我們可透過 bcc 這個高階封裝的框架來幫忙,首先撰寫一個簡單的程式來監控 sync
系統呼叫,若系統呼叫了 sync
則印出訊息
有幾點可以注意
text='...'
: 定義 BPF 程式,是用 C 語言寫的kprobe__sys_sync()
: kernel 利用 kprobes 達成動態追蹤的捷徑, kprobe__
後面的部分代表要追蹤的 kernel function ,在這個例子當中是 sys_sync()
。void *ctx
: context 的參數。.trace_print()
: 一個可以讀取 trace_pipe 並印出 output 的 bcc routine 。再換一個測試程式,如下
bpf_ktime_get_ns()
: 回傳以 nanoseconds 為單位的時間。BPF_HAS(last)
: 建立一個 BPF map object ,本質上是一個 associative array ,名稱為 last
,預設的 key, value 型態是 u64
key = 0
: 只會儲存一組 key/value pair ,而 key 被寫死為 0if (tsp != NULL)
: verifier 規定指標型態的變數都需要經過檢查才能使用last.delete(&key)
: 把 key 從 hash 當中刪除。last.update(&key, &ts)
: 更新 key 對應的 value可用的 event 可在
/sys/kernel/debug/tracing/available_filter_functions
檔案當中找到
務必參閱 CS:APP 第 11 章,以強化自身對電腦網路程式設計的認知。
網頁伺服器運作流程如下:
下圖說明程式面細節,左側為 client,右側為 server:
getaddrinfo()
解析位址並取得 struct addrinfo
,其中包含 IP、通訊埠與服務名稱等資訊socket()
建立通訊端,回傳一個檔案描述子;此步驟僅建立端點,不會觸發網路傳輸bind()
將 socket 與指定 IP 與通訊埠連結 (位於核心空間)listen()
,等待 client 連線accept()
接受連線並為該 client 建立新的 socketconnect()
發出連線要求並等待 server accept
read
/write
(或封裝如 rio,reliable I/O)進行資料交換上述流程僅適合單一 client 與單一 server,若需支援多個 client,可採用《CS:APP》第 12 章介紹的並行模型。在 CS:APP 第 12 章 (並行程式設計) 中,提到接收多個 client 的連線方式,與 khttpd 的方式相近,架構圖如下:
server 持續監聽,當偵測到 client 請求便 fork (或在核心空間建立 worker)處理個別連線。每個子行程或 worker 擁有獨立位址空間,因此彼此互不干擾,能滿足多 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()
在迴圈中執行到連線成功ec->fd
) 加入在 epoll 監聽事件 (efd) 中,所使用到 EPOLL_CTL_ADD
巨集加入監聽事件,並將 efd 事件設定為可寫的狀態,使用 EPOLLOUT
連線初始化完成後,接著由 worker()
處理 I/O 事件的主迴圈。
epoll 監聽:進入無窮迴圈處理所有連線請求,流程如下:
epoll_wait
採輪詢方式等待事件,將所有已就緒的檔案描述子儲存至 evts
陣列htstress.c
中,每個 evts[n].events
表示一個事件狀態,其常見旗標如下:
EPOLLIN
:表示對應 fd 可讀EPOLLOUT
:表示對應 fd 可寫EPOLLERR
:表示對應 fd 發生錯誤EPOLLHUP
:表示對應 fd 已被中斷連線getsockopt()
:用來取得指定 socket 的錯誤狀態。常與 SO_ERROR
搭配使用,若錯誤為 0 表示無異常。使用方式如下:
若成功,錯誤代碼將寫入 error
變數中atomic_fetch_add()
:使用 atomic 操作來統計錯誤發生次數。由於多執行緒環境下多個 socket 可能同時出錯,需使用 atomic 確保計數正確close()
:當偵測到錯誤連線後,立即關閉對應 socket,避免持續佔用系統資源。這是釋放失效連線最直接有效的方式。ISO/IEC 9899:2011 (P.283) : atomic_fetch function
These operations are atomic read-modify-write operations.
client 傳送資料至 server:
EPOLLOUT
(可寫)時,表示對應的 socket 已準備好進行資料傳送。此時應先確認連線仍為有效狀態,接著使用 send()
函式透過對應的檔案描述子傳送資料。傳送內容包含資料指標與長度 (例如可根據檔案 offset),成功傳送後將回傳實際傳送的位元組數。write()
將錯誤訊息寫至標準錯誤輸出 (fd = 2)。補充說明:
EPOLLIN
,表示進入可讀狀態,等待伺服器的回應。server 傳送資料至 client:
EPOLLIN
(可讀)時,代表 client 已傳送資料至伺服器。此時使用 recv()
從 socket 對應的 fd 讀取資料,並將資料存入緩衝區 (如 inbuf
)中。關閉 client 與 server 的連線:
recv()
回傳值為 0,代表連線關閉),應使用 close()
關閉 client 的 fd,以釋放系統資源close()
適當關閉。根據 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 v6.8 的定義 include/linux/kthread.h:
可見到 kthread_create
成功時直接 wake_up_process
,回傳值為 task_struct
。
下方命令可查閱系統上的 kthread:
預期可見:
PPID 為 2
的都屬於 kthread,而 $ ps auxf
可見樹狀結構。
參考輸出結果:
尋找剛才載入的 khttpd 核心模組:
預期可見以下: