linux原子變數
『壹』 新手求教linux下的原子操作該怎麼寫
linux中關於原子操作
2016年08月02日
- 一.整型原子操作定義於#include<asm/atomic.h>分為 定義,獲取,加減,測試,返回。void atomic_set(atomic_t *v,int i); //設置原子變數v的值為iatomic_t v = ATOMIC_INIT(0); //定義原子變數v,並初始化為0;atomic_read(atomic_t* v); //返回原子變數v的值;void atomic_add(int i, atomic_t* v); //原子變數v增加i;void atomic_sub(int i, atomic_t* v); void atomic_inc(atomic_t* v); //原子變數增加1;void atomic_dec(atomic_t* v);int atomic_inc_and_test(atomic_t* v); //先自增1,然後測試其值是否為0,若為0,則返回true,否則返回false;int atomic_dec_and_test(atomic_t* v); int atomic_sub_and_test(int i, atomic_t* v); //先減i,然後測試其值是否為0,若為0,則返回true,否則返回false;注意:只有自加,沒有加操作int atomic_add_return(int i, atomic_t* v); //v的值加i後返回新的值;int atomic_sub_return(int i, atomic_t* v); int atomic_inc_return(atomic_t* v); //v的值自增1後返回新的值;int atomic_dec_return(atomic_t* v);二.位原子操作定義於#include<asm/bitops.h>分為 設置,清除,改變,測試void set_bit(int nr, volatile void* addr); //設置地址addr的第nr位,所謂設置位,就是把位寫為1;void clear_bit(int nr, volatile void* addr); //清除地址addr的第nr位,所謂清除位,就是把位寫為0;void change_bit(int nr, volatile void* addr); //把地址addr的第nr位反轉;int test_bit(int nr, volatile void* addr); //返回地址addr的第nr位;int test_and_set_bit(int nr, volatile void* addr);//測試並設置位;若addr的第nr位非0,則返回true; 若addr的第nr位為0,則返回false;int test_and_clear_bit(int nr, volatile void* addr);//測試並清除位;int test_and_change_bit(int nr, volatile void* addr);//測試並反轉位;上述操作等同於先執行test_bit(nr,voidaddr)然後在執行xxx_bit(nr,voidaddr)
- 舉個簡單例子:為了實現設備只能被一個進程打開,從而避免競態的出現static atomic_t scull_available = ATOMIC_INIT(1);//init atomic在scull_open 函數和scull_close函數中:int scull_open(struct inode *inode, struct file *filp){ struct scull_dev *dev; // device information dev = container_of(inode->i_cdev, struct scull_dev, cdev); filp->private_data = dev;// for other methods if(!atomic_dec_and_test(&scull_available)){ atomic_inc(&scull_available); return -EBUSY; } return 0; // success}int scull_release(struct inode *inode, struct file *filp){ atomic_inc(&scull_available); return 0;}
#if__LINUX_ARM_ARCH__>=6
......(通過ldrex/strex指令的匯編實現)
#else/*ARM_ARCH_6*/
#ifdef CONFIG_SMP
#errorSMPnotsupportedonpre-ARMv6 CPUs
#endif
......(通過關閉CPU中斷的c語言實現)
#endif/*__LINUX_ARM_ARCH__*/
......
#ifndef CONFIG_GENERIC_ATOMIC64
......(通過ldrexd/strexd指令的匯編實現的64bit原子變數的訪問)
#else/*!CONFIG_GENERIC_ATOMIC64*/
#include<asm-generic/atomic64.h>
#endif
#include<asm-generic/atomic-long.h>
/*
*ARMv6 UP 和 SMP 安全原子操作。 我們是用獨占載入和
*獨占存儲來保證這些操作的原子性。我們可能會通過循環
*來保證成功更新變數。
*/
static inline void atomic_add(inti,atomic_t*v)
{
unsigned long tmp;
intresult;
__asm__ __volatile__("@ atomic_add "
"1: ldrex %0, [%3] "
" add %0, %0, %4 "
" strex %1, %0, [%3] "
" teq %1, #0 "
" bne 1b"
:"=&r"(result),"=&r"(tmp),"+Qo"(v->counter)
:"r"(&v->counter),"Ir"(i)
:"cc");
}
A:將該物理地址標記為CPU0獨占訪問,並清除CPU0對其他任何物理地址的任何獨占訪問標記。
B:標記此物理地址為CPU1獨占訪問,並清除CPU1對其他任何物理地址的任何獨占訪問標記。
C:再次標記此物理地址為CPU0獨占訪問,並清除CPU0對其他任何物理地址的任何獨占訪問標記。
D:已被標記為CPU0獨占訪問,進行存儲並清除獨占訪問標記,並返回0(操作成功)。
E:沒有標記為CPU1獨占訪問,不會進行存儲,並返回1(操作失敗)。
F:沒有標記為CPU0獨占訪問,不會進行存儲,並返回1(操作失敗)。
原子操作:就是在執行某一操作時不被打斷。
linux原子操作問題來源於中斷、進程的搶占以及多核smp系統中程序的並發執行。
對於臨界區的操作可以加鎖來保證原子性,對於全局變數或靜態變數操作則需要依賴於硬體平台的原子變數操作。
因此原子操作有兩類:一類是各種臨界區的鎖,一類是操作原子變數的函數。
對於arm來說,單條匯編指令都是原子的,多核smp也是,因為有匯流排仲裁所以cpu可以單獨佔用匯流排直到指令結束,多核系統中的原子操作通常使用內存柵障(memory barrier)來實現,即一個CPU核在執行原子操作時,其他CPU核必須停止對內存操作或者不對指定的內存進行操作,這樣才能避免數據競爭問題。但是對於load update store這個過程可能被中斷、搶占,所以arm指令集有增加了ldrex/strex這樣的實現load update store的原子指令。
但是linux種對於c/c++程序(一條c編譯成多條匯編),由於上述提到的原因不能保證原子性,因此linux提供了一套函數來操作全局變數或靜態變數。
假設原子變數的底層實現是由一個匯編指令實現的,這個原子性必然有保障。但是如果原子變數的實現是由多條指令組合而成的,那麼對於SMP和中斷的介入會不會有什麼影響呢?我在看ARM的原子變數操作實現的時候,發現其是由多條匯編指令(ldrex/strex)實現的。在參考了別的書籍和資料後,發現大部分書中對這兩條指令的描訴都是說他們是支持在SMP系統中實現多核共享內存的互斥訪問。但在UP系統中使用,如果ldrex/strex和之間發生了中斷,並在中斷中也用ldrex/strex操作了同一個原子變數會不會有問題呢?就這個問題,我認真看了一下內核的ARM原子變數源碼和ARM官方對於ldrex/strex的功能解釋,總結如下:
一、ARM構架的原子變數實現結構
對於ARM構架的原子變數實現源碼位於:arch/arm/include/asm/atomic.h
其主要的實現代碼分為ARMv6以上(含v6)構架的實現和ARMv6版本以下的實現。
該文件的主要結構如下:
這樣的安排是依據ARM核心指令集版本的實現來做的:
(1)在ARMv6以上(含v6)構架有了多核的CPU,為了在多核之間同步數據和控制並發,ARM在內存訪問上增加了獨占監測(Exclusive monitors)機制(一種簡單的狀態機),並增加了相關的ldrex/strex指令。請先閱讀以下參考資料(關鍵在於理解local monitor和Global monitor):
1.2.2.Exclusive monitors
4.2.12.LDREX和STREX
(2)對於ARMv6以前的構架不可能有多核CPU,所以對於變數的原子訪問只需要關閉本CPU中斷即可保證原子性。
對於(2),非常好理解。
但是(1)情況,我還是要通過源碼的分析才認同這種代碼,以下我僅僅分析最具有代表性的atomic_add源碼,其他的API原理都一樣。如果讀者還不熟悉C內嵌匯編的格式,請參考《ARM GCC內嵌匯編手冊》
二、內核對於ARM構架的atomic_add源碼分析
源碼分析:
注意:根據內聯匯編的語法,result、tmp、&v->counter對應的數據都放在了寄存器中操作。如果出現上下文切換,切換機制會做寄存器上下文保護。
(1)ldrex %0, [%3]
意思是將&v->counter指向的數據放入result中,並且(分別在Local monitor和Global monitor中)設置獨占標志。
(2)add %0, %0, %4
result = result + i
(3)strex %1, %0, [%3]
意思是將result保存到&v->counter指向的內存中,此時Exclusive monitors會發揮作用,將保存是否成功的標志放入tmp中。
(4)teq %1, #0
測試strex是否成功(tmp == 0??)
(5)bne 1b
如果發現strex失敗,從(1)再次執行。
通過上面的分析,可知關鍵在於strex的操作是否成功的判斷上。而這個就歸功於ARM的Exclusive monitors和ldrex/strex指令的機制。以下通過可能的情況分析ldrex/strex指令機制。(請閱讀時參考4.2.12.LDREX和STREX)
1、UP系統或SMP系統中變數為非CPU間共享訪問的情況
此情況下,僅有一個CPU可能訪問變數,此時僅有Local monitor需要關注。
假設CPU執行到(2)的時候,來了一個中斷,並在中斷里使用ldrex/strex操作了同一個原子變數。則情況如下圖所示:
雖然對於人來說,這種情況比較BT。但是在飛速運行的CPU來說,BT的事情隨時都可能發生。
當然還有其他許多復雜的可能,也可以通過ldrex/strex指令的機制分析出來。從上面列舉的分析中,我們可以看出:ldrex/strex可以保證在任何情況下(包括被中斷)的訪問原子性。所以內核中ARM構架中的原子操作是可以信任的。
『貳』 linux 2.6內核有關原子變數的定義問題
原子意味著不可分割,所謂原子操作就是對變數的讀寫不能被打斷的操作。
舉個簡單點兒的例子:
1. 假如在一個i386體系架構上;
2. 如果有一個進程要將一個int型的變數改成0x12345678;
3. 另一個進程也希望把這同一個變數改成0x87654321。
4. 如果這個變數的地址沒有4位元組對齊,那麼cpu要改寫它的值的話需要兩次匯流排操作。
那麼(假設下面的場景,即下列事件先後發生):
1. 第一個進程剛把高位元組寫入(x=0x1234xxxx)內存(xxxx表示不確定);
2. 第二進程就搶佔了第一個進程的運行,把第一個進程改了一半的變數改成0x87654321。(當然他也需要兩次匯流排操作,但我們假設他的優先順序比第一個進程高)
3. 第二個進程結束運行後,第一個進程又得到了調度,它並不知道自己對變數的操作被另一個進程打斷過,所以他會繼續更改變數的低位元組。
所以,最後這個變數的值就是0x87655678,顯然這是兩個進程都不想要得到的結果。通過上面的分析你應該知道問題的關鍵就在於對存儲空間的訪問被打斷了造成的。所以在內核中定義了一系列的原子操作來保證對變數的操作是「原子」的。這種互斥不是高級語言能實現的,必須用匯編,而且依賴於體系架構。對i386來說就是在讀寫變數的時候先把匯流排鎖住,你可以仔細看看ATOMIC_INIT這個宏的定義。
『叄』 原子操作的Linux
-----------------------------------------------------------
原子操作大部分使用匯編語言實現,因為c語言並不能實現這樣的操作。
* 在x86的原子操作實現代碼中,定義了LOCK宏,這個宏可以放在隨後的內聯匯編指令之前。如果是SMP,LOCK宏被擴展為lock指令;否則被定義為空 -- 單CPU無需防止其它CPU的干擾,鎖內存匯流排完全是在浪費時間。
#ifdef CONFIG_SMP
#define LOCK "lock ; "
#else
#define LOCK ""
#endif
* typedef struct { volatile int counter; } atomic_t;
在所有支持的體系結構上原子類型atomic_t都保存一個int值。在x86的某些處理器上,由於工作方式的原因,原子類型能夠保證的可用范圍只有24位。volatile是一個類型描述符,要求編譯器不要對其描述的對象作優化處理,對它的讀寫都需要從內存中訪問。
* #define ATOMIC_INIT(i) { (i) }
用於在定義原子變數時,初始化為指定的值。如:
static atomic_t count = ATOMIC_INIT⑴;
* static __inline__ void atomic_add(int i,atomic_t *v)
將v指向的原子變數加上i。該函數不關心原子變數的新值,返回void類型。
『肆』 linux 為什麼要使用原子操作
有些操作必須要具有原子性,否則會出現問題.
就拿一個變數自增來說,比如
i++,一般經歷了3步
從內存到寄存器
寄存器的值+1
從寄存器到內存
,如果有這樣一個語句:
inti=0;
i++;
i++;
如果不能保證原子性,在多核的系統中,這兩個i++可能同時運行,這樣就可能造成這個語句的結果最後,i等於1.
在操作系統裡面,必須要保證一些操作一次性完成,不被打斷
『伍』 Linux中的原子變數如何取地址,如何給定義的原子變數賦指定的地址
首先請確定你要做原子操作的對象是誰?是一個地址,還是地址指向的數據?
如果把數據做為原子對象,直接對數據進行原子操作即可,數據的指針不用做原子操作。