想象一下,你是一名 1960 年代 IBM 计算机中心的工程师,每天都在面对一个棘手的问题:如何让更多用户能够使用昂贵的 System/360 大型机。

这台价值数百万美元(相当于今天的数千万美元)的庞然大物,是当时最先进的计算设备,但它的价格实在太昂贵了,即使是大型企业也很难独自承担。
而且这台机器的用户需要提前好几天来登记使用时间,每次只能为单个用户服务,所有任务都在串行执行。当某个程序等待磁带读取时,整个机器就会处于空闲状态,你体验到的是时间和金钱的双重流逝。
显然你会想:为什么当某个程序读写外部慢速设备时,要让宝贵的 CPU 空闲呢?这就好比程序员在等待程序编译完成时,他还可以去写第二个需求的代码啊。必须让任务并行起来。
必须并行起来
要实现这一点,程序必须具备暂停运行以及恢复运行的能力。而要想让程序具备暂停/恢复运行的能力,就必须保存 CPU 的上下文信息。
为此,你必须定义一个结构体来保存处理器的上下文信息:
struct context {
uint32_t eax, ebx, ecx, edx; // 通用寄存器
uint32_t esi, edi; // 源/目标变址寄存器
uint32_t esp, ebp; // 栈指针和基址指针
uint32_t eip; // 指令指针
uint32_t eflags; // 标志寄存器
uint16_t cs, ds, es, fs, gs, ss; // 段寄存器
};
每个运行起来的任务都需要这样一个结构体。当任务需要暂停时,就把处理器上下文保存在 context 结构体中;需要恢复任务运行时,就根据 context 中的数据恢复处理器状态。
现在,你就可以同时运行多个任务了:当任务 A 读取慢速磁带时,就暂停任务 A 的运行,并把 CPU 分配给任务 B。这样,宝贵的机器资源得到了充分利用。
多个程序相互干扰
当系统中可以运行多个任务后,新的问题出现了:多个程序之间会相互干扰。因为在早期计算机系统中,程序被链接到固定的内存基址,且加载器缺乏重定位能力。当多个程序加载到内存时,程序中的变量可能被分配到相同的物理地址,导致数据互相覆盖。
举个例子:
// program1.c
int global_data = 100; // 全局变量
int main() {
while(1) {
global_data++; // 不断增加全局变量的值
...
}
return 0;
}
// program2.c
int global_data = 100; // 同名全局变量
int main() {
while(1) {
global_data--; // 不断减少全局变量的值
...
}
return 0;
}
在这个示例中,两个同时运行的程序里 global_data 变量的内存地址可能相同。因此,一个程序对它的修改会直接影响另一个程序。原因很清楚:它们共享同一个内存空间。
你开始意识到,仅仅依靠程序员的自觉来避免互相干扰是不够的,需要从系统层面提供隔离机制。
于是,你开始设计一个新的抽象概念,让各个运行的程序彼此隔离,为每个程序提供独立的内存空间。你决定采用段式内存管理,每个运行的程序中的各个段(如代码段、数据段、栈段)都拥有自己独立的内存区域:
struct memory_map {
uint32_t code_segment; // 代码段起始地址
uint32_t code_size; // 代码段大小
uint32_t data_segment; // 数据段起始地址
uint32_t data_size; // 数据段大小
uint32_t stack_segment; // 栈段起始地址
uint32_t stack_size; // 栈段大小
};
进程诞生了
现在,你设计了 struct context 以及 struct memory_map。显然,它们都属于某一个“运行起来的程序”。这个“运行起来的程序”是一个全新的概念,你给它起了个名字,叫做 进程(Process)。
现在,进程的上下文以及内存映射信息都可以放到代表进程的结构体中:
struct process {
struct context ctx;
struct memory_map mem;
};
就这样,你实现了操作系统最核心的功能之一:多任务。进程这种设计效果显著:
- 用户程序再也不会意外修改其他程序的数据。
- 可以同时运行多个程序,在它们之间来回切换。
- 即使一个程序崩溃,也不会影响其他程序的运行。
不过,新的挑战也随之而来...
进程切换的性能瓶颈
多任务系统的使用解决了多用户共享计算机的问题。但很快,你就发现了一个令人头疼的新问题:随着系统中运行的进程越来越多,整个系统的响应速度开始明显下降。
通过仔细观察和测试,你发现问题出在进程切换上。每次从一个进程切换到另一个进程时,系统都需要执行大量的工作。
回顾一下你实现的进程结构:
struct process {
struct context ctx;
struct memory_map mem;
};
进程切换时,处理器的上下文(ctx)和内存映射(mem)都需要切换。尤其对于现代操作系统中的页式内存管理来说,内存映射的切换开销非常高(涉及 CR3 寄存器切换、TLB 刷新等操作)。
是否有必要创建过多进程?
真的有必要创建这么多进程吗?你仔细检查了一个开启大量进程的 Web 服务器。该服务器会创建多个工作进程来处理不同的 HTTP 请求。这些工作进程运行的是完全相同的代码,却各自占用一份独立的内存空间。同时,这些进程在切换时又会带来大量的开销。
但是等等,既然这些进程使用的是相同的代码,为什么不能让它们共享这部分内存呢?你开始意识到,也许可以创造一种新的执行单元,它们能共享进程的大部分资源,同时又保持足够的独立性。如果多个执行流可以共享同一个进程的资源,那切换的开销不就能大大降低了吗?
这个想法最终引导你走向了一个全新的概念。
线程概念的诞生
经过反复设计,你找到了一个突破性的解决方案:让同一个进程内部支持多个执行流。这个想法来源于一个关键观察:很多时候,我们其实并不需要完全独立的进程,而只是需要能够并行执行不同的任务就够了。
于是,你设计了一个全新的概念——线程(Thread)。每个线程都是进程内的一个独立执行单元,它们:
- 共享进程的地址空间:这意味着所有线程可以直接访问相同的内存区域。
- 共享打开的文件描述符:避免了重复打开和关闭文件的开销。
- 共享其他系统资源:如信号处理函数、进程工作目录等。
- 仅维护独立的执行栈和寄存器状态:确保每个线程可以独立执行,这正是并发编程的核心基础。
这就是线程的诞生故事。它完美平衡了资源共享和执行独立性,大幅降低了上下文切换的开销,是操作系统发展史上一个重要的里程碑,也为现代高性能服务器和应用程序的开发奠定了坚实的基础。如果你想了解更多计算机系统的底层原理,欢迎在云栈社区与更多开发者一起交流探讨。