說明:內核版本號為3.10.101
一、ARM架構中的原子操作實現
在原子操作(一)中我們已經提到,各個架構組織為“復仇者”聯盟,統一了基本的原子變量操作,這里我們就拿atomic_dec(v)來看看通天ARM的實現。
首先是atomic_dec(v)原子減一操作的宏定義。這個宏的定義在文件arch/arm/include/asm/atomic.h中:
#define atomic_dec(v) atomic_sub(1, v)
對於ARM架構不同的版本,stomic_sub(i,v)的實現是不一樣的。具體而言,在ARMv6之前的版本定義如下:
#define atomic_sub(i, v) (void) atomic_sub_return(i, v)
而ARMv6以后的版本則將atomic_sub()實現為static 內聯函數,具體見第二節。
二、ARMv6以前的版本的實現
ARMv6之前的版本將atomic_sub()宏定義為atomic_sub_return()函數,其實現也在文件arch/arm/include/asm/atomic.h中。而這個atomic_sub_return()函數根據版本的不同也有兩個不同的實現。我們這里只關注ARMv6之前版本的實現。
/* ARMv6 以前的版本:不支持SMP */ static inline int atomic_sub_return(int i, atomic_t *v) { unsigned long flags; int val; raw_local_irq_save(flags); //關本地中斷 val = v->counter; v->counter = val -= i; raw_local_irq_restore(flags); //開中斷 return val; }
可以看到,對v->counter的減一操作是一個臨界區,指令的執行不能被打斷,內存的訪問也需要保持沒有干擾。
ARMv6以前的版本通過關本地中斷來保護這塊臨界區,看起來相當簡單,其奧秘就在於ARMv6以前的版本不支持SMP。
在系統不支持SMP的情況下,我們關掉本地中斷可以防止下面幾種意外:
1) 關掉本地中斷后,在對v->counter實施"減一"的過程中不會被外部中斷打斷;
2) 系統不支持SMP,可以保證在本地cpu訪問v->counter和val變量內存的過程中不會有其他cpu訪問這些內存。
問題:
1. 雖然在臨界區內不會有其他cpu訪問 v->counter和val,但是能夠保證不會有DMA操作這些內存么?
2. 雖然禁止了中斷,但是可以保證期間此cpu不會被搶占或者因為其他原因放棄調度么?
答:
1. DMA在操作內存前會通過DMA中斷、總線仲裁來與cpu的內存訪問進行協調。這里已經關掉本地中斷,且是UP系統,所以不會干擾。
2. 在UP系統中沒有內核搶占;從代碼上來看,臨界區這一段沒有主動放棄cpu;另外,我們禁止了本地中斷,也就是禁止了時鍾中斷,這樣在開中斷前就不會有機會進行調度檢查,保證臨界區在開中斷前一直運行。
三. ARMv7以后的架構
從ARMv6T2以后的版本中,ARM和Thumb指令集開始采用了新一代"獨占訪問"指令"Load-Exclusive and Store-Exclusive "來實現原子操作。獨占訪問的秘訣就在於系統中通過exclusive monitor來實現獨占訪問的監控。內核中atomic_sub()的具體實現如下所示:
/* * ARMv6 UP and SMP safe atomic ops. We use load exclusive and * store exclusive to ensure that these are atomic. We may loop * to ensure that the update happens. */ static inline void atomic_sub(int i, atomic_t *v) { unsigned long tmp; int result; __asm__ __volatile__("@ atomic_sub\n" /* 優化屏障,防止編譯器優化 */ "1: ldrex %0, [%3]\n" /*【1】獨占方式加載v->counter到result*/ " sub %0, %0, %4\n" /*【2】result減一*/ " strex %1, %0, [%3]\n" /*【3】獨占方式將result值寫回v->counter*/ " teq %1, #0\n" /*【4】判斷strex更新內存是否成*/ " bne 1b" /*【5】不成功跳轉到1:*/ : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter) /*輸出部*/ : "r" (&v->counter), "Ir" (i) /*輸入部*/ : "cc"); /*損壞部*/ }
訪存指令LDREX/STREX和普通的LDR/STR訪存指令不一樣,它是“獨占”訪存指令。這對指令訪存過程由一個稱作“exclusive monitor”的部件來監視是否可以進行獨占訪問。
先看看這對獨占訪存指令:
(1)LDREX R1 ,[R0] 指令是以獨占的方式從R0所指的地址中取一個字存放到R0中;
(2)STREX R2,R1,[R0] 指令是以獨占的方式用R1來更新內存,如果獨占訪問條件允許,則更新成功並返回0到R2,否則失敗返回1到R2。
了解LDREX和STREX的基本原理后,理一理上面atomic_sub()原子更新(減一)atomic_t * v的實現流程:
(1) 從內存中讀取v->counter值到一個寄存器中(result),並更新exclusive monitor狀態為獨占訪問;
(2)result減一操作;
(3)嘗試將result的值寫入v->counter地址,如果exclusive monitor允許獨占寫存,則修改內存成功並將tmp設置為0 ,否則tmp設置1;
(4)查看tmp的值是否為0 ,如果不為0表示上面的更新v->counter失敗,再次跳轉會(1)執行。
一旦STREX指令執行成功,就表示這次內存訪問沒有受到其他干擾,保證了內存更新操作的原子性。