进程的概念:程序、进程与 PCB
你有没有注意过一个再普通不过的现象:同一个 Chrome,你能开好几个窗口;同一份 python app.py,你能在多个终端里同时跑起来。它们用的是磁盘上同一份文件,可彼此互不干扰——这个标签页崩了,那个还活着;这个进程算到一半,那个早跑完了。
这说明一件事:磁盘上那个"程序",和正在跑的那个"任务",根本不是一回事。程序是死的、静态的;而正在运行的任务是活的,它有自己当前执行到哪、用了哪些内存、打开了哪些文件这些动态状态。操作系统需要一个东西来代表、来管理这个"活的任务"——这就是进程。
这一篇我们先把进程这个实体拆开看:它到底由哪些部分组成、内核用什么数据结构(PCB)来记录它、从创建到消亡经历了什么——把这套机制看清楚,再回答:为什么操作系统非要发明"进程"这个概念,直接管"程序"不行吗。
一、先把"程序"和"进程"分清楚
这是整篇的地基,必须先钉死。
程序是什么?它就是磁盘上一个静态的文件(比如一个 ELF 可执行文件),里面装着两样东西:要执行的机器指令(代码),和一些初始数据(常量、全局变量初值)。它躺在那里一动不动,不占 CPU、不占运行时内存,就是一堆字节。
进程是什么?它是"程序的一次执行过程"。当你运行这个程序时,操作系统把它的代码和数据装进内存,给它配上运行所需的一切动态状态,让 CPU 开始执行它的指令——这个"活起来的执行过程"就是进程。
程序是静态的指令和数据集合;进程是程序的一次动态执行,带着完整的运行时上下文。同一个程序可以同时对应多个进程,就像同一张乐谱可以被多个乐手同时演奏。
这就解释了开头的现象:一份 python 程序文件,跑三次就产生三个进程,它们指令相同,但各自有独立的内存、各自执行到不同位置、各自打开不同的文件,互不影响。
二、机制:一个进程到底由哪些部分组成
"动态执行上下文"听着抽象,我们把它拆成具体的几块。一个正在运行的进程,至少包含:
- 地址空间:进程独占的一片虚拟内存,里面分区存放——代码段(指令)、数据段(全局变量)、堆(动态分配的内存,
malloc/new来的)、栈(函数调用的局部变量和返回地址)。 - 寄存器现场:CPU 此刻为这个进程保存的一组值,最关键的是程序计数器(PC)(指向下一条要执行的指令)和栈指针(SP)。它代表"这个进程执行到哪了"。
- 打开的资源:进程打开的文件、网络连接等,内核用一张文件描述符表记录(你熟悉的 fd 0/1/2 就是标准输入/输出/错误)。
- 运行状态信息:它现在是在 CPU 上跑、还是在等 I/O、还是排队等 CPU;它的优先级、占用的内存量、统计信息等。
把这几块凑齐,才构成一个完整的、可被管理的执行实体。注意其中地址空间是进程间隔离的核心——每个进程都以为自己独占一整片内存,彼此看不到对方,这正是上几篇说的"隔离"在进程这一层的体现。
三、机制核心:PCB——内核怎么记录一个进程
上面那些信息,内核必须找个地方存起来、随时能查能改。这个数据结构就是 PCB(Process Control Block,进程控制块)。
PCB 不是一个抽象概念或"示意表格",而是内核在内存里为每个进程真实维护的一个数据结构。进程的一切,内核都通过它的 PCB 来掌握和操作。
在 Linux 里,PCB 就是 task_struct 这个结构体(定义在内核源码里,非常庞大)。一个 PCB 里大致记着:
- 进程标识:PID(进程号)、PPID(父进程号)、所属用户等身份信息;
- 状态:运行/就绪/阻塞等(下一篇专门讲);
- 寄存器现场的保存区:进程被切换下 CPU 时,PC、SP 等寄存器值就存在这里,等下次轮到它再恢复;
- 内存管理信息:指向它的地址空间描述结构(页表在哪、各段范围);
- 打开文件表:它的文件描述符指向哪些文件;
- 调度信息:优先级、已用 CPU 时间、调度策略;
- 进程关系:指向父进程、子进程的指针,构成进程树。
内核把所有进程的 PCB 组织起来(链表、哈希表、红黑树等),于是"管理进程"这件事,实质上就变成了"维护和操作这一堆 PCB":创建进程 = 新建一个 PCB 并填好;调度 = 从一堆 PCB 里挑一个;切换 = 把当前 PCB 的现场存好、把目标 PCB 的现场load出来;销毁进程 = 回收资源、删掉 PCB。
四、机制:一个进程的一生
把 PCB 放进时间线,看进程从生到死的完整流程:
- 创建:用户启动程序(或父进程调用
fork),内核新建一个 PCB,分配 PID,建立地址空间(把程序的代码数据装入),初始化寄存器现场(PC 指向程序入口),打开标准输入输出。一个进程就"出生"了。 - 就绪:PCB 被放进就绪队列,等调度器分配 CPU。
- 运行:调度器选中它,把它 PCB 里的寄存器现场恢复到 CPU 上,CPU 开始执行它的指令。
- 阻塞 / 切换:它可能因为等 I/O 被挂起(现场存回 PCB),也可能时间片用完被换下,让位给别的进程。这些状态变化下一篇详谈。
- 终止:进程执行完毕或被杀死,内核回收它的内存、关闭它打开的文件,但PCB 通常先不立即删除——要保留退出状态码,等父进程来"收尸"(调用
wait)。这就是"僵尸进程"的由来:活儿干完了,PCB 还挂着没被回收。
一个进程的生命周期,本质就是它的 PCB 被创建、在各种队列和状态间流转、最后被回收的过程。理解了 PCB,进程管理就不再神秘。
僵尸进程这个点对后端很实用:如果父进程不调用 wait 回收子进程,子进程的 PCB 会一直占着 PID 表项,大量堆积会耗尽 PID 资源。这是写守护进程、用多进程模型时必须处理的细节。
五、为什么需要"进程"这个抽象
机制清楚了,回答设计动机:操作系统为什么非要造"进程",直接调度"程序"不行吗?
因为操作系统要同时支撑很多任务并发运行,它必须解决三个问题,而"程序"这个静态概念一个都解决不了:
- 怎么表示"运行中"的状态? 程序是死的,但运行中的任务有"执行到哪、用了哪些资源"这些动态信息,必须有个实体来承载。进程就是这个实体。
- 怎么做隔离? 多个任务同时跑,必须互不干扰。进程通过独立地址空间,天然提供了隔离的边界——它成了资源分配和保护的基本单位。
- 怎么做调度和切换? 要让多个任务轮流用 CPU,就得能"保存一个任务的现场、恢复另一个"。有了 PCB 这个现场存放处,切换才有可能。
进程是操作系统为了"管理并发执行的任务"而创造的核心抽象。它把"运行中的任务"变成了一个有身份、有边界、可保存可恢复、可调度可回收的实体。
换句话说,进程 = 一份执行流 + 一片独立地址空间 + 一套资源 + 一个记录这一切的 PCB。它是前几篇"资源管理与抽象"思想在"任务"这个维度上的具体落地。
六、为什么"资源分配单位"和"执行单位"会分开
这里埋一个承上启下的点,为后面的线程做铺垫。
进程其实同时扮演了两个角色:它既是资源分配的单位(地址空间、打开的文件都按进程划分),又是CPU 调度执行的单位(CPU 执行的是某个进程的指令流)。
在只有进程的年代,这两个角色是绑在一起的。但人们很快发现一个矛盾:有时候我想在同一片资源(同一个地址空间)里,跑多条执行流——比如一个服务进程,想同时处理多个请求,它们共享同一份数据和连接,但各自独立推进。如果用多进程,每条执行流都要带一整套独立地址空间,又重又难共享数据。
进程把"资源分配"和"执行调度"两个角色捆在了一起,这在需要"共享资源、多流并发"时显得太重。这正是线程要解决的问题——把执行单位从进程里拆出来。
记住这个矛盾,下一组讲线程时你会发现,线程本质就是"共享同一进程资源的多条执行流"。进程和线程的分工,根子就在这里。
再补一个常被忽略却很能体现 PCB 价值的细节:进程间的父子关系也存在 PCB 里。每个进程的 PCB 都记着指向父进程、子进程的指针,整个系统的进程因此组织成一棵进程树(Linux 里所有进程都可追溯到 1 号 init/systemd 进程)。fork 创建子进程时,内核做的就是新建一个 PCB、把父进程的大部分信息复制过去、再建立这层父子指针。这棵树不只是组织形式,它还承载了实际语义:父进程要负责 wait 回收子进程(否则成僵尸);父进程先退出时,子进程会变成"孤儿",被过继给 init 进程接管回收。你在 pstree 里看到的那棵层层缩进的进程树,正是无数 PCB 里父子指针的可视化。理解进程关系存在 PCB 里,你就能解释守护进程、nohup、容器里 1 号进程为什么这么特殊——它们全是进程树语义的直接应用。
七、和工程实践 / 后端开发的联系
进程这个概念,是后端排障和架构设计的基本单位:
- 以进程为单位看监控:
ps、top里每一行就是一个进程(PCB 的外在表现),它的 CPU%、RSS(常驻内存)、打开的 fd 数,都是从 PCB 及其关联结构读出来的。线上排查"哪个服务吃内存/CPU",第一步就是定位到进程。 - 文件句柄泄漏:进程打开文件不关,文件描述符表会被撑满,报
Too many open files。理解 fd 表在 PCB 里,就知道为什么这是按进程计数、按进程排查的。 - 多进程架构的隔离优势:Nginx 的 worker、PostgreSQL 的连接进程、浏览器的多进程模型,都利用了"进程地址空间隔离"——一个进程崩溃不会带垮全局。稳定性和隔离性,是多进程架构的天然红利。
- 僵尸与孤儿进程:写守护进程、用
fork时,必须正确wait回收子进程,否则 PCB 堆积。这是进程生命周期知识的直接应用。
八、一个观察实验:看同一程序的多个进程
动手感受"程序≠进程"最直接。打开两三个终端,分别运行同一个程序(比如 sleep 1000)。然后另开一个窗口运行 ps aux | grep sleep。
你会看到:同一个 sleep 程序,对应了多行、每行一个不同的 PID,各自独立占用资源。它们的指令完全相同(来自同一个文件),却是彼此独立的进程实体。再用 cat /proc/<PID>/status 看某个进程的详细信息——你看到的状态、内存、线程数等,正是内核从这个进程的 PCB(task_struct)里读出来展示的。
重点不是命令多复杂,而是亲眼确认:一份静态程序,确实能对应多个独立的、带各自 PCB 的运行实体。
学习这一部分最容易踩的坑
1. 把进程等同于程序文件
进程是程序"跑起来"之后的动态实体,带着地址空间、寄存器现场、打开的资源。程序只是磁盘上的静态字节。混淆这两者,后面调度、切换、隔离全理解不了。
2. 把 PCB 当成"概念表格"
PCB(Linux 里的 task_struct)是内核在内存里真实维护的数据结构,进程的一切管理动作都落在它身上。轻视它,就抓不住"管理进程=操作 PCB"这条主线。
3. 以为进程间天然共享内存
恰恰相反,进程默认是隔离的,各有独立地址空间,互相看不到对方的内存。要共享得专门用共享内存、管道等机制。把"隔离"当"共享",会对多进程通信产生根本误解。
4. 忽略进程终止后的回收
进程跑完不等于 PCB 立刻消失。父进程不 wait,子进程会变僵尸,PCB 一直占着。写多进程程序却不管回收,迟早踩坑。
总结
进程把静态的程序变成了操作系统可管理的动态执行实体。它是为了支撑并发、实现隔离、支持调度而生的核心抽象,而承载这一切的,是内核为每个进程真实维护的 PCB。后续讲调度、同步、内存时,都会不断回到这个基础。
- 程序是磁盘上静态的指令+数据;进程是程序的一次动态执行,一个程序可对应多个进程;
- 进程由地址空间、寄存器现场、打开的资源、状态信息组成,地址空间是隔离的核心;
- PCB(Linux 的
task_struct)是内核真实维护的数据结构,管理进程就是操作 PCB; - 进程一生 = PCB 的创建 → 在状态/队列间流转 → 回收,终止后需父进程
wait回收,否则成僵尸; - 进程之所以被发明,是为了表示运行态、提供隔离边界、支持现场保存与调度;
- 进程同时是"资源分配单位"和"执行调度单位",这两个角色的捆绑,正是下一步线程要拆解的矛盾。
当你能清楚说出"进程 = 执行流 + 独立地址空间 + 资源 + PCB",并理解它为什么必须存在,下一篇讲进程状态与切换——也就是 PCB 里那个"状态"字段如何流转——就会水到渠成。
参考资源:
- 《现代操作系统》(Andrew S. Tanenbaum)
- 《操作系统导论》(OSTEP)进程抽象章节
- Linux 内核 task_struct 定义
- 《深入理解计算机系统》(CSAPP)进程章节