linux系统中存在大量的并发来源。这个并发就是多个执行单元同时被执行。这个同时并不是精确的指同一时刻,而是同一时间间隔。 举个并发例子,在linux系统里你可以同时听歌和浏览网页。
并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(race condition)。
竞态(竞争状态)的分类:
1.对称多处理器(SMP)的多个CPU
SMP是一种紧耦合、共享存储的系统类型,因为多个CPU同时共享系统总线,因此可以访问共同的外设和存储器
SMP结构示意图:
2.单CPU内进程与抢占它的进程
Linux2.6支持内核抢占调度,一个进程在内核执行的时候可能被另一个高优先级的进程
打断,进程与抢占它的进程访问共享资源时会发生竞争
3.中断(硬中断、软中断、Tasklet、底半部)与进程之间
中断可以打断重在执行的进程、如果中断处理程序访问进程正在访问的资源、此时静态会发生
此外中断被更高级的中断打断时也会发生
而对于竞态的解释就是指两个或多个进程对共享的数据进行读或写的操作时,最终的结果取决于这些进程的执行顺序。 但竞态会造成我们不希望看到的结果,所以我们需要一些策略来解决竞态,即保证对共享资源的互斥访问(即一个执行单元在访问共享资源时,其他的执行单元被禁止访问)
临界资源:系统中同时存在有许多进程,它们共享各种资源,然而有许多资源在某一时刻只能允许一个进程使用。例如打印机、磁带机等硬件设备和变量、队列等数据结构,如果有多个进程同时去使用这类资源就会造成混乱。因此必须保护这些资源,避免两个或多个进程同时访问这类资源。我们把某段时间内只能允许一个进程使用的资源称为临界资源
互斥就是进程之间互相排斥使用临界资源。
访问共享资源的代码区域被称为临界区(critical sections),临界区需要被以某种
互斥机制加以保护。
Linux常见互斥机制:中断屏蔽、原子操作、自旋锁和信号量等是Linux设备驱动中科采用的互斥路径
中断屏蔽(可以保证正在执行的内核执行路径不被中断处理程序抢占,由于Linux内核的进程调度都依赖中断来实现,内核抢占进程之间的竞态就不存在了)
使用方法: local_irq_disable() //屏蔽中断 说明:local_irq_disable()和local_irq_enable()都只能禁止和使能本CPU内的中断
…. 并不能解决SMP多CPU引发的竞争。
critical section //临界区
….
local_irq_enable() //开中断
与local_irq_disable()不同,local_irq_save(flags)除了进行禁止中断操作以外,还保证目前CPU的中断位信息,local_irq_save(flags)进行相反的操作。
致命弱点: 由于Linux系统的异步I/O,进程调度等很多重要操作都依赖于中断,在屏蔽中断期间所有的中断都无法处理,因此长时间屏蔽中断是很危险的,有可能造成数据丢失甚至系统奔溃。
原子操作:执行过程不能被别的代码路径中断的操作就是原子操作。
1:整型原子操作。
有时共享的资源可能恰好是一个简单的整数值。而完整的锁机制对一个简单的整数却又显得浪费。针对这种情况,内核提供了一种原子的整数类型atomic_t
定义在<asm/atomic.h>中。
下面针对这种类型的操作在SMP计算机的所有处理器上都确保是原子的。这种操作速度非常快,因为只要有可能,它们就会被编译成单个的机器指令。
(1)设置原子变量的值
void automic_set(atomic_t *v,i);将原子变量v的值设置为整数值i.或者利用ATOMIC_INIT宏来初始化原子变量的值。
atomic_t v=ATOMIC_INIT(x);定义原子变量并将其值初始化为X。
int atomic_read(atomic_t *v); 返回v的当前值。
void atomic_add(int i, atomic_t *v),将i的值累加到v指向的原子变量。
void atomic_sub( int i ,atomic_t *v );从*v中减去i;
void atomic_inc(atomic_t *v);增加一个原子量
void atomic_dec(atomic_t *v);缩减一个原子量
int atomic_inc_and_test(atomic_t *v);增加,操作结束后,自动判断原子质值为0 则返回值为true,否则返回值false。
int atomic_dec_and_test(atomic_t *v );减少操作结束后,自动判断原子质值为0 则返回值为true,否则返回值false。
int atomic_sub_and_test(int i, atomic_t *v);减去i操作结束后,自动判断原子质值为0 则返回值为true,否则返回值false。
int atomic_add_negative(int i,atomic_t *v);讲整数变量累加到v,返回值在结果为负时为true,否则为false.
操作并返回
int atomic_add_return(int i,atomic_t *v); //这些操作对原子变量进行对应操作,并返回新的值。
int atomic_sub_return(inti, atomic_t *v);
int atomic_inc_return(atomic *v);
int atomic_dec_return(atomic_t *v);
2:
位原子操作
atomic_t对整数操作比较有用,但当需要以原子的形式操作单个的位时这种类型就派不上用场了,因此内核提供了一组可原子的修改和测试单个位的函数。
void set_bit(nr,*addr);设置addr指向的数据的第nr位
vooid clear_bit(nr,*addr);清除addr指向的数据的第nr位
void change_bit(nr,void *addr)切换指定的位;
void test_bit(nr,void *addr);
void test_and_set_bit(nr, void *addr);
void test_and_change_bit(nr,void* addr);
void test_and_clear_bit(nr,void *addr);.
光说不练,不是好汉。这谁说的呢,咋就是记不得呢,看段代码:
static atomic_t ato_avi = ATOMIC_INIT(1);//定义原子变量
staticint ato_open(struct inode *inode,struct file *filp)
{
... if (!atomic_dec_and_test(&ato_avi))
{ atomic_inc(&ato_avi);
return = - EBUSY;//已经打开
}
.. return 0;//已经打开
}
staticint ato_release(struct inode *inode,struct file *filp)
{
atomic_inc(&ato_avi);
return 0;
}
3 :自旋锁
和信号量不同,自旋锁可以在不能休眠的代码中使用,比如中断处理例程。
正如其名,CPU上将要执行的代码将会执行一个测试并设置某个内存变量的原子操作,若测试结果表明锁已经空闲,则程序获得这个自旋
锁继续运行;若仍被占用,则程序将在一个小的循环内重复测试这个"测试并设置"的操作.这就是自旋。
使用方法:1)spinlock_t spin; //定义自旋锁
2)spin_lock_init(lock); //初始化自旋锁
3)spin_lock(lock); //成功获得自旋锁立即返回,否则自旋在那里直到该自旋锁的保持者释放
spin_trylock(lock); //成功获得自旋锁立即返回真,否则返回假,而不是像上一个那样"在 原地打转"
4)spin_unlock(lock);//释放自旋锁
自旋锁一般像下边这样使用:
spinlock_t lock;
spin_lock_init(&lock);
spin_lock (&lock);
....//临界区
spin_unlock(&lock);
利用spin_lock()/spin_unlock()作为自旋锁的基础,将它们和关中断local_irq_disable()/开中断local_irq_enable(),关底半部local_bh_disable()/开底半部local_
bh_enable(),关中断并保存状态字local_irq_save()/开中断并恢复状态local_irq_restore()结合就完成了整套自旋锁机制。
唉吆,我的天啊,不是我说你们开源社区的那些家伙们,说个东西为啥要那么费劲,就为了说完上面那些红色的破话,差点没喘过来,本来在上篇就想说你们的…
好人做到低,送你送到西,嘿嘿..我把上边的关系再帮大家捋捋,免的看着费劲…
spin_lock_irq() = spin_lock() + local_irq_disable() spin_unlock_irq = spin_unlock() + local_irq_enable() spin_lock_irqsave() = spin_unlock() + 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()
又是一口气,这是什么年头,挣点点击率,怎么就这么难呢..不过也是没办法的事,上了Linux这条贼船,就要有牺牲我一个,幸福全中国的决心,不然微软的又要嚣张了,今天黑你一次屏,明天断你一次网,就连去网吧,微软还伸手到你面前说:对不起,你的系统是盗版…
好了,让他们热闹去吧,说说咱们的事。作为Linux驱动程序工程师,你要在心里刻下几条戒律:
1)什么叫自旋锁,就是忙等待,当锁不可用时,CPU除了在那儿拼命的执行"测试并设置"的傻瓜操作外什么都不做,才不管电影中含情脉脉的你是她的谁,她是你的谁的这些事,任你两情相约,也是执手相看泪眼,竟无语凝咽。可见,这是多么的影响系统的性能。
2)what?你不懂爱情,不在乎第一条,我晕…那就给你来个狠的:处理不好自旋锁可能导致系统死锁(dead lock),系统瘫痪。呵呵怕不,等你哭着闹着要上网而不能时,就怕了。那为啥会这样了,很简单,想想:如果我们不小心在一个递归中使用一个自旋锁,说白了就是一个CPU,它已经获得了这个自旋锁,可还贪心地想第二次获得这个自旋锁,这时就死锁了呗。另外,如果一个进程获得自旋锁之后再阻塞,也是很有可能导致死锁的发生。
理论完了,给你来点代码,就当是程序员点的一点交代吧:
int device_count = 0; 定义文件打开的次数计数 static int device_open(struct inode *inode, struct file *filp) { ... spinlock(&device_count); if(device_count) //已经打开 { spin_unlock(&device_count); return -EBUSY; } device_count++; //增加使用计数 spin_unlock(&device_count); ... return 0; } static int device_release(struct inode *inode, struct file *filp) { ... spinlock(&device_count ); device_count--; //减少使用计数 spin_unlock(&device_count ); return 0; }