執行人: yu-hsiennn
專題解說錄影
Lccgth
kernel threads 在小到中等規模的資料集上表現良好,提供了最快的處理時間。然而,在更大的資料集上,不帶 flag 的 CMWQ 表現出更佳的效率
為什麼 kernel threads 在更大的資料集上,不帶 flag 的 CMWQ 表現出更佳的效率?
注意用語:
數據集 -> 資料集
mesohandsome
Lomuto's quicksort 程式碼有錯誤,for 迴圈少了 }
。
重作第六次作業的 ksort,強調 workqueue / CMWQ / kernel thread 對於任務分配的效率 (CPU utilization)。
依據第六次作業規範
insmod
後,Linux 核心模組的符號 (symbol) 如何被 Linux 核心找到 (使用 List API)、MODULE_LICENSE
巨集指定的授權條款又對核心有什麼影響 (GPL 與否對於可用的符號列表有關),以及藉由 strace 追蹤 Linux 核心的掛載,涉及哪些系統呼叫和子系統?read_lock
: 在讀取操作期間,同步對 rx_fifo
kernel FIFO 緩衝區做訪問,確保只有一個執行緒可以從緩衝區讀取資料,防止資料損壞。producer_lock
: 在 workqueue 處理程序內同步,將數據插入到 rx_fifo
FIFO 中。防止多個生產者同時向 FIFO 插入資料。consumer_lock
: 保護 fast_buf
環狀緩衝區的訪問,確保一次只有一個消費者可以進入。Why CMWQ?
早期 Linux 的工作佇列提供了兩種主要類型:單執行緒工作佇列(ST wq)和多執行緒工作佇列(MT wq)。ST wq 使用單一的工作執行緒來處理所有的工作項目,而 MT wq 為每個 CPU 都配置了一個工作執行緒,以提高並行性。隨著時間推移,越來越多的系統使用了 MT wq,加上 CPU 核心數的不斷增加,導致一些系統在啟動時就達到了預設的 32k PID 上限。
儘管 MT wq 消耗了大量資源,其提供的並行性,效果仍然未達到預期。無論是 ST wq 還是 MT wq,這種限制都存在,只不過在 MT wq 上表現較為緩和。每個工作佇列都有自己的獨立 worker pool;在 MT wq 中,每個 CPU 只分配到一個,而 ST wq 在整個系統中只有一個 context。這導致 work item 需要爭奪極為有限的執行 context,進而引發多種問題,例如在單一執行中 context 容易出現 deadlock。
並行性的提供與資源使用之間的矛盾迫使使用者作出不必要的妥協。例如,libata 選擇使用單執行緒工作佇列來輪詢 PIOs,從而接受了一個不必要的限制:無法同時處理兩個輪詢 PIOs。因為多執行緒工作佇列的並行性仍不足以滿足需求,需要更高並行性的用戶,如 async 或 fscache,最終不得不自行開發 thread pool。
每個綁定實際 CPU 的工作池通過與調度器的整合來管理並行性。當工作者喚醒或進入睡眠時,工作池會更新可運行工作者的數量,從而保持工作項目持續處理,避免 CPU 閒置。系統旨在用最少的工作者維持必要的執行頻寬,且會暫時保留但最終終止不活動的工作者。
對於不綁定 CPU 的工作佇列,其後台支援池的數量可以動態調整。使用者可以通過 apply_workqueue_attrs() 自訂工作佇列的屬性,系統將自動創建相應的工作池。此外,進展保證機制依賴於能夠在需要時創建新的工作者,特別是在內存壓力下,某些工作項目會被分配到有專門救援工作者的工作佇列上,以防止因執行 context 耗盡導致的 deadlock。
xoroshiro128+
的原理 (對照〈Scrambled Linear Pseudorandom Number Generators〉論文),並利用 ksort 提供的 xoro
核心模組,比較 Linux 核心內建的 /dev/random
及 /dev/urandom
的速度,說明 xoroshiro128+
是否有速度的優勢?其弱點又是什麼?通過呼叫 workqueue 的介面就能建立核心執行緒。並且可以根據目前系統 CPU 的個數建立執行緒的數量,使得執行緒處理能夠並行化。
所獲得的工作佇列,對系統上的每個處理器各有一個專屬執行緒。
DECLARE_WORK 為靜態初始化,會在編譯時期做好 work_struct 的初始化。
注意書寫規範:
INIT_WORK
為動態初始化,會在執行時期執行。
Makefile
用 make
編譯後,掛載二個核心模組,再執行 dmesg | tail
可得輸出:
確認掛載成功後,我們可以利用 nm
來觀察變數的宣告方式
對於 declare_work_ex.ko
和 init_work_ex.ko
中的符號類型 D
和 B
的理解如下:
DECLARE_WORK
的行為,即在宣告時就完成初始化。ksort 採用的 quicksort 即是從這篇論文中所實作出來的。
現有的快速排序 (Quicksort) 實作在某些情況下表現不佳,特別是在非隨機輸入下效能可能變得很差,例如可能需要 來排序某些特定陣列配置。
在快速排序的最初版本時,是由 Lomuto
所提出來的,具體作法如下。選擇第 1
個元素當作 pivot
,而若陣列已經做好排序(大到小或小到大),會導致時間複雜度提升到
注意書寫規範:
為了解決快速排序在面對已經排序或接近排序的陣列時性能下降的問題,改良版快速排序採取了隨機選擇 pivot 的策略。透過隨機產生一個索引並與陣列的第一個元素交換來實作,這樣 pivot 就被放在了陣列的開始位置。
而分割則採取兩個指標 i
和 j
,其中 i
指向陣列的第二個元素(因為第一個元素是 pivot ),而 j
指向陣列的最後一個元素
i
指標從底部開始,向上尋找直到找到一個大於或等於 pivot 值的元素停下來j
指標從頂部開始,向下尋找直到找到一個小於或等於 pivot 值的元素停下來i
和 j
是否已經交叉。如果沒有交叉(即 i < j
),則交換這兩個位置的值,然後繼續進行上述兩個搜索步驟;如果交叉了(即 i > j
),則將 j 位置的元素與 pivot(陣列的第一個元素)交換。切割完後如下圖所示:
作者進一步增強了排序功能的通用性和靈活性。這個版本的快速排序不再僅僅針對整數資料,而是可以處理任何類型的資料。函式接受一個 char *a
參數,這是指向資料的指標,其中的元素可以是任何資料型態。
es
表示陣列中每個元素的大小(以 Byte 為單位)cmp
比較函式,允許用戶自定義比較的行為為了進一步提升快速排序的效率並減少最壞情況下的性能下降,作者採用了 Tukey 的 ninther
方法來選擇中位數作為 pivot。這個方法是一個擴展的 median of three 技巧,它基於更大範圍的樣本來估計中位數,具體而言,是從三個不同的位置各取三個元素,再從這九個元素中選出中位數。雖然它比以往的 median of three
多了將近 12 次的比較次數,但對於大陣列來說成本不會很大,也能找到更加準確的中位數,不過對於小陣列可能就不是那麼友善了,所以作者經過實驗後來決定是否要用 ninther
,以下的數值是作者經過測試所統計出來的。
提供數學推理和證明。
median of three
,找到後再比較較這三個子陣列中位數的中位數,來避免選到最糟的 pivot
的情況。median of three
示意圖如下 (平均比較次數: )
在 Program 3, 4
中,當面對含有許多重複值的陣列時,其分割方法導致效率顯著下降。這是因為這些方法在處理重複值時可能會引起不必要的比較和交換操作,特別是當這些重複值佔據了大部分陣列時。相比之下,第七版的 quicksort 採用了所謂的 fat partition 方法,這種方法有效地將陣列分割成三部分:
pivot
pivot
pivot
示意圖如下:
最後,來看 ksort 是怎麼運作的:
可以發現到 sort_impl.c 中的 qsort_algo
有用到兩個標籤:
top
: 會先判斷要排序的長度是否小於 7
,是則直接做插入排序,反之則作上述的排序方式,其中
這個判斷式用於決定要不要提前使用插入排序,要進入有兩個條件:
pivot
相同nevermind
: 會先計算分段大小,若兩側的數量都大於 100
,則會分配一個新的 qsort
結構,並將它排入對列進行並行處理,而若其小於 100
且大於 0
,會將左側分段採取地回方式來排序,右側分段採取 goto top
來處理。適度彙整其他學員的成果並予以重現,比較自己實作的表現。要確保排序結果正確。
在 ksort 的基礎,提出得以改善並行的資料排序效能的方案,並予以充分實作,要考慮到 workqueue / CMWQ / kernel thread 對於任務分配的效率 (CPU utilization)。
首先,我們再 sort_mod.c 上面加了以下的函式:
為了測量不同情況下的執行時間,我們採用了三種不同的方法:
workqueue = alloc_workqueue("sortq", 0, WQ_MAX_ACTIVE);
workqueue = alloc_workqueue("sortq", WQ_CPU_INTENSIVE | WQ_MEM_RECLAIM, WQ_MAX_ACTIVE);
WQ_CPU_INTENSIVE
: 被設置此 flag 的 workqueue 中的 work 是特別消耗 CPU 資源的。具體的影響是,這些 workqueue 中的 work item 將歸 scheduler 管理,相關的 work item 不會阻止同一 worker-pools 中的其他 work item 被執行。WQ_MEM_RECLAIM
: kernel 將預先考慮此狀況讓建立的 workqueue 可以無視記憶體的壓力來建立 rescuer thread 以避免 deadlock 的發生。接著,利用 python script 來產生資料
有了資料後,我們還會用到 perf
來觀測,故需安裝 perf
,安裝後,再利用批次檔來執行先前產生的資料
並且修改 user.c
使其可以對應上先前的批次檔。
並依序執行以下命令
100 筆資料的排序 5 次的平均時間:
CMWQ(no flag) | CMWQ(two flag) | kernel threads | |
---|---|---|---|
data1 | 3.2 ns | 3.2 ns | 3.8 ns |
data2 | 4.2 ns | 4.2 ns | 4 ns |
data3 | 0.4 ns | 5.2 ns | 3.6 ns |
data4 | 3.6 ns | 4.4 ns | 4.4 ns |
data5 | 6.2 ns | 5.2 ns | 2.2 ns |
data6 | 7 ns | 6 ns | 2 ns |
data7 | 3.2 ns | 5.2 ns | 3 ns |
data8 | 4.8 ns | 4.6 ns | 4.4 ns |
data9 | 7 ns | 4 ns | 3 ns |
data10 | 7 ns | 2 ns | 3.8 ns |
Average | 4.66 ns | 4.4 ns | 3.42 ns |
1000 筆資料的排序 5 次的平均時間:
CMWQ(no flag) | CMWQ(two flag) | kernel threads | |
---|---|---|---|
data1 | 3.6 ns | 2.8 ns | 3.8 ns |
data2 | 6 ns | 4.4 ns | 3.8 ns |
data3 | 4.6 ns | 3.6 ns | 4 ns |
data4 | 4.2 ns | 0.6 ns | 3.2 ns |
data5 | 3.6 ns | 6.2 ns | 2.2 ns |
data6 | 2.4 ns | 0.4 ns | 4.6 ns |
data7 | 3 ns | 6.2 ns | 4 ns |
data8 | 3.4 ns | 5.4 ns | 3 ns |
data9 | 3.2 ns | 5.6 ns | 2.4 ns |
data10 | 1 ns | 0 ns | 1.8 ns |
Average | 3.4 ns | 3.48 ns | 3.28 ns |
10000 筆資料的排序 5 次的平均時間:
CMWQ(no flag) | CMWQ(two flag) | kernel threads | |
---|---|---|---|
data1 | 3.4 ns | 3.4 ns | 4.4 ns |
data2 | 0.6 ns | 4.2 ns | 4.6 ns |
data3 | 2.6 ns | 4.6 ns | 3.4 ns |
data4 | 3.2 ns | 5.8 ns | 4.8 ns |
data5 | 5.2 ns | 3 ns | 3.6 ns |
data6 | 2.6 ns | 3.6 ns | 5 ns |
data7 | 3.6 ns | 4.4 ns | 4.4 ns |
data8 | 2.6 ns | 3.6 ns | 4.6 ns |
data9 | 3.2 ns | 3 ns | 4.6 ns |
data10 | 4.4 ns | 3.2 ns | 3.4 ns |
Average | 3.02 ns | 3.76 ns | 4.28 ns |
在處理不同規模的資料集時,kernel threads 在小到中等規模的資料集上表現良好,提供了最快的處理時間。然而,在更大的資料集上,不帶 flag 的 CMWQ 表現出更佳的效率
使用 gnuplot 製圖,並留意資料範圍。