前段时间线上的一个使用 google Puppeteer 生成图片的服务炸了 , 每个 Docker 容器内都有几千个孤儿僵死进程没有回收 , 如下图所示 。
文章插图
这篇文章比较长 , 主要就讲了下面这几个问题 。
- 什么情况下会出现僵尸进程、孤儿进程
- Puppeteer 工作过程启动的进程与线上事故分析
- PID 为 1 的进程有什么特殊的地方
- 为什么 node/npm 不应该作为镜像中 PID 为 1 的进程
- 为什么 Bash 可以作为 PID 为 1 的进程 , 以及它做 PID 为 1 的进程有什么缺陷
- 镜像中比较推荐的 init 进程的做法是什么
本案例中使用的场景是使用 Puppeteer 加载 html , 随后截图生成一张分销海报的图片 。文章分析了这个问题背后的原因 , 接下来开始正式的内容 。
进程每个进程都有一个唯一的标识 , 称为 pid , pid 是一个非负的整数值 , 使用 ps 命令可以查看 , 在我的 mac 电脑上执行 ps -ef 可以看到当前运行的所有进程 , 如下所示 。
UIDPIDPPIDC STIMETTYTIME CMD0100 六04下午 ??23:09.18 /sbin/launchd03910 六04下午 ??0:49.66 /usr/sbin/syslogd04010 六04下午 ??0:13.00 /usr/libexec/UserEventAgent (System)
其中 PID 是表示进程号 。系统中每个进程都有对应的父进程 , 上面 ps 输出中的 PPID 就表示进程的父进程号 。最顶层的进程的 PID 为 1 , PPID 为 0 。
打开 iTerm , 在终端中执行一个命令 , 比如 "ls" , 实际上系统会创建新的 iTerm 子进程 , 这个 iTerm 进程又创建了 zsh 子进程 。在 zsh 中输入的 ls 命令 , 则是 zsh 进程又启动了一个 ls 子进程 。在 iTerm 中输入 ls 命令过程的进程关系如下所示 。
UIDPIDPPIDC STIMETTYTIME CMD50132110 六04下午 ??61:01.45 /Applications/iTerm.app/Contents/MacOS/iTerm2 -psn_0_81940501 9792032108:02上午 ttys0390:00.07 /Applications/iTerm.app/Contents/MacOS/iTerm2 --server login -fp arthur0 97921 9792008:02上午 ttys0390:00.03 login -fp arthur501 97922 9792108:02上午 ttys0390:00.29 -zsh501 98369 9792208:14上午 ttys0390:00.00 ./a.out
进程与 fork前面提到的父进程“创建”子进程 , 更严谨的描述是 fork(孵化、衍生) 。下面来看一个实际的例子 , 新建一个 fork_demo.c 文件 。#include <unistd.h>#include <stdio.h>int main() {int ret = fork();if (ret) {printf("enter if blockn");} else {printf("enter else blockn");}return 0;}
执行上的代码 , 会输出如下的语句 。enter if blockenter else block
可以看到 if、else 语句都被执行了 。fork 调用fork 是一个系统调用 , 它的方法声明如下所示 。
pid_t fork(void);
fork 调用完成后会生成一个新的子进程 , 且父子进程都从 fork 返回处继续执行 。这里需要特别注意的是 fork 的返回值的含义 , 在父进程和新的子进程中 , 它们的含义不一样 。- 在父进程中 fork 的返回值是新创建的子进程 id
- 在创建的子进程中 fork 的返回值始终等于 0
#include <unistd.h>#include <stdio.h>#include <stdlib.h>int main() {printf("before fork, pid=%dn", getpid());pid_t childPid;switch (childPid = fork()) {case -1: {// fork 失败printf("fork error, %dn", getpid());exit(1);}case 0: {// 子进程代码进入到这里printf("in child process, pid=%dn", getpid());break;}default: {// 父进程代码进入到这里printf("in parent process, pid=%d, child pid=%dn", getpid(), childPid);break;}}return 0;}
执行上面的代码 , 输出结果如下所示 。before fork, pid=26070in parent process, pid=26070, child pid=26071in child process, pid=26071
子进程是父进程的副本 , 子进程拥有父进程数据空间、堆、栈的复制副本 , fork 采用了 copy-on-write 技术 , fork 操作几乎瞬间可以完成 。只有在子进程修改了相应的区域才会进行真正的拷贝 。
推荐阅读
- 防冻液多久换一次?别等到发动机出故障后才知道
- 礼仪微课五分钟 如何上好一堂礼仪课
- 纱窗几年换一次,纱窗的使用寿命是多少年
- 空调管路杀菌怎么操作,空调管路杀菌多久一次
- 福建名菜佛跳墙,原来做法这么简单,汤汁醇厚,吃一次就念念不忘
- 使用docker部署golang服务
- 储水式电热水器怎么清洗,储水式电热水器多久保养一次
- 什么是Docker?与虚拟机有什么区别?
- |第一次当领导,如何说话才显得有威信?
- 推荐5款好用的开源 Docker 工具