1、并发与竞态
并发指的是多个执行单元同时、并行的被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量)的访问很容易导致竞态。
在Linux内核中,竞态主要发生在以下情况:
- 对称多处理器(SMP)的多个CPU
- SMP是一种紧凑的、共享存储的系统模型,特点是多个CPU使用共同的系统总线,因此可访问共同的外设和存储器。
- 在SMP的情况下,两个核(C0和C1)的竞态可能发生在C0的进程和C1的进程之间、C0的进程与C1的中断之间、C0的中断与C1的中断之间
- 单CPU内进程抢占它的进程
Linux2.6以后的内核支持内核抢占调度,一个进程在内核执行的时候可能耗完了自己的时间片,也可能被另一个高优进程打断,进程与抢占它的进程访问共享资源的情况类似于SMP的多个CPU。 - 中断(硬中断、软中断、Tasklet、底半部)与进程之间
中断可以打断正在执行的进程,如果中断服务程序访问进程正在访问的资源,就会发生竞态。
解决竞态问题的途径就是保证对共享资源的互斥访问。临界区资源需要被某种互斥机制加以保护,如中断屏蔽、原子操作、自旋锁、信号量、互斥体等。
2、编译乱序和执行乱序
现代编译器可以对访存的指令进行乱序,减少逻辑上不必要的访存,以及尽量提高Cache命中率和CPU的load/store单元的工作效率。
解决编译乱序问题,需要通过barrier()编译屏障进行。我们可以在代码中设置barrier()屏障,这个屏障可以阻挡编译器的优化。
乱序执行是CPU的行为,高级的CPU可以根据自己缓存的组织特性,将访存指令重新排序执行。连续访问的地址可能会先执行,因为这样缓存命中率会高,有的访问还允许访存的非阻塞,即如果前面一条指令因为缓存不命中,造成长延迟的存储访问时,后面的访存指令可以先执行,以便从缓存中取数,因此从汇编上看顺序正确的指令,其执行顺序也是不可预知的。
如下两条指令,有些处理器会进行优化:
LDR r0 [r1];
STR r2 [r3];
假设第一条LDR指令导致缓存未命中,这样缓存就会去填充行,并需要较多的周期才能完成,一些老的处理器会等待LDR执行完后才会执行STR指令。而有些处理器会自动识别下一条指令STR去执行,而不是等待LDR执行完成。
尽管每个CPU都是乱序执行,但是这一乱序对于单核程序是不可见的,因为单个CPU在碰到依赖点(后面的指令依赖于前面指令的执行结果)的时候会进行等待,所以程序员感觉不到这个乱序过程。但是这个依赖点等待的过程,对于其他Core是不可见的。
处理器为了处理多核间一个核的内存对另一个核可见的问题,引入了一些内存屏障的指令,如:
- DMB(数据内存屏障):在DMB之后的显示内存访问 执行前,保证所有在DMB指令之前的内存访问完成
- DSB(数据同步屏障):等待所有在DSB指令之前的指令完成(位于此指令前的所有显示内存访问均完成,位于此指令前的所有缓存、跳转预测和TLB维护操作全部完成)
- ISB(指令同步屏障):Flush流水线,使得所有ISB之后执行的指令都是从缓存或者内存中获得。
Linux内核的自旋锁、互斥体等互斥逻辑,需要用到上述指令:
在请求获得锁时,调用屏障指令
在解锁时,也需要调用屏障指令
3、中断屏蔽
在单CPU内避免竞态的最简单的问题就是在进入临界区之间就屏蔽系统的中断。
CPU一般支持关中断和开中断的功能,这样可以保证正在执行的内核执行路径不被中断处理程序抢占。
//屏蔽中断
//临界资源
//开中断
其底层原理就是让CPU不再响应中断,但是Linux的异步IO、进程调度等很多重要操作都依赖于中断,所以中断对于内核的运行非常重要,在屏蔽中断期间所有的中断都无法得到处理,因此长时间屏蔽中断是很危险的,有可能造成数据丢失或者系统崩溃等问题。
4、原子操作
原子操作可以保证对一个整型数据的修改是排他性的。Linux内核提供了一系列的函数来实现内核中的原子操作。
这些函数又分为两类,分别针对位和整型变量进行原子操作。
位和整型变量的原子操作都依赖于底层CPU的原子操作,因此与CPU的具体架构有关。