原子操作和内存屏障

 

若干汇编语言指令具有”读—修改—写”类型----也就是说,它们访问存储器单元两次,第一次读原值,第二次写新值。

假定运行在两个CPU上的两个内核控制路径试图通过执行非原子操作来同时” 读—修改—写”同一存储器单元。首先,两个CPU都试图读同一单元,但是存储器仲裁器插入,只允许其中的一个访问而让另一个延迟。然而,当第一个读操作已经完成后,延迟的CPU从那个存储器单元正好读到同一个值。然后,两个CPU都试图向那个存储器单元写一新值,总线存储器访问再一次被存储器仲裁器串行化,最终,两个写操作都成功。但是,全局的结果是不对的,因为两个CPU写入同一值。因此,两个交错的” 读—修改—写”操作成了一个单独的操作。

避免由于” 读—修改—写”指令引起的竞争条件是最容易的办法,就是确保这样的操作在芯片级是原子的。任何一个这样的操作都必须以单个指令执行,中间不能中断,且避免其它的CPU访问同一存储器单元。这些很小的原子操作可以建立在其它更灵活机制的基础之上以创建临界区。

让我们根据那个分类来回顾一下80x86的指令:

(1)    进行零次或一次对齐内存访问的汇编指令是原子的。

(2)    如果在读操作之后、写操作之前没有其它处理器占用内存总线,那么从内存中读取数据、更新数据并把更新后的数据写回内存中的这些” 读—修改—写”汇编语言指令是原子的。当然,在单处理器系统中,永远都不会发生内存总线窃用的情况。

(3)    操作码前缘是lock字节的” 读—修改—写”汇编语言指令即使在多处理器系统中也是原子的。当控制单元检测到这个前缀时,就”锁定”内存总线,直到这条指令执行完成为止。因此,当加锁的指令执行时,其它处理器不能访问这个内存单元。

(4)    操作码前缀是一个rep字节的汇编语言指令不是原子的,这条指令强行让控制单元多次重复执行相同的指令。控制单元在执行新的循环之前要检查挂起的中断。

在你编写C代码程序时,并不能保证编译器会为a=a+1或甚至像a++这样的操作使用一个原子指令。因此,Linux内核提供了一个专门的atomic_c类型和一些专门的函数和宏(参见表5-4),这些函数和宏作用于atomic_t类型的变量,并当作单独的、原子的汇编语言指令来使用。在多处理器系统中,每条这样的指令都有一个lock字节的前缀。

优化和内存屏障

当使用优化的编译器时,你千万不要认为指令会严格按它们在源代码中出现的顺序执行。例如,编译器可能重新安排汇编语言指令以寄存器以最优的方式使用。此外,现代CPU通常并行地执行若干条指令,且可能重俗人安排内存访问。这种重新排序可以极大地加速程序的执行。

然而,当处理同步时,必须避免指令重新排序。如果放在同步原语之后的一条指令在同步原语之前执行,事情很快就会变得失控。事实上,所有的同步原语起优化和内存屏障的作用。

优化屏障原语保证编译程序不会混淆放在原语操作之前的汇编语言指令和放在原语操作之后的汇编语言指令,这些汇编语言指令在C中都有对应的语句。在Linux中,优化屏蔽就是barrier()宏,它展开为asm volatile(“”:::”memory”)。指令asm告诉编译程序要插入汇编语言片段。volatile关键字禁止编译器把asm指令与程序中的其它指令重新组合。memory关键字强制编译器贫富RAM中的所有内存单元已经被汇编语言指令修改;因此,编译器不能使用存放在CPU寄存器中的内存的值来优化asm指令前的代码。注意,优化屏障并不保证不使当前CPU把汇编语言指令混在一起执行------这是内存屏障的工作。

内存屏障原主确保,在原语之后的操作开始执行之前,原语之前的操作已经完成。因此,内存屏障类似于防火墙,让任何汇编语言指令都不能通过。

在80x86处理器中,下列种类的汇编语言指令是”串行的”,因为它们起内存屏障的作用:

(1)    对I/O端口进行操作的所有指令。

(2)    有lock前缘的所有指令。

(3)    写控制寄存器、系统寄存器或调试寄存器的所有指令。

(4)    在Pentium 4微处理器中引入的汇编语言指令lfence,sfence和mfence,它们分别有效地实现读内存屏障、写内存屏障和读-写内存屏障。

(5)    少数专门的汇编语言指令,终止中断处理程序或异常处理程序的iret指令就是其中的一个。

Linux使用六个内存屏障原语,如表5-6所示。这些原语也被当作优化屏障,因为我们必须保证编译程序不在屏障前后移动汇编语言指令。”读内存屏障”仅仅作用于从内存读的指令,而”写内存屏障”仅仅作用于写内存的指令。内存屏障既用于多处理器系统,也用于单处理器系统。当内存屏障应该防止仅出现于多处理器系统上的竞争条件时,就使用smp_xxx()原语;在单处理器系统上,它们什么也不做。其它的内存屏障防止出现在单处理器和多处理器系统上的竞争条件。

内存屏障原语的实现依赖于系统的体系结构。在80x86微处理器上,如果CPU支持lfence汇编语言指令,就把rmb()宏展开为asm volatile(“lfence”),否则就展开为asm volatile(“lock;adl $0,0(%%esp)”:::”memory”)。asm指令告诉编译器插入一些汇编语言指令并起优化屏障的作用。“lock;adl $0,0(%%esp)汇编指令把0加到栈顶的内存单元;这条指令本身没有价值,但是,lock前缀使得这条指令成为CPU的一个内存屏障。

Intel上的wmb()宏实际上更简单,因为它展开为barrier()。这是因为Intel处理器从不对写内存访问重新排序,因此,没有必要在代码中插入一条串行化汇编指令。不过,这个宏禁止编译器重新组合指令。

注意,在多处理器系统上,在前一节”原子操作”中描述的所有原子操作都起内存屏障的作用,因为它们使用了lock字节。

你可能感兴趣的:(优化,汇编,存储,语言,编译器,linux内核)