Concurrency in The Linux Kernel === ###### tags: `twlkh`, `concurrency`, `lock`, `barrier` kernel 本身就是個 multithread concurrent 的系統。在沒有適當保護下,存取共享資源很容易發生 race condition。共享資源包含周邊 IO 及共用的變數資料結構等。可透過適當的 synchronization 機制提供 critical section 來保護資源的存取。 個人認為 kernel 裡面與 concurrency 相關的機制大致分為以下幾類, 1. atomic operation API a. atomic counter b. atomic bitmask 2. busy-waiting lock:CPU loop 等待直到取得 lock。critical section 中不允許睡眠。通常用來在 SMP 下保護資料數據結構。有以下幾種, a. spinlock b. reader/writer spinlock c. seqlock 3. lock-free synchronization: 透過區分 reader 與 writer 的角色以及deferred destruction 來減少 lock 的使用。 a. rcu b. kfifo c. percpu data 4. blocking synchronization:如果沒有拿到 lock 會進入睡眠。通常用來保護較大的 critical section 與 存取 IO 相關的機制。可分為以下幾種, a. semaphore b. reader/writer semaphore c. mutex d. completion 實現這些機制需要一些來自 CPU 提供的硬體支援 — memory barrier 與 atomic instruction。以下將由此開始介紹。 ## Architecture Support: Barrier 現代 CPU 普遍引入了多層 cache 與 store buffer 以降低 CPU pipeline 由於等待 IO 延遲造成的效能減損;另外,Compiler 與 CPU 必要時可能重新安排執行程式的流程與順序來達到效能的提升。程式由單核心擴展到多核心架構時,CPU 之間會看到彼此對於某些共享資料的存取順序與程式描述可能不同。從 [1] 中我們可以大略了解幾個軟硬體最佳化的技術及其可能造成的資料存取亂序: * **Compiler 最佳化** * Compiler 可依據 CPU 的 instruction issue 數目、執行的 latency cycles 以及程式流程,在不影響程式上下文執行結果下重排或簡化程式。 * **硬體設計最佳化** * **Multiple issue of instructions**:一個 cycle 可以執行多條指令 * **Out-of-order execution**:如果某一條指令 stall 等待之前的結果時,CPU可以先執行下一條沒有相依性的指令。 * **Speculation**:當 CPU 遇到一些條件式判斷的指令時,在判斷出結果前可以先預測性的執行可能的 path 以求得 performance gain。 * **Speculative loads**:在 cacheable region,load instruction 真正被執行前就猜測性的先把資料讀進 cache。 * **Load and store optimizations**:讀寫外部記憶體會花上很長的時間且可能 stall pipeline 等待結果,CPU 可藉由減少傳輸次數提高 performance,例如合併數筆相鄰位址的 store 成一筆。 * **External memory systems**:在許多現今複雜的 SOC 系統中,有許多不同的 master、slave、與以及之間不同的 routing。有些 device 同時接受來自不同 master 的資料傳輸請求。這些傳輸的 transaction 有可能在些 interconnection 之間被 buffer 或是 reorder。 基於以上原因,程式的對記憶體的存取順序與執行流程可能在編譯時被重排或者修改,CPU 執行結果的出現順序又可能與 assembly 看到的不同, Compiler 跟 CPU 只會保證執行上下文結果是正確的。這在 SMP 環境下,其他 CPU 或是 IO 因為上述因素有可能得到非預期的執行結果。實際的情形會依據 architecture 的實現而有所不同,kernel 設計上使用 `abstract memory model` 以達到對於所有 architecture 的相容性。一個簡單的例子[3] 在 kernel 裡的 memory model 下,下列的執行程序 ``` CPU 1 CPU 2 ================== =============== { A == 1; B == 2 } A = 3; x = B; B = 4; y = A; ``` 預期最後被執行的結果有可能是下列其中一種 ``` STORE A=3, STORE B=4, y=LOAD A->3, x=LOAD B->4 STORE A=3, STORE B=4, x=LOAD B->4, y=LOAD A->3 STORE A=3, y=LOAD A->3, STORE B=4, x=LOAD B->4 STORE A=3, y=LOAD A->3, x=LOAD B->2, STORE B=4 STORE A=3, x=LOAD B->2, STORE B=4, y=LOAD A->3 STORE A=3, x=LOAD B->2, y=LOAD A->3, STORE B=4 STORE B=4, STORE A=3, y=LOAD A->3, x=LOAD B->4 STORE B=4, ... ``` 我們在某些 CPU/CPU 或是 CPU/IO 之間需要溝通的地方[2],需要確保 Compiler 與 CPU 能維持與程式一致的存取順序,memory barrier 提供這樣的功能。 **Compiler support** * volatile keyword:volatile 是一個 type qualifier。它聲明所修飾的變數的值有可能被 memory-mapped IO 或者是 asynchronously interrupting function 修改,這個關鍵字告訴 compiler 不要針對此變數的存取做最佳化。你可以對變數設定 volatile,但是他對於所有存取這個變數的地方都會造成效果。這造成效能的減損。Linux kernel 裡面提供 `ACCESS_ONCE()` macro [3] 在使用上進一步最佳化,只在有需要的地方才套用 volatile 這個關鍵字,保留給 programmer 更多彈性。以下是 [linux-3.19 前的實現版本](http://lxr.free-electrons.com/source/tools/virtio/linux/uaccess.h?v=3.19#L5), ```c #define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x)) ``` * 基本上就是在有需要阻止最佳化的地方透過轉換型別來增加 `volatile` 修飾。如下例 * ```c static int rcu_gp_in_progress(struct rcu_state *rsp) { return (ACCESS_ONCE(rsp->completed) != ACCESS_ONCE(rsp->gpnum)); } ``` 表示我們希望讀取 `rsp->gpnum` 與 讀取 `rsp->completed` 的動作不要被最佳化。 **Compiler Barrier** 下面的程式: ```c int A, B; void foo() { A = B + 1; B = 0; } ``` 有可能在加了 `-O2` 後被 compiler 翻譯成: ```c B = 0; A = B + 1; ``` 即一般 C 程式並不保證記憶體讀寫會照程式順序,因為 compiler 翻譯過程中可以調動。 GCC 使用下列的 inline assembly 語法來表示 compiler barrier ```c asm volatile("" ::: "memory"); ``` 這代表一個 compiler code motion 障壁,用來限制 compiler 對記憶體操作的調動。 如將上面程式改寫加入 compiler barrier,可確保產出的組合語言符合預期順序: ```c A = B + 1; asm volatile("" ::: "memory"); B = 0; ``` Linux Kernel 中的 ```barrier()``` 巨集就是這種限制 compiler 對讀寫做調動的 compiler barrier。 compiler barrier 的語意跟 C11 中的 ```atomic_signal_fence()``` 非常類似。 使用情境:例如有個 interrupt handler 確定與 process context 程式跑在同 CPU 上。有個叫 ```data_ready``` 的變數,在 interrupt 外面寫,裡面讀: ```c void interrupt_handler(void) { int t; t = data_ready; barrier(); if (t) do_something(data); } void process_context_code(void) { data = x; barrier(); data_ready = 1; } ``` 在寫之前、讀之後要加上 barrier()。 Linux kernel 在`include/linux/compiler-gcc.h` 中定義 compiler barrier macro `barrier()` : ```c #define barrier() __asm__ __volatile__("": : :"memory") ``` compiler barrier 本身是一個 sequence point[2],詳見 [4]。 **CPU barrier** 每個 CPU architecture 根據各自的 memory model,通常會提供自己的 barrier instruction,以達到不同程度的讀寫順序的保證。如 ARM 的 `dmb` 等,有的有不同程度與作用範圍的 barrier,以達到更細度的控制。 在 Linux 中,這些指令在 arch 下被包成通用的介面,分類介紹如下, * **mb()/rmb()/wmb()**:`rmb()` 確保 barrier 之前的 read operation 都能在 barrier 之後的 read operation 之前發生,簡單來說就是確保 barrier 前後的 read operation 的順序;`wmb()` 如同 `rmb()` 但是只針對 write operation。`mb()` 則是針對所有的 memory access。 * **smp_mb()/smp_rmb()/smp_wmb()**:在 SMP 的系統被定義成 `mb()/rmb()/wmb()` ,UP 時就只是 compiler barrier。可特別用在只於 SMP 時才需要 barrier 的地方[8]。 * **dma_rmb/dma_wmb()**:如果 architecture 對於 barrier 作用的範圍有提供更 fine-grained 的控制,在 device driver 中需要同步 CPU 與 IO 中的 memory data 時我們就不需要使用作用達到整個系統的 barrier。如同 [9] 中 ARM 的例子, ``` barrier Call Explanation --------- -------- ---------------------------------- rmb() dsb() Data synchronization barrier - system dma_rmb() dmb(osh) data memory barrier - outer sharable smp_rmb() dmb(ish) data memory barrier - inner sharable ``` * **smp_load_acquire()/smp_store_release()**:這部分是單向的 barrier。 ACQUIRE 確保之後所有的 memory operation 都只在 ACQUIRE 之後出現;RELEASE 則是確保之前所有的 memory operation 都在 RELEASE 之前出現。通常這兩個都是成對出現。透過這兩個 macro,我們可以確保之前在 critical section 之內的變數存取都會在這次的 critical section 前完成! * **read_barrier_depends()/smp_read_barrier_depends()**:只有在 barrier 上下的資料存取有相依性時才有作用,這樣我們就可以避免使用 `rmb()` 達到更輕量的控制。但是這只有 ALPHA CPU 才有支援,其他的 architecture 都是定義成空巨集。 * **smp_mb__before_atomic()/smp_mb__after_atomic()**:在某些沒有 return value 的 atomic operation 中有些沒有使用 memory barrier。這兩個 macro 讓我們在這些操作前後確保資料一致性。 Barrier 在需要時幫助我們達到記憶體存取的順序的準確與可預測性。但是相對上它也阻止某些對於程式效能的最佳化。它在多核心的溝通與 kernel concurrency 機制的實現上是個不可或缺的角色! ## Architecture Support: Atomic Instruction **CPU instruction support** 我們透過變數來維護程式的狀態及流程,SMP 下同時多個 core 共同存取 share data 時,程式設計上除了重入性以及可能被亂序執行的考量外,另一個就是存取時需確保操作是 atomic:表示說我們對於某個共享記憶體的操作,相對於系統的其他部分來說是完整完成不可中斷的 [10][11] 。包含 load/store atomic 與 Read-Modify-Write(RMW) atomic。我們可以從 [11] 的例子對於 64-bit share memory 的處理很明顯的了解這個概念,以及可能發生的問題(race condition)。 在 single-core 下只要關閉 global interrupt,防止 CPU 被 preempt,就可以確保 atomic,但是在 multicore 時就需要有特殊的硬體資源(且關閉 global interrupt 會影響 response time)。這當中很大一部分會需要確保 RMW operation 是 atomic,特別是在 lock 與 counter 的實作上面。 通常 CPU 會提供有別於一般 load/store memory 的特殊指令來達成 RMW atomic。介紹其中兩類, * **compare-and-swap (CAS) / swap (SWP)** [12]:CAS 寫入時同時提供舊值(之前讀出來的值),如果舊值和記憶體裡的相同,就將新值寫入,確保 RMW 的動作完整性,如 x86 的 CMPXCHG 指令。SWP 在寫入時讀回記憶體的值,可用於與之前讀出的值比對。如 ARMv6 之前的 SWP。 * **load-link/store-conditional (LL/SC)** [13]:這類的指令是成對使用。在 LL 讀取記憶體的值,同時標記進入 exclusive 狀態,SC 寫入時清除 exclusive 狀態。在 exclusive 狀態時如有其他 core 較早執行寫入,導致 exclusive 狀態被清除,則忽略這次的寫入並返回失敗。程式可在下一行發現失敗時重新執行 LL/SC 的動作直到成功為止。POWERPC、MIPS、ARM (ARMv6 以上)等提供這類的指令。 其他還有如 `test-and-set` [14] 等,不在此作介紹。 ARM 早期 CPU 使用的 `SWP` 指令在 multicore 的系統中有一些效能瓶頸[15],所以 ARMv6 以上換成 LL/SC 的指令,也就是 `ldrex/strex`。x86 本身支援記憶體存取的指令可加上 `LOCK` prefix [16] 轉成 atomic 的存取。 透過 atomic instruction 我們可以實現一些基本的 counter 操作與簡單的 SMP lock,如 spinlock、rwlock 等,確保程式狀態維護的正確性。然而當競爭相同 lock 的 CPU 變多時,卻可能會造成 scalability 不佳的問題。 ## Kernel Atomic Operation API [17] Linux kernel 為整數資料提供兩種簡單的 atomic 操作,一種是 atomic counter,另一是 atomic bitmask operation。 **atomic counter** 原則上 atomic counter 實現我們只需要一個 `int` 或是 `long` 的變數,透過 atomic instruction 來包裝成 RMW 的 API 即可。Kernel 中是透過 `atomic_t` 來宣告一個 atomic counter,它的定義如下, ```c typedef struct { int counter; } atomic_t; ``` 封裝在 data structure 中除了有較佳的擴展性外,可避免使用者沒有使用 atmoic API [18]直接透過如下列的程式碼存取,因而被轉譯成一般的 load/store 而造成問題。 ```clike atomic_t my_atom; my_atom ++; ``` atomic counter API 提供下列幾種操作(`atomic_{read, set}()` 表示 `atomic_read()` 與 `atomic_set()` 兩組 API,其他雷同), * **ATOMIC_INIT()**:變數初始化。 * **atomic_{read, set}()**:讀或寫通常可由單個 load/store 指令完成,所以通常只是 C 的實現版本 ```c #define atomic_read(v) ACCESS_ONCE((v)->counter) #define atomic_set(v,i) (((v)->counter) = (i)) ``` * **atomic_{add, sub, inc, dec}()**:針對 counter 做加減動作, `atomic_{inc, dec}()`為加/減 1 的版本。函數實現內部不含 memory barrier。需要確保 counter 與其他變數存取順序時可自行在前後加上 `smp_mb__{before/after}_atomic()` 。 * **atomic_{add, sub, inc, dec}_return()**:加減 counter 值並回傳新值。函數實現內部在 atomic operation 前後有包含 memory barrier。 * **atomic_{inc, dec, sub}_and_test()/atomic_add_negtive()**:`atomic_xxx_and_test()` 加減 counter 值並返回布林值檢查 counter 是否為 0。`atomic_add_negtive()` 則是檢查新的 counter 是否為負數。函數內含 barrier。 * **atomic_xchg()/atomic_cmpxchg()**:`atomic_xchg()` 將 counter 值與參數交換並返回舊值。`atomic_cmpxchg()` 將參數給的舊值與 counter 比對是否相同,相同的話才寫入新值,返回原本 counter 中的舊值。函數內含 barrier。 * **atomic_add_unless()/atomic_inc_not_zero()**:條件判斷是否與某個參數的值相同來決定是否加 counter 值。函數內含 barrier。 另外有 `atomic_long_t` 版本的型態,在此不另行介紹。 **atomic bitmask operation** bitmask 大致上與 atomic counter 雷同,只是操作的動作從加減變成針對 bit 的邏輯運算。bitmask 操作的變數是 `unsigned long` 的整數型態。大致分類如下: * **{set, clear, change}_bit()**:如同 API 的名稱一樣,是用來改變 `unsigned long` 整數中某個 bit 的函數。所有的操作是 atomic 但是不含 memory barrier。 * **test_and_{set, clear, change}_bit()**:與上幾個函數類似,但是會返回 boolean 值表示修改前的 bit 值是否為 1。實作內含 memory barrier。 * **test_and_set_bit_lock()/clear_bit_unlock()**:atomic bitops 也可拿來實作 bit lock。之前使用上都是透過 `test_and_set_bit()` 與 `smp_mb__before_clear_bit(); clear_bit();` 來處理 lock/unlock 的動作,[19] 的 commit 將這兩個操作抽出來當作 bit lock 的 API。由 commit 可看到有用到的地方大致是 page lock、buffer lock、bit_spin_lock、tasklet locks。 * 最後,上述實作有 non-atomic 的版本(加上 `__` prefix 的版本,如 `__set_bit()` )。這些 API 是在在適當 lock 保護的區域內使用。 ## Busy-waiting Lock:spinlock spinlock 基本作法就是 busy waiting loop,一直等到指定的 lock 被釋放之後,取得 lock 才繼續進行 critical section 的操作。由於深度依賴 atomic instruction 的支援,primitive 真正的實現都是在每個 architecture 中(arch/ARCH/include/asm/spinlock.h),原始的實作有如 [20] 中的 pseudo code, ```clike typedef int SpinLock; void InitLock(SpinLock *L) { *L = 0; } void Lock(SpinLock *L) { while (TestAndSet(L)) ; } void UnLock(SpinLock *L) { *L = 0; } ``` 相較於 atomic counter, 透過`spin_lock()/spin_unlock()` 我們提供一個可對更複雜的資料結構操作的 atomic critical section。由於他小巧的實現,spinlock 可說是 SMP 實現的基礎建設,其他的機制如 semaphore 等都會需要它來維持共享資料與狀態的維護。 在 kernel 中情形稍微複雜一點,在 SMP 下資料除了可能在 thread context 間共享外,還有可能在 ISR 或是 bottom half(softirq 與 tasklet)的 context 間被存取。根據共享資料在不同 context 間被存取的可能性 [20],spinlock 提供下列幾類 API,各有不同的使用時機, * **spin_lock()/spin_unlock()/spin_trylock()**:資料只在 user context 間 共享時使用。 * **spin_lock_bh()/spin_unlock_bh()**:資料有可能在 user context 與 softirq/tasklet 間共享,primitive 會一併 disable softirq。 * **spin_lock_irqsave()/spin_lock_irqrestore()**:資料有可能在 user context 或 softirq/tasklet 與 ISR 間共享時使用,primitive 內會 disable IRQ。 原本 spinlock 的 algorithm 就像是演講 Q&A 時點人一樣,先舉手先發問且可重複舉手。先搶先贏的方式有時會不公平,然而不公平造成的更大的問題的是當 lock 競爭激烈時所造成的效能減損,在 [22] 中提到 * On an 8 core (2 socket) Opteron, spinlock unfairness is extremely noticable, with a userspace test having a difference of up to 2x runtime per thread, and some threads are starved or "unfairly" granted the lock up to 1 000 000 (!) times. 所以 Nick Piggin 在 2.6.25 中將 x86 實現的 algorithm 改為 ticket spinlock,現今被許多 architecture 採用。另外為了防止存取亂序最佳化造成 critical section 裡資料存取的動作被排到取得 lock 前或是 釋放 lock 後,在 lock 的實現上通常會包含 barrier [23]。但實際上不管是使用 lock 或是 barrier 對於效能及 scalability 都會造成影響。 ticket spinlock 將取得與釋放 lock 的過程分成取得 `next` 及 `owner` 兩個步驟。整個機制舉例來說就像是你去銀行辦事,假設只有一位行員(針對某個記憶體位址),要辦理業務前(存取記憶體),要先抽號碼牌(取得屬於自己的`owner` 值,並把下一個 `owner` 加 1 ),之後所有等待的人只要盯著叫號機(`next`)的數字是否是自己的號碼(`owner`)來決定下個辦理的人是否是自己。當前一個人的事情辦完後,行員把叫號機號碼加 1(unlock 時會把`next` 加 1 )也許會有鈴聲提醒號碼更新(ARM 會於此時執行 sev )。這時候所有人都會抬頭(wakeup)看看新號碼是否跟自己的符合 ( local `owner` == `next`?)。等待的時候沒事的人可能會打個小瞌睡( ARM 架構會於此時執行 wfe 以節省 power)。 **Lock debugging** 使用 lock 最常遇到的問題就是 deadlock,linux kernel 裡提供 `lockdep` 的機制,可動態檢查 lock 被使用的時機與 primitive 是否正確,在有可能造成問題的 case 顯示 warning 並且列出相關的 call stack 以供檢查。詳細的資訊可參考 Linux Kernel Hacking[24] 一書。有需要時需從 menuconfig 中 kernel hacking 裡開啟相關選項。 ## Busy-waiting Lock: reader-writer spinlock reader-writer spinlock (簡寫為 rwlock)為 spinlock 的一種變體。如果對於共享資料的存取可區分 reader 與 writer 的角色,且讀多寫少的情形下可使用。rwlock 允許多個 reader 同時進入 critical section,但是 reader 與 writer 之間,以及不同的 writer 之間則是互斥。如下圖所示, ![](https://i.imgur.com/aOPhib1.png) 如同圖中標記的幾個特點, 1. shared reader lock:reader 之間允許同時多個 reader 取得 lock。 2. Favor reader over writer:也就是一旦有其中一個 reader 取得 lock,writer 必須等到沒有任何 reader 在 critical section 時才會拿到 lock。另外 writer 與 writer 互斥。 3. mutual exclusive between reader and writer:如果有 writer 在 critical section,新的 reader 必須等到 writer unlock 才能取得 lock。 Primitive 分為 reader lock/unlock 與 writer lock/unlock 的組合。另外與 spinlock 類似,依據共享資料被存取的 context 有單純取得 lock 與一併禁止 IRQ 或是 BH 的不同版本。 ARM 在實現上將 rwlock 視為一個 counter,0 表示沒有任何人取得 lock,正數表示取得 reader lock,負數表示取得 writer lock。 當 lock 大於或等於 0 時可取得 read lock 並加 1,釋放時將 lock 減 1。 取得/釋放 writer lock 則是單純將 sign bit 設立/清除。 rwlock 避免 reader 與 reader 仍必須互鎖的情形,但是 writer 多或是更新頻繁時一樣會造成效能下降。 ## Reference 1. [Chapter 13 Memory ordering, ARM Cortex-A Series Programmer’s Guide for ARMv8-A](http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.den0024a/index.html) 2. [Sequence point](https://en.wikipedia.org/wiki/Sequence_point) 3. [LINUX KERNEL MEMORY BARRIERS](https://www.kernel.org/doc/Documentation/memory-barriers.txt) 4. [Memory Ordering at Compile Time](http://preshing.com/20120625/memory-ordering-at-compile-time/) 5. [C語言: 認識關鍵字volatile](http://newscienceview.blogspot.com/2013/09/c-volatile.html) 6. [How To Add a Sequence Point?](https://gcc.gnu.org/ml/gcc/2013-02/msg00032.html) 7. [Memory ordering](https://en.wikipedia.org/wiki/Memory_ordering) 8. [Ordering and Barriers, Linux kernel development](http://www.makelinux.net/books/lkd2/ch09lev1sec10) 9. [[PATCH v6 2/5] arch: Add lightweight memory barriers dma_rmb() and dma_wmb()](http://permalink.gmane.org/gmane.linux.kernel/1837779) 10. [wiki: atmoic operation](https://en.wikipedia.org/wiki/Linearizability) 11. [Atomic vs. Non-Atomic Operations](http://preshing.com/20130618/atomic-vs-non-atomic-operations/) 12. [wiki: compare and swap](https://en.wikipedia.org/wiki/Compare-and-swap) 13. [wiki: Load-link/store-conditional](https://en.wikipedia.org/wiki/Load-link/store-conditional) 14. [wiki: test and set](https://en.wikipedia.org/wiki/Test-and-set) 15. A.1.2 Limitations of SWP and SWPB in [ARM® Synchronization Primitives](http://infocenter.arm.com/help/topic/com.arm.doc.dht0008a/DHT0008A_arm_synchronization_primitives.pdf) 16. [stackoverflow: Illegal instruction in ASM: lock cmpxchg dest, src](http://stackoverflow.com/a/1746603) 17. [Semantics and Behavior of Atomic and Bitmask Operations](https://www.kernel.org/doc/Documentation/atomic_ops.txt) 18. kernelnewbie maillist: [Re: atomic_t](http://www.spinics.net/lists/newbies/msg29573.html) 19. [lock bitops](http://lwn.net/Articles/233390/) 20. CIS 4307: [SpinLocks](http://www.cis.temple.edu/~giorgio/cis307/readings/spinsem.html#4) 21. [Unreliable Guide To Locking](http://kernelbook.sourceforge.net/kernel-locking.pdf) 22. [Ticket spinlocks](https://lwn.net/Articles/267968/) 23. [volatile considered harmful](https://www.kernel.org/doc/Documentation/volatile-considered-harmful.txt) 24. [Linux Kernel Hacks:改善效能、提昇開發效率及節能的技巧與工具](http://www.books.com.tw/products/0010624519) 25. [spinlocks](http://124.16.139.131:24080/lxr/source/Documentation/spinlocks.txt?v=linux-3.5.4) 26. [Reader-Writer Spin Locks](http://www.makelinux.net/books/lkd2/ch09lev1sec3) 27. [Yet another introduction to linux RCU](http://www.slideshare.net/vh21/yet-another-introduction-of-linux-rcu)