Try   HackMD

N07: ktcp

主講人: jserv / 課程討論區: 2025 年系統軟體課程

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
返回「Linux 核心設計」課程進度表

解說影片

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
預期目標

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
kecho: 執行在 Linux 核心模式的 TCP 伺服器

取得 kecho 原始程式碼並編譯:

$ git clone https://github.com/sysprog21/kecho
$ cd kecho
$ make

預期會見到以下:

  • 執行檔: benchuser-echo-server
  • 核心模組 kecho.kodrop-tcp-socket.ko

接著可進行測試:

$ make check

參考輸出:

Preparing...
Send message via telnet
Progress : [########################################] 100%
Complete

該操作由以下動作組成:

$ 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)

$ sudo insmod kecho.ko port=1999

修改或測試 kecho 的過程,可能因為 TIME-WAIT sockets 持續佔用,導致 rmmod 無法成功,這時可透過給定的 drop-tcp-socket 核心模組來剔除特定的 TCP 連線。請詳細閱讀 kecho 以得知必要的設定和準備工作。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
user-echo-server: 執行於使用者層級的 TCP 伺服器

user-echo-serverkecho 的使用者層級的實作,可對照功能和比較效能,運用 epoll 系統呼叫,會傾聽 port 12345。

不管是 user-echo-server 抑或 kecho,都可搭配給定的 bench 程式來分析效能。請詳細閱讀 kecho 以得知必要的設定和準備工作。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
seHTTPd

seHTTPd 是個高效的 web 伺服器,涵蓋並行處理、I/O 事件模型、epoll, Reactor pattern,和 Web 伺服器在事件驅動架構的考量,可參見 高效 Web 伺服器開發

預先準備的套件: (eBPF 作為後續分析使用)

$ 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

取得程式碼和編譯:

$ git clone https://github.com/sysprog21/sehttpd
$ cd sehttpd
$ make

預期可見 sehttpd 這個執行檔。接著透過內建的 test suite 來測試:

$ make check

對 seHTTPd 進行壓力測試

首先,我們可用「古典」的方法,透過 Apache Benching tool 對 seHTTPd 進行壓力測試。在一個終端機視窗執行以下命令:

$ ./sehttpd

切換到網頁瀏覽器,開啟網址 http://127.0.0.1:8081/ 應在網頁瀏覽器畫面中見到以下輸出:

Welcome!
If you see this page, the seHTTPd web server is successfully working.

然後在另一個終端機視窗執行以下命令:

$ ab -n 10000 -c 500 -k http://127.0.0.1:8081/

參考程式輸出: (數值若跟你的測試結果有顯著出入,實屬正常)

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 說明。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
需要注意的是,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 參數的指定數值) 後,可能會遇到以下錯誤狀況 (部分)

  1. Segmentation fault;
  2. 顯示訊息 [ERROR] (src/http.c:253: errno: Resource temporarily unavailable) rc != 0

可用 $ grep -r log_err 搜尋原始程式碼,以得知現有的例外處理機制 (注意: 裡頭存在若干缺失,切勿「舉燭」)。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
eBPF 追蹤 HTTP 封包

研讀〈Linux 核心設計: 透過 eBPF 觀察作業系統行為 〉以理解核心動態追蹤機制,後者允許我們使用非侵入式的方式,不用去修改我們的作業系統核心內部,不用去修改我們的應用程式,也不用去修改我們的業務程式碼或者任何系統配置,就可快速高效地精確獲取我們想要的資訊。

seHTTPd 原始程式碼的 ebpf 目錄提供簡易的 HTTP 封包分析工具,就是建構在 eBPF 的基礎之上,並透過 IO Visor 提供的工具來運作。

概念示意圖:

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 →

使用方式: (預先在另一個終端機視窗執行 $ ./sehttpd)

$ cd ebpf
$ sudo python3 http-parse-sample.py

注意: 這個工具預設監控 eth0 這個網路介面 (network interface)。倘若你的預設網路介面不是 eth0,你需要依據 ip 工具的輸出,決定監控哪個網路介面。舉例來說,在某台 GNU/Linux 機器上執行以下命令:

$ ip link

你會見到若干輸出,如果你的環境裡頭已執行 Docker,輸出數量會很可觀,但不用擔心,排除 lo, tun, virbr, docker, br-, veth 開頭的輸出,然後就剩下 enp5s0 這樣的網路介面 (端視你的網路硬體而有不同),於是可將上述命令改為:

$ 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

撰寫 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

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
由於我們不直接撰寫 eBPF bytecode ,我們可透過 bcc 這個高階封裝的框架來幫忙,首先撰寫一個簡單的程式來監控 sync 系統呼叫,若系統呼叫了 sync 則印出訊息

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 。

再換一個測試程式,如下

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 檔案當中找到

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
網頁伺服器運作原理

務必參閱 CS:APP 第 11 章,以強化自身對電腦網路程式設計的認知。

網頁伺服器運作流程如下:

下圖說明程式面細節,左側為 client,右側為 server:

  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. 連線建立後,雙方透過 readwrite (或封裝如 rio,reliable I/O)進行資料交換
  8. 當 client 結束會送出 EOF,server 接收後關閉對該 client 的 socket
  9. server 關閉後可選擇再接受下一個 client,或關閉整體服務

上述流程僅適合單一 client 與單一 server,若需支援多個 client,可採用《CS:APP》第 12 章介紹的並行模型。在 CS:APP 第 12 章 (並行程式設計) 中,提到接收多個 client 的連線方式,與 khttpd 的方式相近,架構圖如下:

server 持續監聽,當偵測到 client 請求便 fork (或在核心空間建立 worker)處理個別連線。每個子行程或 worker 擁有獨立位址空間,因此彼此互不干擾,能滿足多 client 需求。從 server 角度來看,就是不斷接受連線並將處理工作分派給新的行程或執行緒。

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →
kHTTPd: 執行在 Linux 核心模式的 Web 伺服器

取得 kHTTPd 原始程式碼並編譯:

$ git clone https://github.com/sysprog21/khttpd
$ cd khttpd
$ make

預期會見到執行檔 htstress 和核心模組 khttpd.ko。接著可進行測試:

$ 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 工具得到,我們可拿來對 http://www.google.com 網址進行測試:

$ ./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)

$ sudo insmod khttpd.ko port=1999

除了用網頁瀏覽器開啟,也可用 wget 工具:

$ 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 狀況:

$ 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 流程

htstress.c 為 client,做為發送給 server 的測試,未傳入參數時可以得到參數的設定模式,如下

$./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 中的敘述:

./htstress -n 100000 -c 1 -t 4 http://localhost:8081/
  • -n : 表示對 server 請求連線的數量
  • -c : 表示總體對 server 的連線數量
  • -t : 表示使用多少執行緒

main 中主要建立與 server 的連線

  1. 設定參數 : 透過 getopt_long() 獲得輸入的參數,再透過 swtich 設定對應的變數
  2. 設定連線所需的資訊 : getaddrinfo 取得多個 addrinfo 結構,裡面含有 server 的 IP 位址
  3. 計算時間 : start_time() 紀錄時間,使用 gettimeofday() 計算運行時間
  4. 測試 server 連線 : 使用 pthread_create 創立參數所設定的執行數數量,執行 worker() 函式對應到每一個創建 client,發送連線請求給 server
  5. 印出測試結果

再來看到 worker() 函式,與 server 進行連線過程,分別要建立與 server 連線的 client 與 epoll 程序監聽

  • 建立 epoll_event 結構陣列儲存監聽資料,變數名稱為 evts[MAX_EVENT] (MAX_EVENT 為設定監聽事件數量的最大值)
struct epoll_event
{
    uint32_t events;	/* Epoll events */
    epoll_data_t data;	/* User data variable */
} __EPOLL_PACKED;
  • epoll_create (變數為 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() : file control,對 fd 更改特性,fctrl(ec->fd, F_SETFL, O_NONBLOCK) 將 socket 的 fd 更改為非阻塞式,相比於阻塞式的方式,不會因為讀取不到資料就會停著
    • connect() : 為系統呼叫,根據 socket 的 fd (ec->fd) 與 server 的 IP 地址連線,因為是 nonblocking 的型式,所以不會等待連線成功的時候才會返回,因此在未連線時會回傳一巨集 EAGAIN 表示未連線,所以將 connect() 在迴圈中執行到連線成功
    • epoll_ctl() : 將連線成功的 socket (ec->fd) 加入在 epoll 監聽事件 (efd) 中,所使用到 EPOLL_CTL_ADD 巨集加入監聽事件,並將 efd 事件設定為可寫的狀態,使用 EPOLLOUT
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 採輪詢方式等待事件,將所有已就緒的檔案描述子儲存至 evts 陣列
  • htstress.c 中,每個 evts[n].events 表示一個事件狀態,其常見旗標如下:
    • EPOLLIN:表示對應 fd 可讀
    • EPOLLOUT:表示對應 fd 可寫
    • EPOLLERR:表示對應 fd 發生錯誤
    • EPOLLHUP:表示對應 fd 已被中斷連線
  • getsockopt():用來取得指定 socket 的錯誤狀態。常與 SO_ERROR 搭配使用,若錯誤為 0 表示無異常。使用方式如下:
    ​​​​getsockopt(efd, SOL_SOCKET, SO_ERROR, (void *)&error, &errlen);
    
    若成功,錯誤代碼將寫入 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.

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() 函式透過對應的檔案描述子傳送資料。傳送內容包含資料指標與長度 (例如可根據檔案 offset),成功傳送後將回傳實際傳送的位元組數。
  • 若傳送失敗,應記錄錯誤訊息。可用 write() 將錯誤訊息寫至標準錯誤輸出 (fd = 2)。補充說明:
    • fd = 0:標準輸入 (STDIN)
    • fd = 1:標準輸出 (STDOUT)
    • fd = 2:標準錯誤 (STDERR)
  • 資料傳送完成後,需將事件狀態設為 EPOLLIN,表示進入可讀狀態,等待伺服器的回應。

server 傳送資料至 client:

  • 當事件狀態為 EPOLLIN (可讀)時,代表 client 已傳送資料至伺服器。此時使用 recv() 從 socket 對應的 fd 讀取資料,並將資料存入緩衝區 (如 inbuf)中。

關閉 client 與 server 的連線:

  • 當通訊結束 (例如 recv() 回傳值為 0,代表連線關閉),應使用 close() 關閉 client 的 fd,以釋放系統資源
  • 特別注意,無論是 epoll 所建立的監聽 fd 或 socket 連線的 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 的相關資源)

// 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

kthread_run 巨集在 Linux v6.8 的定義 include/linux/kthread.h:

#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:

$ 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 核心模組:

$ ps -ef | grep khttpd

預期可見以下:

root     18147     2  0 14:13 ?        00:00:00 [khttpd]