內存屏障主要解決的問題是編譯器的優化和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) ({ \
smp_wmb();\
(p)= (v); \
})#define rcu_dereference(p) ({ \
typeof(p)_________p1 = p; \
smp_read_barrier_depends();\
(_________p1);\
})
rcu_assign_pointer()通常用於寫者的發佈,rcu_dereference()通常用於讀者的訂閱。
寫者:
1 p->a = 1;
2 p->b = 2;
3 p->c = 3;
4 rcu_assign_pointer(gp, p);
讀者:
1 rcu_read_lock();
2 p = rcu_dereference(gp);
3 if (p != NULL) {
4 do_something_with(p->a, p->b, p->c);
5 }
6 rcu_read_unlock();
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)
{
__list_add_rcu(new, head, head->next);
}
static inline void __list_add_rcu(struct list_head * new,
struct list_head * prev, struct list_head * next)
{
new->next = next;
new->prev = prev;
smp_wmb();
next->prev = new;
prev->next = new;
}
讀者:
#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))
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())併發執行。