天天看點

使用PHP腳本來寫Daemon程式

什麼是Daemon程序

這又是一個有趣的概念,daemon在英語中是"精靈"的意思,就像我們經常在迪斯尼動畫裡見到的那些,有些會飛,有些不會,經常圍着卡通片的主人公轉來轉去,啰裡啰唆地提一些忠告,時不時倒黴地撞在柱子上,有時候還會想出一些小小的花招,把主人公從敵人手中救出來,正因如此,daemon有時也被譯作"守護神"。是以,daemon程序在國内也有兩種譯法,有些人譯作"精靈程序",有些人譯作"守護程序",這兩種稱呼的出現頻率都很高。

與真正的daemon相似,daemon程序也習慣于把自己隐藏在人們的視線之外,默默為系統做出貢獻,有時人們也把它們稱作"背景服務程序"。daemon程序的壽命很長,一般來說,從它們一被執行開始,直到整個系統關閉,它們才會退出。幾乎所有的伺服器程式,包括我們熟知的Apache和wu-FTP,都用daemon程序的形式實作。很多Linux下常見的指令如inetd和ftpd,末尾的字母d就是指daemon。

為什麼一定要使用daemon程序呢?Linux中每一個系統與使用者進行交流的界面稱為終端(terminal),每一個從此終端開始運作的程序都會依附于這個終端,這個終端就稱為這些程序的控制終端(Controlling terminal),當控制終端被關閉時,相應的程序都會被自動關閉。關于這點,讀者可以用X-Window中的XTerm試驗一下,(每一個XTerm就是一個打開的終端,)我們可以通過鍵入指令啟動應用程式,比如:$netscape 然後我們關閉XTerm視窗,剛剛啟動的netscape視窗也會随之一同突然蒸發。但是daemon程序卻能夠突破這種限制,即使對應的終端關閉,它也能在系統中長久地存在下去,如果我們想讓某個程序長命百歲,不因為使用者或終端或其他的變化而受到影響,就必須把這個程序變成一個daemon程序。

Daemon程序的程式設計規則

如果想把自己的程序變成daemon程序,我們必須嚴格按照以下步驟進行:

1、調用fork産生一個子程序,同時父程序退出。我們所有後續工作都在子程序中完成。這樣做我們可以:

     1.1 如果我們是從指令行執行的該程式,這可以造成程式執行完畢的假象,shell會回去等待下一條指令;

     1.2 剛剛通過fork産生的新程序一定不會是一個程序組的組長,這為第2步的執行提供了前提保障。

     這樣做還會出現一種很有趣的現象:由于父程序已經先于子程序退出,會造成子程序沒有父程序,變成一個孤兒程序(orphan)。每當系統發現一個孤兒程序,就會自動由1号程序收養它,這樣,原先的子程序就會變成1号程序的子程序。

2、調用setsid系統調用。這是整個過程中最重要的一步。setsid的介紹見附錄2,它的作用是建立一個新的會話(session),并自任該會話的組長(session leader)。如果調用程序是一個程序組的組長,調用就會失敗,但這已經在第1步得到了保證。調用setsid有3個作用:

     2.1 讓程序擺脫原會話的控制;

     2.2 讓程序擺脫原程序組的控制;

     2.3 讓程序擺脫原控制終端的控制;

     總之,就是讓調用程序完全獨立出來,脫離所有其他程序的控制。

3、把目前工作目錄切換到根目錄。

     如果我們是在一個臨時加載的檔案系統上執行這個程序的,比如:/mnt/floppy/,該程序的目前工作目錄就會是/mnt/floppy/。在整個程序運作期間該檔案系統都無法被卸下(umount),而無論我們是否在使用這個檔案系統,這會給我們帶來很多不便。解決的方法是使用chdir系統調用把目前工作目錄變為根目錄,應該不會有人想把根目錄卸下吧。

     關于chdir的用法,參見附錄1。

     當然,在這一步裡,如果有特殊的需要,我們也可以把目前工作目錄換成其他的路徑,比如/tmp。

4、将檔案權限掩碼設為0。

     這需要調用系統調用umask,參見附錄3。每個程序都會從父程序那裡繼承一個檔案權限掩碼,當建立新檔案時,這個掩碼被用于設定檔案的預設通路權限,屏蔽掉某些權限,如一般使用者的寫權限。當另一個程序用exec調用我們編寫的daemon程式時,由于我們不知道那個程序的檔案權限掩碼是什麼,這樣在我們建立新檔案時,就會帶來一些麻煩。是以,我們應該重新設定檔案權限掩碼,我們可以設成任何我們想要的值,但一般情況下,大家都把它設為0,這樣,它就不會屏蔽使用者的任何操作。

     如果你的應用程式根本就不涉及建立新檔案或是檔案通路權限的設定,你也完全可以把檔案權限掩碼一腳踢開,跳過這一步。

5、關閉所有不需要的檔案。

     同檔案權限掩碼一樣,我們的新程序會從父程序那裡繼承一些已經打開了的檔案。這些被打開的檔案可能永遠不被我們的daemon程序讀或寫,但它們一樣消耗系統資源,而且可能導緻所在的檔案系統無法卸下。需要指出的是,檔案描述符為0、1和2的三個檔案(檔案描述符的概念将在下一章介紹),也就是我們常說的輸入、輸出和報錯這三個檔案也需要被關閉。很可能不少讀者會對此感到奇怪,難道我們不需要輸入輸出嗎?但事實是,在上面的第2步後,我們的daemon程序已經與所屬的控制終端失去了聯系,我們從終端輸入的字元不可能達到daemon程序,daemon程序用正常的方法(如printf)輸出的字元也不可能在我們的終端上顯示出來。是以這三個檔案已經失去了存在的價值,也應該被關閉。 

使用PHP編寫Gearman的Worker守護程序

在我之前的文章中,介紹過Gearman的使用。在我的項目中,我使用了PHP來編寫一直運作的Worker。如果按照Gearman官方推薦的例子,隻是簡單的一個循環來等待任務,會有一些問題,包括:1、當代碼進行過修改之後,如何讓代碼的修改生效;2、重新開機Worker的時候,如何保證目前的任務處理完成才重新開機。

針對這個問題,我考慮了以下的解決方法:

1、每次修改完代碼後,Worker需要手工重新開機(先殺死然後啟動)。這個隻能解決重新加載配置檔案的問題。

2、在Worker中設定,單次任務循環完成後,就對Worker進行重新開機。這個方案的問題在于消耗比較大。

3、在Worker中添加一個退出函數,如果需要Worker退出的時候,在Client端發送一個優先級比較高的退出調用。這個需要用戶端配合,在使用背景類任務時,不太适合。

4、在Worker中檢查檔案是否發生變化,如果發生了變化,退出并重新開機自身。

5、為Worker編寫信号控制,接受重新開機指令,類似于 http restart graceful 指令。

最後,結合4和5兩種方法,可以實作這樣一個Daemon,如果配置檔案發生了變化,他就會自動重新開機;如果接受到了使用者的 kill  -1 pid 信号,也會重新啟動。

代碼如下:

<?php

declare( ticks = 1 );

// This case will check the config file regularly, if the config file changed, it will restart it self

// If you want to restart the daemon gracefully, give it a HUP signal

// by shiqiang<[email protected]> at 2011-12-04

$init_md5 = md5_file( 'config.php');

// register signal handler

pcntl_signal( SIGALRM, "signal_handler", true );

pcntl_signal( SIGHUP, 'signal_handler', TRUE );

$job_flag = FALSE;    //Job status flag, to justify if the job has been finished

$signal_flag = FALSE;    //Signal status flag, to justify whether we received the kill -1 signal

while( 1 ){

    $job_flag = FALSE;    //Job status flag

    print "Worker start running ... \n";

    sleep(5);

    print "Worker's task done ... \n";

    $flag = TRUE;    //Job status flag

    AutoStart( $signal_flag );

}

function signal_handler( $signal ) {

    global $job_flag;

    global $signal_flag;

    switch( $signal ){

        case SIGQUIT:

            print date('y-m-d H:i:s', time() ) . " Caught Signal : SIGQUIT - No : $signal \n";

            exit(0);

            break;

        case SIGSTOP:

            print date('y-m-d H:i:s', time() ) . " Caught Signal : SIGSTOP - No : $signal \n";

        case SIGHUP:

            print date('y-m-d H:i:s', time() ) . " Caught Signal : SIGHUP - No : $signal \n";

            if( $flag === TRUE ){

                AutoStart( TRUE );

            }else{

                $signal_flag = TRUE;

            }

        case SIGALRM:

            print date('y-m-d H:i:s', time() ) . " Caught Signal : SIGALRM - No : $signal \n";

            //pcntl_exec( '/bin/ls' );

            pcntl_alarm( 5 );

        default:

    }

function AutoStart( $signal = FALSE, $filename = 'config.php' ){

    global $init_md5;

    if( $signal || md5_file( $filename ) != $init_md5 ){

        print "The config file has been changed, we are going to restart. \n";

        $pid = pcntl_fork();

        if( $pid == -1 ){

            print "Fork error \n";

        }else if( $pid > 0 ){

            print "Parent exit \n";

        }else{

            $init_md5 = md5_file( $filename );

            print "Child continue to run \n";

        }

參考資料: