守護進程
最近正好在學守護進程,簡單的做下筆記, 下面是來自百度文科以及網絡的資料,對守護進程的概念以及實現步驟作一個瞭解:
在linux或者unix操作系統中在系統的引導的時候會開啓很多服務,這些服務就叫做守護進程.爲了增加靈活性,root可以選擇系統開啓的模式,這些模式叫做運行級別,每一種運行級別以一定的方式配置系統.守護進程是脫離於終端並且在後臺運行的進程.守護進程脫離於終端是爲了避免進程在執行過程中的信息在任何終端上顯示並且進程也不會被任何終端所產生的終端信息所打斷.
守護進程簡介
守護進程,也就是通常說的Daemon進程,是Linux中的後臺服務進程.它是一個生存期較長的進程,通常獨立於控制終端並且週期性地執行某種任務或等待處理某些發生的事件.守護進程常常在系統引導裝入時啓動.在系統關閉時終止.Linux系統有很多守護進程,大多數服務都是通過守護進程實現的,同時,守護進程還能完成許多系統任務,例如作業規劃進程crond,打印進程lqd等(這裏的結尾字母d就是Daemon的意思).
由於在Linux中,每一個系統與用戶進行交流的界面稱爲終端,每一個從此終端開始運行的進程都會依附於這個終端,這個終端就稱爲這些進程的控制終端,當控制終端被關閉時,相應的進程都會自動關閉.但是守護進程卻能夠突破這種限制,它從被執行開始運轉,直到整個系統關閉時才退出.如果想讓某個進程不因爲用戶或終端或其他地變化而受到影響,那麼就必須把這個進程變成一個守護進程.
一.守護進程及其特性
守護進程最重要的特性是後臺運行.在這一點上DOS下的常駐內存程序TSR與之相似.其次,守護進程必須與其運行前的環境隔離開來.這些環 境包括未關閉的文件描述符,控制終端,會話和進程組,工作目錄以及文件創建掩模等.這些環境通常是守護進程從執行它的父進程(特別是shell)中繼承下來的.最後,守護進程的啓動方式有其特殊之處.它可以在Linux系統啓動時從啓動腳本/etc/rc*.d中啓動,可以由作業規劃進程crond啓動,還可以由用戶終端(通常是
shell)執行. 總之除了這些特殊性以外,守護進程與普通進程基本上沒有什麼區別.因此編寫守護進程實際上是把一個普通進程按照上述的守護進程的特性改造成爲守護進程.如果對進程有比較深入的認識就更容易理解和編程了.
二.守護進程的編程要點
前面講過,不同Unix環境下守護進程的編程規則並不一致.所幸的是守護進程的編程原則其實都一樣,區別在於具體的實現細節不同.這個原則就是要滿足守護進程的特性.同時Linux是基於Syetem V的SVR4並遵循Posix標準,實現起來與BSD4相比更方便.編程要點如下:
1. 創建守護進程(創建子進程,父進程退出)
這是編寫守護進程的第一步.由於守護進程是脫離控制終端的,因此完成第一步後就會在Shell終端裏造成一程序已經運行完畢的假象.之後的所有工作都在子進程中完成,而用戶在Shell終端裏則可以執行其他命令,從而在形式上做到了與控制終端的脫離.在Linux中父進程先於子進程退出會造成子進程成爲孤兒進程,而每當系統發現一個孤兒進程時,就會自動由1號進程(init)收養它.這樣原先的子進程就會變成init進程的子進程.
2.脫離控制終端,在子進程中創建新會話和進程組
setsid()說明:當進程是會話組長時setsid()調用失敗。但第一點已經保證進程不是會話組長。setsid()調用成功後,進程成爲新的會話組長和新的進程組長,並與原來的登錄會話和進程組脫離。由於會話過程對控制終端的獨佔性,進程同時與控制終端脫離。
這個步驟是創建守護進程中最重要的一步,雖然它的實現非常簡單,但它的意義卻非常重大.在這裏使用的是系統函數setsid,在具體介紹setsid之前,首先要了解兩個概念:進程組和會話期
進程組:是一個或多個進程的集合.進程組有進程組ID來唯一標識.除了進程號(PID)之外,進程組ID也是一個進程的必備屬性.每個進程組都有一個組長進程,其組長進程的進程號等於進程組ID.且該進程組ID不會因組長進程的退出而受到影響.
會話週期:會話期是一個或多個進程組的集合.通常,一個會話開始於用戶登錄,終止於用戶退出,在此期間該用戶運行的所有進程都屬於這個會話期
接下來就可以具體介紹setsid的相關內容:
setsid函數作用:
setsid函數用於創建一個新的會話,並擔任該會話組的組長。調用setsid有下面的3個作用:
讓進程擺脫原會話的控制
讓進程擺脫原進程組的控制
讓進程擺脫原控制終端的控制
那麼,在創建守護進程時爲什麼要調用setsid函數呢?由於創建守護進程的第一步調用了fork函數來創建子進程,再將父進程 退出.由於在調用了fork函數時,子進程全盤拷貝了父進程的會話期,進程組,控制終端等,雖然父進程退出了,但會話期、進程 組、控制終端等並沒有改變,因此這還不是真正意義上的獨立開來,而setsid函數能夠使進程完全獨立出來,從而擺脫其他進程的控制.
3.禁止進程重新打開控制終端
現在進程已經成爲無終端的會話組長.但它可以重新申請打開一個控制終端.可以通過使進程不再成爲會話組長來禁止進程重新打開控制終端
4.關閉打開的文件描述符
進程從創建它的父進程那裏繼承了打開的文件描述符.如不關閉,將會浪費系統資源,造成進程所在的文件系統無法卸下以及引起無法預料的錯誤。
5.改變當前工作目錄
這一步也是必要的步驟。使用fork創建的子進程繼承了父進程的當前工作目錄。由於在進程運行中,當前目錄所在的文件系統(如“/mnt/usb”)是不能卸載的,這對以後的使用會造成諸多的麻煩(比如系統由於某種原因要進入單用戶模式). 因此,通常的做法是讓"/"作爲守護進程的當前工作目錄,這樣就可以避免上述的問題,當然,如有特殊需要,也可以把當前工作目錄換成其他的路徑,如/tmp。改變工作目錄的常見函數式chdir。進程活動時,其工作目錄所在的文件系統不能卸下。一般需要將工作目錄改變到根目錄。對於需要轉儲核心,寫運行日誌 的進程將工作目錄改變到特定目錄如
/tmp. chdir("/tmp")
6.重設文件創建掩模
進程從創建它的父進程那裏繼承了文件創建掩模。它可能修改守護進程所創建的文件的存取位。爲防止這一點,將文件創建掩模清除:umask(0);
注:umask 命令允許你設定文件創建時的缺省模式,對應每一類用戶(文件屬主、同組用戶、其他用戶)存在一個相應的umask值中的數字.對於文件來說,這一數字的最大值分別是6.這裏設置umask(0)時,則該文件的屬性爲666(rw-rw-rw-),系統不允許你在創建一個文本文件時就賦予它執行權限,必須在創建後用chmod命令增加這一權限.目錄則允許設置執行權限,用chmod命令增加執行權限後,這樣針對目錄來說,umask中各個數字最大可以到7.
7.處理SIGCHLD信號
處理SIGCHLD信號並不是必須的.但對於某些進程,特別是服務器進程往往在請求到來時生成子進程處理請求.如果父進程 不等待子進程結束,子進程將成爲殭屍進程(zombie)從而佔用系統資源.如果父進程等待子進程結束,將增加父進程的負擔,影響服務器進程的併發性能。在Linux下 可以簡單地將 SIGCHLD信號的操作設爲SIG_IGN。
signal(SIGCHLD,SIG_IGN); 這樣內核在子進程結束時不會產生殭屍進程.這一點與BSD4不同,BSD4下必須顯式等待子進程結束才能釋放殭屍進程。
三.守護進程實例
下面爲本人測試的例程,在自己的虛擬機上(RedHat Enterprise Linux 5)已經測試成功.
守護進程實例包括兩部分:主程序test.c和初始化程序daemon.c,主程序每隔一分鐘向/tmp目錄中的日誌testd.log報告運行狀態.當收到用戶發出的SIGUSR1信號後產生退出日誌並終止程序.初始化程序中的InitDaemon函數負責生成守護進程.讀者可以利用
InitDaemon函數生成自己的守護程序.
//daemon.c
/*************************************************************
FileName : daemon.c
FileFunc : 負責生成守護進程
Version : V0.1
Author : Sunrier
Date : 2012-05-07
Descp : Linux下守護進程
*************************************************************/
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/param.h>
#include <sys/types.h>
#include <sys/stat.h>
void InitDaemon( void )
{
int i;
pid_t pid;
//第一步
if( pid=fork() )
{
exit(0);//是父進程,結束父進程
}
else
{
if( pid<0 )
exit(1);//fork失敗,退出
}
//第二步
setsid();//是第一個子進程,後臺繼續運行
//第一個子進程成爲新的會話組長和進程組長,並與控制終端分離
//防止子進程結束時產生殭屍進程
signal(SIGPIPE,SIG_IGN);
signal(SIGHUP,SIG_IGN);
signal(SIGTERM,SIG_IGN);
signal(SIGINT,SIG_IGN);
signal(SIGCHLD,SIG_IGN);
//第三步
if( pid=fork() )//禁止進程重新打開控制終端
{
exit(0);//是第一個子進程,結束第一個子進程
}
else
{
if( pid<0 )
exit(1);//fork失敗,退出
}
//第四步
for(i=0; i<NOFILE; i++)
{
close(i);//關閉從父進程那裏繼承的一些已經打開的文件
}
//第五步
chdir("/tmp");//改變工作目錄到/tmp
//第六步
umask(0);//重設文件權限掩碼
}
//test.c
/*************************************************************
FileName : test.c
FileFunc : 測試生成的守護進程
Version : V0.1
Author : Sunrier
Date : 2012-05-07
Descp : Linux下守護進程測試
*************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <signal.h>
void InitDaemon( void );
void Signal_Exit(int signal);
void Signal_Exit(int signal)//退出
{
FILE *fp;
time_t t;
if( (fp=fopen("testd.log","a"))>=0 )
{
t = time(0);
fprintf(fp,"Time is %s of Siganl_Exit !\n",asctime(localtime(&t)));
fclose(fp);
exit(0);
}
else
{
exit(1);
}
}
int main(int argc,char **argv)
{
FILE *fp;
time_t t;
InitDaemon();
signal(SIGUSR1,Signal_Exit);//處理SIGUSR1信號,定義此信號爲退出信號
while(1)
{
sleep(60);
if( (fp=fopen("testd.log","a"))!=NULL )//建議使用a+參數
{
t = time(0);
fprintf(fp,"Time is %s of main !\n",asctime(localtime(&t)));
fclose(fp);
}
else
{
exit(1);
}
}
return 0;
}
注:
a :以附加的方式打開只寫文件。若文件不存在,則會建立該文件,如果文件存在,寫入的數據會被加到文件尾,即文件原先的內容會被保留(EOF符保留) 。
a+ :以附加方式打開可讀寫的文件。若文件不存在,則會建立該文件,如果文件存在,寫入的數據會被加到文件尾後,即文件原先的內容會被保留(原來的EOF符不保留)。
編譯鏈接文件:
[root@localhost Sunrier]#gcc -o test test.c daemon.c
[root@localhost Sunrier]#
運行守護程序:
[root@localhost Sunrier]#./test
[root@localhost Sunrier]#
查看進程:ps -ef
[root@localhost Sunrier]#ps -ef
.
.
.
root 5585 1 0 11:02 ? 00:00:00 ./test
[root@localhost Sunrier]#
ps -A 顯示所有程序。
ps -e 此參數的效果和指定"A"參數相同。
ps f 用ASCII字符顯示樹狀結構,表達程序間的相互關係。
終止程序: kill -s SIGUSR1 PID
[root@localhost Sunrier]#kill -s SIGUSR1 5585
[root@localhost Sunrier]#
kill -9 PID
強行中止一個進程標識號爲PID的進程
如果想要此程序在系統啓動時自動運行,你可以在/etc/rc.d/rc.local裏面用su命令加上一行, 比如: su - root -c '/home/Sunrier/bin/test' 這個命令將以root用戶身份運行/home/Sunrier/bin/test程序
路徑爲你的守護進程程序所在的路徑,這裏爲絕對路徑
-, -l, --login 登錄並改變到所切換的用戶環境
-c, --commmand=COMMAND 執行一個命令,然後退出所切換到的用戶環境
上面su - root -c "/home/Sunrier/bin/test"也一樣,注意-和root 之間有一個空格
附上中斷信號
Signals:
Signal Description
SIGABRT 由調用abort函數產生,進程非正常退出
SIGALRM 用alarm函數設置的timer超時或setitimer函數設置的interval timer超時
SIGBUS 某種特定的硬件異常,通常由內存訪問引起
SIGCANCEL 由Solaris Thread Library內部使用,通常不會使用
SIGCHLD 進程Terminate或Stop的時候,SIGCHLD會發送給它的父進程。缺省情況下該Signal會被忽略
SIGCONT 當被stop的進程恢復運行的時候,自動發送
SIGEMT 和實現相關的硬件異常
SIGFPE 數學相關的異常,如被0除,浮點溢出,等等
SIGFREEZE Solaris專用,Hiberate或者Suspended時候發送
SIGHUP 發送給具有Terminal的Controlling Process,當terminal被disconnect時候發送
SIGILL 非法指令異常
SIGINFO BSD signal。由Status Key產生,通常是CTRL+T。發送給所有Foreground Group的進程
SIGINT 由Interrupt Key產生,通常是CTRL+C或者DELETE。發送給所有ForeGround Group的進程
SIGIO 異步IO事件
SIGIOT 實現相關的硬件異常,一般對應SIGABRT
SIGKILL 無法處理和忽略。中止某個進程
SIGLWP 由Solaris Thread Libray內部使用
SIGPIPE 在reader中止之後寫Pipe的時候發送
SIGPOLL 當某個事件發送給Pollable Device的時候發送
SIGPROF Setitimer指定的Profiling Interval Timer所產生
SIGPWR 和系統相關。和UPS相關。
SIGQUIT 輸入Quit Key的時候(CTRL+\)發送給所有Foreground Group的進程
SIGSEGV 非法內存訪問
SIGSTKFLT Linux專用,數學協處理器的棧異常
SIGSTOP 中止進程。無法處理和忽略。
SIGSYS 非法系統調用
SIGTERM 請求中止進程,kill命令缺省發送
SIGTHAW Solaris專用,從Suspend恢復時候發送
SIGTRAP 實現相關的硬件異常。一般是調試異常
SIGTSTP Suspend Key,一般是Ctrl+Z。發送給所有Foreground Group的進程
SIGTTIN 當Background Group的進程嘗試讀取Terminal的時候發送
SIGTTOU 當Background Group的進程嘗試寫Terminal的時候發送
SIGURG 當out-of-band data接收的時候可能發送
SIGUSR1 用戶自定義signal 1
SIGUSR2 用戶自定義signal 2
SIGVTALRM setitimer函數設置的Virtual Interval Timer超時的時候
SIGWAITING Solaris Thread Library內部實現專用
SIGWINCH 當Terminal的窗口大小改變的時候,發送給Foreground Group的所有進程
SIGXCPU 當CPU時間限制超時的時候
SIGXFSZ 進程超過文件大小限制
SIGXRES Solaris專用,進程超過資源限制的時候發送