IMX6ULL学习--Linux并发与竞争及解决机制

Linux并发与竞争及解决机制

  • 并发与竞争
  • 解决机制
    • 原子操作
      • 变量原子操作API
      • 位原子操作API
    • 加锁
      • 自旋锁
        • 自旋锁特征
        • 自旋锁使用
      • 读写自旋锁
      • 顺序锁
      • 自旋锁使用注意事项
    • 信号量
      • 特点
      • 使用
    • 互斥体

并发与竞争

  • Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱。
  • 产生原因:
    • 多线程并发访问
    • 抢占式并发访问
    • 中断程序并发访问
    • SMP(多核)核间并发访问
      因此要保护共享资源,阻止并发访问,就要保证临界区(共享数据段)是原子访问的,即访问是最基础的访问,不可拆分。

解决机制

为了解决并发访问对临界区的破坏,提出了多种解决机制。针对不同的临界区数据结构和并发情况,采取不同的措施。

原子操作

原子操作是指不能再进行拆分的操作,一般用于变量或者位操作。
原子操作API

  • 变量原子操作API
  • 位原子操作API

变量原子操作API

  • 原子整形定义
    定义原子操作整形数据。
typedef struct {
	int counter;
	} atomic_t;

如果要使用原子操作API,就必须先定义atomic_t的变量。

atomic_t b = ATOMIC_INIT(0); // 定义原子操作变量b 并赋值为0
  • 原子变量操作API
函数 描述
ATOMIC_INIT(init 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,如果结果为负就返回真,否则返回假
  • Demo
atomic_t v =ATOMIC_INIT(0);		/* 定义原子变量,初始化为0 */
atomic_set(&v,10); 	/* 设置v = 10 */
atomic_read(&v); 	/* 读取v的值,返回10 */
atomic_inc(&v);		/* v自加1 */

位原子操作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 位原来的值。

加锁

原子操作是针对整形变量和位操作,但是实际临界区数据不会那么简单,比如结构体变量,针对复杂的临界区,我们要保护它的数据稳定,就要保证线程A访问临界区时,禁止其他线程访问临界区。因此就有了加锁机制,对临界区上锁,只有获得锁的线程可以访问临界区。
针对不同的情况,产生了多种加锁机制。

自旋锁

自旋锁特征

  • 特点
    当线程A获得锁访问临界区,在释放锁之前,线程B请求锁不成功,只能一直请求锁,不可以去处理其他事情,也不能休眠。直到A释放锁。
  • 缺点
    等待自旋锁线程会一直自旋,浪费资源,所以自旋锁持有时间不能过长,适用于轻量级加锁。

自旋锁使用

  • 定义自旋锁
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
函数 描述
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函数,否则会导致死锁现象。比如:线程A获取锁,禁止内核抢占,如果此时A进入了休眠,A就放弃了CPU使用权,线程B获取使用权开始运行,但是线程B也要获取锁,但是锁未被A释放,会一直处于自旋状态,死锁就产生了。

  • 中断与自旋锁
    中断可以使用自旋锁,但是在获取锁之前一定要禁止本地中断,也就是CPU中断。否则也可能导致死锁。比如:线程A获取锁期间中断发生,中断服务函数中也要获取锁,此时就会死锁。因此在A获取锁之前要先禁止中断,释放锁之后要恢复中断状态。
    相关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_irq/spin_unlock_irq 的时候需要用户能够确定加锁之前的中断状态,但实际上内核很庞大,运行也是“千变万化”,我们是很难确定某个时刻的中断状态,因此不推荐使用spin_lock_irq/spin_unlock_irq。建议使用 spin_lock_irqsave/spin_unlock_irqrestore,因为这一组函
数会保存中断状态,在释放锁的时候会恢复中断状态。一般在线程中使用 spin_lock_irqsave/spin_unlock_irqrestore,在中断中使用 spin_lock/spin_unlock。

  • Demo
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) /* 释放锁 */
}

读写自旋锁

读写锁只需要保证读和写不同时发生就行,同一时间只能有一个人获取写操作权限,但是可以多人并发读取。当某个数据结构符合读/写或者生产者/消费者模型的时候,就可以使用读写锁。

  • 定义读写锁
typedef struct {
 arch_rwlock_t raw_lock;
} rwlock_t;
  • API函数
函数 描述
DEFINE_RWLOCK(rwlock_t lock) 定义并初始化读写锁
void rwlock_init(rwlock_t *lock) 初始化读写锁。
读锁
void read_lock(rwlock_t *lock) 获取读锁。
void read_unlock(rwlock_t *lock) 释放读锁。
void read_lock_irq(rwlock_t *lock) 禁止本地中断,并且获取读锁。
void read_unlock_irq(rwlock_t *lock) 打开本地中断,并且释放读锁。
void read_lock_irqsave(rwlock_t *lock,unsigned long flags) 保存中断状态,禁止本地中断,并获取读锁。
void read_unlock_irqrestore(rwlock_t *lock,unsigned long flags) 将中断状态恢复到以前的状态,并且激活本地中断,释放读锁。
void read_lock_bh(rwlock_t *lock) 关闭下半部,并获取读锁。
void read_unlock_bh(rwlock_t *lock) 打开下半部,并释放读锁。
写锁
void write_lock(rwlock_t *lock) 获取写锁。
void write_unlock(rwlock_t *lock) 释放写锁。
void write_lock_irq(rwlock_t *lock) 禁止本地中断,并且获取写锁。
void write_unlock_irq(rwlock_t *lock) 打开本地中断,并且释放写锁。
void write_lock_irqsave(rwlock_t *lock,unsigned long flags) 保存中断状态,禁止本地中断,并获取写锁。
void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags) 将中断状态恢复到以前的状态,并且激活本地中断,释放读锁。
void write_lock_bh(rwlock_t *lock) 关闭下半部,并获取读锁。
void write_unlock_bh(rwlock_t *lock) 打开下半部,并释放读锁。

顺序锁

顺序锁在读写锁的基础上衍生而来的,使用读写锁的时候读操作和写操作不能同时进行。使用顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作。虽然顺序锁的读和写操作可以同时进行,但是如果在读的过程中发生了写操作,最好重新进行读取,保证数据完整性。顺序锁保护的资源不能是指针,因为如果在写操作的时候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如读取野指针导致系统崩溃。

  • 定义顺序锁
typedef struct {
 	struct seqcount seqcount;
 	spinlock_t lock; 
 } seqlock_t;
  • API
函数 描述
DEFINE_SEQLOCK(seqlock_t sl) 定义并初始化顺序锁
void seqlock_ini seqlock_t *sl) 初始化顺序锁。
顺序锁写操作
void write_seqlock(seqlock_t *sl) 获取写顺序锁。
void write_sequnlock(seqlock_t *sl) 释放写顺序锁。
void write_seqlock_irq(seqlock_t *sl) 禁止本地中断,并且获取写顺序锁
void write_sequnlock_irq(seqlock_t *sl) 打开本地中断,并且释放写顺序锁。
void write_seqlock_irqsave(seqlock_t *sl, unsigned long flags) 保存中断状态,禁止本地中断,并获取写顺序
锁。
void write_sequnlock_irqrestore(seqlock_t *sl,unsigned long flags) 将中断状态恢复到以前的状态,并且激活本地中断,释放写顺序锁。
void write_seqlock_bh(seqlock_t *sl) 关闭下半部,并获取写读锁。
void write_sequnlock_bh(seqlock_t *sl) 打开下半部,并释放写读锁。
顺序锁读操作
unsigned read_seqbegin(const seqlock_t *sl) 读单元访问共享资源的时候调用此函数,此函数会返回顺序锁的顺序号。
unsigned read_seqretry(const seqlock_t *sl,unsigned start) 读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读

自旋锁使用注意事项

  • 线程持有锁时间不能太久,否则会降低CPU性能,如果时间较长建议使用其他并发处理方式。
  • 自旋锁临界区内不能调用会导致休眠的API,否则会死锁。
  • 自旋锁不能递归申请,否则会死锁。
  • 编写驱动时,考虑到驱动可移植性,将其当做多核SOC编写驱动。

信号量

特点

  • 因为信号量可以使等待资源线程进入休眠状态,因此适用于那些占用资源比较久的场合。
  • 因此信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。
  • 如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开销要远大于信号量带来的那点优势。

使用

  • 定义
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)| 释放信号量|

  • Demo

struct semaphore sem; /* 定义信号量 */
sema_init(&sem, 1); /* 初始化信号量 */
down(&sem); /* 申请信号量 */
/* 临界区 */
up(&sem); /* 释放信号量 */

互斥体

你可能感兴趣的:(IMX6ULL学习--Linux并发与竞争及解决机制)