掘友等级
获得徽章 0
指令流水线是 CPU 设计中的一个重要思想,对于提升计算机性能有着重要意义。
流水线技术指的是将一条指令的执行过程拆分成多个阶段,每个阶段在一个时钟周期内完成,每一个阶段叫做一个流水线级。
如果我们把一个指令拆分成“取指令 - 指令译码 - 执行指令”这样三个部分,那这就是一个三级的流水线。
现代的 ARM 或者 Intel 的 CPU,流水线级数已经达到了 14 级。
流水线级数也不是越多越好,每一个流水线级之间需要数据传输,当流水线级数增加,数据传输所需要的时间也会随之增加,就会成为性能的瓶颈。 最近被朋友安利了一个叫 Mermaid 的画图工具,一入坑发现真好用!它是类似于 HTML 的标记语言,不要一听到「语言」就腿发软哈,Mermaid 的语法很简单,难度系数和 Markdown 差不多。而且 Mermaid 的能也很强大,支持很多种类型的图,比如流程图、ER 图、类图、甘特图等。具体教程可以看它的官方文档:
mermaid-js.github.io
Mermaid 必须得嵌入到 Markdown 文件中才行。把下面这段代码复制到 Markdown 文件中,就可以绘制一个简单的流程图(图一):
```mermaid
graph TD;
A-->B;
A-->C;
B-->D;
C-->D;
```
用这种标记语言画图的好处就是大大降低了维护的成本。比如你千辛万苦画好了一个流程图,然后突然发现中间漏了一个节点,那你为了补上这个节点,又要拖来拖去的修改,手动调整布局,想想就恶心。但是用标记语言画图的话,也许只需添加一行代码就可以。
比如图一的流程图,如果想加一个与 B、C 并列的节点 E,E 也指向 D,那么把上面的代码稍微改一下就可以:
```mermaid
graph TD;
A-->B;
A-->C;
A-->E;
B-->D;
C-->D;
E-->D;
```
效果如图二所示。Mermaid 在渲染时自动帮你调整了布局,比手动调整效率高很多。
图三四五六展示了 Mermaid 支持的其他类型的图。 一条指令执行的过程可以拆分为 3 个阶段:取指令(Fetch)、指令译码(Decode)、执行指令(Execute)。
取指令是把指令从内存加载到 CPU 的指令寄存器中;指令译码是将指令解析成具体的操作,比如操作哪些寄存器、数据或内存地址;执行指令就是实际去执行这些操作,比如进行算术逻辑操作、数据传输或者直接的地址跳转。
前两个步骤由控制器完成,第三步由算术逻辑单元(ALU),也就是运算器完成。不过,如果是一个简单的无条件地址跳转,那么可以直接在控制器里面完成,不需要用到运算器。
我们把这样的「Fetch - Decode - Execute」循环叫做指令周期。
计算机执行指令的过程就是不断地重复指令周期的过程。 接上一条,这次讲一下为什么有些编程语言中 0.3++0.6=0.899999…
动态链接库就像内存中的共享单车,大家都用这一辆车子,谁需要谁加载,而不是给每个人都配备一辆。
以 C 标准库为例,C 标准库的大小在 1M 以上,Linux 中使用标准输入输出的程序得有上千个,假设同时在内存中运行的程序有 100 个,那如果每个程序都加载一份 C 标准库到内存中的话,100M 内存就出去了。而如果使用动态链接库,只需加载一份就可以了。
由于动态链接库为大家所共享,所以他必须是地址无关的。
为了实现地址无关,动态链接库内部的变量和函数调用使用的都是相对地址。
还有一个问题,应用程序在调用动态链接库中的函数时,可能会有数据的输入和输出,对动态链接库来说,这些数据是外部数据,他在被链接的时候并不知道这些数据的地址。为了解决这个问题,引入了程序链接表(PLT,Procedure Link Table)和全局偏移表(GOT,Global Offset Table),应用程序如果要调用动态链接库里的函数,就去程序链接表里面找对应的地址。GOT 中记录了所有链接动态链接库的程序使用的数据,因为动态链接库在内存中只有一份,而不同的程序用到的数据是不一样的,所以当程序A调用动态链接库时,后者就去 GOT 中查找程序A对应的数据。
PLT 实际上起到了延迟绑定的作用,程序在链接的时候虽然会把动态链接库链接上,但是可能在程序运行时并不会去调用,那么这时就不必在 GOT 中生成数据。只有当程序调用动态链接库时,才会去 PLT 中查找相应的地址,这个时候再往 GOT 里面写入数据。
Linux 下,这些动态链接库是 .so 文件,而 Windows 下的动态链接库就是 .dll 文件。
——《极客时间-深入浅出计算机原理 第10讲》总结 1 KB 大小的内存能不能运行一个 2 GB 的程序?
先说答案,能。
程序运行时,本应需要一段连续的内存,但是现实中由于内存碎片的存在,往往不能满足这一要求。比如运行一个程序需要 128 M 大的内存,但是内存中只有一块连续的 100 M 的内存和一块连续的 64 M 的内存,这样的话,程序就运行不了了,明明内存中空闲内存大于 128 M,程序却运行不了,很不合理。于是人们想到了内存分页的办法。
内存分页就是说,加载程序时不再是给程序分配一整块连续的内存,而是把程序所需的内存分成一个个固定大小的内存页,按页去加载,内存页之间不必是连续的,只要知道这个程序所需的内存页都分布在哪就行。
内存分页是操作系统实现的,对于程序来说,它可以把自己使用的内存看作是连续的,操作系统会将程序中使用的内存地址映射到物理内存。
内存分页不仅解决了内存碎片的问题,也降低了内存交换的成本,而且加载程序时也不需要一次性把整个程序都加载到内存,操作系统可以根据需要去加载部分内存页,这使得小内存运行大程序成为可能。Linux 默认的一个内存页大小是 4 KB。用下面这个命令可以查看:
getconf PAGE_SIZE
那么回到刚才的问题,如何用 1 KB 的内存去运行 2 GB 的程序?我们可以把内存页大小设置为 1 KB,每次只加载 1 个内存页,需要哪一页,就把哪一页从硬盘上交换到内存里面。不过由于磁盘I/O速度远远小于内存的I/O速度,这样运行程序会很卡顿。 1 KB 大小的内存能不能运行一个 2 GB 的程序?
先说答案,能。
程序运行时,本应需要一段连续的内存,但是现实中由于内存碎片的存在,往往不能满足这一要求。比如运行一个程序需要 128 M 大的内存,但是内存中只有一块连续的 100 M 的内存和一块连续的 64 M 的内存,这样的话,程序就运行不了了,明明内存中空闲内存大于 128 M,程序却运行不了,很不合理。于是人们想到了内存分页的办法。
内存分页就是说,加载程序时不再是给程序分配一整块连续的内存,而是把程序所需的内存分成一个个固定大小的内存页,按页去加载,内存页之间不必是连续的,只要知道这个程序所需的内存页都分布在哪就行。
内存分页是操作系统实现的,对于程序来说,它可以把自己使用的内存看作是连续的,操作系统会将程序中使用的内存地址映射到物理内存。
内存分页不仅解决了内存碎片的问题,也降低了内存交换的成本,而且加载程序时也不需要一次性把整个程序都加载到内存,操作系统可以根据需要去加载部分内存页,这使得小内存运行大程序成为可能。Linux 默认的一个内存页大小是 4 KB。用下面这个命令可以查看:
getconf PAGE_SIZE
那么回到刚才的问题,如何用 1 KB 的内存去运行 2 GB 的程序?我们可以把内存页大小设置为 1 KB,每次只加载 1 个内存页,需要哪一页,就把哪一页从硬盘上交换到内存里面。不过由于磁盘I/O速度远远小于内存的I/O速度,这样运行程序会很卡顿。 为什么程序无法同时在 Linux 和 Windows 下运行?
程序要想运行,必须从外部存储(比如硬盘)加载到内存中,保存在外部存储中的程序叫做「可执行文件」,把可执行文件加载到内存中的过程称为装载,是由装载器实现的。Linux 下的可执行文件使用的是一种叫 ELF(Execuatable and Linkable File Format)的文件格式,中文名叫「可执行与可链接文件格式」,Windows 下的可执行文件使用的是一种叫做 PE(Portable Executable Format)的文件格式。Linux 下的装载器只能解析 ELF 格式,而 Windows 下的装载器只能解析 PE 格式,所以同一个程序无法同时在 Linux 和 Windows 上直接运行。
不能直接运行,但是有间接的方式。比如 Java,通过实现不同平台上的 JVM,然后即时翻译 javac 生成的字节码来做到跨平台。使得程序员无需关心操作系统的差别,做到一次编译,处处运行。
再比如 Linux 下著名的开源项目 Wine,就是通过兼容 PE 格式的装载器,使得我们能直接在 Linux 下运行 Windows 程序的,当然,因为很多程序还依赖各种操作系统本身提供的动态链接库,系统调用等等,需要 Wine 提供对应的实现,兼容格式只是万里长征第一步。而现在微软的 Windows 里面也提供了 WSL(Windows Subsystem for Linux),是在 Windows 下运行的一个 Linux 子系统(有点类似于虚拟机,但是比虚拟机轻量级地多),可以解析和加载 ELF 格式的文件。
下一页