Linux设备驱动中必须解决的一个问题是多个进程对共享资源的并发访问,
并发
访问会导致
竞态
。
并发(concurrency)指的是多个执行单元同时、并行被执行,而并行的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(race conditions)。
Source of concurrency
1.对称多处理器(SMP)的多个CPU
2.单CPU内进程与抢占它的进程
3.中断(硬中断、软中断、Tasklet、底半部)与进程之间
除了SMP是真正的并行之外,其他的都是“宏观并行,微观串行”的,但其引发的实质问题和SMP相似。
临界区(critical sections)
解决竞态问题的途径是保证对共享资源的互斥访问,所
谓互斥访问时指一个执行单元在访问共享资源的时候,
其他的执行单元被禁止访问。
访问共享资源的代码区域称为临界区,
临界区需要以某种互斥机制加以保护。中断屏蔽、原子
操作、自旋锁、和信号量等是Linux设备驱动中可采用
的互斥途径。
中断屏蔽
在单CPU范围内避免竞态的一种简单方法是在进入临界区之前屏蔽系统的中断。由于linux内核的进程调度都依赖中断来实现,内核抢占进程之间的并发也就得以避免了。
中断屏蔽的使用方法:
local_irq_disable() //屏蔽中断
...
critical section //临界区
...
local_irq_enable() //开中断
这种方案并不好,应为把屏蔽中断的权利交给用户进程是不明智的。
在屏蔽中断期间所有的中断都无法得到处理!
原子操作
原子操作指的是在执行过程中不会被别的代码路径所中断的操作。
atomic_t,定义在
typedef struct
{
volatile int counter;
}atomic_t;
volatile修饰字段告诉gcc不要对这类型的数据做优化处理,对它的访问都是对内存的访问,而不是对寄存器的访问。
atomic_set(atomic_t *v, int i); //设置原子变量的值为i
atomic_t v = ATOMIC_INT(0); //定义原子变量v并初始化为0
atomic_read(atomic_t *v); //获取原子变量的值
atomic_add(int i, atomic_t *v); //获取原子加
atomic_sub(int i, atomic_t *v); //获取原子减
atomic_inc(atomic_t *v); //原子变量自增
atomic_dec(atomic_t *v); //原子变量自减
int atomic_sub_and_test(int i, atomic_t *v); //将原子变量减1并判断,为0返回真
int atomic_inc_and_test(int i, atomic_t *v); //自增后测试其是否为0,为0返回真
int atomic_add_return(int i, atomic_t *v); //将原子变量v加i,并且返回指向v的指针。
static atomic_t xxx_available = ATOMIC_INIT(1); //定义原子变量
static int xxx_open()
{
......
if(!atomic_dec_and_test(&xxx_available)) {
atomic_inc(&xxx_available);
return BUSY;
}
return 0; //成功
}
int release()
{
atomic_inc(&xxx_available); //释放设备
}
原子操作通常用于实现资源的引用计数,在TCP/IP协议栈的IP碎片处理中,就使用了引用计数。
自旋锁(在原地打转)
spinlock_t spin; //定义自旋锁
spin_lock_init(lock); //动态初始化自旋锁lock
spin_lock(lock); //获取自旋锁,能立即获得则返回真,否则自旋在那里直到其释放
spin_trylock(lock); //
获取自旋锁,能立即获得则返回真,否则返回假
spin_unlock(lock); //释放自旋锁,与spin_trylock或spin_lock配对使用
自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。另外自旋锁不允许任务睡眠(持有自旋锁的任务睡眠会造成自死锁——因为睡眠有可能造成持有锁的内核任务被重新调度,而再次申请自己已持有的锁)
int xxx_count = 0;
static int xxx_open(struct inode *inode, struct file *file)
{
spin_lock(&xxx_lock);
if(xxx_count)
{
spin_unlock(&xxx_lock);
retrun -EBUSY;
}
xxx_count++;
spin_unlock(&xxx_lock);
return 0;
}
static int xxx_release(struct inode *inode, struct file *file)
{
spin_lock(&xxx_lock);
xxx_count--;
spin_unlock(&xxx_lock);
return 0;
}
linux自旋锁和信号量所采用的“获取锁--访问临界区--释放锁”的方式存在于几乎所有的多任务操作系统之中。
自旋锁主要针对SMP或单CPU单内核课抢占的情况.
使用自旋锁需要谨慎,特别要注意以下几个问题:
自旋锁实际上是忙等锁,当锁不可用时,CPU 一直循环执行"测
试并设置" 直到可以取得该锁,CPU 在等待自旋锁时不做任何有用
的工作,仅仅是等待。因此,只有在占用锁时间极短的情况下,使
用自旋锁才是合理的。当临界区很大或有共享设备的时候,需要较
长时间占用锁,使用自旋锁会降低系统的性能。
自旋锁可能导致系统死锁。引发这个问题最常见的情况是递归使
用一个自旋锁,即如果一个已经拥有某个自旋锁的CPU 想第2 次
获得这个锁,则该CPU 死锁。此外,如果进程获得自旋锁后再阻
塞,也有可能导致死锁的发生。copy_from_user()、copy_to_user()
和kmalloc() 等函数都有可能引起阻塞,因此在自旋锁的占用期间
不能调用这些函数。
信号量
信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其他代码。当持有信号量的进程将信号利息量释放后,在等待队列的一个任务被唤醒,从而便可以获得这个信号量。
信号量与自旋锁动能类似,不同的是进程不会原地打转,而是进入休眠等待状态。
struct semaphore {
spinlock_t lock; //自旋锁结构体变量
unsigned int count; //用于记录数量
struct list_head wait_list; //内部链表结构体变量
};
互斥锁信号量:
void init_MUTEX(struct semaphore *sem); //将sem的值置为1
void init_MUTEX_LOCKED(struct semaphore *sem); //将sem的值置为0
DECLARE_MUTEX(sem); //定义初始化合二为一
void down(struct semaphore *sem);//获取信号量,如果大于或者等于0获取且sem-1,否则进入睡眠状态,且不可唤醒
int down_interruptible(struct semaphore *sem);//获取信号量,睡眠可以被唤醒
int down_trylock(struct semaphore *sem); //试图获取,如果不成功则立即返回
void up(struct semaphore *sem); //释放信号量