Try   HackMD

2024q1 Homework6 (integration)

contributed by < yeh-sudo >

開發環境

$ uname -a
Linux andyyeh-ubuntu 6.5.0-21-generic #21~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Feb  9 13:32:52 UTC 2 x86_64 x86_64 x86_64 GNU/Linux

$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Copyright (C) 2021 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:         44 bits physical, 48 bits virtual
  Byte Order:            Little Endian
CPU(s):                  16
  On-line CPU(s) list:   0-15
Vendor ID:               AuthenticAMD
  Model name:            AMD Ryzen 7 4800HS with Radeon Graphics
    CPU family:          23
    Model:               96
    Thread(s) per core:  2
    Core(s) per socket:  8
    Socket(s):           1
    Stepping:            1
    Frequency boost:     enabled
    CPU max MHz:         2900.0000
    CPU min MHz:         1400.0000
    BogoMIPS:            5788.79
Virtualization features: 
  Virtualization:        AMD-V
Caches (sum of all):     
  L1d:                   256 KiB (8 instances)
  L1i:                   256 KiB (8 instances)
  L2:                    4 MiB (8 instances)
  L3:                    8 MiB (2 instances)

閱讀〈Linux 核心模組運作原理

建構核心模組

進行編譯:

make -C /lib/modules/`uname -r`/build M=$PWD module

依照 The Linux Kernel document ,命令的形式如下:

make -C $KDIR M=$PWD [target]
  • -C $KDIR

$KDIR 這個路徑為核心程式碼的位置,執行 make 命令時,會切換到指定的目錄,完成後再切換回來。

  • M=$PWD

提醒 kbuild 此時正在編譯外部模組,而 M 則代表外部模組檔案所在的路徑。

  • [target]
  • modules: 在 $PWD 建構模組,此參數為預設值,所以不用特別打出來,使用此參數不會對核心做出更動。
  • modules_install: 編譯完之後會直接安裝模組。
  • clean: 移除編譯所生成的檔案。
  • help: 列出可以使用的參數。

編譯後的訊息顯示我的編譯器版本與核心開發套件所使用的不一致。

warning: the compiler differs from the one used to build the kernel
  The kernel was built by: x86_64-linux-gnu-gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0
  You are using:           gcc-12 (Ubuntu 12.3.0-1ubuntu1~22.04) 12.3.0

使用 ls /usr/bin/ -l 看到, 我的 gcc-12 是連結 x86_64-linux-gnu-gcc-12 的,所以兩個 gcc 版本是一樣的,所以我就忽略了這個警告。

➜ andyyeh@andyyeh-ubuntu  ~  ls /usr/bin/ -l | grep gcc
lrwxrwxrwx 1 root root          23 May 13  2023 gcc-12 -> x86_64-linux-gnu-gcc-12

掛載核心模組

掛載編譯出來的模組:

sudo insmod hello.ko

接著查看是否掛載成功:

➜ andyyeh@andyyeh-ubuntu  ~/linux2024/mod_test  sudo dmesg | grep Hello
[ 2141.536990] Hello, world

卸載核心模組

進行卸載:

sudo rmmod hello

查看是否卸載成功:

➜ andyyeh@andyyeh-ubuntu  ~/linux2024/mod_test  sudo dmesg | grep Goodbye
[ 2292.659951] Goodbye, cruel world

insmod

透過 strace 來追蹤使用 insmod 載入模組時,使用了哪些系統呼叫。

➜ andyyeh@andyyeh-ubuntu  ~/linux2024/fibdrv git:(master) ✗ sudo strace insmod fibdrv.ko
execve("/usr/sbin/insmod", ["insmod", "fibdrv.ko"], 0x7fff0e130a18 /* 26 vars */) = 0
...
getcwd("/home/andyyeh/linux2024/fibdrv", 4096) = 31
newfstatat(AT_FDCWD, "/home/andyyeh/linux2024/fibdrv/fibdrv.ko", {st_mode=S_IFREG|0664, st_size=274792, ...}, 0) = 0
openat(AT_FDCWD, "/home/andyyeh/linux2024/fibdrv/fibdrv.ko", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1", 6)               = 6
lseek(3, 0, SEEK_SET)                   = 0
newfstatat(3, "", {st_mode=S_IFREG|0664, st_size=274792, ...}, AT_EMPTY_PATH) = 0
mmap(NULL, 274792, PROT_READ, MAP_PRIVATE, 3, 0) = 0x743e55fa4000
finit_module(3, "", 0)                  = 0
munmap(0x743e55fa4000, 274792)          = 0
close(3)                                = 0
exit_group(0)                           = ?
+++ exited with 0 +++

其中的 finit_module 作用為將編譯出來的 ELF 格式的 obejct file 載入核心中, 首先,這個系統呼叫會先呼叫 idempotent_init_module ,而這個函式又會呼叫 init_module_from_file ,接著才會呼叫到 load_module ,其中, Linux 核心使用 struct module 這個結構來儲存模組的相關資訊。

  • /include/linux/module.h
struct module {
	enum module_state state;

	/* Member of list of modules */
	struct list_head list;

	/* Unique handle for this module */
	char name[MODULE_NAME_LEN];
  • /kernel/module/main.c

load_module 中,就會使用 add_unformed_module 這個函式,將模組加入到核心中保存模組的鏈結串列,加入時用到的就是 struct module 中定義的 struct list_head list

/*
 * Allocate and load the module: note that size of section 0 is always
 * zero, and we rely on this for optional sections.
 */
static int load_module(struct load_info *info, const char __user *uargs,
		       int flags)
{
    ...
    /* Reserve our place in the list. */
	err = add_unformed_module(mod);
	if (err)
		goto free_module;
    ...
}

將模組載入鏈結串列時,使用到了 RCU 同步機制,允許多個 reader 在單一 writer 更新資料的同時,得以在不需要 lock 的前提,正確讀取資料。

static int add_unformed_module(struct module *mod)
{
	...
	mod_update_bounds(mod);
	list_add_rcu(&mod->list, &modules);
	mod_tree_insert(mod);
	...
}

load_module 函式中呼叫了 complete_formation ,其中,必須要檢查有沒有重複的符號 (symbol) ,因此呼叫了 verify_exported_symbols ,在這個函式中使用了 find_symbol 去尋找有沒有重複的符號。

  • kernel/module/main.c

函式中也使用了 RCU 的機制,使用 list_for_each_entry_rcu 遍歷整個 modules 的鏈結串列。

/*
 * Find an exported symbol and return it, along with, (optional) crc and
 * (optional) module which owns it.  Needs preempt disabled or module_mutex.
 */
bool find_symbol(struct find_symbol_arg *fsa)
{
        ...
        list_for_each_entry_rcu(mod, &modules, list,
				lockdep_is_held(&module_mutex)) {
		struct symsearch arr[] = {
			{ mod->syms, mod->syms + mod->num_syms, mod->crcs,
			  NOT_GPL_ONLY },
			{ mod->gpl_syms, mod->gpl_syms + mod->num_gpl_syms,
			  mod->gpl_crcs,
			  GPL_ONLY },
		};

		if (mod->state == MODULE_STATE_UNFORMED)
			continue;

		for (i = 0; i < ARRAY_SIZE(arr); i++)
			if (find_exported_symbol_in_section(&arr[i], mod, fsa))
				return true;
	}
	...
}

所以當 Linux 核心要使用或是尋找模組的資訊時,只要遍歷儲存模組的鏈結串列就可以得到想要的資訊。

追蹤程式執行流程

使用〈建構 User-Mode Linux 的實驗環境〉中的方法,使用 GDB 對 insmod 進行追蹤。在 idempotent_init_module 這個函式新增斷點,到最後確實有執行到 load_module

/ # insmod hello.ko 

Thread 1 "vmlinux" hit Breakpoint 1, 0x000000006007c6f1 in idempotent_init_module (flags=<optimized out>, uargs=<optimized out>, f=<optimized out>) at kernel/module/main.c:3164
3164			return -EBADF;
...
1036		WRITE_ONCE(n->pprev, &h->first);
(gdb) s
idempotent_init_module (flags=0, uargs=<optimized out>, f=0x60a77600) at kernel/module/main.c:3173
3173		return idempotent_complete(&idem,
(gdb) s
init_module_from_file (f=f@entry=0x60a77600, uargs=uargs@entry=0x40097ca0 <error: Cannot access memory at address 0x40097ca0>, flags=flags@entry=0) at kernel/module/main.c:3132
3132	{
(gdb) s
3133		struct load_info info = { };
(gdb) n
3134		void *buf = NULL;
(gdb) n
3137		len = kernel_read_file(f, 0, &buf, INT_MAX, NULL, READING_MODULE);
(gdb) n
3138		if (len < 0) {
(gdb) n
3143		if (flags & MODULE_INIT_COMPRESSED_FILE) {
(gdb) n
3152			info.hdr = buf;
(gdb) n
3153			info.len = len;
(gdb) s
3156		return load_module(&info, uargs, flags);
(gdb) s
load_module (info=info@entry=0x648a7d90, uargs=uargs@entry=0x40097ca0 <error: Cannot access memory at address 0x40097ca0>, flags=flags@entry=0) at kernel/module/main.c:2838
2838	{

接著查看 modules 鏈結串列中的值,確認 hello 模組是否有被載入到核心中。

/ # lsmod
Module                  Size  Used by    Tainted: G  
hello                  12288  0 
/ # 
    Thread 1 "vmlinux" received signal SIGUSR1, User defined signal 1.
                                                                      0x00007ffff7c427dc in __GI___sigsuspend (set=set@entry=0x604fbda0) at ../sysdeps/unix/sysv/linux/sigsuspend.c:26
26	in ../sysdeps/unix/sysv/linux/sigsuspend.c
(gdb) p $container_of(modules->next, "struct module", "list")->name
$2 = "hello", '\000' <repeats 50 times>

MODULE_LICENSE 巨集指定的授權條款又對核心有什麼影響

find_symbol 這個函式中,會透過 find_exported_symbol_in_section 來檢查模組是否符合 GPL 的授權條款,若不符合,模組就不會被載入到核心中。在 load_module 中,就會直接跳到 ddebug_cleanup 這個地方,透過 ftrace_release_mod 將加入鏈結串列的模組移除。

 ddebug_cleanup:
	ftrace_release_mod(mod);
	synchronize_rcu();
	kfree(mod->args);

當我把 fibdrvMODULE_LICENSE 修改成 Proprietary ,就會顯示錯誤,因為不符合 GPL 的條款,所以沒辦法使用 GPL 授權的 class_create 等函式,非 GPL 與 GPL 的模組無法進行互動。

➜ andyyeh@andyyeh-ubuntu  ~/linux2024/fibdrv git:(master) ✗ make all
...
ERROR: modpost: GPL-incompatible module fibdrv.ko uses GPL-only symbol 'class_create'
ERROR: modpost: GPL-incompatible module fibdrv.ko uses GPL-only symbol 'device_create'
ERROR: modpost: GPL-incompatible module fibdrv.ko uses GPL-only symbol 'class_destroy'
ERROR: modpost: GPL-incompatible module fibdrv.ko uses GPL-only symbol 'device_destroy'
...

為何有這種規範?

Linux GPL and binary module exception clause?

在 2003 年 12 月 3 日, Kendall Bennett 問了一個問題, Linux GPL and binary module exception clause? 其中寫到,雖然 Linux 核心是使用 GPL 授權條款,但是核心模組可以是其他種授權條款,不需要使用 GPL 的授權條款。

I have heard many people reference the fact that the although the Linux
Kernel is under the GNU GPL license, that the code is licensed with an
exception clause that says binary loadable modules do not have to be
under the GPL.

結果 Linus Torvalds 就出來回應了,他說,沒有這種例外的模組存在,只要是 Linux 核心的衍生作品 (derived work) ,就必須要使用 GPL 的授權條款,所以,核心模組也不例外,一定要是 GPL 授權條款。

Basically:

  • anything that was written with Linux in mind (whether it then also
    works on other operating systems or not) is clearly partially a derived
    work.
  • anything that has knowledge of and plays with fundamental internal
    Linux behaviour is clearly a derived work. If you need to muck around
    with core code, you're derived, no question about it.

另外,在這篇 Making life (even) harder for proprietary modules 文章中也說明了為何只使用 GPL-only 的程式碼,是為了避免侵權,因為 Linux 核心沒辦法有效的判斷一個模組是否為 Linux Torvalds 口中的 derived work ,所以採用了這種機制來確保不會有法律上的疑慮。而且 GPL 這個協議中有規範,若使用者使用了這個程式碼,則他也必須進行開源,若不開源,則不能使用,所以,模組中也有可能使用了 GPL 授權條款的程式碼,所以模組也必須要進行開源,否則違反 GPL 協議,網路上也查的到因為違反 GPL 協議而受到法律制裁的案例。

Distributing a proprietary module might be a copyright violation, though, if the module itself is a derived work of the kernel code. But "derived work" is a fuzzy concept, and the kernel itself cannot really make that judgment. There is a longstanding mechanism in the kernel designed to keep infringing modules out, though: GPL-only exports.

Linux 社群對於 GPL 條款的紛爭歷史真的好長,其中還有提到 Nvidia 等著名公司,還有許多 patch 針對 GPL 相關的程式碼進行修改,不知道這部份的討論能不能當作期末專題?

Linux 核心的掛載,涉及哪些系統呼叫和子系統

syscall 的 manual page 中可以找到 insmod 所呼叫的系統呼叫。

execve

execve 會執行參數 pathname 指定的程式,必須要是一個執行檔或是 interpreter scripts ,呼叫 execve 的行程當中正在執行的程式會被替換成新的程式,且具有新的堆疊和堆積,以及新的資料區段。

brk

當傳入的值是合理的,且系統有足夠的記憶體空間,行程沒有超出他的最大資料大小時, brk 會將資料區段結束的位置設置為其傳入的參數 addr

arch_prctl

這個系統呼叫可以設定特定架構下的行程或是執行緒的狀態。

還有很多


閱讀《The Linux Kernel Module Programming Guide

simrupt 程式碼中的 mutex lock

read_lock

simrupt_read 這個函式中,使用 mutex_lock_interruptibleread_lock 進行加鎖,若沒有拿到鎖,則回傳 -EINTR ,若有拿到鎖,就回傳 0 ,防止多個 reader 同時進入 critical section 去對 kfifo 進行操作。拿到鎖之後,會使用 kfifo_to_userkfifo 中的資料複製到 user space

在核心模組的進入點 simrupt_init ,就有使用 simrupt_fopssimrupt_read 註冊為讀取這個 character device 會執行的函式,在讀取時,就會持續使用 kfifo_to_userkfifo 中的資料持續複製到 user space ,使用 cat 命令讀取,就可以得到 kfifo_to_user 放入 buf 中的資料以顯示在終端機上。

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;

producer_lock, consumer_lock

consumer_lock 確保在多個 writer 的情況下,不會有多個 writer 同時要進行 fast_buf_getfast_buf 中的資料取出,producer_lock 防止在多個 writer 的情況下,同時進行 produce_data 將資料存放入 kfifo

使用 DECLARE_WORK 巨集將 work 指向 simrupt_work_func 函式,接著在 simrupt_tasklet_func 中將 work 放入 simrupt_workqueue , Linux 核心會使用 Complete Fair Scheduler 去執行 simrupt_workqueue 中的任務。

/* Workqueue handler: executed by a kernel thread */
static void simrupt_work_func(struct work_struct *w)
{
    ...
    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);
}

追蹤程式執行


CMWQ

worker-pools 如何指定 Bound 來分配及限制特定 worker 執行於指定的 CPU

建立 work queue 時使用的是 alloc_workqueue ,其中的第二個參數需要傳入 flags ,在 CMWQ 的文件中有很詳細的介紹,在 simrupt 專案中,建立 work queue 使用的是 WQ_UNBOUND 這個 flag ,代表被加入到該 queue 中的 work 是由不指定 CPU 的特殊 worker-pools 所服務的。這種情況下核心不會對該 work queue 提供並行管理, worker-pools 會嘗試盡快開始執行 work queue 中的 work 但使用 WQ_UNBOUND 這個 flag 犧牲了 cache locality ,但在一些 CPU 密集型的任務上會很有用。

Work items queued to an unbound wq are served by the special worker-pools which host workers which are not bound to any specific CPU. This makes the wq behave as a simple execution context provider without concurrency management. The unbound worker-pools try to start execution of work items as soon as possible.

使用 WQ_UNBOUND 這個參數時,執行 sudo cat /dev/simrupt 的結果,可以發現, simrupt_work_func 在很多不同的 CPU 上執行。

➜ andyyeh@andyyeh-ubuntu  ~/linux2024/simrupt git:(main) sudo dmesg | grep simrupt_work_func
...
[ 9908.032725] simrupt: [CPU#0] simrupt_work_func
[ 9908.136392] simrupt: [CPU#0] simrupt_work_func
[ 9908.244375] simrupt: [CPU#0] simrupt_work_func
[ 9908.348636] simrupt: [CPU#0] simrupt_work_func
[ 9908.452656] simrupt: [CPU#6] simrupt_work_func
[ 9908.556653] simrupt: [CPU#7] simrupt_work_func
[ 9908.660766] simrupt: [CPU#7] simrupt_work_func
[ 9908.764465] simrupt: [CPU#7] simrupt_work_func
[ 9908.868648] simrupt: [CPU#7] simrupt_work_func
[ 9908.972670] simrupt: [CPU#7] simrupt_work_func
[ 9909.076656] simrupt: [CPU#7] simrupt_work_func
[ 9909.180402] simrupt: [CPU#7] simrupt_work_func
[ 9909.284641] simrupt: [CPU#7] simrupt_work_func
[ 9909.388660] simrupt: [CPU#6] simrupt_work_func
[ 9909.492652] simrupt: [CPU#6] simrupt_work_func
[ 9909.596658] simrupt: [CPU#6] simrupt_work_func
[ 9909.700626] simrupt: [CPU#6] simrupt_work_func
[ 9909.804624] simrupt: [CPU#6] simrupt_work_func
[ 9909.908666] simrupt: [CPU#6] simrupt_work_func
[ 9910.012654] simrupt: [CPU#6] simrupt_work_func
[ 9910.116699] simrupt: [CPU#6] simrupt_work_func
[ 9910.220374] simrupt: [CPU#10] simrupt_work_func
[ 9910.324594] simrupt: [CPU#7] simrupt_work_func
[ 9910.428693] simrupt: [CPU#10] simrupt_work_func
[ 9910.532638] simrupt: [CPU#10] simrupt_work_func
[ 9910.636661] simrupt: [CPU#7] simrupt_work_func
[ 9910.740395] simrupt: [CPU#6] simrupt_work_func
[ 9910.844651] simrupt: [CPU#6] simrupt_work_func
[ 9910.948462] simrupt: [CPU#6] simrupt_work_func

而將 WQ_UNBOUND 這個 flag 去除之後,執行 simrupt_work_func 都會在同一個 CPU 上。

➜ andyyeh@andyyeh-ubuntu  ~/linux2024/simrupt git:(main) sudo dmesg | grep simrupt_work_func
[10071.428340] simrupt: [CPU#2] simrupt_work_func
[10071.532446] simrupt: [CPU#2] simrupt_work_func
[10071.636625] simrupt: [CPU#2] simrupt_work_func
[10071.740649] simrupt: [CPU#2] simrupt_work_func
[10071.844666] simrupt: [CPU#2] simrupt_work_func
[10071.948397] simrupt: [CPU#2] simrupt_work_func
[10072.052628] simrupt: [CPU#2] simrupt_work_func
[10072.156372] simrupt: [CPU#2] simrupt_work_func
[10072.261169] simrupt: [CPU#2] simrupt_work_func
[10072.364337] simrupt: [CPU#2] simrupt_work_func
[10072.468346] simrupt: [CPU#2] simrupt_work_func
[10072.572443] simrupt: [CPU#2] simrupt_work_func
[10072.676665] simrupt: [CPU#2] simrupt_work_func
[10072.780345] simrupt: [CPU#2] simrupt_work_func

如果在呼叫 alloc_workqueue 時,傳入 WQ_UNBOUND 參數,在配置 work queue 時會呼叫到 alloc_unbound_pwq 配置 pool_workqueue ,在進行 queue_work 時,會使用 wq_select_unbound_cpu 選擇 CPU ,並在這個選定的 CPU 上執行,若沒有傳入 WQ_UNBOUND ,則會為每一個 CPU 都配置 pool_workqueue ,在進行 queue_work 時,則會在呼叫這個函式的 CPU 上運行。

CMWQ 關聯的 worker thread 又如何與 CPU 排程器互動

在初始化 work queue 時,進行的 workqueue_init 會建立 worker_pool 底下的 worker ,其中就包含 kernel thread ,執行 queue_work 將要執行的任務放入 work queue 中,最後會呼叫 insert_work 將任務插入其所屬的 pool_workqueue ,之後排程器就會對 worker 中的任務進行排程。


xoroshiro128+

解釋 xoroshiro128+ 的原理

比較 Linux 核心內建的 /dev/random/dev/urandom 的速度