上一節(jié)主要介紹了Linux內(nèi)核中的自旋鎖,知道了自旋鎖是不能睡眠的,因此只適合用于短時間的保護臨界區(qū)。如果需要較長時間的持有鎖,就不應該再使用自旋鎖了,因為這會大量消耗 cpu 的性能,大大降低整個系統(tǒng)的效率。 不過,在Linux內(nèi)核開發(fā)中,不可避免的會遇到需要長時間保護的臨界區(qū),該怎么辦呢?因此,Linux 內(nèi)核還提供了一種同步共享數(shù)據(jù)的機制——信號量。 信號量在 1968 年由計算機科學家 Edsger Wyde Dijkstra 提出,此后便逐漸成為一種常用的鎖機制。不同于自旋鎖,信號量允許線程睡眠因此即使某個某個線程需要較長時間的持有信號量,也是被允許的,因為其他線程在等待信號量的過程中可以睡眠,Linux 內(nèi)核可以調(diào)度其他線程投入運行。 Linux 內(nèi)核中的信號量的數(shù)據(jù)結構首先來看下Linux 內(nèi)核中的信號量使用的數(shù)據(jù)結構,它是由結構體 semaphore 描述的,相關的C語言代碼如下,請看: struct semaphore { spinlock_t lock; unsigned int count; struct list_head wait_list;}; 容易看出,結構體 semaphore 包含一個自旋鎖,這說明信號量的某些臨界區(qū)也是需要使用自旋鎖保護的。count 則是信號量計數(shù),wait_list 則是一個等待隊列。 自旋鎖則是依賴原子操作實現(xiàn)的,所以原子操作一節(jié),我們提到原子操作是其他同步機制的基礎。 上一節(jié)介紹的自旋鎖,只能同時被一個線程持有,而信號量則不一定,它可以同時被 count 個線程持有。不過大多情況下 count 都等于 1,此時信號量被稱作二值信號量,或者互斥信號量。如果某個信號量已經(jīng)被 count 個線程持有,若還有新線程申請信號量,則該線程會被放入等待隊列等待,處理器會先執(zhí)行其他任務。 創(chuàng)建信號量可以使用 sema_init() 函數(shù)初始化一個信號量,它是一個 inline 函數(shù),相關C語言代碼如下,請看: static inline void sema_init(struct semaphore *sem, int val){ static struct lock_class_key __key; *sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val); lockdep_init_map(&sem->lock.dep_map, 'semaphore->lock', &__key, 0); } 容易看出, sema_init() 函數(shù)其實就是對信號量 sem 賦初值而已,val 會傳遞給 count 成員。因此互斥信號量的初始化只需將把 1 傳遞給 val 就可以了,事實上,Linux 內(nèi)核的確是這么干的,相關C語言代碼如下: #define init_MUTEX(sem) sema_init(sem, 1)#define init_MUTEX_LOCKED(sem) sema_init(sem, 0) init_MUTEX_LOCKED() 宏創(chuàng)建了一個 count 等于 0 的信號量,這說明該信號量一開始就是被初始化線程持有的。 使用信號量早期的信號量支持兩個原子操作 P() 和 V(),分別是指測試操作和增加操作,后來的系統(tǒng)則把這兩種操作命名為 down() 和 up(),Linux 內(nèi)核也遵從這種叫法。down() 函數(shù)負責申請信號量并將信號量的 count 減 1,顯然,如果 count 大于 0,則任務就可以獲得信號量并進入臨界區(qū)。down() 函數(shù)的C語言代碼如下,請看: 52 void down(struct semaphore *sem)- 53 {| 54 unsigned long flags;| 55 | 56 spin_lock_irqsave(&sem->lock, flags);| 57 if (likely(sem->count > 0))| 58 sem->count--;| 59 else| 60 __down(sem);| 61 spin_unlock_irqrestore(&sem->lock, flags);| 62 } 如果 count 不大于 0,則任務就會進入睡眠,并被設置為 TASK_UNINTERRUPTIBLE 狀態(tài),此時任務不會再響應信號。 static noinline void __sched __down(struct semaphore *sem){ __down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);} __down_common()函數(shù)的C語言代碼如下,請看: 如果希望申請信號量失敗而進入睡眠的進程仍然能夠響應信號,則可以使用 down_interruptible() 函數(shù),它的核心代碼如下: static noinline int __sched __down_interruptible(struct semaphore *sem){ return __down_common(sem, TASK_INTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);} 類似的還有 down_killable() 函數(shù),它的C語言代碼如下,請看: static noinline int __sched __down_killable(struct semaphore *sem) { return __down_common(sem, TASK_KILLABLE, MAX_SCHEDULE_TIMEOUT); } 在臨界區(qū)完成工作后,up() 函數(shù)可以釋放信號量,它的C語言代碼如下,請看: void up(struct semaphore *sem){ unsigned long flags; spin_lock_irqsave(&sem->lock, flags); if (likely(list_empty(&sem->wait_list))) sem->count++; else __up(sem); spin_unlock_irqrestore(&sem->lock, flags); } 根據(jù)上述代碼,能夠發(fā)現(xiàn),up() 函數(shù)會在任務隊列為空的時候把信號量的引用計數(shù)count 加一,否則就會調(diào)用 __up() 函數(shù),相關的C語言代碼如下: __up()函數(shù)會喚醒等待隊列里的任務,確保在釋放信號量的時候等待隊列里的任務都有機會執(zhí)行。 小結自旋鎖提供了一種快速簡單的加鎖方法,不過它并不適合較長時間的保護臨界區(qū),這一需求最好借助可以睡眠的信號量。而如果鎖僅會被短時間持有,再使用信號量就不太合適了,因為睡眠和維護等待隊列,以及喚醒任務所花費的開銷可能比鎖占用的全部時間還要大。 另外,由于中斷上下文中是不能調(diào)度的,因此不能使用信號量,因為線程在申請信號量失敗時可能會進入睡眠。引申一點,應該明白信號量最好不要和自旋鎖共用,因為線程持有自旋鎖的時候是不允許睡眠的。 |
|