课程原文: 李程远. 理解进程(1):为什么我在容器中不能kill 1号进程?
本文实践环境:
Operating System: Ubuntu 20.04.2 LTS
Kernel: Linux 5.8.0-50-generic
Architecture: x86-64Docker Client/Server Version: 20.10.5
问题重现
为了直观的说明问题,这里将使用几个示例进行演示。在容器中,通过kill 1命令将1号进程杀掉,即停止容器。
Shell 程序
demonlee@demonlee-ubuntu:process$ ll
total 16
drwxrwxr-x 2 demonlee demonlee 4096 5月 12 23:31 ./
drwxrwxr-x 7 demonlee demonlee 4096 5月 12 23:09 ../
-rw-rw-r-- 1 demonlee demonlee 71 5月 12 23:31 Dockerfile_sh
-rw-rw-r-- 1 demonlee demonlee 104 5月 12 23:21 test-kill.sh
demonlee@demonlee-ubuntu:process$ cat test-kill.sh
#!/bin/bash
while true
do
sleep 2
time=`date '+%Y/%m/%d %H:%M:%S'`
echo "time: $time"
done
demonlee@demonlee-ubuntu:process$ cat Dockerfile_sh
FROM centos:8.1.1911
WORKDIR /home/proc
COPY ./test-kill.sh /home/proc
demonlee@demonlee-ubuntu:process$ docker build -t registry/proc-sh:v1 -f Dockerfile_sh .
Sending build context to Docker daemon 3.072kB
Step 1/3 : FROM centos:8.1.1911
---> 470671670cac
Step 2/3 : WORKDIR /home/proc
---> Running in fe358915b776
Removing intermediate container fe358915b776
---> f7f393c8d161
Step 3/3 : COPY ./test-kill.sh /home/proc
---> 6e01fa9348f0
Successfully built 6e01fa9348f0
Successfully tagged registry/proc-sh:v1
emonlee@demonlee-ubuntu:process$ docker run -d --name sh-kill-demo registry/proc-sh:v1 sh ./test-kill.sh
b1378b6545098fb160cf5118141b5196a3d225b001566565509bf7d1f722ebc9
demonlee@demonlee-ubuntu:process$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b1378b654509 registry/proc-sh:v1 "sh ./test-kill.sh" 10 seconds ago Up 9 seconds sh-kill-demo
demonlee@demonlee-ubuntu:process$ docker logs -f sh-kill-demo
time: 2021/05/12 15:32:28
time: 2021/05/12 15:32:30
time: 2021/05/12 15:32:32
...
demonlee@demonlee-ubuntu:process$ docker exec -it sh-kill-demo bash
[root@b1378b654509 proc]# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 11896 2840 ? Ss 15:32 0:00 sh ./test-kill.sh
root 47 1.0 0.0 12028 3336 pts/0 Ss 15:33 0:00 bash
root 65 0.0 0.0 23032 1400 ? S 15:33 0:00 /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep 2
root 66 0.0 0.0 43964 3284 pts/0 R+ 15:33 0:00 ps aux
[root@b1378b654509 proc]#
[root@b1378b654509 proc]# kill 1
[root@b1378b654509 proc]# kill -9 1
[root@b1378b654509 proc]# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 11896 2840 ? Ss 15:32 0:00 sh ./test-kill.sh
root 47 0.2 0.0 12028 3336 pts/0 Ss 15:33 0:00 bash
root 76 0.0 0.0 23032 1392 ? S 15:33 0:00 /usr/bin/coreutils --coreutils-prog-shebang=sleep /usr/bin/sleep 2
root 77 0.0 0.0 43964 3268 pts/0 R+ 15:33 0:00 ps aux
[root@b1378b654509 proc]#
ok,从上面的演示过程(后面其他的演示程序比较类似,就只贴出核心内容),可以看到shell程序在容器中运行后,通过kill 1 和 kill -9 1都没能将其杀掉。
Java语言程序
demonlee@demonlee-ubuntu:process$ cat ProcKill.java
import java.time.LocalDateTime;
public class ProcKill {
public static void main(String[] args) throws InterruptedException {
while (true) {
Thread.sleep(2000L);
System.out.println("time: " + LocalDateTime.now());
}
}
}
demonlee@demonlee-ubuntu:process$ javac ProcKill.java
demonlee@demonlee-ubuntu:process$ cat Dockerfile_java
FROM centos:8.1.1911
RUN yum install java-11-openjdk-devel -y
RUN yum install java-11-openjdk -y
WORKDIR /home/proc
COPY ./ProcKill.class /home/proc
CMD ["java","ProcKill"]
demonlee@demonlee-ubuntu:process$ docker run -d --name java-kill-demo registry/proc-java:v1
c3a5a13079b4
demonlee@demonlee-ubuntu:process$ docker exec -it java-kill-demo /bin/bash
[root@c3a5a13079b4 proc]# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.4 0.6 4204684 38128 ? Ssl 14:50 0:00 java ProcKill
root 25 3.0 0.0 12028 3320 pts/0 Ss 14:51 0:00 /bin/bash
root 40 0.0 0.0 43964 3324 pts/0 R+ 14:51 0:00 ps aux
[root@c3a5a13079b4 proc]# kill -9 1
[root@c3a5a13079b4 proc]# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.3 0.6 4204684 38128 ? Ssl 14:50 0:00 java ProcKill
root 25 0.3 0.0 12028 3320 pts/0 Ss 14:51 0:00 /bin/bash
root 41 0.0 0.0 43964 3288 pts/0 R+ 14:51 0:00 ps aux
[root@c3a5a13079b4 proc]# kill 1
[root@c3a5a13079b4 proc]# demonlee@demonlee-ubuntu:process$
java程序 kill -9 1 不起作用,但kill 1可以将容器杀掉。
C程序
demonlee@demonlee-ubuntu:process$ cat ProcKill.c
#include <stdio.h>
#include <unistd.h>
#include<time.h>
int main(int argc, char *argv[]) {
time_t t;
while (1) {
sleep(2);
time(&t);
printf("current time is : %s",ctime(&t));
}
return 0;
}
demonlee@demonlee-ubuntu:process$ cat Dockerfile_c
FROM centos:8.1.1911
WORKDIR /home/proc
COPY ./ProcKill /home/proc
CMD ["./ProcKill"]
demonlee@demonlee-ubuntu:process$ docker exec -it c-kill-demo /bin/bash
[root@7b3b70801f39 proc]# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 4340 796 ? Ss 15:08 0:00 ./ProcKill
root 8 1.5 0.0 12028 3280 pts/0 Ss 15:08 0:00 /bin/bash
root 23 0.0 0.0 43964 3356 pts/0 R+ 15:08 0:00 ps aux
[root@7b3b70801f39 proc]# kill -9 1
[root@7b3b70801f39 proc]# kill 1
[root@7b3b70801f39 proc]# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 4340 796 ? Ss 15:08 0:00 ./ProcKill
root 8 0.1 0.0 12028 3280 pts/0 Ss 15:08 0:00 /bin/bash
root 24 0.0 0.0 43964 3332 pts/0 R+ 15:09 0:00 ps aux
[root@7b3b70801f39 proc]#
可以看到,这次跟shell脚本程序一样,无法杀掉容器中的1号进程。
到底是怎么回事?为什么有的可以,有的不行。
为了找到原因,我们需要先弄明白两个东西:1号进程和kill操作的本质。
Linux init 进程
一个Linux系统打开电源,执行BIOS/boot-loader之后,就会由boot-loader负责加载Linux内核。
内核初始化的过程中,会先后初始化0号、1号、2号进程。
-
0号进程:系统创建的第一个进程,这是唯一一个没有没有通过fork或者kernel_thread产生的进程,是进程列表的第一个。
进程0执行的是cpu_idle函数,该函数仅有一条hlt汇编指令,就是系统闲置时用来降低能耗省电的。同时进程0的PCB叫作init_task,在很多链表中起了表头的作用。当就绪队列中没有其他进程时,进程0就会被调度程序选中,以此来省电,减少热量的产生,如果就绪队列中有其他进程,那么0号进程就不会运行了。
-
1号进程:内核启动过程中会调用kernel_thread(kernel_init, NULL, CLONE_FS)创建第2个进程,这个就是1号进程,即init进程。内核调用1号进程的代码时,会从内核态切换到用户态。所以1号进程是一个用户态进程(即用户态所有进程的祖先),其基本功能就是创建出系统中所有的用户态进程,并管理它们。
1号进程运行的是一个可执行文件,会首先尝试运行ramdisk中的"/init"文件,或者普通文件系统上的"/sbin/init",“/etc/init”,“/bin/init”, “/bin/sh”,不同版本的Linux会选择不同的文件启动,但只要有一个起来了就行。目前主流的Linux发行版,都会把/sbin/init作为符号链接指向Systemd,Systemd是目前最流行的Linux init进程。
(这里的ramdisk是系统初始化时基于内存的根文件系统,目的是解决"/init"程序的存储问题。如果从普通存储设备上获取"/init"可执行文件,那就必须要有各种磁盘驱动才能读到我们需要的文件,这样一来,内核就复杂了。而内核访问内存是不需要驱动的,所以实现了一个基于内存的根文件系统,并将"/init"文件放进去。)
demonlee@demonlee-ubuntu:process$ ll /sbin/init lrwxrwxrwx 1 root root 20 4月 10 14:01 /sbin/init -> /lib/systemd/systemd* demonlee@demonlee-ubuntu:process$
内核源码如下:
// init/main.c static int __ref kernel_init(void *unused) { int ret; kernel_init_freeable(); /* need to finish all async __init code before freeing the memory */ async_synchronize_full(); ftrace_free_init_mem(); free_initmem(); mark_readonly(); /* * Kernel mappings are now finalized - update the userspace page-table * to finalize PTI. */ pti_finalize(); system_state = SYSTEM_RUNNING; numa_default_policy(); rcu_end_inkernel_boot(); do_sysctl_args(); if (ramdisk_execute_command) { ret = run_init_process(ramdisk_execute_command); if (!ret) return 0; pr_err("Failed to execute %s (error %d)\n", ramdisk_execute_command, ret); } /* * We try each of these until one succeeds. * * The Bourne shell can be used instead of init if we are * trying to recover a really broken machine. */ if (execute_command) { ret = run_init_process(execute_command); if (!ret) return 0; panic("Requested init %s failed (error %d).", execute_command, ret); } if (CONFIG_DEFAULT_INIT[0] != '\0') { ret = run_init_process(CONFIG_DEFAULT_INIT); if (ret) pr_err("Default init %s failed (error %d)\n", CONFIG_DEFAULT_INIT, ret); else return 0; } if (!try_to_run_init_process("/sbin/init") || !try_to_run_init_process("/etc/init") || !try_to_run_init_process("/bin/init") || !try_to_run_init_process("/bin/sh")) return 0; panic("No working init found. Try passing init= option to kernel. " "See Linux Documentation/admin-guide/init.rst for guidance."); }
-
2号进程:用户态所有进程的祖先是1号进程,而内核态所有进程的祖先是2号进程。内核会通过kernel_thread(kthreadd, NULL, CLONE_FS|CLONE_FILES)创建第3个进程,这个就是2号进程,负责所有内核态的线程调度和管理。
通过在Ubuntu系统上运行ps -ef,对应的第2,3列分别表示进程的id(PID)和父id(PPID),从数字上我们可以进一步理解各个进程之间的关系。
demonlee@demonlee-ubuntu:process$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 11:20 ? 00:00:04 /lib/systemd/systemd --system --deserialize 12
root 2 0 0 11:20 ? 00:00:00 [kthreadd]
root 3 2 0 11:20 ? 00:00:00 [rcu_gp]
root 4 2 0 11:20 ? 00:00:00 [rcu_par_gp]
root 6 2 0 11:20 ? 00:00:00 [kworker/0:0H-kblockd]
root 9 2 0 11:20 ? 00:00:00 [mm_percpu_wq]
root 10 2 0 11:20 ? 00:00:00 [ksoftirqd/0]
...
root 333 2 0 11:20 ? 00:00:00 [iprt-VBoxWQueue]
root 349 2 0 11:20 ? 00:00:00 [cryptd]
systemd+ 579 1 0 11:20 ? 00:00:00 /lib/systemd/systemd-resolved
systemd+ 581 1 0 11:20 ? 00:00:00 /lib/systemd/systemd-timesyncd
...
demonlee 1644 1 0 11:21 ? 00:00:01 /lib/systemd/systemd --user
demonlee 1645 1644 0 11:21 ? 00:00:00 (sd-pam)
demonlee 1650 1644 0 11:21 ? 00:00:00 /usr/bin/pulseaudio --daemonize=no --log-target=journal
demonlee 1652 1644 0 11:21 ? 00:00:00 /usr/libexec/tracker-miner-fs
...
demonlee 1726 1644 0 11:21 ? 00:00:00 /usr/libexec/gvfs-mtp-volume-monitor
demonlee 1757 1718 0 11:21 tty2 00:00:00 /usr/libexec/gnome-session-binary --systemd --systemd --session=ubuntu
demonlee 1828 1 0 11:21 ? 00:00:00 /usr/bin/VBoxClient --clipboard
demonlee 1830 1828 0 11:21 ? 00:00:00 /usr/bin/VBoxClient --clipboard
...
善于思考的小伙伴可以看到,这里ps -ef的结果里面没有0号进程,其实上面已经提到了:0号进程此时没有运行。
有了上面对1号进程的介绍,以此类推,容器内有独立的Pid Namespace,其1号进程就是该容器内的init进程,容器内其他进程都由init进程创建。
Linux 信号
信号(Signal)就是Linux进程收到的一个通知,它一般会从1开始编号,通过kill -l 命令可以查看当前系统中所有信号,并通过man 7 signal 可以阅读说明文档。
demonlee@demonlee-ubuntu:process$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
demonlee@demonlee-ubuntu:process$ man 7 signal
...
DESCRIPTION
Linux supports both POSIX reliable signals (hereinafter "standard signals") and POSIX real-time signals.
Signal dispositions
Each signal has a current disposition, which determines how the process behaves when it is delivered the signal.
The entries in the "Action" column of the table below specify the default disposition for each signal, as follows:
Term Default action is to terminate the process.
Ign Default action is to ignore the signal.
Core Default action is to terminate the process and dump core (see core(5)).
Stop Default action is to stop the process.
Cont Default action is to continue the process if it is currently stopped.
...
Standard signals
Linux supports the standard signals listed below. The second column of the table indicates which standard (if any) specified the signal:
"P1990" indicates that the signal is described in the original POSIX.1-1990 standard; "P2001" indicates that the signal was added in SUSv2 and
POSIX.1-2001.
Signal Standard Action Comment
────────────────────────────────────────────────────────────────────────
SIGABRT P1990 Core Abort signal from abort(3)
SIGALRM P1990 Term Timer signal from alarm(2)
SIGBUS P2001 Core Bus error (bad memory access)
SIGCHLD P1990 Ign Child stopped or terminated
SIGCLD - Ign A synonym for SIGCHLD
SIGCONT P1990 Cont Continue if stopped
SIGEMT - Term Emulator trap
SIGFPE P1990 Core Floating-point exception
SIGHUP P1990 Term Hangup detected on controlling terminal
or death of controlling process
SIGILL P1990 Core Illegal Instruction
SIGINFO - A synonym for SIGPWR
SIGINT P1990 Term Interrupt from keyboard
SIGIO - Term I/O now possible (4.2BSD)
SIGIOT - Core IOT trap. A synonym for SIGABRT
SIGKILL P1990 Term Kill signal
SIGLOST - Term File lock lost (unused)
SIGPIPE P1990 Term Broken pipe: write to pipe with no
readers; see pipe(7)
SIGPOLL P2001 Term Pollable event (Sys V).
Synonym for SIGIO
SIGPROF P2001 Term Profiling timer expired
SIGPWR - Term Power failure (System V)
SIGQUIT P1990 Core Quit from keyboard
SIGSEGV P1990 Core Invalid memory reference
SIGSTKFLT - Term Stack fault on coprocessor (unused)
SIGSTOP P1990 Stop Stop process
SIGTSTP P1990 Stop Stop typed at terminal
SIGSYS P2001 Core Bad system call (SVr4);
see also seccomp(2)
SIGTERM P1990 Term Termination signal
SIGTRAP P2001 Core Trace/breakpoint trap
SIGTTIN P1990 Stop Terminal input for background process
SIGTTOU P1990 Stop Terminal output for background process
SIGUNUSED - Core Synonymous with SIGSYS
SIGURG P2001 Ign Urgent condition on socket (4.2BSD)
SIGUSR1 P1990 Term User-defined signal 1
SIGUSR2 P1990 Term User-defined signal 2
SIGVTALRM P2001 Term Virtual alarm clock (4.2BSD)
SIGXCPU P2001 Core CPU time limit exceeded (4.2BSD);
see setrlimit(2)
SIGXFSZ P2001 Core File size limit exceeded (4.2BSD);
see setrlimit(2)
SIGWINCH - Ign Window resize signal (4.3BSD, Sun)
The signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
...
...
为了便于理解,这里用前面的C程序ProcKill进行说明。当我们在宿主机上直接运行该程序,终端上会打印出对应的日志,此时我们通过键盘按下"Ctrl-C" ,就会看到进程退出了。而这背后其实就是ProcKill进程收到了编号为2的 SIGINT信号:Interrupt from keyboard。
demonlee@demonlee-ubuntu:process$ ./ProcKill
current time is : Sun May 16 00:43:51 2021
current time is : Sun May 16 00:43:53 2021
current time is : Sun May 16 00:43:55 2021
current time is : Sun May 16 00:43:57 2021
current time is : Sun May 16 00:43:59 2021
current time is : Sun May 16 00:44:01 2021
current time is : Sun May 16 00:44:03 2021
current time is : Sun May 16 00:44:05 2021
current time is : Sun May 16 00:44:07 2021
current time is : Sun May 16 00:44:09 2021
^C
demonlee@demonlee-ubuntu:process$
除了通过键盘输入"Ctrl-C",我们还可以通过kill
demonlee@demonlee-ubuntu:process$ sudo strace -p 37088
strace: Process 37088 attached
restart_syscall(<... resuming interrupted read ...>) = 0
stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=573, ...}) = 0
write(1, "current time is : Sun May 16 00:"..., 43) = 43
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=2, tv_nsec=0}, 0x7ffeef5397e0) = 0
stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=573, ...}) = 0
write(1, "current time is : Sun May 16 00:"..., 43) = 43
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=2, tv_nsec=0}, 0x7ffeef5397e0) = 0
stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=573, ...}) = 0
write(1, "current time is : Sun May 16 00:"..., 43) = 43
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=2, tv_nsec=0}, 0x7ffeef5397e0) = 0
stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=573, ...}) = 0
write(1, "current time is : Sun May 16 00:"..., 43) = 43
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=2, tv_nsec=0}, 0x7ffeef5397e0) = 0
stat("/etc/localtime", {st_mode=S_IFREG|0644, st_size=573, ...}) = 0
write(1, "current time is : Sun May 16 00:"..., 43) = 43
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=2, tv_nsec=0}, {tv_sec=0, tv_nsec=921355614}) = ? ERESTART_RESTARTBLOCK (Interrupted by signal)
--- SIGINT {si_signo=SIGINT, si_code=SI_USER, si_pid=37082, si_uid=1000} ---
+++ killed by SIGINT +++
demonlee@demonlee-ubuntu:process$
看到最后几行,果然是SIGINT信号干的。
那收到信号后,进程如何响应呢?在Linux下,对于每一个信号,进程的响应方式有3种选择:
Option | Description |
---|---|
Ignore(忽略) | 对信号不做任何处理,但SIGKILL和SIGSTOP例外 |
Catch(捕获) | 用户进程自己注册针对某个信号的handler,进程收到该信号后,会调用该handler进行处理,但SIGKILL和SIGSTOP例外 |
Default(缺省) | Linux给每个信号都定义了缺省行为,对于大部分信号,用户进程不需要注册自己的handler,使用系统缺省行为即可 |
对于表格中的SIGKILL (9) 和SIGSTOP (19),它们是Linux里面的两个特权信号。
特权信号指的是Linux为kernel和超级用户去删除任意进程所保留的,不能忽略,也不能被捕获, 也就是强制执行。
我们经常通过kill
去杀掉一个进程,其实发送的是SIGTERM (15),意思是让进程可以graceful shutdown,但此时如果进程没有反应,我们会通过 kill -9 来强杀。 那SIGKILL、SIGTERM和SIGSTOP三者之间有啥区别呢,这里有一个提问可以看看。
以SIGINT信号为例,我们在程序中并未Catch它,那它的Default行为是什么呢?从前面的man 7 signal中可以看到:
SIGINT P1990 Term Interrupt from keyboard
而Term这个Action的默认行为是:
Term Default action is to terminate the process.
SIGINT的Default行为是终止进程,这也就解释了上面的结果。那如果我们让进程Catch SIGINT信号,调用自定义行为,比如只打印一些日志,此时再按下"Ctrl-C",理论上进程是不会退出的。接下来,就调整代码来验证一下我们的猜想是否对。
// ProcKillSignal.c
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
void sig_handler(int signo){
if (signo == SIGINT) {
printf("received SIGINT: %d\n", signo);
exit(0);
}
}
int main(int argc, char *argv[]) {
signal(SIGINT, sig_handler);
time_t t;
while (1) {
sleep(2);
time(&t);
printf("current time is : %s",ctime(&t));
}
return 0;
}
编译测试,中间按了两次"Ctrl-C",即下面日志中的"^Creceived SIGINT: 2, ignore…",而进程并没有退出,依旧继续运行。
demonlee@demonlee-ubuntu:process$ gcc -o ProcKillSignal ProcKillSignal.c
demonlee@demonlee-ubuntu:process$ ./ProcKillSignal
current time is : Sun May 16 22:49:23 2021
current time is : Sun May 16 22:49:25 2021
^Creceived SIGINT: 2, ignore...
current time is : Sun May 16 22:49:26 2021
current time is : Sun May 16 22:49:28 2021
^Creceived SIGINT: 2, ignore...
current time is : Sun May 16 22:49:28 2021
current time is : Sun May 16 22:49:30 2021
...
原因分析
有了上面Linux init进程及信号处理相关知识的铺垫,现在再回到主题:为何容器中kill 1,有的可以,有的不行,而kill -9 1 都不行,难道内核根据什么条件判断,将SIGTERM和SIGKILL信号给忽略了?
如何验证我们的猜想呢?还得从kill 命令说起,该命令会调用kill()这个系统调用(即内核接口),从而进入到内核函数sys_kill(),经过层层调用,最终会到达kernel/signal.c这个文件中的sig_task_ignored()函数。
由于内核代码非常多,很复杂(笔者还在初步学习),这里结合上面截图中第2个if判断(第1、3两个if与当前问题没有太大关系,先忽略)进行简单分析:
1)kill的是1号进程,所以第1个子条件满足;
在哪里给1号进程设置的SIGNAL_UNKILLABLE,搜索了一下,如下图所示:
其中is_child_reaper(pid)函数的代码如下,从注释就可以看到,如果是init进程,则返回true。
/* * 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; }
2)容器内执行kill操作,是同一个Namespace中发出的信号,所以force值为0,则第3个子条件也满足;
3)如果信号没有注册自己的Handler,则第2个子条件也满足,所以关键就在于:handler==SIG_DFL。换句话说,第2个if条件整体的意思是:Linux内核针对每个Namespace中的init进程,把只有default handler的信号都忽略掉了。这也就解释了,为什么kill -9 1永远都无效。
那么,如何验证我们的程序到底有没有给SIGTERM信号注册handler呢?答案是查看进程状态中的SigCgt Bitmap。
在Linux下,找到进程Pid,然后查看
/proc/$Pid/status
,其中包含了哪些信号被阻止(SigBlk),被忽略(SigIgn)或被捕获(SigCgt),更多请参考这里。
# cat /proc/1/status
...
SigBlk: 7be3c0fe28014a03
SigIgn: 0000000000001000
SigCgt: 00000001800004ec
...
通过运行命令,我们拿到容器内对应init进程的SigCgt的值,如下所示:
demonlee@demonlee-ubuntu:process$ docker exec c-kill-demo cat /proc/1/status|grep -i SigCgt
SigCgt: 0000000000000000
demonlee@demonlee-ubuntu:process$ docker exec java-kill-demo cat /proc/1/status|grep -i SigCgt
SigCgt: 0000000181005ccf
demonlee@demonlee-ubuntu:process$ docker exec sh-kill-demo cat /proc/1/status|grep -i SigCgt
SigCgt: 0000000000010002
demonlee@demonlee-ubuntu:process$
我们将其从十六进制转换为二进制,由于C程序都是0,所以肯定是没有Catch任何信号,另外两个分析如下(由于数字太长,省略了前面很多0):
0000000181005ccf --> …0001 1000 0001 0000 0000 0101 1100 1100 1111 【第15位为1,表示捕获了SIGTERM信号】
0000000000010002 --> …0000 0000 0000 0000 0001 0000 0000 0000 0010 【第15位为0,表示没有捕获SIGTERM信号】
到这里,就明白了为啥只有java程序使用kill 1生效了,如果要使c程序也能支持呢?很简单,我们为它注册SIGTERM的handler。
// ProcKill.c
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
void sig_handler(int signo){
if (signo == SIGTERM) {
printf("received SIGTERM: %d\n", signo);
exit(0);
}
}
int main(int argc, char *argv[]) {
signal(SIGTERM, sig_handler);
time_t t;
while (1) {
sleep(2);
time(&t);
printf("current time is : %s",ctime(&t));
}
return 0;
}
编译打包重新测试,如下所示,此时执行kill 1,容器进程退出了。
demonlee@demonlee-ubuntu:process$ docker exec -it c-kill-demo-signal /bin/bash
[root@ffd5915b1557 proc]# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.1 0.0 4340 800 ? Ss 16:54 0:00 ./ProcKill
root 8 1.0 0.0 12028 3288 pts/0 Ss 16:54 0:00 /bin/bash
root 22 0.0 0.0 43964 3292 pts/0 R+ 16:54 0:00 ps aux
[root@ffd5915b1557 proc]#
[root@ffd5915b1557 proc]#
[root@ffd5915b1557 proc]#
[root@ffd5915b1557 proc]# kill 1
[root@ffd5915b1557 proc]# demonlee@demonlee-ubuntu:process$
demonlee@demonlee-ubuntu:process$
如果还是最开始的C程序(即没有注册SIGTERM信号的handler),从宿主机上执行kill
依旧分析上面的代码,此时force的值为1了,那么判断的关键就是sig_kernel_only(sig) 这个条件的值,其代码如下:
// include/linux/signal.h
...
#define SIG_KERNEL_ONLY_MASK (\
rt_sigmask(SIGKILL) | rt_sigmask(SIGSTOP)
...
#define sig_kernel_only(sig) siginmask(sig, SIG_KERNEL_ONLY_MASK)
...
从代码可知,如果是SIGKILL和SIGSTOP,则sig_kernel_only(sig)返回true,否则返回false。所以,如果宿主机上kill
总结
- 在容器中,1号init进程不会响应SIGKILL和SIGSTOP两个特权信号;【此处有疑虑,笔者在容器中使用SIGSTOP,1号进程可以响应,待进一步分析】
- 对于其他信号,如果用户进程注册了对应的handler,则1号进程可以响应;
- 使用strace工具跟踪进程的系统调用非常有用。
参考资料
[2] 刘超. 内核初始化:生意做大了就得成立公司
[3] 如何检查进程正在监听的信号