Python实现守护进程Daemon

Daemon 守护进程

概述

系统为了某些功能必须要提供一些服务,但服务的提供总是需要进程的运行,实现这个服务的程序我们就称之为daemon。

守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束。

用户使守护进程独立于所有终端。因为在守护进程从一个终端启动的情况下,这个终端可能被其他的用户使用。用户不希望其他用户在使用该终端的过程中,接收到守护进程的任何错误信息。同样,由终端键入的任何信号也不应该影响先前在该终端启动的任何守护进程的运行。

可以认为daemon就是service,两者不必区分的太清楚。因为达成某个service需要daemon在后台支持,没有daemon就没有service。

从守护进程的概念可以看出,系统所运行的每一种服务,都必须运行一个监听某个端口连接所发生的守护进程。

实现daemon  

 

    首先第一步是创建子进程,终止父进程,让守护进程脱离终端的控制。之后所有的工作都在子进程中完成,而用户在shell终端里则可以执行其他的命令,从而使得程序以僵尸进程形式运行。

  • 创建子进程,终止父进程
try:
            pid = os.fork() #生成子进程
            if pid > 0:
                sys.exit(0)  #父进程退出
        except OSError as err:
            sys.stderr.write('fork #1 failed: {0}\n'.format(err))
            sys.exit(1)
  • 在子进程中创建新会话


    setsid函数用于创建一个新的会话,并担任该会话组的组长。调用setsid有三个作用:让进程摆脱原会话的控制、让进程摆脱原进程组的控制和让进程摆脱原控制终端的控制。

    使用fork创建的子进程也继承了父进程的当前工作目录。由于在进程运行过程中,当前目录所在的文件系统不能卸载,因此,把当前工作目录换成其他的路径,如“/”或“/tmp”等。改变工作目录的常见函数是chdir。

    文件创建掩码是指屏蔽掉文件创建时的对应位。由于使用fork函数新建的子进程继承了父进程的文件创建掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件创建掩码设置为0,可以大大增强该守护进程的灵活性。设置文件创建掩码的函数是umask,通常的使用方法为umask(0)。

实现代码:

 os.chdir('/') #修改工作目录   
        os.setsid()  #设置新的会话连接  
        os.umask(0)  #重新设置文件创建权限  
  • 获得真正的守护进程

虽然当前关闭了和终端的联系,但是后期可能会误操作打开了终端。

第二次fork将确保进程重新打开控制终端,并且产生子-孙进程,而子进程退出后孙进程将成为真正的守护进程。

代码与之前的类似:

   try:
            pid = os.fork()  #第二次fork,禁止进程打开终端
            if pid > 0:
                # exit from second parent
                sys.exit(0) #第二个父进程退出
        except OSError as err:
            sys.stderr.write('fork #2 failed: {0}\n'.format(err))
            sys.exit(1)
  • 关闭文件描述符

用fork新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,可能导致所在的文件系统无法卸载。所以我们要关闭文件描述符。

       # 进程已经是守护进程了,重定向标准文件描述符
        sys.stdout.flush()
        sys.stderr.flush()
        si = open(os.devnull, 'r')
        so = open('/tmp/test.txt', 'a+')
        se = open(os.devnull, 'a+')

        #dup2函数原子化关闭和复制文件描述符
        os.dup2(si.fileno(), sys.stdin.fileno())         
        os.dup2(so.fileno(), sys.stdout.fileno())
        os.dup2(se.fileno(), sys.stderr.fileno())

 

  • 注册退出函数

首先,在daemonize方法外,写一个删除函数,并在daemonize注册它:

# 注意是在daemonize方法外写
    def delpid(self): # 删除pid
        os.remove(self.pidfile)

接着,回到daemonize方法内,

 # 注册删除pid函数
        atexit.register(self.delpid)
        # 根据文件pid判断是否存在进程
        pid = str(os.getpid())
        # 写入pidfile
        with open(self.pidfile, 'w+') as f:
            f.write(pid + '\n')

 

  • 启动进程

启动进程的思路是先检查pid文件是否存在,若存在,则代表守护进程正在运行,并提示错误。若不存在,则启动守护进程。

   def start(self):
        '''
        启动守护进程
        '''
        # 首先,检查pid文件是否存在以探测守护进程守护已运行
        try:
            with open(self.pidfile, 'r') as pf:
                pid = int(pf.read().strip()) 
        except IOError:
            pid = None

        if pid: # pid文件存在,代表守护进程已经在运行
            message = "pidfile {0} already exist. " + \
                      "Daemon already running?\n"
            sys.stderr.write(message.format(self.pidfile))
            sys.exit(1)

        # 守护进程没有运行,启动守护进程
        self.daemonize()
        self.run()
  • 关闭进程

关闭进程的思路是先检查pid文件是否存在,若不存在,则提示无守护进程在运行。若存在,则用kill方法来关闭进程。

    def stop(self):
        '''
        关闭守护进程
        '''
        # 从pid文件中获取pid
        try:
            with open(self.pidfile, 'r') as pf:
                pid = int(pf.read().strip())
        except IOError:
            pid = None

        if not pid:  # 守护进程没有运行
            message = "pidfile {0} does not exist. " + \
                      "Daemon not running?\n"
            sys.stderr.write(message.format(self.pidfile))
            return  # not an error in a restart

        # 使用kill来关闭进程
        try:
            while 1:
                os.kill(pid, signal.SIGTERM) # 信号
                time.sleep(0.1)
        except OSError as err:
            e = str(err.args)
            if e.find("No such process") > 0:
                if os.path.exists(self.pidfile):
                    os.remove(self.pidfile)
            else:
                print(str(err.args))
                sys.exit(1)

  • 重启进程

重启就很简单啦,先关闭再启动就搞定。

    def restart(self):
        '''
        重启守护进程
        '''
        self.stop() # 关闭守护进程
        self.start() # 启动守护进程

 

现在我们新建一个testDaemon.py来测试我们创建的守护进程。

重写run方法
现在我们创建testDaemon.py文件

写入我们的测试类,继承自daemon.py中的Daemon,并构造新的run()方法:

import os, sys, time

from daemon import Daemon

PID_FILE = '/tmp/testDaemon.pid'

class TestDaemon(Daemon):
    '''
    测试类
    测试守护进程是否可以正常运行
    '''
    def run(self):
        while True:
            sys.stdout.write('%s:hello world\n' % (time.ctime(),))
            sys.stdout.flush()
            time.sleep(1)

 

  • 构造main函数

现在来写main函数,这个在这个函数里,我们主要是对类进行实例化,还有实现对命令行不同参数的读取来实现start/stop/restart操作。

if __name__ == '__main__':
    '''
    在终端中输入 python testDaemon.py start/stop/restart 来控制守护进程。
    '''
    daemon = TestDaemon(PID_FILE) # 实例化对象

    if len(sys.argv) == 2:
        if 'start' == sys.argv[1]:
            print("Starting daemon...")
            daemon.start()
        elif 'stop' == sys.argv[1]:
            print("Stopping daemon...")
            daemon.stop()
        elif 'restart' == sys.argv[1]:
            print("Restarting daemon...")
            daemon.restart()
        else:
            print("Unknown command")
            sys.exit(2)
        sys.exit(0)
    else:
        print("usage: {0} start|stop|restart".format(sys.argv[0]))
        sys.exit(2)

完整代码:

daemon.py

import sys,os,time,atexit,signal

class Daemon:
    def __init__(self,pidfile):
        self.pidfile = pidfile


    def daemonize(self):
        try:
            pid = os.fork()
            if pid > 0:
                sys.exit(0)
        except OSError as err:
            sys.stderr.write('fork #1 failed: {0}\n'.format(err))
            sys.exit(1)
        os.chdir('/')
        os.setsid()
        os.umask(0)

        try:
            pid = os.fork()
            if pid > 0:
                sys.exit(0)
        except OSError as err:
            sys.stderr.write('fork #2 failed: {0}\n'.format(err))
            sys.exit(1)
        sys.stdout.flush()
        sys.stderr.flush()
        si = open(os.devnull,'r')
        so = open('/tmp/test.txt','a+')
        se = open(os.devnull,'a+')

        os.dup2(si.fileno(),sys.stdin.fileno())
        os.dup2(so.fileno(),sys.stdout.fileno())
        os.dup2(se.fileno(),sys.stderr.fileno())

    def delpid(self):
        os.remove(self.pidfile)
        atexit.register(self.delpid)
        pid = str(os.getpid())
        with open(self.pidfile,'w+') as f:
            f.write(pid + '\n')

    def start(self):

        try:
            with open(self.pidfile,'r') as pf:
                pid = int(pf.read().strip())
        except IOError:
            pid = None

        if pid:
            message = "pidfile {0} already exist." + \
                      "Daemon already running?\n"
            sys.stderr.write(message.format(self.pidfile))
            sys.exit(1)

        self.daemonize()
        self.run()

    def stop(self):

        try:
            with open(self.pidfile,'r') as pf:
                pid = int(pf.read().strip())
        except IOError:
            pid = None

        if not pid:
            message = "pidfile {0} does not exist." + \
                      "Daemon not running?\n"
            sys.stderr.write(message.format(self.pidfile))
            return
        try:
            while 1:
                os.kill(pid,signal.SIGTERM)
                time.sleep(0.1)
        except OSError as err:
            e = str(err.args)
            if e.find("No such process") > 0:
                if os.path.exists(self.pidfile):
                    os.remove(self.pidfile)
            else:
                print(str(err.args))
                sys.exit(1)

    def restart(self):

        self.stop()
        self.start()

TestDaemon.py

import os,sys,time

from daemon import Daemon

PID_FILE = '/tmp/testDaemon.pid'

class TestDaemon(Daemon):

    def run(self):
        while True:
            sys.stdout.write('%s:hello world\n' % (time.ctime(),))
            sys.stdout.flush()
            time.sleep(1)

if __name__ == '__main__':

    daemon = TestDaemon(PID_FILE)

    if len(sys.argv) == 2:
        if 'start' == sys.argv[1]:
            print("Starting daemon...")
            daemon.start()
        elif 'stop' == sys.argv[1]:
            print("Stopping daemon...")
            daemon.stop()
        elif 'restart' == sys.argv[1]:
            print("Restarting daemon...")
            daemon.restart()
        else:
            print("Unknown command")
            sys.exit(2)
        sys.exit(2)

    else:
        print("usage: {0} start|stop|restart".format(sys.argv[0]))
        sys.exit(2)

 

你可能感兴趣的:(Python,Devops)