这段时间,有小伙伴问我,怎么能让程序在关掉ssh之后还能继续运行?我简单的的说了一句开个screen吧。
实际上呢,在这之后还是有一些比较深奥的原理的,咱就扯扯。
不得不的说的SSH
用Windows的远程桌面的时候,咱可能有这种感受:把远程桌面断开了,里面的程序也欢快的跑着ε=ε=ε=(~ ̄▽ ̄)~但是在Linux我们把ssh连接断开,正在跑的程序却会立刻挂掉。这是为啥捏~( ̄▽ ̄)~*
话说,咱连接到服务器,都是有sshd这个服务来负责的;连接成功之后,sshd会启动bash,之后咱在运行的命令什么的,相当于bash来帮我们启动。
此时的进程树大概是这么样子的:
init->sshd->bash->some_program
通过pstree
可以大概看到进程树的结构
不能继续讲了,因为此时我们要说信号量!
信号量
在不远的过去,有一门课叫《操作系统》,有一个章节叫信号量,有一个哥们叫迪杰斯特拉我一直管他叫迪杰特斯拉(爱迪生:说好的你爱我呢)
噗,跑题了。
话说在Linux下,咱是如何停止某个程序运行的呢?Ctrl+C,没错,但是只有前台进程才会体验到。
通常来说,咱会使用kill来结束某个进程。kill命令会给进程发送一个信号,然后由进程来处理(捕获)这个信号。这就好像是医生跟你说,你可以死了,然后你就选择……自杀吗/(ㄒoㄒ)/~~
kill用法很简单,简单点,一个PID;复杂点,一个signal,一个PID。
也就是说,你想杀掉PID为1087的进程,温柔点就kill 1087
好了。
默认来说,kill会发送SIGTERM这个信号。
使用kill -l
可以列出来所有的信号:
通常有这么几个信号是比较常用的、应该被记住的:
1 - SIGHUP:挂起信号,当终端连接断开时就会发送这个信号;
2 - SIGINT:中断信号,也就是按Ctrl+C;
9 - SIGKILL:最强大的信号,相当于爆头击杀,不能被捕获;进程收到这个信号就会立刻拜拜;
15 - SIGTERM:终止信号,kill默认发送的信号,这就是告诉进程尽可能的终止,咱得跟进程爸爸一个告别儿女收拾善后的一个机会嘛;
19 - SIGSTOP:中断进程,也不能被捕获。咱别看这里写的是STOP,实际上是暂停的意思啦,等同于Ctrl+Z。用18号就恢复了~
这话说的太抽象了,咱不如写个bash脚本来体验一下这几个信号量吧。
#!/bin/bash trap "" INT sleep 10
这代码不能再简单了。运行这个脚本,然后按Ctrl+C,咱会发现这程序并不退出,
因为INT被捕获并忽略啦。
其实bash的坑很多,比如说,trap啥时候运行?咱以上的直觉可能是,接收到对应的信号,trap就会立刻做出相应与处理。其实不是的.
实际上,bash的行为比较特殊。当按下 CTRL-C 之后,它会向当前的整个进程组发出 SIGINT 信号。而 sleep 是由当前脚本调用的,是这个脚本的子进程(
/bin/sleep
)。如果当前正有一个外部命令(
/bin/sleep
)在前台执行,那么 trap 会等待当前命令结束以后再处理信号队列中的信号。也就是trap要等sleep执行完才会捕捉到了信号。前台后台与作业控制
正常咱们的程序是跑在前台的,如果想跑到后台,那么命令结尾加个&
就好了。
比如说bash demo.sh&
那就被丢到后台去执行了,执行完成会弹出来告诉我们done
把正在运行的暂停并丢到后台,那就Ctrl+Z;查看作业列表,就用jobs
;拿回前台就用fg
;把暂停的继续运行就用bg
……
不知道大家懂了没,哈哈哈哈。
连接断开时
首先咱要知道,挂断信号(SIGHUP)默认的动作是终止程序。
那父进程要是先被终止、退出了,子进程会怎么样?一般来说有两种:
- 过继(收养)给init,孤儿进程
- 被划为孤儿进程组,SIGHUP杀死
孤儿进程是先死了爹的进程,可能会有继父(init),也可能被归入进程组然后被SIGHUP杀死;
那要是子进程先死了呢?自然子进程会先把资源释放,然后父进程会给收尸(调用wait),然后内核回收进程控制块(PCB)。假如父进程因为种种原因没调用wait那么子进程的PCB仍然保存在系统中,就成为了僵尸进程。放心,kill 9是杀不掉僵尸进程的,遇到僵尸进程就杀死对应的父进程,然后init就会成为僵尸进程的爹,之后wait回收。
简单的说,sshd发现连接断开之后,bash收到SIGHUP信号从而关闭其所有子进程。(注意,bash的行为比较诡异,如果是输入exit,那就不会发送SIGHUP,也就是说你exit退出的,那么脚本还是欢快的跑下去的)
所以自然终端断了,大家都被SIGHUP干掉了呗~SIGHUP是元凶啊!
一点点小实验:
让我们连到服务器,然后
systemctl stop sshd.service
,然后惊奇的发现当前终端还活着。想想这是为什么?后面我会说的……好了赶紧启动吧,要不当前连接挂了那就不好玩了。
保持程序可靠运行的几种方法
既然咱都知道是因为SIGHUP关闭的了,那咱让程序忽略这个信号不就好了嘛!
几种不好用的办法
比如说&
,Ctrl+Z,这都是不好用的……照样还是处于一个session中,还是sshd->bash->program,还是会收到SIGHUP然后拜拜。
nohup
SIGHUP是元凶,那么nohup
就好了。
比如说:
nohup ping z.cn
这样程序的输出会被重定向到nohup.out
中,更通常我们会nohup ping z.cn &
顺便给丢到后台,此时进程树还是init->sshd->bash->ping,如果我们关闭终端,那么ping
就会成为孤儿进程,然后过继给init,此时我们pstree
一下会发现变为init->ping了
setsid
nohup
是通过忽略SIGHUP来保持运行的,那咱能不能直接让ping
不属于当前会话,自然也就收不到关闭终端时给进程组发的SIGHUP了?能啊怎么不能呢,setsid
啊,用例如下:
setsid ping z.cn
此时pstree
和上面是一样的,ps
能够看到他的父进程是init,(init:我就是万年备胎,专门收垃圾)
disown
如果一不小心程序已经运行了,那么该怎么补救呢?
答案是使用disown
来控制作业(jobs),举例:
sleep 100 disown -h %1
关闭终端之后依旧会持续运行,只是不能用jobs
控制了(但应该能用盖茨控制)。
感觉似乎以上方法都不太好,要么查看输出太费事,要么用起来很别扭,那有什么更好的办法吗?当然了!
screen
screen
是个非常强大的工具,大概就是模拟VT100/ANSI的窗口管理器。说不明白了。反正我大概是这么用的:
screen -S dl
做一点事情……
要回来的时候,screen -r dl
就回到了终端关闭之前的界面。
关掉终端,咱能看到进程树是init->screen->bash->ping
某天我写了个程序
某天我写了个程序,想让它永不停息,于是乎我用了screen,然后某一天这个程序抛异常了,然后我screen回去看了一眼继续启动;某天服务器重启了,我进去再screen……周而复始,这个程序名字叫做ExpressBot,烦不烦呐!!
所以咱得让这个程序成为服务。顺便说一句,比如说你要是官方二进制的MongoDB、MySQL,那么用systemd或者supervisor守护下也是非常好的选择。
使用systemd
systemd是Linux下最流行的init,由Lennart Poettering带头开发。发音不是system d,而是System Five Hundred 因为D是罗马数字里的500.
Lennart:我就是想怼你们的教皇。
命令概述
关于systemd,Arch Linux有一篇写的非常详尽的wiki,我们要想给自己的程序做成服务,通常是要添加单元(unit),单元可以是系统服务(.service)、挂载点(.mount)、sockets(.sockets) 、系统设备(.device)、交换分区(.swap)、文件路径(.path)、启动目标(.target)、由 systemd 管理的计时器(.timer)。如果没有扩展名,那么就会被当作服务,比如说sshd和sshd.service就是一个意思。
system的主要命令是systemctl
,常用的服务相关命令有start
,stop
,restart
,reload
,status
,enable
,disable
,mask
(禁用),unmask
(取消禁用),daemon-reload
,举例(需要root权限):
#立即启动单元: systemctl start <单元> #重新载入 systemd,扫描新的或有变动的单元,在修改了单元文件之后是非常必要的: systemctl daemon-reload #一些其他命令举例: # 重启系统 systemctl reboot # 关闭系统,切断电源 systemctl poweroff #查看启动耗时 systemd-analyze #显示某个 Unit 是否正在运行(一般用于脚本判断) systemctl is-active sshd.service #修改单元 systemctl edit --full sshd.service
systemd单元加载路径
单元文件是有一定的加载顺序的,先加载/etc/system/system
,然后是/run/systemd/system
,最后是/lib/systemd/system
,如下图所示:
service示例:Unit
一个配置文件可以分为unit、service、install 这几个模块。
unit定义描述、启动顺序、依赖等,
Description
给出当前服务的简单描述,Documentation
字段给出文档路径;
After
和Before
指定启动顺序(只是顺序,不是依赖),比如说如果在网络服务之后启动,那么就应该写成
After=network.target
依赖关系,需要使用Wants
和Requires
Wants
表示依赖关系是可选的;Requires
依赖关系是必须的,如果被依赖的服务启动失败或异常退出,那么依赖者也必须退出。
service示例:service
- Exec
service中最重要的就是ExecStart
,定义启动进程时执行的命令,需要使用绝对路径,类似的还有ExecReload
(重启服务时执行)、ExecStop
(停止服务时执行)、ExecStartPre
(启动服务之前执行)、ExecStartPost
(启动服务之后执行)、ExecStopPost
(停止服务之后执行)
- Type
Type定义启动类型,有好多种,比如说simple
、forking
、oneshot
……
咱一般用simple
比较多,意思是ExecStart
字段启动的进程为主进程。
- KillMode
KillMode:定义 如何停止服务,有control-group、process、mixed、none
control-group(默认值):当前控制组里面的所有子进程,都会被杀掉
process:只杀主进程,会话保留
mixed:主进程将收到 SIGTERM 信号,子进程收到 SIGKILL 信号
none:没有进程会被杀掉,只是执行服务的 stop 命令。
我们来看下sshd的配置:
知道为什么sshd服务关了,当前连接还不断吧。
- Restart
Restart定义重启字段,有这么几个值:on、always、on-success、on-failure、on-abnormal、on-abort、on-watching
,具体详情如下表:
还有一个RestartSec
,指的是重启服务之前等待几秒。
- Environment
Environment
定义环境变量。一个比较坑的地方是systemd无法读取/etc/profile
,.bashrc
等环境变量,systemd启动的服务也读取不到这些环境变量(知道为啥用绝对路径了吧),有这么几种解决方案:
1.修改systemd配置文件,使得环境变量在所有单元中可见
在 /etc/systemd/user.conf
文件中使用 DefaultEnvironment
选项。
2.修改单元配置文件,使得环境变量在用户单元中可见
systemctl edit --full expressbot.service
(或者找到对应的文件路径,比如说/lib/system/system/expressbot.service
) 下增加配置文件设置。
- 导入变量
在任何时候, 使用 systemctl --user set-environment
或 systemctl --user import-environment
. 对设置之后启动的所有用户单元有效,但已经启动的用户单元不会生效。
例子:
[Service] Environment="TOKEN=12345" Environment="DB_PATH=/home/ExpressBot/expressbot/bot.db" Environment="TURING_KEY=111111" Environment="DEBUG=0" Restart=always Type=simple ExecStart=/usr/bin/python /home/ExpressBot/expressbot/main.py
此时在这个python脚本中就可以用os.environ.get('DEBUG')
来获取到环境变量啦(类型全部为字符串)。其实这种方式也方便更新,不用再merge了。
service示例:install
install
就比较简单了,就是定义什么情况下启动。
比如说WantedBy=multi-user.target
就意味着在多用户环境下会启动,WantedBy= graphical.target
表示图形用户下启动。
完整配置文件:
[Unit] Description=A Telegram Bot for querying expresses After=network.target network-online.target nss-lookup.target [Service] Environment="TOKEN=12345" Environment="DB_PATH=/home/ExpressBot/expressbot/bot.db" Environment="TURING_KEY=111111" Environment="DEBUG=0" Restart=always Type=simple ExecStart=/usr/bin/python /home/ExpressBot/expressbot/main.py [Install] WantedBy=multi-user.target
更多详情可以参考这里
不好玩的轮子
在以前,我是怎么检测服务是否还在运行的呢……
基本思路是,pidof
能获取到运行中的进程的id,然后根据$?
判断是否还在运行,比如说……
pidof php-fpm >/dev/null if [ $? -eq 0 ] ; then echo "It is running." else echo "At `date` PHP Server was stopped">> /home/wwwlogs/service_log fi
然后加入到crontab中(值得一提的是,systemd也有个timer,类似于crontab,可以参考Arch Linux)。
这样做其实不是很理想,最长可能会导致服务中断近一分钟,这绝对是莱洛三角形。
所以,直接给Restart字段一个on-abnormal
或者on-failure
就好了嘛。
systemd的优缺点
systemd的功能十分强大,配置文件也好写,而且systemd比较稳定(它要是挂了,那系统就挂了)。但是有一点可能不太好,就是并不是所有系统的init都是systemd(systemd争议有点大),比如说Ubuntu 14.04就不是(16.04是,跟上游Debian),Devuan(一个不用systemd的Debian)。哦对了,systemd看日志感觉不是那么方便
另一个备选方案是supervisor,python咱都有吧……
Supervisor
Supervisor 是一个用 Python 写的进程管理工具,可以很方便的用来启动、重启、关闭进程(不仅仅是 Python 进程)。
先用pip安装:
pip install supervisor
配置supervisord
同样,supervisor也有一个配置文件优先路径,$CWD/supervisord.conf
> $CWD/etc/supervisord.conf
> /etc/supervisord.conf
,
我一般会创建一个/etc/supervisord.conf
,然后创建一个/etc/supervisor/SomeService.conf
,然后在/etc/supervisord.conf
中包含后者。
如下创建默认配置:
echo_supervisord_conf > /etc/supervisord.conf
最后一行包含配置:
[include] files = /etc/supervisor/*.conf
然后创建如下“单元”:
[program:expressbot] directory = /home/ExpressBot/expressbot/main.py command = /usr/bin/python /home/ExpressBot/expressbot/main.py autostart = true ; 在 supervisord 启动的时候也自动启动 startsecs = 5 ; 启动 5 秒后没有异常退出,就当作已经正常启动了 autorestart = true ; 程序异常退出后自动重启 startretries = 3 ; 启动失败自动重试次数,默认是 3 user = root ; 用哪个用户启动 redirect_stderr = true ; 把 stderr 重定向到 stdout,默认 false stdout_logfile_maxbytes = 20MB ; stdout 日志文件大小,默认 50MB stdout_logfile_backups = 20 ; stdout 日志文件备份数 stdout_logfile = /var/log/expressbot_stdout.log ;日志文件
然后运行supervisord就可以启动supervisor啦。
控制supervisor
类似systemd,supervisor也有个supervictl
(配置文件查找顺序与supervisord一样),直接输入会进入交互模式,命令主要有:
status、stop、restart、reread(读取有更新新增的配置文件,不会启动新添加的程序)、update(重启配置文件修改过的程序),示例:
或者直接在bash下supervisorctl restart WebTerminal
也是可以的。
注意:有的时候更新配置文件会没用,此时就要supervisorctl reload
啦。
supervisor优缺点
相比systemd,supervisor的特点是精准、小而全,毕竟systemd是大管家嘛;supervisor跨平台,不受init的限制;supervisor提供一个Web页面,使用用户名密码的基本身份验证(在/etc/supervisord.conf
中配置)。
最大的缺点就是,万一我手残kill了supervisord,那也就完蛋啦~可是我要是kill了init呢……没用的,你杀不掉的。