# High Performance Web Server contributed by < `zzzxxx00019`, `ptzling310`, `tsengsam` > --- ## I. 事前學習 ### Linux TCP Socket 連線 `Linux` 的 `TCP Socket` 架構圖如下,功能專注於對單一客戶端的服務,但無法滿足 `Web` 伺服器需同一時間接受大量連線的要求 ![](https://i.imgur.com/sgdm8QC.png) 當 `socket` 埠 (port) 建立後,將 `socket` 狀態 `block` 在 `accept` 的狀態等待連線,一旦 `client` 成功與 `server` 建立連線,`server` 使用 `read` 從 `client` 端讀取發送過來的資訊,並透過 `write` 進行回覆,最後關閉 `socket` 連線狀態。 ### 改善 blocking 問題:改用 Non-blocking I/O > [淺談I/O Model](https://medium.com/@clu1022/%E6%B7%BA%E8%AB%87i-o-model-32da09c619e6) 將 read 改為 non-blocking I/O,且等待有資料時再做處理。 而等待有資料再處理這個動作要透過事件驅動(event-driven)模型來達成。 * I/O models - Blocking / Non-blocking I/O | Models | 有資料可讀取前的使用者 | 舉例 | | -------- | -------- | --------- | | Blocking I/O | blocked 直到資料準備好 | 未設定 O_NONBLOCK 的 read, select, epoll | | Non-blocking I/O | 直接返回且設定`errno`,但要一直詢問 I/O 資料是否準備好 (polling) | 設定 O_NONBLOCK 的 read, select, epoll | ![](https://miro.medium.com/max/602/1*5uPdSnjRALGMiKAYTUZXcA.png) ![](https://miro.medium.com/max/602/1*LpOGE7BnIugjK9_PznlIPw.png) - Synchronous / Asynchronous I/O | Models | 實際進行 I/O 操作的使用者 | 舉例 | | -------- | -------- | -------- | | Synchronous I/O | blocked | read, recvfrom | | Asynchronous I/O | 核心複製完資料才會告知使用者,故不會被 blocked |aio_read(), aio_write() | * I/O multiplexing ![](https://miro.medium.com/max/602/1*jf2x3N2yYhcZDGF7wip9rA.png) 最初所使用的為 `select` ,但會受限要走訪整個 `fd set` 而產生的效率疑慮,故採用`epoll()` 。 ### Linux 核心系統呼叫行為 #### epoll ```c int epoll_create(int size); ``` 建立新的 epoll instance。 - 參數 1. size:自 Linux 2.6.8 後此值只需 `> 0` 即可。 - 傳回值描述:若成功,該系統呼叫會回傳一個 `fd`;錯誤則回傳 `-1`。 ```c int epoll_create1(int flags); ``` 建立新的 epoll instance。 - 參數 1. `flags == 0`,則功能與 `epoll_create` 相同;若為`EPOLL_CLOEXEC` 則表示 child process 無法存取 parent process 設定的 epoll 資源。 - 傳回值描述:若成功,該系統呼叫會回傳一個 `fd`;錯誤則回傳 `-1`。 ```c int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); ``` 新增想要 monitor 的 `fd` 到 epoll set(interest list) 中。 - ready list : epoll set(interest list) 的 subset。 - Argument 1. epfd:`epoll_create` 回傳的 fd,fd 為一個在 kernel 中的 epoll instance 2. op:要對 fd 執行的操作,有以下三種: * Register fd (`EPOLL_CTL_ADD`):將 fd 加入 epoll instance * Delete fd (`EPOLL_CTL_DEL`):將 fd 從 epoll instance 移除,則 processor 不會再得到任何關於此 fd evevnt 的通知 * Modify fd (`EPOLL_CTL_MOD`):修改 fd 監控中的 event 3. fd:想要加入 epoll set (interest list) 的 fd 4. event:指向 structure:`epoll_event` 的 pointer,該 structure 會儲存想監控的 event ```c int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); ``` 等待 epoll instance:`epdf` 內事件發生。 - Argument 1. epfd:要等待事件的 instance 2. events:紀錄事件已發生的 fd 3. maxevents:需 `>0` 4. timeout:epoll_wait() block 的時間 (milliseconds) - Return value:回傳準備好執行 I/O 動作的 fd 數量;錯誤則回傳 `-1`。 #### timerfd 透過 `timerfd_create()` 將時間變成 `fd` 的狀態,一旦 `fd` 超時就讀取檔案內容,此法將計時器轉化為 `fd` 操作,只要時間到就自動進行 `read` 的動作,以下為一些基本的 `API` : ```c int timerfd_create(int clockid, int flags); ``` 建立一個 `timerfd`,回傳 `file descriptor` ```c int timerfd_settime(int ufd, int flags, const struct itimerspec * utmr, struct itimerspec * otmr); ``` 用於啟動與停止定時器 ```c struct timespec { time_t tv_sec; /* Seconds */ long tv_nsec; /* Nanoseconds */ }; struct itimerspec { struct timespec it_interval; /* Interval for periodic timer */ struct timespec it_value; /* Initial expiration */ }; ``` 用於設定時間的參數 `*utmr` 與 `*otmr` ```c int timerfd_gettime(int fd, struct itimerspec *curr_value); ``` 用於取得 `timerfd` 距離下次超時所剩下的時間,若已 `timeout` 且定時器為循環模式,則重啟定時器 [timerfd 結合 epoll](https://kelele67.github.io/2017/08/06/Epoll+Timerfd%E5%AE%9E%E7%8E%B0%E5%AE%9A%E6%97%B6%E5%99%A8/) 可以實踐 `timerfd` 在 `timeout` 觸發事件發生時,使用者透過 `epoll` 去檢視 `fd` 狀態,達到輪詢目的 #### sendfile [sendfile](https://man7.org/linux/man-pages/man2/sendfile.2.html) 主要目的是在兩個 `file descriptor` 之間傳輸資料,將 `fd` 複製到另外一個 `fd` ,因為 `copy` 這動作是在 `kernel` 完成,若直接在 `kernel` 進行這樣的行為,相較於 `read/write` 需將資料反覆 `copy` 到 `user space`,較不需要進行繁瑣的 `context switch` ,以下是 `sendfile` 的 `API` ```c ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count); ``` `in_fd` 為要被 `read` 的 `fd` ,而 `out_fd` 則為將被 `write` 的 `fd` , `offset` 決定 `in_fd` 從何開始讀取,若設為 `NULL` 則從 `in_fd` 的初始位址開始讀取,而 `count` 則為 欲複製的 `bytes` `read/write` 流程圖 ![](https://i.imgur.com/AaUAbcd.png) 主要需要以下步驟 : 1. 使用 `read()` ,資料被 `copy` 到 `kernel buffer` 2. 再資料從 `kernel buffer` 再 `copy` 到 `user buffer` 並 `return` 3. 使用 `write()` ,從 `user buffer` 將資料 `copy` 到 `socket buffer` 4. 最後資料從 `socket buffer` 被 `copy` 到相關的協議引擎 `sendfile` 流程圖 ![](https://i.imgur.com/QeoGOJa.png) 主要需要以下步驟 : 1. 資料被 `copy` 到 `kernel buffer` 2. 將 `kernel buffer` 資料直接 `copy` 到 `socket buffer` 3. 最後資料從 `socket buffer` 被 `copy` 到相關協議引擎 從上述流程圖比較中不難發現, `read/write` 需要在 `user context` 與 `kernel context` 中反覆進行 `context switch` 的動作,而 `sendfile` 直接將接收到的資料直接 `copy` 到 `socket buffer` 進行回覆,無須返回 `user space` 就能完成這樣的動作。 #### io_uring :::info 將原本使用到的系統呼叫 `epoll` 以 `io_uring` 改寫 ::: [io_uring 詳細介紹](https://hackmd.io/@ptzling310/ByOfdvzsv#liburing-library) * 架構圖 ![](https://i.imgur.com/uhdCNbG.png) * 函式庫 - `<liburing.h>` 函式庫,同時在 `fio` 中提供了 `ioengine=io_uring` 的支持,透過 `liburing` 可以更加快速的使用 `io_uring` - 需將 `linux kernel` 更新至 `v5.5` 以上 - 透過以下方法,快速安裝 [liburing](https://github.com/axboe/liburing) 到 `library` ``` $ git clone https://github.com/axboe/liburing $ cd liburing $ ./configure && sudo make install ``` - 編譯使用 `liburing.h` 方法 ``` $ gcc main.c -o main.o -luring ``` * API 及基本結構 建立一個 `io_uring ring`: ```c struct io_uring ring ; ``` 設定並初始化 `ring`: ```c io_uring_queue_init(ENTRIES, &ring, 0); ``` 當要使用 `sqe` 監控 `fd` 狀態,必須從 `ring` 拿出一個 `sqe`: ```c struct io_uring_sqe *sqe = io_uring_get_sqe(&ring); ``` 然後設定 `sqe` 的狀態: ```c io_uring_prep_rw(int op, struct io_uring_sqe *sqe, int fd, const void *addr, unsigned len,__u64 offset); ``` 設定完 `sqe` 狀態後,必須將 `sqe` 提交回 `ring` ,更動 `ring` 的狀態: ```c io_uring_submit(&ring); ``` 然後等待 kernel 將完成的事件放到 CQ 中: ```c io_uring_wait_cqe(&ring, &cqe); ``` 最後增加 CQ 的 head,使下次完成的事件能放到正確的位置: ```c io_uring_cqe_seen(&ring, cqe); ``` ### sehttpd 程式 > [GitHub](https://github.com/sysprog21/sehttpd/tree/master/src) **mainloop** ```cpp= int main() { /* when a fd is closed by remote, writing to this fd will cause system * send SIGPIPE to this process, which exit the program */ if (sigaction(SIGPIPE, &(struct sigaction){.sa_handler = SIG_IGN, .sa_flags = 0}, NULL)) { log_err("Failed to install sigal handler for SIGPIPE"); return 0; } int listenfd = open_listenfd(PORT); int rc UNUSED = sock_set_non_blocking(listenfd); assert(rc == 0 && "sock_set_non_blocking"); /* create epoll and add listenfd */ int epfd = epoll_create1(0 /* flags */); assert(epfd > 0 && "epoll_create1"); struct epoll_event *events = malloc(sizeof(struct epoll_event) * MAXEVENTS); assert(events && "epoll_event: malloc"); http_request_t *request = malloc(sizeof(http_request_t)); init_http_request(request, listenfd, epfd, WEBROOT); struct epoll_event event = { .data.ptr = request, .events = EPOLLIN | EPOLLET, }; epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &event); timer_init(); printf("Web server started.\n"); while(1) { ... /* epoll wait loop */ ... } } ``` 將程式依序解析 : ```c=6 if (sigaction(SIGPIPE, &(struct sigaction){.sa_handler = SIG_IGN, .sa_flags = 0}, NULL)) { log_err("Failed to install sigal handler for SIGPIPE"); return 0; } ``` * `int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);` 當 process 收到 specific signal 就改變 action。 ```c=13 open_listenfd(int port) ``` * 將 `bind`,`listen` 包裝而成的函式,若 `error` 則回傳 `-1` ```c=14 int rc UNUSED = sock_set_non_blocking(listenfd); assert(rc == 0 && "sock_set_non_blocking"); ``` * 將 `fd` 設為 `non_blocking` 狀態,若 `rc == 0` 代表設置成功,程式往下執行 ```c=18 int epfd = epoll_create1(0 /* flags */); assert(epfd > 0 && "epoll_create1"); ``` * `epoll_create1(flag = 0)` 等效於 `epoll_create` * 建立 `epoll` ,若 `epfd > 0` 代表建立成功,程式往下執行 ```c=21 struct epoll_event *events = malloc(sizeof(struct epoll_event) * MAXEVENTS); assert(events && "epoll_event: malloc"); ``` * 分配 `epoll_event` 空間,透過 `*events` 指向空間位址 * 若空間分配成功, `events != NULL` ,程式往下執行 ```c=24 http_request_t *request = malloc(sizeof(http_request_t)); init_http_request(request, listenfd, epfd, WEBROOT); struct epoll_event event = { .data.ptr = request, .events = EPOLLIN | EPOLLET, }; timer_init(); ``` * 分配 `http_request_t` 空間並初始化 * 設定監測事件為 `request` * `EPOLLIN` : 允許 `read() operations` * `EPOLLET` : 偵測發生條件為 `edge-triggered` **epoll_wait loop** ```c=37 while (1) { int time = find_timer(); debug("wait time = %d", time); int n = epoll_wait(epfd, events, MAXEVENTS, time); handle_expired_timers(); for (int i = 0; i < n; i++) { http_request_t *r = events[i].data.ptr; int fd = r->fd; if (listenfd == fd) { /* we hava one or more incoming connections */ while (1) { socklen_t inlen = 1; struct sockaddr_in clientaddr; int infd = accept(listenfd, (struct sockaddr *) &clientaddr, &inlen); if (infd < 0) { if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { /* we have processed all incoming connections */ break; } log_err("accept"); break; } rc = sock_set_non_blocking(infd); assert(rc == 0 && "sock_set_non_blocking"); request = malloc(sizeof(http_request_t)); if (!request) { log_err("malloc"); break; } init_http_request(request, infd, epfd, WEBROOT); event.data.ptr = request; event.events = EPOLLIN | EPOLLET | EPOLLONESHOT; epoll_ctl(epfd, EPOLL_CTL_ADD, infd, &event); add_timer(request, TIMEOUT_DEFAULT, http_close_conn); } } else { if ((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || (!(events[i].events & EPOLLIN))) { log_err("epoll error fd: %d", r->fd); close(fd); continue; } do_request(events[i].data.ptr); } } } ``` 將程式依序解析 : ```c=38 int time = find_timer(); debug("wait time = %d", time); int n = epoll_wait(epfd, events, MAXEVENTS, time); handle_expired_timers(); ``` * 在沒有任何連線進入下 `time` 會設為 `-1` ,也就是 `block` 住直到有事件就緒 * 一旦有事件就緒,回傳 `n` 就緒數量,並進入 `for` 迴圈處理 ```c=44 http_request_t *r = events[i].data.ptr; int fd = r->fd; if (listenfd == fd){ while (1) { ... } } ``` * 記錄下 `event[i]` 中, `request` 的資訊,並在後續做出 `respond` * 若 `listenfd == fd` ,代表 `I/O` 事件為新的連線進入要求 `accept` ```c=49 socklen_t inlen = 1; struct sockaddr_in clientaddr; int infd = accept(listenfd, (struct sockaddr *) &clientaddr, &inlen); if (infd < 0) { if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) { /* we have processed all incoming connections */ break; } log_err("accept"); break; } ``` * 接受 `client` 端連線,若 `infd < 0` 代表連線失敗, `errno` 將記錄錯誤事件發生 * [errno](https://man7.org/linux/man-pages/man3/errno.3.html) 回報連線錯誤 : > EAGAIN - Resource temporarily unavailable (may be the same value as EWOULDBLOCK) ```c=62 rc = sock_set_non_blocking(infd); assert(rc == 0 && "sock_set_non_blocking"); request = malloc(sizeof(http_request_t)); if (!request) { log_err("malloc"); break; } init_http_request(request, infd, epfd, WEBROOT); event.data.ptr = request; event.events = EPOLLIN | EPOLLET | EPOLLONESHOT; epoll_ctl(epfd, EPOLL_CTL_ADD, infd, &event); ``` * 一但 `client` 連線成功,將其 `I/O` 狀態設為 `non_blocking` * 分配 `http_request_t` 空間給這個連線使用,並將 `request` 加入 `epoll` 監控事件 * `EPOLLONESHOT` 允許 `socket` 在處理完這個事件後,還能繼續處理其他事件的發生 ```c=87 do_request(events[i].data.ptr); ``` * 針對每個 `fd` 狀態去處理 * Flow chart ![](https://i.imgur.com/mjC9lQW.jpg =295x720) --- ## II. [開發紀錄](https://hackmd.io/@jT29vQ4-SxmlBU5UMj1TGA/sehttpd) --- ## III. 問題與討論 1. 無法直接在 20.04 LTS 上按照 H10: sehttpd 步驟安裝所需套件,原因是因為 bcc-tools 無法在直接在 20.04 LTS 上安裝。