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

原文來自靜雅齋,轉載請註明出處。javascript

守護進程

守護進程對於Unix運維來講應該是不陌生的,全部的提供服務的進程基本上都是守護進程,一般也能夠稱爲服務。它們由init進程啓動,而且沒有控制終端,是一種執行平常事務的進程。
在Unix系統下,有不少守護進程,在基於BSD的系統下運行下列命令java

ps -axj複製代碼

-a選項顯示全部進程,包括其餘用戶的進程,-x顯示沒有控制終端的進程狀態,-j顯示與做業有關的信息:會話ID、進程組ID、控制終端以及終端進程組ID。在基於SystemV的系統中,對應命令是ps -efj,具體如何須要查看本身的ps命令說明,固然還有一些系統只容許超級用戶查看到其餘的用戶進程,普通用戶不能查看其餘用戶進程。
咱們知道,除了用戶進程之外,還有不少系統進程,好比守護進程,對於大部分Unix環境來講,使用的都是SystemV風格的init啓動方式,首先是Grub引導內核啓動,而後內核會查找/sbin/init程序而且啓動它,init程序會根據一系列的配置文件啓動不一樣的腳本,最終啓動守護進程。固然,目前最新的操做系統基本上都是systemd,因此是不一樣的,可是基本原理仍是相同的,好比一樣都是root權限運行,全部守護進程沒有控制終端,shell

編程規則

編寫守護進程的時候須要遵循一些規則,以避免出現各類問題編程

  1. 首先是調用umask函數設置文件屏蔽字,好比0,由於守護進程是fork產生的,繼承來的文件模式建立屏蔽字可能會被設置爲拒絕某些權限
  2. 調用fork,而後使父進程exit。首先,因爲咱們不知道守護進程是如何產生的,它有多是用戶shell調用後產生的,因此咱們須要讓其能被init託管。其次,子進程雖然繼承了父進程的進程組ID,可是卻有了新的進程ID,也就是說,不多是組長進程了
  3. 調用setsid建立一個會話,使進程成爲新會話的首進程,成爲一個新進程組的組長進程,沒有控制終端
  4. 將當前工做目錄設置爲/目錄。由於從父進程繼承過來的屬性可能會致使文件系統沒法卸載,因此咱們須要使用chdir()函數。
  5. 關閉不須要的文件描述符
  6. 某些守護進程在0、一、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寫入其中。socket

守護進程的慣例

  1. 若是守護進程使用鎖文件,那麼該文件一般存儲在/var/run目錄中。不過須要注意,守護進程須要超級用戶權限才能建立文件。鎖文件名字通常是name.pid
  2. 若是守護進程支持配置文件,則配置文件通常存儲在/etc目錄中。
  3. 守護進程能夠經過命令行啓動,可是一般是使用init腳本啓動的。
  4. 若是一個守護進程有一個配置文件,在啓動的時候會讀取該文件,可是在此以後通常就不會再查看它。若是管理員更改了配置文件,那麼該守護進程可能須要從新啓動,後來在信號機制中加入了SIGHUP信號的捕捉,讓守護進程接收到信號後從新讀取配置文件。

    客戶進程-服務器進程模型

    C/S進程模型在Unix環境中很是常見,守護進程一般就是服務器進程,而後等待客戶進程語氣聯繫,提出某種類型的請求,爲了保證請求的高效處理,服務器進程中調用fork而後exec另外一個程序來提供服務是很是常見的。這些服務器進程一般管理着多種資源。而爲了保證文件描述符不被濫用,因此須要對全部被執行程序不須要的文件描述符設置成執行時關閉(close-on-exec)。
相關文章
相關標籤/搜索