前提概要
信号概念
进程间通信
- socket
- 消息队列
- 管道:类似瀑布开发模式
- 共享内存
- 信号量:一般和共享内存一起使用
- 信号:应急事件、通知
信号,一般是异常情况下的工作模式(其他几种通信方式是常规情况下的),是进程间通信唯一一种异步通信方式,即可以在任何时候发送信号给一个进程。
为了响应各种各样的事件,定义了下面64种信号:
|
|
对收到的信号有三种处理方式
默认动作
term,终止进程;core,终止进程后,使用core dump将当前进程运行状态保存至文件
捕捉
自定义信号处理函数,信号递达时执行相应函数
忽略
不希望处理某些信号的时候,可以忽略此信号。其中SIGKILL/SEGSTOP两个信号是应用程序无法捕捉和忽略的,不然操作系统都没权限管理了
|
|
上表中第一列是各信号的宏定义名称,第二列是各信号的编号,第三列是默认处理动作,Term
表示终止当前进程,Core
表示终止当前进程并且Core Dump,Ign
表示忽略该信号,Stop
表示停止当前进程,Cont
表示继续执行先前停止的进程。
通信过程来看:
是注册信号处理函数
注册函数的系统调用有signal或sigaction函数,将方法和信号关联起来,进程遇到对应信号就执行此方法
发送信号
发送信号(硬件方式和软件方式):
- Ctrl+C: SIGINT 终止;Ctrl+Z: SIGTSTP 停止(硬件);Ctrl-\产生SIGQUIT信号
- 进程访问了非法内存,内存管理模块MMU就会产生异常,然后把信号 SIGSEGV 发送给进程;除0的指令:CPU产生SIGFPE信号给进程 (硬件)
- 当子进程退出时,我们要给父进程发送 SIG_CHLD 信号
- 向读端已关闭的管道写数据时产生 SIGPIPE 信号
- kill等命令执行系统调用
进程或者shell发送信号有四个函数可调用kill/tkill/tgkill/rt_sigqueueinfo,最终调用do_send_sig_info函数,将信号放在对应进程的task_struct的信号数据结构中,信号注册有个判断条件:
- 小于32的信号存于集合中,集合中已注册此信号不会再次注册,因此可能会丢失,称为不可靠信号
- 大于32的存于链表上,可以重复注册,是可靠信号,不会丢失。
ulimit -a
中的pending signals即是队列长度
处理信号
信号产生(Generation)后,实际执行信号的处理动作称为信号递达(Delivery),信号从产生到递达之间的状态,称为信号未决(Pending)。
每个信号都有两个标志位分别表示阻塞和未决,和一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。进程可以选择阻塞(Block)某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:
- 用户程序注册了
SIGQUIT
信号的处理函数sighandler
。 - 当前正在执行
main
函数,这时发生中断或异常切换到内核态。 - 在中断处理完毕后要返回用户态的
main
函数之前检查到有信号SIGQUIT
递达。 - 内核决定返回用户态后不是恢复
main
函数的上下文继续执行,而是执行sighandler
函数,sighandler
和main
函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler
函数返回后自动执行特殊的系统调用sigreturn
再次进入内核态。- 如果没有新的信号要递达,这次再返回用户态就是恢复
main
函数的上下文继续执行了。
内核处理进程收到的signal是在当前进程的上下文,故进程必须是Running状态。当进程唤醒或者调度后获取到CPU,则在从内核态转到用户态时检测是否有signal等待处理。
- 用户程序注册了
信号示例
Core动作:
```bash # 默认关闭,`ulimit -c 1024`设置当前shell进程core file size为1024K (test) [root@whatfuck test]# ulimit -c 0 (test) [root@whatfuck test]# ll 总用量 16 -rw-r--r-- 1 root root 138 6月 28 17:06 Pipfile -rw-r--r-- 1 root root 718 6月 28 17:20 process_test.py -rwxr-xr-x 1 root root 49 6月 28 16:09 test_signal.sh -rwxr-xr-x 1 root root 90 6月 28 18:24 test_zombie.sh (test) [root@whatfuck test]# ulimit -c 1024 (test) [root@whatfuck test]# ulimit -c 1024 (test) [root@whatfuck test]# ./test_signal.sh & [1] 19756 (test) [root@whatfuck test]# kill 19756 (test) [root@whatfuck test]# [1]+ 已终止 ./test_signal.sh (test) [root@whatfuck test]# (test) [root@whatfuck test]# ll 总用量 16 -rw-r--r-- 1 root root 138 6月 28 17:06 Pipfile -rw-r--r-- 1 root root 718 6月 28 17:20 process_test.py -rwxr-xr-x 1 root root 49 6月 28 16:09 test_signal.sh -rwxr-xr-x 1 root root 90 6月 28 18:24 test_zombie.sh (test) [root@whatfuck test]# ./test_signal.sh & [1] 19800 (test) [root@whatfuck test]# kill -11 19800 (test) [root@whatfuck test]# [1]+ 段错误 (吐核)./test_signal.sh (test) [root@whatfuck test]# (test) [root@whatfuck test]# ll 总用量 268 -rw------- 1 root root 479232 6月 28 19:07 core.19800 -rw-r--r-- 1 root root 138 6月 28 17:06 Pipfile -rw-r--r-- 1 root root 718 6月 28 17:20 process_test.py -rwxr-xr-x 1 root root 49 6月 28 16:09 test_signal.sh -rwxr-xr-x 1 root root 90 6月 28 18:24 test_zombie.sh (test) [root@whatfuck test]# (test) [root@whatfuck test]# gdb -c core.19800 bash --quiet Reading symbols from /usr/bin/bash...Reading symbols from /usr/lib/debug/usr/bin/bash.debug...done. done. warning: core file may not match specified executable file. [New LWP 19800] Core was generated by `/bin/bash ./test_signal.sh'. Program terminated with signal 11, Segmentation fault. #0 0x00007fc04c2c14fc in __libc_waitpid (pid=pid@entry=-1, stat_loc=stat_loc@entry=0x7ffd1f5e3830, options=options@entry=0) at ../sysdeps/unix/sysv/linux/waitpid.c:31 31 return INLINE_SYSCALL (wait4, 4, pid, stat_loc, options, NULL); (gdb) ```
nginx进程控制
参考 http://nginx.org/en/docs/control.html和https://zhuanlan.zhihu.com/p/31512482
僵尸进程
什么情况下产生僵尸进程?
进程的终止过程:
进程执行终止动作时,会关闭所有文件描述符,释放在用户空间分配的内存,但它的退出信息还在进程表PCB中保留着:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait
或waitpid
获取这些信息,然后再彻底清除掉这个进程。举个栗子,一个进程的退出状态可以在Shell中用特殊变量$?
查看,因为Shell是它的父进程,当它终止时Shell调用wait
或waitpid
得到它的退出状态同时彻底清除掉这个进程。
如果一个进程已经终止,但是它的父进程尚未调用wait
或waitpid
对它进行清理,这时的进程状态称为僵尸(Zombie)进程。任何进程在刚终止时都是僵尸进程,它是一个正常的中间过程,僵尸进程正常情况下立刻被父进程清理。
🧟♀️进程不占用内存和cpu资源,但是进程表是有限资源,进程表满了那么其他进程当然就没法启动了。
🧟♀️进程是不能用kill
命令清除掉的,因为kill
命令只是用来终止进程的,而僵尸进程已经处于终止状态了。
如何处理🧟♂️进程?
kill杀死元凶父进程(一般不用),使得僵尸进程进程变成孤儿进程,由init充当父进程,并回收资源
父进程用wait或waitpid去回收资源(方案不太好,是个阻塞操作)
SIGCHLD信号机制,在处理函数中调用wait回收资源
子进程在终止时操作系统会给父进程发
SIGCHLD
信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD
信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait
清理子进程即可。僵尸进程kill -9是没用的,因为它本身已经执行完了,处于dead状态了
🧟♀️进程模拟:
|
|
孤儿进程
下面情况就是孤儿进程:
- 父进程终止
- 子进程还存在(这些子进程仍在运行或者已经是僵尸进程了)
这些子进程会立刻被init
进程收养,孤儿进程仍然使用它能使用的所有资源。PID 1有一个独特的职责,就是收割孤儿进程,孤儿进程退出后,由init负责调用wait()
了解下init(systemed)进程,它是一个由内核启动的用户级进程。在centos7中,grub2引导内核被载入内存得到系统控制权,并初始化所有设备驱动和数据结构等,之后就会启动一个用户级进程systemed,由systemd完成后续的进程启动,如终端等。init进程是其他进程的祖先,pstree -lps
命令查看进程树。
孤儿进程模拟:
|
|
容器停止
要考虑的问题:
- 应用进程优雅关闭
- 孤儿/🧟♀️进程问题
容器关闭
docker容器停止命令:docker stop [container]
默认情况下,向容器中的主进程ID为1发送一个SIGTERM信号,超时时间10s,超时后,发送SIGKILL信号相当于docker kill(两者发送的信号类型和超时时间都可以使用相应参数更改默认行为),因此,有下面两点需要我们考虑:
容器应用要能够正确接收到信号
容器应用要能够正确的响应对应的信号才能优雅的关闭
此处大家可以分析下tomcat、dubbo、rocketmq对kill -15信号的是怎么处理的?
容器中pid为1的主进程如何接收信号?写Dockerfile构建容器镜像时,容器中的进程运行命令CMD和ENTRYPOINT有两种写法,分别是shell和exec格式,shell格式的ENTRYPOINT
和CMD
是 /bin/sh -c
的子进程,即应用主进程ID不是1,而是sh,应用收不到docker发来的signals
示例1,能收到信号:
|
|
|
|
|
|
示例2,不能收到信号:
|
|
|
|
一般我们用一个脚本启动程序时,需要确保最终执行的命令是exec或者gosu,即使用新的进程替代原有的进程,保持 PID 不变,不产生新的进程,bash脚本默认是在子shell中执行的:
|
|
如果在停止容器时需要进行一些额外清理工作,与其他容器通信或者协调多个进程,这时需要确保ENTRYPOINT脚本能接收到信号执行相应的动作或者传递给其他进程,如官方示例:
|
|
以上就是docker容器中的进程接收信号和处理信号需要注意的点。
僵尸进程问题
以jenkins为例,类似Jenkins这种应用,🧟♀️进程不可避免的,因为Jenkins代码通常不是由Jenkins维护者编写的,比如我们自己的Jenkins构建脚本。这种情况下就非常有必要去避免出现很多🧟♀️进程的现象。
BASH同样可以收割僵尸进程,但是它默认不会转发信号,除非自己写代码实现,另外bash默认忽略SIGQUIT(3)和SIGTERM(15)信号。
tini通过信号转发解决了这个问题,发向tini的信号,tini也会向子进程发送同样的信号。Jenkins-Dockerfile
tini最主要的两个功能:
- 收割僵尸进程
- 信号转发
POD停止
通常在应用发布更,从应用停止到新版本启动恢复过程中,应保证客户无感知,传统的解决方式是通过将应用更新流程划分为手工摘流量、停应用、更新重启三个步骤,由人工操作实现客户端无对更新感知。这种方式简单而有效,但是限制较多:不仅需要使用借助网关的支持来摘流量,还需要在停应用前人工判断来保证在途请求已经处理完毕。这种需要人工介入的方式运维复杂度较高,只能适用规模较小的应用,无法在大规模系统上使用。
在k8s中,pod是k8s中创建和管理的、最小的可部署计算单元,pod代表的是集群上处于运行状态的一组容器,这些容器共享linux名称空间、控制组、以及怎样运行这些容器的声明等。
pod的停止设计目标是,令你能够请求删除进程,并且知道进程何时被终止,同时也能够确保删除操作最终完成。集群会记录跟踪pod的优雅停止,而不是直接强制干掉pod。
pod本身就是容器组,与容器停止基本一致,另外多了一个preStop钩子及移除端点的动作。
过程:
- API、kubectl等手动请求删除某个特定的pod,默认该pod优雅停止时间30秒
- APIServer中的该pod对象被更新,并记录它的最终死期,此时该pod对象的状态会更新为Terminating,kubelet监听到此状态变化后开始本地pod关闭过程:
- 若有定义preStop回调,kubelet变开始在容器内执行该回调逻辑。如果死期到了,preStop回调还在执行,那么kubelet还会再宽限2秒的时间(这种情况需要修改terminationGracePeriodSeconds属性)。
- kubelet触发容器运行时发送sigterm信号给pod中每个容器的主进程1(多个容器收到的信号顺序不确定的,顺序问题可以考虑preStop处理)
- 与此同时,控制平面将pod从对应的端点列表移除(service、切片等)
- 死期到了后,kubelet强制关闭容器,向pod中的所有容器发送sigkill信号
- APIServer上直接删除pod对象,那么就再没法从任何客户端看到此pod了