Linux系统是个多任务操作系统,会有多个任务同时存在的可能性,这些任务的内存可能相互覆盖,导致内存数据混乱,并发访问带来的问题就是竞争,会有以下问题导致Linux系统并发产生:
①多线程并发访问,这是Linux系统最基本的。
②抢占式并发访问,Linux2.6版本后支持抢占。
③中断程序并发访问
④SMP(多核)核间并发访问
1.保护的是什么
一开始写驱动的时候就要考虑并发与竞争,否则写完驱动后不容易查找错误,导致驱动调试难度加大、费时费力。竞争的东西便是资源(在系统里是数据),故我们需要保护的数据,也就是保护临界区,共享资源。找到保护的数据是重点,也是难点,一般像全局变量,设备结构体这些肯定要保护的,其他的就随机应变了。
对于新手来说,可以仿照其他代码来处理并发与竞争。
2.常用的保护方法
原子操作分为原子整形和原子位操作
场景一:临界区有个a,线程A要a=10,线程B要a=20,线程B的优先级高于线程A,当线程A正在运行的时候,线程B开始执行,最终的结果导致线程A的a=20。(这里涉及汇编,一个赋值,在汇编上面其实是3个步骤)
对于场景一来说,想要解决这样的问题,Linux内核提供了原子操作,保证不会出现这种错误。
一:原子整形操作API函数
在linux内核源码的/include/linux/types.h文件中,定义了这个结构体
typedef struct {
int counter;
} atomic_t;
而在/linux/linux/atomic.h头文件中定义了许多原子变量操作
如
void atomic_set(atomic_t *v) | 给v写入i值 |
---|---|
void atomic_add(int i, atomic_t *v) | 给v加上i值 |
适合范围:当写一个驱动的时候,涉及全局变量是个整形的时候,第一件事就是在定义变量的时候换成原子操作
二:原子位操作API函数
原子位操作是直接对内存进行操作的,如定义一个变量,对变量的地址进行操作
原子操作只能对整形变量和位操作,而实际开发中,怎么可能只有整形,还有结构体,那么原子操作无法保护结构体的原子性,当一个线程要访问某个共享资源的时候首先要先获取相应的锁,锁只能被一个线程持有,只要此线程不释放持有锁,那么其他线程就不会获取此锁
自旋锁,顾名思义,自旋=原地打转,原地打转的目的是为了等待自旋锁可以被使用。这就会导致自旋锁有个缺点,也就是等待自旋锁的线程会一直处于自旋状态,非常浪费处理器时间,降低系统性能,故自旋锁的持有时间不能太长。
Linux使用结构体spinlock_t表示自旋锁,在/include/linux/spinlock_types.h中定义了以下结构体:
typedef struct spinlock {
union {
struct raw_spinlock rlock;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
struct {
u8 __padding[LOCK_PADSIZE];
struct lockdep_map dep_map;
};
#endif
};
} spinlock_t;
使用方法:
spinlock_t lock;//定义自旋锁
最基本的API函数:
函数 | 描述 |
---|---|
int spin_lock_init(spinlock_t *lock) | 初始化自旋锁 |
void spin_lock(spinlock_t *lock) | 获取指定的自旋锁,也叫加锁 |
void spin_unlock(spinlock_t *lock) | 释放指定的锁 |
int spin_trylock(spinlock_t *lock) | 尝试获取指定的自旋锁,失败则返回0 |
自旋锁保证同一时间一个线程访问一个资源,自旋锁适用多核SMP,使用自旋锁要特别注意死锁现象的发生,也就是被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API函数,否则会造成死锁现象发生。一旦发生睡眠和阻塞,自旋锁会自动禁止抢占。
场景二:线程A(spin_lock----------执行的代码---------spin_unlock)
线程A执行一半休眠了
线程B(spin_lock----------执行的代码---------spin_unlock)
当线程A正在运行,如果此时调用了引起线程A睡眠和阻塞的API函数的话,线程A会释放出CPU使用权,此时的锁是线程A获得的,但CPU跑去执行线程B了,线程B也想要获得锁,但线程A已经持有锁了,而且内核抢占还被禁止了,这就导致线程B无法被调度出去,线程A也无法运行,锁也就无法释放,这就导致死锁发生了。
场景三:线程A(spin_lock----------执行的代码---------spin_unlock)
线程A执行一半发生中断了
线程B(spin_lock----------执行的代码---------spin_unlock)
当中断打断了线程A,中断抢走了CPU使用权,出现的情况和场景二一样,同样会导致死锁发生。最好的解决方法就是,获取锁之前关闭本地中断。
void spin_lock_irq(spinlock_t *lock) | 禁止本地中断,并获取自旋锁 |
---|---|
void spin_unlock_irq(spinlock_t *lock) | 激活本地中断,并释放自旋锁 |
注意:
①自旋锁的持有锁时间不能太长,这也是自旋锁的一个缺点
②自旋锁保护的临界区不能调用任何可能导致线程休眠的API函数
③不能递归申请自旋锁
信号量的出现就是解决了自旋锁不能持有锁时间太长的问题,使用信号量会提高处理器的使用效率,毕竟不用自已在原地打转。
信号量有几个特点:
①信号量可以等待资源线程进入休眠状态
②信号量依旧不能用于中断,否则会引起休眠
③共享资源持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势
信号量和自旋锁的使用区别就在于锁的持有时间长与短的区分
互斥访问表示依次只有一个线程可以访问共享资源,不能递归申请互斥体,使用方法和自旋锁一致
互斥体的使用要注意以下几点:
①互斥体可以导致休眠,因此不能再中断中使用互斥体,中断中只能使用自旋锁
②和信号量一样,互斥体保护的临界区可以调用引起阻塞的API函数
③必须由互斥体的持有者释放互斥体,因为一次只有一个线程可以持有互斥体