所謂原子操作,就是“不可中斷的一個或一系列操作”。
硬件級的原子操作:在單處理器系統(UniProcessor)中,能夠在單條指令中完成的操作都可以認爲是“原子操作”,因爲中斷只發生在指令邊緣。在多處理器結構中(Symmetric Multi-Processor)就不同了,由於系統中有多個處理器獨立運行,即使能在單條指令中完成的操作也有可能受到干擾。在X86平臺生,CPU提供了在指令執行期間對總線加鎖的手段。CPU上有一根引線#HLOCK pin連到北橋,如果彙編語言的程序中在一條指令前面加上前綴"LOCK",經過彙編以後的機器代碼就使CPU在執行這條指令的時候把#HLOCK pin的電位拉低,持續到這條指令結束時放開,從而把總線鎖住,這樣同一總線上別的CPU就暫時不能通過總線訪問內存了,保證了這條指令在多處理器環境中的原子性。對於其他平臺的CPU,實現各不相同,有的是通過關中斷來實現原子操作(sparc),有的通過CMPXCHG系列的指令來實現原子操作(IA64)。本文主要探討X86平臺下原子操作的實現。
軟件級別的原子操作:軟件級別的原子操作實現依賴於硬件原子操作的支持。
Linux內核提供了兩組原子操作接口:一組是針對整數進行操作;另一組是針對單獨的位進行操作。
1、原子整數操作
原子操作通常針對int或bit類型的數據,但是Linux並不能直接對int進行原子操作,而只能通過atomic_t的數據結構來進行。目前瞭解到的原因有兩個。
一是在老的Linux版本,atomic_t實際只有24位長,低8位用來做鎖。這是由於Linux是一個跨平臺的實現,可以運行在多種 CPU上,有些類型的CPU比如SPARC並沒有原生的atomic指令支持,所以只能在32位int使用8位來做同步鎖,避免多個線程同時訪問。(最新版SPARC實現已經突破此限制)。原子整數操作最常見的用途就是實現計數器。常見的用法是:atomic_t use_cnt;
atomic_set(&use_cnt, 2);
atomic_add(4, &use_cnt);
atomic_inc(use_cnt);
在X86平臺上,atomic_t定義如下:
- typedef struct {
- int counter;
- } atomic_t;
- static inline void atomic_add(int i, atomic_t *v)
- {
- asm volatile(LOCK_PREFIX "addl %1,%0"
- : "+m" (v->counter)
- : "ir" (i));
- }
- #define LOCK_PREFIX \
- ".section .smp_locks,\"a\"\n" \
- " .align 4\n" \
- " .long 661f\n" /* address */ \
- ".previous\n" \
- "661:\n\tlock; "
- .section .smp_locks,"a"
- .align 4
- .long 661f
- .previous
- 661:
- lock;
.section .smp_locks,"a"
下面的代碼生成到 .smp_locks 段裏,屬性爲"a", allocatable
.align 4
四字節對齊
.long 661f
生成一個整數,值爲下面的 661 標號的實際地址,f 表示向前引用,如果 661 標號出現
在前面,要寫 661b。
.previous
代碼生成恢復到原來的段,也就是 .text
661:
數字標號是局部標號,5.3 Symbol Names
lock;
開始生成指令,lock 前綴
這段代碼彙編後,在 .text 段生成一條 lock 指令前綴 0xf0,在 .smp_locks 段生成四個字節的 lock 前綴的地址,鏈接的時候,所有的.smp_locks 段合併起來,形成一個所有 lock 指令地址的數組,這樣統計 .smp_locks 段就能知道代碼裏有多少個加鎖的指令被生成,猜測是爲了調試目的。
搜索了一下,找到了相關引用處,當一個內核模塊被加載時,會調用module_finalize函數:
- int module_finalize(const Elf_Ehdr *hdr,
- const Elf_Shdr *sechdrs,
- struct module *me)
- {
- const Elf_Shdr *s, *text = NULL, *alt = NULL, *locks = NULL,
- *para = NULL;
- char *secstrings = (void *)hdr + sechdrs[hdr->e_shstrndx].sh_offset;
- for (s = sechdrs; s < sechdrs + hdr->e_shnum; s++) {
- if (!strcmp(".text", secstrings + s->sh_name))
- text = s;
- if (!strcmp(".altinstructions", secstrings + s->sh_name))
- alt = s;
- if (!strcmp(".smp_locks", secstrings + s->sh_name))
- locks= s;
- if (!strcmp(".parainstructions", secstrings + s->sh_name))
- para = s;
- }
- if (alt) {
- /* patch .altinstructions */
- void *aseg = (void *)alt->sh_addr;
- apply_alternatives(aseg, aseg + alt->sh_size);
- }
- if (locks && text) {
- void *lseg = (void *)locks->sh_addr;
- void *tseg = (void *)text->sh_addr;
- alternatives_smp_module_add(me, me->name,
- lseg, lseg + locks->sh_size,
- tseg, tseg + text->sh_size);
- }
- if (para) {
- void *pseg = (void *)para->sh_addr;
- apply_paravirt(pseg, pseg + para->sh_size);
- }
- return module_bug_finalize(hdr, sechdrs, me);
- }
- void alternatives_smp_module_add(struct module *mod, char *name,
- void *locks, void *locks_end,
- void *text, void *text_end)
- {
- struct smp_alt_module *smp;
- if (noreplace_smp)
- return;
- if (smp_alt_once) {
- if (boot_cpu_has(X86_FEATURE_UP))
- alternatives_smp_unlock(locks, locks_end,
- text, text_end);
- return;
- }
- ........//省略無關代碼
- }
- static void alternatives_smp_unlock(u8 **start, u8 **end, u8 *text, u8 *text_end)
- {
- u8 **ptr;
- char insn[1];
- if (noreplace_smp)
- return;
- add_nops(insn, 1);
- for (ptr = start; ptr < end; ptr++) {
- if (*ptr < text)
- continue;
- if (*ptr > text_end)
- continue;
- text_poke(*ptr, insn, 1);
- };
- }
2、原子位操作的實現
編寫代碼時,以如下的方式進行操作
unsigned long word = 0;
set_bit(0, &word); /*第0位被設置*/
set_bit(1, &word); /*第1位被設置*/
clear_bit(1, &word); /*第1位被清空*/
change_bit(0, &word); /*翻轉第0位*/
爲什麼關注原子操作?
1)在確認一個操作是原子的情況下,多線程環境裏面,我們可以避免僅僅爲保護這個操作在外圍加上性能開銷昂貴的鎖。
2)藉助於原子操作,我們可以實現互斥鎖。
3)藉助於互斥鎖,我們可以把一些列操作變爲原子操作。
我們重點關注一下以下兩個函數的實現:
- /**
- * clear_bit - Clears a bit in memory
- * @nr: Bit to clear
- * @addr: Address to start counting from
- *
- * clear_bit() is atomic and may not be reordered. However, it does
- * not contain a memory barrier, so if it is used for locking purposes,
- * you should call smp_mb__before_clear_bit() and/or smp_mb__after_clear_bit()
- * in order to ensure changes are visible on other processors.
- */
- static inline void clear_bit(int nr, volatile void *addr)
- {
- asm volatile(LOCK_PREFIX "btr %1,%0" : ADDR : "Ir" (nr));
- }
- /*
- * clear_bit_unlock - Clears a bit in memory
- * @nr: Bit to clear
- * @addr: Address to start counting from
- *
- * clear_bit() is atomic and implies release semantics before the memory
- * operation. It can be used for an unlock.
- */
- static inline void clear_bit_unlock(unsigned nr, volatile void *addr)
- {
- barrier();
- clear_bit(nr, addr);
- }
這就是有名的“內存屏障“或”內存柵欄“操作,先來補充一下這方面的知識。
可以看一下barrier的定義:
- #define barrier() __asm__ __volatile__("": : :"memory")
解釋一下:__volatitle__是防止編譯器移動該指令的位置或者把它優化掉。"memory",是提示編譯器該指令對內存修改,防止使用某個寄存器中已經load 的內存的值。lock 前綴是讓cpu 的執行下一行指令之前,保證以前的指令都被正確執行。
事實上,不止barrier,還有一個mb系列的函數也起着內存屏障的功能:- #include <asm/system.h>
- "void rmb(void);"
- "void wmb(void);"
- "void mb(void);"
這些函數在已編譯的指令流中插入硬件內存屏障,具體的插入方法是平臺相關的。rmb(讀內存屏障)保證了屏障之前的讀操作一定會在後來的讀操作執行之前完成。wmb保證寫操作不會亂序,mb 指令保證了兩者都不會。這些函數都是 barrier 函數的超集。解釋一下:編譯器或現在的處理器常會自作聰明地對指令序列進行一些處理,比如數據緩存,讀寫指令亂序執行等等。如果優化對象是普通內存,那麼一般會提升性能而且不會產生邏輯錯誤。但如果對I/O 操作進行類似優化很可能造成致命錯誤。所以要使用內存屏障,以強制該語句前後的指令以正確的次序完成。
其實在指令序列中放一個wmb 的效果是使得指令執行到該處時,把所有緩存的數據寫到該寫的地方,同時使得wmb 前面的寫指令一定會在wmb後面 的寫指令之前執行。
回到上面的函數,當clear_bit函數不用於實現鎖的目的時,不用給它加上內存屏障(我的理解:不管是不是讀到最新的數據,這一位就是要清零,不管加不加內存屏障,結果都是一樣的);而當用於實現鎖的目的時,必須使用clear_bit_unlock函數,其實現中使用了內存屏障,以此來確保此處的修改能在其他CPU上看到(我的理解:加鎖操作就是爲了在多個CPU間進行同步的目的,所以要避免寄存器優化,其他CPU每次都讀內存這樣才能看到最新的變化,這塊不是太明白)。這種操作也叫做serialization,即在執行這條指令前,CPU必須要完成前面所有對memory的訪問指令(read and write),這樣是爲了避免編譯器進行某些優化。
同樣使用serialization操作的還有test_and_set_bit函數:
- /**
- * test_and_set_bit - Set a bit and return its old value
- * @nr: Bit to set
- * @addr: Address to count from
- *
- * This operation is atomic and cannot be reordered.
- * It also implies a memory barrier.
- */
- static inline int test_and_set_bit(int nr, volatile void *addr)
- {
- int oldbit;
- asm volatile(LOCK_PREFIX "bts %2,%1\n\t"
- "sbb %0,%0" : "=r" (oldbit), ADDR : "Ir" (nr) : "memory");
- return oldbit;
- }
1)memory 強制gcc 編譯器假設RAM 所有內存單元均被彙編指令修改,這樣cpu 中的registers 和cache 中已緩存的內存單元中的數據將作廢。cpu 將不得不在需要的時候重新讀取內存中的數據。這就阻止了cpu 又將registers,cache 中的數據用於去優化指令,而避免去訪問內存。
2)sbb $0,0(%%esp)表示將數值0 減到esp 寄存器中,而該寄存器指向棧頂的內存單元。減去一個0,esp 寄存器的數值依然不變。即這是一條無用的彙編指令。在此利用這條無價值的彙編指令來配合lock 指令,在__asm__,__volatile__,memory 的作用下,用作cpu 的內存屏障。
這種寫法和前面的clear_bit_unlock中先寫一個barrier函數,再寫一個正常內嵌彙編函數的功能是一樣的。