Linux设备驱动中的并发控制之二(编译乱序和执行乱序)

7.2 编译乱序和执行乱序

理解Linux内核的锁机制,需要理解编译器和处理器的特点。如下面一段代码,写的一端申请一个新的struct foo结构体并初始化其中的a、b、c,之后把结构体地址赋值给全局gp指针:

struct foo {
    int a;
    int b;
    int c;
};

struct foo *gp = NULL;

/* . . . */
p = kmalloc(sizeof(struct foo), GFP_KERNEL);
p->a = 1;
p->b = 2;
p->c = 3;
gp = p;

而读的一端如果简单做如下处理,则程序的运行可能是不符合预期的:

p = gp;
if (p != NULL) {
    do_something_with(p->a, p->b, p->c);
}
有两种可能的原因会造成程序出错,一种可能性是编译乱序,另外一种可能性是执行乱序

编译方面,C语言顺序的“p->a=1;p->b=2;p->c=3;gp=p;”的编译结果的指令顺序可能是gp的赋值指令发生在a、b、c的赋值之前。现代的高性能编译器在目标码优化上都具备对指令进行乱序优化的能力。编译器可以对访存(访问内存)的指令进行乱序,减少逻辑上不必要的访存,以及尽量提高Cache命中率和CPU的Load/Store单元的工作效率。因此在打开编译器优化以后,看到生成的汇编码并没有严格按照代码的逻辑顺序,这是正常的。
解决编译乱序问题,需要通过barrier()编译屏障进行。可以在代码中设置barrier()屏障,这个屏障可以阻挡编译器的优化。对于编译器来说,设置编译屏障可以保证屏障前的语句和屏障后的语句不乱“串门”。
比如,下面的一段代码在e=d[4095]与b=a、c=a之间没有编译屏障:

int main(int argc, char *argv[])
{
        int a = 0, b, c, d[4096], e;

        e = d[4095];

        //......

        b = a;
        c = a;
        printf("a:%d b:%d c:%d e:%d\n", a, b, c, e);
        return 0;
}

用“arm-linux-gnueabihf-gcc - O2”优化编译,反汇编结果是:int main(int argc, char *argv[])
{
        831c: b530 push {r4, r5, lr}
        831e: f5ad 4d80 sub.w sp, sp, #16384 ; 0x4000
        8322: b083 sub sp, #12
        8324: 2100 movs r1, #0
        8326: f50d 4580 add.w r5, sp, #16384 ; 0x4000
        832a: f248 4018 movw r0, #33816 ; 0x8418
        832e: 3504 adds r5, #4
        8330: 460a mov r2, r1 -> b= a;
        8332: 460b mov r3, r1 -> c= a;

        8334: f2c0 0000 movt r0, #0
        8338: 682c ldr r4, [r5, #0]
        833a: 9400 str r4, [sp, #0] -> e = d[4095];
        833c: f7ff efd4 blx 82e8 <_init+0x20>
}

显然,源代码级别b=a、c=a发生在e=d[4095]之后,但目标代码的b=a、c=a指令发生在e=d[4095]之前。

假设重新编写代码,在e=d[4095]与b=a、c=a之间加上编译屏障:

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

int main(int argc, char *argv[])
{
        int a = 0, b, c, d[4096], e;

        e = d[4095];

        //......

        b = a;
        c = a;
        printf("a:%d b:%d c:%d e:%d\n", a, b, c, e);
        return 0;

}

再次用“arm-linux-gnueabihf-gcc - O2”优化编译,反汇编结果是:

int main(int argc, char *argv[])
{
        831c: b510 push {r4, lr}
        831e: f5ad 4d80 sub.w sp, sp, #16384 ; 0x4000
        8322: b082 sub sp, #8
        8324: f50d 4380 add.w r3, sp, #16384 ; 0x4000
        8328: 3304 adds r3, #4
        832a: 681c ldr r4, [r3, #0]
        832c: 2100 movs r1, #0
        832e: f248 4018 movw r0, #33816 ; 0x8418
        8332: f2c0 0000 movt r0, #0
        8336: 9400 str r4, [sp, #0] -> e = d[4095];
        8338: 460a mov r2, r1 -> b= a;
        833a: 460b mov r3, r1 -> c= a;

        833c: f7ff efd4 blx 82e8 <_init+0x20>
}

因为“__asm__ __volatile__("":::"memory")”这个编译屏障的存在,原来的3条指令的顺序“拨乱反正”了。

关于解决编译乱序的问题,C语言volatile(易变的、可变的)关键字的作用较弱,它更多的只是避免内存访问行为的合并,对C编译器而言,volatile是暗示除了当前的执行线索以外,其他的执行线索也可能改变某内存,所以它的含义是“易变的”。如果线程A读取var这个内存中的变量两次而没有修改var,编译器可能觉得读一次就行了,第2次直接取第1次的结果。如果加了volatile关键字来修饰var,告诉编译器线程B、线程C或者其他执行实体可能把var改掉了,因此编译器不会再把线程A代码的第2次内存
读取优化掉了。另外,volatile也不具备保护临界资源的作用。

编译乱序是编译器的行为,执行乱序是处理器运行时的行为。执行乱序是指即便编译的二进制指令的顺序按照“p->a=1;p->b=2;p->c=3;gp=p;”排放,在处理器上执行时,后发射的指令还是可能先执行完,这是处理器的“乱序执行”策略。高级的CPU可以根据自己缓存的组织特性,将访存指令重新排序执行。连续地址的访问可能会先执行,因为这样缓存命中率高。有的还允许访存的非阻塞,即如果前面一条访存指令因为缓存不命中,造成长延时的存储访问时,后面的访存指令可以先执行,以便从缓存中取数。即使是从汇编上看顺序正确的指令,其执行的顺序也是不可预知的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值