原子操作,内存屏障,锁
1.原理:CPU提供了原子操作、关中断、锁内存总线,内存屏障等机制;OS基于这几个CPU硬件机制,就能够实现锁;再基于锁,就能够实现各种各样的同步机制(信号量、消息、Barrier等等等等)。
2.所有的同步操作最基础的理论就是原子操作。内存屏障,锁都是为了保证在不同的平台或者是CPU类型下的原子操作。
3.原子操作在单核,单线程/无中断,且编译器不优化的情况下是确定的,是按照C/C++代码顺序执行的,所以不存在异步问题
解释一下这几个知识点为什么会引起异步操作:
首先了解一下cpu处理指令的步骤:
1.早起的处理器为有序处理器,指令处理顺序:
a.读取指令
b.执行指令如果寄存器可写就从内存取出a的数据到寄存器,寄存器不可写就等待
c.寄存器处理指令
d.将寄存器结果存入内存
2.现在的处理器大多数为乱序处理器,处理顺序:
a.读取指令
b.指令被划分到指令队列
c.指令在队列中等待,如果寄存器可写就从内存取出a的数据到寄存器,寄存器不可写就等待
d.寄存器处理指令
e.将执行结果存入队列(而不是立即写入寄存器堆)
f.只有当所有更早的请求执行的指令结果被写入内存之后,执行的结果才会被存入内存(执行结果重排序,让执行看起来是有序的)
那么问题来了:1.一条简单的a++语句究竟会有这么多条指令,而这一组指令是可以在任意时候异步执行的(共享数据)
a.单核多线程情况下,线程是存在中断的,中断的时候cpu调用另一线程的同一指令组,所以是可能出现交叉执行的可能,也就是说单线程或者关掉中断可以解决异步问题,但很多时候这种做法并不实际
b.多核多线程情况下共享数据被多个核并行处理,不论哪一种处理器都存在同时执行的可能,这就导致了异步问题
其中以前做游戏服务器开发的时候,一开始不理解很多游戏服务器架构为什么业务线程都是一条线程处理,因为游戏中很多涉及到共享数据,所以避免不了的要使用各种锁,但是锁多了问题反而更多。
c.现在的编译器都具有优化及自动优化功能,优化之后可能会对共享变脸的访问顺序进行调整,可能会造成与预期不相符的结果。
4.内存屏障的作用:a.在编译时:拒绝编译器优化屏障前后的指令,防止内存乱序访问;b.在运行时:告诉内存地址总线共享数据地址的数据必须同步(当多个线程同时将一个共享数据地址的数据加载到队列里的时候,先完成处理从cpu到内存的时候总是通知其他线程跟新队列中的该共享数据,从而保证一致性)
Memory barrier 常用场合包括:
1.实现同步原语(synchronization primitives)
2.实现无锁数据结构(lock-free data structures)
3.驱动程序
内存屏障包含4中基本类型:写屏障,数据依赖屏障(常与写屏障成对出现),读屏障,通用内存屏障(包含读写屏障)。
内存屏障还有两种隐式的屏障变种:LOCK和UNLOCK操作(表面上这两个操作的实际用途和原子操作里面的Lock解释有区别,原子操作里面的lock是锁内存总线,这里面的lock是保证执行的执行顺序严格按照lock前,lock中,lock后的顺序执行)
内存屏障按照使用层次可以分为
· 编译器屏障。
· CPU内存屏障。
· MMIO write屏障。
所以:内存屏障只是一种线程同步的手段,并不会阻塞线程;仅保证了代码执行顺序和多核竞争情况下的数据一致性。
5.锁:从上面可以看出内存屏障并不是锁,而锁是使用了内存屏障实现的一种用户层的同步处理方式,锁使用的汇编原语有LOCK,UNLOCK等是内存屏障的一种隐式形式,它们都是LOCK操作和UNLOCK操作的变种,所以几乎所有的锁都使用了内存屏障,
锁包含了:
原子锁:使用了锁总线的方式实现原子操作
自旋锁:while等待,不可抢占的单CPU内核下是无效的,有软中断的情况下,必须使用时本地软中断失效的方法。自旋锁更像是一种用户层控制的while等待处理
读写锁: 读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作
互斥锁:沉睡/休眠等待,所以互斥锁比自旋锁调度耗时。
信号量:用于同一时刻有多个个实例能获取锁,可用于表示同时有多少个client请求允许访问同一个数据块,允许锁个数设置为1的时候就是互斥锁.
读写信号量:对同时拥有的读者数不受限制,只能一个写者,写者发现不需要写的时候降级为读者。
顺序锁:用于能够区分读与写的场合,并且是读操作很多、写操作很少,写操作的优先权大于读操作。
读拷贝锁:RCU(read-copy-update)(RCU也是用于能够区分读与写的场合,并且也是读多写少,但是读操作的优先权大于写操作)
rcuclassic:禁止内核抢占的
rcupreempt:允许内核抢占的,实时性更高,和rcuclassic相反
rcutree:和rcuclassic类似
BKL(大内核锁): 整个内核只有一把这样的锁,一旦一个进程获得大内核锁,进入了被它保护的临界区,不但该临界区被锁住,所有被它保护的其它临界区都将无法访问,直到该进程释放大内核锁
注:以下为摘录整理部分。
详解:
第一章:从硬件层面解释原因
1.概念:
从CPU基本原理开始说起,系统性能提升必须以了解CPU基本原理为前提条件,另外,CPU Cache工作原理也是提升系统整体性能的非常重要的方面,所以本文拿出专门章节对其原理进行了详细介绍。
1. 基本概念
在现代CPU体系设计结构中,一般提供了下面几种机制来提升系统的整体性能:
1)总线加锁、cache一致性管理:以实现对系统内存的原子操作、串行化指令(serializing instructions。这些指令仅对pentium4,Intel Xeon, P6,Pentium处理器有效)。
2)处理器芯片内置的高级可编程中断控制器(APIC)
3)二级缓存(level 2, L2) 对于Pentium4,Intel Xeon, P6处理器,L2 cache已经紧密的封装到了处理器中。而Pentium,Intel486提供了用于支持外部L2 cache的管脚。
4)超线程技术:它能够让一个处理器内核并发的执行两个或两个以上的指令流。
这些机制在对称多处理系统(symmetric-multiprocessing, SMP)中是极其有用的。然而,在RMI这些多核系统中,这些机制也是适用的。
多处理器机制的设计必须满足下面的需求:
1)保持系统内存的完整性(coherency): 当两个或多个处理器试图同时访问系统内存的同一地址时,必须有某种通信机制或内存访问协议来提升数据的完整性,以及在某些情况下,允许一个处理器临时锁定某个内存区域。
2)保持高速缓存的一致性: 当一个处理器访问另一个处理器缓存中的数据时,必须要得到正确的数据。如果这个处理器修改了数据,那么所有的访问这个数据的处理器都要收到被修改后的数据。
3)允许以可预知的顺序写内存: 在某些情况下,从外部观察到的写内存顺序必须要和编程时指定的写内存顺序相一致。
4)在一组处理器中派发中断处理: 当几个处理器正在并行的工作在一个系统中时,有一个集中的机制是必要的,这个机制可以用来接收中断以及把他们派发到某一个适当的处理器。
5)采用现代操作系统和应用程序都具有的多线程和多进程的特性来提升系统的性能
2.一致性原因
在多线程编程中,为了保证数据操作的一致性,操作系统引入了锁机制,用于保证临界区代码的安全。通过锁机制,能够保证在多核多线程环境中,在某一个时间点上,只能有一个线程进入临界区代码,从而保证临界区中操作数据的一致性。
所谓的锁,说白了就是内存中的一个整型数,拥有两种状态:空闲状态和上锁状态。加锁时,判断锁是否空闲,如果空闲,修改为上锁状态,返回成功;如果已经上锁,则返回失败。解锁时,则把锁状态修改为空闲状态。
看起来很简单,大家有没有想过,OS是怎样保证这个锁操作本身的原子性呢?举个例子,在多核环境中,两个核上的代码同时申请一个锁,两个核同时取出锁变量,同时判断说这个锁是空闲状态,然后有同时修改为上锁状态,同时返回成功。。。两个核同时获取到了锁,这种情况可能吗?
废话,当然是不可能,可能的话,我们使用锁还有啥意义。但是,咦?等等,虽然我知道肯定不可能,但是你刚才说的貌似还有点道理,看来OS实现这个锁还不是看起来这么简单,还是有点道道的。
为了弄明白锁的实现原理,我们首先看看如果OS不采用任何其他手段,什么情况下会导致上锁失败?假如我们把加锁过程用如下伪码表示:
1、read lock;
2、判断lock状态;
3、如果已经加锁,失败返回;
4、把锁状态设置为上锁;
5、返回成功。
明白汇编的同学一看就明白上述每一步都能对应到一条汇编语句,所以我们可以认为每一步本身是原子的。
那么什么情况能够导致两个线程同时获取到锁呢?
1、中断:假设线程A执行完第一步,发生中断,中断返回后,OS调度线程B,线程B也来加锁并且加锁成功,这时OS调度线程A执行,线程从第二步开始执行,也加锁成功。
2、多核:当然了,想想上面举的例子,描述的就是两个核同时获取到锁的情况。
既然明白锁失败的原因,解决手段就很明确了:
先考虑单核场景:
1、既然只有中断才能把上锁过程打断,造成多线程操作失败。我先关中断不就得了,在加锁操作完成后再开中断。
2、上面这个手段太笨重了,能不能硬件做一种加锁的原子操作呢?能,大名鼎鼎的“test and set”指令就是做这个事情的。
通过上面的手段,单核环境下,锁的实现问题得到了圆满的解决。那么多核环境呢?简单嘛,还是“test and set”不就得了,这是一条指令,原子的,不会有问题的。
真的吗,单独一条指令能够保证该指令在单个核上执行过程中不会被中断打断,但是两个核同时执行这个指令呢?。。。我再想想,硬件执行时还是得从内存中读取lock,判断并设置状态到内存,貌似这个过程也不是那么原子嘛。对,多个核执行确实会存在这个问题。
怎么办呢?首先我们得明白这个地方的关键点,关键点是两个核会并行操作内存而且从操作内存这个调度来看“test and set”不是原子的,需要先读内存然后再写内存,如果我们保证这个内存操作是原子的,就能保证锁的正确性了。
确实,硬件提供了锁内存总线的机制,我们在锁内存总线的状态下执行test and set操作,就能保证同时只有一个核来test and set,从而避免了多核下发生的问题。
总结一下,在硬件层面,CPU提供了原子操作、关中断、锁内存总线的机制;OS基于这几个CPU硬件机制,就能够实现锁;再基于锁,就能够实现各种各样的同步机制(信号量、消息、Barrier等等等等)。