--- tags: Linux Kernel --- # khttpd contributed by < `kevinshieh0225` > > [作業需求](https://hackmd.io/@sysprog/linux2022-ktcp) > [GitHub: khttpd](https://github.com/kevinshieh0225/khttpd) ## :penguin: 自我檢查清單與作業需求 - [x] 參照 [fibdrv 作業說明](https://hackmd.io/@sysprog/linux2020-fibdrv) 裡頭的「Linux 核心模組掛載機制」一節,解釋 `$ sudo insmod khttpd.ko port=1999` 這命令是如何讓 `port=1999` 傳遞到核心,作為核心模組初始化的參數呢? - [ ] 參照 [CS:APP 第 11 章](https://hackmd.io/s/ByPlLNaTG),給定的 kHTTPd 和書中的 web 伺服器有哪些流程是一致?又有什麼是你認為 kHTTPd 可改進的部分? - [x] `htstress.c` 用到 [epoll](http://man7.org/linux/man-pages/man7/epoll.7.html) 系統呼叫,其作用為何?這樣的 HTTP 效能分析工具原理為何? * 在 GitHub 上 fork [khttpd](https://github.com/sysprog21/khttpd),目標是提供檔案存取功能和修正 `khttpd` 的執行時期缺失。過程中應一併完成以下: - [x] 指出 kHTTPd 實作的缺失 (特別是安全疑慮) 並予以改正 - [x] 引入 [Concurrency Managed Workqueue](https://www.kernel.org/doc/html/v4.15/core-api/workqueue.html) (cmwq),改寫 kHTTPd,分析效能表現和提出改進方案,可參考 [kecho](https://github.com/sysprog21/kecho) - [ ] 實作 [HTTP 1.1 keep-alive](https://en.wikipedia.org/wiki/HTTP_persistent_connection),並提供基本的 [directory listing](https://cwiki.apache.org/confluence/display/httpd/DirectoryListings) 功能 - 可由 Linux 核心模組的參數指定 `WWWROOT`,例如 [httpd](https://github.com/sysprog21/concurrent-programs/tree/master/httpd) ## 程式碼理解 ### htstress.c ### http_parser 專案中的 http_parser 源自於 [nodejs](https://github.com/nodejs/http-parser) 的開源專案:一個用 C 語言撰寫的 http 資料解析器。這份解析器不需要額外的系統呼叫或記憶體配置成本,能夠在輕量的成本處理資料串流。 > Features: > >- No dependencies >- Handles persistent streams (keep-alive). >- Decodes chunked encoding. >- Upgrade support >- Defends against buffer overflow attacks. > >The parser extracts the following >- information from HTTP messages: > >- Header fields and values >- Content-Length >- Request method >- Response status code >- Transfer-Encoding >- HTTP version >- Request URL >- Message body ### http_server #### http_server_daemon 網站伺服器的後台程式碼,接收 client http connect message request。`http_server_daemon` 監聽 client socket request,接收到後建立新的執行緒,讓新的執行緒執行 `http_server_worker` 的函式來回應個別 client socket 的 request。 ```c int http_server_daemon(void *arg) { struct socket *socket; struct task_struct *worker; struct http_server_param *param = (struct http_server_param *) arg; allow_signal(SIGKILL); allow_signal(SIGTERM); while (!kthread_should_stop()) { int err = kernel_accept(param->listen_socket, &socket, 0); ... worker = kthread_run(http_server_worker, socket, KBUILD_MODNAME); ... } } ``` :::warning 在 [kecho pull request #1](https://hackmd.io/@Masamaloka/linux2022-kecho#%E6%AF%94%E8%BC%83-kthread-based-%E5%92%8C-CMWQ-based-kecho_mod) 有探討關於使用 kthread-based 和 CMWQ-based 的實作差異,這將是我們可以動手改進與比較的環節。 ::: #### http_server_worker 1. 初始化接收 TCP 封包的 `parser`、parser 的函式庫 `setting` (記錄處理訊息用的函式指標), 自定義結構含有 socket, url 等等資訊的 `request`。 2. `http_parser_init` API 初始化 `parser` 3. 在核心執行緒尚未結束前,`http_server_recv`函式 接收來自 client 的封包,並透過 `http_parser_execute` API 處理這些封包需求。 ```c static int http_server_worker(void *arg) { char *buf; struct http_parser parser; struct http_parser_settings setting = { /* settings member reference to function pointers */ }; struct http_request request; struct socket *socket = (struct socket *) arg; allow_signal(SIGKILL); allow_signal(SIGTERM); buf = kmalloc(RECV_BUFFER_SIZE, GFP_KERNEL); ... request.socket = socket; http_parser_init(&parser, HTTP_REQUEST); parser.data = &request; while (!kthread_should_stop()) { int ret = http_server_recv(socket, buf, RECV_BUFFER_SIZE - 1); ... http_parser_execute(&parser, &setting, buf, ret); if (request.complete && !http_should_keep_alive(&parser)) break; } kernel_sock_shutdown(socket, SHUT_RDWR); sock_release(socket); kfree(buf); return 0; } ``` ### main 在 `khttpd_init` 函式利用 `open_listen_socket` 設定 server socket 的屬性,並建立核心執行緒執行 `http_server_daemon`。 ## 傳遞核心模組參數 Linux kernel 提供了一個簡單的框架,允許驅動程式聲明參數,使用者在系統啟動或模組掛載時為參數指定相應值,在驅動程式裡,參數的用法如同全域變數。 使用引入標頭檔 [`<linux/moduleparam.h>`](https://github.com/torvalds/linux/blob/master/include/linux/moduleparam.h) 以使用巨集 `module_param(name, type, perm)`。`name` 是參數、`type` 是參數型態、`perm` 指定了在sysfs中相應文件的存取權限。 ```c /** * module_param - typesafe helper for a module/cmdline parameter * @name: the variable to alter, and exposed parameter name. * @type: the type of the parameter * @perm: visibility in sysfs. * * @name becomes the module parameter, or (prefixed by KBUILD_MODNAME and a * ".") the kernel commandline parameter. Note that - is changed to _, so * the user can use "foo-bar=1" even for variable "foo_bar". * * @perm is 0 if the variable is not to appear in sysfs, or 0444 * for world-readable, 0644 for root-writable, etc. Note that if it * is writable, you may need to use kernel_param_lock() around * accesses (esp. charp, which can be kfreed when it changes). * * The @type is simply pasted to refer to a param_ops_##type and a * param_check_##type: for convenience many standard types are provided but * you can create your own by defining those variables. * * Standard types are: * byte, hexint, short, ushort, int, uint, long, ulong * charp: a character pointer * bool: a bool, values 0/1, y/n, Y/N. * invbool: the above, only sense-reversed (N = true). */ #define module_param(name, type, perm) \ module_param_named(name, name, type, perm) /** * module_param_named - typesafe helper for a renamed module/cmdline parameter * @name: a valid C identifier which is the parameter name. * @value: the actual lvalue to alter. * @type: the type of the parameter * @perm: visibility in sysfs. * * Usually it's a good idea to have variable names and user-exposed names the * same, but that's harder if the variable must be non-static or is inside a * structure. This allows exposure under a different name. */ #define module_param_named(name, value, type, perm) \ param_check_##type(name, &(value)); \ module_param_cb(name, &param_ops_##type, &value, perm); \ __MODULE_PARM_TYPE(name, #type) /** * module_param_cb - general callback for a module/cmdline parameter * @name: a valid C identifier which is the parameter name. * @ops: the set & get operations for this parameter. * @arg: args for @ops * @perm: visibility in sysfs. * * The ops can have NULL set or get functions. */ #define module_param_cb(name, ops, arg, perm) \ __module_param_call(MODULE_PARAM_PREFIX, name, ops, arg, perm, -1, 0) ``` `param_check_##type` 來確認型態是否正確;`module_param_cb` 以取得 cmdline 傳入的數值來更改參數存值;`__MODULE_PARM_TYPE` 用以將資訊記錄於 [`__MODULE_INFO`](https://hackmd.io/@sysprog/linux2020-fibdrv#-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) 這裡可以特別注意到的是,傳入的 `name` 會分別傳入傳入 `name` 和 `value` 的參數中,`name` 維持參數名稱,而 `value` 會取其變數位址,以設定其存值。 在 `main.c` 當中的使用情境如下: ```c static ushort port = DEFAULT_PORT; module_param(port, ushort, S_IRUGO); static ushort backlog = DEFAULT_BACKLOG; module_param(backlog, ushort, S_IRUGO); ``` 在掛載模組時插入參數,可參考 [The kernel’s command-line parameters](https://www.kernel.org/doc/html/v4.12/admin-guide/kernel-parameters.html) 規則。 ## kHTTPd 與 [CS:APP 第 11 章](https://hackmd.io/@sysprog/CSAPP-ch11?type=view) 比較 在 CS:APP 11.5 章撰寫了 userspace http server 的範例。 ## `htstress.c` 與 `epoll` 使用 > [kecho 的 user-echo-server epoll I/O](https://hackmd.io/@Masamaloka/linux2022-kecho#epoll-IO-monitor) > 參考 [`haogroot`](https://hackmd.io/@haogroot/2020q1linux-khttpd#2020q1-Homework4-khttpd) 在 `test.sh` 可以看到 htstress 執行的指令: ``` # run HTTP benchmarking ./htstress -n 100000 -c 1 -t 4 http://localhost:8081/ ``` 利用 `getopt_long(argc, argv)` 處理命令行參數。在這個命令意思是 max_requests=100000,每個 worker 開 1 個 `epoll` 並行的接收 I/O,建立 4 個執行序 worker。 ```c int next_option; do { next_option = getopt_long(argc, argv, short_options, long_options, NULL); switch (next_option) { case 'n': max_requests = strtoull(optarg, 0, 10); break; case 'c': concurrency = atoi(optarg); break; case 't': num_threads = atoi(optarg); break; ... } } while (next_option != -1); ``` `main` 在接收命令行參數、確認 http 連線沒問題、準備 request buffer ,隨後建立 pthread 開始執行 `worker`。 ```c static void *worker(void *arg) { ... // 1 int efd = epoll_create(concurrency); ... // 2 for (int n = 0; n < concurrency; ++n) init_conn(efd, ecs + n); //3 for (;;) { do { nevts = epoll_wait(efd, evts, sizeof(evts) / sizeof(evts[0]), -1); } while (!exit_i && nevts < 0 && errno == EINTR); ... } ... } // 2 static void init_conn(int efd, struct econn *ec) { ec->fd = socket(sss.ss_family, SOCK_STREAM, 0); ... do { ret = connect(ec->fd, (struct sockaddr *) &sss, sssln); } while (ret && errno == EAGAIN); ... struct epoll_event evt = { .events = EPOLLOUT, .data.ptr = ec, }; if (epoll_ctl(efd, EPOLL_CTL_ADD, ec->fd, &evt)) { perror("epoll_ctl"); exit(1); } } ``` 1. [`epoll_create`](https://man7.org/linux/man-pages/man2/epoll_create.2.html) 產生 epoll 專用的 file descriptor,裏面所帶參數須大於 0,自從 Linux 2.6.8 後,裏面參數已不代表任何意義。 2. 在 `init_conn` 建立 client socket ,隨後向目標 http server 發起連線 [connect](https://man7.org/linux/man-pages/man2/connect.2.html),並透過 [`epoll_ctl`](https://man7.org/linux/man-pages/man2/epoll_ctl.2.html) 將 client socket 加入到 epoll 中的 interest list,注意:這裡的 `epoll_event` 設定為 `EPOLLOUT`,即是 fd 準備寫入。 ``` EPOLLIN The associated file is available for read(2) operations. EPOLLOUT The associated file is available for write(2) operations. ``` 3. [`epoll_wait`](https://man7.org/linux/man-pages/man2/epoll_wait.2.html) 讓 interest list 當中的 fd 等待(block)直到接收到喚醒的 I/O,讓 fd 進到 ready list 以回應事件。`epoll_wait` 回傳共有多少 fd 已經準備執行。 4. 執行 ready list 上的 fd,首先檢查 `evts[n].events` 排除例外,正常執行中應只有 `EPOLLOUT` 和 `EPOLLIN` 的情形,分別代表準備輸出與接收。 ```c for (int n = 0; n < nevts; ++n) { // 4 exception ... // 5 if (evts[n].events & EPOLLOUT) { ret = send(ec->fd, outbuf + ec->offs, outbufsize - ec->offs, 0); if (ret > 0) { ec->offs += ret; ... /* write done? schedule read */ if (ec->offs == outbufsize) { evts[n].events = EPOLLIN; evts[n].data.ptr = ec; ec->offs = 0; } } // 6 } else if (evts[n].events & EPOLLIN) { for (;;) { ret = recv(ec->fd, inbuf, sizeof(inbuf), 0); ... if (ret <= 0) break; ... ec->offs += ret; } if (!ret) { close(ec->fd); // record the GOOD/BAD REQUEST init_conn(efd, ec); } } } ``` 5. 如果 `evts[n].events` 是 `EPOLLOUT` ,向 httpserver [send](https://man7.org/linux/man-pages/man2/send.2.html) requeset 訊息,如果完成了則轉換為 `EPOLLIN` 準備接收 response。 6. 如果 `evts[n].events` 是 `EPOLLIN` ,向 httpserver [recv](https://man7.org/linux/man-pages/man2/recv.2.html) response 訊息,使用迴圈分成多次接收,直到回傳小於零,記錄本次回傳成功 / 失敗,關閉這個 socket ,並重新執行 `init_conn`。 在此我們歸納 I/O 的模型與 epoll 帶來的好處: > [高效 Web 伺服器開發 #I/O 事件模型](https://hackmd.io/@sysprog/fast-web-server#IO-%E4%BA%8B%E4%BB%B6%E6%A8%A1%E5%9E%8B) Blocking I/O 在等待 I/O 資料準備就緒的過程,使用者層級的行程會被 blocked。為了提高 cpu 使用的效率,可以使用 I/O multiplexing 的機制,在單一行程下一次監聽多個 fd。在 `htstress.c` 中,我們可以一次建立多個 socket fd 放入到 epoll list 中,一旦某個 fd 準備好,就通知 process 來進行讀寫操作, Linux 提供了 3 種這樣的機制,分別為 poll, select 和 epoll。 epoll vs. select / poll - epoll 在 epoll_wait() 時候可以隨時新增、修改或刪除 file descriptors - epoll 只會返回準備好的 file descriptor , select / poll 必須要逐一確認 file descriptor - epoll 的可移植性較差,並不是所有系統都支援。 :::warning 參見 [v.s., vs, vs., v.s 哪個才對!](https://www.storm.mg/lifestyle/516454),`vs.` 是正確,但 `v.s.` 卻是不同的意思。 :notes: jserv ::: ## 如何正確的停止 `kthread` > 參考 [`haogroot`](https://hackmd.io/@haogroot/2020q1linux-khttpd#2020q1-Homework4-khttpd), [`AndybnA`](https://hackmd.io/@AndybnA/khttpd#khttpd-%E7%9A%84%E5%AF%A6%E4%BD%9C%E5%95%8F%E9%A1%8C%E8%88%87%E8%A7%A3%E6%B1%BA%E6%96%B9%E6%B3%95), [`bakudr18`](https://hackmd.io/@bakudr18/SkFA8Favu#Kernel-thread-%E6%9C%AA%E6%AD%A3%E7%A2%BA%E8%A2%AB%E9%87%8B%E6%94%BE), [`KYWeng`](https://hackmd.io/@KYWeng/H1OBDQdKL#%E5%A6%82%E4%BD%95%E6%AD%A3%E7%A2%BA%E7%9A%84%E5%81%9C%E6%AD%A2-kthread) ### `khttpd` 如何終止 `http_server` 在 `khttpd` 的實作中,`khttpd_init` 建立核心執行緒執行 `http_server_daemond`。`http_server_daemon` 接收到 client 連線時,建立新的核心執行序執行 `http_server_worker` 以處理客戶需求。 如果我們今天要卸載 khttpd 時,要如何向 kthread 通知訊息並安全的終止呢?在 khttpd 利用兩個技巧:一個是使用 [`kthread_stop` ](https://docs.huihoo.com/linux/kernel/2.6.26/kernel-api/re79.html),一個利用 [`signal`](https://man7.org/linux/man-pages/man7/signal.7.html) 來傳達終止訊息。 #### `kthread_stop` [`kthread_create`](https://docs.huihoo.com/linux/kernel/2.6.26/kernel-api/re77.html) 建立一個 kthread ,透過 `wake_up_process` 來喚醒執行 threadfn(如果建立後要立即喚醒執行可直接使用 [`kthread_run`](https://www.unix.com/man-page/suse/9/kthread_run))。文件中提到有兩種終止方式: 1. 如果不會有其他地方會執行 `kthread_stop` 來終止這個子執行緒 ,子執行緒可以直接使用 `do_exit` 關閉自己。 2. 主執行緒透過 `kthread_stop` 關閉子執行緒:將子執行緒的 `kthread_should_stop` 設為 1 ,此時喚醒子執行緒讓他確認 flag 的改變,待子執行緒被釋放。 需要注意:執行 `kthread_stop` 需要確保呼叫的執行緒是存在的,如果執行緒自行呼叫 `do_exit` 會發生錯誤,故而這兩種方式在使用上是互斥的。 以 khttp 建立核心執行緒 `http_server` 為例: ```c static int __init khttpd_init(void) { http_server = kthread_run(http_server_daemon, &param, KBUILD_MODNAME); } static void __exit khttpd_exit(void) { kthread_stop(http_server); } int http_server_daemon(void *arg) { while (!kthread_should_stop()) { ... } return 0; } ``` #### `signal` 在 [kernel thread and signal handling](https://www.linuxquestions.org/questions/linux-general-1/kernel-thread-and-signal-handling-372857/), [Thread Signal Handling](https://www.ibm.com/docs/en/aix/7.2?topic=threads-thread-signal-handling) 提到核心中處理訊號是有風險的,故在預設 kernel 環境下接收到的 signal 會忽略,包含 `SIGKILL`。於是透過 [`allow_signal`](https://github.com/torvalds/linux/blob/cb690f5238d71f543f4ce874aa59237cf53a877c/include/linux/signal.h#L299) 令建立之 kthread 得以關注並處理指定的訊號。透過 signal 來關閉執行緒的方式如下: 1. 設定 `allow_signal` 允許接收 terminate 類型的訊號。 2. 向執行緒 `send_sig` 發出終結訊號 3. 執行緒在被喚醒後透過 [`signal_pending`](https://man7.org/linux/man-pages/man3/signal_pending.3.html) 確認收到的訊號,並終止執行。 以 `khttpd` 向 `http_server` 發送終結訊號為例: ```c static void __exit khttpd_exit(void) { send_sig(SIGTERM, http_server, 1); kthread_stop(http_server); } int http_server_daemon(void *arg) { allow_signal(SIGKILL); allow_signal(SIGTERM); while (!kthread_should_stop()) { int err = kernel_accept(param->listen_socket, &socket, 0); if (err < 0) { if (signal_pending(current)) break; pr_err("kernel_accept() error: %d\n", err); continue; } } return 0; } ``` 這裡可以見到 `kthread_stop` 搭配 `signal` 的使用,為何需要這麼做呢?從 [`haogroot`](https://hackmd.io/@haogroot/2020q1linux-khttpd#%E9%87%8B%E6%94%BE-kernel-thread) 的記錄發現如果我們不使用 signal ,就算使用 `kthread_stop` 來改變 `kthread_should_stop` 的狀態,`http_server_daemon` 會因為仍在等待 `kernel_accept` 而進入 non-block waiting ,在無法脫離該狀態並 return 的狀態下,整段進程便卡住無法繼續。利用 `send_sig` 可讓 `http_server_daemon` 從 `kernel_accept` 中喚醒,並順利執行關閉的行為。 ### `khttpd` 的缺失 在卸載 khttpd 時我們確實的釋放了 `http_server`,然而卻無法一併釋放 `http_server_worker`。錯誤此時會發生在使用者正在連線時(瀏覽網頁 http://localhost:8081/ )卸載了核心模組,這使的 `http_server_worker` 並未一同釋放,讓我們依然可以看到並存取網頁上的內容。 目前我想到的解決方法是在 `http_server_daemon` 記錄與追蹤建立的 `http_server_worker` ,當接收到關閉的訊息時,先行釋放追蹤的 `http_server_worker` 再進行 return。 ## 以 cmwq 改寫 參考 `kecho` 的 cmwq 實作改寫 `khttpd`,除了透過 cmwq 協助安排執行資源,提昇並行的效益、減少額外建立 kthread 產生的成本。另外搭配 `kecho` 中管理 cmwq work_struct `echo_service` ,在卸載模組時使用 `free_work` 來釋放正在執行中的 work item ,解決原本 khttpd 實作上的缺陷。 [cmwq 實作程式碼](https://github.com/kevinshieh0225/khttpd/tree/bee714c157a9bad1151cf06a78b925ce8af78c5d) 原先 kthread 版本: ``` requests: 100000 good requests: 100000 [100%] bad requests: 0 [0%] socker errors: 0 [0%] seconds: 2.796 requests/sec: 35771.379 ``` cmwq 版本: ``` requests: 100000 good requests: 100000 [100%] bad requests: 0 [0%] socker errors: 0 [0%] seconds: 1.693 requests/sec: 59055.618 ```