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