土豆不好吃

Linux怎么让程序持续运行:简单说说几种好玩的办法

文章目录[显示]
这篇文章在 2017年12月17日21:44:36 更新了哦~

这段时间,有小伙伴问我,怎么能让程序在关掉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)默认的动作是终止程序。

那父进程要是先被终止、退出了,子进程会怎么样?一般来说有两种:

  1. 过继(收养)给init,孤儿进程
  2. 被划为孤儿进程组,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,常用的服务相关命令有startstoprestartreloadstatusenabledisablemask(禁用),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字段给出文档路径;

AfterBefore指定启动顺序(只是顺序,不是依赖),比如说如果在网络服务之后启动,那么就应该写成

After=network.target

依赖关系,需要使用WantsRequires

Wants表示依赖关系是可选的;Requires依赖关系是必须的,如果被依赖的服务启动失败或异常退出,那么依赖者也必须退出。

service示例:service

service中最重要的就是ExecStart,定义启动进程时执行的命令,需要使用绝对路径,类似的还有ExecReload(重启服务时执行)、ExecStop(停止服务时执行)、ExecStartPre(启动服务之前执行)、ExecStartPost(启动服务之后执行)、ExecStopPost(停止服务之后执行)

Type定义启动类型,有好多种,比如说simpleforkingoneshot……

咱一般用simple比较多,意思是ExecStart字段启动的进程为主进程。

KillMode:定义 如何停止服务,有control-group、process、mixed、none

control-group(默认值):当前控制组里面的所有子进程,都会被杀掉

process:只杀主进程,会话保留

mixed:主进程将收到 SIGTERM 信号,子进程收到 SIGKILL 信号

none:没有进程会被杀掉,只是执行服务的 stop 命令。

我们来看下sshd的配置:

知道为什么sshd服务关了,当前连接还不断吧。

Restart定义重启字段,有这么几个值:on、always、on-success、on-failure、on-abnormal、on-abort、on-watching,具体详情如下表:

还有一个RestartSec,指的是重启服务之前等待几秒。

Environment定义环境变量。一个比较坑的地方是systemd无法读取/etc/profile,.bashrc等环境变量,systemd启动的服务也读取不到这些环境变量(知道为啥用绝对路径了吧),有这么几种解决方案:

1.修改systemd配置文件,使得环境变量在所有单元中可见

/etc/systemd/user.conf文件中使用 DefaultEnvironment 选项。

2.修改单元配置文件,使得环境变量在用户单元中可见

systemctl edit --full expressbot.service(或者找到对应的文件路径,比如说/lib/system/system/expressbot.service) 下增加配置文件设置。

  1. 导入变量

在任何时候, 使用 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就好了嘛。

思考一下:cron最短时间周期是一分钟一次,我就想十秒钟一次,该怎么办?

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呢……没用的,你杀不掉的。


文章版权归原作者所有丨本站默认采用CC-BY-NC-SA 4.0协议进行授权|
转载必须包含本声明,并以超链接形式注明原作者和本文原始地址:
https://dmesg.app/linux-keep-running.html
退出移动版