[操作系统/Linux/容器/0|1|2号进程] 容器内kill 1号进程的坑
1 问题描述: 无法 kill 掉容器中进程号为1的进程
- 为了直观的说明问题,这里将使用几个示例进行演示。
在容器中,通过
kill 1
命令将1号进程杀掉,即停止容器运行。
Shell 程序 : kill 1
操作无效,kill -9 1
操作无效
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
demonlee@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]#
从上面的演示过程(后面其他的演示程序比较类似,就只贴出核心内容),可以看到shell程序在容器中运行后,通过
kill 1
和kill -9 1
都没能将其杀掉。
Java 程序 : kill 1
操作有效,kill -9 1
操作无效
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 程序 : kill 1
操作无效,kill -9 1
操作无效
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操作的本质。
2 问题分析
Linux init 进程 与 内核初始化过程
-
Linux系统打开电源,执行
BIOS/boot-loader
之后,就会由boot-loader
负责加载Linux
内核。 -
内核初始化的过程中,会先后初始化
0
号、1
号、2
号进程。
0号进程 : 操作系统的第1个进程
- 0号进程:系统创建的第一个进程,这是唯一一个没有没有通过
fork
或者kernel_thread
产生的进程,是进程列表的第一个。
进程0执行的是
cpu_idle
函数,该函数仅有一条hlt
汇编指令,就是系统闲置时用来降低能耗省电的。
同时进程0的PCB
叫作init_task
,在很多链表中起了表头的作用。
当就绪队列中没有其他进程时,进程0就会被调度程序选中,以此来省电,减少热量的产生;如果就绪队列中有其他进程,那么0号进程就【不会运行】了。
1号进程 := 用户态所有进程的祖先 := init 进程
- 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号进程 := 内核态所有进程的祖先
- 2号进程:用户态所有进程的祖先是1号进程,而内核态所有进程的祖先是2号进程。
内核会通过
kernel_thread(kthreadd, NULL, CLONE_FS|CLONE_FILES)
创建第3个进程,这个就是2号进程,负责所有内核态的线程调度和管理。
各进程间的关系
- 通过在 Ubuntu Linux 系统上运行
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
来给进程直接发送信号
比如上面的例子,我们可以在另外一个终端中输入
kill -2
来达到同样的效果。
跟踪进程接收到的信号(strace
命令)
- 怎么确定进程收到的是哪个信号呢?可以使用
strace
工具进行跟踪(启动一个新的终端),相关操作如下:
demonlee@demonlee-ubuntu:process$ sudo apt -y install strace
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进程对接收到的信号的响应方式
- 那收到信号后,进程如何响应呢?在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(9)
、SIGTERM(15)
和SIGSTOP(19)
三者之间有啥区别呢,这里有一个提问可以看看。 -
以
SIGINT
信号为例,我们在程序中并未Catch
它,那它的Default
行为是什么呢?
从前面的 man 7 signal中可以看到:
SIGINT P1990 Term Interrupt from keyboard
而
Term(15)
这个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()
函数。
源码:kernel/signal.c
由于内核代码非常多,很复杂,这里进行简单分析:
- 1)kill的是1号进程,所以第1个子条件满足;
在哪里给1号进程设置的SIGNAL_UNKILLABLE :
源码:
kernel/fork.c
其中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
和kill -9
会是什么结果呢? -
依旧分析上面的代码,此时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号进程的,而kill -9
可以。
总结
- 在容器中,1号进程(init进程)不会响应
SIGKILL
和SIGSTOP
两个特权信号;
【此处有疑虑,在容器中使用SIGSTOP,1号进程可以响应,待进一步分析】
- 对于其他信号,如果用户进程注册了对应的handler,则:1号进程可以响应;
- 可使用
strace
工具跟踪进程的系统调用非常有用。
X 参考文献

本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!