自旋锁和互斥锁的区别

使用任何锁都需要消耗系统资源(内存资源和CPU时间),这种资源消耗可以分为两类:

       1.建立锁所需要的资源

       2.当线程被阻塞时所需要的资源

自选锁的主要特征:

        当自旋锁被一个线程获得时,它不能被其它线程获得。如果其他线程尝试去phtread_spin_lock()获得该锁,那么它将不会从该函数返回,而是一直自旋(spin),直到自旋锁可用为止。

使用自旋锁时要注意:

  • 由于自旋时不释放CPU,因而持有自旋锁的线程应该尽快释放自旋锁,否则等待该自旋锁的线程会一直在哪里自旋,这就会浪费CPU时间。

  • 持有自旋锁的线程在sleep之前应该释放自旋锁以便其他共享可以获得该自旋锁。内核编程中,如果持有自旋锁的代码sleep了就可能导致整个系统挂起。(下面会解释)

自旋锁和互斥锁的区别:

          从实现原理上来讲,Mutex属于sleep-waiting类型的 锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和Core1上。假设线程A想要通过 pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞(blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中, 此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。

        而Spin lock则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。

        pthread_mutex_lock()操作如果 没有锁成功的话就会调用system_wait()的系统调用并将当前线程加入该mutex的等待队列里。而spin lock则可以理解为在一个while(1)循环中用内嵌的汇编代码实现的锁操作(印象中看过一篇论文介绍说在linux内核中spin lock操作只需要两条CPU指令,解锁操作只用一条指令就可以完成)。

        对于自旋锁来说,它只需要消耗很少的资源来建立锁;随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间。

        对于互斥锁来说,与自旋锁相比它需要消耗大量的系统资源来建立锁;随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用 时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。

        因此自旋锁和互斥锁适用于不同的场景。自旋锁适用于那些仅需要阻塞很短时间的场景,而互斥锁适用于那些可能会阻塞很长时间的场景。 

自旋锁与linux内核进程调度关系

前面有提到,如果临界区可能包含引起睡眠的代码则不能使用自旋锁,否则可能引起死锁:

那么为什么信号量保护的代码可以睡眠而自旋锁会死锁呢?

先看下自旋锁的实现方法吧,自旋锁的基本形式如下

spin_lock(&mr_lock):
    
    //critical region
 
spin_unlock(&mr_lock);

跟踪一下spin_lock(&mr_lock)的实现

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

        注意到“preempt_disable()”,这个调用的功能是“关抢占”(在spin_unlock中会重新开启抢占功能)。从中可以看出,使用自旋锁保护的区域是工作在非抢占的状态;即使获取不到锁,在“自旋”状态也是禁止抢占的。了解到这,我想咱们应该能够理解为何自旋锁保护 的代码不能睡眠了。        

        试想一下,如果在自旋锁保护的代码中间睡眠,此时发生进程调度,则可能另外一个进程会再次调用spinlock保护的这段代码。而我们知道了即使在获取不到锁的“自旋”状态,也是禁止抢占的,而“自旋”又是动态的,不会再睡眠了,也就是说在这个处理器上不会再有进程调度发生了,那么 死锁自然就发生了。

为什么拥有自旋锁的代码段必须是原子的

看了上面自旋锁的实现与preempt_disable()的实现,可以讲明白为什么spinlock中不能睡眠了:

首先,并不是睡眠了一定会出问题,而是在spinlock中睡眠是一个坏主意:可能有死锁的风险

假设是单处理器场景:

P1进程关闭进程抢占并获取锁,这时P1->thread_info->preempt_count->preemption count++,P1进程不可以被抢占

但是P1能够自己交出自己,假设P1调用了某些可能导致睡眠的函数(kmalloc/copy_from_user/copy_to_user/sleep...),最终会调到schedule()函数

调度器调度P2进程

这时恰好P2进程也去获取同一把spinlock,在真正操作锁之前P2还要关闭自己的进程抢占,这时P2->thread_info->preempt_count->preemption count++,P2进程不可以被抢占且P2进程无法获取spinlock而产生了自旋

P2进程此时的状态就很尴尬: 首先它没有主动调度schedule()换出自己,其次由于自身进程关闭了抢占,外部调度器无法通过抢占换出P2,再加上获取不到锁的自旋,所以P2进入了无尽自旋的状态,导致死锁占住了CPU。

总结下自旋锁的特点:

  • 单CPU非抢占内核下:自旋锁会在编译时被忽略(因为单CPU且非抢占模式情况下,不可能发生进程切换,时钟只有一个进程处于临界区(自旋锁实际没什么用了)

  • 单CPU抢占内核下:自选锁仅仅当作一个设置抢占的开关(因为单CPU不可能有并发访问临界区的情况,禁止抢占就可以保证临街区唯一被拥有)

  • 多CPU下:此时才能完全发挥自旋锁的作用,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争

linux发生抢占的时间

linux抢占发生的时间,抢占分为 用户抢占和 内核抢占。

用户抢占在以下情况下产生:

  • 从系统调用返回用户空间

  • 从中断处理程序返回用户空间

内核抢占会发生在:

  • 当从中断处理程序返回内核空间的时候,且当时内核具有可抢占性

  • 当内核代码再一次具有可抢占性的时候(如:spin_unlock时)

  • 如果内核中的任务显示的调用schedule()

基本的进程调度就是发生在时钟中断后,并且发现进程的时间片已经使用完了,则发生进程抢占。通常我们会利用中断处理程序返回内核空间的时候可进行内核抢占这个特性来提高一些I/O操作的实时性,如:当I/O事件发生的时候,对应的中断处理程序被激活,当它发现有进程在等待这个I/O事件的时候,它 会激活等待进程,并且设置当前正在执行进程的need_resched标志,这样在中断处理程序返回的时候,调度程序被激活,原来在等待I/O事件的进程 (很可能)获得执行权,从而保证了对I/O事件的相对快速响应(毫秒级)。可以看出,在I/O事件发生的时候,I/O事件的处理进程会抢占当前进程,系统 的响应速度与调度时间片的长度无关。

  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值