--- tags: linux2022 --- # 2022q1 Homework6 (ktcp) contributed by < `blueskyson` > > [作業說明](https://hackmd.io/@sysprog/linux2022-ktcp) > [GitHub](https://github.com/blueskyson/kecho) ## 自我檢查清單 ### 參照 [Linux 核心模組掛載機制](https://hackmd.io/@sysprog/linux-kernel-module),解釋 `$ sudo insmod khttpd.ko port=1999` 這命令是如何讓 port=1999 傳遞到核心,作為核心模組初始化的參數呢? 在 kecho_mod.c 中可見以下程式碼,透過巨集 `module_param` 來自行定義參數: ```c static ushort port = DEFAULT_PORT; static ushort backlog = DEFAULT_BACKLOG; static bool bench = false; module_param(port, ushort, S_IRUGO); module_param(backlog, ushort, S_IRUGO); module_param(bench, bool, S_IRUGO); ``` `module_param` 定義在 linux/moduleparam.h 中: ```c= #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) ``` - 第 5 行 `param_check_##type(name, &(value));` 會依據給定的 standard type 展開,standard type 有: ``` 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). ``` 再查看 `param_check_ushort(name, p)` 的定義: ```c #define param_check_ushort(name, p) __param_check(name, p, unsigned short) #define __param_check(name, p, type) \ static inline type __always_unused *__check_##name(void) { return(p); } ``` `__always_unsed` 在 linux/compiler_attribute.h 中被定義為 `__attribute__((__unused__))`。 讀到這裡可以得知第 5 行的用途為自我檢查,為每個核心模組參數宣告一個 `__check_##name` 函式,當參數名稱被重複使用或是型態不正確時,就會錯誤或重複宣告 `__check_##name` 函式,在編譯時期被檢查出來。以 `kecho` 的 `port` 參數為例: ```c static inline ushort __attribute__((__unused__)) *__check_port(void) {return (&port);} ``` - 第 6 行 `module_param_cb(name, &param_ops_##type, &value, perm);` 在展開之後會產生一個命名為 `__param_##name` 的 `struct kernel_param` 變數,關鍵程式碼如下: ```c #define __module_param_call(prefix, name, ops, arg, perm, level, flags) \ static const char __param_str_##name[] = prefix #name; \ static struct kernel_param __moduleparam_const __param_##name \ __used __section("__param") __aligned(__alignof__(struct kernel_param)) \ = { __param_str_##name, THIS_MODULE, ops, \ VERIFY_OCTAL_PERMISSIONS(perm), level, flags, { arg } } ``` 注意到初始化 `__param_##name` 時用了 `__section("__param")`,其展開為 `__attribute__((__section__("__param")))`,參照 [Variable-Attributes](https://gcc.gnu.org/onlinedocs/gcc-3.2/gcc/Variable-Attributes.html):通常編譯器將物件放在 `.data` 和 `.bss`,藉由 section attribute 我們可以自定義一個 section 存放物件,因此所有 `__param_##name` 都會被放在 `__param` section 中。透過以下指令觀察: ``` $ objdump -t -j __param kecho.ko kecho.ko: file format elf64-x86-64 SYMBOL TABLE: 0000000000000000 l d __param 0000000000000000 __param 0000000000000000 l O __param 0000000000000028 __param_bench 0000000000000028 l O __param 0000000000000028 __param_backlog 0000000000000050 l O __param 0000000000000028 __param_port ``` 再看到 `&param_ops_##type` 會被被傳遞給 `struct kernel_param` 的 `ops` 成員,這個物件對稍後核心設定自定義參數非常重要,包含了 `set`、`get`、`free` 的 function pointer,見 [kernel/params.c](https://github.com/torvalds/linux/blob/master/kernel/params.c) 的 `STANDARD_PARAM_DEF(ushort, unsigned short, "%hu", kstrtou16);`。 - 第 7 行 `__MODULE_PARM_TYPE(name, #type)` 展開後會產生一個字元陣列,將參數名稱與對應的型態儲存在 `.modinfo` section 中,關鍵程式碼如下: ```c #define __MODULE_INFO(tag, name, info) \ static const char __UNIQUE_ID(name)[] \ __used __section(".modinfo") __aligned(1) \ = __MODULE_INFO_PREFIX __stringify(tag) "=" info ``` 透過以下命令觀察 `.modinfo` section 內的值: ``` $ objdump -t -j .modinfo kecho.ko kecho.ko: file format elf64-x86-64 SYMBOL TABLE: ... 0000000000000000 l O .modinfo 0000000000000014 __UNIQUE_ID_benchtype611 0000000000000014 l O .modinfo 0000000000000018 __UNIQUE_ID_backlogtype610 000000000000002c l O .modinfo 0000000000000015 __UNIQUE_ID_porttype609 ... $ objdump -s -j .modinfo kecho.ko kecho.ko: file format elf64-x86-64 Contents of section .modinfo: 0000 7061726d 74797065 3d62656e 63683a62 parmtype=bench:b 0010 6f6f6c00 7061726d 74797065 3d626163 ool.parmtype=bac 0020 6b6c6f67 3a757368 6f727400 7061726d klog:ushort.parm 0030 74797065 3d706f72 743a7573 686f7274 type=port:ushort ... ``` 從上述輸出中,SYMBOL TABLE 的 `00`, `14`, `2c` 正好對應 Content of section `.modinfo` 儲存的字串 `"parmtype=bench:bool\0"`、`"parmtype=backlog:ushort\0"`、`"parmtype=port:ushort\0"` 的起始位置。而 `14`、`18`、`15` (16 進位)則表示字串的長度。至此知道編譯核心模組時,會在 `.modinfo` section 存放參數名稱與型態、在 `__param` section 存放每個參數對應的 `struct kernel_param` 物件。 --- 接下來透過 `strace` 追蹤 `insmod` 的系統呼叫: ```= $ sudo strace insmod kecho.ko port=5000 backlog=128 execve("/usr/sbin/insmod", ["insmod", "kecho.ko", "port=5000", "backlog=128"], 0x7ffedd9342d8 /* 25 vars */) = 0 ... openat(AT_FDCWD, "/home/lin/Desktop/sysprog2022/kecho/kecho.ko", O_RDONLY|O_CLOEXEC) = 3 read(3, "\177ELF\2\1", 6) = 6 lseek(3, 0, SEEK_SET) = 0 fstat(3, {st_mode=S_IFREG|0664, st_size=1095800, ...}) = 0 mmap(NULL, 1095800, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f8509854000 finit_module(3, "port=5000 backlog=128", 0) = 0 munmap(0x7f8509854000, 1095800) = 0 close(3) = 0 exit_group(0) = ? +++ exited with 0 +++ ``` 在第 9 行發現 `finit_module(3, "port=5000 backlog=128", 0)` 與初始化核心模組的參數有關,原始碼在 [kernel/module.c](https://github.com/torvalds/linux/blob/master/kernel/module.c): ```c SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags) { struct load_info info = { }; void *buf = NULL; int len; int err; // ... len = kernel_read_file_from_fd(fd, 0, &buf, INT_MAX, NULL, READING_MODULE); if (len < 0) return len; if (flags & MODULE_INIT_COMPRESSED_FILE) { err = module_decompress(&info, buf, len); vfree(buf); /* compressed data is no longer needed */ if (err) return err; } else { info.hdr = buf; info.len = len; } return load_module(&info, uargs, flags); } ``` 上述程式碼參數欄位的 `fd` 對應 `kecho.ko` 的 file descriptor、`uarg` 對應 `"port=5000 backlog=128"`,透過 `kernel_read_file_from_fd` 從硬碟讀取 `kecho.ko` 的原始資料到 `info` 中,然後再呼叫 `load_module(&info, uargs, flags)`: ```c= static int load_module(struct load_info *info, const char __user *uargs, int flags) { // ... /* * Everything checks out, so set up the section info * in the info structure. */ err = setup_load_info(info, flags); // ... /* Figure out module layout, and allocate all the memory. */ mod = layout_and_allocate(info, flags); // ... /* * Now we've got everything in the final locations, we can * find optional sections. */ err = find_module_sections(mod, info); // ... /* Now copy in args */ mod->args = strndup_user(uargs, ~0UL >> 1); // ... /* 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); // ... return do_init_module(mod); } ``` 在上述 `load_module` 程式碼呼叫了許多操作 `info` 與 `mod` 的函式,我展示了我覺得跟傳遞自定義參數關聯較大的 5 個: - 第 8 行 `setup_load_info` 首先將 kernel module 的 `name` 找出來,而 `name` 剛好與自定義參數一樣,都位於 `.modinfo` section。透過 `find_sec` 尋找 `.modinfo` section 的偏移量,儲存到 `info->index.info`(定義在 [kernel/module-internel.h](https://github.com/torvalds/linux/blob/master/kernel/module-internal.h)),程式碼如下: ```c static int setup_load_info(struct load_info *info, int flags) { /* Try to find a name early so we can log errors with a module name */ info->index.info = find_sec(info, ".modinfo"); if (info->index.info) info->name = get_modinfo(info, "name"); // ... } ``` - 第 12 行 `layout_and_allocate` 將 `info->sechdrs[info->index.mod].sh_addr` 傳遞給 `mod`。 - 第 19 行 `find_module_sections` 將 `__param` section 的偏移量回傳給 `mod->kp`: ```c static int find_module_sections(struct module *mod, struct load_info *info) { mod->kp = section_objs(info, "__param", sizeof(*mod->kp), &mod->num_kp); // ... } ``` 至此 `mod` 可以透過 `mod->kp` 知道所有參數的命名、預設值及其他 `struct kernel_param` 包含的相關資訊。 - 第 23 行,`strndup_user` 配置記憶體、複製一份自定義的參數的字串到 `mod->args`。 - 第 27 行 `parse_args` 得以將自定義的參數 `mod->args` 整合進 `mod->kp` 中,關鍵程式碼在 [kernel/params.c](https://github.com/torvalds/linux/blob/master/kernel/params.c#L143) 的第 143 行,在覆寫參數的過程涉及 `struct kernel_param` 的成員 `ops`(亦即 `param_ops_##type`),因為程式碼相對直觀,不列出來贅述。 接下來的 `do_init_module` 和 `do_one_initcall` 就是用來初始化 kernel module。 ```c static noinline int do_init_module(struct module *mod) { /* Start the module */ if (mod->init != NULL) ret = do_one_initcall(mod->init); } ``` `do_one_initcall` 實作在 [init/main.c](https://github.com/torvalds/linux/blob/master/init/main.c): ```c int __init_or_module do_one_initcall(initcall_t fn) { // ... do_trace_initcall_start(fn); ret = fn(); do_trace_initcall_finish(fn, ret); // ... return ret; } ``` 以上說明大致走完了將自定義參數傳遞到核心,作為核心模組初始化的參數的流程。 ### 給定的 `kecho` 已使用 CMWQ,請陳述其優勢和用法 在處理 asynchronous process execution context 時,workqueue (wq) 是常見的手法:將非同步的工作寫在函式中,每當要執行非同步的工作時,一個 work item 會儲存該工作所對應的函式,然後被放進 workqueue 中等待一個 thread 來執行這個函式,這個 thread 又被稱為 worker。當 workqueue 中沒有 work item 時,所有 worker 都會 idle。 CMWQ 的全名為 [Concurrency Managed Workqueue](https://www.kernel.org/doc/html/v4.10/core-api/workqueue.html),用以改善舊有的 multi threaded (MT) WQ 與 single threaded (ST) WQ 。 MTWQ 需要許多 worker thread,甚至在某些大型系統中,系統剛開機就已經幾乎耗盡 32k 的 PID space,在 worker thread 耗用這麼多資源的情況下,level of concurrency 卻不夠讓人滿意,再者,MTWQ 只能為每個 CPU 核提供一個 execution context;STWQ 的並行的效能比 MTWQ 更差,而且 Work items 在競爭單一個 execution context 時更容易導致許多問題,例如 deadlock。 CMWQ 是一個新的 WQ 實作,著重以下議題: - 維持原本實作 workqueue 的 API 相容性。 - 所有 wq 共享 per-CPU unified worker pools,在盡可能不浪費資源的情況下提供更多的 level of concurrency。 - 自動調整 worker pool 和 level of concurrency,使用者可以不用自行管理這些細節。 --- 原本 kecho 的實作是每當接收到一個 request,就跑一個新的 `kthread` 來執行 `echo_server_worker`,見 [bd072cd 的 echo_server.c](https://github.com/sysprog21/kecho/blob/bd072cdd1878813dad9b35bf1d6c253db7cb8263/echo_server.c#L122)。在 kecho 的 [#1](https://github.com/sysprog21/kecho/pull/1) 中用 CMWQ 替代了原本的 `kthread` 實作,每當收到新的 request,就配置一個新的 work item,丟進 workqueue 中。將諸多並行的效能議題交由 CMWQ 解決,在改寫少量程式碼的情況下縮短了一個數量級的回應時間。 CMWQ 的使用方法: 1. 透過 **struct workqueue_struct \*alloc_workqueue(@fmt, @flags, @max_active, ...)** 初始化 workqueue。 - `@fmt` 和 `...` 用來定義 workqueue 的名稱。 - `@flags` 決定 workqueue 的行為,見 [API](https://www.kernel.org/doc/html/v4.10/core-api/workqueue.html#application-programming-interface-api)。 - `@max_active` 限制單一 CPU 核可以並行的 work item,當 `@max_active` 為 `0` 時會使用預設值(`256`),若不需要刻意減少 active work item,建議用預設值即可;如果 work item 必須嚴格按照先後順序執行完畢,則可以結合 `WQ_UNBOUND` 和 `@max_active` 設為 `1` 來達成目的。 2. 透過巨集 **INIT_WORK(_work, _func)** 初始化 work item,`_work` 欄位要放 `struct work_struct*`、`_func` 欄位要放並行工作的函式。 3. 透過 **bool queue_work(struct workqueue_struct \*wq, struct work_struct \*work)** 把 work item 丟進 workqueue 中等待被執行。 :::info 我發現當 `echo_server_woker` 結束執行後沒有釋放 `struct kecho` 的機制,導致 kecho 執行的越久,`daemon.worker` 就累積越多沒有被釋放的 `struct kecho`。直到 `kthread_should_stop()` 跳出 `while` 迴圈時才會統一由 `free_work()` 釋放。 但是實作自動釋放 `struct kecho` 須要考慮同時 `list_add` 和 `list_del` 可能產生的問題。 :::