标题:生人勿近之Linux里养僵尸 出处:Felix021 时间:Sat, 11 Jul 2020 20:22:50 +0000 作者:felix021 地址:https://www.felix021.com/blog/read.php?2226 内容: Linux里养僵尸是怎么回事呢?Linux相信大家都很熟悉,但是Linux里养僵尸是怎么回事呢,下面就让小编带大家一起了解吧。 # - 1 - 上一篇挖了个 SIGHUP 的坑,这篇试着填一下。 之前在《程序员面试指北:面试官视角》里面说过,在结构化面试中,我们会从各个方向去考查候选人,其中之一是操作系统。 上篇介绍了一套题,我还有另一套,一般这么开场: 在终端下启动一个命令,如果在命令结束前关掉终端,它还能正常运行吗? 点击在新窗口中浏览此图片 https://www.felix021.com/blog/attachment.php?fid=562 # - 2 - 这其实是一个很常见的case,但凡 Linux 或者 Mac 用得多一点,都会遇到。 在我还是一个穷酸学生的2009年,每个月都需要支付 20 元巨款(当时能买3根鸭脖),通过一个禁止分享网络的认证客户端接入校园网。 为了共建和谐宿舍 节省网费 ,我历经千辛万苦,交叉编译开源的Linux认证客户端,集成到固件里,并刷到了我的 NETGEAR 路由器上。 然后山水 BBS 的 Linux 版主把我的帖子置顶了 11 年。可见他有多痛恨禁止共享网络 这么一回忆,感觉自己的共享经济思维真是前卫,当时怎么就没想到去搞共享单车呢? 点击在新窗口中浏览此图片 https://www.felix021.com/blog/attachment.php?fid=567 扯远了,在捣腾的过程中,我就踩了这么个坑:当我ssh到路由器上、刚启动认证时,能够正常联网;但是退出ssh后一会,网就断了。 经过一番捣腾后发现,只要一退出ssh,认证程序就凉了,而不是继续在后台保持和认证服务器的通信。 # - 3 - 所以前面那个问题,我以为大部分候选人应该会回答“否”,但没想到竟然还有不少人回答“是”。 其实回答“是”也没什么错,因为确实也有些命令不会随着终端关闭而结束。 问题是当我追问当时执行的是什么命令时,候选人往往又说不出个所以然来。 点击在新窗口中浏览此图片 https://www.felix021.com/blog/attachment.php?fid=563 (借学长的表情一用) 然后我就感到很强的挫败感:这不按剧本来,没法问了啊……只好换题。 当然大部分候选人确实被坑过,于是我可以接着问: 如果确实需要在后台继续执行命令怎么办呢? 有些人只记得要在后面加个 & ;但也有不少人知道前面还得加个 nohup,就像这样: $ nohup python process.py & [1] 1806824 nohup: ignoring input and appending output to 'nohup.out' 注:其实我更喜欢 screen(或 tmux),偶尔也用 setsid 。 然后就可以放心地关闭终端 开始放羊 了。 但我的套题还没结束:为什么加上 nohup 就可以让进程在后台继续运行呢? 点击在新窗口中浏览此图片 https://www.felix021.com/blog/attachment.php?fid=554 (这表情熟悉吗) # - 4 - 铺垫了这么多,总算是可以开始填坑了。 答案其实很好找,man nohup 就能看到: 引用 The nohup utility invokes utility with its arguments and at this time sets the signal SIGHUP to be ignored nohup工具在启动命令的同时会将 SIGHUP 信号设置为忽略。 而关于 SIGHUP,Wikipedia原文是这样介绍的: 引用 On POSIX-compliant platforms, SIGHUP ("signal hang up") is a signal sent to a process when its controlling terminal is closed. wikipedia.org/wiki/SIGHUP 对于 POSIX 兼容的平台(如Unix、Linux、BSD、Mac),当进程所在的控制终端关闭时,系统会给进程发送 SIGHUP 信号(Signal Hang Up,挂断信号)。 为什么叫 SIGHUP 呢?(严正申明:这一问不在套题里[doge]) 我们知道,在上古时代,捉 bug 就已经是码农的必备技能(更准确地说是 moth)。 点击在新窗口中浏览此图片 https://www.felix021.com/blog/attachment.php?fid=564 (我总觉得这个图是假的) 到了远古时代,他们不再需要去机房,通过基于 RS-232 协议的串行线路连接到大型机的终端上,就可以开始收福报。 收完福报,程序员通知自己的猫(modem)挂断(Hang Up)连接;大型机的 OS 检测到连接断开,就会给进程发送信号 —— 所以这信号被称为 SIGHUP 。 这果然是毫无卵用的知识啊。 点击在新窗口中浏览此图片 https://www.felix021.com/blog/attachment.php?fid=565 # - 5 - 很多同学在操作系统的课程上学习了“进程间的通信方式有信号、管道、消息队列、共享内存……”,但是对信号到底是个什么东西,并没有现实的概念。 课堂教学的理论和实践往往是割裂的,在此特别推荐《Unix环境高级编程》(简称APUE)。 APUE在 1.9 - 信号 中写到:信号是通知进程已发生某种条件的一种技术。 而在 Linux/Unix 下,进程对信号的处理有三种选择: * 按系统默认方式处理 * 提供一个回调函数 * 或忽略该信号(有些信号例外,不允许被忽略) 以 SIGHUP 信号为例,系统默认处理方式就是结束进程。 当然终端下打开的第一个进程通常都是shell(例如bash)。shell会给 SIGHUP 信号注册一个回调函数,用于给该 shell 下所有的子进程发送 SIGHUP 信号,然后再主动退出。 对于求生欲很强的程序(例如nohup),可以主动选择忽略该信号。 有一些进程本来就被设计成在后台运行,不需要控制终端,因此它们将 SIGHUP 挪作它用,一个常见的用法就是重新读取配置文件(例如Apache、Nginx),上篇提到的 logrotate 正是利用了这一点。 终于填完了坑。 # - 6 - 说了这么多都还是纸上谈兵,实操中如何主动忽略 SIGHUP 呢? 实际上也很简单,使用 Linux 的 signal 系统调用即可: #include #include int main() { signal(SIGHUP, SIG_IGN); sleep(1000); return 0; } 不妨试试看,编译运行起来,即使关闭终端,它也会在后台继续运行。 signal 也可以用于指定回调函数(或重置为系统默认处理方式),这里就不展开了,感兴趣的同学可以参考 APUE 里的代码,以及阅读 signal 的manual。 使用回调函数还需要注意一个坑: 由于回调函数可能在任意时刻被触发,因此要避免调用不可重入的函数(典型如printf)。常见的做法是 set 一个 flag,然后在程序的主循环中检测该 flag,再按需执行相应任务。 # - 7 - SIGHUP 只是常见的一个信号,在 Linux 下,信号还有大量其他的场景和应用。 当你按下 Ctrl + C ,就是给进程发送了一个 SIGINT 信号。 当你执行 kill -TERM $PID,就是给进程发送了一个 SIGTERM 信号。可能和你期望有出入的是,SIGTERM 是可以被进程忽略的。所以有时候你得用 SIGKILL (kill -9) 。 你还可以使用可自定义的 SIGUSR1、SIGUSR2、SIGURG 来实现一些功能,比如《踩坑记#2:Go服务锁死》中提到 Golang 在其 goroutine 调度中使用了 SIGURG 。 # - 8 - 这次就不总结了,最后再用一个和信号有关的 case 收尾。 Linux 内核会为每一个进程分配一个 task_struct 结构体,用于保存进程的相关信息。 在进程死亡后,系统会发送一个 SIGCHLD 信号给它的父进程。 正确的父进程实现,通常应当使用 wait 系统调用来给子进程收尸 —— 父进程往往需要知道子进程结束这个事件,而且可能还需要得知其退出原因(exit code)。 然后内核才会将对应的 task_struct 释放。 如果父进程没有收尸,task_struct 里的 state 会一直保持为 EXIT_ZOMBIE,这时在 ps 或 top 等命令里,就可以看到该进程的状态为 Z ,而且无法被 kill 。 这就是所谓的僵尸进程,这时候你找九叔都没用。 点击在新窗口中浏览此图片 https://www.felix021.com/blog/attachment.php?fid=566 (大半夜找这图还挺渗人的) 所以Linux里养僵尸,其实就是子进程死了父进程不收尸,大家可能会很惊讶Linux里怎么会养僵尸呢?但事实就是这样,小编也感到非常惊讶。 这就是关于Linux里养僵尸的事情了,大家有什么想法呢,欢迎在评论区告诉小编一起讨论哦! --- 推荐阅读 * 程序员面试指北:面试官视角 * 踩坑记:go服务内存暴涨 * TCP:学得越多越不懂 * UTF-8:一些好像没什么用的冷知识 * [译] C程序员该知道的内存知识 (1) Generated by Bo-blog 2.1.0