天天看點

《深入解析Android 5.0系統》——第6章,第6.1節原子操作

本節書摘來自異步社群《深入解析android 5.0系統》一書中的第6章,第6.1節原子操作,作者 劉超,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

6.1 原子操作

深入解析android 5.0系統

對簡單類型的全局變量進行操作時,即使是一些簡單的操作,如加法、減法等,在彙編級别上也需要多條指令才能完成。整個操作的完成需要先讀取記憶體中的值,在cpu中計算,然後再寫回記憶體中。如果中間發生了線程切換并改變了記憶體中的值,這樣最後執行的結果就會發生錯誤。避免這種問題發生的最好辦法就是使用原子操作。

原子操作中沒有使用鎖,從效率上看要比使用鎖來保護全局變量劃算。但是,原子操作也不是沒有一點性能上的代價,是以還是要盡量避免使用。

android中用彙編語言實作了一套原子操作函數,這些函數在同步機制的實作中被廣泛使用。

6.1.1 android的原子操作函數

1.原子變量的加法操作

6.原子變量的讀取

8.還有兩個原子變量的宏定義

6.1.2 原子操作的實作原理

android原子操作的實作方式和cpu的架構有密切關系,現在的原子操作一般都是在cpu指令級别實作的。這種實作方式不但簡單,而且效率非常高。

雖然原子操作的接口函數有10多個,但是,隻有兩個函數通過彙編代碼真正實作了原子操作,它們是函數android_atomic_add()和android_atomic_cas(),其他函數都隻是在内部調用它們而已。這兩個函數的原理差不多。

arm平台上的實作更複雜一點,下面以arm平台的加法函數為例來分析原子變量的實作原理:

上面的代碼中第一行使用的宏android_atomic_inline的定義如下:

這個宏的作用是把函數定義成了inline函數。

代碼中第二行調用android_memory_barrier()函數的作用表示這裡需要記憶體屏障(下節會介紹記憶體屏障)。

接下來是一段“内嵌彙編”(如果對“内嵌彙編”不了解,可以參考筆者的部落格),“内嵌彙編”比較難懂,但是可以用下面這段展開的僞代碼來表示它:

在add指令的前後有兩條看上去比較陌生的指令:ldrex和strex,這兩條是amrv6新引入的同步指令。ldrex指令的作用是将指針ptr指向的内容放到prev變量中,同時給執行處理器做一個标記(tag),标記上指針ptr的位址,表示這個記憶體位址已經有一個cpu正在通路。當執行到strex指令時,它會檢查是否存在ptr的位址标記,如果标記存在,strex指令會把add指令執行的結果寫入指針ptr指向的位址,并且傳回0,然後清除該标記。傳回的結果0将儲存在status變量中,這樣循環結束,函數傳回結果。

如果在strex指令執行前發生了線程的上下文切換,在切換回來後,ldrx指令設定的标志将會被清除。這時再執行strex指令時,由于沒有了這個标志,strex指令将不會完成對ptr指針的存儲操作,而且status變量中的傳回結果将是1。是以,循環将重新開始執行,直到成功為止。

builtin_expect()是gcc的内建函數,有兩個參數,第一個參數是一個表達式,第二個參數是一個值。表達式的計算結果也是函數的結果。builtin_expect()用來告訴gcc預測表達式更可能的值是什麼,這樣gcc會根據預測值來優化代碼。代碼中表達的含義是預測“status!=0”這個表達式的值為“0”,預測while循環将結束。

圖像說明文字提示 原子操作并沒有禁止中斷的發生或上下文切換,而是讓它們不影響操作的結果。

6.1.3 記憶體屏障和編譯屏障

現代 cpu中指令的執行次序不一定按順序執行,沒有相關性的指令可以打亂次序執行,以充分利用 cpu的指令流水線,提高執行速度。同時,編譯器也會對指令進行優化,例如,調整指令順序來利用cpu的指令流水線。這些優化方式,大部分時候都工作良好,但是在一些比較複雜的情況可能會出現錯誤,例如,執行同步代碼時就有可能因為優化導緻同步原語之後的指令在同步原語前執行。

記憶體屏障和編譯屏障就是用來告訴cpu和編譯器停止優化的手段。編譯屏障是指使用僞指令“memory”告訴編譯器不能把“memory”執行前後的代碼混淆在一起,這時“memory”起到了一種優化屏障的作用。記憶體屏障是在代碼中使用一些特殊指令,如arm中的dmb、dsb和isb指令,x86中的sfence、lfence和mfence指令。cpu遇到這些特殊指令後,要等待前面的指令執行完成才執行後面的指令。這些指令的作用就好像一道屏障把前後指令隔離開了,防止cpu把前後兩段指令颠倒執行。

(1)arm平台的記憶體屏障指令。

dsb:資料同步屏障指令。它的作用是等待所有前面的指令完成後再執行後面的指令。

dmb:資料記憶體屏障指令。它的作用是等待前面通路記憶體的指令完成後再執行後面通路記憶體的指令。

isb:指令同步屏障。它的作用是等待流水線中所有指令執行完成後再執行後面的指令。

(2)x86平台上的記憶體屏障指令。

sfence:存儲屏障指令。它的作用是等待前面寫記憶體的指令完成後再執行後面寫記憶體的指令。

lfence:讀取屏障指令。它的作用是等待前面讀取記憶體的指令完成後再執行後面讀取記憶體的指令。

mfence:混合屏障指令。它的作用是等待前面讀寫記憶體的指令完成後再執行後面讀寫記憶體的指令。

要精确地了解這些指令的含義,需要去查閱處理器的說明。這裡隻是對它們做了一點簡單的介紹。下面看看android是如何利用這些指令來實作記憶體屏障和編譯屏障的:

1.arm平台的函數代碼

(1)編譯屏障:

記憶體屏障的函數中使用了宏android_smp。它的值為0時表示是單cpu,這種情況下隻使用編譯屏障就可以了。在多cpu情況下,同時使用了記憶體屏障指令“dmb”和編譯屏障的僞指令“memory”。函數android_memory_store_barrier()中的dmb指令還使用了選項st,它表示要等待前面所有存儲記憶體的指令執行完後再執行後面的存儲記憶體的指令。

2.x86平台下的函數代碼

x86平台也一樣,如果是單cpu,記憶體屏障的實作隻使用了編譯屏障。在多cpu情況下,函數android_memory_barrier()使用了cpu指令“mfence”,對讀寫記憶體的情況都進行了屏障。但是android_memory_store_barrier()函數隻使用了編譯屏障,這是因為intel的cpu不對寫記憶體的指令重新排序。是以不需要記憶體屏蔽指令。

上一篇: 取值操作
下一篇: 前端 VS 後端

繼續閱讀