计算机组成 -- 指令流水线
单指令周期处理器
一条CPU指令的执行:Fetch -> Decode -> Execute
这个执行过程,最少需要花费一个时钟周期,因为在取指令的时候,需要通过时钟周期的信号,来决定计数器的自增
单指令周期处理器(Single Cycle Processor):在一个时钟周期内,处理器正好能处理一条指令,即CPI为1
时钟周期是固定的,但指令的电路复杂程度是不同的,因此一条指令的实际执行时间是不同的
随着门电路层数的增加,由于门延迟的存在,位数多、计算复杂的指令需要的执行时间会更长
不同指令的执行时间不同,但需要让所有指令都在一个时钟周期内完成,只能把时钟周期和执行时间最长的指令设成一样
快速执行完成的指令,需要等待满一个时钟周期,才能执行下一条指令
CPI能够保持在1,但时钟频率没办法设置太高,因为有些复杂指令是没办法在一个时钟周期内运行完成的
在下一个时钟周期到来,开始执行下一条指令的时候,前一条指令的执行结果可能还没有写入到寄存器里
那么下一条指令读取的数据就是不准确的,会出现错误
无论是PC上使用的Intel CPU,还是手机上使用的ARM CPU,都不是单指令周期处理器 ...
计算机组成 -- 建立数据通路
三种周期指令周期
执行一条指令的过程
Fetch(取得指令)
从PC寄存器里面找到对应的指令地址,根据指令地址从内存里把具体的指令,加载到指令寄存器中
然后把PC寄存器自增,便于未来执行下一条指令
Decode(指令译码)
根据指令寄存器里面的指令,解析成要进行什么样的操作,是R、I、J中的哪一种指令
具体要操作哪些寄存器、数据或者内存地址
Execute(执行指令)
实际运行对应的R、I、J这些特定的指令,进行算术逻辑操作、数据传输或者直接的地址跳转
重复上面步骤
指令周期(Instruction Cycle):Fetch -> Decode -> Execute
涉及的组件
取指令的阶段,指令是放在存储器里的
通过PC寄存器和指令寄存器取出指令的过程,是由控制器(Control Unit)操作的
指令的解码过程,也是由控制器进行的
一旦到了执行指令阶段,R、I型指令都是由算术逻辑单元(ALU)操作
进行算术操作、逻辑操作的R型指令
进行数据传输、条件分支的I型指令
如果是简单的无条件地址跳转,可以直接在控制器里面完成,不需要用到运算器
机器周期
Machin ...
计算机组成 -- 定点数 + 浮点数
浮点数的不精确性12>>> 0.3 + 0.60.8999999999999999
32bit是无法表达所有实数的,只能表达2^32次方不同的数字,差不多40亿
40亿个数字比起无限多的实数集合只是沧海一粟,应该让这40亿个数映射到实数集合上的哪些数字,在实际应用中才最划算
定点数
用4个bit表示0~9的整数,那么32个bit就可以表示8个这样的整数
然后把最右边的2个0~9的整数,当成小数部分;把最左边6个0~9的整数,当成整数部分
这样用32bit可以表示0.00~999999.99这1亿个实数
这种用二进制表示十进制的编码方式,称为BCD编码(Binary-Coded Decimal)
应用非常广泛:超市、银行
缺点
浪费
本来32bit可以表示40亿个不同的数字,但在BCD编码下,只能表示1亿个数
如果精确到分,能够表达的最大金额为100W,非常有限
无法同时表示很大的数字和很小的数字
浮点数
浮点数的科学计数法的表示,有一个IEEE的标准,定义了两个基本的格式
一个是用32bit表示单精度的浮点数,即float
一个是用64bit表示双精度的浮点数,即doubl ...
计算机组成 -- 乘法器
13 × 9
顺序乘法
二进制的乘法,在单个位上,乘数只能是0或1,所以实际的乘法,就退化成位移和加法
被乘数13表示成二进制是1101,乘数9表示成二进制是1001
最后边的个位是1,个位乘以被乘数,被乘数1101得以保留
二位和四位都是0,所以乘以被乘数都是0,保留下来的都是0000
八位是1,仍然需要把被乘数1101复制下来,但需要把复制好的结果向左移动三位
然后把上面四个乘法加位移的结果再加起来
最后一步的加法可以用上一讲的加法器来实现
二进制乘法因为只有0和1两种情况,所以可以做成输入输出都是4个开关,中间有一个开关
中间的开关决定,下面的输出是完全复制输入,还是将输出全部设置为0
位移:左移一位,就错开一位接线;左移两位,就错开两位接线
节约开关(晶体管)
为了节省资源(开关,即晶体管)
类似13×9这样两个四位数的乘法,不需要把四次单位乘法的结果都用四组独立的开关单独记录然后再加起来
否则,如果计算一个32位的整数乘法,就要32组开关,非常浪费
如果顺序地计算,只需要一组开关就好
先拿乘数最右侧的个位乘以被乘数,然后把结果写入用来存放计算结果的开关里面
然后,把被乘 ...
计算机组成 -- 加法器
基本门电路
基本门电路:输入都是两个单独的bit,输出是一个单独的bit
如果要对2个8bit的数字,计算与或非的简单逻辑(无进位),只需要连续摆放8个开关,来代表一个8bit数字
这样的两组开关,从左到右,上下单个的位开关之间,都统一用『与门』或者『或门』连起来
就能实现两个8bit数的AND运算或者OR运算
异或门 + 半加器一bit加法
个位
输入的两位为00和11,对应的输出为0
输入的两位为10和01,对应的输出为1
上面两种关系都是异或门(XOR)的功能
异或门是一个最简单的整数加法,所需要使用的基本门电路
进位
输入的两位为11时,需要向更左侧的一位进行进位,对应一个与门
通过一个异或门计算出个位,通过一个与门计算出是否进位
把这两个门电路打包,叫作半加器(Half Adder)
全加器
半加器只能解决一bit加法的问题,不能解决2bit或以上的加法(因为有进位信号)
二进制加法的竖式,从右往左,第二列称为二位,第三列称为四位,第四列称为八位
全加器:两个半加器和一个或门
把两个半加器的进位输出,作为一个或门的输入
只要两次加法中任何一次需要进位,那么在二位上,就 ...
计算机组成 -- 电路
电报机
电报机的本质:蜂鸣器 + 电线 + 按钮开关
蜂鸣器装在接收方,开关留在发送方,双方通过电线连在一起
继电器
电线的线路越长,电线的电阻越大,当电阻很大,而电压不够时,即使按下开关,蜂鸣器也不会响的
继电器(Relay):为了实现接力传输信号
中继:不断地通过新的电源重新放大已经开始衰减的原有信号
中间所有小电报站都使用『螺旋线圈+磁性开关』的方式,来替代蜂鸣器+普通开关
只在电报的始发和终点用普通开关和蜂鸣器
这样就可以将长距离的电报线路,拆成一个个小的电报线路,接力传输电报信号
继电器别名:电驿,驿站的驿
二进制
有了继电器后,输入端通过开关的『开』和『关』表示1和0,输出端也能表示1和0
输出端的作用,不仅仅是通过一个蜂鸣器或者灯泡,提供一个供人观察的输出信号
还可以通过『螺旋线圈+磁性开关』,使得输出也有『开』和『关』两种状态,表示1和0,作为后续线路的输入信号
与(AND)
在输入端的电路上,提供串联的两个开关,只有两个开关都打开,电路才接通,输出的开关才能接通
或(OR)
在输入端的电路上,提供两条独立的线路到输出端
两条线路上各有一个开关,任意一个开关打开了, ...
计算机组成 -- 二进制编码
补码表示法
原码表示法
0011为3,1011为-3
缺点:0000和1000都表示为0
浪费 + 模凌两可
由此诞生了补码表示法,其实就是一个简单的翻转而已
用补码表示负数,使得整数的相加变得容易,不需要做任何特殊处理,当成普通的二进制相加即可
字符串
ASCII码类似一个字典,用8位二进制中的128个不同的数字,映射到128个不同的字符里
a在ASCII里面是第97个,二进制为0110 0001,对应的十六进制为0x61
字符串9用0011 1001来表示,字符串15用0011 0001和0011 0101来表示,占用更多的空间
因此存储数据的时候,要采用二进制序列化的形式,
而不是简单地把数据通过CSV或者JSON这样的文本格式存储来进行序列化
不管是整点数,还是浮点数,采用二进制序列化比存储文本能节省不少空间
字符集(Charset)和字符编码(Character Encoding)
字符集:字符的集合
Unicode是字符集,包含150种语言的14万个字符
字符编码:对于字符集里的这些字符,怎么用二进制表示出来的一个字典
Unicode可以用UTF-8、UTF-16、U ...
计算机组成 -- 动态链接
静态链接
把对应的不同文件内的代码段合并在一起,成为最后的可执行文件,可以做到代码在开发阶段的复用
很多程序都需要通过装载器装载到内存里面,里面链接好的同样的功能代码,也都需要再装载一遍,再占一遍内存空间
动态链接
动态链接过程中,需要链接的不是存储在硬盘上的目标文件代码,而是加载到内存中的共享库(Shared Libraries)
加载到内存中的共享库会被很多程序的指令调用
Windows的共享库文件是.dll(Dynamic-Link Libary)文件;Linux的共享库文件是.so(Shared Object)文件
地址无关 + 相对地址
要在程序运行时共享代码,这些机器码必须是地址无关的,即编译出来的共享库文件的指令代码,是地址无关码
地址无关码(PIC):Position-Independent Code,无论加载到哪个物理内存地址,都能够正常执行
大部分函数库都能做到地址无关,因为都是接受特定的输入,进行确定的操作,然后给出返回结果
对于所有动态链接共享库的程序来讲
虽然共享库用的都是同一段物理内存地址
但在不同的应用程序里,共享库所在的虚拟内存地址是不同的
不应该要求动 ...
计算机组成 -- 段 + 页
程序装载
背景
通过链接器,把多个目标文件合并成一个最终可执行文件
运行可执行文件时,其实是通过一个装载器,解析ELF或者PE格式的可执行文件
装载器会把对应的指令和数据加载到内存里面,让CPU去执行
装载器需要满足两个条件
可执行程序加载后占用的内存空间应该是连续的
执行程序时,程序计数器是顺序地一条一条指令执行下去
需要同时加载很多个程序,并且不能让程序自己规定在内存中加载的位置
内存地址
虚拟内存地址:指令里用到的内存地址
物理内存地址:内存硬件里的空间地址
一个思路
在物理内存里面找一段连续的内存空间,分配给装载的程序
然后把这段连续的内存空间地址和整个程序指令里指定的内存地址做一个映射
程序里有指令和各种内存地址,而我们只需要关心虚拟内存地址即可
对任何一个程序来说,它所看到的都是同样的内存地址
维护一个虚拟内存到物理内存的映射表
实际程序指令执行的时候,会通过虚拟内存地址,找到对应的物理内存地址,然后执行
因为是连续的内存地址空间,只需要维护映射关系的起始地址和对应的空间大小即可
内存分段
分段(Segmentation):找出一段连续的物理内存和虚拟内存地址 ...
计算机组成 -- ELF + 静态链接
代码拆分源代码add_lib.c12345// add_lib.cint add(int a, int b){ return a+b;}
link_example.c1234567891011// link_example.c#include <stdio.h>int main(){ int a = 10; int b = 5; int c = add(a, b); printf("c=%d\n", c);}
gcc + objdump123$ gcc -g -c add_lib.c link_example.c$ objdump -d -M intel -S add_lib.o$ objdump -d -M intel -S link_example.o
add_lib.o1234567891011121314151617add_lib.o: 文件格式 elf64-x86-64Disassembly of section .text:0000000000000000 <add>:/ ...