[单刷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)。
相关文章
相关标签/搜索