---
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, ¶m_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, ¶m_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
```
再看到 `¶m_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` 可能產生的問題。
:::