在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 函数,否则的话可能导致死锁。
补充:
临界资源:系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源或共享变量。
临界区:指的是一个访问临界资源的程序片段。
还有其他类型的锁,如顺序锁,待学习。