《Linux – Linux高级编程 – 第二部分 进程与线程》第4章 守护进程详解

4.1守护进程概述

4.1.1守护进程的概念

Linux Daemon(守护进程)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。它不需要用户输入就能运行而且提供某种服务,不是对整个系统就是对某个用户程序提供服务。Linux系统的大多数服务器就是通过守护进程实现的;同时,守护进程还能完成许多系统任务。常见的守护进程包括系统日志进程syslogd、 web服务器httpd、邮件服务器sendmail和数据库服务器mysqld等。

由于在Linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。但是守护进程却能够突破这种限制,它从被执行开始运转,直到整个系统关闭时才退出。如果想让某个进程不因为用户或终端或其他地变化而受到影响,那么就必须把这个进程变成一个守护进程。

4.1.2守护进程的特点

(1)运行方式:守护进程最重要的特性是后台运行,并且不与任何控制终端相关联。即使daemon进程是从终端命令行启动的,终端相关的信号如SIGINT、SIGQUIT和SIGTSTP,以及关闭终端,都不会影响到daemon进程的继续执行。在这一点上DOS下的常驻内存程序TSR与之相似。周期性的执行某种任务或等待处理某些发生的事件。Linux系统有很多守护进程,大多数服务都是用守护进程实现的。

守护进程的名称通常以d结尾,比如sshd、xinetd、crond等。

比如:像我们的tftp,samba,nfs等相关服务。同时Linux的大多数服务器就是用守护进程实现的。
比如,Internet服务器inetd,Web服务器httpd等。

(2)生命周期:守护进程会长时间运行,常常在系统启动时就开始运行,直到系统关闭时才终止一旦启动,正常情况下不会终止,一直运行到系统退出。但凡事无绝对:daemon进程其实也是可以停止的, 如很多daemon提供了stop命令, 执行stop命令就可以终止daemon, 或者通过发送信号将其杀死,又或者因为daemon进程代码存在bug而异常退出。这些退出一般都是由手工操作或因异常引发的。

(3)守护进程不依赖于终端:显而异见,从终端开始运行的进程都会依附于这个终端,这个终端称为这些进程的控制终端。当控制终端被关闭时,相应的进程都会被自动关闭。咱们平常写进程时,一个死循环程序关闭终端的同时也关闭了我们的程序,但是对于守护进程来说,其生命周期守护需要突破这种限制,它从开始运行,直到整个系统关闭才会退出,所以守护进程不能依赖于终端。它独立于其运行前的环境,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的。

(4)启动方式:守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(通常是shell)执行。

除了以上这些特征外,守护进程与普通进程基本上没有什么区别。实际上,编写守护进程也就是按照上述的守护进程特征把一个普通进程改造成为守护进程。

总结一下,守护进程一般在系统启动时开始运行,除非强行终止,否则直到系统关机都保持运行。守护进程经常以超级用户(root)权限运行,因为它们要使用特殊的端口(1-1024)或访问某些特殊的资源。

一个守护进程的父进程是init进程,因为它真正的父进程在fork出子进程后就先于子进程exit退出了,所以它是一个由init继承的孤儿进程。守护进程是非交互式程序,没有控制终端,所以任何输出,无论是向标准输出设备stdout还是标准出错设备stderr的输出都需要特殊处理。

4.1.3守护进程的查看与识别

查看系统中的守护进程
ps -ajx
【注】
-a: 显示由其他用户用户拥有的进程状态
-x:显示没有控制终端的进程
-j:显示与作业有关的信息(显示的列):会话期ID(SID),进程组ID(PGID),控制终端(TTY),终端进程组ID(TRGID)

识别一个守护进程

  • 一般所有的守护进程都是以超级用户启动的(UID为0);
  • 没有控制终端(TTY为?);
  • 终端进程组ID为-1(TPGID表示终端进程组ID,该值表示与控制终端相关的前台进程组,如果未和任何终端相关,其值为-1;
  • 所有的守护进程的父进程都为init进程(PID为1的进程)。

举个列子,作业规划进程cron

  PPID    PID   PGID    SID TTY       TPGID STAT   UID   TIME COMMAND
     1   1371   1371   1371 ?            -1 Ss       0   0:00 cron

PPID是父进程的ID;PID是进程的ID;PGID为进程组ID;SID为会话期ID(SID);TTY登入者的终端机位置;TPGID为终端进程组ID;TIME 使用掉的 CPU 时间;

4.1.4守护进程相关概念

1)进程组
进程组是一个或多个进程的集合。进程组由进程组ID(PGID)来唯一标识。每个进程也属于一个进程组;每个进程组都有一个进程组号,该号等于该进程组组长的PID号;一个进程只能为它自己或子进程设置进程组ID号。

2)会话期
会话期是一个或多个进程组的集合。一般一个用户登录后新建一个会话(打开终端),每个会话也有一个ID来标识(SID)。登录后的第一个进程叫做会话领头进程(session leader),通常是一个shell/bash。对于会话领头进程,其PID=SID。setsid()函数可以建立一个对话期。

3)控制终端
一个会话一般会拥有一个控制终端用于执行IO操作。用户登录的终端就成为该会话的控制终端。与控制终端建立连接的会话领头进程也称为控制进程 (controlling process) 。一个会话只能有一个控制终端。

4)前台进程组
该进程组中的进程能够向终端设备进行读、写操作的进程组。

5)后台进程组
该进程组中的进程只能够向终端设备写。每个会话有且只有一个前台进程组,但会有0个或者多个后台进程组。

6)终端进程组ID
每个进程还有一个属性,终端进程组ID(TPGID),用来标识一个进程是否处于一个和终端相关的进程组中。前台进程组中的进程的TPGID=PGID,后台进程组的PGID≠TPGID。若该进程和任何终端无关,其值为-1。通过比较他们来判断一个进程是属于前台进程组,还是后台进程组。

gVxTGq.png

4.2守护进程创建步骤

守护进程的编程本身并不复杂,复杂的是各种版本的Unix的实现机制不尽相同,造成不同Unix环境下守护进程的编程规则并不一致。如果照搬某些书上的规则(特别是BSD4.3和低版本的System V)到Linux会出现错误的。所幸的是守护进程的编程原则都是一样的,区别在于具体的实现细节。这个原则就是要满足守护进程的特征。同时,Linux是基于Syetem V的SVR4并遵循Posix标准,实现起来比BSD4更方便。守护进程编程要点总结如下:

【1】信号处理
1、屏蔽控制终端操作信号
屏蔽一些有关控制终端操作的信号。防止在守护进程没有正常运转起来时,控制终端受到干扰退出或挂起。

// 防止守护进程没有正常运转起来时,因控制终端受到干扰退出或挂起
assert(signal(SIGINT, SIG_IGN) != SIG_ERR); // 终端中断
assert(signal(SIGHUP, SIG_IGN) != SIG_ERR); // 连接挂断
assert(signal(SIGQUIT, SIG_IGN) != SIG_ERR);// 终端退出
assert(signal(SIGPIPE, SIG_IGN) != SIG_ERR);// 向无读进程的管道写数据
assert(signal(SIGTTOU, SIG_IGN) != SIG_ERR);// 后台程序尝试写操作
assert(signal(SIGTTIN, SIG_IGN) != SIG_ERR);// 后台程序尝试读操作
assert(signal(SIGTERM, SIG_IGN) != SIG_ERR);// 终止

说明:
1) SIGHUP
本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。

登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录,wget也能继续下载。

此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

2) SIGTSTP
停止进程的运行, 但该信号可以被处理和忽略。用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号。

3) SIGTTIN
当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号。缺省时这些进程会停止执行。

4) SIGTTOU
类似于SIGTTIN,当后台作业要向用户终端写数据(或修改终端模式)时收到。

2、处理SIGCHLD信号
处理SIGCHLD(子进程结束)信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程来处理请求。如果父进程不等待子进程结束,子进程将成为僵死进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下,可以简单地将SIGCHLD信号的操作设SIG_IGN(父进程对子进程结束状态不感兴趣,忽略子进程结束信号SIG_IGN)。
assert(signal(SIGCHLD, SIG_IGN) != SIG_ERR);// 终止

这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。

另一种避免僵死进程的方法是:fork两次,父进程fork一个子进程,然后继续工作,子进程fork一个孙进程后退出,那么孙进程被init接管,孙进程结束后,init会回收。不过子进程的回收还要父进程来做。

【2】创建子进程,父进程退出
为避免挂起控制终端,将daemon放入后台执行。方法是在程序中调用fork使父进程终止,让daemon在子进程中后台执行。实质是让init进程成为新产生进程的父进程。调用fork函数创建子进程后,使父进程立即退出。从而使产生的子进程将变成孤儿进程,并被init进程接管,同时,所产生的新进程将变为在后台运行;利用前面介绍的父进程先于子进程退出后内核会自动托付子进程给init的原理。

pid = fork();
if (pid > 0)        //父进程终止;子进程继续运行
    exit(0);

执行这一步, 原因有二:

  • 父进程有可能是进程组的组长(在命令行启动的情况下) , 从而不能够执行后面要执行的setsid函数, 子进程继承了父进程的进程组ID, 并且拥有自己的进程ID, 一定不会是进程组的组长, 所以子进程一定可以执行后面要执行的setsid函数。
  • 如果daemon是从终端命令行启动的, 那么父进程退出会被shell检测到, shell会显示shell提示符,让子进程在后台执行。

【3】在子进程中创建新会话
这个步骤是创建守护进程最重要的一步,虽然实现非常简单,但意义却非常重大。因为这一步确保了子进程不再归属于控制终端所关联的会话。因此无论终端是否发送SIGINT、 SIGQUIT或SIGTSTP信号, 也无论终端是否断开, 都与要创建的daemon进程无关, 不会影响到daemon进程的继续执行。有必要先介绍一下Linux中的进程与控制终端、登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。

控制终端、登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是:在前一点的基础上,调用setsid()使进程成为会话组长:
setsid();

  • setsid函数作用

首先内核会创建一个新的会话,并让该进程成为该会话的leader进程;即摆脱原会话的控制。
同时伴随该session的建立,一个新的进程组也会被创建,同时该进程成为该进程组的组长;摆脱原进程组的控制。
该进程此时还没有和任何控制终端关联。如果调用setsid之前该进程有一个控制终端,那么这种联系也被切断,即是摆脱原控制终端的控制。

【注】如果该调用进程已经是一个进程组组长,则此函数返回出错。回想一下我们为了保证不处于这种情况我们是如何处理的?第一步,先调用fork,然后父进程终止,而子进程继续。因为子进程继承了父进程的进程组ID,而其进程ID是新分配的,两者不可能相等,这就保证了子进程不是一个进程组组长。

【4】改变当前目录为根目录
这一步也是必要的步骤。使用fork()创建的子进程继承了父进程的当前工作目录。daemon一直在运行,如果当前工作路径上包含有根文件系统以外的其他文件系统,当前目录所在的文件系统(如“/mnt/usb”等)是不能卸载的,这对以后的使用会造成诸多的麻烦。因此,通常的做法是让“/”作为守护进程的当前工作目录,这样就可以避免上述问题。当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp,只要确保该目录所在的文件系统不会被卸载即可。改变工作目录的常见函数是chdir()。

chdir("/");

【5】重设文件权限掩码
文件权限掩码是指屏蔽掉文件权限中的对应位。比如,有一个文件权限掩码是 050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用 fork 函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦,这可能会修改了守护进程所创建的文件的存取位。。因此,把文件权限掩码设置为 0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是 umask。在这里,通常的使用方法为 umask(0)。

umask(0);

这一步的目的是让daemon进程创建文件的权限属性与shell脱离关系。 因为默认情况下, 进程的umask来源于父进程shell的umask。 如果不执行umask(0),那么父进程shell的umask就会影响到daemon进程的umask。 如果用户改变了shell的umask, 那么也就相当于改变了daemon的umask, 就会造成daemon进程每次执行的umask信息可能会不一致。

【6】再次执行fork,父进程退出,子进程继续
执行完前面两步之后,可以说已经比较圆满了:新建会话,进程是会话的首进程,也是进程组的首进程。进程ID、进程组ID和会话ID, 三者的值相同,进程和终端无关联。那么这里为何还要再执行一次fork函数呢?

原因是,守护进程有可能会打开一个终端设备, 即守护进程可能会根据需要,执行类似如下的代码:

int fd = open("/dev/console", O_RDWR);

这个打开的终端设备是否会成为守护进程的控制终端, 取决于两点:

  • 进程是不是会话的首进程。
  • 系统实现。(BSD风格的实现不会成为守护进程的控制终端, 但是POSIX标准说这由具体实现来决定)。

既然如此,为了确保万无一失,只有确保守护进程不是会话的首进程,才能保证打开的终端设备不会自动成为控制终端。因此,不得不执行第二次fork,fork之后,父进程退出,子进程继续。这时,子进程不再是会话的首进程,也不是进程组的首进程了。

使进程不再成为会话组长来禁止进程重新打开控制终端的代码如下:

pid = fork();
if (pid > 0)        //子进程终止;孙进程继续运行,孙进程不在是会话组长
    exit(0);

【注】也有把第4、5两步放在第六步之后。

【7】关闭打开的文件描述符
同文件权限掩码一样,守护进程从创建它的父进程那里继承了打开的文件描述符。这些被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。

守护进程已经与所属的控制终端失去了联系。因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如 printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为 0、1 和 2 的 3 个文件(常说的输入、输出和报错这 3 个文件),文件描述符分别是0、1和2指向的就是控制终端,现在守护进程已经不再与任意控制终端相关联,因此这三者都没有意义。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:

fdTableSize = getdtablesize();
for (fd=0; fd<fdTableSize; fd++)
close(fd);

或者:

for (i = 0; i < NOFILE; ++i)
{
    close(i);
}

【8】重定向文件描述符0、1和2到/dev/null
某些守护进程打开了/dev/null使其具有文件描述符0、1和2,这样,任何一个试图读标准输入、写标准输出或标准错误的库例程都不会产生任何效果,因为守护进程不在和它们关联。即是守护进程是从交互式回话启动的,但是守护进程是在后台运行的,所以登录回话的终止并不会影响守护进程。如果其他用户从同一个终端设备登录,我们并不希望该终端看到守护进程的输出,也不希望该终端的输入被守护进程读取。这是就需要将将0、1和2重定向到/dev/null。这个重定向是有意义的,防止了后面的程序在文件描述符0、1和2上执行I/O库函数而导致报错。

【9】出错记录和调试信息
因为守护进程已脱离了控制终端,并且关闭了所有文件描述符,所以不能简单地把出错记录或调试信息写到标准输出或标准错误输出中。所幸的是,在Linux/Unix下有个syslogd守护进程,向用户提供了syslog()系统调用。任何程序都可以通过syslog记录信息(信息一般保存在/var/log/messages中)。

【注意】“/var/log/message”系统日志文件只能由拥有 root 权限的超级用户查看。
Syslog 是 Linux 中的系统日志管理服务,通过守护进程 syslogd 来维护。该守护进程在启动时会读一个配置文件“/etc/syslog.conf”。该文件决定了不同种类的消息会发送向何处。例如,紧急消息可被送向系统管理员并在控制台上显示,而警告消息则可记录到一个文件中。该机制提供了 3 个 syslog 函数,分别为 openlog、syslog 和 closelog。

但在有些嵌入式liunx系统中,可能没有syslogd守护进程,因此,无法使用syslog()来记录信息。那就只能重定向标准输入输出了。重定向标准输入输出,需要在关闭打开的文件描述符时应绕过标准输入输出。

4.3守护进程创建实例

4.3.1使用setsid创建守护进程实例

【实例1】

/**
  ******************************************************************************
  * @file           Daemon.c
  * @author         BruceOu
  * @version        V1.1
  * @date           2019.04.17
  * @brief          守护进程的创建
  ******************************************************************************
  */
/**Includes*********************************************************************/
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/param.h>  // NOFILE
#include <sys/stat.h>   // umask
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <assert.h>
#include <fcntl.h>

/**类型定义*********************************************************************/
typedef enum{false,true}bool;

/**宏定义***********************************************************************/
#define ERR_EXIT(m) \
do\
{\
    perror(m);\
    exit(EXIT_FAILURE);\
}\
while (0);\

/**
  * @brief     初始化守护进程函数
  * @param    None
  * @retval    bool 
  */
bool initDaemon()
{
    // 【1】屏蔽一些有关控制终端操作的信号
    // 防止守护进程没有正常运转起来时,因控制终端受到干扰退出或挂起
    assert(signal(SIGINT, SIG_IGN) != SIG_ERR); // 终端中断
    assert(signal(SIGHUP, SIG_IGN) != SIG_ERR); // 连接挂断
    assert(signal(SIGQUIT, SIG_IGN) != SIG_ERR);// 终端退出
    assert(signal(SIGPIPE, SIG_IGN) != SIG_ERR);// 向无读进程的管道写数据
    assert(signal(SIGTTOU, SIG_IGN) != SIG_ERR);// 后台程序尝试写操作
    assert(signal(SIGTTIN, SIG_IGN) != SIG_ERR);// 后台程序尝试读操作
    assert(signal(SIGTERM, SIG_IGN) != SIG_ERR);// 终止

    // 【2】创建一个子进程,父进程退出
    int pid = fork();
    if (pid)
    {
        // 父进程退出
        exit(0);
    }
    else if (pid < 0)
    {
        ERR_EXIT("fork error");
    }

    // 子进程继续运行
    // 【3】在子进程中创建新的会话,setsid有三个作用
    // a.让进程摆脱原会话的控制
    // b.让进程摆脱原进程组的控制
    // c.让进程摆脱原控制终端的控制
    int ret = setsid();
    if (ret < 0)
    {
        ERR_EXIT("setsid error");
    }

    // 【4】改变当前工作目录
    // 进程活动时,其工作目录所在的文件系统不能卸下,一般将工作目录改变到根目录
    ret = chdir("/");
    if (ret < 0)
    {
        ERR_EXIT("chdir error");
    }

    //【5】重新设置文件创建掩码
    // 进程从创建它的父进程那里继承了文件创建掩码,它可能修改守护进程所创建的文件的存取位
    // 所以将文件创建掩码清除
    umask(0);

    //【6】再次执行fork,父进程退出,子进程继续
    // 进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端
    // 可以通过使进程不再成为会话组长来禁止进程重新打开控制终端
    pid = fork();
    if (pid)
    {
        // 结束第一个子进程
        exit(0);
    }
    else if (pid < 0)
    {
        ERR_EXIT("fork error");
    }

    // 第二个子进程继续运行
    // 【7】关闭打开的文件描述符
    // 进程从创建它的父进程那里继承了打开的文件描述符,如果不关闭,将会浪费系统资源,
    // 造成进程所在的文件系统无法卸下以及引起无法预料的错误
    int i;
    for (i = 0; i < NOFILE; ++i)
    {
        close(i);
    }

    //【8】重定向文件描述符0、1和2到/dev/null
    open("/dev/null", O_RDWR);
    dup(0);
    dup(0);

    return true;
}

/**
  * @brief     主函数
  * @param     argc
               argv
  * @retval    None
  */
int main(int argc, char *argv[])
{
    // 初始化守护进程
    bool ret = initDaemon();
    if (!ret)
    {
        ERR_EXIT("Init daemon failed\n");
        //printf("Init daemon failed\n");
        return 1;
    }
    FILE* file = NULL;
    time_t t = 0;

    // 每隔1秒向daemon.log报告运行状态
    while (true)
    {
        sleep(1);
        file = fopen("daemon.log", "a+");
        if(file == NULL)
        {
            ERR_EXIT("open failed\n");
        }
        if (file != NULL)
        {
            t = time(NULL);
            fprintf(file, "I am here at %s", asctime(localtime(&t)));
            fclose(file);
        }
    }
    return 0;
}

结果如下所示:

gVztFs.png

gVzNYn.png

【分析】当普通用户执行a.out时,进程表中并没有出现新创建的守护进程,但当我以root用户执行时,成功了,并在/目录下创建了daemon.log文件,cat查看后确实每个一分钟写入一次。为什么只能root执行,那是因为当我们创建守护进程时,已经将当前目录切换我/目录,所以当我之后创建daemon.log文件是其实是在/目录下,那肯定不行,因为普通用户没有权限,或许你会问那为啥没报错呢?其实是有出错,只不过我们在创建守护进程时已经将标准输入关闭并重定向到/dev/null,所以看不到错误信息。

【实例2】

/**
  ******************************************************************************
  * @file           Daemon.c
  * @author         BruceOu
  * @version        V2.1
  * @date           2019.04.18
  * @brief          守护进程的创建
  ******************************************************************************
  */
/**Includes*********************************************************************/
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/param.h>  // NOFILE
#include <sys/stat.h>   // umask
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <assert.h>
#include <fcntl.h>
#include <syslog.h>

/**类型定义*********************************************************************/
typedef enum{false,true}bool;

/**宏定义***********************************************************************/
#define ERR_EXIT(m) \
do\
{\
    perror(m);\
    exit(EXIT_FAILURE);\
}\
while (0);\

/**
  * @brief     初始化守护进程函数
  * @param    None
  * @retval    bool 
  */
bool initDaemon()
{
    // 【1】屏蔽一些有关控制终端操作的信号
    // 防止守护进程没有正常运转起来时,因控制终端受到干扰退出或挂起
    assert(signal(SIGINT, SIG_IGN) != SIG_ERR); // 终端中断
    assert(signal(SIGHUP, SIG_IGN) != SIG_ERR); // 连接挂断
    assert(signal(SIGQUIT, SIG_IGN) != SIG_ERR);// 终端退出
    assert(signal(SIGPIPE, SIG_IGN) != SIG_ERR);// 向无读进程的管道写数据
    assert(signal(SIGTTOU, SIG_IGN) != SIG_ERR);// 后台程序尝试写操作
    assert(signal(SIGTTIN, SIG_IGN) != SIG_ERR);// 后台程序尝试读操作
    assert(signal(SIGTERM, SIG_IGN) != SIG_ERR);// 终止

    //【2】创建一个子进程,父进程退出
    int pid = fork();
    if (pid)
    {
        // 父进程退出
        exit(0);
    }
    else if (pid < 0)
    {
        ERR_EXIT("fork error");
    }

    /*打开系统日志服务,openlog*/
    openlog("initdaemon",LOG_PID, LOG_DAEMON);

    // 子进程继续运行
    //【3】在子进程中创建新的会话,setsid有三个作用
    // a.让进程摆脱原会话的控制
    // b.让进程摆脱原进程组的控制
    // c.让进程摆脱原控制终端的控制
    int ret = setsid();
    if (ret < 0)
    {
        ERR_EXIT("setsid error");
    }

    //【4】改变当前工作目录
    // 进程活动时,其工作目录所在的文件系统不能卸下,一般将工作目录改变到根目录
    ret = chdir("/");
    if (ret < 0)
    {
        ERR_EXIT("chdir error");
    }

    //【5】重新设置文件创建掩码
    // 进程从创建它的父进程那里继承了文件创建掩码,它可能修改守护进程所创建的文件的存取位
    // 所以将文件创建掩码清除
    umask(0);

    // 【6】再次执行fork,父进程退出,子进程继续
    // 进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端
    // 可以通过使进程不再成为会话组长来禁止进程重新打开控制终端
    pid = fork();
    if (pid)
    {
        // 结束第一个子进程
        exit(0);
    }
    else if (pid < 0)
    {
        ERR_EXIT("fork error");
    }

    // 第二个子进程继续运行
    // 【7】关闭打开的文件描述符
    // 进程从创建它的父进程那里继承了打开的文件描述符,如果不关闭,将会浪费系统资源,
    // 造成进程所在的文件系统无法卸下以及引起无法预料的错误
    int i;
    for (i = 0; i < NOFILE; ++i)
    {
        close(i);
    }

    //【8】重定向文件描述符0、1和2到/dev/null
    int fd0, fd1, fd2;
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);

    if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
        syslog(LOG_ERR, "unexpected file descriptors %d %d %d",
          fd0, fd1, fd2);
        exit(1);
    }

    return true;
}

/**
  * @brief     主函数
  * @param     argc
               argv
  * @retval    None
  */
int main(int argc, char *argv[])
{
    // 初始化守护进程
    bool ret = initDaemon();
    if (!ret)
    {
        ERR_EXIT("Init daemon failed\n");
        //printf("Init daemon failed\n");
        return 1;
    }
    FILE* file = NULL;
    time_t t = 0;

    // 每隔1秒向daemon.log报告运行状态
    while (true)
    {
        sleep(1);
        file = fopen("daemon.log", "a+");
        if(file == NULL)
        {
            syslog(LOG_ERR, "open");
            ERR_EXIT("open failed\n");
        }
        if (file != NULL)
        {
            t = time(NULL);
            fprintf(file, "I am here at %s", asctime(localtime(&t)));
            fclose(file);
        }
    }

    /*
     * Proceed with the rest of the daemon.
     */
    exit(0);
}

【注】实例2主要在实例1的基础上添加了日志服务。

4.3.2使用daemon创建守护进程实例

对于C语言而言, glibc提供了daemon函数, 从而帮我们将程序转化成守护进程。

include <unistd.h>
int daemon(int nochdir, int noclose);

该函数有两个入参,分别控制一种行为,具体如下。

其中的nochdir,用来控制是否将当前工作目录切换到根目录。

  • 0:将当前工作目录切换到/。
  • 1:保持当前工作目录不变。

而noclose,用来控制是否将标准输入、标准输出和标准错误重定向到/dev/null。

  • 0:将标准输入、标准输出和标准错误重定向到/dev/null。
  • 1:保持标准输入、标准输出和标准错误不变。

一般情况下,这两个入参都要为0。

ret = daemon(0,0)

成功时, daemon函数返回0; 失败时, 返回-1, 并置errno。 因为daemon函数内部会调用fork函数和setsid函数, 所以出错时errno可以查看fork函数和setsid函数的出错情形。

glibc的daemon函数做的事情,和前面讨论的大体一致,但是做得并不彻底,没有执行第二次的fork。

【实例3】

/**
  ******************************************************************************
  * @file           Daemon.c
  * @author         BruceOu
  * @version        V3.0
  * @date           2019.04.18
  * @brief          守护进程的创建
  ******************************************************************************
  */
/**Includes*********************************************************************/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>

/**宏定义***********************************************************************/
#define ERR_EXIT(m) \
do\
{\
    perror(m);\
    exit(EXIT_FAILURE);\
}\
while (0);\

/**
  * @brief     主函数
  * @param     argc
               argv
  * @retval    None
  */
int main(int argc, char *argv[])
{
    time_t t;
    int fd;

    if(daemon(0,0) == -1)
    {
        ERR_EXIT("daemon error");
    }
    while(1)
    {
        fd = open("daemon.log",O_WRONLY|O_CREAT|O_APPEND,0644);
        if(fd == -1)
        {
            ERR_EXIT("open error");
        }
        t = time(0);
        char *buf = asctime(localtime(&t));

        write(fd,buf,strlen(buf));

        close(fd);
        sleep(5);

    }
    return 0;
}

gVzsw4.png

结果同上一节的实例1一样,也是只有root才能成功,普通用户执行时看不到错误信息。
现在让daemon(0,1),就是不关闭标准输入输出结果:

gVzyTJ.png

可以看到错误信息输出到终端。

现在让daemon(1,0),就是保持当前目录不变,结果如下:

gVzgYR.png

这次普通用户执行成功了,以为没有切换到/目录下,有权限。

我们可以利用我们刚才写的创建守护进程程序参照daemon()实现。还是使用上一节实例1。

【实例4】

/**
  ******************************************************************************
  * @file           Daemon.c
  * @author         BruceOu
  * @version        V4.1
  * @date           2019.04.18
  * @brief          守护进程的创建
  ******************************************************************************
  */
/**Includes*********************************************************************/
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/param.h>  // NOFILE
#include <sys/stat.h>   // umask
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <assert.h>
#include <fcntl.h>

/**类型定义*********************************************************************/
typedef enum{false,true}bool;

/**宏定义***********************************************************************/
#define ERR_EXIT(m) \
do\
{\
    perror(m);\
    exit(EXIT_FAILURE);\
}\
while (0);\

/**
  * @brief     初始化守护进程函数
  * @param    nochdir 
  *            noclose
  * @retval    bool 
  */
bool initDaemon(int nochdir, int noclose)
{
    // 【1】屏蔽一些有关控制终端操作的信号
    // 防止守护进程没有正常运转起来时,因控制终端受到干扰退出或挂起
    assert(signal(SIGINT, SIG_IGN) != SIG_ERR); // 终端中断
    assert(signal(SIGHUP, SIG_IGN) != SIG_ERR); // 连接挂断
    assert(signal(SIGQUIT, SIG_IGN) != SIG_ERR);// 终端退出
    assert(signal(SIGPIPE, SIG_IGN) != SIG_ERR);// 向无读进程的管道写数据
    assert(signal(SIGTTOU, SIG_IGN) != SIG_ERR);// 后台程序尝试写操作
    assert(signal(SIGTTIN, SIG_IGN) != SIG_ERR);// 后台程序尝试读操作
    assert(signal(SIGTERM, SIG_IGN) != SIG_ERR);// 终止

    // 【2】创建一个子进程,父进程退出
    int pid = fork();
    if (pid)
    {
        // 父进程退出
        exit(0);
    }
    else if (pid < 0)
    {
        ERR_EXIT("fork error");
    }

    // 子进程继续运行
    // 【3】在子进程中创建新的会话,setsid有三个作用
    // a.让进程摆脱原会话的控制
    // b.让进程摆脱原进程组的控制
    // c.让进程摆脱原控制终端的控制
    int ret = setsid();
    if (ret < 0)
    {
        ERR_EXIT("setsid error");
    }

    // 【4】改变当前工作目录
    // 进程活动时,其工作目录所在的文件系统不能卸下,一般将工作目录改变到根目录
    if(nochdir == 0)
    {
        ret = chdir("/");
        if (ret < 0)
        {
            ERR_EXIT("chdir error");
        }
    }
    // 【5】重新设置文件创建掩码
    // 进程从创建它的父进程那里继承了文件创建掩码,它可能修改守护进程所创建的文件的存取位
    // 所以将文件创建掩码清除
    umask(0);

    // 【6】再次执行fork,父进程退出,子进程继续
    // 进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端
    // 可以通过使进程不再成为会话组长来禁止进程重新打开控制终端
    pid = fork();
    if (pid)
    {
        // 结束第一个子进程
        exit(0);
    }
    else if (pid < 0)
    {
        ERR_EXIT("fork error");
    }

    // 第二个子进程继续运行
    // 【7】关闭打开的文件描述符
    // 进程从创建它的父进程那里继承了打开的文件描述符,如果不关闭,将会浪费系统资源,
    // 造成进程所在的文件系统无法卸下以及引起无法预料的错误
    if(noclose == 0)
    {
        int i;
        for (i = 0; i < NOFILE; ++i)
        {
            close(i);
        }

        //【8】重定向文件描述符0、1和2到/dev/null
        open("/dev/null", O_RDWR);
        dup(0);
        dup(0);
    }
    return true;
}

/**
  * @brief     主函数
  * @param     argc
               argv
  * @retval    None
  */
int main(int argc, char *argv[])
{
    // 初始化守护进程
    bool ret = initDaemon(0,0);
    if (!ret)
    {
        ERR_EXIT("Init daemon failed\n");
        //printf("Init daemon failed\n");
        return 1;
    }
    FILE* file = NULL;
    time_t t = 0;

    // 每隔5秒向daemon.log报告运行状态
    while (true)
    {
        sleep(5);
        file = fopen("daemon.log", "a+");
        if(file == NULL)
        {
            ERR_EXIT("open failed\n");
        }
        if (file != NULL)
        {
            t = time(NULL);
            fprintf(file, "I am here at %s", asctime(localtime(&t)));
            fclose(file);
        }
    }
    return 0;
}

我们运行两次看是否可以正常运行。

gVz4OO.png

我们发现还都可以正常运行,一般情况下是不允许有两个线程同时作为守护进程的,那么怎解决这个问题呢?接下来我们就来实现任一时刻只有一个守护进程在运行。

4.4守护进程创建进阶

4.4.1单实例守护进程(Single-Instance Daemons)

单实例守护进程也就是在任一时刻只运行守护进程的一个实例。这是为了避免运行多个实例时而引起的错误。例如,如果同时有多个cron守护进程实例在运行,那么每个实例都可能试图开始某个预定的操作,于是导致该操作被重复执行,这很可能会引起错误。

我们可以通过记录锁机制来保证任何时候系统中只有一个该守护进程的实例在运行。具体方法是:让守护进程创建一个文件并在整个文件上加一把独占性写锁(我们把文件称为守护进程的锁文件);根据记录锁机制,只允许创建一把这样的写锁,所以在此之后如试图再创建一把这样的写锁将会失败,以此向后续的守护进程实例指明当前已有一个实例在运行。在拥有这把写锁的守护进程实例终止时,这把写锁将被自动删除,无需我们自己来清理它。可以用下面的already_running函数来判断系统中是否已有该守护进程的实例在运行,返回值true表示有该守护进程实例在运行,否则无。

bool already_running(void)
{
    int fdLockFile;
    char szPid[32];
    struct flock fl;

    /* 打开锁文件 */
    fdLockFile = open(LOCKFILE, O_RDWR | O_CREAT, LOCKMODE);
    if (fdLockFile < 0)
    {
        syslog(LOG_ERR, "can't open %s: %s", LOCKFILE, strerror(errno));
        exit(EXIT_FAILURE);
    }

    /* 对整个锁文件加写锁 */
    fl.l_type = F_WRLCK;//记录锁类型:独占性写锁
    fl.l_whence = SEEK_SET;//加锁区域起点:距文件开始处l_start个字节
    fl.l_start = 0;
    fl.l_len = 0;//加锁区域终点:0表示加锁区域自起点开始直至文件最大可能偏移量为止,不管写入多少字节在文件末尾,都属于加锁范围

    if (fcntl(fdLockFile, F_SETLK, &fl) < 0)
    {
        if (EACCES == errno || EAGAIN == errno)//系统中已有该守护进程的实例在运行
        {
            close(fdLockFile);
            return true;
        }
        syslog(LOG_ERR, "can't lock %s: %s", LOCKFILE, strerror(errno));
        exit(EXIT_FAILURE);
    }
/* 清空锁文件,然后将当前守护进程pid写入锁文件 */
    ftruncate(fdLockFile, 0);
    sprintf(szPid, "%ld", (long)getpid());
    write(fdLockFile, szPid, strlen(szPid) + 1);

    return  false;
}

4.4.2守护进程惯例

一般的,需要遵循一下惯例:

1、守护进程的锁文件通常保存在/var/run/目录中(守护进程可能需要root用户权限才能在改目录中创建文件),并以name.pid命名,name为该守护进程或服务的名称。

2、守护进程的配置文件通常保存在/etc/目录中(守护进程可能需要root用户权限才能在改目录中创建文件),并以name.conf命名,name为该守护进程或服务的名称。通常,守护进程在启动时读取其配置文件,但在此之后就不再查看它了。如果我们修改了配置文件,就必须重启守护进程,才能使配置文件生效。为了避免此麻烦,有些守护进程捕获SIGHUP信号,当接收到该信号时,重新读取配置文件(因为守护进程已与控制终端脱离关系,控制终端SIGHUP信号对其已无用,所以重复利用它来作为重新读取配置文件信号)。

3、守护进程可用命令行启动,但它们通常由某一系统初始化脚本(/etc/rc或/etc/init.d/)启动。如果守护进程终止时,应该自动重新启动它,我们可以在/etc/inittab中为该守护进程包括_respawn;这样,守护进程终止时init会自动重新启动该守护进程。

4、若一个守护进程有一个配置文件,那么该守护进程就会读取该配置文件,但是之后再也不会查看该配置文件。若更改了配置文件,需要停止守护进程,重新读取配置文件,然后再重新启动守护进程。但是这样很麻烦,于是某些守护进程收到SIGNUP会重新读取配置文件。

4.4.3守护进程综合实例

守护进程实例先通过InitDaemon函数将普通进程改造成守护进程。然后,判断是否已有该守护进程的实例在运行,若有则退出运行;若无则先阻塞SIGHUP和SIGUSR1信号,然后为SIGHUP和SIGUSR1信号分别安装了信号处理函数,SIGHUP用于通知守护进程重新读取配置文件;SIGUSR1用于通知守护进程执行服务,也就是用echo打印从配置文件读到的信息。最后,进入一个死循环(守护进程主体):通过sigsuspend挂起守护进程等待信号发生,然后重新读取配置文件或执行服务,再挂起守护进程等待信号发生……。该实例通过sigprocmask、sigsuspend和sigaction的配合使用,避免了信号SIGHUP、SIGUSR1的丢失。该实例可以选择是否通过syslog输出信息,若用syslog输出信息,编译时加上-DUSE_SYSLOG。该实例也可以选择是否忽略子进程结束信号SIGCHLD,若忽略,编译时加上-DIGN_SIGCHLD。我们可以通过命令“kill -SIGHUP 守护进程pid”,通知守护进程重新读取配置文件;通过“kill -SIGUSR1 守护进程pid”,通知守护进程执行服务。

/**
  ******************************************************************************
  * @file           main.c
  * @author         BruceOu
  * @version        V1.0
  * @date           2019.01.24
  * @brief          
  ******************************************************************************
  */
/**Includes*********************************************************************/
#include <stdio.h>         /* 标准输入输出定义 */
#include <stdlib.h>        /* 标准函数库定义 */
#include <unistd.h>        /* Unix 标准函数定义 */
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <fcntl.h>        /* 文件控制定义 */
#include <errno.h>        /* 错误号定义 */
#include <signal.h>     /* 信号定义 */
#include <time.h>        /* 定时器定义 */
#include <stdarg.h>        /* 可变参数定义 */
#include <syslog.h>        /* syslog定义 */
#include <string.h>
#include <fcntl.h>

#define true    1
#define false    0

typedef unsigned char BYTE;
typedef BYTE bool;
typedef BYTE byte;

#define MAX_BUF_SIZE 1024

#define CONFIG_FILE   "/etc/daemon.conf"
#define LOG_FILE     "/tmp/daemon.log"
#define LOCK_FILE    "/var/run/daemon.pid"
#define LOCK_FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)

static volatile sig_atomic_t g_nUpdateParameter = 1;
static volatile sig_atomic_t g_nServer = 0;
//static volatile int g_nUpdateParameter = 1;
//static volatile int g_nServer = 0;

/*
 * 功能: 写日志
 */
void vWriteLog(int nPriority, const char *fmt, va_list va)
{
#ifdef USE_SYSLOG
    vsyslog(LOG_DAEMON | nPriority, fmt, va);
#else
    FILE *stream;
    if (nPriority & LOG_ERR)
        stream = stderr;
    else
        stream = stdout;
    vfprintf(stream, fmt, va);
    fflush(stream);
#endif
}
void WriteLog(int nPriority, const char *fmt, ...)
{
    va_list va;

    va_start(va, fmt);
    vWriteLog(nPriority, fmt, va);
    va_end(va);
}

/*
 * 功能: 写错误日志,用法类似perror
 */
void ErrorLog(const char *str)
{
    WriteLog(LOG_ERR, "%s: %s\n", str, strerror(errno));
}
/*
 * 功能: 写错误日志,用法类似于printf
 */
void ErrorLogFmt(const char *fmt, ...)
{
    va_list va;

    va_start(va, fmt);
    vWriteLog(LOG_ERR, fmt, va);
    va_end(va);
}

/*
 * 功能: 写日志,用法类似printf
 */
void InfoLog(const char *fmt, ...)
{
    va_list va;

    va_start(va, fmt);
    vWriteLog(LOG_INFO, fmt, va);
    va_end(va);
}

/*
 * 功能: 重定向标准输入输出
 */
void RedirectStdIO(char *szInFile, char *szOutFile, char *szErrFile)
{
    int fd;

    if (NULL != szInFile)
    {
        fd = open(szInFile, O_RDONLY | O_CREAT, 0666);
        if (fd > 0)
        {
            // 标准输入重定向
            if (dup2(fd, STDIN_FILENO) < 0)
            {
                ErrorLog("RedirectStdIO dup2 in");
                exit(EXIT_FAILURE);
            }

            close(fd);
        }
        else
            ErrorLogFmt("RedirectStdIO open %s: %s\n", szInFile, strerror(errno));
    }

    if (NULL != szOutFile)
    {
        fd = open(szOutFile, O_WRONLY | O_CREAT | O_APPEND /*| O_TRUNC*/, 0666);
        if (fd > 0)
        {
            // 标准输出重定向 
            if (dup2(fd, STDOUT_FILENO) < 0)
            {
                ErrorLog("RedirectStdIO dup2 out");
                exit(EXIT_FAILURE);
            }

            close(fd);
        }
        else
            ErrorLogFmt("RedirectStdIO open %s: %s\n", szOutFile, strerror(errno));
    }

    if (NULL != szErrFile)
    {
        fd = open(szErrFile, O_WRONLY | O_CREAT | O_APPEND /*| O_TRUNC*/, 0666);
        if (fd > 0)
        {
            // 标准错误重定向 
            if (dup2(fd, STDERR_FILENO) < 0)
            {
                ErrorLog("RedirectIO dup2 error");
                exit(EXIT_FAILURE);
            }

            close(fd);
        }
        else
            ErrorLogFmt("RedirectStdIO open %s: %s\n", szErrFile, strerror(errno));
    }
}

/*
 * 功能: 读取配置文件SIGHUP信号处理函数
 */
void UpdateHandler(int nSigNum)
{
    g_nUpdateParameter = 1;
}

/*
 * 功能: 折行服务SIG_USR1信号处理函数
 */
void ServerHandler(int nSigNum)
{
    g_nServer = 1;
}

/*
 * 功能:确保任一时刻系统中只有一个该守护进程实例在运行
 */
bool AlreadyRunning()
{
    int fdLockFile;
    char szPid[32];
    struct flock fl;

    /* 打开锁文件 */
    fdLockFile = open(LOCK_FILE, O_RDWR | O_CREAT, LOCK_FILE_MODE);
    if (fdLockFile < 0)
    {
        ErrorLog("AlreadyRunning open");
        exit(EXIT_FAILURE);
    }

    /* 对整个锁文件加写锁 */
    fl.l_type = F_WRLCK;    //记录锁类型:独占性写锁
    fl.l_whence = SEEK_SET;    //加锁区域起点:距文件开始处l_start个字节
    fl.l_start = 0;            
    fl.l_len = 0;            //加锁区域终点:0表示加锁区域自起点开始直至文件最大可能偏移量为止,不管写入多少字节在文件末尾,都属于加锁范围
    if (fcntl(fdLockFile, F_SETLK, &fl) < 0)
    {
        if (EACCES == errno || EAGAIN == errno)    //系统中已有该守护进程的实例在运行
        {
            close(fdLockFile);
            return true;
        }

        ErrorLog("AlreadyRunning fcntl");
        exit(EXIT_FAILURE);
    }

    /* 清空锁文件,然后将当前守护进程pid写入锁文件 */
    ftruncate(fdLockFile, 0);
    sprintf(szPid, "%ld", (long)getpid());
    write(fdLockFile, szPid, strlen(szPid) + 1);

    return false;
}

/*
 * 功能:设置信号nSigNum的处理函数为handler,在调用该信号处理函数前.若handler不为SIG_DEF或SIG_IGN,则系统会将该信号添加到信号屏蔽字中;信号处理函数返回后,信号屏蔽字恢复到原先值.这样,可保证在处理指定信号时,如果该信号再次发生,那么它会被阻塞到上一信号处理结束为止.不过,要是此时信号发生了多次,在对该信号解除阻塞后,也只会调用一次信号处理函数
 */
typedef void (*sighandler)(int);
sighandler MySignal(int nSigNum, sighandler handler)
//void ( *Signal(int nSigNum, void (*handler)(int)) )(int)        
{
    struct sigaction saNew, saOld;

    saNew.sa_handler = handler;
    sigemptyset(&saNew.sa_mask);
    if (SIG_DFL != handler && SIG_IGN != handler)
        sigaddset(&saNew.sa_mask, nSigNum);

    saNew.sa_flags = 0;
    if (SIGALRM == nSigNum)
    {
        //不重启该信号中断的系统调用
#ifdef SA_INTERRUPT
        saNew.sa_flags |= SA_INTERRUPT;
#endif
    }
    else
    {
        //重启该信号中断的系统调用
#ifdef SA_RESTART
        saNew.sa_flags |= SA_RESTART;
#endif
    }

    if (sigaction(nSigNum, &saNew, &saOld) < 0)
        return SIG_ERR;

    return saOld.sa_handler;
}

/*
 * 功能: 将普通进程改造成守护进程
 */
void InitDaemon()
{
    pid_t pid;
    int fd, fdTableSize;

    /* 1、屏蔽控制终端操作信号
     */
    MySignal(SIGTTOU, SIG_IGN); 
    MySignal(SIGTTIN, SIG_IGN); 
    MySignal(SIGTSTP, SIG_IGN); 
    MySignal(SIGHUP, SIG_IGN);

    /* 2、忽略子进程结束信号
     */
#ifdef IGN_SIGCHLD
    signal(SIGCHLD, SIG_IGN);    //忽略子进程结束信号,避免僵死进程产生
#endif

    /* 3、使守护进程后台运行
     * 父进程直接退出,子进程继续运行(让守护进程在子进程中后台运行)
     */
    pid = fork();
    if (pid > 0)        //父进程终止运行;子进程过继给init进程,其退出状态也由init进程处理,避免了产生僵死进程
        exit(EXIT_SUCCESS);
    else if (pid < 0)
    {
        ErrorLog("InitDaemon fork(parent)");
        exit(EXIT_FAILURE);
    }

    /* 4、脱离控制终端,登录会话和进程组
     * 调用setsid()使子进程成为会话组长
     */
    setsid();

    /* 5、禁止进程重新打开控制终端
     * 通过使守护进程不再成为会话组长来禁止进程重新打开控制终端
     */
    pid = fork();
    if (pid > 0)        //子进程终止运行;孙进程过继给init进程,其退出状态也由init进程处理,避免了产生僵死进程
        exit(EXIT_SUCCESS);
    else if (pid < 0)
    {
        ErrorLog("InitDaemon fork(child)");
        exit(EXIT_FAILURE);
    }

    /* 6、重设文件创建掩模 
     */
    umask(0);

    /* 7、关闭打开的文件描述符 
     */
    RedirectStdIO("/dev/null", LOG_FILE, LOG_FILE);    //重定向标准输入输出
    fdTableSize = getdtablesize();
    for (fd=3; fd<fdTableSize; fd++)
        close(fd);

    /* 8、改变当前工作目录
     */
    chdir("/tmp");    
}

/*
 * 功能: 读取守护进程的配置文件,并将获取到的信息保存在szParameter中
 */
void ReadConfigFile(char *szParameter)
{
    FILE *stream;
    int nRet;

    InfoLog("------ ReadConfigFile ------\n");
    stream = fopen(CONFIG_FILE, "r");
    if (NULL != stream)
    {
        nRet = fread(szParameter, sizeof(char), MAX_BUF_SIZE, stream);
        if (nRet >= 0)
            szParameter[nRet - 1] = '\0';
        fclose(stream);
        InfoLog("ReadConfigFile sucesss!\n");
    }
    else
        ErrorLogFmt("ReadConfigFile fopen %s: %s\n", CONFIG_FILE, strerror(errno));
}

/*
 * 功能: 执行守护进程的服务,也就是将szParameter用echo打印出来
 */
void Server(char *szParameter)
{
    int nStatus;
    pid_t pid;

    InfoLog("------ Server ------\n"); 

    pid = vfork();            //生成子进程
#ifdef IGN_SIGCHLD
    InfoLog("ignore child SIGCHLD signal!\n");
    if (0 == pid)            //子进程
    {
        if (execlp("echo", "echo", szParameter, NULL) < 0)
        {
            ErrorLog("Server execlp");
            exit(EXIT_FAILURE);
        }
    }
    else if (pid < 0)
    { 
        ErrorLog("Server vfork(parent)"); 
    } 
#else
    if (pid > 0)            //父进程
    {
        waitpid(pid, &nStatus, 0);    //等待子进程结束,否则子进程会成为僵死进程,一直存在,即便子进程已结束执行
    }
    else if (0 == pid)        //子进程
    {
        pid = vfork();        //生成孙进程
        if (pid > 0) 
        {
            exit(EXIT_SUCCESS);        //子进程退出,孙进程过继给init进程,其退出状态也由init进程处理,与原有父进程无关
        }
        else if (0 == pid)    //孙进程
        {
            if (execlp("echo", "echo", szParameter, NULL) < 0)
            {
                ErrorLog("Server execlp");
                exit(EXIT_FAILURE);
            }
        }
        else
        { 
            ErrorLog("Server vfork(child)"); 
        } 
    }
    else
    { 
        ErrorLog("Server vfork(parent)"); 
    } 
#endif
}

int main()
{
    time_t t;
    sigset_t sigNewMask, sigOldMask;
    char szParameter[MAX_BUF_SIZE];

    //将普通进程改造成守护进程
    InitDaemon();
    if (AlreadyRunning())    //若系统中已有该守护进程的实例在运行,则退出
    {
        ErrorLogFmt("Daemon already running!\n");
        exit(EXIT_FAILURE);
    }

    //阻塞SIGHUP信号和SIGUSR1信号
    sigemptyset(&sigNewMask);
    sigaddset(&sigNewMask, SIGHUP);
    sigaddset(&sigNewMask, SIGUSR1);
    if(sigprocmask(SIG_BLOCK, &sigNewMask, &sigOldMask) < 0)
    {
        ErrorLog("main sigprocmask");
        exit(EXIT_FAILURE);
    }

    //为SIGHUP信号和SIGUSR1信号添加信号处理函数
    MySignal(SIGHUP, UpdateHandler);
    MySignal(SIGUSR1, ServerHandler);

    t = time(NULL);
    InfoLog("Daemon %d start at %s", getpid(), ctime(&t));

    //读取守护进程配置文件
    ReadConfigFile(szParameter);
    while(1)
    {
        sigsuspend(&sigOldMask);//将进程的信号屏蔽字暂时替代为sigOldMask并挂起进程,直到捕捉到一个信号并从其信号处理函数返回,sigsuspend才返回并将信号屏蔽字恢复为调用它之前的值;若捕捉到的是终止进程信号,sigsuspend不返回,进程直接终止
        if (1 == g_nUpdateParameter)    //读取配置文件
        {
            ReadConfigFile(szParameter);
            g_nUpdateParameter = 0;
        }

        if (1 == g_nServer)    //执行服务
        {
            Server(szParameter);
            g_nServer = 0;
        }
   }
   return 0;
}

1、不使用syslog记录信息,也不忽略子进程结束信息SIGCHLD(超级用户运行)

# gcc -Wall test.c -o test
# ./test 
# cat /tmp/daemon.log 
Daemon 3472 start at Wed May 11 11:45:56 2019
------ ReadConfigFile ------
ReadConfigFile fopen /etc/daemon.conf: No such file or directory

# echo 123 > /etc/daemon.conf              
# cat /etc/daemon.conf 
123
# pgrep test
3472
# kill -SIGHUP 3472     #让守护进程重新读取配置文件
# cat /tmp/daemon.log 
Daemon 3472 start at Wed May 11 11:45:56 2019
------ ReadConfigFile ------
ReadConfigFile fopen /etc/daemon.conf: No such file or directory
------ ReadConfigFile ------
ReadConfigFile sucesss!
# kill -SIGUSR1 3472    #让守护进程运行服务,也就是用echo打印先前从配置文件中读取到的信息
# cat /tmp/daemon.log 
Daemon 3472 start at Wed May 11 11:45:56 2019
------ ReadConfigFile ------
ReadConfigFile fopen /etc/daemon.conf: No such file or directory
------ ReadConfigFile ------
ReadConfigFile sucesss!
------ Server ------
123
# ./test            #再次运行守护进程
# cat /tmp/daemon.log 
Daemon 3472 start at Wed May 11 11:45:56 2011
------ ReadConfigFile ------
ReadConfigFile fopen /etc/daemon.conf: No such file or directory
------ ReadConfigFile ------
ReadConfigFile sucesss!
------ Server ------
123
Daemon already running!                     #提示已有守护进程实例在运行

2、使用syslog记录信息,同时忽略子进程结束信息SIGCHLD

# gcc -Wall -DUSE_SYSLOG -DIGN_SIGCHLD test.c -o test
test.c: In function ‘Server’:
test.c:355: warning: unused variable ‘nStatus’
# ./test
# echo 345 > /etc/daemon.conf 
# cat /etc/daemon.conf
345
# pgrep test 
2548
# kill -SIGHUP 2548
# kill -SIGUSR1 2548
# ./test
# tail /var/log/daemon.log
May 11 16:21:42 ubuntu test: Daemon 2548 start at Wed May 11 16:21:42 2019
May 11 16:21:42 ubuntu test: ------ ReadConfigFile ------
May 11 16:21:42 ubuntu test: ReadConfigFile sucesss!
May 11 16:22:27 ubuntu test: ------ ReadConfigFile ------
May 11 16:22:27 ubuntu test: ReadConfigFile sucesss!
May 11 16:22:39 ubuntu test: ------ Server ------
May 11 16:22:39 ubuntu test: ignore child SIGCHLD signal!
May 11 16:22:39 ubuntu test: ignore child SIGCHLD signal!
May 11 16:22:49 ubuntu test: Daemon already running!

# cat /tmp/daemon.log 
345

注意,因为子进程echo继承了父进程(守护进程)的文件描述符,所以echo打印的信息会被输出到/tmp/daemon.log ,而不是终端(屏幕)上。我们调用syslog是设置了LOG_DAEMON,所以日志信息保存在/var/log/daemon.log(一开始以为是保存在/var/log/messages,调了半天程序都没有输出);若设为LOG_USER,日志则保存在/var/log/user.log

如果想要此程序在系统启动时自动运行,可以在/etc/rc.d/rc.local里面用su命令加上一行,比如:

su - ouxiaolong -c "/bin/test"

这个命令将以ouxiaolong用户身份运行/bin/test程序

查看进程:ps -ef
从输出可以发现test守护进程的各种特征满足上面的要求。

$ ps -ef
UID            PID     PPID  C STIME TTY          TIME CMD
ouxiaolong     5209     1  0 15:34    ?          00:00:00 ./test

Related posts

Leave a Comment