自旋锁

1. 自旋锁的的使用

自旋锁(Spin Lock)是一种典型的对临界资源进行互斥访问的手段,其名称来源于它的工作方式。为了获得一个自旋锁,在某个CPU上运行的代码需要先执行一个原子操作, 该操作测试并设置(Test-And-Set)某个内存变量。 由于它是原子操作, 所以再该操作完成之前其他执行单位不可能访问这个内存变量。 如果测试结果表明锁已经空闲, 则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍然被占用,程序将在一个小的循环内重复这个“测试并设置”操作,即进行所谓的“自旋”,通俗地说就是“在原地打转”,如图所示。当自旋锁的持有者通过重置该变量释放这个自旋锁后,某个等待的“测试并设置”操作向其调用的报告锁已经释放。

理解自旋锁最简单的方法是把它作为一个变量看待, 该变量把一个临界标记为“我当前在运行,请稍等一会”或者标记为“我当前不在运行,可以被使用”。如果A执行单元首先进入例程,它将持有自旋锁;当B执行单元试图进入同一个例程时,将获知自旋锁已经被持有,需要等待A执行单元释放后才能进入。

在ARM体系架构下,自旋锁的实现借用了Idres指令、strex指令、ARM处理器内存屏蔽指令dmb和dsb、wfe指令和sev指令,这类似于代码清单的逻辑。可以说既要保证排他性,也要处理好内存屏蔽。

Linux中于自旋锁相关的操作主要有以下4中。

1.定义自旋锁

spinlock_t lock;

2.初始化自旋锁

spinlock_lock_init(lock)    //用于动态初始化自旋锁lock。

3.获得自旋锁

spin_lock(lock)    //用于获得自旋锁lock,如立即获得锁,马上返回,否则,将在那里自旋,直到该锁的保持者释放。

spin_trylock(lock)    //尝试获得自旋锁lock,如立即获得锁,获得并返回true,否则立即返回false,实际上不再“原地打转”

4.释放自旋锁

spin_unlock(lock)    //释放自旋锁lock,和spin_trylock或spin_lock配对使用。

自旋锁一般这样被使用:

/*定义一个自旋锁*/
spinlock_t lock;
spin_lock_init(&lock);

spin_lock(&lock);
.../*临界区*/
spin_unlock(&lock);

自旋锁主要针对SMP或单CPU但是内核可以抢占的情况,对于单CPU和内核不支持抢占的系统,自旋锁退化为空操作。在单CPU和内核可以抢占的系统中,自旋锁持有期间中内核的抢占将被禁止。由于内核可以抢占的单CPU系统的行为实际上很类似于SMP系统,因此,在这样的单CPU系统中使用自旋锁仍然十分必要。另外,在多核SMP的情况下,任何一个内核拿到了自旋锁,该核的抢占调度也暂时禁止了,但是没有禁止另外一个核的抢占调度。

尽管用了自旋锁可以保证临界区不受别的CPU和本CPU内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候,还是可能受到中断和底半部(BH)的影响。为了防止这种影响,就需要用到自旋转的衍生。spin_lock()/spin_unlock()是自旋锁机制的基础,它们和关中断local_irq_disable()/开中断lock_irq_enable()、关底半部local_bh_disable()/开底半部local_bh_enable()、关中断并保存状态字local_irq_save()/开中断并恢复状态字local_irq_restore()结合就形成了整套自旋锁机制,如下:

spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()

spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()

spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() +local_bh_enable()

spin_lock_irq()、spin_lock_irqsave()、spin_lock_bh()类似函数会为自旋锁的使用系好“安全带”,以避免突如其来的中断驶入对系统造成的伤害。

在多核编程的时候, 如果进程和中断可能访问同一片临界资源,我们一般需要在进程上下文中调用spin_lock_irqsave()/spin_unlock_irqrestore(),在中断上下文中调用spin_lock()/spin_unlock(),这样,在CPU0上,无论是进程上下文,还是中断上下文获得了自旋锁,此后,如果CPU无论是进程上下文,还是中断上下文,想获得同一自旋锁,都必须忙等待,这避免一切核间并发的可能性。同时,由于每个核的进程上下文持有锁的时候用的是spin_lock_irqsave(),所以该核上的中断是不可能进入的,这避免了核内并发的可能性。

驱动工程师应该谨慎使用自旋锁,而且在使用中还要特别注意如下几个问题。

1)自旋锁实际上是忙等锁,当锁不可用时,CPU一直循环执行“测试并设置”该锁直到可用而取得该锁,CPU在等待自旋锁时不做任何有用的工作,仅仅是等待。因此,只有在占用锁的时间极短的情况下,使用自旋锁才是合理的。当临界区很大,或者共享设备的时候,需要较长时间占用所,使用自旋锁会降低系统的性能。

2)自旋锁可能导致系统死锁。引发这个问题最常见的情况是递归使用一个自旋锁,即如果一个已经拥有某个自旋锁的PU想第二次获取这个自旋锁,则该CPU将死锁。

3)在自旋锁锁定期间不能调用可能引起进程调度的函数。如果进程获取自旋锁之后再阻塞,如调用copy_from_user()、copy_to_user()、kmalloc()和msleep()等函数,则可能导致内核的崩溃。

4)在单核情况下编程的时候,也应该认为自己的CPU是多核的,驱动特别强调跨平台的概念。比如,在单CPU的情况下,若中断和进程可能访问同一临界区,进程里调用spin_lock_irqsave()是安全的,在中断里其实不调用spin_lock()也没有问题,因为spin_lock_irqsave()可以保证这个CPU的中断服务程序不可能执行。但是,若CPU变成多核,spin_lock_irqsave()不能屏蔽另外一个核的中断,所以另外一个核就可能造成并发问题。因此,无论如何,我们在中断服务程序里也应该调用spin_lock().

如下代码给出了自旋锁的使用例子,它被用于实现使得设备只能被最多1个进程打开。

/*使用自旋锁使设备只能被一个进程打开*/
int xxx_count = 0; //定义文件打开次数计数

static int xxx_open(struct inode *inode, struct file *filp)
{
    ...
    spinlock(&xxx_lock);
    if(xxx_count){    //已经打开
        spin_unlock(&xxx_lock);
        return -EBUSY;
    }
    xxx_count++;    //增加使用计数
    spin_unlock(&xxx_lock);
    ...
    return 0;    //成功
}

static int xxx_release(struct inode *inode, struct file *filp)
{
    ...
    spinlock(&xxx_lock);
    xxx_count--;    //减少使用计数
    spin_unlock(&xxx_lock);

    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值