Linux驱动开发中的并发控制
2021-08-03
关键字:竞态解决方案、同步
驱动开发中共有四种方式可以解决并发竞态问题:
1、原子变量;
2、自旋锁;
3、信号量;
4、完成量;
原子变量的功能是通过硬件来操作变量的值,使该变量的值在更替过程中是原子式的,解决了在内核中因调度导致某变量在变值中途被打断从而影响到最终结果的情况。
自旋锁是一种区域保护型的锁。原子变量只能保护一个整型变量,但自旋锁却可以保护一片区域。自旋锁是一种“忙等待”锁,当条件不满足时会在原地打转而不会交出CPU执行权。
信号量的功能与自旋锁一样,都是区域保护型锁。所不同的是信号量并非“忙等待”型锁。拿不到锁资源的线程不会原地打转,而是将CPU执行权让度出去并进入休眠状态。适用于耗时较长的场景。
完成量与信号量的功能或说用途是一样的。只不过完成量的效率会比信号量高。如果有需要在多线程之间控制执行顺序的需求,优先选择完成量而不是信号量。
1、原子变量
原子变量的定义在以下头文件中:
kernel/include/linux/types.h
原子变量的类型定义如下所示:
typedef struct { int counter; } atomic_t;
需要强调的是:虽然原子类型定义看起来与普通结构体无异,但必须通过 atomic.h 中提供的接口来实现增加、减少及查询需求。
原子变量相关的接口如下所示:
1、初始化原子变量。
atomic_t counter = ATOMIC_INIT(0);
原子变量在声明的时候必须初始化。且初始化宏不能在函数中执行。初始化宏中的参数即是想初始化成的整数值。
2、增加。
static inline void atomic_inc(atomic_t* atm); static inline void atomic_add(int val, atomic_t* atm);
第一个函数表示指定原子变量自增1个值。
第二个函数则是增加指定的值。
还可以使用宏来改变原子变量的值:
#define atomic_set(atm, val)
这里的宏参数要传原子变量的地址。
3、减少。
static inline void atomic_dec(atomic_t* atm); static inline void atomic_sub(int val, atomic_t* atm);
与增加类似,分别用于自减及减去指定值。
同样可以用宏来改变值:
#define atomic_set(atm, val)
参数要传原子变量的地址。
4、查询。
#define atomic_read(atm)
参数要传原子变量的地址。
2、自旋锁
自旋锁因为是“忙等待”型锁,当在加锁时因此锁正被其它线程使用的话会一直在原地打转,因此在一定程度上会“浪费”CPU的执行权,所以程序在占用自旋锁时不宜占用过久。如果确实有需要长时间占用锁资源的应选择“信号量”锁替代。
自旋锁相关的原型定义位于以下所示头文件中:
kernel/include/linux/spinlock.h
kernel/include/linux/spinlock_types.h
自旋锁的类型定义如下:
typedef struct spinlock {
...
} spinlock_t;
自旋锁的使用流程也比较简单,主要步骤如下所示:
1、定义自旋锁变量;
2、初始化自旋锁变量;
3、加锁;
4、执行需要被保护的代码;
5、解锁;
6、结束;
上述步骤涉及到的接口如下所示:
//step 1 spinlock_t mylock; //step 2 spin_lock_init(&mylock); //step 3 spin_lock(&mylock); //step 4 code block //step 5 spin_unlock(&mylock);
3、信号量
信号量与自旋锁最大的区别就是信号量在拿不到锁时有可能会进入睡眠状态,因此信号量不能用在中断处理程序中。另外,线程的睡眠与唤醒是比较消耗计算资源的,只有在区域需要被保护的时间足够长时才宜选择信号量,否则频繁的睡眠上下文切换会更耗资源,此种情况下自旋锁会更好一点。
信号量相关的类型定义位于下述头文件中:
kernel/include/linux/semaphore.h
内核信号量类型原型如下所示:
struct semaphore { spinlock_t lock; unsigned int count; struct list_head wait_list; };
lock变量无须理会,调用接口改变信号量锁状态时系统会自行更改其状态。
count变量的值有三种状态:
1、为0时表示此信号量正被其它线程使用,但是wait_list为空;
2、小于0时与第1种状态类型,不过此时 wait_list 中有值;
3、大于0时表示此信号量当前为空闲状态。
根据count变量值的不同可以将信号量分为两种:
1、二值信号量;
2、计数信号量;
二值信号量是将count值初始化为1时。计数信号量则是将count值初始化为大于1的值,说白了就是同时允许count个线程访问此锁。
信号量的使用方式与自旋锁并无不同,定义变量、初始化、加锁解锁。
//声明变量 struct semaphore sema; //初始化 static inline void sema_init(struct semaphore* sem, int val); //或者可用宏来初始化。初始化为空闲状态 #define init_MUTEX(sem) //初始化为锁定状态 #define init_MUTEX_LOCKED(sem) //加锁 void down(struct semaphore* sem); int down_interruptible(strut semaphore* sem); //此函数可以在中断上下文中使用 //解锁 void up(struct semaphore* sem);
如果加锁的时候信号量无空闲资源,则调用加锁的线程可能会进入睡眠状态。
4、完成量
完成量的定义位于以下文件中:
kernel/include/linux/completion.h
完成量用以下结构体来描述:
struct completion { unsigned int done; wait_queue_head_t wait; };
成员done表示此完成量是否可用。当完成量的值为0时表示此完成量正被其它线程占用。
成员wait用于存放等待该完成量的进程信息。通常此链表中的进程都处于睡眠状态。
与信号量类似,完成量的使用也分以下几个步骤:
1、声明变量;
2、初始化;
3、申请完成量;
4、释放完成量;
步骤对应的函数接口如下:
//step 1 struct completion cpl; //step 2 static inline void init_completion(struct completion* x); #define DECLARE_COMPLETION(work) struct completion work = COMPLETION_INITIALIZER(work) //step 3 void __sched wait_for_completion(struct completion* x); //step 4 void complete(struct completion* x); void complete_all(struct completion* x);
如果申请完成量时被申请的完成量中done成员的值为0,则调用申请完成量的线程可能会进入睡眠状态。