找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2196

积分

1

好友

298

主题
发表于 前天 21:32 | 查看: 2| 回复: 0

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

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)。每个线程都是进程内的一个独立执行单元,它们:

  1. 共享进程的地址空间:这意味着所有线程可以直接访问相同的内存区域。
  2. 共享打开的文件描述符:避免了重复打开和关闭文件的开销。
  3. 共享其他系统资源:如信号处理函数、进程工作目录等。
  4. 仅维护独立的执行栈和寄存器状态:确保每个线程可以独立执行,这正是并发编程的核心基础。

这就是线程的诞生故事。它完美平衡了资源共享和执行独立性,大幅降低了上下文切换的开销,是操作系统发展史上一个重要的里程碑,也为现代高性能服务器和应用程序的开发奠定了坚实的基础。如果你想了解更多计算机系统的底层原理,欢迎在云栈社区与更多开发者一起交流探讨。




上一篇:程序员高效阅读英文技术文档:2025主流翻译工具与场景化策略
下一篇:深入理解 C++ 多线程:Lock-Free 与 Wait-Free 编程实践
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-12 01:09 , Processed in 0.565120 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表