# 2022q1 Homework6 (ktcp)
Contributed by < `oucs638` >
> Requirements: [ktcp](https://hackmd.io/@sysprog/linux2022-ktcp)
> GitHub: [kecho](https://github.com/oucs638/kecho), [khttpd](https://github.com/oucs638/khttpd)
## 實驗環境
```shell
$ gcc --version
gcc (Ubuntu 9.4.0-1ubuntu1~20.04) 9.4.0
$ lscpu
Architecture: x86_64
CPU op-mode(s): 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
Thread(s) per core: 2
Core(s) per socket: 6
Socket(s): 1
NUMA node(s): 1
Vendor ID: GenuineIntel
CPU family: 6
Model: 158
Model name: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
Stepping: 13
CPU MHz: 2600.000
CPU max MHz: 4500.0000
CPU min MHz: 800.0000
BogoMIPS: 5199.98
Virtualization: VT-x
L1d cache: 192 KiB
L1i cache: 192 KiB
L2 cache: 1.5 MiB
L3 cache: 12 MiB
NUMA node0 CPU(s): 0-11
```
## 在掛載 Linux kernel module 時傳遞參數
:::success
參照 [Linux 核心模組掛載機制](https://hackmd.io/@sysprog/linux-kernel-module#Linux-%E6%A0%B8%E5%BF%83%E6%A8%A1%E7%B5%84%E6%8E%9B%E8%BC%89%E6%A9%9F%E5%88%B6),解釋 `$ sudo insmod khttpd.ko port=1999` 這命令如何讓 `port=1999` 傳遞到核心,作為核心模組初始化的參數呢?
:::
- 首先,在 [khttpd/main.c](https://github.com/oucs638/khttpd/blob/master/main.c) 中使用了 `module_param` 巨集向 kernel module 模組傳遞參數
```c
module_param(portm, ushort, S_IRUGO);
```
- 巨集`module_param` 在 [linux/include/linux/moduleparam.h](https://github.com/torvalds/linux/blob/master/include/linux/moduleparam.h) 中定義
```c
#define module_param(name, type, perm) \
module_param_named(name, name, type, perm)
```
- `module_param (name, type, perm)` 向 kernel module 模組傳遞參數
- `name`: 將傳遞參數的變數名稱
- `type`: 將傳遞參數的變數資料型別
- 標準的資料型別有: `byte`, `hexint`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `charp (a character pointer)`, `bool`
- `perm`: 將傳遞參數的 [sysfs](https://en.wikipedia.org/wiki/Sysfs) 訪問權限
- 可以使用 [linux/includeuapi/linux/stat.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/stat.h) 中定義的巨集表示,也可以直接用用數字表示
- 數字的最後三位範圍是 0 ~ 8,將其轉成二進制 `XXX XXX XXX`
- 從左到右每三位為一組,第一組表示檔案擁有者的訪問權限,第二組表示檔案擁有者同組使用者的訪問權限,第三組表示其他非本群組使用者的訪問權限
- 每組的三個數字分別表示讀、寫、執行
- 在 [khttpd/main.c](https://github.com/oucs638/khttpd/blob/master/main.c) 中宣告了將傳給 kernel module 參數 `port`,且預設是 `8081`,設定權限是所有人可讀但不可更動
:::warning
目前沒找到 Linux kernel 中定義`S_IRUGO` 的地方,但 [stack overflow](https://stackoverflow.com/questions/27480369/why-should-the-permisson-attrbute-be-specified-for-every-variable-declared-in-ke) 有相關的討論
:::
```c
#define DEFAULT_PORT 8081
...
static ushort port = DEFAULT_PORT;
module_param(port, ushort, S_IRUGO);
```
- 在用 `insmod` 掛載模組時可以設定變數的值,如未設定則 modinfo 中的變數值就會是預設的值
## kHTTPd 和 CS:APP 中 web 伺服器比較
:::success
- 參照 [CS:APP 第 11 章](https://hackmd.io/@sysprog/CSAPP-ch11?type=view),給定的 kHTTPd 和書中的 web 伺服器有哪些流程是一致?又有什麼是你認為 kHTTPd 可改進的部分?
:::
- CS:APP 中的 Web Server 架構
![](https://i.imgur.com/g5vggiA.png)
- 在 `khttpd/main.c` 中的 `open_listen_socket` 可以找到對應的 `open_listenfd` 流程
```c
static int open_listen_socket(ushort port, ushort backlog, struct socket **res)
{
int err = sock_create(PF_INET, SOCK_STREAM, IPPROTO_TCP, &sock);
...
err = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, 1);
...
err = setsockopt(sock, SOL_TCP, TCP_NODELAY, 1);
...
err = setsockopt(sock, SOL_TCP, TCP_CORK, 0);
...
err = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, 1024 * 1024);
...
err = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, 1024 * 1024);
...
err = kernel_bind(sock, (struct sockaddr *) &s, sizeof(s));
...
err = kernel_listen(sock, backlog);
...
}
```
- 在 `khttpd/http_server.c` 中的 `http_server_daemon` 函式可以找到對應的等待 client request 並進行處理的操作
```c
int http_server_daemon(void *arg)
{
...
int err = kernel_accept(param->listen_socket, &socket, 0);
...
worker = kthread_run(http_server_worker, socket, KBUILD_MODNAME);
...
}
```
- 但因為 CS:APP 中的 server 是在 user space 執行,而 kHTTPd 的 server 是在 kernel space 執行,所以對應的函式使用 kernel api
- 其中 `setsockopt` 是自訂的函式,呼叫 [`kernel_setsockopt`](https://www.kernel.org/doc/html/v5.4/networking/kapi.html#c.kernel_setsockopt) 進行 socket 的設定
- [`kernel_bind`](https://www.kernel.org/doc/html/v5.4/networking/kapi.html#c.kernel_bind) 進行 kernel space 的 adress 和 socket 的綁定
- [`kernel_listen`](https://www.kernel.org/doc/html/v5.4/networking/kapi.html#c.kernel_listen) 設定在 kernel space 運行的 socket 為 listening state
- [`kernel_accept`](https://www.kernel.org/doc/html/v5.4/networking/kapi.html#c.kernel_accept) 等待 client connection
- [`kthread_run`](https://www.kernel.org/doc/html/v4.16/driver-api/basics.html#c.kthread_run) 將處理 client request 任務指派給一個新的 thread
## HTTP 效能分析工具
:::success
htstress.c 用到 [epoll](https://man7.org/linux/man-pages/man7/epoll.7.html) 系統呼叫,其作用為何?這樣的 HTTP 效能分析工具原理為何?
:::
- [epoll](https://en.wikipedia.org/wiki/Epoll) 可以分成三個部分
- `epoll_createl(int size)`
- 在 kernel 建立 epoll object,並回傳 epoll file descriptor
- `size` 表示需要監聽的 file descriptor 數量
- `epoll_crl(int epfd, int op, int fd, struct epoll_event *event)`
- 添加、修改、刪除 `epfd` 對應的 kernel epoll object 所監聽的 event
- `epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)`
- block thread 直到任何一個 `epfd` 對應的 kernel epoll object 所監聽的 event 變為就緒,或是 timeout 計時終了
- 以往會在每次處理一個新的 connect 時,新增一個 thread 監聽、處理對應的 file descriptor。而藉由 epoll,單一一個 thread 可以同時監聽多個 file descriptor,也就是可以用一個 thread 監聽多個 socket
- `htstress.c` 會在開始 send requests 前,先記錄起始時間,然後在完成所有 requests 後紀錄結束時間,最後統計的時候紀錄成功與否和花費的時間
```c
start_time();
/* run test */
for (int n = 0; n < num_threads - 1; ++n)
pthread_create(&useless_thread, 0, &worker, 0);
worker(0);
/* output result */
double delta =
tve.tv_sec - tv.tv_sec + ((double) (tve.tv_usec - tv.tv_usec)) / 1e6;
```
- `htstress.c` 中的 `worker` 函式會使用 `epoll` 來處理 send requests 的任務,並在 send 完所有 requests 後記錄時間
```c
if (max_requests && (m + 1 >= (int) max_requests)) {
end_time();
return NULL;
}
```
## CMWQ
:::success
給定的 kecho 已使用 CMWQ,請陳述其優勢和用法
核心文件 [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 核心開發者的考量
:::
- "Workqueue (WQ)" 常用來處理 asynchronous process execution context,將要執行的 functions 記錄在 work item 並放進 WQ 等待
- 當 WQ 中有 work item,一個獨立的 thread "worker" 會逐個執行記錄在 work item 的 functions
- 當 WQ 中沒有 work item,worker 會 idle,直到有新的 work item 進入 WQ,worker 才會再次執行
- 一開始的 workqueue 實作分為兩種:multi threaded (MT)、single thread (ST)
- MTWQ:每個 CPU 都有一個 worker thread,故每個 MTWQ 都會有和 CPU 數量相同的 worker thread
- STWQ:整個系統只有一個 worker thread
- Kernel 中越來越多的 MTWQ 使用者,有些系統啟動的時候就會用掉預設的 32k PID 空間
- 雖然 MTWQ 浪費了很多資源,但因為每個 MTWQ 只負責自己的 worker pool,導致 MTWQ 的 level of concurrency 不如預期
- 為了改進上述的問題,新的 WQ 實作 CMWQ (Concurrency Managed Workqueue) 被提出,並注重以下目標:
- 相容原本的 WQ API
- 所有 WQ 共用一個 per-CPU unified worker pools,盡可能不浪費資源的提供 flexible level of concurrncy
- 自動調整的 worker pool 和 level of concurrency
## user-echo-server
:::success
解釋 `user-echo-server` 運作原理,特別是 [epoll](https://man7.org/linux/man-pages/man7/epoll.7.html) 系統呼叫的使用
:::
- 在前面 “HTTP 效能分析工具” 部份提到 epoll 常見的 API 有:
- `epoll_create(int size)`
- `epoll_crl(int epfde, int op, int fd, struct epoll_event *event)`
- `epoll_wait(int epfd, struct wpoll_event *events, int maxevents, int timeout)`
- 觀察 `user-echo-server.c` 的 main 函式,首先是標準 tcp socket 建立流程:create, bind, listen
```c
...
if ((listener = socket(PF_INET, SOCK_STREAM, 0)) < 0)
server_err("Fail to create socket", &list);
...
if (bind(listener, (struct sockaddr *) &addr, sizeof(addr)) < 0)
server_err("Fail to bind", &list);
...
if (listen(listener, 128) < 0)
server_err("Fail to listen", &list);
...
```
- 通常的做法是 server 使用 `accept` 函式,等待 client connect,但這樣 server 會 block 直到有 client connect
- `user-echo-server` 的做法是將 listen 後的 socket file descriptor 用 epoll object 監聽,當對應的 epoll_fd 有變更,再做對應的處理
```c
int epoll_fd;
if ((epoll_fd = epoll_create(EPOLL_SIZE)) < 0)
server_err("Fail to create epoll", &list);
static struct epoll_event ev = {.events = EPOLLIN | EPOLLET};
ev.data.fd = listener;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listener, &ev) < 0)
server_err("Fail to control epoll", &list);
printf("Listener (fd=%d) was added to epoll.\n", epoll_fd);
```
- 在 `while(1)` 中,使用 `epoll_wait` 等待監聽的 epoll object 變更
```c
if ((epoll_events_count = epoll_wait(epoll_fd, events, EPOLL_SIZE,
EPOLL_RUN_TIMEOUT)) < 0)
```
- 如果變動的是正在 listen 的 server socket file descriptor,表示有 client 要 connect,所以呼叫 accept 函式接受 client 的連線
- 因為已經確定有 client 要 connect,accept 不會 block
- 在 accept 後,將新的 file descriptor 更新到 epoll,等待新的變動
```c
/* EPOLLIN event for listener (new client connection) */
if (events[i].data.fd == listener) {
int client;
while (
(client = accept(listener, (struct sockaddr *) &client_addr,
&socklen)) > 0) {
printf("Connection from %s:%d, socket assigned: %d\n",
inet_ntoa(client_addr.sin_addr),
ntohs(client_addr.sin_port), client);
setnonblock(client);
ev.data.fd = client;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client, &ev) < 0)
server_err("Fail to control epoll", &list);
push_back_client(&list, client,
inet_ntoa(client_addr.sin_addr));
printf(
"Add new client (fd=%d) and size of client_list is "
"%d\n",
client, size_list(list));
}
if (errno != EWOULDBLOCK)
server_err("Fail to accept", &list);
}
```
- 如果變動的是已經 accept 的 client,表示有 client 要傳訊息,呼叫相對應的函式 handle_message_from_client 進行處理
```c
} else {
/* EPOLLIN event for others (new incoming message from client)
*/
if (handle_message_from_client(events[i].data.fd, &list) < 0)
server_err("Handle message from client", &list);
}
```
## bench
:::success
是否理解 `bench` 原理,能否比較 `kecho` 和 `user-echo-server` 表現?佐以製圖
:::
## drop-tcp-socket
:::success
解釋 `drop-tcp-socket` 核心模組運作原理。`TIME-WAIT` sockets 又是什麼?
:::