内核中解决竞态的方法之 信号量、互斥体、原子操作
1. 信号量
1.1 原理
当一个进程获取到了信号量之后,信号量的值会减为 0 ,如果另外一个进程想获取信号量,会无法获取到值为 0 的信号量资源,然后进入休眠模式。
1.2 注意项
- 信号量对多核 CPU 也有效;
- 获取不到信号量资源的进程,会进入休眠状态,所以这种状态不消耗 CPU 资源;
- 信号量不会有死锁效应(本质也是因为获取不到的进程会休眠,而不是忙等);
- 信号量保护的临界区可以很大,所以信号量保护的临界资源区域中,可以有延时甚至休眠的操作(本质也是因为获取不到的进程会休眠,而不是忙等);
- 信号量不能用在中断中,但是在进程上下文中很好用;
- 信号量上锁的时候是不会关闭抢断的。
1.3 信号量的函数接口
/* 定义一个信号量 */
struct semaphore sem;
/* 初始化信号量 */
sema_init(struct semaphore *sem, int val); // val 通常设置为1 这样才有互斥效果
// 设置为 0 可以用作同步机制的设计 不常用
/* 上锁 */
down(struct semaphore *sem); // 上锁 也就是把信号量的值减 1
down_trylock(struct semaphore *sem); // 尝试获取信号量 成功返回 0 失败返回 1 这个函数非阻塞
/* 解锁 */
up(struct semaphore *sem);
1.4 实例
............
struct semaphore sem; // 定义一个信号量
/* 入口函数 */
static int __init ledDev_init(void)
{
............
sema_init(&sem,1); // 初始化信号量 值设定为1 实现互斥逻辑
return 0;
.............
}
/* 打开设备文件的函数 */
int ledDev_open(struct inode* inode, struct file* file)
{
// down(&sem); // 获取信号量
if((down_trylock(&sem)) == 1) // 其他进程尝试获取信号量 无法成功获取时直接返回设备忙的错误码
{
return -EBUSY;
}
.........
}
............
/* 关闭设备文件的函数 */
int ledDev_close(struct inode* inode, struct file* file)
{
........
up(&sem); // 恢复信号量值
return 0;
}
2. 互斥体
2.1 原理
当前进程获取到互斥体之后,别的进程也尝试获取互斥体时会失败,短暂等待后,进入休眠状态。
2.2 注意项
- 对 多核 CPU 也有效;
- 获取不到互斥体的时候,短暂等待之后才开始休眠;
- 互斥体不会产生死锁;
- 互斥体保护的临界区域很大,里面可以放延时甚至休眠操作;
- 互斥体无法工作在中断,但是很适合用在进程间的上下文切换;
- 互斥体上锁时不会关闭抢占;
- 由于进程在无法获取到互斥体时,会短暂等待,才开始休眠,因此在未知临界区大小的时候,优先使用互斥体。原因是:如果临界区非常小,在执行完临界区的全部操作后,另一个进程还未来得及进入休眠状态(还在短暂等待中),可以直接接管互斥体,省去了进入休眠然后停止休眠的过程,执行效率高。
2.3 互斥体的函数接口
/* 定义一个互斥体 */
struct mutex mutex;
/* 初始化互斥体 */
mutex_init(&mutex);
/* 上锁 */
mutex_lock(struct mutex *lock); // 上锁
mutex_trylock(struct mutex *lock); // 尝试上锁 成功返回 1 失败返回 0
/* 解锁 */
mutex_unlock(struct mutex *lock);
2.4 实例
............
struct mutex mutex; // 定义一个互斥体
............
/* 入口函数 */
static int __init ledDev_init(void)
{
............
mutex_init(&mutex); // 初始化互斥体
return 0;
.............
}
/* 打开设备文件的函数 */
int ledDev_open(struct inode* inode, struct file* file)
{
if((mutex_trylock(&mutex)) == 0) // 其他进程尝试上锁 无法成功上锁时直接返回设备忙的错误码
{
return -EBUSY;
}
.........
}
............
/* 关闭设备文件的函数 */
int ledDev_close(struct inode* inode, struct file* file)
{
........
mutex_unlock(&mutex); // 解锁
return 0;
}
3. 原子操作
3.1 原理
原子操作本质上是在操作原子变量。被设定为原子变量的数据,是无法通过常规的方式进行值的修改的,必须使用特定的函数接口,通过内联汇编实现值的修改。这个过程可以保证同时只有一个进程在执行,也就是不会产生数据竞争问题。可以借助一个原子变量作为标志位,实现对竞态的处理。
3.2 原子操作的函数接口
/* 方案一 */
atomic_t atm = ATOMIC_INIT(1); // 定义并初始化原子变量 值初始化为 1
atomic_dec_and_test(atomic_t *v); // 上锁操作 让原子变量的值减去 1 然后和 0 比较
// 如果结果为 0 表示修改成功(是本进程的操作) 返回真
// 如果结果不为 0 表示修改失败(是别的进程的操作) 返回假
atomic_inc(atomic_t *v); // 解锁
/* 方案二 */
atomic_t atm = ATOMIC_INIT(-1); // 定义并初始化原子变量 值初始化为 -1
atomic_inc_and_test(atomic_t *v); // 上锁操作 让原子变量的值加上 1 然后和 0 比较
// 如果结果为 0 表示修改成功(是本进程的操作) 返回真
// 如果结果不为 0 表示修改失败(是别的进程的操作) 返回假
atomic_dec(atomic_t *v); // 解锁
3.3 实例
/* 打开设备文件的函数 */
int ledDev_open(struct inode* inode, struct file* file)
{
if(!atomic_dec_and_test(&atm)) // 如果走进判断语句 说明原子操作后的结果不为0 说明是别的进程在操作
{
atomic_inc(&atm); // 恢复原子变量的值
return -EBUSY;
}
.........
}
............
/* 关闭设备文件的函数 */
int ledDev_close(struct inode* inode, struct file* file)
{
........
atomic_inc(&atm); // 解锁 恢复原子变量的值
return 0;
}
Tips: 原子操作进行对竞态的解决,本质上原理和自旋锁中的标志位 flag 的实现原理是一样的,只是原子操作的对象是原子变量,天生就可以保证防止竞态。而普通的 flag 变量作为标志位,就不能天生保证防止多个进程同时修改 flag 值,因此需要配合自旋锁进行工作。