如何在Linux中将Python脚本运行如同服务或守护进程

226

我编写了一个Python脚本,用于检查某个电子邮件地址并将新的电子邮件传递给外部程序。如何使此脚本在Linux中成为守护进程或服务并24/7运行?是否需要在程序中添加不断循环的代码,还是可以通过多次重新执行代码来实现?


1
请参阅以下问题:https://dev59.com/LXM_5IYBdhLWcg3wUxjF - mjv
3
检查特定的电子邮件地址并将新的电子邮件传递给外部程序。这不是sendmail所做的吗?您可以定义邮件别名将邮箱路由到脚本。为什么不使用邮件别名来完成这个任务呢? - S.Lott
2
在拥有 systemd 的现代 Linux 上,您可以按照此处所述的方式创建一个以 daemon 模式运行的 systemd 服务。另请参阅:https://www.freedesktop.org/software/systemd/man/systemd.service.html - ccpizza
如果Linux系统支持systemd,请使用此方法在此处概述 - gerardw
16个回答

116

这里有两个选项:

  1. 创建一个合适的cron工作,用于调用你的脚本。Cron是GNU/Linux守护程序的通用名称,它按照你设定的计划周期性地启动脚本。将脚本添加到crontab或将其符号链接放置到特殊目录中,守护程序会处理后台启动任务。你可以在维基百科上阅读更多信息。有各种不同的cron守护程序,但是你的GNU/Linux系统应该已经安装好了。

  2. 使用某种Python方法(例如库),使你的脚本能够自行守护。是的,它需要一个简单的事件循环(其中你的事件是由sleep函数提供的定时器触发)。

我不建议选择第二个选项,因为实际上你会重复cron的功能。Linux系统的范例是让多个简单的工具相互作用并解决你的问题。除非有其他原因需要创建守护进程(除了定期触发之外),否则请选择另一种方法。

此外,如果你使用daemonize并带有循环,当崩溃发生时,没有人会再检查邮件(正如Ivan Nevostruev答案的评论中所指出的)。而如果将脚本添加为cron工作,它只会再次触发。


11
+1 到 Cron 作业。我认为问题没有指定检查本地邮件帐户,因此邮件过滤器不适用。 - John La Rooy
如果在Python程序中使用没有终止条件的循环,并将其注册到“crontab”列表中,会发生什么?如果我将这样的.py设置为每小时运行,那么它会创建许多永远不会终止的进程吗?如果是这样,我认为这就像守护进程。 - Veck Hsiao
1
我可以看出,如果您每分钟检查一次电子邮件(这是Cron的最低时间分辨率),那么Cron是一个显而易见的解决方案。但是,如果我想每10秒钟检查一次电子邮件怎么办?我应该编写Python脚本运行查询60次,这意味着它在50秒后结束,然后让cron在10秒后再次启动脚本吗? - Mads Skjern
1
我没有使用过守护进程/服务,但我的印象是操作系统(OS/init/init.d/upstart或者叫什么名字)会在守护进程结束/崩溃时负责重新启动它。 - Mads Skjern
1
我正在编写一个Python脚本,需要每分钟大约检查20个传感器,每个传感器每分钟10次,全天候不间断。cron仍然是一个好的选择吗?我怀疑使用守护进程会更好。 - Ryan
显示剩余2条评论

84

这是一个很好的类,取自这里:

#!/usr/bin/env python

import sys, os, time, atexit
from signal import SIGTERM

class Daemon:
        """
        A generic daemon class.

        Usage: subclass the Daemon class and override the run() method
        """
        def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
                self.stdin = stdin
                self.stdout = stdout
                self.stderr = stderr
                self.pidfile = pidfile

        def daemonize(self):
                """
                do the UNIX double-fork magic, see Stevens' "Advanced
                Programming in the UNIX Environment" for details (ISBN 0201563177)
                http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
                """
                try:
                        pid = os.fork()
                        if pid > 0:
                                # exit first parent
                                sys.exit(0)
                except OSError, e:
                        sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
                        sys.exit(1)

                # decouple from parent environment
                os.chdir("/")
                os.setsid()
                os.umask(0)

                # do second fork
                try:
                        pid = os.fork()
                        if pid > 0:
                                # exit from second parent
                                sys.exit(0)
                except OSError, e:
                        sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
                        sys.exit(1)

                # redirect standard file descriptors
                sys.stdout.flush()
                sys.stderr.flush()
                si = file(self.stdin, 'r')
                so = file(self.stdout, 'a+')
                se = file(self.stderr, 'a+', 0)
                os.dup2(si.fileno(), sys.stdin.fileno())
                os.dup2(so.fileno(), sys.stdout.fileno())
                os.dup2(se.fileno(), sys.stderr.fileno())

                # write pidfile
                atexit.register(self.delpid)
                pid = str(os.getpid())
                file(self.pidfile,'w+').write("%s\n" % pid)

        def delpid(self):
                os.remove(self.pidfile)

        def start(self):
                """
                Start the daemon
                """
                # Check for a pidfile to see if the daemon already runs
                try:
                        pf = file(self.pidfile,'r')
                        pid = int(pf.read().strip())
                        pf.close()
                except IOError:
                        pid = None

                if pid:
                        message = "pidfile %s already exist. Daemon already running?\n"
                        sys.stderr.write(message % self.pidfile)
                        sys.exit(1)

                # Start the daemon
                self.daemonize()
                self.run()

        def stop(self):
                """
                Stop the daemon
                """
                # Get the pid from the pidfile
                try:
                        pf = file(self.pidfile,'r')
                        pid = int(pf.read().strip())
                        pf.close()
                except IOError:
                        pid = None

                if not pid:
                        message = "pidfile %s does not exist. Daemon not running?\n"
                        sys.stderr.write(message % self.pidfile)
                        return # not an error in a restart

                # Try killing the daemon process       
                try:
                        while 1:
                                os.kill(pid, SIGTERM)
                                time.sleep(0.1)
                except OSError, err:
                        err = str(err)
                        if err.find("No such process") > 0:
                                if os.path.exists(self.pidfile):
                                        os.remove(self.pidfile)
                        else:
                                print str(err)
                                sys.exit(1)

        def restart(self):
                """
                Restart the daemon
                """
                self.stop()
                self.start()

        def run(self):
                """
                You should override this method when you subclass Daemon. It will be called after the process has been
                daemonized by start() or restart().
                """

2
系统重新启动时,它会重新启动吗?因为当系统重新启动时,进程会被终止,对吗? - ShivaPrasad
请问,我该如何使用这段代码将我的程序作为守护进程运行?我不太明白它的工作原理。 - Vinay Kumar

66
你应该使用python-daemon库,它会处理一切。
从PyPI中得知:这是一个实现良好的Unix守护进程的库。

6
同乔尔赫·瓦尔加斯的评论。在查看了代码后,实际上它看起来是一段相当不错的代码,但是完全缺乏文档和示例使得很难使用,这意味着大多数开发者会理所当然地忽略它并选择有更好文档支持的替代品。 - Cerin
1
在Python 3.5中似乎无法正常工作:https://gist.github.com/MartinThoma/fa4deb2b4c71ffcd726b24b7ab581ae2 - Martin Thoma
不像预期的那样工作。如果它能够正常工作,那就太好了。 - Harlin
Unix != Linux - 这可能是问题所在吗? - Dana

63

假设您真的想要让循环作为后台服务运行24/7

如果您不想使用任何库注入代码,可以创建一个服务模板,因为您正在使用Linux:

[Unit]
Description = <Your service description here>
After = network.target # Assuming you want to start after network interfaces are made available
 
[Service]
Type = simple
ExecStart = python <Path of the script you want to run>
User = # User to run the script as
Group = # Group to run the script as
Restart = on-failure # Restart when there are errors
SyslogIdentifier = <Name of logs for the service>
RestartSec = 5
TimeoutStartSec = infinity
 
[Install]
WantedBy = multi-user.target # Make it accessible to other users

将该文件放置于您的守护进程服务文件夹中(通常是/etc/systemd/system/),在一个*.service文件中,使用以下systemctl命令进行安装(可能需要sudo特权):

systemctl enable <service file name without .service extension>

systemctl daemon-reload

systemctl start <service file name without .service extension>

您可以使用以下命令检查服务是否正在运行:

systemctl | grep running

2
@PouJa请发布一个新问题并提供详细信息。 - Yawar
这里有一个详细而简单的描述,说明如何通过systemctl/systemd将Python脚本设置为服务: https://medium.com/codex/setup-a-python-script-as-a-service-through-systemctl-systemd-f0cc55a42267 - Dysmas
这绝对是业界标准实践 +1 - K M Jiaul Islam Jibon
1
如果您希望脚本在其所在的目录中运行,可以将WorkingDirectory设置为“<script path>”。 - purushothaman poovai
当我在Python中使用sys.exit(1)时,服务重启策略会识别并重新启动吗? - S. Gissel
显示剩余2条评论

43
您可以使用fork()将脚本与tty分离,使其继续运行,例如:
import os, sys
fpid = os.fork()
if fpid!=0:
  # Running as daemon now. PID is fpid
  sys.exit(0)

当然,您还需要实现一个无限循环,例如:

while 1:
  do_your_check()
  sleep(5)

希望这能让你开始。


你好,我已经尝试过这个方法并且它对我有效。但是当我关闭终端或退出ssh会话时,脚本也停止工作了!! - David Okwii
1
@DavidOkwii nohup/disown 命令会将进程从控制台中分离出来,使其不会死亡。或者您可以使用 init.d 启动它。 - pholat

18

一个简单并且得到支持的版本Daemonize

从Python包索引(PyPI)安装它:

$ pip install daemonize

然后像这样使用:

...
import os, sys
from daemonize import Daemonize
...
def main()
      # your code here

if __name__ == '__main__':
        myname=os.path.basename(sys.argv[0])
        pidfile='/tmp/%s' % myname       # any name
        daemon = Daemonize(app=myname,pid=pidfile, action=main)
        daemon.start()

2
系统重新启动时,它会重新启动吗?因为当系统重新启动时,进程会被终止,对吗? - ShivaPrasad
@ShivaPrasad,你找到答案了吗? - 1UC1F3R616
系统重启后的重新启动不是守护进程功能。使用cron、systemctl、启动钩子或其他工具在启动时运行您的应用程序。 - fcm
1
@Kush 是的,我想在系统重新启动后重新启动或使用类似命令的功能。我使用了systemd函数,如果想尝试,请查看此链接:https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system_administrators_guide/sect-managing_services_with_systemd-unit_files - ShivaPrasad
@ShivaPrasad 谢谢兄弟 - 1UC1F3R616

16

您还可以使用shell脚本将Python脚本运行为服务。首先创建一个shell脚本来运行Python脚本,如下所示(scriptname是任意名称)

#!/bin/sh
script='/home/.. full path to script'
/usr/bin/python $script &

现在在 /etc/init.d/ 目录下创建一个名为 scriptname 的文件。

#! /bin/sh

PATH=/bin:/usr/bin:/sbin:/usr/sbin
DAEMON=/home/.. path to shell script scriptname created to run python script
PIDFILE=/var/run/scriptname.pid

test -x $DAEMON || exit 0

. /lib/lsb/init-functions

case "$1" in
  start)
     log_daemon_msg "Starting feedparser"
     start_daemon -p $PIDFILE $DAEMON
     log_end_msg $?
   ;;
  stop)
     log_daemon_msg "Stopping feedparser"
     killproc -p $PIDFILE $DAEMON
     PID=`ps x |grep feed | head -1 | awk '{print $1}'`
     kill -9 $PID       
     log_end_msg $?
   ;;
  force-reload|restart)
     $0 stop
     $0 start
   ;;
  status)
     status_of_proc -p $PIDFILE $DAEMON atd && exit 0 || exit $?
   ;;
 *)
   echo "Usage: /etc/init.d/atd {start|stop|restart|force-reload|status}"
   exit 1
  ;;
esac

exit 0

现在你可以使用命令 /etc/init.d/scriptname start 或 stop 来启动或停止你的 Python 脚本。


1
我刚试了一下,发现这会启动进程,但它不会被守护(即仍然附加在终端上)。如果你运行update-rc.d并让它在启动时运行(我假设这些脚本运行时没有终端附加),那么它可能会正常工作,但如果你手动调用它,则无法正常工作。似乎supervisord可能是更好的解决方案。 - ryuusenshi
可能可以使用 disown 来实现? - scribe

15

cron 显然是许多目的的很好的选择。 但它不会像你在 OP 中请求的那样创建服务或守护进程。 cron 只是定期运行作业(意味着作业启动和停止),且最多每分钟运行一次。 cron 存在问题--例如,如果您的脚本的先前实例仍在运行,下一次 cron 计划启动新实例时会发生什么?cron 不处理依赖项;它只会在计划指定的时间尝试启动作业。

如果您遇到真正需要守护进程(永远不停止运行的进程)的情况,请查看 supervisord。 它提供了一种简单的方式来包装普通的非守护式脚本或程序,并使其像守护进程一样运行。 这比创建本地 Python 守护进程要好得多。


/tmp/lock文件非常适合避免脚本的多次运行。如果您可以使用bash文件或在Python脚本中创建锁定文件,我建议采用这种方法。只需在DuckDuckGo上搜索“bash lock file example”。 - beep_check

14

Ubuntu有一种非常简单的方式来管理服务。 对于Python来说,区别在于所有依赖项(软件包)都必须在同一个目录中,其中运行主文件。

我刚刚成功地创建了这样一个服务,向我的客户提供天气信息。 步骤:

  • Create your python application project as you normally do.

  • Install all dependencies locally like: sudo pip3 install package_name -t .

  • Create your command line variables and handle them in code (if you need any)

  • Create the service file. Something (minimalist) like:

      [Unit]
      Description=1Droid Weather meddleware provider
    
      [Service]
      Restart=always
      User=root
      WorkingDirectory=/home/ubuntu/weather
      ExecStart=/usr/bin/python3 /home/ubuntu/weather/main.py httpport=9570  provider=OWMap
    
      [Install]
      WantedBy=multi-user.target
    
  • Save the file as myweather.service (for example)

  • Make sure that your app runs if started in the current directory

      python3  main.py httpport=9570  provider=OWMap
    
  • The service file produced above and named myweather.service (important to have the extension .service) will be treated by the system as the name of your service. That is the name that you will use to interact with your service.

  • Copy the service file:

      sudo cp myweather.service /lib/systemd/system/myweather.service
    
  • Refresh demon registry:

      sudo systemctl daemon-reload
    
  • Stop the service (if it was running)

      sudo service myweather stop
    
  • Start the service:

      sudo service myweather start
    
  • Check the status (log file with where your print statements go):

      tail -f /var/log/syslog
    
  • Or check the status with:

      sudo service myweather status
    
  • Back to the start with another iteration if needed

该服务现在正在运行,即使您退出登录也不会受到影响。是的,如果主机关闭并重新启动,该服务将重新启动...


1
这是一份非常棒的指南。我构建了一个 Webhook "守护进程",将我的 CMS 连接到 Rocketchat,效果非常好。谢谢。 - John Seabourn

11

在Linux上使用$nohup命令怎么样?

我在Bluehost服务器上使用它来运行我的命令。

如果我有错误,请指教。


我也使用它,效果非常好。"如果我错了,请指教。" - Alexandre Mazel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接