一、编程实现1
  • 在编写守护进程程序时需遵循一些基本规则,以防止产生不必要的交互作用

第一步

  • 首先调用umask将文件模式创建屏蔽字设置为一个已知值(通常为0)
  • 由继承得来的文件模式创建屏蔽字可能会被设置为拒绝某些权限。如果守护进程要创建文件,那么它可能要设置特定的权限。例如:若守护进程要创建组可读、组可写的文件,继承的文件模式创建屏蔽字可能会屏蔽上述两种权限中的一种,而使其无法发挥作用
  • 另一方面,如果守护进程调用的库函数创建了文件,那么将文件模式创建屏蔽字设置为一个限制性更强的值(如007)可能会更明智,因为库函数可能不允许调用者通过一个显式的函数参数来设置权限

第二步

  • 调用fork,然后使父进程exit
  • 这样做实现了下面几点:
    • 第一,如果该守护进程是作为一条简单shell命令启动的,那么父进程终止会让shell认为这条命令已经执行完毕
    •  第二,虽然子进程继承了父进程的进程组ID,但获得了一个新的进程ID,这就保证了子进程不是一个进程组的组长进程。这是下面将要进行的setsid调用的先决条件

第三步

  • 调用setsid创建一个新会话
  • 于是执行9.5节中列出的3个步骤,使调用进程:
    • (a) 成为新会话的首进程
    • (b)成为一个新进程组的组长进程
    • (c)没有控制终端

APUE编程:114---进程管理(守护进程的编程实现)_子进程

第四步

  • 将当前工作目录更改为根目录
  • 从父进程继承过来的当前工作目录可能在一个挂载的文件系统中。因为守护进程通常在系统再引导之前是一直存在的,所以如果守护进程的当前工作目录在一个挂载文件系统中,那么该文件系统就不能被卸载
  • 或者,某些守护进程可能会把当前工作目录更改到某个指定位置,并在此位置做它们的全部工作。 例如,行式打印机假脱机守护进程就可能将其工作目录更改到它们的spool目录上

第五步

  • 关闭不再需要的文件描述符
  • 这使守护进程就不再持有从其父进程继承来的任何文件描述符(父进程可能是shell进程,或某个其他进程)
  • 可以使用open_max函数或getrlimit函数来判定最高文件描述符值,并关闭直到该值的所有描述符

第六步

  • 某些守护进程打开/dev/null使其具有文件描述符0、1、2,这样,任何一个试图读标准输入、写标准输出或标准错误的库例程都不会产生任何效果。因为守护进程并不与终端设备相关联,所以其输出无处显示,也无处从交互式用户那里接受输入
  • 即使守护进程是从交互式会话启动的,但是守护进程是在后台运行的,所以登录会话的终止并不影响守护进程
  • 如果其他用户在同一终端设备上登录,我们不希望在该终端上见到守护进程的输出,用户也不期望他们在终端上的输入被守护进程读取
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <syslog.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/types.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)
		printf("%s: can't get file limit\n", cmd);

	if ((pid = fork()) < 0)
		printf("%s: can't fork\n", cmd);
	else if (pid != 0)//父进程终止,子进程的PPID开始变为1
		exit(0);

    /*为什么要关闭父进程:因为非进程组组长才可以setsid开启新会话
    子进程创建一个新会话,新会话没有控制终端(并且子进程成为新会话的组长)*/
	setsid();

	sa.sa_handler = SIG_IGN;//忽略信号
	sigemptyset(&sa.sa_mask);
	sa.sa_flags = 0;
    //忽略ISGHUP信号(会话首进程终止,产生此信号,默认动作是终止进程)
	if (sigaction(SIGHUP, &sa, NULL) < 0)
		printf("%s: can't ignore SIGHUP\n", cmd);
    
	if ((pid = fork()) < 0)
		printf("%s: can't fork\n", cmd);
	else if (pid != 0)第一个子进程终止
		exit(0);

    //子子进程开始(守护进程空间)
    //为什么要再次fork:为了保证守护进程不是会话首进程,防止其取得控制终端
	if (chdir("/") < 0)
		printf("%s: can't change directory to /\n", cmd);

	if (rl.rlim_max == RLIM_INFINITY)
		rl.rlim_max = 1024;
	for (i = 0; i < rl.rlim_max; i++)
		close(i);//关闭所有的文件描述符

    /*因为上面关闭了所有文件描述符,
    所以之后创建的fd0、fd1、fd2不出意外的话,应该是0、1、2*/
    //为什么打开/dev/null:打开/dev/null之后,标准输入、输出、错误输出都会重定向到此处
	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);
	}
}

int main()
{
    daemonize("MyDaemon");
    while(1){
        sleep(1);
    }
}

运行结果

APUE编程:114---进程管理(守护进程的编程实现)_守护进程的编程实现_02

  • 显示结果依次是:UID名称、PID、PPID、PGID、SID、C、STIME、TTY、TIME、CMD
  • 通过ps命令可以看到,其PGID实2751(也就是第一个fork的子进程),但是该子进程exit了,所以意味着,守护进程在一个孤儿进程组中,它不是会话首进程,因此没有机会被分配到一个控制终端
  • 这一结果是在daemonize函数中执行第二个fork造成的,可以看到,守护进程已经被正确的初始化了
二、编程实现2

fork

APUE编程:114---进程管理(守护进程的编程实现)_#include_03

setsid

APUE编程:114---进程管理(守护进程的编程实现)_守护进程_04

忽略SIGHUP信号并在此fork

APUE编程:114---进程管理(守护进程的编程实现)_子进程_05

改变工作目录

APUE编程:114---进程管理(守护进程的编程实现)_子进程_06

将stdin、stdout、stderr重定向到/dev/null

APUE编程:114---进程管理(守护进程的编程实现)_守护进程的编程实现_07

使用syslogd处理错误

APUE编程:114---进程管理(守护进程的编程实现)_#include_08

#include	<syslog.h>

#define	MAXFD	64

int
daemon_init(const char *pname, int facility)
{
	int		i;
	pid_t	pid;

	if ( (pid = Fork()) < 0)
		return (-1);
	else if (pid)
		_exit(0);			/* parent terminates */

	/* child 1 continues... */

	if (setsid() < 0)			/* become session leader */
		return (-1);

	Signal(SIGHUP, SIG_IGN);
	if ( (pid = Fork()) < 0)
		return (-1);
	else if (pid)
		_exit(0);			/* child 1 terminates */

	/* child 2 continues... */

	chdir("/");				/* change working directory */

	/* close off file descriptors */
	for (i = 0; i < MAXFD; i++)
		close(i);

	/* redirect stdin, stdout, and stderr to /dev/null */
	open("/dev/null", O_RDONLY);
	open("/dev/null", O_RDWR);
	open("/dev/null", O_RDWR);

	openlog(pname, LOG_PID, facility);

	return (0);				/* success */
}
三、精简版的守护进程案例
  • 相对于上面的案例,缩短了一些
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>

void creat_daemon()
{
    int i,fd;
    pid_t id;
    struct sigaction sa;
    umask(0);//第一步:调用umask将文件模式创建屏蔽字设置为0.
    if((id = fork()) < 0){
        printf("fork error!\n");
        return;
    }else if(id != 0){
        exit(0);//第二步:调用fork,父进程退出。保证子进程不是话首进程,从而保证后续不和其他终端关联。
    }

    setsid();//第三步:设置新会话

    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;

    if(sigaction(SIGCHLD,&sa,NULL) < 0){
        //注册子进程退出忽略信号
        return;
    }

    if((id = fork()) < 0){//再次创建子进程
        printf("fork 2 error!\n");
        return;
    }else if(id != 0){
         exit(0);
    }
	//守护进程空间
    if(chdir("/") < 0){
        //第四步:更改工作目录到根目录
        printf("child dir error\n");
        return;
    }
    close(0);
    fd = open("/dev/null",O_RDWR);//关闭标准输入,重定向所有标准(输入输出错误)到/dev/NULL
    dup2(fd,1);
    dup2(fd,2);
}

int main()
{
    creat_daemon();
    while(1){
        sleep(1);
    }
}