- 编译系统:预处理器、编译器、汇编器、链接器
- 预处理阶段:预处理器根据以字符#开头的命令,修改原始C程序
-
- 将所有的#define删除,并且展开所有的宏定义
- 处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif等
- 处理#include 预编译指令,将被包含的文件插入到该预编译指令的位置。
- 删除所有注释 “//”和”/* */”.
- 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。
- 保留所有的#pragma编译器指令,因为编译器需要使用它们
- 编译阶段: 把预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码, 编译过程可分为6步:扫描(词法分析)、语法分析、语义分析、源代码优化、代码生成、目标代码优化。
- 词法分析:扫描器(Scanner)将源代的字符序列分割成一系列的记号(Token)。lex工具可实现词法扫描。
- 语法分析:语法分析器将记号(Token)产生语法树(Syntax Tree)。yacc工具可实现语法分析(yacc: Yet Another Compiler Compiler)。
- 语义分析:静态语义(在编译器可以确定的语义)、动态语义(只能在运行期才能确定的语义)。
- 源代码优化:源代码优化器(Source Code Optimizer),将整个语法书转化为中间代码(Intermediate Code)(中间代码是与目标机器和运行环境无关的)。中间代码使得编译器被分为前端和后端。编译器前端负责产生机器无关的中间代码;编译器后端将中间代码转化为目标机器代码。
- 目标代码生成:代码生成器(Code Generator).
- 目标代码优化:目标代码优化器(Target Code Optimizer)。
- 汇编阶段:将文本翻译成机器语言指令,并打包成可重定位(relocatable)目标程序的格式,是一个二进制文件
- 链接阶段: 调用链接器ld来链接程序运行需要的一大堆目标文件,以及所依赖的其它库文件,最后生成可执行文件。 链接的主要过程包括:地址和空间分配(Address and Storage Allocation),符号决议(Symbol Resolution),重定位(Relocation)等。链接分为静态链接和动态链接
- 静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大
- 动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。
- 系统硬件组成
- 总线: 贯穿整个系统的是一组电子管道,称为总线,它携带信息字节并负责在各个部件之间传递。通常总线,被设计成传送定长的字节快,也成为”字“。字中的字节数是一个基本的系统参数
- I/O设备:系统与外界的联系通道:如键盘、鼠标、显示器、磁盘等,每个I/O设备都是通过一个控制器或适配器与I/O总线连接起来
- 主存:是一个临时的存储设备,在处理器执行程序时,它被用来存放程序和程序处理的数据
- 处理器(CPU): 核心是一个被称为程序计数器(PC)的字长大小的存储设备(或者说寄存器register)。在任何一个时间点上,PC都指向主存中的某条机器语言指令。
- 系统软件核心:
- 操作系统(Operating System,简称OS)是管理和控制计算机硬件与软件资源的计算机程序,是直接运行在“裸机”上的最基本的系统软件,任何其他软件都必须在操作系统的支持下才能运行。
- 进程:进程是操作系统对运行程序的一种抽象,多个进程的指令交错执行(上下文切换),操作系统保存进程运行所需的所有状态信息(上下文),进行进程切换时,会保存当前进程的上下文,恢复新进程的上下文。
- 上下文通常包括目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描绘地址空间的页表、包含有关当前进程信息的进程表以及包含进程已打开文件的信息的文件表
- 调度:在进程运行的某个时刻,内核决定抢占当前进程,并重新开始一个先前被抢占的进程
- 线程:一个进程可以由多个线程的执行单元组成,每个线程都运行在进程的上下文中,并共享同样的代码和全局数据
- 虚拟存储器:为每个进程提供一个“假象”。好像每个进程都在独占地使用主存,每个进程看到的存储器都是一致的,称为虚拟地址空间,下图展示了一个Linux进程的地址空间的结构
- 底部四分之三是预留给用户程序的,包括通常的文本、数据、堆和栈段。
- 顶部四分之一是预留给内核的。地址空间的这部分包含内核在代表进程执行指令时(比如当应用程序执行一个系统调用时)使用的代码、数据和栈
- 信息存储
- 字节:8位的块,作为最小的可寻址的存储器单位
- 地址:存储器的每个字节都有唯一的数字标志,所有可能地址的集合成为虚拟地址空间
- 字:每台计算机都有一个字长(word size),指明整数和指针数据的标称大小,虚拟地址都以这样的字来编码,字长决定最重要的系统参数是虚拟地址空间的大小,对于一个字长为n为的机器,虚拟地址的范围为0~2n-1,程序最多访问2n个字节,故对于32位机器,最大地址为4GB
- 数据大小,计算机和编译器使用不同的方式来编码数字,比如不同长度的整数和浮点数,从而支持多钟数据格式
- 寻址和字节顺序:对于跨越多字节的程序对象(数组),都被存储为连续的字节序列,对象的地址为所使用的字节序列中最小的地址
- 用户模式和内核模式:处理器在不同模式之间切换
- 方式位:处理器在用某个控制寄存器中的方式位(mode bit)来限制一个应用程序可以执行的指令以及它可以访问的地址空间范围,该寄存器描述了进程当前享有的权利。
- 当方式位设置了时,进程就运行在内核模式,一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何存储器位置
- 方式位没有设置,进程运行在用户模式,用户模式中的进程不允许执行特权指令,如停止处理器、改变方式位的值或者发起一个I/O操作,也不允许进程直接引用地址空间中内核区内的代码和数据。用户程序必须通过系统调用接口间接地访问内核代码和数据
- 应用程序在用户模式下运行,某些驱动程序在用户模式下运行,
- 核心操作系统组件在内核模式下运行。多个驱动程序在内核模式下运行
- 进程从用户模式变为内核模式的唯一方法是通过注入中断、故障或者陷入系统调用这样的异常
- 方式位:处理器在用某个控制寄存器中的方式位(mode bit)来限制一个应用程序可以执行的指令以及它可以访问的地址空间范围,该寄存器描述了进程当前享有的权利。
- 系统调用:Unix系统提供了大量的系统调用,当应用程序想向内核请求服务时,比如读取一个文件、或者创建一个新的进程,都可以使用这些系统调用,如Linux提供了大约160个系统调用,输入“man syscalls”可以得到完整的列表
- 标准C库提供了一组针对最常用系统调用的方便的包装函数,包装函数将参数打好包,通过适当的系统调用陷入内核,然后将系统调用的返回状态传递给调用程序,如获取进程id,创建、终止进程等等
- 进程控制:
- 回收子进程:当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除,而是保持在一种终止状态中,知道被它的父进程回收(reaped),当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程,从此时开始,该进程就不存在了。
- 僵死进程:一个终止了但还未被回收的进程(仍然消耗系统的存储器资源),出现原因是父进程没有回收它的僵死子进程就终止了