02 | 理解进程(1):为什么我在容器中不能kill 1号进程

本文仅作为学习记录,非商业用途,侵删,如需转载需作者同意。

1、问题

容器中 kill -9 1为什么不起作用?
在这里插入图片描述

2、解释

2.1、如何理解init 进程

init 进程 就是 1号进程

Linux 操作系统中,打开电源执行BIOS/boot-loader之后,boot-loader 负责加载Linux 内核。

Linux内核执行文件一般放在 /boot 目录下,文件名类似 vmlinuxz* ,在内核完成了操作系统的各种初始化后。 这个程序需要执行的第一个用户态进程就是init 进程。

内核代码启动1号进程的时候,默认从几个缺省路径尝试执行1号进程的代码。

系统启动的时候先执行内核态代码,然后在内核中调用1号进程的代码,从内核态切换到用户态。

多数系统会把 /sbin/init 作为符合链接指向 Systemd ,Systemd是目前最流行的Linux init 进程。
在此之前还有 SysVinit、Upstart等Linux init进程。

Linux init 进程的基本功能:创建出Linux系统中其他所有进程,并且管理这些进程

kernel 中代码实现如下:
在这里插入图片描述

容器的 init 进程也被称为 1 号进程。

1 号进程是第一个用户态的进程,由它直接或者间接创建了 Namespace 中的其他进程

2.2、理解Linux 信号
/ # kill -l
 1) HUP
 2) INT
 3) QUIT
 4) ILL
 5) TRAP
 6) ABRT
 7) BUS
 8) FPE
 9) KILL
10) USR1
11) SEGV
12) USR2
13) PIPE
14) ALRM
15) TERM
16) STKFLT
17) CHLD
18) CONT
19) STOP
20) TSTP
21) TTIN
22) TTOU
23) URG
24) XCPU
25) XFSZ
26) VTALRM
27) PROF
28) WINCH
29) POLL
30) PWR
31) SYS
64) RTMAX
/ # 

信号(Signal):Linux进程收到的一个通知。
通知产生的源头,通知的类型都有很多种。

进程接收到信号后,有三种处理方式:
1、忽略(Ignore)
:对接收到的信号不做任何处理,有2个信号除外 SIGTERM(15) 和 SIGKILL(9) 。因为这2个信号的作用是为 Linux kernel 和 超级用户提供删除任意进程的特权。

2、捕获(Catch)
让用户进程可以注册自己针对这个信号的handler。
SIGKILL 和 SIGSTOP 例外,这两个信号不能有用户自己的处理代码,只能执行系统的缺省行为。

3、缺省行为(Default)
每个信号都有缺省的行为。
man 7 signal 来查看每个信号的缺省行为。

在这里插入图片描述

2.3、SIGTERM(15)和 SIGKILL(9)分析

kill 默认发的信号类型是SIGTERM。

SIGTERM 可以被捕获的,即用户进程可以为这个信号注册自己的 handler 。这个handler后面会看到,可以处理进程的 graceful-shutdown问题。

Linux 有2个特权信号,其中之一就是SIGKILL(9)

特权信号:Linux为kernel 和超级用户去删除任意进程所保留的,不能被忽略,也不能被捕获。进程一旦收到 SIGKILL 就要退出。

现象解释

用 bash 作为容器 1 号进程,这样是无法把 1 号进程杀掉的。那么我们再一起来看一看,用别的编程语言写的 1 号进程是否也杀不掉。

我们现在用 C 程序作为 init 进程,尝试一下杀掉 1 号进程。和 bash init 进程一样,无论 SIGTERM 信号还是 SIGKILL 信号,在容器里都不能杀死这个 1 号进程。

# cat c-init-nosig.c
#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
       printf("Process is sleeping\n");
       while (1) {
              sleep(100);
       }

       return 0;
}

# docker stop sig-proc;docker rm sig-proc
# docker run --name sig-proc -d registry/sig-proc:v1 /c-init-nosig
# docker exec -it sig-proc bash
[root@5d3d42a031b1 /]# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 07:48 ?        00:00:00 /c-init-nosig
root         6     0  5 07:48 pts/0    00:00:00 bash
root        19     6  0 07:48 pts/0    00:00:00 ps -ef
[root@5d3d42a031b1 /]# kill 1
[root@5d3d42a031b1 /]# kill -9 1
[root@5d3d42a031b1 /]# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 07:48 ?        00:00:00 /c-init-nosig
root         6     0  0 07:48 pts/0    00:00:00 bash
root        20     6  0 07:49 pts/0    00:00:00 ps -ef

用 Golang 程序作为 1 号进程,我们再在容器中执行 kill -9 1 和 kill 1 。

这次,我们发现 kill -9 1 这个命令仍然不能杀死 1 号进程,也就是说,SIGKILL 信号和之前的两个测试一样不起作用。

但是,我们执行 kill 1 以后,SIGTERM 这个信号把 init 进程给杀了,容器退出了。


# cat go-init.go
package main

import (
       "fmt"
       "time"
)

func main() {
       fmt.Println("Start app\n")
       time.Sleep(time.Duration(100000) * time.Millisecond)
}

# docker stop sig-proc;docker rm sig-proc
# docker run --name sig-proc -d registry/sig-proc:v1 /go-init
# docker exec -it sig-proc bash


[root@234a23aa597b /]# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  1 08:04 ?        00:00:00 /go-init
root        10     0  9 08:04 pts/0    00:00:00 bash
root        23    10  0 08:04 pts/0    00:00:00 ps -ef
[root@234a23aa597b /]# kill -9 1
[root@234a23aa597b /]# kill 1
[root@234a23aa597b /]# [~]# docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

为什么使用不同程序,结果就不一样呢?接下来我们就看看 kill 命令下达之后,Linux 里究竟发生了什么事,我给你系统地梳理一下整个过程。

在我们运行 kill 1 这个命令的时候,希望把 SIGTERM 这个信号发送给 1 号进程,就像下面图里的带箭头虚线。

在 Linux 实现里,kill 命令调用了 kill() 的这个系统调用(所谓系统调用就是内核的调用接口)而进入到了内核函数 sys_kill(), 也就是下图里的实线箭头。

而内核在决定把信号发送给 1 号进程的时候,会调用 sig_task_ignored() 这个函数来做个判断,这个判断有什么用呢?

它会决定内核在哪些情况下会把发送的这个信号给忽略掉。如果信号被忽略了,那么 init 进程就不能收到指令了。

所以,我们想要知道 init 进程为什么收到或者收不到信号,都要去看看 sig_task_ignored() 的这个内核函数的实现。

在这里插入图片描述
在 sig_task_ignored() 这个函数中有三个 if{}判断,第一个和第三个 if{}判断和我们的问题没有关系,并且代码有注释,我们就不讨论了。

我们重点来看第二个 if{}。我来给你分析一下,在容器中执行 kill 1 或者 kill -9 1 的时候,这第二个 if{}里的三个子条件是否可以被满足呢?

我们来看下面这串代码,这里表示一旦这三个子条件都被满足,那么这个信号就不会发送给进程。


kernel/signal.c
static bool sig_task_ignored(struct task_struct *t, int sig, bool force)
{
        void __user *handler;
        handler = sig_handler(t, sig);

        /* SIGKILL and SIGSTOP may not be sent to the global init */
        if (unlikely(is_global_init(t) && sig_kernel_only(sig)))

                return true;

        if (unlikely(t->signal->flags & SIGNAL_UNKILLABLE) &&
            handler == SIG_DFL && !(force && sig_kernel_only(sig)))
                return true;

        /* Only allow kernel generated signals to this kthread */
        if (unlikely((t->flags & PF_KTHREAD) &&
                     (handler == SIG_KTHREAD_KERNEL) && !force))
                return true;

        return sig_handler_ignored(handler, sig);
}

接下来,我们就逐一分析一下这三个子条件,我们来说说这个"!(force && sig_kernel_only(sig))" 。

第一个条件里 force 的值,对于同一个 Namespace 里发出的信号来说,调用值是 0,所以这个条件总是满足的。

我们再来看一下第二个条件 “handler == SIG_DFL”,第二个条件判断信号的 handler 是否是 SIG_DFL。

那么什么是 SIG_DFL 呢?对于每个信号,用户进程如果不注册一个自己的 handler,就会有一个系统缺省的 handler,这个缺省的 handler 就叫作 SIG_DFL。

对于 SIGKILL,我们前面介绍过它是特权信号,是不允许被捕获的,所以它的 handler 就一直是 SIG_DFL。这第二个条件对 SIGKILL 来说总是满足的。

对于 SIGTERM,它是可以被捕获的。也就是说如果用户不注册 handler,那么这个条件对 SIGTERM 也是满足的。

最后再来看一下第三个条件,“t->signal->flags & SIGNAL_UNKILLABLE”,这里的条件判断是这样的,进程必须是 SIGNAL_UNKILLABLE 的。

这个 SIGNAL_UNKILLABLE flag 是在哪里置位的呢?

可以参考我们下面的这段代码,在每个 Namespace 的 init 进程建立的时候,就会打上 SIGNAL_UNKILLABLE 这个标签,也就是说只要是 1 号进程,就会有这个 flag,这个条件也是满足的。


kernel/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 。Linux 内核针对每个 Nnamespace 里的 init 进程,把只有 default handler 的信号都给忽略了。

如果我们自己注册了信号的 handler(应用程序注册信号 handler 被称作"Catch the Signal"),那么这个信号 handler 就不再是 SIG_DFL 。即使是 init 进程在接收到 SIGTERM 之后也是可以退出的。

不过,由于 SIGKILL 是一个特例,因为 SIGKILL 是不允许被注册用户 handler 的(还有一个不允许注册用户 handler 的信号是 SIGSTOP),那么它只有 SIG_DFL handler。

所以 init 进程是永远不能被 SIGKILL 所杀,但是可以被 SIGTERM 杀死。

说到这里,我们该怎么证实这一点呢?我们可以做下面两件事来验证。

第一件事,你可以查看 1 号进程状态中 SigCgt Bitmap。

我们可以看到,在 Golang 程序里,很多信号都注册了自己的 handler,当然也包括了 SIGTERM(15),也就是 bit 15。

而 C 程序里,缺省状态下,一个信号 handler 都没有注册;bash 程序里注册了两个 handler,bit 2 和 bit 17,也就是 SIGINT 和 SIGCHLD,但是没有注册 SIGTERM。

所以,C 程序和 bash 程序里 SIGTERM 的 handler 是 SIG_DFL(系统缺省行为),那么它们就不能被 SIGTERM 所杀。

具体我们可以看一下这段 /proc 系统的进程状态:


### 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

第二件事,给 C 程序注册一下 SIGTERM handler,捕获 SIGTERM。

我们调用 signal() 系统调用注册 SIGTERM 的 handler,在 handler 里主动退出,再看看容器中 kill 1 的结果。

这次我们就可以看到,在进程状态的 SigCgt bitmap 里,bit 15 (SIGTERM) 已经置位了。同时,运行 kill 1 也可以把这个 C 程序的 init 进程给杀死了。


#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

void sig_handler(int signo)
{
    if (signo == SIGTERM) {
           printf("received SIGTERM\n");
           exit(0);
    }
}

int main(int argc, char *argv[])
{
    signal(SIGTERM, sig_handler);

    printf("Process is sleeping\n");
    while (1) {
           sleep(100);
    }
    return 0;
}

# 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             STATUS              PORTS               NAMES

结论:
1、kill -9 1 在容器中不工作,内核阻止了1号进程对SIGKILL 特权信号的响应
2、kill 1 分两种情况,如果1号进程没有注册SIGTERM 的 handler ,那么对SIGTERM 信号也不影响,如果注册了,那就可以响应 SIGTERM信号。

3、重点总结

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

信号的最终处理都是在Linux 内核中进行的

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

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值