讀書筆記(1)—— kernel 屏障問題
核心中的同步和併發
其實併發和同步是作業系統設計中的一個核心問題,隨著CPU的發展,多核多執行緒已經成為了主流,因此併發和同步是作業系統和核心開發者必須面臨的問題。
其實併發包括兩類:
一類是真正的同時發生, 在巨集觀尺度和微觀尺度上都是並行執行的,這是多核CPU多級流水的情況;
另一類是時分複用導致的交錯發生,並不是真正的併發。在巨集觀尺度上是並行 但在微觀尺度上是序列,這是單核處理器,只有一個流水線在執行。
不管是哪類併發,都存在一個共享資源(臨界區)保護的問題。臨界區保護通常採用的手段就是序列化,其中最常見的序列化手段就是鎖(但鎖並不是唯一的保護共享資源的手 段)。鎖分很多種,但是各自有不同的特徵和適宜場景,而且有些鎖並不絕對禁止併發(比如讀寫 自旋鎖、讀寫訊號量等等)。除了鎖以外,其他各種型別的原語也被用於對共享資源的保護(也就是同一時刻資源只能被一個物件所訪問)。在實踐開發中,開發者經常碰到併發與同步相關的問題, 如各種操作原語的特徵、區別、侷限性以及使用上的各種權衡。這些問題往往比較微妙而且難以徹底理解,一旦不正確地使用操作原語,往往會導致各種奇奇怪怪並且難以除錯的BUG。下面簡單介紹幾種同步和原子操作,是在核心中經常被使用的,說的不正確的請批評指點。
屏障問題
編譯器在優化過程中會對指令重新排序,高效能處理器在執行指令的時候也會引入亂序執行。這些行為理論上都是不應該改變原始碼邏輯行為的,但實際上編譯器和處理器只能保證顯式的控制依賴和資料依賴沒有問題。而原始碼中往往存在各種隱式的依賴,這些依賴性如果被破壞就會產生邏輯錯誤。
舉個簡單的生產者-消費者案例:
生產者和消費者是兩個程序,執行在不同的 CPU 上, 它們通過一個緩衝區 buffer 交換共享資料,另有一個變數 flag 來標識緩衝區是否準備好。 原始碼順序如下:
int flag = 0;
char buffer[32] = {0};
void producer(void)
{
memset (buffer, 1, 32);
flag = 1;
}
void consumer(void)
{
char data[32] = {0};
while (!flag) ;
memcopy(&data,buffer,sizeof(buff);
}
按原始碼的字面順序,comsumer()程序應該得到 data 中的各個元素都為1的結果,實際上卻未必。因為:第一, 在producer()中 buffer 和 flag 本身沒有任何顯式的控制依賴和資料依賴,因此 flag 的 賦值可能先於 memset()執行;第二,comsumer()中 buffer 和 flag 本身也沒有任何顯式的 控制依賴和資料依賴,因此 data 的賦值可能先於 while()迴圈執行。
因此,我們必須人工加上屏障來保證順序,以防止指令重排和亂序執行對程式產生的影響。
防止編譯器對指令重排的屏障叫優化屏障,防止處理器亂序執行的屏障叫記憶體屏障。
(1)優化屏障
優化屏障定義如下:
#define barrier() __asm__ __volatile__("": : :"memory")
優化屏障 barrier()是一個__asm__內嵌彙編語句,並不產生任何額外的指令。但是內嵌彙編中的__volatile__關鍵字可以禁止__asm__語句與其他指令重新組合;而memory 關鍵字強制讓編譯器假定__asm__語句修改了記憶體單元,讓本語句前後的訪存操作生成真實的訪存指令而不會通過暫存器來進行優化。
優化屏障可以防止編譯器對前後的訪存指令重新排序,但並不能防止處理器的亂序執行。
這裡拿比較經典的問題,生產者和消費者問題為例:在生產者-消費者中,如果處理器是順序執行的,那麼插入優化屏障即可保證程式的執行邏輯正確,程式碼如下:
int flag = 0;
char buffer[32] = {0};
void producer(void)
{
memset(buffer, 1, 32);
barrier();
flag = 1;
}
void consumer(void)
{
char data[32] = {0};
while (!flag) ;
barrier();
memcopy(&data,buffer,sizeof(buff);
}
為了防止單個變數讀寫的編譯器優化,barrier()還有三個變種:
READ_ONCE()、WRITE_ONCE()和 ACCESS_ONCE(),使用方法如下:
a = READ_ONCE(x):功能上等同於 a = x,但保證對 x 生成真實的讀指令而不被優化。
程式碼實現如下:
#define READ_ONCE(x) __READ_ONCE(x, 1)
#define __READ_ONCE(x, check)___________\
({__________________\
__union { typeof(x) __val; char __c[1]; } __u;______\
__if (check)______________\
______read_once_size(&(x), __u.__c, sizeof(x));___\
__else________________\
______read_once_size_nocheck(&(x), __u.__c, sizeof(x));_\
__smp_read_barrier_depends(); /* Enforce dependency ordering from x */ \
____u.__val;______________\
})
void __read_once_size(const volatile void *p, void *res, int size)
{
____READ_ONCE_SIZE;
}
#define __READ_ONCE_SIZE____________\
({__________________\
__switch (size) {_____________\
__case 1: *(__u8 *)res = *(volatile __u8 *)p; break;____\
__case 2: *(__u16 *)res = *(volatile __u16 *)p; break;____\
__case 4: *(__u32 *)res = *(volatile __u32 *)p; break;____\
__case 8: *(__u64 *)res = *(volatile __u64 *)p; break;____\
__default:______________\
____barrier();____________\
______builtin_memcpy((void *)res, (const void *)p, size);_\
____barrier();____________\
__}_______________\
})
WRITE_ONCE(x, b):功能上等同於 x = b,但保證對 x 生成真實的寫指令而不被優化。
程式碼實現如下:
#define WRITE_ONCE(x, val) \
({______________\
__union { typeof(x) __val; char __c[1]; } __u =_\
____{ .__val = (__force typeof(x)) (val) }; \
____write_once_size(&(x), __u.__c, sizeof(x));__\
___})
static __always_inline void __write_once_size(volatile void *p, void *res, int size)
{
__switch (size) {
__case 1: *(volatile __u8 *)p = *(__u8 *)res; break;
__case 2: *(volatile __u16 *)p = *(__u16 *)res; break;
__case 4: *(volatile __u32 *)p = *(__u32 *)res; break;
__case 8: *(volatile __u64 *)p = *(__u64 *)res; break;
__default:
____barrier();
______builtin_memcpy((void *)p, (const void *)res, size);
____barrier();
__}
}
READ_ONCE()和 WRITE_ONCE()是 Linux-3.19 開始才引入的,在那之前只能用 ACCESS_ONCE():a = ACCESS_ONCE(x)等價於 a = READ_ONCE(x),ACCESS_ONCE(x) = b 等價於 WRITE_ONCE(x, b)。
注意:ACCESS_ONCE()是有缺陷的,只能針對不超過處理器字長的資料型別,否則無法保證原子性。目前在最新的核心程式碼(linux_5.2.1)中已經廢除了ACCESS_ONCE()程式碼實現這裡不做詳細介紹了。READ_ONCE()和 WRITE_ONCE()沒有這樣的缺陷,它們在超過處理器字長的資料型別(比如在結構體和聯合體)上會退化成使用 memcpy()來讀寫。
(2)記憶體屏障
記憶體屏障用於解決記憶體一致性(即記憶體有序性)問題。CPU 的記憶體一致性模型有嚴格 一致、處理器一致、鬆散一致等模型,這些模型具體的實現,這裡不做詳細的介紹。現代高效能 CPU 包括龍芯在內大都使用鬆散一致性模型,訪存指令會存在亂序執行的情況。
為了防止訪存指令在處理器上亂序執行的記憶體屏障有很多種,主要有:
mb(): 全屏障,可以防止讀記憶體操作(Load)和寫記憶體操作(Store)的亂序執行。
#ifndef mb
#define mb()__barrier()
#endif
rmb(): 讀屏障,可以防止讀記憶體操作(Load)亂序執行,不干預寫記憶體操作(Store)。
#ifndef rmb
#define rmb()_mb()
#endif
wmb(): 寫屏障,可以防止寫記憶體操作(Store)亂序執行,不干預寫記憶體操作(Load)。
#ifndef wmb
#define wmb()_mb()
#endif
smp_mb():多處理器版全屏障,在多處理器系統上等價於 mb(),可以防止讀記憶體操作
(Load)和 寫記憶體操作(Store)的亂序執行;在單處理器上等價於優化屏障 barrier()。
#ifndef __smp_mb
#define __smp_mb()__mb()
#endif
smp_rmb():多處理器版讀屏障,在多處理器系統上等價於 rmb(),可以防止讀記憶體操作(Load) 亂序執行,不干預寫記憶體操作(Store);在單處理器上等價於優化屏障 barrier()。
#ifndef __smp_rmb
#define __smp_rmb()_rmb()
#endif
smp_wmb():多處理器版寫屏障,在多處理器系統上等價於 wmb(),可以防止寫記憶體操作(Store) 亂序執行,不干預讀記憶體操作(Load);在單處理器上等價於優化屏障 barrier()。
#ifndef __smp_wmb
#define __smp_wmb()_wmb()
#endif
除了多處理器之間存在記憶體一致性問題,處理器與外設之間(主要是 DMA 控制 器)也存在記憶體一致性問題,因此我們需要強制性記憶體屏障 mb()/rmb()/wmb()來解決。讀屏障和寫屏障應當成對使用,寫端 CPU 上必須用寫屏障,讀端 CPU 上必須用讀屏 障。也就是說在生產者-消費者示例中,如果處理器是亂序執行的,那麼生產者(寫端 CPU) 插入寫屏障,消費者(讀端 CPU)插入讀屏障才可以保證邏輯正確(缺一不可,用錯也不行)。程式碼實現如下:
int flag = 0;
char buffer[32] = {0};
void producer(void)
{
memset(buffer, 1, 32);
smp_wmb();
flag = 1;
}
void consumer(void)
{
char data[32] = {0};
while (!flag) ;
smp_rmb();
memcopy(&data,buffer,sizeof(buff);
}
當然,強屏障總是可以代替弱屏障。比如全屏障可以代替讀屏障和寫屏障,而強制性屏障可以代替多處理器版屏障,記憶體屏障可以代替優化屏障。只不過強屏障一般會比弱屏障更慢,效能損失更多。在龍芯上面,讀屏障、寫屏障和全屏障都是一條 sync 指令,但在語義 上,不同的屏障其功能要求是不一樣的。
多處理器版屏障還有一些變種,比如 smp_mb__before_atomic()和 smp_mb__after_ atomic(),分別用在原子操作的前後,在實現上大都等價於 smb_mb()。
#ifndef __smp_mb__before_atomic
#define __smp_mb__before_atomic()___smp_mb()
#endif
#ifndef __smp_mb__after_atomic
#define __smp_mb__after_atomic()____smp_mb()
#endif
另外有一些記憶體屏障是用來解決 CPU 與外設之間記憶體一致性問題的,比如:
dma_rmb():DMA 讀屏障,在裝置CPU 方向(From Device)的 DMA 中,裝置是寫端,CPU 是讀端,CPU 在讀取標識變數和讀取資料之間,必須插入 DMA 讀屏障。
#ifndef dma_rmb
#define dma_rmb()_rmb()
#endif
dma_wmb():DMA 寫屏障。在 CPU裝置方向(To Device)的 DMA 中,CPU 是寫端,裝置是讀 端,CPU 在寫入資料和寫入標識變數之間,必須插入 DMA 寫屏障。
#ifndef dma_wmb
#define dma_wmb()_wmb()
#endif
mmiowb():MMIO 暫存器寫屏障。對於裝置的 MMIO 暫存器的寫操作有時候是不允許亂序的,在這些場景下需要用 MMIO 暫存器寫屏障。
注意:以上提到的所有記憶體屏障都是雙向的,也就是說,記憶體屏障既要關注屏障前的訪存操作, 也要關注屏障後的訪存操作。
但是核心也提供一些隱式的單向屏障功能,比如 ACQUIRE 操作和 RELEASE 操作。ACQUIRE 的語義是 ACQUIRE 操作後面的訪存必須在 ACQUIRE操作之後完成,但並不關注 ACQUIRE 操作前面的訪存。
RELEASE 的語義是 RELEASE 操作前面的訪存必須在 RELEASE 操作之前完成,但並不關注 RELEASE 操作後面的訪存。
#ifndef __smp_store_release
#define __smp_store_release(p, v)_________\
do {__________________\
__compiletime_assert_atomic_type(*p);_______\
____smp_mb();_____________\
__WRITE_ONCE(*p, v);____________\
} while (0)
#endif
#ifndef __smp_load_acquire
#define __smp_load_acquire(p)___________\
({__________________\
__typeof(*p) ___p1 = READ_ONCE(*p);_______\
__compiletime_assert_atomic_type(*p);_______\
____smp_mb();_____________\
_____p1;________________\
})
另外,加鎖操作通常意味著 ACQUIRE 操作,而解鎖操作通常意味著 RELEASE 操作。
關於記憶體屏障的更多資訊可參閱核心文件 Documentation/memory-barriers.txt。