原创 小牛呼噜噜
- 现代计算机系统
- 存储器
- 寄存器
- CPU时钟周期
- 高速缓存
- 主存
- 固态硬盘
- 机械硬盘
- 压榨CPU性能带来的问题
-
- 原子性问题
- 有序性问题
- 可见性问题
现代计算机系统
大家好,我是呼噜噜,之前写过一篇关于计算机现代储存器的,但觉得写地有瑕疵,最近补充完善了一下。
计算机系统与冯·诺依曼计算机差别不大,最大的区别冯·诺依曼计算机 是 以运算器为中心的,而现代计算机 以储存器为中心:
我们主要来看一下其中与储存相关的组件:
存储器
存储器是用来存放数据和程序。存储器 包含主存和辅存
- 主存:直接与CPU交换信息,就是我们熟悉的内存。断电后内存的数据是会丢失的
- 辅存:辅存可作为主存的后备存储器,不直接与CPU交换信息,容量比主存大,但速度比主存慢。比如
机械硬盘、固态硬盘等。断电后硬盘的数据是不会丢失,硬盘是持久化存储设备。 辅存、输入设备、输出设备统称为IO设备;主机一般包含:CPU、主存
我们先来看看存储器的层次结构,来初步对各个储存器部件有所认识
我们可以发现存储器速度越快的话,相应的价格也会越发昂贵,每一层储存器的读写速度差异都是巨大的!下面我们分别聊聊这些储存器:
寄存器
现代CPU内部 还有一个常见的组件: 寄存器,是CPU内部用来存放数据的一些小型的存储区域,用来暂时存放参与运算的数据以及运算结果。寄存器由电子线路组成,存取速度非常快,寄存器的成本较高,因而数量较少。
在CPU中常用的有六类寄存器: 指令寄存器(IR)、程序计数器(PC)、地址寄存器(AR)、数据寄存器(DR)、累加寄存器(AC)、程序状态字寄存器(PSW)。
大家对寄存器感兴趣的话,可以看看笔者之前一篇文章:聊聊计算机中的寄存器
CPU时钟周期
CPU时钟周期: 通常为节拍脉冲或T周期,即主频的倒数,它是CPU中基本时间单位。平时我们打游戏常说的超频,超的就是这个CPU主频。
从硬件层次来说,CPU 的主频是 其实由⼀个晶体振荡器来实现的,这个晶体振荡器⽣成的电路信号,就是我们的时钟信号
举个例子,主频为3.0GHZ的CPU,一个时钟周期大约是0.3纳秒,内存访问大约需要120纳秒,固态硬盘访问大约需要50-150微秒,机械硬盘访问大约需要1-10毫秒,最后网络访问最慢,得几十毫秒左右。
这个大家可能对时间不怎么敏感,那如果我们把一个时钟周期如果按1秒算的话,内存访问大约就是6分钟 ,固态硬盘大约是2-6天 ,传统硬盘大约是1-12个月,网络访问就得几年了!
我们可以发现CPU的速度和内存等存储器的速度,完全不是一个量级上的!
高速缓存
为了弥补 CPU 与内存两者之间的性能差异,就在 CPU 内部引入了 CPU Cache,也称高速缓存。CPU Cache用的是 SRAM(Static Random-Access Memory)的芯片,也叫静态随机存储器。 其只要有电,数据就可以保持存在,而一旦断电,数据就会丢失。
CPU Cache 通常分为大小不等的三级缓存,分别是 L1 Cache、L2 Cache 和 L3 Cache
| 部件 | CPU访问所需时间 | 备注 |
|---|---|---|
| L1 高速缓存 | 2~4 个时钟周期 | 每个 CPU 核心都有一块属于自己的 L1 高速缓存,L1 高速缓存通常分成指令缓存和数据缓存。 |
| L2 高速缓存 | 10~20 个时钟周期 | L2 高速缓存同样是每个 CPU 核心都有的 |
| L3 高速缓存 | 20~60个时钟周期 | L3 高速缓存是多个 CPU 核心共用的 |
我们可以发现越靠近 CPU 核心的缓存其访问速度越快。
程序执行时,会先将内存中的数据加载到共享的 L3 Cache 中,再加载到每个核心独有的 L2 Cache,最后 进入到最快的 L1 Cache,之后才会被 CPU 读取。层级关系如下图:
主存
主存,直接与CPU交换信息,就是我们熟悉的内存。通常叫做 RAM(Random AccessMemory),也叫随机存取存储器,RAM可分为静态随机存取存储器(Static Random Access Memory,SRAM)和动态随机存取存储器(Dynamic Random Access Memory,DRAM)。
SRAM中的存储单元相当于一个锁存器,只有0,1两个稳态;DRAM则是利用电容存储电荷来保存0和1两种状态,因此需要定时对其进行刷新,否则随着时间的推移,电容其中存储的电荷将逐渐消失。
SRAM:读写速度快,生产成本高,多用于容量较小的高速缓冲存储器。DRAM:读写速度较慢,集成度高,生产成本低,多用于容量较大的主存储器。
现代计算机中更多使用的是一种叫作 DRAM,也叫动态随机存取存储器。断电后内存的数据也是会丢失的。DRAM 芯片的密度更高,功耗更低,有更大的容量,造价比 SRAM 芯片便宜很多,但速度比SRAM 芯片慢的多。
内存速度大概在 200~300 个 时钟周期之间!
除了主存之外,计算机还具有少量的非易失性随机存取存储器。它们与RAM不同,在电源断电后,非易失性随机访问存储器并不会丢失内容。
ROM只读存储器,是以非破坏性读出方式工作,它非易失性存储器,当电源被移除时,其数据内容不会被擦除,它还有个特点就是只能读出而不能写入信息,其所存的数据,一般是装入整机前事先写好的,整机工作过程中只能读出。
在计算机中,一般用于启动计算机的引导加载模块(bootstrap)就存放在ROM中。需要注意的是: 虽然ROM和硬盘有一些共性,但不能简单地说ROM就是硬盘
固态硬盘
固体硬盘(Solid-state Disk, SSD),数据直接存在闪存颗粒中,并且由主控单元记录数据存储位置和数据操作,每一个闪存颗粒的存储容量是有限的;
固体硬盘不像磁盘,固态硬盘并没有可以移动的部分,外形也不像唱片,并且数据是存储在 存储器(闪存) 中,但是它相比内存的优点是断电后数据还是存在的,这点与磁盘一样。
SSD固体硬盘的读写速度虽然比内存的大概慢10~1000 倍,但比机械硬盘快多了,当然价格也昂贵很多。不过随着时代的发展,固态硬盘的价格慢慢趋向接近机械硬盘。
机械硬盘
机械硬盘(Hard Disk Drive, HDD),它是通过物理读写的方式来访问数据的,机械硬盘在盘面上写数据、磁盘转动,机械臂移动,比较原始的数据读写方式,就像近现代的留声机发声原理一样。
机械硬盘(HDD)主要由:盘片,磁头,盘片转轴及控制电机,磁头控制器,数据转换器,接口,缓存等几个部分组成在现代磁盘中,较外部的柱面比较内部的柱面有更多的扇区。机械臂从一个柱面移动到相邻的柱面大约需要1ms。而随机移到一个柱面的典型时间为5ms至10ms,具体情况以驱动器为准。
由于受限于转盘转速与指针寻址的时间限制,因此它访问速度是非常慢的,它的速度比内存慢 10W 倍左右。当然机械硬盘也是有其优点的:容量大,价格便宜,恢复数据难度低,因此数据放在机械硬盘中比较保险。
压榨CPU性能带来的问题
由于CPU速度非常快,且价格非常昂贵,我们必须得充分压榨CPU,得像生产队的驴一样,让它不停地工作为了合理利用 CPU 的高性能,同时尽可能地节约成本,现代计算机将这些储存器充分的结合起来,由于这些硬件的数据存取速度差异导致了计算机系统编程中的各种问题:
原子性问题
为了平衡CPU 与 I/O 设备的速度差异,操作系统OS增加了进程、线程概念,以分时复用 CPU,但同时导致了原子性问题。
原子操作就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。
当一个程序去I/O 设备读取数据, 由于I/O 设备数据存入读取速度,相比于CPU的执行速度来说度日如年,CPU这么牛逼这么昂贵的宝贝,怎么能让它歇着,得让它一直干活,去切换执行其他程序。也就是将CPU的时间进行分片,让各个程序在CPU上轮转执行。
但被剥夺执行权的程序,等它从IO读取完数据后,还是得让CPU继续执行的,这时需要一个数据结构来保存,以便之后恢复继续执行,这个就是进程。
Unix是第一个支持多进程分时复用的操作系统。随着时代的发展,一开始进程中只有一个"执行流",干活的人就一个。随着任务越来越多,发现进程不够用了,经常导致整个程序被阻塞,这时计算机让进程有多个执行流,干活的人变多了,那程序就不会再被阻塞了,"执行流" 就是线程。
**
**
对操作系统来说,进程是资源分配的基本单位,而线程则是任务调度的基本单位。 我们还需要了解一个重要的概念虚拟内存,虚拟内存是一种内存管理技术,是虚拟的、逻辑上存在的存储空间 ,核心是将不连续的物理内存映射成连续的虚拟内存。每个进程都有属于自己的、私有的、地址连续的虚拟内存。
早期的操作系统是基于进程来调度 CPU,每个进程有自己独立的、地址连续的虚拟地址空间,进程内的所有线程共享进程的虚拟地址空间。
进程上下文切换、线程上下文切换最重要的区别是:进程之间切换涉及虚拟地址空间的转换,而同一个进程中线程都是共享一个虚拟地址空间的,线程之间切换不涉及地址空间转换。所以线程做任务切换时的成本就比较低,更轻量。
对于进程上下文切换、线程上下文切换,感兴趣地可阅读:聊聊Linux中CPU上下文切换
我们现在基本都使用高级语言编程,往往一条语句,底层需要多条CPU 指令才能完成。比如最常见的i++,其需要这几条CPU指令:
-
首先,需要把变量i 从内存加载到 CPU 的寄存器
-
其次,在寄存器中对变量i执行
+1操作 -
最后,将结果回写到内存中。其中高速缓存的存在,可能导致先回写到CPU 缓存,再到内存中
此时有线程1,线程2,同时执行i++操作。当线程1执行指令+1操作时,这个时候发现线程调度,线程2一口气全部执行完这3个指令,此时线程2中的i的值为1。然后线程1恢复线程上下文,继续执行指令+1操作,此时线程1中i的值还是为0的,然后执行+1操作。最终结果为1,而不是我们预期结果2。
操作系统OS理论上是可以在任意一条CPU 指令执行完就进行线程上下文切换,发生线程调度。这样就容易导致原子性问题。
有序性问题
为了充分压榨CPU的性能,CPU 会对指令乱序执行或者语言的编译器会指令重排, 让CPU一直工作不停歇 , 但同时会导致有序性问题。
在CPU中为了能够让指令的执行尽可能地同时运行起来,采用了指令流水线。一个 CPU 指令的执行过程可以分成 4 个阶段:取指、译码、执行、写回。这 4 个阶段分别由 4 个独立物理执行单元来完成。
理想的情况是:指令之间无依赖,可以使流水线的并行度最大化。但是如果两条指令的前后存在依赖关系,比如数据依赖,控制依赖等,此时后一条语句就必需等到前一条指令完成后,才能开始。
所以CPU为了提高流水线的运行效率,对无依赖的前后指令做适当的乱序和调度。还有一种情况编译器会指令重排, 比如java语言,JVM 的编译器会对其指令进行重排序的优化(指令重排)。
所谓指令重排是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。比如:
int i = 0;
int j = 0;
i = 10; //语句1
j = 1; //语句2
但由于指令重排序问题,代码的执行顺序未必就是编写代码时候的顺序。语句可能的执行顺序如下:
- 语句1 语句2
- 语句2 语句1
无论是编译期的指令重排还是CPU 的乱序执行,主要都是为了让 CPU 内部的指令流水线可以“填满”,提高指令执行的并行度,充分利用CPU的高性能。但同时会导致有序性问题,即调整指令执行顺序,影响到了最终的结果。
具体的例子可见:懒汉式单例 -- 双重校验锁 synchronized版
可见性问题
为了平衡CPU的寄存器和内存的速度差异,计算机的CPU 增加了高速缓存,但同时导致了 可见性问题。
我们知道当程序执行时,一般CPU会去从内存中读取数据,来进行计算。CPU计算完之后,需要把数据重新放回到内存中。当CPU是单核的时代还好,因为所有的线程都是在同一颗 CPU核心上执行,那么他们都是操作同一个 CPU 的缓存,一个线程对缓存的写,对另外一个线程来说一定是可见的。比如上图中,线程1和线程2一般无法直接操作内存中的变量A,CPU核心会先从从内存中读取变量A到高速缓存中,线程 1和线程 2 操作同一个 CPU核心中的缓存,所以线程1更新了变量A的值,那么线程2之后再访问变量A,得到的一定是变量A的最新值(线程1写过的值)
当CPU的多个核心参与一个程序的运行,从内存中读取一个共享变量的数据,当不同核心间进行了各自的计算,把计算后的值放入自己的缓存中而不选择立即写入内存中(CPU写入内存的时机是不确定的)。那么在CPU的缓存中,这个共享变量有可能存放着不同的数据,这就导致了缓存的可见性问题。即一个线程对数据的修改无法对其他线程可见。
如何解决这3个问题,就是并发、多线程需要处理的事,当然这是后话-Java内存模型(JMM)详解。
参考资料:
《深入理解计算机系统》
《计算机组成原理》