关于python脚本以linux守护进程(daemon)运行

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/chenbogger/article/details/99312582

linux守护进程及其特性

守护进程最重要的特性是后台执行
在这一点上DOS下的常驻内存程序TSR与之类似。其次,守护进程必须与其执行前的环境隔离开来。这些环境包含未关闭的文件描写叙述符。控制终端。会话和进程组,工作文件夹以及文件创建掩模等。

linux守护进程与普通进程的区别

  1. 默认情况下,进程是在前台运行的,这时就把shell占据了(有很多日志打印输出),我们无法进行其他操作。所以对于没有交互的进程,很多时候我们希望将其在后台启动,可以在启动参数的时候加一个&实现这个目的。
  2. 守护进程已经完全脱离终端控制了,而后台进程并未完全脱离终端,在终端未关闭前还是会往终端输出结果。
  3. 守护进程在关闭终端控制台时不会受影响,而后台程序会随用户退出而停止,需要在以nohup command &格式运行才能避免影响。
  4. 守护进程的会话组和当前目录,文件描述符都是独立的,后台运行只是终端进行了一次fork,让程序在后台执行,这些都没改变。

Python编写守护进程程序思路

  1. fork子进程,父进程退出
    通常,我们执行服务端程序的时候都会通过终端连接到服务器,成功连接后会加载shell环境,终端和shell都是进程,shell进程是终端进程的子进程,通过ps命令可以很容易的查看到。在这个shell环境下一开始执行的程序都是shell进程的子进程,自然会受到shell进程的影响。在程序里fork子进程后,父进程退出,对了shell进程来说,这个父进程就算执行完了,而产生的子进程会被init进程接管,从而也就脱离了终端的控制。
  2. 修改子进程的工作目录
    子进程在创建的时候会继承父进程的工作目录,比如Nginx就有它的默认工作目录 /etc/nginx/conf.d/default.conf
  3. 创建进程组
    使用setsid后,子进程就会成为新会话的首进程(session leader);子进程会成为新进程组的组长进程;子进程没有控制终端。
  4. 修改umask
    由于umask会屏蔽权限,所以设定为0,这样可以避免读写文件时碰到权限问题。
  5. fork孙子进程,子进程退出
    经过上面几个步骤后,子进程会成为新的进程组老大,可以重新申请打开终端,为了避免这个问题,fork孙子进程出来。
  6. 重定向孙子进程的标准输入流、标准输出流、标准错误流到/dev/null
    因为是守护进程,本身已经脱离了终端,那么标准输入流、标准输出流、标准错误流就没有什么意义了。所以都转向到/dev/null,就是都丢弃的意思。

代码实现

这里我以类的方式实现,方便以后直接使用

首先创建一个日志记录器

def creat_handler():
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.INFO)
    # logs/log--需要修改成自己定义的路径&文件名
    # 创建日志记录器,指明日志保存的路径、每个日志文件的最大大小、保存的日志文件个数上限
    file_log_handler = RotatingFileHandler(LOG_FILE_PATH, maxBytes=1024 * 1024 * 100, backupCount=10)
    # 创建日志记录的格式 日志等级 输入日志信息的文件名 行数 日志信息
    formatter = logging.Formatter('%(levelname)s %(filename)s:%(lineno)d - %(asctime)s - %(name)s - %(message)s')
    # 为刚创建的日志记录器设置日志记录格式
    file_log_handler.setFormatter(formatter)
    logger.addHandler(file_log_handler)
    return logger

定义创建守护进程类

class Daemon(object):
    """python模拟linux的守护进程"""
    def __init__(self, pidfile, base_path, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
        # 需要获取调试信息,改为stdin='/dev/stdin', stdout='/dev/stdout', stderr='/dev/stderr',以root身份运行。
        self.stdin = stdin
        self.stdout = stdout
        self.stderr = stderr
        self.pidfile = pidfile
        self.base_path = base_path

    def _daemonize(self):
        try:
            pid = os.fork()  # 第一次fork,生成子进程,脱离父进程
            if pid > 0:
                sys.exit(0)  # 退出主进程
        except OSError as e:
            logger.error('fork #1 failed: %d (%s)\n' % (e.errno, e.strerror))
            sys.exit(1)

        os.chdir("/")  # 修改工作目录
        os.setsid()  # 设置新的会话连接
        os.umask(0)  # 重新设置文件创建权限

        try:
            pid = os.fork()  # 第二次fork,禁止进程打开终端
            if pid > 0:
                sys.exit(0)
        except OSError as e:
            logger.error('fork #2 failed: %d (%s)\n' % (e.errno, e.strerror))
            sys.exit(1)

        # 重定向文件描述符
        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())

        # 注册退出函数,根据文件pid判断是否存在进程
        atexit.register(self.delpid)
        pid = str(os.getpid())
        open(self.pidfile, 'w+').write('%s\n' % pid)

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

    def start(self):
        # 检查pid文件是否存在以探测是否存在进程
        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'
            logger.warning(message % self.pidfile)
            sys.exit(message % self.pidfile)

            # 启动监控
        self._daemonize()
        self._run()

    def stop(self):
        # 从pid文件中获取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'
            logger.error(message % self.pidfile)
            return

        # 杀死进程
        try:
            while 1:
                os.kill(pid, 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:
                sys.exit(str(err))

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

    def _run(self):
        """ 运行自定义函数"""
        pass

使用示例

继承Daemon,重写run方法

class MyDaemon(Daemon):
    def _run(self):
        while True:
            os.system("echo 'hello world' >> a.txt")
            time.sleep(1)
            

书写启动方式

if __name__ == "__main__":
	daemon = MyDaemon('/tmp/process.pid', BASE_PATH, stdout='/tmp/stdout.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)

启动、停止、重启

python xxx.py start
python xxx.py stop
python xxx.py restart

猜你喜欢

转载自blog.csdn.net/chenbogger/article/details/99312582