Linux kernel 內存屏障在RCU上的應用


內存屏障主要解決的問題是編譯器的優化和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_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)
{
__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))


寫者通過調用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())併發執行。

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