iT邦幫忙

2021 iThome 鐵人賽

DAY 21
0
自我挑戰組

當你凝視linux, linux也在凝視你系列 第 21

Day21 atomic, memory barrier

  • 分享至 

  • xImage
  •  

前言

昨天講完了最後一天的記憶體管理方法,了解了如何管理匿名分頁 anonymous page,也知道了RMAP為何要存在,以及存在RMAP的好處。 只是前面講的都是以單一行程的角度來看記憶體管理,當這些資料有非常多行程需要同時管理與同時使用呢? 如果有多個行程對某個資料同時存取,該如何保證資料的正確性呢? 這個時候就需要同步管理的機制,讓行程可以依序拿到正確的資料。

atomic

Atomic Operation 若翻譯為「原子操作」,同樣會因漢語的語意而讓我們理解受限,可改稱為「最小操作」,即某個動作執行時,中間沒有辦法分割。

在開始描述最小操作前,我們先看看一個例子。

static int i = 0;

void thread_A(){
    i++;
}

void thread_B(){
    i++;
}

上述程式碼的結果可能為1也可能為2,端看兩個程式的執行順序,理由是因為 i++ 的過程,可以拆解成三個,讀取i -> i++ -> 存回i , 存在一種狀況,如果兩個thread 同時取得 i=0 的狀態,則兩個function的存回值都會是 i=1, 因此原子操作最基本的想法,就是把 "讀取-> 修改 ->寫入" 這個流程整合成一塊,必須要三個步驟都完成,才能夠讓資料開放給其他人讀取,這麼能保持資料的完整性。
在Linux kernel 內有 atomic_t 的結構維護最小操作

<include/linux/type.h>

type def struct{
    int counter;
}atomic_t;

以下說說幾種 Linux內提供的基本最小操作函數

1. 基本的最小操作函數

<include/asm-generic/atomic.h>

void ATOMIC_INIT(i)                  //定義某個變量為 atomic_t
void atomic_set(atomic_t *v, int i); //設置v值為i
int atomic_read(atomic_t *v);        //讀取v得值

atomic_set() , atomic_read() 這兩個函數可以利用 READ_ONCE()WRITE_ONCE() 達成。只是讓我們看看 READ_ONCE() WRITE_ONCE()的註釋

Prevent the compiler from merging or refetching reads or writes. The compiler is also forbidden from reordering successive instances of READ_ONCE and WRITE_ONCE, but only when the compiler is aware of some particular ordering. One way to make the compiler aware of ordering is to put the two invocations of READ_ONCE or WRITE_ONCE in different C statements.

可以發現,這樣的最小操作函數只能應用在單核,因為 READ_ONCE() WRITE_ONCE() 這兩個函數只能禁止編譯器進行重排指令的動作,所以這邊的指令在多核就會失去效果。

2. 沒有返回值的最小操作

static inline void atomic_add(int i, atomic_t *v) 
static inline void atomic_sub(int i, atomic_t *v)
#define atomic_inc(v)			atomic_add(1, (v))
#define atomic_dec(v)			atomic_sub(1, (v))
atomic_{and, or, xor}(int i, atomic_t *v))


#define ATOMIC_OP(op, c_op)						\
static inline void atomic_##op(int i, atomic_t *v)			\
{									\
	int c, old;							\
									\
	c = v->counter;							\
	while ((old = cmpxchg(&v->counter, c, c c_op i)) != c)		\
		c = old;						\
}

上述的函數就可以解決多處理器同時存取同一個資料所引發的問題了。

3. 有返回值的原子操作函數

inline function補充

The point of making a function inline is to hint to the compiler that it is worth making some form of extra effort to call the function faster than it would otherwise - generally by substituting the code of the function into its caller. As well as eliminating the need for a call and return sequence, it might allow the compiler to perform certain optimizations between the bodies of both functions.

間單來說就是因為跳到呼叫的函數的時間,比函數本身的執行時間還要長,所以直接將那部份的程式碼再編譯時期就鑲嵌進原本的程式碼中。

4. 記憶體屏障 (memory barrier)

記憶體屏障(英語:Memory barrier),也稱記憶體柵欄,記憶體柵障,屏障指令等,是一類同步屏障指令,它使得 CPU 或編譯器在對記憶體進行操作的時候, 嚴格按照一定的順序來執行, 也就是說在memory barrier 之前的指令和memory barrier之後的指令不會由於系統最佳化等原因而導致亂序。
大多數現代電腦為了提高效能而採取亂序執行,這使得記憶體屏障成為必須。
語意上,記憶體屏障之前的所有寫操作都要寫入記憶體;記憶體屏障之後的讀操作都可以獲得同步屏障之前的寫操作的結果。因此,對於敏感的程式塊,寫操作之後、讀操作之前可以插入記憶體屏障。

以下是幾個Linux kernel中的記憶體屏障函數

barrier()                       //編譯優化屏障,阻止編譯器為了性能優化而進行指令重排 
mb()                            //記憶體屏障,包含讀跟寫
rmb()                           //讀記憶體屏障
wmb()                           //寫記憶體屏障
smp_mb()                        //用於smp的記憶體屏障
smp_rmb()                       //用於SMP的讀取記憶體屏障
smp_wmb()                       //用於SMP的寫入記憶體屏障


/* The "volatile" is due to gcc bugs */
#define barrier() asm volatile("": : :"memory")
/*volatile: 告訴編譯器 barrier()周圍的指令不要優化
memory:告訴編譯器組合語言會使記憶體的值更改,編譯器該使用記憶體內的新值而不是使用暫存器內部的舊值。
*/

##補充1
##補充2

Token concatenation
The ## operator (known as the "Token Pasting Operator") concatenates two tokens into one token.

Example:

#define DECLARE_STRUCT_TYPE(name) typedef struct name##_s name##_t
DECLARE_STRUCT_TYPE(g_object); // Outputs: typedef struct g_object_s g_object_t;

並行程式設計: Atomics 操作


上一篇
Day20 Anonymous page 與 RMAP
下一篇
Day22 跟著 spinlock 旋轉吧
系列文
當你凝視linux, linux也在凝視你30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言