前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >为什么无法用SIGTERM终止容器1号进程

为什么无法用SIGTERM终止容器1号进程

原创
作者头像
cdh
修改2023-08-15 08:12:46
6510
修改2023-08-15 08:12:46
举报
文章被收录于专栏:笔记+

kubernetes官网资料介绍在停止一个pod时会先发送SIGTERM给Pod各个容器的1号进程实现优雅退出,实际使用容器时会有用户没有关注到如果容器1号进程执行的程序或者脚本如果缺少注册SIGTERM信号handler会导致容器无法优雅退出,直到terminationGracePeriodSeconds时间到达后发送SIGKILL强制杀掉尚未退出的容器。这篇文章从内核实现机制分析为什么容器1号进程不注册SIGTERM信号handler会导致无法优雅停止容器。

为了模拟这个过程进行如下操作:

代码语言:javascript
复制
使用如下bash脚本作为容器的1号进程启动,脚本通过参数0和1控制脚本启动时是否注册SIGTERM信号handler:
# cat /test.sh
#!/bin/bash

# 定义一个名为sigterm_handler的函数
sigterm_handler() {
  echo "捕获到SIGTERM信号,正在退出..."
  exit 0
}

if [ "$#" -ne 1 ]; then
  echo "用法: $0 [0|1]"
  echo "0: 不注册SIGTERM handler"
  echo "1: 注册SIGTERM handler"
  exit 1
fi

if [ "$1" -eq 1 ]; then
  # 使用trap命令注册sigterm_handler函数,当接收到SIGTERM信号时执行
  trap 'sigterm_handler' SIGTERM
  echo "已注册SIGTERM handler"
else
  echo "未注册SIGTERM handler"
fi


echo "脚本正在运行,按Ctrl+C发送SIGINT信号,使用'kill -15 <PID>'发送SIGTERM信号"
while true; do
  sleep 1
done


先看下不注册SIGTERM handler的运行脚本的情况:
# docker ps | grep test-no-handler
e23e875616b5     test-sigterm:latest"/test.sh 0"     6 minutes ago       Up 6 minutes  test-no-handler

"/test.sh 0" 是容器test-no-handler的1号进程,对应到节点上的进程pid为2754618
[root@VM-0-20-centos ~]# docker inspect e23e875616b5 | grep -i pid
            "Pid": 2754618,
            "PidMode": "",
            "PidsLimit": null,

[root@VM-0-20-centos ~]# ps -elf | grep 2754618
4 S root     2754618 2754595  0  80   0 -  2237 do_wai 23:32 pts/0    00:00:00 /bin/bash /test.sh 0



在节点上对容器1号进程(对应节点pid=2754618)发送SIGTERM信号,容器1号进程并不会退出
[root@VM-0-20-centos ~]# kill -15 2754618
[root@VM-0-20-centos ~]# ps -elf | grep 2754618
4 S root     2754618 2754595  0  80   0 -  2237 do_wai 23:32 pts/0    00:00:00 /bin/bash /test.sh 0


[root@VM-0-20-centos ~]# docker ps | grep test-no-handler
e23e875616b5        test-sigterm:latest   "/test.sh 0"             10 minutes ago      Up 10 minutes                           test-no-handler
[root@VM-0-20-centos ~]#

接下来分析下这里的机制:

当我们通过kill给进程发送信号时,内核会通过如下调用路径来决定这个信号是否丢弃:

__send_signal->prepare_signal->sig_ignored->sig_task_ignored

首先我们先来看下sig_task_ignored这个函数什么情况会返回1

代码语言:javascript
复制
static int sig_task_ignored(struct task_struct *t, int sig, bool force)
{
        void __user *handler;

        handler = sig_handler(t, sig);

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

        return sig_handler_ignored(handler, sig);
}


函数sig_task_ignored如果要返回1,则这里的条件要成立:
if (unlikely(t->signal->flags & SIGNAL_UNKILLABLE) &&
            handler == SIG_DFL && !(force && sig_kernel_only(sig)))
                return 1;

 第一个条件“t->signal->flags & SIGNAL_UNKILLABLE”   的flags SIGNAL_UNKILLABLE 在创建进程时当进程是
 namespace的1号进程时会设置
 kernel/fork.c:
 struct task_struct *copy_process(
                                        unsigned long clone_flags,
                                        unsigned long stack_start,
                                        unsigned long stack_size,
                                        int __user *child_tidptr,
                                        struct pid *pid,
                                        int trace,
                                        unsigned long tls,
                                        int node)
 {
      .....
      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;
}

可以通过live crash来确认p->signal->flags是否为SIGNAL_UNKILLABLE:
#define SIGNAL_UNKILLABLE       0x00000040 /* for init: ignore fatal signals */

crash> bt 2754618
PID: 2754618  TASK: ffff88815f908000  CPU: 0   COMMAND: "test.sh"
 
crash>
crash> task_struct.signal ffff88815f908000
  signal = 0xffff888169e50000
crash> struct signal_struct.flags 0xffff888169e50000 -x
  flags = 0x40
crash> eval -b 0x40
hexadecimal: 40
    decimal: 64
      octal: 100
     binary: 0000000000000000000000000000000000000000000000000000000001000000
   bits set: 6
crash>



第二个条件 “handler == SIG_DFL”,第二个条件判断信号的handler是否是SIG_DFL。
对于每个信号,用户进程如果不注册一个自己的handler,就会有一个系统缺省的handler,
这个缺省的handler就叫作SIG_DFL: 

同样可以通过live crash来确认进程的SIGTERM信号handler为0,sig为15,因此对应action[sig - 1]为
t->sighand->action[14].sa.sa_handler
static void __user *sig_handler(struct task_struct *t, int sig)
{
        return t->sighand->action[sig - 1].sa.sa_handler;
}
 
crash> bt 2754618
PID: 2754618  TASK: ffff88815f908000  CPU: 2   COMMAND: "test.sh"

crash> task_struct.sighand ffff88815f908000 -x
  sighand = 0xffff888216fd0000

crash> struct sighand_struct.action[14] 0xffff888216fd0000
  action[14] =   {
    sa = {
      sa_handler = 0x0,
      sa_flags = 0,
      sa_restorer = 0x0,
      sa_mask = {
        sig = {0}
      }
    }
  },
crash>

通过/proc/2754618/status也可以确定bit14(对应15:SIGTERM)为0未注册SIGTERM信号handler:
# cat /proc/2754618/status | grep SigCgt
SigCgt: 0000000000010002 //16进制



第三个条件!(force && sig_kernel_only(sig) 这里sig_kernel_only(sig)对于SIGTERM信号返回值为0

#define siginmask(sig, mask) \
        ((sig) < SIGRTMIN && (rt_sigmask(sig) & (mask)))
#define SIG_KERNEL_ONLY_MASK (\
        rt_sigmask(SIGKILL)   |  rt_sigmask(SIGSTOP))
#define sig_kernel_only(sig)            siginmask(sig, SIG_KERNEL_ONLY_MASK)  

因此容器1号进程未注册handler的情况下这三个条件都成立,sig_task_ignored返回值1。

代码语言:javascript
复制

返回sig_task_ignored的上一级函数sig_ignored,通过live crash可以看到进程的t->ptrace为0,所以最终
返回的是sig_task_ignored的返回值:

crash> task_struct.ptrace ffff88815f908000
  ptrace = 0
crash>

static int sig_ignored(struct task_struct *t, int sig, bool force)
{
        /*
         * Blocked signals are never ignored, since the
         * signal handler may change by the time it is
         * unblocked.
         */
        if (sigismember(&t->blocked, sig) || sigismember(&t->real_blocked, sig))
                return 0;

        /*
         * Tracers may want to know about even ignored signal unless it
         * is SIGKILL which can't be reported anyway but can be ignored
         * by SIGNAL_UNKILLABLE task.
         */    
        if (t->ptrace && sig != SIGKILL)
                return 0;

        return sig_task_ignored(t, sig, force);
}


这里sig_ignored返回1,所以prepare_signal返回值为0
static bool prepare_signal(int sig, struct task_struct *p, bool force)
{
    ....
    return !sig_ignored(p, sig, force);
    ....
}

因此当!prepare_signal条件为1时__send_signal直接goto到ret:位置返回跳过了给目标进程发送信号的逻辑:
static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
                        int group, int from_ancestor_ns)
{
    ....
    result = TRACE_SIGNAL_IGNORED; //TRACE_SIGNAL_IGNORED值为1
        if (!prepare_signal(sig, t,
                        from_ancestor_ns || (info == SEND_SIG_PRIV) || (info == SEND_SIG_FORCED)))
                goto ret;

    ....
    
    ....
    out_set:
        signalfd_notify(t, sig);
        sigaddset(&pending->signal, sig);
        complete_signal(sig, t, group);
    ret:
        trace_signal_generate(sig, info, t, group, result);
        return ret;
}

从__send_signal函数可以看到函数退出前会调用trace_signal_generate调用trace点,因此可以通过perf trace来跟踪:

代码语言:javascript
复制
起一个终端,该终端负责执行kill -15给容器1号进程发送SIGTERM信号,先获取下该终端的进程pid
[root@VM-0-20-centos ~]# echo $$
3492032

再另外一个终端执行perf trace跟中pid 3492032发送的信号:
#perf trace -e signal:signal_generate --pid=3492032

回到进程ID为3492032的bash终端,执行kill -15给容器1号进程发送SIGTERM信号
[root@VM-0-20-centos ~]# echo $$
3492032
[root@VM-0-20-centos ~]#
[root@VM-0-20-centos ~]# ps -elf | grep test.sh | grep -v grep
4 S root     2754618 2754595  0  80   0 -  2237 do_wai Aug10 pts/0    00:00:11 /bin/bash /test.sh 0
[root@VM-0-20-centos ~]#
[root@VM-0-20-centos ~]# kill -15 2754618
[root@VM-0-20-centos ~]#

这时在执行perf trace的终端可以看到如下心信息,因为
trace_signal_generate(sig, info, t, group, result)的第四个参数result值为res=1,1对应的是
TRACE_SIGNAL_IGNORED就是忽略信号。也就是发送给容器1号进程(对应节点pid=2754618)被内核drop掉了,
容器1号进程并不会收到这个信号:
[root@VM-0-20-centos ~]# perf trace -e signal:signal_generate --pid=3492032
 84645.140 signal:signal_generate:sig=15 errno=0 code=0 comm=test.sh pid=2754618 grp=1 res=1

有人可能会疑惑既然内核drop了这个信号,为啥用strace跟踪容器的1号进程时还能捕获到SIGTERM信号发送给容器1号进程?

--- SIGTERM {si_signo=SIGTERM, si_code=SI_USER, si_pid=0, si_uid=0} ---

这里的原因是因为当对一个进程做了strace后,会把进程task_struct.ptrace设置为1,我们再看下前面提到的sig_ignored

函数会有if(t->ptrace && sig !=SIGKILL)的判断逻辑:

代码语言:javascript
复制


static int sig_ignored(struct task_struct *t, int sig, bool force)
{
      ....
        /*
         * Tracers may want to know about even ignored signal unless it
         * is SIGKILL which can't be reported anyway but can be ignored
         * by SIGNAL_UNKILLABLE task.
         */    
        if (t->ptrace && sig != SIGKILL)
                return 0;
   ....
     
}

当容器1号进程没有被strace时,ptrace值为0,当执行了strace后被strace的进程task_struct.ptrace会被设置为非0:
crash> task_struct.ptrace ffff88815f908000
  ptrace = 0

 对容器1号进程进行strace后再看ptrace值被设置为非0 
crash> task_struct.ptrace ffff88815f908000 -x
  ptrace = 0x10289
crash>

当对容器1号进程做了strace后,执行kill -15 $pid时通过perf trace可以
看到res=0也就是TRACE_SIGNAL_DELIVERED,信号确实发送给了容器1号进程(对应节点pid=2754618),
只不过当进程task_struct.ptrace设置了ptrace后,信号响应处理函数do_signal处理逻辑针对SIGTERM不会终止进程。
# perf trace -e signal:signal_generate --pid=3492032
     0.000 signal:signal_generate:sig=15 errno=0 code=0 comm=test.sh pid=2754618 grp=1 res=0

接下来再来看下当脚本注册了信号SIGTERM的handler后是什么效果,还是参考上面的方法模拟:

代码语言:javascript
复制
启动一个容器并且容器1号进程执行/bin/bash /test.sh 1, test.sh脚本会注册SIGTERM信号handler:
# ps -elf | grep "test.sh 1" | grep -v grep
4 S root     3581760 3581739  1  80   0 -  2237 do_wai 11:28 pts/0    00:00:00 /bin/bash /test.sh 1


crash> bt 3581760
PID: 3581760  TASK: ffff888181e68000  CPU: 1   COMMAND: "test.sh"
 
crash>
crash>
crash> task_struct.sighand ffff888181e68000
  sighand = 0xffff8881898c8000
crash> struct sighand_struct.action[14] 0xffff8881898c8000
  action[14] =   {
    sa = {
      sa_handler = 0x560076d84990,
      sa_flags = 67108864,
      sa_restorer = 0x7fbbb81acf00,
      sa_mask = {
        sig = {0}
      }
    }
  },
crash>
通过live crash可以看到当注册了handler后,sa_handler不为0,也就是sig_task_ignored函数的
handler == SIG_DF这个判断条件是不成立的,因此sig_task_ignored返回0。
static int sig_task_ignored(struct task_struct *t, int sig, bool force)
{
        void __user *handler;

        handler = sig_handler(t, sig);

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

        return sig_handler_ignored(handler, sig);
}

static int sig_handler_ignored(void __user *handler, int sig)
{
        /* Is it explicitly or implicitly ignored? */
        return handler == SIG_IGN ||
                (handler == SIG_DFL && sig_kernel_ignore(sig));
}

进程的status的SigCgt bit14也被设置为1:
# cat /proc/3581760/status | grep SigCgt
SigCgt: 0000000000014002

hexadecimal: 14002
     binary: 0000000000000000000000000000000000000000000000010100000000000010
   bits set: 16 14 1

同样在pid为3492032的控制终端发起kill -15 给注册了SIGTERM 信号handler的容器1号进程
# echo $$
3492032
# kill -15 3581760

perf trace跟踪pid 3492032发送的信号可以看到给容器1号进程(对应节点pid=3581760)发送的SIGTERM信号被内核提交给了
容器1号进程(这里对应节点pid=3581760),res=0代表TRACE_SIGNAL_DELIVERED:
# perf trace -e signal:signal_generate --pid=3492032
     0.000 signal:signal_generate:sig=15 errno=0 code=0 comm=test.sh pid=3581760 grp=1 res=0
 
 docker log也可以看到容器1号进程响应了SIGTERM并执行了注册的handler:        
[root@VM-0-20-centos ~]# docker logs 6a7abc307a6b
已注册SIGTERM handler
脚本正在运行,按Ctrl+C发送SIGINT信号,使用'kill -15 <PID>'发送SIGTERM信号
捕获到SIGTERM信号,正在退出...

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档