如果一個任務獲取訊號量失敗,該任務就必須等待,直到其他任務釋放訊號量。本文的重點是,在Linux中,當有任務釋放訊號量之後,如何喚醒正在等待該訊號量的任務。
訊號量定義如下:
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
其中wait_list
連結串列用於管理因沒有成功獲取訊號量而處於睡眠狀態的任務。
任務通過呼叫down()
函式,嘗試獲取訊號量,如果獲取訊號量失敗,呼叫__down()
函式。__down()
函式內部呼叫了__down_common
函式。(事實上down()
函式有多個變種,如down_interruptible
,在獲取訊號量失敗時呼叫__down_interruptible
,__down_interruptible
也會呼叫__down_common
函式。不同的down()
函式最終呼叫__down_common
時傳入不同的引數,以處理不同的獲取訊號量的情況)。
同時,整個down()函式使用sem->lock
保護起來。
void down(struct semaphore *sem)
{
unsigned long flags;
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(sem->count > 0))
sem->count--;
else
__down(sem);
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
static noinline void __sched __down(struct semaphore *sem)
{
__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
下面是重點:__down_common
函式如何使任務休眠,休眠中的任務如何被喚醒並獲得訊號量。
semaphore_waiter
是一個關鍵的資料結構,代表一個獲取訊號量失敗,正在等待的任務。up
欄位標識了該任務是否是被該訊號量喚醒,也就是休眠中的任務收到某種訊號被喚醒之後,判斷是否是被等待中的訊號量喚醒的。
struct semaphore_waiter {
struct list_head list;
struct task_struct *task;
bool up;
};
__down_common
函式首先初始化了一個semaphore_waiter
。task欄位標識當前任務,up設定為false。
static inline int __sched __down_common(struct semaphore *sem, long state,
long timeout)
{
struct semaphore_waiter waiter;
list_add_tail(&waiter.list, &sem->wait_list);
waiter.task = current;
waiter.up = false;
...
然後休眠當前任務,呼叫 schedule_timeout()主動讓出 CPU。上文提到整個函式都是在sem->lock
的臨界區中,但是在自旋鎖的臨界區是不可以休眠的,所以這裡實際上在休眠之前釋放了鎖,被喚醒之後再重新獲得鎖。
當任務被喚醒後,如果waiter.up
是否為真,則該任務可以獲得訊號量。waiter.up
是必須要判斷的,取決於__set_current_state()
函式傳入的引數不同,任務可能處於不同的休眠狀態,可能被不同的訊號喚醒,而未必是被等待的訊號喚醒。
for (;;) {
if (signal_pending_state(state, current))
goto interrupted;
if (unlikely(timeout <= 0))
goto timed_out;
__set_current_state(state);
raw_spin_unlock_irq(&sem->lock);
timeout = schedule_timeout(timeout);
raw_spin_lock_irq(&sem->lock);
if (waiter.up)
return 0;
}
timed_out:
list_del(&waiter.list);
return -ETIME;
interrupted:
list_del(&waiter.list);
return -EINTR;
}
當一個任務釋放訊號量時,如果訊號量的等待佇列中存在任務,則將佇列中的第一個任務的 up
標記為true,並喚醒,同時從等待佇列中刪除。
同時,只有在等待佇列為空的情況下,才會更新sem->count
,確保了等待佇列中的任務優先於新來的任務獲得訊號量,保證了嚴格的先進先出,不會因為新來的任務導致等待佇列中的任務飢餓。
void up(struct semaphore *sem)
{
unsigned long flags;
raw_spin_lock_irqsave(&sem->lock, flags);
if (likely(list_empty(&sem->wait_list)))
sem->count++;
else
__up(sem);
raw_spin_unlock_irqrestore(&sem->lock, flags);
}
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 = true;
wake_up_process(waiter->task);
}
任務被喚醒之後,檢測到up
為true,返回0,成功獲得訊號量。