04 | 理解进程(3):为什么我在容器中的进程被强制杀死了?

你好,我是程远。

今天我们来讲容器中init进程的最后一讲,为什么容器中的进程被强制杀死了。理解了这个问题,能够帮助你更好地管理进程,让容器中的进程可以graceful shutdown。

我先给你说说,为什么进程管理中做到这点很重要。在实际生产环境中,我们有不少应用在退出的时候需要做一些清理工作,比如清理一些远端的链接,或者是清除一些本地的临时数据。

这样的清理工作,可以尽可能避免远端或者本地的错误发生,比如减少丢包等问题的出现。而这些退出清理的工作,通常是在SIGTERM这个信号用户注册的handler里进行的。

但是,如果我们的进程收到了SIGKILL,那应用程序就没机会执行这些清理工作了。这就意味着,一旦进程不能graceful shutdown,就会增加应用的出错率。

所以接下来,我们来重现一下,进程在容器退出时都发生了什么。

场景再现

在容器平台上,你想要停止一个容器,无论是在Kubernetes中去删除一个pod,或者用Docker停止一个容器,最后都会用到Containerd这个服务。

而Containerd在停止容器的时候,就会向容器的init进程发送一个SIGTERM信号。

我们会发现,在init进程退出之后,容器内的其他进程也都立刻退出了。不过不同的是,init进程收到的是SIGTERM信号,而其他进程收到的是SIGKILL信号。

在理解进程的第一讲中,我们提到过SIGKILL信号是不能被捕获的(catch)的,也就是用户不能注册自己的handler,而SIGTERM信号却允许用户注册自己的handler,这样的话差别就很大了。

那么,我们就一起来看看当容器退出的时候,如何才能让容器中的进程都收到SIGTERM信号,而不是SIGKILL信号。

延续前面课程中处理问题的思路,我们同样可以运行一个简单的容器,来重现这个问题,用这里的代码执行一下 make image ,然后用Docker启动这个容器镜像。

docker run -d --name fwd_sig registry/fwd_sig:v1 /c-init-sig

你会发现,在我们用 docker stop 停止这个容器的时候,如果用strace工具来监控,就能看到容器里的init进程和另外一个进程收到的信号情况。

在下面的例子里,进程号为15909的就是容器里的init进程,而进程号为15959的是容器里另外一个进程。

在命令输出中我们可以看到,init进程(15909)收到的是SIGTERM信号,而另外一个进程(15959)收到的果然是SIGKILL信号。

# ps -ef | grep c-init-sig
root     15857 14391  0 06:23 pts/0    00:00:00 docker run -it registry/fwd_sig:v1 /c-init-sig
root     15909 15879  0 06:23 pts/0    00:00:00 /c-init-sig
root     15959 15909  0 06:23 pts/0    00:00:00 /c-init-sig
root     16046 14607  0 06:23 pts/3    00:00:00 grep --color=auto c-init-sig

# strace -p 15909
strace: Process 15909 attached
restart_syscall(<... resuming interrupted read ...>) = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=0, si_uid=0} ---
write(1, "received SIGTERM\n", 17)      = 17
exit_group(0)                           = ?
+++ exited with 0 +++

# strace -p 15959
strace: Process 15959 attached
restart_syscall(<... resuming interrupted read ...>) = ?
+++ killed by SIGKILL +++

知识详解:信号的两个系统调用

我们想要理解刚才的例子,就需要搞懂信号背后的两个系统调用,它们分别是kill()系统调用和signal()系统调用。

这里呢,我们可以结合前面讲过的信号来理解这两个系统调用。在容器init进程的第一讲里,我们介绍过信号的基本概念了,信号就是Linux进程收到的一个通知。

等你学完如何使用这两个系统调用之后,就会更清楚Linux信号是怎么一回事,遇到容器里信号相关的问题,你就能更好地理清思路了。

我还会再给你举个使用函数的例子,帮助你进一步理解进程是如何实现graceful shutdown的。

进程对信号的处理其实就包括两个问题,一个是进程如何发送信号,另一个是进程收到信号后如何处理。

我们在Linux中发送信号的系统调用是kill(),之前很多例子里面我们用的命令 kill ,它内部的实现就是调用了kill()这个函数。

下面是Linux Programmer’s Manual里对kill()函数的定义。

这个函数有两个参数,一个是 sig,代表需要发送哪个信号,比如sig的值是15的话,就是指发送SIGTERM;另一个参数是 pid,也就是指信号需要发送给哪个进程,比如值是1的话,就是指发送给进程号是1的进程。

NAME
       kill - send signal to a process

SYNOPSIS
       #include <sys/types.h>
       #include <signal.h>

       int kill(pid_t pid, int sig);

我们知道了发送信号的系统调用之后,再来看另一个系统调用,也就是signal()系统调用这个函数,它可以给信号注册handler。

下面是signal()在Linux Programmer’s Manual里的定义,参数 signum 也就是信号的编号,例如数值15,就是信号SIGTERM;参数 handler 是一个函数指针参数,用来注册用户的信号handler。

NAME
       signal - ANSI C signal handling

SYNOPSIS
       #include <signal.h>
       typedef void (*sighandler_t)(int);
       sighandler_t signal(int signum, sighandler_t handler);

在容器init进程的第一讲里,我们学过进程对每种信号的处理,包括三个选择:调用系统缺省行为、捕获、忽略。而这里的选择,其实就是程序中如何去调用signal()这个系统调用。

第一个选择就是缺省,如果我们在代码中对某个信号,比如SIGTERM信号,不做任何signal()相关的系统调用,那么在进程运行的时候,如果接收到信号SIGTERM,进程就会执行内核中SIGTERM信号的缺省代码。

对于SIGTERM这个信号来说,它的缺省行为就是进程退出(terminate)。

内核中对不同的信号有不同的缺省行为,一般会采用退出(terminate),暂停(stop),忽略(ignore)这三种行为中的一种。

那第二个选择捕获又是什么意思呢?

捕获指的就是我们在代码中为某个信号,调用signal()注册自己的handler。这样进程在运行的时候,一旦接收到信号,就不会再去执行内核中的缺省代码,而是会执行通过signal()注册的handler。

比如下面这段代码,我们为SIGTERM这个信号注册了一个handler,在handler里只是做了一个打印操作。

那么这个程序在运行的时候,如果收到SIGTERM信号,它就不会退出了,而是只在屏幕上显示出"received SIGTERM"。

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

int main(int argc, char *argv[])

{
...
  signal(SIGTERM, sig_handler);
...
}

我们再来看看第三个选择,如果要让进程“忽略”一个信号,我们就要通过signal()这个系统调用,为这个信号注册一个特殊的handler,也就是 SIG_IGN

比如下面的这段代码,就是为SIGTERM这个信号注册SIG_IGN

这样操作的效果,就是在程序运行的时候,如果收到SIGTERM信号,程序既不会退出,也不会在屏幕上输出log,而是什么反应也没有,就像完全没有收到这个信号一样。

int main(int argc, char *argv[])
{
...
  signal(SIGTERM, SIG_IGN);
...
}

好了,我们通过讲解signal()这个系统调用,帮助你回顾了信号处理的三个选择:缺省行为、捕获和忽略。

这里我还想要提醒你一点, SIGKILL和SIGSTOP信号是两个特权信号,它们不可以被捕获和忽略,这个特点也反映在signal()调用上。

我们可以运行下面的这段代码,如果我们用signal()为SIGKILL注册handler,那么它就会返回SIG_ERR,不允许我们做捕获操作。

# cat reg_sigkill.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <signal.h>

typedef void (*sighandler_t)(int);

void sig_handler(int signo)
{
            if (signo == SIGKILL) {
                        printf("received SIGKILL\n");
                        exit(0);
            }
}
 
int main(int argc, char *argv[])
{
            sighandler_t h_ret;

            h_ret = signal(SIGKILL, sig_handler);
            if (h_ret == SIG_ERR) {
                        perror("SIG_ERR");
            }
            return 0;
}

# ./reg_sigkill
SIG_ERR: Invalid argument

最后,我用下面这段代码来做个小结。

这段代码里,我们用signal()对SIGTERM这个信号做了忽略,捕获以及恢复它的缺省行为,并且每一次都用kill()系统调用向进程自己发送SIGTERM信号,这样做可以确认进程对SIGTERM信号的选择。

#include <stdio.h>
#include <signal.h>

typedef void (*sighandler_t)(int);

void sig_handler(int signo)
{
        if (signo == SIGTERM) {
                printf("received SIGTERM\n\n");
                // Set SIGTERM handler to default
                signal(SIGTERM, SIG_DFL);
        }
}

int main(int argc, char *argv[])
{
        //Ignore SIGTERM, and send SIGTERM
        // to process itself.

        signal(SIGTERM, SIG_IGN);
        printf("Ignore SIGTERM\n\n");
        kill(0, SIGTERM);

        //Catch SIGERM, and send SIGTERM
        // to process itself.
        signal(SIGTERM, sig_handler);
        printf("Catch SIGTERM\n");
        kill(0, SIGTERM);

 
        //Default SIGTERM. In sig_handler, it sets
        //SIGTERM handler back to default one.
        printf("Default SIGTERM\n");
        kill(0, SIGTERM);

        return 0;
}

我们一起来总结一下刚才讲的两个系统调用:

先说说kill()这个系统调用,它其实很简单,输入两个参数:进程号和信号,就把特定的信号发送给指定的进程了。

再说说signal()这个调用,它决定了进程收到特定的信号如何来处理,SIG_DFL参数把对应信号恢复为缺省handler,也可以用自定义的函数作为handler,或者用SIG_IGN参数让进程忽略信号。

对于SIGKILL信号,如果调用signal()函数,为它注册自定义的handler,系统就会拒绝。

解决问题

我们在学习了kill()和signal()这个两个信号相关的系统调用之后,再回到这一讲最初的问题上,为什么在停止一个容器的时候,容器init进程收到的SIGTERM信号,而容器中其他进程却会收到SIGKILL信号呢?

当Linux进程收到SIGTERM信号并且使进程退出,这时Linux内核对处理进程退出的入口点就是do_exit()函数,do_exit()函数中会释放进程的相关资源,比如内存,文件句柄,信号量等等。

Linux内核对处理进程退出的入口点就是do_exit()函数,do_exit()函数中会释放进程的相关资源,比如内存,文件句柄,信号量等等。

在做完这些工作之后,它会调用一个exit_notify()函数,用来通知和这个进程相关的父子进程等。

对于容器来说,还要考虑Pid Namespace里的其他进程。这里调用的就是 zap_pid_ns_processes()这个函数,而在这个函数中,如果是处于退出状态的init进程,它会向Namespace中的其他进程都发送一个SIGKILL信号。

整个流程如下图所示。

你还可以看一下,内核代码是这样的。

    /*
         * The last thread in the cgroup-init thread group is terminating.
         * Find remaining pid_ts in the namespace, signal and wait for them
         * to exit.
         *
         * Note:  This signals each threads in the namespace - even those that
         *        belong to the same thread group, To avoid this, we would have
         *        to walk the entire tasklist looking a processes in this
         *        namespace, but that could be unnecessarily expensive if the
         *        pid namespace has just a few processes. Or we need to
         *        maintain a tasklist for each pid namespace.
         *
         */

        rcu_read_lock();
        read_lock(&tasklist_lock);
        nr = 2;
        idr_for_each_entry_continue(&pid_ns->idr, pid, nr) {
                task = pid_task(pid, PIDTYPE_PID);
                if (task && !__fatal_signal_pending(task))
                        group_send_sig_info(SIGKILL, SEND_SIG_PRIV, task, PIDTYPE_MAX);
        }

说到这里,我们也就明白为什么容器init进程收到的SIGTERM信号,而容器中其他进程却会收到SIGKILL信号了。

前面我讲过,SIGKILL是个特权信号(特权信号是Linux为kernel和超级用户去删除任意进程所保留的,不能被忽略也不能被捕获)。

所以进程收到这个信号后,就立刻退出了,没有机会调用一些释放资源的handler之后,再做退出动作。

而SIGTERM是可以被捕获的,用户是可以注册自己的handler的。因此,容器中的程序在stop container的时候,我们更希望进程收到SIGTERM信号而不是SIGKILL信号。

那在容器被停止的时候,我们该怎么做,才能让容器中的进程收到SIGTERM信号呢?

你可能已经想到了,就是让容器init进程来转发SIGTERM信号。的确是这样,比如Docker Container里使用的tini作为init进程,tini的代码中就会调用sigtimedwait()这个函数来查看自己收到的信号,然后调用kill() 把信号发给子进程。

我给你举个具体的例子说明,从下面的这段代码中,我们可以看到除了SIGCHLD这个信号外,tini会把其他所有的信号都转发给它的子进程。

 int wait_and_forward_signal(sigset_t const* const parent_sigset_ptr, pid_t const child_pid) {

        siginfo_t sig;

        if (sigtimedwait(parent_sigset_ptr, &sig, &ts) == -1) {
                switch (errno) {
…
                }
        } else {
                /* There is a signal to handle here */
                switch (sig.si_signo) {
                        case SIGCHLD:
                                /* Special-cased, as we don't forward SIGCHLD. Instead, we'll
                                 * fallthrough to reaping processes.
                                 */
                                PRINT_DEBUG("Received SIGCHLD");
                                break;
                        default:
                                PRINT_DEBUG("Passing signal: '%s'", strsignal(sig.si_signo));
                                /* Forward anything else */
                                if (kill(kill_process_group ? -child_pid : child_pid, sig.si_signo)) {
                                        if (errno == ESRCH) {
                                                PRINT_WARNING("Child was dead when forwarding signal");
                                        } else {
                                                PRINT_FATAL("Unexpected error when forwarding signal: '%s'", strerror(errno));

                                                return 1;
                                        }
                                }
                                break;
                }
        }
        return 0;
}

那么我们在这里明确一下,怎么解决停止容器的时候,容器内应用程序被强制杀死的问题呢?

解决的方法就是在容器的init进程中对收到的信号做个转发,发送到容器中的其他子进程,这样容器中的所有进程在停止时,都会收到SIGTERM,而不是SIGKILL信号了。

重点小结

这一讲我们要解决的问题是让容器中的进程,在容器停止的时候,有机会graceful shutdown,而不是收到SIGKILL信号而被强制杀死。

首先我们通过对kill()和signal()这个两个系统调用的学习,进一步理解了进程是怎样处理Linux信号的,重点是信号在接收处理的三个选择:忽略,捕获和缺省行为

通过代码例子,我们知道SIGTERM是可以被忽略和捕获的,但是SIGKILL是不可以被忽略和捕获的。

了解这一点以后,我们就找到了问题的解决方向,也就是我们需要在停止容器时,让容器中的应用收到SIGTERM,而不是SIGKILL。

具体怎么操作呢?我们可以在容器的init进程中对收到的信号做个转发,发送到容器中的其他子进程。这样一来,容器中的所有进程在停止容器时,都会收到SIGTERM,而不是SIGKILL信号了。

我认为,解决init进程信号的这类问题其实并不难。

我们只需要先梳理一下和这个问题相关的几个知识点,再写个小程序,让它跑在容器里,稍微做几个试验。然后,我们再看一下内核和Docker的源代码,就可以很快得出结论了。

思考题

请你回顾一下基本概念中最后的这段代码,你可以想一想,在不做编译运行的情况下,它的输出是什么?

#include <stdio.h>
#include <signal.h>

typedef void (*sighandler_t)(int);

void sig_handler(int signo)
{
        if (signo == SIGTERM) {
                printf("received SIGTERM\n\n");
                // Set SIGTERM handler to default
                signal(SIGTERM, SIG_DFL);
        }
}

int main(int argc, char *argv[])
{
        //Ignore SIGTERM, and send SIGTERM
        // to process itself.

        signal(SIGTERM, SIG_IGN);
        printf("Ignore SIGTERM\n\n");
        kill(0, SIGTERM);

        //Catch SIGERM, and send SIGTERM
        // to process itself.
        signal(SIGTERM, sig_handler);
        printf("Catch SIGTERM\n");
        kill(0, SIGTERM);

 
        //Default SIGTERM. In sig_handler, it sets
        //SIGTERM handler back to default one.
        printf("Default SIGTERM\n");
        kill(0, SIGTERM);

        return 0;
}

欢迎留言和我分享你的想法和疑问。如果读完这篇文章有所收获,也欢迎你分享给自己的朋友,共同学习和进步。

精选留言

  • 胖胖虎

    2020-11-30 22:00:45

    简单总结了下,子进程被kill杀死的原因是,父进程在退出时,执行do_exit中,由于是cgroup_init 组的进程,因此向所有的子进程发送了sigkill信号。而导致这个的原因是,一般情况下,容器起来的第一个进程都不是专业的init进程,没有考虑过这些细节问题。由于正常情况下,父进程被终结,信号不会传递到子进程,exit时也不会给子进程发终结命令。这会导致多进程容器在关闭时,无法被终止。为了保证容器能够被正常终结。设计者在do_exit中做文章,使用sigkill这个不可屏蔽信号,而是为了能够在没有任何前提条件的情况下,能够把容器中所有的进程关掉。而一个优雅的解决方法是,使用一个专业的init进程作为容器的第一个进程,来处理相关业务。实现容器的优雅关闭。当然,如果子进程也把SigTerm做了劫持,那也是有可能导致容器无法关闭。
    作者回复

    @胖胖虎, 很好的总结!

    2020-12-01 23:02:37

  • JianXu

    2020-11-28 17:47:44

    CY , 能帮忙解释一下我们公司生产环境在容器image patching 过程中应用程序受影响的事情吗。

    1. 我们的胖容器肯定是多进程的,那当容器收到kill 命令的时候,我们现在也是子容器都被SIGKill 吗?还是我们其实都是配置了Init 进程,而init 进程其实都像文中说的转发了 SIGTERM 命令?

    2. 如果应用程序写的不够好,不相应SIGTERM 命令。所以我们才在一段时间容器还没有被杀死的情况下执行 Kill -9 吗?

    3. 我们大部分的应用程序都是web 程序,使用标准JVM , 比如 Tomcat 加 OpenJDK , 不大明白为什么不能正常响应SIGTERM 做graceful shutdown 。 Kubernetes 标准操作,当我们做OS patching的时候都是换image 的,这时候当前POD 会被干掉,我们是那个POD 因为不能响应SIGTERM 而始终处于terminating 吗?
    作者回复

    @JianXu,
    你说的情况是这样的,
    胖容器的init进程其实是一个bash脚本run.sh, 由它来启动jvm的程序。
    但是run.sh本身没有注册SIGTERM handler, 也不forward SIGTERM给子进程jvm。
    当stop容器的时候,run.sh先收到一个SIGTERM, run.sh没有注册SIGTERM, 所以呢对SIGTERM没有反应,contaienrd过30秒,会发SIGKILL给run.sh, 这样run.sh退出do_exit(),在退出的时候同样给子进程jvm程序发送了SIGKILL而不是SIGTERM。其实呢,jvm的程序是注册了SIGTERM handler的,但是没有机会调用handler了。

    2020-11-29 17:16:09

  • po

    2020-11-25 10:20:39

    老师,我做了个测试,现象有点迷惑,我打开两个终端,用sleep进行测试,方法和现象如下:
    1. 在第一个终端启动sleep,在另外一个终端通过命令去kill,能通过sigterm正常杀掉进程。
    # strace sleep 30000
    execve("/usr/bin/sleep", ["sleep", "30000"], [/* 25 vars */]) = 0
    ................................................................................................
    --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=1505, si_uid=0} ---
    +++ killed by SIGTERM +++

    2. 启动一个容器里面的命令是sleep 30000,用strace跟踪进程,我使用kill,杀不掉sleep进程,然后通过docker stop发现,先是发送sigterm信号,没有成功,最后被强制杀掉了:
    # strace -p 2207
    strace: Process 2207 attached
    restart_syscall(<... resuming interrupted nanosleep ...>) = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
    --- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=0, si_uid=0} ---
    restart_syscall(<... resuming interrupted restart_syscall ...>
    ) = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
    --- SIGWINCH {si_signo=SIGWINCH, si_code=SI_USER, si_pid=0, si_uid=0} ---
    restart_syscall(<... resuming interrupted restart_syscall ...>
    ) = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
    +++ killed by SIGKILL +++

    我有点迷惑,老师能解释一下为什么在宿主机或者用docker不能用sigterm来杀死容器的进程吗?
    作者回复

    @po,
    对于第二个问题,我假设sleep进程在宿主机上的pid是2207, 你还是可以先查看"cat /proc/2207/status | grep SigCgt", 我的理解是SIGTERM handler应该还是没有注册,那么即使从宿主机上发送SIGTERM给这个容器里的1号进程,那么也是不能杀死的。

    "docker stop"在停止容器的时候,先给容器里的1号进程发送SIGTERM, 如果不起作用,那么等待30秒后会发送SIGKILL。我想这个是你看到的现象了。

    至于为什么即使在宿主机机上向容器1号进程发送SIGTERM,在1号进程没有注册handler的情况下,不能被杀死的问题 (思考题), 原因是这样的:

    开始要看内核里的那段代码,“ !(force && sig_kernel_only(sig))”,
    虽然由不同的namespace发送信号, 虽然force是1了,但是sig_kernel_only(sig)对于SIGTERM来说还是0, 这里是个&&, 那么 !(1 && 0) = 1。

    #define sig_kernel_only(sig) siginmask(sig, SIG_KERNEL_ONLY_MASK)
    #define SIG_KERNEL_ONLY_MASK (\
    rt_sigmask(SIGKILL) | rt_sigmask(SIGSTOP))

    2020-11-25 14:07:29

  • JianXu

    2020-11-28 17:28:18

    老师,这里的逻辑我还没有理顺。

    1. 你说的容器init 进程,是不是就是容器的第一个进程?还有是不是如果我使用docker , 容器的第一个进程一定不是我自己的进程,而是tini 进程?

    2. 上文所SIGTerm 发送后,触发do exit 函数,SIGkill 其实是在内核往容器内的其他子进程发送的。那当我在init 进程配置了Sig term handler 截取信号转发sigterm 以后,do exit 函数还会被调用吗?如果不被调用,do exit 里其他的退出逻辑怎么被执行呢?如果被调用,怎么就不执行sigkill 了呢?
    作者回复

    > 1
    是的, init进程就是容器里的第一个进程。容器里的第一个进程大部分情况应该是我们自己的进程,除非有容器用户有意识的去使用tini进程作为init进程。

    > 2
    很好的问题。
    init 进程自己退出,还是会调用do_exit()的。所以呢,为了保证子进程先收到转发的SIGTERM, 类似tini的做法是,自己在收到SIGTERM的时候不退出,转发SIGTERM给子进程,子进程收到SIGTERM退出之后会给父进程发送SIGCHILD, tini是收到SIGCHILD之后主动整个程序退出。

    2020-11-29 17:04:52

  • po

    2020-11-25 10:37:13

    老师,我能提个建议吗?这几天学习容器进程和信号相关的知识点,有点乱,自己理出来也好像怪怪的,你能不能画个图,把进程的信号相关的给我们捋一遍呢?还有我们程序代码该如何更好的设计能给一点建议吗?感觉用tini这种方式改动有点大,之前我们一直都是应用程序作为PID1来运行的,好像也没啥问题。谢谢
    作者回复

    @po,
    谢谢你这几天提的问题,看的出来,你做了很多的测试,也有很多的思考!我们可以一起来,先把你前面的问题逐个理清了。

    你说的画图来捋一遍信号概念的,这个我会考虑的 (目前我需要先完成课程后面章节的内容 :-), 你也可以给我一些更详细的建议,可能可以和你问的具体问题想结合。

    > 之前我们一直都是应用程序作为PID1来运行的,好像也没啥问题

    信号对容器中进程的影响的多少,也有多方面的原因,比如程序本身对错误的容忍度比较高, 容器建立删除的频率不高,那么也就看不出有什么影响。

    如果你的程序的容器化程度较高,几乎是一个容器一个进程的程度,那么不需要考虑用tini来做改动。

    我觉得容器里的init进程,应该是具备这些信号处理的能力:
    1. 至少转发SIGTERM给容器里其他的关键子进程。
    2. 能够接受到外部的SIGTERM信号而退出,(这里可以是通过注册SIGTERM handler, 也可以像tini一样先转发SIGTERM 给子进程,然后收到SIGCHILD后自己主动退出)
    3. 具有回收zombie进程的能力。

    2020-11-25 23:45:18

  • 宝仔

    2020-11-25 08:59:38

    老师,容器的最佳实践一般都是一个容器即一个进程,一般如果按照这种做法,就只需要在应用程序进程中对sigterm信号做捕获并处理就行了吧,无需转发吧
    作者回复

    是的!

    2020-11-25 23:56:47

  • Alery

    2020-12-01 18:44:48

    老师,我有个疑问哈,tini没有注册SIGTERM,按照前面将的,内核是不会把这个信号发送给tini进程的,为啥它又能接收所有的信号(除了SIGHILD)并转发给子进程呢?我对这块的理解的不是很清晰,望指教。
    作者回复

    很好的问题!
    因为在tini里调用的sigtimedwait()系统调用,直接把发送给tini的信号先截了下来,这时候tini有没有SIGTERM的handler就没有关系了。

    2020-12-02 22:43:05

  • Alery

    2020-12-01 18:31:45

    老师,请教一个问题,tini 会把其他所有的信号都转发给它的子进程,假如我的子进程又创建了子进程(也就是tini的孙子进程),tini会把信号转发给孙子进程吗?
    作者回复

    我们可以从tini转发信号的代码看一下。如果 “kill_process_group” 没有设置, 为0时,这也是tini缺省的配置,那么SIGTERM只会转发给子进程,而子子进程就不会收到转发的SIGTERM。当子进程退出的时候,子子进程就会收到SIGKILL。

    而如果kill_process_group > 0的时候,同时子进程与子子进程在同一个process group的时候 (缺省fork出来的子进程会和父进程在同一个process group), 那么子子进程就会收到SIGTERM




    if (kill(kill_process_group ? -child_pid : child_pid, sig.si_signo))

    2020-12-01 21:58:06

  • Tendrun

    2020-12-27 13:54:35

    不太明白zap_pid_ns_processes()这个函数为啥是发送SIGKILL信号,不能设计成发送SIGTERM么,如果是term信号,岂不是就没有容器中子进程中收到sigkill信号的问题了么
    作者回复

    @Tendrun
    好问题!
    不过只有SIGKILL才可以强制杀进程。如果namespace中有进程忽略了SIGTERM,那么就会有进程残留了。

    2020-12-29 22:49:36

  • 上邪忘川

    2020-11-23 21:32:41

    可以用strace跟踪进程的信号,用法参考https://www.cnblogs.com/machangwei-8/p/10388883.html

    运维同学表示代码没看懂,哈哈
    作者回复

    @上邪忘川
    strace 主要用来查看程序调用了哪些系统调用已经收到什么信号。

    2020-11-24 11:57:10

  • po

    2020-11-24 18:37:21

    老师,如果通过tini转发信号给子进程,那么子子进程是收到子进程的信号吧?那么子子进程收到的信号是sigkill还是sigterm呢?
    作者回复

    @po 非常好的问题。
    我们可以从tini转发信号的代码看一下。如果 “kill_process_group” 没有设置, 为0时,这也是tini缺省的配置,那么SIGTERM只会转发给子进程,而子子进程就不会收到转发的SIGTERM。当子进程退出的时候,子子进程就会收到SIGKILL。

    而如果kill_process_group > 0的时候,同时子进程与子子进程在同一个process group的时候 (缺省fork出来的子进程会和父进程在同一个process group), 那么子子进程就会收到SIGTERM




    if (kill(kill_process_group ? -child_pid : child_pid, sig.si_signo))

    2020-11-24 22:57:57

  • Geek2014

    2020-11-23 14:59:06

    输出:
    Ignore SIGTERM

    Catch SIGTERM
    received SIGTERM

    Default SIGTERM
    作者回复

    Yes.

    2020-11-24 13:01:22

  • 庸俗且无趣

    2020-12-10 21:16:54

    想问下老师,那k8s里的优雅关闭选项是否就是做了这个操作
    作者回复

    k8s在delete pod的时候,通过containerd先向容器发送SIGTERM.
    这个graceful shutdown是需要容器中的进程自己来处理SIGTERM

    2020-12-20 23:12:48

  • Geek_0a698d

    2021-09-06 12:56:26

    看了老师的文章,大致能看懂,但是感觉又理解的不深刻,请问老师是否需要补一下Linux的基础,如果需要的话有书籍推荐吗?
  • Lemon

    2021-01-27 22:49:40

    老师,我有一个困惑,希望能得到您的解答:
    从文中的图上看,Linux 进程在收到 SIGTERM 信号进入 do_exit() 释放相关资源后,调用 exit_notify() 函数通知和这个进程相关的父子进程。这个流程在我看起来已经很完整了。
    init 进程就是容器的 1号进程,Kill 它不就可以触发资源回收,通过 exit_notify() 函数通知父子进程了,而对于普通容器进程也可以使用 Kill 命令退出,那为什么容器内的进程需要在 exit_notify() 函数后再调用 zap_pid_ns_processes() 函数呢?
    还是说,这里的意思是 Linux 中普通进程 KILL 中调用的是 exit_notify() 函数,而容器内的进程调用的是 zap_pid_ns_processes() 函数呢?
  • 小鱼

    2020-11-30 08:53:58

    我的容器的init进程是通过bash拉起的Linux守护进程,然后守护进程会创建子进程一个MySQL实例,为了优雅退出,我该如何改写init进程呢?
    作者回复

    @小鱼,
    你这里的“Linux守护进程”指的是mysqld吗?如果是只是想mysqld graceful shutdown, 可以用tini来启动mysqld, 不过还需要看你的bash里有没有做其他的工作。

    2020-12-01 16:17:34

  • 朱雯

    2020-11-24 14:57:08

    看完这篇课程后,兴冲冲的看生产环境的init进程是什么,有没有机会改成tini,结果一看,/bin/bash,我其实一头问号。
    作者回复

    你指的是你们的生产环境中,容器的init进程是/bin/bash?

    2020-11-24 22:59:21

  • 孟令泽

    2020-11-23 20:55:16

    老师,转发sigterm信号的代码有推荐吗?
    作者回复

    @孟令泽
    可以看一下tini的代码,我在“04 | 理解进程(3)”也有提到。

    2020-11-24 11:54:35

  • 谢哈哈

    2020-11-23 20:12:26

    如果就对这个启动的程序调用kill命令,那么会输出
    ignore SIGTERM
    catch SIGTERM
    recieved SIGTERM
    default SIGTERM

    如果发送一个其它的信号(除sigkill 或-9)
    那么
    输出
    defualt SIGTERM
  • 橙汁

    2025-03-02 17:03:04

    老师 你开个直播吧 我去刷点礼物