Linux中的spinlock和mutex

less than 1 minute read

背景

最近在压测PostgreSQL同步流复制时,遇到一个mutex锁的瓶颈问题。

具体见 《PostgreSQL 同步流复制锁瓶颈分析》

PG是以backend process睡眠,然后通过sender唤醒的方式来处理同步等待的问题。

转一篇文章,了解一下spinlock, mutex。

http://www.aichengxu.com/view/2456962

spinlock (自旋锁)

自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁)。

自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被争用(已经被持有)的自旋锁,那么这个任务就会一直进行忙循环——旋转——等待锁重新可用。

要是锁未被争用,请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。

事实上,自旋锁的初衷就是:

在短期间内进行轻量级的锁定。一个进程去获取被争用的自旋锁时,请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长(等待时CPU被独占)。如果需要长时间锁定的话, 最好使用信号量(睡眠,CPU资源可出让)。

简单的说,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。另外自旋锁不允许任务睡眠(持有自旋锁的任务睡眠会造成自死锁——因为睡眠有可能造成持有锁的内核任务被重新调度,而再次申请自己已持有的锁),它能够在中断上下文中使用。

死锁:假设有一个或多个内核任务和一个或多个资源,每个内核都在等待其中的一个资源,但所有的资源都已经被占用了。这便会发生所有内核任务都在相互等待,但它们永远不会释放已经占有的资源,于是任何内核任务都无法获得所需要的资源,无法继续运行,这便意味着死锁发生了。自死琐是说自己占有了某个资源,然后自己又申请自己已占有的资源,显然不可能再获得该资源,因此就自缚手脚了。

spinlock特性:

防止多处理器并发访问临界区,

1、非睡眠(该进程/LWP(Light Weight Process)始终处于Running的状态)

2、忙等 (cpu一直检测锁是否已经被其他cpu释放)

3、短期(低开销)加锁

4、适合中断上下文锁定

5、多cpu的机器才有意义(需要等待其他cpu释放锁)

信号量与互斥量

Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。

信号量的睡眠特性,使得信号量适用于锁会被长时间持有的情况;只能在进程上下文中使用,因为中断上下文中是不能被调度的;另外当代码持有信号量时,不可以再持有自旋锁。

信号量基本使用形式为:

static DECLARE_MUTEX(mr_sem); //声明互斥信号量

if(down_interruptible(&mr_sem))

//可被中断的睡眠,当信号来到,睡眠的任务被唤醒

//临界区

up(&mr_sem);

struct semaphore数据类型,down(struct semaphore * sem)和up(struct semaphore * sem)是占用和释放

struct mutex数据类型,mutex_lock(struct mutex *lock)和mutex_unlock(struct mutex *lock)是加锁和解锁

竞争信号量与互斥量时需要进行进程睡眠和唤醒,代价较高,所以不适于短期代码保护,适用于保护较长的临界区

互斥量与信号量的区别

1. 互斥量用于线程的互斥,信号量用于线程的同步

这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别

互斥:

是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的

同步:

是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源

2. 互斥量值只能为0/1,信号量值可以为非负整数

也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问

3. 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到

特性:

1、睡眠 (系统会将CPU切换给其他的进程/LWP运行。)

2、必须进程上下文(可调度)

3、长期加锁

信号量和自旋锁区别

虽然听起来两者之间的使用条件复杂,其实在实际使用中信号量和自旋锁并不易混淆。注意以下原则:

如果代码需要睡眠——这往往是发生在和用户空间同步时——使用信号量是唯一的选择。由于不受睡眠的限制,使用信号量通常来说更加简单一些。

如果需要在自旋锁和信号量中作选择,应该取决于锁被持有的时间长短。理想情况是所有的锁都应该尽可能短的被持有,但是如果锁的持有时间较长的话,使用信号量是更好的选择。

另外,信号量不同于自旋锁,它不会关闭内核抢占,所以持有信号量的代码可以被抢占。这意味者信号量不会对影响调度反应时间带来负面影响。

自旋锁和信号量的选择

需求 建议的加锁方法
低开销加锁 优先使用自旋锁
短期锁定 优先使用自旋锁
长期加锁 优先使用信号量
中断上下文中加锁 使用自旋锁
持有锁是需要睡眠、调度 使用信号量

以上有部分内容转自 http://www.linuxidc.com/Linux/2011-03/33741.htm

以下内容转自 http://blog.sina.com.cn/s/blog_0001988f0101f42l.html

spinlock mutex语义上是一样的,都是对一临界区加锁保护,

区别是mutex得不到锁会睡眠,因此不能在中断上下文中使用。

另外,解锁的一定是上锁的那个 semaphore 得不到锁会睡眠,也不能用在中断中, 上锁的不一定负责解锁 。

rwlock 很好理解了,可多个读,只有一个写者,同样会引起睡眠

最重要的就是只有spinlock 可以用在中断上下文中.

至于wait_queue,不是同步手段,是内核管理sleeping进程的一种手段

什么是等待队列?

在软件开发中任务经常由于某种条件没有得到满足而不得不进入睡眠状态,然后等待条件得到满足的时候再继续运行,进入运行状态。这种需求需要等待队列机制的支持。

而且semaphore(我不知道所有的实现是不是这样)中也用到了wait_queue_head


spinlock和信号量sem的区别

重点:死循环/睡眠

spinlock只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁。sem则会导致调用睡眠。然后应用上就是前者可以在中断处理中使用,后者不行。

信号量是针对使用时间比较长的共享资源,而自旋锁的则一般时间较短.一般的申请锁被其它保存则循环不止的等待.

  • Spinlock不断的检查等待的对象是否就绪,该进程/LWP始终处于Running的状态

  • Sem使该进程/LWP进入Wait转台,系统会将CPU切换给其他的进程/LWP运行。

由于这个根本特性的不同,导致了以下用法上的不同:

1. Spinlock 只适用于短暂的等待,因为没有进程切换所以对于短暂等待他的效率会比较高。但是对于长时间等待,由于它的CPU占用是100% 等的越长越不合算。

2. Spinlock只能在多个CPU的系统上用,因为等待者占据了100%的CPU,只由另外一个CPU上的进程才能解锁。

两者都是用于Linux内核互斥。避免并发,防止竞争,对系统公共资源或者共有数据进行合理保护的。SpinLock的出现是因为Symmetric Multi-Processor的出现,如果是UniProcessor,用简单的DisableIRQ就可以满足其要求。Spin Lock是通过Poll方式的,其可以说是一个Test and Set or Test andClear的模型的延伸。而Semaphore则是传统的IPC,是通过Sleep and Wake up方式实现的。通过SpinLock and Semaphore两者的实现机制则,我们可以_很明晰的_看出两者的应用场合。

(在高并发的地方,轮询比睡眠更高效)

Semaphore是一件可以容纳N人的房间,如果人不满就可以进去,如果人满了,就要等待有人出来。对于N=1的情况,称为binarysemaphore。一般的用法是,用于限制对于某一资源的同时访问。

Binarysemaphore与Mutex的差异:

在有的系统中Binarysemaphore与Mutex是没有差异的。在有的系统上,主要的差异是mutex一定要由获得锁的进程来释放。而semaphore可以由其它进程释放(这时的semaphore实际就是个原子的变量,大家可以加或减),因此semaphore可以用于进程间同步。Semaphore的同步功能是所有系统都支持的,而Mutex能否由其他进程释放则未定,因此建议mutex只用于保护criticalsection。而semaphore则用于保护某变量,或者同步。

另一个概念是spin lock,这是一个内核态概念。spinlock与semaphore的主要区别是spin lock是busywaiting,而semaphore是sleep。对于可以sleep的进程来说,busywaiting当然没有意义。对于单CPU的系统,busywaiting当然更没意义(没有CPU可以释放锁)。因此,只有多CPU的内核态非进程空间,才会用到spin lock。Linuxkernel的spinlock在非SMP的情况下,只是关irq,没有别的操作,用于确保该段程序的运行不会被打断。其实也就是类似mutex的作用,串行化对critical
section的访问。但是mutex不能保护中断的打断,也不能在中断处理程序中被调用。而spinlock也一般没有必要用于可以sleep的进程空间。

spinlock是多CPU下的同步机制,在获取锁时,如果失败,它不会挂起当前的执行过程。与之相对的,mutex和semaphore等同步机制,如果获取mutex或semaphore失败,它会挂起当前的执行过程,而在mutex或semaphore退出是,唤醒相应的过程。

不同的同步机制,是为了解决不同的问题。在单cpu上,不可能有spinlock,因为当前只能有一个活动的执行路径。而mutex或者semaphore则可以挂起当前的线程或者进程,CPU这时可以做其他的事情,等到挂在mutex或者semaphore上的进程被唤醒时,再继续执行被挂起的路径。

可以想象,spinlock的设计并不是不能支持挂起当前执行过程的操作。只是,在内核中,挂起当前的执行过程,就必须先能够标识这个过程。内核线程,或者进程,都有相应的结构;但是,哪些不属于这两种的执行过程,就必须使用spinlock。还有,如果一组进程在执行过程中,要求必须是同步执行,不能被打断,这种情况下spinlock也是必须的。

说了这么多,主要的意思就是说,不能的同步机制适用于不同的场景,解决不同的问题。没有一种同步机制可以解决所有的问题,这也是为什么linux里面不断有新的同步机制被引入内核。当新的问题出现时,现有的机制不能解决,或者不能很好解决这个问题时,就有必要引入新的机制来解决这个问题。

锁是一种协议,是有共享临界区的执行过程之间达成的协议。有了api,并不能保证程序的正确,还需要正确的,合理的使用api。尤其是在多cpu情况下,锁的使用,对性能有很大的影响。最好的办法就是不使用锁,每个CPU都是完全独立的运行。这也要看具体的应用,如果数据之间没有关联,当然可以独立地去处理;反之,则必须要锁来保护。

比如用multicore实现网络包的转发时,当然可以把某个流绑定到某个CPU上,假设流与流直接是完全独立的,这种情况下,每个CPU都可以独立的处理属于自己的流,不需要和其他CPU共享数据。但是,由于不同的流,流量是不同的,这样,CPU的能力没有被充分的利用,资源的使用也不平衡。这样的设计并不能很好的解决问题。

选择哪种设计方案,要综合的去考虑,没有最完美的解决方案,因为很多需求是相互冲突的,一个折衷的方案就是最后的选择。

Flag Counter

digoal’s 大量PostgreSQL文章入口