# 2020q1 Homework4 (khttpd) contributed by < [`KYG-yaya573142`](https://github.com/KYG-yaya573142/khttpd) > > [H08: khttpd](https://hackmd.io/@sysprog/linux2020-khttpd) ## 預期目標 * 學習 Linux 核心的 kernel thread 處理機制 * 預習電腦網路原理 * 整合 fibdrv 開發成果 * 學習 [Concurrency Managed Workqueue (cmwq)](https://www.kernel.org/doc/html/v4.15/core-api/workqueue.html) ## 模組掛載如何使用初始化的參數 khttpd 可在掛載模組時自訂 `port` 和 `backlog` 的初始化數值,相關的程式碼如下 ```cpp static ushort port = DEFAULT_PORT; module_param(port, ushort, S_IRUGO); static ushort backlog = DEFAULT_BACKLOG; module_param(backlog, ushort, S_IRUGO); ``` * 使用 [`module_param`](https://elixir.bootlin.com/linux/latest/source/include/linux/moduleparam.h#L126) 來達成自訂初始化數值的功能 * S_IRUGO 的定義可在 [/linux/stat.h](https://elixir.bootlin.com/linux/latest/source/include/linux/stat.h#L11) 查詢 * 其實也可以直接用數字表示權限,[File permissions in the kernel](https://lwn.net/Articles/696227/) 有相關的討論 從 [/include/linux/moduleparam.h](https://elixir.bootlin.com/linux/latest/source/include/linux/moduleparam.h#L126) 觀察可以發現 `module_param` 被很多層 wrapper 包住,以下截取相關的部分 ```cpp #define module_param(name, type, perm) \ module_param_named(name, name, type, perm) #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) #define module_param_cb(name, ops, arg, perm) \ __module_param_call(MODULE_PARAM_PREFIX, name, ops, arg, perm, -1, 0) #define MODULE_PARAM_PREFIX /* empty */ ``` * 最終負責的部分是 `__module_param_call` * 以 `module_param(port, ushort, S_IRUGO)` 為例,展開後是 `__module_param_call(MODULE_PARAM_PREFIX, port, &param_ops_ushort, &port, S_IRUGO, -1, 0)` * `param_check_##type(name, &(value))` 會在 compile-time checking,可參閱 [`__param_check`](https://elixir.bootlin.com/linux/latest/source/include/linux/moduleparam.h#L408) 的註解 * [`__MODULE_PARM_TYPE`](https://elixir.bootlin.com/linux/latest/source/include/linux/moduleparam.h#L408) 實際上會展開成 `__MODULE_INFO`,相關的說明可參閱 fibdrv 作業說明 - [Linux 核心模組掛載機制](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) ```cpp /* This is the fundamental function for registering boot/module parameters. */ #define __module_param_call(prefix, name, ops, arg, perm, level, flags) \ /* Default value instead of permissions? */ \ static const char __param_str_##name[] = prefix #name; \ static struct kernel_param __moduleparam_const __param_##name \ __used \ __attribute__ ((unused,__section__ ("__param"),aligned(sizeof(void *)))) \ = { __param_str_##name, THIS_MODULE, ops, \ VERIFY_OCTAL_PERMISSIONS(perm), level, flags, { arg } } ``` * `#` - [Stringification](https://gcc.gnu.org/onlinedocs/gcc-3.0.1/cpp_3.html#SEC17) * `##` - [Concatenation](https://gcc.gnu.org/onlinedocs/gcc-3.0.1/cpp_3.html#SEC18) * 關鍵字 [`__attribute__`](https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html#index-used-variable-attribute) 會告知 compiler 以下事項 * 此變數可能不會被使用,不須警告 * 要放在 `__param` 這個 section,可用 `objdump -s khttpd.ko` 查詢 * 要以 pointer 的大小進行對齊 * 因此 `__module_param_call` 最終會定義一個資料類型為 `struct kernel_param` 的物件並放在 khttpd.ko 的 `__param` 這個 section 根據 [`struct kernel_param`](https://elixir.bootlin.com/linux/latest/source/include/linux/moduleparam.h#L69) 的定義,以 `module_param(port, ushort, S_IRUGO)` 為例進行展開如下 ```cpp struct kernel_param { const char *name = "port"; struct module *mod = THIS_MODULE; const struct kernel_param_ops *ops = &param_ops_ushort; const u16 perm = 0444; //S_IRUGO s8 level = -1; u8 flags = 0; union { void *arg = &port; const struct kparam_string *str; const struct kparam_array *arr; }; }__param_port; ``` 用 `readelf -r khttpd.ko` 觀察,可以看到 `__param` 內確實有對應的資料 ```shell Relocation section '.rela__param' at offset 0xcb78 contains 8 entries: Offset Info Type Sym. Value Sym. Name + Addend 000000000000 000500000001 R_X86_64_64 0000000000000000 .rodata + 1240 000000000008 004200000001 R_X86_64_64 0000000000000000 __this_module + 0 000000000010 004000000001 R_X86_64_64 0000000000000000 param_ops_ushort + 0 000000000020 000b00000001 R_X86_64_64 0000000000000000 .data + 4 000000000028 000500000001 R_X86_64_64 0000000000000000 .rodata + 1248 000000000030 004200000001 R_X86_64_64 0000000000000000 __this_module + 0 000000000038 004000000001 R_X86_64_64 0000000000000000 param_ops_ushort + 0 000000000048 000b00000001 R_X86_64_64 0000000000000000 .data + 6.data ``` 再用 `objdump -s -j .rodata khttpd.ko` 對照 `.rodata` 的部分,可以確認 `__param` 內的兩筆資料就是 `module_param(port, ushort, S_IRUGO)` 與 `module_param(backlog, ushort, S_IRUGO)` ```shell khttpd.ko: file format elf64-x86-64 Contents of section .rodata: 0000 00000000 00000000 00000000 00000000 ................ ... 1240 6261636b 6c6f6700 706f7274 00 backlog.port. ``` 接著使用 [strace](http://man7.org/linux/man-pages/man1/strace.1.html) 觀察 insmod 時使用的 system calls,比對指定參數和不指定參數的狀況,可以發現差異主要是 `finit_module` 的參數會不同 ```shell $ sudo strace insmod khttpd.ko ... finit_module(3, "", 0) = 0 ... $ sudo strace insmod khttpd.ko port=1999 ... finit_module(3, "port=1999", 0) = 0 ... ``` `finit_module` 是 system call,其定義可以在 [/kernel/module.c](https://elixir.bootlin.com/linux/latest/source/kernel/module.c#L3956) 中找到 ```cpp SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags) { ... return load_module(&info, uargs, flags); } ``` * 主要的實作則是 [`load_module`](https://elixir.bootlin.com/linux/latest/source/kernel/module.c#L3740) * `finit_module(3, "port=1999", 0)` 的第 2 個參數 `"port=1999"` 對照到 `uargs` ```cpp /* Allocate and load the module: note that size of section 0 is always zero, and we rely on this for optional sections. */ static int load_module(struct load_info *info, const char __user *uargs, int flags) { ... /* Now copy in args */ mod->args = strndup_user(uargs, ~0UL >> 1); if (IS_ERR(mod->args)) { err = PTR_ERR(mod->args); goto free_arch_cleanup; } ... /* Module is ready to execute: parsing args may do that. */ after_dashes = parse_args(mod->name, mod->args, mod->kp, mod->num_kp, -32768, 32767, mod, unknown_module_param_cb); ... } ``` * `"port=1999"` 會被複製到 `mod->args` * 接著使用 [`parse_args`](https://elixir.bootlin.com/linux/latest/source/arch/x86/tools/insn_sanity.c#L162) 來執行 ```cpp /* Args looks like "foo=bar,bar2 baz=fuz wiz". */ char *parse_args(const char *doing, char *args, const struct kernel_param *params, unsigned num, s16 min_level, s16 max_level, void *arg, int (*unknown)(char *param, char *val, const char *doing, void *arg)) { char *param, *val, *err = NULL; /* Chew leading spaces */ args = skip_spaces(args); if (*args) pr_debug("doing %s, parsing ARGS: '%s'\n", doing, args); while (*args) { int ret; int irq_was_disabled; args = next_arg(args, &param, &val); /* Stop at -- */ if (!val && strcmp(param, "--") == 0) return err ?: args; irq_was_disabled = irqs_disabled(); ret = parse_one(param, val, doing, params, num, min_level, max_level, arg, unknown); ... } ``` * [`next_arg`](https://elixir.bootlin.com/linux/latest/source/lib/cmdline.c#L201) 會解析原本的 `"param=val"` 然後將結果存到變數 `param` 和 `val` * [`parse_one`](https://elixir.bootlin.com/linux/latest/source/kernel/params.c#L115) 是真正將數值更新到對應的 `kernel_param` 物件的函數 ```cpp static int parse_one(char *param, char *val, const char *doing, const struct kernel_param *params, unsigned num_params, s16 min_level, s16 max_level, void *arg, int (*handle_unknown)(char *param, char *val, const char *doing, void *arg)) { unsigned int i; int err; /* Find parameter */ for (i = 0; i < num_params; i++) { if (parameq(param, params[i].name)) { ... kernel_param_lock(params[i].mod); if (param_check_unsafe(&params[i])) err = params[i].ops->set(val, &params[i]); else err = -EPERM; kernel_param_unlock(params[i].mod); return err; } } if (handle_unknown) { pr_debug("doing %s: %s='%s'\n", doing, param, val); return handle_unknown(param, val, doing, arg); } pr_debug("Unknown argument '%s'\n", param); return -ENOENT; } ``` * `if (parameq(param, params[i].name))` 尋找參數對應到的 `kernel_param` 物件 * `err = params[i].ops->set(val, &params[i])` 使用 `kernel_param` 物件中參照的 handler 來設置數值 * 接續前面的例子, `ops` 指的就是 `param_ops_ushort` ## kHTTPd 與 CSAPP server 的異同 [CSAPP](http://www.cs.cmu.edu/afs/cs/academic/class/15213-f17/www/schedule.html) 中提到的 Web server 架構如下圖 ![](https://i.imgur.com/rm2xcwv.png) [kHTTPd](https://hackmd.io/@sysprog/linux2020-khttpd) 執行 server 的流程如下 ```graphviz digraph khttpd { node[shape=record]; rankdir=LR; subgraph cluster_init { label ="khttpd_init"; proc1 [label="{{open_listen_socket|sock_create\nkernel_bind\nkernel_listen}}"]; next1 [label="kthread_run", shape=box]; } subgraph cluster_daemon { label ="http_server_daemon"; accept [label="kernel_accept", shape=box]; next2 [label="kthread_run", shape=box]; } worker1 [label ="worker"]; worker2 [label ="worker"]; worker3 [label ="worker"]; proc1->next1 next1->accept accept->next2 next2->worker1 next2->worker2 next2->worker3 } ``` 比對兩者,可以發現差異主要是 * CSAPP 的 server 架構在 user space 實行 * CSAPP 的示意圖雖然沒有使用 thread,但後續的課程有改寫為使用 thread 的 server * kHTTPd 的 server 架構則在 kernel space 實行 * 對應的 kernel api 如下 * [`sock_create`](https://www.kernel.org/doc/html/v5.4/networking/kapi.html#c.sock_create) * [`kernel_bind`](https://www.kernel.org/doc/html/v5.4/networking/kapi.html#c.kernel_bind) * [`kernel_listen`](https://www.kernel.org/doc/html/v5.4/networking/kapi.html#c.kernel_listen) * [`kernel_accept`](https://www.kernel.org/doc/html/v5.4/networking/kapi.html#c.kernel_accept) ### 觀察 Linux Networking api 觀察 [`/net/socket.c`](https://elixir.bootlin.com/linux/latest/source/net/socket.c) 中 `kernel_bind` 、 `kernel_listen` 與 `kernel_accept` 的實作,會發現全部都是呼叫 `sock->ops` 中對應的 function pointer,顯然不同的 protocol family 會定義各自的實作,再根據呼叫 `sock_create` 時使用的參數來取用對應的 function pointer ```cpp int kernel_bind(struct socket *sock, struct sockaddr *addr, int addrlen) { return sock->ops->bind(sock, addr, addrlen); } int kernel_listen(struct socket *sock, int backlog) { return sock->ops->listen(sock, backlog); } int kernel_accept(struct socket *sock, struct socket **newsock, int flags) { ... err = sock->ops->accept(sock, *newsock, flags, true); ... } ``` `sock->ops` 會在 [`sock_create`](https://elixir.bootlin.com/linux/latest/source/net/socket.c#L1482) 內進行初始化,但整個流程很不直觀,首先觀察 `sock_create` 中的 `pf->create` ```cpp int __sock_create(struct net *net, int family, int type, int protocol, struct socket **res, int kern) { ... const struct net_proto_family *pf; ... pf = rcu_dereference(net_families[family]); ... err = pf->create(net, sock, protocol, kern); ... } ``` 從 [`/net/ipv4/af_inet.c`](https://elixir.bootlin.com/linux/latest/source/net/ipv4/af_inet.c#L1079) 可以發現 `pf->create` 其實就是 [`inet_create`](https://elixir.bootlin.com/linux/latest/source/net/ipv4/af_inet.c#L247) (註:[`inet_init`](https://elixir.bootlin.com/linux/latest/source/net/ipv4/af_inet.c#L1909) 會呼叫 [`sock_register`](https://elixir.bootlin.com/linux/latest/source/net/socket.c#L2975) 來初始化 `net_families[]`) ```cpp static const struct net_proto_family inet_family_ops = { .family = PF_INET, .create = inet_create, .owner = THIS_MODULE, }; ``` 觀察 `inet_create` 會發現 `sock->ops` 就是在這裡被設置的,其中 [`list_for_each_entry_rcu`](https://elixir.bootlin.com/linux/latest/source/include/linux/rculist.h#L370) 會將 `answer` 指向 `inetsw[]` 中對應的 type/protocol pair ```cpp static int inet_create(struct net *net, struct socket *sock, int protocol, int kern) { ... /* Look for the requested type/protocol pair. */ lookup_protocol: err = -ESOCKTNOSUPPORT; rcu_read_lock(); list_for_each_entry_rcu(answer, &inetsw[sock->type], list) { ... } ... sock->ops = answer->ops; ... } ``` 進一步查看 [`inetsw`](https://elixir.bootlin.com/linux/latest/source/net/ipv4/af_inet.c#L1088) 會發現其中確實記錄著 type/protocol pair 的相關資料,例如當 type 是 `SOCK_STREAM` 時,對應的 `ops` 為 `inet_stream_ops` ```cpp /* Upon startup we insert all the elements in inetsw_array[] into * the linked list inetsw. */ static struct inet_protosw inetsw_array[] = { { .type = SOCK_STREAM, .protocol = IPPROTO_TCP, .prot = &tcp_prot, .ops = &inet_stream_ops, .flags = INET_PROTOSW_PERMANENT | INET_PROTOSW_ICSK, }, ... } ``` 最後就可以在 [`inet_stream_ops[]`](https://elixir.bootlin.com/linux/latest/source/net/ipv4/af_inet.c#L983) 中找到對應函數的實際名稱了 ```cpp const struct proto_ops inet_stream_ops = { .family = PF_INET, .owner = THIS_MODULE, .release = inet_release, .bind = inet_bind, .connect = inet_stream_connect, .socketpair = sock_no_socketpair, .accept = inet_accept, .getname = inet_getname, .poll = tcp_poll, .ioctl = inet_ioctl, .gettstamp = sock_gettstamp, .listen = inet_listen, .shutdown = inet_shutdown, .setsockopt = sock_common_setsockopt, .getsockopt = sock_common_getsockopt, .sendmsg = inet_sendmsg, .recvmsg = inet_recvmsg, #ifdef CONFIG_MMU .mmap = tcp_mmap, #endif .sendpage = inet_sendpage, .splice_read = tcp_splice_read, .read_sock = tcp_read_sock, .sendmsg_locked = tcp_sendmsg_locked, .sendpage_locked = tcp_sendpage_locked, .peek_len = tcp_peek_len, #ifdef CONFIG_COMPAT .compat_setsockopt = compat_sock_common_setsockopt, .compat_getsockopt = compat_sock_common_getsockopt, .compat_ioctl = inet_compat_ioctl, #endif .set_rcvlowat = tcp_set_rcvlowat, }; ``` ## 整合 fibdrv ### khttpd 運作邏輯 khttpd 由 1 個 kthread `http_server_daemon` 負責 `kernel_accept`,每次 accept 成功後會產生 kthread `http_server_worker` 來實際執行任務。 ```cpp static int http_server_worker(void *arg) { char *buf; struct http_parser parser; struct http_parser_settings setting = { .on_message_begin = http_parser_callback_message_begin, .on_url = http_parser_callback_request_url, .on_header_field = http_parser_callback_header_field, .on_header_value = http_parser_callback_header_value, .on_headers_complete = http_parser_callback_headers_complete, .on_body = http_parser_callback_body, .on_message_complete = http_parser_callback_message_complete}; ... while (!kthread_should_stop()) { int ret = http_server_recv(socket, buf, RECV_BUFFER_SIZE - 1); if (ret <= 0) { if (ret) pr_err("recv error: %d\n", ret); break; } http_parser_execute(&parser, &setting, buf, ret); if (request.complete && !http_should_keep_alive(&parser)) break; } ... } ``` * 運作的架構使用 [HTTP Parser APIs](https://github.com/nodejs/http-parser) * 每個連線都會有一個對應的 `http_parser` 物件 * 根據設置在 `http_parser_settings` 物件的 callback functions 來處理 [HTTP Messages](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages) * callback functions 需藉由 `parser->data` 來取用 kthread scope 的資料 (thread safe) * 運作的邏輯大致如下 * 初始化 HTTP Parser 相關的部分,其中 `parser->data` 會指向 worker 用來儲存資料的 `http_request` 物件 * `http_server_recv` 從 socket 接收訊息至 `buf` * 實作為 [kernel_recvmsg](https://www.kernel.org/doc/html/v5.4/networking/kapi.html#c.kernel_recvmsg) * `http_parser_execute` 開始執行 HTTP Parser 的各個 callback functions * 處理 url (使用 `on_url` 指向的 callback) * 依序處理 [HTTP header fields](https://en.wikipedia.org/wiki/List_of_HTTP_header_fields) 和 values (使用 `on_header_field` 與 `on_header_value` 指向的 callback) * 處理 HTTP body (使用 `on_body` 指向的 callback) * `http_server_response` 執行對應的動作,產生送回給 client 的資料 * `http_server_send` 送回資料給 client * 實作為 [kernel_sendmsg](https://www.kernel.org/doc/html/v5.4/networking/kapi.html#c.kernel_sendmsg) ### 加入 fibdrv `http_server_response` 原本執行的動作很單純,將 URL 輸出至 kernel ring buffer,然後根據 client request 的內容回傳不同的固定訊息給 client ```cpp static int http_server_response(struct http_request *request, int keep_alive) { char *response; pr_info("requested_url = %s\n", request->request_url); if (request->method != HTTP_GET) response = keep_alive ? HTTP_RESPONSE_501_KEEPALIVE : HTTP_RESPONSE_501; else response = keep_alive ? HTTP_RESPONSE_200_KEEPALIVE_DUMMY : HTTP_RESPONSE_200_DUMMY; http_server_send(request->socket, response, strlen(response)); return 0; } ``` 接下來會改寫 `http_server_response`,追加能計算費氏數列的功能,改寫的過程主要可分為兩個項目 * `http_server_response` - 追加從 url 偵測關鍵字並呼叫 `do_fibonacci` 的能力 * `do_fibonacci` - 除了計算費氏數列外,還包含如何從 url 拆解所需參數、如何實現字串轉與數值的轉換、以及簡易的防呆處理 #### `http_server_response` ```cpp #include "fibdrv.h" ... #define HTTP_RESPONSE_200 \ "" \ "HTTP/1.1 %s" CRLF "Server: " KBUILD_MODNAME CRLF \ "Content-Type: text/plain" CRLF "Content-Length: %lu" CRLF \ "Connection: %s" CRLF CRLF "%s" CRLF ... static int http_server_response(struct http_request *request, int keep_alive) { char *connection = keep_alive? "Keep-Alive" : "Close"; char *status = "501 Not Implemented"; char *body = "501 Not Implemented"; char *response = NULL; char *target = NULL; bool flag = 0; pr_info("requested_url = %s\n", request->request_url); if (request->method == HTTP_GET) { status = "200 OK"; body = "Hello World!"; //default msg } /* fib function entry */ if ((target = strstr(request->request_url, "/fib/"))) { flag = 1; body = do_fibonacci(target); } response = kmalloc(MSG_BUFFER_SIZE, GFP_KERNEL); snprintf(response, MSG_BUFFER_SIZE, HTTP_RESPONSE_200, status, strlen(body), connection, body); http_server_send(request->socket, response, strlen(response)); kfree(response); if (flag) kfree(body); return 0; } ``` * `if ((target = strstr(request->request_url, "/fib/")))` 負責偵測是否需要呼叫 `do_fibonacci`,關鍵字為 `"\fib\"` * 使用 [`strstr`](https://www.kernel.org/doc/html/v5.4/core-api/kernel-api.html#c.strstr) 而不是 [`strnstr`](https://www.kernel.org/doc/html/v5.4/core-api/kernel-api.html#c.strnstr) 是因為我有改寫 `http_parser_callback_request_url`,確保 `request->request_url` 一定是 null terminated,後續討論會提到 * 由於 `do_fibonacci` 會使用 `kmalloc` 來存放回傳的字串,因此 server 回傳資料後須 `kfree` * 只是單純要加入費氏數列計算的功能的話,並不需要將整個 `http_server_response` 改寫,但目前這種寫法保留了更好的擴充性 * 只要將 `body` 指向欲回傳的內容,[`snprintf`](https://www.kernel.org/doc/html/v5.4/core-api/kernel-api.html#c.snprintf) 就會負責組成完整的回傳內容 * `connection` 和 `status` 也可以用一樣的方式更換內容 * 注意 `snprintf` 的 src 和 dest 不能 overlap,這也是為何 `do_fibonacci` 會再 `kmalloc` 的原因 #### `do_fibonacci` ```cpp static char *do_fibonacci(char* num_ptr) { int n = -1; char *msg = kmalloc(MSG_BUFFER_SIZE, GFP_KERNEL); char *next_tok = NULL; int num_length = 0; num_ptr += 5; //skip "/fib/" next_tok = strstr(num_ptr, "/"); if (!next_tok) num_length = strlen(num_ptr); else num_length = (uint64_t) next_tok - (uint64_t) num_ptr; strncpy(msg, num_ptr, num_length); //no modification on original URL msg[num_length] = '\0'; kstrtoint(msg, 10, &n); if (n < 0) { snprintf(msg, MSG_BUFFER_SIZE, "fib(%d): invalid arguments!", n); } else { long long result = fib_sequence_fdouble(n); snprintf(msg, MSG_BUFFER_SIZE, "fib(%d): %lld", n, result); } return msg; } ``` * `do_fibonacci` 是費氏數列計算函數 `fib_sequence_fdouble` 的 wrapper,負責處理字串的部分 * 傳入 `do_fibonacci` 的 url 會包含關鍵字之後的部分,如 `"\fib\..."` * 使用 `strstr(num_ptr, "/")` 來定位數字所在的位置,因此可接受數字用 `'/'` 結尾或是數字直接置於 url 末端 * `".../fib/22"` * `".../fib/22/"` * 使用 [`kstrtoint`](https://www.kernel.org/doc/html/v5.4/core-api/kernel-api.html#c.kstrtoint) 來將數字字串轉換為數值,由於 `kstrtoint` 只能處理 null terminated 的字串,須確保數字的末端是 `'\0'`, * 雖然可以直接使用 `request->request_url` 的空間,但為了擴充性 (例如還有別的函式會針對 url 進行判斷),會先將原本的 url `strcpy` 到新的空間再進行處理 * 一樣使用 `snprintf` 將費氏數列結果轉換成字串並回傳 * 防呆可以預防非數字、負數與無參數的狀況 * `".../fib/"` * `".../fib/-87"` * `".../fib//87"` * `".../fib/nan"` * 目前 `fib_sequence_fdouble` 是含有缺陷的簡易版實作,尚未處裡 overflow 與應用大數運算,待補完 fibdrv 相關的部分再修正 ~~欠的債遲早要還的~~ ## `htstress.c` 使用 `epoll` 系統呼叫,其作用為何? ### `epoll` 簡介 [man 7 epoll](http://man7.org/linux/man-pages/man7/epoll.7.html) > The epoll API performs a similar task to poll(2): monitoring multiple file descriptors to see if I/O is possible on any of them. epoll API 可用來監測數個 file descriptors (fd) 是否有可用的 I/O 事件,epoll API 的關鍵是 epoll instance (一個 kernel data structure - [`struct eventpoll`](https://elixir.bootlin.com/linux/latest/source/fs/eventpoll.c#L181)),使用時概念上可將 epoll instance 視為包含兩個清單的物件 * interest list - 包含欲監測的 fd * ready list - 包含有可用 I/O 事件的 fd (註:ready list 是 interest list 的子集合) 以下簡單介紹 epoll API 的使用方式 ```cpp #include <sys/epoll.h> int epoll_create(int size); int epoll_create1(int flags); ``` * 創立一個 epoll instance 並回傳其 fd * 實際上是創立一個 [`struct file`](https://elixir.bootlin.com/linux/latest/source/include/linux/fs.h#L936),並將 `struct eventpoll` 放在 `file->private_data`,這也是為何 epoll instance 是藉由 fd 來使用,且使用完畢後須 [`close`](http://man7.org/linux/man-pages/man2/close.2.html) * `size` - 若 Linux 核心版本大於 2.6.8 不會有實際用途,但輸入值須大於 0,詳見 [man epoll_create](http://man7.org/linux/man-pages/man2/epoll_create.2.html) ```cpp int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); ``` * 修改 epoll instance 中的 interest list * `epfd` - 欲修改的 epoll instance 對應的 file descriptor * `op` - 欲進行的操作 * `EPOLL_CTL_ADD` 新增 `fd` 至 interest list * `EPOLL_CTL_DEL` 從 interest list 中移除 `fd` * `EPOLL_CTL_MOD` 修改 interest list 中 `fd` 被註冊的 event * `fd` - 受操作的目標 fd * `event` - 指向一個與 `fd` 關聯的 `epoll_event` 物件,而 `epoll_event` 包含 `events` 與 `data` 這兩個資料成員 * `event->events` - 以 bitmask 的形式記錄欲對目標 fd 監測的事件種類,例如 * `EPOLLIN` 監測 fd 可被 read * `EPOLLOUT` 監測 fd 可被 write * `EPOLLERR` 監測 fd 發生錯誤 * 以 OR 操作來同時註冊多個 event * 尚有其他 event,詳見 [man epoll_ctl](http://man7.org/linux/man-pages/man2/epoll_ctl.2.html) * `event->data` - 紀錄與 `fd` 關聯的資料 * 以 `union` 的方式定義,視使用者用途來決定如何使用,以 socket 的應用來說就是登記 `fd` ``` typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; ``` ```cpp int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask); ``` * 等待 epoll instance 中的 ready list 是否有可用的 fd * `epfd` - 欲等待的 epoll instance 對應的 file descriptor * `events` - `epoll_wait` 會將 ready list 中可用的 fd 對應的 `epoll_event` 存入 `events` 指向的 buffer * `events->events` 紀錄此 fd 可用的 I/O 事件 * `events->data` 會與最近一次對此 fd 執行 `epoll_ctl` 時註冊的內容一致 * `maxevents` - `epoll_wait` 回傳 `epoll_event` 的數量上限 * `timeout` - 指定 `epoll_wait` 等待的時間 (ms) * `-1` 代表無等待時間限制 * `0` 代表 ready list 無可用 fd 會直接返回* * 其餘說明詳見 [man epoll_wait](http://man7.org/linux/man-pages/man2/epoll_wait.2.html) ### `epoll` vs `select` `epoll` 和 `select` 具有類似的作用,但 `epoll` 有更好的效能,也沒有監測數量上限的問題。在 `epoll` 的 [Wiki page](https://en.wikipedia.org/wiki/Epoll) 提到,相較於 `select` 的時間複雜度為 $O(n)$,`epoll` 的時間複雜度僅為 $O(1)$,然而從 [/fs/eventpoll.c](https://elixir.bootlin.com/linux/latest/source/fs/eventpoll.c#L137) 觀察 `epoll_ctl` 的實作,可以發現是使用 [RB tree](https://en.wikipedia.org/wiki/Red%E2%80%93black_tree) 來管理加入 epoll instance 的 fd,因此準確來說是 `epoll_wait` 的時間複雜度為 $O(1)$,而 `epoll_ctl` 的時間複雜度為 $O(log(n))$ ```cpp /* * Each file descriptor added to the eventpoll interface will * have an entry of this type linked to the "rbr" RB tree. * Avoid increasing the size of this struct, there can be many thousands * of these on a server and we do not want this to take another cache line. */ struct epitem { union { /* RB tree node links this structure to the eventpoll RB tree */ struct rb_node rbn; /* Used to free the struct epitem */ struct rcu_head rcu; }; /* List header used to link this structure to the eventpoll ready list */ struct list_head rdllink; ... } ``` 同時也可以觀察到,只有在 `epoll_ctl` 的時候會使用 `copy_from_user` ```cpp /* * The following function implements the controller interface for * the eventpoll file that enables the insertion/removal/change of * file descriptors inside the interest set. */ SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd, struct epoll_event __user *, event) { struct epoll_event epds; if (ep_op_has_event(op) && copy_from_user(&epds, event, sizeof(struct epoll_event))) return -EFAULT; return do_epoll_ctl(epfd, op, fd, &epds, false); } ``` 接著從 [/fs/select.c](https://elixir.bootlin.com/linux/latest/source/fs/select.c#L722) 觀察 `select` 的實作,會發現每次 `select` 都會使用 `copy_from_user`,而且還用了不只一次 (在 `core_sys_select` 也有使用) ```cpp static int kern_select(int n, fd_set __user *inp, fd_set __user *outp, fd_set __user *exp, struct __kernel_old_timeval __user *tvp) { struct timespec64 end_time, *to = NULL; struct __kernel_old_timeval tv; int ret; if (tvp) { if (copy_from_user(&tv, tvp, sizeof(tv))) return -EFAULT; to = &end_time; if (poll_select_set_timeout(to, tv.tv_sec + (tv.tv_usec / USEC_PER_SEC), (tv.tv_usec % USEC_PER_SEC) * NSEC_PER_USEC)) return -EINVAL; } ret = core_sys_select(n, inp, outp, exp, to); return poll_select_finish(&end_time, tvp, PT_TIMEVAL, ret); } SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp, fd_set __user *, exp, struct __kernel_old_timeval __user *, tvp) { return kern_select(n, inp, outp, exp, tvp); } ``` `select` 每次執行都需對整個 `fd_set` 內註冊的 fd 進行檢驗,導致時間複雜度是 $O(n)$,在執行過程還使用了數次的 `copy_from_user`;相較之下 `epoll_wait` 僅需確認 ready list 中是否存在可用的 fd,時間複雜度僅為 $O(1)$,就算使用 `epoll_ctl` 修改列表時的複雜度也僅為 $O(log(n))$,且只會用到一次 `copy_from_user`,綜合上述因素,造成 `epoll` 的效能好於 `select` ### `htstress.c` 的運作原理 `htstress` 是一個 client,根據使用者定義的參數向 server 請求數據,最後會列出 server 回應每個連線平均所花費的時間,使用者需定義的參數如下 ```shell Usage: htstress [options] [http://]hostname[:port]/path Options: -n, --number total number of requests (0 for inifinite, Ctrl-C to abort) -c, --concurrency number of concurrent connections -t, --threads number of threads (set this to the number of CPU cores) -u, --udaddr path to unix domain socket -h, --host host to use for http request -d, --debug debug HTTP response --help display this message ``` * `-n` 向 server 請求的 request 總數 * `-c` 每個 worker thread 向 server 連線的總數 * `-t` 建立的 worker thread 總數 `htstress` 的運作主要分成兩個部分,負責連線前置作業與列出測試結果的 `main`,以及實際和 server 建立連線的 `worker` thread,以下將簡單列出兩者的運作流程與重點,首先是 `main` 的部分 ```flow st=>start: htstress op1=>operation: parse arguments parse URL getaddrinfo() op4=>operation: start_time() op5=>parallel: pthread_create() e=>end: output result sub1=>subroutine: worker(s) st->op1->op4->op5(path1, bottom)->e op5(path2, right)->sub1 ``` * parse arguments 的部分主要使用 [`getopt_long`](https://linux.die.net/man/3/getopt_long) * 用 [`getaddrinfo`](http://man7.org/linux/man-pages/man3/getaddrinfo.3.html) 來取得 Host * [`sockaddr_in`](http://man7.org/linux/man-pages/man7/ip.7.html)、[`sockaddr_un`](http://man7.org/linux/man-pages/man7/unix.7.html)、[`sockaddr_in6`](http://man7.org/linux/man-pages/man7/ipv6.7.html) * `start_time` 單純的紀錄當下的時間 * 題外話,也可以參照 parse URL 的部分來實作解析關鍵字 `"\fib\"` 接下來是 `worker` thread 的部分 ```flow st=>start: worker op1=>operation: epoll_create() op2=>operation: socket() connect() epoll_ctl() op3=>operation: epoll_wait() op4=>operation: communicate with server cond1=>condition: all requests? e=>end: end_time() return st(right)->op1(right)->op2->op3->op4->cond1 cond1(yes)->e cond1(no)->op3 ``` * 每個 `worker` 會向 server 建立共 concurrency 個連線 * 與 server 建立連線成功後,將 socket fd 加入 epoll * 接下來會是一個 for loop,會不斷迴圈直到所有 worker thread 累積處理完畢的 request 數量達到指定的數量 * 首先使用 `epoll_wait` 來監測 socket,再**以輪詢的方式**依序使用 `epoll_wait` 回傳可用的 fd * 利用 socket 註冊的 epoll event 來區分此 socket 和 server 應進行的步驟 * 每個 socket 最初都是 `EPOLLOUT`,也就是監測 socket 是否能向 server 傳送資料 * 傳送成功後,會將註冊的 epoll event 改為 `EPOLLIN`,也就是監測 socket 是否能向 server 接收資料 * 接收資料成功後,將完成的 request 計數加 1 * 接收資料成功後,檢查所有 worker thread 累積處理完畢的 request 數量是否已達標 * 若尚未達標,再次迴圈 * 若已足夠,使用 `end_time` 紀錄結束時間並結束 thread * 離開迴圈的同時也代表結束時間已由 `end_time` 確認,此時 `main` 就可以輸出測試結果了 ## 可改善的部分 ### `http_parser_callback_request_url` ```cpp static int http_parser_callback_request_url(http_parser *parser, const char *p, size_t len) { struct http_request *request = parser->data; size_t old_len = strlen(request->request_url); if ((len + old_len) > 127) { // max length = 128 - 1 ('\0') pr_err("url error: url truncated!\n"); len = 127 - old_len; } strncat(request->request_url, p, len); return 0; } ``` * `strncat` 保證會 null terminated,但須確保有足夠的空間 * 確保寫入的大小不超過 127 bytes * 由於已經確保 `request->request_url` 會 null terminated,搜尋 URL 關鍵字時可安全的使用 `strstr` ### `recv error` 使用 `wget` 測試時,發現會出現以下錯誤訊息 ```shell [139290.526853] khttpd: recv error: -104 ``` 查詢後從 [errno.h](https://elixir.bootlin.com/linux/latest/source/include/uapi/asm-generic/errno.h#L87) 得知錯誤訊息是 "Connection reset by peer",也就是上個連線被其中一方強制關閉,經確認後可以發現 kernel thread 在建立連線時確實被告知是 Keep-Alive,所以應該是 client 主動關閉連線。查看相關的程式碼,會發現這個錯誤其實不會對 server 造成實質的影響,只會列出錯誤訊息後將連線提早中斷 ```cpp while (!kthread_should_stop()) { int ret = http_server_recv(socket, buf, RECV_BUFFER_SIZE - 1); if (ret <= 0) { if (ret) pr_err("recv error: %d\n", ret); break; } http_parser_execute(&parser, &setting, buf, ret); if (request.complete && !http_should_keep_alive(&parser)) break; } ``` 查詢 [GNU Wget 1.18 Manual: HTTP Options](https://www.gnu.org/software/wget/manual/html_node/HTTP-Options.html) 發現可以追加參數來避免 `wget` 將連線標註為 Keep-Alive,進而避免錯誤訊息出現 ```shell $> wget localhost:8081/fib/2 --no-http-keep-alive ``` ## 如何正確的停止 kthread khttpd 中的 kthread 具有 3 種結束方式 * 接收到 `SIGKILL` 或 `SIGTERM` * 受到 `kthread_stop` 終止 * 發生錯誤 (連線錯誤、記憶體配置錯誤等因素) 接下來會針對 `kthread_stop` 以及 signal 的使用方式進行說明,再針對 khttpd 實作中的問題進行討論 ### `kthread_stop` 閱覽 [`kthread_create`](https://www.fsl.cs.sunysb.edu/kernel-api/re69.html) 與 [`kthread_stop`](https://www.fsl.cs.sunysb.edu/kernel-api/re71.html) 的說明,會發現其中明確的指出 2 種停止 kthread 的方法,且兩種方法**互斥** * 直接 `do_exit`,前提是**沒有**其他地方會 `kthread_stop` 此 kthread * 呼叫 `kthread_stop` 來使 `kthread_should_stop` 為真後,kthread 才 `return` (`return` 的數值會是 `kthread_stop` 的返回值) 進一步根據此篇 [stackoverflow 的文章](https://stackoverflow.com/questions/10177641/proper-way-of-handling-threads-in-kernel) 的說明,一路查詢 `kthread_run` 的實作,可以看到最終是 [`kthread`](https://elixir.bootlin.com/linux/latest/source/kernel/kthread.c#L214) 這個函式會呼叫使用者定義的函式,接著再根據該函示 `return` 的數值來 `do_exit`,也就是說直接在 kthread 內使用 `return` 和 `do_exit` 會有一樣的結果,因此上述的說明可以再進一步理解為 * `kthread_stop` 只能對還在運行的 kthread 使用,也就是不能用在已經 `return` 或是 `do_exit` 的 kthread * 在 kthread 中使用 `return` 或是 `do_exit` 結束具有一樣的結果 ### signal in kthread 在 kernel thread 中不像在 user space 時可以使用 signal handler 來處理 signal,需自行建立檢查點來檢查是否有收到 signal,具體的用法可參考 `http_server_daemon` ```cpp 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; } ``` * 先以 [`allow_signal`](https://elixir.bootlin.com/linux/latest/source/include/linux/signal.h#L288) 登記要接收的 signal * 再以 [`signal_pending`](https://elixir.bootlin.com/linux/latest/source/include/linux/sched/signal.h#L347) 主動確認是否有 signal * 其他 thread 中可以使用 [`send_sig`](https://elixir.bootlin.com/linux/latest/source/kernel/signal.c#L1617) 來發送 signal 到指定的 kthread * user 可以 `sudo kill -s signal [pid]` 來發送 signal :::warning 從 `http_server_daemon` 的實作中可以發現 `signal_pending` 只有在 `kernel_accept` 失敗時才會被呼叫,經實測後發現這麼做的目的為 * 確保 `kernel_accept` 在成功連線後,直到完成建立 worker 前不會被 signal 中斷 * 避免模組卸載時 `http_server_daemon` 卡在 `kernel_accept` 這個步驟,因為 `kernel_accept` 會被 signal 中斷 (但目前我不知道 `kernel_accept` 可以被中斷的原理) ::: ### 使用 `kthread_stop` + signal 將上述 `kthread_stop` 與 signal 的部分放在一起思考,會發現若要同時使用兩者,需要注意 **`kthread_stop` 不能用在已經被 signal 停止的 kthread 上**,而目前我想到能避免這個狀況的方式為 * 讓 kthread 在收到 signal 後只是停止原本執行的作業,但仍需等到 `kthread_stop` 被呼叫才 `return` 或 `do_exit` * 避免對已經停止的 kthread 使用 `kthread_stop` 首先是第一個方式,讓 kthread 在收到 signal 後只是停止原本執行的作業,等到 `kthread_stop` 被呼叫才 `return` 或 `do_exit`。等待的迴圈目前是使用 [`schedule_timeout`](https://elixir.bootlin.com/linux/latest/source/kernel/time/timer.c#L1856),另外根據這篇 [stackoverflow 的討論](https://stackoverflow.com/questions/26050745/is%20there%20a%20way%20inside%20the%20kernel%20of%20killing%20a%20kernel%20kthread%20just%20like%20kill%209),`set_current_state(TASK_INTERRUPTIBLE)` 應該寫在第 2 個迴圈外,避免 race condition ```cpp static int http_server_worker(void *arg) { while (!kthread_should_stop()) { /* do work here */ if (signal_pending(current)) break; } ... /* wait for kthread_stop to be called */ set_current_state(TASK_INTERRUPTIBLE); while (!kthread_should_stop()) { schedule_timeout(timeout); } return 0; } ``` 第二個方式是避免對已經停止的 kthread 使用 `kthread_stop`,目前想到的實作方式為 * 在 `http_server_daemon` 執行 worker 前先用 [`get_task_struct`](https://elixir.bootlin.com/linux/latest/source/include/linux/sched/task.h#L111) 來增加 worker 的 reference count,如此一來就算 kthread 已經停止,仍可以安全的對其使用 `kthread_stop` * 但不適合用於 khttpd,因為這代表所有 `task_struct` 都需要等到模組卸載時才會被釋放 * 建立可靠的清單,用於追蹤還在運作中 worker,避免對已經停止的 kthread 使用 `kthread_stop` * 這點與模組卸載時如何停止所有 kthread 類似,於下個小節一併討論 ### 模組卸載時如何停止所有 kthread 觀察 khttpd 模組卸載的程式碼 `khttpd_exit` ```cpp static void __exit khttpd_exit(void) { send_sig(SIGTERM, http_server, 1); kthread_stop(http_server); close_listen_socket(listen_socket); pr_info("module unloaded\n"); } ``` * 使用 `send_sig` 來避免 `http_server_daemon` 卡在 `kernel_accept` * 若移除 `send_sig`,卸載模組時很可能會卡住,直到下一個連線建立完成後才會成功卸載 * 模組卸載時僅停止 `http_server_daemon` (`http_server` 是 `http_server_daemon` 對應的 `task_struct`),卻沒有停止 server 產生的 `http_server_worker`,這造成模組卸載後標註為 Keep-Alive 的 kthread 仍然存在 * 可以使用 `$ ps -ef | grep khttpd` 確認 顯然要避免模組卸載後 worker 仍然存在的情況,方法就是在模組卸載時追加停止 worker 的步驟,然而綜合前面的討論結果,會發現最麻煩的部分其實是 `kthread_stop` 的使用,主因是 worker 有可能在 `kthread_stop` 呼叫之前就停止了 (收到 signal、連線中斷、發生錯誤等因素),因此如果要同時使用 signal + `kthread_stop`,並同時保證模組卸載時會確實停止所有 worker,可能實行的方式有 * 建立可靠的清單,用於追蹤還在運作中 worker * worker 一旦停止運作 (收到 signal、連線中斷、發生錯誤等因素),就必須從清單中移除 * 最後模組卸載時,根據清單來對運作中的 worker 執行 `kthread_stop` * 同時要避免查詢清單時顯示還在運作中,但剛好在呼叫 `kthread_stop` 的前一瞬間就停止的 race condition * 由於涉及同步的議題,之後如果有時間會嘗試探討 * 乾脆不要依賴 `kthread_stop` 及 `kthread_should_stop` 來停止 worker,建立 global flag 並將 worker 的迴圈改為以此 flag 的數值為判斷依據,當 server 停止時才改變 flag 使所有 worker 跳離迴圈並停止 * [kecho](https://github.com/sysprog21/kecho) 就是使用這種方式,配合使用 [cmwq](https://www.kernel.org/doc/html/v4.15/core-api/workqueue.html) 來確認所有 worker 在模組卸載時停止 * 目前暫定使用此方式修正,後續會進行討論 ## 引入 Concurrency Managed Workqueue 接下來會參照 [kecho](https://github.com/sysprog21/kecho) 的寫法,將 [cmwq](https://www.kernel.org/doc/html/v4.15/core-api/workqueue.html) 應用到 khttpd,以下說明會根據檔案名稱列出相關修改的部分 ### main.c workqueue 除了在模組掛載與卸載時會被取用,`http_server_daemon` 也會動態的將建立的 worker 新增進去,因此 workqueue 的 pointer 須為 global variable ```cpp struct workqueue_struct *khttpd_wq; ``` 在模擬掛載及卸載時,分別追加創立及清除 workqueue 的部分 ```diff static int __init khttpd_init(void) { ... + khttpd_wq = alloc_workqueue("khpptd_wq", WQ_UNBOUND, 0); http_server = kthread_run(http_server_daemon, &param, KBUILD_MODNAME); ... } static void __exit khttpd_exit(void) { send_sig(SIGTERM, http_server, 1); kthread_stop(http_server); close_listen_socket(listen_socket); + destroy_workqueue(khttpd_wq); pr_info("module unloaded\n"); } ``` ### http_server.h workqueue 傳遞參數的方式與 `kthread_create` 不同,為了讓 `http_server_daemon` 將 socket 的資訊傳遞給 worker,需要定義資料結構將參數包起來,worker 再配合使用 [`container_of`](https://elixir.bootlin.com/linux/latest/source/scripts/kconfig/list.h#L19) 來取得參數 ```cpp struct khttpd_server { bool is_stopped; struct list_head worker_head; }; struct khttpd { struct socket *socket; struct list_head list; struct work_struct worker; }; ``` * 因為 worker 的數量不固定,代表需要動態配置 `khttpd` 資料結構的空間,因此會再搭配使用 The Linux Kernel API 中 [List Management Functions](https://www.kernel.org/doc/html/v5.4/core-api/kernel-api.html#list-management-functions) 的部分的來管理,確保模組卸載時能 `kfree` 所有配置的記憶體 * `is_stopped` 用來取代本來 worker 內的 `kthread_should_stop`,當 `http_server_daemon` 停止後會改變 `is_stopped` 來通知所有 worker 停止 ### http_server.c 首先新增需要的 global variable ```cpp struct khttpd_server daemon = {.is_stopped = false}; extern struct workqueue_struct *khttpd_wq; ``` 修改最關鍵的 `http_server_daemon` ```diff int http_server_daemon(void *arg) { struct socket *socket; - struct task_struct *worker; + struct work_struct *worker; struct http_server_param *param = (struct http_server_param *) arg; ... + INIT_LIST_HEAD(&daemon.worker_head); while (!kthread_should_stop()) { ... - worker = kthread_run(http_server_worker, socket, KBUILD_MODNAME); + if (!(worker = create_work(socket))) { + pr_err("can't create more worker process\n"); + kernel_sock_shutdown(socket, SHUT_RDWR); + sock_release(socket); + continue; + } + /* start server worker */ + queue_work(khttpd_wq, worker); } + daemon.is_stopped = true; /* notify all worker to stop */ + free_work(); return 0; } ``` * 接收連線前先使用 [`INIT_LIST_HEAD`](https://elixir.bootlin.com/linux/latest/source/include/linux/list.h#L33) 初始化 worker list * `create_work` 負責初始化 worker 所需的訊息,之後會提到 * 使用 [`queue_work`](https://www.kernel.org/doc/html/v4.15/core-api/workqueue.html#c.queue_work) 將 worker 推送至 workqueue 執行 * 一旦 daemon 結束,會設置 `is_stopped` 來通知所有 worker 停止,並呼叫 `free_work` 來清除所有登記於 worker list 內的 worker 接下來看 `create_work` 與 `free_work` 兩個輔助函數的實作 ```cpp static struct work_struct *create_work(struct socket *socket) { struct khttpd *work; if (!(work = kmalloc(sizeof(struct khttpd), GFP_KERNEL))) return NULL; work->socket = socket; INIT_WORK(&work->worker, http_server_worker); list_add(&work->list, &daemon.worker_head); return &work->worker; } ``` * 負責配置 `khttpd` 資料結構所需的空間 * 將 worker 所需的相關資料都加入 `khttpd` 資料結構 * 最後使用 `list_add` 將 `khttpd` 資料結構加入 worker list ```cpp static void free_work(void) { struct khttpd *tar, *tmp; list_for_each_entry_safe (tar, tmp, &daemon.worker_head, list) { kernel_sock_shutdown(tar->socket, SHUT_RDWR); flush_work(&tar->worker); sock_release(tar->socket); kfree(tar); } } ``` * 使用 [`list_for_each_entry_safe` ](https://elixir.bootlin.com/linux/latest/source/include/linux/list.h#L687) 來依序清除 worker list * 當初 worker 是 `kmalloc` 配置來的,記得 `kfree` * 雖然 worker 在結束時也會 `kernel_sock_shutdown`,但在此使用 `kernel_sock_shutdown` 可以避免 worker 因為仍在連線狀態而卡在 `http_server_recv` 最後是微幅修改 `http_server_worker` ```diff -static int http_server_worker(void *arg) +static void http_server_worker(struct work_struct *work) { + struct khttpd *worker = container_of(work, struct khttpd, worker); ... - allow_signal(SIGKILL); - allow_signal(SIGTERM); ... - while (!kthread_should_stop()) { + while (!daemon.is_stopped) { ... } kernel_sock_shutdown(worker->socket, SHUT_RDWR); - sock_release(worker->socket); kfree(buf); } ``` * 改變傳遞參數的方式 * 可參閱 [The Magical container_of() Macro](https://radek.io/2012/11/10/magical-container_of-macro/) * 用不到 signal * 主迴圈改為根據 `is_stopped` 判斷是否結束 * 須注意不能在 worker 結束時就 `sock_release`,這會導致模組卸載時 `free_work` 對已經不存在的 socket 執行 `sock_release` ### 效能差異 直接使用 `make check` 確認 #### 修改前 ``` requests: 100000 good requests: 100000 [100%] bad requests: 0 [0%] socker errors: 0 [0%] seconds: 3.368 requests/sec: 29687.994 ``` #### 修改成 cmwq 後 ``` requests: 100000 good requests: 100000 [100%] bad requests: 0 [0%] socker errors: 0 [0%] seconds: 1.852 requests/sec: 53986.148 ``` ## reference [GNU Internet Socket Example](https://www.gnu.org/software/libc/manual/html_node/Inet-Example.html) [Driver porting: more module changes](https://lwn.net/Articles/22197/) [Common Variable Attributes](https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html#index-used-variable-attribute) [Concurrency Managed Workqueue (cmwq)](https://www.kernel.org/doc/html/v5.6/core-api/workqueue.html) [HTTP Messages](https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages) [The method to epoll’s madness](https://medium.com/@copyconstruct/the-method-to-epolls-madness-d9d2d6378642) ###### tags: `linux 2020`