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的賦值孰先孰後,屏障則不做干預。
有了內存屏障,就可以在隱式因果關係的場景中,保證因果關係邏輯正確。