第3章 进程——“活着的“程序
你每天都在敲回车跑程序,但有没有想过:回车按下的那一瞬间,操作系统对着磁盘上那个文件到底干了什么?这一章,我们来看程序"活过来"之后的样子——进程(Process)。
本章回答什么问题
- 程序(Program)和进程,难道不是同一个东西吗?
- 同一个程序我跑了两遍,为什么两份数据互不相干?
- 怎么看我的机器上正在跑哪些进程、谁在吃 CPU?
- 想查某个进程的详细档案,去哪儿查?
- 新进程是从天上掉下来的吗?它从哪儿来?
正文
程序是死的,进程是活的
先说结论:程序和进程不是一回事。
你写好的程序,躺在磁盘上就是一个文件:一个 Python 脚本、一个编译好的可执行文件。它不占 CPU,一条指令都没跑,就是一堆安静的字节。
当你按下回车执行它,事情变了:操作系统把这个文件读进内存,分配资源,让 CPU 一条一条执行它的指令——这个正在运行的程序,就是进程。
用做饭来打比方:
- 程序 = 菜谱。写在纸上的东西,可以复印无数份,但菜谱本身做不出菜;
- 进程 = 照着菜谱做菜的整个过程。要占灶台(CPU)、用食材和碗碟(内存),还有进度——切完菜了、正在炒、快出锅了。
而且,同一份菜谱可以同时开好几份菜:同一个程序也能同时跑好几个进程。这一点马上用命令验证。
类比在这里失效了:做菜没人管你进行到哪一步,而进程的状态是操作系统强制管理的——它处于什么状态,操作系统说了算。
PID:每个进程的工号
操作系统同时管着成百上千个进程,怎么分清谁是谁?办法很朴素:给每个进程发一个编号,叫 PID(Process ID,进程号)。进程活着的时候,PID 是唯一的;进程结束后编号会被回收,过一会儿可能发给别人。
有个特殊编号要记住:PID 1。开机后操作系统启动的第一个进程就是它(Ubuntu 上一般是 systemd),后面所有进程都是它的子子孙孙。
光有个编号还不够。操作系统还给每个进程建了一份"任务档案",叫 PCB(Process Control Block,进程控制块):这个进程现在什么状态、用了多少内存、打开了哪些文件,都记在里面。说白了,PCB 就是操作系统的小本本,后面讲的调度、管理全靠它。这份档案由内核维护,你摸不到原件,但内核会把其中一部分内容放进一个叫 /proc 的目录供你随时查看——后面细说。
进程的几种状态:运行、睡眠、僵尸
任意时刻,一个进程大致处于这么几种状态:
- 运行态(Running,ps 里显示 R):正在 CPU 上执行,或者在排队等 CPU。
- 睡眠态(Sleeping,ps 里显示 S):在等一样东西——等磁盘读写、等键盘输入,或者像
sleep命令那样单纯等时间过去。机器上绝大多数进程在绝大多数时候都在睡觉,看到一堆 S 不用慌,这是常态。 - 僵尸态(Zombie,ps 里显示 Z):进程已经干完活退出了,但它的父进程还没来"收尸"。操作系统只能给它留一小块地方,记下"它是怎么死的、用了多少资源"这类遗言,等父进程来领。
僵尸进程听着吓人,其实它不吃 CPU 也不吃多少内存——它已经死了,只是还没下葬。真正的麻烦是僵尸太多:那说明某个父进程在偷懒,不收尸。
先留个观察点:top 输出的第二行 Tasks: 68 total, 1 running, 67 sleeping, 0 zombie,就是全机进程的状态分布,动手实验里会看到。
ps 拍合影,top 盯大屏
两个看进程的主力命令,性格完全不同。
ps(Process Status)拍的是快照:按下回车的瞬间,系统里有哪些进程,列出来给你看。
ps aux | head -n 5
下面是本机(WSL2 + Ubuntu)的实测输出,做了截取,你机器上的进程列表不会长这样:
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 18.1 0.0 24176 15424 ? Ss 23:33 0:00 /sbin/init
root 45 6.3 0.1 42072 16624 ? S<s 23:33 0:00 /usr/lib/systemd/systemd-journald
systemd+ 62 3.6 0.0 22540 14672 ? Ss 23:33 0:00 /usr/lib/systemd/systemd-resolved
先盯这几列:
USER:这进程是谁的;PID:工号;%CPU/%MEM:吃掉了多少 CPU 和内存;STAT:状态。S睡眠、R运行、Z僵尸,后面跟的s、+等是附加信息,初学不用纠结;COMMAND:这个进程是跑什么命令起来的。
看第一行:PID 为 1 的 /sbin/init,正是前面说的开机第一个进程。
top 是实时监控大屏:每隔几秒刷新一次,按 q 退出。用批处理模式 top -bn1 可以只打一屏,方便贴笔记。下面是实测输出(进程列表截取了几行):
top - 23:30:38 up 0 min, 1 user, load average: 0.00, 0.00, 0.00
Tasks: 68 total, 1 running, 67 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 15852.6 total, 14519.0 free, 746.7 used, 784.0 buff/cache
MiB Swap: 4096.0 total, 4096.0 free, 0.0 used. 15106.0 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 24164 15468 11672 S 0.0 0.1 0:00.38 systemd
45 root 19 -1 42068 16552 15320 S 0.0 0.1 0:00.09 systemd+
87 root 20 0 35464 12472 9276 S 0.0 0.1 0:00.14 systemd+
上面几行是总览:Tasks 是进程总数和状态分布;%Cpu(s) 是 CPU 占用,id 那一项是空闲比例;Mem 是内存。下面的表格每行一个进程,S 列是状态,%CPU、%MEM 最值得盯。top 默认按 CPU 占用排序——想知道"谁在拖慢机器",排在最前面的那位多半就是元凶。
/proc:进程的档案柜
ps 和 top 给你的是列表。想翻某个进程的详细档案,去 /proc 目录。
/proc 很特别:每个活着的进程,在里面都有一个以自己 PID 命名的子目录,装着这个进程的各种档案。换句话说,/proc 就是操作系统给进程开的档案柜。它的特殊之处第 7 章讲文件系统时还会再遇,这里先用起来:
sleep 1000 & # 后台起一个睡 1000 秒的进程,下一节细讲
head -n 10 /proc/390/status # 把 390 换成你的 PID
下面是实测输出:
Name: sleep
Umask: 0022
State: S (sleeping)
Tgid: 390
Ngid: 0
Pid: 390
PPid: 320
TracerPid: 0
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
逐行看:Name 是进程名;State: S (sleeping) 说明它正在睡觉;Pid 是它自己的工号;PPid 是它父进程的工号——没错,进程是有爹的。这就引出下一节。
进程从哪儿来:fork 分身术
系统里这么多进程,除了开机那一个,其余都是从哪儿来的?答案是:全部由已有进程"复制"出来。执行复制的系统调用(System Call)叫 fork()。
fork() 的行为很有意思:调用一次,返回两次。它把当前进程复制出一个几乎一模一样的子进程,父子俩从同一行代码继续往下跑。怎么知道自己是爹还是儿子?看 fork() 的返回值:
- 在父进程里,返回值是子进程的 PID;
- 在子进程里,返回值是 0。
写一段最小的 C 代码验证一下,保存为 fork_demo.c:
#include <stdio.h>
#include <unistd.h>
int main(void) {
printf("fork 之前:只有一个进程,PID = %d\n", getpid());
pid_t pid = fork();
if (pid < 0) {
printf("fork 失败了\n");
return 1;
}
if (pid == 0) {
printf("子进程:我的 PID = %d,我的父进程 PPID = %d\n",
getpid(), getppid());
} else {
printf("父进程:我的 PID = %d,fork 告诉我子进程的 PID = %d\n",
getpid(), pid);
}
return 0;
}
编译并运行:
gcc fork_demo.c -o fork_demo
./fork_demo
(示意输出,实际以本机为准:编号会变,结构不变)
fork 之前:只有一个进程,PID = 1234
父进程:我的 PID = 1234,fork 告诉我子进程的 PID = 1235
子进程:我的 PID = 1235,我的父进程 PPID = 1234
拆开看这段输出:
- 第一行在
fork()之前打印,那时世界上只有一个进程; fork()之后,世界一分为二:走父进程分支的那位拿到pid = 1235(儿子的工号),走子进程分支的那位拿到pid = 0;- 子进程用
getpid()报出的工号(1235),恰好是父进程从fork()拿到的那个数;子进程的getppid()(1234)又恰好是父进程的 PID——父子互相把对方认了出来。
还有一个容易忽略的点:父子谁先打印,是不固定的。fork() 之后两者平起平坐,谁先被 CPU 执行由调度器说了算(调度器怎么工作,第 4 章讲)。如果你的两行输出顺序和我不一样,完全正常。
给个类比:fork 就像大厨喊来一个学徒,学徒面前的厨房是大厨原样复制的。类比在这儿失效:现实中师徒共用一个厨房,而 fork() 之后,父子各有一份完全独立的内存——复制完成之后,各改各的,互不影响。
为什么要设计成"复制"这么怪的方式?因为在 Linux 上,跑一个新程序的标准流程就是:当前进程先 fork() 出一个子进程,再让子进程"变身"成新程序——你敲的每条 shell 命令都是这么跑起来的。"变身"的细节本章装不下,第 11 章讲系统调用时展开。
动手实验
环境:Ubuntu 22.04+ 的终端。下面的命令都可以直接敲,输出数字和你的会不一样,结构一样就对。
实验 1:用 ps 找到你的 shell
打开终端,第一个跑起来的进程就是你的 shell(默认是 bash)。它自己就是个进程:
ps aux | grep bash | grep -v grep
预期观察:能看到至少一行 bash,USER 是你自己的用户名。(示意输出,实际以本机为准)
yourname 388 0.0 0.0 6136 5200 pts/0 Ss 23:28 0:00 -bash
一句话解释:你敲命令的窗口本身就是个活着的进程;STAT 里的 S 说明它大多数时候在等你输入——睡觉是常态。
实验 2:用 top 看 CPU 和内存两列
top
预期观察:顶部是 Tasks、%Cpu(s)、Mem 总览;下面的列表每隔几秒刷新,绝大多数进程 S 列是 S、%CPU 是 0.0。按 q 退出。
一句话解释:机器闲着的时候,进程几乎全在睡觉;睡觉不吃 CPU,所以 %CPU 全零很正常。
实验 3:造一个后台进程,翻它的档案
sleep 1000 &
预期观察:shell 打印类似 [1] 390 的一行,最后那个数字就是新进程的 PID。一句话解释:& 把命令丢到后台跑,shell 不等它,立刻把提示符还给你。
head -n 10 /proc/390/status # 把 390 换成你刚拿到的 PID
预期观察:Name: sleep、State: S (sleeping),PPid 正好是你 shell 的 PID(用实验 1 查到的那个)。一句话解释:它在等 1000 秒过去,所以是睡眠态;它是被你的 shell 生出来的,所以爸爸是 shell。
实验 4:看看"一份菜谱,两道菜"
sleep 1000 &
sleep 1000 &
ps aux | grep "sleep 1000" | grep -v grep
实测输出:
yurutu 421 0.0 0.0 16112 7684 pts/0 S+ 23:16 0:00 sleep 1000
yurutu 422 0.0 0.0 16112 7856 pts/0 S+ 23:16 0:00 sleep 1000
预期观察:两行,命令一模一样,PID 各不相同。一句话解释:同一个程序可以是两个独立进程——菜谱是死的,菜是各做各的。
收尾:干掉自己造的进程
kill 390 421 422 # 换成你实际的 PID
ps aux | grep "sleep 1000" | grep -v grep
预期观察:第二条命令没有输出——进程没了,/proc 下对应的目录也消失了。一句话解释:kill 给进程发一个终止信号,进程收到就退出;信号的完整机制第 4 章讲。
⚠️ 危险范围说明:
kill只作用于你指定的 PID。动手前确认 PID 是自己刚起的sleep,别敲错数字;kill -9这种"强制手段"先别用,为什么,第 4 章说。
常见误区
-
误解:进程就是程序的另一种说法。 → 真相:程序是磁盘上死着的文件,进程是运行起来的活体。同一个程序启动两次就是两个独立进程,各有各的 PID 和内存,互相看不见对方的数据。
-
误解:内存占得高,CPU 一定也占得高。 → 真相:
top里这两列没有必然联系。一个进程可以占着几个 G 内存安安静静睡觉(%CPU为 0),也可以内存很小却把 CPU 烧满。两列分开看。 -
误解:僵尸进程是卡死的进程,得赶紧 kill 掉。 → 真相:僵尸已经死了,不吃 CPU,
kill一个死人也毫无作用。它只占一个 PID 和一小块记录遗言的地方;该处理的是那个不来收尸的父进程。
小结
- 程序是躺在磁盘上的文件,进程是它跑起来之后的活体。
- 一份菜谱能同时做几道菜:同一程序可以有多个进程,各跑各的。
- 操作系统用 PID 区分进程,给每个进程建档案(PCB),PID 1 是万物之祖。
- 进程状态看三种:运行 R、睡眠 S、僵尸 Z;绝大多数时候大家都在睡觉,僵尸只是没人收尸。
ps拍快照,top实时盯,%CPU和%MEM是最值得盯的两列。/proc/PID/是进程的档案柜,status里能看到状态和它的爹(PPid)。- 新进程都是老进程用
fork()复制出来的;shell 跑命令靠的就是它,第 11 章展开。
思考题
- 浏览器开了几十个标签页,每个标签页往往就是一个进程——为什么你的 CPU 没有被立刻挤爆?(提示:想想大多数进程此刻处于什么状态。第 4 章会从调度器的角度验证你的猜想。)
- 执行
sleep 1000 &之后,如果它的父进程(你的 shell)先退出了,这个子进程会变成什么样?再去看一眼/proc/<PID>/status里的PPid,变成多少了? - 在 fork 示例里,如果
fork()之后子进程先结束,而父进程什么都不做、接着睡 30 秒——这 30 秒里子进程处于什么状态?用top的Tasks行验证一下。
一句话总结:程序只是躺在磁盘上的菜谱,进程是开火做饭的活现场——操作系统给每个进程发工号(PID)、建档案(PCB)、挂在
/proc里供你查,而所有新进程,都是fork()出来的分身。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)