1. 进程与线程的千年纠葛

1.1 从冯·诺依曼体系说起

说实话,每次我给学生讲多线程,总得先聊聊冯·诺依曼。为什么?因为现代操作系统的并发革命,根子就在这儿。

冯·诺依曼体系的核心是什么?存储程序、顺序执行。说白了,CPU 一条一条地取指令、执行指令。早期计算机就是这么干的——一个程序跑完,下一个才能上。你想想看,那时候的计算机多「傻」啊,CPU 大部分时间都在等 I/O,利用率低得可怜。

我刚开始做嵌入式开发时,遇到过一台老式工控机。它跑的是单任务系统,一个数据采集程序没跑完,你就别想干别的。有一次采集卡卡住了,整个系统就僵在那,我盯着屏幕干瞪眼。嗯,从那时起我就明白了:并发不是锦上添花,是刚需。

1.2 进程的诞生:PCB 的底层数据结构

进程是什么?教科书上说:进程是程序的一次执行。但我觉得,更准确的说法是:进程是操作系统管理资源的最小单位。

每个进程都有一个「身份证」——进程控制块(PCB)。我习惯把它想象成一张档案卡,操作系统靠它来管理所有进程。

PCB 里到底存了什么?

  • 进程标识符(PID):每个进程的唯一编号。就像身份证号。
  • 程序计数器(PC):下一条指令的地址。进程切换时,CPU 靠它恢复现场。
  • CPU 寄存器:通用寄存器、栈指针、状态寄存器等。说白了,就是 CPU 的「快照」。
  • 内存管理信息:页表、段表、内存限制等。进程的「地盘」有多大,全看它。
  • 文件描述符表:进程打开的文件列表。Linux 下一切皆文件,这个表很重要。
  • 调度信息:优先级、状态(就绪/运行/阻塞)、时间片等。调度器靠它决定谁上 CPU。

我曾在一次性能调优中,发现某个进程的 PCB 里文件描述符表爆了。进程打开了几千个 socket 没关,系统直接卡死。查了半天,原来是个低级 bug。从那以后,我写代码都会定期检查文件描述符的用量。

PCB 在 Linux 内核里是用 task_struct 结构体实现的。这个结构体很大,几百行代码。我挑几个关键字段给你看看:

// Linux 内核 task_struct 简化版
struct task_struct {
    volatile long state;          // 进程状态:-1不可运行,0可运行,>0停止
    void *stack;                  // 内核栈指针
    unsigned int flags;           // 进程标志
    int prio, static_prio;       // 优先级
    struct list_head tasks;       // 进程链表
    struct mm_struct *mm;         // 内存描述符
    struct files_struct *files;   // 文件描述符表
    pid_t pid;                    // 进程ID
    struct task_struct *parent;   // 父进程
    struct list_head children;    // 子进程链表
    // ... 还有几百行
};

你看,光是这个结构体就包含了进程的所有「家当」。每次进程切换,内核都要保存当前进程的 PCB,再加载下一个进程的 PCB。这个过程叫上下文切换,开销不小。

1.3 线程:轻量级进程的本质

进程的缺点很明显:创建慢、切换慢、通信慢。为什么?因为每个进程都有自己的独立地址空间,切换时要换页表、刷 TLB,开销很大。

那怎么办?人们想:能不能让多个「执行流」共享同一个地址空间?这样切换不就快了吗?

嗯,这就是线程的由来。

线程的本质是什么?说白了,线程就是「轻量级进程」。它共享进程的地址空间、文件描述符、信号处理等资源,但有自己的栈、寄存器和程序计数器。

我打个比方:进程就像一栋楼,线程就是楼里的住户。楼是固定的(地址空间),但每个住户有自己的房间(栈)和钥匙(寄存器)。住户之间串门很方便(共享内存),但要是有人把楼拆了(进程退出),所有住户都得搬家。

线程 vs 进程:核心区别

对比项 进程 线程
地址空间 独立 共享
创建开销 大(复制页表等) 小(仅分配栈)
切换开销 大(换页表) 小(不换页表)
通信方式 IPC(管道、消息队列等) 直接读写共享内存
安全性 高(隔离性好) 低(一个线程崩溃可能影响整个进程)

在 Linux 内核里,线程其实是用 clone() 系统调用创建的。它和进程的 fork() 本质上是同一套机制,只是参数不同:

// 创建进程:复制所有资源
pid_t pid = fork();

// 创建线程:共享地址空间、文件描述符等
// CLONE_VM 表示共享内存空间
// CLONE_FS 表示共享文件系统信息
// CLONE_FILES 表示共享文件描述符表
pid_t tid = clone(CLONE_VM | CLONE_FS | CLONE_FILES, ...);

你看,线程和进程在底层其实是一家人。Linux 内核里没有专门的「线程结构体」,线程就是用 task_struct 表示的,只不过它和父进程共享了某些资源。

我曾经在调试一个多线程服务器时,发现线程间共享了一个全局变量,结果两个线程同时写,数据全乱了。嗯,这就是典型的竞态条件。从那以后,我写多线程代码必加互斥锁,或者用原子操作。

1.4 并发革命的本质

为什么说进程和线程的「纠葛」推动了并发革命?

你想想看,早期的操作系统只有进程,一个进程阻塞了,整个系统就闲着。后来有了线程,一个线程阻塞了,其他线程还能跑。再后来,多核 CPU 出现了,线程可以真正并行执行——一个核跑一个线程。

但问题也来了:线程多了,同步就成了噩梦。死锁、活锁、饥饿、数据竞争……我见过太多项目因为多线程 bug 而延期。有一次,一个同事写的线程池代码,在压力测试下直接死锁,所有线程都在等一个锁,CPU 占用率 100% 但吞吐量为 0。查了两天才发现是锁的获取顺序不一致。

避坑指南:我曾经踩过的坑

  • 不要滥用线程:线程不是越多越好。上下文切换的开销可能抵消并行带来的收益。
  • 注意线程安全:全局变量、静态变量、共享资源都要加锁。别偷懒。
  • 避免死锁:锁的获取顺序要一致。我习惯用「锁排序」策略,所有线程按相同顺序获取锁。
  • 小心信号处理:在多线程程序里,信号的处理方式很复杂。我建议尽量用专门的信号线程。

1.5 知识体系总览

下面这张图是我自己画的,把本章的核心知识点串起来了。你看一眼,应该能对进程和线程的关系有个整体认识。

进程与线程知识体系 冯·诺依曼体系 存储程序 · 顺序执行 进程 (Process) 资源管理最小单位 PCB (task_struct) PID · PC · 寄存器 内存信息 · 文件描述符 调度信息 · 优先级 线程 (Thread) 轻量级进程 线程特点 共享地址空间 独立栈和寄存器 创建/切换开销小 并发革命:多核并行 · 同步与互斥 核心思想:进程是资源容器,线程是执行单元 Linux 实现:统一用 task_struct,通过 clone() 参数控制资源共享 ⚠ 避坑:线程同步是最大难点,死锁、竞态、饥饿都是常见问题

这张图把本章的核心逻辑串起来了:从冯·诺依曼体系出发,引出进程和 PCB,再引出线程这个「轻量级进程」,最后落到并发革命和同步问题。你多看几遍,应该能建立起一个清晰的知识框架。

好了,第一章就讲到这里。进程和线程的「纠葛」远不止这些,后面我们会深入线程的创建、同步、调度等细节。嗯,慢慢来,先把基础打牢。


可可精品资料库