--- tags: linux2023 --- # [2023 年暑期 Linux 核心課程](https://hackmd.io/@sysprog/linux2023-summer)第 3 次作業 > :information_source: 請提交到 ==[作業區](https://hackmd.io/@sysprog/S1zfQwD62)==,預計九月安排作業檢討的線上講座 ### 挑戰 `1` :::success 作業描述: 1. 研讀[第 2 次測驗題](https://hackmd.io/@sysprog/linux2023-summer-quiz2)的測驗 `1`,研讀程式碼註解提及的論文〈[Cilk: An Efficient Multithreaded Runtime System](http://supertech.csail.mit.edu/papers/PPoPP95.pdf)〉,解釋程式碼運作原理 2. 指出 [work-stealing](https://github.com/sysprog21/concurrent-programs/tree/master/work-steal) 程式碼可改進之處並著手進行。(歡迎提交 pull request 到 [concurrent-programs](https://github.com/sysprog21/concurrent-programs)) 3. 利用 work stealing 改寫[第 1 次作業](https://hackmd.io/@sysprog/linux2023-summer-quiz0)測驗 $\gamma$ 提及的並行化快速排序程式碼,並確保能充分運用硬體資源 4. 研讀 Linux 核心的 [CMWQ](https://www.kernel.org/doc/html/next/core-api/workqueue.html),討論其 work stealing 的實作手法 ::: --- ### 挑戰 `2` [ticket spinlock](https://en.wikipedia.org/wiki/Ticket_lock) 藉由「抽號碼牌」的機制,讓每個處理器核都有機會持有 (acquire) lock。示範程式碼: ```c struct spinlock { unsigned short owner; unsigned short next; }; void spin_lock(struct spinlock *lock) { unsigned short next = xadd(&lock->next, 1); while (lock->owner != next); } void spin_unlock(struct spinlock *lock) { lock->owner++; } ``` 其中 `xadd(a ,b)` 是 atomic 操作,回傳 a 的內含值並將 `a + b` 的結果寫回記憶體,整個動作一氣呵成 (atomic)。當上述程式碼的 Owner 等於 spin_lock() 中 next 的值時,也就代表號碼牌叫到該執行緒了,這時候,該執行緒就可以跳出迴圈並進入到 CS ,等到結束執行 CS 後,在將 lock 的擁有者交給下一個執行緒。 其中 `xadd(a, b)` 是個 atomic 操作,回傳變數 `a` 的內含值,並將結果 `a + b` 寫回到記憶體,整個過程是不可分割的(atomic)。當上述程式碼中的 `owner` 等於 `spin_lock()` 中 `next` 的值時,意味著著輪到該執行緒操作。這時,該執行緒可退出迴圈,進入到臨界區段 (criticial section),在完成臨界區段操作後,將 lock 的持有權傳遞給下個執行緒。 透過上面的解說可以發現: 當搶佔鎖的執行緒一多,就會造成 Cache 被反覆的更新。這樣不止會浪費通道頻寬,更會增加存取速度的延遲。 也因為這樣,電腦科學家又設計了其他 Spinlock 試圖解決這些問題。 現在考慮 [ticket spinlock](https://en.wikipedia.org/wiki/Ticket_lock) 在現代處理器中可能引起的性能問題。假設有一個 4 核 (core) 的處理器,每個處理器核之上的執行緒都想存取 spinlock。在這種情況下,情況如下: 1. CPU~0~ 自主記憶體存取 spinlock、更新其 cache 並持有 spinlock 2. CPU~0~ 更新主記憶體中的 `next` 值。 3. CPU~1~ 自主記憶體中存取 spinlock 並更新其 cache 4. CPU~1~ 標記對應到 spinlock 的 cache 為失效 (invalidate) 並更新 `next` 的值,隨即發現 `next != owner`,於是等待 5. CPU~2~ 自主記憶體存取 spinlock,更新其 cache 6. CPU~2~ 標記對應到 spinlock 的 cache 為失效 (invalidate) 並更新 `next` 的值,隨即發現 `next != owner`,於是等待 7. CPU~3~ 自主記憶體中存取 spinlock 並更新其 cache 8. CPU~3~ 標記對應到 spinlock 的 cache 為失效 (invalidate) 並更新 `next` 的值,隨即發現 `next != owner`,於是等待 9. CPU~0~ 釋出 spinlock,更新對應的 cache 及 owner 的值,這使得其他 CPU cache 中的 spinlock 失效 10. CPU~1~ 注意到 cache 中的 `lock->owne`r 失效,更新 cache 並持有 spinlock(即 `next == lock->owner`) 由上可見,在多核處理器中,一旦多個執行緒競爭同一個 spinlock 時,將導致 cache 被反覆更新。這不僅會浪費記憶體存取的頻寬,還會造成存取速度的延遲,詳見〈[從 CPU cache coherence 談 Linux spinlock 可擴展能力議題](https://hackmd.io/@sysprog/linux-spinlock-scalability)〉。 為了避免 spinlock 操作過程中,因 cache 反覆更新致使嚴重性能衝擊,[MCS lock](https://lwn.net/Articles/590243/) 提出以解決這樣的問題。,其設計理念是讓每個處理器核各自持有一個 lock,每個獨立的處理器核只需要觀察並維護自身的 lock 即可。示意的結構體如下: ```c struct mcs_spinlock { struct mcs_spinlock *next; int locked; /* 1 if lock acquired */ }; ``` 不僅每個處理器核持有一份 spinlock,MCS spinlock 尚有一個 main lock,作為該結構體的首位。當 main lock 被初始化時,其狀態如下圖: ![](https://hackmd.io/_uploads/HkpuHPMT3.png) 當某個處理器核嘗試持有 spinlock 時,它會建立一個屬於自己的 MCS spinlock,然後與 main lock)進行 atomic 操作。若 main lock 的 `next` 為 `NULL`,就意味著目前沒有其他執行緒在等待,我們可將 main lock 指向該處理器核持有的 MCS spinlock,從而讓該處理器核持有 spinlock。對應的交換程式碼如下: ```c swap(main_lock, cpu_lock) { ret = main_lock; &main_lock->next = cpu_lock; &main_lock->locked = &cpu_lock->locked; return ret; } ``` 藉由上述 atomic 操作,MCS spinlock 的狀態更新如下: ![](https://hackmd.io/_uploads/r1EwIDf62.png) - [ ] 當 spinlock 已被他人持有時 假設 CPU~1~ 已持有 spinlock,現 CPU~2~ 嘗試存取 spinlock。藉由 atomic 操作,main lock 會指向 CPU~2~ 持有的 spinlock: ![](https://hackmd.io/_uploads/S1eJGDwMpn.png) 從 main lock 的狀態可見目前 spinlock 已被他人持有 (即 `main_lock->next != NULL`),於是 CPU~2~ 只能等待,這時會將 CPU~1~ 的 MCS spinlock 指向 CPU~2~ 所持有的 MCS spinlock: ![](https://hackmd.io/_uploads/SJZIvDGah.png) - [ ] 當 spinlock 被釋放 當 CPU~1~ 完成任務,會對 main lock 比較和交換,若 main lock 指向自身,代表沒有執行緒排隊等候,於是只要更新 main lock 的 `locked` 值,並讓 main lock 指向 `NULL`,最後將 CPU~1~ 持有的 MCS spinlock 釋放即可: ![](https://hackmd.io/_uploads/Sy2iDDMph.png) 反之,若 main lock 指向的不是自身,那就變更 CPU~2~ 的 `locked` 值,再釋放自身持有的 MCS spinlock: ![](https://hackmd.io/_uploads/SJDJ_PG6n.png) 延伸閱讀: * [A simple correctness proof of the MCS contention-free lock](https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=ba210c9f21b09ae56bb4b808c2331b2569e06639) 實作程式碼可見 [mcslock](https://github.com/sysprog21/concurrent-programs/tree/master/mcslock)。 :::success 作業描述: 1. 目前 [mcslock](https://github.com/sysprog21/concurrent-programs/tree/master/mcslock) 實作採用 [GCC atomic builtins](https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html),請用 C11 Atomics 改寫,並確認實作行為與上述描述是否符合。 (歡迎提交 pull request 到 [concurrent-programs](https://github.com/sysprog21/concurrent-programs)) 2. 研讀 Linux 核心的 [locking 實作](https://github.com/torvalds/linux/tree/master/kernel/locking),闡述你對於 Linux 核心原始程式碼 [mcs_spinlock.h](https://github.com/torvalds/linux/blob/master/kernel/locking/mcs_spinlock.h) 的認知,對照閱讀核心文件 [locking](https://docs.kernel.org/locking/index.html)。 3. 針對 MCS lock 所要改進 ticket lock 在多核處理器的效能議題,請設計實驗來說明其效益。 ::: --- ### 挑戰 `3` [rwlock](https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock) 寓意為 reader-writer spin lock,不同於尋常的 spinlock,它區分「讀取」(read) 和「寫入」(write) 的操作:若目前沒有寫入者,那麼多個讀取者可同時持有該 rwlock,反之,若目前沒有任何讀取者,那寫入者可持有該 rwlock。 ![](https://hackmd.io/_uploads/SJysuo463.jpg) rwlock 限制讀取者與寫入者之間,及寫入者與寫入者之間的互斥性,但並未對讀取者與讀取者之間的互斥性做出限制。 ![](https://hackmd.io/_uploads/rkICuoVa2.jpg) 舉例來說,當多個 CPU 需要對某個鏈結串列進行大部分是節點走訪和查詢的操作,而不需要新增、刪除或移動元素時,就適合使用 rwlock。 對於讀取者,使用 `read_lock()` 和 `read_unlock()` 來限制讀取臨界區段。 ```c read_lock(); /* --> queued_read_lock() */ /* 臨界區段 */ read_unlock(); /* --> queued_read_unlock() */ ``` 當一個讀取者進入臨界區段時,需要將讀取臨界區的引用計數 (reference count) 加 `1`,離開時則減 `1`。只有當此引用計數為 `0` 時,寫入者才能執行其臨界區程式碼。 ```c #define _QR_BIAS (1U << 9) void queued_read_lock_slowpath(struct qrwlock *lock) { atomic_add(_QR_BIAS, &lock->cnts); /* ... */ } ``` 對於寫入者,使用 `write_lock()` 和 `write_unlock()` 來限制寫入臨界區段。 ```c write_lock(); // --> queued_write_lock() /* 臨界區段 */ write_unlock(); // --> queued_write_unlock() ``` 不同讀取者的臨界區可以平行進行,但一個寫入者的臨界區段不得與其他寫入者/讀取者的臨界區並行運作。 ![](https://hackmd.io/_uploads/SkRAFjNa2.jpg) 看似 rwlock 較原本的 spinlock 更細緻地控制讀取和寫入的情境,這似乎讓應用場景更高效,對嗎?然而,rwlock 存在一個問題:若有多個讀取者正在使用某個 rwlock,寫入者需要等待所有讀取者釋放該 rwlock,方可持有 rwlock,這可能導致寫入者的執行被延遲,這種情況被稱為「飢餓」(starvation)。而且,在寫入者等待的過程中,讀取者仍可持續加入並執行,這對於寫入者來說顯然是不公平的。即使寫入者的優先順序更高,他們也不能在優先順序更低的讀取者之前執行。換言之,在 rwlock 中,讀取者還是寫入者這樣的身份決定一切。 由於上述的限制,其他 spinlock 實作出現在 Linux 核心,也有開發者改用 RCU。 seqlock 全名是「序列鎖」(sequential lock),相較於 rwlock,seqlock 進一步解除讀取者與寫入者之間的互斥性,僅保留了寫入者與寫入者之間的互斥性:只要沒有其他寫入者持有此 seqlock (即使目前有讀取者持有該 seqlock),試圖獲取該 seqlock 的第一個寫入者就能成功持有。 倘若當讀取者在讀取共享變數的同時,寫入者對變數進行修改,是否會導致讀取的資料不一致呢? 首先,我們來觀察 Linux 核心 seqlock 結構體定義(程式碼位於 [include/linux/seqlock.h](https://github.com/torvalds/linux/blob/master/include/linux/seqlock.h)): ```c typedef struct { struct seqcount seqcount; spinlock_t lock; } seqlock_t; ``` 可見 seqlock 在尋常 spinlock 的基礎上新增名為 seqcount 的成員,而 seqlock 正是利用這個 seqcount 的序列號 (sequence number) 來確保資料的一致性。 - [ ] 開始寫入 每當寫入者成功持有 seqlock 後,序列號的值會增加 1: ```c static inline void write_seqlock(seqlock_t *sl) { spin_lock(&sl->lock); sl->seqcount.sequence++; } ``` - [ ] 結束寫入 當寫入者在釋放 seqlock 前,序列號的值再次增加 1: ```c static inline void write_sequnlock(seqlock_t *sl) { sl->seqcount.sequence++; spin_unlock(&sl->lock); } ``` 初始情況下,序列號的值是一個偶數 (even),因此當寫入者持有 seqlock 時,序列號的值會是奇數 (odd),在寫入操作完成並釋放鎖後,序列號的值又會變回偶數。這樣的設計確保序列號的值的奇偶性,可用來標記是否存在寫入操作,進而幫助確保讀取者在讀取期間不會看到不一致的資料。 ![](https://hackmd.io/_uploads/B1M82oVa2.jpg) - [ ] 開始讀取 當讀取者在讀取共享變數之前,首先需要讀取序列號的值。若該序列號為奇數,表示目前有寫入者正在修改此變數,因此讀取者需要等待,直到序列號變為偶數,方可開始進行變數讀取。 ```c static inline unsigned __read_seqcount_begin(const seqcount_t *s) { unsigned ret; repeat: ret = READ_ONCE(s->sequence); if (unlikely(ret & 1)) { cpu_relax(); goto repeat; } return ret; } ``` - [ ] 結束讀取 在讀取完成變數後,讀取者需要再次讀取序列號的值,並將其與之前讀取前的序列號進行比較,以判斷是否相等。若相等,則表明在此期間內未有寫入者的操作。 ```c static inline int __read_seqcount_retry(const seqcount_t *s, unsigned start) { return unlikely(s->sequence != start); } ``` - [ ] 適用場景 在 seqlock 中,讀取者可隨時讀取 (相當於在讀取一側未加鎖)。在此過程中,寫入者的操作不會受到影響,但讀取者可能需要進行多次讀取。寫入者只會被其他寫入者影響,不再受到讀取者的影響。 顯然,此設計傾向於寫入者。seqlock 適用於讀取者較多而寫入者較少的情況,在 Linux 核心中,它的重要應用之一是表示時間的 jiffies,後者記錄自系統啟動後的時鐘節拍數。 ```c do { seq = read_seqbegin(&jiffies_lock); ret = jiffies_64; } while (read_seqretry(&jiffies_lock, seq)); ``` 實作程式碼可見 [seqlock](https://github.com/sysprog21/concurrent-programs/tree/master/seqlock)。 延伸閱讀: * [Eliminating rwlocks and IRQF_DISABLED](https://lwn.net/Articles/364583/) * [Sequence counters and sequential locks](https://docs.kernel.org/locking/seqlock.html) * [Rust: SeqLock](https://docs.rs/seqlock/latest/seqlock) :::success 作業描述: 1. 目前 [seqlock](https://github.com/sysprog21/concurrent-programs/tree/master/seqlock) 實作採用 [GCC atomic builtins](https://gcc.gnu.org/onlinedocs/gcc/_005f_005fatomic-Builtins.html),請用 C11 Atomics 改寫,並確認實作行為與上述描述是否符合。 (歡迎提交 pull request 到 [concurrent-programs](https://github.com/sysprog21/concurrent-programs)) 2. 研讀 Linux 核心的 [seqlock.h](https://github.com/torvalds/linux/blob/master/include/linux/seqlock.h) 實作,闡述你對於 Linux 核心原始程式碼的認知,對照閱讀核心文件 [locking](https://docs.kernel.org/locking/index.html)。 3. 參照[第 2 次測驗題](https://hackmd.io/@sysprog/linux2023-summer-quiz2)的測驗 `2` 提及的 [mpmc](https://github.com/sysprog21/concurrent-programs/tree/master/mpmc) 及 [spmc](https://github.com/sysprog21/concurrent-programs/tree/master/spmc) 程式碼,改寫為大量讀取者-單一寫入者 (multiple-reader/single-write) 的場景,並運用上述 seqlock 程式碼,談及其具體效益。 (歡迎提交 pull request 到 [concurrent-programs](https://github.com/sysprog21/concurrent-programs)) ::: --- ### 挑戰 `4` 考慮 cmap 是個仿效 C++ [std::unordered_map](https://en.cppreference.com/w/cpp/container/unordered_map) 的並行實作,比照 [Linux 核心的 RCU 機制](https://hackmd.io/@sysprog/linux-rcu),針對大量讀取者-單一寫入者 (multiple-reader/single-write) 的場景,以 C11 Atomics 開發。測試程式 `test-cmap` 預設建立 3 個讀取端執行緒及單一寫入端執行緒,在 5 秒內進行並行讀寫 (可由命令列參數變更數量及時間),參考執行輸出: ``` $ ./test-cmap #checks: 4, #inserts: 0, #removes: 0, cmap elements: 49, utilization: 0.00 #checks: 13698, #inserts: 2281, #removes: 1265, cmap elements: 1065, utilization: 0.59 #checks: 27386, #inserts: 4562, #removes: 2769, cmap elements: 1842, utilization: 1.37 #checks: 41034, #inserts: 6842, #removes: 4242, cmap elements: 2649, utilization: 0.85 #checks: 54706, #inserts: 9124, #removes: 5753, cmap elements: 3420, utilization: 1.20 #checks: 68213, #inserts: 11385, #removes: 7383, cmap elements: 4051, utilization: 1.49 #checks: 81784, #inserts: 13666, #removes: 8749, cmap elements: 4966, utilization: 0.76 #checks: 95345, #inserts: 15945, #removes: 10190, cmap elements: 5804, utilization: 0.97 #checks: 109042, #inserts: 18223, #removes: 11749, cmap elements: 6523, utilization: 1.16 #checks: 122738, #inserts: 20503, #removes: 13376, cmap elements: 7176, utilization: 1.33 #checks: 136430, #inserts: 22785, #removes: 15040, cmap elements: 7794, utilization: 1.48 #checks: 150123, #inserts: 25065, #removes: 16651, cmap elements: 8463, utilization: 0.60 #checks: 163809, #inserts: 27344, #removes: 17985, cmap elements: 9408, utilization: 0.71 #checks: 177196, #inserts: 29568, #removes: 19341, cmap elements: 10276, utilization: 0.82 #checks: 190831, #inserts: 31830, #removes: 20846, cmap elements: 11033, utilization: 0.92 #checks: 204352, #inserts: 34064, #removes: 22341, cmap elements: 11772, utilization: 1.01 #checks: 217901, #inserts: 36307, #removes: 23824, cmap elements: 12532, utilization: 1.10 #checks: 231488, #inserts: 38573, #removes: 25409, cmap elements: 13213, utilization: 1.19 #checks: 245149, #inserts: 40843, #removes: 26979, cmap elements: 13913, utilization: 1.27 #checks: 258702, #inserts: 43094, #removes: 28560, cmap elements: 14583, utilization: 1.35 ``` 程式碼可見 [cmap](https://github.com/sysprog21/concurrent-programs/tree/master/cmap)。 :::success 作業描述: 1. 說明上述程式碼運作原理,目前 [cmap](https://github.com/sysprog21/concurrent-programs/tree/master/cmap) 無法通過 ThreadSanitizer,請指出其問題並修正。 (歡迎提交 pull request 到 [concurrent-programs](https://github.com/sysprog21/concurrent-programs)) 2. 上述程式碼的輸出有一欄是 utilization,解釋其計算基準,並探討如何進一步提升 throughput/scalability,請善用 futex 和第二次作業的成果。 3. 嘗試執行 [kernel-module-rcu-test](https://github.com/sbcd90/kernel-module-rcu-test) 並將上述程式碼移植到 Linux 核心 (使用 Linux 核心的 RCU, spinlock, mutex, condition variable),過程中你應該會重新體會到 Linux 核心的並行機制。 ::: ---