1. 程式人生 > >原子操作,記憶體屏障,鎖

原子操作,記憶體屏障,鎖

原文地址:https://m.2cto.com/os/201604/503190.html

文章目錄

1.引言

原理:CPU提供了原子操作、關中斷、鎖記憶體匯流排,記憶體屏障等機制;OS基於這幾個CPU硬體機制,就能夠實現鎖;再基於鎖,就能夠實現各種各樣的同步機制(訊號量、訊息、Barrier等等等等)。

所有的同步操作最基礎的理論就是原子操作。記憶體屏障,鎖都是為了保證在不同的平臺或者是CPU型別下的原子操作。

原子操作在單核,單執行緒/無中斷,且編譯器不優化的情況下是確定的,是按照C/C++程式碼順序執行的,所以不存在非同步問題

1.1 為什麼會引起非同步操作

首先了解一下cpu處理指令的步驟:
(1)早期的處理器為有序處理器,指令處理順序:

  • 讀取指令
  • 執行指令。如果暫存器可寫就從記憶體取出a的資料到暫存器,暫存器不可寫就等待
  • 暫存器處理指令
  • .將暫存器結果存入記憶體

(2)現在的處理器大多數為亂序處理器,處理順序:

  • 讀取指令
  • 指令被劃分到指令佇列
  • 指令在佇列中等待,如果暫存器可寫就從記憶體取出a的資料到暫存器,暫存器不可寫就等待
  • 暫存器處理指令
  • 將執行結果存入佇列(而不是立即寫入暫存器堆)
  • 只有當所有更早的請求執行的指令結果被寫入記憶體之後,執行的結果才會被存入記憶體(執行結果重排序,讓執行看起來是有序的)

CPU執行指令過程中的同步問題
一條簡單的a++語句究竟會有這麼多條指令,而這一組指令是可以在任意時候非同步執行的(共享資料)

  • 單核多執行緒情況下,執行緒是存在中斷的,中斷的時候cpu呼叫另一執行緒的同一指令組,所以是可能出現交叉執行的可能,也就是說單執行緒或者關掉中斷可以解決非同步問題,但很多時候這種做法並不實際
  • 多核多執行緒情況下共享資料被多個核並行處理,不論哪一種處理器都存在同時執行的可能,這就導致了非同步問題
  • 現在的編譯器都具有優化及自動優化功能,優化之後可能會對共享變臉的訪問順序進行調整,可能會造成與預期不相符的結果。

2.記憶體屏障

記憶體屏障的作用:

  • 在編譯時:拒絕編譯器優化屏障前後的指令,防止記憶體亂序訪問;
  • 在執行時:告訴記憶體地址匯流排共享資料地址的資料必須同步(當多個執行緒同時將一個共享資料地址的資料載入到佇列裡的時候,先完成處理從cpu到記憶體的時候總是通知其他執行緒跟新佇列中的該共享資料,從而保證一致性)

記憶體屏障常用場合包括:

  1. 實現同步原語(synchronization primitives)
  2. 實現無鎖資料結構(lock-free data structures)
  3. 驅動程式

記憶體屏障分類
基本的記憶體屏障有4種:

  1. 寫屏障
  2. 資料依賴屏障(常與寫屏障成對出現);
  3. 讀屏障;
  4. 通用記憶體屏障

記憶體屏障還有兩種隱式的屏障變種: LOCK和UNLOCK操作(表面上這兩個操作的實際用途和原子操作裡面的Lock解釋有區別,原子操作裡面的lock是鎖記憶體匯流排,這裡面的lock是保證執行的執行順按照lock前,lock中,lock後的順序執行)

記憶體屏障按照使用層次可以分為

  • 編譯器屏障。
  • CPU記憶體屏障。
  • MMIO write屏障。

所以:記憶體屏障只是一種執行緒同步的手段,並不會阻塞執行緒,僅保證了程式碼執行順序和多核競爭情況下的資料一致性。

4.鎖

在多執行緒程式設計中,為了保證資料操作的一致性,作業系統引入了鎖機制,用於保證臨界區程式碼的安全。通過鎖機制,能夠保證在多核多執行緒環境中,在某一個時間點上,只能有一個執行緒進入臨界區程式碼,從而保證臨界區中操作資料的一致性。

所謂的鎖,說白了就是記憶體中的一個整型數,擁有兩種狀態:空閒狀態和上鎖狀態。加鎖時,判斷鎖是否空閒,如果空閒,修改為上鎖狀態,返回成功;如果已經上鎖,則返回失敗。解鎖時,則把鎖狀態修改為空閒狀態。

4.1 鎖的重要性

我們首先看看如果OS不採用任何其他手段,什麼情況下會導致上鎖失敗?假如我們把加鎖過程用如下偽碼錶示:

  1. read lock;
  2. 判斷lock狀態;判斷lock狀態;
  3. 如果已經加鎖,失敗返回;如果已經加鎖,失敗返回
  4. 把鎖狀態設定為上鎖;把鎖狀態設定為上鎖;
  5. 返回成功。返回成功。

明白彙編的同學一看就明白上述每一步都能對應到一條彙編語句,所以我們可以認為每一步本身是原子的。

那麼什麼情況能夠導致兩個執行緒同時獲取到鎖呢?
(1)中斷
假設執行緒A執行完第一步,發生中斷,中斷返回後,OS排程執行緒B,執行緒B也來加鎖並且加鎖成功,這時OS排程執行緒A執行,執行緒從第二步開始執行,也加鎖成功。
(2)多核
當然了,想想上面舉的例子,描述的就是兩個核同時獲取到鎖的情況。

4.2 鎖的實現

既然明白鎖失敗的原因,解決手段就很明確了
先考慮單核場景:

  1. 既然只有中斷才能把上鎖過程打斷,造成多執行緒操作失敗。我先關中斷不就得了,在加鎖操作完成後再開中斷。

  2. 上面這個手段太笨重了,能不能硬體做一種加鎖的原子操作呢?能,大名鼎鼎的“test and set”指令就是做這個事情的。

  3. 在多核情況下,test and set指令可能會被多個cpu同時執行,還需要附加機制:鎖記憶體匯流排的機制。硬體提供了鎖記憶體匯流排的機制,我們在鎖記憶體匯流排的狀態下執行test and set操作,就能保證同時只有一個核來test and set,從而避免了多核下發生的問題(所謂匯流排鎖就是使用處理器提供的一個LOCK#訊號,當一個處理器在總線上輸出此訊號時,其他處理器的請求將被阻塞住,那麼該處理器可以獨佔使用共享記憶體。)

總結一下,在硬體層面,CPU提供了原子操作、關中斷、鎖記憶體匯流排的機制OS基於這幾個CPU硬體機制,就能夠實現鎖再基於鎖,就能夠實現各種各樣的同步機制(訊號量、訊息、Barrier等等等等)。

4.3 鎖分類

  • 原子鎖:使用了鎖匯流排的方式實現原子操作
  • 自旋鎖:while等待,不可搶佔的單CPU核心下是無效的,有軟中斷的情況下,必須使用時本地軟中斷失效的方法。自旋鎖更像是一種使用者層控制的while等待處理
  • 讀寫鎖: 讀寫鎖實際是一種特殊的自旋鎖,它把對共享資源的訪問者劃分成讀者和寫者,讀者只對共享資源進行讀訪問,寫者則需要對共享資源進行寫操作
  • 互斥鎖:沉睡/休眠等待,所以互斥鎖比自旋鎖排程耗時。
  • 訊號量:用於同一時刻有多個個例項能獲取鎖,可用於表示同時有多少個client請求允許訪問同一個資料塊,允許鎖個數設定為1的時候就是互斥鎖.
  • 讀寫訊號量:對同時擁有的讀者數不受限制,只能一個寫者,寫者發現不需要寫的時候降級為讀者。
  • 順序鎖:用於能夠區分讀與寫的場合,並且是讀操作很多、寫操作很少,寫操作的優先權大於讀操作。
  • 讀拷貝鎖:RCU(read-copy-update)(RCU也是用於能夠區分讀與寫的場合,並且也是讀多寫少,但是讀操作的優先權大於寫操作)
    • rcuclassic:禁止核心搶佔的
    • rcupreempt:允許核心搶佔的,實時性更高,和rcuclassic相反
    • rcutree:和rcuclassic類似
  • BKL(大核心鎖): 整個核心只有一把這樣的鎖,一旦一個程序獲得大核心鎖,進入了被它保護的臨界區,不但該臨界區被鎖住,所有被它保護的其它臨界區都將無法訪問,直到該程序釋放大核心鎖