## 1. 概述
?守護進程是在后臺運行且不與任何控制終端關聯的進程。unix系統通常有很多守護進程在后臺運行,執行不同的管理任務。
?守護進程沒有控制終端通常源于它們由系統初始化腳本啟動。然而守護進程也可能從某個終端由用戶在shell提示符下鍵入命令行啟動,這樣的守護進程必須親自脫離與控制終端的關聯,從而避免與作業控制,終端會話管理,終端產生信號等發生任何不期望的交互,也可以避免在后臺運行的守護進程非預期的輸出到終端。
?守護進程有多種啟動方法:
?1.在系統啟動階段,許多守護進程由系統初始化腳本啟動。這些腳本通常位于/etc目錄或以/etc/rc開頭的某個目錄中,它們的具體位置和內容卻是實現相關的。由這些腳本啟動的守護進程一開始擁有超級用戶權限。
?有若干個網絡服務器通常從這些腳本啟動:inetd超級服務器,web服務器,郵件服務器(經常是sendmail)。
?2. 許多網絡服務器由inetd超級服務器啟動。inetd自身由上一條中的某個腳本啟動。inetd監聽網絡請求,每當有一個請求到達時,啟動相應的實際服務器(telnet服務器,FTP服務器等)
?3. cron守護進程按照規則定期執行一些程序,而由它啟動執行的程序同樣作為守護進程運行。cron自身由第一條啟動方法中的某個腳本啟動
?4. at命令用于指定將來某個時刻的程序執行。這些程序的執行時刻到來時,通常由cron守護進程啟動執行它們,因此這些程序同樣作為守護進程運行。
?5.守護進程還可以從用戶終端或在前臺或在后臺啟動。這么做往往是為了測試守護進程或重啟因某種原因而終止了的某個守護進程。
?因為守護進程沒有控制終端,所以當有事發生時它們得有輸出消息的某種方法可用,而這些消息既可能是普通的通告性消息,也可能是需由系統管理員處理的緊急事件消息。syslog函數是輸出這些消息的標準方法,它把這些消息發送給syslogd守護進程。
## 2. syslog函數,openlog函數和closelog函數
備注:遇到類似的函數,具體說明請查看APUE
~~~
#include <syslog.h>
void syslog(int priority, const char *message,...);
void openlog(const char *ident, int options, int facility);
void closelog(void);
~~~
### 1) 作為守護進程運行的協議無關時間獲取服務器程序
服務器程序daytimetcpsrv.c:
~~~
#include <stdio.h>
#include <netdb.h>
#include <sys/socket.h>
#include <time.h>
#include <syslog.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
extern int errno;
int daemon_proc;
#define MAXLINE 1024
#define MAXFD 64
int daemon_init(const char *pname, int facility);
int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp);
int main(int argc, char **argv)
{
int listenfd, connfd;
socklen_t len;
char buff[MAXLINE];
time_t ticks;
struct sockaddr_in cliaddr;
daemon_init(argv[0], 0);
listenfd = tcp_listen(argv[1], argv[2], NULL);
for (; ;){
len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &len);
inet_ntop(AF_INET, &cliaddr.sin_addr, buff, sizeof(buff));
strcat(buff, ".this is a test\n");
syslog(LOG_INFO, buff);
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
write(connfd, buff, strlen(buff));
close(connfd);
}
}
int daemon_init(const char *pname, int facility)
{
int i;
pid_t pid;
if ((pid = fork()) < 0)
return -1;
else if (pid)
_exit(0);
if (setsid() < 0)
return -1;
signal(SIGHUP, SIG_IGN);
if ((pid = fork()) < 0)
return -1;
else if (pid)
_exit(0);
daemon_proc = 1;
chdir("/");
for (i = 0; i < MAXFD; i++)
close(i);
open("/dev/null", O_RDONLY);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
openlog(pname, LOG_PID, facility);
}
int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp)
{
int listenfd, n;
const int on = 1;
struct addrinfo hints, *res, *ressave;
bzero(&hints, sizeof(struct addrinfo));
hints.ai_flags = AI_PASSIVE;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if ((n = getaddrinfo(host, serv, &hints, &res)) != 0){
printf("tcp_listen error for %s,%s:%s\n", host, serv, gai_strerror(n));
exit(1);
}
ressave = res;
do{
listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (listenfd < 0)
continue;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0)
break;
close(listenfd);
} while ((res = res->ai_next) != NULL);
if (res == NULL)
printf("tcp_listen error for %s,%s\n", host, serv);
listen(listenfd, 5);
if (addrlenp)
*addrlenp = res->ai_addrlen;
freeaddrinfo(ressave);
return listenfd;
}
~~~
客戶端程序daytimetcpcli.c:
~~~
#include <stdio.h>
#include <netdb.h>
#include <sys/socket.h>
#define MAXLINE 1024
int tcp_connect(const char *host, const char *serv);
int main(int argc, char **argv)
{
int sockfd, n;
char recvline[MAXLINE + 1];
socklen_t len;
struct sockaddr_in cliaddr;
if (argc != 3){
printf("argument should be 3\n");
exit(1);
}
sockfd = tcp_connect(argv[1], argv[2]);
len = sizeof(cliaddr);
getpeername(sockfd, (struct sockaddr *)&cliaddr, len);
inet_ntop(AF_INET, &cliaddr.sin_addr, recvline, sizeof(recvline));
printf("connect to %s\n", recvline);
while ((n = read(sockfd, recvline, MAXLINE)) > 0){
recvline[n] = 0;
fputs(recvline, stdout);
}
exit(0);
}
int tcp_connect(const char *host, const char *serv)
{
int sockfd, n;
struct addrinfo hints, *res, *ressave;
struct sockaddr_in *cliaddr;
bzero(&hints, sizeof(struct addrinfo));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
if ((n = getaddrinfo(host, serv, &hints, &res)) != 0){
printf("tcp_connect error for %s,%s:%s\n", host, serv, gai_strerror(n));
exit(1);
}
ressave = res;
do{
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
if (sockfd < 0)
continue;
if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0)
break;
cliaddr = (struct sockaddr_in *)res->ai_addr;
close(sockfd);
} while ((res = res->ai_next) != NULL);
if (res == NULL)
printf("tcp_connect error for %s,%s\n", host, serv);
freeaddrinfo(ressave);
return sockfd;
}
~~~
程序運行如下:
服務端:
~~~
leichaojian@ThinkPad-T430i:~$ ./daytimetcpsrv ThinkPad-T430i 9878
~~~
客戶端:
~~~
leichaojian@ThinkPad-T430i:~$ ./daytimetcpcli ThinkPad-T430i 9878
connect to 0.0.0.0
Wed Oct 8 21:07:35 2014
~~~
然后我們查看/var/log/syslog這個文件,通過查找字符串“this is a test”,發現如下的語句:
~~~
Oct 8 21:07:35 ThinkPad-T430i ./daytimetcpsrv[10528]: 127.0.0.1.this is a test
~~~
### 2) 對daemon_init函數的分析
### (1)fork
用于產生子進程
### (2)setsid
setsid用于創建一個新的回話。當前進程變為新會話的會話頭進程以及新進程組的進程組頭進程,從而不再有控制終端。
### (3)忽略SIGHUP信號并再次fork
忽略SIGHUP信號并再次調用fork。該函數返回時,父進程實際上是上一次調用fork產生的子進程,它被終止掉,留下新的子進程繼續運行。再次fork的目的是確保本守護進程將來即使打開了一個終端設備,也不會自動獲得控制終端。當沒有控制終端的一個會話頭進程打開一個終端設備時(該終端不會是當前某個其他會話的控制終端),該終端自動成為這個會話頭進程的控制終端。然而再次調用fork之后,我們確保新的子進程不再是一個會話頭進程,從而不能自動獲得一個控制終端。這里必須霍略SIGHUP信號,因為當會話頭進程(即首次fork產生的子進程)終止時,其會話中的所有進程(即再次fork產生的子進程)都收到SIGHUP信號。
### (4)將stdin,stdout和stderr重定向到/dev/null
因為之前關閉了所有的描述符,所以要打開這三個基本描述符并且重定向,讓read返回0,write系統調用丟棄所寫的數據(書上說如果調用了syslog函數,則不要調用類似printf之類的函數,因為會被簡單的忽略掉)。因為如果繼續關閉,則萬一有新的進程打開一個描述符,卻占用了0,1,2這三個描述符,則可能導致將錯誤的數據發送給客戶端。
## 3. inetd守護進程
?? 舊的服務器只是等待客戶請求的到達,如FTP,Telnet,TFTP等。這些進程都是在系統自舉階段從/etc/rc文件中啟動,而且每個進程執行幾乎相同的啟動任務:創建一個套接字,把本服務器的眾所周知端口捆綁到該套接字,等待一個連接或一個數據報,然后派生子進程。子進程為客戶提供服務,父進程則繼續等待下一個客戶請求。這個模型存在兩個問題:
(1)所有這些守護進程含有幾乎相同的啟動代碼,既表現在創建套接字上,也表現在演變成守護進程上(類似我們的daemon_init函數)
(2)每個守護進程在進程表中占據一個表項,然而它們大部分時間處于睡眠狀態。
?? 而新版本的系統通過提供inetd守護進程(因特網超級服務器)來簡化問題:
(1)通過inetd處理普通守護進程的大部分啟動細節來簡化守護進程的編寫。這么一來每個服務器不再有調用daemon_init函數的必要。
(2)單個進程就能為多個服務等待外來的客戶請求,以此取代每個服務一個進程的做法。這么做減少了系統中的進程總數。
### 1) inetd守護進程的工作流程

### (0)對xinetd.conf文件的說明
| 字段 | 說明 |
|-----|-----|
| service_name | 必須在/etc/services文件中定義 |
| socket_type | stream(對于tcp)或dgram(對于udp) |
| protocol | 必須在/etc/protocols文件中定義:tcp或udp |
| wait-falg | 對于TCP一半為nowait,對于UDP一般為wait |
| login-name | 來自/etc/passwd的用戶名,一般為root |
| server-program | 調用exec指定的完整路徑名 |
| server-program-arguments | 調用exec指定的命令行參數 |
下面是xinetd.conf文件中的若干行:
| ftp | stream | tcp | nowait | root | /usr/bin/ftpd | ftpd -l |
|-----|-----|-----|-----|-----|-----|-----|
| telnet | stream | tcp | nowait | root | /usr/bin/telnetd | telnetd |
### (1)socket()
?? 在啟動階段,讀入/etc/xinetd.conf文件并給該文件中指定的每個服務創建一個適當類型(字節流或數據報)的套接字。inetd能夠處理的服務器的最大數目取決于inetd能夠創建的描述符的最大數目。新創建的每個套接字都被加入到將由某個select調用使用的一個描述符集中。
### (2)bind()
?? 為每個套接字調用bind,指定捆綁相應服務器的眾所周知端口和通配地址。這個TCP或UDP端口號通過調用getservbyname獲得,作為函數參數的是相應服務器在配置文件中的service-name字段和protocol字段。
### (3)listen()
?? 對于每個TCP套接字,調用listen以接收外來的連接請求。對于數據報套接字則不執行本步驟
### (4)select()等待可讀條件
?? 創建完畢所有套接字之后,調用select等待其中任何一個套接字變為可讀。TCP監聽套接字將在有一個新連接準備好可被接受時變為可讀,UDP套接字將在有一個數據報到達時變為可讀。inetd的不部分時間花在阻塞于select調用內部,等待某個套接字變為可讀。
### (5)accept()
?? 當select返回指出某個套接字已可讀之后,如果該套接字是一個TCP套接字,而且其服務器的wait-flag值為nowait,那就調用accept接受這個新連接。
### (6)fork()
?? inetd守護進程調用fork派生進程,并由子進程處理服務請求。子進程關閉要處理的套接字描述符之外的所有描述符:對于TCP服務器來說,這個套接字是由accept返回的新的已連接套接字,對于UDP服務器來說,這個套接字是父進程最初創建的UDP套接字。子進程dup2三次,把這個待處理套接字的描述符復制到描述符0,1和2,然后關閉原套接字描述符(由accept返回的已連接的TCP套接字)。
?? 子進程然后調用exec執行由相應的server-program字段指定的程序來具體處理請求,相應的server-program-arguments字段值則作為命令行參數傳遞給該程序。
?? 如果第五步中的select返回的是一個字節流套接字,那么父進程必須關閉已連接套接字(就像標準并發服務器那樣)。父進程再次調用select,等待下一個變為可讀的套接字。(因為TCP設置的nowait,意味著inetd不必等待某個子進程終止就可以接收對于該子進程所提供之服務的另一個連接。如果對于某個子進程所提供之服務的另一個連接確實在該子進程終止之前到達:accept返回,那么父進程再次調用select:意味著要關閉已連接的套接字,繼續執行步驟4,5,6)
?? 給一個數據報服務指定wait標志導致父進程執行的步驟發生變化。這個標志要求inet必須在這個套接字再次稱為slect調用的候選套接字之前等待當前服務該套接字的子進程終止。發生的變化有以下幾點:
[1]fork返回到父進程時,父進程保存子進程的進程ID。這么做使得父進程能夠通過查看由waitpid返回的值確定這個子進程的終止時間
[2]父進程通過使用FD_CLR宏關閉這個套接字在select所用描述符集中對應的位,達成在將來的select調用中禁止這個套接字的目的。這點意味著子進程將接管該套接字,直到自身終止為止。
[3]當子進程終止時,父進程被通知一個SIGCHLD信號,而父進程的信號處理函數將取得這個子進程的進程ID。父進程通過打開相應的套接字在select所用描述符集中對應的位,使得該套接字重新成為select的候選套接字。
2)inetd守護進程的服務器程序
~~~
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <signal.h>
#include <fcntl.h>
#include <unistd.h>
#include <time.h>
#include <netinet/in.h>
#define MAXLINE 1024
int main(int argc, char **argv)
{
socklen_t len;
struct sockaddr_in cliaddr;
char buff[MAXLINE];
time_t ticks;
openlog(argv[0], 0);
len = sizeof(cliaddr);
getpeername(0, (struct sockaddr *)&cliaddr, &len);
inet_ntop(AF_INET, (struct sockaddr *)&cliaddr.sin_addr, buff, sizeof(buff));
printf("connect from %s\n", buff);
ticks = time(NULL);
snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
write(0, buff, strlen(buff));
close(0);
exit(0);
}
~~~
在/etc/service中增加:
~~~
mydaytime 9999/tcp
~~~
在/etc/xinetd.conf中增加:
~~~
mydaytime stream tcp nowait leichaojian /home/leichaojian/newdaytimetcpserv3 newdaytimetcpserv3
~~~
程序輸出:
~~~
leichaojian@ThinkPad-T430i:~$ ./newdaytimetcpserv3
connect from 0.0.0.0
Fri Oct 3 14:27:31 2014
~~~