深入理解计算机系统(第三版)第一章读书笔记
计算机中的信息
对于程序员来说,HelloWorld 程序再熟悉不过了,下面是 C 语言写的 HelloWorld 程序。
#include<stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
这段程序很简单,仅仅是在屏幕上输出hello world
,对于程序员很容易阅读,但是从计算机的角度来看,也是这么简单吗?
我们都知道,计算机的世界只有 0 和 1组成的位(比特),这对于人类来说太不友好了,因此计算机科学家们设计了 ASCⅡ 码来表示现实世界的符号。每 8 个位构成一个组(称为一个字节),用一个组来表示一个符号,上面的 C 程序用 ASCⅡ 表示就是下面的样式。
因此对于计算机来说,所有的数据都是由一串比特表示的,要区分这些数据对象的唯一方法就是读到它们时的上下文。
信息 = 位 + 上下文
源程序编译
虽然我们把 C 语言程序编写完成了,但是它并不能运行,因为它目前还仅仅是由 ASCⅡ 字符构成的文本文件,计算机并不能运行文本文件。
想要得到 HelloWorld 的可执行文件,需要经过预处理、编译、汇编、链接四个阶段。
预处理阶段,预处理器会根据以字符#
开头的命令去修改源程序,预处理器去读取系统头文件stdio.h
中的内容,并将其直接插入程序文本中,结果就得到了另一个 C 程序—— hello.i。
编译阶段,编译器会把 hello.i 程序翻译成汇编语言程序 hello.s,汇编语言本质上就是机器语言,此时的程序仍然是文本文件。
汇编阶段,汇编器会将 hello.s 翻译成机器语言指令,保存在 hello.o 文件中,此时得到的就是二进制文件了。
程序中使用了 printf 函数,这是由编译器提供的标准 C 库中的函数,它存储在 printf.o 文件中,链接器会将这个文件合并到 hello.o 中,结果就得到一个可执行的 hello 文件,存储于磁盘中。
系统组成及程序运行
在运行可执行文件 hello 文件之前,应该先了解一下系统的硬件组成,一个典型系统的硬件组织如下图所示。
为了运行 hello 程序,我们需要在 shell 中输入指令./hello
,指令通过键盘经 I/O 总线 --> I/O 桥 --> 总线接口 --> 寄存器 --> I/O 桥存于主存中,当敲击回车键时,等于告诉 shell 程序,命令的属于已经结束, shell 将会执行一系列指令来加载 hello 文件。
利用**直接存取(DMA)**技术,hello 文件不需要通过寄存器就能到达主存,当目标文件 hello 到达主存中,处理器就开始执行 hello 程序的机器指令。其指令即将 “hello, world\n” 字符串的字节从主存复制到寄存器,再从寄存器复制到显示设备中,最终显示在屏幕上面。
我们发现这个简单的 HelloWorld 程序会让系统花费大量的时间把信息从一个地方挪到另一个地方,而我们都清楚,寄存器、主存、磁盘之间的访问速度是相互差了好几个数量级的,而这种复制的开销会严重减慢程序的运行,为了加快这些复制操作的速度,系统设计者就引入了高速缓存。
抽象的重要性
计算机科学中最为重要的概念之一就是抽象,指令集提供了对硬件处理器的抽象,操作系统同样通过进程、虚拟内存、文件(磁盘、键盘、网络都可以看成文件)这几个抽象概念为应用程序提供简单一致的机制来控制低级硬件设备,同时防止硬件被失控的应用程序滥用。
进程是对正在运行的程序的抽象,在一个系统上可以同时运行多个进程,但是每个进程看起来都好像在独占的使用硬件,CPU 看上去是在并发的执行多个进程,这就需要操作系统进行上下文切换。
在我们还没有输入./hello
之前,只有 shell 进程在运行,当我们让其运行 hello 程序时,shell 会通过系统调用来执行我们的请求。操作系统首先保存 shell 进程的上下文(PC、寄存器等信息),然后创建一个新的 hello 进程,并将控制权转交给 hello 进程。