揭秘操作系统最核心技术:进程与线程是如何一步步发明出来的?

今天我想跟你聊聊一个计算机发展史上的精彩故事——进程与线程是如何一步步被发明出来的?

让我带你穿越时光隧道,看看这些关键概念是如何从无到有,最终成为现代操作系统基石的。

一、"一人独占"的早期计算机时代

想象一下 20 世纪 50 年代的计算机实验室:一台体积庞大、价格昂贵的 UNIVAC 或 IBM 704 计算机占据了整个房间。这些早期计算机每次只能执行一个程序,用户必须排队等候。

更令人沮丧的是,当程序在等待磁带或打印机等慢速设备时,这些价值不菲的计算机就会闲置着——就像一辆豪华跑车被迫停在路边等待红灯一样浪费!

这种运行方式被称为"批处理系统"。用户把程序和数据打在卡片上交给操作员,然后等待——可能是几个小时,甚至是整整一天!如果前面的程序出了 bug 导致死循环,后面所有人都只能干等。

有一天,麻省理工学院的计算机科学家 Fernando Corbató 盯着实验室里的计算机发呆,突然灵光一闪:

"为什么要让昂贵的计算机资源在等待慢速 I/O 操作时空闲呢?我们应该让它能同时处理多个任务!"

二、分时系统:打破"独占"的第一步

于是,Corbató 和他的团队开始研发全新的系统——CTSS(Compatible Time-Sharing System,兼容分时系统),这被认为是现代多任务操作系统的雏形。

CTSS 的核心理念很简单:在内存中同时加载多个程序,当一个程序等待 I/O 操作时,CPU 可以切换去执行另一个程序。这就是"多道程序设计"的开端。

但问题来了:如何让一个正在运行的程序暂停,然后在未来某个时刻恢复执行呢?

答案是:必须保存程序的"上下文"信息!就像你在读一本书时,如果要暂时去做别的事,你需要记住当前读到第几页第几行,这样回来才能继续读。

于是,调查研究后,Corbató 和他的团队设计出了一个关键的数据结构来保存程序的"状态快照":

复制
struct cpu_state { uint32_t r0, r1, r2, r3; // 通用寄存器 uint32_t pc; // 程序计数器 uint32_t sp; // 栈指针 uint32_t status; // 状态寄存器 // 其他必要的寄存器状态... };1.2.3.4.5.6.7.

有了这个结构,当系统需要切换任务时,就可以保存当前程序的所有寄存器状态,然后加载另一个程序的状态,让 CPU 继续执行新的程序。

三、内存保护:解决程序"打架"问题

但实施分时系统后,一个新问题很快浮出水面。在贝尔实验室工作的 Dennis Ritchie(C 语言和 UNIX 的创始人之一)回忆道,他们的早期系统经常崩溃,而且数据会莫名其妙地被修改。

调查后,他们发现了根本原因:

不同的程序在同一内存空间中互相干扰!

原因很简单:在早期系统中,所有程序共享同一块内存空间,没有任何隔离机制。就像几个小孩在同一张纸上画画,互相涂抹对方的作品一样混乱。

看看这个灾难性的例子:

复制
// 计费程序 : interest.c struct account { char name[50]; double balance; } customer = {"Zhang San", 1000.0}; void update_balance() { while(1) { customer.balance *= 1.05; // 计算利息 sleep(1); } } // 同时运行的取款程序 : withdraw.c struct account { char name[50]; double balance; } customer = {"Zhang San", 1000.0}; void withdraw() { while(1) { customer.balance -= 100; // 定期取款 sleep(1); } }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.

在早期系统中,由于缺乏地址空间隔离机制,这两个独立编译的程序使用了相同的内存地址范围,导致它们的 customer 变量实际上重叠在物理内存的同一区域,结果就是一个程序计算利息增加余额,另一个程序同时在取款减少余额,导致完全不可预测的结果。

系统科学家们意识到,必须从根本上解决这个问题——需要在操作系统层面提供内存隔离机制!

四、进程的诞生:给每个程序一个"隔离舱"

1964年,MIT、贝尔实验室和通用电气公司联合开发了 MULTICS 系统。在这个过程中,一个革命性的概念诞生了——"进程"。

什么是进程?简单说,进程就是一个运行中的程序实例,拥有独立的内存空间和系统资源。

MULTICS 的设计者创建了一个内存映射系统,确保每个进程都有自己的独立内存区域:

复制
struct mem_layout { void* text_begin; // 代码区起始地址 size_t text_size; // 代码区大小 void* heap_begin; // 堆区起始地址 size_t heap_size; // 堆区大小 // 其他内存区域... };1.2.3.4.5.6.7.

将这个结构与前面的状态快照结构结合起来,就形成了完整的进程定义:

复制
struct task_control { struct cpu_state registers; // CPU状态 struct mem_layout memory; // 内存布局 pid_t id; // 任务标识符 uint8_t state; // 任务状态 resource_list_t resources; // 资源列表 // ... 其他控制信息 };1.2.3.4.5.6.7.8.

这就是操作系统中"进程控制块"(PCB)的雏形!有了它,操作系统就能管理多个进程的执行,实现真正的多任务处理。

但这又带来了新问题:如何公平地分配 CPU 时间给多个进程?

五、时间片轮转调度:让每个人都有发言权

在多进程系统中,如果让一个进程一直运行到结束,其他进程就得一直等待,这显然不合理。特别是对于交互式程序,用户会感觉系统反应迟钝。

为解决这个问题,MIT 的研究人员设计了一种全新的调度算法——"时间片轮转调度"(Round-Robin Scheduling)。

这个概念非常简单:给每个进程分配一个固定长度的 CPU 使用时间(称为"时间片",通常为几十毫秒),当一个进程用完自己的时间片,操作系统就强制将 CPU 分配给下一个等待的进程。

复制
// 时间片轮转调度的伪代码 void scheduler() { while(true) { process = get_next_process_from_queue(); set_timer(TIME_SLICE); // 设置时钟中断 context_switch(process); // 切换到该进程 // 时间片用完后,时钟中断处理程序会调用scheduler() } }1.2.3.4.5.6.7.8.9.

这就像是一场公平的会议,每个人都有固定的发言时间。即使有人滔滔不绝,到时间也必须让下一位发言。

时间片的长度设置很有讲究:

太短:进程切换开销占比太大,系统效率降低太长:响应时间变长,用户体验差

典型的时间片长度是 10-100 毫秒,这对人类感知来说足够短,让用户感觉多个程序在"同时"运行,这就是我们熟悉的"并发"。

进程的诞生和时间片轮转调度带来了巨大的好处:

不同程序之间彻底隔离,不会相互干扰系统可以同时运行多个程序,大大提高了效率即使一个程序崩溃,也不会影响其他程序的运行所有进程都能获得公平的 CPU 使用时间

就像给每个画画的小孩分发独立的画纸,再也不用担心谁会涂抹谁的作品了!同时,每个小孩都能轮流使用那支珍贵的金色画笔(CPU)。

六、进程间通信与切换瓶颈

随着进程模型的普及,新的需求出现了——进程之间需要交换数据和协调行动。UNIX的创始人们发明了各种"进程间通信"(IPC)机制,如 管道、消息队列和共享内存等。

但随着系统中运行的进程越来越多,一个严重的问题浮出水面——进程切换的开销实在太大了!每次切换进程,系统都需要:

保存当前进程的struct cpu_state(所有寄存器状态)更新struct mem_layout(切换完整的内存映射)—— 最耗时刷新 TLB 缓存和其他处理器缓存恢复新进程的struct cpu_state

这个过程就像一名摄影师在拍摄多个场景之间切换:不仅要记住每个场景的拍摄角度和相机设置(CPU 状态),还要搬运和重新布置整套灯光装备、背景和道具(内存映射)。即使摄影师技术再好,这种场景切换的时间成本也是无法避免的!

在加州大学伯克利分校,研究人员还观察到一个有趣的现象:服务器应用程序创建了大量进程来处理不同的请求,这些进程运行完全相同的代码,却各自占用独立的内存空间。这简直是在浪费资源!

面对这个问题,他们提出了一个革命性的疑问:"如果多个任务需要执行相同的程序代码,为何不创造一种新机制,让它们共享代码和数据,只在需要时保持独立?"

七、线程的诞生:轻量级的执行单元

1979年,Xerox PARC 的研究人员 David Boggs 和 Butler Lampson 在开发 Alto 操作系统时,提出了一个革命性的想法——"线程"。

线程被设计为进程内的"轻量级执行单元",它们:

共享所属进程的代码段、数据段和系统资源拥有自己的执行栈和寄存器状态可以独立调度执行
复制
struct execution_flow { uint32_t flow_id; // 流ID uint8_t status; // 运行状态 struct cpu_state regs; // 寄存器状态 void *stack; // 栈空间 struct task_control *owner; // 所属任务 };1.2.3.4.5.6.7.

线程相比进程有哪些优势?太多了!

创建和销毁更快:不需要分配新的地址空间,只需要一个新的栈切换开销小:线程间切换不需要切换地址空间,速度提升好多倍!资源共享自然:同一进程的线程共享内存,可以直接读写共享变量通信简单高效:线程间通信不需要特殊的IPC机制

就像网络游戏中的"组队"功能——同一队伍的玩家可以共享地图信息,直接交流,而不同队伍之间则需要特殊渠道才能通信。

八、实战对比:进程vs线程

理论讲完了,来看个简单例子,直观感受进程与线程的区别:

多进程版web服务器:

复制
import os from http.server import HTTPServer, BaseHTTPRequestHandler def run_server(port): server = HTTPServer((, port), BaseHTTPRequestHandler) server.serve_forever() # 创建5个进程,每个监听不同端口 for i in range(5): pid = os.fork() # 创建新进程 if pid == 0: # 子进程 run_server(8000 + i) break# 子进程不再继续循环 # 父进程需要等待子进程(简化处理) # ...1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.

多线程版web服务器:

复制
import threading from http.server import HTTPServer, BaseHTTPRequestHandler def run_server(port): server = HTTPServer((, port), BaseHTTPRequestHandler) server.serve_forever() # 创建5个线程,每个监听不同端口 threads = [] for i in range(5): t = threading.Thread(target=run_server, args=(8000 + i,)) threads.append(t) t.start() # 等待所有线程结束 for t in threads: t.join() # ...1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.

代码看起来很相似,但背后的区别巨大:

多进程版本中,每个服务器进程有完全独立的内存空间,资源消耗更大多线程版本中,所有线程共享同一个进程的内存空间,资源利用更高效多进程版更适合需要隔离的任务,多线程版更适合共享数据的任务九、线程安全与未来趋势

线程虽然解决了很多问题,但也带来了新的挑战,最大的就是"线程安全"问题。由于多个线程共享同一地址空间,并发访问共享数据会导致"竞态条件"。

看一个经典的计数器问题:

复制
// 全局共享变量 int counter = 0; // 线程函数 void* increment_counter(void* arg) { for (int i = 0; i < 100000; i++) { counter++; // 实际上是: 读取counter, +1, 写回counter } return NULL; }1.2.3.4.5.6.7.8.9.10.

当两个线程同时执行时,最终 counter 值可能小于期望的 200000,因为线程可能同时读取相同的值,各自加 1 后写回,导致一个增量丢失。

为了解决这个问题,操作系统引入了各种同步原语,如互斥锁、条件变量和信号量等。

技术永远在进化。如今,新一代的并发模型已经出现:协程(Coroutine)比线程更轻量,由程序自己控制切换,不需要操作系统介入。

历史回顾与结语

回顾这段历史,我们看到了计算机从"一台机器只能做一件事"到"同时处理成千上万任务"的惊人进化:

批处理系统(1950s):一次只运行一个程序,如早期的 IBM 704分时系统(1961):MIT 的 CTSS 首次实现了多用户时间共享进程(1964):MULTICS 系统引入进程概念,为程序提供独立执行环境线程(1979):Xerox Alto 操作系统引入轻量级执行单位,共享进程资源协程(2010s):用户态的轻量级线程,进一步降低并发开销

这就像交通工具从"一次只载一个人的独木舟",逐步发展为如今的"高速磁悬浮列车"一样神奇。

每当你打开电脑,运行十几个应用程序,同时浏览网页、听音乐、下载文件,这一切看似理所当然的便利,都是几代计算机科学家智慧的结晶。

进程和线程,这两个看似抽象的概念,让我们的数字生活变得如此丰富多彩。下次当你的电脑同时运行多个应用时,别忘了感谢那些 60 年代的工程师们——正是他们的创新思维,让今天的多任务处理成为可能!

THE END
本站服务器由亿华云赞助提供-企业级高防云服务器