目录
一、并发与竞争简介
二、原子操作
2.1 原子操作简介
2.2 原子整形操作API
2.3 原子位操作API
2.4 原子操作驱动代码
三、自旋锁
3.1 自旋锁简介
3.2 自旋锁API
3.3 自旋锁驱动代码
四、信号量
4.1 信号量简介
4.2 信号量API
4.3 信号量驱动代码
一、并发与竞争简介
Linux系统是个多线程操作系统,会存在多个线程同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱,重者可能会导致系统崩溃。Linux系统并发访问产生的原因很复杂,总结有以下原因:
①多线程并发访问,这是最基本的原因。
②抢占式并发访问。
③中断程序并发访问。
④SMP/多核核间并发访问。
并发访问导致竞争,对于临界区(共享数据段)必须保证一次只有一个线程访问,也就是要保证临界区是原子访问(原子即是不可再分的基本微粒,原子访问就表示这一个访问是一个步骤,不能再进行拆分)的。如果多个线程同时操作临界区就表示存在竞争,编写驱动的时候一定要注意避免并发和防止竞争访问!
二、原子操作
2.1 原子操作简介
原子操作指不能再进一步分割的操作,一般原子操作用于变量或者位操作。
假如现在要对无符号整形变量a赋值为3,对于C语言就是:
a = 3
但在C语言编译为汇编指令后,因为ARM架构不支持直接对寄存器进行读写操作,要借助寄存器 R0、 R1等来完成赋值操作,所以编译后的汇编指令:
ldr r0, =0X30000000 /* 变量a地址 */
ldr r1, = 3 /* 要写入的值 */
str r1, [r0] /* 将3写入到a变量中 */
假设现在线程A要向a变量写入10这个值,而线程B也要向a变量写入20这个值,我们理想中的执行顺序如图:
上图确实可以实现线程A将a变量设置为10,线程B将a变量设置为20。但是实际上的执行流程可能如图:
线程A最终将变量a设置为了20,而并不是要求的10。 要解决这个问题就要保证三行汇编指令作为一个整体运行,也就是作为一个原子存在。
2.2 原子整形操作API
Linux内核定义了叫做atomic_t的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在include/linux/types.h文件中,定义如下:
typedef struct {
int counter;
} atomic_t;
如果要使用原子操作API函数,首先要先定义一个atomic_t 的变量:
atomic_t a; //定义a
也可以在定义原子变量的时候给原子变量赋初值:
atomic_t b = ATOMIC_INIT(0); //定义原子变量b 并赋初值为0
还有读、写、增加、减少等等:
函数 | 描述 |
---|---|
ATOMIC_INIT(int i) | 定义原子变量的时候对其初始化。 |
int atomic_read(atomic_t *v) | 读取v的值,并且返回。 |
void atomic_set(atomic_t *v, int i) | 向v写入i值。 |
void atomic_add(int i, atomic_t *v) | 给v加上i值。 |
void atomic_sub(int i, atomic_t *v) | 从v减去i值。 |
void atomic_inc(atomic_t *v) | 给v加1,也就是自增。 |
void atomic_dec(atomic_t *v) | 从v减1,也就是自减。 |
int atomic_dec_return(atomic_t *v) | 从v减1,并且返回v的值。 |
int atomic_inc_return(atomic_t *v) | 给v加1,并且返回v的值。 |
int atomic_sub_and_test(int i, atomic_t *v) | 从v减i,如果结果为0就返回真,否则返回假 |
int atomic_dec_and_test(atomic_t *v) | 从v减1,如果结果为0就返回真,否则返回假 |
int atomic_inc_and_test(atomic_t *v) | 给v加 1,如果结果为0就返回真,否则返回假 |
int atomic_add_negative(int i, atomic_t *v) | 给v加i,如果结果为负就返回真,否则返回假 |
2.3 原子位操作API
位操作也是很常用的操作,它不像原子整形变量那样有个atomic_t的数据结构,而是直接对内存进行操作:
函数 | 描述 |
---|---|
void set_bit(int nr, void *p) | 将p地址的第nr位置1。 |
void clear_bit(int nr,void *p) | 将p地址的第nr位清零。 |
void change_bit(int nr, void *p) | 将p地址的第nr位进行翻转。 |
int test_bit(int nr, void *p) | 获取p地址的第nr位的值。 |
int test_and_set_bit(int nr, void *p) | 将p地址的第nr位置1,并且返回nr位原来的值。 |
int test_and_clear_bit(int nr, void *p) | 将p地址的第nr位清零,并且返回nr位原来的值。 |
int test_and_change_bit(int nr, void *p) | 将p地址的第nr位翻转,并且返回nr位原来的值。 |
2.4 原子操作驱动代码
先定义全局变量:
atomic_t lock;
在open操作函数里添加以下代码:
/* 通过判断原子变量的值来检查有没有被别的应用使用 */
if (!atomic_dec_and_test(&gpioled.lock)) {
atomic_inc(&gpioled.lock);/* 小于0的话就加1,使其原子变量等于0 */
return -EBUSY; /* LED被使用,返回忙 */
}
在release操作函数里添加以下代码:
/* 关闭驱动文件的时候释放原子变量 */
atomic_inc(&dev->lock);
在驱动入口函数里添加以下代码:
/* 初始化原子变量 */
atomic_set(&gpioled.lock, 1); /* 原子变量初始值为1 */
三、自旋锁
3.1 自旋锁简介
原子操作只能对整形变量或者位进行保护,但事实上临界区不止有这些类型,比如还有设备结构体变量——自旋锁便可胜任。
当一个线程要访问某个共享资源的时候首先要先获取相应的锁,锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。对于自旋锁而言,如果自旋锁正在被线A持有,线程B想要获取自旋锁,那么线程B就会处于忙循环-旋转-等待状态,线程B不会进入休眠状态或者说去做其他的处理,而是会一直等待锁。
3.2 自旋锁API
Linux内核使用结构体spinlock_t表示自旋锁:
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;
最基本的自旋锁API函数:
函数 | 描述 |
---|---|
DEFINE_SPINLOCK(spinlock_t lock) | 定义并初始化一个自选变量。 |
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。 |
int spin_is_locked(spinlock_t *lock) | 检查指定的自旋锁是否被获取,如果没有被获取就返回非0否则返回0。 |
自旋锁API函数适用于用于线程与线程之间,被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的API函数,否则的话会可能会导致死锁现象的发生。自旋锁会自动禁止抢占。
中断里面可以使用自旋锁,但是在中断里面使用自旋锁的时候,在获取锁之前一定要先禁止本地中断(也就是本CPU中断,对于多核SOC来说会有多个CPU核),否则可能导致锁死现象的发生。
线程A 先运行,并且获取到了lock 这个锁,当线程A 运行functionA函数的时候中断发生了,中断抢走了CPU使用权。右边的中断服务函数也要获取lock这个锁,但是这个锁被线程A占有着,中断就会一直自旋,等待锁有效。最好的解决方法就是获取锁之前关闭本地中断,Linux内核提供了相应的内核提供了相应的API:
函数 | 描述 |
---|---|
void spin_lock_irq(spinlock_t *lock) | 禁止本地中断,并获取自旋锁。 |
void spin_unlock_irq(spinlock_t *lock) | 激活本地中断,并释放自旋锁。 |
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags) | 保存中断状态,禁止本地中断,并获取自旋锁。 |
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) | 将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。 |
一般在线程中用 spin_lock_irqsave/ spin_unlock_irqrestore,在中断中用spin_lock/spin_unlock:
DEFINE_SPINLOCK(lock) /* 定义并初始化一个锁 */
/* 线程A */
void functionA (){
unsigned long flags; /* 中断状态 */
spin_lock_irqsave(&lock, flags) /* 获取锁 */
/* 临界区 */
spin_unlock_irqrestore(&lock, flags) /* 释放锁 */
}
/* 中断服务函数 */
void irq() {
spin_lock(&lock) /* 获取锁 */
/* 临界区 */
spin_unlock(&lock) /* 释放锁 */
}
3.3 自旋锁驱动代码
先定义全局变量:
int dev_stats; /* 设备状态,0,设备未使用;>0,设备已经被使用 */
spinlock_t lock; /* 自旋锁 */
在open操作函数里添加以下代码:
spin_lock_irqsave(&gpioled.lock, flags); /* 上锁 */
if (gpioled.dev_stats) { /* 如果设备被使用了 */
spin_unlock_irqrestore(&gpioled.lock, flags); /* 解锁 */
return -EBUSY;
}
gpioled.dev_stats++; /* 如果设备没有打开,那么就标记已经打开了 */
spin_unlock_irqrestore(&gpioled.lock, flags);/* 解锁 */
在release操作函数里添加以下代码:
spin_lock_irqsave(&dev->lock, flags); /* 上锁 */
if (dev->dev_stats) {
dev->dev_stats--;
}
spin_unlock_irqrestore(&dev->lock, flags);/* 解锁 */
在驱动入口函数里添加以下代码:
/* 初始化自旋锁 */
spin_lock_init(&gpioled.lock);
四、信号量
4.1 信号量简介
相比于自旋锁,信号量可以使线程进入休眠状态,不会让线程一直等待。但是,信号量的开销要比自旋锁大,因为信号量使线程进入休眠状态以后会切换线程,切换线程就会有开销。信号量总结:
①因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。
②因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
③如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
在初始化的时候将信号量值设置的大于1,那么这个信号量就是计数型信号量,计数型信号量不能用于互斥访问,因为它允许多个线程同时访问共享资源。如果要互斥的访问共享资源那么信号量的值就不能大于1,此时的信号量就是一个二值信号量。
4.2 信号量API
Linux内核使用semaphore结构体表示信号量:
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
信号量的API函数:
函数 | 描述 |
---|---|
DEFINE_SEAMPHORE(name) | 定义一个信号量,并且设置信号量的值为1。 |
void sema_init(struct semaphore *sem, int val) | 初始化信号量sem,设置信号量值为 val。 |
void down(struct semaphore *sem) | 获取信号量,因为会导致休眠,因此不能在中断中使用。 |
int down_trylock(struct semaphore *sem); | 尝试获取信号量,如果能获取到信号量就获取,并且返回0。如果不能就返回非0,并且不会进入休眠。 |
int down_interruptible(struct semaphore *sem) | 获取信号量,和down类似,只是使用 down进入休眠状态的线程不能被信号打断。而使用此函数进入休眠以后是可以被信号打断的。 |
void up(struct semaphore *sem) | 释放信号量 |
信号量的使用如下所示:
struct semaphore sem; /* 定义信号量 */
sema_init(&sem, 1); /* 初始化信号量 */
down(&sem); /* 申请信号量 */
/* 临界区 */
up(&sem); /* 释放信号量 */
4.3 信号量驱动代码
头文件加入以下代码:
#include <linux/semaphore.h>
定义全局变量:
struct semaphore sem; /* 信号量 */
在open操作函数里添加以下代码:
/* 获取信号量,进入休眠状态的进程可以被信号打断 */
if (down_interruptible(&gpioled.sem)) {
return -ERESTARTSYS;
}
#if 0
down(&gpioled.sem); /* 不能被信号打断 */
#endif
在release操作函数里添加以下代码:
up(&dev->sem); /* 释放信号量,信号量值加1 */
在驱动入口函数里添加以下代码:
/* 初始化信号量 */
sema_init(&gpioled.sem, 1);