实现守护进程

在linux或者unix操作系统中,守护进程(Daemon)是一种运行在后台的特殊进程,它独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。由于在linux中,每个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端被称为这些进程的控制终端,当控制终端被关闭的时候,相应的进程都会自动关闭。但是守护进程却能突破这种限制,它脱离于终端并且在后台运行,并且它脱离终端的目的是为了避免进程在运行的过程中的信息在任何终端中显示并且进程也不会被任何终端所产生的终端信息所打断。它从被执行的时候开始运转,直到整个系统关闭才退出。

实现守护进程的一般步骤(可参考《APUE》):

  1. 父进程fork出子进程并exit退出
  2. 子进程调用setsid创建新会话
  3. 子进程调用系统函数chdir将根目录"/"成为子进程的工作目录
  4. 子进程调用系统函数umask将该进程的umask设置为0
  5. 子进程关闭从父进程继承的所有不需要的文件描述符

以下用python实现一个守护进程:

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

################################################

# 子进程调用setsid创建新会话
# (fork后父进程退出,当前执行的已是子进程)
os.setsid()

################################################

# 子进程调用系统函数chdir将根目录"/"成为子进程的工作目录
os.chdir("/")

################################################

# 子进程调用系统函数umask将该进程的umask设置为0
os.umask(0)

#######################

# 第二次fork,禁止进程重新打开控制终端

try:
    pid = os.fork()
    if pid > 0:
        sys.exit(0)
except OSError, e:
    sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
    sys.exit(1)

################################################

# 子进程关闭从父进程继承的所有不需要的文件描述符
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())

return pid

以上是用python实现一个守护进程的基本步骤,在此基础上继续封装一下就可以拿去用了。

那么问题来了

Q. 这几个步骤用意何处呢?

父进程fork出子进程并exit退出有两个目的:第一,父进程如果是被一条shell命令启动的,那么父进程终止就会让shell认为这条命令已经正常执行完毕,这样少了很多不必要的麻烦;第二,父进程退出后子进程就继承了父进程的进程组ID,并且自己有一个新的进程ID,这就保证了子进程一定不是一个组长进程(如果父进程不是组长进程,那么子进程肯定不是组长进程;如果父进程是组长进程,则子进程继承进程组ID,此时该进程组无进程组组长),使得子进程可以顺利调用系统函数setsid()

子进程调用setsid创建新会话目的是创建一个新会话,如果调用系统函数setsid的进程是进程组组长的话,将会报错,这也是上面第一步必须要做的原因,setsid做三个操作:1. 调用进程成为新会话的首进程,2. 调用进程成为新进程组的组长(组长ID就是调用进程ID),3. 没有控制终端

子进程调用系统函数chdir将根目录"/"成为子进程的工作目录的目的是避免挂载磁盘一直被占用,假设一种情况,如果该进程是在挂载的一个文件系统里面启动的,那么在该进程结束前,你将无法正常卸载该文件系统

子进程调用系统函数umask将该进程的umask设置为0的目的是避免守护进程创建一个可读可写文件时可能失败的情况,因为自父进程继承的umask可能会屏蔽一些权限

子进程关闭从父进程继承的所有不需要的文件描述符,这个和是不是实现守护进程关系不大,一般从父进程fork过来得到的文件描述符都要关闭

Q. 父进程fork出来的子进程调用setsid是必须的吗?如果跳过调用setsid,将会怎么样?

成为守护进程一个基本条件是不与任何终端有瓜葛,这里简单介绍一个会话和进程组的关系,在一个会话里面,可以包含多个进程组,每个会话可以拥有一个终端,也就是说该进程组很有可能与会话里面的其他进程组共享了一个会话终端,这是不允许的,setsid就是要把这一可能有的关系给斩除。

Q. 为啥在第一次fork之后还需要第二次fork来禁止进程重新打开终端呢?

A. 再fork得到的子进程不再是会话组首进程,非会话组首进程(子进程)无法自动获得控制终端

Q. 为什么要重定向标准输入、标准输出和标准错误呢?

A. 因为有可能从父进程继承过来的被父进程重定向过,现在重定向以重置

最后附上python守护进程实现的源码:

#! /usr/bin/env python
#encoding=utf-8
import sys, os, time, atexit
from signal import SIGTERM
class Daemon:
    def __init__(self, pidfile=None, stderr='/dev/null', stdout='/dev/null', stdin='/dev/null'):
        self.stdin = stdin
        self.stdout = stdout
        self.stderr = stderr
        self.pidfile = pidfile

    def _daemonize(self):
        # 父进程fork出子进程并exit退出
        try:
            pid = os.fork()
            if pid > 0:
                sys.exit(0)
        except OSError, e:
            sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
            sys.exit(1)

        #脱离终端 
        os.setsid()
        #修改当前工作目录  
        #os.chdir("/")
        #重设文件创建权限
        os.umask(0)

        #第二次fork,禁止进程重新打开控制终端
        try:
            pid = os.fork()
            if pid > 0:
                sys.exit(0)
        except OSError, e:
            sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
            sys.exit(1)

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

        return pid
    
    def _write_pid(self):
        #注册程序退出时的函数,即删掉pid文件
        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):
        # Check for a pidfile to see if the daemon already runs
        if self.pidfile != None:
            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)
            self._daemonize()
            self._write_pid()
        else:
            self._daemonize()
        
    def stop(self):
        # 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):
        stop()
        start()
                

if __name__ == "__main__":
    import time
    d = Daemon()
    d.start()
    while 1:
        os.system("echo 'hello world' >> /tmp/text ")
        time.sleep(1)
    pass

最后,我水平有限,谬误之处,恳请指教~

你可能感兴趣的:(实现守护进程)