Linux內核同步機制

Linux內核同步機制,常用的有自旋鎖,信號量,互斥體,原子操作,順序鎖,RCU,內存屏障等。
本篇主要介紹原子操作,自旋鎖,信號量和內存屏障。

原子操作

原子操作可以保證指令以原子的方式進行執行,執行過程不被打斷。

API 描述
static inline void atomic_add(int i, atomic_t *v) 給一個原子變量v增加i
static inline int atomic_add_return(int i, atomic_t *v) 同上,只不過將變量v的最新值返回
static inline void atomic_sub(int i, atomic_t *v) 給一個原子變量v減去i
static inline int atomic_sub_return(int i, atomic_t *v) 同上,只不過將變量v的最新值返回
static inline int atomic_cmpxchg(atomic_t *ptr, int old, int new) 比較old和原子變量ptr中的值,如果相等,那麼就把new值賦給原子變量。 返回舊的原子變量ptr中的值
atomic_read 獲取原子變量的值
atomic_set 設定原子變量的值
atomic_inc(v) 原子變量的值加一
atomic_inc_return(v) 同上,只不過將變量v的最新值返回
atomic_dec(v) 原子變量的值減去一
atomic_dec_return(v) 同上,只不過將變量v的最新值返回
atomic_sub_and_test(i, v) 給一個原子變量v減去i,並判斷變量v的最新值是否等於0
atomic_add_negative(i,v) 給一個原子變量v增加i,並判斷變量v的最新值是否是負數
static inline int atomic_add_unless(atomic_t *v, int a, int u) 只要原子變量v不等於u,那麼就執行原子變量v加a的操作。如果v不等於u,返回非0值,否則返回0值

內核提供了兩組原子操作接口,一組針對數組,一組針對單獨的位進行操作:

原子整數操作

//定義
atomic_t v;
//初始化
atomic_t u = ATOMIC_INIT(0);

//操作
atomic_set(&v,4);      // v = 4
atomic_add(2,&v);     // v = v + 2 = 6
atomic_inc(&v);         // v = v + 1 = 7

//實現原子操作函數實現
static inline void atomic_add(int i, atomic_t *v)
{
    unsigned long tmp;
    int result;
    __asm__ __volatile__("@ atomic_add\n"
    "1:      ldrex      %0, [%3]\n"
    "  add %0, %0, %4\n"
    "  strex      %1, %0, [%3]\n"
    "  teq  %1, #0\n"
    "  bne 1b"
      : "=&r" (result), "=&r" (tmp), "+Qo" (v->counter)
      : "r" (&v->counter), "Ir" (i)
      : "cc");
}

原子位操作

//定義
unsigned long word = 0;

//操作
set_bit(0,&word);       //第0位被設置1
set_bit(0,&word);       //第1位被設置1
clear_bit(1,&word);   //第1位被清空0

//原子位操作函數實現
static inline void ____atomic_set_bit(unsigned int bit, volatile unsigned long *p)
{
       unsigned long flags;
       unsigned long mask = 1UL << (bit & 31);

       p += bit >> 5;
       raw_local_irq_save(flags);
       *p |= mask;
       raw_local_irq_restore(flags);
}

自旋鎖

自旋鎖,某個進程在試圖加鎖的時候,若當前鎖已經處於“鎖定”狀態,試圖加鎖進程就進行不斷的“旋轉”,用一個死循環測試鎖的狀態,直到成功的獲得鎖。
自旋鎖用結構spinlock_t描述,在include/linux/spinlock.h

typedef struct {
    raw_spinlock_t raw_lock;
#ifdef CONFIG_GENERIC_LOCKBREAK    /*引入另一個自旋鎖*/
    unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK   /*用於調試自旋鎖*/
    unsigned int magic, owner_cpu;
    void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;  /*映射lock實例到lock-class對象
#endif
} spinlock_t;
API 描述
spin_lock_init(lock) 初始化自旋鎖,將自旋鎖設置爲1,表示有一個資源可用。
spin_is_locked(lock) 如果自旋鎖被置爲1(未鎖),返回0,否則返回1。
spin_unlock_wait(lock) 等待直到自旋鎖解鎖(爲1),返回0;否則返回1。
spin_trylock(lock) 嘗試鎖上自旋鎖(置0),如果原來鎖的值爲1,返回1,否則返回0。
spin_lock(lock) 循環等待直到自旋鎖解鎖(置爲1),然後,將自旋鎖鎖上(置爲0)。
spin_unlock(lock) 將自旋鎖解鎖(置爲1)。
spin_lock_irqsave(lock, flags) 循環等待直到自旋鎖解鎖(置爲1),然後,將自旋鎖鎖上(置爲0)。關中斷,將狀態寄存器值存入flags。
spin_unlock_irqrestore(lock, flags) 自旋鎖解鎖(置爲1)。開中斷,將狀態寄存器值從flags存入狀態寄存器。
spin_lock_irq(lock) 循環等待直到自旋鎖解鎖(置爲1),然後,將自旋鎖鎖上(置爲0)。關中斷。
spin_unlock_irq(lock) 將自旋鎖解鎖(置爲1)。開中斷。
spin_unlock_bh(lock) 將自旋鎖解鎖(置爲1)。開啓底半部的執行。
spin_lock_bh(lock) 循環等待直到自旋鎖解鎖(置爲1),然後,將自旋鎖鎖上(置爲0)。阻止軟中斷的底半部的執行。

在Linux內核中何時使用spin_lock,何時使用spin_lock_irqsave很容易混淆,下面詳細解釋各個API。

spin_lock (spin_lock –> raw_spin_lock)*

static inline void __raw_spin_lock(raw_spinlock_t *lock)  
{  
        preempt_disable();  
        spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);  
        LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);  
}   

只禁止內核搶佔,不會關閉本地中斷

spin_lock_irq ( spin_lock_irq—> raw_spin_lock_irq )

static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)  
{  
         local_irq_disable();  
         preempt_disable();  
         spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);  
        LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);  
 }  

spin lock irq禁止內核搶佔,且關閉本地中斷
假如中斷中也想獲得這個鎖,使用spin_lock_irq將中斷禁止,就不會出現死鎖的情況

spin_lock_irqsave (spin_lock_irqsave—>__raw_spin_lock_irqsave)

static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock)
{
         unsigned long flags;

     local_irq_save(flags);
     preempt_disable();
     spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
      /*
       * On lockdep we dont want the hand-coded irq-enable of
      * do_raw_spin_lock_flags() code, because lockdep assumes
      * that interrupts are not re-enabled during lock-acquire:
      */
 #ifdef CONFIG_LOCKDEP
    LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
 #else
     do_raw_spin_lock_flags(lock, &flags);
 #endif
    return flags;
 }

spin_lock_irqsave禁止內核搶佔,關閉中斷,保存中斷狀態寄存器的標誌位.
spin_lock_irqsave在鎖返回時,之前開的中斷,之後也是開的;之前關,之後也是關。而spin_lock_irq在自旋的時候,不會保存當前的中斷標誌寄存器,只會在自旋結束後,將之前的中斷打開。

因此,spin_lock時要明確知道該鎖不會在中斷處理程序中使用,如果在中斷處理程序中使用,根據你期望在離開臨界區後,是否改變中斷的開啓/關閉狀態,來選擇使用spin_lock_irq 或者 spin_lock_irqsave

自旋鎖使用注意事項
1.自旋鎖不應該長時間的持有。自旋是一種忙等待,當條件不滿足時,會一直不斷的循環判斷條件是否滿足,如果滿足就解鎖,運行之後的代碼。因此會對linux的系統的性能有些影響。
2.自旋鎖不能遞歸使用。

自旋鎖使用示例:
如果有一個臨界資源rx_signal,對該資源頻繁的讀寫時的打開時,就有可能出現錯誤的rx_signal的狀態,所以必須對rx_signal進行保護

int  rx_signal;
spinlock_t  spinlock;
int    xxxx_init(void)
{
    spin_lock_init(&spinlock);
    ............
}
int  xxxx_open(struct  inode *inode, struct file *filp)
{
    ............
    spin_lock(&spinlock);
    if (rx_signal){
        spin_unlock(&spinlock);
        return -EBUSY;
    }
        rx_signal ++;
        spin_unlock(&spinlock);
        ...........
}

int  xxxx_release(struct  inode *inode, struct file *filp)
{
    ............
    spin_lock(&spinlock);
    rx_signal --;
    ...........
}

信號量

信號量也是一種鎖,和自旋鎖不同的是,線程獲取不到信號量的時候,不會像自旋鎖一樣循環的去試圖獲取鎖,而是進入睡眠,直至有信號量釋放出來時,纔會喚醒睡眠的線程,進入臨界區執行。
由於使用信號量時,線程會睡眠,所以等待的過程不會佔用CPU時間。所以信號量適用於等待時間較長的臨界區。
持有信號量的代碼可以被搶佔,可以在持有信號量時去睡眠,但是當佔用信號量的時候不能同時佔有自旋鎖,因爲在等待信號量時可能會睡眠,而在持有自旋鎖時是不允許睡眠的。
信號量可以同時允許任意數量的鎖持有者,通常只有一個持有者的信號量叫互斥信號量,我們常用dowm_inperruptible函數獲取信號量,它將把調用進程設置爲TASK_INTERRUPTIBLE狀態進入睡眠,如果用dowm函數獲取信號量,它則將把調用進程設置爲TASK_UNINTERRUPTIBLE狀態進入睡眠,但這樣進程在等待信號量的時候就不再響應信號了,所以,常用dowm_inperruptible函數獲取信號量,用up函數釋放信號量。

信號量結構體具體如下:

struct semaphore {
    spinlock_t        lock;
    unsigned int        count;
    struct list_head    wait_list;
};

信號量結構體中有個自旋鎖,這個自旋鎖的作用是保證信號量的down和up等操作不會被中斷處理程序打斷。

API 描述
sema_init(struct semaphore *, int) 以指定的計數值初始化動態創建的信號量
init_MUTEX(struct semaphore *) 以計數值1初始化動態創建的信號量
init_MUTEX_LOCKED(struct semaphore *) 以計數值0初始化動態創建的信號量(初始爲加鎖狀態)
down_interruptible(struct semaphore *) 以試圖獲得指定的信號量,如果信號量已被爭用,則進入可中斷睡眠狀態
down(struct semaphore *) 以試圖獲得指定的信號量,如果信號量已被爭用,則進入不可中斷睡眠狀態
down_trylock(struct semaphore *) 以試圖獲得指定的信號量,如果信號量已被爭用,則立即返回非0值
up(struct semaphore *) 以釋放指定的信號量,如果睡眠隊列不空,則喚醒其中一個任務

使用方法

/* 定義並聲明一個信號量,名字爲mr_sem,用於信號量計數 */
static DECLARE_MUTEX(mr_sem);
/* 試圖獲取信號量...., 信號未獲取成功時,進入睡眠
 * 此時,線程狀態爲 TASK_INTERRUPTIBLE
 */
down_interruptible(&mr_sem);
/* 這裏也可以用:
 * down(&mr_sem);
 * 這個方法把線程狀態置爲 TASK_UNINTERRUPTIBLE 後睡眠
 */
/* 臨界區 ... */
/* 釋放給定的信號量 */
up(&mr_sem);

內存屏障

編譯器和處理器爲了提升效率,可能對讀和寫進行排序,即“亂序”。我們需要一些手段來干預編譯器和CPU, 使其限制指令順序。內存屏障就是這樣的干預手段. 他能保證處於內存屏障兩邊的內存操作滿足有序執行。

比如下面的代碼:
a = 1;
b = 2;

編譯器和處理器看不出a和b之間的依賴關係,有可能“優化”爲b在a之前被賦值。

如果是
a = 1;
b= a;
這種情況下,因爲a和b有依賴關係,所以編譯器和處理器會順序執行

內存屏障主要有:讀屏障、寫屏障、通用屏障等。

#define mb()    __asm__ __volatile__("mb": : :"memory")
#define rmb()   __asm__ __volatile__("mb": : :"memory")
#define wmb()   __asm__ __volatile__("wmb": : :"memory")

以讀屏障爲例,它用於保證讀操作有序。屏障之前的讀操作一定會先於屏障之後的讀操作完成,寫操作不受影響,同屬於屏障的某一側的讀操作也不受影響。類似的,寫屏障用於限制寫操作。而通用屏障則對讀寫操作都有作用。而優化屏障則用於限制編譯器的指令重排,不區分讀寫。前三種屏障都隱含了優化屏障的功能。比如:
tmp = ttt; *addr = 5; mb(); val = *data;
有了內存屏障就了確保先設置地址端口,再讀數據端口。而至於設置地址端口與tmp的賦值孰先孰後,屏障則不做干預。

有了內存屏障,就可以在隱式因果關係的場景中,保證因果關係邏輯正確。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章