Linux 内核中的并发和竞态

Last edited
Last updated September 23, 2023
Pages
Tags

1. 什么是并发

多进程访问同同一个临界资源称作并发

2. 竞态的产生原因

多个进程同时访问一个驱动的临界资源,造成资源的争抢,产生竞态。本质原因:
  1. 对于单核处理器,如果支持资源抢占,那么会存在竞态
  1. 对于多核处理器,核与核直接本身就存在竞态的现象
  1. 中断和进程之间存在竞态
  1. 中断和中断之间的竞态,取决于中断是否支持嵌套(由芯片GIC中断控制器决定,FSMP1A开发板不支持中断嵌套)
      • 支持嵌套的芯片,会像进程一样打断
      • 不支持嵌套,会进入队列的最前端

3. 竞态的解决方法

3.1 中断屏蔽

这种解决竞态的方法只适用于单核处理器,在进程访问临界资源时将中断屏蔽掉,要求屏蔽中断的时间尽可能短(ns)。一般屏蔽中断不使用,内核开发调试过程中会用到。
local_irq_disable(); // 屏蔽中断 // 临界资源 local_irq_enable(); // 使能中断

3.2 自旋锁(盲等锁)

概述:当一个进程访问临界资源时,给临界资源上锁。另一个进程也想访问这个临界资源,此时访问不到,此时这个进程进入自旋状态,原地等待。
特点
  • 自旋状态下的进程处于运行态,会消耗 CPU 的资源
  • 自旋锁适用于临界资源比较小的场景下,在临界区中不能有延时、耗时或者休眠的操作,也不能有 copy_to_user 或者 copy_from_user 这种数据拷贝函数
  • 自旋锁会产生死锁现象(多次获取未解锁的自旋锁)
  • 自旋锁可以工作于进程的上下文,也可用于中断过程
  • 自旋锁上锁前会关闭抢占(因此要求临界资源尽可能小)
API
// 1. 定义自旋锁 spinlock_t lock; // 2. 初始化自旋锁 spin_lock_init(spinlock_t *lock) // 3. 上锁 spin_lock(spinlock_t *lock) // 4. 解锁 spin_unlock(spinlock_t *lock)
实例代码
  • 以上代码并不能很好解决 LED 控制中的冲突,只是进行用法演示

3.3 信号量

概述:一个进程想要访问资源,首先获取信号量,访问完毕之后再将获取的信号量释放。如果进程资源获取不到信号量,此时这个进程进入休眠状态。
特点
  • 进程进入休眠状态后不再消耗 CPU 的资源,但是进程状态的切换需要消耗 CPU 的资源
  • 信号量保护的临界区可以很大,也可以很小,另外在临界区中可以有延迟、消耗甚至休眠的操作
  • 信号量不会出现死锁
  • 信号量可以用于进程上下文,但是不可用于中断中
  • 信号量也不会关闭抢占
API
// 1. 定义信号量 struct semaphore sem; // 2. 初始化信号量 void sema_init(struct semaphore *sem, int val); // 3. 申请信号量(上锁) void down(struct semaphore *sem); // 信号量数值 -1 // 4. 释放信号(解锁) void up(struct semaphore *sem); // 信号量数值 +1
实例代码

3.4 互斥体

概述:当一个进程想要访问临界资源时,需要先获取互斥体,获取到后访问,如果进程获取不到互斥体,此时获取不到的进程进入休眠状态
特点
  • 进程进入休眠状态后不再消耗 CPU 的资源,但是进程状态的切换需要消耗 CPU 的资源
  • 互斥体保护的临界区可以很大,也可以很小,另外在临界区中可以有延迟、消耗甚至休眠的操作
  • 互斥体不会出现死锁
  • 互斥体可以用于进程上下文,但是不可用于中断中
  • 互斥体也不会关闭抢占
  • 使用互斥体保护临界资源时,如果进程获取不到互斥体,此时进程不会立即进入休眠状态,而是等一段时间,时间过了之后仍然获取不到互斥体,此时进程才进入休眠,互斥体的效率要比信号量高
// 1. 定义互斥体 struct mutex mutex; // 2. 初始化互斥体 mutex_init(&mutex); // 3. 上锁 void mutex_lock(struct mutex *lock); void mutex_trylock(struct mutex *lock); // 4. 解锁 void mutex_unlock(struct mutex *lock);
实例代码

3.5 原子操作

概述:将进程访问临界资源的这个过程变成一个不可分割的原子状态,另一个进程想要访问这个临界资源,但是它无法打破这个原子状态。原子操作是通过对原子变量的数值的修改来实现的,而原子变量数值的修改通过内联汇编来完成。
原子操作会关闭内核抢占
API
/*******************************/ // 1. 定义一个原子变量并初始化 atomic_t atm = ATOMIC_INIT(1); // 将原子变量数值初始化为 1 // 2. 对原子变量数值修改来上锁 int atomic_dev_and_test(atomic_t *v); /* 功能:将原子变量的数值减 1 并且把运算后的结果和 0 比较 * 参数: * v:原子变量的指针 * 返回值:若运算后的结果为 0,返回真,否则返回假 */ // 3. 对原子变量数值修改来解锁 void atomic_inc(atomic_t *v); /* 功能:对原子变量的数值加 1 */ /*******************************/ // 1. 定义一个原子变量并初始化 atomic_t atm = ATOMIC_INIT(-1); // 将原子变量数值初始化为 -1 // 2. 对原子变量数值修改来上锁 int atomic_inc_and_test(atomic_t *v); // 3. 对原子变量数值修改来解锁 void atomic_dec(atomic_t *v);
实例代码