操作系统学习笔记 · 第 4 课 · 进程——正在运行的程序
本课属于 第一阶段 · 走进操作系统(第 1~5 课)。
前几课我们一直在聊"系统整体":怎么开机、怎么响应中断。这一课,镜头终于对准了操作系统真正的主角——进程。你双击一个图标、在命令行敲下一个命令,背后"活过来"的那个东西,就是它。
从"双击图标"说起
你现在做一件再普通不过的事:双击桌面上的浏览器图标。
那一瞬间,磁盘上那个安静的 .exe(或 Linux 下的可执行文件),被"激活"了——操作系统为它分配内存、登记身份、安排 CPU 去执行它。这个"活过来"的东西,就叫进程(Process)。
你可能会想:这跟我双击之前那个文件,难道不是一回事吗?不是。 这正是这一课最核心、也最容易搞混的分界。
4.1 程序 vs 进程:菜谱和厨师
一句话就能说清:
程序是"菜谱",进程是"正在炒菜的人"。
- 程序(Program):静静地躺在磁盘上的代码文件。它是静态的,你关机、重启,它都还在那里,一动不动。
- 进程(Process):程序被加载进内存、开始运行时,那个活生生的实体。它占用内存、占用 CPU、拥有自己的一堆资源(打开的文件、网络连接……)。
菜谱和厨师的差别,处处可见:
- 菜谱只有一份,但厨师可以有好几个——你同时开 3 个浏览器窗口,就是同一份程序(同一张菜谱)对应 3 个进程(3 个厨师),他们各炒各的,互不打扰;
- 菜谱被撕了(删了程序文件),正在炒菜的厨师不受影响——他已经把菜谱"记在脑子里"了(加载进内存了);
- 厨师炒完菜(进程结束),菜谱(程序文件)还在,下次还能再用。
一句话理解:程序是磁盘上"死的"代码,进程是内存里"活的"运行实体。同一个程序可以同时"活"出多个进程。
4.2 PCB——进程的"身份证"
操作系统同时要管成百上千个进程,它靠什么记住每个进程的"底细"?答案是给每个进程建一份档案,这份档案叫 PCB(Process Control Block,进程控制块),在 Linux 内核里,它对应的结构体叫 task_struct。
一句话理解:PCB 是内核为每个进程建立的"身份证",上面记录了这个进程的一切。
这张"身份证"上,密密麻麻写满了东西:
- PID:进程编号,唯一标识一个进程;
- 状态:它现在在跑、在等、还是睡着了(下一节详说);
- 程序计数器、寄存器:记住它"跑到哪一行了"(被中断打断时靠这个恢复);
- 内存映射:它的代码、数据放在内存哪些位置;
- 打开的文件表:它打开了哪些文件、哪些网络连接;
- 优先级:它该被 CPU 多照顾,还是往后排。
你在终端敲 ps -ef 看到的那些信息——PID、父进程、启动时间……绝大部分都是从 PCB 里读出来的。PCB 就是进程在操作系统里的"户口本"。
小知识点:PID 是整数,从 1 开始分配,用完了会回收复用。我们上一课说的 PID 1(init/systemd),就是"元老级"的那个身份证号。
4.3 进程状态机:不只是"跑/没跑"
很多人的直觉是:进程要么在跑,要么没在跑。实际比这丰富得多。 一个进程的完整一生,是在下面这个状态机里来回切换的:
被调度选中(拿到CPU)
就绪 Ready ──────────────→ 运行 Running
↑ │ │
│ │ 时间片用完/被抢占 │ 等I/O、等锁、sleep
│ └──────────────────────┘
│ ↓
└────── 阻塞 Blocked ←──────┘
│
└── 等待的事件发生(I/O完成等) → 回到就绪核心状态有三个:
- 就绪(Ready):我想跑,但 CPU 现在被别人占着,我在排队;
- 运行(Running):我正占着 CPU 干活;
- 阻塞(Blocked):我想跑也没用,因为我在等别的东西(等硬盘读完、等网络数据、等一个锁),根本没法继续。
转移规则一共四条,抓住了就全懂了:
- 就绪 → 运行:调度器把 CPU 分给它了;
- 运行 → 就绪:时间片用完了,或被更重要的进程抢占,先让位;
- 运行 → 阻塞:它主动去等 I/O、等锁、或者
sleep了; - 阻塞 → 就绪:等的事情终于发生(比如 I/O 完成了),它又能跑了。
这里有一个反直觉但极重要的点:
你在下载一个大文件时,那个下载进程大部分时间都处于「阻塞」状态,而不是占着 CPU。
为什么?因为"下载"的本质是等网卡把数据送过来。网卡很慢(相对 CPU 而言),进程等数据的那几毫秒里,CPU 完全可以去干别的。所以一个聪明的进程,是"干活一小会儿,等数据一大段时间"——它把 CPU 让出来给别人用,自己进「阻塞」状态睡大觉,数据一到就被叫醒(还记得上一课的中断吗?就是网卡中断把它叫醒的)。
4.4 进程树与创建:所有进程都是"生"出来的
进程不会凭空出现。每一个进程,都是被另一个进程"生"出来的——用 fork 这个系统调用(下一课会详细拆)。
一句话理解:所有进程都是"生"出来的——由父进程 fork 出子进程,一路往上追溯,最后都归到 PID 1。
所以进程之间是有父子关系的,形成一棵进程树:
- 你在终端敲一个命令,那个 shell 就是这个命令进程的父进程;
- 往上一层,这个 shell 又是由它的父进程(可能是终端模拟器、可能是 systemd)生出来的;
- 一路往上,最终追溯到 PID 1,那个"老祖宗"。
用 pstree -p 可以直观地看到这棵树的形状。
这也解释了上一课埋的那个问题:PID 1 为什么特殊?因为它是所有进程的祖先,还是"孤儿收容所"(见下面深入点)。
动手实验:用命令看进程
打开终端,一条条试:
# 1. 看所有进程,重点注意 PID 和 PPID(父进程 ID)
ps -ef
# 2. 树状看父子关系(-p 显示 PID)
pstree -p
# 3. 数一数同一个程序开了几个进程(比如 chrome)
ps aux | grep chrome | grep -v grep | wc -l
# 4. 看某个进程的详细档案(把 <pid> 换成真实 PID)
cat /proc/<pid>/status第 4 条特别值得玩味。/proc 是个神奇的目录,里面每个数字文件夹对应一个进程,status 文件里就写着它的状态和资源。你会看到这样的字段:
State: S (sleeping) ← 进程当前状态
Pid: 1234 ← 进程号
PPid: 1000 ← 父进程号那个 State 后面的字母,是进程状态的标准缩写,记这几个最常见的就够:
| 字母 | 含义 |
|---|---|
R | 运行(Running,正在跑或在排队) |
S | 可中断睡眠(Sleeping,最常见,等着被叫醒) |
D | 不可中断睡眠(通常在等 I/O,强行 kill 不掉) |
Z | 僵尸(Zombie,见下面深入点) |
T | 停止(Stopped,被挂起) |
深入点:僵尸、孤儿、写时复制
① 僵尸进程(Zombie)
子进程结束之后,它不能"一走了之"——它的退出状态、运行统计这些临终信息还得留给父进程来领。如果父进程一直不来"收尸"(调用 wait),子进程的 PCB 就残留在内核里,变成 Z 状态——僵尸进程。
僵尸进程"死了又没死透":它已经不占 CPU、不占内存了,但还占着 PID。僵尸太多,PID 会被耗尽,系统就没法开新进程了。
你在 ps 里看到 Z 状态的进程,多半就是某个父进程"生而不养"造成的。解决方法是让父进程及时 wait,或者把父进程 kill 掉(这样僵尸会被 PID 1 收走)。
② 孤儿进程(Orphan)
反过来,如果父进程先死了,子进程怎么办?别担心,内核有个温暖的规矩:
父进程先走,子进程会被 PID 1(init/systemd)收养,由这位"老祖宗"负责给它们收尸。
所以孤儿进程不会变成僵尸,因为它们有了新的"监护人"。Linux 就是这么设计的——保证每一个进程,最终都有人管。
③ fork 的写时复制(COW)
父进程 fork 出子进程时,如果老老实实把整个内存都复制一份,会非常浪费(很多页面其实根本不会改)。Linux 用了聪明的一招:写时复制(Copy-On-Write,COW)。
先不真复制,父子俩共享同一份内存;谁要是想"写"(改),系统才临时给谁复制一份。这样既快又省。
这个概念我们留到第 7 课讲 fork 时再展开,你现在记住这个"共享到改写时才复制"的巧思即可。
小结与思考题
这一课,我们认识了操作系统的主角——进程:
- 程序是菜谱,进程是厨师:一个静态,一个动态;
- PCB(task_struct)是进程的"身份证",记录 PID、状态、寄存器、内存映射、打开的文件等一切;
- 进程在就绪 / 运行 / 阻塞三个状态间来回迁移,其中"等 I/O"是阻塞,不占 CPU;
- 进程是生出来的,父进程 fork 子进程,一路追溯到 PID 1,形成进程树。
留三个问题给你:
- 用一句话分别说清"程序"和"进程"。
- 一个进程卡在下载大文件(等网卡数据),它此刻处于什么状态?它占 CPU 吗?
- 用
ps找一个Z状态的进程,想想它为什么会变成僵尸?它的父进程是不是"生而不养"了?
下一课,我们要往进程里面再钻一层,见见一个更轻巧的角色:线程。一个浏览器进程,怎么做到一边渲染网页、一边下载图片、一边响应你的点击?靠的就是进程里的这些"分身"。