Python高级技术之:`Python`的`daemon`进程:如何设计和管理后台服务。

各位老铁,早上好!今天咱们来聊聊Python里的“隐身侠”—— daemon 进程,也就是传说中的后台服务。这玩意儿,听起来高大上,其实没那么神秘。 咱们的目标是:让你不仅知道啥是 daemon 进程,还能自己动手撸一个出来,并且把它管得服服帖帖的。

一、啥是 Daemon 进程?为啥要用它?

想象一下,你开了一家餐厅,总不能老板亲自端盘子、洗碗吧? 得找些默默无闻的“后台服务员”——它们在厨房里忙活,处理各种任务,但顾客(用户)根本看不到他们。

Daemon 进程,就是 Linux/Unix 系统里的这种“后台服务员”。它有以下特点:

  • 独立自主: 脱离终端控制,不会因为你关掉终端就挂掉。
  • 默默奉献: 在后台运行,不和用户直接交互(除非你专门设计了交互接口)。
  • 持续工作: 一般来说,除非遇到错误或者被手动停止,否则会一直运行下去。

为啥要用 daemon 进程?

  • 长时间运行的任务: 比如监控服务器状态、定时备份数据、处理网络请求等等,这些任务需要一直运行,不能依赖用户的登录状态。
  • 提高系统资源利用率: 可以让程序在后台运行,释放终端资源,让用户可以继续做其他事情。
  • 构建稳定可靠的服务: 可以通过各种机制(例如守护进程管理器)来保证 daemon 进程的稳定运行。

二、Daemon 进程的“变身”过程:关键步骤

让一个普通的 Python 脚本变成 daemon 进程,需要经历一系列的“变身”步骤。 咱们一步一步来,保证你听得明白,看得清楚。

  1. 脱离终端:os.setsid()

    这一步是关键,相当于给进程“断奶”,让它不再依赖于创建它的终端。 os.setsid() 会创建一个新的会话(session),让进程成为会话的领导者(session leader),并且脱离原来的控制终端。

    import os
    
    def daemonize():
        """将当前进程变为守护进程"""
    
        # 1. Fork 一次,让父进程退出,子进程变成孤儿进程
        pid = os.fork()
        if pid > 0:
            # 父进程退出
            exit(0)
        elif pid < 0:
            # Fork 失败
            exit(1)
    
        # 2. 子进程成为新的会话的领导者,脱离终端
        os.setsid()
    
        # 3. 再次 Fork,防止进程重新获得控制终端
        pid = os.fork()
        if pid > 0:
            # 父进程退出
            exit(0)
        elif pid < 0:
            # Fork 失败
            exit(1)
    
        # 现在,我们是真正的守护进程了

    为啥要 fork() 两次?

    第一次 fork()是为了让父进程退出,这样子进程就变成了孤儿进程,会被 init 进程收养。

    第二次 fork()是为了防止进程重新获得控制终端。 如果进程是会话的领导者,并且没有控制终端,那么它可以打开一个终端设备来获取控制终端。 第二次 fork()之后,子进程不再是会话的领导者,就无法重新获得控制终端了。

  2. 改变工作目录:os.chdir()

    守护进程一般不应该依赖于当前的工作目录,所以最好把它改变到一个安全的地方,比如根目录 / 或者一个专门为它创建的目录。

    import os
    
    def daemonize():
        # ... (前面的代码) ...
    
        # 4. 改变工作目录
        os.chdir("/")
  3. 重定向标准输入、输出、错误:sys.stdin, sys.stdout, sys.stderr

    守护进程不需要和用户交互,所以可以把标准输入、输出、错误重定向到 /dev/null,或者一个日志文件。

    import os
    import sys
    
    def daemonize():
        # ... (前面的代码) ...
    
        # 5. 重定向标准输入、输出、错误
        sys.stdin.close()
        sys.stdout.close()
        sys.stderr.close()
    
        sys.stdin = open('/dev/null', 'r')
        sys.stdout = open('/dev/null', 'w')  # 或者一个日志文件
        sys.stderr = open('/dev/null', 'w')  # 或者一个日志文件

    注意: 如果你需要记录守护进程的日志,应该把标准输出和标准错误重定向到一个日志文件,而不是 /dev/null

  4. 设置文件权限掩码:os.umask()

    守护进程创建的文件,权限应该有一定的限制。 可以使用 os.umask() 来设置文件权限掩码。

    import os
    
    def daemonize():
        # ... (前面的代码) ...
    
        # 6. 设置文件权限掩码
        os.umask(0) # 允许所有权限

    os.umask() 是啥?

    os.umask() 设置的是一个“掩码”,它会从默认的文件权限中移除一些权限。 比如,如果 umask0022,那么新创建的文件权限会是 644 (0666 – 0022 = 0644),目录权限会是 755 (0777 – 0022 = 0755)。

    设置 os.umask(0) 意味着允许所有权限。

  5. 编写守护进程的主逻辑

    import time
    import os
    import sys
    
    def daemonize():
        # ... (前面的代码) ...
        pass
    
    def main():
        """守护进程的主逻辑"""
        while True:
            try:
                with open("/tmp/daemon.log", "a") as f:
                    f.write(f"{time.ctime()} - Daemon process is running...n")
                time.sleep(5)
            except Exception as e:
                print(f"Error: {e}")
                time.sleep(5)
    
    if __name__ == "__main__":
        daemonize()
        main()

    这个 main() 函数就是守护进程要执行的任务。 这里只是简单地每 5 秒钟向 /tmp/daemon.log 文件写入一条日志。

三、一个完整的 Daemon 进程示例

import os
import sys
import time
import atexit
import signal

class Daemon:
    """
    一个通用的守护进程类。

    用法: 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):
        """
        将当前进程变为守护进程。
        """

        # 1. Fork 一次
        try:
            pid = os.fork()
            if pid > 0:
                # 父进程退出
                sys.exit(0)
        except OSError as e:
            sys.stderr.write("fork #1 failed: %d (%s)n" % (e.errno, e.strerror))
            sys.exit(1)

        # 2. 脱离终端
        os.setsid()

        # 3. 再次 Fork
        try:
            pid = os.fork()
            if pid > 0:
                # 父进程退出
                sys.exit(0)
        except OSError as e:
            sys.stderr.write("fork #2 failed: %d (%s)n" % (e.errno, e.strerror))
            sys.exit(1)

        # 4. 改变工作目录
        os.chdir("/")

        # 5. 设置文件权限掩码
        os.umask(0)

        # 6. 重定向标准输入、输出、错误
        sys.stdout.flush()
        sys.stderr.flush()
        si = open(self.stdin, 'r')
        so = open(self.stdout, 'a+')
        se = open(self.stderr, 'a+')
        os.dup2(si.fileno(), sys.stdin.fileno())
        os.dup2(so.fileno(), sys.stdout.fileno())
        os.dup2(se.fileno(), sys.stderr.fileno())

        # 7. 注册退出函数,当进程退出时删除pidfile
        atexit.register(self.delpid)

        # 8. 将PID写入pidfile
        pid = str(os.getpid())
        open(self.pidfile,'w+').write("%sn" % pid)

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

    def start(self):
        """
        启动守护进程
        """

        # 1. 检查pidfile是否存在以判断进程是否已经运行
        try:
            pf = open(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)

        # 2. 启动守护进程
        self.daemonize()
        self.run()

    def stop(self):
        """
        停止守护进程
        """
        # 1. 从pidfile中获取PID
        try:
            pf = open(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

        # 2. 尝试杀死守护进程
        try:
            while 1:
                os.kill(pid, signal.SIGTERM)
                time.sleep(0.1)
        except OSError as 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):
        """
        重启守护进程
        """
        self.stop()
        self.start()

    def run(self):
        """
        你需要在子类中重写这个方法,
        它将在守护进程启动后被调用。
        """
        pass

# 示例用法
class MyDaemon(Daemon):
    def run(self):
        while True:
            try:
                with open("/tmp/mydaemon.log", "a") as f:
                    f.write(f"{time.ctime()} - MyDaemon is running...n")
                time.sleep(5)
            except Exception as e:
                print(f"Error: {e}")
                time.sleep(5)

if __name__ == "__main__":
    daemon = MyDaemon('/tmp/mydaemon.pid', 
                      stdout='/tmp/mydaemon.log', 
                      stderr='/tmp/mydaemon.log')
    if len(sys.argv) == 2:
        if 'start' == sys.argv[1]:
            daemon.start()
        elif 'stop' == sys.argv[1]:
            daemon.stop()
        elif 'restart' == sys.argv[1]:
            daemon.restart()
        else:
            print ("Unknown command")
            sys.exit(2)
        sys.exit(0)
    else:
        print ("usage: %s start|stop|restart" % sys.argv[0])
        sys.exit(2)

如何使用这个例子:

  1. 保存代码: 将代码保存为 mydaemon.py
  2. 启动守护进程: 运行 python mydaemon.py start
  3. 停止守护进程: 运行 python mydaemon.py stop
  4. 重启守护进程: 运行 python mydaemon.py restart
  5. 查看日志: 查看 /tmp/mydaemon.log 文件,可以看到守护进程的日志输出。
  6. 检查 PID 文件: /tmp/mydaemon.pid 中保存了守护进程的 PID。

四、守护进程的管理:让它听话

光有 daemon 进程还不够,还得能管理它,让它按照我们的意愿启动、停止、重启。

  1. PID 文件:

    守护进程通常会把自己的 PID 写入一个文件(PID 文件),方便我们找到它,并向它发送信号。

    在上面的代码中,我们使用了 /tmp/mydaemon.pid 作为 PID 文件。

  2. 信号处理:

    可以使用信号来控制守护进程。常用的信号有:

    信号 含义
    SIGTERM 正常终止信号,告诉进程优雅地退出。
    SIGKILL 强制终止信号,立即杀死进程(不建议使用)。
    SIGHUP 挂起信号,通常用于重新加载配置文件。

    在上面的代码中,stop() 函数使用 SIGTERM 信号来停止守护进程。

  3. 守护进程管理器:

    手动管理守护进程太麻烦,可以使用守护进程管理器来自动管理。 常用的守护进程管理器有:

    • Systemd: 现代 Linux 系统推荐使用的守护进程管理器。
    • Supervisor: Python 编写的守护进程管理器,配置简单,功能强大。
    • Upstart: Ubuntu 系统早期使用的守护进程管理器(现在已经被 Systemd 取代)。

    使用 Systemd 管理守护进程:

    1. 创建 Systemd 服务文件:

      /etc/systemd/system/ 目录下创建一个 .service 文件,比如 mydaemon.service

      [Unit]
      Description=My Daemon Service
      After=network.target
      
      [Service]
      User=root  # 运行用户
      WorkingDirectory=/opt/mydaemon  # 工作目录
      ExecStart=/usr/bin/python3 /opt/mydaemon/mydaemon.py start  # 启动命令
      ExecStop=/usr/bin/python3 /opt/mydaemon/mydaemon.py stop  # 停止命令
      Restart=on-failure  # 失败时自动重启
      Type=forking  # 进程类型
      
      [Install]
      WantedBy=multi-user.target
    2. 启用服务:

      运行以下命令来启用服务:

      sudo systemctl enable mydaemon.service
    3. 启动服务:

      运行以下命令来启动服务:

      sudo systemctl start mydaemon.service
    4. 停止服务:

      运行以下命令来停止服务:

      sudo systemctl stop mydaemon.service
    5. 重启服务:

      运行以下命令来重启服务:

      sudo systemctl restart mydaemon.service
    6. 查看服务状态:

      运行以下命令来查看服务状态:

      sudo systemctl status mydaemon.service

五、Daemon 进程的注意事项

  • 异常处理: 守护进程需要在 run() 方法中做好异常处理,防止程序崩溃。
  • 日志记录: 守护进程需要记录详细的日志,方便排查问题。
  • 资源释放: 守护进程需要及时释放资源,防止内存泄漏。
  • 安全性: 守护进程应该以低权限用户运行,防止被恶意利用。

六、总结

Daemon 进程是 Python 开发中非常有用的技术,可以用来构建各种后台服务。 掌握 Daemon 进程的原理和使用方法,可以让你更好地管理你的 Python 程序,提高系统的稳定性和可靠性。

记住,代码是最好的老师。 多练习,多实践,你也能成为 Daemon 进程的高手!

好了,今天的分享就到这里, 祝大家编程愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注