linux上編寫守護進程的例程

轉載學習: 

linux上編寫守護進程的例程

  摘自《開放系統世界》2004年第5期郭吉平、任蓮的文章“親自動手編寫守護進程”。
/*郭吉平、任蓮 親自動手編寫守護進程*/
#include
#include
#include

void main(int argc, char ** argv){
time_t now;
int childpid, fd, fdtablesize;
int error, in, out;

/*忽略終端 I/O信號,STOP信號*/
signal(SIGTTOU,SIG_IGN);
signal(SIGTTIN,SIG_IGN);
signal(SIGTSTP,SIG_IGN);
signal(SIGHUP,SIG_IGN);

/*父進程退出,程序進入後臺運行*/
if( fork()!=0 ) exit(1);
if( setsid()<0 ) exit(1); /*創建一個新的會議組*/

/*子進程退出,孫進程沒有控制終端了*/
if( fork()!=0 ) exit(1);
if( chdir("/tmp")==-1 )exit(1);

/*關閉打開的文件描述符,包括標準輸入、標準輸出和標準錯誤輸出*/
for( fd=0, fdtablesize=getdtablesize(); fd< fdtablesize;fd++) close(fd);

umask(0);/*重設文件創建掩模*/
signal(SIGCHLD,SIG_IGN);/*忽略SIGCHLD信號*/
/*打開log系統*/
syslog(LOG_USER|LOG_INFO,"守護進程測試!/n");
while(1){
time(&now);
syslog(LOG_USER|LOG_INFO,"當前時間:/t%s/t/t/n",ctime(&now));
sleep(6);
}




守護進程在Linux/Unix系統中有着廣泛的應用。有時,開發人員也想把自己的程序變成守護進程。在創建一個守護進程的時候,要接觸到子進程、進程組、會晤期、信號機制、文件、目錄和控制終端等多個概念。因此守護進程還是比較複雜的,在這裏詳細地討論Linux/Unix的守護進程的編寫,總結出八條經驗,並給出應用範例。

    編程要點

    1.屏蔽一些有關控制終端操作的信號。防止在守護進程沒有正常運轉起來時,控制終端受到干擾退出或掛起。示例如下:
signal(SIGTTOU,SIG_IGN); signal(SIGTTIN,SIG_IGN); signal(SIGTSTP,SIG_IGN); signal(SIGHUP ,SIG_IGN);


    所有的信號都有自己的名字。這些名字都以“SIG”開頭,只是後面有所不同。開發人員可以通過這些名字瞭解到系統中發生了什麼事。當信號出現時,開發人員可以要求系統進行以下三種操作:
    ◆ 忽略信號。大多數信號都是採取這種方式進行處理的,這裏就採用了這種用法。但值得注意的是對SIGKILL和SIGSTOP信號不能做忽略處理。
    ◆ 捕捉信號。最常見的情況就是,如果捕捉到SIGCHID信號,則表示子進程已經終止。然後可在此信號的捕捉函數中調用waitpid()函數取得該子進程的進程ID和它的終止狀態。另外,如果進程創建了臨時文件,那麼就要爲進程終止信號SIGTERM編寫一個信號捕捉函數來清除這些臨時文件。
    ◆ 執行系統的默認動作。對絕大多數信號而言,系統的默認動作都是終止該進程。

    對這些有關終端的信號,一般採用忽略處理,從而保障了終端免受干擾。

    這類信號分別是,SIGTTOU(表示後臺進程寫控制終端)、SIGTTIN(表示後臺進程讀控制終端)、SIGTSTP(表示終端掛起)和SIGHUP(進程組長退出時向所有會議成員發出的)。

    2.將程序進入後臺執行。由於守護進程最終脫離控制終端,到後臺去運行。方法是在進程中調用fork使父進程終止,讓Daemon在子進程中後臺執行。這就是常說的“脫殼”。子進程繼續函數fork()的定義如下:
#include <sys/types.h> #include <unistd.h> pid_t fork(void);

 


    該函數是Linux/Unix編程中非常重要的函數。它被調用一次,但返回兩次。這兩次返回的區別是子進程的返回值爲“0”,而父進程的返回值爲子進程的ID。如果出錯則返回“-1”。

    3.脫離控制終端、登錄會話和進程組。開發人員如果要擺脫它們,不受它們的影響,一般使用 setsid() 設置新會話的領頭進程,並與原來的登錄會話和進程組脫離。這只是其中的一種方法,也有如下處理的辦法:
if ((fd = open("/dev/tty",O_RDWR)) >= 0) { ioctl(fd,TIOCNOTTY,NULL); close(fd); }
 


    其中/dev/tty是一個流設備,也是終端映射,調用close()函數將終端關閉。

    4.禁止進程重新打開控制終端。進程已經成爲無終端的會話組長,但它可以重新申請打開一個控制終端。開發人員可以通過不再讓進程成爲會話組長的方式來禁止進程重新打開控制終端,需要再次調用fork函數。
    上面的程序代碼表示結束第一子進程,第二子進程繼續(第二子進程不再是會話組長)。

    5. 關閉打開的文件描述符,並重定向標準輸入、標準輸出和標準錯誤輸出的文件描述符。進程從創建它的父進程那裏繼承了打開的文件描述符。如果不關閉,將會浪費系統資源,引起無法預料的錯誤。關閉三者的代碼如下:
 

for (fd = 0, fdtablesize = getdtablesize(); fd < fdtablesize; fd++) close(fd);
 


    但標準輸入、標準輸出和標準錯誤輸出的重定向是可選的。也許有的程序想保留標準輸入(0)、標準輸出(1)和標準錯誤輸出(2),那麼循環應繞過這三者。代碼如下:
for (fd =3, fdtablesize = getdtablesize(); fd < fdtablesize; fd++) close(fd);
 


    有的程序有些特殊的需求,還需要將這三者重新定向。示例如下:
error=open("/tmp/error",O_WRONLY|O_CREAT, 0600); dup2(error,2); close(error); in=open("/tmp/in",O_RDONLY|O_CREAT,0600); if(dup2(in,0)==-1) perror("in"); close(in); out=open("/tmp/out",O_WRONLY|O_CREAT,0600); if(dup2(out,1)==-1) perror("out"); close(out);

 


    6.改變工作目錄到根目錄或特定目錄進程活動時,其工作目錄所在的文件系統不能卸下。

    一般需要將工作目錄改變到根目錄或特定目錄,注意用戶對此目錄需要有讀寫權。防止超級用戶卸載設備時系統報告設備忙。

    7.處理SIGCHLD信號。SIGCHLD信號是子進程結束時,向內核發送的信號。

如果父進程不等待子進程結束,子進程將成爲殭屍進程(zombie)從而佔用系統資源。因此需要對SIGCHLD信號做出處理,回收殭屍進程的資源,避免造成不必要的資源浪費。可以用如下語句:
    signal(SIGCHLD,(void *)reap_status);

    捕捉信號SIGCHLD,用下面的函數進行處理:
void reap_status() { int pid; union wait status; while ((pid = wait3(&status,WNOHANG,NULL)) > 0) …… }

 


    8.在Linux/Unix下有個syslogd的守護進程,向用戶提供了syslog()系統調用。任何程序都可以通過syslog記錄事件。

    由於syslog非常好用和易配置,所以很多程序都使用syslog來發送它們的記錄信息。一般守護進程也使用syslog向系統輸出信息。syslog有三個函數,一般只需要用syslog(...)函數,openlog()/closelog()可有可無。syslog()在shslog.h定義如下: #include <syslog.h> void syslog(int priority,char *format,...);

 


    其中參數priority指明瞭進程要寫入信息的等級和用途。第二個參數是一個格式串,指定了記錄輸出的格式。在這個串的最後需要指定一個%m,對應errno錯誤碼。

    應用範例

 


    此程序在Turbo Linux 4.0下編譯通過。這個程序比較簡單,但基本體現了守護進程的編程要點。讀者針對實際應用中不同的需要,還可以做相應的調整。Linux聯盟收集整理



如何實現在Linux下創建服務程序


 
       
Linux系統能提供強大可靠的網絡服務,並有管理程序對服務進行管理。例如我們熟悉的Web、FTP和電子郵件等,它們既可以單獨運行,也可以被守護進程inetd調用,而且運行得都非常好。但我們不能僅停留在讚歎中,下面就給出兩個服務程序程序和一個客戶程序的例子,介紹服務程序和客戶程序之間是
溝通的。另外還要編輯配置一些文件,讓服務程序也能接受服務管理程序管理。

這兩個服務程序功能相同,但一個是獨立服務程序,另一個是被inetd調用的服務程序。這是TCP/IP網絡服務的兩大類,這裏將兩個程序放在一起是爲了比較程序結構和運行方式。兩服務程序都在Red Hat Linux 7.1和TurboLinux 7.0上調試通過。

獨立服務器

TCP和UDP是兩大TCP/IP數據傳輸方式,套接口是建立服務器客戶機連接的機制,首先介紹它們建立通信聯繫的過程,然後給出一個TCP服務程序例子。

1.TCP套接口通信方式

對於TCP服務器端,服務程序首先調用建立套接口的函數socket(),然後調用綁定服務IP地址和協議端口號函數bind()。綁定成功後調用被動監聽函數listen()等待客戶連接,還要調用獲取連接請求函數accept(),並一直阻塞到客戶連接請求的到達,這個函數獲取客戶機IP地址和協議端口號。

對於TCP客戶端,客戶程序啓動後後調用建立套接口函數socket(),然後調用連接函數connect(),此函數與服務器通過三次握手建立連接。

服務器和客戶機建立連接後,就可以使用讀函數read()和寫函數write()收發數據了。數據交換完成後便各自調用關閉套接口函數close()刪除套接口。

 


2.UDP套接口通信方式

UDP程序與TCP的區別是無需建立連接。服務器首先啓動,然後等待用戶請求。客戶機啓動後便直接向服務器請求服務,服務器接到請求後給出應答。

對於UDP服務器端,服務程序首先調用套接口函數socket(),然後調用綁定IP地址和協議端口號函數bind()。之後調用函數recvfrom()接收客戶數據,調用sendto()向客戶發送數據。

對於UDP客戶端,客戶機程序啓動後調用套接口函數socket(),然後調用sendto()向服務器發送數據,調用recvfrom()接收服務器數據。

雙方數據交換成功後,各自調用關閉套接口函數close()關閉套接口。UDP套接口通信方式見圖2所示。

 


下面給出獨立服務程序的例子。這個程序雖然簡單,但是與複雜程序有着相同的結構。

//程序名:server.c
//功能:服務器從客戶機讀入一個字符,並將排在此字符後面的字符回送客戶機
//服務器端口:9000
#include "sys/types.h"
#include "sys/socket.h"
#include "stdio.h"
#include "netinet/in.h"
#include "arpa/inet.h"
#include "unistd.h"
int main()
{
int pid; //用於存放fork()執行結果
int server_sockfd,client_sockfd; //用於服務器和客戶機套接口描述符
int bind_flag,listen_flag; //用於存放bind()和listen()執行結果
int server_address_length,client_address_length; //作爲服務器客戶機地址長變量
struct sockaddr_in server_address; //作爲服務器地址結構變量(含地址和端口)
struct sockaddr_in client_address; //作爲客戶機地址結構變量(含地址和端口)
if((pid=fork())!=0) //用fork()產生新進程
exit(0) ;
setsid() ; //以子進程開始下面的程序

函數socket(),創建一個套接口,成功則返回套接口描述符。

server_sockfd=socket(AF_INET,SOCK_STREAM,0);
if(server_sockfd<0)
{
printf(“socket error /n”);
exit(1);
}
server_address.sin_family=AF_INET;

函數htonl()用於將32位主機字節順序轉換爲網絡字節順序,其中參數INADDR_ANY表示任何IP地址。

server_address.sin_addr.s_addr=htonl(INADDR_ANY);

函數htons()用於將16位主機字節順序轉換爲網絡字節順序,其中的參數是綁定的端口號,讀者可根據環境自行改動,目的是不與其它服務端口衝突。

server_address.sin_port=htons(9000);
server_address_length=sizeof(server_address);

函數bind()用於綁定本地地址和服務端口號,若調用成功返回值爲0。

bind_flag=bind(server_sockfd,/
(struct sockaddr *)&server_address,/
server_address_length);
if(bind_flag<0)
{
printf(“bind error /n”);
exit(1);
}

函數listen(),指明服務器的隊列長度,被動等待客戶連接,調用成功返回值爲0。

listen_flag=listen(server_sockfd,5);
if(listen_flag<0)
{
printf(“listen error /n”);
exit(1);
}

while(1)
{
char ch;

函數accept()等待和獲取用戶請求,爲每個新連接請求創建一個新的套接口,調用成功返回新套接口描述符。

client_sockfd=accept(server_sockfd,/
(struct sockaddr *)&client_address,/
&client_address_length);

函數read()和write()用於在服務器和客戶機之間傳送數據,調用成功返回讀和寫的字節數。

函數close(),用於程序使用完一個套接口後關閉套接口,調用成功返回值0。其中的參數爲accept()創建的套接口的描述符client_sockfd。

read(client_sockfd,&ch,1);
printf(“cli_ch=%c”,ch);
ch++;
write(client_sockfd,&ch,1);

close(client_sockfd);
}
}

程序完成後就可以使用命令進行編譯。在命令行中輸入“gcc -o server server.c”,將server.c編譯成可執行程序server,這時便可用客戶程序進行測試。在命令行執行“./server”啓動服務程序,執行“netstat -na”查看有無server的服務端口。如果存在,則執行下面編寫的客戶程序“./client”。不過這僅是手工啓動的方法,下面給出用服務管理程序管理server程序的方法。只要在目錄/etc/rc.d/init.d下放入服務程序的腳本就能被服務程序讀到。在命令行執行“touch server”創建文件server,並將文件屬性改成可執行。在管理程序中並不能看到此服務名,腳本文件必須有一些結構才能被管理程序認爲是服務程序腳本。

爲了減少工作量,拷貝/etc/rc.d/init.d下腳本httpd,將拷貝腳本名命名爲server,然後對其編輯。

(1)執行“cp httpd server”。

(2)用文本編輯器vi(其它編輯器亦可)將server打開進入編輯狀態。首先用字符串server替換httpd。然後找到daemon server行,如果編寫的程序放在變量PATH目錄中,不需要修改此行;如果把服務程序放在其它目錄中,就要寫服務的全路徑。例如程序在/root的目錄中,就要寫成daemon /root/server,還要刪除“rm -f /var/run/server.pid”這一行。

(3)執行“chmod 755 server”,將server屬性設定爲可執行。

此時就可以用chkconfig、ntsysv等工具,在希望的運行級中增加這個新服務程序,然後測試客戶機與服務器能否通信。

被xinetd調用的服務程序

在Linux系統中,有很多服務是被xinetd(較早版本使用的是inetd)超級守護服務器啓動的。其實凡是基於TCP和UDP的服務都可使用超級守護進程啓動,只是在服務量很大影響效率的情況下不被採用。

1.依賴xinetd啓動的服務建立通信過程

爲了與獨立服務器程序比較,我們看一下依賴xinetd的服務器是如何啓動的。

(1)xinetd啓動時讀取/etc/xinetd目錄中的文件(早期版本爲/etc/inetd文件),根據其中的內容給所有允許啓動的服務創建一個指定類型的套接口,並將套接口放入select()中的描述符集合中。

(2)對每個套接口綁定bind(),所用的端口號和其它參數來自/etc/xinetd目錄下每個服務的配置文件。

(3)如果是TCP套接口就調用函數listen(),等待用戶連接。如果是UDP套接口,就不需調用此函數。

(4)所有套接口建立後,調用函數select()檢查哪些套接口是活動的。

(5)若select()返回TCP套接口,就調用accept()接收這個連接。如果爲UDP,就不需調用此函數。

(6)xinetd調用fork()創建子進程,由子進程處理連接請求。

◆ 子進程關閉所有其它描述符,只剩下套接口描述符。這個套接口描述符對於TCP是accept()返回的套接口,對於UDP爲最初建立的套接口。然後子進程連續三次dup()函數,將套接口描述符複製到0、1和2,它們分別對應標準輸入、標準輸出和標準錯誤輸出,並關閉套接口描述符。

◆ 子進程查看/etc/xinetd下文件中的用戶,如果不是root用戶,就用調用命令setuid和setgid將用戶ID和組ID改成文件中指定的用戶。

(7)對於TCP套接口,與用戶交流結束後父進程需要關閉已連接套接口。父進程重新處於select()狀態,等待下一個可讀的套接口。

最後調用配置文件中指定的外部服務程序,外部程序啓動後就可與用戶進行信息傳遞了。

2.爲xinetd編寫專門的服務程序

除了獨立服務程序能被xinetd啓動外,還可以爲xinetd編寫專門的程序。此處的例子程序與上面server.c功能相同。不過兩者的程序區別是很大的,此例的代碼僅相當於上面傳輸數據的部分。我們還將程序名定爲server.c,所以不能放在相同目錄中,同名僅是爲了和上面程序對照。

#include "unistd.h"
int main()
{
char ch;
read(0,&ch,1);
ch++;
write(1,&ch,1);
}

將程序編譯成可執行文件,並做些設置就可被xinetd啓動。注意不要和上面的獨立服務程序server一起啓動,因爲客戶程序寫得比較簡單,訪問的是固定端口,服務器都設成了相同的端口號。

(1)編輯/etc/services文件,在行末增加一條記錄:

server 9000/tcp



(2)在目錄/etc/xinetd.d下編寫文件server,內容爲:

service server
{
disable = no
socket_type = stream
protocol = tcp
wait = no
user = root
server = /home/test/server (此處設置成自己程序所在的目錄)
}



如果使用的是較早版本,則需在/etc/inetd.conf文件中添加下面的行:

server tcp nowait root /path/to/yourdirectory/server



(3)執行/etc/rc.d/initd.d/xinetd restart重新啓動xinetd服務器。早期版本執行/etc/rc.d/initd.d/inetd restart重新啓動inetd。

(4)執行netstat -an查看有沒有server程序使用的端口號,如果有就可使用下面客戶機程序進行測試了。

客戶機程序

下面就客戶機函數做一簡單介紹。

//程序名client.c
/*功能:從客戶的控制檯輸入一個字符,然後將這個字符送到服務器,並將服務器返回的字符顯示出來*/
#include "sys/types.h"
#include "sys/socket.h"
#include "stdio.h"
#include "netinet/in.h"
#include "arpa/inet.h"
#include "unistd.h"
int main()
{
int sockfd;//
int address_len;
int connect_flag;
struct sockaddr_in address;
int connect_result;
char client_ch,server_ch;

函數socket()用於建立一個套接口,創建成功返回套接口描述符。

sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd<0)
{
printf(“sockfd error /n”);
}
address.sin_family=AF_INET;
address.sin_addr.s_addr=inet_addr(“192.168.0.1”);/*讀者根據自己環境改成服務器地址*/
address.sin_port=htons(9000);

address_len=sizeof(address);

函數connect()用於與服務器建立一個主動連接,調用成功返回值爲0。

connect_flag=connect(sockfd,(struct sockaddr *)&address,address_len);

if(connect_flag==-1)
{
perror(“client”);
exit(1);
}
printf(“Input a character :”);

函數scanf()用於從控制檯輸入一個字符,並將字符存入client_ch的地址。函數write()和read()用於傳輸數據。函數printf()在客戶機屏幕上顯示服務器傳回的字符。函數close()關閉套接口。

scanf(“%c”,&client_ch);
write(sockfd,&client_ch,1);
read(sockfd,&server_ch,1);
printf(“character from server : %c/n”,server_ch);
close(sockfd);
exit(0);
}

執行命令“gcc -o client client.c”,將client.c編譯成client。執行“./client”,在程序提示下輸入一個字符,就能看到服務器傳回的字符。

以上介紹的僅是簡單的例子。平時見到的服務程序遠比它複雜,而且很多是多協議服務程序或是多協議多服務程序。多協議服務程序就是在main()中分別創建供服務的TCP和UDP套接口。爲每個服務分別寫出相應程序好處是便於控制,但是這樣每個服務都啓動兩個服務器,而它們的算法響應是一樣的,就要耗費不必要的資源,並且出了問題排錯也較困難。多服務是將不同的服務集成在一起由一個程序完成,可用一個數組表示服務,數組中的每一項表示某協議某服務的一種,這樣很容易擴展程序的服務功能。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章