登录
  • 人们都希望被别人需要 却往往事与愿违
  • 不是具有中国特色的社会主义, 而是具有中国特色的社会达尔文主义!

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

Linux Benny 小土豆 17623 次浏览 8024 字 39 个评论
文章目录 [显示]
这篇文章在 2017 年 12 月 17 日 21:44:36 更新了哦~

这段时间,有小伙伴问我,怎么能让程序在关掉 ssh 之后还能继续运行?我简单的的说了一句开个 screen 吧。

实际上呢,在这之后还是有一些比较深奥的原理的,咱就扯扯

不得不的说的 SSH

用 Windows 的远程桌面的时候,咱可能有这种感受:把远程桌面断开了,里面的程序也欢快的跑着ε=ε=ε=(~ ̄▽ ̄)~ 但是在 Linux 我们把 ssh 连接断开,正在跑的程序却会立刻挂掉。这是为啥捏~( ̄▽ ̄)~*

话说,咱连接到服务器,都是有 sshd 这个服务来负责的;连接成功之后,sshd 会启动 bash,之后咱在运行的命令什么的,相当于 bash 来帮我们启动。

此时的进程树大概是这么样子的:

init->sshd->bash->some_program

通过pstree可以大概看到进程树的结构

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

不能继续讲了,因为此时我们要说信号量!

信号量

在不远的过去,有一门课叫《操作系统》,有一个章节叫信号量,有一个哥们叫迪杰斯特拉我一直管他叫迪杰特斯拉(爱迪生:说好的你爱我呢)

噗,跑题了。

话说在 Linux 下,咱是如何停止某个程序运行的呢?Ctrl+C,没错,但是只有前台进程才会体验到。

通常来说,咱会使用 kill 来结束某个进程。kill 命令会给进程发送一个信号,然后由进程来处理(捕获)这个信号。这就好像是医生跟你说,你可以死了,然后你就选择…… 自杀吗 /(ㄒ o ㄒ)/~~

kill 用法很简单,简单点,一个 PID;复杂点,一个 signal,一个 PID。

也就是说,你想杀掉 PID 为 1087 的进程,温柔点就kill 1087好了。

默认来说,kill 会发送 SIGTERM 这个信号。

使用kill -l可以列出来所有的信号:

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

通常有这么几个信号是比较常用的、应该被记住的:

1 - SIGHUP:挂起信号,当终端连接断开时就会发送这个信号;

2 - SIGINT:中断信号,也就是按 Ctrl+C;

9 - SIGKILL:最强大的信号,相当于爆头击杀,不能被捕获;进程收到这个信号就会立刻拜拜;

15 - SIGTERM:终止信号,kill 默认发送的信号,这就是告诉进程尽可能的终止,咱得跟进程爸爸一个告别儿女收拾善后的一个机会嘛;

19 - SIGSTOP:中断进程,也不能被捕获。咱别看这里写的是 STOP,实际上是暂停的意思啦,等同于 Ctrl+Z。用 18 号就恢复了~

这话说的太抽象了,咱不如写个 bash 脚本来体验一下这几个信号量吧。

  1. #!/bin/bash
  2. trap "" INT
  3. sleep 10

这代码不能再简单了。运行这个脚本,然后按 Ctrl+C,咱会发现这程序并不退出, Linux怎么让程序持续运行:简单说说几种好玩的办法

因为 INT 被捕获并忽略啦。

备注:
其实 bash 的坑很多,比如说,trap 啥时候运行?咱以上的直觉可能是,接收到对应的信号,trap 就会立刻做出相应与处理。其实不是的.
实际上,bash 的行为比较特殊。当按下 CTRL-C 之后,它会向当前的整个进程组发出 SIGINT 信号。而 sleep 是由当前脚本调用的,是这个脚本的子进程(/bin/sleep)。
如果当前正有一个外部命令(/bin/sleep)在前台执行,那么 trap 会等待当前命令结束以后再处理信号队列中的信号。也就是 trap 要等 sleep 执行完才会捕捉到了信号。

前台后台与作业控制

正常咱们的程序是跑在前台的,如果想跑到后台,那么命令结尾加个&就好了。

比如说bash demo.sh&那就被丢到后台去执行了,执行完成会弹出来告诉我们 done

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

把正在运行的暂停并丢到后台,那就 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就好了。

比如说:

  1. nohup ping z.cn

这样程序的输出会被重定向到nohup.out中,更通常我们会nohup ping z.cn &顺便给丢到后台,此时进程树还是 init->sshd->bash->ping,如果我们关闭终端,那么ping就会成为孤儿进程,然后过继给 init,此时我们pstree一下会发现变为 init->ping 了

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

setsid

nohup是通过忽略 SIGHUP 来保持运行的,那咱能不能直接让ping不属于当前会话,自然也就收不到关闭终端时给进程组发的 SIGHUP 了?能啊怎么不能呢,setsid啊,用例如下:

  1. setsid ping z.cn

此时pstree和上面是一样的,ps能够看到他的父进程是 init,(init:我就是万年备胎,专门收垃圾)

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

disown

如果一不小心程序已经运行了,那么该怎么补救呢?

答案是使用disown来控制作业(jobs),举例:

  1. sleep 100
  2. disown -h %1

关闭终端之后依旧会持续运行,只是不能用jobs控制了(但应该能用盖茨控制)。

感觉似乎以上方法都不太好,要么查看输出太费事,要么用起来很别扭,那有什么更好的办法吗?当然了!

screen

screen是个非常强大的工具,大概就是模拟 VT100/ANSI 的窗口管理器。说不明白了。反正我大概是这么用的:

  1. screen -S dl

做一点事情……

要回来的时候,screen -r dl就回到了终端关闭之前的界面。

关掉终端,咱能看到进程树是 init->screen->bash->ping

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

某天我写了个程序

某天我写了个程序,想让它永不停息,于是乎我用了 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 权限):

  1. #立即启动单元:
  2. systemctl start <单元>
  3. #重新载入 systemd,扫描新的或有变动的单元,在修改了单元文件之后是非常必要的:
  4. systemctl daemon-reload
  5. #一些其他命令举例:
  6. # 重启系统
  7. systemctl reboot
  8. # 关闭系统,切断电源
  9. systemctl poweroff
  10. #查看启动耗时
  11. systemd-analyze
  12. #显示某个 Unit 是否正在运行(一般用于脚本判断)
  13. systemctl is-active sshd.service
  14. #修改单元
  15. systemctl edit --full sshd.service

systemd 单元加载路径

单元文件是有一定的加载顺序的,先加载/etc/system/system,然后是/run/systemd/system,最后是/lib/systemd/system,如下图所示:

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

service 示例:Unit

一个配置文件可以分为 unit、service、install 这几个模块。

unit 定义描述、启动顺序、依赖等,

Description给出当前服务的简单描述,Documentation字段给出文档路径;

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

  1. After=network.target

依赖关系,需要使用WantsRequires

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

service 示例:service

  • Exec

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

  • Type

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

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

  • KillMode

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

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

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

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

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

我们来看下 sshd 的配置:

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

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

  • Restart

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

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

还有一个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) 下增加配置文件设置。

  1. 导入变量

在任何时候, 使用 systemctl --user set-environment systemctl --user import-environment. 对设置之后启动的所有用户单元有效,但已经启动的用户单元不会生效。

例子:

  1. [Service]
  2. Environment="TOKEN=12345"
  3. Environment="DB_PATH=/home/ExpressBot/expressbot/bot.db"
  4. Environment="TURING_KEY=111111"
  5. Environment="DEBUG=0"
  6.  
  7. Restart=always
  8. Type=simple
  9. 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表示图形用户下启动。

完整配置文件:

  1. [Unit]
  2. Description=A Telegram Bot for querying expresses
  3. After=network.target network-online.target nss-lookup.target
  4.  
  5. [Service]
  6. Environment="TOKEN=12345"
  7. Environment="DB_PATH=/home/ExpressBot/expressbot/bot.db"
  8. Environment="TURING_KEY=111111"
  9. Environment="DEBUG=0"
  10. Restart=always
  11. Type=simple
  12. ExecStart=/usr/bin/python /home/ExpressBot/expressbot/main.py
  13.  
  14. [Install]
  15. WantedBy=multi-user.target

更多详情可以参考这里

不好玩的轮子

在以前,我是怎么检测服务是否还在运行的呢……

基本思路是,pidof能获取到运行中的进程的 id,然后根据$?判断是否还在运行,比如说……

  1. pidof php-fpm >/dev/null
  2. if [ $? -eq 0 ] ; then
  3. echo "It is running."
  4. else
  5. echo "At `date` PHP Server was stopped">> /home/wwwlogs/service_log
  6. 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 安装:

  1. 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中包含后者。

如下创建默认配置:

  1. echo_supervisord_conf > /etc/supervisord.conf

最后一行包含配置:

  1. [include]
  2. files = /etc/supervisor/*.conf

然后创建如下 “单元”:

  1. [program:expressbot]
  2. directory = /home/ExpressBot/expressbot/main.py
  3. command = /usr/bin/python /home/ExpressBot/expressbot/main.py
  4. autostart = true ; supervisord 启动的时候也自动启动
  5. startsecs = 5 ; 启动 5 秒后没有异常退出,就当作已经正常启动了
  6. autorestart = true ; 程序异常退出后自动重启
  7. startretries = 3 ; 启动失败自动重试次数,默认是 3
  8. user = root ; 用哪个用户启动
  9. redirect_stderr = true ; stderr 重定向到 stdout,默认 false
  10. stdout_logfile_maxbytes = 20MB ; stdout 日志文件大小,默认 50MB
  11. stdout_logfile_backups = 20 ; stdout 日志文件备份数
  12. stdout_logfile = /var/log/expressbot_stdout.log ;日志文件

然后运行 supervisord 就可以启动 supervisor 啦。

控制 supervisor

类似 systemd,supervisor 也有个supervictl(配置文件查找顺序与 supervisord 一样),直接输入会进入交互模式,命令主要有:

status、stop、restart、reread(读取有更新新增的配置文件,不会启动新添加的程序)、update(重启配置文件修改过的程序),示例:

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

或者直接在 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
喜欢 (13)
分享:-)
关于作者:
If you have any further questions, feel free to contact me in English or Chinese.
发表我的评论
取消评论

                     

去你妹的实名制!

  • 昵称 (必填)
  • 邮箱 (必填,不要邮件提醒可以随便写)
  • 网址 (选填)
(39) 个小伙伴在吐槽
  1. 大佬写的真好,这个和 Telebot 的教程帮了我大忙
    BlacK2012019-07-31 11:12 回复
  2. 文章写的很妙啊,点赞
    连晋 2019-02-21 14:34 回复
    • 感谢~
      Benny 小土豆 2019-02-21 15:04 回复
  3. pkill的存在感呢?
    布偶君 2018-06-30 16:31 回复
    • pkill 怎么了?
      Benny 小土豆 2018-07-02 12:22 回复
      • 用 pkill 可以按进程名结束进程,但是你似乎没说的样子……
        布偶君 2018-07-02 12:50 回复
        • 有啥关系呢?默认也应该是 15 吧
          Benny 小土豆 2018-07-04 10:41
  4. 不错不错,支持博主,希望能长久的坚持下去,语言动感十足,画面感十足
    smoothmiao2018-01-17 10:24 回复
    • 画面感十足?
      Benny 小土豆 2018-01-17 16:14 回复
  5. 另:作为某站 “科学上网” 关键字的榜二,总感觉突然哪天博主就会被相关部门请去喝茶聊聊日常的样子.. 博主你老人家可要好好保重哦,各方面注意安全!
    ddw 皮蛋蛋 2018-01-10 16:31 回复
    • 一起回复给你:感谢关心,我会注意的(然并卵
      Benny 小土豆 2018-01-10 20:16 回复
  6. 这篇很赞耶,学到了! 感谢博主无私分享, 谢谢啦!
    ddw 皮蛋蛋 2018-01-10 16:22 回复
  7. 膜拜 dalao~ 能不能开机运行 screen 呢?
    永远的萌新 2017-12-18 16:32 回复
    • 这个……… 你想干嘛?开机就 screen 有点不太友好啊。
      Benny 小土豆 2017-12-18 18:19 回复
      • 想要开机运行一个服务端程序, 需要像 screen 这种方式运行, 随时可以切换回去看一下服务端的状态或者执行指令...
        永远的萌新 2017-12-18 18:46 回复
        • 用 systemd 或者 supervisor 吧,比较理想
          Benny 小土豆 2017-12-18 19:03
        • 试试 screen -dmS 名称 命令
          cy2018-10-23 02:34
      • 那个... 好像只是以服务进程的方式运行而已啊... 我要的是可以切换回去并且执行指令的... 就好像 vim 编辑文件那样, 还是可以切换回去继续操作的
        永远的萌新 2017-12-20 11:07 回复
        • 你这只能 screen 了……
          Benny 小土豆 2017-12-20 11:10
  8. 差点忘说,依云说这 Node.js 好像不要nohup的样子:https://blog.lilydjwg.me/2017/9/28/to-hup-or-not-to-hup.210812.html
    布偶君 2017-12-17 19:59 回复
    • ? 感谢 刚刚补充了下 bash 如果 exit 不会 SIGHUP
      Benny 小土豆 2017-12-17 21:46 回复
  9. 平常看进程都用top, 但是这东西好像用的不是正常的 ncurses 模式
    布偶君 2017-12-15 15:43 回复
    • htop 是 ncurses 的~
      Benny 小土豆 2017-12-15 17:38 回复
      • 除了……htop 这东西放在 Screen 里面就全乱掉了?
        布偶君 2018-06-03 06:40 回复
        • 也许 你需要调整 width
          Benny 小土豆 2018-06-03 11:32
  10. 你的下雪,打字的烟花,…… 嗯,这是 10 年前流行的东西。
    落格博客 2017-12-14 14:06 回复
    • 对对对<(^-^)>我大概活在 2007 年,我还年轻,可爱 (●'◡'●)
      Benny 小土豆 2017-12-14 15:15 回复
  11. nohup,简单好用。 看来你在乡非葬爱的路上越走越远了啊。
    落格博客 2017-12-14 13:32 回复
    • 确实简单好用~ 不过 乡非葬爱是什么意思嘛?
      Benny 小土豆 2017-12-14 13:53 回复
      • 就是乡村非主流 + 葬爱家族。比如说:https://goo.gl/YxFRW5
        落格博客 2017-12-14 14:00 回复
        • ? 这图片搜索结果吓死我
          你大概在吐槽我这脑残的粒子特效吧哈哈哈
          Benny 小土豆 2017-12-14 14:03
  12. 用了最多的就是 nohup 和 tmux
    Kios2017-12-09 09:43 回复
    • ??nohup 有一丢丢简陋
      Benny 小土豆 2017-12-09 09:44 回复
  13. ? systemd..... 一直以为 d=daemon
    ホロ 2017-12-08 23:02 回复
    • 我也是这么以为的…… 但是读 system 500 真的好别扭唉~
      Benny 小土豆 2017-12-08 23:04 回复
  14. 感觉 tmux 比 screen 好用……
    TJM2017-12-08 22:26 回复
    • 就不用,哼!
      Benny 小土豆 2017-12-08 22:33 回复
  15. 前排。。
    Yandere2017-12-07 22:28 回复
    • ? 买瓜子吗?
      Benny 小土豆 2017-12-07 22:32 回复
您直接访问了本站! 莫非您记住了我的域名. 厉害~ 我倍感荣幸啊 嘿嘿