实现守护进程

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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容