锁相关知识 & mutex怎么实现的 & spinlock怎么用的 & 怎样避免死锁 & 内核同步机制 & 读写锁

时间:2023-11-10 19:50:50

spinlock在上一篇文章有提到:http://www.cnblogs.com/charlesblc/p/6254437.html  通过锁数据总线来实现。

而看了这篇文章说明:mutex内部也用到了spinlock http://blog.chinaunix.net/uid-21918657-id-2683763.html

获取互斥锁。

实际上是先给count做自减操作,然后使用本身的自旋锁进入临界区操作。首先取得count的值,在将count置为-1,判断如果原来count的置为1,也即互斥锁可以获得,则直接获取,跳出。否则进入循环反复测试互斥锁的状态。在循环中,也是先取得互斥锁原来的状态,在将其之为-1,判断如果可以获取(等于1),则退出循环,否则设置当前进程的状态为不可中断状态,解锁自身的自旋锁,进入睡眠状态,待被在调度唤醒时,再获得自身的自旋锁,进入新一次的查询其自身状态(该互斥锁的状态)的循环。

那么,SpinLock是不是用的CAS操作呢。看起来是的。

http://blog.csdn.net/goondrift/article/details/19044361

为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

unlock中的释放锁操作同样只用一条指令实现,以保证它的原子性。
也许还有读者好奇,“挂起等待”和“唤醒等待线程”的操作如何实现?每个Mutex有一个等待队列,一个线程要在Mutex上挂起等待,首先把自己加入等待队列中,然后置线程状态为睡眠,接着(系统)调用调度器函数切换到别的线程。一个线程要唤醒等待队列中的其它线程,只需从等待队列中取出一项,把它的状态从睡眠改为就绪,加入就绪队列,那么下次调度器函数执行时就有可能切换到被唤醒的线程。

一般情况下,如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了,这叫做死锁(Deadlock)。另一种典型的死锁情形是这样:线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起自己等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起自己等待线程A释放锁1,于是线程A和B都永远处于挂起状态了。不难想象,如果涉及到更多的线程和更多的锁,有没有可能死锁的问题将会变得复杂和难以判断。

避免死锁的方法:

1. 不要递归调用锁(有些锁没有允许递归调用的选项)

2. 不同线程加锁的顺序要一致,都要A,B,C

3. 尽量使用pthread_mutex_trylock调用代替pthread_mutex_lock调用

mutex和spinlock各自的问题:

mutex的问题是,它一旦上锁失败就会进入sleep,让其他thread运行,这 就需要内核将thread切换到sleep状态,如果mutex又在很短的时间内被释放掉了,那么又需要将此thread再次唤醒,这需要消耗许多CPU 指令和时间,这种消耗还不如让thread去轮讯。也就是说,其他thread解锁时间很短的话会导致CPU的资源浪费。

spinlock的问题是,和上面正好相反,如果其他thread解锁的时间很长的话,这种spinlock进行轮讯的方式将会浪费很多CPU资源。

解决方案:

对于single-core/single-CPU,spinlock将一直浪费 CPU资源,如果采用mutex,反而可以立刻让其他的thread运行,可能去释放mutex lock。

对于multi-core/mutil-CPU,会存在很多短时间被占用的lock,如果总是去让thread sleep,紧接着去wake up,这样会浪费很多CPU资源,从而降低了系统性能,所以应该尽量使用spinlock。

大多数现代操作系统已经使用了混合 mutex(hybrid mutex)和混合spinlock(hybrid spinlock)。说白了就是将两者的特点相结合。

hydrid mutex:在一个multi-core系统上,hybrid mutex首先像一个spinlock一样,当thread加锁失败的时候不会立即被设置成sleep,但是,当过了一定的时间(或则其他的策略)还没有 获得lock,就会被设置成sleep,之后再被wake up。而在一个single-core系统上,hybrid mutex就不会表现出spinlock的特性,而是如果加锁失败就直接被设置成sleep。

hybrid spinlock:和hybrid mutex相似,只不过,thread加锁失败后在spinlock一段很短的时间后,会被stop而不是被设置成sleep,stop是正常的进程调 度,应该会比先让thread sleep然后再wake up的开销小一些。

总结:

写程序的时候,如果对mutex和spinlock有任何疑惑,请选择使用mutex。

http://blog.chinaunix.net/uid-24227137-id-3563249.html

和spinlock 相关的文件主要有两个,一个是include/linux/spinlock.h,主要是提供关于和硬件无关的spinlock的几个对外主函数,一个是 include/asm-XXX/spinlock.h,用来提供和硬件相关的功能函数。另外,在2.6的内核中,又多了一个文件, include/linux/preempt.h,为新增加的抢占式多任务功能提供一些服务。

spinlock 函数的使用前提:首先,spinklock函数只能使用在内核中,或者说只能使用在内核状态下,在2.6以前的内核是不可抢占的,也就是说,当运行于内核状态下时,是不容许切换到其他进程的。而在2.6以后的内核中,编译内核的时候多了一个选项,可以配置内核是否可以被抢占,这也就是为什么在2.6的内核中多了一个preempt.h的原因。

需要澄清的是,互斥手段的选择,不是根据临界区的大小,而是根据临界区的性质,以及 
有哪些部分的代码,即哪些内核执行路径来争夺。

从严格意义上说,semaphore和spinlock_XXX属于不同层次的互斥手段,前者的 
实现有赖于后者,这有点象HTTP和TCP的关系,都是协议,但层次是不同的。

先说semaphore,它是进程级的,用于多个进程之间对资源的互斥,虽然也是在 
内核中,但是该内核执行路径是以进程的身份,代表进程来争夺资源的。如果 
竞争不上,会有context switch,进程可以去sleep,但CPU不会停,会接着运行 
其他的执行路径。从概念上说,这和单CPU或多CPU没有直接的关系,只是在 
semaphore本身的实现上,为了保证semaphore结构存取的原子性,在多CPU中需要 
spinlock来互斥。

在内核中,更多的是要保持内核各个执行路径之间的数据访问互斥,这是最基本的 
互斥问题,即保持数据修改的原子性。semaphore的实现,也要依赖这个。在单CPU 
中,主要是中断和bottom_half的问题,因此,开关中断就可以了。在多CPU中, 
又加上了其他CPU的干扰,因此需要spinlock来帮助。这两个部分结合起来, 
就形成了spinlock_XXX。它的特点是,一旦CPU进入了spinlock_XXX,它就不会 
干别的,而是一直空转,直到锁定成功为止。因此,这就决定了被 
spinlock_XXX锁住的临界区不能停,更不能context switch,要存取完数据后赶快 
出来,以便其他的在空转的执行路径能够获得spinlock。这也是spinlock的原则 
所在。如果当前执行路径一定要进行context switch,那就要在schedule()之前 
释放spinlock,否则,容易死锁。因为在中断和bh中,没有context,无法进行 
context switch,只能空转等待spinlock,你context switch走了,谁知道猴年 
马月才能回来。

那么,在什么情况下具体用哪个呢?这要看是在什么内核执行路径中,以及要与哪些内核 
执行路径相互斥。我们知道,内核中的执行路径主要有:

1  用户进程的内核态,此时有进程context,主要是代表进程在执行系统调用

    等。

 2  中断或者异常或者自陷等,从概念上说,此时没有进程context,不能进行

    context switch。

 3  bottom_half,从概念上说,此时也没有进程context。

 4  同时,相同的执行路径还可能在其他的CPU上运行。 

如果只要和其他CPU 
互斥,就要用spin_lock/spin_unlock,如果要和irq及其他CPU互斥,就要用 
spin_lock_irq/spin_unlock_irq。。。

自旋锁的使用

http://www.linuxidc.com/Linux/2012-02/54313.htm

        int  OpenCloseStatus;

        spinlock_t     spinlock;

        int    xxxx_init(void)

        {

              ............

              spin_lock_init(&spinlock);

             ............

          }

         int  xxxx_open(struct  inode *inode, struct file *filp)

         {

                ............

                spin_lock(&spinlock);

                if(OpenCloseStatus)

                {

                         spin_unlock(&spinlock);

                        return -EBUSY;

                }

                OpenCloseStatus ++;

                 spin_unlock(&spinlock);

                 ...........

         }

         int  xxxx_release(struct  inode *inode, struct file *filp)

         {

                ............

                spin_lock(&spinlock);

                OpenCloseStatus --;

                 spin_unlock(&spinlock);

                 ...........

         }

5:自旋锁使用注意事项

       1):自旋锁一种忙等待,当条件不满足时,会一直不断的循环判断条件是否满足,如果满足就解锁,运行之后的代码。因此会对linux的系统的性能有些影响。所以在实际编程时,需要注意自旋锁不应该长时间的持有。它适合于短时间的的轻量级的加锁机制。

        2):自旋锁不能递归使用,这是因为自旋锁,在设计之初就被设计成在不同进程或者函数之间同步。所以不能用于递归使用。    

还不错,讲的比较具体

http://www.wowotech.net/kernel_synchronization/spinlock.html

对于spin lock,其保护的资源可能来自多个CPU CORE上的进程上下文和中断上下文的中的访问,其中,进程上下文包括:用户进程通过系统调用访问,内核线程直接访问,来自workqueue中work function的访问(本质上也是内核线程)。中断上下文包括:HW interrupt context(中断handler)、软中断上下文(soft irq,当然由于各种原因,该softirq被推迟到softirqd的内核线程中执行的时候就不属于这个场景了,属于进程上下文那个分类了)、timer的callback函数(本质上也是softirq)、tasklet(本质上也是softirq)。

先看最简单的单CPU上的进程上下文的访问。如果一个全局的资源被多个进程上下文访问,这时候,内核如何交错执行呢?对于那些没有打开preemptive选项(Linux2.6石达开的,也就是可抢占的)的内核,所有的系统调用都是串行化执行的,因此不存在资源争抢的问题。如果内核线程也访问这个全局资源呢?本质上内核线程也是进程,类似普通进程,只不过普通进程时而在用户态运行、时而通过系统调用陷入内核执行,而内核线程永远都是在内核态运行,但是,结果是一样的,对于non-preemptive的linux kernel,只要在内核态,就不会发生进程调度,因此,这种场景下,共享数据根本不需要保护(没有并发,谈何保护呢)。如果时间停留在这里该多么好,单纯而美好,在继续前进之前,让我们先享受这一刻。

(注:所以说SpinLock主要保护的是系统调用,而mutex更多是应用级别)

当打开premptive选项后,事情变得复杂了,我们考虑下面的场景:

(1)进程A在某个系统调用过程中访问了共享资源R

(2)进程B在某个系统调用过程中也访问了共享资源R

OK,我们加上spin lock看看如何:A在进入临界区之前获取了spin lock,同样的,在A访问共享资源R的过程中发生了中断,中断唤醒了沉睡中的,优先级更高的B,B在访问临界区之前仍然会试图获取spin lock,这时候由于A进程持有spin lock而导致B进程进入了永久的spin……怎么破?

linux的kernel很简单,在A进程获取spin lock的时候,禁止本CPU上的抢占(上面的永久spin的场合仅仅在本CPU的进程抢占本CPU的当前进程这样的场景中发生)。(不同的CPU的话,反正总会执行回来的。除非另一个锁锁住了,那就是多个锁的死锁了,另外的问题了)。

如果A和B运行在不同的CPU上,那么情况会简单一些:A进程虽然持有spin lock而导致B进程进入spin状态,不过由于运行在不同的CPU上,A进程会持续执行并会很快释放spin lock,解除B进程的spin状态。

多CPU core的场景和单核CPU打开preemptive选项的效果是一样的,这里不再赘述。

我们继续向前分析,现在要加入中断上下文这个因素。

访问共享资源的thread包括:

(1)运行在CPU0上的进程A在某个系统调用过程中访问了共享资源R

(2)运行在CPU1上的进程B在某个系统调用过程中也访问了共享资源R

(3)外设P的中断handler中也会访问共享资源R

在这样的场景下,使用spin lock可以保护访问共享资源R的临界区吗?

我们假设CPU0上的进程A持有spin lock进入临界区,这时候,外设P发生了中断事件,并且调度到了CPU1上执行,看起来没有什么问题,执行在CPU1上的handler会稍微等待一会CPU0上的进程A,等它立刻临界区就会释放spin lock的,但是,如果外设P的中断事件被调度到了CPU0上执行会怎么样?CPU0上的进程A在持有spin lock的状态下被中断上下文抢占,而抢占它的CPU0上的handler在进入临界区之前仍然会试图获取spin lock,悲剧发生了,CPU0上的P外设的中断handler永远的进入spin状态,这时候,CPU1上的进程B也不可避免在试图持有spin lock的时候失败而导致进入spin状态。

为了解决这样的问题,linux kernel采用了这样的办法:如果涉及到中断上下文的访问,spin lock需要和禁止本CPU上的中断联合使用。也就是不准handler里面调用SpinLock吧。

下面这句不太懂:

linux kernel中提供了丰富的bottom half的机制,虽然同属中断上下文,不过还是稍有不同。我们可以把上面的场景简单修改一下:外设P不是中断handler中访问共享资源R,而是在的bottom half中访问。使用spin lock+禁止本地中断当然是可以达到保护共享资源的效果,但是使用牛刀来杀鸡似乎有点小题大做,这时候disable bottom half就OK了。

最后,我们讨论一下中断上下文之间的竞争。同一种中断handler之间在uni core和multi core上都不会并行执行,这是linux kernel的特性。如果不同中断handler需要使用spin lock保护共享资源,对于新的内核(不区分fast handler和slow handler),所有handler都是关闭中断的,因此使用spin lock不需要关闭中断的配合。

bottom half又分成softirq和tasklet,同一种softirq会在不同的CPU上并发执行,因此如果某个驱动中的sofirq的handler中会访问某个全局变量,对该全局变量是需要使用spin lock保护的,不用配合disable CPU中断或者bottom half。tasklet更简单,因为同一种tasklet不会多个CPU上并发,具体我就不分析了,大家自行思考吧。

Linux内核同步机制之(五):Read/Write spin lock

http://www.wowotech.net/kernel_synchronization/rw-spinlock.html

我们首先看看加锁的逻辑:

(1)假设临界区内没有任何的thread,这时候任何read thread或者write thread可以进入,但是只能是其一。

(2)假设临界区内有一个read thread,这时候新来的read thread可以任意进入,但是write thread不可以进入

(3)假设临界区内有一个write thread,这时候任何的read thread或者write thread都不可以进入

(4)假设临界区内有一个或者多个read thread,write thread当然不可以进入临界区,但是该write thread也无法阻止后续read thread的进入,他要一直等到临界区一个read thread也没有的时候,才可以进入,多么可怜的write thread。

unlock的逻辑如下:

(1)在write thread离开临界区的时候,由于write thread是排他的,因此临界区有且只有一个write thread,这时候,如果write thread执行unlock操作,释放掉锁,那些处于spin的各个thread(read或者write)可以竞争上岗。

(2)在read thread离开临界区的时候,需要根据情况来决定是否让其他处于spin的write thread们参与竞争。如果临界区仍然有read thread,那么write thread还是需要spin(注意:这时候read thread可以进入临界区,听起来也是不公平的)直到所有的read thread释放锁(离开临界区),这时候write thread们可以参与到临界区的竞争中,如果获取到锁,那么该write thread可以进入。