一、引言
目前,Unix系統V在微機上應用比較普遍,如:System V Release 4.2/386、SCOUnix、UnixWare 1.1或UnixWare 2.0等,在Unix系統V中有許多daemon(幽靈)進程,它們是Unix系統的重要組成部分。daemon進程一般有兩種運行方式:有的 daemon進程一直在運行並時刻等待某一事件的發生,如cron進程一直在運行並在指定的時間啟用某一程式,sendmail進程一直在一個連接埠上偵聽 是否有新郵件的到來等;有的daemon進程周期性地被啟用,並且在完成一個任務後終止,如uucp系統串連和檔案傳輸的uucico進程,當一個遠程機 器登入時,它能夠被一個登入shell啟用,calendar進程能夠在夜晚被cron進程啟用去檢查使用者的日曆和通知使用者即將到來的事件等。那麼什麼是 daemon呢?所謂daemon就是能夠自動執行循環性任務的程式,而且通常不為使用者所感知。普遍使用者可以根據需要編寫自己的daemon程式,系統程 序員發現某個管理工作需經常運行時,也可以為它編寫一個daemon程式。接觸過Unix系統的使用者都會覺得寫一個daemon程式是比較簡單的,但寫一 個正確的daemon程式卻不容易,因為使用者在進行一次普遍的登入會晤時,可以忽略諸如設定使用者和組的登入ID、建立會晤和進程組、分配控制終端、作業管 理控制等,這些工作Unix系統都替使用者做了,而對於daemon進程則要複雜得多,如果daemon進程是在一次登入會晤現場之外被啟用的(如:用 /etc/rc),則它需要自己負責設定作業管理控制等功能;如果是在一次登入會晤現場之內被啟用的,則它可能需要取消某些系統為它做的設定。因此一個好 的daemon程式應該可以從任何現場被啟用。
本文主要討論如何編寫有效daemon程式和如何啟動daemon進程。
二、daem的編程準則
根據我們工作的體驗,要編寫一個好的daemon程式一般需遵守以下一些準則。
1.應該使daemon進程不受後台作業I/O的影響
每個在登入會晤期間啟動並執行程式都與一個控制終端相關,該控制終端就是使用者從此處登入系統的。如果daemon進程是通過後台方式被啟用的,則在讀或寫控 制終端時可能會使daem on進程出錯。解決該問題最簡單的辦法是將daemon進程與它的控制終端脫聯。但是在某些場合下,daemon進程需要執行某些設定檢查或在失去它的控 制終端前顯示錯誤資訊。在這種情況下不能將daemon進程與它的控制終端脫聯,但是如果讓daemon進程忽略SIGTTOU訊號,則能進行可靠輸出。 為了安全起見,daemon進程還要忽略SIGTTIN和SIGTSTP訊號,其中忽略SIGTT IN訊號是避免daemon進程去讀取控制終端的輸入。
2.將daemon進程與它的原始進程組和控制終端脫聯
一個進程組就是一個進程的集合,它可以通過一個進程組ID被引用,每個進程是某個進程組的成員,可以用進程組ID將一個訊號發給該進程組中的所有進程。 例如:終端產生的訊號可以發送給與這個終端相聯的進程組中的每個進程。daemon在一次登入會晤時被啟用,它繼承控制終端、進程組和該會晤的現場,只要 該daemon進程還在與控制終端相聯的進程組中,它就接收終端產生的訊號(如:SIGINT或SIGHUP)。此外,當daemon進程還在它所啟動的 進程組中, 當另一個進程給這個進程組發送訊號時(如:通過kill系統調用),該daemon進程也將接收該訊號。為了防止daemon進程接收這些不需要的訊號, 一種簡單辦法是忽略所有的訊號,這樣,da emon進程就不能用訊號機制進行工作。
例如:進程間通訊等。事實上,這種辦法是不可行的, 因為有些訊號是不能被忽略的,如:SIGKILL或SIGSTOP,所以必須有一種有效辦法將daemon 進程與它所繼承的進程組分離開來。
在Unix系統V中,用setpgrp系統調用可以實現這種功能,此外也可以用setsid系統調用建立一個新的會晤、進程組,與原來的控制終端脫聯。 然而為了保證setpgrp或setsid系統調用達到目的,該進程不能是一個進程組的頭,所謂進程組的頭就是該進程的進程ID與進程組的ID相等。該程 序必須先調用fork系統調用複製自己,這樣在調用setpgrp或setsid之前確保已不是一個進程組的頭。子進程不是一個進程組的頭,因為子進程從 它的父進程處得到進程組的ID,但是從作業系統的核心得到一個新的進程ID,因此子進程的進程ID不可能與進程組的ID相等。
讓daemon進程用fork系統調用複製自己是非常有用的。許多daemon進程永遠不終止。如果一個使用者從shell啟用一個程式,但是忘記加後台 命令,shell就會一直等待該daemon的結束。假設該daemon進程屏蔽了鍵盤訊號,則此shell就不可用了。為了防止這種偶然事故的發生, daemon進程需立即建立一個子進程,並且在子進程中運行,父進程不等待子進程的結束就終止,這樣的執行順序使shell確信該daemon進程已經結 束,因為父進程已終止,儘管子進程還在不感知地運行著。
3.daemon進程不要重新獲得一個控制終端
一旦daemon進程成為沒有控制終端的一個進程組頭,它還有可能重新獲得一個控制終端。如果這樣,其它進程可能不能正確地獲得它們的控制終端。在 Unix系統V中,當一個沒有控制終端的進程組的頭開啟一個終端時,將獲得一個新的控制終端,如:daemon能夠用開啟/dev/console來獲得 一個控制終端。執行登入和錯誤輸出,儘管daemon進程隨後關閉了該終端,但是如果還有其它的進程開啟相同的終端,則daemon還可以用此終端。此時 用setpgrp或setsi d系統調用已不起作用,因為調用者是進程組的頭,所以必須防止重新獲得一個控制終端。一種防止獲得新控制終端的簡單辦法是:在調用setpgrp或 setsid後再建立一個子進程,daemon 在第二個子進程中運行,它的父進程(即第一個子進程)立即終止。因為終止的父進程是一個進程組的頭,子進程的進程組設定為0。這樣,該daemon進程沒 有控制終端,並處在一個新的進程組中,因此可以避免從終端獲得訊號,也不會重新獲得一個新的控制終端。
4.關閉所有開啟的檔案,特別是標準輸入、標準輸出和標準錯誤輸出
如果有某些檔案是終端裝置,則它們必須被關閉,不要維持開啟,即使確信開啟一個終端裝置時,該daemon將不會重新獲得一個控制終端。因為這有更深一 層的考慮:終端的狀態設定,如:傳輸速率、訊號字元的定義等,只有當最近開啟的進程關閉它時才能重新設定。如果該daemon進程一直開啟此終端,這樣在該 終端上進行一次新的登入前,沒有重新設定終端的屬性。
5.最好將記錄錯誤和狀態的資訊寫入一個磁碟檔案
因為該daemon進程沒有一個控制終端,所以不能寫到任何一個標準輸出檔案中。寫到總控台也不是一個好的辦法,因為總控台可能在一個視窗系統下運行。 但是使用者也會發現:將錯誤和狀態的資訊寫入自己定義的磁碟檔案也是有困難的,因為使用者必須知道哪些檔案被哪些daemon所修改,而且使用者必須時刻監視這 些檔案。那麼程式員該如何做呢?使用syslog機制。syslog機制是由4.2BSD引進的,在Unix系統V中也支援這種集中式的log機制。C庫 為daemon進程提供使用log機制的函數有:openlog, setlogmask, syslog,closelog。
6.將當前工作目錄改為根目錄
每個進程都有一個當前工作目錄,在該進程的生命週期,作業系統的核心使該目錄檔案處於開啟狀態。如果一個daemon進程的目前的目錄是在一個可裝卸的文 件系統上,則該檔案系統處於忙狀態,它就不能被超級使用者所拆卸,只有殺掉該daemon進程才能進行,因此daemon進程的當前工作目錄最好不要在一個 可裝卸的檔案系統上,最可靠的選擇是根目錄。子進程繼承父進程的當前工作目錄,但是子進程可以用chdir系統調用來修改自己的當前工作目錄。
根據以上討論的注意事項,下面我們給出一個daemon程式的架構:
例:daemon程式的架構
main(argc.argv)
int argc;
char *argv[];/* argv[1] is the file name of daemon program */
{
/* Try to ensure that execv() will succeed because we won′t
* be able to report any execv() error after daemonsetup()
* closes all our open file descriptors.
*/
if (access(argv[1], X-OK) <0) {
fprintf(stderr, ″%s: Can′t execute/n″, argv[0]);
perror(argv[1]);
exit(2);
}
daemonsetup(); /* setup a correct daemon environment */
openlog(″My own daemon″ ,LOG-PID│LOG-CONS, LOG-DAEMON);
syslog (LOG-INFO, ″Daemon starting...″);
closelog();
execv(argv[1], &argv[1]); /*launch the daemon command */
/* NOTREACHED */
exit(3);
}
/*
* daemonsetup() --Setup a correct daemon environment
*/
daemonsetup()
{
int fd; /* file descriptor */
if (getppid()==1) /* if parent is init */
goto out; /* can skip ahead */
/* igmore background process attempts write signal */
signal(SIGTTOU,SIG-IGN);
/* ignore background process attempts read signal */
signal(SIGTTIN,SIG-IGN);
/* ignore keyboard-generated stop signal */
signal(SIGTSTP,SIG-IGN);
/* fork child process */
if(fork()!=0)
exit(0); /* parent terminates */
/* child code: */
setsid();/* become session leader and group process leader with no controll
ing terminal */
out:
for (fd=0;fd<NOFILE;fd++) /* for all (possible) open files */
close(fd); /* close them */
chdir(″/″); /* move current directory off mounted file system */
return;
}
三、daemon進程的啟動方式
daemon進程的啟動方式主要有以下幾種。
1.用系統V Unix中的at命令。at命令是從標準輸入讀取daemon程式名並在以後某個時刻運行該daemon程式。at允許使用者指定何時運行該daemon,但日期和時間的格式須遵守環境變數LC-TIME所規定的。
2.使用者自己為daemon進程編寫一個crontab檔案。由cron進程根據crontab檔案的內容來啟用daemon進程。
3.在shell上使用後台命令(&)。
4.在每次使用者登入時通過一個shell開機檔案來啟用使用者的daemon進程(如:cshell中的.login、.cshrc檔案;shell的.profile檔案等)。這種啟用方式主要用於使用者的daemon進程。
5.通過init進程。系統初始化init進程直接或間接地負責啟動系統中的所有進程(核心的swapper、pageout進程等除外)。init進 程維持著它直接建立的所有進程的蹤跡。如果這些進程死亡時,它可以有選擇地重新啟用它們;或者系統的運行狀態改變為一個新狀態時,它可以殺死這些進程。 /etc/inittab檔案指定了某個運行狀態時init程式須啟用或者在它們死亡時時須重新啟用的程式名。在Unix系統V中,系統的daemon進 程通常放在/etc/rc2.d目錄下,當系統運行狀態從單使用者變為多使用者時,由/etc/rc啟用。此外,系統管理員也可以將系統daemon程式放在 /etc/inittab檔案中,由init進程直接啟用。這樣,當daemon進程死亡時,ini t進程可以重新啟用它;當系統狀態改變時,init進程也可以殺死該daemon進程。這種啟用方式主要用於系統的daemon進程。--