深入淺出Linux內核中的內存屏障

工程師的聖地—Linux內核, 談談內核的架構
5個方面分析內核架構
linux內核,進程調度器的實現,完全公平調度器 CFS
深透剖析Linux內核字符與塊設備驅動程序


抽象內存模型

module.jpg

指令重排

每個 CPU 運行一個程序,程序的執行產生內存訪問操作。在這個抽象 CPU 中,內存 操作的順序是鬆散的,CPU 假定進程間不依靠內存直接通信,在不改變程序執行 結果 的推測下由自己方便的順序執行內存訪問操作。

例如,考慮下面的執行過程:

{ A = 1 b = 2}

在這裏插入圖片描述

這有 24 中內存訪問操作的組合,每種組合都有可能出現:

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, ...
...

從而產生 4 種結果:

x == 2, y == 1
x == 2, y == 3
x == 4, y == 1
x == 4, y == 3

更殘酷的是,一個 CPU 已經提交的 store 操作,另一個 CPU 可能不會感知到,從而 load 操作取到舊的值。

比如:

{
   
   A = 1, B = 2, C = 3, P = &A, Q = &C}

CPU 1	CPU 2
B=4;	Q=p;
P=&B;	D=*Q;

可以產生 4 種結果:

(Q == &A) and (D == 1)
(Q == &B) and (D == 2)
(Q == &B) and (D == 4)

設備操作

一些設備將自己的控制接口映射成一個內存地址,訪問這些地址的指令順序是極重要 的。比如一個擁有一系列內部寄存器的網卡,可以通過一個地址寄存器 (A) 和一個數 據寄存器 (D) 訪問它們。如果要訪問內部寄存器 5 ,則使用下面的代碼:

*A = 5;
x = *D;

但這個代碼可能生成以下兩種執行順序:

STORE *A = 5, x = LOAD *D
x = LOAD *D, STORE *A = 5

合併內存訪問

CPU 還可能將內存操作合併。比如

X = *A; Y = *(A + 4);

可能會生成下面任何一種執行順序:

X = LOAD *A; Y = LOAD *(A + 4);
Y = LOAD *(A + 4); X = LOAD *A;
{
   
   X, Y} = LOAD {
   
   *A, *(A + 4) };

*A = X; *(A + 4) = Y;

則可能生成下面任何一種執行:

STORE *A = X; STORE *(A + 4) = Y;
STORE *(A + 4) = Y; STORE *A = X;
STORE {
   
   *A, *(A + 4) } = {
   
   X, Y};

最小保證

可以期望 CPU 提供了一些最小保證,不滿足最小保證的 CPU 都是假的 CPU。

有依賴關係的內存訪問操作是有順序的。也就是說:

Q = READ_ONCE(P); smp_read_barrier_depends(); D = READ_ONCE(*Q);

總是在 CPU 中以這樣的順序執行:

Q = LOAD P, D = LOAD *Q

smp_read_barrier_depends() 只在 DEC Alpha 中有用,READ_ONCE 的作用在 這裏 提到。

在一個 CPU 中的覆蓋 load-store 操作是有順序的。比如

a = READ_ONCE(*X); WRITE_ONCE(*X, b);

總是以這樣的順序執行:

a = LOAD *X, STORE *X = b

WRITE_ONCE(*X, c); d = READ_ONCE(*X);

總是以下面的順序執行:

STORE *X = c, d = LOAD *X

最小保證不適用於位圖。假設我們有一個長度爲 8 的位圖,CPU 1 要將 1 位置 1, CPU 2 要將 2 位 置 1:

{ A = 0 }
在這裏插入圖片描述
可能有三種個結果:

A = 2
A = 4
A = 6

內存屏障

正如之前看到的,內存訪問操作的順序是隨機的,這會造成 CPU 間通信或者 I/O 問 題。需要一種介入保證指令的順序以獲得期望結果。內存屏障就是這樣一種介入。

4+2種內存屏障

寫屏障
寫屏障保證任何出現在寫屏障之前的 STORE 操作先於出現在寫屏障之後的任何 STORE 操作執行。

寫屏障一般與讀屏障或者數據依賴屏障配合使用。

數據依賴屏障
數據依賴屏障是一個弱的讀屏障。用於兩個讀操作之間,且第二個讀操作依賴第一個 讀操作(比如第一個讀操作獲得第二個讀操作所用的地址)。讀屏障用於保證第二個讀 操作的目標已經被第一個讀操作獲得。

數據依賴屏障只能用於確實存在數據依賴的地方。

讀屏障
讀屏障保證任何出現在讀屏障之前的 LOAD 操作先於出現在讀屏障之後的任何 LOAD 操作執行。

讀屏障一般和寫屏障配合使用。

一般內存屏障
一般內存屏障保證所有出現在屏障之前的內存訪問 (LOAD 和 STORE) 先於出現在屏障 之後的內存訪問執行。

此外還有兩種不常見的內存屏障

ACQUIRE 操作
保證出現在 ACQUIRE 之後的操作確實在 ACQUIRE 之後執行。而出現在 ACQUIRE 之前 的內存操作可能執行於 ACQUIRE 之後。

ACQUIRE 和 RELEASE 配合使用。

RELEASE 操作
保證出現在 RELEASE 之前的操作確實在 RELEASE 之前執行。而出現在 RELEASE 之後 的內存操作可能執行於 RELEASE 之前。

數據依賴屏障

數據依賴屏障需要並不總是那麼明顯。舉個例子。

{ A = 1, B = 2, C = 3, P = &A, Q = &C }在這裏插入圖片描述
這個例子中,D 要麼是 &A, 要麼是 &B:

(Q == &A) implies (D == 1)
(Q == &B) implies (D == 4)

但是,CPU 2 察覺到的 P 的更新可能比先於 B 的更新被察覺,這導致下面的結果:

(Q == &B) and (D == 2)

現實世界中有 CPU 是這麼表現的,比如 DEC Alpha。要獲得想要的結果,需要一個數 據依賴屏障:
在這裏插入圖片描述
數據依賴憑證保證了只會出現兩種可預期的結果。

數據依賴屏障也可以序列化依賴前一指令的寫操作:

{
   
    A = 1, B = 2, C = 3, P = &A, Q = &C }

在這裏插入圖片描述
數據依賴憑證保證了只會出現一種結果:

(Q == &B) && (B == 4)

【文章福利】小編推薦自己的Linux、C/C++技術交流羣:【960994558】整理了一些個人覺得比較好的學習書籍、大廠面試題、有趣的項目和熱門技術教學視頻資料共享在裏面(包括C/C++,Linux,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒體,CDN,P2P,K8S,Docker,TCP/IP,協程,DPDK等等.),有需要的可以自行添加哦!~
在這裏插入圖片描述
這裏推薦一個大佬的免費課程,這個跟以往所見到的只會空談理論的有所不同,正在學習的朋友可以體驗一下
【免費】C/C++Linux服務器開發/後臺架構師


控制依賴

現在的編譯器是不能理解控制依賴的,需要人開動腦筋做一些微小的工作。這一小節 可以防止你的代碼不被傲慢無知的編譯器破壞。

考慮下面的代碼:

q = a;
if (q) {
   
   
    p = b;
}

如果編譯器推測 q 總是 非 0, 它會覺得有必要對這段代碼進行優化,優化後的結果 爲:

q = a;
p = b;

編譯器也可能合併 a 的讀操作到其它位置,或者合併 p 的寫操作。爲了阻止這些情 況發生,需要將代碼改爲:

q = READ_ONCE(a);
if (q) {
   
   
    p = READ_ONCE(b);
}

即使這樣,CPU 也會因爲預執行而引起其它 CPU 感知到 b 的 LOAD 先於 a 的 LOAD。 這裏需要一個讀屏障來防止這樣的事情發生:

q = READ_ONCE(a);
if (q) {
   
   
    <read barrier>
    p = READ_ONCE(b);
}

但寫操作是不會預執行的,下面的代碼所見即所得:

q = READ_ONCE(a);
if (q) {
   
   
    p = WRITE_ONCE(b);
}

編譯器也可能將它覺得重複的操作移出合併,比如:

q = READ_ONCE(a);
if (q) {
   
   
        barrier();
        WRITE_ONCE(b, 1);
        do_something();
} else {
   
   
        barrier();
        WRITE_ONCE(b, 1);
        do_something_else();
}

被轉化爲:

q = READ_ONCE(a);
barrier();
WRITE_ONCE(b, 1);  /* BUG: No ordering vs. load from a!!! */
if (q) {
   
   
        /* WRITE_ONCE(b, 1); -- moved up, BUG!!! */
        do_something();
} else {
   
   
        /* WRITE_ONCE(b, 1); -- moved up, BUG!!! */
        do_something_else();
}

如果需要嚴格控制這個過程,可以使用 smp_store_release()

q = READ_ONCE(a);
if (q) {
   
   
        smp_store_release(&b, 1);
        do_something();
} else {
   
   
        smp_store_release(&b, 1);
        do_something_else();
}

或者令兩條語句不同從而編譯器無法將共同代碼提出判斷條件:

q = READ_ONCE(a);
if (q) {
   
   
        WRITE_ONCE(b, 1);
        do_something();
} else {
   
   
        WRITE_ONCE(b, 2);
        do_something_else();
}
另外,需要小心編譯器的推斷,比如:

q = READ_ONCE(a);
if (q % MAX) {
   
   
        WRITE_ONCE(b, 1);
        do_something();
} else {
   
   
        WRITE_ONCE(b, 2);
        do_something_else();
}

如果 MAX 被定義爲 1, 那麼編譯器將推測出 (q % MAC) = 0,那麼編譯器就會覺得 有必要將判斷條件優化掉:

q = READ_ONCE(a);
WRITE_ONCE(b, 1);
do_something_else();

如果優化成這樣, CPU 將沒有義務保證 LOAD a 和 STORE b 的順序。如果這個判斷 是必要的,那麼 MAX 必須保證大於 1:

q = READ_ONCE(a);
BUILD_BUG_ON(MAX <= 1); /* Order load from a with store to b. */
if (q % MAX) {
   
   
        WRITE_ONCE(b, 1);
        do_something();
} else {
   
   
        WRITE_ONCE(b, 2);
        do_something_else();
}

注意兩個 LOAD 操作是不同的,否則編譯器會將其提出判斷條件外。

還必須注意不要太依賴 bool 運算的短路計算,比如:

q = READ_ONCE(a);
if (q || 1 > 0)
        WRITE_ONCE(b, 1);

由於第一個條件不會出錯,而第二個條件總是 TRUE, 編譯器會將其轉化成:

q = READ_ONCE(a);
WRITE_ONCE(b, 1);

不要令編譯器看透你的代碼,否則連 READ_ONCE() 也不能阻止這種情況發生。

另外,控制依賴只能控制 then-clause 和 else-clause 的順序,並不能保證判斷語 句之外的代碼:

q = READ_ONCE(a);
if (q) {
   
   
        WRITE_ONCE(b, 1);
} else {
   
   
        WRITE_ONCE(b, 2);
}
WRITE_ONCE(c, 1);  /* BUG: No ordering against the read from 'a'. */

也許有人會說編譯器不能重排 b 的 STORE 因爲這是 volatile 的訪問,但編譯器可 能生成如下代碼:

ld r1,a
cmp r1,$0
cmov,ne r4,$1
cmov,eq r4,$2
st r4,b
st $1,c

CPU 沒有義務保證 c 的 STORE 必須在 a 的 LOAD 之後。

舉例

寫屏障規定了 STORE 操作的順序。
在這裏插入圖片描述
這意味着無序集合 {STORE A, STORE B, STORE C} 中所有指令都出現在無序集合 {STORE D, STORE E} 任何指令之前。

數據依賴屏障保證了 LOAD 依賴的前一個 STORE 操作已經執行。
考慮下面的代碼:

{ B = 7; X = 9; Y = 8; C = &Y }在這裏插入圖片描述
如果沒有數據依賴屏障, CPU 2 執行 LOAD C 時感知到了 CPU 1 的 STORE C = &B, 但可能 LOAD *C 時卻沒有感知到 CPU 1 的 STORE B = 2,而得到結果 LOAD *C = 7。

這裏需要一個數據依賴屏障來讓 CPU 2 感知 B 的改變。

{ B = 7; X = 9; Y = 8; C = &Y }在這裏插入圖片描述
這樣得到的結果就是 LOAD *C = 2。

讀屏障保證 STORE 操作是有順序的
{ A = 0, B = 9 }
在這裏插入圖片描述
沒有讀屏障,CPU 2 感知到 CPU 1 的隨機順序,根本不管 CPU 1 上的寫屏障。 這裏甚至可能出現 A = 0, B = 2 的情況。


所以這裏需要一個寫屏障:

{ A = 0, B = 9 }
在這裏插入圖片描述
這個寫屏障保證瞭如果 B=2 那麼 A=1。

讀屏障和預取

很多 CPU 有預取技術,如果 CPU 發現需要用到一個內存中的數據,並且總線空閒, 即使沒有執行到真正需要這個值的指令,CPU 也會提前將其取出。如果 CPU 發現並不 需要這個值(在分支條件中可能出現這種情況),它會將其丟棄,或者將其緩存。

考慮下面的代碼:

LOAD B
DIVIDE
DIVIDE
LOAD A

DIVIDE 指令需要消耗大量的時間,此時 A 已經被預取,如果在 DIVIDE 執行過程中, 另一個 CPU 修改了 A 的值,CPU 2 是不管的,因爲 A 已經預取了。

此時需要一個內存屏障:

LOAD B
DIVIDE
DIVIDE
<read barrier>
LOAD A

在這個屏障的作用下,如果預取之後 A 被修改,CPU 將重新取得 A 的值。

傳遞性

一般內存屏障提供了傳遞性。考慮下面的代碼:

{ X = 0, Y = 0 }
在這裏插入圖片描述
假設 CPU2 LAOD X = 1, LOAD Y = 0,這表示 CPU2 的 LOAD X 後於 CPU1 的 STORE X = 1,CPU2 的 LOAD Y 先於 CPU3 的 STORE Y = 1。這時候 CPU3 能否出現 LOAD X = 0?

在 Linux 內核中,一般內存屏障提供了傳遞性,所以這裏的 CPU3 中 LOAD X 只能得 到 1。

但是, 讀屏障和寫屏障不提供傳遞性,比如下面的代碼:

{ X = 0, Y = 0 }
在這裏插入圖片描述
這可能出現 CPU2 LOAD X = 1, CPU2 LOAD Y = 0, CPU3 LOAD X = 0。

CPU2 的讀屏障雖然保證了它自己 LOAD 的順序,但不能保證 CPU1 的 STORE。如果 CPU1 和 CPU2 共享同一個 cache,CPU2 可能更早的感知到 CPU1 的 STORE X = 1。

Linux 內核屏障

Linux 內核擁有三種級別的屏障:

  • 編譯器屏障
  • CPU 內存屏障
  • MMIO 寫屏障

編譯器屏障

Linux 內核提供了編譯器屏障函數,它防止編譯器將屏障以便的內存操作移動到另一邊:

barrier();

這是個一般屏障,READ_ONCE() 和 WRITE_ONCE() 可以認爲是 barier() 的弱化版本。

barrier() 有一下兩個功能:

(1) 防止編譯器將 barrier() 之後的內存訪問重排到 barrier() 之前。 (2) 在循環中,迫使編譯器在循環裏面每次都取條件判斷中需要的值,而不是隻取一次。

READ_ONCE() 和 WRITE_ONCE() 可以防止編譯器的一些優化,這些優化在但線程環境 中是無害的,但在多線程環境中會引發錯誤。

(1) 編譯器有權重排 LOAD 和 STORE 指令以達到優化目的:

a[0] = x;
a[1] = x;

可能會被重排成先賦值 (a[1]), 後賦值 (a[0])。使用 READ_ONCE() 可以防止編譯器重排:

a[0] = READ_ONCE(x);
a[1] = READ_ONCE(x);

一句話:READ_ONCE() 和 WRITE_ONCE() 提供了多 CPU 訪問一個變量的緩存一致性。

(2) 編譯器有權合併 LOAD 操作以達到優化目的,比如:

while (tmp = a)
        do_something_with(tmp);

這會被編譯器優化成:

if (tmp = a)
        for (;;)
                do_something_with(tmp);

這一定不是你想要的,使用 RAED_ONCE() 防止編譯器這麼做:

while (tmp = READ_ONCE(a))
        do_something_with(tmp);

(3) 在寄存器緊張的時候,編譯器可能會生蟲重新取內存中的值的代碼:

while (tmp = a)
        do_something_with(tmp);

將變成:

while (a)
        do_something_with(a);

這在單線程環境中是沒有問題的,但在並行環境中會引起錯誤。同樣使用 READ_ONCE() 抑制編譯器的自作多情:

while (tmp = READ_ONCE(a))
        do_something_with(tmp);

(4) 如果編譯器“知道”一個變量的值是什麼,它就有權忽略 LOAD 操作。如果編譯器 推測 a 總是0,那麼

while (tmp = a)
        do_something_with(tmp);

會被優化成:

do {
   
    } while (0);

這在單線程環境中是個極大的優化,但多線程環境中是個問題。其它線程可能會修改 a 以改變它爲 0 的命運,但生成的代碼並不會知道這些。這裏應該使用 READ_ONCE() 告訴編譯器它不瞭解情況:

while (tmp = READ_ONCE(a))
        do_something_with(tmp);

(5) 如果編譯器發現一個變量要賦一個相同的值,那麼這個賦值操作將被忽略:

a = 0;
... Code that does not store to variable a ...
a = 0;

百年一起發現 a 已經是 0 了,所以它覺得第二個 a = 0 沒有用,就將其忽略。這同 樣導致多線程環境中的問題。使用 WRITE_ONCE() 告訴編譯器事情比它想象中的複雜:

WRITE_ONCE(a, 0);
... Code that does not store to variable a ...
WRITE_ONCE(a, 0);

(6) 編譯器有權重排內存訪問的順序,除非你告訴它不這麼做。比如:

void process_level(void)
{
   
   
        msg = get_message();
        flag = true;
}

void interrupt_handler(void)
{
   
   
        if (flag)
                process_message(msg);
}
編譯器可能將 process_level() 重排成:

void process_level(void)
{
   
   
        flag = true;
        msg = get_message();
}

如果一箇中斷在這兩條語句中間發生,那麼 interrupt_handler 中的 process_message 使用的將是爲初始化的 msg。

使用 READ_ONCE() 和 WRITE_ONCE() 告知編譯器不要重排:

void process_level(void)
{
   
   
        WRITE_ONCE(msg, get_message());
        WRITE_ONCE(flag, true);
}

void interrupt_handler(void)
{
   
   
        if (READ_ONCE(flag))
                process_message(READ_ONCE(msg));
}

(7) 編譯器有權優化一個條件控制語句,比如:

if (a)
        b = a;
else
        b = 42;
可被優化成:

b = 42;
if (a)
        b = a;

這是有道理的,因爲省略了一個判斷分支。同樣地,造成了多線程環境的問題。使用 WRITE_ONCE() 防止編譯器這麼做:

if (a)
        WRITE_ONCE(b, a);
else
        WRITE_ONCE(b, 42);

(8) 編譯器可能會把沒有對齊的內存地址操作轉化爲兩個指令,例如下面的代碼:

p = 0x00010002;

可能被轉換成兩條 16-bit 的指令實現這個 32-bit 操作。使用:

WRITE_ONCE(p, 0x00010002);

可以避免這樣的指令分割。這種分割在 attribute__((__packed)) 的 struct 中很常見。

CPU 內存屏障

Linux 內核擁有 8 個基本的內存屏障:
在這裏插入圖片描述
除了數據依賴屏障,其它每個內存屏障都隱含這編譯器屏障。數據依賴屏障不影響編 譯器的生成的指令順序。

SMP 內存屏障在單處理器編譯系統上的作用跟編譯器屏障一樣,因爲我們假設單 CPU 是不會把事情搞砸的。

在 SMP 系統中,必須使用 SMP 內存屏障來規範共享內存的訪問次序,即使使用鎖已 經足夠了。

除了 8 個基本函數,還有一些高級屏障函數:

(1) smp_store_mb(var, value)

相當於將 var 賦值後插入一個內存屏障。

(2) smp_mb__before_atomic() 和 smp_mb__after_atomic()

這兩個函數用於沒有返回值的原子操作函數,多用在引用計數中。這兩個函數不包含 內存屏障。

比如,考慮下面的代碼,它將一個 object 標記爲死亡,然後遞減 object 的引用計 數:

obj->dead = 1;
smp_mb__before_atomic();
atomic_dec(&obj->ref_count);

這保證了 obj->dead = 1 在引用計數遞減之前被感知到。

(3) lockless_dereference()

作用等同於指針解引用和 smp_read_barrier_depends() 數據依賴屏障的結合。

(4) dma_wmb() 和 dma_rmb()

這兩個函數保證了一塊 DMA 和 CPU 共享的內存的訪問指令順序。

例如下面的代碼,它使用 desc->status 區分 desc 屬於 CPU 還是設備,當 desc 可 用時就去按門鈴:

if (desc->status != DEVICE_OWN) {
   
   
        /* do not read data until we own descriptor */
        dma_rmb();

        /* read/modify data */
        read_data = desc->data;
        desc->data = write_data;

        /* flush modifications before status update */
        dma_wmb();

        /* assign ownership */
        desc->status = DEVICE_OWN;

        /* force memory to sync before notifying device via MMIO */
        wmb();

        /* notify device of new descriptors */
        writel(DESC_NOTIFY, doorbell);
}

dma_rmb() 保證在讀數據之前設備已經釋放了它對 desc 的所有權,dma_wmb() 保證 在設備察覺 desc->status = DEVICE_OWN 時數據已經寫入。smb() 用於保證緩存一致 的內存寫操作在緩存不一致的 MMIO 寫之前已經完成。

MMIO 寫屏障

Linux 內核有一個專門用於 MMIO 寫的屏障:

mmiowb()

隱藏的內存屏障

Linux 內核中一些鎖或者調度函數暗含了內存屏障。

鎖函數

Linux 內核有很多鎖的設計:

  • spin locks
  • R/W spin locks
  • mutexes
  • semaphores
  • R/W semaphores

這些設計中都包含了 ACQUIRE 操作和 RELEASE 操作,或者它們的變種。這些操作暗 含了內存屏障。

但 ACQUIRE 和 RELEASE 並不實現完全的內存屏障。

中斷禁止函數

啓動或禁止終端的函數的作用僅僅是作爲編譯器屏障,所以要使用內存或者 I/O 屏障 的場合,必須用別的函數。

SLEEP 和 WAKE-UP 以及其它調度函數

使用 SLEEP 和 WAKE-UP 函數時要改變 task 的狀態標誌,這需要使用合適的內存屏 障保證修改的順序。

多 CPU 間 ACQUIRE 的作用

ACQUIRE 和內存訪問

考慮下面的代碼,系統有一對自旋鎖 M 和 Q ,三個 CPU:
在這裏插入圖片描述
CPU 3 察覺到的修改的順序不能爲以下任何一個:

*B, *C or *D preceding ACQUIRE M
*A, *B or *C following RELEASE M
*F, *G or *H preceding ACQUIRE Q
*E, *F or *G following RELEASE Q

ACQUIRE 和 I/O

在一些結構下,比如 NUMA,兩個位於不同 CPU 間的自旋鎖區域的 I/O 訪問在 PCI 上 可能是交錯的,因爲 PCI 沒有必要考慮緩存一致協議。

比如:在這裏插入圖片描述
在 PCI 橋中可能出現下面的序列:

STORE *ADDR = 0, STORE *ADDR = 4, STORE *DATA = 1, STORE *DATA = 5

這可能會造成硬件故障。在釋放 spin_lock 之前需要一個 mmiowb() :在這裏插入圖片描述

這保證了 CPI 橋看到的 CPU1 的 STORE 操作出現在 CPU2 之前。

但下面的 store-by-load 操作卻不需要 mmiowb(), 因爲 load 迫使 store 操作先完 成。在這裏插入圖片描述

何處使用內存屏障

在下面的 4 中情況下需要用內存屏障保證指令順序:

  • 處理器間交互
  • 原子操作
  • 訪問設備
  • 中斷

處理器間交互

當系統擁有多個處理器時,超過一個的 CPU 可能會在同一時間操作同一個數據集。這 可能會有同步問題,一般的解決辦法是使用鎖。但鎖是很昂貴的,所以儘可能不用。這 種情況下每個 CPU 的操作指令都需要嚴格控制。

考慮一個 R/W 的信號量,結構中有一個等待隊列,保存指向等待進程的指針:

struct rw_semaphore {
   
   
        ...
        spinlock_t lock;
        struct list_head waiters;
};

struct rwsem_waiter {
   
   
        struct list_head list;
        struct task_struct *task;
};

爲了喚醒一個等待隊列中的進程,up_read() 或者 up_write() 函數必須:

(1) 讀等待者的 next 指針,以得知下一個等待者的結構在哪裏;
(2) 讀指向等待者 task struct 的指針;
(3) 清除 task 指針,告知等待者信號量已經給它了;
(4) wake_up_process();
(6) 釋放 task struct 中的引用。



換一種語言:

LOAD waiter->list.next;
LOAD waiter->task;
STORE waiter->task;
CALL wakeup
RELEASE task

如果上述順序不能保證,整個過程在多 CPU 環境中就會有問題。需要在2-3行之間插 入一個 smp_mb()。

原子操作

有些原子操作包含了完整的內存屏障,但一些根本沒有。任何操作一個內存並擁有內 存值返回的原子操作都實現了內存屏障(smp_mb()),在操作前後都有,這些函數包括:

xchg();
atomic_xchg();                  atomic_long_xchg();
atomic_inc_return();            atomic_long_inc_return();
atomic_dec_return();            atomic_long_dec_return();
atomic_add_return();            atomic_long_add_return();
atomic_sub_return();            atomic_long_sub_return();
atomic_inc_and_test();          atomic_long_inc_and_test();
atomic_dec_and_test();          atomic_long_dec_and_test();
atomic_sub_and_test();          atomic_long_sub_and_test();
atomic_add_negative();          atomic_long_add_negative();
test_and_set_bit();
test_and_clear_bit();
test_and_change_bit();

/* when succeeds */
cmpxchg();
atomic_cmpxchg();               atomic_long_cmpxchg();
atomic_add_unless();            atomic_long_add_unless();

下面的函數是不包含內存屏障的,但它們可能用於實現類似 RELEASE 的東西:

atomic_set();
set_bit();
clear_bit();
change_bit();

使用它們的時候,內存屏障是必要的,應該使用諸如 smp_mb__before_atomic() 之 類的函數。

雖然可能在某些環境中包含內存屏障,但不確保的函數:

atomic_add();
atomic_sub();
atomic_inc();
atomic_dec();

當這些函數用於實現一個鎖標誌位時,內存屏障是必要的。

訪問設備

一些設備可能被映射成內存地址,CPU 訪問這些內存地址時需要保證正確的指令順序。 擁有過於先進的 CPU 和 過於先進的編譯器的我們需要用內存屏障類給這個保證。參 看前面 mmiowb() 的例子。

中斷

驅動可能被自己的中斷服務函數中斷,這兩部分可能會互相干涉。考慮下面的訪問網 卡的代碼:

LOCAL IRQ DISABLE
writew(ADDR, 3);
writew(DATA, y);
LOCAL IRQ ENABLE
<interrupt>
writew(ADDR, 4);
q = readw(DATA);
</interrupt>

第一個 STORE DATE 寄存器的操作可能出現在第二個 STORE ADDR 操作之後:

STORE *ADDR = 3, STORE *ADDR = 4, STORE *DATA = y, q = LOAD *DATA
需要一個 I/O 屏障保證這個順序。mmiowb() 是個不錯的選擇。

CPU cache

邏輯上講,內存屏障擢用於內存和 CPU 的分界線上:

<--- CPU --->         :       <----------- Memory ----------->
	                  :
+--------+    +--------+  :   +--------+    +-----------+
|        |    |        |  :   |        |    |           |    +--------+
|  CPU   |    | Memory |  :   | CPU    |    |           |    |        |
|  Core  |--->| Access |----->| Cache  |<-->|           |    |        |
|        |    | Queue  |  :   |        |    |           |--->| Memory |
|        |    |        |  :   |        |    |           |    |        |
+--------+    +--------+  :   +--------+    |           |    |        |
	                      :                 | Cache     |    +--------+
	                      :                 | Coherency |
	                      :                 | Mechanism |    +--------+
+--------+    +--------+  :   +--------+    |           |    |	      |
|        |    |        |  :   |        |    |           |    |        |
|  CPU   |    | Memory |  :   | CPU    |    |           |--->| Device |
|  Core  |--->| Access |----->| Cache  |<-->|           |    |        |
|        |    | Queue  |  :   |        |    |           |    |        |
|        |    |        |  :   |        |    |           |    +--------+
+--------+    +--------+  :   +--------+    +-----------+
	                      :
	                      :

cache 一致性

某個 CPU 對內存的修改最後都會被所有其它 CPU 感知到,但感知順序卻不能保證。

考慮下面的架構:

	        :
	        :                          +--------+
	        :      +---------+         |        |
+--------+  : +--->| Cache A |<------->|        |
|        |  : |    +---------+         |        |
|  CPU 1 |<---+                        |        |
|        |  : |    +---------+         |        |
+--------+  : +--->| Cache B |<------->|        |
	        :      +---------+         |        |
	        :                          | Memory |
	        :      +---------+         | System |
+--------+  : +--->| Cache C |<------->|        |
|        |  : |    +---------+         |        |
|  CPU 2 |<---+                        |        |
|        |  : |    +---------+         |        |
+--------+  : +--->| Cache D |<------->|        |
	        :      +---------+         |        |
	        :                          +--------+
	        :

這個系統擁有下面的特性:

  • 奇數緩存行保存在 A, C 或者內存中;
  • 偶數緩存行保存在 B, D 或者內存中;
  • 當 CPU 詢問一個 cache 時,其它 cache 可能會利用總線訪問其它部分,比如置換 髒頁,或者預取;
  • 每個 cache 都有一個操作隊列,用於維護一致性;
  • 一致性隊列中的對 cache 中已經存在的數據的 load 不會引起隊列的刷新。

當一個 CPU 訪問內存時,操作順序如下:
在這裏插入圖片描述
smp_wmb() 保證其它 CPU 感知到的兩個改變是有序的。但如果第二個 CPU 想要訪問這些值:

q = p;
x = *q;

這會可能會出現 q = &v 但 v = 1 的情況:在這裏插入圖片描述
CPU2 的兩條指令之間需要一個 smp_read_barrier_depends() 防止這一情況。這在 DEC Alpha 處理器中是可能出現的情況。

CACHE 和 DMA

不是所有的系統都會維護 DMA 和 cache 間的一致性。DMA 可能會從內存拿到陳腐的 數據而新的數據還在 cache 中。DMA 寫 RAM 的時候 CPU 的 cache 可能會刷新它的 髒行到內存從而引起不一致。

CACHE 和 MMIO

MMIO 不會走 CACHE, 在訪問 MMIO 之前保證讀取到的數據是一致的即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章