在学习unix环境高级编程的时候,我觉得自己是在看一本函数库的注释,没有多大的体会,深感操作系统方面知识的缺乏,对于操作系统,我只是了解到部分知识,如进程、线程,而其他方面的知识要么只是单纯的用过一些示例,或者知道其存在。
所以,今天开始好好学操作系统=_=….unix环境编程就暂时先放下。
PS:记录的是学习过程中的摘抄或者是个人理解,并不会完全记录在此,因为内容太多辣= =
C语言程序编译过程
预编译:预处理器根据以字符#开头的命令,修改原始的C程序,如#include<stdio.h>
命令告诉预处理器读取系统头文件的内容,然后把它插入到程序文本中,结果就得到一个 .i 扩展名的文件。
编译器:把通过预编译的.i文件通过编译器将程序代码翻译为一个汇编程序,生成一个 .s扩展名的文件。
汇编器:把汇编.s文件通过汇编器翻译为机器语言指令,并把这些指令打包成一个可重定位目标程序,并将结果保存到.o文件。
链接阶段:把我们在程序中所使用的库函数文件链接到我们的程序.o文件中,比如我们使用了stdio.h
的printf函数,则链接器就会找到printf.o文件,将其链接到我们的程序汇编器生成的.o文件中,最后生成一个可执行目标文件。
典型系统的硬件组织
1、总线:
贯穿整个系统的是一组电子管道,称作总线。它携带着信息字节在各个部件间传递。
2、I/O设备:
键盘、显示器、硬盘之类的都属于I/O设备,也就是计算机与外界联系的通道,每个I/O设备都与一个控制器或者适配器与总线相连,在I/O总线上与I/O设备之间传递信息。就比如下图中的设备管理器:
3、主存
主存是一个临时存储设备,也就是一块特殊内存,在处理器执行程序的时候,存放程序与程序处理的数据。物理上来说是由一组动态随机存取存储器芯片组成的。
4、处理器
解释存储在主存中指令的引擎,CPU的核心是一个寄存器,称为PC(程序计数器),在任何时刻,PC都指向主存中的某条机器语言指令(含有其指令的地址 )。
其中指令按照严格的执行顺序执行,而执行一条指令包含执行一系列的步骤。处理器从寄存器指向的内存处读取指令,并解释指令中的位,执行该指令的简单操作,然后更新寄存器,使其指向下一条指令。
而简单的操作并不多,它们围绕着主存‘寄存器文件与ALU(算数/逻辑单元)进行,ALU负责计算新的数据和地址值。
CPU在指令的要求下可能执行:
1、加载:从主存中复制一个字节或者一个字到寄存器,以覆盖寄存器原先的内容。
2、存储:从寄存器中复制一个字节或一个字复制到主存的某个位置,以覆盖这个位置的内容。
3、操作:把两个寄存器的内容复制到ALU进行逻辑运算,并将结果复制到一个寄存器中。
4、跳转:从指令本身中抽取一个字,用以覆盖到寄存器中原来的值。
Amdahl定律
假设电脑打开某软件的时间为T,而我们要提升电脑某部分硬件性能来提升时间,假设该部分性能提升的时间T1与总时间T的比例为a(即T1/T=a),当我们提升了该硬件后,提升后的该硬件性能是提升前的k倍,那么有:
提升性能后的Tnew=T*(1-a)+T*a/k
加速比=T/Tnew=1/((1-a)+a/k)
位运算
正如加减乘除,关于比特的基本逻辑运算也有四种,可以看做是布尔代数的子集。对于 0 和 1 来说,是这样的:
与 And:A=1 且 B=1 时,A&B = 1
或 Or:A=1 或 B=1 时,A|B = 1
非 Not:A=1 时,~A=0;A=0 时,~A=1
异或 Exclusive-Or(Xor):A=1 或 B=1 时,A^B = 1;A=1 且 B=1 时,A^B = 0
对应与集合运算则是交集、并集、差集和补集,假设集合 A 是 {0, 3, 5, 6},集合 B 是 {0, 2, 4, 6},全集为 {0, 1, 2, 3, 4, 5, 6, 7}那么:
& 交集 Intersection {0, 6}
| 并集 Union {0, 2, 3, 4, 5, 6}
^ 差集 Symmetric difference {2, 3, 4, 5}
~ 补集 Complement {1, 3, 5, 7}
有了这些知识,我们就可以来具体看看不同类型的数据在计算机中是如何存储和进行运算的了。
整数的原码、反码、补码
以下皆为有符号的表示方法,其中x表示为二进制向量(数组),w为位数:
反码:B2O(x,w)=-x[w-1]*(2^(w-1)-1)+(x[0]*2^0+x[1]*2^1+…..x[w-2]*2^w-2);
补码:B2T(x,w)=-x[w-1]*2^(w-1)+(x[0]*2^0+x[1]*2^1+…..x[w-2]*2^w-2);
原码:B2S(x,w)=(-1)^x[w-1]*(x[0]*2^0+x[1]*2^1+…..x[w-2]*2^w-2);
逻辑移位(右移或者左移):无符号数,右移是逻辑移位,即移位后填充0.
算数移位(…):移动后填充最高位的数字。
在移位的过程中,就比如移动k位,那么C语言的位移量为 k mod w。(w为由w位组成的数据类型)。
计算机存储负数大多数是以补码的方式保存,且在我们编程的过程中,使用类型转换,如基础类型的转换,int,char,short……,但位数高的数据类型向位数低的数据类型转换会产生截断数字,如下图,由于123456大于short的范围所以t是负数,但int转换为short的时候,就保留16位,其他位置填充1,或者说是123456 mod 2^16 - 65536= -7616.
在C中表示补码最小值
在看书的过程中,看到补码的表示方式如下:
#define INT_MAX 2147483647
#define INT_MIN (-INT_MAX - 1)
感觉挺奇怪的,为什么不直接写成0x80000000
,百度后知道了。
C语言中的不对称的补码表示法和C语言转化规则使我们不得不写成这样的形式。
当编译器遇到 -X 形式的数字,它会首先决定 X 的类型和值,之后再转化为相反数(注:如果是无符号数,则将该数看作补码形式,取其相反数,但最后结果仍然按无符号数处理),但值2147483648
已经超过int型的上界(这个值已经大于TMax32,这就是补码表示法的不对称性)。编译器试图去决定这个值的类型并且分配一个正确的值。如果是ISOC90
标准,编译器在遍历int到long,之后再到unsigned,才会发现能够表示 2147483648 的类型。所以结果是unsigned类型(因为不会发生类型转换,原来是unsigned类型,结果仍然为unsigned类型),并且值为2147483648
。如果是ISO C99标准,编译器会遍历int到long到long long类型。long long 类型为64位,2147483648
和 -2147483648
的表示是不同的,所以结果的数据类型是long long,值为-2147483648
。
对于32位机器上的十六进制数0×80000000
,编译器通过表一的Hexdecimal列来进行类似的遍历。对于两种语言版本来说,编译器首先将这个数与TMax32(0x7FFFFFFF
)对比,因为这个数更大一些,所以编译器决定这个数不能被表示为int类型。编译器之后在把它和 UMax32(0xFFFFFFFF)
比较,因为这个数更小,所以编译器选择了unsigned类型。
在64位的机器上情况就略有不同。对于两种语言版本,十进制的 -2147483648都被表示为long型并且值为 -2147483648
,而十六进制被表示为unsigned型并且值为0×8000000
(即2147483648
)。
实验
1、bitXor(x,y),只用 & 和 ~ 实现 x^y。
int bitXor(int x, int y) {
return ~(~(~x&y)&~(x&~y));
}
我是用集合的概念去理解的,可以画一个韦恩图更容易理解。
allOddBits(x),所有的奇数位都为 1 吗。
bool allOddBits(int x) {
return !~(x|0x55555555);
}