0. OS以及组成原理费曼串讲

OS 和组成原理通篇都在讲的事情是怎么用硬件和软件来实现面向用户的计算机,从底层开始。

首先是数据要存储在硬件中,硬件只能存储/区分 0 和 1,这涉及到将人类可读的数据如何转成机器可读,即 10 进制转成 2 进制,又因为 2 进制位实在是太多了,阅读不便,故又引申出 16 进制的表示。

现在硬件可以以 0 和 1 的方式存储在硬件中了,接着就是用什么模式来识别这些 0 和 1,这涉及到读取底层中存储的 0 和 1 的规则(读多少个、每个 0 和 1 表示什么意思),具体来讲分为整数类和浮点数类,对应的标准分别是补码以及 IEEE 754 标准。

有了数据之后则是存储系统,由于硬件限制,存储系统是分为多级的,CPU-Cache-主存-外存,数据在这些存储器之间是如何传递的,以及如何转换的。主要如下:

所有的硬件中,核心的是 CPU,周围硬件都是围绕 CPU 展开,CPU 是 64 位(也叫机器字长)的意味着其一次可以处理 64 个宽度的数据,即一次处理 64 个 0 和 1,这需要 ALU 能够支持 64 位整数运算,也需要周围的通用寄存器(累加、基址、计数、数据、栈基、栈顶、源、目标等)都是 64 位、地址(即指针)也是 64 位. 数据每次会以 64 位(即 8 字节)为单位送到 CPU 的 ALU 中进行计算,这个送就是通过数据总线进行的,也就是说数据总线的宽度就是 64 位。下一级的 Cache 部分,Cache 是按照 Cache 行来组织的,每个行内包含一个 Cache 块,一般情况下是 64 个字节(仅包含数据区,不包含 Tag、index 等辅助信息的存储),即一个 Cache 块内的数据可以让 CPU 读 8 次。(正是利用了局部性原理来提升效率)。再下一级则是主存,主存的块与 Cache 的块大小是完全相等的。

主存中的数据访问速度是计算机运行的时候相对可接受的底线速度,整个程序设计都是基于主存去做的,程序要运行的时候,需要将他的数据存入主存中以供 CPU 随时调用,但主存大小又有限,于是不得不搞一套虚拟内存出来,让程序以为他能够全部存在主存中,也方便应用层的程序员编写程序,这套规则就是请求页式虚拟存储规则,让程序的数据只有在使用的时候才调入主存,不用的时候调出主存,放在外存的交换分区里以供之后调入主存。这整套的请求页式虚拟存储规则由操作系统来实现。

但这样做的话,程序根本不知道他哪块被调入哪块被调出了,所以只能让程序以为他有一段连续的地址空间(即虚拟地址)然后他的 code 就是在这一段地址空间上进行一些操作。而计算机需要一套机制(MMU)来将这种虚拟地址替换换成物理地址,然后结合操作系统进行实际的数据读取和主存页换入换出操作。

完成的流程是:程序的数据和代码(指令)最开始的时候存储在磁盘上,当程序执行的时候,首先 OS 会把磁盘上的指令和数据放入到内存中,OS 可以通过文件类型和文件中的标记来区分出来哪些是指令、哪些是数据,然后 CPU 把 PC 的内容设置为指令最开始的内存地址(虚拟地址,取指令、取数据、写数据均是虚拟地址),同时,由于内存大小可能无法装下完整的程序,所以磁盘上的指令和数据放入内存的时候是按照页进行放的,同时 OS 会维护一个页表,将该进程的所有页的虚拟地址、物理地址都维护在页表的,并将页表的起始地址放到 CPU 中的寄存器中,然后将控制权交给 CPU,这时候 CPU 就是从 PC 的位置开始,取一条指令,这个取的过程则是通过页表起始地址(物理地址)进行 PC 中的地址翻译,将虚拟地址翻译成物理地址,然后使用物理地址访问内存,最后将读取到的数据放入 IR 中,然后 CU 会进行译码以确定是什么指令,接着根据指令中要取得数据从内存中获取对应的数据。

该过程中重要的「接口」:
CPU 是 32 位,意味着数据寄存器 DR 是 32 位,意味着数据总线宽度是 32 位,即一次可以从内存中取 32 位(4 字节)数据出来。取数据的时候,需要使用虚拟地址,虚拟地址也是 32 位大小。由于虚拟地址和物理地址都是页号+页内偏移的结构,他们的页大小也一样。

数据在内存中存储的时候并不是连续的存在每个比特位上,因为读取数据的时候只能一次并行的读地址总线宽度位的数据即 32 位的数据,内存的寻址能力比较强,可以按照字节的方式编址&寻址,而 CPU 通常以字为单位,一个字通常是 4 字节或者是 8 字节(即数据总线宽度)所以如果在内存中存储数据的时候是完全连续的就可能导致一个字节会分布在两个字之间,需要读两次,这会效率很低,所以数据是以对齐的方式存储在内存中的,即每个数据的起始地址都是 2^n 的倍数。这样一次读取或者写入操作在一个总线周期就可以传输完成。

还有就是大小端的问题,大小端是说一个多字节的数据存储的时候,字节之间的顺序,比如现在有一个数字 0x1234 共 16 位,占两个字节,其中 0x12 是高字节,0x34 是低字节,如果按照大端的方式存储(符合人类阅读习惯)的话,就是内存地址从低到高存储 0x12 0x34
如果是小端方式存储的话,就是 0x34 0x12,这种方式符合计算机的逻辑,因为在连续的虚拟内存空间中,用户态可访问的用户空间(其中包含代码、数据等)处于低地址区,而内核态可访问的内核空间(其中包括页表、内核代码等)是从高址开始的。

内核空间从高地址开始的好处是:

  1. 对于应用程序员来说,他们可见的内存地址从 0 开始是最自然的,最符合直觉的方式。
  2. 反过来,如果内核空间在低址,用户访问空指针的时候会直接影响到内核的安全。
  3. 效率角度,所有程序可以共享一个内核空间,当切换程序的时候,系统需要更新页表基址寄存器中的地址,放在高址意味着系统调用或者中断的时候可以不需要重新加载整个页表,即所有的程序再进行虚拟-物理地址转换的时候,访问 TLB 和页表的时候都可以使用相同的地址。

注意字节内部的顺序不变。

现在 CU 拿到了译码之后的执行,CU 会生成信号和时钟,驱动电路硬件完成执行的执行。
完整的流程则是取指、译码、执行、访存、写回。前三步是任何指令都有的,但是后两部则不是,可能一些指定不需要读取内存的数据,或者不需要将数据写回到内存,但是在五阶段流水线中,这几步是都会出现的,只不过对应的阶段留空就可以。
由于这五个步骤是由 CPU 中的不同部分的硬件实现的,所以可以搞成流水线,一个时钟周期内多个部分硬件分别工作,这样效率就会快很多。
能这样做的前提就是这些一起工作的硬件部分不能够在同一时间访问同一个硬件(结构冒险)、不能下一个指令依赖上一个指令的数据(数据冒险)、不能改写 PC(控制冒险)