我们平常所编写的代码,无论是c代码、go代码、rust代码或其他代码,大都是以文本文件的形式存储在计算机硬盘上。以最简单的hello_world程序为例:

1
2
3
4
5
6
#include <stdio.h>

int main() {
		printf("hello world\n");
		return 0;
}

上述代码在磁盘中以hello.c文件存在。想要让上述代码成功在系统中运行,每条c语言语句都必须被其他程序转化为一系列的低级机器语言指令。这些指令按照一种称为可执行目标程序的格式打包,并以二进制磁盘文件的形式存放起来。

上述从源文件到目标文件的转化是由编译器驱动程序完成的:

1
linux>gcc -o hello hello.c

gcc编译器驱动程序读取源文件hello.c并把它翻译成一个可执行的目标文件hello。这个翻译过程主要分四个阶段完成:

  1. 预处理阶段:预处理器(cpp)以字符#开头的命令,修改原始c代码,将相关头文件代码插入程序文本,得到通常以.i结尾的文件;
  2. 编译阶段:编译器(cc1)将文本hello.i翻译成文本文件hello.s,它包含一个汇编语言程序。汇编语言为不同高级语言的不同编译器提供了通用的输出语言;
  3. 汇编阶段:汇编器将hello.s翻译成机器语言指令,并把指令打包成一种叫做可重定位目标程序的格式,并将结果存在hello.o的二进制文件中;
  4. 链接阶段:链接器负责将我们需要的预编译好的文件以某种方式合并到我们的hello.o中(例如C标准库中的printf就保存在printf.o中),结果得到hello文件,它就是一个可执行目标文件,它能够被加载到内存中,由系统执行。

硬件系统层面

与程序运行相关的最主要的硬件组成部分主要包括以下内容:

  1. 总线:它是贯穿系统的一组电子管道,携带信息字节并负责在各个部件之间传递。通常总线被设计成传输定长的字节块,也就是。字中字节数称为字长,是一个基本的系统参数,各个系统不尽相同。常见的字长要么是4字节(32位)要么是8字节(64位);
  2. I/O设备:它是系统与外部世界的联系通道。每个I/O设备都通过一个控制器或适配器与I/O总线相连;
  3. 主存:它是一个临时存储设备,在处理器执行程序时,用来存放程序和程序处理的数据;
  4. 处理器:CPU,是解释或执行存储在主存中指令的引擎。处理器的核心是一个大小为一个字的存储设备(寄存器),称为程序计数器。在任何时候,程序计数器都指向主存中的某条机器语言指令。

那么hello程序在执行过程中,硬件系统主要经历了哪些呢?

  1. 当我们在键盘上输入字符串./hello后,shell程序将字符串逐一读入寄存器,再把它放到内存中;
  2. 当我们键入enter后,shell程序就知道我们已经结束了输入命令,然后shell执行一系列指令来加载可执行的hello文件,从而将hello目标文件中的代码和数据从磁盘复制到内存
  3. 处理器开始执行hello程序的mian程序中的机器语言指令,这些指令将hello world\n的字节从主存复制到寄存器文件,再从寄存器文件复制到显示设备。

操作系统层面

在我们执行hello程序的过程中,我们编写的代码并没有直接去操作键盘、显示设备,这些工作其实是由操作系统提供服务。我们可以把操作系统当做是应用程序和硬件之间的一层软件。操作系统有两个基本功能:

  1. 防止硬件被失控应用程序滥用;
  2. 向应用程序提供简单一致的机制来控制复杂而又通常各不相同的低级硬件设备。

操作系统通过几个层次的抽象概念来实现上述两个重要功能:

文件:对I/O设备的抽象表示

文件就是字节序列。每个I/O设备,包括磁盘、键盘、显示器甚至网络都可以看做是文件。它向应用程序提供了一个统一的视图,看待系统中可能含有所有各式各样的I/O设备。

虚拟内存:对主存和磁盘I/O设备的抽象表示

虚拟内存是一个抽象概念。它为每个进程提供了一个假象:每个进程都在独占地使用主存。每个进程看到的内存都是一致的,称为虚拟地址空间。虚拟地址空间包括以下内容:

  1. 程序代码和数据:对于所有进程来说,代码是从同一固定地址开始的,代码和数据区是直接按照可执行目标文件的内容初始化的;
  2. 堆:代码和数据区后紧随的是运行时,在调用mallocfree这样C标准库函数时,堆可以在运行时动态地扩展或收缩;
  3. 共享库:用于存放像C标准库和数学库这样的共享库的代码和数据;
  4. 栈:位于用户虚拟地址空间的顶部是用户栈,编译器用它来实现函数调用,每次我们调用一个函数,栈就增长,从一个函数返回时,栈就会收缩;
  5. 内核虚拟内存:地址空间顶部是为内核保留的,它们必须调用内核来执行这个区的代码。

进程:对处理器、主存和I/O设备的抽象表示

进程是操作系统对一个正在运行的程序的一种抽象。一个CPU可以并发地执行多个进程,这是通过处理器在进程间切换来完成的。操作系统实现这种交错执行的机制称为上下文切换

操作系统保持跟踪进程运行所需的所有状态信息,这些状态信息称为上下文。当操作系统决定把控制权从当前进程转移给某个新进程时,就会进行上下文切换:保存当前进程的上下文,恢复新进程的上下文,然后将控制权转移给新进程,这样新进程就会从它上次停止的地方继续执行。

上边我们通过shell进程启动hello程序的过程中,shell通过调用一个专门的函数(系统调用)将控制权传递给操作系统,操作系统保存shell进程上下文并创建一个新的hello进程及其上下文,并将控制权传递给hello进程。

从一个进程到另一个进程的转换是由操作系统内核管理的。内核是操作系统代码常驻主存的部分,内核不是一个进程,相反,它是系统管理全部进程所有代码和数据结构的集合。