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信号,正在退出...