重新认识Hello world:
#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}
在Unix系统上,从源文件到目标文件到转化是由编译器驱动程序完成的:
gcc -o hello hello.c
编译过程可分为四个阶段完成,分别是预处理、编译、汇编、链接,四者合起来一起构成了编译系统。
- 预处理阶段:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第1行的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果就得到了另一个C程序,通常是以.i作为文件扩展名;
- 编译阶段:编译器(ccl)将文本文件hello.i翻译成文本文件.s,它包含一个汇编语言程序。该程序包含main函数的定义。
- 汇编阶段:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。hello.o是一个二进制文件。
- 链接阶段:上面的hello程序调用了printf函数,printf存在于一个名为printf.o的单独的预编译好了的目标文件中,而这个文件必须以某种方式合并到我们的hello.o程序中。连接器(ld)就负责处理这种合并。结果就得到hello文件——可执行文件。
系统的硬件组成
- 总线
- I/O设备
- 主存
- 处理器
运行hello程序
初始时,shell程序执行它的指令,等待我们输入一个命令。当我们在键盘上输入字符串"./hello"后,shell程序将字符逐一读入寄存器,再把它放到内存中:
当我们在键盘上敲回车键时,shell’程序就知道我们已经结束了命令的输入。然后shell执行一系列指令来加载可执行的hello文件,这些指令将hello目标文件中的代码和数据从磁盘复制到主存,数据包括最终会被输出的字符串“hello,world\n”。
利用直接存储器存取技术(DMA),数据可以不通过处理器而直接从磁盘到达主存**:
当目标文件hello中的代码和数据被加载到主存,处理器就开始执行hello程序的main函数中的机器语言指令。这些指令将“hello,world\n”字符串中的字节从主存复制到寄存器文件,再从寄存器文件中复制到显示设备,最终显示在屏幕上:
高速缓存
为了应对处理器和主存之间的差异,设计出了高速缓存存储器(cache memory) ,存放处理器近期可能会需要的信息。
存储设备层次结构
操作系统管理硬件
当shell加载和运行上面的hello程序时,以及hello程序输出自己的消息时,shell和hello程序都没有直接访问键盘、显示器、磁盘或者主存。而是依靠操作系统提供的服务。可以把操作系统看成是应用程序和硬件之间插入的一层软件,所有应用程序对硬件的操作都必须通过操作系统:
操作系统有两个基本功能:
- 防止硬件被失控的应用程序滥用;
- 向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备。
操作系统通过进程、虚拟内存和文件这几个基本的抽象概念来实现上面两个功能:
其中,文件是对I/O设备的抽象表示,虚拟内存是对主存和磁盘I/O设备的抽象表示,进程则是对处理器、主存和I/O设备的抽象表示。
进程及上下文
进程是操作系统对一个正在运行的程序的一种抽象。操作系统保持跟踪进程运行所需的所有状态信息,这种状态就是上下文。包括PC和寄存器文件的当前值、主存的内容等。
当操作系统决定把控制权从当前进程转移到某个新进程时,就会进行上下文切换。
线程
一个进程实际上可以由多个线程组成,线程是进程的执行单元,每个线程都运行在进程的上下文中,共享同样的代码和全局数据。
虚拟内存
虚拟内存是一个抽象概念,它为每个进程提供了一个假象:每个进程都在独自地使用主存。
每个进程看到的内存都是一致的,成为虚拟地址空间。
下图是Linux进程的虚拟地址空间:
在Linux中,地址空间最上面的区域是保留给操作系统中的代码和数据的,这堆所有进程来说都是一样。地址空间的底部区域存放用户进程定义的代码和数据,图中的地址是从下往上增大的。
- 程序代码和数据:对所有进程来说,代码是从同一固定地址开始,紧接着的是和C全局变量相对应的数据位置。代码和数据区是直接按照可执行文件的内容初始化的。
- 堆:代码和数据区后紧接着的是运行时堆。代码和数据区在进程一开始运行时就被指定了大小,与此不同的是,当调用malloc和free这样的C标准库函数时,对可以在运行时动态地扩展和收缩。
- 共享库:大约在地址空间的中间部分是一块用来存放香C标准库和数学库这样的共享库的代码和数据的区域。
- 栈:位于用户虚拟地址空间顶部的是用户栈,编译器用它来实现函数调用。和堆一样,用户栈在程序执行期间可以动态地扩展和收缩。特别地,每次我们调用一个函数时,栈就会增长;从一个函数返回时,栈就会收缩。
联想到gdb调试的栈回溯,应该就是用到了这里的原理
- 内核虚拟内存:地址空间顶部的区域是为内核保留的。不允许应用程序读写这个区域的内容或者直接调用内核代码定义的函数。
文件
简单到极致地说,文件就是字节序列,仅此而已。
每个I/O设备,包括磁盘、键盘、显示器,甚至网络,都可以看成是文件。系统中的所有输入输出设备都是通过使用一小组成为Unix I/O的系统函数调用读写文件来实现的。
系统之间利用网络通信
从一个单独的系统来看,网络可视为一个I/O设备。
利用telnet通过网络远程运行hello的图示:
并发和并行
按照系统层次结构中由高到低的顺序,可以从三个层次讨论并发:
- 线程级并发
- 指令级并行——处理器可以同时执行多条指令的属性称为指令级并行,早期的处理器,需要多个时钟周期来执行一条指令,现在的处理器可以保持每个时钟周期2-4条指令的执行效率
- 单指令、多数据并行——在最低层次上,许多现代处理器拥有特殊的硬件,允许一条指令产生多个可以并行执行的操作,这种方式称为单指令、多数据,即SIMD并行。主要是用来提高处理影响、声音和视频数据应用的执行速度。