互斥是指對資源的排他性訪問,而同步是對進程執行的先後順序作出妥善的安排。
所謂競態,就是多個執行路徑有可能對同一資源進行操作時可能導致的資源數據紊亂的行爲。把對共享的資源進行訪問的代碼片段成爲臨界區。
併發的來源:中斷處理路徑(中斷處理函數與被中斷的進程之間形成的併發)、調度器的可搶佔性(調度器被搶佔,形成進程間的併發)、多處理器的併發執行(進程之間嚴格意義上的併發)。
1、 local_irq_enable與local_irq_disable
local_irq_enable宏用來打開本地處理器的中斷,而local_irq_disable是相反操作。它們是通過關中斷的方式進行互斥操作,必須確保處於兩者之間的代碼效率高,否則影響性能。
2、 自旋鎖
目的是實現在多處理器系統中提供對共享數據的保護,核心思想是:設置一個多處理器之間共享的全局變量鎖V,並定義當V=1時爲上鎖狀態,當V=0是爲解鎖狀態。如果V非0表明有其他處理器上的代碼正在對共享數據進行訪問,此時訪問處理器進入忙等待即自旋狀態。如果V=0表明當前沒有其他處理器上代碼進入臨界區,訪問處理器可以訪問該資源。
(1)、spin_lock
函數定義:#define raw_spin_lock(lock) _raw_spin_lock(lock),從定可看出是一個宏定義,因此進入_raw_spin_lock(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);
}
說明:preempt_disable()宏是用來定義內核是否支持可搶佔的操作,如果定義,則關閉調度器的可搶佔性;如果沒有定義,此語句不做任何操作。此定義用於支持SMP系統。
(2)spin_lock的變體
情景:當進程A通過調用spin_lock進入臨界區後,正處於臨界區中時,進程A所在的處理器上發生了一個外部硬件中斷,此時系統暫停當前進程A的操作進而處理外部中斷。假設該中斷也恰好要操作進程A的資源,因爲資源是一個全局變量,所以操作之前也要調用spin_lock試圖去獲得自旋鎖,因爲該鎖已被進程A擁有,所以中斷處理例程要進入自旋狀態。這是非常致命的狀態。
爲解決上述場景,出現了spin_lock_irq 和spin_lock_irqsave。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);
}
從代碼中可以看出,在調用preempt_disable()之前調用了local_irq_disable(),即先關閉本地處理器響應外部中斷的能力,這樣在獲取一個鎖時就可以確保不會發生中斷,從而避免場景中出現的死鎖問題。
使用自旋鎖的一條規則:任何擁有自旋鎖的代碼必須是原子的,不能睡眠。
當知道一個自旋鎖在中斷處理的上下文中有可能被使用時,應該使用spin_lock_irq函數,而不是spin_lock,後者只有在能確定中斷上下文中不會使用到自旋鎖的情況下才能使用。
另一個spinlock版本是:spin_lock_bh函數,該函數用來處理進程與延遲處理導致的併發中的互斥問題。
3、 信號量(semaphone)
信號量的最大特點是允許調用它的進程進入睡眠狀態。這也會導致對處理器擁有權的喪失,也即出現進程的切換。
在驅動程序程序中定義了一個struct semaphone型的信號量變量,需要注意的是不要直接對該變量進行賦值,而應該使用sema_init函數來初始化該信號量。
信號量上的主要操作是DOWN和UP,linux內核中對信號量的DOWN操作有:
void down(struct semaphore *sem);
void down_interruptible(struct semaphore *sem);
void down_killable(struct semaphore *sem);
void down_trylock(struct semaphore *sem);
void down_timeout(struct semaphore *sem, long jiffies);
驅動中最常使用的是down_interruptible函數,
(1) DOWN操作
代碼如下:
static noinline void __sched __down(struct semaphore *sem)
{__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);}
static noinline int __sched __down_interruptible(struct semaphore *sem)
{return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT}
static noinline int __sched __down_killable(struct semaphore *sem)
{return __down_common(sem, TASK_KILLABLE, MAX_SCHEDULE_TIMEOUT);}
static noinline int __sched __down_timeout(struct semaphore *sem, long jiffies)
{return __down_common(sem, TASK_UNINTERRUPTIBLE, jiffies);}
從以上可以看出,四類函數最終調用的均是__down_common函數。
首先把當前進程放到信號量sem的成員變量管理隊列中,接着把當前進程的狀態設置爲TASK_INTERRUPTIBLE,在調用schedule_timeout使得當前進程進入睡眠狀態,函數將停留在schedule_timeout調用上,直到再次被調度執行。當進程被再一次調度執行時,schedule_timeout開始返回,接着根據進程被再次調度的原因進行處理:如果waiter.up不爲0,說明進程在信號量sem的隊列中被該信號量的UP操作所喚醒,進程可以獲得信號量,返回0。如果進程是因爲被用戶空間發送的信號所中斷或者超時引起的喚醒,則返回相應的錯誤代碼。
所以,對down_interruptible的調用總是應該檢查其返回值,以確定函數是已經獲得了信號量還是因爲被中斷因而需要特別處理。
(2) UP操作
Linux下只有一個UP版本。
即使不是信號量的擁有者,也可以調用UP函數來釋放一個信號量。信號量的常用用途是實現互斥機制,任意一個時刻只允許一個進程進入臨界區。
4、 互斥鎖mutex
Linux內核針對count=1的信號量重新定義了一個新的數據結構struct mutex,稱之爲互斥鎖。
互斥鎖的DOWN操作,在linux內核中爲mutex_lock函數,函數定義如下:
void __sched mutex_lock(struct mutex *lock)
{
might_sleep();
__mutex_fastpath_lock(&lock->count, __mutex_lock_slowpath);
mutex_set_owner(lock);
}
__mutex_fastpath_lock用來快速判斷當前可否獲得互斥鎖,如果成功獲得鎖,則函數直接返回,否則進入到__mutex_lock_slowpath函數中。在進程進入__mutex_lock_slowpath之前,會多次檢查是否有互斥鎖被釋放。這基於這樣一個事實:擁有互斥鎖的進程總是會在儘可能短的時間裏釋放該鎖。
互斥鎖的UP操作,在linux內核中爲mutex_unlock函數,函數定義如下:
void __sched mutex_unlock(struct mutex *lock)
{
#ifndef CONFIG_DEBUG_MUTEXES
mutex_clear_owner(lock);
#endif
__mutex_fastpath_unlock(&lock->count, __mutex_unlock_slowpath);
}
__mutex_fastpath_unlock和__mutex_unlock_slowpath分別對應互斥鎖的快速和慢速解鎖操作。__mutex_fastpath_unlock函數完成的工作是把count->counter的值加1,然後返回。如果有別的進程在競爭該互斥鎖,那麼函數進入__mutex_unlock_slowpath,此函數主要用來喚醒在當前mutex的wait_list中休眠的進程。
5、 順序鎖seqlock
順序鎖的設計思想是:對某一共享數據讀取時不加鎖,寫的時候加鎖。爲保證讀取過程中不會因爲寫入者的出現導致該共享數據的更新,在寫入者與讀取者之間引入一整型變量,稱爲順序值。讀取者在開始讀取之前讀取該sequence,在讀取後再重新讀該值,如果與之前讀取的不一致,則說明本次讀取操作過程中發生了數據更新,讀取操作無效。因此要求寫入者在寫入的時候更新sequence的值。
6、 RCU
RCU全稱Read-Copy-Update,意即讀/寫-複製-更新,在linux提供的所有內核互斥設施當中屬於一種免鎖機制。RCU的適用模型也是讀取者與寫入者共存的系統。不同的是,rcu中不需要考慮讀取者與寫入者之間的互斥問題。
RCU的原理:將讀取者和寫入者要訪問的共享數據放在一個指針P中,讀取者通過P來訪問其中的數據,而寫入者則通過修改P來更新數據。具體實現上,讀取者一方並沒有太多的事情要做,大量的工作集中在寫入者一方。
(1)、讀取者RCU臨界區
調用rcu_read_lock和rcu_read_unlock函數構建讀取者自己的臨界區,在臨界區中獲得指向共享數據區的指針,實際的讀取操作就是對該指針的引用。對指針的引用必須在臨界區中完成,離開臨界區之後不應該出現任何形式的對該指針的引用。
(2)、寫入者RCU的操作
RCU操作中寫入者要完成的工作是重新分配一個被保護的共享數據區,將老數據區的數據複製到新數據區,然後根據需要修改新數據區,最後用新數據區指針替換掉老的指針,替換指針的操作是一個原子操作,不需要與讀取者進行互斥操作。
7、 等待隊列
等待隊列不是互斥機制中的一種方案。在內核中是一種常用數據結構。
等待隊列本質上是一雙向鏈表,有等待隊列頭和隊列節點構成,當運行的進程要獲得某一資源而得不到時,進程有時候需要等待,此時它可以進入睡眠狀態,內核爲此生成一個新的等待隊列節點將睡眠的進程掛載到等待隊列中。
定義等待隊列的兩種方法:
其一、通過DECLARE_WAIT_QUEUE_HEAD宏來完成等待隊列頭對象的靜態定義與初始化;
其二、通過init_waitqueue_head宏在程序運行期間初始化一個頭結點對象;
內核中對等待隊列的核心操作是等待(wait)與喚醒(wake up)。
8、 完成接口completion
該機制主要用來在多個執行路徑間作同步使用,也即協調多個執行路徑的執行順序。