进程不是被生产出来的,是被复制出来的——除了 1 号,所有进程都是某个已有进程 fork 出来的。所以每个进程都有进程号与父进程号,整台机器是一棵进程树。
第二章讲完了用户模式那几层:系统调用、包装函数、标准库、命令。从这里开始往下一层——内核里第一个要拆的东西是【进程】,因为它是【谁在跑】这件事的全部答案。
一、进程是什么:一个正在跑的程序的全部家当
程序是文件,进程是活人。同一个 /bin/ls,你同时开三个窗口跑,就是三个进程——它们共用磁盘上那一份代码,但各有各的家当。
家当包括:地址空间(代码段、数据段、堆、栈)、打开的文件、当前目录、信号处理方式、环境变量、调度优先级。
内核给每个进程记一份这样的档案,档案的名字就叫进程控制块。所谓【创建进程】,本质就是造一份这样的档案,再加上一块地址空间。
二、三种来源,没有【凭空造】这一条路
一、内核亲手造。开机时内核造出 1 号进程,之后它再长出系统里所有别的进程。它没有父进程(父号写作 0)。
二、把已有的进程复制一份(fork)。子进程是父进程的复印件:地址空间内容一样、打开的文件一样、连【下一步要执行到哪一行】都一样。
三、复制完再换内容(fork + exec)。这是 shell 跑命令的标准两步:先复制一个自己,再把它换掉。
为什么非要走【复制】这一步?因为复制能顺带把父进程的【上下文】抄过去:环境变量、打开的文件、当前目录、权限身份。新进程一出生就知道自己该在哪干活。
三、进程树:每个进程都有爹
每个进程有两个号:自己的进程号(PID)、父进程号(PPID)。
整台机器就是一棵树:1 号在最上面,下面挂着一堆;你用 ps -ef 或 pstree 就能看见这棵树。
这棵树还解释了一件事:你在终端里敲 Ctrl+C,为什么只杀掉了前台那个程序?因为信号是发给【进程组】的,而进程组是树上的一个枝。
树断了会怎样:父进程先死,子进程就【过继】给 1 号(或某个 subreaper)。这就是为什么守护进程要故意脱离父进程——它不想跟着终端一起死。
四、Linux 与 Windows 最根本的分岔
Linux 用 fork:复制一个自己,代价是【复制】。 Windows 用 CreateProcess:直接指定要跑哪个程序,从头造一个新的。 两边的后果完全不同: 1. Linux 上【换程序】必须两次调用(fork 再 exec);Windows 上一次就够。 2. Linux 上子进程天然继承父进程的一切;Windows 要显式传句柄与参数。 3. Linux 的进程树很硬(父子关系真实存在);Windows 的父子关系弱,父进程死了子进程照样活。 没有谁更好,是两套不同的抽象。本书讲的是 Linux 这一套,所以后面所有细节都建立在 fork 上。
五、动手 Demo:造一个进程——它从哪来,谁是它爹
下面起一个子进程,把三个号摆在一起看:我自己的号、子进程的号、子进程报的父号。 注意:沙箱是 Windows,所以会看到一个 Linux 上不会出现的现象——子进程报的父号未必等于我的号。这不是代码错,是 Windows 创建进程的方式不同。
import os, subprocess, sys
me = os.getpid()
code = "import os; print(os.getpid(), os.getppid())"
pr = subprocess.Popen([sys.executable, "-c", code],
stdout=subprocess.PIPE, text=True)
cpid, cppid = map(int, pr.communicate()[0].split())
print("我", me, "父号", os.getppid())
print("子", cpid, "父号", cppid)
实跑输出:
--------------------------------------------------------------------
造一个进程——它从哪来,谁是它爹
--------------------------------------------------------------------
① 三个号码,说明一件事
角色 进程号 父进程号
-------------------------------------------
我(当前这个程序) 1632 14972
我刚起的那个进程 4892 5740
子进程报的父进程号就是我吗 False
我自己的父进程号 14972
**Linux 上 fork 出来的子进程,父进程号一定等于起它的那个进程。**
**Windows 上不一样**:它没有 fork,创建进程走 CreateProcess,
中间可能隔一层(所以这里读到的父号未必是我)。**这正是两种系统最根本的分岔。**
**每个进程都有爹,除了 1 号。** 1 号是内核启动时亲手造的第一个进程,
它的父进程号写作 0(或者干脆没有)——**整棵进程树从那儿长出来。**
② 造进程的三种来源
来源 谁造的 典型例子
------------------------------------------------------
内核亲手造 内核 1 号进程、内核线程
已有的进程复制 fork shell 起命令
复制完再换内容 fork + exec shell 跑 ls
**没有【凭空造一个】这条路的(除了 1 号)。**
**所以进程不是被生产出来的,是被【复制】出来的**——这一点决定了后面两节的全部细节。
子进程一定有自己独立的进程号——这是【进程】这个概念的身份证。
Linux 上 cppid 必然等于 me:fork 出来的孩子,父亲写得很清楚。Windows 上不保证:CreateProcess 造新进程时,中间可能隔着服务、隔着一层启动器,父子关系只是记账,不是血缘。
这就把 3.1 的最后一段坐实了:fork 与 CreateProcess 不只是 API 不同,它们对【进程从哪来】这个问题的回答就不一样。
📋 在真 Linux 上看整棵树:pstree -p 会把 1 号到你的 shell 的链路画出来;ps -eo pid,ppid,stat,comm --forest 用缩进表示层级。看一眼就知道自己挂在哪根枝上。
30 秒实验
echo $$拿到当前 shell 的进程号,再ps -o pid,ppid,cmd -p $$,看你的 shell 挂在谁下面。pstree -p | head看整棵树的顶端;pstree -p $$只看你这一枝。- 在 shell 里跑
sleep 300 &,然后ps -o pid,ppid,stat,cmd -p $!,看后台进程的父号是不是当前 shell。
常见误区
- 【进程就是程序】。程序是磁盘上的文件,进程是它跑起来之后的家当:地址空间、打开的文件、当前目录、信号处理。同一份程序可以有很多个进程。
- 【进程号是复用的,所以可以拿来当唯一标识】。进程号会循环复用,长跑的服务不能只靠 pid 判身份——所以才有 pidfile 加启动时间、cgroup 这些做法。
- 【父进程死了子进程就一定死】。Linux 上子进程会被过继给 1 号,照样活着。要靠进程组、会话、cgroup 才能一起收掉。
自测三问(点开对答案)
1. 进程的【家当】包括哪几样? —— 地址空间(代码/数据/堆/栈)、打开的文件、当前目录、信号处理、环境变量、调度优先级。 2. 为什么说【进程不是被生产出来的,是被复制出来的】? —— 除了 1 号由内核亲手造,其余都是 fork 已有进程。**复制顺带把上下文(环境、文件、目录)抄过去,新进程一出生就知道该在哪干活。** 3. Linux 的 fork 与 Windows 的 CreateProcess,后果上最大的差别是什么? —— Linux 上【换程序】要两步(fork + exec),子进程天然继承一切,父子关系硬;Windows 一步造新进程,继承要显式传,父子关系弱。- ☐ 3.2 fork()函数
相关笔记
- OS提供的程序 — shell 就是在这里 fork 的
- fork()函数 — 复制的细节
- 结束进程 — 死法也很讲究
- 🐧 Linux是怎样工作的 · 索引