前言
這個是在面試的時候遇到的問題,當(dāng)時沒有答出來。回到家以后查了查,整理記錄下來。 原問題:什么指令集支持原子操作?其原理是什么?如果考慮到全部的指令集,問題太大了,這里簡化下。以X86和ARM為例。 原子操作是不可分割的操作,在執(zhí)行完畢時它不會被任何事件中斷。在單處理器系統(tǒng)(UniProcessor,簡稱 UP)中,能夠在單條指令中完成的操作都可以認為是原子操作,因為中斷只能發(fā)生在指令與指令之間。 比如,C語言代碼 ? 如果未經(jīng)優(yōu)化,有可能生成如下匯編: ? ? 這樣在有多個進程執(zhí)行這段代碼時,就有可能產(chǎn)生并發(fā)問題: ? ? 這就會出現(xiàn)問題。 在單處理器中,解決這個問題的方法是,將count++語句翻譯成單指令操作 X86指令集支持inc操作,這樣count操作可以在一條指內(nèi)完成。 進程的上下文切換總是在一條指令執(zhí)行之后完成,所以不會出現(xiàn)上述的并發(fā)問題。對于單處理器來說,一條處理器指令就是一個原子操作。 同樣,ARM里的SWP和X86里的XCHG都是對于單處理器來說,是原子操作。 但是,在多處理器系統(tǒng)(Symmetric Multi-Processor,簡稱 SMP)中情況有所不同,由于系統(tǒng)中有多個處理器在獨立的運行,即使在能單條指令中完成的操作也可能受到干擾。因為這個時候并發(fā)的主題不再是進程,而是處理器。
X86架構(gòu)
Intel X86指令集提供了指令前綴lock用于鎖定前端串行總線FSB,保證了指令執(zhí)行時不會收到其他處理器的干擾。 比如: 使用lock指令前綴之后,處理期間對count內(nèi)存的并發(fā)訪問(Read/Write)被禁止,從而保證了指令的原子性。 如圖所示: X86LOCK 其原理在Intel開發(fā)手冊有如下說明:
在執(zhí)行伴隨的指令期間使處理器的LOCK#信號有效(將指令變?yōu)樵又噶睿T诙嗵幚砥鳝h(huán)境中,LOCK#信號確保處理器在信號有效時獨占使用任何共享存儲器。 LOCK前綴只能附加在下面的指令之前,并且只適用于那些目標(biāo)操作數(shù)是內(nèi)存操作數(shù)的指令格式:ADD,ADC,AND,BTC,BTR,BTS,CMPXCHG,CMPXCH8B,CMPXCHG16B,DEC,INC, NEG,NOT,OR,SBB,SUB,XOR,XADD和XCHG。 如果LOCK前綴與這些指令之一一起使用,并且源操作數(shù)是內(nèi)存操作數(shù),則可能會生成未定義的操作碼異常(#UD)。如果LOCK前綴與任何不在上述列表中的指令一起使用,也會產(chǎn)生未定義的操作碼異常。無論是否存在LOCK前綴,XCHG指令都始終聲明LOCK#信號。 LOCK前綴通常與BTS指令一起使用,以在共享存儲器環(huán)境中的存儲器位置上執(zhí)行讀取 – 修改 – 寫入操作。 LOCK前綴的完整性不受存儲器字段對齊的影響。內(nèi)存鎖定是針對任意不對齊的字段。
操作系統(tǒng)中的實現(xiàn)
Linux源碼中對于原子自增一是如下定義的: ? LOCK_PREFIX的定義如下所示: ? 可見:在對稱多處理器架構(gòu)的情況下,LOCK_PREFIX被解釋為指令前綴lock。而對于單處理器架構(gòu),LOCK_PREFIX不包含任何內(nèi)容。 另外,對于CAS,有cmpxchg指令進行操作。代碼如下:
static__always_inlineintatomic_cmpxchg(atomic_t*v,intold,intnew) { returncmpxchg(&v->counter,old,new); } #definecmpxchg(ptr,old,new) __cmpxchg(ptr,old,new,sizeof(*(ptr))) #define__cmpxchg(ptr,old,new,size) __raw_cmpxchg((ptr),(old),(new),(size),LOCK_PREFIX) #define__raw_cmpxchg(ptr,old,new,size,lock) ({ __typeof__(*(ptr))__ret; __typeof__(*(ptr))__old=(old); __typeof__(*(ptr))__new=(new); switch(size){ case__X86_CASE_B: { volatileu8*__ptr=(volatileu8*)(ptr); asmvolatile(lock"cmpxchgb%2,%1" :"=a"(__ret),"+m"(*__ptr) :"q"(__new),"0"(__old) :"memory"); break; } case__X86_CASE_W: { volatileu16*__ptr=(volatileu16*)(ptr); asmvolatile(lock"cmpxchgw%2,%1" :"=a"(__ret),"+m"(*__ptr) :"r"(__new),"0"(__old) :"memory"); break; } case__X86_CASE_L: { volatileu32*__ptr=(volatileu32*)(ptr); asmvolatile(lock"cmpxchgl%2,%1" :"=a"(__ret),"+m"(*__ptr) :"r"(__new),"0"(__old) :"memory"); break; } case__X86_CASE_Q: { volatileu64*__ptr=(volatileu64*)(ptr); asmvolatile(lock"cmpxchgq%2,%1" :"=a"(__ret),"+m"(*__ptr) :"r"(__new),"0"(__old) :"memory"); break; } default: __cmpxchg_wrong_size(); } __ret; })
ARM架構(gòu)
在ARM架構(gòu)下,沒有LOCK#指令,其具體實現(xiàn)如下:## ARMv6之前 早期的ARM架構(gòu)是不支持SMP的,這些單核架構(gòu)的CPU實現(xiàn)原子操作的方式就是通過關(guān)閉CPU中斷來完成的。 在Linux對于ARM架構(gòu)的代碼下 有如下: ? 這個是好多操作共用的一套代碼。 對于cmpxchg: ? 可以看到,對v->counter的操作是一個臨界區(qū),指令的執(zhí)行不能被打斷,內(nèi)存的訪問也需要保持沒有干擾。 ARMv6以前的版本通過關(guān)本地中斷來保護這塊臨界區(qū),看起來相當(dāng)簡單,其奧秘就在于ARMv6以前的版本不支持SMP。 比如經(jīng)典的read-modify-write問題,其本質(zhì)是保持一個對內(nèi)存read和write訪問的原子性問題,也就是說內(nèi)存的讀和寫的訪問不能被打斷。對該問題的解決可以通過硬件、軟件或者軟硬件結(jié)合的方法來進行。 早期的ARM CPU給出的方案就是依賴硬件:SWP這個匯編指令執(zhí)行了一次讀內(nèi)存操作、一次寫內(nèi)存操作,但是從程序員的角度看,SWP這條指令就是原子的,讀寫之間不會被任何的異步事件打斷。具體底層的硬件是如何做的呢?這時候,硬件會提供一個lock signal,在進行memory操作的時候設(shè)定lock信號,告訴總線這是一個不可被中斷的內(nèi)存訪問,直到完成了SWP需要進行的兩次內(nèi)存訪問之后再clear lock信號。 多說一點關(guān)于SWP和SWPB的內(nèi)容 這兩個指令是用來同步的,不是用來執(zhí)行原子操作的。在將獨占訪問引入ARM架構(gòu)之前,SWP和SWPB指令常用于同步。 其局限性是:如果中斷在觸發(fā)交換操作時觸發(fā),則處理器必須在執(zhí)行中斷之前完成指令的加載和存儲部分,從而增加中斷延遲。由于獨立加載和獨占存儲是單獨的指令,因此在使用新的同步基元時會降低此效果。 但是在多核系統(tǒng)中,交換指令期間阻止所有處理器訪問主存會降低系統(tǒng)性能。在處理器工作在不同頻率但是共享相同主存的多核系統(tǒng)中,情況尤其如此。 所以在ARMv6及以后的版本中,棄用了SWP,ARMv6架構(gòu)引入了獨占訪問內(nèi)存為止的概念,提供了更靈活的原子內(nèi)存更新。 ARMv6體系結(jié)構(gòu)以Load-Exclusive和Store-Exclusive同步原語LDREX和STREX的形式引入了Load Link和Store Conditional指令。從ARMv6T2開始,這些指令在ARM和Thumb指令集中可用。獨立加載和專有存儲提供了靈活和可擴展的同步,取代了棄用的SWP和SWPB指令。 后來使用的是LDREX和STREX指令,在armv7之后就用了ldrex和strex: ? 訪存指令LDREX/STREX和普通的LDR/STR訪存指令不一樣,它是“獨占”訪存指令。這對指令訪存過程由一個稱作“exclusive monitor”的部件來監(jiān)視是否可以進行獨占訪問。 ? 獨占訪存指令: (1)LDREX R1 ,[R0] 指令是以獨占的方式從R0所指的地址中取一個字存放到R0中; (2)STREX R2,R1,[R0] 指令是以獨占的方式用R1來更新內(nèi)存,如果獨占訪問條件允許,則更新成功并返回0到R2,否則失敗返回1到R2。 最后,大家知道答案嗎?
-
處理器
+關(guān)注
關(guān)注
68文章
19259瀏覽量
229649 -
C語言
+關(guān)注
關(guān)注
180文章
7604瀏覽量
136683 -
指令集
+關(guān)注
關(guān)注
0文章
222瀏覽量
23378
原文標(biāo)題:對 int 變量賦值的操作是原子的嗎?
文章出處:【微信號:技術(shù)讓夢想更偉大,微信公眾號:技術(shù)讓夢想更偉大】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論