驱动开发之始,(三)并发与竞态,信号量与自旋锁

 

1.并发与竞态

单处理器结构中,各个进程从宏观上看是并行的,因为处理器执行速度非常快,但从微观上看进程是串行执行,因为在一个时刻只有一个进程能被处理器运行,称为并发。换句话说,在一段时间内,处理器能够同时做多件事。举个例子,A进程先在CPU上得到执行,突然被B进程打断(硬件中断或拥有更高优先级),CPU此刻暂时放下A进程,转而为B进程服务,当B进程服务完成后,在接着执行A进程,从这一段时间来看,CPU同时做了两件事,服务了A和B。

支持对称多处理SMP(Symmertric Multi Processing)结构中情况就变得复杂的多,多个用户空间的进程可以同时运行在不同的处理器上,SMP系统甚至可在不同的处理器上同时执行我们的代码,内核是可抢占的,驱动程序在任何时刻都有可能失去对处理器的使用权,而在处理器上运行的进程可能正在调用这段驱动程序。设备中断是异步事件,也会导致并发执行。(后续章节中会讲到异步事件)

并发将有可能导致竞态的出现。竞态会导致对共享资源(临界资源)的非法访问,当两个执行线程(正在运行代码的任意上下文)需要访问相同的数据结构或硬件资源时,产生非预期的结果。下面看一个因为并发而产生竞态的例子,在驱动程序中有这么一段代码

if( !ptrace->data[pos] )
{
    ptrace->data[pos] = kmalloc(size, flag); 
    if( !ptrace )
    {
        goto somewhere;
    }
}

假设有两个进程A、B都在调用这个驱动,在同一时刻到达第一个if语句,且指针都是NULL,每个进程都会等到一个分配到的内存地址,因为两个指针都对同一位置赋值,此时只能有一个进程赋值成功!结果是第二个完成赋值的进程“胜出”。如果A进程先赋值,那么B进程将覆盖A的操作,因此A分配的内存将会丢失,永远也不会返回到系统中。该竞态带来的结果是内存泄露。

资源共享的硬规则:在单个执行线程之外共享硬件过软件资源的任何时候,因为另外一个线程可能产生对该资源的不一致观察,因此必须显示地管理对该资源访问。使得代码要么看到已经分配好的内存,要么知道内存还没分配或将要由其他人分配。在示例中,从进程B的角度所看到的数据是不一致的,就是说,B进程不知道进程A已经为共享的设备分配了内存,因此B自己会进行分配,并覆盖A的操作。对于多进程并发执行,需要按照一定的规则和顺序对共享资源进行访问。

访问管理常见的技术“锁”或“互斥”——确保一次只有一个执行线程可操作共享资源。

2.信号量与自旋锁

从上面的示例中知道,在对临界资源进行访问时,任意时刻只能被一个线程执行,称为“互斥”(Mutex)访问。避免多个进程同时在一个临界区中运行。

一个信号量(semaphore)本质上是一个整数值,它和一堆函数P和V联合使用。试图进入临界区的进程在相关信号量上调用P,如果信号量的值大于零,则该值会减1,而进程可继续,如果信号量的值小于或等于零,进程必须等待直到其他进程释放该信号量;如果进程完成对临界资源的访问,通过调用V完成信号量的释放,增加信号量的值。互斥是信号量的值为1的情况。

在内核源码<asm/semaphore.h>中,struct semaphore,通过sema_init创建信号量

void sema_init(struct semaphore *sem, int val);

P函数称为down,指该函数减小了信号量的值,它也许会将调用者置于休眠状态,然后等待信号量变得可用,之后授予调用者对被保护资源的访问。

void down(struct semaphore *sem);
int __must_check down_interruptible(struct semaphore *sem);
int __must_check down_trylock(struct semaphore *sem);

down减小信号量的值,并在必要时一直等待。

down_interruptible完成相同的工作,但操作是可中断的,它允许等待在某个信号量上的用户空间进程可被用户中断。使用时需要小心,如果操作被中断,该函数会返回非零值,而调用者不会拥有该信号量,正确使用需要时钟检查返回值,并作出相应的相应。

down_trylock永远不会休眠,如果信号量在调用时不可获得,down_trylock会立即返回一个非零值。

当一个线程成功调用上述down的某个版本后,就称该线程“拥有”(或叫获得)了该信号量,那么该线程就被赋予访问由该信号量保护的临界区的权利。

当互斥操作完成后,必须释放该信号量,调用V函数,即up

void up(struct semaphore *sem)

调用up之后,调用者不再拥有该信号量,任何拿到信号量的线程都必须通过一次(只有一次)对up的调用而释放该信号量。在出现错误情况下,如果在拥有一个信号量时发生错误,必须在将错误状态放回给调用者之前释放该信号量。

3.自旋锁API

一个自旋锁(spinlock)是一个互斥设备,它只能有两个值“锁定”和“解锁”,它通常实现为某个整数值中的单个位,如果锁可用,则“锁定”位被设置,代码继续进入临界区;如果锁被其他进程获得(锁不可用),则代码进入忙循环并重复检查这个锁,直到锁可用为止,这个循环就是自旋锁的“自旋”部分,等待执行忙循环的处理器做不了任何有用的工作。

自旋锁需要包含的文件是<linux/spinlock.h>,锁具有spinlock_t类型,一个自旋锁必须被初始化,在编译时

spinlock_t my_lock = SPIN_LOCK_UNLOCKED;

或调用下面的宏

#define spin_lock_init(_lock)

在进入临界区之前,代码必须调用下面的函数获得需要的锁:

void spin_lock(spinlock_t *lock)

注意,所有的自旋锁等待在本质上都是不可中断的,一旦调用了spin_lock,在获得锁之前将一直处于自旋状态。

释放锁后启用中断;

void spin_lock_irq(spinlock_t *lock)

在获得锁之前禁止中断,并把先前的中断状态保存在flags中

void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);	

如果不在硬件中断例程中访问自旋锁,但可能在软件中断中访问,则应该使用spin_lock_bh,以便在安全地避免死锁的同时还能服务硬件中断。

void spin_lock_bh(spinlock_t *lock)

释放已经获得的锁,可调用:

void spin_unlock(spinlock_t *lock)

4.自旋锁与原子上下文

假定驱动程序获得了一个自旋锁,然后进入临界区,在这个过程中,有更高优先级的进程抢占了CPU,使得被抢占的进程进入休眠,而这段代码拥有这个自旋锁,如果其他某个线程试图获得相同的锁,可能使得试图获得锁的进程等待很长时间,甚至,系统将进入死锁状态。

因此,适用于自旋锁的核心规则:任何拥有自旋锁的代码都必须是原子的。它不能休眠,不能因为任何原因放弃处理器。只要内核代码拥有自旋锁,在相关处理器上的抢占就会被禁止。谨记拥有锁的时间越短越好。

我们来看一个拥有锁的进程,可以被中断时的情形:某一驱动程序正在被运行,并且已经获得了一个锁,拥有了对设备访问权限,这时产生了一个硬件中断,它将导致中断处理例程被调用,而中断例程恰好要对该设备进行访问,从而试图来获得相应的锁,中断例程运行在当初拥有锁进程所在的处理器上时,在中断例程自旋时,拥有锁的进程又没有机会来释放这个锁,处理器将会自旋下去。

本章只是给出了信号量和锁的一些核心概念,更多细节和知识,在以后使用中再进行分析。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值