Linux设备驱动开发——Linux下的并发与竟态

并发与竞态

并发是指多个执行单元同时/并行被执行,而并发的执行单元在对共享资源同时进行访问时就导致竟态。Linux内核中存在一下:

  1. 对称多处理器系统
    SMP是一种紧耦合,共享存储的处理器模型,多CPU使用共享总线,因此可以访问共同的外设和存储器。SMP下,竞态可能发生在多核处理器线程之间,不同核心的进程和中断之间;
  2. 单CPU内进程之间抢占
    Linux2.6内核之后支持抢占调度,进程可能被其他进程打断,如果两个进程共享资源,就会造成竞态。
  3. 中断与进程之间
    中断可以打破当前的执行状态,Linux2.6.35后取消了中断嵌套,因此竞态有可能会在进程和中断之间发生。

编译乱序和乱序执行

1)对于一段指令,编译器在编译时可能会调整代码的顺序,现代的高性能编译器在目标码优化上都具备对指令乱序优化的能力。编译器可以对访存的指令进行乱序,减少逻辑上不必要的访存,以尽量提高cache命中率和CPU访存效率。因此,编译器编译后的汇编码并没有严格按照代码的顺序。
2)解决编译乱序问题,可以使用barrier()编译屏障来解决。在代码中设置屏障,屏障会阻止编译器的优化,这样就能得到正确的顺序。对于编译器来说,编译屏障可以保证屏障前后的代码顺序不乱序。
C语言中volatile关键词也能阻止编译器优化代码,但volatile更多的是避免访存行为的合并,对编译器而言,volatile暗示的是当前有其他线程有可能改变内存,因此阻止编译器合并对同一内存的访问。volatile不具备保护临界资源的能力。Linux内核也不太喜欢volatile关键字
3)编译乱序是编译器为了优化而造成的,执行乱序则是处理器运行时的行为,在处理器执行指令时,后发射的指令有可能先完成。这是CPU的乱序执行策略,高性能CPU可以根据自己的缓存的组织特性,将访存指令重新排序执行。连续地址的访存可能会较先执行,而且允许访存的非阻塞如果前面指令没有命中Cache,有可能比命中cache的后条指令更晚执行。
处理器为了解决多核之间的内存屏障问题,引入了一些内存屏障指令。ARm处理器的屏障指令包括:

DMB数据内存屏障
DSB数据同步屏障
ISB指令同步屏障

Linux内核中存在自旋锁。互斥体等结构用来隔离竞争,其原理也是借助上述屏障指令。

由于多核处理器还可以乱序执行,但是每个CPU在遇到断点时会等待,所以执行乱序对单个CPU不可见。但是,当程序访问外设寄存器时,这些寄存器的访问顺序在CPU的逻辑上构不成依赖关系,但是从外设的角度讲,可能需要固定的顺序读取寄存器。这就需要CPU的内存屏障指令。
在内核中定义了读写屏障mb() 读屏障rmb()写屏障wmb()以及作用于寄存器读写的iormb()、iomb()、iowmb()这样的屏障API。readl_relaxed()和readl()的区别就在于是否加内存屏障。


中断屏蔽

对于单核CPU,避免中断引起的竞态可以采用进入临界区前关闭中断的方式,但对于SMP处理器则无效。

中断屏蔽的使用方式:
local_ire_disable();
/------临界区--------/
local_ire_enable();
由于Linux的异步操作,进程调度都依赖于中断,因此长时间屏蔽中断是一件危险的事情。
local_ire_disable();只能屏蔽本核心的中断,对于SMP处理器的其他核心中断则无效,


原子操作

原子操作可以保证对一个整型数据的操作是排他性的。Linux提供了一系列的函数来实现原子操作,这些函数共分为两大类,分别针对位和整型变量进行操作。位和整型变量的原子操作都依赖于CPU层面的原子操作。这些函数和CPU架构紧密相关。对于ARM处理器而言,底层使用LDREX和STREX指令。
ldrex和strex指令配合使用,可以让总线监控ldrex到strex之间有无其他实体访存地址,如果有并发访存发生,执行strex指令时,相应寄存器值会被置一,并且访存也不能成功。如果没有并发 的访存,strex在寄存器置零,并且访存也是成功的。ldrex和strex不但适用于多核之间的并发,也适用于单核内部并发的处理。

整型原子操作

设置原子变量值
void atomic_set(atomic_t *v ,int t);
atomic_t v = ATMOCIC_INIT( 0 );
获取原子变量值
atomic_read(atomic_t *v);
原子变量加减操作
void atomic_add(int t,atomic_t *v );
void atomic_sub(int t,atomic_t *v );
原子变量自增自减
void atomic_inc(atomic_t *v );
void atomic_dec(atomic_t *v );
操作并测试
void atomic_inc_and_test (atomic_t *v );
void atomic_sub_and_test (atomic_t *v );
void atomic_dec_and_test (atomic_t *v );
操作并返回
void atomic_add_return (int t,atomic_t *v );
void atomic_sub_return (int t,atomic_t *v );
void atomic_dec_return (int t,atomic_t *v );
void atomic_inc_return (int t,atomic_t *v );

位原子操作

设置位
void set_bit (nr,void *addr);
清除位
void clear_bit (nr,void *addr);
改变位
void change_bit (nr,void *addr);
测试位
void test_bit (nr,void *addr);
测试并操作位
void test_and_set_bit (nr,void *addr);

示例

static atomic_t xxx_available = ATOMIC_INIT(1);

static int xxx_open(struct inode *inode ,struct file *filp )
{
	...
	if (!void atomic_dec_and_test (&xxx_available) )
	{
		atomic_inc(&xxx_available);
		return -EBUSY                                    // 打开失败,已被占用
	}
	...
	return 0;                                           //成功
}
static int xxx_release(struct inode *inode ,struct file *filp )
{
	atomic_inc(&xxx_available);
	return 0;
}

自旋锁

自旋锁是一种典型的对临界资源进行互斥访问的手段,为了获得一个自旋锁,在某CPU上运行的代码首先执行一个原子操作,该操作测试并设置某个内存变量,如果测试结果表明锁已空闲,则程序可以获取自旋锁并继续执行;否则表明锁仍被占用,程序会在小循环内重复这个测试并设置的过程。

定义自旋锁: spinlock_t lock;
初始化自旋锁:spin_lock_init(lock);
获得自旋锁:spin_lock(lock);/ spin_trylock(lock);
临界区代码:/------------------------------------/
释放自旋锁:spin_unlock(lock);

自旋锁主要针对SMP系统或者可抢占式单核系统,对于单CPU和内核不支持抢占的系统,原子锁自动退化为空操作。注意到在SMP系统中,获得自旋锁的核心上抢占调度会被禁止,但是其他核心并不受影响。尽管采用了原子锁后,可以保证临界区不被其他CPU和核心和本核心内的抢占调度影响,但是中断和底半部仍然可能造成并发,因此原子锁需要和关中断配合使用。内核中提供这两个函数的组合
spin_lock_irq = spin_lock + local_irq_disable;

在多核心编程时,如果进程和中断可能访问同一片临界资源,那么在进程上下文就需要使用spin_lock_irq,而在中断上下文使用spin_lock;从而避免了内核中并发的可能。

另外,自旋锁的在使用需要注意一下问题:
1)自旋锁时忙等锁,CPU在测试锁时一直在等待,因此只有在锁被占用极短时间的情况下使用自旋锁才是合理的。当临界区很大,自旋锁需要等待较长的时间,这会极大的浪费CPU性能。
2)自旋锁可能导致系统死锁,引发这个问题的常见情况是递归引用自旋锁,如果一个已经拥有自旋锁的进程再次试图获得这个自旋锁,系统将会死锁。
3)自旋锁锁定期间不能调用可能引起系统调度的函数。如个进程获得自旋锁之后,再阻塞,可能会引起系统崩溃。
4)再多核心处理器下,spin_lock_irq只能阻止单核中断并发,对于其他核心的并发无能为力,因此在中断中也要使用spin_lock

读写自旋锁

自旋锁并不关心临界区在做什么事情,实际上,对共享资源并发访问时,多个执行单元同时读是没有问题的。从自旋锁衍生而来的读写锁允许读并发,而不允许写并发。读写自旋锁rmlock。读写自旋锁使用如下

//定义和初始化
rwlock_t myrwlock;
rwlock_init (&myrelock);
//读锁定
void read_lock (rwlock_t *rwlock);
void read_lock_irqsave (rwlock_t *rwlock,unsigned long flag);
void read_lock_irq (rwlock_t *rwlock);
void read_lock_bn (rwlock_t *rwlock);
//读解锁
void read_unlock(rwlock_t *rwlock)void read_unlock_irqsave (rwlock_t *rwlock,unsigned long flag);
void read_unlock_irq (rwlock_t *rwlock);
void read_unlock_bn (rwlock_t *rwlock);
//写锁定
void write_lock (rwlock_t *rwlock);
void write_lock_irqsave (rwlock_t *rwlock,unsigned long flag);
void write_lock_irq (rwlock_t *rwlock);
void write_lock_bn (rwlock_t *rwlock);
//写解锁
void write_unlock(rwlock_t *rwlock)void write_unlock_irqsave (rwlock_t *rwlock,unsigned long flag);
void write_unlock_irq (rwlock_t *rwlock);
void write_unlock_bn (rwlock_t *rwlock);

顺序锁


信号量

信号量是操作系统中典型的用于同步和互斥的手段。信号量的值可以是0/1或者n,信号量与计算机系统中经典的PV操作对应。
P(S):
1)将信号量的值减一
2)如果S大于等于零,继续执行,否则进入等待状态,排入排队队列
V(S)
1)将信号量的值加一
2)如果S>0,唤醒队列
Linux关于信号量的操作如下

定义信号量
struct semphore sem;
初始化信号量
void sem_init(struct semphore *sem,int val);
获得信号量
void down (struct semphore *sem);
//会导致睡眠,不可用于中断
void down_interruptible (struct semphore *sem);
void down_trylock (struct semphore *sem);
释放信号量
void up (struct semphore *sem);

新内核下直接使用mutex作为互斥手段,信号量不再被推荐使用
另外,信号量还可以用作同步手段,进程A等待信号量,进程B释放后唤醒A进程,完成同步

互斥体

尽量信号量机制已经可以完成互斥访问,但是内核中更推荐mutex互斥体来完成互斥操作
内核提供的操作如下:

定义互斥体并初始化
struct mutex my_mutex;
mutex_init(&my_mutex);
获取互斥体
void mutex_lock (struct mutex lock);
void mutex_lock_interruptible (struct mutex lock);
void mutex_trylock (struct mutex lock);
释放互斥体
void mutex_unlock (struct mutex lock);

互斥体和自旋锁属于不同层次上的互斥手段,互斥体依赖于自旋锁实现。互斥体是进程级的,用于多个进程之间对互斥资源的访问,当资源不可获得时,进程将会休眠。由于进程切换的开销较大,因此只有才资源使用时间较长的情况下使用互斥体是合理的。当需要保护的临界资源较短时,是自旋锁更加合理。由于CPU得不到锁后会空转,因此要求锁不能在临界区长留,否则浪费性能。

参考:
韦东山,嵌入式开发完全手册
宋宝华:Linux设备驱动开发详解

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值