热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

7.2编译乱序和执行乱序理解Linux内核的锁机制,需要理解编译器和处理器的特点。如下面一段代码,写的一端申请一个新的structfoo结构体并初始化其中的a、b、c,之后把结构体地

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可以根据自己缓存的组织特性,将访存指令重新排序执行。连续地址的访问可能会先执行,因为这样缓存命中率高。有的还允许访存的非阻塞,即如果前面一条访存指令因为缓存不命中,造成长延时的存储访问时,后面的访存指令可以先执行,以便从缓存中取数。即使是从汇编上看顺序正确的指令,其执行的顺序也是不可预知的。


推荐阅读
  • eclipse学习(第三章:ssh中的Hibernate)——11.Hibernate的缓存(2级缓存,get和load)
    本文介绍了eclipse学习中的第三章内容,主要讲解了ssh中的Hibernate的缓存,包括2级缓存和get方法、load方法的区别。文章还涉及了项目实践和相关知识点的讲解。 ... [详细]
  • 深入理解Java虚拟机的并发编程与性能优化
    本文主要介绍了Java内存模型与线程的相关概念,探讨了并发编程在服务端应用中的重要性。同时,介绍了Java语言和虚拟机提供的工具,帮助开发人员处理并发方面的问题,提高程序的并发能力和性能优化。文章指出,充分利用计算机处理器的能力和协调线程之间的并发操作是提高服务端程序性能的关键。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • 解决Sharepoint 2013运行状况分析出现的“一个或多个服务器未响应”问题的方法
    本文介绍了解决Sharepoint 2013运行状况分析中出现的“一个或多个服务器未响应”问题的方法。对于有高要求的客户来说,系统检测问题的存在是不可接受的。文章详细描述了解决该问题的步骤,包括删除服务器、处理分布式缓存留下的记录以及使用代码等方法。同时还提供了相关关键词和错误提示信息,以帮助读者更好地理解和解决该问题。 ... [详细]
  • 本文整理了Java中com.evernote.android.job.JobRequest.getTransientExtras()方法的一些代码示例,展示了 ... [详细]
  • python中安装并使用redis相关的知识
    本文介绍了在python中安装并使用redis的相关知识,包括redis的数据缓存系统和支持的数据类型,以及在pycharm中安装redis模块和常用的字符串操作。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 本文介绍了在Oracle数据库中创建序列时如何选择cache或nocache参数。cache参数可以提高序列的存取速度,但可能会导致序列丢失;nocache参数可以避免序列丢失,但在高并发访问时可能导致性能问题。文章详细解释了两者的区别和使用场景。 ... [详细]
  • Java中包装类的设计原因以及操作方法
    本文主要介绍了Java中设计包装类的原因以及操作方法。在Java中,除了对象类型,还有八大基本类型,为了将基本类型转换成对象,Java引入了包装类。文章通过介绍包装类的定义和实现,解答了为什么需要包装类的问题,并提供了简单易用的操作方法。通过本文的学习,读者可以更好地理解和应用Java中的包装类。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 本文介绍了Codeforces Round #321 (Div. 2)比赛中的问题Kefa and Dishes,通过状压和spfa算法解决了这个问题。给定一个有向图,求在不超过m步的情况下,能获得的最大权值和。点不能重复走。文章详细介绍了问题的题意、解题思路和代码实现。 ... [详细]
  • 全面介绍Windows内存管理机制及C++内存分配实例(四):内存映射文件
    本文旨在全面介绍Windows内存管理机制及C++内存分配实例中的内存映射文件。通过对内存映射文件的使用场合和与虚拟内存的区别进行解析,帮助读者更好地理解操作系统的内存管理机制。同时,本文还提供了相关章节的链接,方便读者深入学习Windows内存管理及C++内存分配实例的其他内容。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
author-avatar
小男生2502863203
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有