编程知识 cdmana.com

计算机原理探险系列(一)CPU

cpu制作短视频链接:
https://www.bilibili.com/video/BV1tr4y1K7Bs?from=search&seid=12992058713602054171

CPU的制作过程

晶圆切片
如果问及CPU的原料是什么,大家都会轻而易举的给出答案—是硅。这是不假,但硅又来自哪里呢?其实就是那些最不起眼的沙子。不过不是随便抓一把沙子就可以做原料的,一定要精挑细选,从中提取出最最纯净的硅原料才行。

在这里插入图片描述

为了能够保证硅原料在后续加工的步骤中能够成型,所以人们会将硅原料放入一个石英容器中,进行高温熔化处理。
(高纯度的硅)单晶硅,在高温条件下,利用旋转拉伸的方式得到一个圆柱体的硅锭。从目前所使用的工艺来看,硅锭圆形横截面的直径为200毫米。intel公司光是在硅锭这块就花了数十亿美元的金额成本,可见这类技术的复杂程度之高。
在这里插入图片描述

在这里插入图片描述

在制成硅锭并确保其是一个绝对的圆柱体之后,下一个步骤就是将这个圆柱体硅锭切片(这种切片我们一般称之为晶圆),切片越薄,用料越省,自然可以生产的处理器芯片就更多。切片还要镜面精加工的处理来确保表面绝对光滑,之后检查是否有扭曲或其它问题。这一步的质量检验尤为重要,它直接决定了成品CPU的质量。

在这里插入图片描述

刚切出来的晶圆一般还不足以进行导电,所以需要加入一定的杂质,让一些其他种类的原子可以渗透到硅原子之间的空隙,增加导电的能力。
在掺入化学物质的工作完成之后,标准的切片就完成了。然后将每一个切片放入高温炉中加热,通过控制加温时间而使得切片表面生成一层二氧化硅膜。

在这里插入图片描述

最后需要加入一层光雕刻液体,这一层物质用于同一层中的其它控制应用。这层物质在干燥时具有很好的感光效果,而且在光刻蚀过程结束之后,能够通过化学方法将其溶解并除去。

在这里插入图片描述

接下来便是在感光层上边进行雕刻,这部环节结束之后可以在已有的晶圆上边造出电路图案。掺入金属杂质,为了后续的晶体管电路铺设所服务,最后进入不断的测试环节。

在这里插入图片描述

在这里插入图片描述

将原本的晶圆进行不断地打磨,切割,电路测试,封装等一系列的步骤环节,最终切割出数百个处理器。
最后将表面残余的光雕刻胶溶解掉,又进行了一轮电镀,离子注入,晶体管注入等一系列相当复杂的环节,便形成了最终对外售卖的cpu。

在这里插入图片描述

更多详细步骤参考:
https://www.techug.com/post/how-cpu-make-out.html

现代计算机模型

现代中的大多数计算机都是围绕着上世纪提出的冯诺伊曼计算机模型所构建。那么什么是冯诺伊曼计算机模型呢?简单总结就是以下几个步骤:

  1. 计算机从内存中取出运算指令
  2. 计算机按照指令,从存储器里取出数据进行计算加工
  3. 最后将计算加工的结果数据写回到内存里
  4. 继续执行下一条指令

在这种模型下所发明的计算机主要划分为了五个核心模块的组成:

  • 控制器:主要负责程序内部的地址查询、调度程序、协调计算机各个部分的工作任务安排
  • 运算器:负责计算机的核心计算
  • 存储器:负责计算机数据的存储
  • 输入设备:接收一些计算机外界传入的数据信息,常见的有键盘、鼠标、麦克风等硬件设备
  • 输出设备:负责将计算机运算的结果输出给外界使用者,常见的有显示终端、音响等

以intel生产的cpu模型来说,在计算机底层的交互逻辑图基本的设计如下:

在这里插入图片描述

外界的硬件设备如果需要和内核态的cpu进行打交道,需要通过一个叫做总线接口的介质进行交互,然后将用户态的一些信号参数传输到内核态,再传递给到cpu做相关的计算。

什么是计算机总线
简单来说,总线就是计算机内部各种部件进行信息通信的一个公共干线(本质是一条数据导线)。

ps:总线(Bus)是计算机各种功能部件之间传送信息的公共通信干线,它是由导线组成的传输线束, 按照计算机所传输的信息种类,计算机的总线可以划分为数据总线、地址总线和控制总线,分别用来传输数据、数据地址和控制信号。总线是一种内部结构,它是cpu、内存、输入、输出设备传递信息的公用通道,主机的各个部件通过总线相连接,外部设备通过相应的接口电路再与总线相连接,从而形成了计算机硬件系统。在计算机系统中,各个部件之间传送信息的公共通路叫总线,微型计算机是以总线结构来连接各个功能部件的。

执行一个简单的程序,CPU内部发生了什么

在这里插入图片描述
算术逻辑单元(ALU)是专门执行算术和逻辑运算的数字电路
寄存器 (REGISTER)
程序计数器 (PC)
缓存 (CACHE)

通常我们写的程序指令都是存储在用户态中的内存,当cpu执行内存中的指令时,首先PC会从内存中查找并记录下执行指令的地址。假设执行的指令是:

int a = 1+1+1int b = a+1;

此时需要用到一些变量,那么就需要将这些个变量信息通通加载到CPU里面去,并且将它们存放到cpu中。
详细流程图
在这里插入图片描述

cpu的变量存放在哪?
在cpu的内部有一个叫做寄存器的模块,专门用于存放这些程序指令产生的变量信息。

CPU的寄存器有数十个,常见的种类有
数据寄存器:保存操作数的计算结果
变址寄存器:存放数据在存储单元的地址偏移量
指针寄存器:主要用于访问堆栈内的存储单元
段寄存器,指令指针寄存器,标志寄存器等等…

获取到指令和变量后,alu会开始进行相关的计算(本质是通过电路进行运算)。最后计算完毕之后将结果重新写回到用户态的内存中。

关于电路计算原理感兴趣的可以查看以下链接:
https://www.bilibili.com/video/BV1Lb411J7oq?p=4

既然alu,register,pc已经能够解决掉程序执行的结果,那么为什么还需要加入cache模块的设计呢?
假设说缺少了cache这一环节,那么cpu获取变量数据的步骤就是这么个流程:

alu-->register-->内存

在这个流程中寄存器和内存分别负责的模块为:

寄存器
找到寄存器内部的数据,然后读取即可。如果没有在寄存器获取到数据,那么就到内存中查询。

内存
需要找到相应的数据指针,然后mmu(内存管理单元)将虚拟地址转换为物理地址,再提交给memory controller(内存控制器)查询数据在哪一根内存插槽上边。最后再将数据传输给寄存器。

ps:
内存管理单元 类似于哈希表的结构,将内核态的虚拟地址和用户态的物理地址做映射管理。
在这里插入图片描述
内存控制器 可以理解成根据地址查询数据的一个工具。

在这里插入图片描述
这一系列的操作过程中,每一步都有一定的延迟,整体消耗的时间要比寄存器里面消耗的时间周期多出了上千倍。
这里我从别处找了一张比较合理的时间消耗图供大家参考:
在这里插入图片描述

也就是说一次cpu的计算周期中,寄存器的计算速度远超过内存的计算时间,在内存计算期间,cpu是处于空闲状态,利用率大大降低。为了提升cpu的利用率,于是就引入了cache的设计。于是整体的设计就基本是:
寄存器往下就是各级的cache,有L1 cache,L2,甚至有L3的,以及TLB这些(TLB也可以认为是cache),之后就是内存。

在提升cpu利用率方面,硬件设计师做出了许多努力,包括在cpu内部设置cache、优化cpu工作方式,尽量一次性从内存读取指令所要用到的全部数据等等。

缓存一致性问题
程序运行的指令实际上是在用户态的内存设备中存储的,当需要进行计算的时候是需要借助 IO 总线,让 CPU 读取指令进行数据寻址,并且将数据通过总线接口加载到 CPU 的运算单元进行计算。

多级缓存的加载思路
首先到 CPU 的内部寄存器查询,如果没有则在 L1 缓存中查询,如果 L1 也没有则在 L2 中查询,如果 L2 没有则在 L3 查询,通常情况下 80%的数据都能在 L1、L2 中查询得到,在同一块计算机主版上有多个 CPU 的场景下,L3 是属于多个 CPU 共用的部分。在 Intel 的 CPU 中,查询速度通常是alu> 寄存器 > L1 > L2 > L3 > 内存。(这里我们通常会把 L1、L2、L3 称之为缓存,其速度要比内存快得多)

当多个 CPU 共用同一个缓存的时候,因此这里就容易出现一个问题,即缓存一致性。
(intel i7 多核cpu基本架构)
在这里插入图片描述

假设有这么一段程序:

int count = 0;
count = count +1

某一时刻同时有两个不同的线程 t1、t2 分别从不同的 CPU 将其加载到自己的存储单元内做运算,count=count+1 处理的时候就会出现一个问题,不同的 CPU 读取到的 count 都是 1,互相之间互不可见,这样就会导致一个问题,CPU 对 count 实际上做了两次从 count+1 操作,最终结果的结果却是 1。这种不一致的现象我们通常称之为缓存不一致性问题。

解决方案如下:

总线加锁方案
对总线进行加锁处理,当有两个及以上的 CPU 需要通过 IO 总线去访问内存同一个地址数据的时候进行加锁处理,一次只允许一个 CPU 将数据加载到寄存器当中进行计算,然后将其写回到内存设备之后再释放锁,让另一个 CPU 去进行计算工作。
弊端:性能过低,因为 IO 总线上边除了内存设备在访问之外还有其他的硬件设备也在进行访问,会出现堵塞状态。

缓存一致性协议方案
除了上边提及的 IO 总线加锁方案之外,采用缓存一致性协议也可以实现相关的优化处理。这里我们列举最常见的缓存一致性协议 MESI:

M 被修改(Modified)
E 独享的(Exclusive)
S 共享的(Shared)
I 无效的(Invalid)

总线嗅探机制和 MESI 协议
要解决缓存一致性问题,首先要解决的是多个 CPU 之间的数据传播问题。
总线嗅探(Bus Snooping):最常见的一种解决多核 CPU 数据广播问题的方案。本质上就是把所有的读写请求都通过总线(Bus) 广播给所有的 CPU 核心,然后让各个核心去“嗅探”这些请求,再根据本地的情况进行响应。
MESI 协议:(intel模型的cpu)基于总线嗅探机制的缓存一致性协议的一种实现,MESI 协议也是在 Pentium 时代被引入到 Intel CPU。

MESI 协议

Exclusive:只有当前 CPU 的缓存行占有数据,其他 CPU 没有读取数据,当前缓存行的数据和 CPU 中的数据是一致的。
在这里插入图片描述

Shared:当前 CPU 和其他 CPU 中的缓存行数据都是一致的,并且也和内存中的数据保持一致。(当多个 CPU 读取同一个变量到寄存器中进行计算的时候,变量就会变成该状态)
在这里插入图片描述

Modified:意味着当前 CPU 的缓存行数据发生了修改,和内存的数据不一致,此时以 CPU 中的缓存行数据为准(其他 CPU 内部的缓存行不准确)(回写数据阶段,处于 M 状态,此时其他 CPU 通过嗅探机制会监听到此信号,此时其他 CPU 的指定缓存行是处于无效状态 I )

在这里插入图片描述

Invalid:即当前 CPU 的缓存假若失效了,那么此时应该重新从内存中获取数据。而且每次失效都是整个缓存行失效。(一般都是不同核心的cpu对同一块缓存声明为modified,最终谁先写回L3,则其他核心中的缓存就要声明失效)

在这里插入图片描述

不同的cpu如何知道同一个缓存行上边出现数据修改?
通过对缓存行加缓存锁,当某个数据发生修改的时候,会通知到其他的核心的cpu进行缓存更新。

虽然上述的 MESI 协议看似没有问题,但是在某些情况下依然是会有出现失效的情况,下边我列举一些采用缓存一致性导致失效的场景:
1.缓存的数据体积大于一个缓存行的时候,需要加总线锁。一个数据横跨多个缓存行。
2.cpu不支持缓存一致性协议问题。(常见的cpu都支持这种协议)

intel cpu中,当采用mesi协议进行多cpu缓存信息同步失效时,就会采用总线加锁的这种兜底方案进行同步。

伪共享问题
缓存行(cache line)是什么
cache line 是缓存里面的最小单位,不同操作系统的缓存行一般不一样,常见的大小是 32 或 64 byte。内存和高速缓存之间或高速缓存之间的数据移动不是以单个字节或甚至word完成的。相反,移动的最小数据单位称为缓存行,有时称为缓存块。查看自己计算机缓存行的大小命令:

[[email protected] ~]# cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size 
64 

在这里插入图片描述

如图所示,当两个核心对同一个cache line进行访问的时候,core1 只关心对b数据的修改,core2只关心对a数据的修改。那么就会造成在回写数据到L3 cache的时候出现了缓存锁竞争,导致其中的某一方需要将自身缓存声明为invalid,然后重新读取cache line的数据,重新回写。但是整个流程下来实际上对于core2关心的数据并没有任何改动,反而是造成了额外的性能开销,这就是所谓的伪共享问题。

如何查看自己服务器的三级缓存信息?可以通过lscpu指令:

[[email protected] ~]# lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                1
On-line CPU(s) list:   0
Thread(s) per core:    1
Core(s) per socket:    1
Socket(s):             1
NUMA node(s):          1
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 79
Model name:            Intel(R) Xeon(R) CPU E5-2682 v4 @ 2.50GHz
Stepping:              1
CPU MHz:               2494.220
BogoMIPS:              4988.44
Hypervisor vendor:     KVM
Virtualization type:   full
L1d cache:             32K //一级数据缓存
L1i cache:             32K //一级指令缓存
L2 cache:              256K  //二级缓存
L3 cache:              40960K  //三级缓存
NUMA node0 CPU(s):     0

超线程技术
本质就是同一个核心上边有多个pc,register,然后alu在进行计算的时候从不同的pc,register中取出数据,进而实现将一个物理内核模拟成两个逻辑内核,从而提升计算效率。
当两个线程都同时需要某一个资源时,其中一个要暂时停止,并让出资源,直到这些资源闲置后才能继续。因此超线程的性能并不等于两颗CPU的性能。

在这里插入图片描述

CPU执行任务

cpu内部是有划分为多个级别的,ring0,ring1,ring2,ring3,不同级别的cpu相互隔离起到了一定的安全防范效果。
在这里插入图片描述

Linux使用了Ring3级别运行用户态,Ring0标识内核态
Ring0作为内核态,没有使用Ring1和Ring2。
Ring3状态不能访问Ring0的地址 空间,包括代码和数据。

Linux进程的4GB(假设有4GB)地址空间,3G-4G部分大家是共享的,是内核态的地址空间,这里存放在整个内核的代码和所有的内核模块, 以及内核所维护的数据。用户运行一个程序,该程序所创建的进程开始是运行在用户态的,如果要执行文件操作,网络数据发送等操作,必须通过 write,send等系统调用,这些系统调用会调用内核中的代码来完成操作,这时,必须切换到Ring0,然后进入内核地址空间去执 行这些代码完成操作,完成后,切换回Ring3,回到用户态。这样,用户态的程序就不能随意操作内核地址空间,具有一定的安全保护作用。

创建一个进程的细节流程:
在这里插入图片描述
进程既可以在用户空间运行,也可以在内核空间运行,在不同的空间运作的时候,其状态一般被我们称之为用户态或者内核态。
为什么从用户态到内核态的调用过程会比较慢呢?

假设我们用户态的某段程序希望进行一次io读取操作,那么就需要往操作系统发送一次系统调用操作。
1.用户态程序发送一个中断信号(0x80)以及希望调用的内核函数id给到cpu。cpu会根据内核函数的id到中断向量表里面查找对应的函数。

(ps:发送一段中断信号的指令给到cpu内核的ring3,ring3需要暂停当前执行的任务,根据发送过来的内核函数id去中断向量表里面查询。)

2.用户态在发送了相关信号之后,操作系统会进入到内核态,此时需要将用户空间的应用程序上下文做一个保存的操作。将一些堆栈信息,程序计数器等变量进行记录,保存在一个叫做eax的寄存器中。

3.内核态中执行中断例程的system_call,查找到相应例程,执行并且将结果返回给到用户态,恢复用户态的上下文。

4.用户态的程序继续执行。
所以整体的流程为:CPU 寄存器里原来用户态的指令位置,需要先保存起来。接着,为了执行内核态代码,CPU 寄存器需要更新为内核态指令的新位置。最后才是跳转到内核态运行内核任务,最终将结果返回给到用户态。所以一次系统调用需要两次CPU上下文切换,用户态–》内核态,内核态–》用户态。

主要的步骤基本如上,其中每个环节都会有些许耗时,所以整个系统调用过程其实是开销比较大的操作。

光说内核调用慢,难道工程师就不会再做一些额外的优化功能吗?
其实不然,工程师为了提升系统调用的性能,提出了vsyscall调用的优化机制,本质就是在ring0层中设计部分变量映射了ring3层的变量信息,有点类似于加了一层缓存进行管理。

光说内核调用慢,难道工程师就不会再做一些额外的优化功能吗?
其实不然,工程师为了提升系统调用的性能,提出了vsyscall调用的优化机制,本质就是在ring0层中设计部分变量映射了ring3层的变量信息,有点类似于加了一层缓存进行管理。

知乎讨论链接:https://www.zhihu.com/question/412297720

CPU上下文的切换

在理解了系统内核的调用之后,我们再来理解多个进程之间的切换:
多个进程之间的切换
通常进程的切换是需要由内核态进行控制的,所以一般进程切换过程中难免会需要进行系统的内核调用(也就是上边我所讲到的点)。

在内核态中,会有统一的机制进行这些进程的调度管理,这里面需要:
对cpu的寄存器值进行更新。
保存和更新一些进程的虚拟内存信息,栈信息等。 在这里插入图片描述
线程之间的切换
同一个进程之间的线程切换
同一个进程里虚拟内存是共享的,所以只需要更新寄存器相关的地址,在进行上下文切换的时候开销会较小一些,但是依旧难免需要进行系统的调用。

不同进程之间的线程切换
由于虚拟内存不同,所以在进行上下文切换的时候和多进程之间切换差不多。

中断上下文的切换
中断上下文切换的时候,主要发生在用户内核态,中断处理会打断进程的正常的调度和执行,被打断的进程或者线程需要暂时记录下它的上下文信息。过多的上下文切换,会把 CPU 时间消耗在寄存器、内核栈以及虚拟内存等数据的保存和恢复上,从而缩短进程真正运行的时间,导致系统的整体性能大幅下降。

进程上下文切换跟系统调用又有什么区别?
首先,进程是由内核来管理和调度的,进程的切换只能发生在内核态。所以,进程的上下文不仅包括了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的状态。

因此,进程的上下文切换就比系统调用时多了一步:在保存当前进程的内核状态和 CPU 寄存器之前,需要先把该进程的虚拟内存、栈等保存下来;而加载了下一进程的内核态后,还需要刷新进程的虚拟内存和用户栈。

如何分析操作系统中的cpu上下文切换情况?
vmstat指令:每隔三秒查看一次操作系统整体的上下文切换情况
vmstat 3
案例:
在这里插入图片描述

测试机器查看情况

cs(context switch)是每秒上下文切换的次数。
in(interrupt)则是每秒中断的次数。
r(Running or Runnable)是就绪队列的长度,也就是正在运行和等待 CPU 的进程数。
b(Blocked)则是处于不可中断睡眠状态的进程数。

pidstat指令:查看整体的切换次数可能在实际问题排查的时候会感觉还不够用,所以这个时候可以结合pidstat指令查看每个线程的上下文切换详情。

pidstat -w

在这里插入图片描述

自愿上下文切换(cswch)
是指进程无法获取所需资源,导致的上下文切换。比如说, I/O、内存等系统资源不足时,就会发生自愿上下文切换。

非自愿上下文切换(nvcswch)
则是指进程由于时间片已到等原因,被系统强制调度,进而发生的上下文切换。比如说,大量进程都在争抢 CPU 时,就容易发生非自愿上下文切换。

自愿上下文切换变多了,说明进程都在等待资源,有可能发生了 I/O 等其他问题。
非自愿上下文切换变多了,说明进程都在被强制调度,也就是都在争抢 CPU,说明 CPU 的确成了瓶颈。

如何理解进程,线程,纤程

进程
分配资源的单位 (分配虚拟内存空间)
线程
执行程序的调度单位(不会分配任何内存空间,共享进程的内存空间)

纤程(fiber)
JVM 运行在用户空间,当它 new 一个 Thread 时,会对应在 OS 中起一个线程(内核空间,通常消耗内存大小为1m左右),所以这叫重量级线程。而纤程则是在用户态自己起一个逻辑空间,在语言层面进行调度, 和线程比起来,协程的切换不需要操作系统进行保存和恢复CPU 上下文,自己的缓存数据等,因为所有的协程都存在于同一个线程之中,所以协程的切换开销很小。一个线程包含多个纤程,又叫协程,只是协程是一个概念,而纤程是 Windows 系统对协程的一个具体实现。

Fiber在语言层面中通过用户态自己维护的栈空间来管理程序计数器,寄存信息保存等操作,不需要和os内核态打交道,这一点比线程要高效许多。
所谓市面上常说的绿色线程更多的是一个逻辑层面的概念,依赖于虚拟机来实现。操作系统对于虚拟机内部如何进行线程的切换并不清楚,从虚拟机外部来看,或者说站在操作系统的角度看,这些都是不可见的。

了解了纤程之后,我们再来看看go语言内部的设计。goroutine是Go并行设计的核心。goroutine说到底其实就是协程,但是它比线程更小,几十个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享。

目前也有一些人私底下开发了些类库以支持Java语言使用fiber,但是不是特别成熟,截止到JDK13,Java对fiber的支持不是很友好,据说JDK14会较好的支持,蛮期待的。

版权声明
本文为[Danny_idea]所创,转载请带上原文链接,感谢
https://blog.csdn.net/Danny_idea/article/details/114692922

Scroll to Top