linux中的并发控制(1)

  1. 并发与竞态
  2. 编译乱序和执行乱序
    1. 编译乱序及编译屏障的运用
    2. 执行乱序及内存屏障

并发与竞态

并发指的是多个执行单元同时、并行执行,而并发的执行单元对共享资源的访问则很容易造成竞态。这样会导致执行顺序混乱或产生许多意想不到的结果。在设备驱动程序中,并发于竞态的问题时有发生。

  • 对称多处理器(SMP)

SMP是一种紧耦合、共享存储的系统模型,多个CPU单元具有同等的地位,共享同一个存储体。

在SMP中,多个核心上的进程是并发的,在不同核心上运行的进程于进程之间,进程与中断处理程序之间,中断处理与中断处理之间都存在竞态的可能。

  • 单核处理器

对于单核处理器来讲,若进程调度不可被抢占,则不存在竞态问题。但大多数实现中,都是可抢占式的进程调度,这样造成的结果就是,表现为了多个进程的并发执行。从而就造成了竞态问题。

不论是在SMP中还是在单核上,竞态发生的条件都是对共享资源的并发访问上,从上面的讨论我们知道在SMP和单核上进程的执行,中断的执行都具有并发性,因此在如今的体系结构和系统设计概念下,竞态看来是不可避免的。

因此解决竞态问题就变得尤为重要,解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥是指一个执行单元在访问共享资源时,禁止其它执行单元访问共享资源。

访问共享资源的代码区域称为临界区,临界区需要被以某种互斥机制加以保护。其中中断屏蔽、原子操作、自旋锁、信号量、互斥体等时linux中可采用的常用操作。

编译乱序和执行乱序

对于编译乱序与执行乱序问题,我们从如下问题来引出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct test {
int a;
int b;
int c;
};
struct test *gt = NULL;

void xxx_write(...) {
...
p = kmalloc(sizeof (*p), GFP_KERNEL);
p->a = 1;
p->b = 2;
p->c = 3;

gt = p;
...
}

void xxx_read(...) {
...
p = gt;
if (p != NULL) {
function(p->a, p->b, p->c);
}
...
}

如上代码从逻辑上看不存在任何问题,但真的没问题吗?

以上的逻辑在很多的程序开发中可能许多人都喜欢这样用,但是其中却包含着一些致命的缺陷会导致程序执行出错。其中可能的问题就是编译乱序和执行乱序。

编译乱序及编译屏障的运用

关于编译方面,c语言顺序的“p->a = 1;p->b = 2;p->c = 3;gt = p;”而言,经过编译后gt的赋值可能发生在a,b,c的赋值之前。这是编译器的乱序优化能力造成的。编译器可以对访存的指令进行乱序,减少逻辑上不必要的访存,以尽量提高Cache的命中率和CPU的LOAD/STORE的执行效率。因此,如果在打开编译优化的选项后,其生成的汇编及机器码是乱序的这是正常的。

解决编译乱序问题,可以使用编译屏障barrier()。在代码中设置屏障barrier()可以保证屏障前的语句和屏障后的语句不会乱序。

1
#define barrier() __asm__ __volatile__("": : :"memory")

其中编译屏障的使用方法如下:

1
2
3
4
5
6
7
8
p = kmalloc(sizeof (*p), GFP_KERNEL);
p->a = 1;
p->b = 2;
p->c = 3;

barrier();

gt = p;

关于编译乱序问题,C语言的volatile关键字的作用较弱,其主要作用更多的体现在避免内存访问行为的合并。

执行乱序及内存屏障

编译乱序是编译期间的问题,而执行乱序则是执行期间的问题。执行乱序是指即使编译得到的二进制指令的顺序符合预期的逻辑顺序,即“p->a = 1;p->b = 2;p->c = 3;gt = p;”。但当处理器执行时,后发射的指令仍可能先执行完,这是由于处理器的乱序执行策略造成的。

高级的CPU可以根据自身缓存的组织特性将访存指令重新排序执行。连续地址的访存可能先执行,因为这样做缓存命中率更高。有的还允许访存的非阻塞,即前一条指令因访存不命中,造成长时间的存储访问时,后面的访存指令可以先执行,以便从缓存中取数。因此从编译上看符合顺序的指令,从执行上来说顺序也是不可预知的。

在大多数体系结构中,每个CPU都时乱序执行的。但这对于单核的程序执行而言是不可见的,因为单个CPU在碰到依赖点时会等待。但在SMP架构下,这个在依赖点处的等待过程是不可见的。如在CPU0上执行如下过程:

1
2
while (f == 0);
print x

CPU1上执行如下过程:

1
2
x = 1;
f = 1;

这时在CPU0上的执行打印的结果就不一定是1。为了解决在多核处理器上一个核的内存行为对另外一个核可见的问题,常采用内存屏障技术。如下为ARM处理器的内存屏障指令。

  • 数据内存屏障(DMB): 在DMB之后的显示内存访问执行前,保证所有DMB之前的所有内存访问均完成
  • 数据同步屏障(DSB): 等待所有在DSB之前的指令完成(包括内存访问,缓存、分支预测和TLB维护操作)
  • 指令同步屏障(ISB): Flush流水线,保证ISB之后的指令都时从缓存或内存中获得

有了内存屏障技术,就可以保证在多核下的内存访存行为有一个统一的依赖点,在这个依赖点前与后的执行不会发生乱序。从这一点上来看,可以从最大限度地保留CPU乱序执行的能力,又能够保证乱序执行后对于最终结果的正确性,其中的关键就是在适当的依赖点处加入内存屏障。

对于linux设备驱动程序而言,不仅仅需要关心内存访存乱序导致的结果错误的问题,其中还包含I/O访存乱序所造成的结果错误,因此在linux中定义了读写屏障mb()、读屏障rmb()、写屏障wmb()、寄存器读屏障__iormb()、寄存器写屏障__iowmb()这样的屏障API和寄存器读写的API:readl_relaxed()和readl()、writel_relaxed()和writel()。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 yxhlfx@163.com

文章标题:linux中的并发控制(1)

本文作者:红尘追风

发布时间:2016-04-25, 12:02:01

原始链接:http://www.micernel.com/2016/04/25/linux%E4%B8%AD%E7%9A%84%E5%B9%B6%E5%8F%91%E6%8E%A7%E5%88%B6(1)/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录