Linux内核之自旋锁和信号量

来源:互联网 发布:sql 替换字符 编辑:程序博客网 时间:2024/03/29 20:12

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内核用的最多的是自旋锁,而不是信号量的原因)
0 0
原创粉丝点击