Try   HackMD

2020q1 Homework4 (khttpd)

contributed by < foxhoundsk >
kernel version: 5.1.10

tags: linux2020

自我檢查清單

模組插入工具 insmod 是如何將初始化參數傳入核心模組

insmod 使用 finit_module(2) 進行將模組植入核心的動作,而這個系統呼叫的第二個參數(param_values)即是指向帶入參數的字串的指標。

finit_module 做完權限檢查以及將欲載入的模組預載(這邊使用預載一詞是因為之後會再 kmalloc 一塊實際使用的記憶體)到記憶體後會呼叫 load_module 以繼續進行模組載入。

load_module 在實際配置並載入(memcpy)模組需要的記憶體後,會呼叫一名為 find_module_sections 的函數,其作用是將模組的 ELF 檔中的各個 section 的位置找到並個別填入模組結構體(struct module)中對應的 field 內,這邊我們想關注的是結構體中名為 kp (struct kernel_param) 的 field,這個指標對應的 ELF section 是 __param

先解釋一下 __param 這個 ELF section 是怎麼來的。在 main.c 中,我們使用 module_param 這個巨集來將全域變數指定為 module parameter。以下解析假設代入的變數是 port。這個巨集展開後如下:

#define module_param(name, type, perm) \
        module_param_named(name, name, type, perm)

module_param_named 展開後如下:

#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)

撇開 param_check_##type 這個靜態型別檢查巨集不看,我們看到 module_param_cb 這個巨集,其展開後如下:

#define module_param_cb(name, ops, arg, perm)				      \
	__module_param_call(MODULE_PARAM_PREFIX, name, ops, arg, perm, -1, 0)

__module_param_call 展開後如下:

#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 } }

其中第 3 行首先宣告一個字串。替換後第 3 行會是:

static const char __param_str_port[] = "khttpd.port";

接著在第 4 行是宣告一個資料型態為 struct kernel_param 的結構體,其中的 __used 展開後是 GNU extension,其用意為告知編譯器這個結構體雖然在這個 compilation unit 沒有被用到,但它其實時有用到的,請編譯器務必不要優化掉這個結構體。

再往下看,第 6 行旨在告知編譯器這個結構體在這個 compilation unit 本來就沒有用到,請編譯器不要發警告。接著 __section__ ("__param") 是要求這個結構體要放在名為 __param 的 section 中。最後一個參數 aligned(sizeof(void *)) 是要求這個結構體的佔用空間要為 sizeof(void *) 的倍數,以利記憶體對齊。

接著看到第 7 行,這邊先列出 struct kernel_param 的所有成員:

struct kernel_param {
	const char *name;
	struct module *mod;
	const struct kernel_param_ops *ops;
	const u16 perm;
	s8 level;
	u8 flags;
	union {
		void *arg;
		const struct kparam_string *str;
		const struct kparam_array *arr;
	};
};

這邊我們要關注的是 ops 以及 arg 這兩個成員。賦值給 ops 的是巨集參數 ops (與結構體成員同名),這個參數替換後是 &param_ops_##type,然後 concatenation 做完後變成 &param_ops_ushort,這邊我們先看到另一份原始碼檔案 kernel/params.c 的一部分:

#define STANDARD_PARAM_DEF(name, type, format, strtolfn)      		\
	int param_set_##name(const char *val, const struct kernel_param *kp) \
	{								\
		return strtolfn(val, 0, (type *)kp->arg);		\
	}								\
	int param_get_##name(char *buffer, const struct kernel_param *kp) \
	{								\
		return scnprintf(buffer, PAGE_SIZE, format "\n",	\
				*((type *)kp->arg));			\
	}								\
	const struct kernel_param_ops param_ops_##name = {			\
		.set = param_set_##name,				\
		.get = param_get_##name,				\
	};								\
	EXPORT_SYMBOL(param_set_##name);				\
	EXPORT_SYMBOL(param_get_##name);				\
	EXPORT_SYMBOL(param_ops_##name)

STANDARD_PARAM_DEF(ushort, unsigned short, "%hu", kstrtou16);

巨集 STANDARD_PARAM_DEF 是個通用巨集,所有模組參數的資料型態都會用此巨集做個別的宣告(這邊僅貼上 ushort 相關的宣告)。經過一連串 concatenation 後,我們可以拿到名為 param_ops_ushortstruct kernel_param 結構體,其中的兩個 function pointer 則是個別指向 ushort 專用(kstrtou16ushort 專用的字串轉數值 method)的 get 以及 set 的 method。

現在我們可以看出 struct kernel_paramops 成員拿到的是 ushort 專用的參數操作 method。

接著我們看到 arg 這個成員,這邊使用 union 的原因是模組參數的資料型態有一般數值(int, bool 等等)、字串以及陣列,但一個 struct kernel_param 只用到一種資料型態,所以使用 union 可以節省些許記憶體。

巨集參數 arg (與結構體成員同名) 替換後是 &port,這個 port 即為我們當初傳入的全域變數,現在他的位置被保存至 arg (struct kernel_param 的成員) 內。

至此相關巨集已介紹完,現在我們回來看 load_module

load_module 再來會呼叫 strndup_user 將 userspace 傳入的字串參數複製到 kernelspace,以利待會做字串解析相關操作。

再往下,load_module 呼叫 parse_args 進行字串解析。parse_args 主要使用 while 迴圈搭配 next_arg 以及 parse_one 這兩個 method 來解析字串參數中所有的參數:

...
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);
    if (irq_was_disabled && !irqs_disabled())
        pr_warn("%s: option '%s' enabled irq's!\n",
            doing, param);

    switch (ret) {
    case 0:
        continue;
    case -ENOENT:
        pr_err("%s: Unknown parameter `%s'\n", doing, param);
        break;
    case -ENOSPC:
        pr_err("%s: `%s' too large for parameter `%s'\n",
               doing, val ?: "", param);
        break;
    default:
        pr_err("%s: `%s' invalid for parameter `%s'\n",
               doing, val ?: "", param);
        break;
    }

    err = ERR_PTR(ret);
}
...

其中 parse_one 會在解析完成後將參數賦值給核心模組中的全域變數,也就是 port

以下是 parse_one 主要程式碼:

... for (i = 0; i < num_params; i++) { if (parameq(param, params[i].name)) { if (params[i].level < min_level || params[i].level > max_level) return 0; /* No one handled NULL, so do it here. */ if (!val && !(params[i].ops->flags & KERNEL_PARAM_OPS_FL_NOARG)) return -EINVAL; pr_debug("handling %s with %p\n", param, params[i].ops->set); kernel_param_lock(params[i].mod); param_check_unsafe(&params[i]); err = params[i].ops->set(val, &params[i]); kernel_param_unlock(params[i].mod); return err; } } ...

由於所有的模組參數呼叫了巨集 module_param,所以他們個別的 struct kernel_param 會坐落於一塊連續的記憶體中,也就是 ELF 檔中的 __param section,因此我們可以看到第 3 行在比較參數名稱時是使用 params[i] (params 為一指向 struct kernel_param 的指標) 來參照參數名稱。

最後我們看到第 15 行,這邊呼叫的 set 即是前面提到的用巨集封裝宣告的 method。第一個傳入參數 val 即是準備要賦值給全域變數 port 的字串參數(set 會做字串解析以及賦值)。第二個傳入參數則是 port 對應的 struct kernel_param 結構體。

以上為模組參數從 insmod 以 cmdline 參數傳入至寫入模組全域變數 port 的過程。

htstress.c 用到 epoll 系統呼叫,其作用為何?這樣的 HTTP 效能分析工具原理為何?

epoll 系統呼叫,用於監視單個行程下多個 fd (file descriptor)。比起 pool(2) 以及 select(2), epoll 在事件發生(由 epoll_wait(2) 通知)時,行程可直接處理已就緒的若干個 fd,而不需自己 traverse 所有監聽的 fd,因此在 fd 數量大時有較佳的效率。

htstress 的原理

筆記

memory allocation

比起每次關閉連線時都將其對應記憶體釋放,將其另外保存至其他 list 會更好,因為配置記憶體是不小的開銷。關於額外的 list,可以用兩串 list 實做,用兩串 list 是為了防止 entry 一新增到 list 就被 sweeper (GC, garbage collection) 回收掉。如此一來,一個被釋放的 entry 在真正被釋放前會被保存在 list 中至少一個 GC cycle。而如果 list 中有 entry 存在,又,我們剛好需要新 entry,這時我們就不用做記憶體配置而是直接從 list 取用即可。

需注意若實做不佳很可能會浪費省下的時間,例如: lock contention。

module param data type: invbool

核心模組參數的資料型態中比較特別的是 invbool,這個型態顧名思義,會將輸入反相。也就是說,如果使用者帶入 true,實際上會被 evaluate 為 false

GNU extension - aligned

儘管可以使用 aligned attribute 告知 GCC 欲對齊的大小,但如果 linker 沒有支援到該大小的話,則會被限制至 linker 支援的最大 alignment 大小。

參考資料