Linux內核之自旋鎖和信號量

Linux內核實現了多種同步方法,指令級支持的原子操作、自旋鎖、信號量、互斥鎖、完成量、大內核鎖等等,我就挑比較有代表性的兩個鎖——自旋鎖和信號量來分析。

自旋鎖

Linux內核中最常用的鎖就是自旋鎖(spin lock),自旋鎖最多隻能被一個執行線程持有。如果一個執行線程試圖獲得一個被已經持有(即所謂爭用)的自旋鎖,那麼該線程就會一直進行忙循環-旋轉-等待鎖重新可用。在任意時間,自旋鎖都可以防止多於一個的執行線程同時進入臨界區。自旋鎖的實現和體系結構密切相關,代碼往往通過彙編實現。

#define __LOCK(lock) \
  do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)

#define __LOCK_BH(lock) \
  do { local_bh_disable(); __LOCK(lock); } while (0)

#define __LOCK_IRQ(lock) \
  do { local_irq_disable(); __LOCK(lock); } while (0)

#define __LOCK_IRQSAVE(lock, flags) \
  do { local_irq_save(flags); __LOCK(lock); } while (0)

#define __UNLOCK(lock) \
  do { preempt_enable(); __release(lock); (void)(lock); } while (0)

#define __UNLOCK_BH(lock) \
  do { preempt_enable_no_resched(); local_bh_enable(); __release(lock); (void)(lock); } while (0)

#define __UNLOCK_IRQ(lock) \
  do { local_irq_enable(); __UNLOCK(lock); } while (0)

#define __UNLOCK_IRQRESTORE(lock, flags) \
  do { local_irq_restore(flags); __UNLOCK(lock); } while (0)

#define _spin_lock(lock)			__LOCK(lock)
#define _spin_lock_nested(lock, subclass)	__LOCK(lock)
#define _read_lock(lock)			__LOCK(lock)
#define _write_lock(lock)			__LOCK(lock)
#define _spin_lock_bh(lock)			__LOCK_BH(lock)
#define _read_lock_bh(lock)			__LOCK_BH(lock)
#define _write_lock_bh(lock)			__LOCK_BH(lock)
#define _spin_lock_irq(lock)			__LOCK_IRQ(lock)
#define _read_lock_irq(lock)			__LOCK_IRQ(lock)
#define _write_lock_irq(lock)			__LOCK_IRQ(lock)
#define _spin_lock_irqsave(lock, flags)		__LOCK_IRQSAVE(lock, flags)
#define _read_lock_irqsave(lock, flags)		__LOCK_IRQSAVE(lock, flags)
#define _write_lock_irqsave(lock, flags)	__LOCK_IRQSAVE(lock, flags)
#define _spin_trylock(lock)			({ __LOCK(lock); 1; })
#define _read_trylock(lock)			({ __LOCK(lock); 1; })
#define _write_trylock(lock)			({ __LOCK(lock); 1; })
#define _spin_trylock_bh(lock)			({ __LOCK_BH(lock); 1; })
#define _spin_unlock(lock)			__UNLOCK(lock)
#define _read_unlock(lock)			__UNLOCK(lock)
#define _write_unlock(lock)			__UNLOCK(lock)
#define _spin_unlock_bh(lock)			__UNLOCK_BH(lock)
#define _write_unlock_bh(lock)			__UNLOCK_BH(lock)
#define _read_unlock_bh(lock)			__UNLOCK_BH(lock)
#define _spin_unlock_irq(lock)			__UNLOCK_IRQ(lock)
#define _read_unlock_irq(lock)			__UNLOCK_IRQ(lock)
#define _write_unlock_irq(lock)			__UNLOCK_IRQ(lock)
#define _spin_unlock_irqrestore(lock, flags)	__UNLOCK_IRQRESTORE(lock, flags)
#define _read_unlock_irqrestore(lock, flags)	__UNLOCK_IRQRESTORE(lock, flags)
#define _write_unlock_irqrestore(lock, flags)	__UNLOCK_IRQRESTORE(lock, flags)


在spinlock_api_up.h文件中定義了所有的自旋鎖函數的實現,基本方式就是禁止中斷->禁止搶佔->獲得鎖,spin_lock這個函數沒有禁止中斷,因爲它主要用於中斷處理程序中保護臨界區,中斷處理程序本身就是禁止中斷的。

因爲自旋鎖在同一時刻至多被一個執行線程持有,所以一個時刻只能有一個線程位於臨界區內,這就是爲多處理機器提供了防止併發訪問所需的保護機制。注意在單處理器機器上,編譯的時候並不會加入自旋鎖。它僅僅被當做一個設置內核搶佔機制是否被啓用的開關。如果禁止內核搶佔,那麼在編譯時自旋鎖會被完全剔除出內核。

關於自旋鎖的使用注意

1.自旋鎖不可遞歸

Linux內核實現的自旋鎖是不可遞歸的,這點不同於自旋鎖在其他操作系統的實現,所以如果你試圖得到一個你正在持有的鎖,你必須自旋,等待你自己釋放這個鎖。但是你處於自旋忙等待中,你永遠沒有機會釋放鎖,內核就崩了。

2.中斷重入會導致死鎖

在中斷處理程序中使用自旋鎖時,一定要在獲取鎖之前,首先禁止本地中斷(在當前處理器上的中斷請求),否則,中斷處理程序就會打斷正在持有鎖的內核代碼,有可能會試圖去爭用這個已經被持有的自旋鎖。這樣一來,中斷處理程序就會自旋,等待該鎖重新可用,但是鎖的持有者在這個中斷處理程序執行完畢前不可能運行,所以就造成了死鎖。這裏要注意一點,需要關閉的只是本地中斷(當前處理器)。如果中斷髮生在不同的處理器上,即使中斷處理程序在同一鎖上自旋,也不會妨礙鎖的持有者(在不同處理器上)最終釋放鎖。

3.持有自旋鎖時不可睡眠

持有自旋鎖後如果進程睡眠了,那麼就不知道何時才能重新被喚醒,此時如果還有另外的進程要獲得這個自旋鎖,那它就會一直忙等,非常浪費處理器資源


信號量

不同於自旋鎖,Linux中的信號量是一種睡眠鎖。如果有一個任務試圖獲得一個不可用(已經被佔用)的信號量時,信號量會將其推進一個等待隊列,然後讓其睡眠。這時處理器能重獲自由,從而去執行其他代碼。當持有的信號量可用(被釋放)後,處於等待隊列的那個任務將被喚醒,並獲得該信號量。

接下來看看信號量是怎麼實現的

void down(struct semaphore *sem)
{
	unsigned long flags;

	spin_lock_irqsave(&sem->lock, flags);
	if (likely(sem->count > 0))
		sem->count--;
	else
		__down(sem);
	spin_unlock_irqrestore(&sem->lock, flags);
}
static noinline void __sched __down(struct semaphore *sem)
{
	__down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
static inline int __sched __down_common(struct semaphore *sem, long state,
								long timeout)
{
	struct task_struct *task = current;
	struct semaphore_waiter waiter;


	//把當前進程加入信號量的等待隊列
	list_add_tail(&waiter.list, &sem->wait_list);
	waiter.task = task;
	waiter.up = 0;


	for (;;) {
		//如果有信號要處理,那就當什麼都沒發生,直接退出
		if (state == TASK_INTERRUPTIBLE && signal_pending(task))
			goto interrupted;
		//如果當前進程收到了SIGKILL信號,代表這個進程要被殺死了,所以也啥都不幹
		if (state == TASK_KILLABLE && fatal_signal_pending(task))
			goto interrupted;
		if (timeout <= 0)
			goto timed_out;
		__set_task_state(task, state);
		//調度之前一定要釋放自旋鎖
		spin_unlock_irq(&sem->lock);
		//延遲調度,如果timeout=MAX_SCHEDULE_TIMEOUT,和直接調動schedule函數沒區別
		timeout = schedule_timeout(timeout);
		//重新拿到鎖
		spin_lock_irq(&sem->lock);
		//如果是由up函數喚醒的返回正常,如果是用戶空間的信號所中斷或超時信號所引起的喚醒
		//那就接着執行,返回錯誤
		if (waiter.up)
			return 0;
	}


 timed_out:
	list_del(&waiter.list);
	return -ETIME;


 interrupted:
	list_del(&waiter.list);
	return -EINTR;
}
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);
}
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);
	//表明是該進程是up函數喚醒
	waiter->up = 1;
	//喚醒進程
	wake_up_process(waiter->task);
}

從上面的代碼可以看到,信號量也是依賴於自旋鎖的。
信號量分爲計數信號量和二值信號量(也叫互斥信號量),計數信號量不能用來進行強制互斥,因爲它允許多個執行線程同時訪問臨界區。相反,這種信號量用來對特定代碼加以限制,內核使用它的機會不多,基本用到的都是互斥信號量(計數等於1的信號量),除互斥外,信號量還能進行進程同步,把計數初始化爲0就可以實現進程間的同步。

再來說說自旋鎖和信號量使用場景上的區別
  • 自旋鎖適用於較短時間持有鎖,信號量適用於較長時間持有鎖
  • 自旋鎖是忙等,而信號量是睡眠
  • 自旋鎖可用於進程上下文和中斷上下文,而信號量只能用於進程上下文
  • 自旋鎖禁止內核搶佔,信號量不是
  • 在你佔用信號量的同時不能佔用自旋鎖。因爲在你等待信號量時可能會睡眠,而在持有自旋鎖時時不允許睡眠的
  • 自旋鎖不會發生上下文切換,而信號量會(我猜這就是爲什麼Linux內核用的最多的是自旋鎖,而不是信號量的原因)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章