一、并发与竞争简介
并发与竞争:对于多核 CPU,多线程同时运行时,有可能访问同一个共享资源,可能会相互覆盖这些共享资源,导致内存数据混乱,因此要保证共享资源的有序访问。
临界区:指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。当有线程进入临界区段时,其他线程或是进程必须等待。
linux 系统并发与竞争产生原因:
1、多线程并发访问
2、抢占式并发访问
3、中断程序并发访问
4、SMP(多核)间并发访问
二、并发与竞争处理方法
1、原子操作
1)原子操作简介
原子操作就是指不能在进一步分割的指令,一般原子操作用于变量或者位操作。假设对一个整形变量赋值,c 语言下的表现为:
a = 3;
而在汇编的表现为:
ldr r0, =0X30000000 /* 变量a地址 */
ldr r1, = 5 /* 要写入的值 */
str r1, [r0] /* 将5写入到a变量中 */
在汇编环境下,如果代码执行完第二行但还没执行第三行时,有另一个线程覆盖了 r1 寄存器的值,这就会导致最后的赋值不正确。如果把三行汇编语句作为一个整体去运行,也就是作为原子存在,就不会出现以上情况。
2)原子操作结构体
linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在 include/linux/types.h 文件中,定义如下:
typedef struct {
int counter;
} atomic_t;
使用原子操作 API 函数前,先定义一个原子变量:
atomic_t a;
可通过宏 ATOMIC_INIT 赋初值:
a = ATOMIC_INIT(0);
3)原子整形操作常用API函数(32位)
函数 | 描述 |
---|---|
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,如果结果为负返回真,否则返回假。 |
4)原子整形位操作常用API函数
函数 | 描述 |
---|---|
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、自旋锁
1)自旋锁简介
当一个线程要访问某个共享资源的时候首先要先获取相应的锁,锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。
对于自旋锁而言,如果自旋锁正在被线程 A 持有,线程 B 想要获取自旋锁,那么线程 B 就会处于忙循环 -旋转 -等待状态,线程 B 不会进入休眠状态或者说去做其他的处理。因此如果线程锁持有时间较长,就不应该使用自旋锁来处理。
2)自旋锁结构体
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;
使用自旋锁之前,先定义一个自旋锁变量:
spinlock_t lock;
3)自旋锁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。 |
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) | 将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。 |
void spin_lock_bh(spinlock_t *lock) | 关闭下半部,并获取自旋锁。 |
void spin_unlock_bh(spinlock_t *lock) | 打开下半部,并释放自旋锁。 |
注意事项:
被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的 API 函数,否则的话会可能会导致死锁现象的发生。自旋锁会自动禁止抢占,也就说当线程 A 得到锁以后会暂时禁止内核抢占。如果线程 A 在持有锁期间进入了休眠状态,那么线程 A 会自动放弃 CPU 使用权。线程 B 开始运行,线程 B 也想要获取锁,但是此时锁被 A 线程持有,而且内核抢占还被禁止了!线程 B 无法被调度出去,那么线程 A 就无法运行,锁也就无法释放。
中断里面也可以使用自旋锁,但是使用自旋锁时要先禁止本地中断,否则可能会发生死锁现象。因为中断内获取了自旋锁,此时假设被另一中断打断,而该中断也要获取自旋锁,但是自旋锁在前一个中断还没被释放,因此就会发生死锁。
推荐一般在线程中使用 spin_lock_irqsave / spin_unlock_irqrestore,以保存一些中断的状态。而中断中使用 spin_lock / spin_unlock。
不能递归申请自旋锁。
4)自旋锁使用示例:
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、信号量
1)信号量简介
相比于自旋锁,信号量可以使等待的线程进入休眠状态。等临界区被释放后,休眠等待的线程会被唤醒。信号量有以下特点:
-
因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。
-
信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
-
如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。
信号量分为两种,一种是计数型信号量,一种是二值型信号量。
计数型信号量有一个信号量值,假设设置该量值为 5,则同时访问共享资源的线程可以是 5 个。
二值型信号量,信号量值只能是 0 或 1,使得共享资源只能互斥访问。
2)信号量结构体
Linux内核使用 semaphore 结构体表示信号量,结构体内容如下所示:
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
3)信号量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 进入休眠状态的线程不能被信号打断。而使用此函数进入休眠以后是可以被信号打断的。如果有中断打断的话会返回–EINTR。 |
void up(struct semaphore *sem) | 释放信号量 |
4)信号量使用示例:
struct semaphore sem; /* 定义信号量 */
sema_init(&sem, 1); /* 初始化信号量 */
down(&sem); /* 申请信号量 */
/* 临界区 */
up(&sem); /* 释放信号量 */
4、互斥体
1)互斥体简介
信号量设置为 1 可以实现共享资源的互斥访问,但是 linux 内核中提供了一个更专业的处理机制,互斥体(mutex)。互斥访问表示一次只有一个线程可以访问共享资源。使用 mutex 时需要注意以下几点:
- mutex 可以导致休眠,因此不能在中断中使用 mutex,中断中只能使用自旋锁。
- 和信号量一样,mutex 保护的临界区可以调用引起阻塞的 API 函数。
- 因为一次只有一个线程可以持有 mutex,因此,必须由 mutex 的持有者释放 mutex。并且 mutex 不能递归上锁和解锁。
2)互斥体结构体
linux 内核使用 mutex 来表示互斥体,定义如下:
struct mutex { /* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
};
3)互斥体API函数
函数 | 描述 |
---|---|
DEFINE_MUTEX(name) | 定义并初始化一个 mutex 变量。 |
void mutex_init(mutex *lock) | 初始化 mutex。 |
void mutex_lock(struct mutex *lock) | 获取 mutex,也就是给 mutex 上锁。如果获取不到就进休眠。 |
void mutex_unlock(struct mutex *lock) | 释放 mutex,也就给 mutex 解锁。 |
int mutex_trylock(struct mutex *lock) | 尝试获取 mutex,如果成功就返回 1,如果失败就返回 0。 |
int mutex_is_locked(struct mutex *lock) | 判断 mutex 是否被获取,如果是的话就返回 1,否则返回 0。 |
int mutex_lock_interruptible(struct mutex *lock) | 使用此函数获取信号量失败进入休眠以后可以被信号打断。 |
4)互斥体使用示例:
struct mutex lock; /* 定义一个互斥体 */
mutex_init(&lock); /* 初始化互斥体 */
mutex_lock(&lock); /* 上锁 */
/* 临界区 */
mutex_unlock(&lock); /* 解锁 */