背景由于多处理器环境下某些资源的限制,有时需要互斥,此时需要引入锁的概念,只有获得锁的任务可以访问该资源。由于多线程的核心是CPU的时间片,同一时刻只能有一个任务获取锁。当内核发生资源访问冲突时,通常有两种处理方式:一种是原地等待,另一种是挂起当前进程,调度其他进程执行(休眠)自旋锁。自旋锁是内核中提供的一种比较常见的锁。机制上,自旋锁是一种解决资源冲突的“原地等待”方式。即一个线程获取自旋锁后,另一个线程期望获取自旋锁,但是如果获取不到,就只能原地“自旋”(忙等待)。由于自旋锁的忙等待特性,注定了它在使用场景上的局限——自旋锁不应该被长时间持有(消耗CPU资源)。自旋锁的优点自旋锁不会切换线程状态,会一直处于用户态,即线程一直处于活动状态;不会导致线程进入阻塞状态,减少不必要的上下文切换,执行速度快。非自旋锁在无法获取到锁时会进入阻塞状态,从而进入内核态。当获取到锁后,需要从内核态中恢复,需要进行线程上下文切换。(线程阻塞后进入内核(Linux)调度状态,会导致系统在用户态和内核态之间来回切换,严重影响锁的性能)。linux内核实现中自旋锁的使用经常会遇到这样的场景:共享数据被中断上下文和进程上下文访问,如何保护?如果只有进程上下文访问,那么可以考虑使用信号量或互斥量的锁机制,但是现在中断上下文也涉及到,那些能导致睡眠的锁就不能用了。这时候可以考虑使用自旋锁。在中断上下文中,不允许sleep,所以这里需要的是一个不会造成sleep-spinlock的锁。换句话说,中断上下文使用锁,自旋锁是首选。使用自旋锁,有两种定义锁的方式:动态:spinlock_tlock;自旋锁初始化(&锁);静态:DEFINE_SPINLOCK(锁);使用步骤自旋锁的使用非常简单:我们需要先申请自旋锁才能访问关键资源;获取不到锁就自旋,获取到锁就进入临界区;当自旋锁被释放时,在这个锁上自旋的任务可以获得锁并进入临界区,而退出临界区的任务必须释放自旋锁。使用示例staticspinlock_tlock;staticintflage=1;spin_lock_init(&lock);staticinthello_open(structinode*inode,structfile*filep){spin_lock(&lock);if(flage!=1){spin_unlock(&lock);return-EBUSY;}flage=0;spin_unlock(&lock);return0;}staticinthello_release(structinode*inode,structfile*filep){flage=1;return0;}补充中断上下文不能sleep的原因有:1.中断处理过程中,不应发生进程切换,因为在中断上下文中,唯一能打断当前中断处理程序的是更高优先级的中断,它不会被进程打断,如果它在中断上下文中休眠,就没有办法唤醒它,因为所有wake_up_xxx是针对某个进程而言的,在中断上下文中,没有进程的概念,也没有task_struct(softirq和tasklet也是如此),所以它真的休眠了,比如调用一个会导致阻塞的例程,内核几乎肯定会死掉.2.schedule()在切换进程时保存当前进程上下文(CPU寄存器的值、进程的状态、栈中的内容),以便以后恢复进程。中断发生后,内核会先保存当前被中断的进程上下文(调用中断处理程序后恢复);但在中断处理程序中,CPU寄存器的值肯定发生了变化(最重要的程序计数器PC,堆栈SP等),如果此时由于休眠或阻塞操作调用schedule(),则保存的进程上下文为不是当前进程上下文。所以schedule()不能在中断处理程序中调用。3、内核中的schedule()函数进来的时候判断是否处于中断上下文中:if(unlikely(in_interrupt()))BUG();因此,强行调用schedule()的结果是一个内核BUG。4、中断处理程序会使用被中断的进程内核栈,但不会对其产生任何影响,因为处理程序用完后会彻底清除它使用的那部分栈,恢复被中断前的原貌.5.在中断上下文中,内核不能被抢占。所以如果你休眠,内核必须挂起。自旋锁的死锁自旋锁不是递归的,等待已经获得的锁会导致死锁。自旋锁可以用在中断上下文中,但是想象一个场景:一个线程获取锁,但是被一个中断处理程序打断,中断处理程序也获取了锁(但是之前已经加锁了,获取不到,只能自旋),中断无法退出,线程中后面释放锁的代码无法执行,导致死锁。(如果确认中断不会访问与线程相同的锁,则无所谓)。1、考虑如下场景(内核抢占场景):(1)进程A在系统调用中访问共享资源R(2)进程B在系统调用中也访问共享资源R什么原因导致冲突?假设A在访问共享资源R的过程中发生了中断,中断以更高的优先级唤醒了正在休眠的B。当中断返回现场时,发生进程切换,B开始执行,并通过系统调用访问R。如果没有锁保护,两个线程会进入临界区,导致程序执行错误。OK,让我们加一个自旋锁,看看它是如何工作的:A在进入临界区之前获得了自旋锁。同理,A在访问共享资源R的过程中产生中断,中断唤醒休眠的优先级更高的。B、B在访问临界区之前仍然会尝试获取自旋锁。这时,因为A进程持有自旋锁,B进程进入永久自旋……如何破解呢?Linux内核很简单,自旋锁是在A进程中获取的,禁止在本CPU上抢占(上面的永久自旋只发生在本CPU的进程抢占本CPU当前进程的场景)。如果A和B运行在不同的CPU上,那么情况会更简单:虽然A进程持有自旋锁,B进程进入自旋状态,但是因为运行在不同的CPU上,所以A进程会继续执行,会很快释放掉自旋锁并释放B进程的自旋状态。2、考虑如下场景(中断上下文场景):运行在CPU0上的进程A在一次系统调用中访问了共享资源R。在CPU1上运行的进程B在系统调用期间也访问共享资源。资源R外设P的中断处理程序也访问了共享资源R。在这样的场景下,使用自旋锁是否可以保护访问共享资源R的临界区?我们假设CPU0上的进程A持有自旋锁并进入临界区。此时外设P发生中断事件,调度到CPU1上执行。好像没什么问题。在CPU1上执行的handler会在CPU0上为进程A等待一段时间,临界区锁后会立即释放自旋,但是如果外设P的中断事件调度到CPU0上执行会怎样呢?CPU0上的进程A在持有自旋锁的同时被中断上下文抢占,抢占它的CPU0上的处理程序在进入临界区之前,仍然会尝试获取自旋锁。悲剧发生了。CPU0上P外设的中断处理程序会一直进入自旋状态。这时CPU1上的进程B在试图持有自旋锁时必然会失败。导致进入自旋状态。为了解决此类问题,linux内核采用了这样一种方法:如果涉及到中断上下文的访问,则需要结合使用自旋锁来禁止本CPU上的中断。3.考虑以下场景(bottomhalf场景)linux内核提供了丰富的bottomhalf机制。虽然属于中断上下文,但还是略有不同。我们可以简单修改一下上面的场景:外设P在中断处理程序中不访问共享资源R,而是在设备的下半部访问。使用自旋锁+禁止本地中断当然可以达到保护共享资源的效果,但是用大锤杀鸡好像有点大材小用了。这时候disablebottomhalf就够了。4、中断上下文之间的竞争同一个中断处理程序在单核和多核上不会并行执行,这是linux内核的特点。如果不同的中断处理程序需要使用自旋锁来保护共享资源,对于新内核(不区分快处理程序和慢处理程序),所有处理程序都禁用中断,因此使用自旋锁不需要关闭中断的配合。下半部分分为softirq和tasklet。同一个softirq会在不同的CPU上并发执行。因此,如果在某个驱动的softirq的handler中访问了某个全局变量,则需要用自旋锁来保护这个全局变量。无需配合disableCPUinterrupt或bottomhalf。Tasklet更简单,因为同一个tasklet不会同时运行在多个CPU上。自旋锁的实现原理数据结构中首先定义了一个spinlock_t数据类型,它本质上是一个整型值(对该值的操作需要保证原子性),该值表示自旋锁是否可用。它在初始化期间设置为1。当线程想要持有锁时,调用spin_lock函数。该函数将自旋锁的整数值减1,然后进行判断。如果等于0,表示可以获得自旋锁。如果为负数,说明其他线程持有锁,本线程需要自旋。spinlock_t在内核中的数据类型定义如下:通用(适用于各种arch)自旋锁使用类型名称spinlock_t,各种arches定义了它们自己的structraw_spinlock。听起来是个好主意和命名,直到linux实时树(PREEMPT_RT)挑战自旋锁。自旋锁的命名约定定义如下:rtlinux时自旋锁可能被抢占(配置PREEMPT_RT)(实际底层可能使用支持PI(优先级反转)的mutext)。raw_spinlock,即使配置了PREEMPT_RT,也一定是顽强自旋arch_spinlock。自旋锁与架构有关。ARM架构系统的arch_spin_lock接口实现了同样的加锁。这里只是一个典型的用于分析的API。其他的可以自己学。.我们选择arch_spin_lock,它的ARM32代码如下:staticinlinevoidarch_spin_lock(arch_spinlock_t*lock){unsignedlongtmp;u32newval;arch_spinlock_tlockval;prefetchw(&lock->slock);---------(0)__asm____volatile__("1:ldrex%0,[%3]\n"---------(1)"add%1,%0,%4\n"--------(2)"strex%2,%1,[%3]\n"--------(3)"teq%2,#0\n"-----------(4)"bne1b":"=&r"(lockval),"=&r"(newval),"=&r"(tmp):"r"(&lock->slock),"I"(1<
