进程的创建与结束
复习
- 内核态与用户态:为什么普通程序不能随意执行危险操作
- 系统调用:用户程序怎样安全地请求内核服务
- 程序如何变成进程:区分静态的程序文件和运行中的进程
TL;DR
- 进程通常由另一个进程创建,形成父子关系
- 创建常常分两步:先复制一份(
fork),再装入新程序(exec) - 进程结束时调用
exit,资源由内核回收 - 退出状态要由父进程收取,否则会留下“僵尸进程”
正文
上一章说,内核能把程序变成进程。可内核自己并不主动去“造”进程——真正发起创建的,往往是一个已经存在的进程。
父与子
在大多数操作系统里,创建进程的基本方式是:当前进程调用系统调用,复制出一个新进程。
被复制出来的叫子进程,原来的叫父进程。子进程几乎是父进程的翻版——同样的代码、同样的数据、同样的打开文件。之后,父子各走各的路,互不干扰。
很多系统把这个过程拆成两步:
fork:复制出一个和父进程几乎一样的子进程exec:在子进程里装入一个全新的程序,替换掉原来的代码和数据
“先克隆,再换人”——这样设计很灵活:可以先在克隆体里做些准备(比如重定向输入输出),再让它变成真正要跑的程序。
结束
进程干完活,会调用 exit 退出,并留下一个退出状态(比如 0 表示成功,非 0 表示出错)。
但退出不等于“彻底消失”。进程占用的内存、文件等资源,需要内核来回收;而它的退出状态,得由父进程来“收尸”。
如果父进程一直不去收,这个已经退出、却还没被清理的进程,就成了僵尸进程(zombie)。它不再运行,却还占着系统里的一条记录。反过来,如果父进程先退出了,留下的子进程会被系统接管,成为孤儿进程(orphan)。
一条完整的生命周期
父进程
│ fork:复制出子进程
▼
子进程
│ exec:装入新程序
▼
运行…… 父进程 wait:收取退出状态
│ exit:退出并留下状态 ▲
▼ │
等待被回收 ────────────────────────┘
│
▼
彻底消失
把这张图记住,就能理解很多现象:为什么启动一个程序,常常是“先复制、再替换”;为什么写服务器时,父进程要不停地“收”子进程,否则会攒一堆僵尸。
进程树
因为进程都是由别的进程创建的,整个系统里的进程其实构成一棵树:最顶上是一个“祖先进程”,下面分出子进程、孙进程……系统启动后,第一个进程就是这棵树的根。
这棵树不是摆设。权限、资源、信号,很多都沿着父子关系传递。理解了“创建靠复制、结束靠回收”,也就理解了这棵树是怎么长起来、又怎么修剪的。
思考题
既然
fork出来的子进程和父进程几乎一模一样,为什么不干脆“创建一个空进程、再装程序”,而要绕一道“先复制”?这样的设计带来了哪些灵活之处?
小结
知识点
- 进程由已有进程创建,形成父子关系
fork复制进程,exec装入新程序exit退出并留下退出状态- 父进程用
wait收取状态,否则产生僵尸进程 - 进程在系统中构成一棵树
参考资料
- Wikipedia(zh):fork:创建进程的系统调用
- Wikipedia(zh):exec:替换进程映像
- Wikipedia(zh):僵尸进程:僵尸与孤儿进程
思考题答案(仅供参考)
“先复制、再替换”的妙处在于:克隆出来的子进程继承了父进程已准备好的上下文——打开的文件、设置的输入输出、环境变量等。父进程可以先在克隆体里调整这些,再让 exec 换成真正的程序。比如 shell 要运行一条命令并把它的输出重定向到文件,就可以先在子进程里改好输出方向,再替换成命令程序。若直接“创建空进程”,这些准备就得另想办法。多一步复制,换来的是组合上的极大灵活。
协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
封面图
设计师 | 南国微雪