跳转至

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-进程与线程 — 深入理解进程的概念、生命周期,以及线程与协程的设计