哈喽,大家好呀,我是呼噜噜,在这个科技浪潮奔涌不息的时代,当我们在键盘上飞速敲击,海量资讯从屏幕中如潮水般涌来,通过视频直播拉近人与人的距离,或在游戏世界肆意驰骋,计算机早已融入我们的日常起居与工作生活
而看似普普通通的计算机,其实背后亦有一套精密的运行机制,而这一切都离不开冯・诺依曼架构,它宛如现代计算机的灵魂,从诞生之初就奠定了计算机发展的基石。

现代计算机系统与冯·诺依曼架构的计算机差别不大,最大的区别冯·诺依曼计算机 是 以运算器为中心的,而现代计算机 以储存器为中心:
如今的现代计算机系统,与冯・诺依曼架构计算机,在诸多方面一脉相承。但随着计算机发展过程中,逐渐转变为以存储器为中心,以适应CPU的;而冯·诺依曼计算机是以运算器为中心的,所有的数据运算和处理操作都紧密围绕运算器展开

接下来,我们将重点聚焦于其中与存储相关的组件,一探究竟

存储器
在计算机的存储体系中,存储器是极为关键的部分,用于存放数据和程序,它主要涵盖主存与辅存两大类别:
- 主存:能够直接与 CPU 进行信息交换,就是我们熟悉的内存。主存有一个明显的特点,即一旦断电,其所存储的数据便会丢失
- 辅存:作为主存的后备存储器,并不直接和CPU交换信息。与主存相比,
辅存的容量更大,像常见的机械硬盘、固态硬盘都属于辅存范畴,但它的存取速度相对较慢。而辅存亦有独特的优势,断电后数据不会丢失,属于持久化存储设备
另外辅存、输入设备、输出设备 统称为IO设备;主机一般包含:CPU、主存,它们协同工作,支撑起计算机的基本运行。
想要对不同存储部件建立初步认知,我们不妨先从探究存储器的层次结构入手

我们可以发现,存储器的速度与价格紧密挂钩,读写速度提升的同时,价格也越发地昂贵。而且从寄存器到机械硬盘,各层级存储器之间的读写速度差距,都不是一个数量级的。接下来,让我们深入到存储世界,逐个剖析这些组件
寄存器
在现代 CPU 的复杂架构内部,有一个常见的组件寄存器,是CPU内部用来临时存放数据的一些小型的存储区域,用来临时存放参与运算的数据以及运算结果,提高计算机的性能
寄存器是由精密电子线路组成的,这赋予了它超乎寻常快的存取速度,不过也正因如此,它的成本相对较高,所以在一台家用计算机芯片中,寄存器的数量上比较有限。

在CPU中常用的有六类寄存器:
- 指令寄存器IR,用来保存当前正在执行的指令,是计算机中的"指令收纳员"
- 程序计数器PC,始终追踪着下一条指令的地址,如同一个"行程规划师"
- 地址寄存器AR,用来保存CPU当前所访问的内存的地址,是计算机中的"地址管家"
- 数据寄存器DR,用于存放操作数据,一般负责在CPU与内存、外设之间传递数据,是计算机中的 "数据搬运工"
- 累加寄存器AC,是一个通用寄存器,
常在运算过程中发挥着重要作用,如同一个"运算助手" - 程序状态字寄存器PSW,
用来保存各类运算指令或测试指令的结果的各种状态信息,是计算机中的 "状态监测员"
大家对寄存器的细节感兴趣的话,不妨去看看笔者之前一篇文章:聊聊计算机中的寄存器
CPU时钟周期
在CPU中,CPU时钟周期是一个极为关键的概念,是CPU运行的基本时间单位。它也叫"节拍脉冲"或者"T周期",本质上是CPU主频的倒数,大家平时打游戏时,总爱提到的"超频",就是提升CPU、显卡的主频。一旦成功超频,CPU就如同获得了超能力,数据处理速度大幅提升,游戏画面也变得更加丝滑流畅

我们可以发现CPU的速度和内存、硬盘等存储器的速度,完全不是一个量级上的。拥有"光速"运算能力的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),也叫随机存取存储器。这种存储器根据技术原理的差异,又可进一步细分为静态随机存取存储器(英文缩写为 SRAM,全称是 Static Random Access Memory )以及动态随机存取存储器(英文缩写为 DRAM,全称是 Dynamic Random Access Memory )
SRAM 的存储单元,其工作原理类似于一个锁存器,它仅具备 0 和 1 这两个稳定状态,以此来实现数据的存储。
而 DRAM 则采用了截然不同的机制,它利用电容存储电荷的特性来保存数据,当电容器充满电荷时,表示存储了一个1;反之,当电容器放电后表示存储了一个0。
不过,这种基于电容存储的方式存在一个局限性,由于电容器的电荷会不可避免地随着时间的推移逐渐泄漏;为了确保数据的准确性和完整性,就需要定时对 DRAM 进行刷新操作,以此来补充电容中逐渐流失的电荷,确保其能准确地反映存储的信息。
SRAM:读写速度快,生产成本高,多用于容量较小的高速缓冲存储器。
DRAM:读写速度较慢,集成度高,生产成本低,多用于容量较大的主存储器。

现代计算机中更多使用的是DRAM,它断电后内存的数据也是会丢失的。DRAM 芯片的密度更高,功耗更低,有更大的容量,造价比 SRAM 芯片便宜很多
内存速度大概在
200~300个 时钟周期之间!
固态硬盘
固体硬盘(Solid-state Disk, SSD),数据直接存储在闪存颗粒当中,由主控单元负责记录数据的存储位置以及各类数据操作。需要注意的是,每个闪存颗粒的存储容量都有一定限度

与传统磁盘不同,固态硬盘没有可移动部件,外形也和唱片毫无相似之处。它将数据存储于存储器(闪存)内。与内存相比,它具备一个显著优势:即便断电,数据依然能够完整保存,这一点和磁盘是一致的。
在读写速度方面,虽然固态硬盘比内存慢大概 10 - 1000 倍,但相较于机械硬盘,它的速度优势明显。当然,价格层面上,固态硬盘也相对昂贵许多。不过,随着时代的不断发展和技术的持续进步,固态硬盘的价格正逐渐向机械硬盘靠拢。
机械硬盘
机械硬盘(Hard Disk Drive, HDD),它是通过物理读写的方式来访问数据的,往机械硬盘写入数据时,磁盘开始转动,同时机械臂随之移动,完成数据的写入。这种数据读写方式较为原始,其原理和近现代留声机的发声颇为相似,都是利用物理机械的运动来实现信息的存储与读取

机械硬盘主要包含以下几个关键部分:
- 盘片,它是数据存储的载体;
- 磁头,负责数据的读写操作;
- 盘片转轴及控制电机,盘片转轴用于支撑盘片转动,控制电机则为盘片的旋转提供动力;
- 磁头控制器,主要作用是精准控制磁头的移动;
- 数据转换器,承担着数据格式转换的任务;
- 接口,用于实现与计算机其他部件的数据传输;
- 缓存,能够临时存储数据,提高数据读写的效率
在现代磁盘中,相比内部柱面,外部柱面容纳着更多扇区。机械臂在相邻柱面间移动,所需时间大致为 1 毫秒。而随机移动到任意一个柱面时,通常耗时在 5 毫秒至 10 毫秒之间,具体时长会因驱动器的不同而产生差异
机械硬盘的访问速度,受限于转盘转速以及指针寻址的时间,极为缓慢,大约比内存慢 10 万倍。不过,机械硬盘也有着自身独特的优势:存储容量大,价格亲民,数据恢复难度较低,正因如此,将数据存储在机械硬盘中,相对来说更保险
压榨CPU性能带来的问题
由于CPU速度非常快,且价格非常昂贵,我们必须得充分压榨CPU的性能,就如同让生产队里不知疲倦的驴一般,持续不断地工作

为了既能合理运用 CPU 的卓越性能,又能最大程度地控制成本,现代计算机巧妙地将各类储存器有机结合起来。然而,这些硬件在数据存取速度上存在显著差异,这也导致了计算机系统编程过程中的一系列问题:
原子性问题
为了平衡CPU 与 I/O 设备的速度差异,操作系统OS增加了进程、线程概念,以分时复用 CPU,但同时导致了原子性问题。
原子操作就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。
当一个程序去I/O 设备读取数据, 由于I/O 设备数据存入读取速度,与 CPU 的执行速度相比,简直是度日如年,CPU这么牛逼这么昂贵的宝贝,怎么能让它歇着,得让它一直干活,去切换执行其他程序。也就是将CPU的时间进行分片,让各个程序在CPU上轮转执行。但被剥夺执行权的程序,在完成数据从 I/O 设备的读取后,仍然需要 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 会对不存在依赖关系的前后指令进行合理的乱序处理和调度。通过这种方式,充分利用 CPU 资源,减少指令执行过程中的等待时间,从而提升整体运算速度。
除了 CPU 层面的乱序执行,编译器层也会进行指令重排。以 Java 语言为例,JVM(Java 虚拟机) 会对其指令进行重排序的优化(指令重排)。所谓指令重排是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。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,获取到的必然是经线程 1 修改后的最新值
而当一个程序的运行涉及到 CPU 的多个核心时,这些核心会从内存中读取某个共享变量的数据,当不同核心间进行了各自的计算,会把计算得出的值存入自身的缓存里,而且并不会马上将其写入内存(因为CPU写入内存的时机是不确定的)。那么在不同 CPU 核心的缓存中,这个共享变量就可能存在各不相同的数据内容。这种情况就引发了缓存的可见性问题,也就是说,一个线程对数据做出的修改,并不能及时被其他线程所感知到。

至于如何妥善解决这三个问题,正是并发编程与多线程领域需要重点处理的核心任务。不过,深入探讨这个话题属于后续内容,我们下期再见!
参考资料:
《深入理解计算机系统》
《计算机组成原理》
到这里,这篇文章就告一段落啦。要是它对你有所帮助,麻烦动动手指点个免费的赞,你的支持会化作我前进的动力,激励我产出更多高质量内容,由衷感谢!
如果你对计算机内功、源码剖析、科技轶事、项目实操、面试秘籍等硬核内容感兴趣,首发于公众号「小牛呼噜噜」,所有文章都在那里首发。咱们下期不见不散!
作者:小牛呼噜噜
本文到这里就结束啦,感谢阅读,关注同名公众号:小牛呼噜噜,防失联+获取更多技术干货