# 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 核心模組運作原理 [待補]