日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

rcu_assign_pointer、rcu_dereference、ACCESS_ONCE

發布時間:2025/3/21 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 rcu_assign_pointer、rcu_dereference、ACCESS_ONCE 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

題記看代碼不要死死的一條一條往后看,要適當的聯系上下文,看出整體邏輯性,流程性。事實上,編譯器和處理器也不是呆板的一條一條往后執行的,它也會有預取和邏輯判斷。

?

由內存屏障到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 =ACCESS_ONCE(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的更新。

?

RCU引出的ACCESS_ONCE

定義

它的定義很簡單,在?include/linux/compiler.h的底部:

PLAIN TEXT

C:

1.??#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))

僅從語法上講,這似乎毫無意義,先取其地址,在通過指針取其值。而實際上不然,多了一個關鍵詞?volatile,所以它的含義就是強制編譯器每次使用?x都從內存中獲取。

原因
僅僅從定義來看基本上看不大出來為什么要引入這么一個東西。可以通過幾個例子(均來自Paul,我做了小的修改)看一下。

1.?循環中有每次都要讀取的全局變量:

PLAIN TEXT

C:

1.??...

2.??static?int?should_continue;

3.??static?void?do_something(void);

4.??...

5.??? ? ? ? ? ? ? ?while?(should_continue)

6.??? ? ? ? ? ? ? ?? ? ? ?do_something();

假設?do_something()函數中并沒有對變量?should_continue做任何修改,那么,編譯器完全有可能把它優化成:

PLAIN TEXT

C:

1.??...

2.??? ? ? ? ? ? ? ?if?(should_continue)

3.??? ? ? ? ? ? ? ?? ? ? ?for?(;;)

4.??? ? ? ? ? ? ? ?? ? ? ? ? ? ? ?do_something();

這很好理解,不是嗎?對于單線程的程序,這么做完全沒問題,可是對于多線程,問題就出來了:如果這個線程在執行do_something()的期間,另外一個線程改變了?should_continue的值,那么上面的優化就是完全錯誤的了!更嚴重的問題是,編譯器根本就沒有辦法知道這段代碼是不是并發的,也就無從決定進行的優化是不是正確的!

這里有兩種解決辦法:1)?should_continue加鎖,畢竟多個進程訪問和修改全局變量需要鎖是很自然的;2)禁止編譯器做此優化。加鎖的方法有些過了,畢竟?should_continue只是一個布爾,而且退一步講,就算每次讀到的值不是最新的?should_continue的值也可能是無所謂的,大不了多循環幾次,所以禁止編譯器做優化是一個更簡單也更容易的解決辦法。我們使用?ACCESS_ONCE()來訪問?should_continue

PLAIN TEXT

C:

1.??...

2.??? ? ?while?(ACCESS_ONCE(should_continue))

3.??? ? ? ? ? ? ? ?? ? ? ?do_something();

2.?指針讀取一次,但要dereference多次:

PLAIN TEXT

C:

1.??...

2.??? ??p?=?global_ptr;

3.??? ??if?(p?&&?p->s?&&?p->s->func)

4.??? ? ? ? p->s->func();

那么編譯器也有可能把它編譯成:

PLAIN TEXT

C:

1.??...

2.??? ??if?(global_ptr?&&?global_ptr->s?&&?global_ptr->s->func)

3.??? ? ? ? global_ptr->s->func();

你可以譴責編譯器有些笨了,但事實上這是C標準允許的。這種情況下,另外的進程做了?global_ptr = NULL;就會導致后一段代碼?segfault,而前一段代碼沒問題。同上,所以這時候也要用?ACCESS_ONCE()

PLAIN TEXT

C:

1.??...

2.??? ??p?=?ACCESS_ONCE(global_ptr);

3.??? ??if?(p?&&?p->s?&&?p->s->func)

4.??? ? ? ? p->s->func();

3. watchdog中的變量:

PLAIN TEXT

C:

1.??for?(;;)?{

2.??? ? ? ? ? ? ? ?? ? ? ?still_working?=?1;

3.??? ? ? ? ? ? ? ?? ? ? ?do_something();

4.??? ? ? ? ? ? ? ?}

假設?do_something()定義是可見的,而且沒有修改?still_working的值,那么,編譯器可能會把它優化成:

PLAIN TEXT

C:

1.??still_working?=?1;

2.??? ? ? ? ? ? ? ?for?(;;)?{

3.??? ? ? ? ? ? ? ?? ? ? ?do_something();

4.??? ? ? ? ? ? ? ?}

如果其它進程同時執行了:

PLAIN TEXT

C:

1.??for?(;;)?{

2.??? ? ? ? ? ? ? ?? ? ? ?still_working?=?0;

3.??? ? ? ? ? ? ? ?? ? ? ?sleep(10);

4.??? ? ? ? ? ? ? ?? ? ? ?if?(!still_working)

5.??? ? ? ? ? ? ? ?? ? ? ? ? ? ? ?panic();

6.??? ? ? ? ? ? ? ?}

通過?still_working變量來檢測?wathcdog是否停止了,并且等待10秒后,它確實停止了,panic()!經過編譯器優化后,就算它沒有停止也會?panic!!所以也應該加上?ACCESS_ONCE()

PLAIN TEXT

C:

1.??for?(;;)?{

2.??? ? ? ? ? ? ? ?? ? ? ?ACCESS_ONCE(still_working)?=?1;

3.??? ? ? ? ? ? ? ?? ? ? ?do_something();

4.??? ? ? ? ? ? ? ?}

綜上,我們不難看出,需要使用?ACCESS_ONCE()的兩個條件是:

1.?無鎖的情況下訪問全局變量;
2.?
對該變量的訪問可能被編譯器優化成合并成一次(上面第13個例子)或者拆分成多次(上面第2個例子)。

例子

Linus?在郵件中給出的另外一個例子是:

編譯器有可能把下面的代碼:

PLAIN TEXT

C:

1.??if?(a> MEMORY)?{

2.??? ? ? ? do1;

3.??? ? ? ? do2;

4.??? ? ? ? do3;

5.??? ??}?else?{

6.??? ? ? ? do2;

7.??? ??}

優化成:

PLAIN TEXT

C:

1.??if?(a> MEMORY)

2.??? ? ? ? do1;

3.??? ? do2;

4.??? ??if?(a> MEMORY)

5.??? ? ? ? do3;

這里完全符合上面我總結出來的兩個條件,所以也應該使用?ACCESS_ONCE()。正如?Linus?所說,不是編譯器一定會這么優化,而是你無法證明它不會做這樣的優化。

總結

以上是生活随笔為你收集整理的rcu_assign_pointer、rcu_dereference、ACCESS_ONCE的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。