---
tags: linux2022
---
# 2022q1 Homework6 (ktcp & khttpd)
contributed by < [`qwe661234`](https://github.com/qwe661234) >
> [作業要求](https://hackmd.io/@sysprog/linux2022-ktcp#K08-ktcp)
> [GitHub(kecho)](https://github.com/qwe661234/kecho)
> [GitHub(khttpd)](https://github.com/qwe661234/khttpd)
::: spoiler 實驗環境
```shell
$ gcc --version
gcc (Ubuntu 9.4.0-1ubuntu1~20.04) 9.4.0
$ lscpu
架構: x86_64
CPU 作業模式: 32-bit, 64-bit
Byte Order: Little Endian
Address sizes: 39 bits physical, 48 bits virtual
CPU(s): 12
On-line CPU(s) list: 0-11
每核心執行緒數: 2
每通訊端核心數: 6
Socket(s): 1
NUMA 節點: 1
供應商識別號: GenuineIntel
CPU 家族: 6
型號: 158
Model name: Intel(R) Core(TM) i7-8750H CPU @ 2.20GHz
製程: 10
CPU MHz: 2200.000
CPU max MHz: 4100.0000
CPU min MHz: 800.0000
BogoMIPS: 4399.99
虛擬: VT-x
L1d 快取: 192 KiB
L1i 快取: 192 KiB
L2 快取: 1.5 MiB
L3 快取: 9 MiB
NUMA node0 CPU(s): 0-11
```
:::
## CMWQ (Concurrency Managed Workqueue) 優勢和用法
### 舊有 workqueue
舊有的 workqueue 有兩種,single-thread 和 per-CPU thread。
* single-thread: kernel 會建立一個 kernel thread 用於執行 workqueue 中的任務,並且該 kernel thread 不會綁定到某個 CPU 上。
* per-CPU thread: kernel 會針對每個 CPU 上的 workqueue 建立一個kernel thread,每個 kthread 會綁定到一個CPU上。
### 舊有 workqueue 的缺點
1. kernel thread 數量過多導致原本用來儲存 PID 的 32k 空間不足
因為舊有的 workqueue 會綁定 kernel thread,如果使用者頻繁建立 workqueue,kernel thread 也會跟著建立,造成 kernel thread 數量過多的問題。
2. concurrency 程度不佳
舊有的 workqueue 會綁定 kernel thread,假如使用 per-CPU workqueue,當任務被加到屬於某一個 CPU 的 workqueue 時,該任務只能由綁定該 CPU 的 kernel thread 來執行。假設綁定 CPU0 的 kernel thread 正在執行的任務 block 住了,接在 CPU0 的 woekqueue 中的任務只能等待被 block 住的任務執行完才能被執行,即使其他 CPU 上的 kernel thread 是 idle 的。
3. dead lock
假設任務 A 和任務 B 被加到同一個 CPU 的 workqueue 上,任務 A 先被 kernel thread 執行了,但任務 A 的執行必須依賴任務 B 的結果,可是任務 B 必須等待任務 A 執行完才能執行,dead lock 因此而產生。
4. 缺乏彈性
使用者只能選擇建立一個 kernel thread 或是在每個 CPU 都建立一個 kernel thread,缺乏彈性。
### CMWQ
![](https://ask.qcloudimg.com/http-save/yehe-5465588/okaxmvaxxl.jpeg)
CMWQ 將 workqueue 和 kernel thread 拆開,使 kernel thread 不再由 workqueue 綁定,進而提出 thread pool 概念,並且這些 thread pool 是由所有 workqueue 共享。
* workqueue: 有兩種,一種是 default workqueue 由系統建立,另一種是由使用者自己建立的 user created workqueue。
* thread pool: 除了每個 CPU 都會有一個 thread pool,還有沒有綁定 CPU 的 unbounded thread pool。
1. 綁定 CPU 的 thread pool:
這種類型的 thread pool 又分為 normal thread pool 和 high priority thread pool,分別用來管理不在意優先權的 kernel thread 和高優先權的 kernel thread,而這兩種 kernel thread 分別用來處理不在意優先權的和高優先權的任務。此類型的 thread pool 數量是固定的,根據系統中的 CPU 數量,假設系統有 n 個 CPU 在運行,就會創建 2n 個 thread pool。
2. unbounded thread pool:
可以運行在任意 CPU 上。這種 thread pool 是動態建立的,和 thread pool 的屬性有關,包括該 thread pool 所建立的 worker thread 的優先權(nice value),以及可以運行的 CPU 列表等。如果系統中已經有了相同屬性的 thread pool,那麼不需要建立新的 thread pool,否則建立新的 thread pool。
### CMWQ 優點
1. 維持與舊有 workqueue 的相容性。
2. 使用者只需要知道 API 如何使用, 不需要知道實作細節, CMWQ 會自動調整 level of concurrency 和 worker pool。
3. 解決 kernel thread 數量過多問題
CMWQ 的策略是當 thread pool 中處於運行狀態的 kernel thread 為0,且有需要處理的任務時,thread pool 就會建立新的 kernel thread 當kernel thread 處於 idle 時,不會立刻銷毀,而是等待一段時間,如果此時有建立新的 kernel thread 的需求,那麼直接 wakeup 這個 idle 的 kernel thread 來處理任務。一段時間過去仍然沒有任務要處理,就銷毀這個 kernel thread。
4. 提升 concurrency 程度
因為 CMWQ 任務沒有綁定 CPU,因此某個 CPU 上的 kernel thread 正在執行的任務 block 住了,其他任務可以交由其他 CPU 上正在 idle 的 kernel thread 來執行,以此解決發生舊有 workqueue 中 concurrency 程度不佳以及 deadlock 的問題。
#### reference: [currency Managed Workqueue(CMWQ)概述](https://cloud.tencent.com/developer/article/1517995)
### CMWQ 用法
* `alloc_workqueue`: 建立 workqueue
第二個參數 flag 可以設定 workqueue feature, e.g. WQ_UNBOUND, 代表不綁定 cpu
* `destroy_workqueue`: 移除 workqueue
* `INIT_WORK`: 將 function 初始化成 `struct work_struct`
* `flush_work`: 等待 workqueue 上的最後一項任務執行完畢
* `queue_work`: 將任務加入到 workqueue 中進行排程
## 核心文件 [Concurrency Managed Workqueue (cmwq)](https://www.kernel.org/doc/html/latest/core-api/workqueue.html) 提到 "The original create_`*`workqueue() functions are deprecated and scheduled for removal",請參閱 Linux 核心的 git log,揣摩 Linux 核心開發者的考量
參考 commit [workqueue: s/__create_workqueue()/alloc_workqueue()/, and add system …](https://github.com/torvalds/linux/commit/d320c03830b17af64e4547075003b1eeb274bc6c), 內文提到
> *Now that workqueue is more featureful, there should be a public
workqueue creation function which takes paramters to control them.
Rename __create_workqueue() to alloc_workqueue() and make 0
max_active mean WQ_DFL_ACTIVE. In the long run, all
create_workqueue_*() will be converted over to alloc_workqueue().
關於作者寫道的第一句 "Now that workqueue is more featureful, there should be a public workqueue creation function which takes paramters to control them.", 我們可以參考 commit [workqueue: merge feature parameters into flags](https://github.com/torvalds/linux/commit/97e37d7b9e65), 推測作者考量到以後 workqueue 的 feature 會被擴充, 如果每增加一個 feature 就要加入一個 parameter, 需要更動到大量的程式碼, 因此作者將 parameter 改成 flag 的方式, 用特定的 bit 去控制 feature, 不過這邊看不出為何要更改 function name。
接著參考 [OscarShiang 的開發紀錄](https://hackmd.io/@oscarshiang/linux_kecho#%E6%94%B9%E4%BB%A5-alloc_workqueue-%E5%AF%A6%E4%BD%9C-workqueue-%E7%9A%84%E5%8E%9F%E5%9B%A0), 作者認為這個改動最主要的原因是為了簡化新增 workqueue 特徵的流程, 其中提到的一點是上述所說的作者將 parameter 改為 flag, 另外作者也提供 commit [workqueue: introduce create_rt_workqueue](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/include/linux/workqueue.h?id=0d557dc97f4bb501f086a03d0f00b99a7855d794) 來舉例, 在這個 commit 中, 為了增加 real-time feature, 也就是加入了一個新的 parameter, 改動到了許多處的程式碼。
不過我們可以發現其實 commit [workqueue: merge feature parameters into flags](https://github.com/torvalds/linux/commit/97e37d7b9e65) 其實在更改 function name 的 commit 前提交, 因此其實就算不更改 function name 也可以, 推測另一個更改 function name 的原因是提醒使用舊版的使用者這項改動。
## 解釋 `user-echo-server` 運作原理,特別是 [epoll](http://man7.org/linux/man-pages/man7/epoll.7.html) 系統呼叫的使用
### 建立連線
![](https://media.geeksforgeeks.org/wp-content/uploads/20220330131350/StatediagramforserverandclientmodelofSocketdrawio2-448x660.png)
[圖片來源](https://www.geeksforgeeks.org/socket-programming-cc/)
`user-echo-server` 首先利用 `socket()` 建立 scoket, 將 socket 的 I/O event 行為設為 `nonblock`, 接著利用 `bind()` 綁定 socket 以及 localhost 與指定的 port, 接著 call `listen()` 來監聽 request
```c
struct sockaddr_in addr = {
.sin_family = PF_INET,
.sin_port = htons(SERVER_PORT),
.sin_addr.s_addr = htonl(INADDR_ANY),
};
socklen_t socklen = sizeof(addr);
int listener;
if ((listener = socket(PF_INET, SOCK_STREAM, 0)) < 0)
...
if (setnonblock(listener) == -1)
...
if (bind(listener, (struct sockaddr *) &addr, sizeof(addr)) < 0)
...
if (listen(listener, 128) < 0)
...
```
### epoll
epoll 為 I/O multiplexing model, 其實作利用兩個 queue, 一個是 interest queue, queue 上接的是被 epoll 監視且正在等待 I/O event ready 的 thread, 另一是 ready queue, 上面接的是 I/O event 已經 ready, 可以準備做 I/O 的 thread。
實作方面, 首先利用 `epoll_create` 建立 epoll instance, 接著利用 `epoll_ctl`, 這邊利用到 `EPOLL_CTL_ADD`, 這個 operator 是用來將剛剛建立的 socket listener 接在 epoll 的 interest queue 上, 讓 epoll 來 monitor 其 I/O event。
### struct epoll_event
* epoll_data: 保存該事件的 file discriptor
* events: the type of event
```c
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* epoll event */
epoll_data_t data; /* User data variable */
};
```
```c
int epoll_fd;
if ((epoll_fd = epoll_create(EPOLL_SIZE)) < 0)
...
static struct epoll_event ev = {.events = EPOLLIN | EPOLLET};
ev.data.fd = listener;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listener, &ev) < 0)
...
```
將 socket listener 交給 epoll monitor 後, 以 `epoll_wait` 等待 interest queue 上的 thread 的 I/O event 是否 ready, `epoll_wait` 的第二個參數是 event buffer, 用來存放回傳事件的 data, 而第四個參數 timeout 為等待時間, 這邊設為 -1, 其行為會是 ready queue 為空時一直 block 住, 當 ready queue 不為空時立刻回傳。
epoll_events_count 代表有多少 I/O event 已經 ready, 接著依照 epoll_events_count 的數量來處理 event, 這邊會收到的 event 分為兩種, 一種是 socket listener 接收到 client 端的 `connect` 請求, server 端會利用 `accept()`, 取出在 socket listener queue 上的第一個 `connect` 請求, 並為其建立新的 socket client。
建立新的 socket client 後將 socket client 的 I/O event 行為設為 `nonblock` 並利用 `epoll_ctl` 將 socket client 加入 epoll 的 interest queue, 交給 epoll monitor, 除了加到 epoll 外, 也會將 client 加到 client_list 中, 等到 client 端發送給 socket client 的 request, 例如: `send()` 被 epoll 加到 ready queue 時, 也就是第二種 event, 會將 event 交由 function `handle_message_from_client` 處理。
`if (events[i].data.fd == listener)` 為 `true` 表示第一種 event, 反之為第二種。
```c
while (1) {
struct sockaddr_in client_addr;
int epoll_events_count;
if ((epoll_events_count = epoll_wait(epoll_fd, events, EPOLL_SIZE,
EPOLL_RUN_TIMEOUT)) < 0)
...
for (int i = 0; i < epoll_events_count; i++) {
if (events[i].data.fd == listener) {
int client;
while (
(client = accept(listener, (struct sockaddr *) &client_addr,
&socklen)) > 0) {
...
setnonblock(client);
ev.data.fd = client;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client, &ev) < 0)
...
push_back_client(&list, client,
inet_ntoa(client_addr.sin_addr));
...
}
...
} else {
if (handle_message_from_client(events[i].data.fd, &list) < 0)
...
}
}
}
```
### client_list
client_list 主要是用來儲存 server 新建立的 socket client 資訊, 透過 `size_list` 可計算出剩餘多少由 server 建立的 client socket。
#### handle_message_from_client
利用 `recv()` 接收 client 端送出的 data, 並利用 `send()` 發送 request 給 client 端, 如果 `recv()` 的回傳為 0, 表示 client 端的 socket 已經關閉, 因此可以將 server 端新建立的 client socket 關閉, 並將其從 client_list 中 delete 掉。
## 是否理解 bench 原理,能否比較 kecho 和 user-echo-server 表現?佐以製圖
bench 是一個用來評估 server 回應速度的測量工具, 其運作原理是 create 1000(變數`MAX_THREAD`) 個 thread, 而每個 thread 的任務是連線到 echo_server, 並傳送一段 dummy message 給 echo server, 接著接收 echo_server 回傳的 message, 送出以及接收的字串必須是一樣的, 每個 thread 都會記錄送出訊息後到成功接收訊息這段時間。
bench 會重複做 10 次(變數`BENCH_COUNT`) 上述操作, 接著將 10 次的結果相加取平, 單位是微秒。
為了確保成功建立這 1000 個 thread 後, 才對 echo_server 進行連線以及收發訊息, `bench.c` 利用 condition variable `worker_wait`, 讓先建立好的 thread 等待, 等到第 1000 個 thread 建立成功, 再發通知喚醒等待的 thread。
```c
#define MAX_THREAD 1000
pthread_mutex_lock(&worker_lock);
if (++n_retry == MAX_THREAD) {
pthread_cond_broadcast(&worker_wait);
} else {
while (n_retry < MAX_THREAD) {
if (pthread_cond_wait(&worker_wait, &worker_lock)) {
puts("pthread_cond_wait failed");
exit(-1);
}
}
}
pthread_mutex_unlock(&worker_lock);
```
## 比較 kecho 和 user-echo-server 表現?佐以製圖
kecho 是架設在 kernel space 的 server, 而 user-echo-server 則架設在 user space
### kecho
![](https://i.imgur.com/nBpa5QC.png)
### user-echo-server
![](https://i.imgur.com/EB4NE3X.png)
由結果圖可看出 kecho 的效能比 user-echo-server 好非常多, 參考論文 [Analysis of Delivery of Web Contents for Kernel-mode and User-mode Web Servers](https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.206.3959&rep=rep1&type=pdf) 的結論, 由於在 kernel space 的 server 可以避免 process scheduling 以及資料的複製, 引此在 kernel space 的 kecho 效能會比在 user space 的 user-echo-server 好。
## 若使用者層級的程式頻繁傳遞過長的字串給 kecho 核心模組,會發生什麼事?
當傳遞超過 kecho buffer_size 的字串時, 傳送訊息的行為會被拆分, 假如 buffer size 是 20, 而傳送的字串長度是 31, 那會被拆成長度為 20 與長度為 13 傳送(呼叫 send 兩次)。
當頻繁傳遞過長的字串給 kecho 核心模組時, 觀察發現會有遺漏訊息的現象, 如果有 1000 個 thread, printk 印出相同字串會是 2000 個, 因為有接收訊息以及發送訊息, 但當字串過長拆分成兩次發送時, 前半段訊息只印出 1834 個而後半段是 1835 個, 而不是正確的 2000 個, 因此當頻繁傳遞過長的字串給 kecho 核心模組時, 會有遺漏訊息的情況。
而傳遞超過 user-echo-server buffer_size 給 user-echo-server, 字串也會被拆成兩份並且呼叫 send 兩次, 但並不會有訊息遺漏的問題發生。
### TCP backlog
TCP 連線建立是透過三次握手(three-way handshake) 來建立練線, 當 client 呼叫 `connect()` 發出 SYN 號給 server 時, server 收到請求會變成 `SYN_RECV` 的狀態, 接著發送 SYN 以及 ACK(確認收到 client 端的 SYN 信號), 接著 client 發送 ACK, 通知 server 收到 server 端的 SYN 訊號, 完成後 client 和 server 都會變為 `ESTABLISH` 的狀態, 等待 server 呼叫 `accpet()`。
![](https://img-blog.csdn.net/20170305084830550)
[圖片來源](https://blog.csdn.net/yangbodong22011/article/details/60399728)
而 backlog 參數是用來指定呈現 `ESTABLISHED` 狀態等待被 `accept()` 的 socket 有幾個, 而其他的 socket 則會那在 `SYN_RECV` 的狀態, 等待有 `ESTABLISHED` 狀態的 socket 被 accept 後, 才會有新的在 `SYN_RECV` 狀態的 socket 變成 `ESTABLISHED` 狀態。
backlog 的數量是用到系統限制的, 檔案 `/proc/sys/net/core/somaxconn` 中的值為 backlog 最大值, 如果 backlog 設定超過最大值則被設定為最大值。
我們可以透過指令 `# ss -ln | head -n1 ;ss -ln | grep {port}` 來確認 backlog, Recv-Q 表示已經佔用的 backlog, 也就是呈現 `ESTABLISHED` 狀態等待被 `accept()` 的 socket 有幾個, Send-Q 則表示 backlog 的大小。
```
Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
tcp LISTEN 5 4 0.0.0.0:8081 0.0.0.0:*
```
這邊我們設定 backlog 為 4, 但 Recv-Q 的個數卻是 5 個, 原因我們可以看到 [linux/include/net/sock.h](https://github.com/torvalds/linux/blob/169e77764adc041b1dacba84ea90516a895d43b2/include/net/sock.h)
```c
static inline bool sk_acceptq_is_full(const struct sock *sk)
{
return READ_ONCE(sk->sk_ack_backlog) > READ_ONCE(sk->sk_max_ack_backlog);
}
```
因為這邊判斷 accept queue 是否滿了是用大於, 所以我們設定最大值為 4, 當 accept queue 中的 socket 為 5 時才代表滿了。
## 參照 [CS:APP 第 11 章](https://hackmd.io/s/ByPlLNaTG),給定的 kHTTPd 和書中的 web 伺服器有哪些流程是一致?又有什麼是你認為 kHTTPd 可改進的部分?
kHTTPd 在建立伺服器時會先呼叫 `sock_create()` 來產生 socket, 接著呼叫 `kernel_bind()` 將 socket 綁定在我們給定的 IP 位置以及 port,最後呼叫 `kernel_listen()` 來監聽 client 端的連線。
監聽連線的 socket 完成後會進到 function `http_server_daemon()` 中等待 client 端連線, 收到 client 端連線後, 呼叫 `kernel_accept()` 確認連線並建立新的 socket, 交由 kthread 去接收 client 端的 request 並發送對應的 response, 最後當錯誤發生或關閉 server 時, 會呼叫 `close_listen_socket()` 來關閉 socket, 這一係建立伺服器流程與書中一致。
### wget error
```
wget localhost:8081
```
利用 wget 向 khttpd server 發出請求, 雖然成功得到 index.html, 不過 kernel 卻印出 error message
```
[84131.206885] khttpd: requested_url = /
[84131.208045] khttpd: recv error: -104
```
參考 [linux error code](https://www.thegeekstuff.com/2010/10/linux-error-codes/), error number 104 是 ECONNRESET, 代表 Connection reset by peer。
參考 [【TCP】Connection reset by peer 原因分析定位](https://blog.csdn.net/simonchi/article/details/105807660) 發現這個 error 跟我們將連線設定為 Keep-Alive 模式有關, 當使用Keep-Alive 模式時,client 與 server 的連接持續, 持續時間與系統設定有關
#### Keep-alive
TCP Keep-alive 機制是當 client 與 server 在一段時間無資料互動後, server 會 傳送 keep-alive probe 封包去探測連線是否以斷開
* /proc/sys/net/ipv4/tcp_keepalive_time (最後一個封包發送到發送 keep-alive probe 封包的時間間隔)
* /proc/sys/net/ipv4/tcp_keepalive_probes (發送 keep-alive probe 封包次數)
* /proc/sys/net/ipv4/tcp_keepalive_intvl (兩次 keep-alive probe 封包的時間間隔)
參考 [wget manual](https://www.gnu.org/software/wget/manual/wget.html), 在指令後面加上 `--no-http-keep-alive` 來關閉 Keep-alive feature, 這樣就不會跳出 error。
```
wget localhost:8081 --no-http-keep-alive
```
## 引入 Concurrency Managed Workqueue (cmwq),改寫 kHTTPd,分析效能表現和提出改進方案
引入 cmwq 後, 以效能分析工具 htstress 進行效能分析, 可發現在以入 cmwq 後, 效能有顯著的提升, 不過會發生 socker errors。
### with cmwq
```
requests: 100000
good requests: 100000 [100%]
bad requests: 0 [0%]
socker errors: 864 [0%]
seconds: 1.063
requests/sec: 94102.856
```
### without cmwq
```
requests: 100000
good requests: 100000 [100%]
bad requests: 0 [0%]
socker errors: 0 [0%]
seconds: 4.882
requests/sec: 20483.396
```
:::warning
TODO: 排除錯誤
:::
## 自我檢查清單
- [ ] 參照 [Linux 核心模組掛載機制](https://hackmd.io/@sysprog/linux-kernel-module),解釋 `$ sudo insmod khttpd.ko port=1999` 這命令是如何讓 `port=1999` 傳遞到核心,作為核心模組初始化的參數呢?
> 過程中也會參照到 [你所不知道的 C 語言:連結器和執行檔資訊](https://hackmd.io/@sysprog/c-linker-loader)
- [ ] 參照 [CS:APP 第 11 章](https://hackmd.io/s/ByPlLNaTG),給定的 kHTTPd 和書中的 web 伺服器有哪些流程是一致?又有什麼是你認為 kHTTPd 可改進的部分?
- [ ] `htstress.c` 用到 [epoll](http://man7.org/linux/man-pages/man7/epoll.7.html) 系統呼叫,其作用為何?這樣的 HTTP 效能分析工具原理為何?
- [x] 給定的 `kecho` 已使用 CMWQ,請陳述其優勢和用法
- [x] 核心文件 [Concurrency Managed Workqueue (cmwq)](https://www.kernel.org/doc/html/latest/core-api/workqueue.html) 提到 "The original create_`*`workqueue() functions are deprecated and scheduled for removal",請參閱 Linux 核心的 git log (不要用 Google 搜尋!),揣摩 Linux 核心開發者的考量
- [x] 解釋 `user-echo-server` 運作原理,特別是 [epoll](http://man7.org/linux/man-pages/man7/epoll.7.html) 系統呼叫的使用
- [x] 是否理解 `bench` 原理,能否比較 `kecho` 和 `user-echo-server` 表現?佐以製圖
- [ ] 解釋 `drop-tcp-socket` 核心模組運作原理。`TIME-WAIT` sockets 又是什麼?