Skip to content

Chapter 6 Structures

1 Operating System Services

详解
OS Services Features
program execution 加载并运行程序,支持正常终止和异常终止(返回错误码)
I/O operations 允许程序访问 I/O 设备(键盘、磁盘等),管理设备读写
file systems 提供文件/目录的抽象层,允许程序执行创建/删除/读写,实现权限管理
communication 支持进程间信息交换,包括共享内存、消息传递两种模型
error detection 监控 CPU、内存、I/O 及用户程序的错误,触发相应处理机制
resource allocation 决定 CPU、内存、I/O 等资源的分配策略
accounting 跟踪用户/进程的资源使用情况,支持设置使用限制,用于统计分析
protection and security 控制资源访问权限,强制用户认证,保护用户数据安全

2 User and Operating System-Interface

  1. 命令行接口(Command line interface, CLI)

    • 又称命令解释器(command interpreter)支持直接输入命令
    • 可在内核中实现,也可通过系统程序实现
    • 有多种实现形态,即 shell
    • 主要功能是从用户处获取命令并执行
    • 命令可内置或仅作为程序名称,后者新增功能无需修改 shell
  2. 图形用户接口(Graphical User Interface, GUI)

    • 桌面隐喻,使用鼠标、键盘和显示器进行操作,用图标表示文件、程序、操作等对象
    示例
    系统 接口情况
    Windows 带有 CLI 命令 Shell 的 GUI 系统
    Mac OS X 采用 Aqua GUI 界面,底层基于 UNIX 内核,支持 Shell
    Unix/Linux 以 CLI 为基础,支持可选的 GUI 界面(如 CDE、KDE、GNOME)
  3. 触摸屏接口(Touchscreen Interfaces)

    • 适配场景:无鼠标设备或触控需求
    • 操作方式:手势识别、虚拟键盘输入、语音命令

3 System Calls

3. 1 基础知识

  • 用于访问操作系统所提供服务的编程接口,通常用高级语言(C 或 C++)实现
  • 通过高层应用程序编程接口(API)访问,而非直接调用
syscall_table
Syscall No. Name Entry point Implementation
0 read sys_read fs/read_write.c
1 write sys_write fs/read_write.c
2 open sys_open fs/open.c
3 close sys_close fs/open.c
4 stat sys_newstat fs/stat.c
5 fstat sys_newfstat fs/stat.c
x86_64 架构下 write() 系统调用的执行流程

  1. 用户代码调用 libc(C 标准库)提供的包装函数 writemain 函数通过 call 442e60 <__libc_write> 调用 libc 封装的 __libc_write 函数,此时 %rdi/%rsi/%rdx 已带齐 3 个参数

    00000000004018b5 <main>:
    4018b5: f3 0f 1e fa          endbr64
    4018b9: 55                    push   %rbp                 # 保存上一层函数的栈基址到当前栈中
    4018ba: 48 89 e5              mov    %rsp,%rbp            # 建立当前函数的栈帧
    4018bd: ba 0d 00 00 00        mov    $0xd,%edx            # 写入长度
    4018c2: 48 8d 35 3b b7 08 00  lea    0x8b73b(%rip),%rsi   # 字符串缓冲区地址
    4018c9: bf 01 00 00 00        mov    $0x1,%edi            # 文件描述符 fd=1,stdout 标准输出
    4018ce: e8 8d 15 04 00        call   442e60 <__libc_write>
    4018d3: b8 00 00 00 00        mov    $0x0,%eax            # 函数返回值
    4018d8: 5d                    pop    %rbp                 # 栈帧销毁,恢复上一层函数的栈基址
    4018d9: c3                    ret
    4018da: 66 0f 1f 44 00 00     nopw   0x0(%rax,%rax,1)
    
  2. write 对应的系统调用号(x86_64 中为 0x1)** 存入 %eax,然后执行 syscall 指令,从用户态切换到内核态(类似 32位 x86 的 int $0x80、ARM64 的 svc

    0000000000442e60 <__libc_write>:
    442e60: f3 0f 1e fa          endbr64
    442e64: 64 8b 04 25 18 00 00  mov    %fs:0x18,%eax  
            00                    
    442e6c: 85 c0                 test   %eax,%eax
    442e6e: 75 10                 jne    442e80 <__libc_write+0x20>  
    442e70: b8 01 00 00 00        mov    $0x1,%eax
    442e75: 0f 05                 syscall
    442e77: 48 3d 00 f0 ff ff     cmp    $0xfffffffffffff000,%rax  
    442e7d: 77 51                 ja     442ed0 <__libc_write+0x70>
    442e7f: c3                    ret
    442e80: 48 83 ec 28           sub 
    
  3. 进入内核态的处理流程

    1. 内核执行 kernel_entry 代码,保存用户态的寄存器,即保存用户上下文
    2. 查询 syscall_table,找到系统调用的实际处理函数并调用对应的内核函数
  4. 写操作完成后,内核执行 ret_to_user,恢复用户上下文并切回用户态继续执行

运行时环境(Run-time Environment, RTE)

  • 运行特定编程语言编写的应用程序所需的全套软件,包括该语言的编译器/解释器,以及库、加载器等其他软件
  • 提供了一个系统调用接口,该接口会拦截 API 中的函数调用,并在操作系统内部调用必要的系统调用
系统调用示例 cp in.txt out.txt

  • strace(Linux)和 dtruss(Mac OS X)可以跟踪程序执行过程中的系统调用,支持统计调用次数、附加到运行中进程
cp 命令复制一个大文件时,strace 可以观察到什么?

由于大文件无法一次性载入内存,程序会进行大量的系统调用,出现 read()write() 的高频循环

标准 API 示例:以 read() 函数为例
#include <unistd.h>
ssize_t  read(int fd, void *buf, size_t count)
┕━━━━━┙  ┕━━┙   ┕━━━━━━━━━━━━━━━━━━━━┙
 返回值  函数名         参数

读取成功时,返回实际读取的字节数;返回值 0 表示已到达文件末尾;若发生错误,read() 会返回 -1

  • time 命令是一种简单的程序执行计时方式,其精度和分辨率不算高,会报告三种时间:
    • real 时间:挂钟时间(也称为流逝时间、执行时间、运行时间等)
    • user 时间:在用户代码中消耗的时间(用户态)
    • system 时间:在系统调用中消耗的时间(内核态)

3. 2 参数传递

  • 系统调用参数传递的三种方法

    • 通过寄存器传递参数(有时参数数量可能会超过寄存器的可用数量)
    • 将参数存储在内存的一个块(或表)中,再将该块的地址通过寄存器作为参数传递

    • 由程序将参数入,再由操作系统从栈中出栈获取参数

  • 块传递和栈传递的方法不会限制所传递参数的数量或长度

  • Linux 采用了寄存器传递与块传递相结合的方式

3. 3 系统调用类型

系统调用类型 主要功能
Process control 创建/终止进程、结束、异常终止、加载/执行程序、获取/设置进程属性、等待指定时间、等待事件、发送事件信号、内存分配/释放、出错时转储内存、调试支持单步执行、用于管理进程间共享数据访问的
File management 创建/删除文件、打开/关闭文件、读取/写入/定位文件、获取/设置文件属性
Device management 请求/释放设备、读取/写入/定位设备数据、获取/设置设备属性、连接/断开设备
Information maintenance 获取/设置时间和日期、获取/设置系统数据、获取/设置进程、文件或设备属性
Communications 创建/删除通信连接、从客户端到服务器主机名进程名收发消息(若采用消息传递模型)、共享内存模型(创建并获取对内存区域的访问权限)、连接/断开远程设备
Protection 资源访问控制、权限管理、用户访问授权

4 System Services

  • 为程序的开发与执行提供便捷的环境,其中一部分只是系统调用的用户接口,另一部分则复杂得多
系统服务 主要功能
File management 创建、删除、复制、重命名、打印、转储、列出文件及目录
Status information \(\textbf{·}\) 查询日期、时间、可用内存大小、磁盘空间、用户数量
\(\textbf{·}\) 查询详细性能、日志记录与调试信息
\(\textbf{·}\) 通常会将输出格式化并打印到终端或其他输出设备上
\(\textbf{·}\) 部分系统会实现注册表(registry),用于存储和检索配置信息
File modification \(\textbf{·}\) 用于创建和修改文件的文本编辑器
\(\textbf{·}\) 用于搜索文件内容或对文本进行转换的专用命令
Programming-language support 提供编译器、汇编器、调试器和解释器
Program loading and execution 包含绝对加载器、可重定位加载器、链接编辑器、覆盖加载器,以及针对高级语言和机器语言的调试系统
Communications \(\textbf{·}\) 提供在进程、用户与计算机系统之间建立虚拟连接的机制
\(\textbf{·}\) 允许用户向彼此的屏幕发送消息、浏览网页、发送电子邮件、远程登录,以及在不同机器之间传输文件
Background Services \(\textbf{·}\) 开机启动:有些用于系统启动流程,完成后即终止;有些从系统启动持续运行到关机
\(\textbf{·}\) 提供磁盘检查、进程调度、错误日志记录、打印等功能
\(\textbf{·}\) 运行在用户上下文(而非内核上下文)中
\(\textbf{·}\) 被称为服务(services)、子系统(subsystems)、守护进程(daemons)
Application programs 与系统无关,由用户运行,通常不被视为操作系统的一部分,可通过命令行、鼠标点击、手指触屏点击等方式启动

5 Linkers and Loaders

5. 1 基本概念

  • 可重定位目标文件(relocatable object file):源代码经编译后生成的可加载至任意物理内存位置的目标文件
  • 链接器(Linker):将多个目标文件(.o)及依赖库合并为单个二进制可执行文件,完成地址重定位
  • 加载器(Loader):将可执行文件从磁盘加载到内存并分配最终内存地址
流程示意图

  • 现代系统不将库文件直接链接到可执行文件中,而是按需加载动态链接库(在 Windows 中称为 DLL),由所有使用同一版本该库的程序共享(仅加载一次)

5. 2 ELF 二进制文件基础(Executable and Linkable Format)

常见命令
  • readelf -S a.out:查看可执行文件 a.out 的所有段(Sections)
  • readelf -h a.out:读取并显示 a.out 的文件头(Header)
  • objdump -d a.out:对 a.out 进行反汇编(Disassemble)
  • 核心结构:ELF 头、程序头表(供加载器使用)、段头表(供链接器使用)
  • 关键段功能:

    • .text:存放可执行代码
    • .rodata:存放常量等已初始化的只读数据(如 static const)
    • .data:存放已初始化的全局变量、静态变量(如 static int x = 10
    • .bss:存放未初始化的全局变量、静态变量(如 static int x
    ELF 文件的内部结构示意图

  • 设置 ELF 文件的内存映射

    • 内核:将 ELF 段映射到进程虚拟地址空间;初始化栈(存储参数、环境变量)与堆(动态内存基础)
    • 加载器(动态链接场景):映射依赖库、解析符号、完成重定位
    程序运行时的内存布局示意图

    • Kernel space:高地址,用户态不可访问
    • Stack:函数调用栈,向下增长(存储局部变量、函数参数、返回地址)
    • Memory Mapping Segment:文件映射(含动态库)、匿名映射
    • Heap:动态内存分配区域,向上增长
静态链接 动态链接
文件体积 所有代码(含依赖库)打包进单个二进制可执行文件,体积大 仅打包自身代码,复用系统库文件,ELF 文件体积小
加载器依赖 无需动态加载器 依赖动态加载器(如/lib64/ld-linux-x86-64.so.2
.interp 无(无需指定动态链接器路径) 有(动态链接 ELF 文件的专属段,用于存储动态链接器的路径)
程序入口执行逻辑 execve 调用后,直接执行自身 _start 先执行动态链接器(加载共享库、解析符号),再跳转到自身 _start
库更新 需重新编译链接 库更新后无需重新编译
内存占用 多个程序使用同一库时重复加载 库仅加载一次,多程序共享
静态链接 ELF 程序的启动流程示意图

动态链接 ELF 程序的启动流程示意图

思考
120 000000000000063a <main>:
121 63a: 55                    push   %rbp
122 63b: 48 89 e5              mov    %rsp,%rbp
123 63e: 48 8d 3d 9f 00 00 00  lea    0x9f(%rip),%rdi
124 645: e8 c6 fe ff ff        callq  510 <puts@plt>
125 64a: b8 00 00 00 00        mov    $0x0,%eax
126 64f: 5d                    pop    %rbp
127 650: c3                    retq

When is puts resolved?

答案

动态链接采用延迟绑定(Lazy Binding)机制以减少程序启动时间和内存开销。

程序启动时,动态链接器仅初始化 PLT(过程链接表)/ GOT(全局偏移表)的占位条目,不会解析 puts等动态符号。

首次调用 puts,会执行 callq <puts@plt> 触发动态链接器,查找 putslibc.so 中的实际内存地址并更新 GOT 表,后续调用 puts 时会直接通过 GOT 表跳转到实际地址,无需重复解析。

Why Applications are Operating System Specific?
  • 每个操作系统都有其独有的系统调用和文件格式,在一个系统上编译的应用程序通常无法在其他操作系统上运行
  • 应用程序跨系统运行的方案
    1. 使用 Python、Ruby 等解释型语言编写,依赖多系统通用的解释器
    2. 使用包含虚拟机的语言(如Java)编写应用,通过跨系统的虚拟机(JVM)承载应用运行
    3. 使用 C 等标准语言编写,在每个目标 OS 上单独编译并运行
  • 应用程序二进制接口(ABI)相当于架构层面的 API,定义了特定架构、CPU 等环境下特定 OS 的二进制代码各组件的交互规则

6 OS Design and Implementation

6. 1 设计目标与规范

  1. 用户目标与系统目标

    • 用户目标:操作系统应便于使用、易于学习、可靠、安全且高效
    • 系统目标:操作系统应便于设计、实现与维护,同时具备灵活性、可靠性、无差错性与高效性
  2. 策略与机制分离的原则

    • 机制(How to do)负责实现核心功能
    • 策略(What to do)负责决策,允许策略独立修改而不影响机制
    示例

    调度机制(如何切换进程)与调度策略(选择哪个进程)分离

6. 2 实现语言

  • 底层:汇编语言(硬件相关操作)
  • 主体:C(高效、可移植)
  • 系统程序:C/C++、Shell、Python、Perl等脚本语言
  • 越高级的语言越容易移植(port)到其他硬件,但运行速度会更慢
  • 仿真技术(emulation)可以让操作系统在非原生硬件上运行

7 Operating System Structure

  1. 简单结构:无明确分层,功能混合,实现简单,如 MS-DOS
  2. 单体结构(Monolithic):内核所有功能(文件系统、调度、内存管理)集成在一个地址空间,支持可加载模块,如 Unix、Linux

    传统 UNIX 系统的结构示意图

    • 受硬件功能的限制,原始 UNIX 操作系统的结构化程度较低,由两个可分离的部分组成:
      • 系统程序
      • 内核:包含系统调用接口之下、物理硬件之上的所有内容,提供文件系统、CPU 调度、内存管理及其他操作系统功能,在单一层级中集成了大量功能
    Linux 系统的结构示意图

  3. 分层结构(Layered)

    • 操作系统被划分为若干个层(级别),每一层都构建在更低层级的基础之上,其中最底层(第 0 层)是硬件,最高层(第 N 层)是用户接口
    • 借助模块化特性,每一层仅使用更低层级的功能(操作)与服务
    分层结构示意图

  4. 微内核结构(Microkernel)

    • 内核仅保留最核心功能(进程间通信、地址空间管理),其他功能移至用户态,如 Mach
    • 用户模块之间通过消息传递进行通信
    • 优势:更容易扩展微内核,更容易将操作系统移植到新架构,更可靠(在内核模式下运行的代码更少),更安全
    • 劣势:用户空间与内核空间通信带来的性能开销
    微内核的系统结构示意图

  5. 模块结构(Modular)

    • 可加载内核模块(LKMs):采用面向对象的方法,每个核心组件都是独立的,通过已知接口与其他组件通信,并可根据内核中的实际需求进行加载
    • 与分层结构类似,但灵活性更高,Linux、Solaris 等系统都采用了这种方式
  6. 混合结构(Hybrid):结合多种设计方法以满足性能、安全性、可用性方面的需求,是大多数现代 OS 的实现方式

主流 OS 架构
  • LinuxSolaris:内核运行在内核地址空间中(属于单体结构),同时加入了模块化设计以实现功能的动态加载
  • Windows:主要采用单体结构,同时结合了微内核来实现不同子系统的特性
  • Mac OS X :混合分层架构,包含 Aqua 用户界面与 Cocoa 编程环境,其底层内核由 Mach 微内核、BSD Unix 组件构成,同时包含 I/O 工具包与可动态加载的模块(称为内核扩展
macOS 与 iOS 的系统结构示意图

  • Android:
    • 栈结构与 iOS 类似
    • 基于 Linux 内核进行修改:提供进程、内存、设备驱动管理功能,新增电源管理功能
    • 运行时环境包含核心库集合与 Dalvik 虚拟机,应用程序基于 Java 语言结合 Android API 开发,Java 类文件先编译为 Java 字节码,再转换为可执行文件,随后在 Dalvik 虚拟机中运行
    • 库包含网页浏览器(WebKit)、数据库(SQLite)、多媒体等框架,以及精简版的 libc 库
Android 的系统结构示意图

  • openEuler:华为开源的基于 Linux 的服务器 OS,支持模块化扩展

8 Building and Booting an Operating System

8. 1 构建流程(以 Linux 为例)

  1. 下载源码:从 kernel.org 获取内核源码
  2. 配置内核:make menuconfig(图形化配置核心功能与模块)
  3. 编译内核:make(生成内核镜像 vmlinuz
  4. 编译模块:make modules(编译可加载模块)
  5. 安装模块:make modules_install(将模块安装到内核镜像)
  6. 安装内核:make install(将新内核添加到系统启动项)

8. 2 启动流程

  1. 启动触发:系统通电初始化,程序从固定内存地址开始执行
  2. 硬件识别:执行 ROM/EEPROM 中的 BIOS/UEFI,定位内核,将其加载到内存并启动
  3. 引导加载:BIOS/UEFI 加载引导加载器(如支持多磁盘、多内核版本、多内核选项的启动选择的 GRUB);有时是 ROM 代码先加载固定位置的引导块,再由引导块从磁盘加载引导加载程序(通常支持多种启动状态,例如单用户模式)
  4. 系统启动:内核加载完成后,系统开始运行

9 Operating System Debugging

Kernighan’s Law

Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.

  1. 调试文件类型

    • 日志文件(log files):记录系统错误信息、进程运行状态
    • 核心转储(Core Dump)程序崩溃时保存进程内存镜像,用于事后调试
    • 崩溃转储(Crash Dump)OS 崩溃时保存内核内存镜像
  2. 性能调优(performance tuning)

    • 对指令指针进行周期性采样,以查找统计趋势,目标是通过消除瓶颈来提升性能
    • 操作系统必须提供计算并展示系统行为指标的方式,如 Linux 的 top 程序或 Windows 任务管理器
  3. 跟踪(tracing)

    • 收集特定事件的数据,比如系统调用触发过程中的各个步骤
    • 工具:strace(跟踪进程所调用的系统调用)、gdb(源代码级调试器)、perf(Linux性能分析)、tcpdump(网络包捕获)
Linux bcc/BPF 跟踪工具的架构示意图