程序运行环境

需掌握 CPU 运行模式以及程序的链接和装入方式,可能会在选择题中考察。

CPU 运行模式

CPU的两种 运行模式内核模式(又称为特权模式、系统模式或超级用户模式)和 用户模式 是操作系统设计中用于隔离系统关键任务与普通任务的机制,以提高系统的安全性和稳定性。

Users
System
Programs
Application
Programs
User
Programs
Library Routines
Operating System
Computer Hardware
More
abstract
Less
abstract
User
Mode
Kernel
Mode
注意

用户态/内核态到底是操作系统还是 cpu 的模式?

大多数现代 CPU(如 x86、ARM)都有多级 权限(ring 0~ring 3),通常:

  • Ring 0内核态):可执行一切指令,访问所有硬件资源。
  • Ring 3用户态):受限,只能执行普通指令,无法直接操作硬件或访问内核数据结构。

所以,内核态/用户态 本质上 CPU运行权限级别,是硬件提供的机制。

但操作系统是这个机制的“使用者”和“管理者”,操作系统借助这套机制实现进程隔离、安全保护、系统调用等功能。

内核模式

内核模式(Kernel Mode)是操作系统内核运行的环境,具有 最高权限,能够直接访问硬件和所有系统资源。

特点

  • 完全权限:内核模式下的代码可以直接操作硬件(如CPU、内存、I/O 设备等),执行特权指令。
  • 核心功能:操作系统内核负责管理进程、内存、文件系统和设备驱动等核心功能,这些都在内核模式下完成。
  • 上下文切换:用户模式程序通过系统调用进入内核模式,完成操作后返回用户模式。
  • 高风险:内核模式的错误可能导致系统崩溃,因此需要极高的稳定性。
特权指令

特权指令(Privileged Instruction)是指在计算机系统中只能由操作系统 内核态下 的程序执行的特殊指令。这些指令通常涉及对硬件资源或系统关键功能的直接控制,具有较高的权限,常见的特权指令包含如下类型:

  • 硬件控制:如设置中断使能、修改处理器状态寄存器。
  • 内存管理:如修改页表、设置内存保护。
  • 进程管理:如创建或终止进程、更改进程优先级。
  • I/O 操作:直接访问硬件设备或端口。
  • 系统调用相关:如切换到内核态以执行系统服务。

用户模式

用户模式(User Mode)是普通 应用程序 运行的环境,运行在 受限的权限级别。用户程序(如浏览器、文本编辑器等)通常在此模式下运行。

特点

  • 权限受限:用户模式下的程序无法直接访问硬件或核心系统资源(如内存、CPU 寄存器等),需要通过系统调用(System Call)请求操作系统服务。
  • 隔离性:每个用户程序运行在自己的地址空间,相互隔离,防止程序直接干扰其他程序或系统。
  • 安全性:由于权限受限,用户程序的错误(如崩溃)通常不会直接影响操作系统。
  • 执行方式:用户程序通过调用库函数或API间接与操作系统交互。

用户模式和内核模式是操作系统实现安全性和稳定性的核心机制。用户模式提供隔离和安全的环境运行应用程序,而内核模式负责核心资源管理和特权操作。两者通过系统调用等机制协作,共同完成计算任务。

系统调用

系统调用(system call)是运行在用户模式的应用程序与操作系统内核之间的 接口。当应用程序需要执行一些它在用户模式下不能直接完成的任务(如文件操作、网络通信、创建进程等)时,它可以通过系统调用来请求操作系统内核在内核模式下为其执行这些操作。

用户程序执行
发起系统调用
执行系统调用
从系统调用返回
用户态
内核态

系统调用的过程如上图所示,从用户态进入内核态,完成后再返回用户态:

  • 当用户程序执行系统调用(或触发中断)时,CPU 会自动 切换到内核态,这时控制权就交给了操作系统。
  • 操作系统执行完后,再通过特定指令(如iret, sysret, eret)切回用户态

特点

  • 特权级的转换:应用程序通常在用户模式下运行,而操作系统内核在内核模式下运行。系统调用提供了从用户模式到内核模式的一种安全的转换机制,这样内核可以代表应用程序执行特权操作。
  • 系统调用的类型:常见的系统调用类型包括进程管理(如创建、终止进程)、文件操作(如打开、读取、写入、关闭文件)、网络通信、设备控制、内存管理等。
  • 性能开销:执行系统调用涉及上下文切换,从用户模式到内核模式,然后再返回。这会带来一定的性能开销。因此,频繁地进行系统调用可能会影响应用程序的性能。

举个实际的 x86 例子

  1. 用户程序执行 int 0x80(系统调用指令);
  2. CPU 自动:
    • 切换到 Ring 0(即内核态);
    • 跳转到操作系统设定的系统调用处理函数;
  3. 操作系统执行相关服务;
  4. 执行 iret 返回用户程序;
  5. CPU 自动:
    • 切回 Ring 3(即用户态);
    • 恢复用户程序继续运行。

程序的链接

程序的链接是将编译后的代码模块(通常是目标文件)和其他所需的库组合在一起,生成一个可以执行的程序或库的过程。链接过程由 链接器(linker)完成。

根据所使用的库的链接方式,链接可以分为 静态链接动态链接

  • 静态链接
    • 当使用静态链接时,外部代码和库在链接阶段被 整合到最终的可执行文件中。这意味着,如果程序使用了某个库的函数,那么这些函数的代码会被复制到最终的二进制文件中。
    • 结果是一个较大的可执行文件,因为它包含了所有必要的代码以独立运行。
  • 动态链接
    • 使用动态链接时,外部库不会被直接嵌入到最终的可执行文件中。相反,程序包含了对动态链接库(如 Linux 中的.so 文件或 Windows 中的.dll 文件)的引用。当程序启动时,这些库会被 动态加载 到内存中供程序使用。
    • 动态链接的库通常称为动态链接库(Dynamic Link Libraries,DLL)或共享对象(Shared Object)。

静态链接

printf.o
strlen.o
rand.o
libc.a
foo.o
bar.o
baz.o
libstrange.a
Static
Linker
main.o
main.o
printf.o
baz.o
main.o
printf.o
baz.o
prog.exe
prog.exe
拷贝到另一台机器
Static Linking

简单来说,对于 静态链接依赖 直接作为 二进制 被打包进最后的 可执行程序 中,所以可以直接 跨机器运行(当然操作系统得一样)。

动态链接

printf.o
strlen.o
rand.o
libc.so
foo.o
bar.o
baz.o
libstrange.so
Dynamic
Linker
main.o
prog.exe
prog.exe
拷贝到另一台机器
Dynamic Linking
main.o
libc.so
libstrange.so
main.o
libc.so
libstrange.so
printf.o
strlen.o
rand.o
libc.so
libstrange.so
? ? ?

对于 动态链接,程序只保存依赖的库地址,依赖与可执行程序 分开保存,当程序执行时再去 动态地加载依赖库。对于动态链接,拷贝可执行程序到另一台机器不一定能直接运行,这需要另外一台机器也保存有相应依赖。

两者的区别如下表所示:

特点/链接方式静态链接动态链接
文件大小通常较大,因为库代码被整合到可执行文件中通常较小,只包含对库的引用
运行依赖不需要外部库文件需要相应版本的动态链接库文件
存储效率较低,每个程序都有库的一个副本较高,多个程序共享同一个库文件
更新便利性较差,更新库需要重新链接和分发程序较好,只需更新库文件
启动性能通常更快,无需加载外部库可能稍慢,需要加载外部库
跨版本兼容性较好,因为程序包含了特定版本的库代码可能出现问题,特别是当库接口发生变化时

程序的装入

程序的装入是指将程序或进程的代码和数据从磁盘加载到主存(RAM)中的过程,使其准备好被 CPU 执行。装入过程在程序执行周期中是必不可少的一部分,通常由操作系统中的 装入器(loader)完成。

绝对装入

适用于 单道程序环境。在编译时,若知道程序驻留在内存的某个位置,则编译程序将产生绝对地址的目标地址。绝对装入程序按照装入模块的地址,将程序和数据装入内存。由于程序中的逻辑地址与实际地址完全相同,因此不需要对程序和数据的地址进行修改。

另外,程序中所用的绝对地址,可在编译或汇编时给出,也可由程序员直接赋予。而通常情况下在程序中采用的是符号地址,编译或汇编时在转换为绝对地址。

可重定位装入

多道程序环境 下,多个目标模块的起始地址通常都从 0 开始,程序中的其他地址都是相对于起始地址的,此时应采用可重定位装入方式。根据内存的当前情况,将装入模块装入内存的适当位置。在装入时对目标程序中指令和数据地址的修改过程称为 重定位,又因为地位变换通常是在进程装入时一次完成的,故称为 静态重定位

当一个作业装入内存时,必须给它分配要求的全部内存空间,若没有足够的内存,则无法装入。此外,作业一旦装入内存,整个运行期间就不能在内存中移动,也不能再申请内存空间。

LOAD 1, 6
Add      1,8
Store  1,10
A               
B               
LOAD 1,106
Add   1,108
Store 1,110
A               
B               
0
2
4
6
8
100
102
104
106
108
10
110
地址空间
存储空间

动态运行时装入

也称为 动态重定位。装入程序把装入模块装入内存后,并不立即把装入模块的相对地址转换为绝对地址,而是把这种地址转换推迟到程序真正要执行时进行。因此,装入内存后的所有地址都是相对地址。这种地址需要一个重定位寄存器的支持。

LOAD      500
12345
500
1000
+
LOAD      1.500
12345
0
100
500
1000
1500
地址空间
存储空间

动态重定位的优点在于可以将程序分配到不连续的存储区;在程序运行之前可以只装入部分代码即可运行,然后在程序运行期间,根据需要动态申请分配内存。