天天看點

(轉)信号量、互斥體和自旋鎖

信号量、互斥體和自旋鎖

一、信号量

      信号量又稱為信号燈,它是用來協調不同程序間的資料對象的,而最主要的應用是共享記憶體方式的程序間通信。本質上,信号量是一個計數器,它用來記錄對某個資源(如共享記憶體)的存取狀況。一般說來,為了獲得共享資源,程序需要執行下列操作: 

   (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可以為負數。  

使用場所:

     信号量主要适用于程序間通信,當然,也可用于線程間通信。而互斥鎖隻能用于線程間通信。

轉自:http://www.cnblogs.com/biyeymyhjob/archive/2012/07/21/2602015.html#3173263

繼續閱讀