程序运行环境
这一节选择题还是经常考察的,重点在于理解 用户态和内核态 以及 系统调用 这两个概念,程序的链接和装入相对考察频率比较低。
CPU 运行模式
CPU 提供 内核模式(又称特权模式、系统模式)和 用户模式 等运行权限级别。操作系统利用硬件权限机制隔离内核与普通应用,提高系统的安全性和稳定性。
注意
用户态/内核态到底是操作系统还是 cpu 的模式?
大多数现代 CPU(如 x86、ARM)都有多级 权限(ring 0~ring 3),通常:
- Ring 0(内核态):可执行一切指令,访问所有硬件资源。
- Ring 3(用户态):受限,只能执行普通指令,无法直接操作硬件或访问内核数据结构。
所以,内核态/用户态 本质上是 CPU 的运行权限级别,是硬件提供、由操作系统管理和使用的机制。
但操作系统是这个机制的“使用者”和“管理者”,操作系统借助这套机制实现进程隔离、安全保护、系统调用等功能。
内核模式
内核模式(Kernel Mode)是操作系统内核运行的环境,具有 最高权限,能够直接访问硬件和所有系统资源。
特点
- 完全权限:内核模式下的代码可以直接操作硬件(如CPU、内存、I/O 设备等),执行特权指令。
- 核心功能:操作系统内核负责管理进程、内存、文件系统和设备驱动等核心功能,这些都在内核模式下完成。
- 上下文切换:用户模式程序通过系统调用进入内核模式,完成操作后返回用户模式。
- 高风险:内核模式的错误可能导致系统崩溃,因此需要极高的稳定性。
特权指令
特权指令(Privileged Instruction)是指在计算机系统中只能由操作系统 内核态下 的程序执行的特殊指令。这些指令通常涉及对硬件资源或系统关键功能的直接控制,具有较高的权限,常见的特权指令包含如下类型:
- 硬件控制:如设置中断使能、修改处理器状态寄存器。
- 内存管理:如修改页表、设置内存保护。
- I/O 操作:直接访问硬件设备或端口。
创建进程、终止进程和更改优先级是操作系统服务,不是某一类 CPU 特权指令;用户程序通常通过系统调用提出请求。用于发起系统调用的陷入指令本身必须允许用户态执行,否则用户程序无法进入内核;真正的特权操作由内核在完成参数和权限检查后执行。
用户模式
用户模式(User Mode)是普通 应用程序 运行的环境,运行在 受限的权限级别。用户程序(如浏览器、文本编辑器等)通常在此模式下运行。
特点
- 权限受限:用户模式下的程序可以正常使用通用寄存器并访问本进程获准的虚拟内存,但不能直接访问设备、内核地址空间以及控制寄存器等特权资源;需要通过系统调用(System Call)请求操作系统服务。
- 隔离性:每个用户程序运行在自己的地址空间,相互隔离,防止程序直接干扰其他程序或系统。
- 安全性:由于权限受限,用户程序的错误(如崩溃)通常不会直接影响操作系统。
- 执行方式:用户程序通过库函数或 API 间接与操作系统交互;只有需要内核服务时才进一步发起系统调用。
用户模式和内核模式是操作系统实现安全性和稳定性的核心机制。用户模式提供隔离和安全的环境运行应用程序,而内核模式负责核心资源管理和特权操作。两者通过系统调用等机制协作,共同完成计算任务。
系统调用
系统调用(system call)是运行在用户模式的应用程序与操作系统内核之间的 接口。当应用程序需要执行一些它在用户模式下不能直接完成的任务(如文件操作、网络通信、创建进程等)时,它可以通过系统调用来请求操作系统内核在内核模式下为其执行这些操作。
一次系统调用里:进程、CPU 寄存器与 PCB 如何接力
按步骤查看 P1 从用户态陷入内核;若服务触发调度,再观察 P2 的 PCB 如何把寄存器现场恢复到 CPU。
- 状态
- 运行
- 已存现场
- 上次可恢复现场:P1
- 状态
- 就绪
- 已存现场
- PC=0x2000,等待恢复
P1 在用户态执行P1 只能使用受限特权集;PC、SP 和通用寄存器都承载 P1 当前的用户态执行现场。
区分:模式切换本身不等于进程切换:第 1—3 步仍是 P1 的执行流。只有 P1 阻塞、时间片到或调度器另作决定时,才会保存 P1、恢复 P2。不同体系结构先由硬件保存的最少控制状态位置不同;图中用 PCB 及其关联内核栈概括可恢复现场。
系统调用的过程如上图所示,从用户态进入内核态,完成后再返回用户态:
- 当用户程序执行系统调用陷入指令时,CPU 按预设入口切换到内核态。外部中断和异常也可使控制转入内核,但它们的触发来源与系统调用不同。
- 操作系统执行完后,再通过特定指令(如iret, sysret, eret)切回用户态。
特点
- 特权级的转换:应用程序通常在用户模式下运行,而操作系统内核在内核模式下运行。系统调用提供了从用户模式到内核模式的一种安全的转换机制,这样内核可以代表应用程序执行特权操作。
- 系统调用的类型:常见的系统调用类型包括进程管理(如创建、终止进程)、文件操作(如打开、读取、写入、关闭文件)、网络通信、设备控制、内存管理等。
- 性能开销:系统调用必然发生用户态/内核态的模式切换和现场保存,但不一定发生进程或线程上下文切换;只有调度器改为运行另一个执行流时才属于上下文切换。模式切换、参数检查和可能的数据复制都会产生开销。
举个实际的 x86 例子
- 用户程序执行
int 0x80(系统调用指令); - CPU 自动:
- 切换到 Ring 0(即内核态);
- 跳转到操作系统设定的系统调用处理函数;
- 操作系统执行相关服务;
- 执行
iret返回用户程序; - CPU 自动:
- 切回 Ring 3(即用户态);
- 恢复用户程序继续运行。
程序的链接
程序的链接是将编译后的代码模块(通常是目标文件)和其他所需的库组合在一起,生成一个可以执行的程序或库的过程。链接过程由 链接器(linker)完成。
根据所使用的库的链接方式,链接可以分为 静态链接 和 动态链接。
- 静态链接:
- 当使用静态链接时,外部代码和库在链接阶段被 整合到最终的可执行文件中。这意味着,如果程序使用了某个库的函数,那么这些函数的代码会被复制到最终的二进制文件中。
- 结果是一个较大的可执行文件,因为它包含了所有必要的代码以独立运行。
- 动态链接:
- 使用动态链接时,外部库不会被直接嵌入最终可执行文件。程序保存对动态链接库(如 Linux 中的
.so文件或 Windows 中的.dll文件)的引用,库可在装入或首次调用时被加载和解析。 - 动态链接的库通常称为动态链接库(Dynamic Link Libraries,DLL)或共享对象(Shared Object)。
- 使用动态链接时,外部库不会被直接嵌入最终可执行文件。程序保存对动态链接库(如 Linux 中的
静态链接
简单来说,静态链接把所需库代码复制进最终可执行文件,运行时不再依赖相应动态库;但能否跨机器运行仍取决于处理器指令集、ABI、操作系统接口和其他运行环境,不能仅凭“操作系统相同”判断。
动态链接
对于 动态链接,程序只保存依赖的库地址,依赖与可执行程序 分开保存,当程序执行时再去 动态地加载依赖库。对于动态链接,拷贝可执行程序到另一台机器不一定能直接运行,这需要另外一台机器也保存有相应依赖。
静态链接与动态链接对比
| 特点/链接方式 | 静态链接 | 动态链接 |
|---|---|---|
| 文件大小 | 通常较大,库代码被整合进可执行文件 | 通常较小,主要保存动态库引用与重定位信息 |
| 运行依赖 | 不依赖相应动态库文件 | 依赖兼容版本的动态链接库 |
| 存储效率 | 多个程序可能各自包含同一库代码 | 库文件及其只读代码页可被多个进程共享 |
| 更新便利性 | 更新库后通常需要重新链接和分发程序 | 兼容更新可只替换库,但 ABI 变化会导致不兼容 |
| 装入开销 | 通常无需运行时解析该库 | 装入或首次调用时需要加载、符号解析与重定位 |
| 可移植边界 | 仍受指令集、ABI 和系统调用接口约束 | 还额外受动态库版本与搜索路径约束 |
程序的装入
程序的装入是指将程序或进程的代码和数据从磁盘加载到主存(RAM)中的过程,使其准备好被 CPU 执行。装入过程在程序执行周期中是必不可少的一部分,通常由操作系统中的 装入器(loader)完成。
绝对装入
适用于 单道程序环境。在编译时,若知道程序驻留在内存的某个位置,则编译程序将产生绝对地址的目标地址。绝对装入程序按照装入模块的地址,将程序和数据装入内存。由于程序中的逻辑地址与实际地址完全相同,因此不需要对程序和数据的地址进行修改。
另外,程序中所用的绝对地址可在编译或汇编时给出,也可由程序员直接赋予。通常程序采用符号地址,编译或汇编时再转换为绝对地址。
可重定位装入
在 多道程序环境 下,多个目标模块的起始地址通常都从 0 开始,程序中的其他地址都是相对于起始地址的,此时应采用可重定位装入方式。根据内存的当前情况,将装入模块装入内存的适当位置。在装入时对目标程序中指令和数据地址的修改过程称为 重定位,又因为地位变换通常是在进程装入时一次完成的,故称为 静态重定位。
当一个作业装入内存时,必须给它分配要求的全部内存空间,若没有足够的内存,则无法装入。此外,作业一旦装入内存,整个运行期间就不能在内存中移动,也不能再申请内存空间。
静态重定位:装入时一次性改写地址
逐步查看逻辑地址、装入器和主存:基址确定后,地址字段在装入阶段一次性完成重定位,运行时不再逐次相加。
装入器选定物理基址 B 后,直接把模块中需要修改的相对地址改成绝对地址。
目标模块仍只认识逻辑地址模块入口和指令中的地址都从相对起点计算;此时还没有确定它会落在主存的哪里。
此刻要记住:相对地址的意义是“距模块起点的偏移”,不是物理地址。
动态运行时装入
也称为 动态重定位。装入程序把装入模块装入内存后,并不立即把装入模块的相对地址转换为绝对地址,而是把这种地址转换推迟到程序真正要执行时进行。因此,装入内存后的所有地址都是相对地址。这种地址需要一个重定位寄存器的支持。
动态重定位:每次访存由重定位寄存器形成物理地址
逐步查看同一个逻辑地址在基址变化前后的结果:程序整体移动后只需更新基址寄存器,逻辑地址本身保持不变。
程序保留相对地址;CPU / MMU 在每次访存时用重定位寄存器 BR 与逻辑地址相加。
装入后保留相对地址程序 P 先被放入 [400…699],但其指令中的地址 100 仍表示“距 P 起点 100”。
此刻要记住:动态重定位没有在装入时把每个逻辑地址永久改写为物理地址。
动态重定位把地址变换推迟到每次访存时完成,因此程序装入后仍可整体移动,并可在运行期间按需申请或扩展内存。需要注意:只使用一个基址/重定位寄存器的连续分配模型,程序仍须占用一段连续物理内存;要把程序离散地放入不连续区域,必须进一步采用分页、分段或段页式等映射机制。支持请求调页时,程序也可以只装入当前需要的部分,但这不是单个重定位寄存器本身提供的能力。