這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
上一篇文章我們說到sync.Mutex的源碼實現,核心就是使用到了CPU指令CAS,從並發效能上來說atomic的效率是要高於mutex的,畢竟mutex做了不少的其他步驟,而atomic的核心其實就是和處理器密切關係的,通過一兩個指令就能完成的原子操作,我們接下來來看看atomic在golang中的一些細節。 通過目錄:
64bit_arm.go asm_amd64p32.s asm_linux_arm.s atomic_test.go race.goasm_386.s asm_arm.s asm_netbsd_arm.s doc.goasm_amd64.s asm_freebsd_arm.s atomic_linux_arm_test.go export_linux_arm_test.go
發現golang主要還是依賴彙編來來實現的原子操作,不同的CPU架構是有對應不同的.s彙編檔案的。 我們重點看下asm_amd64.s X86-64cpu架構下的實現:
TEXT ·CompareAndSwapUint64(SB),NOSPLIT,$0-25 // http://godoc.org/sync/atomic#CompareAndSwapUint64 // 函數原型是:func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool) // 把addr參數寫入BP寄存器,為什麼是+0(FP),我猜測應該是當前呼叫堆疊棧底 + 位移來實現的變數儲存,而golang中的指標應該是8個位元組 MOVQ addr+0(FP), BP // 把old參數寫入到AX寄存器,當前位移=8,因為uint64佔用8個位元組 MOVQ old+8(FP), AX // 把new參數寫入到CX寄存器,當前位移=16 MOVQ new+16(FP), CX // 使用LOCK指令,表示多核下需要匯流排鎖或者使用MESI協議來保證原子指令的原子性,後面我會重點介紹 LOCK // CMPXCHG r/m,r 將累加器AL/AX/EAX/RAX中的值與首運算元(目的運算元)比較,如果相等, // 第2運算元(源運算元)的值裝載到首運算元,zf置1。如果不等, 首運算元的值裝載到AL/AX/EAX/RAX並將zf清0 // 我們看到了CMPXCHG的定義,那麼很明顯這裡使用了AX和CX來做對比 CMPXCHGQ CX, 0(BP) // 最後我們用SETE指令來判斷ZF位,是否等於1來判斷操作時候成功,而且把值寫入到傳回值swapped中,當前位移=24 SETEQ swapped+24(FP) RET
整個彙編就結束了,其他atomic中的函數應該都是類似的,這裡不一個個分析了。
Bus Locking & MESI
下面我們重點來看看MESI協議以及匯流排鎖到底是怎麼回事。 我這裡主要使用“Intel 64 and IA-32 Architectures Software Developer’s Manual”文檔作為參考,第8章的多核管理和第11章的記憶體緩衝管理。
8.1.2 Bus Locking Intel 64 and IA-32 processors provide a LOCK# signal that is asserted automatically during certain critical memory operations to lock the system bus or equivalent link. While this output signal is asserted, requests from other processors or bus agents for control of the bus are blocked. Software can specify other occasions when the LOCK semantics are to be followed by prepending the LOCK prefix to an instruction.
8.1.2中提到匯流排鎖在Intel 64和IA-32處理器中提供了LOCK# 訊號來鎖定匯流排,來自其他處理器的請求或者是匯流排代理的匯流排控制請求全部會阻塞,我當時就非常納悶如果這樣來搞,是不是整個系統都hang住(雖然是一瞬間,單條指令足夠快),始終覺得不夠優雅,然後我驚奇的發現Intel其實提供了更優的方式。
For the P6 and more recent processor families, if the memory area being accessed is cached internally in the processor, the LOCK# signal is generally not asserted; instead, locking is only applied to the processor’s caches (see Section 8.1.4, “Effects of a LOCK Operation on Internal Processor Caches”).
P6以上的cpu家族,如果訪問的記憶體地區已經被L1/2/3之類的CPU cache緩衝過,那麼LOCK# 訊號不會觸發斷言(即不鎖匯流排),替代的使用了cpu cache協議一致性來保證指令的原子性。 我們先看11.2章提到一些緩衝相關的術語:
cache line fill(cache line填充):當cpu認為從記憶體中讀取的位元組是可以被緩衝的時候,處理器讀取記憶體填充cache line寫入到從L1/2/3;
cache hit(cache命中):當記憶體位址中的資料仍然被緩衝的時候,處理器可以直接從L1/2/3中擷取資料而不是從記憶體中讀取;
write hit(寫命中):當處理器嘗試寫記憶體資料的緩衝時,首先先檢測cache line是否存在該記憶體位址,如果不存在,處理器(根據已經生效的策略)會直接寫緩衝L1/2/3而不是記憶體;
修改態(Modified):此cache line已被修改過(髒行),內容已不同於主存並且為此cache line專有;
獨佔態(Exclusive):此cache line內容同於主存,但不出現於其它cache中;
共用態(Shared):此cache line內容同於主存,但也出現於其它cache中;
無效態(Invalid):此cache行內容無效(空行);
因為L1/2/3cpu快取的存在,我們試想時候可以利用緩衝的一些特性和狀態來保證原子操作的完成呢?答案是肯定的:
在多核系統中,IA-32cpu(i486開始)和Intel 64處理器有嗅探其他處理器訪問系統記憶體到自己的處理器快取的能力了,利用這個特性就能實現緩衝一致性了,比如在奔騰和P6家族的處理器中,通過嗅探其他處理器發現有一個處理器嘗試寫一個記憶體位址(它處於共用狀態)那麼嗅探者會立即無效它自身的cache line而且在下次訪問該記憶體位址的時候強制一個cache line fill操作。
當一個處理器通過嗅探發現另外一個處理器嘗試訪問處於modifyed狀態的記憶體位址快取(還未寫回write-back到系統記憶體的地址),那麼嗅探的處理器會發送一個HITM# 訊號給其他處理器告訴該cache line處於已經修改的狀態,而且即將會觸發一個隱式的寫回操作。
隱式的寫回操作的意思是:直接傳送到初始要求處理常式和嗅探的記憶體控制器,以確保系統儲存空間已被更新。這裡,攜帶有效資料的處理器會直接傳送給其他處理器而不是寫回系統記憶體,因為他把寫會系統記憶體的責任託管給了記憶體控制器。
11.4 The following section describes the cache control protocol currently defined for the Intel 64 and IA-32 architectures. In the L1 data cache and in the L2/L3 unified caches, the MESI (modified, exclusive, shared, invalid) cache protocol maintains consistency with caches of other processors. The L1 data cache and the L2/L3 unified caches have two MESI status flags per cache line. Each line can be marked as being in one of the states defined in Table 11-4. In general, the operation of the MESI protocol is transparent to programs.
11.4節中提到了緩衝管理協議MESI(修改,獨佔,共用,無效)來實現不同cpu之間的緩衝一致性。每個cpu cache line都有對應的兩個MESI狀態標誌位,我們看看下錶:
Cache Line 狀態 |
M(Modifed) |
E(Exclusive) |
S(Shared) |
I(Invalid) |
cache line 是否失效? |
是 |
是 |
是 |
否 |
記憶體副本(L1/2/3) |
到期 |
有效 |
有效 |
- |
副本是否存在於其他核 |
否 |
否 |
可能 |
可能 |
對cache line的寫操作 |
不經過系統匯流排 |
不經過系統匯流排 |
引起該核獨佔cache line |
直接使用系統匯流排 |
整個和sync.atomic相關的cpu知識和源碼介紹到這裡:)