contributed by < Appmedia06 >
研讀前述 Linux 效能分析 描述,在自己的實體電腦運作 GNU/Linux,做好必要的設定和準備工作 從中也該理解為何不希望在虛擬機器中進行實驗;
閱讀〈Linux 核心模組運作原理〉並對照 Linux 核心原始程式碼 (v6.1+),解釋 insmod
後,Linux 核心模組的符號 (symbol) 如何被 Linux 核心找到 (使用 List API)、MODULE_LICENSE
巨集指定的授權條款又對核心有什麼影響 (GPL 與否對於可用的符號列表有關),以及藉由 strace 追蹤 Linux 核心的掛載,涉及哪些系統呼叫和子系統?
閱讀《The Linux Kernel Module Programming Guide》(LKMPG) 並解釋 simrupt 程式碼裡頭的 mutex lock 的使用方式,並探討能否改寫為 lock-free;
參照 2021 年的筆記。歡迎貢獻 LKMPG!
搭配閱讀〈並行和多執行緒程式設計〉
探討 Timsort, Pattern Defeating Quicksort (pdqsort) 及 Linux 核心 lib/sort.c 在排序過程中的平均比較次數,並提供對應的數學證明;
研讀 CMWQ (Concurrency Managed Workqueue) 文件,對照 simrupt 專案的執行表現,留意到 worker-pools 類型可指定 "Bound" 來分配及限制特定 worker 執行於指定的 CPU,Linux 核心如何做到?CMWQ 關聯的 worker thread 又如何與 CPU 排程器互動?
搭配閱讀《Demystifying the Linux CPU Scheduler》
解釋 xoroshiro128+
的原理 (對照〈Scrambled Linear Pseudorandom Number Generators〉論文),並利用 ksort 提供的 xoro
核心模組,比較 Linux 核心內建的 /dev/random
及 /dev/urandom
的速度,說明 xoroshiro128+
是否有速度的優勢?其弱點又是什麼?
搭配閱讀: 不亂的「亂數」
解釋 ksort 如何運用 CMWQ 達到並行的排序;
根據 M06: integration 的步驟準備。我的電腦上已經有 Linux 系統了。我們需要將 UEFI Secure Boot 的功能關閉。可以重新開機,選擇 UEFI 的選項,找到進階設定中的 Security ,把 Secure Boot 設置為 Disable 。
接著就可以按照步驟將 ksort
專案載入,但在使用 make check
命令編譯後,出現以下警告,是編譯器版本不同的問題,但尚未處理。
透過以下命令就可以將模組載入核心,使用 insmod
將模組載入核心後才會有下面的裝置檔案 /dev/sort
。
insmod
解釋在解釋 insmod
命令之前,先來說明 Linux 核心模組(module) 到底是什麼?
Linux 核心模組是可以動態載入到 Linux 核心而無需重新啟動的程式碼片段。它旨在擴展核心的功能,允許包含新功能或支援各種硬體設備或檔案系統。
核心模組允許在核心中新增或刪除程式碼,而無需重新編譯或修改整個核心。由於它們允許按需添加設備驅動程序,因此它們對於支援編譯核心時不可用的硬體特別有用。
有動態載當就就靜態載入。靜態載入的缺點就是有問題會很難處理,每次修改一個地方就要重新編譯核心並下載,導致效率不好。而若採用靜態載入的驅動程式較多,就會導致核心非常龐大,浪費資源。
核心模組儲存在副檔名為 .ko
的檔案中,而這個檔案是一個可執行的二進制文件,我們要透過 insmod
或 modprobe
命令將其載入到核心中,若是載入核心,就要在 kernel space 執行,需要將原本在 user space 中的文件透過 system call 的方式處理文件並將其複製到 kernel space 的記憶體空間中。一旦加載後,該模組就成為作業系統核心的一個組成部分,並且可以與其他系統元件互動。
資料及圖片來源: What is a Kernel Module in Linux?
以教材中的 Hello 程式來解釋。
根據 GPL 可以知道 GPL(General Public License)是一個自由軟體授權條款,而從 Linux kernel licensing rules 得知有以下幾種條款。
GPL
:GNU General Public License,通用公共許可證。GPL v2
:GNU General Public License version 2,第二版的通用公共許可證。GPL and additional rights
:表示模組除了遵守 GPL 還有額外的權利。Dual BSD/GPL
:表示模組同時遵守 BSD 和 GPL 許可證。Proprietary
:表示模組是專有軟件,不遵守 GPL。<loglevel>
表示日誌級別,用於指示消息的嚴重程度。範例中的 KERN_INFO
就是信息性消息。rmmod
命令從核心卸載模組時呼叫函數。輸入 $ sudo strace insmod fibdrv.ko
命令,即可以觀察式執行的 system call。
arch_prctl
。mmap
將文件映射到記憶體空間上,access
檢查呼叫程序是否可以存取該文件路徑名。mprotect
更改呼叫的存取保護行程的記憶體區間。openat
打開模組文件,最後再用 close
關起來。finit_module
將可 ELF(Executable and Linking Format) 載入到 kernel space 並初始化模組。simrupt 模擬硬體中斷,並將模擬的中斷事件數據傳遞到 User space。而整體的操作流程如下:
simrupt_init
函式加載模組,初始化 FIFO 緩衝區並設置定時器。timer_handler
。process_data
函式生成模擬數據並將其放入快速緩衝區。值得注意的是, FIFO 緩衝區是使用定義在 include/linux/kfifo.h 的 Linux API 。它是一個環狀佇列。並且在 One producer One Consumer(1P1C) 的情況下是可以無鎖。以下是 kfifo 的示意圖:
先看一下 kfifo 的結構體:
kfifo 使用了 in
和 out
兩個變量分別作為加入和離開索引。而在最簡單的多執行緒,也就是 1P1C 的情況下,兩個執行緒做加入和離開時就不會發生同時存取到同一個變數,進而達成無鎖的狀況。
n
個數據 in
就加 n
m
個數據 out
就加 m
out
不能比 in
大,若 out
等於 in
代表佇列為空in
不能比 out
大超過 FIFO 的空間考慮到如果一直有數據加入,in
的數值會越來越大,但因為其是 unsigned int
型態,所以當 in
到 後,就會自動變成 0 了。
這邊整理幾個 kfifo 的特色:
in
和 out
使用 unsigned int
型態,在加入和離開時不對其取餘數,而是讓其自然溢出,讓其保持 in - out
永遠是佇列的大小。mask
變數去 AND 上 in
或 out
,而 mask
本身就是佇列大小減一。根據 lib/kfifo.c:smp_rmb
保證讀取操作之間不會出現亂序smp_wmb
保證寫入操作之間不會出現亂序smp_mb
保證讀寫操作都不會出現亂序本專案旨在模擬一個中斷的 Linux 設備驅動程式,並將資料傳回 User space。先上流程圖:
這邊一步一步來解釋:
使用計時器 timer_handler
來模擬週期性的 IRQ (Interrupt request) 。會發出中斷請求。
收到 Timer 的中斷信號後,process_data
函式呼叫fast_buf_put(update_simrupt_data());
,其中 update_simrupt_data()
會產生資料,這些資料的範圍在 0x20 到 0x7E 之間,即 ASCII 中的可顯示字元,這些資料會被放入 circular buffer 中,最後交由 tasklet_schedule
進行排程。
Simrupt 在將產生的資料放入 kfifo 前,會先使用這個 「更快」的緩衝區來存放資料。
fast circular buffer,顧名思義,是一個環狀的緩衝區,它用兩個變數 head
和 tail
來只是佇列的大小。當 head
和 tail
相同時代表緩衝區為空。
fast_buf_put
扮演一個 producer 的角色,藉由 CIRC_SPACE()
判斷 buffer 中是否有剩餘空間,並更新 head index
。它會將生成的資料放入 fast circular buffer 。fast_buf_get
扮演一個 consumer 的角色,會從緩衝區中取得資料,並更新 tail index
。它會將 fast circular buffer 的資料放入 work queue。先來考慮一下 Linux 中斷的流程,大致分成兩個部份:
對於中斷 handler (top half) 是中斷中緊急的部份,例如cpu與週邊裝置之間的交互,取得狀態,ack狀態,收發資料等等。而 deferable task 的工作則是相對不緊急的,例如後續的資料收發。
因此對於作業系統而言,我可以設計一個機制來將 top half 和 bottom half 的任務分離,盡可能加速 top half 的工作。而 tasklet 就是這樣的機制,用來推遲不重要的任務的執行。
tasklet 是基於 softirq 之上建立的,但最大的差別在於 tasklet 可以動態配置且可以被用在驅動裝置上。
使用 queue_work
將 work 放入 workqueue 中,並記錄執行時間。
請參照下面筆記提到 workqueue 的運作原理。
這裡用了兩個 mutex_lock ,一個是 producer,一個是 comsumer 的鎖。producer 的鎖是用來防止多個任務寫入 kfifo ,comsumer 的鎖是用來阻止其他任務取得 circular buffer 的資料。
使用 produce_data
將資料插入 kfifo 的緩衝區裡。
最後,使用 kfifo_to_user
將最多 len 個 bytes 資料從 fifo 移到 userspace。再透過 simrupt_read
從 user space 讀取資料。
從上面的說明可以知道在 1P1C 的情況下 kfifo 是無鎖並行的,因此不需要對 kfifo 處理,而是要處理快速緩衝區 fast_buf
要改為無鎖操作。
原本的 mutex lock 為一個行程進入臨界區,若是無法獲得鎖的情況下,會進入阻塞狀態。而這邊使用到的是 mutex_lock_interruptible
函式,其和一般的 mutex lock 的差別就是提供 mutex 在睡眠狀態下仍然能被中斷。
參考了並行程式設計: Atomics 操作裡面提到:
mutex 導致效能下降的因素,往往是因臨界區過長且過於頻繁,也就限制並行程度,或競爭過於激烈 (context switch 開銷大幅增加)。lock-free/wait-free 演算法的價值在於,其保證一個或所有執行緒始終有進展,而非絕對的高效率。lock-free 和 wait-free 演算法在一種狀況會獲得更好的效率:演算法本身可用少量 atomic 指令實作 —— 實作 lock 也要用到 atomic 指令,當演算法本身用少數指令即可完成時,相比額外用 lock 當然就會有優勢。
因此我嘗試引入 linux/atomic.h
的原子操作來實現,但還未確定效能上是否有較 mutex lock 更優異。
本節要探討 Timsort、pdqsort、list_sort 的平均比較次數,Timsort 和 list_sort 我之前已經分析過了,而 pdqsort 則是結合了插入排序法(Insertion sort)、堆積排序法(Heap sort)以及快速排序法(Quick sort)。
參考 Chang-Chia-Chi
Pattern Defeating Quicksort(pdqsort) 是一種多種理想排序的混合排序演算法,其核心思想就是判斷現在數組的狀況來選擇要使用的演算法。和快速排序法一樣是 unstable 的,而時間複雜度上,在最好的情況為 ,而最差的情況則為 。
Algorithm | Best case | Average case | Worst case | Stability |
---|---|---|---|---|
Insertion sort | Stable | |||
Quick sort | Unstable | |||
Heap sort | Unstable |
從上表可以看出三個不同演算法的特性:
pivot
的選擇不好時,就會退化到 ,因此選擇一個好的 pivot
是快速排序法的重點。這邊可以參考我之前做的實驗
根據這三個演算法各自的優點,pdqsort 會根據不同情況使用不同演算法:
我將 Timsort、pdqsort、list_sort 三種排序演算法整理,來探討其比較次數及執行時間的差別。
原始碼請參照 Appmedia06/sort-comparison
實驗環境如本筆記開頭所示。克隆專案後,可以直接執行 make
命令編譯,便可以執行實驗程式:
list_sort
Timsort
pdqsort
這邊說明一下,在做實驗前都有將快取的資料清除,而因為本程式包含生成數列的部份以及因為數列部份不一,因此 perf 效能工具不好量測,因此我在排序函式前後放置量測時間變數。
list_sort | Timsort | pdqsort | |
---|---|---|---|
比較次數(次) | 121006 | 131788 | 165964 |
排序時間(sec) | 0.004242 | 0.004398 | 0.001908 |
list_sort | Timsort | pdqsort | |
---|---|---|---|
比較次數(次) | 721306 | 770839 | 954597 |
排序時間(sec) | 0.011339 | 0.010800 | 0.009194 |
list_sort | Timsort | pdqsort | |
---|---|---|---|
比較次數(次) | 98943 | 75600 | 125043 |
排序時間(sec) | 0.002629 | 0.002530 | 0.001190 |
list_sort | Timsort | pdqsort | |
---|---|---|---|
比較次數(次) | 579353 | 451795 | 670515 |
排序時間(sec) | 0.007334 | 0.006555 | 0.006200 |
list_sort | Timsort | pdqsort | |
---|---|---|---|
比較次數(次) | 73777 | 21070 | 20008 |
排序時間(sec) | 0.000972 | 0.000354 | 0.000103 |
list_sort | Timsort | pdqsort | |
---|---|---|---|
比較次數(次) | 437120 | 105515 | 100008 |
排序時間(sec) | 0.003127 | 0.001418 | 0.000532 |
比較次數,數列長度 50000
時間,數列長度 50000
探討 pdqsort ,作為混合排序法的演算法,可以看到當數列從亂序到已排序,比較次數明顯下降,便可以推斷出因為以排序了,導致快速排序法的效能不好,因此改為堆積排序法。
而 pdqsort 不管在哪個數列的情況都是贏過另外兩個演算法,也證明了混合各自演算法優點是一個好的決策。
這邊有一個有趣的發現,就是前面提到每次實驗都會先將快取清除才開始。Timsort 和 pdqsort 其實有沒有清掉快取較能並沒有插到非常多。但是 list_sort 若是沒有清掉快取效能速度竟然可以快了 倍,這也證明了 list_sort 是對快取友善的。
Pattern-defeating Quicksort
Pattern-defeating Quicksort 閱讀筆記
打造 Go 语言最快的排序算法
hankluo6/pdqsort
可以參照以下筆記,寫的非常詳細
M06: integration
Linux 核心提供 workqueue 介面以達成非同步 (asynchronous) 的流程執行,使用者以一個 "work item" 來描述需被執行的 context,將其加入到所建立佇列之中,Linux 核心隨後會選出合適的 "worker" thread 來完成,處理結束者會從佇列中刪除,目的是要簡化執行緒的建立,可根據目前系統的 CPU 個數建立執行緒,使得執行緒得以並行。
而為了解決以前 workqueue 的問題:
就有了 CMWQ 的產生,其具有以下特性:
考慮到 worker-pools 有兩種類型: