提出问题:
想修改容器镜像里的一个 bug,但是因为网络配置的问题,又不想为了重建 pod 而改变 pod IP。在 k8s 上没有 restart pod 这个指令。所以使用 kill pid 1
让容器原地重启。
测试想法:
使用 github 案例 Dockerfile 构建镜像。
使用 init.sh 作为容器的 init 进程,执行 kill 1 和 kill -9 1,发现容器并未停止。
1 | ahojliu@ubuntu:~/Downloads/training/init_proc/handle_sig$ docker run --name sig-proc -d sig-proc:v1 /init.sh |
使用 c 程序作为 init 进程,尝试上述操作,发现无法杀死这个 1 号进程。
1 | ahojliu@ubuntu:~/Downloads/training/init_proc/handle_sig$ docker run --name sig-proc-c -d sig-proc:v1 /c-init-nosig |
使用 go 程序作为 init 进程,再次执行上述操作,发现 kill -9 1 不能杀死,但是 kill 1 可以杀死进程,容器退出。
1 | ahojliu@ubuntu:~/Downloads/training/init_proc/handle_sig$ docker run --name sig-proc -d sig-proc:v1 /go-init |
干货:
kill 1
向 1 号进程发送了 SIGTERM(15) 信号;kill -9 1
向 1 号进程发送 SIGKILL(9) 信号;Ctrl+c
是 SIGINT(2) 信号。
查看信号编号和名称:kill -l
1 | ahojliu@ubuntu:~/Downloads/training/init_proc/handle_sig$ kill -l |
进程在收到信号后的处理方式:
1)忽略(Ignore),对于信号不作任何处理,但是除 SIGKILL 和 SIGSTOP 两个特权信号(这两个作用是为 kernel 和 超级用户提供删除任意进程的特权);
2)捕获(Catch),用户进程可以注册针对这个信号的 handler(特权信号不能有用户自己的处理代码,只能执行系统缺省行为);
3)缺省(Default),Linux 系统对每一个信号都定义了一个缺省行为(man 7 signal
查看)。
运行 kill 1
命令时候,Linux 调用 kill()
系统调用进入内核函数 sys_kill()
,内核会调用 sig_task_ignored()
函数来判断是否将信号发送给 1 号进程。sig_task_ignored()
实现如下:
1 | kernel/signal.c |
第一个 if:如果是 SIGKILL(9) 和 SIGSTOP(19) 并且发送给 init 进程就忽略。
第三个 if:仅允许内核生成的信号发送到 kthread。
重点是第二个 if:
!(force && sig_kernel_only(sig))
其中force
对于同一个 Namespace 发送的信号来说是 0,不同 Namespace 发出的是 1。handler==SIG_DFL
判断是否为系统缺省的 handler,用户如果不注册自己的 handler 此项为 true。t->signal->flags & SIGNAL_UNKILLABLE
进程必须是 UNKILLABLE 的,这个 flag 在每个 Namespace 的 init 进程建立的时候就会打上,也就是只要是 1 号进程就会有这个 flag(参考一下代码:kernel/fork.c)。总结:最关键一点就是1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17kernel/fork.c
if (is_child_reaper(pid)) {
ns_of_pid(pid)->child_reaper = p;
p->signal->flags |= SIGNAL_UNKILLABLE;
}
/*
* is_child_reaper returns true if the pid is the init process
* of the current namespace. As this one could be checked before
* pid_ns->child_reaper is assigned in copy_process, we check
* with the pid number.
*/
static inline bool is_child_reaper(struct pid *pid)
{
return pid->numbers[pid->level].nr == 1;
}handler==SIG_DFL
,所以 init 进程是永远不能被 SIGKILL 杀掉,但是可以被 SIGTERM 杀掉(SIGKILL 不允许注册 handler)。
查看进程注册了那些信号:cat /proc/<pid>/status | grep SigCgt
,掩码位解释:qastack.cn。
bash 程序注册了 2 个 handler,SIGINT(2) 和 SIGCHLD(17),但是没有注册 SIGTERM 所以 kill 1
杀不掉;
c 程序缺省状态下,没有注册任何信号的 handler,所以默认都被系统忽略;
go 程序中注册了很多 handler,包括了 SIGTERM(15) 所以可以被 kill 1
干掉。
给 c 程序增加 SIGTERM 的 handler,在 handler 中主动退出,运行 kill 1
如期:
1 |
|
测试:
1 | ahojliu@ubuntu:~$ docker exec -it 6b07b9b46a43 bash |
在宿主机上 kill <pid>
不可以杀掉容器 init 进程,!(force && sig_kernel_only(sig))
中 force 为 1 且 SIGTERM 不是内核信号,所以忽略;kill -9 <pid>
可以杀掉容器 init 进程。
1 | ahojliu@ubuntu:~/Downloads/training/init_proc/handle_sig$ docker ps |
但是其实在生产中很多普通用户是没有宿主机权限的。
EOF