前言
Linux系统是个多任务的操作系统,会存在多个任务同时访问同一片内存区域的情况,造成内存数据混乱,严重的话可能会导致系统崩溃;因此我们需要针对这一现象做处理。
产生并发的主要原因有:①最基本的原因是多线程并发访问;②抢占式并发访问;③中断程序并发访问;④多核CPU间并发访问。
并发访问产生对临界区(即共享数据段)的竞争关系,所以对于临界区要保证只能有一个线程访问。
1.原子操作
1.1 概念
原子操作就是指不能再进一步分割的操作 ,一般原子操作用于整形变量或者位的操作。
Linux 内核定义了叫做atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变量来代替整形变量,此结构体定义在include/linux/types.h 文件中,
typedef struct {
int counter;
} atomic_t;
64位的SOC的话,就要用到 64位的原子变量, Linux内核也定义了64位原子结构体:
#ifdef CONFIG_64BIT
typedef struct {
long counter;
} atomic64_t;
#endif
1.2 常用API函数
1.2.1 整形原子变量API
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,如果结果为负就返回真,否则返回假。 |
示例:
atomic_t var = ATOMIC_INIT(1); //初始化为1
atomic_set(var, 2); //var设置为2
atomic_add(1, var); //var值加1
1.2.2 原子位操作API
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. 自旋锁
2.1 概念
原子操作只能对整形变量或者位进行访问保护,自旋锁的“自旋”是“原地打转”的意思,目的是为了等待自旋锁可以使用,可以访问共享资源。
当一个线程要访问某个共享资源时,首先要先获取相应的锁,锁只能被一个线程持有,只要此线程不释放持有的锁,那么其他的线程就在原地打转等待,不能立即获取此锁。
自旋锁的缺点:自旋锁适用于短时期的轻量级加锁,如果遇到需要长时间持有锁的场景那就不适用了。
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;
2.2 自旋锁API函数
自旋锁API 函数适用于多核并发 或支持抢占的单CPU 下线程之间的并发访问,也就是用于线程与线程之间;被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞的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。 |
使用示例:
spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock);
spin_unlock(&lock);
2.2.1 中断场景中使用
为了避免死锁在中断函数中,每次获取自旋锁的时候,要先关闭本地中断。
一般在线程中使用 spin_lock_irqsave/ spin_unlock_irqrestore;
在中断中使用 spin_lock/spin_unlock。
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) | 打开下半部,并释放自旋锁。 |
2.2.2 读、写自旋锁
读写自旋锁为读和写操作提供了不同的锁;
一次只能允许一个写操作,也就是只能一个线程持有写锁,而且不能进行读操作。
但是当没有写操作的时候允许一个或多个线程持有读锁,可以进行并发的读操作。
typedef struct raw_spinlock {
arch_spinlock_t raw_lock;
#ifdef CONFIG_GENERIC_LOCKBREAK
unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
unsigned int magic, owner_cpu;
void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
} raw_spinlock_t;
2.2.3 顺序锁
顺序锁在读写锁的基础上衍生而来的,使用读写锁的时候读操作和写操作不能同时进行。使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作。
3. 信号量
3.1 概念
信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。
信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开 销要远大于信号量带来的那点优势。
信号量有一个信号量值,信号量值为1的称为二值信号量。
信号量结构体定义在linux/spinlock.h
struct semaphore {
raw_spinlock_t lock;
unsigned int count;
struct list_head wait_list;
};
3.2 信号量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, 10);
down(&sem);
up(&sem);
4. 互斥体
4.1 概念
将信号量的值设置为1就可以达到互斥体的效果,Linux中有个专门的互斥体操作。
一次只有一个线程可以访问共享资源,不能递归申请互斥体。
互斥体可以导致休眠,因此不能在中断中使用 互斥体,中断中只能使用自旋锁。
互斥体的结构体定义在linux/mutex.h
struct mutex {
/* 1: unlocked, 0: locked, negative: locked, possible waiters */
atomic_t count;
spinlock_t wait_lock;
struct list_head wait_list;
};
4.2 互斥体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) | 使用此函数获取信号量失败进入休眠以后可 以被信号打断。 |
总结
并发的问题在多线程应用中很常见,需要做好共享数据段的访问处理。另外该文章是对正点原子的Linux驱动开发相关章节的归纳总结。