--- title: 2025 年 Linux 核心設計課程作業 —— ktcp image: https://hackmd.io/_uploads/HybjLFz1ll.png description: 檢驗學員對 Linux 核心 kthread 和 workqueue 處理機制的認知 tags: linux2025 --- # N07: ktcp > 主講人: [jserv](https://wiki.csie.ncku.edu.tw/User/jserv) / 課程討論區: [2025 年系統軟體課程](https://www.facebook.com/groups/system.software2025/) :mega: 返回「[Linux 核心設計](https://wiki.csie.ncku.edu.tw/linux/schedule)」課程進度表 ==[解說影片](https://youtu.be/zxGEER-1ru8)== ## :memo: 預期目標 * 學習〈[Linux 核心設計: 針對事件驅動的 I/O 模型演化](https://hackmd.io/@sysprog/linux-io-model)〉 * 探討電腦網路原理和 TCP 伺服器開發議題 * 學習 Linux 核心的 kernel thread 和 workqueue 處理機制 * 學習 [Concurrency Managed Workqueue](https://www.kernel.org/doc/html/latest/core-api/workqueue.html) (cmwq),搭配閱讀《Demystifying the Linux CPU Scheduler》第 1 章和第 2 章,得知 CPU 排程器和 workqueue/CMWQ 的互動 * 學習 [Ftrace](https://docs.kernel.org/trace/ftrace.html),搭配閱讀《Demystifying the Linux CPU Scheduler》第 6 章 ## :rocket: `kecho`: 執行在 Linux 核心模式的 TCP 伺服器 取得 kecho 原始程式碼並編譯: ```shell $ git clone https://github.com/sysprog21/kecho $ cd kecho $ make ``` 預期會見到以下: * 執行檔: `bench` 及 `user-echo-server` * 核心模組 `kecho.ko` 及 `drop-tcp-socket.ko` 接著可進行測試: ```shell $ make check ``` 參考輸出: ``` Preparing... Send message via telnet Progress : [########################################] 100% Complete ``` 該操作由以下動作組成: ```shell $ sudo insmod kecho.ko $ telnet localhost 12345 ``` 會出現以下輸出: ``` Trying ::1... Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. ``` 可輸入任何字元 (記得按下 Enter),然後就會看到 telnet 回應你剛才輸入的字元。 按下 `Ctrl` 和 `]` 組合鍵,之後按下 `q`,即可離開 telnet 畫面。接著可以試著在 `$ telnet localhost 12345` 時不要輸入任何字元,只是等待,會看到以下的 kernel 訊息 (可用 `$ dmesg` 觀察): ``` cope le: 4404 kB RssShmem: 0 kB VmData: 880 kB VmStk: 132 kB VmExe: 136 kB VmLib: 6336 kB VmPTE: 212 kB VmSwap: 0 kB HugetlbPages: 0 kB CoreDumping: 0 Threads: 1 SigQ: 0/31543 ``` kecho 掛載時可指定 port 號碼: (預設是 `port=12345`) ```shell $ sudo insmod kecho.ko port=1999 ``` 修改或測試 kecho 的過程,可能因為 `TIME-WAIT` sockets 持續佔用,導致 `rmmod` 無法成功,這時可透過給定的 `drop-tcp-socket` 核心模組來剔除特定的 TCP 連線。請詳細閱讀 [kecho](https://github.com/sysprog21/kecho) 以得知必要的設定和準備工作。 ## :house: `user-echo-server`: 執行於使用者層級的 TCP 伺服器 `user-echo-server` 是 `kecho` 的使用者層級的實作,可對照功能和比較效能,運用 [epoll](http://man7.org/linux/man-pages/man7/epoll.7.html) 系統呼叫,會傾聽 port 12345。 不管是 `user-echo-server` 抑或 `kecho`,都可搭配給定的 `bench` 程式來分析效能。請詳細閱讀 [kecho](https://github.com/sysprog21/kecho) 以得知必要的設定和準備工作。 ## :shark: seHTTPd [seHTTPd](https://github.com/sysprog21/sehttpd) 是個高效的 web 伺服器,涵蓋並行處理、I/O 事件模型、epoll, [Reactor pattern](http://en.wikipedia.org/wiki/Reactor_pattern),和 Web 伺服器在事件驅動架構的考量,可參見 [高效 Web 伺服器開發](https://hackmd.io/@sysprog/fast-web-server)。 預先準備的套件: (eBPF 作為後續分析使用) ```shell $ sudo apt install wget $ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 4052245BD4284CDD $ echo "deb https://repo.iovisor.org/apt/$(lsb_release -cs) $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/iovisor.list $ sudo apt-get update $ sudo apt-get install bcc-tools linux-headers-$(uname -r) $ sudo apt install apache2-utils ``` 取得程式碼和編譯: ```shell $ git clone https://github.com/sysprog21/sehttpd $ cd sehttpd $ make ``` 預期可見 `sehttpd` 這個執行檔。接著透過內建的 test suite 來測試: ```shell $ make check ``` ### 對 seHTTPd 進行壓力測試 首先,我們可用「古典」的方法,透過 [Apache Benching tool](https://httpd.apache.org/docs/current/programs/ab.html) 對 seHTTPd 進行壓力測試。在一個終端機視窗執行以下命令: ```shell $ ./sehttpd ``` 切換到網頁瀏覽器,開啟網址 `http://127.0.0.1:8081/` 應在網頁瀏覽器畫面中見到以下輸出: :::info Welcome! If you see this page, the seHTTPd web server is successfully working. ::: 然後在另一個終端機視窗執行以下命令: ```shell $ ab -n 10000 -c 500 -k http://127.0.0.1:8081/ ``` 參考程式輸出: (數值若跟你的測試結果有顯著出入,實屬正常) ```shell Server Software: seHTTPd Server Hostname: 127.0.0.1 Server Port: 8081 Document Path: / Document Length: 241 bytes Concurrency Level: 500 Time taken for tests: 0.927 seconds Complete requests: 10000 Failed requests: 0 Keep-Alive requests: 10000 Total transferred: 4180000 bytes HTML transferred: 2410000 bytes Requests per second: 10784.81 [#/sec] (mean) Time per request: 46.361 [ms] (mean) Time per request: 0.093 [ms] (mean, across all concurrent requests) Transfer rate: 4402.39 [Kbytes/sec] received ``` 留意到上述幾項: * `-k` 參數: 表示 "Enable the HTTP KeepAlive feature",也就是在一個 HTTP session 中執行多筆請求 * `-c` 參數: 表示 concurrency,即同時要下達請求的數量 * `-n` 參數: 表示壓力測試過程中,期望下達的請求總量 關於輸出結果,請詳閱 [ab - Apache HTTP server benchmarking tool](https://httpd.apache.org/docs/current/programs/ab.html) 說明。 :notebook: 需要注意的是,`ab` 無法有效反映出多執行緒的特性 (`ab` 自身就消耗單核 100% 的運算量),因此我們才會在 [khttpd](https://github.com/sysprog21/khttpd) 提供 [htstress.c](https://github.com/sysprog21/khttpd/blob/master/htstress.c),後者提供 `-t` 選項,能夠依據測試環境的有效 CPU 個數進行分配。 [ab - Apache HTTP server benchmarking tool](https://httpd.apache.org/docs/current/programs/ab.html) 的實作從今日的 GNU/Linux 或 FreeBSD 來說,算是過時且未能反映系統特性,除了 `htstress`,尚可使用 [wrk](https://github.com/wg/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](https://github.com/httperf/httperf)。 ### 例外處理 倘若你將 seHTTPd 執行後,不立刻關閉,隨即較長時間的等待和重新用上述 `ab` 多次測試 (變更 `-n` 和 `-c` 參數的指定數值) 後,可能會遇到以下錯誤狀況 (部分) 1. Segmentation fault; 2. 顯示訊息 `[ERROR] (src/http.c:253: errno: Resource temporarily unavailable) rc != 0` 可用 `$ grep -r log_err` 搜尋原始程式碼,以得知現有的例外處理機制 (注意: 裡頭存在若干缺失,切勿「舉燭」)。 ### :microscope: 以 [eBPF](https://en.wikipedia.org/wiki/Berkeley_Packet_Filter) 追蹤 HTTP 封包 研讀〈[Linux 核心設計: 透過 eBPF 觀察作業系統行為](https://hackmd.io/@sysprog/linux-ebpf) 〉以理解核心動態追蹤機制,後者允許我們使用非侵入式的方式,不用去修改我們的作業系統核心內部,不用去修改我們的應用程式,也不用去修改我們的業務程式碼或者任何系統配置,就可快速高效地精確獲取我們想要的資訊。 在 [seHTTPd](https://github.com/sysprog21/sehttpd) 原始程式碼的 [ebpf 目錄](https://github.com/sysprog21/sehttpd/tree/master/ebpf)提供簡易的 HTTP 封包分析工具,就是建構在 eBPF 的基礎之上,並透過 [IO Visor](https://github.com/iovisor) 提供的工具來運作。 概念示意圖: ![image](https://hackmd.io/_uploads/rJTKJoJJlg.png) 使用方式: (預先在另一個終端機視窗執行 `$ ./sehttpd`) ```shell $ cd ebpf $ sudo python3 http-parse-sample.py ``` 注意: 這個工具預設監控 `eth0` 這個網路介面 (network interface)。倘若你的預設網路介面不是 `eth0`,你需要依據 `ip` 工具的輸出,決定監控哪個網路介面。舉例來說,在某台 GNU/Linux 機器上執行以下命令: ```shell $ ip link ``` 你會見到若干輸出,如果你的環境裡頭已執行 [Docker](https://www.docker.com/),輸出數量會很可觀,但不用擔心,排除 `lo`, `tun`, `virbr`, `docker`, `br-`, `veth` 開頭的輸出,然後就剩下 `enp5s0` 這樣的網路介面 (端視你的網路硬體而有不同),於是可將上述命令改為: ```shell $ sudo python3 http-parse-sample.py -i enp5s0 ``` 然後打開網頁瀏覽器,多次存取和刷新 `http://127.0.0.1:8081/` 網址,然後你應可在上述執行 Python 程式的終端機見到類似以下的輸出: ``` TCP src port '51670' TCP dst port '8081' ¢GET / HTTP/1.1 IP hdr length '20' IP src '192.168.50.97' IP dst '61.70.212.51' TCP src port '8081' TCP dst port '51670' ÌHTTP/1.1 304 Not Modified ``` 關於上述程式運作的概況,可參考 [Appendix C](https://hackmd.io/@0xff07/r1f4B8aGI)。 ### 撰寫 eBPF program 多數來說我們不會直接撰寫 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 上。 1. Verification 確保 eBPF 的運行是安全的,它會驗證程式的幾個特性例如: * 確保要載入 eBPF program 的行程是具有特權的 (priviledge) 。除非特別註明非特權行程也能載入 eBPF program ,否則只有具特權的程式可以。 * 確保程式不會 crash * 確保程式總是會執行完畢 (不存在無窮迴圈等等) 2. JIT Compilation 將 generic bytecode 轉譯為 machine specific instruction,改進執行速度,使得 eBPF 程式的運行跟核心本體一樣有效率。 ### Maps eBPF Maps 作為一個非常重要的元件,提供整個系統共享資源的能力,不管是核心或使用者層級當中的 eBPF 程式都可透過系統呼叫存取 eBPF Maps ,而 Maps 由很多不同的資料結構組成,包括 hash table, LRU, Ring Buffer 等等。 ![image](https://hackmd.io/_uploads/B1aNmjyJle.png) ### Helper call eBPF 不能直接呼叫隨意的核心函式,否則會使 eBPF 程式和特定版本的核心過度關聯而喪失相容性,Linux 核心因此特別提供一組 API 稱為 helper functions 給 eBPF 程式,例如有以下幾種功能 * Generate random numbers * Get current time & date * eBPF map access * Get process/cgroup context * Manipulate network packets and forwarding logic ### Tail & Function Calls eBPF program 由 tail calls 和 functions calls 組成。 function calls 使 eBPF 程式可定義並呼叫函式,Tail calls 則是可以呼叫並執行另外一個 eBPF 程式,被呼叫的程式會取代目前的 context 繼續執行,和 `execve()` 系統呼叫的概念類似。 ### bcc python tutorial [bcc Python Developer Tutorial](https://github.com/iovisor/bcc/blob/master/docs/tutorial_bcc_python_developer.md) 由於我們不直接撰寫 eBPF bytecode ,我們可透過 bcc 這個高階封裝的框架來幫忙,首先撰寫一個簡單的程式來監控 `sync` 系統呼叫,若系統呼叫了 `sync` 則印出訊息 ```python from bcc import BPF BPF(text='int kprobe__sys_sync(void *ctx) { bpf_trace_printk("Tracing sys_sync()... Ctrl-C to end\\n"); return 0; }').trace_print() ``` 有幾點可以注意 * `text='...'` : 定義 BPF 程式,是用 C 語言寫的 * `kprobe__sys_sync()` : kernel 利用 kprobes 達成動態追蹤的捷徑, `kprobe__` 後面的部分代表要追蹤的 kernel function ,在這個例子當中是 `sys_sync()` 。 * `void *ctx` : context 的參數。 * `.trace_print()` : 一個可以讀取 trace_pipe 並印出 output 的 bcc routine 。 再換一個測試程式,如下 ```python from __future__ import print_function from bcc import BPF from bcc.utils import printb # load BPF program b = BPF(text=""" #include <uapi/linux/ptrace.h> BPF_HASH(last); int do_trace(struct pt_regs *ctx) { u64 ts, *tsp, delta, key = 0; // attempt to read stored timestamp tsp = last.lookup(&key); if (tsp != NULL) { delta = bpf_ktime_get_ns() - *tsp; if (delta < 1000000000) { // output if time is less than 1 second bpf_trace_printk("%d\\n", delta / 1000000); } last.delete(&key); } // update stored timestamp ts = bpf_ktime_get_ns(); last.update(&key, &ts); return 0; } """) b.attach_kprobe(event=b.get_syscall_fnname("sync"), fn_name="do_trace") print("Tracing for quick sync's... Ctrl-C to end") # format output start = 0 while 1: try: (task, pid, cpu, flags, ts, ms) = b.trace_fields() if start == 0: start = ts ts = ts - start printb(b"At time %.2f s: multiple syncs detected, last %s ms ago" % (ts, ms)) except KeyboardInterrupt: exit() ``` * `bpf_ktime_get_ns()` : 回傳以 nanoseconds 為單位的時間。 * `BPF_HAS(last)` : 建立一個 BPF map object ,本質上是一個 associative array ,名稱為 `last` ,預設的 key, value 型態是 `u64` * `key = 0` : 只會儲存一組 key/value pair ,而 key 被寫死為 0 * `if (tsp != NULL)` : verifier 規定指標型態的變數都需要經過檢查才能使用 * `last.delete(&key)` : 把 key 從 hash 當中刪除。 * `last.update(&key, &ts)` : 更新 key 對應的 value > 可用的 event 可在 `/sys/kernel/debug/tracing/available_filter_functions` 檔案當中找到 ## :icecream: 網頁伺服器運作原理 務必參閱 [CS:APP 第 11 章](https://hackmd.io/@sysprog/CSAPP-ch11),以強化自身對電腦網路程式設計的認知。 網頁伺服器運作流程如下: ![](https://hackmd.io/_uploads/B1_un3szn.png) 下圖說明程式面細節,左側為 client,右側為 server: ![](https://hackmd.io/_uploads/rk0Y3hif3.png) 1. 雙方先呼叫 `getaddrinfo()` 解析位址並取得 `struct addrinfo`,其中包含 IP、通訊埠與服務名稱等資訊 2. 接著透過 `socket()` 建立通訊端,回傳一個檔案描述子;此步驟僅建立端點,不會觸發網路傳輸 3. server 呼叫 `bind()` 將 socket 與指定 IP 與通訊埠連結 (位於核心空間) 4. server 進入監聽狀態並呼叫 `listen()`,等待 client 連線 5. server 使用 `accept()` 接受連線並為該 client 建立新的 socket 6. client 端則在 `connect()` 發出連線要求並等待 server `accept` 7. 連線建立後,雙方透過 `read`/`write` (或封裝如 rio,reliable I/O)進行資料交換 8. 當 client 結束會送出 EOF,server 接收後關閉對該 client 的 socket 9. server 關閉後可選擇再接受下一個 client,或關閉整體服務 上述流程僅適合單一 client 與單一 server,若需支援多個 client,可採用《CS:APP》第 12 章介紹的並行模型。在 CS:APP 第 12 章 (並行程式設計) 中,提到接收多個 client 的連線方式,與 khttpd 的方式相近,架構圖如下: ![](https://hackmd.io/_uploads/SyMih2jMn.png) server 持續監聽,當偵測到 client 請求便 fork (或在核心空間建立 worker)處理個別連線。每個子行程或 worker 擁有獨立位址空間,因此彼此互不干擾,能滿足多 client 需求。從 server 角度來看,就是不斷接受連線並將處理工作分派給新的行程或執行緒。 ## :rocket: kHTTPd: 執行在 Linux 核心模式的 Web 伺服器 取得 kHTTPd 原始程式碼並編譯: ```shell $ git clone https://github.com/sysprog21/khttpd $ cd khttpd $ make ``` 預期會見到執行檔 `htstress` 和核心模組 `khttpd.ko`。接著可進行測試: ```shell $ make check ``` 參考輸出: ``` 0 requests 10000 requests 20000 requests 30000 requests 40000 requests 50000 requests 60000 requests 70000 requests 80000 requests 90000 requests requests: 100000 good requests: 100000 [100%] bad requests: 0 [0%] socker errors: 0 [0%] seconds: 4.722 requests/sec: 21177.090 ``` 這台電腦的實驗結果顯示,我們的 kHTTPd 每秒可處理超過 20K 個 HTTP 請求。上述實驗透過修改過的 [htstress](https://github.com/arut/htstress) 工具得到,我們可拿來對 `http://www.google.com` 網址進行測試: ```shell $ ./htstress -n 1000 -c 1 -t 4 http://www.google.com/ ``` 參考輸出: ``` requests: 1000 good requests: 1000 [100%] bad requests: 0 [0%] socker errors: 0 [0%] seconds: 17.539 requests/sec: 57.015 ``` kHTTPd 掛載時可指定 port 號碼: (預設是 `port=8081`) ```shell $ sudo insmod khttpd.ko port=1999 ``` 除了用網頁瀏覽器開啟,也可用 `wget` 工具: ```shell $ wget localhost:1999 ``` 參考 `wget` 執行輸出: ``` Resolving localhost (localhost)... ::1, 127.0.0.1 Connecting to localhost (localhost)|::1|:1999... failed: Connection refused. Connecting to localhost (localhost)|127.0.0.1|:1999... connected. HTTP request sent, awaiting response... 200 OK Length: 12 [text/plain] Saving to: 'index.html' ``` 得到的 `index.html` 內容就是 `Hello World!` 字串。 下方命令可追蹤 kHTTPd 傾聽的 port 狀況: ```shell $ sudo netstat -apn | grep 8081 ``` 注意,在多次透過網頁瀏覽器存取 kHTTPd 所建立的連線後,可能在 module unload 時,看到 `dmesg` 輸出以下: ``` CPU: 37 PID: 78277 Comm: khttpd Tainted: G D WC OE K 4.15.0-91-generic #92-Ubuntu Hardware name: System manufacturer System Product Name/ROG STRIX X399-E GAMING, BIOS 0808 10/12/2018 RIP: 0010:0xffffffffc121b845 RSP: 0018:ffffabfc9cf47d68 EFLAGS: 00010282 RAX: 0000000000000000 RBX: ffff8f1bc898a000 RCX: 0000000000000218 RDX: 0000000000000000 RSI: 0000000000000000 RDI: ffffffff84c5107f RBP: ffffabfc9cf47dd8 R08: ffff8f14c7e34e00 R09: 0000000180400024 R10: ffff8f152697cfc0 R11: 000499a097a8e100 R12: ffff8f152697cfc0 R13: ffffabfc9a6afe10 R14: ffff8f152697cfc0 R15: ffff8f1d4a5f8000 FS: 0000000000000000(0000) GS:ffff8f154f540000(0000) knlGS:0000000000000000 CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 CR2: ffffffffc121b845 CR3: 000000138b00a000 CR4: 00000000003406e0 Call Trace: ? __schedule+0x256/0x880 ? kthread+0x121/0x140 ? kthread_create_worker_on_cpu+0x70/0x70 ? ret_from_fork+0x22/0x40 Code: Bad RIP value. RIP: 0xffffffffc121b845 RSP: ffffabfc9cf47d68 CR2: ffffffffc121b845 ---[ end trace 76d6d2ce81c97c71 ]--- ``` ### [`htstress.c`](https://github.com/sysprog21/khttpd/blob/master/htstress.c) 流程 `htstress.c` 為 client,做為發送給 server 的測試,未傳入參數時可以得到參數的設定模式,如下 ```shell $./htstress Usage: htstress [options] [http://]hostname[:port]/path Options: -n, --number total number of requests (0 for inifinite, Ctrl-C to abort) -c, --concurrency number of concurrent connections -t, --threads number of threads (set this to the number of CPU cores) -u, --udaddr path to unix domain socket -h, --host host to use for http request -d, --debug debug HTTP response --help display this message ``` 對應在 `script/test.sh` 中的敘述: ```shell ./htstress -n 100000 -c 1 -t 4 http://localhost:8081/ ``` * `-n` : 表示對 server 請求連線的數量 * `-c` : 表示總體對 server 的連線數量 * `-t` : 表示使用多少執行緒 `main` 中主要建立與 server 的連線 1. 設定參數 : 透過 [`getopt_long()`](https://linux.die.net/man/3/getopt_long) 獲得輸入的參數,再透過 `swtich` 設定對應的變數 2. 設定連線所需的資訊 : [`getaddrinfo`](https://man7.org/linux/man-pages/man3/getaddrinfo.3.html) 取得多個 `addrinfo` 結構,裡面含有 server 的 IP 位址 3. 計算時間 : `start_time()` 紀錄時間,使用 [`gettimeofday()`](https://man7.org/linux/man-pages/man2/gettimeofday.2.html) 計算運行時間 4. 測試 server 連線 : 使用 [`pthread_create`](https://man7.org/linux/man-pages/man3/pthread_create.3.html) 創立參數所設定的執行數數量,執行 `worker()` 函式對應到每一個創建 client,發送連線請求給 server 5. 印出測試結果 再來看到 `worker()` 函式,與 server 進行連線過程,分別要建立與 server 連線的 client 與 epoll 程序監聽 * 建立 `epoll_event` 結構陣列儲存監聽資料,變數名稱為 `evts[MAX_EVENT]` (MAX_EVENT 為設定監聽事件數量的最大值) ```c struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ } __EPOLL_PACKED; ``` * [`epoll_create`](https://man7.org/linux/man-pages/man2/epoll_create.2.html) (變數為 `efd`)建立總體對 server 的 concurrency(1) 連線 > 不過自從 Linux2.6.8 後 epoll_create 中 size 的引數是被忽略的,建立好後占用一個 fd,使用後必須呼叫 close() 關閉,否則會導致資源的浪費 * socket 連線方式,定義於函式 `init_conn()`,並設定 epoll 程序 * socket 連線定義於 `struct econn ecs[concurrency], *ec` 中,進行初始化將 efd(epoll fd) 與 socket(ecs) 傳入 `init_conn()` 中 * 先透過 `socket()` 建立與 server 的連線,並返回 fd,傳入 `ec->fd` 中 * [fcntl()](https://man7.org/linux/man-pages/man2/fcntl.2.html) : file control,對 fd 更改特性,`fctrl(ec->fd, F_SETFL, O_NONBLOCK)` 將 socket 的 fd 更改為非阻塞式,相比於阻塞式的方式,不會因為讀取不到資料就會停著 * [connect()](https://man7.org/linux/man-pages/man2/connect.2.html) : 為系統呼叫,根據 socket 的 fd (`ec->fd`) 與 server 的 IP 地址連線,因為是 nonblocking 的型式,所以不會等待連線成功的時候才會返回,因此在未連線時會回傳一巨集 `EAGAIN` 表示未連線,所以將 `connect()` 在迴圈中執行到連線成功 * [epoll_ctl()](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html) : 將連線成功的 socket (`ec->fd`) 加入在 epoll 監聽事件 (efd) 中,所使用到 `EPOLL_CTL_ADD` 巨集加入監聽事件,並將 efd 事件設定為可寫的狀態,使用 `EPOLLOUT` ```c static void init_conn(int efd, struct econn *ec) { int ret; // 建立連線 ec->fd = socket(sss.ss_family, SOCK_STREAM, 0); ... // 設定 fd 控制權為 nonblock 形式 fcntl(ec->fd, F_SETFL, O_NONBLOCK); // sys call 連線 do { ret = connect(ec->fd, (struct sockaddr *) &sss, sssln); } while (ret && errno == EAGAIN); ... // 設定 epoll fd 的事件狀態,並指向 socket struct epoll_event evt = { .events = EPOLLOUT, .data.ptr = ec, }; // 加入已完成連線的 socket 加入 epoll 監聽程序中 if (epoll_ctl(efd, EPOLL_CTL_ADD, ec->fd, &evt)) { ... } } ``` 連線初始化完成後,接著由 `worker()` 處理 I/O 事件的主迴圈。 epoll 監聽:進入無窮迴圈處理所有連線請求,流程如下: - 使用 [`epoll_wait`](https://man7.org/linux/man-pages/man2/epoll_wait.2.html) 採輪詢方式等待事件,將所有已就緒的檔案描述子儲存至 `evts` 陣列 - 在 `htstress.c` 中,每個 `evts[n].events` 表示一個事件狀態,其常見旗標如下: - `EPOLLIN`:表示對應 fd 可讀 - `EPOLLOUT`:表示對應 fd 可寫 - `EPOLLERR`:表示對應 fd 發生錯誤 - `EPOLLHUP`:表示對應 fd 已被中斷連線 - [`getsockopt()`](https://man7.org/linux/man-pages/man2/getsockopt.2.html):用來取得指定 socket 的錯誤狀態。常與 `SO_ERROR` 搭配使用,若錯誤為 0 表示無異常。使用方式如下: ```c getsockopt(efd, SOL_SOCKET, SO_ERROR, (void *)&error, &errlen); ``` 若成功,錯誤代碼將寫入 `error` 變數中 - [`atomic_fetch_add()`](https://en.cppreference.com/w/c/atomic/atomic_fetch_add):使用 atomic 操作來統計錯誤發生次數。由於多執行緒環境下多個 socket 可能同時出錯,需使用 atomic 確保計數正確 - [`close()`](https://man7.org/linux/man-pages/man2/close.2.html):當偵測到錯誤連線後,立即關閉對應 socket,避免持續佔用系統資源。這是釋放失效連線最直接有效的方式。 > [ISO/IEC 9899:2011 (P.283) : atomic_fetch function](chrome-extension://efaidnbmnnnibpcajpcglclefindmkaj/https://www.open-std.org/JTC1/SC22/WG14/www/docs/n1548.pdf) > These operations are atomic read-modify-write operations. ```c if (evts[n].events & EPOLLERR) { /* normally this should not happen */ ... if (getsockopt(efd, SOL_SOCKET, SO_ERROR, (void *) &error, &errlen) == 0) {...} ... // 計數錯誤 atomic_fetch_add(&socket_errors, 1); // 關閉有錯誤的 socket fd close(ec->fd); ... // 重新初始化連線 init_conn(efd, ec); } ``` client 傳送資料至 server: - 當事件狀態為 `EPOLLOUT` (可寫)時,表示對應的 socket 已準備好進行資料傳送。此時應先確認連線仍為有效狀態,接著使用 [`send()`](https://man7.org/linux/man-pages/man2/send.2.html) 函式透過對應的檔案描述子傳送資料。傳送內容包含資料指標與長度 (例如可根據檔案 offset),成功傳送後將回傳實際傳送的位元組數。 - 若傳送失敗,應記錄錯誤訊息。可用 [`write()`](https://man7.org/linux/man-pages/man2/write.2.html) 將錯誤訊息寫至標準錯誤輸出 (fd = 2)。補充說明: - fd = 0:標準輸入 (STDIN) - fd = 1:標準輸出 (STDOUT) - fd = 2:標準錯誤 (STDERR) - 資料傳送完成後,需將事件狀態設為 `EPOLLIN`,表示進入可讀狀態,等待伺服器的回應。 server 傳送資料至 client: - 當事件狀態為 `EPOLLIN` (可讀)時,代表 client 已傳送資料至伺服器。此時使用 [`recv()`](https://man7.org/linux/man-pages/man2/recv.2.html) 從 socket 對應的 fd 讀取資料,並將資料存入緩衝區 (如 `inbuf`)中。 關閉 client 與 server 的連線: - 當通訊結束 (例如 `recv()` 回傳值為 0,代表連線關閉),應使用 [`close()`](https://man7.org/linux/man-pages/man2/close.2.html) 關閉 client 的 fd,以釋放系統資源 - 特別注意,無論是 epoll 所建立的監聽 fd 或 socket 連線的 fd,在使用完畢後皆應呼叫 `close()` 適當關閉。根據 `htstress.c` 的實作,epoll 所使用的 fd 目前尚未看到明確的 `close()` 處理,可能導致資源洩漏 > [epoll_create(2)](https://man7.org/linux/man-pages/man2/epoll_create.2.html) 提到: > 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 的相關資源) ```c // client 傳送訊息,確認事件狀態為可寫 if (evts[n].events & EPOLLOUT) { ret = send(ec->fd, outbuf + ec->offs, outbufsize - ec->offs, 0); ... // 將錯誤訊息存入 if (debug & HTTP_REQUEST_DEBUG) write(2, outbuf + ec->offs, outbufsize - ec->offs); ... /* write done? schedule read */ if (ec->offs == outbufsize) { evts[n].events = EPOLLIN; evts[n].data.ptr = ec; ... // 事件可讀狀態 if (evts[n].events & EPOLLIN) { ... // 獲得從 server 傳來的資料 ret = recv(ec->fd, inbuf, sizeof(inbuf), 0); ... // 所有請求處理結束 if (!ret) { // 關閉 socket 連線 close(ec->fd); ... } ``` ### 核心 API 許多 Linux 裝置驅動程式或子系統會透過 kernel threads (簡稱`kthread`),在背景執行提供特定服務,然後等待特定 events 的發生。等待的過程中,kthread 會進入 sleep 狀態,當 events 發生時,kthread 會被喚醒執行一些耗時的工作,如此一來,可防止 main thread 被 blocked。 使用示範: [kernel-threads.c](https://github.com/muratdemirtas/Linux-Kernel-Examples/blob/master/kernel-threads.c) `kthread_run` 巨集在 Linux v6.8 的定義 [include/linux/kthread.h](https://elixir.bootlin.com/linux/v6.8/source/include/linux/kthread.h#L43): ```c #define kthread_run(threadfn, data, namefmt, ...) \ ({ \ struct task_struct *__k \ = kthread_create(threadfn, data, namefmt, ## __VA_ARGS__); \ if (!IS_ERR(__k)) \ wake_up_process(__k); \ __k; \ }) ``` 可見到 `kthread_create` 成功時直接 `wake_up_process`,回傳值為 `task_struct`。 下方命令可查閱系統上的 kthread: ```shell $ ps -ef ``` 預期可見: ``` root 2 0 0 Feb17 ? 00:00:01 [kthreadd] ``` PPID 為 `2` 的都屬於 kthread,而 `$ ps auxf` 可見樹狀結構。 參考輸出結果: ``` 0:01 /usr/sbin/sshd -D 0:00 \_ sshd: jserv [priv] 0:05 | \_ sshd: jserv@pts/11 0:03 | \_ -bash 0:00 | \_ ps auxf 0:00 | \_ less ``` 尋找剛才載入的 khttpd 核心模組: ```shell $ ps -ef | grep khttpd ``` 預期可見以下: ``` root 18147 2 0 14:13 ? 00:00:00 [khttpd] ```