Skip to content

COA 2021

Posted on:2021.12.21

TOC

Open TOC

大纲

20f1d4a2eab84c7cb0a563345b9ed28e.jpg

计算机系统概述

W 1 / 2

Y 1

计算机

通用电子数字计算机:

组织与结构

操作单元及其相互连接

包括:控制信号,存储技术,……

例如:实现乘法是通过硬件单元还是重复加法?

直接影响程序逻辑执行的属性

包括:指令集,表示数据类型的位数,…

例如:是否有乘法指令?

冯 · 诺伊曼结构

5e4b39dfbb7d4543a7a746fc027b4ea3.jpg

CPU 性能

计算机的顶层视图

W 3

Y 1

计算机的功能视图

8abd3760522f441a8f4fd4c682a56f08.jpg

计算机的顶层视图

ca59bcd8377c4c82aec864338e32189f.jpg

计算机体系结构遇到的问题及解决方案

数据的机器级表示

W 9

Y 2

编码

用少量简单的基本符号对复杂多样的信息进行一定规律的组合

8f29416c58344a6eb1ca5914d298a68c.jpg

整数的二进制表示

浮点数的二进制表示

IEEE 754

c65a33cade8e48ebb6dc86e9c490f727.jpg

二进制编码的十进制数表示

自然 BCD 码

NBCD 码

8421 码

Transformer

主要讲一讲 floatToBinary

public String floatToBinary(String floatStr) {
Float num = Float.parseFloat(floatStr);
if (num > Float.MAX_VALUE)
return "+Inf";
else if (num < -Float.MAX_VALUE)
return "-Inf";
else {
StringBuilder builder = new StringBuilder();
if (num < 0) {
builder.append("1");
num = -num;
} else {
builder.append("0");
}
if (num < Float.MIN_NORMAL) { // 非规格化数
builder.append("00000000");
for (int i = 0; i < 149; ++i) {
num *= 2;
}
int frac = Math.round(num);
for (int i = 22; i >= 0; --i) {
if (Math.pow(2, i) <= frac) {
builder.append("1");
frac -= Math.pow(2, i);
} else {
builder.append("0");
}
}
return builder.toString();

由于非规格化数的最大值为 2126×(1223)=212621492^{-126} × (1-2^{-23}) = 2^{-126} - 2^{-149}

所以我们安全的将浮点数真值乘上 21492^{149},其最大值仅为 22312^{23} -1,且必为整数

接着我们将这个整数转换为二进制,即得 frac 部分

} else { // 规格化数
int exp = 0;
for (int i = 127; i >= -126; --i) {
if (num >= Math.pow(2, i)) {
exp = i;
break;
}
}
num = num / (float) Math.pow(2, exp) - 1;
for (int i = 7; i >= 0; --i) {
if (Math.pow(2, i) <= exp + 127) {
builder.append("1");
exp -= Math.pow(2, i);
} else {
builder.append("0");
}
}
for (int i = 0; i < 23; ++i) {
num *= 2;
}
long frac = Math.round(num);
for (int i = 22; i >= 0; --i) {
if (Math.pow(2, i) <= frac) {
builder.append("1");
frac -= Math.pow(2, i);
} else {
builder.append("0");
}
}
return builder.toString();
}
}
}

由于规格化数的范围为 [2126,2127×(2223)][2^{-126},2^{127}×(2-2^{-23})]

我们首先需要提取 exp 和 frac 部分,即将浮点数真值转化为 2exp×(frac+1)2^{exp}×(frac+1) 的形式

此时的 exp[126,127]exp \in[-126,127],需要加上偏置常数 127

而此时的 frac[0,1]frac \in [0,1],我们需要乘上 2232^{23},使其化为一个整数,再转换为二进制即可

数据校验码

W 5

Y 2

纠错

117f6cb103c14ff1abdf61b62822696f.jpg

注意图中的符号有略微不同:

443d6af42ea2447cab631dd407008594.jpg

奇偶校验码

海明码

汉明码 Pa■t1,如何克服噪■

汉明码 part2,优雅的全貌

数据位 MM 和校验位 KK 的关系:

2KK+M+12^K \ge K + M + 1

拓展:SEC-DED

Single-error correction, double-error detection

循环冗余校验

奇偶校验 → 将数据分为字节

循环冗余校验 → 流格式的数据

模 2 除法

1111000 对除数 1101 做模 2 除法:

1 0 1 1 // 商
---------------
1 1 1 1 0 0 0 // 被除数
1 1 0 1 // 被除数首位为 1,上除数 1101
---------------
0 1 0 0 // 按位异或
0 0 0 0 // 被除数首位为 0,上 0000
---------------
1 0 0 0 // 按位异或
1 1 0 1 // 被除数首位为 1,上除数 1101
---------------
1 0 1 0 // 按位异或
1 1 0 1 // 被除数首位为 1,上除数 1101
---------------
1 1 1 // 按位异或,得到余数

请与算术除法区分

编程实现

public static char[] Calculate(char[] data, String polynomial) {
String trans = "";
for (int i = 0; i < data.length; ++i) {
trans += data[i];
}
for (int i = 0; i < polynomial.length() - 1; ++i) {
trans += "0";
}
int[] tmp = new int[polynomial.length()];
for (int i = polynomial.length() - 1; i < trans.length(); ++i) {
if (i == polynomial.length() - 1) {
String sub = trans.substring(0, i + 1);
assert sub.startsWith("1");
for (int j = 1; j < polynomial.length(); ++j) {
tmp[j] = sub.charAt(j) - '0' ^ polynomial.charAt(j) - '0';
}
} else {
String sub = "";
for (int j = 1; j < tmp.length; ++j) {
sub += tmp[j];
}
sub += trans.charAt(i);
if (sub.startsWith("1")) {
for (int j = 1; j < polynomial.length(); ++j) {
tmp[j] = sub.charAt(j) - '0' ^ polynomial.charAt(j) - '0';
}
} else {
for (int j = 1; j < polynomial.length(); ++j) {
tmp[j] = sub.charAt(j) - '0' ^ 0;
}
}
}
}
String res = "";
for (int i = 1; i < tmp.length; ++i) {
res += tmp[i];
}
return res.toCharArray();
}

基本思想是使用 tmp 数组存放中间 polynomial 长度的运算结果

整数运算

W 9

Y 3

全加器

注意这里加法代表按位或,乘法代表按位与

拓展:半加器

c0d5f4db0d884730a105245c10711ca1.jpg

使用半加器实现全加器:

6066b70b1c27464b8cd73436259273f6.jpg

请自行验证等价性

设:

则:

串行进位加法器

2f52cbc925e64eff9b60d499a47949ec.jpg

沿用上述的延迟,有:

全先行进位加法器

Ci+1=XiYi+(Xi+Yi)CiC_{i+1} = X_iY_i + (X_i+Y_i)C_i

pi=XiYi,gi=Xi+Yip_i = X_iY_i,g_i = X_i+Y_i

Ci+1=pi+giCiC_{i+1} = p_i + g_iC_i

将所有的 CiC_i 展开即可

07969f4115454a51918c65d16fa2b4c3.jpg

沿用上述的延迟,有:

部分先行进位加法器

模块内先行进位,模块间串行进位

e278d3920de54618aceb68bbd3557889.jpg

沿用上述的延迟,有:

拓展:延迟优化

成功说服 rtw 使其加入下一届课件

我们的想法是让块间的进位信号也能快速传递

由上述推导可知 C8=G0+P0C0C_8 = G_0 + P_0C_0,其中:

G0=g7+p7g6+p7p6g5++p7p6p5p4p3p2p1g0G_0=g_7+p_7g_6+p_7p_6g_5+\cdots+p_7p_6p_5p_4p_3p_2p_1g_0

P0=p7p6p5p4p3p2p1p0P_0=p_7p_6p_5p_4p_3p_2p_1p_0

类似的有 C8i+8=Gi+PiC8iC_{8i+8} = G_{i} + P_iC_{8i},其中:

Gi=g8i+7+p8i+7g8i+6+p8i+7p8i+6g8i+5++p8i+7p8i+6p8i+5p8i+4p8i+3p8i+2p8i+1g8iG_i=g_{8i+7}+p_{8i+7}g_{8i+6}+p_{8i+7}p_{8i+6}g_{8i+5}+\cdots+p_{8i+7}p_{8i+6}p_{8i+5}p_{8i+4}p_{8i+3}p_{8i+2}p_{8i+1}g_{8i}

Pi=p8i+7p8i+6p8i+5p8i+4p8i+3p8i+2p8i+1p8iP_i=p_{8i+7}p_{8i+6}p_{8i+5}p_{8i+4}p_{8i+3}p_{8i+2}p_{8i+1}p_{8i}

于是我们可以使用 GiG_iPiP_i,在电路复杂性和延迟之间进行进一步优化

e894e98bc5924f249547c84c371e6672.jpg

沿用上述的延迟,有:

加法与减法

4e2c972cc07d4a17aab27c52caad9b48.jpg

一些标志信息:

ZFSFCFOF
有符号运算是否为零最高位符号位无意义CnCn1C_n\oplus C_{n-1}
无符号运算是否为零无意义SubCnSub\oplus C_n无意义

注意减法是将减数按位取反,并置 SubSub (即 CinC_{in}) 为 1

这里的 CnC_nCoutC_{out}

乘法

无符号数

手工演算——硬件优化

fca2e9c5c6eb4c1e881c3432aa11c147.jpg

PP 初始置全 00

有符号数

布斯算法

521e4e2dbc8c47f1805bace60f7f5ba2.jpg

注意 YY 多了一位 00

右移时为算术右移

除法

无符号数

手工演算——硬件优化

4cba6c1f6be44b0aa1cea8b9078761ab.jpg

余数 寄存器和 余数 / 商 寄存器初始置被除数的扩展(有符号数,符号扩展;无符号数,零扩展)

有符号数

关于补码除法的余数:

  • 余数与被除数同号
  • 余数的绝对值尽量小

如果余数和除数的符号相同,做减法;如果余数和除数的符号不同,做加法

观察运算后余数是否变号,来判断够不够减

运算后,如果除数和被除数不同号,则将商替换为其相反数

==Talk is cheap. Show me the code. ——Linus Torvalds==

符号约定:

XX - 被除数

YY - 除数

RR - 余数

QQ - 商

nn - 补码位数

示例算术为 7/3=2(1)-7/3=-2(-1),使用 4 位补码表示:

X - 1001
Y - 0011

首先判断除数是否为 0,接下来置 RRQQ 为被除数的符号扩展

R - 1111
Q - 1001

对应代码:

String destStr = dest.toString();
StringBuilder R = new StringBuilder();
for (int i = 31; i >= 0; --i) R.append(destStr.charAt(0)); // 符号扩展
StringBuilder Q = new StringBuilder(destStr);

第一步,需要判断除数和被除数符号是否相同,此处不相同:

sign(X) != sign(Y)
R = R + Y
R1 = 0010

对应代码:

// initial
if (src.toString().charAt(0) - '0' == dest.toString().charAt(0) - '0') {
// 如果除数和被除数符号相同,则做减法;否则,做加法
DataType res = sub(src, new DataType(R.toString()));
R = new StringBuilder(res.toString());
} else {
DataType res = add(src, new DataType(R.toString()));
R = new StringBuilder(res.toString());
}
boolean flag = R.toString().charAt(0) - '0' == src.toString().charAt(0) - '0';

得到 R1R_1,并设置 flag 判断余数和除数符号是否相同

第二步,迭代 nn 次,根据余数和除数符号进行运算:

---------
| R | Q |
---------
R1 = 0010
sign(R1) == sign(Y) →┐
|
0010 1001 |
0101 0011 ←┤ // 左移,上商 1
|
R2 = 2R1 - Y = 0010 ←┘
sign(R2) == sign(Y)
0010 0011
0100 0111
R3 = 2R2 - Y = 0001
sign(R3) == sign(Y)
0001 0111
0010 1111
R4 = 2R3 - Y = 1111
sign(R4) != sign(Y)
1111 1111
1111 1111
R5 = 2R4 + Y = 0010
sign(R5) == sign(Y) →┐
|
0010 1110 |
1101 ←┘

注意最后一次只需要左移 QQ

对应代码如下,请注意 flag 的更新和左移操作的位置:

if (flag) {
// 如果余数和除数符号相同,则商𝑄𝑛=1;否则商𝑄𝑛=0
// 同时左移商和余数
R.append(Q.charAt(0));
R.deleteCharAt(0);
Q.deleteCharAt(0);
Q.append("1");
} else {
R.append(Q.charAt(0));
R.deleteCharAt(0);
Q.deleteCharAt(0);
Q.append("0");
}
// iterate
for (int i = 31; i >= 0; --i) {
if (flag) {
// 如果余数和除数符号相同,𝑅𝑖+1=2𝑅𝑖−𝑌;否则𝑅𝑖+1=2𝑅𝑖+𝑌
// 第一次循环,在之前已经移位了
DataType res = sub(src, new DataType(R.toString()));
R = new StringBuilder(res.toString());
} else {
DataType res = add(src, new DataType(R.toString()));
R = new StringBuilder(res.toString());
}
// update flag
flag = R.toString().charAt(0) - '0' == src.toString().charAt(0) - '0';
if (flag) {
// 如果余数和除数符号相同,则商𝑄𝑛=1;否则商𝑄𝑛=0
// 同时左移商和余数
if (i != 0) {
R.append(Q.charAt(0));
R.deleteCharAt(0);
}
Q.deleteCharAt(0);
Q.append("1");
} else {
if (i != 0) {
R.append(Q.charAt(0));
R.deleteCharAt(0);
}
Q.deleteCharAt(0);
Q.append("0");
}
}

第三步,一些结果上的调整:

sign(X) != sign(Y)
Q = 1101
Q = Q + 1 = 1110
sign(X) != sign(R)
sign(X) != sign(Y)
R = 0010
R = R - Y = 0010 + 1101 = 1111

对应代码:

// final
// 如果除数和被除数符号不同,商加 1
if (src.toString().charAt(0) - '0' != dest.toString().charAt(0) - '0') {
DataType res = add(new DataType("00000000000000000000000000000001"), new DataType(Q.toString()));
Q = new StringBuilder(res.toString());
}
// 若被除数和余数符号不同:
// 若被除数和除数符号不同,余数减除数,反之加除数
if (R.toString().charAt(0) - '0' != dest.toString().charAt(0) - '0') {
if (src.toString().charAt(0) - '0' != dest.toString().charAt(0) - '0') {
DataType res = sub(src, new DataType(R.toString()));
R = new StringBuilder(res.toString());
} else {
DataType res = add(src, new DataType(R.toString()));
R = new StringBuilder(res.toString());
}
}
remainderReg = new DataType(R.toString());
// bug fix
if (R.toString().equals(src.toString())) {
// 若除数和余数相同,商加 1,余数置 0
remainderReg = new DataType("00000000000000000000000000000000");
DataType res = add(new DataType("00000000000000000000000000000001"), new DataType(Q.toString()));
Q = new StringBuilder(res.toString());
} else if (sub(new DataType(src.toString()), new DataType("00000000000000000000000000000000")).toString().equals(R.toString())) {
// 若除数的相反数和余数相同,商减 1,余数置 0
remainderReg = new DataType("00000000000000000000000000000000");
DataType res = sub(new DataType("00000000000000000000000000000001"), new DataType(Q.toString()));
Q = new StringBuilder(res.toString());
}
return new DataType(Q.toString());

Bug Fix:

浮点数运算

==Talk is cheap. Show me the code. ——Linus Torvalds==

W 9

Y 3

加法和减法

基本思想

  1. 将减法化归为加法
  2. 为了避免原码加法运算,需要填充一些位并转换为补码表示,计算后再将结果转换为原码表示
  3. 基于 GRS 保护位标准

实现

// 处理边界情况
if (cornerCheck(addCorner, destStr, srcStr) != null)
return new DataType(cornerCheck(addCorner, destStr, srcStr));
if (destStr.matches(IEEE754Float.NaN_Regular) || srcStr.matches(IEEE754Float.NaN_Regular))
return new DataType(IEEE754Float.NaN);
// 提取符号、阶码、尾数
String destSym = destStr.substring(0, 1);
String srcSym = srcStr.substring(0, 1);
String destExp = destStr.substring(1, 9);
String srcExp = srcStr.substring(1, 9);
String destFrac = destStr.substring(9);
String srcFrac = srcStr.substring(9);
// 非规格化 → 规格化
if (destExp.equals("00000000")) {
destExp = "00000001";
destFrac = "0".concat(destFrac).concat("000");
} else {
destFrac = "1".concat(destFrac).concat("000");
}
if (srcExp.equals("00000000")) {
srcExp = "00000001";
srcFrac = "0".concat(srcFrac).concat("000");
} else {
srcFrac = "1".concat(srcFrac).concat("000");
}

提取结束后尾数的位数应该等于 1 + 23 + 3 = 27

采用小阶向大阶看齐的方式,小阶增加至大阶,同时尾数右移,保证对应真值不变:

// 对阶
int destExpNum = Integer.parseInt(transformer.binaryToInt(destExp));
int srcExpNum = Integer.parseInt(transformer.binaryToInt(srcExp));
int resExpNum;
if (destExpNum <= srcExpNum) {
destFrac = rightShift(destFrac, srcExpNum - destExpNum);
resExpNum = srcExpNum;
} else {
srcFrac = rightShift(srcFrac, destExpNum - srcExpNum);
resExpNum = destExpNum;
}

尾数右移时不能直接将最低位去掉,使用 rightShift 对尾数进行右移。

if (destSym.equals("1")) {
StringBuilder builder = new StringBuilder();
builder.append("1");
// padding
builder.append("1");
builder.append("1");
builder.append("1");
builder.append("1");
int index = destFrac.lastIndexOf("1");
if (index != -1) {
for (int i = 0; i < index; ++i)
builder.append(destFrac.charAt(i) == '0' ? '1' : '0');
for (int i = index; i < destFrac.length(); ++i)
builder.append(destFrac.charAt(i));
} else {
for (int i = 0; i < destFrac.length(); ++i)
builder.append(destFrac.charAt(i) == '0' ? '1' : '0');
}
destFracSym = builder.toString();
} else {
StringBuilder builder = new StringBuilder();
builder.append("0");
// padding
builder.append("0");
builder.append("0");
builder.append("0");
builder.append("0");
for (int i = 0; i < destFrac.length(); ++i)
builder.append(destFrac.charAt(i));
destFracSym = builder.toString();
}
...
resFracSym = alu.add(new DataType(srcFracSym), new DataType(destFracSym)).toString();
if (resFracSym.startsWith("1")) {
resSymNum = 1;
resFrac = resFracSym.substring(4);
StringBuilder builder = new StringBuilder();
int index = resFrac.lastIndexOf("1");
if (index != -1) {
for (int i = 0; i < index; ++i)
builder.append(resFrac.charAt(i) == '0' ? '1' : '0');
for (int i = index; i < resFrac.length(); ++i)
builder.append(resFrac.charAt(i));
} else {
for (int i = 0; i < resFrac.length(); ++i)
builder.append(resFrac.charAt(i) == '0' ? '1' : '0');
}
resFrac = builder.toString();
} else {
resSymNum = 0;
resFrac = resFracSym.substring(4);
}

填充一些位至 32 位,并转换为补码表示,从而调用 alu 类的 add 方法,最后还要将结果转换为原码表示。

此时的 resFrac 有 28 位。

// 右规
if (resFrac.startsWith("1")) {
if (resExpNum == 254) {
return resSymNum == 0 ?
new DataType(IEEE754Float.P_INF) :
new DataType(IEEE754Float.N_INF);
} else {
resExpNum++;
resFrac = rightShift(resFrac, 1);
}
}
// 左规
while (!resFrac.substring(1).startsWith("1")) {
if (resExpNum > 1) {
resFrac = alu.leftShift(resFrac, 1);
resExpNum--;
} else if (resExpNum == 1){
resExpNum--;
break;
}
}
resFrac = resFrac.substring(1);

右规:当运算后尾数大于 27 位时(检查 resFrac 最高位),此时应该将尾数右移 1 位并将阶码加 1

左规:当运算后尾数小于 27 位时(检查 resFrac 次高位,由于是规格化表示,次高位为 1 表示尾数为 27 位),此时应该不断将尾数左移并将阶码减少,直至尾数达到 27 位或阶码已经减为 0

最后别忘了去掉 resFrac 最高位。

String res = round(resSymNum == 1 ? '1' : '0',
transformer.intToBinary(String.valueOf(resExpNum)).substring(24),
resFrac);
return new DataType(res);

使用 round 进行舍入,传入的参数为 1 位符号位、8 位阶码、27 位尾数。

补充:原码加法

我们只考虑加法,因为容易发现减法是可以化归为加法的

例子:

0.8125 + 0.625 = 1.4375
1101
+ 1010
------
10111
0111
0.4375
0.8125 + (-0.625) = 0.1875
1101
+ 0110
------
10011
0.1875
0.625 + (-0.8125) = -0.1875
1010
+ 0011
------
1101
0011
-0.1875

注意这里的符号只用于判断,不参与计算

乘法和除法

类似加减法,略去。

类似加减法,略去。

对于阶码的计算,与加减法运算不同的是,乘除法运算不再需要对阶操作,而是直接计算结果阶码。其计算过程分别为:

  1. 乘法:尾数相乘,阶码相加后减去偏置常数
  2. 除法:尾数相除,阶码相减后加上偏置常数

对于乘法而言,为 27 位无符号数乘法,返回 54 位结果:

// 27 位无符号数乘法
// 返回 54 位结果
private String multiply_27(String a, String b) {
assert a.length() == 27 && b.length() == 27;
StringBuilder res = new StringBuilder();
for (int i = 0; i < 27; ++i)
res.append("0");
for (int i = 0; i < 27; ++i)
res.append(b.charAt(i));
StringBuilder temp = new StringBuilder();
for (int i = 0; i < 27; ++i) {
if (res.charAt(53) == '1') {
temp.delete(0, 27);
temp.append(a);
} else {
temp.delete(0, 27);
for (int j = 0; j < 27; ++j)
temp.append("0");
}
int carry = 0;
for (int j = 26; j >= 0; --j) {
int p = res.charAt(j) - '0';
int q = temp.charAt(j) - '0';
int ans = p ^ q ^ carry;
carry = (p & carry) | (q & carry) | (p & q);
res.replace(j, j + 1, String.valueOf(ans));
}
res.deleteCharAt(53);
res.insert(0, String.valueOf(carry));
}
return res.toString();
}

其调用上下文如下:

// 尾数相乘
String resFrac = multiply_27(srcFrac, destFrac);
// System.out.println(resFrac);
resExpNum += 1;
// 尾数左移,阶码减 1
while (resFrac.charAt(0) == '0' && resExpNum > 0) {
StringBuilder temp = new StringBuilder(resFrac);
resFrac = temp.deleteCharAt(0).append(0).toString();
resExpNum--;
}
// 尾数右移,阶码加 1
while (!resFrac.startsWith("000000000000000000000000000")
&& resExpNum < 0) {
resFrac = rightShift(resFrac, 1);
resExpNum++;
}
if (resExpNum > 254) {
// 阶码上溢
return new DataType(resSymNum == 0 ?
IEEE754Float.P_INF : IEEE754Float.N_INF);
} else if (resExpNum < 0) {
// 阶码下溢
return new DataType(resSymNum == 0 ?
IEEE754Float.P_ZERO : IEEE754Float.N_ZERO);
} else if (resExpNum == 0) {
// 尾数右移一次化为非规格化数
resFrac = rightShift(resFrac, 1);
} else {
// 此时阶码正常,无需任何操作
}

由于乘积的隐藏位为 2 位,所以需要通过阶码加 1 的方式来保证尾数的隐藏位均为 1 位。

对于除法而言,为 27 位无符号数除法,要求被除数为 54 位,除数为 27 位,返回商的低 27 位:

// 27 位无符号数除法
// 要求被除数为 54 位,除数为 27 位
// 返回 27 位结果
public String divide_27(String a, String b) {
assert a.length() == 54 && b.length() == 27;
assert !b.equals("000000000000000000000000000");
// --- 54-bit --- --- 54-bit ---
StringBuilder res = new StringBuilder();
for (int i = 0; i < 54; ++i)
res.append(0);
for (int i = 0; i < 54; ++i)
res.append(a.charAt(i));
StringBuilder pos_b = new StringBuilder();
for (int i = 0; i < 27; ++i)
pos_b.append(0);
for (int i = 0; i < 27; ++i)
pos_b.append(b.charAt(i));
StringBuilder neg_b = new StringBuilder();
int index = pos_b.lastIndexOf("1");
for (int i = 0; i < index; ++i)
neg_b.append(pos_b.charAt(i) == '1' ? '0' : '1');
for (int i = index; i < 54; ++i)
neg_b.append(pos_b.charAt(i));
for (int i = 0; i < 54; ++i) {
if (compare_54(res.substring(0, 54), pos_b.toString())) {
int carry = 0;
for (int j = 53; j >= 0; --j) {
int p = res.charAt(j) - '0';
int q = neg_b.charAt(j) - '0';
int ans = p ^ q ^ carry;
carry = (p & carry) | (q & carry) | (p & q);
res.replace(j, j + 1, String.valueOf(ans));
}
res.deleteCharAt(0);
res.append(1);
} else {
res.deleteCharAt(0);
res.append(0);
}
}
StringBuilder ans = new StringBuilder(res.substring(54 + 27)); // 商的低 27 位
return ans.toString();
}

关于位数的考量:

所以在调用上下文中需要将被除数末尾加上 27 个零:

destFrac += "000000000000000000000000000";
String resFrac = divide_27(destFrac, srcFrac);
// System.out.println(resFrac);
if (resExpNum > 254) {
return new DataType(resSymNum == 0 ?
IEEE754Float.P_INF : IEEE754Float.N_INF);
} else if (resExpNum < 0) {
return new DataType(resSymNum == 0 ?
IEEE754Float.P_ZERO : IEEE754Float.N_ZERO);
} else if (resExpNum == 0) {
// 尾数右移一次化为非规格化数
resFrac = rightShift(resFrac, 1);
} else {
// 此时阶码正常,无需任何操作
}

目前只考虑规格化数的情况,所以可以省略左规和右规的步骤。

二进制编码的十进制数运算

加法

两处观察:

减法

需要对减数进行预处理:

0000 → 1001
0001 → 1000
0010 → 0111
...
1001 → 0000

记该操作为反转

最后一位需要加 1,然后同加法运算之后,需要调整结果

编程实现

核心思想还是化归,我们只需处理如下两种情形:

内部存储器

W 5

Y 7

存储器

存储器由一定数量的单元构成,每个单元可以被唯一标识,每个单元都有存储一个数值的能力:

存储器层次结构

b9879a4fcb2047098151decf3200a41f.jpg

半导体存储器类型

5958dcbf57964b029bd8a6ceffb4fefb.jpg

RAM

3637b5c68ce240bbba1aaf6d79b0196e.jpg

DRAM

在电容器上用电容充电的方式存储数据,电容器中有无电荷在分别代表二进制的 1 与 0

需要周期地充电刷新以维护数据存储

SRAM

使用传统触发器、逻辑门配置来存储二进制值

比较

DRAM 比相应的 SRAM 密度更高,价格更低,速度更慢

SRAM 一般用于高速缓存,DRAM 用于主存

高级的 DRAM 架构

SDRAM

Synchronous DRAM

SDRAM 与处理器的数据交互同步与外部的时钟信号,并且以处理器 / 存储器总线的最高速度运行,而不需要插入等待状态

DDR SDRAM

每个时钟周期发送两次数据,一次在时钟脉冲的上升沿,一次在下降沿

DDR → DDR2 → DDR3 → DDR4

  • 增加操作频率

  • 增加预取缓冲区

ROM

PROM

芯片出厂时内容为全 0,只能写入一次,电写入

可擦除存储器

EPROM

光擦除,电写入

芯片级

EEPROM

电擦除,电写入

字节级

快闪存储器

电擦除,电写入

块级

比较

EPROM < 快闪 < EEPROM

价格和功能 ↑

从位元到主存

位元

半导体存储器的基本元件,用于存储 1 位数据

特性:

寻址单元

由若干相同地址的位元组成

寻址模式:

存储阵列

由大量寻址单元组成

以 DRAM 为例:

33456d66a417448aa31b97c8ff02baeb.jpg

寻址

地址译码器

减少地址引脚线:

刷新

按行刷新

集中式刷新,Centralized refresh

分散式刷新,Decentralized refresh

异步刷新,Asynchronous refresh

芯片

以 DRAM 为例,其芯片引脚可能如下:

29624149d7fa40618d60c1bcb1a13000.jpg

模块组织

主存

使用插槽组合多个存储模块

Cache

W 4

Y 7

局部性原理

时间局部性

int factorial = 1;
for (int i = 2; i <= n; i++) {
factorial = factorial * i;
}

空间局部性

for (int i = 0; i < num; i++) {
score[i] = final[i] * 0.4 + midterm[i] * 0.3 + assign[i] * 0.2 + activity[i] * 0.1;
}

平均访问时间

设命中率为 pp

TCT_C 为访问 Cache 的时间

TMT_M 为访问主存的时间

则总时间为 TA=pTC+(1p)(TC+TM)=Tc+(1p)TMT_A = p T_C + (1-p)(T_C+T_M)=T_c+(1-p)T_M

可以推出,若 TA<TMT_A<T_M,则 p>TCTMp>\frac{T_C}{T_M}

Cache 容量

容量 ↑

命中率 pp

访问 Cache 的时间 TCT_C

映射功能

d164824765814dfe88dfe35e6eb39054.jpg

关联度

一个主存块映射到 cache 中可能存放的位置个数

影响因素:

替换算法

对于关联度 2\ge 2 而言:

写策略

主存和 cache 的一致性:当 cache 中的某个数据块被替换时,需要考虑该数据块是否被修改

行大小

行大小 ↑

Cache 数目

一级 vs. 多级

统一 vs. 分立

编程实现

差点把 coa2021-programming08 搞没了,Git 是个危险的东西……

单独将 src 目录拷贝到一个新的文件夹中,在其中创建项目,初始化 Git,并创建 revised 分支书写标程的思路

基本内容

Cache 容量:

虚拟地址与 Cache 信息的互相转换:

VA (32 位) = 标记和组号 (22 位) + 块内地址 (10 位)

标记和组号即 blockNO

注意区分组号 (setNO) 和行号 (rowNO)

标记的占位长度为 2222 位,若关联度为 CC,则有效长度为 12+log2(C)12 + log_2(C)

考虑 1KB 对齐的情形,可以通过行号反推标记和组号,即 calculatePAddr 方法:

public String calculatePAddr(int rowNO) {
// TODO
char[] tag = cache.get(rowNO).tag;
int setLength = (int) (Math.log(SETS) / Math.log(2));
String setNOStr = transformer.intToBinary(String.valueOf(rowNO / setSize)).substring(32 - setLength);
int tagLength = 22 - setLength;
StringBuilder builder = new StringBuilder();
for (int i = 0; i < tagLength; ++i)
builder.append(tag[i]);
builder.append(setNOStr).append("0000000000");
return builder.toString();
}

通过 rowNO / setSize 得到组号

映射策略

为简单期间,考虑 VA 为 1KB 对齐的情形,Cache 类的 read 和 write 函数都会通过 fetch 函数得到行号。并进行读写操作:

private int fetch(String pAddr) {
// TODO
int blockNO = getBlockNO(pAddr);
int rowNO = map(blockNO);
if (rowNO == -1) {
rowNO = loadBlock(blockNO);
}
assert rowNO != -1;
return rowNO;
}

fetch 函数将功能分解,首先取标记和组号,即 VA 的前 22 位,然后调用 map 方法:

private int map(int blockNO) {
// TODO
int setNO = blockNO % SETS;
char[] addrTag = calculateTag(blockNO);
int st = setNO * setSize;
int ed = (setNO + 1) * setSize;
for (int rowNO = st; rowNO < ed; ++rowNO) {
if (isTagMatch(rowNO, addrTag)) {
replacementStrategy.hit(rowNO);
return rowNO;
}
}
return -1;
}

map 方法根据具体的映射策略,取出 12+log2(C)12 + log_2(C)​ 位的标记信息,并与 cache 中对应组的标记进行比对:

若未命中,fetch 方法调用 loadBlock 方法,传递标记和组号:

private int loadBlock(int blockNO) {
// TODO
int setNO = blockNO % SETS;
char[] addrTag = calculateTag(blockNO);
int st = setNO * setSize;
int ed = (setNO + 1) * setSize;
char[] data = Memory.getMemory().read(transformer.intToBinary(String.valueOf(blockNO * LINE_SIZE_B)), LINE_SIZE_B);
if (SETS == 1024) { // 直接映射,无替换策略而言
int rowNO = setNO;
update(rowNO, addrTag, data);
return rowNO;
} else {
for (int rowNO = st; rowNO < ed; ++rowNO) {
if (cache.get(rowNO).validBit == false) {
update(rowNO, addrTag, data);
return rowNO;
}
}
return replacementStrategy.replace(st, ed, addrTag, data);
}
}

这里需要注意根据标记和组号,得到数据在内存中的起始地址为 blockNO * LINE_SIZE_B

loadBlock 方法会根据具体的映射策略,进行 cache 的替换与更新:

public void update(int rowNO, char[] tag, char[] input) {
// TODO
CacheLine line = cache.get(rowNO);
line.update(tag, input);
}

update 方法将具体的更新信息转发给 CacheLine 内部类中:

private static class CacheLine {
...
void update(char[] tag, char[] data) {
// TODO
validBit = true;
visited = 1;
timeStamp = System.currentTimeMillis();
System.arraycopy(tag, 0, this.tag, 0, tag.length);
System.arraycopy(data, 0, this.data, 0, data.length);
}
}

需要注意对替换策略相关字段的初始化。

替换策略

其中,LFU 通过 visited 字段标记最近的使用频数,而 LRU 通过 timeStamp 时间戳标记最近的使用时间,FIFO 也使用时间戳,不过其 hit 方法的实现中不需要更新时间戳。

写策略

主要修改 write 方法,测试用例只针对 FIFO 替换策略进行了测试。

// TODO
if (isWriteBack) {
cache.get(rowNO).dirty = true;
} else {
Memory.getMemory().write(calculatePAddr(rowNO), LINE_SIZE_B, cache_data);
}

需要调用上述介绍的 calculatePAddr 方法,写内存注意是 1KB 对齐

更新 cache 之前,将 dirty 数据写回内存:

if (Cache.isWriteBack) {
if (cache.isDirty(index) == true && cache.isValid(index) == true) {
Memory.getMemory().write(cache.calculatePAddr(index), Cache.LINE_SIZE_B, cache.getData(index));
}
}
cache.update(index, addrTag, input);

需要判断有效性。

外部存储器

W 6

Y 8

硬盘

材料

磁盘是由涂有可磁化材料的非磁性材料(基材)构成的圆形盘片

玻璃基材 → 支持磁头较低的飞行高度

结构

68750f48a8044bb885ccd653e97c2b18.jpg

读写机制

在读或写操作期间,磁头静止,而盘片在其下方旋转

TODO

数据组织

83f095d1cbfd4bcdb0837832376580c3.jpg b78a727c96964acfa3c8a1ba21ab1779.jpg

扇区划分

534f4ed2bcfa41c1b0c3e5f5f611ccb6.jpg

格式化

需要一些额外的控制数据,用来定位具体的扇区,并标注一些信息

331edbc514ec4c09a914bb8f16d52ee6.jpg

I/O 访问时间

寻道时间:磁头定位到所需移动到的磁道所花费的时间,通常取平均时间

旋转延迟:等待响应扇区的起始处到达磁头所需的时间,通常取磁道旋转半周所需的时间

传送时间:数据传输所需的时间

则平均访问时间为:

Ta=Ts+12r+brNT_a = T_s + \frac{1}{2r} + \frac{b}{rN}

其中:

磁头寻道

目标:当有多个访问磁盘任务时,使得平均寻道时间最小

可能产生饥饿现象,尤其是位于两端的磁道请求

总是按照一个方向进行磁盘调度,直到该方向上的边缘,然后改变方向

只有磁头朝某个方向移动时才会响应请求,移动到边缘后立即让磁头返回起点,返回途中不做任何处理

SCAN 算法的升级,只要磁头移动方向上不再有请求,就立即改变磁头的方向

C-SCAN 算法的改进,只要在磁头移动方向上不再有请求,就立即让磁头返回起点

光存储器

4abce8779f844d5b9d4295b52f5e2dc5.jpg

磁带

顺序读取

并行记录 vs. 串行记录(蛇形记录)

U 盘和固态硬盘

U 盘:采用了快闪存储器,属于非易失性半导体存储器

固态硬盘:与 U 盘没有本质区别:容量更大,存储性能更好

RAID

Redundant Arrays of Independent Disks

W 6

Y 8

基本思想:

特性:

277e0d99462c4e86b6059c15581f7aea.jpg

RAID 0

条带化,非冗余:

6bd838f9bc224e1b9a22bfb493440ae8.jpg

与单个大容量磁盘相比:

RAID 1

条带化,备份所有数据:

36c360c6cef742c28d9847be94a76958.jpg

与单个大容量磁盘相比:

RAID 01 vs. RAID 10

RAID 10 的数据可用性较高(先做冗余备份)

RAID 10 Vs RAID 01 (RAID 1+0 Vs RAID 0+1) Explained with Diagram

RAID 2

Deprecated

采用并行存取技术

希望所有磁盘都参与每个 I/O 请求的执行

条带非常小,经常只有一个字节或一个字

5681dd735df34f67b41f74b9f84587e8.jpg

使用海明码纠错:

适用于多磁盘易出错环境,对于单个磁盘和磁盘驱动器已经具备高可靠性的情况没有意义。

RAID 3

类似 RAID 2,使用奇偶校验码纠错:

0d9cf5eed8fb4599aebc0df4fcfc34cb.jpg

当某一磁盘损坏时,可以用于重构数据。

这里已经明确定位到了出错的磁盘,所以奇偶校验码可以实现纠错的功能。

假设磁盘 00 出错,由:

P(b)=b0b1b2b3P(b)=b_0 \oplus b_1 \oplus b_2 \oplus b_3

两边异或 P(b)b0P(b)\oplus b_0,立得:

b0=p(b)b1b2b3b_0=p(b) \oplus b_1 \oplus b_2 \oplus b_3

与单个大容量磁盘相比:

RAID 4

Deprecated

采用独立存取技术

每个磁盘成员的操作是独立的,各个 I/O 请求能够并行处理

采用相对较大的数据条带

a273bdbff0ad44fd8fbbd4c2ea393a4d.jpg

与单个大容量磁盘相比:

P(b)=P(b)b0b0P'(b)=P(b) \oplus b_0 \oplus b_0'

RAID 5

类似 RAID 4,将校验盘分解到每个物理盘上:

1fefb58dbf7e415bae183027dcadf947.jpg

若修改 block 0,第一行会受影响,校验位所在的列也会受影响。

访问时的两读两写

P(b)=P(b)b0b0P'(b)=P(b) \oplus b_0 \oplus b_0'

最好情形:两读并行,两写并行

最坏情形:无并行

RAID 50

先做 RAID 5,再做 RAID 0

RAID 6

采用两种不同的校验码,并将校验码以分开的块存于不同的磁盘中:

042feebdd6164c6988d26832e8daaada.jpg

虚拟存储器

W 8

Y 7

存储器管理

多道程序系统中,主存需要进一步划分给多个任务,划分的任务由操作系统动态执行。

如何将更多任务装入主存:

分区

分区方式将主存分为两大区域

分区方式有如下两种

分页

把主存分成固定长且比较小的存储块,称为页框(物理地址),每个任务也被划分成固定长的程序块,称为页(逻辑地址)

将页装入页框中,且无需采用连续的页框来存放一个任务中所有的页

虚拟存储器

请求分页,仅将当前需要的的页面调入主存

通过硬件(页表)将逻辑地址转换为物理地址,未命中时在主存和硬盘之间交换信息

设计的一些问题

以分页为例,指内存

类似 Cache 和主存之间,由于主存和硬盘之间更大的速度鸿沟,考量下述设计要素:

分页式虚拟存储器

主存储器和虚拟地址空间都被划分为大小相等的页面:

页表

4aac5e7977284ad78fa488b41fe4c027.jpg

存放位置信息:

一些细节:

地址转换:

虚拟页号 + 页内偏移量物理页号 + 页内偏移量

VA → PA

TLB

Translation Lookaside Buffer

页表的使用增加了主存的访问次数,为了减少访存次数,把页表中最活跃的几个页表项复制到高速缓存中:

CPU 访存过程

87275d5036de4db5ac7c6d14994b7f9a.jpg

TLB、页表、Cache 的缺失组合

c42e5915d5ae4ad4a4d62b8ec9d30186.jpg

替换掉的不在主存中的页,TLE 和 Cache 中一定不会有相应有效的项

访存次数分析:

分段式虚拟存储器

将程序和数据分成不同长度的段,将所需的段加载到主存中

虚拟地址:段号 + 段内偏移量

优点:段的分界与程序的自然分界相对应,易于编译、管理、修改和保护

缺点:段的长度不固定

段页式虚拟存储器

将程序和数据分段,段内再进行分页,每个分段都有一个页表

虚拟地址:段号 + 页号 + 页内偏移量

优点:程序按段实现共享与保护

缺点:需要多次查表

编程实现

RTFM:

  • INTEL 80386 PROGRAMMER’S REFERENCE MANUAL 1986
    • Chapter 5 Memory Management

预备知识

Segment = trueSegment = false
Page = true段页式不存在
Page = false分段式实模式

段选择符

1470acfbe0b8413eb9b8271060ab954d.jpg

高 13 位的索引值用来确定当前使用的段描述符在段描述符表中的位置,表示是其中的第几个段表项。

段寄存器

203ab126ff1b402dacfbefe9f138d21f.jpg

段选择符存放在段寄存器中。

不过在编程作业中我们并不关心这一点……

段描述符

ba6057410b6544938a2624e28fed5956.jpg

段描述符是一种数据结构,实际上就是分段方式下的段表项。

一个段描述符占用 8 个字节,下面是一些字段的含义:

Defines the location of the segment within the 4 gigabyte linear address space. The processor concatenates the three fragments of the base address to form a single 32-bit value.

Defines the size of the segment. When the processor concatenates the two parts of the limit field, a 20-bit value results. The processor interprets the limit field in one of two ways, depending on the setting of the granularity bit:

  1. In units of one byte, to define a limit of up to 1 megabyte.

  2. In units of 4 Kilobytes, to define a limit of up to 4 gigabytes. The limit is shifted left by 12 bits when loaded, and low-order one-bits are inserted.

Specifies the units with which the LIMIT field is interpreted. When thebit is clear, the limit is interpreted in units of one byte; when set, the limit is interpreted in units of 4 Kilobytes.

其模拟如下:

private static class SegDescriptor {
private char[] base = new char[32];
private char[] limit = new char[20];
private boolean validBit = false;
private boolean granularity = false;
}

段描述符表

段描述符表实际上就是分段方式下的段表,由若干个段描述符组成:

46444e4e518843eba69682be568121b0.jpg

目前只考虑 GDT,其模拟如下:

private SegDescriptor[] GDT = new SegDescriptor[8 * 1024];

页表

页表项模拟如下:

private static class PageItem {
private char[] pageFrame;
private boolean isInMem = false;
}

页表模拟如下:

private final PageItem[] pageTbl = new PageItem[Disk.DISK_SIZE_B / Memory.PAGE_SIZE_B];

注意页表中包含了所有虚拟页的信息,而我们认为虚拟地址空间的大小为磁盘的大小,所以页表项的个数为Disk.DISK_SIZE_B / Memory.PAGE_SIZE_B

TLB

TLB 项模拟如下:

private static class TLBLine {
boolean valid = false;
int vPageNo;
char[] pageFrame = new char[20];
Long timeStamp = 0L;
}

注意 TLB 项在 TLB 中的下标与 vPageNo 不一致,不要和页表项混淆了。

timeStamp 用于 FIFO,所以 TLB hit 不必更新时间戳。

地址转换概述

逻辑地址 → 线性地址

15 0 31 0
LOGICAL +----------------+ +-------------------------------------+
ADDRESS | SELECTOR | | OFFSET |
+---+---------+--+ +-------------------+-----------------+
+------+ V |
| DESCRIPTOR TABLE |
| +------------+ |
| | | |
| | | |
| | | |
| | | |
| |------------| |
| | SEGMENT | BASE +---+ |
+->| DESCRIPTOR |-------------->| + |<------+
|------------| ADDRESS +-+-+
| | |
+------------+ |
V
LINEAR +------------+-----------+--------------+
ADDRESS | DIR | PAGE | OFFSET |
+------------+-----------+--------------+

48 位的逻辑地址包含 16 位的段选择符和 32 位的段内偏移量。

MMU 首先通过段选择符内的 13 位索引值,从段描述符表中找到对应的段描述符,从中取出 32 位的基地址,与逻辑地址中 32 位的段内偏移量相加,就得到 32 位线性地址。

线性地址 → 物理地址

PAGE FRAME
+-----------+-----------+----------+ +---------------+
| DIR | PAGE | OFFSET | | |
+-----+-----+-----+-----+-----+----+ | |
| | | | |
+-------------+ | +------------->| PHYSICAL |
| | | ADDRESS |
| PAGE DIRECTORY | PAGE TABLE | |
| +---------------+ | +---------------+ | |
| | | | | | +---------------+
| | | | |---------------| ^
| | | +-->| PG TBL ENTRY |--------------+
| |---------------| |---------------|
+->| DIR ENTRY |--+ | |
|---------------| | | |
| | | | |
+---------------+ | +---------------+
^ | ^
+-------+ | +---------------+
| CR3 |--------+
+-------+

上图为 80386 的地址转换过程,在编程作业中简化为:

虚拟页号 + 页内偏移量物理页号 + 页内偏移量

其中页号 20 位,页内偏移量 12 位。

实现

地址转换全貌

核心方法 addressTranslation:

private String addressTranslation(String logicAddr, int length) {
String linearAddr; // 32 位线性地址
String physicalAddr; // 32 位物理地址
if (!Memory.SEGMENT) {
// 实模式:线性地址等于物理地址
linearAddr = toRealLinearAddr(logicAddr);
memory.real_load(linearAddr, length); // 从磁盘中加载到内存
physicalAddr = linearAddr;
} else {
// 分段模式
int segIndex = getSegIndex(logicAddr);
if (!memory.isValidSegDes(segIndex)) {
// 缺段中断,该段不在内存中,内存从磁盘加载该段索引的数据
memory.seg_load(segIndex);
}
linearAddr = toSegLinearAddr(logicAddr);
// 权限检查
int start = Integer.parseInt(transformer.binaryToInt(linearAddr));
int base = chars2int(memory.getBaseOfSegDes(segIndex));
long limit = chars2int(memory.getLimitOfSegDes(segIndex));
if (memory.isGranularitySegDes(segIndex)) {
limit = (limit + 1) * Memory.PAGE_SIZE_B - 1;
}
if ((start < base) || (start + length > base + limit)) {
throw new SecurityException("Segmentation Fault");
}
if (!Memory.PAGE) {
// 分段模式:线性地址等于物理地址
physicalAddr = linearAddr;
} else {
// 段页式
int startvPageNo = Integer.parseInt(transformer.binaryToInt(linearAddr.substring(0, 20))); // 高 20 位表示虚拟页号
int offset = Integer.parseInt(transformer.binaryToInt(linearAddr.substring(20, 32))); // 低 12 位的页内偏移
int pages = (length - offset + Memory.PAGE_SIZE_B - 1) / Memory.PAGE_SIZE_B;
if (offset > 0) pages++;
int endvPageNo = startvPageNo + pages - 1;
for (int i = startvPageNo; i <= endvPageNo; i++) {
if (TLB.isAvailable) {
if (tlb.getPageFrameFromTLB(i) == null) {
// TLB 缺失
if (!memory.isValidPage(i)) {
// TLB 缺失且缺页
memory.page_load(i); // 内存从磁盘加载该页的数据
tlb.write(i); // 填 TLB
} else {
// TLB 缺失但页表命中
tlb.write(i); // 填 TLB
}
} else {
// do nothing
}
} else {
if (!memory.isValidPage(i)) {
// 缺页中断,该页不在内存中,内存从磁盘加载该页的数据
memory.page_load(i);
}
}
}
physicalAddr = toPagePhysicalAddr(linearAddr);
}
}
return physicalAddr;
}

startvPageNo ~ endvPageNo 的遍历是考虑到地址访问跨页的情形。

下面我们需要实现三个地址转换和三个数据加载的方法。

实模式

逻辑地址通过如下变换:

物理地址 = 基址 << 4 + 偏移量

转换为物理地址。

得到物理地址直接从磁盘中加载数据到内存。

实现略。

分段式

地址转换如下:

private String toSegLinearAddr(String logicAddr) {
int segmentDescriptorIndex = Integer.valueOf(logicAddr.substring(0, 13), 2);
int base = chars2int(memory.getBaseOfSegDes(segmentDescriptorIndex));
int offset = Integer.valueOf(logicAddr.substring(16), 2);
int linearAddr = base + offset;
String linearAddrStr = String.valueOf(linearAddr);
return transformer.intToBinary(linearAddrStr);
}

数据加载如下:

public void seg_load(int segIndex) {
SegDescriptor sd = getSegDescriptor(segIndex);
sd.base = "00000000000000000000000000000000".toCharArray();
sd.limit = "11111111111111111111".toCharArray();
sd.validBit = true;
sd.granularity = PAGE;
if (!PAGE) {
String pAddr = String.valueOf(sd.base);
int len = Integer.parseInt(transformer.binaryToInt(String.valueOf(sd.limit)));
char[] data = disk.read(pAddr, len);
write(pAddr, len, data);
}
}

我们约定,每个由 MMU 装载进入 GDT 的段,其段基址均为全 0,其限长均为全 1,未开启分页时粒度为 false,开启分页后粒度为 true

在分段式中,我们会加载硬盘前 1048575 B ≈ 1 MB 的数据到内存。

在段页式下,此处无需加载数据,只需要填写 GDT。

Linux 操作系统为了使它能够移植到绝大多数流行的处理器平台,就是把所有段基址设为全 0 的、段限长设为全 1 的。但即使 Linux 操作系统这么做了,在进行地址转换的时候,它也是需要去查 GDT 的。可以说,查 GDT 是所有系统的必备步骤。因此,我们即使规定了段基址为全 0,我们也需要考察大家是否正确进行了查表操作。

段页式

地址转换如下:

private String toPagePhysicalAddr(String linearAddr) {
String vPageNo = linearAddr.substring(0, 20);
String offset = linearAddr.substring(20);
char[] page;
if (TLB.isAvailable) {
page = TLB.getTLB().getPageFrameFromTLB(Integer.valueOf(vPageNo, 2));
} else {
page = memory.getFrameOfPage(Integer.valueOf(vPageNo, 2));
}
assert page != null;
StringBuilder builder = new StringBuilder(String.valueOf(page));
builder.append(offset);
assert builder.length() == 32;
return builder.toString();
}

数据加载如下:

public void page_load(int vPageNo) {
int pPageNo = -1;
for (int i = 0; i < MEM_SIZE_B / PAGE_SIZE_B; ++i) {
if (!pageValid[i]) {
pPageNo = i;
break;
}
}
assert pPageNo >= 0;
PageItem page = getPageItem(vPageNo);
page.isInMem = true;
pageValid[pPageNo] = true;
page.pageFrame = transformer.intToBinary(String.valueOf(pPageNo)).substring(12).toCharArray();
int unit = (int) Math.pow(2, 12);
int base = vPageNo * unit;
String baseAddr = transformer.intToBinary(String.valueOf(base));
char[] data = disk.read(baseAddr, unit);
StringBuilder builder = new StringBuilder(String.valueOf(page.pageFrame));
builder.append("000000000000");
assert builder.length() == 32;
write(builder.toString(), unit, data);
}

需要注意的是虚实之间的关系,考虑加载第 xx 个虚页:

再强调一遍,页表中包含了所有虚拟页的信息,物理页对应的是内存:

分页机制的基本思想其实就是,为每个进程都提供一个独立的、极大的虚拟地址空间,将主存里放不下的页放到磁盘上去。

在真实的计算机磁盘中,会有专门的地方来存放虚页:在 Windows 下是 pagefile.sys 文件,在 Linux 下是 swap 分区。

因此,无论是在实模式还是分段式还是段页式,当你访问磁盘的时候,请保证你用来访问磁盘的是线性地址。上面所说的用来加载段的时候用的段基址、加载页的时候用的虚页起始地址,都属于线性地址。

总线

W 3

Y 8

总线概念

总线是连接两个或多个设备的通信通路

总线类型

我们主要研究系统总线

总线结构

总线上数据传输的特点

总线仲裁

当多个设备需要与总线通信时,通过某种策略选择一个设备

平衡因素:

集中式

由仲裁器或总线控制器负责分配总线使用权

链式查询

74663dbd7d084a22a577de280e35858f.jpg

在总线不忙的前提上,各设备向总线仲裁器发起请求

总线仲裁器 → 设备 1 → 设备 2 → ...

对电路故障敏感

限制总线的速度

计数器查询

93de2b336a964165bb53c8f5aff67e5a.jpg

在总线不忙的前提上,各设备向总线仲裁器发起请求

总线仲裁器 → 设备 1
总线仲裁器 → 设备 2
...

通过使用不同的初始计数,可以灵活地确定设备优先级

限制总线的速度

独立请求

78e7ede4352742b3b28517fea7d098a6.jpg

在总线不忙的前提上,各设备向总线仲裁器独立发起请求

总线仲裁器决定(策略灵活)哪个设备可以使用总线

快速响应

分布式

每个设备都包含访问控制逻辑,各设备共同作用分享总线

自举式

88ebd8f14aee4539b92a4875c37c179a.jpg

优先级:

设备 3 > 设备 2 > 设备 1 > 设备 0

冲突检测

只要总线不忙,就去使用总线

若产生冲突,所有使用总线的设备停止数据传输,并分别在随机间隔时间后再次请求总线

时序

确定每个总线事务的开始和结束时间

总线事务:地址 + 数据 + … + 数据

同步时序

事件的发生由时钟决定

异步时序

一个事件的发生取决于前一个事件的发生

握手策略

2c04866f928e49058e693d8f0f8726d6.jpg

Ready → 请求发起方,以 CPU 为例

Ack → 响应方,以 Memory 为例

非互锁:

半互锁:

全互锁:

异步数据传输

这里将地址线和数据线复用:

eb7ef2d610f64de6b5558111c6f117ae.jpg

半同步

同步时序和异步时序相结合

准备和响应信号在时钟上升沿有效,从而减少了噪声的影响

分离事务

将一个总线事件分离为两个过程

设备准备数据期间释放总线

总线带宽和数据传输速率

区分总线带宽与总线宽度

如何提高

层次结构

单总线

一条系统总线

双总线

多总线

指令系统

W 10 / 11

Y 4

指令集

CPU 能执行的各种不同指令的集合

指令要素

操作码

主要是控制转移指令:

操作数

一个指令需要有 4 个地址引用:

ef230d833c924c7eb8a21e3e505d39f6.jpg

所以可以分为:

操作数引用

记号:

寻址方式:

一图胜千言:

238be265646945babdb018f26c610fd2.jpg

指令格式

不是现在应该深究的内容

指令集设计

不是现在应该深究的内容

指令周期和指令流水线

W 12

Y 5 / 6

指令周期

db704e6969c042c8b58cde479ee1186e.jpg

注意间接周期是取操作数的有效地址,而非操作数,操作数在执行阶段取

数据流

取指周期

97243454cfa143d59b57adf7c8657967.jpg

PC → MAR → 指令地址在地址总线

存储器读 → 指令数据在数据总线

指令数据 → MBR → IR

间接(间址)周期

7b23eb8749d14e429368284e24a0a5d5.jpg

控制器检查 IR 中是否有使用间接寻址的操作数

MBR → MAR → 操作数引用在地址总线

存储器读 → 操作数的有效地址在数据总线

中断周期

66b3feefd3eb487bb66890558040f981.jpg

PC → MBR → 数据总线

一个存储器位置 → MAR → 地址总线

存储器写 → 保存 PC 在某个位置

中断子程序的地址 → PC

流水线

两阶段

d05134c799d643a19ec9acf146b46fcd.jpg

取指和执行

限制:

六阶段

读下一个预期的指令到缓冲器

确定操作码和操作数指定器

计算每个源操作数的有效地址,这可能涉及到偏移、寄存器间接、间接或其他形式的地址计算

由存储器取每个操作数。寄存器中的操作数不需要取

完成指定的操作。若有指定的目标操作数位置,则将结果写入此位置

将结果存入存储器

限制:

流水线性能

708db10e39f845ff91eb3c8a39c52fe1.jpg

99 条指令:

冒险

结构冒险

b0487ba6a4fc4b09880ac8450ddf4144.jpg

存储器 → 指令存储器、数据存储器

寄存器 → 上升沿写、下降沿读

数据冒险

Read After Write

add $1, $2, $3
xor $4, $1, $2
d534d5603e1549f3ac4df15fb3f6a42b.jpg

上一条指令的 ALU 输出立即转发到下一条指令的 ALU 输入

Load Use

lw $1, (0)$2
xor $4, $1, $2

控制冒险

指令的执行顺序被更改:

解决:

控制器

W 12 / 15 / 16

Y 5

用户可见寄存器

设计出发点

  1. 通用还是专用
  2. 数量
  3. 长度

保存与恢复

控制和状态寄存器

设计出发点

微操作

数据流的实现

一条机器指令的分解:

82fd5146b3c8457fa45bc04f136d453f.jpg

取指周期

t1: MAR ← (PC)
t2: MBR ← Memory
PC ← (PC) + I
t3: IR ← (MBR)

内存读操作要使用 MAR 中的地址

MBR ← MemoryIR ← (MBR) 这两个微操作不应出现在同一时间单位里

所用的时间单位应尽可能少

间址周期

t1: MAR ← (IR(Address))
t2: MBR ← Memory
t3: IR(Address) ← (MBR(Address))

操作数的有效地址在 IR 的地址字段

执行周期

对于不同的操作码,会出现不同的微操作序列

中断周期

t1: MBR ← (PC)
MAR ← Save_Address
t2: PC ← Routine_Address
Memory ← (MBR)

指令周期代码

Instruction Cycle Code, ICC

假设一个 2 位的 ICC 寄存器,明确 CPU 处于指令周期哪个阶段

5338e58e38974a3cab7a599c980877d6.jpg

控制器

基本任务

输入输出

5180b5ea2e34480c8f7d47e7ee95b549.jpg

控制信号

ALU 和寄存器都连接到 CPU 内部总线上

为了数据在该内部总线和各寄存器之间传递,内部总线和寄存器之间有门和控制信号

控制线控制着数据和系统总线(外部)的交换以及 ALU 的操作

10e8cb3c05784ac48f4ff6b0ee7f9abb.jpg

以取指周期为例:

t1: MAR ← (PC)
c2
t2: MBR ← Memory
PC ← (PC) + I
C0 CR C5
t3: IR ← (MBR)
C4

PC 自增没有在图中体现出来

硬布线实现

3129f770fa3f4e22b0317fc77525c17d.jpg

为每个输出的控制信号设计一个关于控制器输入的布尔表达式

以上述 C5C_5 为例:

C5=PQT2+PQT2+PQ(ADD+AND)T2C_5 = \overline{P} \cdot \overline{Q} \cdot T_2 + \overline{P} \cdot Q \cdot T_2 + P \cdot \overline{Q} \cdot (ADD + AND) \cdot T_2

其中 PPQQ 指示指令周期哪个阶段,TiT_i 指示子周期的当前时间

翻译为:C5C_5 在取指、间接、执行周期(指令 ADDADDANDAND)的第二个时间单位有效

微程序实现

构造一个控制字,每位代表一根控制线,将这些控制字串在一起,可以表示控制器需要完成的微操作序列。

由于微操作序列不是固定的,把控制字放入一个存储器单元中,每个字有自己唯一的地址。

整体结构如下:

2edb0ec66efb4b34b29bfc60c8646b69.jpg

其中控制存储器如下:

4627edff89714c1c8cd9908104d12fe2.jpg

其中最重要的部分是定序逻辑,请自行理解……

输入输出

W 7

Y 8

外围设备

不属于计算机系统

不能把外设直接连接到系统总线上:

I/O 模块

属于计算机系统

功能

结构

01e8d170cd354be3991c9f62511c4baf.jpg f6c356aef90f43968ac28fb4a7ebcd19.jpg

I/O 操作技术

6261e13433e3405e9c14633e962fe306.jpg

编程式 I/O

9fffe05e86bf4914b7316dbc8dd80793.jpg

中断驱动式 I/O

54c82cbc41a449fc83447f6fa5588cb5.jpg

直接存储器存取

c43d9d470cb14bd0af95a677744bb420.jpg adf35940fa464ad18c849edb3e31da48.jpg

I/O 模块的演变