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