01

对话

操作系统的诞生

阅读量:78 · 预计 18 分钟读完

OS历史系统调用
关联层级:L5 虚拟机代码
阅读进度5%

第一章 对话:走进操作系统的世界

导读

当我们打开电脑、启动手机、或者运行一个程序时,我们很少会思考背后究竟发生了什么。键盘上的每一次敲击、鼠标的每一次点击、屏幕上的每一次刷新,都依赖于一个默默运行的软件层——操作系统(Operating System, OS)。操作系统是计算机世界中最重要、最基础的系统软件,它既是硬件资源的管理者,也是应用程序与硬件之间的桥梁。

本章将带领读者走进操作系统的世界,通过对话的形式,逐步揭示操作系统的本质、职责和设计哲学。我们将从最基本的问题出发:"什么是操作系统?",逐步深入到操作系统的核心概念、发展历程、三大主题(虚拟化、并发、持久性),以及系统调用、用户态与内核态等关键机制。无论你是计算机科学专业的学生,还是对底层技术感兴趣的开发者,本章都将为你建立一个清晰而扎实的操作系统认知框架。


1.1 什么是操作系统

1.1.1 操作系统的定义

操作系统是一组控制和管理计算机硬件与软件资源、合理组织和调度计算机工作流程的系统软件的集合。简单来说,操作系统是计算机的"大管家"——它负责分配和管理CPU、内存、磁盘、网络等硬件资源,同时为上层应用程序提供统一的接口和服务。

从用户的角度看,操作系统提供了一个友好的交互界面(命令行或图形界面),让用户能够方便地使用计算机。从程序员的角度看,操作系统提供了一组系统调用(System Call),让程序能够请求操作系统代为执行某些操作,比如读写文件、分配内存、创建进程等。

一个经典的类比是:如果把计算机比作一家餐厅,那么硬件就是厨房的灶台、锅碗瓢盆和食材,应用程序就是各种菜品的菜谱,而操作系统就是餐厅的经理——它负责安排厨师(CPU)的工作顺序、管理食材和调料的存取(内存和磁盘)、协调多个订单的处理(并发),并确保每道菜都能正确、高效地完成。

1.1.2 操作系统的核心角色

操作系统在计算机系统中扮演着两个核心角色:

第一,操作系统是资源管理者(Resource Manager)。 计算机的硬件资源是有限的——CPU的处理能力有限、内存的容量有限、磁盘的空间有限、网络的带宽也有限。当多个程序同时运行时,操作系统需要公平、高效地分配这些资源。例如,当你在电脑上同时运行浏览器、音乐播放器和文字处理器时,操作系统负责决定每个程序何时使用CPU、分配多少内存、如何访问磁盘。

第二,操作系统是抽象提供者(Abstraction Provider)。 硬件是复杂而底层的——直接与硬件打交道需要了解电路、时序、寄存器级别的知识。操作系统通过抽象(Abstraction)将复杂的硬件转化为简洁、易用的接口。例如,程序员不需要知道磁盘的物理结构(磁道、扇区、磁头),只需要调用 open()read()write() 等系统调用就能完成文件的读写。这种抽象极大地降低了软件开发的复杂度。

1.1.3 操作系统的组成

一个典型的操作系统包含以下核心组件:

  • 内核(Kernel):操作系统的核心部分,常驻内存,负责最基础的资源管理和系统调用处理。内核运行在最高权限级别(内核态),可以直接访问所有硬件资源。
  • 进程管理模块:负责创建、调度、终止进程,处理进程间的通信与同步。
  • 内存管理模块:负责内存的分配与回收、虚拟内存的实现、内存保护等。
  • 文件系统模块:负责文件的组织、存储、检索和保护。
  • 设备驱动模块:负责与各种硬件设备通信,提供统一的设备访问接口。
  • 用户接口:包括命令行界面(CLI)和图形用户界面(GUI),提供用户与系统交互的入口。

1.2 操作系统的发展历史

1.2.1 早期计算机(1940s-1950s)

在计算机发展的最初阶段,并没有操作系统的概念。程序员直接通过拨插线路、设置开关来操作计算机,后来发展为使用打孔卡片和机器语言编程。这一时期的"操作系统"最多只是一个简单的加载程序(Bootstrap),负责将用户程序从纸带读入内存并执行。

1.2.2 批处理系统(1950s-1960s)

随着计算机速度的提升,人们发现CPU大量时间浪费在人工操作上(装卸纸带、等待结果)。于是出现了批处理系统(Batch System)——将多个作业(Job)按顺序组织成一批,由监控程序(Monitor)自动依次加载和执行。这是操作系统的雏形。IBM 7094 上的 IBSYS 是早期批处理系统的代表。

批处理系统引入了许多至今仍在使用的概念,如作业调度、内存保护(通过硬件的基址/界限寄存器实现)、中断机制等。

1.2.3 多道程序与时分系统(1960s-1970s)

批处理系统的最大问题是CPU利用率低——当一个作业等待I/O操作时,CPU只能空闲。多道程序设计(Multiprogramming)解决了这个问题:内存中同时驻留多个程序,当一个程序等待I/O时,CPU切换到另一个程序执行。这极大地提高了CPU利用率。

在此基础上,时分系统(Time-Sharing System)应运而生。CPU的时间被切分为很短的时间片(Time Slice),轮流分配给不同的用户程序。由于切换速度极快(通常几十毫秒),每个用户都感觉自己独占了整台计算机。IBM System/360 上的 CTSS 和 Multics 是这一时期的代表系统。

1.2.4 UNIX 的诞生(1970s)

1969年,Ken Thompson 和 Dennis Ritchie 在贝尔实验室开发了 UNIX 操作系统。UNIX 的设计哲学——"做一件事,做好它"、"提供机制而非策略"——深刻影响了后续所有操作系统的设计。UNIX 用C语言重写(而非汇编语言),开创了可移植操作系统的先河。

UNIX 引入了许多影响深远的概念:层次化的文件系统、管道(Pipe)、Shell 作为用户接口、"一切皆文件"的设计思想等。可以说,今天所有主流操作系统(Linux、macOS、Android、iOS)都直接或间接地继承了 UNIX 的血脉。

1.2.5 个人计算机时代(1980s-1990s)

随着微处理器的普及,个人计算机(PC)走入千家万户。MS-DOS、Windows、Mac OS 等面向个人用户的操作系统相继出现。这一时期的操作系统更加注重用户界面的友好性,图形用户界面(GUI)成为标配。

1.2.6 现代操作系统(2000s至今)

进入21世纪,操作系统的发展呈现出几个显著趋势:多核处理器的普及推动了并行和并发编程的发展;移动操作系统(iOS、Android)的崛起改变了人机交互方式;云计算和虚拟化技术的成熟催生了容器化(Docker)和微服务架构;安全性的重视程度不断提高。


1.3 操作系统的三大主题

《操作系统导论》(OSTEP)将操作系统的内容组织为三大主题:虚拟化(Virtualization)、并发(Concurrency)和持久性(Persistence)。这三大主题涵盖了操作系统最核心的功能和设计挑战。

1.3.1 虚拟化(Virtualization)

虚拟化是操作系统最基本的功能。操作系统通过虚拟化,将物理硬件资源转化为更易用、更强大的虚拟资源。

CPU虚拟化:操作系统将物理CPU虚拟化为无限多个虚拟CPU,每个程序都感觉自己独占了一个处理器。这种虚拟化的实现机制就是进程(Process)和调度(Scheduling)。虽然物理上只有一个或几个CPU核心,但操作系统通过快速切换(Context Switch),让每个进程都认为自己拥有独立的CPU。

内存虚拟化:操作系统将物理内存虚拟为一个巨大的、连续的地址空间,每个程序都认为自己拥有从0开始的完整内存空间。这种虚拟化的实现机制包括分段(Segmentation)、分页(Paging)和TLB等。

虚拟化的核心思想是:通过时间或空间的复用,将有限的物理资源转化为看似无限的虚拟资源,同时保证各虚拟资源之间的隔离性。

1.3.2 并发(Concurrency)

在操作系统中,并发问题无处不在。多个进程同时访问内存、多个线程同时操作共享数据、多个I/O设备同时发出中断请求——这些场景都需要操作系统正确处理并发问题。

并发的核心挑战在于:当多个执行流同时访问共享资源时,如果不加以控制,就会出现竞态条件(Race Condition),导致程序行为不确定甚至错误。操作系统提供了多种同步机制来解决并发问题,包括锁(Lock)、信号量(Semaphore)、条件变量(Condition Variable)等。

并发不仅是一个理论问题,更是一个实践问题。现代多核处理器的普及使得并发编程变得越来越重要。理解并发原理,对于编写正确的多线程程序至关重要。

1.3.3 持久性(Persistence)

内存是易失性的——断电后数据就会丢失。而许多数据需要长期保存,这就是持久性要解决的问题。操作系统通过文件系统(File System)来管理持久化存储在磁盘上的数据。

文件系统需要解决的核心问题包括:如何组织文件(目录结构)、如何存储文件(数据块分配)、如何高效检索文件(索引机制)、如何保证数据的一致性(日志和检查点)、如何处理故障(崩溃恢复)等。

此外,磁盘I/O的性能优化也是持久性的重要课题。磁盘调度算法、缓存策略、RAID技术等都是为了在有限的硬件性能下提供尽可能高的I/O吞吐量。


1.4 系统调用:用户与内核的桥梁

1.4.1 什么是系统调用

系统调用(System Call)是操作系统提供给用户程序的服务接口。当用户程序需要执行某些特权操作(如读写文件、分配内存、创建进程等)时,它不能直接操作硬件,而必须通过系统调用请求内核代为执行。

系统调用的本质是一种受控的、安全的从用户态到内核态的转换机制。它允许用户程序在不完全放弃控制权的前提下,获得内核提供的服务。

1.4.2 系统调用的执行过程

一个典型的系统调用执行过程如下:

用户程序准备参数:将系统调用号和参数放入指定的寄存器或内存位置。

触发陷阱指令:程序执行一条特殊的陷阱指令(Trap Instruction),如 x86 的 int 0x80syscall

硬件切换到内核态:CPU 自动切换到内核态(提升权限级别),并跳转到预先设定的中断处理程序入口。

内核处理系统调用:内核根据系统调用号查找系统调用表(System Call Table),调用对应的处理函数。

返回用户态:处理完成后,内核将结果放入寄存器,执行返回指令,CPU 切换回用户态,程序继续执行。

1.4.3 常见的系统调用

POSIX 标准定义了一系列系统调用,以下是几类常见的系统调用:

文件操作

  • open() - 打开文件
  • close() - 关闭文件
  • read() - 读取文件
  • write() - 写入文件
  • lseek() - 移动文件指针
  • stat() - 获取文件信息

进程控制

  • fork() - 创建子进程
  • exec() - 执行新程序
  • exit() - 终止进程
  • wait() - 等待子进程结束

内存管理

  • mmap() - 映射内存
  • brk() - 改变数据段大小

1.4.4 标准库与系统调用的关系

在实际编程中,程序员很少直接调用系统调用,而是通过标准库(如 C 语言的 glibc)提供的函数来间接使用。标准库函数在系统调用的基础上提供了更高级的接口和额外的功能。例如,printf() 函数内部会调用 write() 系统调用,但 printf() 还提供了格式化输出的功能。

标准库与系统调用的关系可以类比为:系统调用是操作系统提供的"原材料",而标准库函数是经过加工的"成品"。


1.5 用户态与内核态

1.5.1 为什么需要两种模式

现代CPU通常提供至少两种运行模式:用户态(User Mode)和内核态(Kernel Mode,也称特权模式、管理模式)。这种设计的根本目的是保护——防止用户程序直接访问硬件资源或执行特权指令,从而保证系统的稳定性和安全性。

如果没有这种区分,一个有bug的用户程序可能会覆盖操作系统的代码、访问其他进程的内存、或者直接操作磁盘导致数据损坏。通过限制用户程序的权限,操作系统可以确保即使某个程序出错或恶意运行,也不会影响整个系统。

1.5.2 用户态与内核态的区别

特性用户态内核态
权限级别
可执行指令仅非特权指令所有指令(包括特权指令)
内存访问受限(只能访问自己的地址空间)不受限(可以访问所有物理内存)
硬件访问不能直接访问可以直接访问
执行代码用户程序、标准库操作系统内核

1.5.3 模式切换的触发条件

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

系统调用(System Call):用户程序主动请求内核服务。

异常(Exception):程序执行过程中出现的特殊情况,如缺页异常(Page Fault)、除零错误等。异常由当前执行的程序触发,内核需要介入处理。

中断(Interrupt):外部硬件设备发出的信号,如键盘输入、定时器到期、磁盘I/O完成等。中断是异步的,与当前执行的程序无关。

无论哪种方式,CPU都会自动完成权限提升和上下文保存,确保能够正确返回用户态继续执行。


1.6 进程的基本概念

1.6.1 什么是进程

进程(Process)是操作系统进行资源分配和调度的基本单位。一个正在运行的程序就是一个进程——它不仅包含程序的代码,还包含当前的执行状态(程序计数器的值、寄存器的内容)、分配的内存、打开的文件等。

同一个程序可以同时运行多个实例(即多个进程)。例如,你可以同时打开多个浏览器窗口,每个窗口都是一个独立的进程。

1.6.2 进程地址空间

每个进程都拥有自己独立的地址空间(Address Space),这是操作系统提供的重要抽象。进程的地址空间通常包含以下部分:

  • 代码段(Text Segment):存放程序的可执行指令。
  • 数据段(Data Segment):存放全局变量和静态变量。
  • 堆(Heap):用于动态内存分配(如 malloc()),从低地址向高地址增长。
  • 栈(Stack):用于函数调用,保存局部变量和返回地址,从高地址向低地址增长。

1.6.3 进程状态

进程在其生命周期中会经历不同的状态:

  • 运行(Running):进程正在CPU上执行。
  • 就绪(Ready):进程已准备好运行,等待CPU分配。
  • 阻塞(Blocked):进程因等待某个事件(如I/O完成、信号量)而暂停执行。
  • 创建(New):进程正在被创建。
  • 终止(Terminated):进程执行完毕或被强制终止。

1.7 常见误区

误区一:操作系统只是一个"大程序"

很多人认为操作系统就是一个普通的大型程序。实际上,操作系统与普通程序有本质区别:操作系统运行在最高权限级别,管理所有其他程序,并且必须始终可用。操作系统的任何错误都可能导致整个系统崩溃,因此其设计和实现需要格外谨慎。

误区二:系统调用就是普通的函数调用

系统调用和普通函数调用在形式上相似,但本质完全不同。系统调用涉及用户态到内核态的切换,代价远高于普通函数调用。一次系统调用的开销可能是一次普通函数调用的数十倍甚至上百倍。因此,在性能敏感的代码中,应尽量减少系统调用的次数。

误区三:多核处理器不需要操作系统

有些开发者认为,多核处理器提供了足够的计算能力,可以绕过操作系统直接编程。这种做法不仅困难(需要直接管理硬件),而且危险(缺乏保护机制)。操作系统在多核环境下的重要性不减反增——它需要管理更多的CPU核心、处理更复杂的并发问题、提供更精细的资源分配策略。

误区四:虚拟化的性能开销很大

早期的虚拟化技术确实存在较大的性能开销,但现代虚拟化技术(如硬件辅助虚拟化、半虚拟化)已经将开销降低到几乎可以忽略的程度。在大多数场景下,虚拟机的性能与裸机性能相差不到5%。

误区五:操作系统的设计已经成熟,没有创新空间

操作系统是一个持续演进的领域。从单内核到微内核、从单处理器到多处理器、从物理机到虚拟机和容器、从本地计算到云计算,操作系统的架构和设计一直在不断创新。近年来,Rust 语言在操作系统开发中的应用、形式化验证在操作系统内核中的实践,都是活跃的研究方向。


1.8 实践应用

1.8.1 观察系统调用

在 Linux 系统中,可以使用 strace 工具观察程序执行的系统调用:

bash
strace ls

这条命令会显示 ls 命令执行过程中所有的系统调用,包括 open()read()write()close() 等。通过观察系统调用,可以深入理解程序与操作系统的交互方式。

1.8.2 查看进程信息

使用 ps 命令可以查看系统中的进程信息:

bash
ps aux

输出包括进程ID(PID)、CPU使用率、内存使用量、进程状态、启动时间等信息。这有助于理解操作系统如何管理和监控进程。

1.8.3 理解用户态与内核态

在 Linux 中,可以通过 /proc 文件系统查看进程在内核态和用户态分别花费的时间:

bash
cat /proc/[pid]/stat

其中第14个字段(utime)是用户态时间,第15个字段(stime)是内核态时间。通过比较这两个值,可以评估程序的系统调用开销。

1.8.4 编写简单的系统调用程序

以下是一个使用系统调用的简单 C 程序:

c
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main() {
    int fd = open("test.txt", O_WRONLY | O_CREAT, 0644);
    write(fd, "Hello, OS!\n", 11);
    close(fd);
    return 0;
}

这个程序使用了 open()write()close() 三个系统调用,分别用于打开文件、写入数据和关闭文件。


1.9 本章小结

本章通过对话的形式,介绍了操作系统的基本概念和核心原理。主要内容包括:

操作系统的定义与角色:操作系统是资源管理者和抽象提供者,它管理硬件资源并为上层应用提供统一接口。

操作系统的发展历史:从早期的批处理系统到现代的通用操作系统,操作系统经历了数十年的演进。UNIX 的设计哲学至今仍在深刻影响着操作系统的设计。

操作系统的三大主题:虚拟化(将物理资源转化为易用的虚拟资源)、并发(处理多个执行流同时访问共享资源的问题)、持久性(管理持久化存储的数据)。

系统调用:系统调用是用户程序请求内核服务的机制,涉及用户态到内核态的切换,是理解操作系统工作原理的关键。

用户态与内核态:CPU的两种运行模式,通过权限隔离保护系统的稳定性和安全性。

进程的基本概念:进程是操作系统进行资源分配和调度的基本单位,拥有独立的地址空间和生命周期。

理解这些基础概念,是深入学习操作系统后续章节的前提。在接下来的章节中,我们将逐一深入探讨操作系统的三大主题,揭示操作系统设计的精妙之处。