🐧 第三章 进程管理
📄 一句话概括

进程不是被生产出来的,是被复制出来的——除了 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 秒实验

  1. echo $$ 拿到当前 shell 的进程号,再 ps -o pid,ppid,cmd -p $$,看你的 shell 挂在谁下面。
  2. pstree -p | head 看整棵树的顶端;pstree -p $$ 只看你这一枝。
  3. 在 shell 里跑 sleep 300 &,然后 ps -o pid,ppid,stat,cmd -p $!,看后台进程的父号是不是当前 shell。

常见误区

  1. 【进程就是程序】。程序是磁盘上的文件,进程是它跑起来之后的家当:地址空间、打开的文件、当前目录、信号处理。同一份程序可以有很多个进程。
  2. 【进程号是复用的,所以可以拿来当唯一标识】。进程号会循环复用,长跑的服务不能只靠 pid 判身份——所以才有 pidfile 加启动时间、cgroup 这些做法。
  3. 【父进程死了子进程就一定死】。Linux 上子进程会被过继给 1 号,照样活着。要靠进程组、会话、cgroup 才能一起收掉。
自测三问(点开对答案) 1. 进程的【家当】包括哪几样? —— 地址空间(代码/数据/堆/栈)、打开的文件、当前目录、信号处理、环境变量、调度优先级。 2. 为什么说【进程不是被生产出来的,是被复制出来的】? —— 除了 1 号由内核亲手造,其余都是 fork 已有进程。**复制顺带把上下文(环境、文件、目录)抄过去,新进程一出生就知道该在哪干活。** 3. Linux 的 fork 与 Windows 的 CreateProcess,后果上最大的差别是什么? —— Linux 上【换程序】要两步(fork + exec),子进程天然继承一切,父子关系硬;Windows 一步造新进程,继承要显式传,父子关系弱。

相关笔记