『壹』 新手求教linux下的原子操作該怎麼寫
linux中關於原子操作
2016年08月02日
原子操作:就是在執行某一操作時不被打斷。
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版本以下的實現。
該文件的主要結構如下:
#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>
這樣的安排是依據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源碼分析
/*
*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");
}
源碼分析:
注意:根據內聯匯編的語法,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的事情隨時都可能發生。
A:將該物理地址標記為CPU0獨占訪問,並清除CPU0對其他任何物理地址的任何獨占訪問標記。
B:標記此物理地址為CPU1獨占訪問,並清除CPU1對其他任何物理地址的任何獨占訪問標記。
C:再次標記此物理地址為CPU0獨占訪問,並清除CPU0對其他任何物理地址的任何獨占訪問標記。
D:已被標記為CPU0獨占訪問,進行存儲並清除獨占訪問標記,並返回0(操作成功)。
E:沒有標記為CPU1獨占訪問,不會進行存儲,並返回1(操作失敗)。
F:沒有標記為CPU0獨占訪問,不會進行存儲,並返回1(操作失敗)。
當然還有其他許多復雜的可能,也可以通過ldrex/strex指令的機制分析出來。從上面列舉的分析中,我們可以看出:ldrex/strex可以保證在任何情況下(包括被中斷)的訪問原子性。所以內核中ARM構架中的原子操作是可以信任的。
『貳』 Linux內核空間內存動態申請
在Linux內核空間中申請內存涉及的函數主要包括kmalloc () 、_get_free _pages ()和vmalloc(等。kmalloc()和_get_free pages ()(及其類似函數)申請的內存位於DMA和常規區域的映射區,而且在物理上也是連續的,它們與真實的物理地址只有一個固定的偏移,因此存在較簡單的轉換關系。而vmalloc()在虛擬內存空間給出一塊連續的內存區,實質上,這片連續的虛擬內存在物理內存中並不一定連續,而vmalloc ()申請的虛擬內存和物理內存之間也沒有簡單的換算關系。
1.kmalloc ( )
給kmalloc() 的第一個參數是要分配的塊的大小;第二個參數為分配標志,用於控制kmalloc ()的行為。最常用的分配標志是GFP_KERNEL,其含義是在內核空間的進程中申請內存。kmalloc ()的底層依賴於_get_free pages ()來實現,分配標志的前綴GFP正好是這個底層函數的縮寫。使用GFP_KERNEL標志申請內存時,若暫時不能滿足,則進程會睡眠等待頁,即會引起阻塞,因此不能在中斷上下文或持有自旋鎖的時候使用GFP_KERNE申請內存。由於在中斷處理函數、tasklet和內核定時器等非進程上下文中不能阻塞,所以此時驅動應當使用GFP_ATOMIC標志來申請內存。當使用GFP_ATOMIC標志申請內存時,若不存在空閑頁,則不等待,直接返回。
其他的申請標志還包括GFP_USER(用來為用戶空間頁分配內存,可能阻塞)、GFP_HIGHUSER(類似GFP_USER,但是它從高端內存分配)、GFP_DMA(從DMA區域分配內存)、GFP_NOIO(不允許任何IO初始化)、GFP_NOFS(不允許進行任何文件系統調用)、__GFP_ HIGHMEM(指示分配的內存可以位於高端內存)、__(GFP COLD(請求一個較長時間不訪問的頁)、_GFP_NOWARN(當一個分配無法滿足時,阻止內核發出警告)、_GFP_HIGH(高優先順序請求,允許獲得被內核保留給緊急狀況使用的最後的內存頁)、GFP_REPEAT(分配失敗,則盡力重復嘗試)、_GFP_NOFAIL(標志只許申請成功,不推薦)和__GFPNORETRY(若申請不到,則立即放棄)等。
使用kmalloc()申請的內存應使用kfree()釋放,這個函數的用法和用戶空間的free()類似。
2._get_free_pages ()
_get_free pages ()系列函數/宏本質上是Linux內核最底層用於獲取空閑內存的方法,因為底層的buddy演算法以2n頁為單位管理空閑內存,所以最底層的內存申請總是以2n頁為單位的。
get_free _pages ()系列函數/宏包括get_zeroed _page () 、_get_free_page ()和get_free pages () 。
__get_free_pages(unsigned int flags, unsigned int order) 該函數可分配多個頁並返回分配內存的首地址,分配的頁數為2order,分配的頁也不清零。order允許的最大值是10(即1024頁)或者11(即2048頁),這取決於具體的硬體平台。
『叄』 Linux內核中如何申請和釋放內存
1、首先打開Linux命令窗口,可使用快捷鍵Ctrl+Alt+T打開。
『肆』 linux c 什麼時候用到 atomic
1.atomic_read與atomic_set函數是原子變數的操作,就是原子讀和原子設置的作用內. 2.原子操作,就是執行操作的時候容,其數值不會被其它線程或者中斷所影響 3.原子操作是linux內核中一種同步的方式
『伍』 linux內核同步問題
Linux內核設計與實現 十、內核同步方法
手把手教Linux驅動5-自旋鎖、信號量、互斥體概述
== 基礎概念: ==
並發 :多個執行單元同時進行或多個執行單元微觀串列執行,宏觀並行執行
競態 :並發的執行單元對共享資源(硬體資源和軟體上的全局變數)的訪問而導致的竟態狀態。
臨界資源 :多個進程訪問的資源
臨界區 :多個進程訪問的代碼段
== 並發場合: ==
1、單CPU之間進程間的並發 :時間片輪轉,調度進程。 A進程訪問列印機,時間片用完,OS調度B進程訪問列印機。
2、單cpu上進程和中斷之間並發 :CPU必須停止當前進程的執行中斷;
3、多cpu之間
4、單CPU上中斷之間的並發
== 使用偏向: ==
==信號量用於進程之間的同步,進程在信號量保護的臨界區代碼裡面是可以睡眠的(需要進行進程調度),這是與自旋鎖最大的區別。==
信號量又稱為信號燈,它是用來協調不同進程間的數據對象的,而最主要的應用是共享內存方式的進程間通信。本質上,信號量是一個計數器,它用來記錄對某個資源(如共享內存)的存取狀況。它負責協調各個進程,以保證他們能夠正確、合理的使用公共資源。它和spin lock最大的不同之處就是:無法獲取信號量的進程可以睡眠,因此會導致系統調度。
1、==用於進程與進程之間的同步==
2、==允許多個進程進入臨界區代碼執行,臨界區代碼允許睡眠;==
3、信號量本質是==基於調度器的==,在UP和SMP下沒有區別;進程獲取不到信號量將陷入休眠,並讓出CPU;
4、不支持進程和中斷之間的同步
5、==進程調度也是會消耗系統資源的,如果一個int型共享變數就需要使用信號量,將極大的浪費系統資源==
6、信號量可以用於多個線程,用於資源的計數(有多種狀態)
==信號量加鎖以及解鎖過程:==
sema_init(&sp->dead_sem, 0); / 初始化 /
down(&sema);
臨界區代碼
up(&sema);
==信號量定義:==
==信號量初始化:==
==dowm函數實現:==
==up函數實現:==
信號量一般可以用來標記可用資源的個數。
舉2個生活中的例子:
==dowm函數實現原理解析:==
(1)down
判斷sem->count是否 > 0,大於0則說明系統資源夠用,分配一個給該進程,否則進入__down(sem);
(2)__down
調用__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);其中TASK_UNINTERRUPTIBLE=2代表進入睡眠,且不可以打斷;MAX_SCHEDULE_TIMEOUT休眠最長LONG_MAX時間;
(3)list_add_tail(&waiter.list, &sem->wait_list);
把當前進程加入到sem->wait_list中;
(3)先解鎖後加鎖;
進入__down_common前已經加鎖了,先把解鎖,調用schele_timeout(timeout),當waiter.up=1後跳出for循環;退出函數之前再加鎖;
Linux內核ARM構架中原子變數的底層實現研究
rk3288 原子操作和原子位操作
原子變數適用於只共享一個int型變數;
1、原子操作是指不被打斷的操作,即它是最小的執行單位。
2、最簡單的原子操作就是一條條的匯編指令(不包括一些偽指令,偽指令會被匯編器解釋成多條匯編指令)
==常見函數:==
==以atomic_inc為例介紹實現過程==
在Linux內核文件archarmincludeasmatomic.h中。 執行atomic_read、atomic_set這些操作都只需要一條匯編指令,所以它們本身就是不可打斷的。 需要特別研究的是atomic_inc、atomic_dec這類讀出、修改、寫回的函數。
所以atomic_add的原型是下面這個宏:
atomic_add等效於:
result(%0) tmp(%1) (v->counter)(%2) (&v->counter)(%3) i(%4)
注意:根據內聯匯編的語法,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)再次執行。
Spinlock 是內核中提供的一種比較常見的鎖機制,==自旋鎖是「原地等待」的方式解決資源沖突的==,即,一個線程獲取了一個自旋鎖後,另外一個線程期望獲取該自旋鎖,獲取不到,只能夠原地「打轉」(忙等待)。由於自旋鎖的這個忙等待的特性,註定了它使用場景上的限制 —— 自旋鎖不應該被長時間的持有(消耗 CPU 資源),一般應用在==中斷上下文==。
1、spinlock是一種死等機制
2、信號量可以允許多個執行單元進入,spinlock不行,一次只能允許一個執行單元獲取鎖,並且進入臨界區,其他執行單元都是在門口不斷的死等
3、由於不休眠,因此spinlock可以應用在中斷上下文中;
4、由於spinlock死等的特性,因此臨界區執行代碼盡可能的短;
==spinlock加鎖以及解鎖過程:==
spin_lock(&devices_lock);
臨界區代碼
spin_unlock(&devices_lock);
==spinlock初始化==
==進程和進程之間同步==
==本地軟中斷之間同步==
==本地硬中斷之間同步==
==本地硬中斷之間同步並且保存本地中斷狀態==
==嘗試獲取鎖==
== arch_spinlock_t結構體定義如下: ==
== arch_spin_lock的實現如下: ==
lockval(%0) newval(%1) tmp(%2) &lock->slock(%3) 1 << TICKET_SHIFT(%4)
(1)ldrex %0, [%3]
把lock->slock的值賦值給lockval;並且(分別在Local monitor和Global monitor中)設置獨占標志。
(2)add %1, %0, %4
newval =lockval +(1<<16); 相當於next+1;
(3)strex %2, %1, [%3]
newval =lockval +(1<<16); 相當於next+1;
意思是將newval保存到 &lock->slock指向的內存中, 此時 Exclusive monitors會發揮作用,將保存是否成功的標志放入tmp中。
(4) teq %2, #0
測試strex是否成功
(5)bne 1b
如果發現strex失敗,從(1)再次執行。
通過上面的分析,可知關鍵在於strex的操作是否成功的判斷上。而這個就歸功於ARM的Exclusive monitors和ldrex/strex指令的機制。
(6)while (lockval.tickets.next != lockval.tickets.owner)
如何lockval.tickets的next和owner是否相等。相同則跳出while循環,否則在循環內等待判斷;
* (7)wfe()和smp_mb() 最終調用#define barrier() asm volatile ("": : :"memory") *
阻止編譯器重排,保證編譯程序時在優化屏障之前的指令不會在優化屏障之後執行。
== arch_spin_unlock的實現如下: ==
退出鎖時:tickets.owner++
== 出現死鎖的情況: ==
1、擁有自旋鎖的進程A在內核態阻塞了,內核調度B進程,碰巧B進程也要獲得自旋鎖,此時B只能自旋轉。 而此時搶占已經關閉,(單核)不會調度A進程了,B永遠自旋,產生死鎖。
2、進程A擁有自旋鎖,中斷到來,CPU執行中斷函數,中斷處理函數,中斷處理函數需要獲得自旋鎖,訪問共享資源,此時無法獲得鎖,只能自旋,產生死鎖。
== 如何避免死鎖: ==
1、如果中斷處理函數中也要獲得自旋鎖,那麼驅動程序需要在擁有自旋鎖時禁止中斷;
2、自旋鎖必須在可能的最短時間內擁有
3、避免某個獲得鎖的函數調用其他同樣試圖獲取這個鎖的函數,否則代碼就會死鎖;不論是信號量還是自旋鎖,都不允許鎖擁有者第二次獲得這個鎖,如果試圖這么做,系統將掛起;
4、鎖的順序規則(a) 按同樣的順序獲得鎖;b) 如果必須獲得一個局部鎖和一個屬於內核更中心位置的鎖,則應該首先獲取自己的局部鎖 ;c) 如果我們擁有信號量和自旋鎖的組合,則必須首先獲得信號量;在擁有自旋鎖時調用down(可導致休眠)是個嚴重的錯誤的;)
== rw(read/write)spinlock: ==
加鎖邏輯:
1、假設臨界區內沒有任何的thread,這個時候任何的讀線程和寫線程都可以鍵入
2、假設臨界區內有一個讀線程,這時候信賴的read線程可以任意進入,但是寫線程不能進入;
3、假設臨界區有一個寫線程,這時候任何的讀、寫線程都不可以進入;
4、假設臨界區內有一個或者多個讀線程,寫線程不可以進入臨界區,但是寫線程也無法阻止後續的讀線程繼續進去,要等到臨界區所有的讀線程都結束了,才可以進入,可見:==rw(read/write)spinlock更加有利於讀線程;==
== seqlock(順序鎖): ==
加鎖邏輯:
1、假設臨界區內沒有任何的thread,這個時候任何的讀線程和寫線程都可以鍵入
2、假設臨界區內沒有寫線程的情況下,read線程可以任意進入;
3、假設臨界區有一個寫線程,這時候任何的讀、寫線程都不可以進入;
4、假設臨界區內只有read線程的情況下,寫線程可以理解執行,不會等待,可見:==seqlock(順序鎖)更加有利於寫線程;==
讀寫速度 : CPU > 一級緩存 > 二級緩存 > 內存 ,因此某一個CPU0的lock修改了,其他的CPU的lock就會失效;那麼其他CPU就會依次去L1 L2和主存中讀取lock值,一旦其他CPU去讀取了主存,就存在系統性能降低的風險;
mutex用於互斥操作。
互斥體只能用於一個線程,資源只有兩種狀態(佔用或者空閑)
1、mutex的語義相對於信號量要簡單輕便一些,在鎖爭用激烈的測試場景下,mutex比信號量執行速度更快,可擴展
性更好,
2、另外mutex數據結構的定義比信號量小;、
3、同一時刻只有一個線程可以持有mutex
4、不允許遞歸地加鎖和解鎖
5、當進程持有mutex時,進程不可以退出。
• mutex必須使用官方API來初始化。
• mutex可以睡眠,所以不允許在中斷處理程序或者中斷下半部中使用,例如tasklet、定時器等
==常見操作:==
struct mutex mutex_1;
mutex_init(&mutex_1);
mutex_lock(&mutex_1)
臨界區代碼;
mutex_unlock(&mutex_1)
==常見函數:==
=