--- title: 2024 年 Linux 核心設計/實作課程作業 —— ktcp image: https://repository-images.githubusercontent.com/181623502/5a221200-560c-11ea-8a63-53e08f8c367c description: 檢驗學員對 Linux 核心 kthread 和 workqueue 處理機制的認知 tags: linux2024 --- # L11: ktcp > 主講人: [jserv](https://wiki.csie.ncku.edu.tw/User/jserv) / 課程討論區: [2024 年系統軟體課程](https://www.facebook.com/groups/system.software2024/) :mega: 返回「[Linux 核心設計/實作](https://wiki.csie.ncku.edu.tw/linux/schedule)」課程進度表 ==[解說錄影](https://youtu.be/fXPfIkiG-Go)== (2024 年) ==[解說錄影](https://youtu.be/XMKNPiIYVCE)== (2022 年) ## :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) 提供的工具來運作。 概念示意圖: ![](https://i.imgur.com/3cgRcmI.png) 使用方式: (預先在另一個終端機視窗執行 `$ ./sehttpd`) ```shell $ cd ebpf $ sudo python 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 python 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 追蹤 [fibdrv](https://github.com/sysprog21/fibdrv) 核心模組的運作機制,可參見 [0xff07 的共筆](https://hackmd.io/@0xff07/S1vfNHWB8) * [對應的程式碼](https://github.com/0xff07/fibdrv/) ## :icecream: 電腦網路概論 預習 [CS:APP 第 11 章](https://hackmd.io/s/ByPlLNaTG): Network Programming,搭配閱讀: * [nstack 開發紀錄 (1)](https://hackmd.io/s/ryfvFmZ0f) * [nstack 開發紀錄 (2)](https://hackmd.io/s/r1PUn3KGV) 網頁伺服器流程,參見下圖: ![](https://hackmd.io/_uploads/B1_un3szn.png) * 基本的 Client-Server 運作概念: 1. client 發送一個連線請求 2. server 接收到請求後,再根據 server 本身的資源處理相對應請求 3. server 回應(回傳相關資訊) 4. client 獲得從 server 回傳的結果 ![](https://hackmd.io/_uploads/rk0Y3hif3.png) * 再來看到細部的運作流程,左半部分為 client,右半部分為 server 1. 在 client 與 server 先透過 [`getaddrinfo()`](https://man7.org/linux/man-pages/man3/getaddrinfo.3.html) 啟用程序,回傳值為 `struct addrinfo` 的結構,裡面就含有連線所需要的資料,如:IP 位址、 port ([通訊埠](https://zh.m.wikipedia.org/zh-tw/%E9%80%9A%E8%A8%8A%E5%9F%A0))、 server 名稱...等等 2. 再來 client 與 server 呼叫(call) `socket()` 建立連接,回傳值為 `file descriptor` ,注意只有建立連結但不會操作系統,也不會往網路上傳送任何內容 > [socket man page](https://man7.org/linux/man-pages/man2/socket.2.html) > socket() creates an endpoint for communication and returns a file descriptor that refers to that endpoint. 3. server 使用 [`bind()`](https://man7.org/linux/man-pages/man2/bind.2.html) 函式將 `socket` 與特定的 IP 位址和 port 連接起來(在 kernel space 中進行) 4. server 開啟監聽狀態,呼叫 [`listen()`](https://man7.org/linux/man-pages/man2/listen.2.html) ,準備接受來自 client 的請求 5. server 就可以使用 [`accept()`](https://man7.org/linux/man-pages/man2/accept.2.html) 將 client 連接 6. 而 client 使用 [`connect()`](https://man7.org/linux/man-pages/man2/connect.2.html) 發送 Connection request 等待 server `accept` 7. 成功取得連線後,client 與 server 就可以進行通訊,使用 read 、 write 的方式(rio, reliable I/O,一部分來自 Unix I/O 系統,可用於讀寫 rom 並處理一些底層 I/O 的操作) 8. 當 client 結束連線,會發送結束連線的請求(EOF, End of file),server 獲得此訊息後結束對 client 的連線。 9. 結束一個 client 的連線後,server 可以再接續下一個 client 的連線或是關閉整個 server ==此流程只能用於單一的 server/client 的連線,依照需求適用於小型連線系統,如:路由器內部系統設定== * 在 CS:APP 第 12 章 (並行程式設計)中,提到接收多個 client 的連線方式,與 khttpd 的方式相近,架構圖: ![](https://hackmd.io/_uploads/SyMih2jMn.png) * server 開啟監聽過程,當有任何一個 client 請求連線時,server 就會 `fork` 行程去處理到對應的 client ,所以在處理的過程中,子行程只要對應 client 就好,不會干涉到其他子行程的運作(`Address space` 獨立),能完成多 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 事件的無限 for-loop * epoll 監聽 : * 進入無限的 for-loop 中處理所有的連線請求 * 使用 [`epoll_wait`](https://hackmd.io/-YilHq7jQgS3S9LdUgqJmA#%E8%AC%9B%E8%A7%A3-htstressc-%E7%94%A8%E5%88%B0-epoll-%E7%B3%BB%E7%B5%B1%E5%91%BC%E5%8F%AB) 輪詢的方式將可用的 fd 儲存至 `evts` 陣列中 * 在 `htstress.c` 中 `evts.event` 表示事件狀態的巨集: * EPOLLIN : 表示對應 fd 可讀 * EPOLLOUT : 表示對應 fd 可寫 * EPOLLERR : 表示對應 fd 發生錯誤 * EPOLLHUP : 表示對應 fd 被結束連線 * epoll 的錯誤處理,以 `if (evts[n].events & EPOLLERR){ ... }` 判斷事件是否為錯誤狀態 * [getsockopt()](https://man7.org/linux/man-pages/man2/getsockopt.2.html) : 可以獲得 epoll 監聽 socket 的狀態,透過巨集 `SO_ERROR` 紀錄錯誤訊息(0 為沒有錯誤的產生),看到宣告方式 `if (getsockopt(efd, SOL_SOCKET, SO_ERROR, (void *) &error, &errlen) == 0)`,讀取到 `efd` 的資料將檢查的結果寫入至 `error` 變數中 * [atomic_fetch_add()](https://en.cppreference.com/w/c/atomic/atomic_fetch_add) : 使用到 atomic 的操作方式,紀錄錯誤產生的數量,確保在記錄錯誤數量的時候不會被多執行緒干擾(每個 socket 都有機會互相干擾程序,所以要確保計數的正確性) * [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(表示可寫) : 先確認是事件的狀態為可寫,並確保連線的狀態是可用的,使用 [`send()`](https://man7.org/linux/man-pages/man2/send.2.html) 函式開啟要傳送資料的 fd,再來傳送資料(包含傳送的資料與長度,以檔案的 offset 表示),傳送成功後會返回傳送資料的長度 * 若傳送有問題時,紀錄錯誤訊息(使用 [`write()`](https://man7.org/linux/man-pages/man2/write.2.html),注意到 `write` 的第一個引數為 fd,這裡使用 `2`,[參考文章](http://codewiki.wikidot.com/c:system-calls:write)解釋,0 表示 `STDIN` 標準輸入(鍵盤),1 表示 `STDOUT` 標準輸出(終端機視窗),2 表示 `STDERR` 標準錯誤輸出(將錯誤訊息輸出至終端機)) * 確認資料是否有完整傳送至 server,將事件改為 `EPOLLIN` 可讀的狀態,等待 server 傳送資料 * server 傳送數據至 client : * 事件狀態為 `EPOLLIN`,使用 [`recv()`](https://man7.org/linux/man-pages/man2/recv.2.html) 得到從 server 傳送來的資料,從 socket 的 fd 獲得,將獲得的資料存入 `buffer(inbuf)` 中。 * 關閉 client 與 server 連線: * 當處理完所有的通訊資料後(也就是 `ret = 0`) 時,使用 [`close()`](https://man7.org/linux/man-pages/man2/close.2.html) 關閉 client 的 fd(要關閉否則會占用資源),這裡要注意的是在建立 epoll 監聽與 socket 連線,同時都要有對應的 `close()` 關閉其 fd,不過在 `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 v5.5 的定義 [include/linux/kthread.h](https://elixir.bootlin.com/linux/v5.5/source/include/linux/kthread.h#L43) : ```cpp #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] ```