Linux 设备驱动中必须要解决的一个问题是多个进程对共享的资源的并发访问,并发的访问会导致竞态,即使是经验丰富的驱动工程师也常常设计出包含并发问题bug 的驱动程序。
一、基础概念
1、Linux 并发相关基础概念
a -- 并发(concurrency):并发指的是多个执行单元同时、并发被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(race condition);
b -- 竞态(race condition) :竞态简单的说就是两个或两个以上的进程同时访问一个资源,同时引起资源的错误;
c -- 临界区(Critical Section):每个进程中访问临界资源的那段代码称为临界区;
d -- 临界资源 :一次仅允许一个进程使用的资源称为临界资源;多道程序系统中存在许多进程,它们共享各种资源,然而有很多资源一次只能供一个进程使用;
在宏观上并行或者真正意义上的并行(这里为什么是宏观意义的并行呢?我们应该知道“时间片”这个概念,微观上还是串行的,所以这里称为宏观上的并行),可能会导致竞争; 类似两条十字交叉的道路上运行的车。当他们同一时刻要经过共同的资源(交叉点)的时候,如果没有交通信号灯,就可能出现混乱。在linux 系统中也有可能存在这种情况:
2、并发产生的场合
a -- 对称多处理器(SMP)的多个CPU
SMP 是一种共享存储的系统模型,它的特点是多个CPU使用共同的系统总线,因此可访问共同的外设和储存器,这里可以实现真正的并行;
b -- 单CPU内进程与抢占它的进程
一个进程在内核执行的时候有可能被另一个高优先级进程打断;
c -- 中断和进程之间
中断可以打断正在执行的进程,如果中断处理函数程序访问进程正在访问的资源,则竞态也会发生;
3、解决竞态问题的途径
解决竞态问题的途径最重要的是保证对共享资源的互斥访问,所谓互斥访问是指一个执行单元在访问共享资源的时候,其他的执行单元被禁止访问。
Linux 设备中提供了可采用的互斥途径来避免这种竞争。主要有原子操作,信号量,自旋锁。
那么这三种有什么相同的地方,有什么区别呢?适用什么不同的场合呢?会带来什么边际效应?要彻底弄清楚这些问题,要从其所处的环境来进行细化分类处理。是UP(单CPU)还是SMP(多CPU);是抢占式内核还是非抢占式内核;是在中断上下文不是进程上下文。似交通信号灯一样的措施来避免这种竞争。
先看一下三种并发机制的简单概念:
原子锁:原子操作不可能被其他的任务给调开,一切(包括中断),针对单个变量。
自旋锁:使用忙等待锁来确保互斥锁的一种特别方法,针对是临界区。
信号量:包括一个变量及对它进行的两个原语操作,此变量就称之为信号量,针对是临界区。
二、并发处理途径详解
1、中断屏蔽
在单CPU范围内避免静态的一种简单而省事的方法是在进入临界区之前屏蔽系统的中断,这项功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,防止某些竞争条件的发生。具体而言
a -- 中断屏蔽将使得中断和进程之间的并发不再发生;
b -- 由于Linux内核的进程调度等操作都依赖中断来实现,内核抢占进程之间的并发也得以避免;
中断屏蔽的使用方法:
local_irq_disable()
local_irq_enable()
只能禁止和使能本地CPU的中断,所以不能解决多CPU引发的竞态
local_irq_save(flags)
local_irq_restore(flags)
除了能禁止和使能中断外,还保存和还原目前的CPU中断位信息
local_bh_disable()
local_bh_disable()
如果只是想禁止中断的底半部,这是个不错的选择。
但是要注意:
a -- 中断对系统正常运行很重要,长时间屏蔽很危险,有可能造成数据丢失乃至系统崩溃,所以中断屏蔽后应尽可能快的执行完毕。
b -- 宜与自旋锁联合使用。
所以,不建议使用中断屏蔽。
2、原子操作
原子操作(分为原子整型操作和原子位操作)就是绝不会在执行完毕前被任何其他任务和时间打断,不会执行一半,又去执行其他代码。原子操作需要硬件的支持,因此是架构相关的,其API和原子类型的定义都在include/asm/atomic.h中,使用汇编语言实现。
在linux中,原子变量的定义如下:
typedef struct { volatile int counter; } atomic_t;
关键字volat