Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

进程的创建与结束

复习

  • 内核态与用户态:为什么普通程序不能随意执行危险操作
  • 系统调用:用户程序怎样安全地请求内核服务
  • 程序如何变成进程:区分静态的程序文件和运行中的进程

TL;DR

  • 进程通常由另一个进程创建,形成父子关系
  • 创建常常分两步:先复制一份(fork),再装入新程序(exec
  • 进程结束时调用 exit,资源由内核回收
  • 退出状态要由父进程收取,否则会留下“僵尸进程”

正文

  上一章说,内核能把程序变成进程。可内核自己并不主动去“造”进程——真正发起创建的,往往是一个已经存在的进程。

父与子

  在大多数操作系统里,创建进程的基本方式是:当前进程调用系统调用,复制出一个新进程。

  被复制出来的叫子进程,原来的叫父进程。子进程几乎是父进程的翻版——同样的代码、同样的数据、同样的打开文件。之后,父子各走各的路,互不干扰。

  很多系统把这个过程拆成两步:

  1. fork:复制出一个和父进程几乎一样的子进程
  2. exec:在子进程里装入一个全新的程序,替换掉原来的代码和数据

  “先克隆,再换人”——这样设计很灵活:可以先在克隆体里做些准备(比如重定向输入输出),再让它变成真正要跑的程序。

结束

  进程干完活,会调用 exit 退出,并留下一个退出状态(比如 0 表示成功,非 0 表示出错)。

  但退出不等于“彻底消失”。进程占用的内存、文件等资源,需要内核来回收;而它的退出状态,得由父进程来“收尸”。

  如果父进程一直不去收,这个已经退出、却还没被清理的进程,就成了僵尸进程(zombie)。它不再运行,却还占着系统里的一条记录。反过来,如果父进程先退出了,留下的子进程会被系统接管,成为孤儿进程(orphan)。

一条完整的生命周期

父进程
  │ fork:复制出子进程
  ▼
子进程
  │ exec:装入新程序
  ▼
运行……            父进程 wait:收取退出状态
  │ exit:退出并留下状态          ▲
  ▼                               │
等待被回收 ────────────────────────┘
  │
  ▼
彻底消失

  把这张图记住,就能理解很多现象:为什么启动一个程序,常常是“先复制、再替换”;为什么写服务器时,父进程要不停地“收”子进程,否则会攒一堆僵尸。

进程树

  因为进程都是由别的进程创建的,整个系统里的进程其实构成一棵树:最顶上是一个“祖先进程”,下面分出子进程、孙进程……系统启动后,第一个进程就是这棵树的根。

  这棵树不是摆设。权限、资源、信号,很多都沿着父子关系传递。理解了“创建靠复制、结束靠回收”,也就理解了这棵树是怎么长起来、又怎么修剪的。

思考题

  既然 fork 出来的子进程和父进程几乎一模一样,为什么不干脆“创建一个空进程、再装程序”,而要绕一道“先复制”?这样的设计带来了哪些灵活之处?

小结

知识点

  • 进程由已有进程创建,形成父子关系
  • fork 复制进程,exec 装入新程序
  • exit 退出并留下退出状态
  • 父进程用 wait 收取状态,否则产生僵尸进程
  • 进程在系统中构成一棵树

参考资料

  1. Wikipedia(zh):fork:创建进程的系统调用
  2. Wikipedia(zh):exec:替换进程映像
  3. Wikipedia(zh):僵尸进程:僵尸与孤儿进程

思考题答案(仅供参考)

  “先复制、再替换”的妙处在于:克隆出来的子进程继承了父进程已准备好的上下文——打开的文件、设置的输入输出、环境变量等。父进程可以先在克隆体里调整这些,再让 exec 换成真正的程序。比如 shell 要运行一条命令并把它的输出重定向到文件,就可以先在子进程里改好输出方向,再替换成命令程序。若直接“创建空进程”,这些准备就得另想办法。多一步复制,换来的是组合上的极大灵活。

协议

  本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

封面图

设计师 | 南国微雪