# 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 又是什麼? :::