Try   HackMD

2024q1 Homework6 (integration)

contributed by < csotaku0926 >

開發環境

$ gcc --version
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0

$ lscpu
Architecture:            x86_64
CPU op-mode(s):          32-bit, 64-bit
Byte Order:              Little Endian
Address sizes:           39 bits physical, 48 bits virtual
CPU(s):                  12
On-line CPU(s) list:     0-11
Thread(s) per core:      2
Core(s) per socket:      8
Socket(s):               1
NUMA node(s):            1
Vendor ID:               GenuineIntel
CPU family:              6
Model:                   154
Model name:              12th Gen Intel(R) Core(TM) i5-12450H
Stepping:                3
CPU max MHz:             4400.0000
CPU min MHz:             400.0000
BogoMIPS:                4992.00
Virtualization:          VT-x
L1d cache:               320 KiB 
L1i cache:               384 KiB 
L2 cache:                7 MiB 
L3 cache:                12 MiB 
NUMA node0 CPU(s):       0-11

自我檢查清單

  • 研讀前述 Linux 效能分析 描述,在自己的實體電腦運作 GNU/Linux,做好必要的設定和準備工作 → 從中也該理解為何不希望在虛擬機器中進行實驗;

先前於 Windows 系統的電腦額外安裝 Ubuntu 22.04.01,以雙核心系統實作本次作業

$ uname -a
Linux csotaku-Thin-GF63-12UC 6.5.0-27-generic #28~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Fri Mar 15 10:51:06 UTC 2 x86_64 x86_64 x86_64 GNU/Linux
  • 閱讀〈Linux 核心模組運作原理〉並對照 Linux 核心原始程式碼 (v6.1+),解釋 insmod 後,Linux 核心模組的符號 (symbol) 如何被 Linux 核心找到 (使用 List API)、MODULE_LICENSE 巨集指定的授權條款又對核心有什麼影響 (GPL 與否對於可用的符號列表有關),以及藉由 strace 追蹤 Linux 核心的掛載,涉及哪些些系統呼叫和子系統?

  • 解釋 insmod 模組掛載機制 (參閱 Linux v6.1+)
    在教材提及的範例程式中,當我們撰寫以下核心程式碼後,編譯取得對應 kernel object,之後執行 insmod 命令,作為 module_init() 參數的 hello_init() 得以執行於核心

#include <linux/init.h>
#include <linux/module.h>

// ...

module_init(hello_init);
module_exit(hello_exit);

使用以下命令編譯:

make -C /lib/modules/`uname -r`/build M=`pwd` modules

根據我的核心版本,查看 v6.5 /include/linux/module.h 於第 88 行發現尚未定義 module 的:

#ifndef MODULE
#define module_init(x)	__initcall(x);

還有定義 module 的:

#else /* MODULE */
#define module_init(initfn)					\
	static inline initcall_t __maybe_unused __inittest(void)		\
	{ return initfn; }					\
	int init_module(void) __copy(initfn)			\
		__attribute__((alias(#initfn)));		\
	___ADDRESSABLE(init_module, __initdata);

該怎麼判斷那一個才是我使用的定義?
回到 bootlin 網站追本溯源,發現 MODULE 這個定義符也在 /include/linux/init.h 出現多次,如第十行:

/* Built-in __init functions needn't be compiled with retpoline */
#if defined(__noretpoline) && !defined(MODULE)
#define __noinitretpoline __noretpoline
#else
#define __noinitretpoline
#endif

只靠觀察核心程式碼,我還是不能肯定要用哪個版本,先暫時依照教材指示觀察 MODULE 未定義的狀況

可以發現 module_init(initfn) 被替換為__initcall(initfn),又被替換為 device_initcall(fn),最後是 __define_initcall(fn, 6)又一直延伸下去

當程式碼規模越來越大時,明明是同個函式卻會出現不同的別名,這是閱讀核心程式碼的一個難點

事實上,經 教材 以及 論壇 題點,可以透過以下命令觀察經過前置處理 (preprocess) 的程式,就不用以靜態方式一層層回推巨集了 (這樣顯得很沒效率)

export TREE=/usr/src/linux-headers-`uname -r`
gcc -E hello.c -I$TREE/include -I$TREE/arch/x86/include 
                                    -I$TREE/arch/x86/include/generated

搭配第 7 週教材提到的 UML 和 QEMU 來追蹤。

奇怪的是,輸出結果和教材的很不一樣

static initcall_t __initcall____KBUILD_MODNAME__99_16_hello_init6
    __attribute__((__used__)) 
    __attribute__((__section__(".initcall6" ".init"))) = hello_init;;;

這裡出現多次 GNU extension __attribute__
(暫時想不到為何有如此大的差異:是gcc版本不對?或是 -I 輸入 include path 有錯?)

很好,表示你找到可貢獻的地方。

  • 核心模組的符號如何被找到?
    hello.c 加入 MODULE_AUTHOR 以及 MODULE_LICENSE
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("CSOTAKU, NYCU");

可以在經過前處理的檔案中發現:

static const char __UNIQUE_ID(file)[] __attribute__((__used__)) __attribute__((__section__(".modinfo"))) __attribute__((__aligned__(1))) = KBUILD_MODNAME "." "file" "=" KBUILD_MODFILE; static const char __UNIQUE_ID(license)[] __attribute__((__used__)) __attribute__((__section__(".modinfo"))) __attribute__((__aligned__(1))) = KBUILD_MODNAME "." "license" "=" "Dual BSD/GPL";

static const char __UNIQUE_ID(author)[] __attribute__((__used__)) __attribute__((__section__(".modinfo"))) __attribute__((__aligned__(1))) = KBUILD_MODNAME "." "author" "=" "CSOTAKU, NYCU";

include/linux/module.h 中發現

/* Generic info of form tag = "info" */
#define MODULE_INFO(tag, info) __MODULE_INFO(tag, tag, info)
...
#define MODULE_AUTHOR(_author) MODULE_INFO(author, _author)

最後,發現了與教材不一樣的地方,大概是版本更新太多了 (v6.5)

#define __MODULE_INFO(tag, name, info)					  \
	static const char __UNIQUE_ID(name)[]				  \
		__used __section(".modinfo") __aligned(1)		  \
		= __MODULE_INFO_PREFIX __stringify(tag) "=" info

以往的定義前面還有個 #ifdef MODULE ,好像更改了

總之,藉由 GNU extension __attribute__ 可以告訴編譯器

  • __attribute__((__section__(".modinfo"))) : 這段訊息要放在 .modinfo

  • __attribute__((__aligned__(1))): 確保這段訊息對齊 1-byte boundary

  • GNU: Using the GNU Compiler Collection

  • 閱讀《The Linux Kernel Module Programming Guide》(LKMPG) 並解釋 simrupt 程式碼裡頭的 mutex lock 的使用方式,並探討能否改寫為 lock-free;

Mutex lock 可以想像成類似 token ring network 中的 token,在多執行緒 (multi-threaded) 系統中,只有拿到 mutex lock 的執行緒可以行動

simrupt 對於 mutex lock 的應用在 producer consumer problem
當核心線程呼叫 simrupt_work_func() ,於每次消費者讀取 workqueue 上的資料時,以 mutex_lock 將對應的 consumer_lock 鎖上,生產者生產資料同理

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);
    }
  • 研讀 CMWQ (Concurrency Managed Workqueue) 文件,對照 simrupt 專案的執行表現,留意到 worker-pools 類型可指定 "Bound" 來分配及限制特定 worker 執行於指定的 CPU,Linux 核心如何做到?CMWQ 關聯的 worker thread 又如何與 CPU 排程器互動?

根據文件說明,workqueue 的使用是為了非同步流程 (asynchronous process execution) 的執行。這時,工作項 (work item) 將會被放置於 wq,紀錄了需要被執行的內容。每個線程 (thread) 視為 worker,負責執行佇列內的工作項

文件也提到過往的 MT wq,每個 CPU 都有一個 worker thread,隨著 CPU 數量增長,原先 32-bit PID space 不夠用了,而且在並行等級及資源限制的取捨下,不論是 ST 或是 MT 都有不完善的地方

於是,CMWQ 出現了。正如其名,其並行程度與 worker pool 的平衡是內部調整的,並且所有 wq 共享各 CPU 上的 worker pool。worker pool 又可分為 bound 以及 unbound,分別是指定特定 CPU 以及動態分配

於 Linux 核心中提供了 alloc_workqueue 這項 API,取代原有 __create_workqueue,透過