# 2024q1 Homework6 (integration) contributed by < [`SuNsHiNe-75`](https://github.com/SuNsHiNe-75) > ## 有關〈[Linux 核心模組運作原理](https://hackmd.io/@sysprog/linux-kernel-module)〉 ### 解釋 `insmod` 後,Linux 核心模組的符號 (symbol) 如何被 Linux 核心找到 首先要知道,`insmod` 程式的主要目的是「將模組 (module) 掛載至核心」。 可用 [strace](https://linux.die.net/man/1/strace) 來追蹤執行 `insmod` 時,會執行哪些系統呼叫,會發現其呼叫 `idempotent_init_module` 後在該函式當中還會呼叫 `init_module_from_file` ,在 `init_module_from_file` 才真正呼叫到 **`load_module`**。 > `load_module`:大致就是 Linux 核心為模組配置記憶體和載入模組相關資料的地方。 觀察其所在的檔案位置 [kernel/module.c](https://elixir.bootlin.com/linux/v4.18/source/kernel/module.c) 中,有 `lookup_module_symbol_name` 及 `lookup_module_symbol_attrs` 等與尋找 symbol 相關的函式,部分程式碼展示如下: ```c= int lookup_module_symbol_name(unsigned long addr, char *symname) { //... list_for_each_entry_rcu(mod, &modules, list) { if (mod->state == MODULE_STATE_UNFORMED) continue; if (within_module(addr, mod)) { const char *sym; sym = get_ksymbol(mod, addr, NULL, NULL); //... } } //... } ``` 可以看到第 12 行涉及有關 symbol 的操作,延伸探討 `get_ksymbol` 函式,其同樣定義在 [kernel/module.c](https://elixir.bootlin.com/linux/v4.18/source/kernel/module.c) 中,如下: ```c= static const char *get_ksymbol(struct module *mod, unsigned long addr, unsigned long *size, unsigned long *offset) { unsigned int i, best = 0; unsigned long nextval; struct mod_kallsyms *kallsyms = rcu_dereference_sched(mod->kallsyms); /* At worse, next value is at end of module */ if (within_module_init(addr, mod)) nextval = (unsigned long)mod->init_layout.base+mod->init_layout.text_size; else nextval = (unsigned long)mod->core_layout.base+mod->core_layout.text_size; /* Scan for closest preceding symbol, and next symbol. (ELF starts real symbols at 1). */ for (i = 1; i < kallsyms->num_symtab; i++) { if (kallsyms->symtab[i].st_shndx == SHN_UNDEF) continue; /* We ignore unnamed symbols: they're uninformative * and inserted at a whim. */ if (*symname(kallsyms, i) == '\0' || is_arm_mapping_symbol(symname(kallsyms, i))) continue; if (kallsyms->symtab[i].st_value <= addr && kallsyms->symtab[i].st_value > kallsyms->symtab[best].st_value) best = i; if (kallsyms->symtab[i].st_value > addr && kallsyms->symtab[i].st_value < nextval) nextval = kallsyms->symtab[i].st_value; } if (!best) return NULL; if (size) *size = nextval - kallsyms->symtab[best].st_value; if (offset) *offset = addr - kallsyms->symtab[best].st_value; return symname(kallsyms, best); } ``` :::danger 注意用語! ::: 推測此函式主要用途為「從 Linux 模組中尋找符號」。 指定一個位址,其<s>遍歷</s> 符號表,從第一個符號開始找尋「比該位址小且最接近該位址的符號」,之後回傳最接近該位址的符號名稱,然後計算符號的大小和位址偏移量。最後,根據「是否在模組的初始化段內」,計算下一個符號的位址。 :::danger 用第七週介紹的 UML 和 QEMU 來追蹤 Linux 核心的行為以驗證。 ::: 同樣觀察 [kernel/module.c](https://elixir.bootlin.com/linux/v4.18/source/kernel/module.c) 中的 `find_symbol` 函式,其呼叫了 `each_symbol_section` 函式,擷取如下: ```c= /* Returns true as soon as fn returns true, otherwise false. */ bool each_symbol_section(bool (*fn)(const struct symsearch *arr, struct module *owner, void *data), void *data) { struct module *mod; static const struct symsearch arr[] = { { __start___ksymtab, __stop___ksymtab, __start___kcrctab, NOT_GPL_ONLY, false }, { __start___ksymtab_gpl, __stop___ksymtab_gpl, __start___kcrctab_gpl, GPL_ONLY, false }, { __start___ksymtab_gpl_future, __stop___ksymtab_gpl_future, __start___kcrctab_gpl_future, WILL_BE_GPL_ONLY, false }, #ifdef CONFIG_UNUSED_SYMBOLS { __start___ksymtab_unused, __stop___ksymtab_unused, __start___kcrctab_unused, NOT_GPL_ONLY, true }, { __start___ksymtab_unused_gpl, __stop___ksymtab_unused_gpl, __start___kcrctab_unused_gpl, GPL_ONLY, true }, #endif }; module_assert_mutex_or_preempt(); if (each_symbol_in_section(arr, ARRAY_SIZE(arr), NULL, fn, data)) return true; list_for_each_entry_rcu(mod, &modules, list) { struct symsearch arr[] = { { mod->syms, mod->syms + mod->num_syms, mod->crcs, NOT_GPL_ONLY, false }, { mod->gpl_syms, mod->gpl_syms + mod->num_gpl_syms, mod->gpl_crcs, GPL_ONLY, false }, { mod->gpl_future_syms, mod->gpl_future_syms + mod->num_gpl_future_syms, mod->gpl_future_crcs, WILL_BE_GPL_ONLY, false }, #ifdef CONFIG_UNUSED_SYMBOLS { mod->unused_syms, mod->unused_syms + mod->num_unused_syms, mod->unused_crcs, NOT_GPL_ONLY, true }, { mod->unused_gpl_syms, mod->unused_gpl_syms + mod->num_unused_gpl_syms, mod->unused_gpl_crcs, GPL_ONLY, true }, #endif }; if (mod->state == MODULE_STATE_UNFORMED) continue; if (each_symbol_in_section(arr, ARRAY_SIZE(arr), mod, fn, data)) return true; } return false; } ``` 此函式透過 `list_for_each_entry_rcu` 來走訪 Linux 模組的 Symbol table,並對每個符號執行指定的函式。 其先<s>遍歷</s> 一組 Static symbol table,然後遍歷「已載入的模組」,對每個模組的 Symbol table 執行相同的操作並檢查其狀態。 > 如果其中任何一個函式返回 true,則 `each_symbol_section` 也會回傳 true,否則回傳 false。 綜上所述,我認為 `insmod` 程式將 module 的資料載入核心時,核心會將 module 中沒有定義過的 symbol 連結到核心的「Symbol Table」中(如上述程式碼之 `symtab[]`)。 :::danger 只閱讀程式碼,難免陷入「舉燭」的境界。 ::: 當需要使用到時,透過 `list_for_each_entry_rcu` 走訪 modules,若有對應 symbol 在 Symbol table 中,則成功找到 symbol;反之,將該未定義的 symbol 連到核心的 Symbol table 內。 > RCU (Read-Copy Update) 是一種高效的同步機制,常用於在多核系統中進行共享資料的讀取操作,以提高性能和降低<s>內存</s>消耗。 :::danger 注意用語! ::: ### `MODULE_LICENSE` 巨集指定的授權條款對核心有什麼影響 參照 [include/linux/module.h](https://elixir.bootlin.com/linux/v4.18/source/include/linux/module.h#L199) 中的 `MODULE_LICENSE` 巨集定義及註解如下: ```c /* * The following license idents are currently accepted as indicating free * software modules * * "GPL" [GNU Public License v2 or later] * "GPL v2" [GNU Public License v2] * "GPL and additional rights" [GNU Public License v2 rights and more] * "Dual BSD/GPL" [GNU Public License v2 * or BSD license choice] * "Dual MIT/GPL" [GNU Public License v2 * or MIT license choice] * "Dual MPL/GPL" [GNU Public License v2 * or Mozilla license choice] * * The following other idents are available * * "Proprietary" [Non free products] * * There are dual licensed components, but when running with Linux it is the * GPL that is relevant so this is a non issue. Similarly LGPL linked with GPL * is a GPL combined work. * * This exists for several reasons * 1. So modinfo can show license info for users wanting to vet their setup * is free * 2. So the community can ignore bug reports including proprietary modules * 3. So vendors can do likewise based on their own policies */ #define MODULE_LICENSE(_license) MODULE_INFO(license, _license) ``` 另參照 [Linux 核心模組掛載機制](https://hackmd.io/@sysprog/linux-kernel-module#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) 中,以 `MODULE_AUTHOR` 為例對 `MODULE_INFO` 展開討論,可知 `MODULE_LICENSE` 會根據傳入的參數(如 `Dual MIT/GPL`),寫入 `.modinfo` 的區段內,以宣告程式的 License。 > 根據 GNU GCC 文件說明對於 Variable attribute 的解說,`section` 會特別將此 variable 放到指定的 ELF section 中,這邊為 `.modinfo`。 GPL 全名為 [GNU General Public License](https://zh.wikipedia.org/zh-tw/GNU%E9%80%9A%E7%94%A8%E5%85%AC%E5%85%B1%E8%AE%B8%E5%8F%AF%E8%AF%81),根據上述巨集之註解推測,如果授權條款是 **GPL v2** 以上,則該模組會被視為「自由軟體」,可以與核心互動,並且其符號將包含在核心的 Symbol table 中,供其他模組使用。 :::danger 為何要有此機制? ::: 反之,如果授權條款是 **Proprietary**,則模組被視為「專有軟體」,核心不會提供對其符號的存取權限,表示其他模組無法使用該模組的符號。 如上所述,我認為授權條款的指定會影響其他模組對其符號的可用性,需透過指定正確的授權條款,來確保模組與核心的相容性。 :::danger 有什麼第一手的資料佐證?查閱 Linux Kernel Mailing List (LKML) ::: ### 藉由 [strace](https://man7.org/linux/man-pages/man1/strace.1.html) 追蹤 Linux 核心的掛載,涉及哪些系統呼叫和子系統 根據 [strace(1)](https://man7.org/linux/man-pages/man1/strace.1.html),strace 命令所做的工作簡述如下: > Each line in the trace contains the system call name, followed by its arguments in parentheses and its return value. - 若追蹤 Signal(如 `sleep`),`strace` 會將其解碼成 siginfo 結構,並以 signal symbol 的形式輸出。 - 在多線程的環境下同時使用一系統呼叫,`strace` 會記錄其先後順序,並根據是否完成(回傳),以 `unfinished` 或 `resumed` 來標示它們。 - 若遇到 parameter,會以「符號」形式輸出。 - Structure pointers 被解引用並根據需要顯示其成員。 - `strace` 未知的系統呼叫會以「原始方式」輸出,將 system call number 以十六進制輸出並以 "`syscall_`" 為前綴。舉例如下: ```cshell syscall_0xbad(0x1, 0x2, 0x3, 0x4, 0x5, 0x6) = -1 ENOSYS (Function not implemented) ``` - Character pointers 被解引用並以 C 字符串的形式輸出。 - 「基本類型和陣列」的 pointers 則使用方括號(`[]`)輸出。 系統呼叫舉例,可參照 [Linux 核心模組掛載機制](https://hackmd.io/@sysprog/linux-kernel-module#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) 追蹤執行 `insmod fibdrv.ko` 的過程如下: ```shell execve("/sbin/insmod", ["insmod", "fibdrv.ko"], 0x7ffeab43f308 /* 25 vars */) = 0 brk(NULL) = 0x561084511000 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=83948, ...}) = 0 mmap(NULL, 83948, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f0621290000 close(3) = 0 ... close(3) = 0 getcwd("/tmp/fibdrv", 4096) = 24 stat("/tmp/fibdrv/fibdrv.ko", {st_mode=S_IFREG|0644, st_size=8288, ...}) = 0 openat(AT_FDCWD, "/tmp/fibdrv/fibdrv.ko", O_RDONLY|O_CLOEXEC) = 3 fstat(3, {st_mode=S_IFREG|0644, st_size=8288, ...}) = 0 mmap(NULL, 8288, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f06212a2000 finit_module(3, "", 0) = 0 munmap(0x7f06212a2000, 8288) = 0 close(3) = 0 exit_group(0) = ? +++ exited with 0 +++m ``` ## 有關〈[The Linux Kernel Module Programming Guide](https://sysprog21.github.io/lkmpg/)〉 ### 解釋 [simrupt](https://github.com/sysprog21/simrupt) 程式碼裡頭的 mutex lock 的使用方式 該程式碼有兩種 mutex lock 的使用,分別為 `read_lock` 及 `producer_lock` & `consumer_lock` 的配合,我想如下分開敘述。 #### `read_lock` 互斥鎖定義及註解如下: ```c /* NOTE: the usage of kfifo is safe (no need for extra locking), until there is * only one concurrent reader and one concurrent writer. Writes are serialized * from the interrupt context, readers are serialized using this mutex. */ static DEFINE_MUTEX(read_lock); ``` > 注意註解:readers are serialized using this mutex。 此互斥鎖機制使用在 `simrupt_read` 函式中,如下: ```c= static ssize_t simrupt_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { unsigned int read; int ret; pr_debug("simrupt: %s(%p, %zd, %lld)\n", __func__, buf, count, *ppos); if (unlikely(!access_ok(buf, count))) return -EFAULT; if (mutex_lock_interruptible(&read_lock)) return -ERESTARTSYS; do { ret = kfifo_to_user(&rx_fifo, buf, count, &read); if (unlikely(ret < 0)) break; if (read) break; if (file->f_flags & O_NONBLOCK) { ret = -EAGAIN; break; } ret = wait_event_interruptible(rx_wait, kfifo_len(&rx_fifo)); } while (ret == 0); pr_debug("simrupt: %s: out %u/%u bytes\n", __func__, read, kfifo_len(&rx_fifo)); mutex_unlock(&read_lock); return ret ? ret : read; } ``` 觀察第 14、15 行,該段程式碼鎖定一個互斥鎖 `read_lock` 來保護後續對共享資源 `rx_fifo` 的存取,如果此時被中斷(例如收到信號),則回傳錯誤 `-ERESTARTSYS`。 共享資源 `rx_fifo` 存取完畢後,即在第 32 行解鎖 `read_lock`,以供其他 process 存取(如果想要的話)。 #### `producer_lock` & `consumer_lock` 互斥鎖定義及註解如下: ```c /* Mutex to serialize kfifo writers within the workqueue handler */ static DEFINE_MUTEX(producer_lock); /* Mutex to serialize fast_buf consumers: we can use a mutex because consumers * run in workqueue handler (kernel thread context). */ static DEFINE_MUTEX(consumer_lock); ``` 此互斥鎖機制實現在 `simrupt_work_func` 函式中,如下: ```c= /* Workqueue handler: executed by a kernel thread */ static void simrupt_work_func(struct work_struct *w) { int val, cpu; /* This code runs from a kernel thread, so softirqs and hard-irqs must * be enabled. */ WARN_ON_ONCE(in_softirq()); WARN_ON_ONCE(in_interrupt()); /* Pretend to simulate access to per-CPU data, disabling preemption * during the pr_info(). */ cpu = get_cpu(); pr_info("simrupt: [CPU#%d] %s\n", cpu, __func__); put_cpu(); while (1) { /* Consume data from the circular buffer */ mutex_lock(&consumer_lock); val = fast_buf_get(); mutex_unlock(&consumer_lock); if (val < 0) break; /* Store data to the kfifo buffer */ mutex_lock(&producer_lock); produce_data(val); mutex_unlock(&producer_lock); } wake_up_interruptible(&rx_wait); } ``` 該 `consumer_lock` 用於保護對 Circular buffer 的存取,確保在讀取資料時不會與其他執行緒的寫入操作衝突-用 `mutex_lock` 函式鎖定 `consumer_lock`,從 `fast_buf` 中存取資料後,再用 `mutex_unlock` 解鎖 `consumer_lock`,讓其他執行緒可以有機會存取該 buffer 的資料,從而避免 **Race condition**。 另外,`producer_lock` 用於保護對 kfifo buffer 的存取,描述與 `consumer_lock` 類似,都可避免與其他執行緒發生 Race condition 的情況。 :::danger 上方程式碼有改進空間,不要看到程式碼就急著「舉燭」,你需要有辦法推論和從實驗來得知其行為和限制。 ::: #### 改寫為 [lock-free](https://hackmd.io/@sysprog/concurrency-lockfree) 所謂 lock-free,即透過「Atomic Operation」或「Compare and Swap (CAS)」等機制,實現對資料存取的「**不可分割性**」-[Lock-Free Programming](https://hackmd.io/@sysprog/concurrency-lockfree#Deeper-Look-Lock-Free-Programming) 文章中的舉例很不錯: > 本來對資料的改變,外面的人是看不到(至少不該看到)的 (只看得到轉帳前的狀態);而 atomic write 做的事是「把你做的所有改變讓大家看到」(就是把轉帳後的餘額顯示出來)。 另外要注意的是 lock-less 與 lock-free 的意義有重疊,但仍有定義上的不同-lock-less 顧名思義就是「不使用 lock 的前提」,確保資料能正確地被執行緒們存取;而「不使用 lock」不代表其為 lock-free,其定義比較像是指:整個程式的執行不會被其單個執行緒 lock 鎖住。 參照 [2021q3 Homework (simrupt)](https://hackmd.io/@linD026/simrupt-vwifi#%E6%94%B9%E5%AF%AB-producer_lock-%E5%92%8C-consumer_lock) 章節。 > kfifo 以及 circular buffer 會遭遇到並行的問題,因為前者只提供 one reader 和 one writer 的情況;後者則只提供資料結構以及計算使用量等 API 。 因此,原透過 `consumer_lock` 保護 `fast_buf` 資料存取的部分,可以透過導入 Atomic Operation 改寫 `fast_buf` 相關操作函式 `fast_buf_get` 及 `fast_buf_put`,以實現 lock-free 的概念。 > 程式碼參考:[改寫 `producer_lock` 和 `consumer_lock`](https://hackmd.io/@linD026/simrupt-vwifi#%E6%94%B9%E5%AF%AB-producer_lock-%E5%92%8C-consumer_lock) `producer_lock` 的話,會因 workqueue 導致可能有「多個 writer」同時想存取 kfifo buffer,因此 mutex_lock 的機制需要保留,不可移除 `producer_lock`。