1. 程式人生 > 其它 >訊號量、互斥體和自旋鎖

訊號量、互斥體和自旋鎖

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上形成的佇列如下圖:

下載(13 KB)  2010-7-8 14:02

(注:將一個程序阻塞,一般的經過是先把程序放到等待佇列中,接著改變程序的狀態,比如設為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可以為負數。   

 

使用場所:

     訊號量主要適用於程序間通訊,當然,也可用於執行緒間通訊。而互斥鎖只能用於執行緒間通訊。