跳转至

01 - 操作系统概述

操作系统概述分层示意图

建议学习时间:1.5 小时 🎯 难度等级:⭐⭐ 📋 前置知识:计算机组成原理基础、C/Python 编程基础


📋 本章目录


一、操作系统的定义

1.1 什么是操作系统?

操作系统(Operating System, OS)是管理计算机硬件和软件资源的系统软件,是计算机系统中最基本的系统软件。它可以从两个角度来理解:

角度一:资源管理者(Resource Manager)

计算机系统拥有多种资源:CPU 时间、内存空间、磁盘空间、I/O 设备等。当多个程序同时运行时,操作系统负责合理分配和管理这些资源,确保每个程序都能获得所需的资源,同时防止资源冲突和浪费。

Text Only
              ┌─────────────┐
              │   用户程序    │
              │ App1 App2 .. │
              └──────┬───────┘
                     │ 资源请求
              ┌──────▼───────┐
              │  操作系统     │  ← 资源管理者
              │  (OS Kernel) │
              └──────┬───────┘
                     │ 管理和分配
        ┌────────┬───┴───┬────────┐
        ▼        ▼       ▼        ▼
      CPU      内存    磁盘    I/O设备

角度二:抽象机器(Extended Machine / Virtual Machine)

操作系统对底层硬件进行抽象和封装,向上层应用程序提供简洁、统一的接口。程序员不需要知道磁盘的具体物理结构,只需要调用 open()read()write() 等系统调用就能操作文件。

Text Only
没有操作系统时:
  程序员需要直接控制磁盘控制器的寄存器
  → 设置磁头位置、柱面号、扇区号
  → 发送读写命令、等待中断
  → 处理错误和重试
  → 极其复杂!

有操作系统后:
  程序员只需要:
  fd = open("data.txt", O_RDONLY);
  read(fd, buffer, size);
  close(fd);
  → 简单、安全、高效!

操作系统的标准定义

操作系统是一种运行在内核态(Kernel Mode)的软件,它管理计算机的硬件资源,为应用程序提供运行环境,并向用户和程序员提供访问硬件的接口。

1.2 操作系统的地位

操作系统在计算机软件层次结构中处于核心位置:

Text Only
┌─────────────────────────────────┐
│         用户(User)             │  ← 最上层
├─────────────────────────────────┤
│       应用程序(Applications)   │
│    浏览器、Office、游戏...       │
├─────────────────────────────────┤
│     系统工具(System Utilities) │
│    编译器、Shell、包管理器...    │
├─────────────────────────────────┤
│     ★ 操作系统(OS Kernel)★    │  ← 核心位置
├─────────────────────────────────┤
│         硬件(Hardware)         │  ← 最底层
│    CPU、内存、磁盘、网卡...      │
└─────────────────────────────────┘

1.3 操作系统与其他软件的区别

特征 操作系统 应用软件
运行模式 内核态(Ring 0) 用户态(Ring 3)
启动时机 开机时就启动,常驻内存 用户按需启动
资源访问 可以直接访问所有硬件 必须通过系统调用间接访问
生命周期 与计算机运行同生共死 可以被启动和终止
唯一性 同一时刻只有一个 OS 运行 可以同时运行多个应用

二、操作系统的发展历史

操作系统的发展历程是一部不断追求更高效率更好抽象的历史。每一代操作系统都是为了解决上一代的不足而诞生的。

2.1 第零代:无操作系统(1940s-1950s)

硬件背景:第一代电子管计算机(如 ENIAC)

特点: - 没有操作系统,程序员直接操作硬件 - 使用纸带或插线板输入程序 - 一次只能运行一个程序 - 程序员需要预约机时,亲自到机房操作

问题: - CPU 利用率极低(大部分时间在等待人工操作) - 编程极其困难(机器语言 + 手动接线)

Text Only
典型工作流程:
1. 程序员预约机器时间(可能是凌晨 3 点)
2. 携带纸带到机房
3. 装载纸带、开机、运行
4. 观察结果、收集输出
5. 关机、离开

→ CPU 可能 90% 的时间在空闲等待!

2.2 第一代:批处理系统(1950s-1960s)

2.2.1 单道批处理系统

核心思想:用监控程序(Monitor)代替人工操作,自动加载和执行作业。

工作方式: - 操作员将多个作业收集到磁带上,一次性提交给计算机 - 监控程序自动依次加载和执行每个作业 - 作业之间无需人工干预

Text Only
┌──────────┐    ┌──────────┐    ┌──────────┐
│  作业 1   │───►│  作业 2   │───►│  作业 3   │
│ (计算)    │    │ (计算)    │    │ (计算)    │
└──────────┘    └──────────┘    └──────────┘
                 顺序执行,无重叠

优点:减少了人工操作时间,提高了吞吐量 缺点: - CPU 在 I/O 时仍然空闲(I/O 速度远慢于 CPU) - 没有交互性,用户提交作业后只能等待结果

2.2.2 多道批处理系统

核心思想多道程序设计(Multiprogramming)——内存中同时存放多个作业,当一个作业因为 I/O 而等待时,CPU 立即切换执行另一个作业。

工作方式

Text Only
时间轴:
        ┌──计算──┐  ┌──计算──┐     ┌──计算──┐
作业A:  │████████│  │████████│     │████████│
        └────────┘  └────────┘     └────────┘

        ┌─I/O─┐  ┌──计算──┐  ┌─I/O─┐  ┌──计算──┐
作业B:  │▓▓▓▓▓│  │████████│  │▓▓▓▓▓│  │████████│
        └─────┘  └────────┘  └─────┘  └────────┘

CPU:    │A│  B计算  │  A计算  │  B计算  │  A计算  │
        CPU几乎不空闲!

关键技术: - 内存管理:需要将多个作业同时放入内存 - 作业调度:决定哪些作业进入内存 - CPU 调度:决定将 CPU 分配给哪个作业 - 内存保护:防止作业之间互相干扰

优点:CPU 利用率大幅提高,吞吐量增加 缺点: - 仍然没有交互性 - 作业从提交到完成可能要等很长时间 - 难以调试程序

2.3 第二代:分时系统(1960s-1970s)

核心思想:将 CPU 时间分成时间片(Time Slice),多个用户通过终端同时使用一台计算机,每个用户感觉自己独占了一台计算机。

代表系统: - CTSS(Compatible Time-Sharing System,MIT,1961) - Multics(MIT + GE + Bell Labs,1964) - UNIX(Ken Thompson & Dennis Ritchie,AT&T Bell Labs,1969)

工作方式

Text Only
用户A 用户B 用户C 用户D
  │     │     │     │
  ▼     ▼     ▼     ▼
┌─────────────────────┐
│     分时操作系统       │
│                     │
│  时间片轮转:         │
│  [A][B][C][D][A][B]..│
│  每个时间片约20ms    │
└──────────┬──────────┘
         CPU

特点: - 交互性好:用户可以直接与系统交互 - 多用户:支持多个用户同时使用 - 响应时间短:通常在几秒钟内响应 - 资源共享:多个用户共享 CPU、内存、磁盘等资源

分时系统 vs 批处理系统

特征 批处理系统 分时系统
主要目标 吞吐量 响应时间
用户交互 实时交互
CPU 分配 一个作业占用直到 I/O 时间片轮转
用户数量 无直接用户 多用户
典型响应时间 小时/天

2.4 第三代:实时操作系统(1970s-)

核心思想:保证在严格的时间约束内完成特定任务。

分类

Text Only
实时操作系统(RTOS)
├── 硬实时(Hard Real-Time)
│   └── 必须在截止时间前完成,否则可能造成灾难
│       例:飞行控制系统、心脏起搏器、ABS刹车系统
└── 软实时(Soft Real-Time)
    └── 偶尔超过截止时间可以接受,但会降低性能
        例:视频播放、音频处理、游戏

代表系统:VxWorks、FreeRTOS、QNX、RT-Linux

关键特性: - 确定性:任务的响应时间可以预测 - 优先级调度:高优先级任务可以抢占低优先级任务 - 最小中断延迟:从中断发生到开始处理的时间尽可能短 - 内核可抢占:内核代码也可以被高优先级任务抢占

2.5 第四代:微内核架构(1980s-1990s)

设计动机:传统的宏内核(Monolithic Kernel)将所有功能(进程管理、内存管理、文件系统、设备驱动、网络协议栈...)都放在内核态,导致内核庞大且难以维护。

核心思想:只在内核中保留最基本的功能(进程间通信 IPC、基本调度、内存保护),其他功能作为用户态的服务器进程运行。

Text Only
宏内核架构:
┌───────────────────────────────┐
│         用户态(User Mode)     │
│    App1    App2    App3       │
├───────────────────────────────┤
│         内核态(Kernel Mode)   │
│  进程管理 │ 内存管理 │ 文件系统  │
│  设备驱动 │ 网络     │ 安全     │
│  ...(所有功能都在内核态)       │
└───────────────────────────────┘

微内核架构:
┌────────────────────────────────────────────┐
│              用户态(User Mode)              │
│  App1  App2  │ 文件服务  网络服务  驱动程序   │
│              │(以用户态服务器进程运行)       │
├────────────────────────────────────────────┤
│              内核态(Kernel Mode)            │
│           IPC │ 调度 │ 内存保护              │
│          (最小化的微内核)                   │
└────────────────────────────────────────────┘

代表系统:Mach、L4、MINIX 3、QNX

微内核 vs 宏内核

特征 宏内核 微内核
内核大小 大(数百万行代码) 小(数万行代码)
性能 高(函数调用) 较低(需要 IPC)
可靠性 一个模块崩溃可能导致整个系统崩溃 服务崩溃不影响内核
可维护性 难以维护和调试 模块化,易于维护
安全性 攻击面大 攻击面小
代表系统 Linux、FreeBSD QNX、MINIX 3

2.6 第五代:现代混合内核(1990s-至今)

核心思想:结合宏内核的性能优势和微内核的模块化优势

代表系统: - Windows NT/10/11:混合内核,核心功能在内核态,部分子系统(如 Win32)在用户态 - macOS/iOS (XNU):Mach 微内核 + BSD 宏内核组件 - Linux:本质是宏内核,但通过可加载内核模块(LKM)实现了模块化 - 鸿蒙 OS:微内核架构,面向 IoT 场景

Text Only
现代操作系统发展时间线:

1940s   1950s   1960s   1970s   1980s   1990s   2000s   2010s   2020s
  │       │       │       │       │       │       │       │       │
  │  无OS  │ 批处理  │ 分时   │ RTOS  │ 微内核 │ 混合   │  移动   │ 云+IoT │
  │       │       │  UNIX  │       │ Mach  │ NT    │ Android│ 鸿蒙   │
  │       │       │ Multics│       │ L4    │ Linux │ iOS    │ Fuchsia│
  ▼───────▼───────▼───────▼───────▼───────▼───────▼───────▼───────▼

2.7 发展历史总结表

时代 系统类型 解决的问题 关键技术 代表系统
1940s 无 OS - 手动操作 -
1950s 单道批处理 减少人工干预 监控程序 FMS
1960s 多道批处理 提高CPU利用率 多道程序设计 OS/360
1960s 分时系统 提供交互性 时间片轮转 UNIX
1970s 实时系统 满足时间约束 优先级调度 VxWorks
1980s 微内核 可靠性/模块化 IPC + 服务器进程 Mach
1990s至今 混合内核 性能 + 模块化 宏内核 + 模块化 Linux/Windows

三、操作系统的五大功能

操作系统的核心功能可以归纳为五大类,每一类管理一种关键资源:

3.1 进程管理(CPU 管理)

管理对象:CPU 时间

核心任务: - 进程创建与终止:创建新进程、终止已完成或异常的进程 - 进程调度:在多个就绪进程中,决定哪一个获得 CPU 使用权 - 进程同步与互斥:协调多个进程对共享资源的访问 - 进程通信:提供进程间通信机制(IPC) - 死锁处理:预防、避免、检测和解除死锁

Text Only
进程管理相关系统调用:
├── fork()      // 创建子进程
├── exec()      // 加载新程序
├── wait()      // 等待子进程结束
├── exit()      // 终止进程
├── kill()      // 发送信号
├── getpid()    // 获取进程 ID
└── pthread_create()  // 创建线程

3.2 内存管理

管理对象:主存(RAM)

核心任务: - 内存分配与回收:为进程分配内存,进程结束后回收 - 地址翻译:将逻辑地址转换为物理地址 - 内存保护:防止进程访问非法内存区域 - 虚拟内存:通过页面调度扩展可用内存 - 内存共享:允许多个进程共享同一段物理内存

3.3 文件管理

管理对象:磁盘上的文件和目录

核心任务: - 文件的创建、删除、读写 - 目录管理:层级目录结构 - 磁盘空间分配:管理空闲磁盘块 - 文件权限控制:access control - 文件系统一致性:防止数据损坏

3.4 I/O 设备管理

管理对象:各种 I/O 设备(键盘、屏幕、磁盘、网卡等)

核心任务: - 设备驱动程序管理:为每种设备提供统一的接口 - 缓冲管理:通过缓冲区协调 CPU 和 I/O 设备的速度差异 - 设备分配:将设备分配给请求的进程 - 中断处理:响应来自设备的中断信号

3.5 用户接口

提供给用户和程序员的访问方式: - 命令行接口(CLI):Shell(bash, zsh, PowerShell) - 图形用户接口(GUI):桌面环境(GNOME, KDE, Windows Desktop) - 系统调用接口(API):程序通过系统调用访问 OS 服务

Text Only
用户接口的层次:
┌──────────────┐
│  GUI / CLI   │  ← 用户直接使用
├──────────────┤
│   库函数     │  ← printf() 封装了 write()
│  (libc etc.) │
├──────────────┤
│  系统调用    │  ← 用户态到内核态的接口
│ (syscall)    │
├──────────────┤
│  OS 内核     │
└──────────────┘

3.6 五大功能总结

Text Only
┌──────────────────────────────────────────────┐
│                  操作系统                      │
│                                              │
│  ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌────┐ │
│  │进程  │ │内存  │ │文件  │ │ I/O  │ │用户│ │
│  │管理  │ │管理  │ │管理  │ │ 管理 │ │接口│ │
│  │      │ │      │ │      │ │      │ │    │ │
│  │ CPU  │ │ RAM  │ │ Disk │ │Device│ │CLI │ │
│  │调度  │ │虚拟  │ │文件  │ │驱动  │ │GUI │ │
│  │同步  │ │分页  │ │目录  │ │缓冲  │ │API │ │
│  │通信  │ │保护  │ │权限  │ │中断  │ │    │ │
│  └──────┘ └──────┘ └──────┘ └──────┘ └────┘ │
└──────────────────────────────────────────────┘

四、内核态与用户态

4.1 为什么需要区分内核态和用户态?

假设没有内核态/用户态的区分,所有程序都可以直接访问硬件:

Text Only
危险场景:
1. 恶意程序直接读写其他进程的内存 → 窃取密码
2. 有 bug 的程序直接操作磁盘控制器 → 破坏文件系统
3. 普通程序禁用中断 → 导致系统死机
4. 用户程序修改页表 → 绕过所有安全限制

因此,现代 CPU 设计了特权级(Privilege Level)机制,将指令分为特权指令非特权指令

  • 特权指令:只能在内核态执行(如修改页表、禁用中断、直接 I/O)
  • 非特权指令:在两种态下都可以执行(如加减运算、比较、跳转)

4.2 x86 特权级体系

x86 处理器定义了 4 个特权级(Protection Ring),用 CPU 状态寄存器 CS 段的低两位表示:

Text Only
              Ring 0(内核态)
           ┌────────────────┐
           │   OS Kernel    │  最高特权
           │  可执行所有指令 │
        ┌──┴────────────────┴──┐
        │     Ring 1           │  驱动程序
        │   (通常未使用)        │  (某些OS用)
     ┌──┴──────────────────────┴──┐
     │        Ring 2               │
     │      (通常未使用)            │
  ┌──┴────────────────────────────┴──┐
  │           Ring 3(用户态)         │
  │        用户应用程序               │  最低特权
  │     只能执行非特权指令            │
  └──────────────────────────────────┘

在实际的 Linux/Windows 系统中,通常只使用 Ring 0(内核态)Ring 3(用户态)两个级别。

4.3 内核态与用户态的切换

CPU 从用户态切换到内核态只有三种途径:

途径一:系统调用(System Call)— 主动切换

用户程序通过特殊的 trap 指令(如 x86 的 int 0x80syscall 指令)主动请求内核服务。

Text Only
系统调用完整流程:

用户态                    内核态
  │                        │
  │  1. 准备参数            │
  │     (寄存器/栈)         │
  │                        │
  │  2. 执行 syscall 指令   │
  │  ─────────────────────► │
  │        trap            │  3. 保存用户态上下文
  │                        │     (寄存器、PC、SP)
  │                        │
  │                        │  4. 查系统调用表
  │                        │     根据系统调用号
  │                        │     跳转到对应处理函数
  │                        │
  │                        │  5. 执行系统调用
  │                        │     (如 read/write)
  │                        │
  │                        │  6. 恢复用户态上下文
  │  ◄───────────────────── │
  │        返回(iret)     │  7. 切换回用户态
  │                        │
  │  8. 获取返回值          │
  │                        │

系统调用号示例(Linux x86-64)

系统调用号 名称 功能
0 read 读文件
1 write 写文件
2 open 打开文件
3 close 关闭文件
39 getpid 获取进程 ID
57 fork 创建子进程
59 execve 执行程序
60 exit 终止进程

途径二:异常(Exception)— 被动切换

程序执行过程中发生了异常情况(如除零、缺页),CPU 自动切换到内核态处理。

途径三:外部中断(Interrupt)— 被动切换

外部设备(如定时器、键盘、磁盘)向 CPU 发送中断信号,CPU 暂停当前任务,切换到内核态处理中断。

4.4 系统调用的详细机制

以 Linux x86-64 平台的 write() 系统调用为例:

C
// 用户程序调用 write()
#include <unistd.h>
ssize_t result = write(1, "Hello\n", 6);  // 向标准输出写入

// 实际的调用链:
// write() [C库函数, glibc]
//   → 将系统调用号 1 放入 rax 寄存器
//   → 将参数放入 rdi, rsi, rdx 寄存器
//   → 执行 syscall 指令
//   → CPU 切换到内核态
//   → 根据 rax 中的调用号,在 sys_call_table 中查找
//   → 调用 sys_write() 内核函数
//   → 内核完成写操作
//   → 将返回值放入 rax
//   → 执行 sysret/iretq 返回用户态

系统调用的开销

Text Only
系统调用的开销包括:
1. 保存/恢复寄存器上下文        ~100 cycles
2. 切换特权级(Ring 3 → Ring 0) ~50 cycles
3. TLB 刷新(如果需要)          ~variable
4. 内核代码执行                  ~variable
5. 切换回用户态                  ~50 cycles

总计:一次系统调用约 200-1000 CPU 周期
      对于 3GHz CPU,约 0.1-0.3 微秒

→ 所以应尽量减少系统调用次数(如用缓冲I/O代替直接I/O)

4.5 内核态与用户态的关键区别总结

比较维度 用户态(Ring 3) 内核态(Ring 0)
权限级别 最低 最高
可执行指令 仅非特权指令 所有指令
内存访问 仅用户空间(低地址) 全部地址空间
硬件访问 不能直接访问 可以直接访问
崩溃影响 仅影响当前进程 导致系统崩溃(内核恐慌)
切换方式 系统调用/异常/中断 iret/sysret 返回

五、中断与异常

中断和异常是操作系统响应外部事件和处理内部错误的核心机制。没有中断机制,操作系统将无法实现多任务处理。

5.1 为什么需要中断?

假设没有中断机制,CPU 想知道键盘是否有按键,只能不断地去轮询(Polling)键盘控制器:

Text Only
没有中断 → 轮询方式:
while (true) {
    if (keyboard_ready()) {       // 不断检查键盘
        char c = read_keyboard();
        process(c);
    }
    // CPU 大部分时间在做无用的检查!
}

有中断 → 中断驱动方式:
// CPU 正常执行其他任务
// 当键盘有按键时:
//   1. 键盘控制器发送中断信号给 CPU
//   2. CPU 暂停当前任务
//   3. 跳转到键盘中断处理程序
//   4. 读取按键、处理
//   5. 返回继续执行之前的任务
// → CPU 不浪费时间轮询!

5.2 中断的分类

Text Only
CPU 接收到的事件
├── 中断(Interrupt)— 来自外部,异步
│   ├── 可屏蔽中断(Maskable Interrupt)
│   │   ├── 硬中断(Hardware Interrupt)
│   │   │   ├── 定时器中断(Timer Interrupt)
│   │   │   ├── 键盘中断
│   │   │   ├── 磁盘中断
│   │   │   └── 网卡中断
│   │   └── 软中断(Software Interrupt)
│   │       ├── Linux 的 softirq(下半部处理)
│   │       └── tasklet
│   └── 不可屏蔽中断(NMI, Non-Maskable Interrupt)
│       └── 硬件故障(如内存校验错误)
└── 异常(Exception)— CPU 内部,同步
    ├── Fault(故障)— 可恢复
    │   ├── 缺页异常(Page Fault)
    │   ├── 段错误的某些情况
    │   └── 一般保护异常
    ├── Trap(陷阱)— 有意触发
    │   ├── 系统调用(int 0x80 / syscall)
    │   ├── 断点(int 3,调试用)
    │   └── 溢出检测(into)
    └── Abort(终止)— 不可恢复
        ├── 硬件错误(如总线错误)
        └── 双重故障(Double Fault)

5.3 硬中断与软中断

硬中断(Hardware Interrupt)

由外部硬件设备触发,通过中断控制器(如 APIC)传递给 CPU。

特点: - 异步:随时可能发生,与 CPU 执行的指令无关 - 可屏蔽:CPU 可以通过设置中断标志位(IF)暂时屏蔽 - 优先级:不同设备的中断有不同优先级

硬中断处理流程

Text Only
1. 设备完成操作,向中断控制器发送中断信号
2. 中断控制器判断优先级,向 CPU 的 INTR 引脚发信号
3. CPU 在当前指令执行完后检查 INTR
4. 如果 IF=1(中断未屏蔽),CPU 响应中断:
   a. 保存当前上下文(PC、PSW、寄存器)到栈
   b. 根据中断号查 IDT(中断描述符表)
   c. 获取中断处理程序的入口地址
   d. 关中断(防止嵌套)
   e. 跳转到中断处理程序执行
5. 中断处理程序执行完毕
6. 恢复上下文,开中断
7. 返回被中断的程序继续执行

软中断(Software Interrupt)

在 Linux 中,软中断是中断处理的下半部分(Bottom Half)机制,用于延迟处理不紧急的中断工作。

为什么需要软中断?

Text Only
问题:硬中断处理时间不能太长(会屏蔽其他中断)
解决:将中断处理分为两部分

上半部(Top Half)—— 硬中断处理
├── 快速执行
├── 屏蔽中断
├── 只做最紧急的工作(如从设备读数据到缓冲区)
└── 标记软中断,告诉下半部有工作要做

下半部(Bottom Half)—— 软中断处理
├── 延后执行
├── 不屏蔽中断
├── 处理不紧急的工作(如网络协议栈处理)
└── 通过 softirq / tasklet / workqueue 实现

网络数据包接收示例

Text Only
网卡收到数据包
  ▼ [硬中断 - 上半部]
  1. 将数据从网卡 DMA 缓冲区复制到内核缓冲区
  2. 标记 NET_RX_SOFTIRQ 软中断
  3. 硬中断返回(非常快,约几微秒)
  ▼ [软中断 - 下半部]
  4. 处理网络协议栈(IP、TCP/UDP 解析)
  5. 将数据放入 Socket 接收缓冲区
  6. 唤醒等待数据的用户进程

5.4 异常的三种类型详解

Fault(故障)— 可恢复的错误

特点: - 发生异常的指令可以被重新执行 - 异常处理后,控制权返回到引起 Fault 的那条指令 - 如果无法恢复,则终止进程

最常见的 Fault:缺页异常(Page Fault)

Text Only
进程访问一个页面 P
  ├── P 在物理内存中 → 正常访问(无异常)
  └── P 不在物理内存中 → 触发缺页 Fault
      缺页异常处理程序:
        1. 检查地址是否合法
           ├── 不合法 → 发送 SIGSEGV(段错误),终止进程
           └── 合法 → 继续
        2. 在磁盘上找到对应的页面
        3. 分配一个空闲物理页框
        4. 将页面从磁盘读入物理页框
        5. 更新页表
        6. 返回,重新执行引起缺页的那条指令
        → 这次成功访问!

Trap(陷阱)— 有意触发

特点: - 由程序故意触发,是一种预设的机制 - 异常处理后,控制权返回到 trap 指令的下一条指令 - 最典型的应用:系统调用

Text Only
系统调用使用 trap 实现:

用户程序:
  mov rax, 1        ; 系统调用号 = 1 (write)
  mov rdi, 1        ; fd = 1 (stdout)
  mov rsi, msg      ; 缓冲区地址
  mov rdx, len      ; 长度
  syscall            ; ← 触发 trap,切换到内核态
  ; ← 系统调用返回后执行这里

内核:
  1. 保存上下文
  2. 根据 rax=1 找到 sys_write
  3. 执行 sys_write
  4. 恢复上下文
  5. 返回到 syscall 的下一条指令

Abort(终止)— 不可恢复的错误

特点: - 发生了严重的硬件错误,无法恢复 - 直接终止引起异常的进程(或导致系统崩溃)

常见的 Abort: - 硬件故障(如 CPU 内部错误) - 双重故障(Double Fault):处理一个异常时又发生了另一个异常 - Machine Check Exception:处理器检测到内部错误

5.5 Fault vs Trap vs Abort 对比

类型 触发原因 是否可恢复 返回位置 示例
Fault 执行指令时出错 可能可恢复 引起异常的指令 缺页、除零、一般保护
Trap 程序故意触发 总是可恢复 下一条指令 系统调用、断点
Abort 严重硬件错误 不可恢复 不返回 Machine Check、Double Fault

5.6 中断处理的完整流程

Text Only
┌─────────────┐
│ 中断/异常发生 │
└──────┬──────┘
┌─────────────────┐
│ 1. CPU 完成当前  │
│    指令的执行    │
└──────┬──────────┘
┌─────────────────┐
│ 2. CPU 自动保存  │
│    PC、PSW 到栈  │
│    (硬件完成)    │
└──────┬──────────┘
┌─────────────────┐
│ 3. 根据中断号    │
│    查 IDT 表     │
│    获取处理程序   │
│    入口地址      │
└──────┬──────────┘
┌─────────────────┐
│ 4. 切换到内核态  │
│    (如果在用户态) │
└──────┬──────────┘
┌─────────────────┐
│ 5. 保存更多上下文│
│    (通用寄存器)  │
│    (OS 完成)     │
└──────┬──────────┘
┌─────────────────┐
│ 6. 执行中断      │
│    处理程序      │
└──────┬──────────┘
┌─────────────────┐
│ 7. 恢复上下文    │
└──────┬──────────┘
┌─────────────────┐
│ 8. 执行 iret     │
│    返回被中断的   │
│    程序继续执行   │
└─────────────────┘

六、操作系统分类

6.1 按任务数分类

类型 特点 示例
单任务 OS 一次只能运行一个程序 MS-DOS
多任务 OS 可以同时运行多个程序(并发) Windows、Linux、macOS

6.2 按用户数分类

类型 特点 示例
单用户 OS 一次只允许一个用户使用 MS-DOS、早期 Windows
多用户 OS 支持多个用户同时使用 UNIX、Linux

6.3 按响应方式分类

类型 特点 应用场景
批处理 OS 依次处理作业,无交互 大规模科学计算
分时 OS 时间片轮转,多用户交互 通用计算
实时 OS 严格的时间约束 航空航天、工业控制

6.4 按体系结构分类

类型 特点 代表
宏内核 所有功能在内核态 Linux、FreeBSD
微内核 最小化内核,服务在用户态 QNX、MINIX 3
混合内核 结合两者优点 Windows NT、macOS
外核(Exokernel) 最小抽象,让应用直接管理资源 MIT Exokernel(研究)

6.5 按应用领域分类

类型 特点 代表
桌面 OS 强调用户体验和兼容性 Windows、macOS
服务器 OS 强调稳定性和并发 Linux Server、Windows Server
嵌入式 OS 资源受限、实时性 FreeRTOS、RT-Thread
移动 OS 触控、省电、应用生态 Android、iOS
分布式 OS 多台计算机协同工作 Plan 9(研究)
云操作系统 管理数据中心资源 OpenStack、K8s(某种意义上)

七、代码示例:系统调用

7.1 Python 中的系统调用

Python
"""
系统调用示例 — Python 版
展示常见系统调用的 Python 封装
"""

import os
import sys

# ============ 1. 进程相关系统调用 ============

# getpid() - 获取当前进程 ID
print(f"当前进程 PID: {os.getpid()}")
print(f"父进程 PID: {os.getppid()}")

# fork() - 创建子进程(仅 Unix/Linux/macOS)
# 注意:Windows 不支持 fork
if hasattr(os, 'fork'):  # hasattr/getattr/setattr动态操作对象属性
    pid = os.fork()
    if pid == 0:
        # 子进程
        print(f"[子进程] PID={os.getpid()}, 父进程PID={os.getppid()}")
        os._exit(0)  # 子进程退出
    else:
        # 父进程
        print(f"[父进程] PID={os.getpid()}, 创建了子进程PID={pid}")
        os.wait()  # 等待子进程结束

# ============ 2. 文件相关系统调用 ============

# open() + write() + close() — 低级别文件操作
fd = os.open("test_syscall.txt", os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)
os.write(fd, b"Hello from syscall!\n")
os.close(fd)

# read() — 读取文件
fd = os.open("test_syscall.txt", os.O_RDONLY)
data = os.read(fd, 1024)
print(f"读取到: {data.decode()}")
os.close(fd)

# stat() — 获取文件信息
stat_info = os.stat("test_syscall.txt")
print(f"文件大小: {stat_info.st_size} 字节")
print(f"修改时间: {stat_info.st_mtime}")

# unlink() — 删除文件
os.unlink("test_syscall.txt")

# ============ 3. 目录相关系统调用 ============

# getcwd() — 获取当前工作目录
print(f"当前目录: {os.getcwd()}")

# mkdir() + rmdir() — 创建和删除目录
os.mkdir("test_dir")
print("创建了 test_dir 目录")
os.rmdir("test_dir")
print("删除了 test_dir 目录")

print("\n系统调用示例完成!")

7.2 C 语言中的系统调用

C
/**
 * 系统调用示例 — C 语言版
 * 编译:gcc -o syscall_demo syscall_demo.c
 * 运行:./syscall_demo
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <string.h>

int main() {
    // ========== 1. 进程相关 ==========
    printf("当前进程 PID: %d\n", getpid());
    printf("父进程 PID: %d\n", getppid());

    pid_t pid = fork();
    if (pid < 0) {
        perror("fork failed");
        exit(1);
    } else if (pid == 0) {
        // 子进程
        printf("[子进程] PID=%d, 父进程=%d\n", getpid(), getppid());

        // exec: 用新程序替换当前进程
        // execlp("ls", "ls", "-l", NULL);
        // 如果exec成功,下面的代码不会执行

        exit(0);
    } else {
        // 父进程
        printf("[父进程] PID=%d, 创建了子进程=%d\n", getpid(), pid);

        int status;
        wait(&status);  // 等待子进程结束

        if (WIFEXITED(status)) {
            printf("子进程正常退出,退出码: %d\n", WEXITSTATUS(status));
        }
    }

    // ========== 2. 文件相关 ==========

    // open() — 打开/创建文件
    int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd < 0) {
        perror("open failed");
        exit(1);
    }

    // write() — 写入文件
    const char *msg = "Hello, Syscall!\n";
    write(fd, msg, strlen(msg));

    // close() — 关闭文件
    close(fd);

    // open() + read() — 读取文件
    fd = open("test.txt", O_RDONLY);
    char buf[128];
    ssize_t n = read(fd, buf, sizeof(buf) - 1);
    buf[n] = '\0';
    printf("读取到: %s", buf);
    close(fd);

    // unlink() — 删除文件
    unlink("test.txt");

    return 0;
}

7.3 使用内联汇编直接执行系统调用(Linux x86-64)

C
/**
 * 直接使用汇编指令执行系统调用
 * 不通过 C 库(glibc)封装
 * 仅适用于 Linux x86-64
 */

#include <stdio.h>  // 引入头文件

// 直接通过 syscall 指令执行 write 系统调用
ssize_t my_write(int fd, const void *buf, size_t count) {  // 指针:存储变量的内存地址
    ssize_t ret;

    __asm__ volatile (
        "movq $1, %%rax\n"      // 系统调用号 1 = write
        "movq %1, %%rdi\n"      // 参数1: fd
        "movq %2, %%rsi\n"      // 参数2: buf
        "movq %3, %%rdx\n"      // 参数3: count
        "syscall\n"              // 触发系统调用
        "movq %%rax, %0\n"      // 返回值
        : "=r" (ret)
        : "r" ((long)fd), "r" (buf), "r" (count)
        : "rax", "rdi", "rsi", "rdx", "rcx", "r11", "memory"
    );

    return ret;
}

// 直接通过 syscall 指令执行 getpid 系统调用
pid_t my_getpid(void) {
    pid_t ret;

    __asm__ volatile (
        "movq $39, %%rax\n"     // 系统调用号 39 = getpid
        "syscall\n"
        "movl %%eax, %0\n"
        : "=r" (ret)
        :
        : "rax", "rcx", "r11"
    );

    return ret;
}

int main() {
    // 使用自定义的系统调用函数
    char msg[] = "Hello from raw syscall!\n";
    my_write(1, msg, sizeof(msg) - 1);

    // 对比两种方式获取 PID
    printf("通过 glibc: PID = %d\n", getpid());
    printf("通过 raw syscall: PID = %d\n", my_getpid());

    return 0;
}

7.4 使用 strace 追踪系统调用

Bash
# strace 是 Linux 下追踪系统调用的工具
# 查看 ls 命令执行了哪些系统调用

$ strace ls /tmp

# 输出示例:
# execve("/usr/bin/ls", ["ls", "/tmp"], ...) = 0
# brk(NULL)                          = 0x560a8b12b000
# openat(AT_FDCWD, "/etc/ld.so.cache", ...) = 3
# read(3, "\177ELF...", 832)         = 832
# mmap(NULL, 8192, ...)              = 0x7f6e9c7fe000
# ...
# openat(AT_FDCWD, "/tmp", ...)      = 3
# getdents64(3, ...)                 = 480
# write(1, "file1.txt  file2.txt\n", 21) = 21
# close(3)                           = 0
# exit_group(0)                      = ?

# 统计系统调用次数
$ strace -c ls /tmp

# % time     seconds  usecs/call     calls    errors syscall
# ------ ----------- ----------- --------- --------- --------
#  25.00    0.000012           6         2           read
#  18.75    0.000009           3         3           openat
#  14.58    0.000007           3         2         1 access
#  ...

八、练习题

选择题

1. 操作系统的核心功能不包括以下哪一项? - A. 进程管理 - B. 内存管理 - C. 编译程序 - D. 文件管理

答案:C。编译程序是应用软件(系统工具),不是操作系统内核的功能。


2. 以下哪种情况会导致 CPU 从用户态切换到内核态? - A. 执行一条加法指令 - B. 调用 printf() 函数(最终执行 write 系统调用) - C. 执行一条赋值语句 - D. 调用自定义的普通函数

答案:B。printf() 最终会调用 write() 系统调用,触发 trap,从用户态切换到内核态。


3. 缺页异常属于以下哪种类型? - A. 硬中断 - B. 软中断 - C. Fault(故障) - D. Abort(终止)

答案:C。缺页异常是一种 Fault,处理后会重新执行引起缺页的那条指令


4. 微内核与宏内核相比,最主要的优势是: - A. 运行速度更快 - B. 可靠性和模块化更好 - C. 支持更多的系统调用 - D. 可以运行更多的进程

答案:B。微内核的核心优势是可靠性(一个服务崩溃不会导致整个系统崩溃)和模块化(易于维护和扩展)。


5. 在多道批处理系统中引入多道程序设计的主要目的是: - A. 提高用户交互体验 - B. 提高 CPU 利用率 - C. 减小内存使用量 - D. 简化操作系统设计

答案:B。多道程序设计的核心目的是在一个程序等待 I/O 时让另一个程序使用 CPU,从而提高 CPU 利用率。


简答题

1. 简述操作系统从批处理到分时系统演变的动机和关键技术变化。

参考答案: 批处理系统虽然通过多道程序设计提高了 CPU 利用率,但没有交互性——用户提交作业后只能等待结果,无法在程序运行过程中与之交互。这对于程序开发和调试极为不便。

分时系统的核心动机是提供用户交互性。关键技术变化是引入了时间片(Time Slice)轮转调度:将 CPU 时间分成固定的小片段(如 20ms),让每个用户轮流使用 CPU。由于切换速度很快,每个用户感觉自己独占了一台计算机。

主要技术变化: - 调度策略:从"运行直到完成或I/O"变为"时间片轮转" - 响应目标:从追求吞吐量变为追求响应时间 - 用户交互:引入终端设备,支持在线交互 - 内存管理:需要更好的内存管理来支持更多同时在线的用户


2. 解释硬中断和软中断的区别,并说明 Linux 为什么要将中断处理分为上半部和下半部。

参考答案

硬中断由外部硬件设备触发(如定时器、网卡、磁盘),是异步事件;软中断是由内核代码触发的,用于延迟处理不紧急的中断工作。

Linux 将中断处理分为上半部和下半部的原因:

硬中断处理期间通常会屏蔽中断(至少屏蔽同级中断),如果处理时间过长,会导致: 1. 其他中断得不到及时响应 2. 系统响应能力下降 3. 可能丢失中断信号

因此,Linux 采用"上半部 + 下半部"的分离策略: - 上半部(Top Half):在硬中断上下文中执行,只做最紧急的工作(如从设备读取数据到缓冲区),然后标记软中断,快速返回 - 下半部(Bottom Half):通过 softirq、tasklet 或 workqueue 在较低优先级的上下文中执行,处理不紧急的工作(如网络协议栈处理)

这种设计在响应速度处理完整性之间取得了良好的平衡。


九、自检清单

学完本章后,请检查自己是否能回答以下问题:

基础概念

  • 能用自己的话定义"操作系统",并解释"资源管理者"和"抽象机器"两个角色
  • 能列出操作系统的五大功能
  • 能说出至少 3 种操作系统分类方式

发展历史

  • 能按时间顺序描述操作系统的发展历程(批处理→分时→实时→微内核→混合)
  • 能解释每一代为什么比上一代好(解决了什么问题)
  • 能比较宏内核、微内核、混合内核的优缺点

内核态与用户态

  • 能解释为什么需要内核态和用户态的区分
  • 能说出 CPU 从用户态切换到内核态的三种方式
  • 能描述一次系统调用的完整流程(从用户程序调用到返回)
  • 知道 x86 的 Ring 0 和 Ring 3

中断与异常

  • 能画出中断和异常的分类树
  • 能区分硬中断、软中断、Fault、Trap、Abort
  • 能解释 Linux 为什么要将中断处理分为上半部和下半部
  • 能描述中断处理的完整流程

十、面试要点

🔥 高频面试题

Q1:什么是操作系统?它有哪些功能?

回答要点: 1. 定义(一句话):操作系统是管理计算机硬件和软件资源的系统软件 2. 两个身份:资源管理者 + 抽象机器 3. 五大功能:进程管理、内存管理、文件管理、I/O管理、用户接口 4. 补充:运行在内核态,是计算机启动后常驻内存的第一个软件

Q2:用户态和内核态的区别是什么?如何切换?

回答要点: 1. 区别:权限不同——内核态可以执行所有指令(包括特权指令),访问所有内存;用户态只能执行非特权指令,只能访问用户空间 2. 切换方式:系统调用(主动)、异常(被动)、外部中断(被动) 3. 切换代价:需要保存/恢复上下文、切换内存映射,约 200-1000 CPU 周期 4. 设计目的:保护系统安全和稳定性

Q3:中断和异常有什么区别?

回答要点

中断(Interrupt) 异常(Exception)
来源 外部设备 CPU 内部
时机 异步(随时发生) 同步(执行某指令时)
举例 定时器中断、键盘中断 缺页、除零、系统调用

然后补充 Fault/Trap/Abort 的区别: - Fault 可恢复,返回同一条指令(如缺页) - Trap 是故意触发,返回下一条指令(如系统调用) - Abort 不可恢复,终止进程

Q4:系统调用的过程是怎样的?

回答要点(按步骤描述): 1. 用户程序将参数放入寄存器 2. 将系统调用号放入 eax/rax 寄存器 3. 执行 syscall/int 0x80 指令,触发 trap 4. CPU 切换到内核态,保存用户上下文 5. 根据系统调用号查表,跳转到对应的内核函数 6. 执行系统调用 7. 将返回值放入 rax 8. 恢复用户上下文,执行 sysret/iretq 返回用户态

Q5:宏内核和微内核的区别?Linux 是哪种?

回答要点: - 宏内核:所有 OS 功能在内核态,优点是性能高(函数调用),缺点是一个模块崩溃可能导致系统崩溃 - 微内核:只有最基本的功能在内核态,其他作为用户态服务,优点是可靠(模块隔离),缺点是性能开销(频繁 IPC) - Linux:技术上是宏内核,但通过可加载内核模块(LKM)实现了部分模块化。Linus Torvalds 和 Tanenbaum 在 1992 年有一场著名的论战(Tanenbaum–Torvalds debate),讨论宏内核 vs 微内核的优劣。


📝 面试回答模板

"概念 → 原理 → 对比 → 实例"

以 Q2 为例的完整回答:

用户态和内核态是 CPU 的两种运行模式,相当于两个不同的"权限等级"。

原理上,现代 CPU(如 x86)通过 Ring 机制实现特权级控制。Ring 0 是内核态,可以执行所有特权指令(如修改页表、关中断、直接 I/O);Ring 3 是用户态,只能执行非特权指令。如果用户态程序试图执行特权指令,CPU 会产生一个保护异常。

切换方式有三种:系统调用(程序主动请求 OS 服务)、异常(如缺页、除零)、外部中断(如定时器中断触发进程调度)。所有这些都会使 CPU 从用户态进入内核态。

设计目的是保护和隔离。如果所有程序都运行在内核态,一个有 bug 的程序可能直接修改其他进程的内存或破坏文件系统。有了特权级保护,即使用户程序崩溃,也只影响自身进程,不会导致整个系统崩溃。

举个例子,当应用程序需要读取文件时,它调用 read() 函数,glibc 将系统调用号放入 rax 寄存器,执行 syscall 指令触发 trap。CPU 保存当前上下文,切换到内核态,查找系统调用表执行对应的内核函数,完成后恢复上下文返回用户态。


📌 下一章02-进程与线程 — 深入理解进程的概念、生命周期,以及线程与协程的设计