一、需要了解的概念知识
概念1:临界区
(1)维基百科对临界区的定义:
在同步的程序设计中,临界区段(Critical section)指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。
当有线程进入临界区段时,其他线程或是进程必须等待(例如:bounded waiting 等待法),有一些同步的机制必须在临界区段的进入点与离开点实现,以确保这些共用资源是被互斥或的使用,例如:semaphore。
(2)形象点的解释:
对某一代码段A来说,在程序中可能被多次执行,把A的一次执行过程称为A的代码执行路径(简称代码路径)。
当两个或两个以上的代码路径要竞争共享资源(变量/缓冲区/设备)时,此时代码段A就是临界区。
概念2:互斥机制
前面我们知道,访问共享资源的代码区叫做临界区,这里的共享资源可能被多个线程需要,但这些共享资源又不能被同时访问,因此临界区需要以某种互斥机制加以保护,以确保共享资源被互斥访问。
我们可以编造一个故事,这样也方便大家理解:
很久很久以前,有一个神医,他的医术非常高明,以至于每个人都想亲自上门拜访他,找他看病。
但这人脾气特别不好,还有个特别的习惯,就是一次只能给一个病人看病,其他人想要得到他的医治必须得安安静静的排队,要是多一个傻逼闯进来,他就破罐破摔,撒手不干了。
于是,为了防止其他病人的干扰,让神医可以专心为眼前的病人医治,人们默认的排起队来,还给他提供了一种保护机制,免得一群愣头愣脑的傻逼突然闯进去触怒神医的天威,这种保护就叫做互斥机制。
概念3:用户空间与内核空间
为了安全考虑,Linux系统分为内核态和用户态,分别运行在内核空间和用户空间。内核态的程序可以执行特权指令,操作系统本身也在其中运行;用户态则不允许直接访问操作系统的核心数据、设备等关键资源,必须先通过系统调用或者中断进入内核态才可以访问,当系统调用或中断返回时,重新回到用户空间运行。
这就像皇帝(内核态)和平民(用户态),皇帝在上层社会(内核空间),手中握着帝国的大权,平民则安安分分的在底层世界(用户空间)努力工作。突然有一天,平民遇见了一件实在不得已只能拜托皇帝给予他特权才敢去做的大事,那他就需要上书请求觐见皇帝。
但是皇帝日理万机,怎么可能接受那么多平民的请求?!于是,这里皇帝设置了一种机制,就是平民要想觐见皇帝,必须要通过官员(系统调用和中断)的审核和批准,然后才被允许等待上殿觐见。
当然,等解决了问题之后,平民还是平民,还是要安安分分的回到底层世界(用户空间)努力工作。
二、Linux 互斥机制
如上所述,Linux 为保证共享资源的互斥访问,提供了一种互斥机制,这里的互斥机制有四种方式,分别是:中断屏蔽、原子操作、自旋锁及其各种衍生锁和信号量。
『用户空间的互斥方式:信号量
内核空间的互斥方式:中断屏蔽、原子操作、自旋锁及其各种衍生锁』
(1)信号量
在用户空间只有进程的概念。当一个临界区有多个用户态进程竞争时,最好的方法是用信号量保护这个临界区。只有得到信号量进程才能执行临界区代码,当获取不到信号量时,进程进入休眠状态。
因此,我们可以说,信号量是进程级的互斥机制,它代表进程来争夺共享资源,如果竞争失败,就会发生进程上下文切换,当前进程进入睡眠状态,CPU运行其他进程。由于进程上下文切换的开销很大,因此,只有当进程占用资源时间较长时,用信号量才是最好的选择。
此外,信号量在SMP(对称多处理器)系统同样起作用。
(2)中断屏蔽
中断是一个完全异步的事件,它的发生与正在运行的进程没有任何关系,它没有进程上下文切换。
CPU具备屏蔽中断和打开中断的功能,这项功能可以保证正在执行的内核执行路径不被中断处理程序抢占,防止竟态的产生。
但是,内核的正常运行依赖于中断机制。在屏蔽中断期间,任何中断都无法得到处理,而必须等待屏蔽解除。因此长时间屏蔽中断对内核的运行起到很大的影响,其后果可能导致数据丢失,甚至系统崩溃。
实际情况是:在中断服务全过程屏蔽中断会丢失中断;如果开中断,又容易引起互斥问题。为了解决这个问题,Linux 把中断分为前半段TH(Top Half)和后半段BH(Bottom Half)。TH 屏蔽中断,执行一些少量的关键性动作;BH 可以开中断,允许中断延迟执行。
(3)原子操作
指在执行过程中不会被别的代码路径所中断的操作,芯片级的原子操作一般都表现为一条汇编指令。
Linux 内核提供了两类函数来实现内核中的原子操作,分别是整型原子操作和位原子操作。它们的共同点是所有的操作都是原子的,内核可以安全的调用它们而不被中断,而且它们都依赖底层CPU的原子操作实现,因此所有的这些函数都是与CPU架构相关的。
(4)自旋锁
自旋锁是为实现保护共享资源而提出一种锁机制。
自旋锁的原理:一个执行单元要想访问被自旋锁保护的共享资源,必须先得到锁,并且在任何时刻最多只能有一个执行单元获得锁;而在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元保持该锁,那么将立即得到锁;如果在获取自旋锁时锁已经有保持者,那么获取锁操作将一直循环在那里,直到该自旋锁的保持者释放了锁,"自旋"一词就是因此而得名。
事实上,自旋锁的初衷是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁被持有的时间不应该过长。如果需要长时间锁定的话, 最好使用信号量。
三、信号量与自旋锁比较
信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用(_trylock的变种能够在中断上下文使用),而自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用。
如果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。当然,如果一定要使用信号量,则只能通过down_trylock()方式进行,不能获取就立即返回以避免阻塞。
自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。自旋锁只有在内核可抢占或SMP的情况下才真正需要,在单CPU且不可抢占的内核下,自旋锁的所有操作都是空操作。
信号量所保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区。因为阻塞意味着要发生进程上下文切换,而如果进程被切换出去后,另一个进程企图获得本自旋锁,就会造成死锁。