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);
}
从上面的代码可以看到,信号量也是依赖于自旋锁的。