# 2020q1 Homework5 (kecho)
contributed by < `eecheng87` >
###### tags: `linux2020`
## 自我檢查清單
### 給定的 kecho 已使用 CMWQ,請陳述其優勢和用法
在上個作業 [khttpd](https://hackmd.io/egwpIL4HTKWvPv_1QlRxwQ#%E5%BC%95%E5%85%A5-Concurrency-Managed-Workqueue) 已經提及 CMWQ 的優勢。用法可以參照 [kecho_mod.c](https://github.com/sysprog21/kecho/blob/master/kecho_mod.c) 內初始化模組所呼叫的 `alloc_workqueue`。此外,工作列的維護也可在 [echo_server.c](https://github.com/sysprog21/kecho/blob/master/echo_server.c) 中的 `daemon` 找到相關用法。
### 解釋 user-echo-server 運作原理,特別是 epoll 系統呼叫的使用
有別於在 [khttpd](https://github.com/eecheng87/khttpd/blob/master/htstress.c) 中 epoll 的使用,本專案將 epoll 使用在 server 端。順帶一題,在 server 端使用 epoll 的範例較容易在網路上找到,流程也算是標準,接著來看看 epoll 如何在 user-server 內使用。
首先,如果對 epoll 系列的函數或不同型態變數的功能不熟的同學,可以參考以下我整理的內容:
#### epoll 變數( 命名皆參考 man. 範例 )
* `epfd` 或 `epollfd` 可以想成是 epoll 的一個 object,這個 object 上面有 interset list 和 ready list,後者是前者的子集。若資料從不可讀變成可讀,則會出現在 ready list。
* `fd` 在 create socket 時回傳的 file descriptor。有時會命名成 `listen_sock`。
* `events` 或 `ev` 可以想成是用來描述 `fd` 的資料結構。例如: `ev.events = EPOLLIN` 就是描述 `fd` 為可讀的資料。
* **[註]:** 這裡的 `events` 有別於 `user-echo-server.c` 中的 `struct epoll_event events`,後者是準備給 `epoll_wait` 使用的參數(用來記錄改變的 epfd)。
#### epoll 函數
接著解釋常見的函數
* `epoll_create` 創立一個 epoll object。聽起來很抽象,但可以想成就是把前面 `epfd` 實體化。用 c++ 的概念大概就是: `epfd = new epoll()`。
* `epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)` 是對前面創立的 `epfd` 做操作( 如: 新增監聽成員 )。參數 `epfd` 就是掌管目前 interest list 和 ready list 的資料結構。`op` 可以填入一些巨集,表明想刪除或添加等。`fd` 是想新增/刪減的某 socket 的 fd。而 `event` 則是記錄著 `fd` 更多的資料。
* `int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)` 的功能就是告知使用者目前可以用(讀/寫)的 `fd` 有誰。參數 `epfd` 內含兩條 list,`epoll_wait` 會檢查 ready list 上有誰並告知。參數 `events` 就是前面提到的告知的地方,`epoll_wait` 會把 ready 的 `fd` 存到 `events` 上,所以通常 `events` 是一個陣列。參數 `timeout` 可以設定等待的時間。而回傳值若 >= 0 代表 ready 的個數,反之則沒有。除了以上所說之外,可以參考這篇[文章](https://medium.com/@copyconstruct/the-method-to-epolls-madness-d9d2d6378642),整理得蠻好的。
#### 解釋 user-echo-server 運作原理
以上,假設已經了解 epoll 使用。接著來從 main 開始觀察運作原理。因為我們在做的是一個 server,所以標準流程跑不掉,我們需要 create socket, bind, listen 和 accept。但是除此之外,我們需要做一個更棒的事。在先前的版本,我們直接呼叫 `accept` 等待 client 的 `connect`,這會導致 server blocking 直到有 client 想 `connect`,這當然是一件壞事,而透過 epoll,我們只要觀察 `epoll_event *events` 是否有變(透過 `epoll_wait`),再做相對應的處理(稍後解釋何謂相對應處理)。
在 main 裡面的 while 之前,基本上就是做一些制式的 server 端設定(listen 以前),真正的重頭戲是在 while 內。
```cpp
while (1) {
..
if ((epoll_events_count = epoll_wait(epoll_fd, events, EPOLL_SIZE,
EPOLL_RUN_TIMEOUT)) < 0)
server_err("Fail to wait epoll", &list);
..
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) {
..}
} 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);
}
}
}
```
以上節錄 while 內主要的骨架解釋。首先在 `wait` 時,當目前監聽的 `epoll_fd` 有變動(有人想讀, 請求連線等等..)時,改變 `events` 內的資料,用來記錄誰改了甚麼。接著拿著剛剛得到的變更紀錄(`events`),開始遍歷成員,分別判斷是哪種情況,並給出相對應的動作(前面所提到的"做相對應的處理")。主要分成兩種情況:
* 如果變動的 `fd` 就是一開始 server 新增一個 socket 回傳的 file descriptor,那代表有新的 client 想和 server 做連線,即 client 做了 `connect` 的動作。所以這個時候 server 要 `accept`,這個 `accept` 並不會 block,因為確定有人要連了。此外,還有一個重點是,記得要把變更後的 `fd` 新增到 epoll 的 list 上,這樣在未來這個新增的 client 想對 server 傳東西時,才會產生 `fd` 的變動,進而紀錄在 `events` 內。(註: `accept` 在接受後,會發一組新的 `fd` 給 client 用,所以從此以後他們的溝通透過此 `fd`。詳細的 `accept` 解說可以參考這篇[文章](http://beej.us/guide/bgnet/html/#acceptthank-you-for-calling-port-3490.),講得非常清楚)
* 如果變動的不是 server 的 `fd`,這個代表是前一個步驟新增的 `fd` 產生變動,這個就是代表 client 想傳東西來了,所以做出相對應的動作 `handle_message_from_client`。
### 是否理解 bench 原理,能否比較 kecho 和 user-echo-server 表現
#### condition variable 在 pthread 的應用
關於 pthread 系列的函數功能可以參考[這裡](https://docs.oracle.com/cd/E19253-01/819-7051/sync-41991/index.html)。以下只說明呼叫這些函數需要注意的地方。在[手冊](https://linux.die.net/man/3/pthread_cond_broadcast)中有提到兩點:
> When each thread unblocked as a result of a pthread_cond_broadcast() or pthread_cond_signal() returns from its call to pthread_cond_wait() or pthread_cond_timedwait(), the thread shall own the mutex with which it called pthread_cond_wait() or pthread_cond_timedwait().
>
> The pthread_cond_broadcast() or pthread_cond_signal() functions may be called by a thread whether or not it currently owns the mutex that threads calling pthread_cond_wait() or pthread_cond_timedwait() have associated with the condition variable during their waits
>
這兩段話可以透過以下範例程式快速理解:
```cpp
/* Thread 1 */
pthread_mutex_lock(&mut);
while (x <= y) {
pthread_cond_wait(&cond, &mut);
}
/* operate on x and y */
pthread_mutex_unlock(&mut);
/* Thread 2 */
pthread_mutex_lock(&mut);
/* modify x and y */
if (x > y) pthread_cond_broadcast(&cond);
pthread_mutex_unlock(&mut);
```
上面有兩個 thread,其中 thread 1 會 block 直到 `x > y`。而在進入 `pthread_cond_wait` 時,會釋放鎖 `mut`。此時 thread 2 將可以拿到鎖,進入 critical section 來改變 `x` 和 `y` 的值,接著若 `x > y` 則呼叫 `pthread_cond_broadcast`,這會讓在 thread 1 卡住的 `wait` return。這裡需要注意到的重點是,如同上面引文所説,`wait` 回傳後,會再次拿到鎖 `mut`。
#### bench 原理
綜觀 bench,其想達到的效果是評估伺服器回應的速度。透過的方法是,建立多執行緒,對伺服器做傳送和接收各一次(原版本傳和收的內容均為 `dummy message`)。記住傳 + 收的時間,輸出等待作圖。
接著是運作的細節講解。首先這不是 driver,所以從 main 開始看。main 呼叫 `bench()`,`bench()` 評估 `BENCH_COUNT` 次效能,每次會開多個 thread 來測試。接著解釋所謂的**每次**怎麼評估。
在迭代的每個回合,會經歷 `create_worker`, `pthread_cond_broadcast` 和 `pthread_join`。
* create_worker: 開多個執行緒(`pthread_create`),每個執行緒做的事是 `bench_worker`。
* bench_worker: 目的是做傳字串 `dummy_message` 到 server 和收回來,計算時間。雖然講起來簡單,但是 bench_worker 裡面有一些細節還是可以拿出來講。首先,在函數的一開始,透過
```cpp
pthread_mutex_lock(&worker_lock);
while (!ready)
if (pthread_cond_wait(&worker_wait, &worker_lock)) {
puts("pthread_cond_wait failed");
exit(-1);
}
pthread_mutex_unlock(&worker_lock);
```
來達到讓此執行緒等待其他執行緒宣佈可以做的時候才繼續。為什麼能達到這種效果?首先,此 thread 先拿到 `worker_lock` 進入迴圈執行 `wait`,此時效果如前面提到的,這個 thread 會被加到 condition variable 上休息(block),然後**會釋放剛剛拿到的 worker_lock**(這點非常重要,否則其他 thread 無法再次拿到鎖,進而無法 wait)。接著假設所有 thread 都已經執行完 `wait` 後,目前所有 thread 都睡在 condition variable 上了。再來回到 `bench`。
* pthread_cond_broadcast: 功能是把**所有**睡在 condition variable 上的 thread 叫醒,這樣就能達到一起做的效果(可比較 [pthread_cond_signal](https://linux.die.net/man/3/pthread_cond_signal))。可以注意到在廣播之前,先把 `ready` 設成 `true`,這是為了避免在廣播時還有人嘗試想睡在 condition variable 上(若 `ready` 為真,則不會呼叫 `pthread_cond_wait`)。既然廣播了,那我們又要回到 `bench_worker` 繼續做事。
* bench_worker: 因為被廣播了,所以從 `pthread_cond_wait` 回傳,繼續往下做。接著做的事就是標準的 socket programing,另外要注意的是,`bench` 是偽裝成 client 的程式,所以我們要呼叫的流程應該參考 client 端的 socket programing。接著評估一次傳送 + 一次接受所需要的時間:
```cpp
gettimeofday(&start, NULL);
send(sock_fd, msg_dum, strlen(msg_dum), 0);
recv(sock_fd, dummy, MAX_MSG_LEN, 0);
gettimeofday(&end, NULL);
```
這邊是傳固定的字串,和真實世界的行為有落差。若很在意的話,可以引入 `lab0.c` 內產生隨機字串的 API。
* pthread_join: 避免主執行緒先 return,這樣可以確保所有執行緒做完才 return。
#### kecho 和 user-echo-server 表現
簡單來說,這兩個分別就是架在 kernel 的伺服器和在 user 的伺服器。想當然爾,在 kernel 的伺服器效能理當比較好。以下提供數據,分別是在 kernel 和 user space:
![](https://i.imgur.com/IZ2cYGW.png)
![](https://i.imgur.com/GDDg1Tv.png)
:::danger
變更上圖標題,確保標題符合實驗對象
:notes: jserv
:::
### 解釋 drop-tcp-socket 核心模組運作原理。TIME-WAIT sockets 又是什麼?
#### TIME-WAIT socket
關於 Time-wait socket 可以參可這篇[文章](http://www.serverframework.com/asynchronousevents/2011/01/time-wait-and-its-design-implications-for-protocols-and-scalable-servers.html)。以下整理文章的重點:
以下是 TCP 在結束連線時會做的事情,可以觀察到完整的結束連線是會在 server 和 client 間傳遞共四次資料作為確認。
![](https://i.imgur.com/Y3ksrpe.png)
我們可以看到我們的主角 TIME-WAIT 狀態是真正結束連線的最後一個狀態,這個狀態會維持 2 個 MLS (2 x Maximum Segment Lifetime),也就是一個封包會在 socket 的最多時間),直到時間到,才會離開 TIME-WAIT 狀態。換成實際的時間大概是數分鐘 (可以透過執行 `./bench` 後再 `netstat -n | grep WAIT` 觀察有哪些 socket 處在 TIME-WAIT。而隔幾分鐘再回來看可以發現會自己不見)。
當系統有很多 TIME-WAIT 狀態 socket 的缺點很明顯,就是他們佔用了大量的 port,這樣使得 OS 很難再去找能用的 port,只能慢慢等時間到,進而降低效能。但是,TIME-WAIT 也是有它存在的理由,考慮以下狀況:
* 理由一,假設有A, B兩次接續的連線(A 關閉後 B 馬上連,而且都在同一個 port)。若伺服器傳東西給 A 時 A 馬上關閉,此時 B 再快速連進來,這個時候剛剛伺服器打算傳給 A 的東西就會被誤傳到 B。倘若新增了 TIME-WAIT 狀態,確保了 2 MLS 時間內不會收任何東西(Any segments that arrive whilst a connection is in the 2MSL wait state are discarded),這樣就能解決誤傳的困擾。當然在現實世界中,這種情況幾乎不可能發生,因為 OS 不會這樣幫 B 選剛關閉的 port 當成新的。
* 理由二,為了確保 TCP full-duplex 終止連線的可靠性。假設 client 終止連線的最後一個 ACK (由 server 傳來)在傳輸過程丟了,則 client 會再傳一次 FIN,若此時沒有 TIME-WAIT 的機制,client 將會收到傳送錯誤的訊息 [RST](https://www.pico.net/kb/what-is-a-tcp-reset-rst),因為 server 已經關閉了。
#### drop-tcp-socket 功能
前面有提到,client 關掉 socket 後並不會馬上進入 close state,而是會在 time-wait state 停留 2 個 MSL 時間,才會消失。這點可以透過以下操作觀察到:
```shell
./bench # 製造大量連線
./netstat -n | grep WAIT # 觀察目前有哪些 socket 是在 time wait
```
預期得到以下輸出:
```shell
tcp 0 0 127.0.0.1:52002 127.0.0.1:12345 TIME_WAIT
tcp 0 0 127.0.0.1:55426 127.0.0.1:12345 TIME_WAIT
tcp 0 0 127.0.0.1:58582 127.0.0.1:12345 TIME_WAIT
tcp 0 0 127.0.0.1:51196 127.0.0.1:12345 TIME_WAIT
tcp 0 0 127.0.0.1:60934 127.0.0.1:12345 TIME_WAIT
```
舉例來說:可以發現 client 127.0.0.1:60934 目前正在 time-wait 狀態。
接著我們來應證前面文章所説的,過多的 time-wait socket 會影響效能表現。實驗過程是在沒有 socket 正在 wait 的情況下跑 `bench` 然後作圖,觀察速度。接著預期會有一堆 socket 在 wait,所以**馬上**再跑一次 `bench`,然後畫圖。分別得到以下兩張圖
![](https://i.imgur.com/IZ2cYGW.png)
![](https://i.imgur.com/O0A3tjD.png)
可以看到的確在多數 socket 被佔用的情況下,效能會被拖慢。不過這個時候 `drop-tcp-socket` 就可以登場了,因為它能夠把所有在 time-wait 狀態的 socket 全部清掉,命令如下:
```shell
netstat -n | grep WAIT | grep 12345 | awk '{print $4" "$5}' | sudo tee /proc/net/drop_tcp_sock
```
檢查的方式可以再次 `netstat -n | grep WAIT` 會發現 socket 都被清乾淨了。
#### 解釋 drop-tcp-socket 核心模組運作原理
[待補]