一篇围绕 Unix 展开的操作系统概述笔记。
每当介绍操作系统时,往往无非分成这样几个章节
- 概述 - 操作系统的作用/组成/发展历史等
- 进程 - 操作系统分配资源的最小单位
- 内存管理 - 内存如何虚拟化,操作系统如何分配内存资源
- 文件管理 - 文件系统与第一个I/O设备 —— 磁盘
- I/O设备 - 磁盘/网络/用户终端等
而此篇文章介绍的是 概述 部分,在此处对操作系统(以 Unix 为例)自顶向下地做一个简要梳理。
一、操作系统的作用
一般而言,这里说的操作系统主要指内核(kernel)这一部分。它长期驻留在内存中,负责最核心的系统管理工作。
从学习角度看,可以先把它的作用概括成 3 件事:
- 资源分配 - 决定操作系统上的各个用户作业如何分配有限的资源
- 提供抽象 - 封装硬件,用户可以通过编程接口便捷地操作硬件
- 权限管理 - 决定用户/进程可以访问哪些资源,并如何安全地访问资源
1. 资源分配
如果把操作系统看作系统视角下的“管理者”,那么它首先是一个资源分配器。
一台计算机的资源并不是无限的,例如:
- CPU 时间一次只能被少数执行流占用
- 内存空间总是有限的
- 磁盘、终端、网卡等 I/O 设备需要排队使用
- 文件、端口、锁等逻辑资源也需要协调访问
在 Unix 这类系统里,资源分配的基本承载单位通常是进程。一个进程可以简单理解成“一个正在运行的程序实例”,它不仅包含程序代码,也包含运行时状态,例如:
- 进程标识信息,例如
pid、父进程ppid - 当前执行位置,例如程序计数器和寄存器现场
- 自己的地址空间,例如代码区、数据区、堆、栈
- 打开的文件描述符表
- 与调度相关的信息,例如优先级、运行状态等
之所以需要操作系统统一管理这些进程,是因为很多资源请求会彼此冲突。
例如,多个进程都想使用 CPU 时,内核就需要调度;多个进程都想读写磁盘时,内核就需要安排 I/O;多个进程都需要内存时,内核就需要决定哪些页放在内存里、哪些暂时换出。
从 CPU 的角度看,所谓调度,本质上就是操作系统决定“下一个让谁运行”。
这里中断很关键。因为如果没有中断,CPU 可能会一直执行某个进程而不回来;有了时钟中断之后,内核就能周期性地夺回控制权,保存当前进程现场,再切换到别的进程。于是多个进程就能共享同一个 CPU,看起来像在“同时运行”。
从内存的角度看,进程通常都以为自己独占一片连续地址空间,但实际上背后是内核在做映射、保护和分配。后面学习虚拟内存时会发现,程序看到的地址和物理内存地址并不是一回事。
2. 提供抽象
如果说资源分配回答的是“怎么公平、有效地共用硬件”,那么抽象回答的就是“怎么让程序员不用直接面对硬件细节”。
可以先把这一层理解成:
| 底层真实对象 | 操作系统提供的抽象 | 程序员常见接口 |
|---|---|---|
| CPU | 进程 / 执行流 | fork exec wait |
| 物理内存 | 虚拟地址空间 | mmap brk |
| 磁盘与目录项 | 文件与目录 | open read write close |
| 终端、管道、socket 等设备 | 文件描述符 | read write socket |
对 Unix 程序员来说,最常接触的就是系统调用(system call)这一层,例如:
open/read/write/close处理文件与 I/Ofork/exec/wait处理进程mmap/brk处理部分内存相关功能socket/bind/accept处理网络通信
这些接口的意义很直接:应用程序不需要关心寄存器、控制器和具体设备时序,只需要按照统一约定调用接口即可。
Unix 的一个重要风格就是“尽量把很多对象都统一到文件接口附近”。普通文件、管道、终端、套接字虽然底层不同,但从程序使用上看,都可以通过文件描述符和读写接口来操作。
3. 权限管理
操作系统不仅要“让程序能做事”,还要“限制程序不能乱做事”。
先把权限问题拆成两层:
| 层次 | 解决的问题 | 典型机制 |
|---|---|---|
| 用户态 / 内核态 | 用户程序不能直接执行特权操作 | 硬件模式位、trap、中断 |
| 用户 / 用户之间 | 不同用户能访问哪些资源 | 用户身份、文件权限位、访问控制 |
本文先讨论第一层,也就是用户态与内核态之间的边界。
最简单的权限模型只包含两种权限模式:
| 模式 | 能做什么 |
|---|---|
| 用户态 | 运行普通应用程序,但不能直接执行特权指令 |
| 内核态 | 访问关键硬件资源,执行特权操作 |
应用程序平时运行在用户态。如果它想执行特权指令,如读文件、创建进程、申请内存,就必须通过系统调用陷入“陷阱”,将控制权交给内核,内核执行结束后,再从陷阱中恢复。
这个过程可以压缩成下面这张表:
| 阶段 | 发生的事 |
|---|---|
| 1. 用户态准备参数 | C 函数封装系统调用号和参数 |
| 2. 执行特殊指令 | 底层汇编入口执行系统调用指令 |
| 3. 跳转到内核入口 | CPU 根据向量表或入口表项转入内核 |
| 4. 保存现场 | 硬件先保存最基本信息,内核继续保存进程现场 |
| 5. 切换权限级 | CPU 从用户态进入内核态 |
| 6. 执行内核服务 | 内核根据系统调用号完成对应操作 |
| 7. 恢复并返回 | 恢复寄存器和状态,回到用户态继续执行 |
中断和陷阱(trap)可以从“相同点 / 不同点”两个角度来看。
相同点:
- 都会打断当前执行流,把控制权交给内核
- 都会让 CPU 跳转到预先设置好的入口
- 都需要保存现场,处理结束后再恢复
- 都可能导致 CPU 从用户态切换到内核态
不同点:
| 对比项 | 中断 interrupt | 陷阱 trap |
|---|---|---|
| 触发来源 | 外部硬件事件,例如时钟、磁盘、键盘 | 当前程序主动执行特定指令,或执行过程中触发异常 |
| 与当前指令的关系 | 不一定由当前指令直接引起 | 通常和当前正在执行的指令直接相关 |
| 时机 | 往往是异步发生的 | 通常是同步发生的 |
| 常见用途 | I/O 完成通知、时钟打点、设备请求处理 | 系统调用、断点、缺页、非法指令等异常处理 |
| 是否由程序主动发起 | 否 | 通常是,至少入口与当前程序执行直接相关 |
如果只记一句话,那么可以先这样区分:
- 中断更像是“外设通知 CPU:现在有事情需要处理”
- trap 更像是“当前程序执行到这里,必须立刻进入内核处理”
正是因为有了这条边界,硬件相关操作才能由内核统一而安全地执行。
至于 Unix 文件系统中的读、写、执行权限位,以及进程所属用户身份,这些属于第二层,也就是“用户与用户之间”的权限控制。
二、操作系统的组成
前面说的是“操作系统做什么”,这里再看“它大致由什么构成”。
如果只从最核心的内核角度看,可以先按“它在管理哪类硬件资源”来理解操作系统的组成:
- 进程与调度管理:把 CPU 组织成多个进程共享的执行环境
- 内存管理:把物理内存组织成进程可使用的地址空间
- 文件系统:把磁盘上的数据组织成文件与目录
- I/O 子系统与设备驱动:负责管理磁盘之外的其他外设,并为具体设备提供统一访问方式
这里的 4 类内容都直接对应某类硬件资源或硬件对象。至于系统调用、中断、异常、权限检查这些内容,更适合作为贯穿这些模块的公共机制来理解,而不是和前面 4 类资源管理模块并列放在同一层。
这些模块之间并不是彼此孤立的。
例如,一个 read 调用就会把这些模块串起来:
| 步骤 | 对应模块 |
|---|---|
| 用户进程通过系统调用进入内核 | 公共机制:系统调用 |
| 查询文件描述符表,找到打开文件对象 | 进程与调度管理 |
| 检查页缓存是否已有目标数据 | 内存管理 |
| 缓存未命中时定位文件对应数据块 | 文件系统 |
| 发起实际磁盘 I/O | I/O 子系统与设备驱动 |
| I/O 完成后通过中断通知内核 | 公共机制:中断 |
| 把数据拷回用户缓冲区并返回 | 进程上下文恢复 |
也正因为操作系统内部关系紧密,所以内核结构设计一直是很重要的话题。
1. 宏内核与微内核
先看“内核如何扩展”:
| 方式 | 特点 |
|---|---|
| 修改源码后重新编译内核 | 改动直接进入内核本体 |
| 动态加载模块或驱动 | 运行时按需加入功能 |
关键不只是“能不能扩展”,而是“扩展代码运行在哪个权限级别”。如果更多功能直接跑在内核态,就更接近宏内核;如果只把最基础机制留在内核态,而把更多服务放到用户态,就更接近微内核。
1.1 宏内核
宏内核(monolithic kernel)会把大部分核心功能都放在内核态中运行,例如调度、内存管理、文件系统、网络栈、设备驱动等。
| 方面 | 宏内核 |
|---|---|
| 运行位置 | 大部分系统服务直接运行在内核态 |
| 优点 | 模块通信直接、性能通常较好、实现路径直接 |
| 代价 | 内核体积较大,单个模块出错可能影响整个系统 |
| 代表印象 | Unix、Linux 更接近这一思路 |
1.2 微内核
微内核(microkernel)倾向于把尽量少的功能留在内核态,只保留最基本的机制,例如地址空间管理、线程调度、进程间通信等;而把文件系统、驱动、网络服务等尽量放到用户态服务里。
| 方面 | 微内核 |
|---|---|
| 运行位置 | 只保留最基础机制在内核态,更多服务放到用户态 |
| 优点 | 模块隔离更好,单个服务更容易替换,局部故障不一定拖垮整个系统 |
| 代价 | 服务之间需要更多消息传递,通信开销更明显 |
| 设计重点 | 把机制留在内核,把策略和服务往外移 |
实际系统里,很多内核并不是“纯粹”的宏内核或微内核,而是折中设计。
2. Unix 视角下的系统组成
如果从 Unix 使用者角度去观察整台机器,通常还能把系统分成几层:
| 层次 | 内容 |
|---|---|
| 应用程序 | 编辑器、浏览器、数据库、业务程序 |
| 系统程序 | init、Shell、守护进程、编译器、基础命令 |
| 内核 | 调度、内存管理、文件系统、驱动等核心机制 |
| 硬件 | CPU、内存、磁盘、终端、网络设备 |
其中内核不直接等于“整套操作系统发行版”。我们平时安装和使用的一整套系统,除了内核,还有大量系统程序与用户空间工具。只是从操作系统原理课程的语境里,讨论“操作系统”时往往主要指内核。
系统启动流程也可以压缩成:
- 固件(ROM或EPROM)中的引导程序启动
- 引导程序初始化最基本硬件,并把内核加载到内存
- 内核启动后初始化核心子系统
- 再拉起最早的一批用户空间系统进程,例如
init
三、发展历史
操作系统的发展历史可以参考跟多文章,这里只做一个摘要。
可以粗略记成下面这张表:
| 阶段 | 重点 |
|---|---|
| 早期计算机 | 机器昂贵,重点是尽量别让硬件闲着 |
| 多道程序 / 分时系统 | 多个作业或多个用户共享一台机器 |
| Unix 出现后 | 形成了进程、文件描述符、管道、可组合工具这些经典风格 |
| 现代操作系统 | 面向桌面、服务器、移动端、嵌入式、云环境等多种场景 |
Unix 的影响主要体现在这些风格上:
- 用进程作为核心执行单位
- 用文件描述符统一很多 I/O 接口
- 用“小程序 + 管道”组合复杂任务
- 倾向于提供简洁、稳定、可组合的接口
现代系统的侧重点则常常不同:
| 场景 | 更关注什么 |
|---|---|
| 个人计算机 | 交互体验 |
| 服务器 | 吞吐量、稳定性、隔离 |
| 移动设备 | 能耗、响应速度、安全模型 |
| 嵌入式系统 | 专用性、可靠性、成本 |
笔者使用的操作系统是MacOS,它的内核是darwin,而后者是由BSD发展而来的。因此与Unix联系十分紧密,这也为学习操作系统带来了不少的便捷。
虽然操作系统形态变化很多,但核心问题并没有变:如何管理有限硬件资源、如何向上提供好用抽象、以及如何在共享环境中保证安全和秩序。
四、小结
如果把这篇文章压缩成几句话,那么可以记住下面几点:
- 操作系统位于应用程序和硬件之间,是资源管理者,也是统一接口提供者
- 它的核心工作可以归纳为对 CPU、内存、存储和其他外设的组织与管理
- 从编写应用程序的程序员视角看,应用程序主要通过系统调用进入内核请求服务
- 用户态与内核态的边界,和用户之间的权限控制,是两层不同的权限问题
- 中断和 trap 都会让控制流进入内核,只是触发来源不同
- Unix 的重要特点之一,是把许多对象统一为“文件描述符 + 读写接口”
后面继续学习进程、内存、文件系统和 I/O 时,会发现这些内容其实都只是这一层“概述”的展开。