1. 程式人生 > 實用技巧 >python:守護程序deamon

python:守護程序deamon

一、守護程序的基本編碼規範

詳細參見:《AdvancedProgrammingin The Unix Environment》Section 13.3 Page 583

本小節將介紹一些守護程序的基本編碼規範,這些規範將阻止守護程序與當前環境產生一些不必要的互動。本節將通過一個函式daemonize實現這些規範。
1. 首先要做的被稱為 umask,這一步驟會將檔案建立掩碼重置為0。這一步的原因是守護程序繼承(inherited)得到的檔案掩碼有可能會拒絕某些特定的檔案操作許可權。如果守護程序想要建立檔案,那有可能它需要設定特定的檔案操作許可權。例如,如果守護程序想要建立允許組讀和寫(group-readand group-write)許可權的檔案,但繼承得到的檔案建立掩碼遮蔽了這個許可權,則建立操作不會成功。


2. 呼叫 fork 並使父程序退出(exit)。這一步驟的目的在於。首先,如果守護程序是通過一個簡單的shell命令建立的,那麼在父程序終止的時候shell會認為命令已經結束了繼而結束守護程序。其次,子程序繼承得到父程序的groupID同時也獲得了一個新的程序號,所以我們必須得保證子程序不能擔任groupleader的角色,這是下一步setsid 操作的前提。
注:此步驟是使得程序在後臺執行。

3. 呼叫 setsid 建立一個新的會話。有三個步驟將會執行:(a)程序將成為這個新會話的sessionleader角色,(b)此程序將會成為一個新的程序組的groupleader,(c)此程序將不會有控制終端。
注:此步驟將使得程序脫離控制終端、登入會話以及程序組。

在基於SystemV的系統中,有建議再一次呼叫fork 並使父程序退出。而新產生的程序將會成為真正的守護程序。這一步驟將保證守護程序不是一個sessionleader,進而阻止它獲取一個控制終端。或者另一種阻止守護程序獲取控制終端的方案是任意時刻開啟一個終端裝置的時候確保指定O_NOCTTY。
注:此步驟將禁止程序重新開啟控制終端。

4. 將當前的工作目錄切換到系統根目錄下。因為從父程序整合來的當前工作目錄可能是一個被掛載的檔案系統。因為守護程序通常是直到系統重啟的時候才會退出,如果守護程序的工作目錄在一個掛載的檔案系統上,那麼這個檔案系統就不能被解除安裝(unmounted)。

有的守護程序可能會將當前的工作目錄切換到一些特定的路徑,在這些路徑下它們將完成它們的工作。例如,lineprinter spoolingdaemons 通常將工作目錄切換為spool目錄。

5. 一些不必要的檔案描述符將會被關閉。這個步驟將阻止守護程序保持從父程序整合到的任何已經開啟的檔案描述符(也可能是shell或其他程序)。我們可以使用 open_max 函式或 getrlimit 函式來查詢當前優先順序最高的檔案描述符並關閉此描述符之下的所有其他描述符。
注:保持開啟的描述符將會佔用系統資源並可能使某些檔案不能被解除安裝。

6. 有一些守護程序將開啟檔案描述符0, 1, 2 指向 /dev/null ,這樣一來所有試圖從標準輸入、輸出及錯誤讀取守護程序資訊的操作都不能成功。因為守護程序當前已經不與任何終端裝置相關聯,沒有地方顯示其輸出或接受使用者的輸入。即使守護程序是從一個互動式session建立的,守護程序也將執行在後臺,任何終端的登入與終止將不會影響守護程序。如果有其他使用者通過當前的終端登入,我們也不希望守護程序的輸出出現在終端上,並且該使用者的任何輸入也不會被守護程序接收。

總結起來就是:
1) 第一次fork將會建立父-子程序,同時使得父程序退出保證守護程序能夠執行在後臺。
2) 通過setsid步驟使得程序與控制終端、登入會話以及程序組脫離。
3) 第二次fork將確保程序重新開啟控制終端,並且產生子-孫程序,而子程序退出後孫程序將成為真正的守護程序。
4) 其他還有一些諸如工作目錄設定、關閉檔案描述符、設定檔案建立掩碼之類的操作。

二、python守護程序的一個例子

詳見:https://gist.github.com/jamiesun/3097215,樣例程式碼如下

#! /usr/bin/env python2.7
#encoding:utf-8
#@description:一個python守護程序的例子
#@tags:python,daemon
import sys
import os
import time
import atexit
from signal import SIGTERM 
 
 
class Daemon:
    """
    A generic daemon class.
    
    Usage: subclass the Daemon class and override the run() method
    """
    def __init__(self, pidfile, stdin='/dev/null', stdout='/dev/null', stderr='/dev/null'):
        self.stdin = stdin
        self.stdout = stdout
        self.stderr = stderr
        self.pidfile = pidfile
    
    def daemonize(self):
        """
        do the UNIX double-fork magic, see Stevens' "Advanced 
        Programming in the UNIX Environment" for details (ISBN 0201563177)
        http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
        """
        try: 
            pid = os.fork() 
            if pid > 0:
                # exit first parent
                sys.exit(0) 
        except OSError, e: 
            sys.stderr.write("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror))
            sys.exit(1)
    
        # decouple from parent environment
        os.chdir("/") 
        os.setsid() 
        os.umask(0) 
    
        # do second fork
        try: 
            pid = os.fork() 
            if pid > 0:
                # exit from second parent
                sys.exit(0) 
        except OSError, e: 
            sys.stderr.write("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror))
            sys.exit(1) 
    
        # redirect standard file descriptors
        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())
    
        # write pidfile
        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):
        """
        Start the daemon
        """
        # Check for a pidfile to see if the daemon already runs
        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)
        
        # Start the daemon
        self.daemonize()
        self.run()
 
    def stop(self):
        """
        Stop the daemon
        """
        # 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):
        """
        Restart the daemon
        """
        self.stop()
        self.start()
 
    def run(self):
        """
        You should override this method when you subclass Daemon. It will be called after the process has been
        daemonized by start() or restart().
        """        
 
 
class MyDaemon(Daemon):
    def run(self):
        while True:
            time.sleep(60)
            print 'daemon runing'
 
 
 
 
if __name__ == "__main__":
    daemon = MyDaemon("/var/run/demodaemon.pid")
    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)

三、編寫的針對primeton ESB SERVER程式的守護程序樣例

監控程式的程序是否存在,程式的埠是否正常

#!/usr/bin/python
# encoding:utf-8
# @description:一個python守護程序
# @tags:python,daemon
import atexit
import os
from signal import SIGTERM 
import socket
import sys
import time


class ESBDaemon:
    # 建構函式
    def __init__(self, pidfile, ip, port, findCmd, runCmd, stopCmd, 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.ip = ip
        self.port = port
        self.findCmd = findCmd
        self.runCmd = runCmd
        self.stopCmd = stopCmd
       

    def daemonize(self):
        # 第一次fork將會建立父-子程序,同時使得父程序退出保證守護程序能夠執行在後臺
        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步驟使得程序與控制終端、登入會話以及程序組脫離
        os.chdir("/") 
        os.setsid() 
        os.umask(0) 
    
        # 第二次fork將確保程序重新開啟控制終端,並且產生子-孫程序,而子程序退出後孫程序將成為真正的守護程序
        # 建立子程序
        try: 
            pid = os.fork() 
            if pid > 0:
                # exit from second parent
                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())
    
        # 建立processid檔案
        atexit.register(self.delpid)
        pid = str(os.getpid())
        file(self.pidfile, 'w+').write("%s\n" % pid)

  
    def delpid(self):
        os.remove(self.pidfile)
        
    # 可用於檢測程式是否正常,如檢測redis是否正常,即檢測redis的6379埠是否正常
    def check_aliveness(self):
        sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        sk.settimeout(1)
        try:
            sk.connect((self.ip, self.port))
            return True
        except Exception:
            return False
        finally:
            sk.close()
            return False
        
    def writeFile(self, msg):
        f = file("/var/run/esb.log", 'a+')
        f.write(msg + time.strftime('%Y-%m-%d %H:%M:%S') + "\r\n")  # write text to file
        f.close()

    def start(self):
        # 檢查pid檔案是否存在以探測是否存在程序
        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)
            self.writeFile(message % self.pidfile)
            sys.exit(1)
        
        # 啟動監控
        self.writeFile("啟動監控")
        self.daemonize()
        self.run()

    def stop(self):
        self.writeFile("停止程式")
        # 從pid檔案中獲取pid
        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)
            self.writeFile(message % self.pidfile)
            return  # 重啟不報錯
 
        # 殺死程序
        try:
            while 1:
                os.kill(pid, SIGTERM)
                time.sleep(0.1)
                os.system(self.stopCmd)
        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)
                self.writeFile(str(err))
                sys.exit(1)
    def restart(self):
        self.writeFile("重啟程式")
        self.stop()
        self.start()
 
    def run(self):   
        while True:
            try:
                self.writeFile("檢查程式是否正常")
                esb = os.popen(self.findCmd).read().strip()
                portNormal=self.check_aliveness()
                if esb == '0' and (not portNormal):
                    self.writeFile("程式程序不存在 :" + str(esb) + " " + str(portNormal))
                    os.system(self.runCmd)
                time.sleep(30)
            except OSError, err:
               self.writeFile(str(err)) 
    
if __name__ == "__main__":
    daemon = ESBDaemon(
		"/var/run/dfsdaemon.pid",
		"172.168.27.153",
		6200,
		'ps -fe | grep "primeton" | grep "server" | grep -v "grep" | wc -l',
		'nohup /home/primeton/esb/startServer.sh > /home/primeton/esb/server.log 2>&1 &',
		'/home/primeton/esb/stopServer.sh'
		)
    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)

四、對應的shell版本

監控程式的程序是否存在,程式的埠是否正常

#/bin/sh
host="172.168.27.153"
port=6200
appName="server"

while true;
do
    count=`ps -fe | grep "primeton" | grep "$appName" | grep -v "grep" | wc -l`
    nport=`echo ""|telnet $host $port 2>/dev/null|grep "\^]"|wc -l`
    
    echo "程式程序數:" $count "埠是否通:" $nport
    
    if [ "$count" != "1" -a $nport -eq 0 ]; then
        echo "重新啟動程式"
        rm -rf /home/primeton/esb/server/EOS/_srv/work/mq_data/localhost/kr-store/data
        nohup /home/primeton/esb/startServer.sh > /home/primeton/esb/server.log 2>&1 &
    fi
    sleep 2
done

五、參考資料

http://slaytanic.blog.51cto.com/2057708/742049

http://blog.csdn.net/dysj4099/article/details/18219411

https://gist.github.com/jamiesun/3097215

來源:站長資訊