容器实战高手课
李程远
eBay 总监级工程师,云平台架构师
4426 人已学习
立即订阅
登录后,你可以任选4讲全文学习
推荐试读
换一换
01 | 认识容器:容器的基本操作和实现原理
07 | Load Average:加了CPU Cgroup限制,为什么我的容器还是很慢?
11 | 容器文件系统:我在容器中读写文件怎么变慢了?
课程目录
已完结/共 31 讲
开篇词 (2讲)
开篇词 | 一个态度两个步骤,成为容器实战高手
01 | 认识容器:容器的基本操作和实现原理
容器进程 (6讲)
02 | 理解进程(1):为什么我在容器中不能kill 1号进程?
03|理解进程(2):为什么我的容器里有这么多僵尸进程?
04 | 理解进程(3):为什么我在容器中的进程被强制杀死了?
05|容器CPU(1):怎么限制容器的CPU使用?
06 | 容器CPU(2):如何正确地拿到容器CPU的开销?
07 | Load Average:加了CPU Cgroup限制,为什么我的容器还是很慢?
容器内存 (3讲)
08 | 容器内存:我的容器为什么被杀了?
09 | Page Cache:为什么我的容器内存使用量总是在临界点?
10 | Swap:容器可以使用Swap空间吗?
容器存储 (4讲)
11 | 容器文件系统:我在容器中读写文件怎么变慢了?
12 | 容器文件Quota:容器为什么把宿主机的磁盘写满了?
13 | 容器磁盘限速:我的容器里磁盘读写为什么不稳定?
14 | 容器中的内存与I/O:容器写文件的延时为什么波动很大?
容器网络 (4讲)
15 | 容器网络:我修改了/proc/sys/net下的参数,为什么在容器中不起效?
16 | 容器网络配置(1):容器网络不通了要怎么调试?
17|容器网络配置(2):容器网络延时要比宿主机上的高吗?
18 | 容器网络配置(3):容器中的网络乱序包怎么这么高?
容器安全 (2讲)
19 | 容器安全(1):我的容器真的需要privileged权限吗?
20 | 容器安全(2):在容器中,我不以root用户来运行程序可以吗?
结束语 (4讲)
结束语 | 跳出舒适区,突破思考的惰性
结课测试|这些容器技术的问题,你都掌握了么?
用户故事 | 莫名:相信坚持的力量,终会厚积薄发
加餐福利 | 课后思考题答案合集
专题加餐 (6讲)
加餐01 | 案例分析:怎么解决海量IPVS规则带来的网络延时抖动问题?
加餐02 | 理解perf:怎么用perf聚焦热点函数?
加餐03 | 理解ftrace(1):怎么应用ftrace查看长延时内核函数?
加餐04 | 理解ftrace(2):怎么理解ftrace背后的技术tracepoint和kprobe?
加餐05 | eBPF:怎么更加深入地查看内核中的函数?
加餐06 | BCC:入门eBPF的前端工具
容器实战高手课
15
15
1.0x
00:00/00:00
登录|注册
开通超级会员可免费学习本课程,还可解锁海量内容免费学特权。

02 | 理解进程(1):为什么我在容器中不能kill 1号进程?

你好,我是程远。
今天,我们正式进入理解进程的模块。我会通过 3 讲内容,带你了解容器 init 进程的特殊之处,还有它需要具备哪些功能,才能保证容器在运行过程中不会出现类似僵尸进程,或者应用程序无法 graceful shutdown 的问题。
那么通过这一讲,我会带你掌握 init 进程和 Linux 信号的核心概念。

问题再现

接下来,我们一起再现用 kill 1 命令重启容器的问题。
我猜你肯定想问,为什么要在容器中执行 kill 1 或者 kill -9 1 的命令呢?其实这是我们团队里的一位同学提出的问题。
这位同学当时遇到的情况是这样的,他想修改容器镜像里的一个 bug,但因为网路配置的问题,这个同学又不想为了重建 pod 去改变 pod IP。
如果你用过 Kubernetes 的话,你也肯定知道,Kubernetes 上是没有 restart pod 这个命令的。这样看来,他似乎只能让 pod 做个原地重启了。当时我首先想到的,就是在容器中使用 kill pid 1 的方式重启容器。
为了模拟这个过程,我们可以进行下面的这段操作。
如果你没有在容器中做过 kill 1 ,你可以下载我在 GitHub 上的这个例子运行 make image 来做一个容器镜像。
然后,我们用 Docker 构建一个容器,用例子中的 init.sh 脚本作为这个容器的 init 进程。
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/1000字
划线
笔记
复制
01 | 认识容器:容器的基本操作和实现原理
07 | Load Average:加了CPU Cgroup限制,为什么我的容器还是很慢?
11 | 容器文件系统:我在容器中读写文件怎么变慢了?
13 | 容器磁盘限速:我的容器里磁盘读写为什么不稳定?
加餐03 | 理解ftrace(1):怎么应用ftrace查看长延时内核函数?
加餐05 | eBPF:怎么更加深入地查看内核中的函数?
开通超级会员免费畅看本课程
开通会员
该文章仅可免费阅读部分内容,如需阅读完整文章,请开通超级会员或单独购买本课程。
登录 后留言

精选留言(53)

  • 赵守忠[开心每一天]
    kill 1 分两种情况,如果 1 号进程没有注册 SIGTERM 的 handler,那么对 SIGTERM 信号也不响应,如果注册了 handler,那么就可以响应 SIGTERM 信号。
    ---在k8s的容器环境内测试,基于tini。和老师讲的有些出入:
    bash-5.0# ps -ef
    PID USER TIME COMMAND
        1 root 0:00 /tini -- /bin/sh -c java -javaagent:/opt/jmx/jmx.jar=7080:config.yaml $JAVA_OPTS -jar /app.jar
    bash-5.0# cat /proc/1/status|grep SigCgt
    SigCgt: 0000000000000000
    bash-5.0# kill 1
    bash-5.0# command terminated with non-zero exit code: Error executing in Docker Container: 137

    实际情况是容器重启了。响应了kill 1操作。

    作者回复: @赵守忠[开心每一天]
    非常好的发现啊!
    tini的确没有注册SIGTERM,它的退出并不是因为SIGTERM的信号让它退出的,而是它发现它子进程都退出之后,主动退出的,这样容器也退出了。

    可以看一下tini的源代码,它把所有接收到的信号(除了SIGHILD)都转发给了子进程,也包括了SIGTERM, 那么子进程收到SIGTERM就退出了,而tini自己可以收到SIGHILD, 然后tini自己退出,并且容器退出。

    tini 的源代码和主循环:
    https://github.com/krallin/tini/blob/master/src/tini.c
            while (1) {
                    /* Wait for one signal, and forward it */
                    if (wait_and_forward_signal(&parent_sigset, child_pid)) {
                            return 1;
                    }

                    /* Now, reap zombies */
                    if (reap_zombies(child_pid, &child_exitcode)) {
                            return 1;
                    }

                    if (child_exitcode != -1) {
                            PRINT_TRACE("Exiting: child has exited");
                            return child_exitcode;
                    }
            }

    2020-11-21
    3
    38
  • Helios
    有一种老师说了一大圈,但是没有说容器的本质就是宿主机上的一个进程这个本质。

    作者回复: @Helios,
    很好的一个问题。
    很多介绍容器的文章可能都会强调容器是进程,不过它们讨论的背景应该是和虚拟机做比较之后这么说的,因为在容器之前虚拟机是云平台上最流行的技术。强调容器是进程的目的是区分容器与虚拟机的差别,但是我不认为这个是容器的本质。

    其实无论是namespace (pid namespace)还是cgroups都是在管理进程, 容器中运行是进程,这个是个明显的特征了,但不是本质。

    我们如果换一个角度去思考,如果容器流行先于虚拟机技术, 我们是否还会强调容器是进程了呢?

    2020-11-19
    3
    23
  • 上邪忘川
    从网上找到了不错的关于SigCgt 掩码位的解释,不懂的可以看一下,豁然开朗。https://qastack.cn/unix/85364/how-can-i-check-what-signals-a-process-is-listening-to

    作者回复: 谢谢 @上邪忘川, 很好的补充材料!

    2020-11-21
    10
  • 莫名
    man pid_namespace 提到了老师在文中强调的两个细节。

    -----------------------------

    Only signals for which the "init" process has established a signal
           handler can be sent to the "init" process by other members of the PID
           namespace. This restriction applies even to privileged processes,
           and prevents other members of the PID namespace from accidentally
           killing the "init" process.

           Likewise, a process in an ancestor namespace can—subject to the usual
           permission checks described in kill(2)—send signals to the "init"
           process of a child PID namespace only if the "init" process has
           established a handler for that signal. SIGKILL or SIGSTOP are treated exceptionally: these signals are
           forcibly delivered when sent from an

    作者回复: 谢谢 @莫名!
    仔细读文档也是很有用的!

    2020-11-19
    9
  • daydreamer
    思考题:
    kill <pid> 不可以杀掉容器init进程
    kill -9 <pid> 可以
    不同点在于SIGTERM不是内核信号,所以!(force && sig_kernel_only(sig)为True,加上前面两个if也为true,所以忽略;SIGKILL是内核信号 !(force && sig_kernel_only(sig)为False,信号没有办法忽略,所以被杀掉

    作者回复: @daydreamer
    赞!

    2020-11-28
    3
    8
  • 良凯尔
    虽然在容器内kill 1号进程行不通,但是我可以在宿主上kill容器的1号进程来达到重启容器的目的,是这样吗?

    作者回复: 在宿主机上kill容器的1号进程是可以的。不过,有时候容器的用户没有宿主机登陆的权限。

    2020-11-20
    6
  • 维c
    查了一下资料,貌似sig_kernel_only函数是用了判断信号是不是kill或者stop的,是这两个信号才会返回true,这就意味着force不为0,同时信号是kill或者stop的时候信号是不会被忽略的,这也就解释了为什么宿主机是可以通过kill信号来杀掉容器里的进程,而sigterm由于force的值可能会被忽略,那么force的值又是又什么决定的呢?

    作者回复: @维c
    很好的分析!force 由发送信号的进程和接受信号进程是否在同一个namespace里决定。你可以再看一下代码。

    2020-12-03
    2
    5
  • 朱雯
    关于SigCgt bitmap 其实我是有些疑惑的,第一为什么是16位?我最开始是这样猜测的,就是一个位置代表一个信号量,那最多只能说明第一到第十六的信号量。 后面看到加了handle处理的函数是这个样子

    if (signo == SIGTERM) { printf("received SIGTERM\n"); exit(0); }
    然后他的sigcgt bitmap是:0000000000004000 ,如果按照这样的算法,一个数字代表4位,那一共16位,应该有64种信号量,为啥只有31种还是32种,剩下的是没定义?

    作者回复: @朱雯
    很好的问题!我在文中只是说了31个posix标准里的signal. Linux 里还有编号32-64的real-time signal的扩展定义。因为和这一讲的问题没有太大的关系,我没有具体展开了。

    2020-11-19
    5
  • Action
    Host Namespace 向c程序init进程,发送SIGTERM,忽略了,发送SIGKILL杀掉了,是特权信号就给杀掉了对吧? 还有一块不明白,在handler == SIG_DFL这里,SIGTERM,它是可以被捕获的。也就是说如果用户不注册handler,那么这个条件对 SIGTERM 也是满足的,为什么呢?

    作者回复: 至于为什么即使在宿主机机上向容器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))

    2021-01-05
    4
  • 1900
    在容器中不能响应SIGKILL 和 SIGSTOP,但是在宿主机中可以响应,因为在宿主机中所看到的“容器1号进程”在宿主机上只是一个普通进程

    作者回复: @1900, 如果可以的话,你可以动手试一下,看看结果是不是和你分析的一样 :-)

    2020-11-19
    3
    4
  • 白夜行
    老师讲的太棒了! 留言区也能学到很多。

    请教老师一个可能不太相关的问题,golang为什么要默认注册很多信号处理函数,而不是保持C的行为? 
    2021-01-20
    3
  • Helios
    不知道什么要在容器内执行,直接在去宿主机上docker kill不行么。或者直接edit一下编排文件加个环境变量啥的,不就能触发原地升级么。
    比较新的信号应该不止31个了,还增加了31个可靠信号,为了解决以前linux中信号堆积忽略信号的问题。

    作者回复: > 不知道什么要在容器内执行,直接在去宿主机上docker kill不行么。或者直接edit一下编排文件加个环境变量啥的,不就能触发原地升级么。

    在生产环境中,用户是没有权限登陆宿主机的。 在Pod spec部分,runtime允许修改的只有 cotainer image了。

    > 比较新的信号应该不止31个了,还增加了31个可靠信号,为了解决以前linux中信号堆积忽略信号的问题。

    你说的没错,还有新的 32-64 可靠信号。因为和这一讲的问题没有太大关系,在这里就不展开了。

    2020-11-19
    3
  • Geek_71d4ac
    关于特权信号的那一段表述,我觉得是有问题的。当任务处于task uninterrupt状态时,是不能接收任何信号的。

    作者回复: @Geek_71d4ac
    你说的没错, D state (uninterruptible) 进程 还有 Zombie进程都是不能接受任何信号的。我在后面的章节里还有介绍。

    2020-11-18
    3
  • JianXu
    if (unlikely(t->signal->flags & SIGNAL_UNKILLABLE) && handler == SIG_DFL && !(force && sig_kernel_only(sig))) { return true;}

    老师, 我是不是可以这样理解,在容器内部的时候对于没有安装SIGTERM handler的情况下,force=0 并且SIGNAL_UNKILLABLE 也是置位的,所以这个if 语句返回真,所以SIGTERM 被忽略。但是在宿主机上的时候,因为不是同一个namespace 所以force = 1 ,因为不是宿主机上的第一个进程所以 UNKILLABLE 也没有置位(其实在force=1的时候已经不重要了) 所以这个 if 返回false, 而因为不是Kernal sig, 所以接下来第三个 if 也不会返回true, 于是这三个if 都不会起作用,所以从宿主机可以干掉该进程。

    能不能进一步介绍一下为什么在Kernel 里面要放置这三个 if 语句来 ignore signal 呢?

    作者回复: @JIanxu,
    > 因为不是宿主机上的第一个进程所以 UNKILLABLE 也没有置位

    这个SIGNAL_UNKILLABLE 是目标进程(容器里的1号进程)在创建的时候置位的,所以无论发送信号的进程在容器namespace还是在host namespace, 这个flag都是存在的。

    > 能不能进一步介绍一下为什么在Kernel 里面要放置这三个 if 语句来 ignore signal 呢?

    这里可以看一下这段代码最初check-in的comments. 我的理解是如果1号进程被杀,会是整个系统处于混乱并且难调试的状态,我们要尽量的避免这种情况。

    commit 86989c41b5ea08776c450cb759592532314a4ed6
    Author: Eric W. Biederman <ebiederm@xmission.com>
    Date: Thu Jul 19 19:47:27 2018 -0500

        signal: Always ignore SIGKILL and SIGSTOP sent to the global init

        If the first process started (aka /sbin/init) receives a SIGKILL it
        will panic the system if it is delivered. Making the system unusable
        and undebugable. It isn't much better if the first process started
        receives SIGSTOP.

        So always ignore SIGSTOP and SIGKILL sent to init.

        This is done in a separate clause in sig_task_ignored as force_sig_info
        can clear SIG_UNKILLABLE and this protection should work even then.

        Reviewed-by: Thomas Gleixner <tglx@linutronix.de>
        Signed-off-by: "Eric W. Biederman" <ebiederm@xmission.com>

    2020-11-19
    2
  • 争光 Alan
    能不能讲完后,讲下社区的最佳实践,比如docker现在提供了docker run --init参数避免这个问题,内核层面有没有相关的优化跟进

    作者回复: @争光Alan, 谢谢,很好的建议!
    我在这一章的最后,也拿了tini做了example来做一些简单best practice的说明。

    2020-11-19
    2
  • 朱雯
    关于思考题:
    这一讲的最开始,有这样一个 C 语言的 init 进程,它没有注册任何信号的 handler。如果我们从 Host Namespace 向它发送 SIGTERM,会发生什么情况呢?
    啥叫从host namespace向他发送sigterm,这是啥意思,是宿主机对他发送sigterm吗,宿主机发送,那就直接把他杀了,不仅法sigterm会杀,发kill也多半会杀,因为在宿主机,不同的namespace,force不一定为0,所以肯定不会被忽略,我的问题在于SIGNAL_UNKILLABLE 标签还会不会打上,打上以后是对宿主机这个标签也生效吗。

    作者回复: > 啥叫从host namespace向他发送sigterm,这是啥意思,是宿主机对他发送sigterm吗
    是的,在宿主机上的缺省namespace是host namespace.

    > 不同的namespace,force不一定为0
    是的, signal sender不在同一个namespace的时候, force不为0.

    > 我的问题在于SIGNAL_UNKILLABLE 标签还会不会打上,打上以后是对宿主机这个标签也生效吗。
    进程创建后这个标签是一直有的,只是pid在容器namespace里看到的是1,而在宿主机的namespace里是另外一个pid值

    2020-11-19
    2
  • Geek_8664a7
    老师,请教下,我用docker方式起了MSSQL server exporter来监控数据库,但容器经常会退出(系统资源充足),我看日志最后是无法连接数据库了,但数据库是Azure上的,都是正常的,是否有办法让容器自动重连数据库

    作者回复: 1. 你可以解决程序无法连接数据而退出的问题。
    2. 或者你需要一个controllerl来监控容器状态,自动使容器重启,这个如果使用kubernetes就可以了

    2021-01-19
    1
  • 春华秋实
    讲解的很深入,这是我学到讲解最深的,跟着老师好好学

    作者回复: 谢谢,华仔!

    2020-12-01
    1
  • po
    老师,
    问题A: 那么对于容器内无法通过sigterm或者sigkill杀掉容器PID1,那么如何重启容器呢?

    问题B:对于容器内无法通过sigterm或者sigkill杀掉容器,是不是在宿主机上可以直接通过sigterm或者sigkill杀掉容器在宿主机上的那个进程?

    作者回复: > 问题A
    在容器内部无法kill 1/ kill -9 1, 但是在宿主机上是可以SIGKILL 容器pid1在host namespace里对应的pid的。

    > 问题B
    是的。在宿主机上SIGKILL肯定可以杀掉容器的pid1, 而SIGTERM也要看情况。

    2020-11-20
    1
  • ch_ort
    “ 容器中,1 号进程永远不会响应 SIGKILL 和 SIGSTOP 这两个特权信号 ”
    容器外,1号进程响应 SIGKILL和SIGSTOP吗?

    核心问题是:是所有1号进程都忽略了缺省行为的信号,还是只有容器的1号进程?
    2022-01-19
收起评论
53
返回
顶部