Linux內(nèi)核同步機(jī)制原子操作
變量的修改
我們平時(shí)的編程中可能經(jīng)常需要修改變量和寄存器,大概是這樣操作的:
讀一個(gè)位于memory中的變量的值然后寫到寄存器中修改該變量的值將寄存器中的值寫回memory中的變量值 如果這三個(gè)步驟是串行化的,并且是在一個(gè)線程中串行執(zhí)行,那么這樣做是沒有問題的,然而,世界中的事情總是不能如你所愿。在多CPU體系架構(gòu)中,運(yùn)行在兩個(gè)CPU上的兩個(gè)內(nèi)核控制路徑同時(shí)并行執(zhí)行上面操作序列,有可能發(fā)生下面的場(chǎng)景:CPU和內(nèi)存是通過總線進(jìn)行互聯(lián)的,在任意時(shí)刻,只能有一個(gè)CPU訪問內(nèi)存。因此,來自兩個(gè)CPU上的讀內(nèi)存操作被串行化執(zhí)行,分別獲得了同樣的舊值。完成修改后,兩個(gè)CPU都想進(jìn)行寫操作,把修改之后的值寫回到內(nèi)存。但是,CPU的寫回操作也必須是串行化的,因此CPU1首先獲得了訪問權(quán),進(jìn)行寫回動(dòng)作,隨后,CPU2完成寫回動(dòng)作。在這種情況下,CPU1的對(duì)內(nèi)存的修改被CPU2的寫操作覆蓋了,因此執(zhí)行結(jié)果是錯(cuò)誤的。
不僅是多CPU會(huì)存在這種問題,在單CPU上也會(huì)由于內(nèi)核控制路徑的交錯(cuò)導(dǎo)致上面的錯(cuò)誤。一個(gè)簡(jiǎn)單的例子就是中斷:
(資料圖)
系統(tǒng)調(diào)用的控制路徑上,完成讀操作之后,硬件觸發(fā)中斷,開始執(zhí)行中斷處理函數(shù)。中斷處理函數(shù)的寫回操作被系統(tǒng)調(diào)用控制路徑上的寫回操作覆蓋了,導(dǎo)致結(jié)果不一致。
正確的操作
對(duì)于那些有多個(gè)內(nèi)核控制路徑進(jìn)行讀-修改-寫回的變量,內(nèi)核提供了一個(gè)特殊的類型atomic_t,具體定義如下:
typedef struct { int counter;} atomic_t;
從定義上來看,atomic_t實(shí)際上就是一個(gè)int類型的變量counter,內(nèi)核中定義了很多關(guān)于atomic_xxx的接口函數(shù),這些函數(shù)只會(huì)接收atomic_t類型的參數(shù)。這樣就確保了atomic_xxx的函數(shù)只會(huì)操作atomic_t類型的數(shù)據(jù)。
內(nèi)核中具體的接口API函數(shù)如下:
接口函數(shù) | 功能描述 |
---|---|
staticinline void atomic_add(int i, atomic_t *v) | 原子變量v增加i |
static inline void atomic_sub(int i, atomic_t *v) | 原子變量v減去i |
static inline void atomic_inc(atomic_t *v) | 原子變量增加1 |
static inline void atomic_dec(atomic_t *v) | 原子變量減去1 |
static inline int atomic_read(const atomic_t *v) | 讀取原子變量的值 |
static inline void atomic_set(atomic_t *v, int i) | 設(shè)置原子變量的值 |
static inline int atomic_dec_and_test(atomic_t *v) | 原子變量的值減去1,判斷原子變量的值是否等于0 |
static inline int atomic_cmpxchg(atomic_t *v, int oldval, int newval) | 比較oldval的值和原子變量v的值是否相等,如果相等,把newval的值賦值給原子變量v |
底層實(shí)現(xiàn)原理
ARMv6之前的CPU并不支持SMP架構(gòu),之后的ARM架構(gòu)都是支持SMP架構(gòu)的。內(nèi)核中關(guān)于原子操作的實(shí)現(xiàn)通過#if __LINUX_ARM_ARCH__ >= 6
條件變量進(jìn)行區(qū)分。ARMv6之前的實(shí)現(xiàn)原理是通過關(guān)閉CPU中斷實(shí)現(xiàn)的,ARMv6之后的實(shí)現(xiàn)是通過新增加的兩個(gè)CPU指令ldrex、strex
實(shí)現(xiàn)的。 通過下面的代碼可以具體的看到實(shí)現(xiàn)的細(xì)節(jié):
prefetchhw
是預(yù)取操作和cache有關(guān),主要是為了提高性能。__volatile__
主要是用來防止編譯器優(yōu)化的。在編譯c代碼的時(shí)候,如果使用優(yōu)化選項(xiàng)(-O)進(jìn)行編譯,對(duì)于那些沒有聲明__volatile__
的嵌入式匯編代碼,編譯器有可能會(huì)對(duì)其進(jìn)行編譯優(yōu)化,編譯的結(jié)果可能不是原來的匯編代碼,有了__volatile__
之后,編譯器就會(huì)停止對(duì)該段代碼的任何優(yōu)化。
獨(dú)占訪問指令ldrex和strex
ldrex/strex是ARMv6架構(gòu)及之后架構(gòu)的同步原語(yǔ),屬于硬件層面的同步機(jī)制。只要某個(gè)時(shí)刻只允許一個(gè)執(zhí)行單元訪問共享資源那么就必須進(jìn)行同步,共享資源可以是內(nèi)存、外設(shè)設(shè)備,執(zhí)行單元可以是處理器、進(jìn)程或者線程。
ldrex/strex這兩個(gè)指令配合獨(dú)占監(jiān)控器(獨(dú)占監(jiān)控器會(huì)跟蹤獨(dú)占內(nèi)存訪問)可以實(shí)現(xiàn)原子地更新內(nèi)存數(shù)據(jù)。
ldrex R1, [R0]ldrex指令從R0寄存器表示的地址中讀取一個(gè)字,存放在R1寄存器中,并且更新獨(dú)占監(jiān)控器狀態(tài)為獨(dú)占狀態(tài)
strex < Rd >, < Rt >, [< Rn >]strex指令存儲(chǔ)一個(gè)字到內(nèi)存中,但是這個(gè)存儲(chǔ)指令是有條件的,如果獨(dú)占監(jiān)控器允許這個(gè)存儲(chǔ)操作,那么對(duì)應(yīng)的內(nèi)存地址就會(huì)更新,并且將返回值0保存在目標(biāo)寄存器中,代表此次操作成功。如果獨(dú)占監(jiān)控器不允許存儲(chǔ)操作,那么就不會(huì)更新獨(dú)占監(jiān)控器,并且將返回值1保存在目標(biāo)寄存器中,代表此次操作失敗。
獨(dú)占監(jiān)控器
在上面的描述中我們提到獨(dú)占監(jiān)控器,獨(dú)占監(jiān)控器是一種簡(jiǎn)單的狀態(tài)機(jī),有兩種狀態(tài):打開或者獨(dú)占。為了實(shí)現(xiàn)多個(gè)處理器間的同步,一般會(huì)存在兩類獨(dú)占監(jiān)控器:本地監(jiān)控器和全局監(jiān)控器。
"1: ldrex %0, [%3]\\n"
其中%3
就是input operand list
中的"r" (&v->counter
),r是限制符(constraint
),用來告訴編譯器gcc
,選擇一個(gè)通用寄存器保存該操作數(shù)。%0
對(duì)應(yīng)output openrand list
中的"=&r" (result
),=
表示該操作數(shù)是write only
的,&表示該操作數(shù)是一個(gè)earlyclobber operand
,編譯器在處理嵌入式匯編的時(shí)候,傾向于使用盡可能少的寄存器,如果output operand
沒有&修飾的話,匯編指令中的input
和output
操作數(shù)會(huì)使用同一個(gè)寄存器。&確保了%3
和%0
使用不同的寄存器?,F(xiàn)在%0
這個(gè)output
操作數(shù)已經(jīng)被賦值為atomic_t
變量的old value
,毫無疑問,這里的操作是要給old value
加上i
。這里%4
對(duì)應(yīng)"Ir" (i
),這里“I”表示這是一個(gè)有特定限制的立即數(shù),該數(shù)必須是0~255之間的一個(gè)整數(shù)通過rotation
的操作得到的一個(gè)32bit的立即數(shù)。每個(gè)指令32個(gè)bit,其中12個(gè)bit被用來表示立即數(shù),其中8個(gè)bit是真正的數(shù)據(jù),4個(gè)bit用來表示如何rotation
。這一步將修改后的new value
保存在atomic_t
變量中。是否能夠正確操作的狀態(tài)標(biāo)記保存在%1
操作數(shù)中,也就是"=&r" (tmp
)。最后檢查memory update
的操作是否正確完成,如果發(fā)生了問題,需要跳轉(zhuǎn)到lable 1
那里,重新進(jìn)行一次read-modify-write
的操作。
#define ATOMIC_OP(op, c_op, asm_op) \\static inline void atomic_##op(int i, atomic_t *v) \\{ \\ unsigned long tmp; \\ int result; \\ \\ prefetchw(&v- >counter); \\ __asm__ __volatile__("@ atomic_" #op "\\n" \\"1: ldrex %0, [%3]\\n" \\" " #asm_op " %0, %0, %4\\n" \\" strex %1, %0, [%3]\\n" \\" teq %1, #0\\n" \\" bne 1b" \\ : "=&r" (result), "=&r" (tmp), "+Qo" (v- >counter) \\ : "r" (&v- >counter), "Ir" (i) \\ : "cc"); \\}
#define ATOMIC_OP(op, c_op, asm_op) \\static inline void atomic_##op(int i, atomic_t *v) \\{ \\ unsigned long flags; \\ \\ raw_local_irq_save(flags); \\ v- >counter c_op i; \\ raw_local_irq_restore(flags); \\}
總結(jié)
本篇主要介紹了Linux內(nèi)核的同步機(jī)制之一原子操作,從原子的操作的API接口到原子操作的底層實(shí)現(xiàn)原理,進(jìn)行了簡(jiǎn)單分析。
關(guān)鍵詞: