一篇围绕 Unix 展开的操作系统概述笔记。


每当介绍操作系统时,往往无非分成这样几个章节

  1. 概述 - 操作系统的作用/组成/发展历史等
  2. 进程 - 操作系统分配资源的最小单位
  3. 内存管理 - 内存如何虚拟化,操作系统如何分配内存资源
  4. 文件管理 - 文件系统与第一个I/O设备 —— 磁盘
  5. I/O设备 - 磁盘/网络/用户终端等

而此篇文章介绍的是 概述 部分,在此处对操作系统(以 Unix 为例)自顶向下地做一个简要梳理。

一、操作系统的作用

一般而言,这里说的操作系统主要指内核(kernel)这一部分。它长期驻留在内存中,负责最核心的系统管理工作。

从学习角度看,可以先把它的作用概括成 3 件事:

  1. 资源分配 - 决定操作系统上的各个用户作业如何分配有限的资源
  2. 提供抽象 - 封装硬件,用户可以通过编程接口便捷地操作硬件
  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/O
  • fork / 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/OI/O 子系统与设备驱动
I/O 完成后通过中断通知内核公共机制:中断
把数据拷回用户缓冲区并返回进程上下文恢复

也正因为操作系统内部关系紧密,所以内核结构设计一直是很重要的话题。

1. 宏内核与微内核

先看“内核如何扩展”:

方式特点
修改源码后重新编译内核改动直接进入内核本体
动态加载模块或驱动运行时按需加入功能

关键不只是“能不能扩展”,而是“扩展代码运行在哪个权限级别”。如果更多功能直接跑在内核态,就更接近宏内核;如果只把最基础机制留在内核态,而把更多服务放到用户态,就更接近微内核。

1.1 宏内核

宏内核(monolithic kernel)会把大部分核心功能都放在内核态中运行,例如调度、内存管理、文件系统、网络栈、设备驱动等。

方面宏内核
运行位置大部分系统服务直接运行在内核态
优点模块通信直接、性能通常较好、实现路径直接
代价内核体积较大,单个模块出错可能影响整个系统
代表印象Unix、Linux 更接近这一思路

1.2 微内核

微内核(microkernel)倾向于把尽量少的功能留在内核态,只保留最基本的机制,例如地址空间管理、线程调度、进程间通信等;而把文件系统、驱动、网络服务等尽量放到用户态服务里。

方面微内核
运行位置只保留最基础机制在内核态,更多服务放到用户态
优点模块隔离更好,单个服务更容易替换,局部故障不一定拖垮整个系统
代价服务之间需要更多消息传递,通信开销更明显
设计重点把机制留在内核,把策略和服务往外移

实际系统里,很多内核并不是“纯粹”的宏内核或微内核,而是折中设计。

2. Unix 视角下的系统组成

如果从 Unix 使用者角度去观察整台机器,通常还能把系统分成几层:

层次内容
应用程序编辑器、浏览器、数据库、业务程序
系统程序init、Shell、守护进程、编译器、基础命令
内核调度、内存管理、文件系统、驱动等核心机制
硬件CPU、内存、磁盘、终端、网络设备

其中内核不直接等于“整套操作系统发行版”。我们平时安装和使用的一整套系统,除了内核,还有大量系统程序与用户空间工具。只是从操作系统原理课程的语境里,讨论“操作系统”时往往主要指内核。

系统启动流程也可以压缩成:

  1. 固件(ROM或EPROM)中的引导程序启动
  2. 引导程序初始化最基本硬件,并把内核加载到内存
  3. 内核启动后初始化核心子系统
  4. 再拉起最早的一批用户空间系统进程,例如 init

三、发展历史

操作系统的发展历史可以参考跟多文章,这里只做一个摘要。

可以粗略记成下面这张表:

阶段重点
早期计算机机器昂贵,重点是尽量别让硬件闲着
多道程序 / 分时系统多个作业或多个用户共享一台机器
Unix 出现后形成了进程、文件描述符、管道、可组合工具这些经典风格
现代操作系统面向桌面、服务器、移动端、嵌入式、云环境等多种场景

Unix 的影响主要体现在这些风格上:

  • 用进程作为核心执行单位
  • 用文件描述符统一很多 I/O 接口
  • 用“小程序 + 管道”组合复杂任务
  • 倾向于提供简洁、稳定、可组合的接口

现代系统的侧重点则常常不同:

场景更关注什么
个人计算机交互体验
服务器吞吐量、稳定性、隔离
移动设备能耗、响应速度、安全模型
嵌入式系统专用性、可靠性、成本

笔者使用的操作系统是MacOS,它的内核是darwin,而后者是由BSD发展而来的。因此与Unix联系十分紧密,这也为学习操作系统带来了不少的便捷。

虽然操作系统形态变化很多,但核心问题并没有变:如何管理有限硬件资源、如何向上提供好用抽象、以及如何在共享环境中保证安全和秩序。

四、小结

如果把这篇文章压缩成几句话,那么可以记住下面几点:

  • 操作系统位于应用程序和硬件之间,是资源管理者,也是统一接口提供者
  • 它的核心工作可以归纳为对 CPU、内存、存储和其他外设的组织与管理
  • 从编写应用程序的程序员视角看,应用程序主要通过系统调用进入内核请求服务
  • 用户态与内核态的边界,和用户之间的权限控制,是两层不同的权限问题
  • 中断和 trap 都会让控制流进入内核,只是触发来源不同
  • Unix 的重要特点之一,是把许多对象统一为“文件描述符 + 读写接口”

后面继续学习进程、内存、文件系统和 I/O 时,会发现这些内容其实都只是这一层“概述”的展开。