Linux中訊號量的實現

vptvpt發表於2022-04-18

如果一個任務獲取訊號量失敗,該任務就必須等待,直到其他任務釋放訊號量。本文的重點是,在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,成功獲得訊號量。

相關文章