设备驱动中的并发控制-自旋锁

        在linux中提供了一些锁机制来避免竞争,引入锁的机制是因为单独的原子操作不能满足复杂的内核设计需求。Linux中一般可以认为有两种锁,一种是自旋锁,另一种是信号量。这两种锁是为了解决内核中遇到的不同问题开发的。其实现机制和应用场合有所不同。

自旋锁是一种简单的并发控制机制,其是实现信号量和完成量的基础。自旋锁对资源有很好的保护作用。

        自旋锁的使用

在linux中,自旋锁的类型为struct spinlock_t。内核提供了一系列的函数对struct spinlock_t进行操作。

1. 定义和初始化自旋锁

        spinlock_t lock; /* 定义自旋锁 */

        自旋锁必须初始化才能被使用,对自旋锁的初始化可以在编译阶段通过宏来实现。初始化自旋锁可以使用宏SPIN_LOCK_UNLOCKED,这个宏表示一个没有锁定的自旋锁。

        spinlock_t lock = SPIN_LOCK_UNLOCKED; /* 初始化一个未使用的自旋锁 */

        在运行阶段,可以使用spin_lock_init()函数动态的初始化一个自旋锁:

        void spin_lock_init(spinlock_t lock);

2. 锁定自旋锁

        在进入临界区前,需要使用spin_lock宏来获得自旋锁。spin_lock宏定义如下:

        #define spin_lock(lock)   _spin_lock(lock)

        这个宏用来获得lock自旋锁,如果能够立即获得自旋锁,则宏立刻返回;否则,这个锁会一直自旋在那里,直到该锁被其他线程释放为止。

3. 释放自旋锁

        当不在使用临界区时,需要使用spin_unlock宏释放自旋锁。spin_unlock宏定义如下:

        #define spin_unlock(lock)   _spin_unlock(lock)

        这个宏用来释放lock自旋锁,当调用该宏之后,锁立刻被释放。

4. 使用自旋锁

        最基本的自旋锁 API 函数如下所示:

4.1 定义并初始化一个自旋锁变量。

      DEFINE_SPINLOCK(spinlock_t lock);
4.2 初始化自旋锁:

        int spin_lock_init(spinlock_t *lock);
4.3 获取指定的自旋锁,也叫做加锁。

        void spin_lock(spinlock_t *lock);

4.4释放指定的自旋锁。

        void spin_unlock(spinlock_t *lock);
4.5 尝试获取指定的自旋锁,如果没有获取到就返回 0

        int spin_trylock(spinlock_t *lock);
4.6 检查指定的自旋锁是否被获取,如果没有被获取就返回非 0,否则返回 0。

         int spin_is_locked(spinlock_t *lock);

        自旋锁API函数适用于SMP或支持抢占的单CPU下线程之间的并发访问,也就是用于线程与线程之间。

        使用自旋锁的方法:

        spinlock_t lock;        /* 定义自旋锁 */

        spin_lock_init(&lock);   /* 初始化自旋锁 */

        spin_lock(&lock);       /* 获得自旋锁 */

        /* 临界资源 */

        spin_unlock(&lock);     /* 释放自旋锁 */

        在驱动程序中,有些设备只允许打开一次,那么就需要一个自旋锁保护表示设备的打开或关闭状态的变量count。此处count属于临界资源,如果不对count进行保护,当设备频繁打开时,可能会出现错误得count计数。使用自旋锁包含count的代码如下:

int count=0;
spinlock_t lock;
int xxx_int(void)
{
    ...
    spin_lock_init(&lock);
    ...
}
/* 文件打开函数 */
int xxx_open(struct inode *inode, struct file *filp)
{
    ...
    spin_lock(&lock);
    if(count)
    {
        spin_unlock(&lock);
        return -RBUSY;
    }
    count++;
    spin_unlock(&lock);
    ...
}
/* 文件释放函数 */
int xxx_release(struct inode *inode, struct file *filp)
{
    ...  
    spin_lock(&lock);
    count--;
    spin_unlock(&lock);
    ...
}

5. 死锁

        两种死锁场景:

5.1 由睡眠引起的死锁

        被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API函数,否则的话会可能会导致死锁现象的发生。自旋锁会自动禁止抢占,也就说当线程A得到锁以后会暂时禁止内核抢占。如果线程 A 在持有锁期间进入了休眠状态,那么线程 A 会自动放弃 CPU 使用权。线程 B 开始运行,线程 B 也想要获取锁,但是此时锁被 A 线程持有,而且内核抢占还被禁止了。线程 B 无法被调度出去,那么线程 A 就无法运行,锁也就无法释放,死锁发生了。

5.2 由中断引起的死锁

        如果线程 A 时运行,中断也想访问共享资源,那该怎么办呢?首先可以肯定的是,中断里面可以使用自旋锁,但是在中断里面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断(也就是本 CPU 中断,对于多核 SOC来说会有多个 CPU 核),否则可能导致锁死现象的发生,如图下所示:

         在图中,线程 A 先运行,并且获取到了 lock 这个锁,当线程 A 运行 functionA 函数的时候中断发生了,中断抢走了 CPU 使用权。右边的中断服务函数也要获取 lock 这个锁,但是这个锁被线程 A 占有着,中断就会一直自旋,等待锁有效。但是在中断服务函数执行完之前,线程 A 是不可能执行的,场面就这么僵持着,死锁发生。

5.3 解决死锁问题

        最好的解决方法就是获取锁之前关闭本地中断,Linux 内核提供了相应的 API 函数,如下所示:

5.3.1 禁止本地中断,并获取自旋锁。

        void spin_lock_irq(spinlock_t *lock);

5.3.2 激活本地中断,并释放自旋锁。

        void spin_unlock_irq(spinlock_t *lock);

5.3.3 保存中断状态,禁止本地中断,并获取自旋锁。

        void spin_lock_irqsave(spinlock_t *lock, unsigned long flags)

5.3.4 将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。

        void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)

        使用 spin_lock_irq/spin_unlock_irq 的时候需要用户能够确定加锁之前的中断状态,但实际上内核很庞大,运行也是“千变万化”,我们是很难确定某个时刻的中断状态,因此不推荐使用

spin_lock_irq/spin_unlock_irq。建议使用 spin_lock_irqsave/spin_unlock_irqrestore,因为这一组函数会保存中断状态,在释放锁的时候会恢复中断状态。一般在线程中使用 spin_lock_irqsave/

spin_unlock_irqrestore,在中断中使用 spin_lock/spin_unlock,示例代码如下所示:

/* 线程 A */
void functionA (){
	unsigned long flags; /* 中断状态 */
	spin_lock_irqsave(&lock, flags) /* 获取锁 */
	/* 临界区 */
	spin_unlock_irqrestore(&lock, flags) /* 释放锁 */
}

/* 中断服务函数 */
void irq() {
	spin_lock(&lock) /* 获取锁 */
	/* 临界区 */
	spin_unlock(&lock) /* 释放锁 */
}

6. 自旋锁的使用注意事项

        在使用自旋锁时,需要注意以下几项:

6.1 自旋锁是一种忙等待。Linux中,自旋锁当条件不满足时,会一直不断地循环条件是否被满足。如果满足就解锁,继续运行下面的代码。这种忙等待机制对系统的性能是有影响的。所以应该注意使用自旋锁时,自旋锁不应该长时间的持有,它是一种适合短时间锁定的轻量级的加锁机制。

6.2 自旋锁不能递归使用。这是因为自旋锁被设计成在不同线程或函数之间同步。如果一个线程在已经持有自旋锁时,其处于忙等待状态,则已经没有机会释放自己持有的锁了。如果这时在调用自身,则自旋锁永远没有执行的机会了。所以类似下面的递归形式不能使用自旋锁:

void A()
{
    锁定自旋锁;
    A();
    锁定自旋锁;
}

6.3 自旋锁保护的临界区内不能调用任何可能导致线程休眠的 API 函数,否则的话可能导致死锁。

补充:

临界资源:系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源或共享变量。

临界区:指的是一个访问临界资源的程序片段。

还有其他类型的锁,如顺序锁,待学习。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值