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内核用的最多的是自旋锁,而不是信号量的原因)
- Linux内核之自旋锁和信号量
- 内核同步之自旋锁和信号量
- 内核同步之自旋锁和信号量
- 内核同步之自旋锁和信号量
- Linux 内核同步之自旋锁与信号量的异同
- linux 自旋锁和信号量
- linux 自旋锁和信号量
- linux 自旋锁和信号量
- linux 自旋锁和信号量
- linux 自旋锁和信号量
- linux驱动之 信号量 自旋锁 互斥体
- linux之自旋锁、信号量、互斥锁
- 驱动开发之 信号量和自旋锁
- 信号量和自旋锁
- 信号量和自旋锁
- 信号量和自旋锁
- 信号量和自旋锁
- 信号量和自旋锁
- qt5.7.0creator打不开解决办法
- hiho 1496 : 寻找最大值 高维前缀最大&次大
- RBF神经网络
- HTTP请求
- C++ 常量指针和指针常量
- Linux内核之自旋锁和信号量
- 删除右键菜单
- Android之绘制文本(FontMetrics) 获取文本高度
- 蓝桥杯 快速排序
- 安卓webview和js+html交互利用的addJavascriptInterface和webview.loadUrl("javascript:**");
- String、StringBuffer和StringBuilder
- 如何爬取了知乎用户信息,并做了简单的分析
- xshell连接ubantu connection failed
- Unity -- Json文件读取