contributed by < Wufangni
>
Linux 核心 4.4 版以後 Ubuntu Linux 預設使用
EFI_SECURE_BOOT_SIG_ENFORCE
,若核心模組沒有簽章則無法掛載至核心,因此需要預先關閉 UEFI Secure Boot 的功能
mokutil --sb-state
查看 Secure Boot 是否停用成功。你現在執行的 Linux 核心版本是什麼?
insmod
後,Linux 核心模組的符號 (symbol) 如何被 Linux 核心找到 (使用 List API)、MODULE_LICENSE
巨集指定的授權條款又對核心有什麼影響 (GPL 與否對於可用的符號列表有關),以及藉由 strace 追蹤 Linux 核心的掛載,涉及哪些系統呼叫和子系統?練習 hello.c 測試
hello.c
及 Makefile
,編譯核心模組產生 hello.ko
。dmesg
顯示核心訊息fibdrv: 可輸出 Fibonacci 數列的 Linux 核心模組
fibdrv
程式碼進行 make check
編譯測試,觀察核心模組 fibdrv.ko
。檔案 fibdrv.c
裡頭的 MODULE_LICENSE
, MODULE_AUTHOR
, MODULE_DESCRIPTION
, MODULE_VERSION
會在編譯 .ko
檔後提供對應的資訊,在 Linux 核心模組運作原理 中以 MODULE_AUTHOR
為例。
在 fibdrv.c
可看到對巨集的定義:
回朔至 include/linux/module.h 找到 MODULE_AUTHOR
使用 MODULE_INFO
結構
再從 include/linux/moduleparam.h 觀察, __MODULE_INFO
使用到 __UNIQUE_ID
。
從 include/linux/compiler-gcc.h 看到定義過程使用了 __PASTE
。
再對 __UNIQUE_ID
繼續做展開:
__UNIQUE_ID
使用傳入的兩參數產生一個不重複的名字,__attribute__
告訴編譯器文字存放的位置及其餘格式設定,__stringify
會將引數轉換成字串,因此 MODULE_
系列的巨集最後會轉變成唯一獨立的變數。
藉由 strace 追蹤 Linux 核心的掛載
追蹤過程中可看到 finit_module
被系統執行,此函數用於動態加載 linux 核心模組,擴展系統功能性及添加新的驅動程序。
insmod
去載入一個核心模組時,為何 module_init
所設定的函式得以執行呢?Linux 核心做了什麼事呢?從 include/linux/module.h 觀察 module_init
的定義方式:
static inline initcall_t __maybe_unused __inittest(void)
標記函數為未使用並傳回名為 initfn
的初始化函數指針。__attribute__((alias("target")))
用於給予函數或變量設置別名,讓其指向另一個函數,在這裡 init_module
被給予另一個別名 initfn
,使得加載模組時不會直接使用到 init_module
,而是調用 module_init
函數。module_init()
進行初始化函數。系統在執行 finit_module
後會執行 load_module
,從 kernel/module.c 觀察 load_module
定義:
使用 layout_and_allocate
對模組進行記憶體配置,接著以 do_init_module
對模組進行初始化。
再從 init/main.c 中追述 do_one_initcall
的定義,可看到ret = fn()
實際上針對函數 fn
進行初始化工作。
$ readelf -a fibdrv.ko
, 觀察裡頭的資訊和原始程式碼及 modinfo
的關聯,搭配上述提問,解釋像 fibdrv.ko
這樣的 ELF 執行檔案是如何「植入」到 Linux 核心Executable and Linking Format(ELF) 可以表示成一種二進制文件格式,用於儲存 Executable File、Shared Library 和 Object File,可分成 ELF header、Section(s)、Section Header(s)三部份。
從 ELF header
中可觀察到 fibdrv.ko
沒有 program headers 等訊息,因此 fibdrv.ko
不是能在 shell 呼叫的可執行檔,需要透過可執行檔 insmod
來將其植入核心,所需使用到 system call 將核心模組的資料複製到 kernel space。
simrupt_work_func
使用到 kfifo buffer
與 circular buffer
兩種儲存 data 的方式作為例子,先從 kfifo
觀察,為一種 First-In-First-Out (FIFO) 的結構體,在此定義一個 rx_fifo
的 kfifo 資料結構儲存傳遞到 userspace 的 data。
kfifo
生產資料使用到 kfifo_in
函數,此函數會先確認 buffer 剩餘空間大小是否裝的下生產的資料總長度,若不夠大則以剩餘空間長度為主複製資料到 buffer 中,足夠大則全部複製過去,最後更新 buffer 中插入資料的 index 並回傳插入資料的長度。
Circular Buffer
為一種固定大小的環狀記憶體緩衝區,head index
用於指向可插入資料的剩餘空間索引,tail index
則指向消費者即將取出資料的索引位置。
在 include/linux/circ_buf.h 可看到結構體的定義:
CIRC_CNT
被消費者所使用到,用於計算 buffer 內含有的物體數量; CIRC_SPACE
則被生產者所使用,用於計算剩餘空間數量。
fast_buf_get
函數代表消費者,分別取得 buffer 的 head
及 tail
後先使用 smp_rmb()
作為記憶體緩衝區保護取出資料的過程,ret = ring->buf[tail]
取出該筆資料後執行 smp_mb()
,並對 tail
做更新,最後回傳取出的資料內容。
接著從 linux/include/linux/workqueue.h 看到兩個 mutex lock
的定義:
producer_lock
作為生產者執行程式時的保護鎖,以 mutex_lock
及 mutex_unlock
方式防止其餘 thread 干擾包在其中的程式,而 consumer_lock
作為消費者的保護鎖,功能與 producer_lock
相似。
在 simrupt_work_func
函式中以 val = fast_buf_get()
執行消費者從 circular buffer
中取出資料,而 produce_data(val)
則是將剛才取出的資料放至 kfifo buffer
中,兩個過程分別都由 mutex_lock
及 mutex_unlock
保護,確保只有一個 thread 可對 buffer
進行存取。
在 simrupt
中單純使用 lock 作為共同資料存取的保護手段,但在多執行緒環境下此方法容易有阻塞問題,也就是說當其中一個 thread 再對共同記憶體進行存取過程時因為 lock 的性質,導致其餘欲使用到此空間的 thread 執行停滯,而若此時原進行存取的執行緒被系統做搶先或直接被 kill 則會造成所有 thread 都被迫暫停工作的情形,因此為了提昇多執行緒的效能達成並行成效,可使用 lock-free 方法做改進。
lock-free 為一種實現並行操作的方法,避免使用 lock 減少執行緒間的競爭關系,它使得執行緒間不須等待其他人釋放 lock 就可以自由操作,主要依賴於 Compare and Swap (CAS)
實現對共用資源的並行存取操作。
CAS
有三種須代入的參數,分別為用來比較的指標變數 *a
、指標原先的舊數值 old_val
以及要做替換的新數值 new_val
。
使用 CAS
包覆的迴圈作為 Critical Section,其空間為共用記憶體做存取,若執行到該空間的內容則 CAS
會去檢查 p
的值是否與原先 old
的值相同,一樣代表此空間沒有被其他執行緒使用過可以將 p
的值替換為 new
,而若不相同則重新跑一次迴圈。
workqueue
作為 linux 核心對非同步的執行手段,其中 work item
代表欲執行的一種工作項目,創建時會先將其加入到等待佇列( workqueue )中,等待 linux 核心找到適合的 work thread
執行,以往 workqueue
的作法分成兩種方式:
work thread
來處理系統上所有的 work item
,因此若前面正執行的 work item
非常龐大,其餘所有的 work 都須等待該work thread
執行結束,雖然有可節省資源的優勢在(只需要少數的 work thread
),但會造成系統執行效率低落的問題。work thread
,對於每個 workqueue
會有自己的一個 thread pool
且數目固定,對於分配到該 CPU 的 work item
有嚴格的綁定關係,不能隨意移到別的 work thread
上執行,需要多個 work thread
配合故十分消耗資源,雖然可緩解執行效率低落的現象,但在並行的效果有限,且不可避免的還是會有死結情況產生。因此 CMWQ(Concurrent Multi-Queue Workqueue)方法被提出用於解決 ST 及 MT 效能和耗能上的問題,它打破一般對於 workqueue
與特定的 thread pool
之間的關聯性,建立若干個 worker pool
(其等於 thread pool
的概念) 可共享給所有的 workqueue
,且可以根據 CPU 的負載情況動態調整 worker pool
大小釋放 work thread
,更好的實現並行,其主要實現的目標有:
workqueue
的兼容性workqueue
相容的情況下原始程式碼可以更容易的移植到 CMWQ,不需要進行過大的改動。workqueue
都可共用 per-CPU worker pools,也就是說不同的 workqueue
可共同使用一個 work pool
, 減少資源浪費。worker pool
和並行等級共同封裝在系統內部,使用者只須使用系統提供的 API 進行提交和管理。在 CMWQ 設計理念中分為兩種 work pool
類型,屬於Bound 類型的 work pool
會被限制與特定的 CPU 綁定,且每個 CPU 會有兩個 work pool
,一個提供給普通的 work,一個則給予高等級的 work,藉由 workqueue
上的 flag
決定由哪一個來執行;屬於 Unbound 類型的 work pool
未與 CPU 做綁定,可動態釋放 work thread
減少資源消耗。
透過 workqueue API
生成的 work item
會根據 workqueue
參數和本身的屬性決定該交由哪個種類的 work pool
處理,而只要 CPU 上存在一個或個正在執行的 worker thread
, work pool
就會停止執行新的 work 直到最後一個 worker thread
執行結束,確保不會有尚有 work item
但 worker thread
卻閒置的問題,且不會過度產生 worker thread
。