1. 程式人生 > 其它 >linux原始碼解讀(十一):多程序/執行緒的互斥和同步

linux原始碼解讀(十一):多程序/執行緒的互斥和同步

  為了提高cpu的使用率,硬體層面的cpu和軟體層面的作業系統都支援多程序/多執行緒同時執行,這就必然涉及到同一個資源的互斥/有序訪問,保證結果在邏輯上正確;由此誕生了原子變數、自旋鎖、讀寫鎖、順序鎖、互斥體、訊號量等互斥和同步的手段!這麼多的方式、手段,很容易混淆,所以這裡做了這6種互斥/同步方式要點的總結和對比,如下:

  詳細的程式碼解讀可以參考末尾的連結,都比較詳細,這裡不再贅述!從這些結構體的定義可以看出,在C語言層面並沒有太大的區別,都是靠著某個變數(再直白一點就是某個記憶體)的取值來決定是否繼續進入臨界區執行;如果當前無法進入臨界區,要麼一直霸佔著cpu自旋空轉,要麼主動sleep把cpu讓出給其他程序,然後加入等待佇列待喚醒

; 這裡最基本的兩種資料結構就是原子變數和自旋鎖了,C層面的結構體如上圖所示。然而所有的程式碼都會經過編譯器變成彙編程式碼,不同硬體平臺在彙編層面又是怎麼實現這些基本的功能了?

 1、先看看windows+x86平臺是怎麼實現自旋鎖的,最精妙的就是紅框框這句了:lock bts dword ptr [ecx],0; 這句程式碼加了lock字首,保證了當前cpu對[ecx]這塊記憶體的獨佔;在這行程式碼沒執行完前,其他cpu是無法讀寫[ecx]這塊記憶體的,這很關鍵,保證了程式碼執行的原子性(能完整執行而不被打斷)!正式分析前,先解釋一下bts dword ptr [ecx],0的功能:

  •   把[ecx]的第0位賦給標誌位暫存器的CF位
  • 把[ecx]的第0位設定置1

由於加了lock字首,上述兩個功能在cpu硬體層面會保證100%執行完成! 在執行bts程式碼時:

(1)如果[ecx]是0,說明鎖還沒被程序/執行緒獲取,還是空閒狀態,當前程序/執行緒是可以獲取鎖,然後繼續進入臨界區執行的。那麼獲取鎖繼續進入臨界區著兩個功能該怎麼用程式碼實現了?

  • 獲取鎖:鎖的本質就是一段記憶體,這裡是[ecx],所以需要把[ecx]置1,這個功能bts指令執行完後就能實現
  • 繼續進入臨界區:此時CF=0,會導致下面的jb語句不執行,而後就是retn 4返回了,標明獲取鎖的方法已經執行完畢,呼叫該方法的程序/執行緒可以繼續往下執行了,這裡取名A;

(2)如果A還在臨界區執行,此時B也呼叫這個獲取鎖的方法,B該怎麼執行了?因為A還在臨界區,所以此時[ecx]還是1,鎖還在A手上;B執行bts語句的結果:

  • 把[ecx]的第0位賦值給CF;由於此時[ecx]=1,所以CF=1
  • 把[ecx]置1,這裡沒變

bts執行完畢後繼續下一條jb程式碼:由於CF=1,jb跳轉的條件滿足,立即跳轉到0x469a12處執行;

  • test和jz程式碼檢查[ecx]的值,如果還是1,也就是鎖還被佔用,就不執行jz跳轉,繼續往下執行pause和jmp0x469a12,周而復始地檢查[ecx]的值,也就是鎖是否被釋放了;自旋鎖的阻塞和空轉就是這樣實現的
  • 如果A執行完畢退出臨界區,也釋放了鎖,讓[ecx]=1;B執行tes時發現了[ecx]=1,會通過jz跳回0x469a08處獲取執行bts語句獲取鎖,然後退出該函式進入臨界區
  • 從上述流程可以看出:spinlock沒有佇列機制,等待鎖的程序不一定是先到先得;如果有多個程序都在等鎖,就看誰運氣好,先執行jz 0x469a08者就能先跳回去執行bts得到鎖

(3)上述spinlock是windows的實現,linux在x86平臺是怎麼實現的了?如下:核心還是使用lock字首讓decb執行時其他cpu不能訪問lock->slock這塊記憶體,保證decb執行的原子性!這裡的空轉和阻塞是通過rep;nop來實現的,據說效率比pause要高!

typedef struct {
                unsigned int slock;
        } raw_spinlock_t;
#define __RAW_SPIN_LOCK_UNLOCKED { 1 } static inline void __raw_spin_lock(raw_spinlock_t *lock) { asm volatile("\n1:\t" LOCK_PREFIX " ; decb %0\n\t" // lock->slock減1 "jns 3f\n" //如果不為負.跳轉到3f.3f後面沒有任何指令,即為退出 "2:\t" "rep;nop\n\t" //重複執行nop.nop是x86的小延遲函式 "cmpb $0,%0\n\t" "jle 2b\n\t" //如果lock->slock不大於0,跳轉到標號2,即繼續重複執行nop "jmp 1b\n" //如果lock->slock大於0,跳轉到標號1,重新判斷鎖的slock成員 "3:\n\t" : "+m" (lock->slock) : : "memory"); }

  相比之下,解鎖就簡單多了:直接把lock->slock置1,這裡也不需要lock指令了;知道原因麼? 解鎖指令是臨界區的最後一行程式碼,說明同一時間只能有一個程序/執行緒執行該程式碼,這種情況還有必要加lock麼?這點也引申出了spinlock的另一個特性:

  •   A程序加鎖,也只能由A程序解鎖;如果A程序在執行臨界區時意外退出,這鎖就解不了了
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
        {
                asm volatile("movb $1,%0" : "+m" (lock->slock) :: "memory");
        }

  2、再來看看arm v6及以上硬體平臺是怎麼實現spinlock的,如下:

#if __LINUX_ARM_ARCH__ < 6
        #error SMP not supported on pre-ARMv6 CPUs //ARMv6後,才有多核ARM處理器
        #endif
        ……
        static inline void __raw_spin_lock(raw_spinlock_t *lock)
        {
                unsigned long tmp;
                __asm__ __volatile__(
        "1: ldrex        %0, [%1]\n"
        //取lock->lock放在 tmp裡,並且設定&lock->lock這個記憶體地址為獨佔訪問
        "        teq %0, #0\n"
        //測試lock_lock是否為0,影響標誌位z
        #ifdef CONFIG_CPU_32v6K
        "        wfene\n"
        #endif
        "        strexeq %0, %2, [%1]\n"
        //如果lock_lock是0,並且是獨佔訪問這個記憶體,就向lock->lock裡寫入1,並向tmp返回0,同時清除獨佔標記
        "        teqeq %0, #0\n"
        //如果lock_lock是0,並且strexeq返回了0,表示加鎖成功,返回
        " bne 1b"
        //如果上面的條件(1:lock->lock裡不為0,2:strexeq失敗)有一個符合,就在原地打轉
                : "=&r" (tmp) //%0:輸出放在tmp裡,可以是任意暫存器
                : "r" (&lock->lock), "r" (1)
        //%1:取&lock->lock放在任意暫存器,%2:任意暫存器放入1
                : "cc"); //狀態暫存器可能會改變
                smp_mb();
        }

  核心的指令就是ldrex和strexeq了;ldr和str指令很常見,就是從記憶體載入資料到暫存器,然後從暫存器輸出到記憶體;兩條指令分別加ex(就是exclusive獨佔),可以讓匯流排監控LDREX和STREX指令之間有無其它CPU和DMA來存取過這個地址,若有的話STREX指令的第一個暫存器裡設定為1(動作失敗); 若沒有,指令的第一個暫存器裡設定為0(動作成功);個人覺得和x86的lock指令沒有本質區別,都是通過獨佔某塊記憶體然後設定為1達到加鎖的目的

  解鎖的原理和x86一樣,直接用str設定為1就行了,都不需要再獨佔了,程式碼如下:

static inline void __raw_spin_unlock(raw_spinlock_t *lock)
        {
                smp_mb();
                __asm__ __volatile__(
        "         str %1, [%0]\n" // 向lock->lock裡寫0,解鎖
        #ifdef CONFIG_CPU_32v6K
        "         mcr p15, 0, %1, c7, c10, 4\n"
        "         sev"
        #endif
                :
                : "r" (&lock->lock), "r" (0) //%0取&lock->lock放在任意暫存器,%1:任意暫存器放入0
                : "cc");
        }

   atomic的實現方式類似,這裡不再贅述!

  

參考:

1、https://blog.51cto.com/u_15127625/2731250 linux競爭併發

2、https://blog.csdn.net/u012603457/article/details/52895537 linux原始碼自旋鎖的實現

3、https://zhuanlan.zhihu.com/p/364044713 linux讀寫鎖

4、https://zhuanlan.zhihu.com/p/364044850 linux順序鎖

5、https://blog.csdn.net/zhoutaopower/article/details/86611798 linux核心同步-順序鎖

6、https://zhuanlan.zhihu.com/p/363982620 linux原子操作

7、https://zhuanlan.zhihu.com/p/364130923 linux 互斥體

8、https://www.cnblogs.com/crybaby/p/13061627.html linux同步原語-訊號量

9、https://codeantenna.com/a/F2a5C0tK3a Linux中Spinlock在ARM及X86平臺上的實現