linux驱动学习笔记(二)并发处理


前言

当多个单元同时对一个共享资源进行操作时很容易出现竞态问题
例如:我们想输出一句话(驱动学习)

线程A线程B

最终变成了(驱动啊学啊)


一、中断屏蔽

中断屏蔽可以保证内核执行流程不被中断打断,防止一些竟态的发生,由于内核进程调度等操作都是依赖中断来实现的,所以内核抢占进程的并发情况也可以避免。但是长时间屏蔽中断是很危险的,有可能造成一些无法预料的后果,因此应当尽快的执行完临界区的代码

local_irq_disable() /* 屏蔽中断 */
/*
	临界区
*/
local_irq_enable() /* 开中断*/

local_irq_disablelocal_irq_enable函数只能屏蔽本CPU的中断,无法屏蔽因多CPU引发的竟态,所以一般不推荐。但是中断屏蔽适合与自旋锁一起使用

local_irq_save() /* 屏蔽中断 */
/*
	临界区
*/
local_irq_restore() /* 开中断*/

local_irq_savelocal_irq_restore 可以禁止和恢复中断外,还可以保存cpu的中断信息,也就是保存CPSR寄存器。

local_bh_disable() /* 屏蔽中断 */
/*
	临界区
*/
local_bh_enable() /* 开中断*/

local_bh_disablelocal_bh_disable 可以禁止和恢复中断下半部。

二、原子操作

1.整形变量的原子操作

/*
	设置原子变量的值
*/
void atomic_set(atomic_t *v, int i); /* 设置原子变量的值为i */
atomic_t v = ATOMIC_INIT(0); /* 定义原子变量v并初始化为0 */
/*
	获取原子变量的值
*/
atomic_read(atomic_t *v); /* 返回原子变量的值*/
/*
	原子变量的加减
*/
void atomic_add(int i, atomic_t *v); /* 原子变量增加i */
void atomic_sub(int i, atomic_t *v); /* 原子变量减少i */
void atomic_inc(atomic_t *v); /* 原子变量增加1 */
void atomic_dec(atomic_t *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就返回true,否则返回false*/
int atomic_dec_and_test(atomic_t *v) /*从v减1,如果结果为0就返回true,否则返回false*/
int atomic_inc_and_test(atomic_t *v) /*给v加1,如果结果为0就返回true,否则返回false*/
int atomic_add_negative(int i, atomic_t *v)/*给v加i,如果结果为负就返回true,否则返回false*/

2.位原子操作

void set_bit(nr, void *addr);//设置位,将地址的第nr位置1
void clear_bit(nr, void *addr);//清除位,将地址的第nr位置0
void change_bit(nr, void *addr);//反转位,将地址的第nr位置取反
int test_bit(nr, void *addr);//返回nr位
/*
测试并操作nr位
*/
int test_and_set_bit(nr, void *addr);
int test_and_clear_bit(nr, void *addr);
int test_and_change_bit(nr, void *addr);

示例

使用原子变量的设备只能被一个进程打开

static atomic_t xxx_val = ATOMIC_INIT(1); /* 定义原子变量并设为1*/
static int xxx_open(struct inode *inode, struct file *filp)
{
	...
	if (!atomic_dec_and_test(&xxx_val)) {
		atomic_inc(&xxx_val);
		return - EBUSY; /* 已经打开*/
	}
	...
	return 0; /*操作成功 */
}
static int xxx_release(struct inode *inode, struct file *filp)
{
	atomic_inc(&xxx_val); /* 释放设备 */
	return 0;
}

三、自旋锁

自旋锁是一种常用的互斥手段,相当于一个原子操作,该操作测试并设置某个变量。 由于它是原子操作, 所以在该操作完成之前其他执行单元不可能访问这个变量。如果程序空闲则程序持有这个自旋锁并继续执行,反之则原地自旋等待。

spinlock_t lock;//定义自旋锁
spin_lock_init(lock);//初始化自旋锁
DEFINE_SPINLOCK(spinlock_t lock) //定义并初始化一个自选变量。
spin_lock(lock);//持有自旋锁或者等待一会等持有自旋锁的程序释放后再持有
spin_unlock(lock);//持有自旋锁的程序释放
spin_trylock(lock);//看看能不能持有自旋锁,能就拿到不行就撤
spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()

多核编程中一般需要在进程上下文中调用spin_lock_irqsave( ) /spin_unlock_irqrestore( ),
中断上下文中调用spin_lock( ) /spin_unlock( ),

应当谨慎使用自旋锁
1.只有在占用锁极短的时间下才可以使用自旋锁,当临界区很大适合需要较长时间占用锁时,使用自旋锁会降低系统性能
2.如果拥有某个自旋锁的进程二次获得自旋锁,则死锁
3.在自旋锁锁定期间不能调用可能引起进程调度(休眠)的函数。如果进程获得自旋锁之后再阻塞则导致系统崩溃(copy_from_user( ) 、 copy_to_user( ) 、 kmalloc( ) 和msleep( ) 等)


## 示例
int xxx_count = 0;/* 定义文件打开次数计数*/
static int xxx_open(struct inode *inode, struct file *filp)
{
	...
	spin_lock(&xxx_lock);
	if (xxx_count) {/* 已经打开*/
		spin_unlock(&xxx_lock);
		return -EBUSY;
	}
	xxx_count++;/* 增加使用计数*/
	spin_unlock(&xxx_lock);
	...
	return 0;/* 成功 */
}
	static int xxx_release(struct inode *inode, struct file *filp)
{
	...
	spin_lock(&xxx_lock);
	xxx_count--;/* 减少使用计数*/
	spin_unlock(&xxx_lock);
	return 0;
}

四、读写锁

读写锁保留了自旋锁的概念 ,但是同时只能有一个进程写 ,可以有多个进程读 ,但是读写不能同时进行。

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) 打开下半部,并释放读锁。

示例

rwlock_t lock; /* 定义rwlock */
rwlock_init(&lock); /* 初始化rwlock */
/* 读时获取锁*/
read_lock(&lock);
	临界资源
read_unlock(&lock);
/* 写时获取锁*/
write_lock_irqsave(&lock, flags);
	临界资源
write_unlock_irqrestore(&lock, flags);

五、顺序锁

顺序锁的话可以允许在写的时候进行读操作,也就是实现同时读写,但是不允许同时进行并发的写操作。
顺序锁保护的资源不能是指针,因为如果在写操作的时候可能会导致指针无效,而这个时候恰巧有读操作访问指针的话就可能导致意外发生,比如读取野指针导致系统崩溃。

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)读结束以后调用此函数检查在读的过程中有没有对资源进行写操作,如果有的话就要重读

六、信号量

信号量作为最典型的同步互斥手段,例如我有一个房子四张床,每进去一个人床就少一张,进去四个人没人出来第五个人就没办法在床上躺着了,除非你要抱着那四个人其中一个人睡,哈哈哈。
一般内核中使用信号量不是用来做互斥的因为linux内核更倾向于mutex作为互斥手段,所以信号量一般都为计数型

DEFINE_SEAMPHORE(name) 定义一个信号量,并且设置信号量的值为 1void 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) 释放信号量

应当谨慎使用信号量
1.down函数用于获得信号量 ,会导致睡眠,所以一定不能在中断上下文中使用,在中断中可以使用down_trylock
2.down适合那些占用资源比较久的情况,不适用占用资源比较短的情况,因为频繁的引起休眠,线程切换开销较大

七、互斥体

互斥体作为解决互斥问题的基本手段。

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,如果失败就返回 0int mutex_is_locked(struct mutex *lock)判断 mutex 是否被获取,如果是的话就返回1,否则返回 0int mutex_lock_interruptible(struct mutex *lock)使用此函数获取信号量失败进入休眠以后可以被信号打断。

互斥体用于多个事件之间对资源的互斥,若争夺资源失败,则睡眠并发生进程上下文切换,因此只有当资源占用时间过长时候非中断上下文时候才可以使用互斥体

互斥体与自旋锁使用场景区分
1.若临界区比较小, 宜使用自旋锁, 若临界区很大, 应使用互斥体。
2.互斥体所保护的临界区可包含可能引起阻塞的代码, 而自旋锁则绝对要避免用来保护包含这样代码的临界区。 因为阻塞意味着要进行进程的切换, 如果进程被切换出去后, 另一个进程企图获取本自旋锁, 死锁就会发生
3.如果被保护的共享资源需要在中断或软中断情况下使用, 则在互斥体和自旋锁之间只能选择自旋锁。 当然, 如果一定要使用互斥体, 则只能通过mutex_trylock方式进行, 不能获取就立即返回以避免阻塞

八、completion

用于一个执行单元等待另一个执行单元完成某个事件

struct completion s_completion;
init_completion(&s_completion);
reinit_completion(&s_completion)//重新初始化为0
void wait_for_completion(struct completion *c);//等待一个completion完成被唤醒
void complete(struct completion *c);//唤醒completion
void complete_all(struct completion *c);//唤醒completion

总结

中断屏蔽很少使用, 原子操作只能针对整数进行,自旋锁不允许阻塞 临界区必须小,互斥体可以阻塞 适用临界区大的情况.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值