# 2024q1 Homework6 (integration) contributed by < [`SHChang-Anderson`](https://github.com/SHChang-Anderson) > ## 開發環境 ```shell $ gcc --version gcc (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0 Copyright (C) 2022 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. $ lscpu Architecture: x86_64 CPU op-mode(s): 32-bit, 64-bit Address sizes: 48 bits physical, 48 bits virtual Byte Order: Little Endian CPU(s): 12 On-line CPU(s) list: 0-11 Vendor ID: AuthenticAMD Model name: AMD Ryzen 5 5500U with Radeon Graphics CPU family: 23 Model: 104 Thread(s) per core: 2 Core(s) per socket: 6 Socket(s): 1 Stepping: 1 CPU max MHz: 4056.0000 CPU min MHz: 400.0000 BogoMIPS: 4192.17 $ uname -r 6.5.0-27-generic ``` ## 自我檢查清單 ### 閱讀〈[Linux 核心模組運作原理](https://hackmd.io/@sysprog/linux-kernel-module)〉回答相關問題 ### 解釋 `insmod` 後,Linux 核心模組的符號 (symbol) 如何被 Linux 核心找到? 在使用 `insmod` 命令將一個模組載入 Linux 核心之後, Linux 核心是如何找到該模組中所使用、但在核心中尚未定義的那些外部符號的? 摘自 [Linux Device Driver 3/e](https://static.lwn.net/images/pdf/LDD3/ch02.pdf) 第二章: >The module can call printk because, after insmod has loaded it, the module is linked to the kernel and can access the kernel’s public symbols (functions and variables, as detailed in the next section). 上述提到在 [hello.c](https://hackmd.io/@sysprog/linux-kernel-module#%E7%A8%8B%E5%BC%8F%E7%A2%BC%E6%BA%96%E5%82%99) 中 module 能夠呼叫 `printk`,因為 `insmod` 在載入之後,module 會連結到 kernel 並且能夠訪問 Linux 核心模組的符號。接著我們繼續往下看同樣參照 [Linux Device Driver 3/e](https://static.lwn.net/images/pdf/LDD3/ch02.pdf) 第二章 : >The program loads the module code and data into the kernel, which, in turn, performs a function similar to that of ld, in that it links any unresolved symbol in the module to the symbol table of the kernel. :::danger 注意用語: * prgram 是「程式」 * kernel 的拼寫沒有 "a" 字母 * data 是「資料」,而非「數據」 ::: 這段敘述寫道:程式將 module 的程式碼和資料載入到 kernel 中,而 kernel 將模組中任何尚未定義的外部符號 (symbol) 連結到 kernel 的 symbol table 中。 在 [module/main.c](https://elixir.bootlin.com/linux/latest/source/kernel/module/main.c) 有關於這部份的實作: 首先在 [module/internal.h](https://elixir.bootlin.com/linux/latest/source/kernel/module/internal.h#L35) 找到關於 kernel_symbol 結構體: ```c struct kernel_symbol { #ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS int value_offset; int name_offset; int namespace_offset; #else unsigned long value; const char *name; const char *namespace; #endif }; ``` 接著看到 `symsearch` 這個結構體: ```c struct symsearch { const struct kernel_symbol *start, *stop; const s32 *crcs; enum mod_license license; }; ``` 推測這邊的 `start` 與 `stop` 指標分別指向 symbol table 的開始與結尾。 ```c bool find_symbol(struct find_symbol_arg *fsa) { //... for (i = 0; i < ARRAY_SIZE(arr); i++) if (find_exported_symbol_in_section(&arr[i], NULL, fsa)) return true; list_for_each_entry_rcu(mod, &modules, list, lockdep_is_held(&module_mutex)) { //... for (i = 0; i < ARRAY_SIZE(arr); i++) if (find_exported_symbol_in_section(&arr[i], mod, fsa)) return true; } pr_debug("Failed to find symbol %s\n", fsa->name); return false; } ``` 以上程式碼實作了 `find_symbol` 函式,目的是在Linux核心中搜尋指定 symbol 。以下是程式碼的主要流程: * `find_exported_symbol_in_section` 函式在內建 symbol table 尋找指定的 symbol。 * 如果在內建 symbol table 中沒找到,就開始走訪已載入的核心模組 modules。值得注意的是,這邊使用 ListAPI `list_for_each_entry_rcu` 走訪每個 mudules 。 * 探索每個模組內的 symbol table ,找到指定的 symbol 。 :::danger 使用第 7 週課程介紹的「測試 Linux 核心的虛擬化環境」和「建構 User-Mode Linux 的實驗環境」來追蹤上述流程,通常 Linux 核心書籍的內容都會跟實際程式碼會有落差。 ::: 透過建立 User-Mode Linux 實驗環境,我試圖追蹤 Linux 核心在 insmod 過程中如何定位核心模組的符號。 參考了 [建立 User-Mode Linux 實驗環境](https://hackmd.io/@sysprog/user-mode-linux-env) 的步驟,我成功建立了實驗環境並透過 GDB 進行測試。 為了了解符號如何被找到,我在 insmod 過程中使用 GDB 加入斷點,當程式執行到 `find_symbol` 函式時中斷,並逐行觀察程式碼的執行過程。 ```shell (gdb) break find_symbol Breakpoint 2 at 0x6007997a: file kernel/module/main.c, line 303. ``` ```shell / # insmod hello.ko Thread 1 "vmlinux" hit Breakpoint 2, find_symbol (fsa=fsa@entry=0x64883c10) at kernel/module/main.c:303 303 { (gdb) n 317 if (find_exported_symbol_in_section(&arr[i], NULL, fsa)) (gdb) n resolve_symbol (mod=mod@entry=0x64815040, info=info@entry=0x64883d90, name=name@entry=0x648b77f5 "_printk", ownername=ownername@entry=0x64883d08 "") at kernel/module/main.c:1131 1131 if (fsa.license == GPL_ONLY) (gdb) n 1134 if (!inherit_taint(mod, fsa.owner, name)) { ``` 由實驗可以觀察到在 insmod 的過程確實觸發了中斷進入到 `simplify_symbols` 這個函式當中並執行了上述過程。 ### `MODULE_LICENSE` 巨集指定的授權條款又對核心有什麼影響? 參照 [Linux Device Driver 3/e](https://static.lwn.net/images/pdf/LDD3/ch01.pdf) 第一章,License Terms 章節: >The main goal of such a license is to allow the growth of knowledge by permitting everybody to modify programs at will; at the same time, people selling software to the public can still do their job. 授權條款的主要目的是讓每個人都可以隨意修改程式,同時,允許向公眾出售軟體的人繼續他們的業務。雖然授權條款允許任何人修改程式並分享,但對於專業軟體開發人員或公司來說,他們仍然可以按照自己的業務模式銷售軟體。也就是說,他們可以將他們開發的軟體進行商業銷售,同時遵守 GPL 授權條款的要求。 再來我們回到 [Linux Device Driver 3/e](https://static.lwn.net/images/pdf/LDD3/ch02.pdf) 第二章: > When a module is loaded, any symbol exported by the module becomes part of the kernel symbol table. In the usual case, a module implements its own functionality without the need to export any symbols at all. You need to export symbols, however, whenever other modules may benefit from using them. 當其他模組需要使用該模組導出的 symbols 時,可以將這些 symbols 導出。 接著再看到: > EXPORT_SYMBOL(name); EXPORT_SYMBOL_GPL(name); > >Either of the above macros makes the given symbol available outside the module. The _GPL version makes the symbol available to GPL-licensed modules only. 如果導出時使用到 `EXPORT_SYMBOL_GPL` 函式則僅將 symbols 提供給具有 GPL 授權條款的 module 使用。 :::danger 為何要有這樣的規範? ::: ### 藉由 strace 追蹤 Linux 核心的掛載,涉及哪些系統呼叫和子系統? 參考 [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= $ sudo strace insmod fibdrv.ko 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 ``` 上述第 18 行可以發現呼叫到 `finit_module` 。查看 [module/main.c](https://elixir.bootlin.com/linux/latest/source/kernel/module/main.c) : ```c SYSCALL_DEFINE3(finit_module, int, fd, const char __user *, uargs, int, flags) { int err; struct fd f; err = may_init_module(); if (err) return err; pr_debug("finit_module: fd=%d, uargs=%p, flags=%i\n", fd, uargs, flags); if (flags & ~(MODULE_INIT_IGNORE_MODVERSIONS |MODULE_INIT_IGNORE_VERMAGIC |MODULE_INIT_COMPRESSED_FILE)) return -EINVAL; f = fdget(fd); err = idempotent_init_module(f.file, uargs, flags); fdput(f); return err; } ``` 得知 `finit_module` 系統呼叫將模組初始化,這使得新的功能能夠在運行時加載到 kernel 中。 ## 閱讀《[The Linux Kernel Module Programming Guide](https://sysprog21.github.io/lkmpg/)》回答相關問題 ### CMWQ 理解 > 參考自:[CMWQ](https://hackmd.io/@sysprog/linux2024-integration-c#Concurrency-Managed-Workqueue) :::danger 注意用語: * thread 是「執行緒」 ::: * **工作佇列 ( Workqueue )**: 一個存放待處理任務的佇列結構,當有新任務 ( work item ) 進來時會被加入佇列中,而工作執行緒 ( worker thread ) 則會從佇列中取出任務並執行。 當工作佇列中有待處理任務時,閒置的工作執行線程會被分配任務執行。若當前沒有可用的工作執行線程,任務將暫時存放在佇列中,直到有線程可用。相反地,當工作佇列中暫無任務時,工作執行線程將保持等待狀態,而不會立即被釋放,以確保能立即處理新增的任務。 * **Multi-threaded Workqueue**: 每個 CPU 上會有一個 worker thread,work item 可以配置不同的 CPU 上執行。 * **Single-threaded workqueue**: 僅以單一的 worker thread 來完成系統上所有的 work item 。 Single-threaded workqueue 得到的並行等級較低,但較省資源。然而使用 Multi-threaded Workqueue 固然有嚴重的消耗資源,能提高的並行等級卻很有限。這樣不對等的 trade off 讓過去的 workqueue 並不是那麼好用。 CMWQ 就在此背景下誕生,並專注在以下目標: * 維持與原始 workqueue API 的相容性 * 捨棄各個 workqueue 獨立對應一組 worker-pools 的作法。取而代之,所有 workqueue 會共享 per-CPU worker pools,並按需提供靈活的並行等級,避免大量的資源耗損 * worker pool 和並行等級間的調整由系統內部處理,對使用者來說可以視為一個黑盒子 :::danger 不要只會摘錄,你嘗試從 Linux 核心的內部去確認嗎? ::: #### Linux 核心內部的 Workqueue Workqueue 中每個執行單位為 work item ,在 [linux/workqueue_types.h](https://elixir.bootlin.com/linux/latest/source/include/linux/workqueue_types.h#L16) 中有個這個執行單位的結構體定義: ```c struct work_struct { atomic_long_t data; struct list_head entry; work_func_t func; #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map; #endif }; ``` 可以參照 [core-api/workqueue.rst](https://www.kernel.org/doc/Documentation/core-api/workqueue.rst) 有這樣的解釋: >A work item is a simple struct that holds a pointer to the function that is to be executed asynchronously. Whenever a driver or subsystem wants a function to be executed asynchronously it has to set up a work item pointing to that function and queue that work item on a workqueue. 因此可得知 `work_func_t func;` 為 work item 欲執行的函式指標,當需要執行某個函式時,必須設定一個指向該函式的 work item ,並將其排入工作佇列中。 接著同樣在 [core-api/workqueue.rst](https://www.kernel.org/doc/Documentation/core-api/workqueue.rst) 提到: >There are two worker-pools, one for normal work items and the other for high priority ones, for each possible CPU and some extra worker-pools to serve work items queued on unbound workqueues - the number of these backing pools is dynamic. CMWQ 中對於每個 CPU 會有兩個 worker-pools。一個為普通的工作的 worker-pools,另一個則提供給高優先權的工作。此外還有一些未綁定 CPU 的額外 worker-pools 。 對應到 [kernel/workqueue.c](https://elixir.bootlin.com/linux/latest/source/kernel/workqueue.c#L463) 程式碼: ```c void __init workqueue_init_early(void) { //... /* initialize CPU pools */ for_each_possible_cpu(cpu) { struct worker_pool *pool; i = 0; for_each_cpu_worker_pool(pool, cpu) { BUG_ON(init_worker_pool(pool)); pool->cpu = cpu; cpumask_copy(pool->attrs->cpumask, cpumask_of(cpu)); cpumask_copy(pool->attrs->__pod_cpumask, cpumask_of(cpu)); pool->attrs->nice = std_nice[i++]; pool->attrs->affn_strict = true; pool->node = cpu_to_node(cpu); /* alloc pool ID */ mutex_lock(&wq_pool_mutex); BUG_ON(worker_pool_assign_id(pool)); mutex_unlock(&wq_pool_mutex); } } //... } ``` 觀察 `for_each_cpu_worker_pool` 巨集的展開可以發現: ```c #define for_each_cpu_worker_pool(pool, cpu) \ for ((pool) = &per_cpu(cpu_worker_pools, cpu)[0]; \ (pool) < &per_cpu(cpu_worker_pools, cpu)[NR_STD_WORKER_POOLS]; \ (pool)++) ``` 此巨集走訪了 CPU 上的每一個個 worker pools ,其中透過 `NR_STD_WORKER_POOLS = 2, /* # standard pools per cpu */` 可以了解到預設存在兩個 worker pools ,同時我觀察到初始化的過程中使用 `int std_nice[NR_STD_WORKER_POOLS] = { 0, HIGHPRI_NICE_LEVEL };` ,這行程式碼,分別將兩個 worker pools 對應到不同的優先級 0 (普通優先級)和HIGHPRI_NICE_LEVEL (高優先級),這樣的實作應證了上述的說明。 在 [core-api/workqueue.rst](https://www.kernel.org/doc/Documentation/core-api/workqueue.rst) 還提到了: >BH workqueues use the same framework. However, as there can only be one concurrent execution context, there's no need to worry about concurrency. Each per-CPU BH worker pool contains only one pseudo worker which represents the BH execution context. A BH workqueue can be considered a convenience interface to softirq. 原文中提到,對於 BH 工作隊列,每一個 CPU 都只會有一個執行單元 (pseudo worker)。由於同一時間只能有一個執行可以進行取,因此不需要處理並行存取的問題。 不過,我對於 Linux 核心內部如何實作 BH 工作隊列的細節,並沒有了解透徹,也還不太清楚 Linux核心 的實際做法,因此此部份還須釐清。 至於裝置驅動程式,可以透過 workqueue 提供的 API,來建立及排程需要非同步執行的工作項目。驅動開發者可以藉由設定工作隊列的旗標,來影響工作項目的執行方式。 #### Workqueue API ```c struct workqueue_struct *alloc_workqueue(const char *fmt, unsigned int flags, int max_active, ...) ``` `alloc_workqueue()` 用於分配一個工作隊列,此函式接受 `name` 、`flags` 、 `max_active` 三個參數,其中參數 `name` 表 workqueue 名稱,而使用者可透過`flags` 、 `max_active` 參數來管理工作如何調度與執行。 ### 解釋 [simrupt](https://github.com/sysprog21/simrupt) 程式碼裡頭的 mutex lock 的使用方式 閱讀 [中斷處理和現代架構考量](https://hackmd.io/@sysprog/linux-interrupt) 教材提到 SoftIRQs 是 Linux kernel 的一種可延後處理的中斷,它們可以進行排程以便在適當的時候執行。Tasklet 與 SoftIRQs 相似,也可以動態地新增和移除。相比之下,Work Queues 則在 userspace 的process context 中運行,這使得它們能夠進行睡眠等操作。 允許睡眠的情況下 Linux kernel 中的 mutex 實作方式可以參照 [mutex.c](https://elixir.bootlin.com/linux/latest/source/kernel/locking/mutex.c#L982) 程式碼: ```c int __sched mutex_lock_interruptible(struct mutex *lock) { might_sleep(); if (__mutex_trylock_fast(lock)) return 0; return __mutex_lock_interruptible_slowpath(lock); } ``` 可以看到註解的說明:若 process 為睡眠狀態函式會回傳 -EINTR 無法成功獲得 Lock ,反之則回傳 0 ,其主要目的為進入睡眠狀態時,避免 process 空轉消耗 CPU 資源。 在 [simrupt](https://github.com/sysprog21/simrupt) 程式碼中 `simrupt_read` 函式可以看到 `mutex_lock_interruptible` 的使用。 ## simrupt 首先由《[The Linux Kernel Module Programming Guide](https://sysprog21.github.io/lkmpg/)》4.2 章節可得知,模組由 ` __init` 開始執行 `__exit` 作為模組結束,可對應到 [simrupt.c](https://github.com/sysprog21/simrupt/blob/main/simrupt.c) 中的 `static int __init simrupt_init(void)` 以及 `static void __exit simrupt_exit(void)` 。 接著探究 `static int __init simrupt_init(void)` 執行了哪些步驟。 `kfifo_alloc(&rx_fifo, PAGE_SIZE, GFP_KERNEL)` :分配一個先進先出(FIFO)緩衝區,大小為一個頁面(page)。 `simrupt` 是一個 [character device](https://derekmolloy.ie/writing-a-linux-kernel-module-part-2-a-character-device/) ,使用 `alloc_chrdev_region(&dev_id, 0, NR_SIMRUPT, DEV_NAME);` 來完成設備的註冊。值得注意的是註冊完成後會得到 major/minor numbers,可以參閱《[The Linux Kernel Module Programming Guide](https://sysprog21.github.io/lkmpg/)》5.6 得知: > The major number tells you which driver is used to access the hardware. Each driver is assigned a unique major number; >The minor number is used by the driver to distinguish between the various hardware it controls. :::danger 注意用語: * access 是「存取」 * device driver 是「裝置驅動程式」,不該輕易忽略「裝置」一詞 * device 是「裝置」,不是「設備」 * file descriptor 是「檔案描述子」 * file 是「檔案」,而非「文件」(document) * data 是「資料」,而非「數據」 摘自教育部重編國語辭典: * [數據](https://dict.revised.moe.edu.tw/dictView.jsp?ID=133742): 經由調查或實驗得到,而尚未經過有效處理的數值。 * [資料](https://dict.revised.moe.edu.tw/dictView.jsp?ID=137229): 計算機中一切數值、記號和事實的概稱。 ::: Major number 表<s>訪問</s> 存取硬體設備的裝置驅動程式,因此不同裝置驅動程式有獨自的 major number 。 minor number 用來區分它控制的各種硬體設備,若不同硬體設備使用相同裝置驅動程式,則使用 minor number 來做區分。 ```c cdev_init(&simrupt_cdev, &simrupt_fops); ret = cdev_add(&simrupt_cdev, dev_id, NR_SIMRUPT); ``` 接著將 Character Device 添加到系統中使其可以被使用。 《[The Linux Kernel Module Programming Guide](https://sysprog21.github.io/lkmpg/)》6.1: > every character driver needs to define a function that reads from the device. The file_operations structure holds the address of the module’s function that performs that operation. 在 [include/linux/fs.h](https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/fs.h) 定義了 `file_operations` ,每個 Character Device 都需要定義一個從設備存取資料的函式,提供 userspace 做使用。 ```c static const struct file_operations simrupt_fops = { .read = simrupt_read, .llseek = no_llseek, .open = simrupt_open, .release = simrupt_release, .owner = THIS_MODULE, }; ``` [simrupt.c](https://github.com/sysprog21/simrupt/blob/main/simrupt.c) 定義了 `read` 、 `open` 、 `release` 操作。因此在作業說明 [simrupt 的使用](https://hackmd.io/@sysprog/linux2024-integration/%2F%40sysprog%2Flinux2024-integration-c#simrupt-%E7%9A%84%E4%BD%BF%E7%94%A8) 輸入命令: ```shell $ sudo cat /dev/simrupt ``` 來執行這個模組並得到輸出,執行 `cat /dev/simrupt` 這樣的命令時,會觸發 kernel 的 `.open` 文件操作即 `simrupt_read` 函式; `cat` 命令會嘗試從打開的<s>文件描述符</s> 檔案描述子讀取<s>數據</s> 資料 。這個過程會觸發 <s>kernal</s> kernel 中的 `.read` 操作,對應到 [simrupt.c](https://github.com/sysprog21/simrupt/blob/main/simrupt.c) 中的 `simrupt_read` 函式。 :::danger 用物件導向程式設計的手法來說明 Linux 核心的 VFS 操作。 注意用語,避免低劣的簡體中文翻譯詞彙和風格。 ::: `simrupt_open` 函式中可以看到`atomic_inc_return(&open_cnt)` 這樣的操作,對照 [linux/atomic/atomic-instrumented.h](https://elixir.bootlin.com/linux/latest/source/include/linux/atomic/atomic-instrumented.h#L450) 得知目的為回傳 `open_cnt` 的值 + 1 ,亦即 `.open` 觸發次數。 ```c static int simrupt_open(struct inode *inode, struct file *filp) { pr_debug("simrupt: %s\n", __func__); if (atomic_inc_return(&open_cnt) == 1) mod_timer(&timer, jiffies + msecs_to_jiffies(delay)); pr_info("openm current cnt: %d\n", atomic_read(&open_cnt)); return 0; } ``` 在這邊我不確定 `mod_timer(&timer, jiffies + msecs_to_jiffies(delay));` 想達到的目的,在 [kernel/time/timer.c](https://elixir.bootlin.com/linux/v4.4/source/kernel/time/timer.c#L906) 有這樣的實作,其作用為設定延遲時間後觸發,因此我猜測此部分可能用於模擬開啟檔案後發生硬體中斷。 :::danger 搭配 UML 和 QEMU 追蹤 ::: `simrupt_read` 的主要目的為讀取 kfifo 中的資料。 ```c static ssize_t simrupt_read(struct file *file, char __user *buf, size_t count, loff_t *ppos) { ... 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); ... mutex_unlock(&read_lock); return ret ? ret : read; } ``` `mutex_lock_interruptible` 取得 lock 且允許被打斷,同時只有一個執行緒能夠讀取檔案內容。在這邊不使用 `mutex_lock()` 可以參閱 [locking.rst](https://www.kernel.org/doc/Documentation/kernel-hacking/locking.rst) : >If you have a data structure which is only ever accessed from user context, then you can use a simple mutex (``include/linux/mutex.h``) to protect it. This is the most trivial case: you initialize the mutex. Then you can call mutex_lock_interruptible() to grab the mutex, and mutex_unlock() to release it. There is also a mutex_lock(), which should be avoided, because it will not return if a signal is received. 如果使用 `mutex_lock()` 當收到 signal 時,不會被中斷,因此可能造成阻塞的風險。 接著可以看到一個讀取 `kfifo` 的迴圈,讀取完畢後使用到 `wait_event_interruptible` ,閱讀 [debian.org - wait_event_interruptible](https://manpages.debian.org/jessie/linux-manual-3.16/wait_event_interruptible.9.en.html) 說明: >The process is put to sleep (TASK_INTERRUPTIBLE) until the condition evaluates to true or a signal is received. The condition is checked each time the waitqueue wq is woken up. wake_up has to be called after changing any variable that could change the result of the wait condition. The function will return -ERESTARTSYS if it was interrupted by a signal and 0 if condition evaluated to true. 等待的過程會被置於睡眠狀態,直到條件成立或接收到 signal 為止,這邊的條件指 `kfifo` 不為空,則回傳 0 ,使得迴圈繼續讀取內容,反之若收到 signal ,則回傳 `-ERESTARTSYS` 。 [What does ERESTARTSYS used while writing linux driver?](https://stackoverflow.com/questions/9576604/what-does-erestartsys-used-while-writing-linux-driver) 提到了 `-ERESTARTSYS` 將重新執行 <s>system call</s> 系統呼叫 ,因此當收到 signal 時,處理完畢後,讀取操作 ( read ) 可以自動重新開始。 :::danger 避免非必要的中英文辭彙交錯。 ::: ## 整合井字遊戲對弈 ### 繪製棋盤 在這段程式碼中,我發現 simrupt 原始的設計是先將產生的字元資料存入 `fast_buf`,然後再從中讀取到 `kfifo` 資料結構中,最終傳送到 userspace。因此,為了能夠實現棋盤的繪製和狀態的更新,首先初始化了一個 `table_buff` 陣列,用來代表將要傳送到 userspace 的棋盤狀態。 在 `update_simrupt_data` 函式中,生成了棋手的放置棋子位置資料,並將其放入 `fast_buf` 中。然後,在 `produce_data` 函式中,讀取了 `fast_buff` 的內容並更新了 `table_buff` 陣列。最後,使用 `kfifo_in` 將更新後的棋盤狀態傳送到 `userspace`。這樣一來,就實現了棋盤的繪製和狀態的更新效果。 依序在棋盤中放入 `O` `X` : ```shell $ sudo cat /dev/simrupt --------- |X| | | | | | | | | | | | | | | | | | | --------- --------- |X|O| | | | | | | | | | | | | | | | | | --------- --------- |X|O|X| | | | | | | | | | | | | | | | | --------- --------- |X|O|X|O| | | | | | | | | | | | | | | | --------- ``` > commit [1718b18](https://github.com/SHChang-Anderson/simrupt/commit/1718b18c127fa5cbba2c8fd1ebefefa20be9bd38)