【Linux程式設計】守護程序(daemon)詳解與建立
一、概述
Daemon(守護程序)是執行在後臺的一種特殊程序。它獨立於控制終端並且週期性地執行某種任務或等待處理某些發生的事件。它不需要使用者輸入就能執行而且提供某種服務,不是對整個系統就是對某個使用者程式提供服務。Linux系統的大多數伺服器就是通過守護程序實現的。常見的守護程序包括系統日誌程序syslogd、 web伺服器httpd、郵件伺服器sendmail和資料庫伺服器mysqld等。
守護程序一般在系統啟動時開始執行,除非強行終止,否則直到系統關機都保持執行。守護程序經常以超級使用者(root)許可權執行,因為它們要使用特殊的埠(1-1024)或訪問某些特殊的資源。
守護程序的父程序是init程序,因為它真正的父程序在fork出子程序後就先於子程序exit退出了,所以它是一個由init繼承的孤兒程序。守護程序是非互動式程式,沒有控制終端,所以任何輸出,無論是向標準輸出裝置stdout還是標準出錯裝置stderr的輸出都需要特殊處理。
守護程序的名稱通常以d結尾,比如sshd、xinetd、crond等。
二、守護程序的建立
首先我們需要理解一些基本概念:
- 程序組(process group): 一個或多個程序的集合,每個程序都有一個程序組ID,這個ID就是程序組長的程序ID
- 會話期(session): 一個或多個程序組的集合,每個會話有唯一一個會話首程序(session leader),會話ID為會話首程序ID
- 控制終端(controlling terminal) :每一個會話可以有一個單獨的控制終端,與控制終端連線的會話首程序就是控制程序(controlling process)。 這時候,與當前終端互動的就是前臺程序組,其他的都是後臺程序組。
建立守護程序的過程中會用到一個關鍵函式:setsid(),這個函式用於建立一個新的會話期。
給出 setsid() 的 Linux 描述
#include <unistd.h> pid_t setsid(void); DESCRIPTION setsid() creates a new session if the calling process is not a process group leader. The calling process is the leader of the new session, the process group leader of the new process group, and has no control- ling tty. The process group ID and session ID of the calling process are set to the PID of the calling process. The calling process will be the only process in this new process group and in this new session. RETURN VALUE On success, the (new) session ID of the calling process is returned. On error, (pid_t) -1 is returned, and errno is set to indicate the error.
程序呼叫 setsid()函式會:
首先請注意:只有當該程序不是一個程序組長時,才會成功建立一個新的會話期。
(1)擺脫原會話的控制。該程序變成新會話期的首程序
(2)擺脫原程序組。成為一個新程序組的組長
(3)擺脫終端控制。如果在呼叫 setsid() 前,該程序有控制終端,那麼與該終端的聯絡被解除。 如果該程序是一個程序組的組長,
此函式返回錯誤。
建立守護程序的的一般步驟:
1、fork()建立子程序,父程序exit()退出
這是建立守護程序的第一步。由於守護程序是脫離控制終端的,因此,完成第一步後就會在Shell終端裡造成程式已經執行完畢的假象。之後的所有工作都在子程序中完成,而使用者在Shell終端裡則可以執行其他命令,從而在形式上做到了與控制終端的脫離,在後臺工作。
2、在子程序中呼叫 setsid() 函式建立新的會話
在呼叫了 fork() 函式後,子程序全盤拷貝了父程序的會話期、程序組、控制終端等,雖然父程序退出了,但會話期、程序組、控制終端等並沒有改變,因此,這還不是真正意義上的獨立開來,而 setsid() 函式能夠使程序完全獨立出來。
3、再次 fork() 一個子程序並讓父程序退出。
現在,程序已經成為無終端的會話組長,但它可以重新申請開啟一個控制終端,可以通過 fork() 一個子程序,該子程序不是會話首程序,該程序將不能重新開啟控制終端。退出父程序。
4、在子程序中呼叫 chdir() 函式,讓根目錄 ”/” 成為子程序的工作目錄
這一步也是必要的步驟。使用fork建立的子程序繼承了父程序的當前工作目錄。由於在程序執行中,當前目錄所在的檔案系統(如“/mnt/usb”)是不能解除安裝的,這對以後的使用會造成諸多的麻煩(比如系統由於某種原因要進入單使用者模式)。因此,通常的做法是讓"/"作為守護程序的當前工作目錄,這樣就可以避免上述的問題,當然,如有特殊需要,也可以把當前工作目錄換成其他的路徑,如/tmp。改變工作目錄的常見函式是chdir。
5、在子程序中呼叫 umask() 函式,設定程序的檔案許可權掩碼為0
檔案許可權掩碼是指遮蔽掉檔案許可權中的對應位。比如,有個檔案許可權掩碼是050,它就遮蔽了檔案組擁有者的可讀與可執行許可權。由於使用fork函式新建的子程序繼承了父程序的檔案許可權掩碼,這就給該子程序使用檔案帶來了諸多的麻煩。因此,把檔案許可權掩碼設定為0,可以大大增強該守護程序的靈活性。設定檔案許可權掩碼的函式是umask。在這裡,通常的使用方法為umask(0)。
6、在子程序中關閉任何不需要的檔案描述符
同文件許可權碼一樣,用fork函式新建的子程序會從父程序那裡繼承一些已經打開了的檔案。這些被開啟的檔案可能永遠不會被守護程序讀寫,但它們一樣消耗系統資源,而且可能導致所在的檔案系統無法卸下。
在上面的第二步之後,守護程序已經與所屬的控制終端失去了聯絡。因此從終端輸入的字元不可能達到守護程序,守護程序中用常規方法(如printf)輸出的字元也不可能在終端上顯示出來。所以,檔案描述符為0、1和2 的3個檔案(常說的輸入、輸出和報錯)已經失去了存在的價值,也應被關閉。
7、守護程序退出處理
當用戶需要外部停止守護程序執行時,往往會使用 kill 命令停止該守護程序。所以,守護程序中需要編碼來實現 kill 發出的signal訊號處理,達到程序的正常退出。
一張簡單的圖可以完美詮釋之前幾個步驟:
以下程式是建立一個守護程序,然後利用這個守護程序每隔一分鐘向daemon.log檔案中寫入當前時間,當守護程序收到 SIGQUIT 訊號後退出。
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
#include <stdio.h>
static bool flag = true;
void create_daemon();
void handler(int);
int main()
{
time_t t;
int fd;
create_daemon();
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if(sigaction(SIGQUIT, &act, NULL))
{
printf("sigaction error.\n");
exit(0);
}
while(flag)
{
fd = open("/home/mick/daemon.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
if(fd == -1)
{
printf("open error\n");
}
t = time(0);
char *buf = asctime(localtime(&t));
write(fd, buf, strlen(buf));
close(fd);
sleep(60);
}
return 0;
}
void handler(int sig)
{
printf("I got a signal %d\nI'm quitting.\n", sig);
flag = false;
}
void create_daemon()
{
pid_t pid;
pid = fork();
if(pid == -1)
{
printf("fork error\n");
exit(1);
}
else if(pid)
{
exit(0);
}
if(-1 == setsid())
{
printf("setsid error\n");
exit(1);
}
pid = fork();
if(pid == -1)
{
printf("fork error\n");
exit(1);
}
else if(pid)
{
exit(0);
}
chdir("/");
int i;
for(i = 0; i < 3; ++i)
{
close(i);
}
umask(0);
return;
}
注意守護程序一般需要在 root 許可權下執行。
通過
ps -ef | grep 'daemon'
可以看到:
root 26454 2025 0 14:20 ? 00:00:00 ./daemon
並且產生了 daemon.log,裡面是這樣的時間標籤
Thu Dec 8 14:35:11 2016
Thu Dec 8 14:36:11 2016
Thu Dec 8 14:37:11 2016
最後我們想退出守護程序,只需給守護程序傳送 SIGQUIT 訊號即可
sudo kill -3 26454
再次使用 ps 會發現程序已經退出。
三、利用庫函式 daemon()建立守護程序
其實我們完全可以利用 daemon() 函式建立守護程序,其函式原型:
#include <unistd.h>
int daemon(int nochdir, int noclose);
DESCRIPTION
The daemon() function is for programs wishing to detach themselves
from the controlling terminal and run in the background as system
daemons.
If nochdir is zero, daemon() changes the process's current working
directory to the root directory ("/"); otherwise, the current working
directory is left unchanged.
If noclose is zero, daemon() redirects standard input, standard
output and standard error to /dev/null; otherwise, no changes are
made to these file descriptors.
RETURN VALUE
(This function forks, and if the fork(2) succeeds, the parent calls
_exit(2), so that further errors are seen by the child only.) On
success daemon() returns zero. If an error occurs, daemon() returns
-1 and sets errno to any of the errors specified for the fork(2) and
setsid(2).
現在讓我們使用 daemon() 函式來再次建立一次守護程序,其實就是用 daemon() 替換掉我們自己的 create_daemon():
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <time.h>
#include <stdio.h>
static bool flag = true;
void handler(int);
int main()
{
time_t t;
int fd;
if(-1 == daemon(0, 0))
{
printf("daemon error\n");
exit(1);
}
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if(sigaction(SIGQUIT, &act, NULL))
{
printf("sigaction error.\n");
exit(0);
}
while(flag)
{
fd = open("/home/mick/daemon.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
if(fd == -1)
{
printf("open error\n");
}
t = time(0);
char *buf = asctime(localtime(&t));
write(fd, buf, strlen(buf));
close(fd);
sleep(60);
}
return 0;
}
void handler(int sig)
{
printf("I got a signal %d\nI'm quitting.\n", sig);
flag = false;
}
沒問題,和之前一樣。