为什么我在容器中不能 kill 1 号进程?

2024-02-03 15:25:48 浏览数 (1)

前言

使用容器的理想境界是一个容器只启动一个进程,现实中有时是做不到的。比如容器除了主进程外还启动辅助进程,做监控或者logs;再比如程序本身就是多进程的。

init进程

linux OS在打开电源,执行BIOS/boot-loader后,由boot-loader负责加载linux内核。完成内核初始化后,boot-loader需要执行的第一个用户态进程就是init进程。

init进程的基本功能就是创建出其他进程并管理它们。

而容器中也是由init进程直接或间接创建了Namespace中的其他进程。

linux信号

而为什么不能在容器中kill 1号进程呢?进程在收到信号后,就会去做相应的处理。

  • 第一个选择是忽略这个信号,但有两个信号例外:SIGKILL 和 SIGSTOP,进程不能忽略。它们的主要作用是为内核和超级用户提供删除任意进程的特权。
  • 第二个选择是捕获,指让用户进程可以注册自己针对这个信号的 handler。SIGKILL 和 SIGSTOP 也同样例外,不能有用户自己的处理代码,只能执行系统的缺省行为。
  • 最后一个选择是缺省行为(Default),Linux 为每个信号定义了一个缺省行为,对于大部分的信号而言,应用程序不需要注册自己的 handler,使用系统缺省定义行为即可。

SIGTERM(15)

由Linux命令kill缺省发出。如kill 1,通过kill向1号进程发送信号。在没有别的参数时这个信号类型默认为SIGTERM,是可以被捕获的

SIGKILL(9)

Linux 里两个特权信号之一,不能被忽略也不能被捕获。进程一旦收到 SIGKILL就要退出。运行命令 kill -9 1 里的参数“-9”,就是指发送编号为 9 的这个 SIGKILL 信号给 1 号进程。

为什么在容器中不能kill 1号进程?

对于不同的程序,结果是不同的。把c程序作为1号进程就无法在容器中杀死,而go程序作为1号进程却可以。

运行 kill 1 时,希望把 SIGTERM 发送给 1 号进程,就像下图中带箭头虚线。

在 Linux 中,kill 命令调用了 kill() 系统调用(内核的调用接口)而进入到了内核函数 sys_kill()。而内核在决定把信号发送给 1 号进程时会调用 sig_task_ignored() 函数进行判断,它会决定内核在哪些情况下会把发送的这个信号给忽略掉。如果信号被忽略了,那么 init 进程就不能收到指令了。

想要知道 init 进程为什么收到或者收不到信号,就要去看 sig_task_ignored()的实现。

问题和第二个if语句有关,一旦这三个子条件都被满足,那么信号就不会发送给进程。

  • !(force && sig_kernel_only(sig)):如果是同一个Namespace发出的信号,值为0。所以这个条件总是满足。
  • handler == SIG_DFL:判断信号的handler是否为SIG_DFL(default handler)。SIGKILL不允许捕获,handler一直是SIG_DFL,该条件总是满足。SIGTERM可捕获,不一定满足。
  • t->signal->flags & SIGNAL_UNKILLABLE:进程必须是GINAL_UNKILLABLE的,在每个namespace的init进程建立时就会打上这个标签。

可以看出最关键的一点就是 handler == SIG_DFL 。Linux 内核针对每个 Namespace 里的 init 进程,把只有 default handler 的信号都给忽略了。

如果我们自己注册了信号的 handler,那么即使是 init 进程在接收到 SIGTERM 之后也是可以退出的。所以 init 进程是永远不能被 SIGKILL 所杀,但可以被 SIGTERM 杀死。

该怎么证实这一点呢?

  1. 查看 1 号进程状态中 SigCgt Bitmap。在 Go 程序里,很多信号都注册了自己的 handler,包括 SIGTERM(15),也就是 bit 15。而 C 程序里缺省状态下,一个信号 handler 都没有注册;bash 程序注册了两个 handler,bit 2 和 bit 17,也就是 SIGINT 和 SIGCHLD,但是没有注册 SIGTERM。所以C 程序和 bash 程序不能被 SIGTERM 所杀。
代码语言:javascript复制
### golang init 
# cat /proc/1/status | grep -i 
SigCgt SigCgt: fffffffe7fc1feff 

### C init 
# cat /proc/1/status | grep -i SigCgt 
SigCgt: 0000000000000000

### Bash init 
# cat /proc/1/status | grep -i SigCgt 
SigCgt: 0000000000010002

  1. 给c程序注册SIGTERM handler,捕获SIGTERM
代码语言:javascript复制
# docker stop sig-proc;docker rm sig-proc 
# docker run --name sig-proc -d registry/sig-proc:v1 /c-init-sig 
# docker exec -it sig-proc bash [root@043f4f717cb5 /]
# ps -ef 
UID PID PPID C STIME TTY TIME CMD
root 1   0   0 09:05 ? 00:00:00 /c-init-sig 
root 6   0  18 09:06 pts/0 00:00:00 bash 
root 19  6   0 09:06 pts/0 00:00:00 ps -ef 
[root@043f4f717cb5 /]# cat /proc/1/status | grep SigCgt 
SigCgt: 0000000000004000 
[root@043f4f717cb5 /]# kill 1 # docker ps CONTAINER ID IMAGE COMMAND CREATED

重点总结

“为什么我在容器中不能 kill 1 号进程?”。解决这个问题需要掌握两个基本概念。

  • Linux 1 号进程。它是第一个用户态的进程。它直接或者间接创建了 Namespace 中的其他进程。
  • Linux 信号。Linux 有 31 个基本信号,进程在处理大部分信号时有三个选择:忽略、捕获和缺省行为。其中两个特权信号 SIGKILL 和 SIGSTOP 不能被忽略或者捕获。

我们尝试了用 bash, C 还有 Go 程序作为容器 init 进程,发现它们对 kill 1 的反应是不同的。因为信号的最终处理都是在 Linux 内核中进行的,因此,我们需要对 Linux 内核代码进行分析。

  1. 容器里 1 号进程对信号处理的两个要点:
  2. 在容器中,1 号进程永远不会响应 SIGKILL 和 SIGSTOP 这两个特权信号;对于其他的信号,如果用户自己注册了 handler,1 号进程可以响应。

0 人点赞