1. 内核中竞态的产生原因
多个进程同时访问同一个设备驱动文件(临界资源),竞态就产生了。具体来说有以下几种情况:
-
单核 CPU 情况下,如果内核支持抢占(目前的内核一般都支持),就会产生竞态(优先级高的进程抢优先级低的进程的资源);
-
多核 CPU 情况下,多核之间本身就是并行执行,有可能会有不同的核心的进程同时访问同一个临界资源;
-
中断和进程之间也会产生竞态。
2. 自旋锁
2.1 原理
当一个进程获取到自旋锁后,其他进程在之前的进程没有解锁之前,都无法获取锁,也就无法操作临界资源。此时无法获取锁,但想要获取锁的进程就会进入“自旋”状态。
所谓“自旋”状态,因为可以叫“忙等”状态,即:获取不到锁,就不断请求获取锁,直到获取到锁为止,因此自旋状态的进程是需要消耗 CPU 资源的。
2.2 注意点
-
自旋锁是针对多核 CPU 设计的;
-
自旋状态会消耗 CPU 资源;
-
在同一进程中,多次获取同一把还没解锁的自旋锁,会导致死锁。原因是:获取不到锁,会不断请求获取锁,而此时未解锁的锁已经没有了解锁的机会;
-
自旋锁保护的临界区要尽可能小。原因是:自旋锁保护区间内,其他进程想要获取锁,会处于忙等状态,不断消耗 CPU 资源,因此要尽量压缩这个区间。所以在自旋锁保护的区间内,不要使用延时甚至休眠的操作;
-
在自旋锁保护的临界区间内,不能使用 copy_to_user 以及 copy_from_user 函数。原因是:这两个函数被设计了一种机制:在执行这两个函数的时候,有可能会切换到其他进程进行执行任务(防止拷贝数据时间太长影响其他进程),而这个机制就与我们保护竞态资源的初衷冲突了;
-
可以在中断函数中使用自旋锁,这也是自旋锁的主要使用场景;
-
自旋锁在上锁之前会关闭抢占。
2.3 自旋锁的函数接口
/* 定义一个自旋锁 */
spinlock_t lock;
/* 初始化自旋锁 */
spin_lock_init(spinlock_t *lock);
/* 上锁 */
spin_lock(spinlock_t *lock); // 支持被中断打断
spin_lock_irq(lock); // 不支持被中断打断
spin_lock_irqsave(lock, flags); // 上锁前保存中断的状态
/* 解锁 */
spin_unlock(spinlock_t *lock)
2.4 实例(2种典型错误思路 + 正确思路)
2.4.1 第一类错误思路
/* ## 此思路是错误示例 ## */
...........
spinlock_t lock; //定义自旋锁
/* 打开设备文件函数 */
int ledDev_open(struct inode* inode, struct file* file)
{
spin_lock(&lock); // 上锁
...........
}
/* 读 写等函数*/
............
/* 关闭设备文件函数 */
int ledDev_close(struct inode* inode, struct file* file)
{
.........
spin_unlock(&lock); // 解锁
return 0;
}
错误分析:
-
该思路可以概括为:在打开设备文件的函数中上锁,然后在关闭设备文件的函数中再进行解锁;
-
由此可见,如果这样使用自旋锁的话,保护区间太大了,效率非常低下;
-
另外,这样就会把 copy_to_user 以及 copy_from_user 函数囊括进自旋锁的保护区间,上文提到这样做是不对的。
2.4.2 第二类错误思路
/* ## 此思路是错误示例 ## */
...........
spinlock_t lock; //定义自旋锁
/* 打开设备文件函数 */
int ledDev_open(struct inode* inode, struct file* file)
{
spin_lock(&lock); // 上锁
...........
spin_unlock(&lock); // 解锁
}
/* 读 写等函数*/
............
/* 关闭设备文件函数 */
int ledDev_close(struct inode* inode, struct file* file)
{
spin_lock(&lock); // 上锁
.........
spin_unlock(&lock); // 解锁
return 0;
}
错误分析:
-
该思路概括而言,就是在打开设备文件的函数以及关闭设备文件的函数中分别单独使用上锁和解锁;
-
该思路相比较上一种思路,把自旋锁保护的区间缩小了,也避免了把 copy_to_user 以及 copy_from_user 函数囊括进自旋锁的保护区间;
-
但是该思路犯了更低级的错误:在打开设备文件函数中进行解锁后,其他进程就开始可以获取到锁了,此时其他进程就也可以进入设备文件,对临界资源进行操作了,这与我们保护临界资源被当前进程独享的初衷是违背的。
2.4.3 正确思路
因此重新梳理思路,理清我们需要的目标,我们需要的是实现如下效果:
-
保证当前进程在使用该设备文件(打开,关闭,读,写等操作)的全程,其他设备无法干涉;
-
自旋锁保护区间尽可能小,而且不能包含 copy_to_user 以及 copy_from_user 函数;
-
最好在当前进程使用设备驱动时,若其他进程尝试打开设备驱动,接收到返回值:驱动设备忙。
以上效果可以通过自旋锁配合上标志位进行实现:
...........
spinlock_t lock; //定义自旋锁
int flag = 0; // 标志位
/* 打开设备文件函数 */
int ledDev_open(struct inode* inode, struct file* file)
{
spin_lock(&lock); // 上锁
/* 其他进程虽然可以拿到锁 但是由于标志位的原因 会直接进入判断语句并返回 相当于被阻断 */
if (flag != 0) // 其他进程可以拿到锁 但是由于此时的标志位已经置 1
{
spin_unlock(&lock); // 其他进程进入判断语句 直接解锁
return -EBUSY; // 直接返回 错误码为设备忙
}
flag = 1; // 设置标志位 阻断其他进程进入
...........
spin_unlock(&lock); // 解锁 此时其他进程可以获取到锁
}
/* 读 写等函数*/
............
/* 关闭设备文件函数 */
int ledDev_close(struct inode* inode, struct file* file)
{
spin_lock(&lock); // 上锁
flag = 0; // 恢复标志位
spin_unlock(&lock); // 解锁
return 0;
}
使用以上方法,巧妙实现了我们需求的效果:既保证了自旋锁保护区域相对比较小,也没有把 copy_to_user 以及 copy_from_user 函数包括进去,同时配合上标志位,让每一个即使获取到锁的进程也会直接退出,相当于一个 “劝退” 的效果。综合来说,相当于用最好的方式保证了每次只能有一个进程在使用设备驱动文件。