Linux内核的并发与竞态、信号量、互斥锁、自旋锁
/************************************************************************************
*本文为个人学习记录,如有错误,欢迎指正。
* https://blog.csdn.net/liu_sheng_1991/article/details/52291427
* https://blog.csdn.net/smallfish_love/article/details/50753932
* https://www.cnblogs.com/biaohc/p/6679195.html
* https://www.cnblogs.com/lonelyxmas/p/4287338.html?utm_source=debugrun&utm_medium=referral
************************************************************************************/
1. 并发与竞态
(1)并发:并发是指多个执行单元同时、并行被执行。
(2)竞态:并发的执行单元对共享资源(硬件资源和软件上的全局变量,静态变量等)的访问容易发生竞态。
(3)竞态发生的情况:
1)对称多处理器(SMP)的多个CPU:SMP是一种紧耦合、共享存储的系统模型,它的特点是多个CPU使用共同的系统总线,因此可以访问共同的外设和存储器。
2)单CPU内进程与抢占它的进程:Linux内核支持内核抢占,一个进程在内核执行的时候可能被另一个优先级高的进程打断,进程与抢占它的进程访问共享资源的情况类似于SMP的多个CPU。
3)中断(硬中断、软中断)与进程之间:中断可以打断正在执行的进程,如果中断处理程序访问进程正在访问的资源,竞态也会发生。中断也可能被新的更高优先级的中断打断,因此,多个中断之间也可能引起并发而导致竞态发生。
2. 竞态的解决方法
解决竞态的途径是保证对共享资源的互斥访问,所谓互斥访问是指一个执行单元在访问共享资源的时候,其他的执行单元是被禁止访问的。
访问共享资源的代码区域被称为临界区,临界区需要被以某种互斥机制加以保护。中断屏蔽、原子操作、自旋锁和信号量等是Linux设备驱动中可采用的互斥途径。
2.1 中断屏蔽
(1)基本概念
在单个CPU范围内避免竞态的一种简单省事的方法是在进入临界区之前屏蔽系统的中断。CPU一般具备屏蔽和打开中断的能力,这样可以保证正在执行的内核路径不被中断处理程序抢占,防止竞态条件的发生。具体而言,中断屏蔽使得中断与进程之间的并发不再发生,而且,由于Linux内核的进程调度等操作依赖中断来实现,内核抢占进程之间的并发得以避免。但是不能长时间屏蔽中断,因为在中断屏蔽期间,所有的中断得不到处理,有可能造成数据丢失和系统崩溃的可能。这就要求在中断屏蔽之后,当前的内核执行路径应当尽快的执行完临界区的代码。
(2)操作方法
Linux内核中提供以下API来实现中断屏蔽。
local_irq_disable(); //屏蔽中断 local_irq_enable(); //打开中断 local_irq_save(flags);//禁止中断并保存当前cpu的中断位信息
2.2 原子操作
原子操作是指在执行过程中不会被别的代码途径所中断的操作,分为整形原子操作和位原子操作。
2.3 信号量(进程级)
(1)基本概念
本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。它是对临界区保护的一种常用方法,他的使用方法和自旋锁差不多。与自旋锁相同只有得到信号量的进程才能执行临界区的代码。但是与自旋锁不同的是,当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。
(2)操作方法
Linux内核中提供以下API来操作信号量。
//1)定义信号量 Struct semaphore sem; //2)初始化信号量 void sema_init(struct semaphore *sem, int val); //初始化sem为val,当然还有系统定义的其他宏初始化,这里不列举 //3)获得信号量 void down(struct semaphore *sem); //获得信号量sem,其会导致睡眠,并不能被信号打断 int down_interruptible(struct semaphore *sem); //进入睡眠可以被信号打断 int down_trylock(struct semaphore *sem); //不会睡眠 //4)释放信号量 void up(struct semaphore *sem); //释放信号量,唤醒等待进程
2.4 互斥锁(进程级)
(1)基本概念
互斥锁是一种特殊的信号量。
(2)操作方法
Linux内核中提供以下API来操作互斥锁。
//1)定义互斥锁lock mutex_init(struct mutex* lock); //2)获取互斥锁 mutex_lock(struct mutex *lock); //3)释放互斥锁 mutex_unlock(struct mutex *lock);
2.5 自旋锁
(1)基本概念
自旋锁是一种典型的对临界资源进行互斥访问的手段,其名称来源于他的工作方式。为了获得一个自旋锁,在某CPU上运行的代码需要先执行一个原子操作,该操作测试并设置某个内存变量,由于是原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量。如果测试表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试表明自旋锁被占用,程序将在一个小的循环中重复这个“测试并设置”的操作,即所谓的“自旋”。当自旋锁的持有者通过重置该变量释放这个自旋锁之后,某个“等待的测试并设置”操作向其调用者报告锁已释放。
(2)操作方法
Linux内核中提供以下API来操作自旋锁。
//1)定义自旋锁 spinlock_t lock;
//2)初始化自旋锁 spin_lock_init(lock);
//3)获取自旋锁 spin_lock(lock); //获得自旋锁lock spin_trylock(lock);//尝试获取lock如果不能获得锁,返回假值,不在原地打转
//4)释放自旋锁 spin_unlock(lock); //释放自旋锁
P.S.:信号量与自旋锁的区别:
自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环查看是否该自旋锁的保持者已经释放了锁,"自旋"就是"在原地打转"。而信号量则引起调用者睡眠,它把进程从运行队列上拖出去,除非获得锁。
鉴于自旋锁与信号量的上述特点,一般而言,自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用;信号量适合于保持时间较长的情况,会只能在进程上下文使用。如果被保护的共享资源只在进程上下文访问,则可以以信号量来保护该共享资源,如果对共享资源的访问时间非常短,自旋锁也是好的选择。但是,如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。