首页 > 技术文章 > python 守护进程

taurusfy 2017-12-01 11:09 原文

在linux环境上,使用守护进程保护python程序在后台运行,不受会话控制终端影响。

0x01 守护进程和后台运行的区别:

1、让程序在后台运行,只用在程序启动的时候在结尾加&,这样在控制终端进行输入不会影响程序的运行。

如python main.py&  程序启动后,可以在该控制继续进行输入,不影响main.py的运行。但是如果关闭会话连接,main.py进程就关闭了。

2、后台运行的程序有控制终端,守护进程没有。即如果main.py运行的会话连接断开,不影响main.py进程的运行。

     父进程退出后,子进程被pid为1的进程(init进程)接管,时候子进程不受终端退出的影响。

0x02 守护进程编程原理:

1、创建子进程,父进程退出

使用os.fork创建进程,并将父进程退出。  

目的:让子进程脱离父进程控制,被init进程接管,即变成pid为1的进程的子进程。

2、在子进程中创建新的会话

使用setid创建新会话,并担任该会话组的组长。

目的:

  • 让进程摆脱原会话的控制
  • 让进程摆脱原进程组的控制
  • 让进程摆脱原控制终端的控制

由于创建守护进程的第一步调用了fork函数来创建子进程,再将父进程退出。由于在调用了fork函数时,子进程全盘拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,因此,这还不是真正意义上的独立开来,而setsid函数能够使进程完全独立出来,从而摆脱其他进程的控制。

3、重设文件权限掩码

使用umask(0)重新设置文件权限,是为了去掉父进程遗留的文件权限设置。

4、禁止进程重新打开控制终端

在基于SystemV的系统中,有建议再一次调用fork 并使父进程退出。而新产生的进程将会成为真正的守护进程。这一步骤将保证守护进程不是一个sessionleader,进而阻止它获取一个控制终端。

第二次fork不是必须的,打开一个控制终端的条件是该进程必须是session leader。第一次fork,setsid之后,子进程成为session leader,进程可以打开终端;第二次fork产生的进程,不再是session leader,进程则无法打开终端。

5、改变当前工作目录

防止占用别的路径的working dir的fd,导致一些block不能unmount。但注意这里修改了以后,守护进程下面的子进程log打印就都在被修改的路径下了。

 

0x03 DEMO

#!/usr/bin/env python
#coding: utf-8
#pythonlinux的守护进程

import sys
import os
import time
import string
import ctypes
import datetime
from logger import *

logyyx = Logger('tsl.log', logging.ERROR, logging.DEBUG)

class Daemon:
    def __init__(self, findCmd, runCmd, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
        self.findCmd = findCmd
        self.runCmd = runCmd
        self.stdin = stdin
        self.stdout = stdout
        self.stderr = stderr
        #self.logger = logging.getLogger()
    '''
    def LoggerInit(self):
        logfile = '/home/***/log/tsl.log'
        hdlr=logging.FileHandler(logfile)
        formatter = logging.Formatter('\n%(asctime)s   %(filename)s[line:%(lineno)d]   %(levelname)s\n%(message)s')
        hdlr.setFormatter(formatter)
        self.logger.addHandler(hdlr)
        self.logger.setLevel(logging.NOTSET)
        return
    '''
    def daemonize(self):
        try:
            #第一次fork,生成子进程,脱离父进程
            if os.fork() > 0:
                raise SystemExit(0)      #退出主进程
        except OSError as e:
            logyyx.error("fork #1 failed:\n")
            #sys.exit(1)
            raise RuntimeError('fork #1 faild: {0} ({1})\n'.format(e.errno, e.strerror))

        os.setsid()        #设置新的会话连接
        os.umask(0)        #重新设置文件创建权限
        try:
            #第二次fork,禁止进程打开终端
            if os.fork() > 0:
                raise SystemExit(0)
        except OSError as e:
            logyyx.error("fork #2 failed:\n")
            #sys.exit(1)
            raise RuntimeError('fork #2 faild: {0} ({1})\n'.format(e.errno, e.strerror))
        os.chdir("/")  # 修改工作目录
        # Flush I/O buffers
        sys.stdout.flush()
        sys.stderr.flush()

        # Replace file descriptors for stdin, stdout, and stderr
        with open(self.stdin, 'rb', 0) as f:
            os.dup2(f.fileno(), sys.stdin.fileno())
        with open(self.stdout, 'ab', 0) as f:
            os.dup2(f.fileno(), sys.stdout.fileno())
        with open(self.stderr, 'ab', 0) as f:
            os.dup2(f.fileno(), sys.stderr.fileno())

        return

    def start(self):
        #检查pid文件是否存在以探测是否存在进程
        esb = os.popen(self.findCmd).read().strip()
        if not (esb == '0'):
            print"the deamon is already running!!!"
            return
        else:
            #启动监控
            self.daemonize()
            self.run()

    def run(self):
        now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        while True:
            try:
                esb = os.popen(self.findCmd).read().strip()
                if (esb == '0'):
                    logyyx.info("deamon on  %s" %now)
                    os.system(self.runCmd)
            except:
                pass
            time.sleep(10)

    def KillPid(self,name):
        ps_str = 'ps aux |grep '+name+' | grep -v grep'
        x= os.popen(ps_str).read()
        if x:
            proc = x.split('\n')
            for line in proc:
                print line
                try:
                    proc_id = line.split()[1]
                    os.system('kill -9 %s' % proc_id)
                except:
                    pass
        else:
            return

    def checkpid(self, name):
        findCmd='ps -fe |grep '+name+' | grep -v grep | wc -l'
        esb = os.popen(findCmd).read().strip()
        if not (esb == '0'):
            #杀进程
            try:
                self.KillPid(name)
            except:
                print"kill %s failed!!!" % name
                logyyx.error("the deamon  %s  kill failed" % name)
                return
        return
    def stop(self):
        self.checkpid('main.py')
        self.checkpid('deamon.py')
        return

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

if __name__ == "__main__":
    findCmd = 'ps -fe |grep main.py | grep -v grep | wc -l'
    runCmd = 'python /home/***/main.py'
    LOG = './tsl.log'
    daemon = Daemon(findCmd, runCmd, stdout=LOG, stderr=LOG)

    #daemon.start()
    if len(sys.argv) != 2:
        print('Usage: {} [start|stop]'.format(sys.argv[0]))
        raise SystemExit(1)
    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 {0}'.format(sys.argv[1]))
        raise SystemExit(1)

 

推荐阅读