如何使 Python 脚本像 Linux 中的服务或守护程序一样运行

Posted

技术标签:

【中文标题】如何使 Python 脚本像 Linux 中的服务或守护程序一样运行【英文标题】:How to make a Python script run like a service or daemon in Linux 【发布时间】:2010-12-08 20:31:04 【问题描述】:

我编写了一个 Python 脚本来检查某个电子邮件地址并将新电子邮件传递给外部程序。我怎样才能让这个脚本 24/7 执行,比如在 Linux 中把它变成守护进程或服务。我是否还需要一个在程序中永远不会结束的循环,还是可以通过多次重新执行代码来完成?

【问题讨论】:

查看 SO 问题:***.com/questions/1423345/… "检查某个电子邮件地址并将新电子邮件传递给外部程序" sendmail 不就是这样做的吗?您可以定义邮件别名以将邮箱路由到脚本。你为什么不使用邮件别名来做到这一点? 在具有systemd 的现代Linux 上,您可以按照here 的描述在daemon 模式下创建systemd 服务。另见:freedesktop.org/software/systemd/man/systemd.service.html 如果linux系统支持systemd,使用outlined here的方式。 【参考方案1】:

这里有两个选择。

    制作一个适当的cron 作业 来调用您的脚本。 Cron 是 GNU/Linux 守护程序的通用名称,它根据您设置的计划定期启动脚本。您将脚本添加到 crontab 中或将符号链接放置到特殊目录中,守护程序将处理在后台启动它的工作。你可以在***上read more。有多种不同的 cron 守护程序,但您的 GNU/Linux 系统应该已经安装了它。

    使用某种 python 方法(例如,一个库)让您的脚本能够自行守护进程。是的,它需要一个简单的事件循环(您的事件是定时器触发,可能由 sleep 函数提供)。

我不建议您选择 2.,因为实际上您会重复 cron 功能。 Linux 系统范式是让多个简单的工具交互并解决您的问题。除非有其他原因需要您制作守护程序(除了定期触发),否则请选择其他方法。

另外,如果你使用 daemonize 和一个循环并且发生了崩溃,那么之后没有人会检查邮件(正如 Ivan Nevostruev 在 cmets 到 this 的回答中指出的那样)。而如果脚本作为 cron 作业添加,它只会再次触发。

【讨论】:

+1 到 cronjob。我不认为问题指定它正在检查本地邮件帐户,因此邮件过滤器不适用 在 Python 程序中使用没有终止的循环然后将其注册到 crontab 列表中会发生什么?如果我每小时设置这样的.py,它会创建许多永远不会终止的进程吗?如果是这样,我认为这很像守护进程。 如果您每分钟检查一次电子邮件(这是 Cron 的最低时间分辨率),我可以看到 cron 是一个明显的解决方案。但是,如果我想每 10 秒检查一次电子邮件怎么办?我是否应该编写 Python 脚本来运行查询 60 次,也就是说它在 50 秒后结束,然后让 cron 在 10 秒后再次启动脚本? 我没有使用过守护进程/服务,但我的印象是它(OS/init/init.d/upstart 或它的名字)负责在何时/如果重启守护进程它结束/崩溃。 @VeckHsiao 是的,crontab 调用了一个脚本,所以你的 python 脚本的很多实例都会被调用,每个人都会调用它的循环......【参考方案2】:

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

#!/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().
                """

【讨论】:

系统重启后是否重启?因为当系统重新启动时,进程会被杀死对吗? 你能告诉我,我怎样才能使用这个代码来运行我的程序作为守护进程?我没有得到,它将如何工作。【参考方案3】:

您应该使用 python-daemon 库,它会处理所有事情。

来自 PyPI:用于实现行为良好的 Unix 守护进程的库。

【讨论】:

同上 Jorge Vargas 的评论。在查看代码后,它实际上看起来是一段相当不错的代码,但是完全缺乏文档和示例使得它很难使用,这意味着大多数开发人员会理所当然地忽略它以获得更好的文档替代方案。 在 Python 3.5 中似乎无法正常工作:gist.github.com/MartinThoma/fa4deb2b4c71ffcd726b24b7ab581ae2 未按预期工作。如果有的话会不会很好。 Unix != Linux - 这可能是个问题吗?【参考方案4】:

您可以使用 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 会话时,脚本也会停止工作!! @DavidOkwii nohup/disown 命令会将进程与控制台分离,并且不会死。或者你可以用 init.d 开始它【参考方案5】:

假设您真的希望循环作为后台服务 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

【讨论】:

这是我的首选解决方案,因为如果您有 systemd 则没有 deps,但您能否将 .service 文件共享为代码 sn-p 而不是屏幕截图 - 哈哈? 嗨,这种方法听起来很简单。当我使用它在后台运行我的 python 脚本时,一切都很好,直到系统重新启动。由于订购周期错误,启动功能似乎失败了。错误是:Job active_controller.service/start 已删除以中断从 sysinit.target/start 开始的订购周期,如果可以,请提供帮助。 @PouJa 请发布一个包含详细信息的新问题。 这里有详细而简单的说明:medium.com/codex/…【参考方案6】:

您还可以使用 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 脚本。

【讨论】:

我刚试过这个,事实证明这将启动进程,但它不会被守护(即它仍然连接到终端)。如果您运行 update-rc.d 并使其在启动时运行(我假设运行这些脚本时没有附加终端),它可能会正常工作,但如果您手动调用它,它就不起作用。似乎 supervisord 可能是一个更好的解决方案。 有人可能会以某种方式使用disown【参考方案7】:

一个简单且受支持的version 是Daemonize

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

$ pip install daemonize

然后使用like:

...
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()

【讨论】:

系统重启后是否重启?因为当系统重新启动时,进程会被杀死对吗? @ShivaPrasad 你找到答案了吗? 系统重启后重启不是恶魔功能。使用 cron、systemctl、启动挂钩或其他工具在启动时运行您的应用。 @Kush 是的,我想在系统重新启动后重新启动或使用我使用 systemd 功能的类似命令,如果想尝试检查这个access.redhat.com/documentation/en-us/red_hat_enterprise_linux/… @ShivaPrasad 谢谢兄弟【参考方案8】:

cron 显然是许多用途的绝佳选择。但是,它不会按照您在 OP 中的要求创建服务或守护程序。 cron 只是定期运行作业(意味着作业开始和停止),并且不超过每分钟一次。 cron 存在问题——例如,如果您的脚本的先前实例仍在运行,那么下次 cron 计划出现并启动新实例时,可以吗? cron 不处理依赖关系;它只是在计划要求时尝试开始工作。

如果您发现确实需要守护程序(永不停止运行的进程)的情况,请查看supervisord。它提供了一种简单的方法来包装普通的、非守护程序的脚本或程序,并使其像守护程序一样运行。这比创建原生 Python 守护进程要好得多。

【讨论】:

/tmp/lock 文件非常适合避免多次运行脚本。如果您可以使用 bash 文件或在 python 脚本中创建锁定文件,我建议您使用这种方法。只是 duckduckgo "bash 锁定文件示例"【参考方案9】:

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

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

如果我错了请指教。

【讨论】:

我也使用它,就像一个魅力。 “如果我错了,请指教。”【参考方案10】:

如果您使用的是终端(ssh 或其他),并且希望在从终端注销后保持长时间的脚本工作,您可以试试这个:

screen

apt-get install screen

在里面创建一个虚拟终端(即abc):screen -dmS abc

现在我们连接到 abc:screen -r abc

所以,现在我们可以运行 python 脚本了:python keep_sending_mails.py

从现在开始,您可以直接关闭终端,但是python脚本将继续运行而不是被关闭

由于这个keep_sending_mails.py的PID是虚拟屏幕的子进程而不是 终端(ssh)

如果你想回去检查你的脚本运行状态,你可以再次使用screen -r abc

【讨论】:

虽然这可行,但它非常快速且肮脏,应避免在生产中使用【参考方案11】:

Ubuntu 有一种非常简单的方法来管理服务。 对于 python,不同之处在于所有依赖项(包)都必须位于运行主文件的同一目录中。

我只是设法创建这样的服务来向我的客户提供天气信息。 步骤:

像往常一样创建您的 python 应用程序项目。

在本地安装所有依赖项,例如: sudo pip3 install package_name -t .

创建命令行变量并在代码中处理它们(如果需要)

创建服务文件。一些(极简主义)比如:

  [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

将文件另存为 myweather.service(例如)

确保您的应用在当前目录中启动时运行

  python3  main.py httpport=9570  provider=OWMap

上面生成的名为 myweather.service 的服务文件(扩展名为 .service 很重要)将被系统视为您的服务名称。这是您将用于与服务交互的名称。

复制服务文件:

  sudo cp myweather.service /lib/systemd/system/myweather.service

刷新恶魔注册表:

  sudo systemctl daemon-reload

停止服务(如果它正在运行)

  sudo service myweatherr stop

启动服务:

  sudo service myweather start

检查状态(您的打印语句所在的日志文件):

  tail -f /var/log/syslog

或通过以下方式检查状态:

  sudo service myweather status

如果需要,重新开始进行另一次迭代

此服务现在正在运行,即使您注销它也不会受到影响。 是的,如果主机关闭并重新启动,此服务将重新启动...我的移动 android 应用程序的信息...

【讨论】:

【参考方案12】:

首先,阅读邮件别名。邮件别名将在邮件系统中执行此操作,而无需您使用守护程序或服务或任何类似的东西。

您可以编写一个简单的脚本,每次将邮件发送到特定邮箱时,sendmail 都会执行该脚本。

见http://www.feep.net/sendmail/tutorial/intro/aliases.html

如果你真的想编写一个不必要的复杂服务器,你可以这样做。

nohup python myscript.py &

仅此而已。您的脚本只是循环和休眠。

import time
def do_the_work():
    # one round of polling -- checking email, whatever.
while True:
    time.sleep( 600 ) # 10 min.
    try:
        do_the_work()
    except:
        pass

【讨论】:

这里的问题是do_the_work() 可以使脚本崩溃并且没有人再次运行它 如果函数 do_the_work() 崩溃,它将在 10 分钟后再次调用,因为只有一个函数调用会引发错误。但不是使循环崩溃,只是 try 部分失败,except: 部分将被调用(在这种情况下没有),但循环将继续并继续尝试调用该函数。【参考方案13】:

我会推荐这个解决方案。您需要继承和覆盖方法run

import sys
import os
from signal import SIGTERM
from abc import ABCMeta, abstractmethod



class Daemon(object):
    __metaclass__ = ABCMeta


    def __init__(self, pidfile):
        self._pidfile = pidfile


    @abstractmethod
    def run(self):
        pass


    def _daemonize(self):
        # decouple threads
        pid = os.fork()

        # stop first thread
        if pid > 0:
            sys.exit(0)

        # write pid into a pidfile
        with open(self._pidfile, 'w') as f:
            print >> f, os.getpid()


    def start(self):
        # if daemon is started throw an error
        if os.path.exists(self._pidfile):
            raise Exception("Daemon is already started")

        # create and switch to daemon thread
        self._daemonize()

        # run the body of the daemon
        self.run()


    def stop(self):
        # check the pidfile existing
        if os.path.exists(self._pidfile):
            # read pid from the file
            with open(self._pidfile, 'r') as f:
                pid = int(f.read().strip())

            # remove the pidfile
            os.remove(self._pidfile)

            # kill daemon
            os.kill(pid, SIGTERM)

        else:
            raise Exception("Daemon is not started")


    def restart(self):
        self.stop()
        self.start()

【讨论】:

【参考方案14】:

要创建一些像服务一样运行的东西,你可以使用这个东西:

您必须做的第一件事是安装Cement 框架: Cement frame work 是一个 CLI 框架,您可以在其上部署应用程序。

应用的命令行界面:

interface.py

 from cement.core.foundation import CementApp
 from cement.core.controller import CementBaseController, expose
 from YourApp import yourApp

 class Meta:
    label = 'base'
    description = "your application description"
    arguments = [
        (['-r' , '--run'],
          dict(action='store_true', help='Run your application')),
        (['-v', '--version'],
          dict(action='version', version="Your app version")),
        ]
        (['-s', '--stop'],
          dict(action='store_true', help="Stop your application")),
        ]

    @expose(hide=True)
    def default(self):
        if self.app.pargs.run:
            #Start to running the your app from there !
            YourApp.yourApp()
        if self.app.pargs.stop:
            #Stop your application
            YourApp.yourApp.stop()

 class App(CementApp):
       class Meta:
       label = 'Uptime'
       base_controller = 'base'
       handlers = [MyBaseController]

 with App() as app:
       app.run()

YourApp.py 类:

 import threading

 class yourApp:
     def __init__:
        self.loger = log_exception.exception_loger()
        thread = threading.Thread(target=self.start, args=())
        thread.daemon = True
        thread.start()

     def start(self):
        #Do every thing you want
        pass
     def stop(self):
        #Do some things to stop your application

请记住,您的应用必须在线程上运行才能成为守护进程

要运行应用程序,只需在命令行中执行此操作

python interface.py --help

【讨论】:

【参考方案15】:

使用您的系统提供的任何服务管理器 - 例如在 Ubuntu 下使用 upstart。这将为您处理所有细节,例如启动时启动、崩溃时重启等。

【讨论】:

以上是关于如何使 Python 脚本像 Linux 中的服务或守护程序一样运行的主要内容,如果未能解决你的问题,请参考以下文章

如何使 PHP 永久运行脚本“忘记”一切并几乎像重新启动一样执行

如何从 Linux 客户端调用驻留在 Windows 服务器上的 Python 脚本中的 RPC 调用

如何使 Python 脚本作为服务运行?

如何使2台Linux服务器通过FTP自动同步文件?(用Shell脚本)

我如何使用像 CD 和 LS 这样的 Linux 终端命令? [复制]

python 使python脚本在linux上可执行