[單刷APUE系列]第十三章——守護程式

山河永寂發表於2019-05-13

守護程式

守護程式對於Unix運維來說應該是不陌生的,所有的提供服務的程式基本上都是守護程式,通常也可以稱為服務。它們由init程式啟動,並且沒有控制終端,是一種執行日常事務的程式。
在Unix系統下,有很多守護程式,在基於BSD的系統下執行下列命令

ps -axj

-a選項顯示所有程式,包括其他使用者的程式,-x顯示沒有控制終端的程式狀態,-j顯示與作業有關的資訊:會話ID、程式組ID、控制終端以及終端程式組ID。在基於SystemV的系統中,對應命令是ps -efj,具體如何需要檢視自己的ps命令說明,當然還有一些系統只允許超級使用者檢視到其他的使用者程式,普通使用者不能檢視其他使用者程式。
我們知道,除了使用者程式以外,還有很多系統程式,比如守護程式,對於大部分Unix環境來說,使用的都是SystemV風格的init啟動方式,首先是Grub引導核心啟動,然後核心會查詢/sbin/init程式並且啟動它,init程式會根據一系列的配置檔案啟動不同的指令碼,最終啟動守護程式。當然,目前最新的作業系統基本上都是systemd,所以是不同的,但是基本原理還是相同的,比如同樣都是root許可權執行,所有守護程式沒有控制終端,

程式設計規則

編寫守護程式的時候需要遵循一些規則,以免出現各種問題

  1. 首先是呼叫umask函式設定檔案遮蔽字,比如0,因為守護程式是fork產生的,繼承來的檔案模式建立遮蔽字可能會被設定為拒絕某些許可權

  2. 呼叫fork,然後使父程式exit。首先,由於我們不知道守護程式是如何產生的,它有可能是使用者shell呼叫後產生的,所以我們需要讓其能被init託管。其次,子程式雖然繼承了父程式的程式組ID,但是卻有了新的程式ID,也就是說,不可能是組長程式了

  3. 呼叫setsid建立一個會話,使程式成為新會話的首程式,成為一個新程式組的組長程式,沒有控制終端

  4. 將當前工作目錄設定為/目錄。因為從父程式繼承過來的屬性可能會導致檔案系統無法解除安裝,所以我們需要使用chdir()函式。

  5. 關閉不需要的檔案描述符

  6. 某些守護程式在0、1、2上開啟/dev/null來保證不會有標準輸入輸出。

#include "include/apue.h"
#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>

void daemonize(const char *cmd)
{
    int i, fd0, fd1, fd2;
    pid_t pid;
    struct rlimit rl;
    struct sigaction sa;
    
    umask(0);
    
    if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
        err_quit("%s: can`t get file limit", cmd);
        
    if ((pid = fork()) < 0)
        err_quit("%s: can`t fork", cmd);
    else if (pid != 0)
        exit();
    setsid();
    
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if (sigaction(SIGHUP, &sa, NULL) < 0)
        err_quit("%s: can`t ignore SIGHUP", cmd);
    if ((pid = fork()) < 0)
        err_quit("%s: can`t fork", cmd);
    else if (pid != 0)
        exit();
        
    if (chdir("/") < 0)
        err_quit("%s: can`t change directory to /", cmd);
    
    if (rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;
    for (i = 0; i < rl.rlim_max; ++i)
        close(i);
        
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);
    
    openlog(cmd, LOG_CONS, LOG_DAEMON);
    if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
        syslog(LOG_ERR, "unexpected file descriptors %d %d %d", fd0, fd1, fd2);
        exit(1);
    }
}

上面這個函式實際上就是之前所說必須遵循的規則的寫法,只需要通過main函式呼叫這個函式就能使程式變為守護程式,當然,由於許可權問題,實際上並不是實際的寫法。

日誌

守護程式的一個問題就是日誌問題,我們知道,任何的程式必然需要有方式記錄下自己的活動日誌,對於大部分情況來說,都是通過標準輸入標準輸出標準錯誤的形式記錄日誌,但是守護程式沒有控制終端,不能寫到標準錯誤上,而且我們也不會希望它寫到終端上,其中一個解決方法是寫到一個單獨的檔案中,但是這樣會讓運維人員非常頭痛,因為程式一多就會非常混亂,所以就需要一個集中式的守護程式來記錄日誌。
syslog是BSD伯克利開發的,廣泛運用於BSD系列的系統中,後來成為了Unix標準之一。當然,目前由於systemd的實質性接管,所以syslog的作用正在被systemd蠶食,這裡不是討論的重點。
syslog的架構很簡單,但是很有效,syslogd作為系統服務啟動,然後偵聽/dev/logsocket、/dev/klogsocket和UDP514埠。其中/dev/log用於接收本地使用者程式的日誌資訊,UDP514埠接收網路上的日誌資訊,/dev/klog則是監聽核心的日誌資訊。
由於這種架構,所以開發者可以使用三種方法產生日誌資訊

  1. 核心例程可以呼叫log函式,任何一個使用者程式都可以開啟讀取/dev/klog讀取資訊

  2. 守護程式呼叫syslog函式來產生日誌資訊,最終這些資訊將被髮送到/dev/log

  3. 任何程式都可以向UDP514埠傳送日誌資訊。

syslogd在啟動的時候回讀取配置檔案,一般在/etc/syslog.conf,裡面決定了訊息應當被髮送到何處,甚至有可能重要資訊會被在管理員控制檯上列印。

void openlog(const char *ident, int option, int facility);
void syslog(int priority, const char *format, ...);
void closelog(void);
int setlogmask(int maskpri);

openlog函式讓開發者指定一個ident引數,也就是識別符號,以後,這個ident將被加到每則日誌訊息中。option引數則是指定了各種選項位遮蔽。

option 說明
LOG_CONS 若日誌資訊不能通過Unix Domain資料包,則將該訊息寫入控制檯
LONG_NDELAY 立即開啟至syslogd守護程式的Unix Domain資料包套接字,不要等到第一條資訊已經被記錄時候再開啟。通常,在記錄第一條資訊之前,不開啟套接字
LOG_NOWAIT 不要等待在將訊息記錄日誌過程中可能以建立的子程式,因為在syslog呼叫wait時,應用程式可能已獲得了子程式的狀態。這種處理阻止了與捕捉SIGCHLD訊號的應用程式之前產生的衝突
LOG_ODELAY 在第一條訊息被記錄之前延遲開啟連結
LOG_PERROR 除將日誌訊息傳送給syslogd以外,還將其寫入到標準錯誤
LOG_PID 記錄每條資訊都要包含程式ID

openlog的facility引數值則包含了很多可選值,但是非常遺憾的是,只有少部分是能被跨平臺使用的。具體可以參見各平臺的Unix手冊。
syslog函式則會產生一條日誌,其priority引數是facility和level的組合,format引數則是格式化字串,基本和vsprintf函式一樣。
setlogmask函式用於設定程式的記錄優先順序遮蔽字。它返回撥用它之前的遮蔽字,也就是可以用來儲存著或者瞭解之前的遮蔽字狀態,各條訊息除非已在記錄優先順序遮蔽字中進行了設定,否則不會被記錄。

單例項守護程式

很多情況下,守護程式只是一個程式,因為不需要併發地進行操作,而且這樣很有可能導致資源競爭,所以在很多情況下,守護程式只會實現在任意時刻只存在守護程式一個副本,所以為了保證只存在一個副本,就需要一種機制來保證。而檔案和記錄鎖就是這樣一種保證方式,實際上,不單單是單例項守護程式,幾乎所有的守護程式都採用了這種方式。

#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <syslog.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <sys/stat.h>

#define LOCKFILE "/var/run/daemon.pid"
#define LOCKMODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)

extern int lockfile(int);

int already_running(void)
{
    int fd;
    char buf[16];
    
    fd = open(LOCKFILE, O_RDWR | O_CREATE, LOCKMODE);
    if (fd < 0) {
        syslog(LOG_ERR, "can`t open %s: %s", LOCKFILE, strerror(error));
        exit(1);
    }
    if (lockfile(fd) < 0) {
        if (errno == EACCES || errno == EAGAIN) {
            close(fd);
            return(1);
        }
        syslog(LOG_ERR, "can`t lock %s: %s", LOCKFILE, strerror(errno));
        exit(1);
    }
    ftruncate(fd, 0);
    sprintf(buf, "%ld", (long)getpid());
    write(fd, buf, strlen(buf) + 1);
    return(0);
}

實際上上面的行為非常常見,守護程式啟動的時候試圖建立一個檔案並且將程式ID寫入其中,如果該檔案加鎖,則lockfile函式將會失敗,並且返回,表明已經有守護程式正在執行。否則將檔案長度截斷為0,將程式ID寫入其中。

守護程式的慣例

  1. 如果守護程式使用鎖檔案,那麼該檔案通常儲存在/var/run目錄中。不過需要注意,守護程式需要超級使用者許可權才能建立檔案。鎖檔名字一般是name.pid

  2. 如果守護程式支援配置檔案,則配置檔案一般儲存在/etc目錄中。

  3. 守護程式可以通過命令列啟動,但是通常是使用init指令碼啟動的。

  4. 如果一個守護程式有一個配置檔案,在啟動的時候會讀取該檔案,但是在此之後一般就不會再檢視它。如果管理員更改了配置檔案,那麼該守護程式可能需要重新啟動,後來在訊號機制中加入了SIGHUP訊號的捕捉,讓守護程式接收到訊號後重新讀取配置檔案。

客戶程式-伺服器程式模型

C/S程式模型在Unix環境中非常常見,守護程式通常就是伺服器程式,然後等待客戶程式語氣聯絡,提出某種型別的請求,為了保證請求的高效處理,伺服器程式中呼叫fork然後exec另一個程式來提供服務是非常常見的。這些伺服器程式通常管理著多種資源。而為了保證檔案描述符不被濫用,所以需要對所有被執行程式不需要的檔案描述符設定成執行時關閉(close-on-exec)。

相關文章