我们都知道,带有优化的编译器,会尝试重新排序汇编指令,以提高程序的执行速度。但是,当在处理同步问题的时候,重新排序的指令应该被避免。因为重新排序可能会打乱我们之前想要的同步效果。其实,所有的同步原语都可以充当优化和内存屏障。
然后看看标准C++基金会(https://isocpp.org)怎么说的(官方链接):
" 处理器内存屏障 “ 针对 ” CPU " 之间的内存访问乱序 和 CPU 访问外设乱序 问题 ;
由以下博客的分析可以知道,内核的kfifo使用了很多技巧以实现其高效性。比如,通过限定写入的数据不能溢出和内存屏障实现在单线程写单线程读的情况下不使用锁。因为锁是使用在共享资源可能存在冲突的情况下。还用设置buffer缓冲区的大小为2的幂次方,以简化求模运算,这样求模运算就演变为 (fifo->in & (fifo->size – 1))。通过使用unsigned int为kfifo的下标,可以不用考虑每次下标超过size时对下表进行取模运算赋值,这里使用到了无符号整数的溢出回零的特性。由于指示读写指针的下标一直在增加,没有进行取模运算,知道其溢出,在这种情况下写满和读完就是不一样的标志,写满是两者指针之差为fifo->size,读完的标志是两者指针相等。后面有一篇博客还介绍了VxWorks下的环形缓冲区的实现机制点击打开链接,从而可以看出linux下的fifo的灵巧性和高效性。
cmpxchg是X86比较交换指令,这个指令在各大底层系统实现的原子操作和各种同步原语中都有广泛的使用,比如linux内核,JVM,GCC编译器等,cmpxchg就是比较交换指令,了解cmpxchg之前先了解原子操作。
1、 内存访问操作/指令执行操作的乱序:假设每个CPU都分别运行着一个会触发内存访问操作的程序。那么对于这样一个CPU,其内存访问顺序是非常松散的,在保证程序上下文逻辑关系的前提下,CPU可能乱序执行内存操作。此外,编译器也可以将它输出的指令安排成任何它喜欢的顺序,只要保证不影响程序表面的执行逻辑。这里就涉及到了两次可能发生指令重排的情况:一个是编译的时候,由编译原理的知识知道,编译器会对代码进行优化,这一步就涉及到指令重排,当然,编译完成之后的目标代码中指令的顺序就是确定的,不同线程执行该代码的顺序是一样的;另一个就是CPU在执行具体的指令的时候,也会因为计算机当前的状态(比如寄存器的占用情况、ALU的使用情况,cup缓存层的存在等原因)的不同导致指令最终的执行顺序发生变化(实际上,cpu本身并不会对指令进行重排,它本身是按照编译后的顺序来执行指令的,只是由于执行不同的指令需要的时间长短不同,以及缓存层的存在,再加上CPU执行指令的流水线并不是串行化等因素,那么就有可能出现排在靠前位置的指令还没执行完,而排在靠后的指令已经执行完了的情况,这一情况就是所谓的CPU执行指令的乱序,具体原因后面会更详细地解释),尽管这个变化可能不影响最终结果的正确性。
导读:文章内容较多,也有不少代码,但是作者写的也很认真,对理解并发编程会有帮助,值得一读。 阅读完大约需要15分钟,如果对 linux 实在不太感冒,也可以选择性从 double-check 章节开始看起。
操作系统堪称是IT皇冠上的明珠,Linux阅码场专注Linux操作系统内核研究, 它的文章云集了国内众多知名企业一线工程师的心得,畅销著作有《linux设备驱动开发详解 》等。
内存屏障 , 又称为 " 屏障指令 " , 用于保证 " 编译器 “ 或 ” CPU “ 访问内存时 , 保证 按照顺序执行 , 即 ” 内存屏障 之前 “ 的指令 与 ” 内存屏障 之后 " 的指令 不会犹豫 编译器 和 CPU 优化导致 顺序混乱 ;
现在大多数现代计算机为了提高性能而采取乱序执行,这可能会导致程序运行不符合我们预期,内存屏障就是一类同步屏障指令,是CPU或者编译器在对内存随机访问的操作中的一个同步点,只有在此点之前的所有读写操作都执行后才可以执行此点之后的操作。
如果要使用 " 内存屏障 " , 如 : 禁止 内核 抢占 " 方法保护临界区 " :
函数介绍:local_irq_enable函数用于将CPSR寄存器中的中断使能位设为1,从而使得CPU能够响应中断。
最近Rust For Linux的项目,随着Rust的火爆也开始逐渐升温,但是谷歌的强烈支持以及rCore OS、Redox等各种Rust操作系统项目的经验积累,Rust想进入到Linux的真正核心,也还是有很长的路要走,之前笔者已经撰文对于Rust在汇编支持、panic和alloc等系统操作等方面的问题进行过简要说明了。这里再对于Rust进入到Linux内核的最大拦路虎-也就是内存模型方面的问题,做一下介绍。
既然是锁CPU,那就都是针对多核处理器或多CPU处理器。单核的话,只有发生中断会使任务被抢占,那么可以进入临界区之前先关中断,但是对多核CPU光关中断就不够了,因为对当前CPU关了中断只能使得当前CPU不会运行其它要进入临界区的程序,但其它CPU还是可能执行进入临界区的程序。
这似乎毫无疑问。但是了解编译、链接原理的“底层”知识,则不会轻易下定论。特别是在用到多线程涉及到内存共享没有加锁的时候,也会暴露这个问题。
自旋锁主要用来解决SMP和调度引发的竞态问题,但是普通的自旋锁并不关心临界区在执行什么操作,对读和写都一视同仁,这样就会存在一些弊端!
Disruptor的RingBuffer, 之所以可以做到完全无锁,也是因为"单线程写",这是所有"前提的前提",离了这个前提条件,没有任何技术可以做到完全无锁。Redis、Netty等等高性能技术框架的设计都是这个核心思想。
CPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU。这就造成了高性能能的内存和硬盘价格及其昂贵。然而CPU的高度运算需要高速的数据。为了解决这个问题,CPU厂商在CPU中内置了少量的高速缓存以解决I\O速度和CPU运算速度之间的不匹配问题。
原文地址:牛客网论坛最具争议的Linux内核成神笔记,GitHub已下载量已过百万
每一种技术的出现必然是因为某种需求。正因为人的本性是贪婪的,所以科技的创新才能日新月异。
Volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。它有两条特性:
内存屏障介绍 内存屏障(memory barrier)是一种保证内存顺序访问的方法,用来解决下面这些内存乱序访问的问题。出现内存乱序访问一般有3个方面的因素 编译器编译代码时候可能会重新排列汇编指令,使编译出来的程序在处理器上更快,但是有时候优化的结果可能不符合程序设计者的意图。 现代处理器采用标量体系结构和乱序执行 技术,能够在一个时钟周期内并发执行多条指令,处理器按照指令顺序取出,分析出没有依赖关系的指令然后发给多个处理器执行,最后按照顺序提交执行结果。用一句话总结就是顺序取出,乱序执行,顺序提交结果执
特殊的是StoreLoad,会使该屏障之前的所有内存访问指令(装载和存储指令)完成之后,才执行该屏障之后的内存访问指令;是一个”全能型”的屏障,它同时具有其他三个屏障的效果
在Linux设备驱动中,我们必须要解决的一个问题是:多个进程对共享资源的并发访问,并发的访问会导致竞态。
对volitile变量的写入操作之后要保证不能和读之后的读操作重排序。这是内存重排序的一种解决方案happenbeforr中规定的底层通过刚开始讲的内存屏障保证不会重排序。
对于基础类型操作,使用原子变量就可以做到线程安全,那原子操作是如何保证线程安全的呢?linux中的原子变量如下:
我们可以把内核想象成一个服务器,专门响应各种请求。这些请求可以是CPU上正在运行的进程发起的请求,也可以是外部的设备发起的中断请求。所以说,内核并不是串行运行,而是交错执行。既然是交错执行,就会产生竞态条件,我们可以采用同步技术消除这种竞态条件。
事情是这样的,一位读者看了我的一篇文章,不认同我文章里面的观点,于是有了下面的交流。
1、select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
Java 语言在设计之初就引入了线程的概念,以充分利用现代处理器的计算能力,这既带来了强大、灵活的多线程机制,也带来了线程安全等令人混淆的问题,而 Java 内存模型(Java Memory Model,JMM)为我们提供了一个在纷乱之中达成一致的指导准则。
TLB 是页表项的物理 cache,用于加速虚拟地址到物理地址的转换。CPU 在访问一个虚拟地址时,首先会在 TLB 中查找,如果找不到对应的表项,那么就称之为 TLB miss,此时就需要去内存里查询页表,如果页表项是合法的,那么就会把它添加到 TLB 中。如果内核修改了页表,那么就需要主动的去清空一下当前的 TLB。
如今大数据,云计算,分布式系统等对算力要求高的方向如火如荼。提升计算机算力的一个低成本方法是增加CPU核心,而不是提高单个硬件工作效率。
本文简介本文介绍Linux RCU的基本概念。这不是一篇单独的文章,这是《谢宝友:深入理解Linux RCU》系列的第3篇,前序文章:谢宝友: 深入理解Linux RCU之一——从硬件说起= 谢宝友:
(1)把工作内存1中更新过的共享变量刷新到主内存中 (2)将主内存中最新的共享变量的值更新到工作内存2中
本文讲述了Linux中RCU(Read-Copy-Update)机制在内存管理中的重要作用,以及如何在Linux内核中实现和管理RCU。在Linux内核中,RCU用于在多个进程共享相同内存空间时,保证这些进程之间的数据一致性。本文首先介绍了RCU的基本原理,然后逐步深入介绍了Linux内核中RCU的实现细节。最后,通过一个具体的例子,展示了如何在Linux内核中实现一个简单的RCU。
编译器优化乱序和CPU执行乱序的问题可以分别使用优化屏障 (Optimization Barrier)和内存屏障 (Memory Barrier)这两个机制来解决:
volatile 关键字可以说是 Java 虚拟机提供的最轻量级的同步机制,但是它并不容易被正确、完整地理解,以至于许多程序员都习惯去避免使用它,遇到需要处理多线程数据竞争问题的时候一律使用 synchronized 来进行同步。了解 volatile 变量的语义对理解多线程操作的其他特性很有意义。
那在众多的单例实现方式中呢,双重检查单例(Double Check Lock)又是比较常用的一种实现方案,简称DCL;
以前一直听说有Disruptor这个东西,都是性能很强大,所以这几天自己也看了一下。 下面是自己的学习笔记,另外推荐几篇自己看到写的比较好的博客: Disruptor——一种可替代有界队列完成并发线程间数据交换的高性能解决方案 Disruptor3.0的实现细节
相信很多 Java 开发,都使用了 Java 的各种并发同步机制,例如 volatile,synchronized 以及 Lock 等等。也有很多人读过 JSR 第十七章 Threads and Locks(地址:https://docs.oracle.com/javase/specs/jls/se17/html/jls-17.html),其中包括同步、Wait/Notify、Sleep & Yield 以及内存模型等等做了很多规范讲解。但是也相信大多数人和我一样,第一次读的时候,感觉就是在看热闹,看完了只是知道他是这么规定的,但是为啥要这么规定,不这么规定会怎么样,并没有很清晰的认识。同时,结合 Hotspot 的实现,以及针对 Hotspot 的源码的解读,我们甚至还会发现,由于 javac 的静态代码编译优化以及 C1、C2 的 JIT 编译优化,导致最后代码的表现与我们的从规范上理解出代码可能的表现是不太一致的。并且,这种不一致,导致我们在学习 Java 内存模型(JMM,Java Memory Model),理解 Java 内存模型设计的时候,如果想通过实际的代码去试,结果是与自己本来可能正确的理解被带偏了,导致误解。 我本人也是不断地尝试理解 Java 内存模型,重读 JLS 以及各路大神的分析。这个系列,会梳理我个人在阅读这些规范以及分析还有通过 jcstress 做的一些实验而得出的一些理解,希望对于大家对 Java 9 之后的 Java 内存模型以及 API 抽象的理解有所帮助。但是,还是强调一点,内存模型的设计,出发点是让大家可以不用关心底层而抽象出来的一些设计,涉及的东西很多,我的水平有限,可能理解的也不到位,我会尽量把每一个论点的论据以及参考都摆出来,请大家不要完全相信这里的所有观点,如果有任何异议欢迎带着具体的实例反驳并留言。
init 函数在包初始化时自动执行,这意味着它在程序开始执行前,由于 init 函数是由 Go 运行时自动调用的,并且在程序生命周期中只会被调用一次,它可以被用来初始化单例。
DCL单例模式的对象创建的过程(申请内存空间,初始化对象,引用指向对象的内存空间地址这三个操作不是原子操作,会发生指令重排,所以加上volatile防止指令重排)
在上一篇博客 【Linux 内核 内存管理】RCU 机制 ① ( RCU 机制简介 | RCU 机制的优势与弊端 | RCU 机制的链表应用场景 ) 中 , 分析了 RCU 机制的优势与弊端 ;
由于在进行运算时如果直接操作内存效率会比较低,用CPU又比较消耗资源,所以在中间加了一层高速缓存区,作为内存和处理器之间的缓存。进行运算时可以先在高速缓存区进行读写操作,运算结束后将结果同步到内存中。
前言:非常早之前就接触过同步这个概念了,可是一直都非常模糊。没有深入地学习了解过,最近有时间了,就花时间研习了一下《linux内核标准教程》和《深入linux设备驱动程序内核机制》这两本书的相关章节。趁刚看完,就把相关的内容总结一下。
现代计算机体系结构上,CPU执行指令的速度远远大于CPU访问内存的速度,于是引入Cache机制来加速内存访问速度。除了Cache以外,分支预测和指令预取也在很大程度上提升了CPU的执行速度。随着SMP的出现,多线程编程模型被广泛应用,在多线程模型下对共享变量的访问变成了一个复杂的问题。于是我们有必要了解一下内存模型,这是多处理器架构下并发编程里必须掌握的一个基础概念。
大家好,我是程栩,一个专注于性能的大厂程序员,分享包括但不限于计算机体系结构、性能优化、云原生的知识。
领取专属 10元无门槛券
手把手带您无忧上云