訊號量、互斥體和自旋鎖
https://blog.csdn.net/weixin_34265814/article/details/85508992?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_default&utm_relevant_index=2
轉載:
一、訊號量
訊號量又稱為訊號燈,它是用來協調不同程序間的資料物件的,而最主要的應用是共享記憶體方式的程序間通訊。本質上,訊號量是一個計數器,它用來記錄對某個資源(如共享記憶體)的存取狀況。一般說來,為了獲得共享資源,程序需要執行下列操作:
(1) 測試控制該資源的訊號量。
(2) 若此訊號量的值為正,則允許進行使用該資源。程序將訊號量減1。
(3) 若此訊號量為0,則該資源目前不可用,程序進入睡眠狀態,直至訊號量值大於0,程序被喚醒,轉入步驟(1)。
(4) 當程序不再使用一個訊號量控制的資源時,訊號量值加1。如果此時有程序正在睡眠等待此訊號量,則喚醒此程序。
維護訊號量狀態的是Linux核心作業系統而不是使用者程序。我們可以從標頭檔案/usr/src/linux/include/linux/sem.h 中看到核心用來維護訊號量狀態的各個結構的定義。訊號量是一個數據集合,使用者可以單獨使用這一集合的每個元素。要呼叫的第一個函式是semget,用以獲得一個訊號量ID。Linux2.6.26下定義的訊號量結構體:
struct semaphore { spinlock_t lock; unsigned int count; struct list_head wait_list; };
從以上訊號量的定義中,可以看到訊號量底層使用到了spin lock的鎖定機制,這個spinlock主要用來確保對count成員的原子性的操作(count--)和測試(count > 0)。
1.訊號量的P操作:
(1).void down(struct semaphore *sem);
(2).int down_interruptible(struct semaphore *sem);
(3).int down_trylock(struct semaphore *sem);
說明:
(1)中的函式根據2.6.26中的程式碼註釋,這個函式已經out了(Use of this function is deprecated),所以從實用角度,徹底忘了它吧。
(2)最常用,函式原型
/** * down_interruptible - acquire the semaphore unless interrupted * @sem: the semaphore to be acquired * * Attempts to acquire the semaphore. If no more tasks are allowed to * acquire the semaphore, calling this function will put the task to sleep. * If the sleep is interrupted by a signal, this function will return -EINTR. * If the semaphore is successfully acquired, this function returns 0. */ int down_interruptible(struct semaphore *sem) { unsigned long flags; int result = 0; spin_lock_irqsave(&sem->lock, flags); if (likely(sem->count > 0)) sem->count--; else result = __down_interruptible(sem); spin_unlock_irqrestore(&sem->lock, flags); return result; }
對此函式的理解:在保證原子操作的前提下,先測試count是否大於0,如果是說明可以獲得訊號量,這種情況下需要先將count--,以確保別的程序能否獲得該訊號量,然後函式返回,其呼叫者開始進入臨界區。如果沒有獲得訊號量,當前程序利用struct semaphore 中wait_list加入等待佇列,開始睡眠。
對於需要休眠的情況,在__down_interruptible()函式中,會構造一個struct semaphore_waiter型別的變數(struct semaphore_waiter定義如下:
struct semaphore_waiter { struct list_head list; struct task_struct *task; int up; };
),將當前程序賦給task,並利用其list成員將該變數的節點加入到以sem中的wait_list為頭部的一個列表中,假設有多個程序在sem上呼叫down_interruptible,則sem的wait_list上形成的佇列如下圖:
(注:將一個程序阻塞,一般的經過是先把程序放到等待佇列中,接著改變程序的狀態,比如設為TASK_INTERRUPTIBLE,然後呼叫排程函式schedule(),後者將會把當前程序從cpu的執行佇列中摘下)
(3)試圖去獲得一個訊號量,如果沒有獲得,函式立刻返回1而不會讓當前程序進入睡眠狀態。
2.訊號量的V操作
void up(struct semaphore *sem);
原型如下:
/** * up - release the semaphore * @sem: the semaphore to release * * Release the semaphore. Unlike mutexes, up() may be called from any * context and even by tasks which have never called down(). */ 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); }
如果沒有其他執行緒等待在目前即將釋放的訊號量上,那麼只需將count++即可。如果有其他執行緒正因為等待該訊號量而睡眠,那麼呼叫__up.
__up的定義:
static noinline void __sched __up(struct semaphore *sem) { struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list, struct semaphore_waiter, list); list_del(&waiter->list); waiter->up = 1; wake_up_process(waiter->task); }
這個函式首先獲得sem所在的wait_list為頭部的連結串列的第一個有效節點,然後從連結串列中將其刪除,然後喚醒該節點上睡眠的程序。
由此可見,對於sem上的每次down_interruptible呼叫,都會在sem的wait_list連結串列尾部加入一新的節點。對於sem上的每次up呼叫,都會刪除掉wait_list連結串列中的第一個有效節點,並喚醒睡眠在該節點上的程序。
關於Linux環境下訊號量其他API 詳見LKD和ULD
二、互斥體
互斥體實現了“互相排斥”(mutual exclusion)同步的簡單形式(所以名為互斥體(mutex))。互斥體禁止多個執行緒同時進入受保護的程式碼“臨界區”(critical section)。因此,在任意時刻,只有一個執行緒被允許進入這樣的程式碼保護區。
任何執行緒在進入臨界區之前,必須獲取(acquire)與此區域相關聯的互斥體的所有權。如果已有另一執行緒擁有了臨界區的互斥體,其他執行緒就不能再進入其中。這些執行緒必須等待,直到當前的屬主執行緒釋放(release)該互斥體。
什麼時候需要使用互斥體呢?互斥體用於保護共享的易變程式碼,也就是,全域性或靜態資料。這樣的資料必須通過互斥體進行保護,以防止它們在多個執行緒同時訪問時損壞
Linux 2.6.26中mutex的定義:
struct mutex { /* 1: unlocked, 0: locked, negative: locked, possible waiters */ atomic_t count; spinlock_t wait_lock; struct list_head wait_list; #ifdef CONFIG_DEBUG_MUTEXES struct thread_info *owner; const char *name; void *magic; #endif #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map; #endif };
對比前面的struct semaphore,struct mutex除了增加了幾個作為debug用途的成員變數外,和semaphore幾乎長得一樣。但是mutex的引入主要是為了提供互斥機制,以避免多個程序同時在一個臨界區中執行。
如果靜態宣告一個count=1的semaphore變數,可以使用DECLARE_MUTEX(name),DECLARE_MUTEX(name)實際上是定義一個semaphore,所以它的使用應該對應訊號量的P,V函式.
如果要定義一個靜態mutex型變數,應該使用DEFINE_MUTEX
如果在程式執行期要初始化一個mutex變數,可以使用mutex_init(mutex),mutex_init是個巨集,在該巨集定義的內部,會呼叫__mutex_init函式。
#define mutex_init(mutex) \ do { \ static struct lock_class_key __key; \ \ __mutex_init((mutex), #mutex, &__key); \ } while (0)
__mutex_init定義如下:
/*** * mutex_init - initialize the mutex * @lock: the mutex to be initialized * * Initialize the mutex to unlocked state. * * It is not allowed to initialize an already locked mutex. */ void __mutex_init(struct mutex *lock, const char *name, struct lock_class_key *key) { atomic_set(&lock->count, 1); spin_lock_init(&lock->wait_lock); INIT_LIST_HEAD(&lock->wait_list); debug_mutex_init(lock, name, key); }
從__mutex_init的定義可以看出,在使用mutex_init巨集來初始化一個mutex變數時,應該使用mutex的指標型。
mutex上的P,V操作:void mutex_lock(struct mutex *lock)和void __sched mutex_unlock(struct mutex *lock)
從原理上講,mutex實際上是count=1情況下的semaphore,所以其PV操作應該和semaphore是一樣的。但是在實際的Linux程式碼上,出於效能優化的角度,並非只是單純的重用down_interruptible和up的程式碼。以ARM平臺的mutex_lock為例,實際上是將mutex_lock分成兩部分實現:fast
path和slow path,主要是基於這樣一個事實:在絕大多數情況下,試圖獲得互斥體的程式碼總是可以成功獲得。所以Linux的程式碼針對這一事實用ARM
V6上的LDREX和STREX指令來實現fast path以期獲得最佳的執行效能。這裡對於mutex的實現細節,不再多說,如欲深入瞭解,參考APUE和ULD
三、自旋鎖
自旋鎖它是為為實現保護共享資源而提出一種鎖機制。其實,自旋鎖與互斥鎖比較類似,它們都是為了解決對某項資源的互斥使用。無論是互斥鎖,還是自旋鎖,在任何時刻,最多隻能有一個保持者,也就說,在任何時刻最多隻能有一個執行單元獲得鎖。但是兩者在排程機制上略有不同。對於互斥鎖,如果資源已經被佔用,資源申請者只能進入睡眠狀態。但是自旋鎖不會引起呼叫者睡眠,如果自旋鎖已經被別的執行單元保持,呼叫者就一直迴圈在那裡看是否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是因此而得名。
自旋鎖一般原理
跟互斥鎖一樣,一個執行單元要想訪問被自旋鎖保護的共享資源,必須先得到鎖,在訪問完共享資源後,必須釋放鎖。如果在獲取自旋鎖時,沒有任何執行單元保持該鎖,那麼將立即得到鎖;如果在獲取自旋鎖時鎖已經有保持者,那麼獲取鎖操作將自旋在那裡,直到該自旋鎖的保持者釋放了鎖。由此我們可以看出,自旋鎖是一種比較低階的保護資料結構或程式碼片段的原始方式,這種鎖可能存在兩個問題:死鎖和過多佔用cpu資源。
自旋鎖適用情況
自旋鎖比較適用於鎖使用者保持鎖時間比較短的情況。正是由於自旋鎖使用者一般保持鎖時間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠高於互斥鎖。訊號量和讀寫訊號量適合於保持時間較長的情況,它們會導致呼叫者睡眠,因此只能在程序上下文使用,而自旋鎖適合於保持時間非常短的情況,它可以在任何上下文使用。如果被保護的共享資源只在程序上下文訪問,使用訊號量保護該共享資源非常合適,如果對共享資源的訪問時間非常短,自旋鎖也可以。但是如果被保護的共享資源需要在中斷上下文訪問(包括底半部即中斷處理控制代碼和頂半部即軟中斷),就必須使用自旋鎖。自旋鎖保持期間是搶佔失效的,而訊號量和讀寫訊號量保持期間是可以被搶佔的。自旋鎖只有在核心可搶佔或SMP(多處理器)的情況下才真正需要,在單CPU且不可搶佔的核心下,自旋鎖的所有操作都是空操作。另外格外注意一點:自旋鎖不能遞迴使用。
關於自旋鎖的定義以及相應的API
自旋鎖定義: linux/Spinlock.h
typedef struct spinlock { union { //聯合 struct raw_spinlock rlock; #ifdef CONFIG_DEBUG_LOCK_ALLOC # define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map)) struct{ u8 __padding[LOCK_PADSIZE]; struct lockdep_map dep_map; }; #endif }; } spinlock_t;
定義和初始化
spinlock_t my_lock = SPIN_LOCK_UNLOCKED;
void spin_lock_init(spinlock_t *lock);
自旋鎖操作:
//加鎖一個自旋鎖函式 void spin_lock(spinlock_t *lock); //獲取指定的自旋鎖 void spin_lock_irq(spinlock_t *lock); //禁止本地中斷獲取指定的鎖 void spin_lock_irqsave(spinlock_t *lock, unsigned long flags); //儲存本地中斷的狀態,禁止本地中斷,並獲取指定的鎖 void spin_lock_bh(spinlock_t *lock) //安全地避免死鎖, 而仍然允許硬體中斷被服務 //釋放一個自旋鎖函式 void spin_unlock(spinlock_t *lock); //釋放指定的鎖 void spin_unlock_irq(spinlock_t *lock); //釋放指定的鎖,並激活本地中斷 void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags); //釋放指定的鎖,並讓本地中斷恢復到以前的狀態 void spin_unlock_bh(spinlock_t *lock); //對應於spin_lock_bh //非阻塞鎖 int spin_trylock(spinlock_t *lock); //試圖獲得某個特定的自旋鎖,如果該鎖已經被爭用,該方法會立刻返回一個非0值,
//而不會自旋等待鎖被釋放,如果成果獲得了這個鎖,那麼就返回0. int spin_trylock_bh(spinlock_t *lock); //這些函式成功時返回非零( 獲得了鎖 ), 否則 0. 沒有"try"版本來禁止中斷. //其他 int spin_is_locked(spinlock_t *lock); //和try_lock()差不多
四、訊號量、互斥體和自旋鎖的區別
訊號量/互斥體和自旋鎖的區別
訊號量/互斥體允許程序睡眠屬於睡眠鎖,自旋鎖則不允許呼叫者睡眠,而是讓其迴圈等待,所以有以下區別應用
1)、訊號量和讀寫訊號量適合於保持時間較長的情況,它們會導致呼叫者睡眠,因而自旋鎖適合於保持時間非常短的情況
2)、自旋鎖可以用於中斷,不能用於程序上下文(會引起死鎖)。而訊號量不允許使用在中斷中,而可以用於程序上下文
3)、自旋鎖保持期間是搶佔失效的,自旋鎖被持有時,核心不能被搶佔,而訊號量和讀寫訊號量保持期間是可以被搶佔的
另外需要注意的是
1)、訊號量鎖保護的臨界區可包含可能引起阻塞的程式碼,而自旋鎖則絕對要避免用來保護包含這樣程式碼的臨界區,因為阻塞意味著要進行程序的切換,如果程序被切換出去後,另一程序企圖獲取本自旋鎖,死鎖就會發生。
2)、在你佔用訊號量的同時不能佔用自旋鎖,因為在你等待訊號量時可能會睡眠,而在持有自旋鎖時是不允許睡眠的。
訊號量和互斥體之間的區別
概念上的區別:
訊號量:是程序間(執行緒間)同步用的,一個程序(執行緒)完成了某一個動作就通過訊號量告訴別的程序(執行緒),別的程序(執行緒)再進行某些動作。有二值和多值訊號量之分。
互斥鎖:是執行緒間互斥用的,一個執行緒佔用了某一個共享資源,那麼別的執行緒就無法訪問,直到這個執行緒離開,其他的執行緒才開始可以使用這個共享資源。可以把互斥鎖看成二值訊號量。
上鎖時:
訊號量: 只要訊號量的value大於0,其他執行緒就可以sem_wait成功,成功後訊號量的value減一。若value值不大於0,則sem_wait阻塞,直到sem_post釋放後value值加一。一句話,訊號量的value>=0。
互斥鎖: 只要被鎖住,其他任何執行緒都不可以訪問被保護的資源。如果沒有鎖,獲得資源成功,否則進行阻塞等待資源可用。一句話,執行緒互斥鎖的vlaue可以為負數。
使用場所:
訊號量主要適用於程序間通訊,當然,也可用於執行緒間通訊。而互斥鎖只能用於執行緒間通訊。