Linux kernel 記憶體屏障在RCU上的應用

2021-06-06 04:30:30 字數 3229 閱讀 1528

記憶體屏障主要解決的問題是編譯器的優化和cpu的亂序執行。

編譯器在優化的時候,生成的彙編指令可能和c語言程式的執行順序不一樣,在需要程式嚴格按照c語言順序執行時,需要顯式的告訴編譯不需要優化,這在linux下是通過barrier()巨集完成的,它依靠volidate關鍵字和memory關鍵字,前者告訴編譯barrier()周圍的指令不要被優化,後者作用是告訴編譯器彙編**會使記憶體裡面的值更改,編譯器應使用記憶體裡的新值而非暫存器裡儲存的老值。

同樣,cpu執行會通過亂序以提高效能。彙編裡的指令不一定是按照我們看到的順序執行的。linux中通過mb()系列巨集來保證執行的順序。簡單的說,如果在程式某處插入了mb()/rmb()/wmb()巨集,則巨集之前的程式保證比巨集之後的程式先執行,從而實現序列化。

即使是編譯器生成的彙編碼有序,處理器也不一定能保證有序。就算編譯器生成了有序的彙編碼,到了處理器那裡也拿不準是不 是會按照**順序執行。所以就算編譯器保證有序了,程式設計師也還是要往**裡面加記憶體屏障才能保證絕對訪存有序,這倒不如編譯器乾脆不管算了,因為記憶體屏障 本身就是乙個sequence point,加入後已經能夠保證編譯器也有序。

處理器雖然亂序執行,但最終會得出正確的結果,所以邏輯上講程式設計師本不需要關心處理器亂序的問題。但是在

smp併發執行的情況下,處理器無法知道併發程式之間的邏輯,比如,在不同

core

上的讀者和寫者之間的邏輯。簡單講,處理器只保證在單個

core

上按照code

中的順序給出最終結果。這就要求程式設計師通過

mb()/rmb()/wmb()/read_barrier_depends

來告知處理器,從而得到正確的併發結果。記憶體屏障、資料依賴屏障都是為了處理

smp環境下的資料同步問題,

up根本不存在這個問題。

下面分析下記憶體屏障在rcu上的應用:

#define rcu_assign_pointer(p, v)        ()

#define rcu_dereference(p)     (

6 rcu_read_unlock();

rcu_assign_pointer()是說,先把那塊記憶體寫好,再把指標指過去。這裡使用的記憶體寫屏障是為了保證併發的讀者讀到資料一致性。在這條語句之前的讀者讀到舊的指標和舊的記憶體,這條語句之後的讀者讀到新的指標和新的記憶體。如果沒有這條語句,很有可能出現讀者讀到新的指標和舊的記憶體。也就是說,這裡通過記憶體屏障重新整理了p所指向的記憶體的值,至於gp本身的值有沒有更新還不確定。實際上,gp本身值的真正更新要等到併發的讀者來促發。

rcu_dereference() 原語用的是資料依賴屏障,smp_read_barrier_dependence,它要求後面的讀操作如果依賴前面的讀操作,則前面的讀操作需要首先完成。根據資料之間的依賴,要讀p->a, p->b, p->c, 就必須先讀p,要先讀p,就必須先讀p1,要先讀p1,就必須先讀gp。也就是說讀者所在的core在進行後續的操作之前,gp必須是同步過的當前時刻的最新值。如果沒有這個資料依賴屏障,有可能讀者所在的core很長一段時間內一直用的是舊的gp值。所以,這裡使用資料依賴屏障是為了督促寫者將gp值準備好,是為了呼應寫者,這個呼應的訴求是通過資料之間的依賴關係來促發的,也就是說到了非呼應不可的地步了。

下面看看kernel中常用的鍊錶操作是如何使用這樣的發布、訂閱機制的:

寫者:static inline void list_add_rcu(struct list_head *new, struct list_head *head)

static inline void __list_add_rcu(struct list_head * new,

struct list_head * prev, struct list_head * next)

讀者:#define list_for_each_entry_rcu(pos, head, member) \

for(pos = list_entry((head)->next, typeof(*pos), member); \

prefetch(rcu_dereference(pos)->member.next),\

&pos->member!= (head); \

pos= list_entry(pos->member.next, typeof(*pos), member))

寫者通過呼叫list_add_rcu來發布新的節點,其實是發布next->prev, prev->next這兩個指標。讀者通過list_for_each_entry_rcu來訂閱這連個指標,我們將list_for_each_entry_rcu訂閱部分簡化如下:

pos = prev->next;

prefetch(rcu_dereference(pos)->next);

讀者通過

rcu_dereference

訂閱的是

pos,而由於資料依賴關係,又間接訂閱了

prev->next

指標,或者說是促發prev->next的更新。

safe版本的iterate的函式?為什麼就safe了?

#define list_for_each_safe(pos,n, head) \

for(pos = (head)->next, n = pos->next; pos != (head); \

pos= n, n = pos->next)

#define list_for_each(pos, head)\

for(pos = (head)->next; prefetch(pos->next), pos != (head); \

pos= pos->next)

當在iterate的過程中執行刪除操作的時候,比如:

list_for_each(pos,head)

list_del(pos)

這樣會斷鏈,為了避免這種斷鏈,增加了safe版本的iterate函式。另外,由於preftech的緣故,有可能引用乙個無效的指標list_poison1。這裡的safe是指,為避免有些cpu的preftech的影響,乾脆在iterate的過程中去掉preftech。

還有乙個既有rcu+safe版本的iterative函式:

#definelist_for_each_safe_rcu(pos, n, head) \

for(pos = (head)->next; \

n= rcu_dereference(pos)->next, pos != (head); \

pos= n)

只要用這個版本的iterate函式,就可以和多個_rcu版本的寫操作(如:list_add_rcu())併發執行。

Linux kernel 記憶體屏障在RCU上的應用

記憶體屏障主要解決的問題是編譯器的優化和cpu的亂序執行。編譯器在優化的時候,生成的彙編指令可能和c語言程式的執行順序不一樣,在需要程式嚴格按照c語言順序執行時,需要顯式的告訴編譯不需要優化,這在linux下是通過barrier 巨集完成的,它依靠volidate關鍵字和memory關鍵字,前者告訴...

優化屏障和記憶體屏障

優化屏障和記憶體屏障 優化屏障 編譯器編譯源 時,會將源 進行優化,將源 的指令進行重排序,以適合於cpu的並行執行。然而,核心同步必須避免指令重新排序,優化屏障 optimization barrier 避免編譯器的重排序優化操作,保證編譯程式時在優化屏障之前的指令不會在優化屏障之後執行。linu...

JVM層級的記憶體屏障 JSR記憶體屏障

jsr記憶體屏障 loadload 對於這樣的語句load1 loadload load2,在load2及後續的讀操作要讀取的資料被訪問前,保證load1要讀取的資料被讀取完畢 storestore 對於這樣的語句store1 storestore store2,在store2及後續的寫操作執行前,...