總結一下 O/S 課程里面和鎖相關的內容. 本文是 6.S081 課程的相關內容總結回顧結合 Real World 的 Linux 講解各種鎖和 RCU lock free 機制原理, 前置知識是基本的操作系統知識以及部分組成原理知識:線程與並發的概念, 中斷與管態用戶態概念, 以及基本的並發編程鎖模型如讀寫鎖等和部分數據結構. 最好掌握的:高速緩存一致性協議,CPU 亂序執行,內存屏障。
RCU 部分涉及的論文和擴展閱讀:
RCU Usage In the Linux Kernel: One Decade Later ,Paul E. McKenney,Jonathan Walpole
https://www.kernel.org/doc/Documentation/RCU/whatisRCU.txt
What is RCU, Fundamentally? [LWN.net]
共享數據結構的一致性(為什么要做鎖?)
對於 shared data structure, 需要保證讀寫的 critical section 時具備 consistency, 特別是讀的時候, 不希望讀到一個不完整的數據或者數據結構的不完整的結構. 比如一個鏈表在多個線程的讀寫過程中可能會出現的混亂的指針.
單核本來就沒有並行 (誰需要鎖?)
先談論 single core 的情況, 我們只需要通過關中斷 就可以實現 sequential access, 具體的思想實驗是如果一個結構正在被某個 thread (這里的 thread 泛指 kernel 編程里面的 process) 占用, 我們希望他處理結束之后再 context switch, 這樣實際上不會出現共享訪問, 也就是沒有並行(對於共享結構本身就希望訪問並行的序列化). 也就是 local_irq_disable() 和 preempt_disable() 就能實現了. 某個數據結構一開始是未 locked 的, 一旦 locked 了就關閉 timer interrupt, 從而保證了在本線程結束前后的數據一致性.
多核爭用時自旋等待 (多核怎么做鎖?)
對於 multicore 的情況, 則需要考慮更多, 對於一個數據結構, 一旦他已經被一個 core 給 lock 了, 當前運行在另一個 core 上的 thread 就需要等待鎖釋放, 所以需要一個循環等待的過程, 叫做自旋. 具體實現的關鍵部分是通過 CPU 提供的一種 swap 指令, 在 RISC-V 上這個指令是 amoswap, 其功能是執行一個原子操作的讀出值和放入新值. 這樣只需要把 true 旋進去, 拿出來的如果是 true, 說明本來就被鎖了, 放個 true 進去不改變原始值, 如果拿出來是 false 說明當前 thread/core 拿到這個鎖. 我們看 xv6 里面的實現:
void acquire(struct spinlock *lk) { push_off(); // disable interrupts to avoid deadlock. if(holding(lk)) panic("acquire"); // On RISC-V, sync_lock_test_and_set turns into an atomic swap: // a5 = 1 // s1 = &lk->locked // amoswap.w.aq a5, a5, (s1) while(__sync_lock_test_and_set(&lk->locked, 1) != 0) ; // Tell the C compiler and the processor to not move loads or stores // past this point, to ensure that the critical section's memory // references happen strictly after the lock is acquired. // On RISC-V, this emits a fence instruction. __sync_synchronize(); // Record info about lock acquisition for holding() and debugging. lk->cpu = mycpu(); }
其中內存屏障做的事情在編譯器上很好理解就算防止編譯過程的指令重排導致的持有鎖狀態更新和鎖實際狀態的不一致, 而其在 CPU 上其實就是做一個等價於清空流水線的操作(具體實際上是涉及多核的 Cache 一致性,Kernel documentation 里也有講解 memory barrier 的 txt 文檔,下文也會提及一部分內存屏障的詳細介紹), 這一點我們在體系結構課程和 CSAPP 里面的對於簡易流水線和 tomasulo 技術相關的地方學習過了. 值得提一下 spinlock 是約定不允許在 context switch (關閉中斷后只有 yield() 會引發) 時候持有的(這個下文也會講), 否則會有可能導致問題如 deadlock(當持有兩個鎖順序不同時), 這一點和關中斷的原因一樣, 顯而易見 (會導致別的共享數據結構的線程自旋無法獲取到鎖).
關搶占實現性能保障 (怎么推動盡快釋放鎖?)
spinlock, 他的 overhead 有內存屏障導致的清空流水線浪費一個流水線長度, 然后主要就是循環等待的不斷 CAS 的過程, 還有一個就算這個 amoswap 涉及多核 CPU 的 cache coherence MESI 的東西. 當然這個單核時使用 spinlock 最簡單的就算一個退化過程, 不需要單獨編寫 (Linux 里面對單核 CPU 和多核的方案當然不一樣). 他的性能則是由關搶占來保證的, 關中斷后能夠使需要鎖的操作快速運行完, 防止拿到鎖后 context switch 出去導致別的線程/核心需要等該核心輪轉.
補充一個要點,我后來才注意到 Linux spinlock 的實現:spinlock 持有 lock 之后關了 preempt 不關 interrupt。但是對於某些情況,有一個 irqsave 版本會關。irqsave 版本涉及的要點是:process 和 中斷都想獲取一個資源的時候,就要 avoid deadlock。
應用層自旋鎖性能捉急 (鎖的性能怎么樣?)
spinlock 也無法很好地用於應用層, 這是從語言 runtime 角度上看的, 他無法提供一個操作系統關中斷的方法, 也就是沒有上述代碼這種模式中的 push_off() 部分, 回想編寫 spinlock 的過程, 我們依據關中斷實現序列化, 多核循環等待並且規定 context switch 時不能持有鎖來避免死鎖. 一些庫實現了應用態的 spinlock, 但是沒有關中斷的操作, 這樣的 spinlock 和上述的 spinlock 就不一樣了,
我們具體分析就是, 單核和多核下都可以通過 timer interrupt 來避免死鎖, 但是由於沒有關中斷, 從而無法保證需要持有鎖的任務會快速完成 (而這個是性能的關鍵). 即內核態 spinlock 讓一些核空轉等待,並督促持有核推進當前任務,而應用層 spinlock 只有空轉等待,沒有督促效應,持鎖核很有可能三心二意執行中途其他任務導致等待核持續空轉。
應用層自旋鎖如何把控性能 (怎么做應用層的鎖?)
改進用戶態 spinlock 可以參考 nginx 的 ngx_spinlock, 他通過用 ngx_cpu_pause() 來告知處理器優化 spin-wait loop 性能和單核下 ngx_sched_yield() 快速切換 (本段已經說了應用層的 spinlock 和 O/S 層的機制不一樣, 所以允許 context switch)。
這里詳細講解一下 nginx 的方案: 首先是 intel 的 PAUSE 指令, 就是spin-wait loop 由於有很多的 hazard 的 load 和 store (至於 CAS 指令是原子的為什么會導致 load 和 store 這一點想一下流水線本質執行的是精簡指令就能理解, 具體不深究), 容易使處理器的流水線指令重排機制認為出現 memory order violation, 所以要保證安全就要頻繁地清空流水線, pause 能避免大量循環后再 context switch。
但是我們知道現代處理器用了巨量寄存器來進行提前計算,即 tomasulo 是不進行清空的, 我們通過最后指令 retire 之前進行 reorder, 而這個 reorder 本質上是一些邏輯電路在做, 也要占用 CPU cycles, Intel 說會引發 25 倍性能損失, 所以這個 PAUSE 直接讓 CPU 執行一些 NOP 類似的效果(好像用 NOP 效果更好), 也就不需要 reorder 的這部分 overhead 了. Intel 還說 busy loop 會導致 CPU 發熱功耗增高, 這個涉及具體的 CPU 實現. 第二點是 yield , 也就是提前 yield, 這一點則很好思想實驗, 就是提前 timer interrupt 了而已. 博客園知識庫還有一篇文章分析了 C runtime 的 spinlock 的改進, 主要是匯編代碼, 值得一看 自旋鎖spinlock剖析與改進_知識庫_博客園 (cnblogs.com).
睡眠鎖把輪詢型轉向通知型 (如何去掉循環浪費CPU?)
spinlock 講到這里了, 所以 busy-wait-loop 就很浪費 CPU cycles, 不止是應用層, kernel 里也一樣. 對於長阻塞操作, 我們可以做 sleeplock, sleeplock 具備 sleep() 和 wakeup(), 很明顯是需要 O/S 支持的, 所以就是內核下的. 其具體實現很容易思想實驗, 我們必須在內核進程 PCB 建立一個 condition 的字段用來存儲進程睡在誰上面, 然后就是 sleep syscall trap 進來 RUNNING 變 SLEEP 了. 針對多核對 sleep lock 的問題, 所以 sleeplock 本身以及 process controll block 本身都要受到 spinlock 的保護.
然后我們要明確的是 sleeplock 的 sleep syscall 本身是可以被 preempt 或者其他東西 interrupt 的, 畢竟都在 process 的 kernel space 下了, 不會引發一致性問題, 當然涉及 PCB 和 sleeplock 本身的信息是被 spinlock 鎖了的.
sleeplock 是不能在 trap handler 里面用的, 這是因為我們知道 PCB 具備 context, contex 要么是其 kernel space 的要么是 user space 的, 而 trap 的 vector 是在 trampoline 里面 map 來跳轉的, handler 要實現調用 sched 還原 context, 其本身是 kernel 的 temp code, 並不具備 context, 更不用說支援多次中斷了. 所以也不能使用 sleeplock, 因為 wakeup 一個 process 並不能返回到 handler 的某個 context 里.
Linux 中的 sleeplock —— semaphore (Real Word 分析)
xv6 的 sleeplock 在 linux 內核中是叫做 mutex , 早期只有一個 semaphore (具體兩者有一些差異, 主要是互斥數量吧) linux 的 mutex 實現十分的復雜, 涉及多種分類討論的機制, 如果具體學習很復雜沒有 xv6 這個簡單. 所以先看一下 Linux semaphore 的源碼. 因為涉及一個分類討論的做法,所以回來補充了 mutex 的分析,講完 semaphore 就講 mutex。
可以看到進行了多次嘗試, 最后在 schedule() 處進入了睡眠. 一旦恢復 RUNNABLE 並被 schedule 之后, 將會返回到 for 循環下一個迭代中獲取鎖, 這一點和 xv6 中 acquire 中一個 while 里面 sleep 是一樣的代碼結構, 當然 wakeup 之后並不一定能獲取鎖, 很有可能重新進入 sleep, 看誰搶的快了.
Linux 的 mutex 則是基本原理和 semaphore 差不多,實現上復雜一些。
Linux 中的 spinlock + sleeplock —— mutex(Real Word 分析)
Linux 的 mutex 則是基本原理和 semaphore 差不多(一個是如其名只能讓 0 和 1 的公用即 mutex 互斥,一個則是 Dijkstra 信號量),實現上復雜一些(增加了多路徑優化)。
我們將在后面講 Java 的鎖的實現時候看到,對於不同的情況 spinlock 和 阻塞的 sleeplock 的性能效果也不同。盡管是在內核里用鎖沒有 trap 的開銷但仍然由 schedule() 導致的 contex switch,這也是 mutex 的另一個優化點,在具體談論怎么做時,我們給出一個內核開發鎖選用的建議:
盡管我沒有具體截圖,但是觀看這段注釋相信就能想到這接下來的代碼是什么了,他甚至不用像 Java 那樣先試着 spin 10 次再 sleep,而是直接判斷一個鎖正在 SMP 機的另一個核上進程持有並運行者,就直接 spin 等待而不是 sleep。當然會有一個疑問,我們的 sleeplock 設計上是 preemp_on 的,不過這一點思想實驗也可以解決,只需要在 spin 的循環里面判斷 owner switch 出去的話我就 break 從而順延到普通的 sleeplock 上。所以為什么下文提到的 Java 不采用這種方案呢?可能和虛擬機的多核多線程本質是由操作系統內核調度的有關,盡管可能是一對一的線程模型,但是 Java 並不能了解被調度運行的線程是否真實運行在 CPU 核心上也無法獲取 owner 線程是否 Running 的,所以這也是 Java 的線程狀態沒有 Running 只有 Runnable 的原因吧。
sleeplock 提供系統調用提高應用層鎖性能 (如何讓應用層也去掉輪詢?)
於是再看到應用層的, 即用戶態下可以實現 spinlock, 那么是否可以實現 sleeplock 呢? 答案是否定的, 我們無法在應用層下實現一個需要涉及 PCB 操作的功能. 但是可以在內核下實現 futex 供應用層使用.
linux 下 c runtime 的 pthread_mutex 獲取鎖分為兩階段,第一階段在用戶態采用spinlock鎖總線的方式獲取一次鎖,如果成功立即返回;否則進入第二階段,調用系統的futex鎖去sleep,當鎖可用后被喚醒,繼續競爭鎖。
這樣實現的好處是對於大部分情況, 不需要進入到 kernel space, 直接在 runtime 的 user space 就完成了. 對於確實需要等待的, trap 進 kernel sleep. 主要的 overhead 只是 trap 而已. futex 是給 user program 做 syscall 的這個和 pthread 的用戶態 mutex 不一樣.
應用層我們可以看 pthread 庫下的 mutex 實現原理, 當然這里還涉及 pthread 的實現, 具體就不說了. 主要看這個模式, 是怎么的思想方法. 可以看到死循環里面如果沒有拿到鎖, 就會通過調用 suspend 來調用 futex 的 syscall trap 進去從而 sleep.
然而我們實際上應用層用到 涉及 syscall 的sleeplock 的性能是不好的,這是因為這里涉及到 context switch,context switch 不可避免的耗費大量 CPU cycles 去執行 trapframe 保存恢復和 trap handler 的指令上去,而且自從 Meltdown 的 paper 出來后,Linux 也把原來的共享的 pagetable 換成了 KPTI(isolation),從而 syscall 進入的 kernel space context switch 必須連帶 flush TLB... (Intel 更新了流水線先檢查權限再 load 可能就不用 KPTI 了,不過 AMD 本來就沒有 Meltdown 漏洞),而 flush TLB 浪費的 cycles 可不能算少了。所以 C++ 11 搞的 atomic 當然性能比 mutex 不知道高到哪里去,人家那是純粹 runtime 提供的 user space 的 CPU atomic instruction 而已。
那么如果沒有了 context switch,是不是就全部用 sleeplock 才是最優解呢?好像的確是這樣的。雖然講的是 O/S, 不妨 Java 其實也是 O/S 吧,Java 里面有管程(jvm 對象頭)能用來記錄鎖信息(Thread 睡在哪個鎖上),所以很適合實現 sleeplock,事實上,synchronized 關鍵字),ReentrantLock 都是用這種 sleeplock 方案,會把自己掛起,不過其實 sleep 和 wakeup 這兩件事都是很花時間的,如果鎖被占用的時間很短,自旋等待的效果就會非常好。反之,如果鎖被占用的時間很長,那么自旋的線程只會白浪費處理器資源。。。。所以為了通用,Java 搞一個縫合方案:如果自旋超過了限定次數(默認是10次)沒有成功獲得鎖,就應當掛起線程(進入睡眠鎖)。
自旋鎖做的讀寫鎖 (什么是讀者寫者模型?)
然后我們講到鎖的應用部分, 主要在 kernel 里面有一個常見的情況, 就是很多 read 和部分 write 的情況, 這個也很好想明白, 單是內核那一堆內存里的 buf、cache 就夠多這種應用場景了. 我們知道有 reader-writer lock, 他主要是基於 spinlock 的, 當然, 這是由於kernel 里面對內存上的 shared data structure 讀寫上並不需要太多的時間, 所以 spinlock 來序列化訪問是解決並發沖突的一個好用的方法了, 我們知道睡眠鎖也是有循環的(醒來后鎖又不可用了)不過他適合那種長時間阻塞的共享資源,我們要實現讀者寫者模型里面讀者和讀者是沒有沖突的,這樣 sleeplock 也會退化為 spinlock(就一個 CAS 而已),所以理論上讀寫之間其實也能做成 sleeplock,讀讀之間則沿用 spin 就行了。讀寫鎖是高效的的與普通鎖相比. 讀寫鎖有三種狀態:讀模式下加速(共享)、寫模式下加鎖(獨占)以及不加鎖。kernel 的 rwlock 只是封裝了這個狀態限定的 spinlock, 具體實現是用一個字段而已, 很簡單. 我們需要分析的是他的缺點從而引入今天學習的 RCU.
給出一種 rwlock 的偽代碼:
我們可以分析問題, 這是想回想一個 multicore CPU 的實現, 首先可以考慮讀寫並發時必須串行化操作,這一點就是 spinlock 的特色,然后由於多讀者時不用串行化,這就讓讀寫鎖的性能提升的,唯一的串行化僅發生在 CAS 處。
讀寫鎖讀者性能剖析 (為什么還要改進?)
那么我們為什么要做 RCU 呢?這是因為實際上的讀寫鎖性能並沒有那么好!! 我們必須 dive deep 看,考慮最壞情況 4 個線程都同時請求讀鎖的時候,會發生什么。下圖為搞笑示意圖。
某個時刻,4個線程同時請求執行 CAS 指令(Compare And Swap 本身就是為了保證對信號量的修改必須是串行的具備一致性的),假如此時 Core 1 CAS 成功,則其他的 cores 都會失敗並回到藍色地方重新讀取信號量的值。所以理論上我們這里 T(n) = O(n方),因為每次同時 CAS 后剩下的 cores 都要回去重新讀一遍。
這有什么?而且對於並發錯開時間的 CAS 不應該就成功了嗎?不過是區區讀變量?然而我們必須意識到這里的信號量是共享的,意味着其由於 CAS 強制串行更新信號量的時候,CPU 訪存無法享受光速寄存器以及極速的 L1 Cache 待遇,當 Core 1 修改了他時,他就標記為臟而進入 Cache Coherence 的 MESI 協議流程去了,而此流程將消耗許多 CPU cycles,因此這個 O(n方) 是切實的消耗,其實對於 rw 並發也是如此。
可以體會到, 造成多核讀寫鎖 heavy overhead 的一個重要原因就是哪個 x 的讀入, 即 O/S 課線程同步類題目的 semaphore 讀寫那個共享變量導致的循環 CAS 判斷開銷. 當然這個理論上最好情況的 O(n方) 已經比原來我們分析 spinlock 里前置串行所有讀的 busy-wait-loop 純粹的多次浪費的 CAS 好很多了….
RCU 功能特色 (解決什么問題?)
所以我們需要搞一些東西來提高性能. 這個叫做 RCU, read + copy update, 他的鎖設計必須是和 data structure 一起合作的. 我們先做思想實驗,上面我們的問題在於,對於信號量本身是一個共享變量去控制對一個共享數據結構的訪問本身必須串行化一部分操作,並且引發了平方的訪問和 Cache 同步問題。如果我們能改掉這個問題,那么不僅多讀性能提高,是不是我們順便也可以解決讀寫的問題,甚至支援讀寫並發!回想一開始我們舉例的鏈表,很容易就能想到 copy on write的思路。不過 RCU 沒有這么暴力的復制一個結構,而是把這個思路運用到局部中。
根據 paper, RCU 解決的問題是三個:
- 支援 concurrent reading 和 updating, 沒有之前 rwlock 的 write 時候強 mutex.
- deterministic complexity 出於對某些軟件工程上的需求. (我猜想可能是一些實時應用)
- performance on both computation and storage. 就是小 complexity 咯.
RCU 實現——以 rculist 鏈表 為例 (怎么做 RCU ?)
下面給出論文的 RCU 偽代碼原語.
RCU 實現關鍵點1 結構性更新
update data not in place, 鏈表例子, 我們不修改內容,而修改結構,如圖這樣分三步並且保留原有結構體的部分就能保證 reader 讀到的數據都是完整的, 沒有 mixture 狀態. 我們對鏈表操作的時候,先創建 copy,然后讓對 copy 的訪問和對原件的訪問具有數據結構上的一致性,最后再更新。我們必須理解(其實這個思路在文件系統的 log 實現斷電一致性上類似)不同線程(或者進程)對於這個鏈表的訪問必定是要么在步驟 3 完成前,要么在步驟三完成后,具體來說,就算到了最底層的 RISC 指令上,訪問 l -> next 的時候,next 要么是已經被修改指向 copy 了,要么是指向原來的 E2,不會出現野指針的情況。
這個關鍵點導致 RCU 對數據結構有要求, 比如雙向鏈表就用不了 RCU 了. 對樹也不錯. 這個 head 指針的概念在版本控制軟件里也很常見.
RCU 實現關鍵點2 內存屏障
內存屏障, 我們必須保證上述三部的順序問題. 這一點很重要. 具體的 barrier 放在哪里我也不是很清楚(視頻這里本來講的有點模糊, prof Robert 沒有解答為什么要用 barrier 的一個學生問題,也許是鏈表這個例子不會出現編譯器重排也能跑的優化或者投機執行后錯誤,我具體網絡學習后發現這里其實是涉及到多核的 Cache 問題). 我們了解底層是 speculative execution ,out-of-order 的,編譯器重排指令很好禁止。對於 CPU 的話內存屏障則涉及 Cache coherence,這里其實是硬件的妥協,我們知道多核一個 CPU 對變量 Cache 出現讀寫修改后,就告知其他 Core read Invalidate 不可讀的信號請求清空該 Cache 塊,然后本 Core Cache 寫內存,一旦其他 CPU 再讀,就能讀到新的值了。
然而 CPU 執行運算是光速的,而處理指令和進行數據傳送則慢一點,所以又要搞一個 Store buffer 異步等待等具體我們不用深究,請看下一句話,那么問題就在於,如果有這種情況:無效信號已經確認了大家都沒有涉及更新的 Cache line 了,此時本 Core 要進行寫存更新內存,而其他 Core 在寫存瞬間又進行了讀取,又把內存加載進 Cache 了,從而導致非一致性問題。具體結果就是,對於多線程應用,CPU無能為力,應用程序必須手動添加內存屏障。具體內容是涉及 flush store buffer 和 cpu stalling。這里當然涉及性能的比較,Is it worth it?不過答案好像是是。
補充:上次我寫的這些東西基本讀不明白了。有必要詳細回顧一遍,爭取把這個東西一次講清楚。
所以又要搞一個 Store buffer 異步等待
首先這個 load buffer 和 store buffer 本身是在流水線里面的(在 L1 Cache 之上!),他本質是一大堆關聯存儲器的寄存器,CPU 所有的讀寫操作都會先緩存在這里,比如接下來的指令會 load 一堆內存,load buffer 里面會有一堆重命名寄存器來存這些內存以加速指令執行。同樣的,也有一大堆的 store 指令,而且還有一個問題是他們還沒有確定會 retire,所以更加不能寫了。
在外面保證把內存寫回去之前,其他的 CPU 肯定有可能執行到訪問這些內存的地方,這些檢查點由於亂序了,導致變化根本沒有反映到內存上,所以這種 volatile 是沒有 volatile 的屬於。
無效信號已經確認了大家都沒有涉及更新的 Cache line 了,此時本 Core 要進行寫存更新內存,而其他 Core 在寫存瞬間又進行了讀取,又把內存加載進 Cache 了,從而導致非一致性問題。
要理解我寫的這句話,首先要明白 MESI 做了什么,他負責的是 Cache line 的一致性,一旦 Cache 要 dirty 了馬上引發 invalidate 操作,造成其他線程對某些內存的不可讀。
然而多線程運行的時候,這些指令沒有 retire,所以不可能 fall 到下面的 Cache 上!如果你需要實時的結果,比如這里 RCU 需要保證其他線程能馬上訪問到你連上了新的,必須手動沖刷這些指令,逼迫他們馬上 retire,不然 retire 的時候指令們可能就會不按原來的 1分配,2重復指向,3重連的指令順序發射。
為什么需要在編譯層面也禁止指令重排?這一點則是因為指令的 retire 與否可能會與其他指令的結果有關,如果指令重排了,僅僅在 checkpoint 時引發一次強制沖刷,並不會保證能馬上引發一些指令的 retire。
RCU 實現關鍵點3 Commit 提交同步點
一些讀寫的規則保證能搶占 commit :1. 讀者不能在讀 RCU 的時候進行 context switch (推動他完成本次讀,而不是沒輕沒重地去干別的事情)。2. 我們需要提交 update 當且僅當所有的 read 結束, 搶占進來, free 掉所有的正在被 read 的過氣的歷史數據結構的某個部分, 當然阻塞 updater 也不對, 所以有 callback 版本的 call_rcu .
具體怎么樣如判斷 read 結束呢? 論文里面討論的最簡單的一種方法是通過調整線程調度器,使得寫入線程簡短的在操作系統的每個CPU核上都運行一下,這個過程中每個CPU核必然完成了一次 context switch。上面的原語中的 run_on 意思就是等一個 context switch 周期. 因為數據讀取者不能在context switch的時候持有數據的引用,所以經過這個過程,數據寫入者可以確保沒有數據讀取者還在持有數據。這部分內容就和 GC 很像了, 由於 GC 語言一般具備追蹤數據是否被使用,實際上我們可以在 GC 語言上實現這樣的 free 操作, 但是 kernel 是否適用 GC 之前 Lecture 中我們又談論過了(教授們完全使用 Golang 實現 Unix Kernel 並對 nginx redis 評估). 當然我們也有 asynchronous 的方案,通過 call_rcu 托管數據結構的 old piece 給 kernel 或者什么 thread,然后注冊一個 callback freeing function(請記住 RCU 是一種涉及內核態的機制,但他需要自定義數據結構,比如 Linux kernel 可能有個 RCU 鏈表 RCU 數,call_rcu 能統一管理)給 kernel 去完成這個檢測 context switch 的內容。
具體一些課程的詳細內容復習其他也可以參考大佬翻譯的上課課程內容完全稿.
RCU 性能分析 (什么時候用 RCU ?)
RCU 數據讀取非常的快。唯一額外的工作就是在rcu_read_lock和rcu_read_unlock里面設置好不要觸發context switch,並且在 rcu_dereference中設置 memory barrier,這些可能會消耗幾十個CPU cycle,但是相比鎖來說代價要小的多。
對於數據寫入者,性能會糟糕一點。首先鎖的獲取鎖和釋放鎖是一個。還有一個可能非常耗時的synchronize_rcu函數調用。實際上在synchronize_rcu內部會出讓CPU,所以代碼在這不會通過消耗CPU來實現等待,但是它可能會消耗大量時間來等待其他所有的CPU核完成context switch。所以基於數據寫入時的多種原因,和數據讀取時的工作量,數據寫入者需要消耗更多的時間完成操作。如果數據讀取區域很短(注,這樣就可以很快可以恢復context switch),並且數據寫入並沒有很多,那么數據寫入慢一些也沒關系。所以當人們將RCU應用到內核中時,必須要做一些性能測試來確認使用RCU是否能帶來好處,因為這取決於實際的工作負載。Linux 中大量使用了 RCU 數據結構。
總結
略(挖個坑)稍后學 Linux 內核源碼分析的時候再補全 Linux kernel 中完整的 RCU 數據結構分析。







