《UNUX环境高级编程》(13)守护进程

1、引言

  • 守护进程(daemon)是生存期长的一种进程。它们常常在系统引导装入时启动,仅在系统关闭时才终止。因为它们没有控制终端,所以说它们是在后台运行的

2、守护进程的特征

  • Linux的大多数服务就是用守护进程实现的。这些守护进程名通常以d结尾,如inetd提供网络服务,sshd提供ssh登录服务,httpd提供web服务等待。
    • 大多数守护进程都以超级用户权限运行
    • 所有守护进程都没有控制终端。用户层守护进程缺少控制终端可能是守护进程调用了setsid的结果(setsid会断开与控制终端的联系)。
    • 大多数用户层守护进程都是进程组的组长进程以及会话的首进程,而且是这些进程组和会话中的唯一进程
    • 用户层进程的父进程是init(1)进程。

3、编程规则

  • 编写守护进程程序时需要遵循一些基本规则,以防止产生不必要的交互作用。
  • 实例:下面函数可由一个想要初始化为守护进程的程序调用。
    #include "apue.h"
    #include 
    #include 
    #include 
    
    void
    daemonize(const char *cmd)
    {
    	int					i, fd0, fd1, fd2;
    	pid_t				pid;
    	struct rlimit		rl;
    	struct sigaction	sa;
    
    	/*(1)清空文件模式创建屏蔽字*/
    	umask(0);
    
    	/*
    	 * 获取最大文件描述符
    	 */
    	if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
    		err_quit("%s: can't get file limit", cmd);
    
    	/*(2)调用fork,然后使父进程exit退出*/
    	if ((pid = fork()) < 0)
    		err_quit("%s: can't fork", cmd);
    	else if (pid != 0) /* parent */
    		exit(0);
    	/*(3)调用setsid创建一个新会话*/
    	setsid();
    
    	/*(3`)再次调用fork,终止父进程,继续使用子进程中的守护进程。
    	  这就保证了该守护进程不是会话首进程,可以防止它取得控制终端*/
    	sa.sa_handler = SIG_IGN;
    	sigemptyset(&sa.sa_mask);
    	sa.sa_flags = 0;
    	if (sigaction(SIGHUP, &sa, NULL) < 0)//忽略SIGHUP信号,见9.10节孤儿进程组
    		err_quit("%s: can't ignore SIGHUP", cmd);
    	if ((pid = fork()) < 0)
    		err_quit("%s: can't fork", cmd);
    	else if (pid != 0) /* parent */
    		exit(0);
    
    	/*(4)将当前工作目录更改为根目录*/
    	if (chdir("/") < 0)
    		err_quit("%s: can't change directory to /", cmd);
    
    	/*(5)关闭所有不再需要的文件描述符*/
    	if (rl.rlim_max == RLIM_INFINITY)
    		rl.rlim_max = 1024;
    	for (i = 0; i < rl.rlim_max; i++)
    		close(i);
    
    	/*(6)将文件描述符0/1/2指向/dev/null*/
    	fd0 = open("/dev/null", O_RDWR);
    	fd1 = dup(0);
    	fd2 = dup(0);
    
    	/*初始化log文件*/
    	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);
    	}
    }
    
    • 下面是关于守护进程的编程规则
    • (1)调用umask将文件模式创建屏蔽字设置为一个已知值(通常是0),见4.8节相关内容。由继承(如fork)得来的文件模式创建屏蔽字可能会被设置为拒绝某些权限。如果守护进程要创建文件,那么它可能要设置特定的权限。
    • (2)调用fork,然后使父进程exit退出,这样会实现以下几点:
      • 如果该守护进程是shell命令启动的,那么父进程终止会让shell认为这条命令已经执行完毕。
      • 虽然子进程继承了父进程的进程组ID,但是获得了一个新的进程ID,因此子进程不是该进程组的组长进程,这是接下来进行setsid调用的先决条件。
    • (3)调用setsid创建一个新会话,见9.5节相关内容,这样会使调用进程:
      • 成为新会话的首进程
      • 成为新进程组的组长进程
      • 没有控制终端
        >> 有些建议此时再次调用fork,终止父进程,继续使用子进程中的守护进程,这就保证了该守护进程不是会话首进程,可以防止它取得控制终端
        >> 为了避免取得控制终端的另一种方法是:当用open函数打开终端设备时,设置O_NOCTTY标志。
    • (4)将当前工作目录更改为根目录
      • 从父进程处继承过来的当前工作目录可能在一个挂载的文件系统处。因为是守护进程通常在系统再引导前一直存在,所以如果守护进程的当前工作目录在一个挂载文件系统中,那么该文件系统就不能被卸载。
    • (5)关闭不再需要的文件描述符。这使得守护进程不再持有从其父进程继承来的任何文件描述符:可以通过getrlimit函数判定最高文件描述符值,并关闭直到该值的所有描述符。
    • (6)某些守护进程打开/dev/null使文件描述符0/1/2指向该文件。这样使得任何一个试图读标准输入、写标准输出或标准错误的例程都不会产生任何效果。因为守护进程不与终端设备关联,因此其输出无处显式,也无处从交互式用户那里接收输入。
      • /dev/null文件:
        • 一个字符设备文件。称为空设备,它丢弃一切写入其中的数据(但报告写入操作成功),读取它则会立即得到一个EOF
        • /dev/null 被称为位桶(bit bucket)或者黑洞(black hole)。空设备通常被用于丢弃不需要的输出流,或作为用于输入流的空文件。这些操作通常由重定向完成。
      • /dev/zero文件:
        • 一个字符设备文件。当你读它的时候,它会提供无限的空字符(NULL, 即0x00)。写入/dev/zero的内容会丢失不见。
      • /dev/random/dev/urandom文件:
        • 字符设备文件。随机数设备,提供不间断的随机字节流。二者的区别是/dev/random产生随机数据依赖系统中断,当系统中断不足时,/dev/random设备会“挂起”,因而产生数据速度较慢,但随机性好;/dev/urandom不依赖系统中断,数据产生速度快,但随机性较低。

4、出错记录

  • syslog设施
    • 因为守护进程不应该有控制终端,所以不能只是将出错消息写到标准错误上。我们不希望所有守护进程都写到控制台设备上,也不希望每个守护进程将它自己的出错消息写到一个单独的文件中。
    • 因此需要关心哪一个守护进程写到哪一个记录文件中,可以通过一个集中的 守护进程出错记录设施 来进行这种管理操作
    • 大多数守护进程使用syslog设施,其组织结构如下:
      《UNUX环境高级编程》(13)守护进程_第1张图片
    • 有以下3种产生日志消息的方法:
      • 大多数用户守护进程调用syslog函数产生日志消息,该函数将消息发送至UNIX域数据报套接字/dev/log/dev/log是一个套接字类型文件
      • 无论一个用户进程在此主机上,还是在通过TCP/IP网络连接到此主机的其他主机上,都可以将日志消息发送到UDP端口514
      • 内核例程调用log函数产生日志消息
    • 其中syslogd是一个守护进程。不同的进程(client)都可以将log 输送给syslogdserver),由syslogd 集中收集。syslogd可以将log保存到本地,也可以发送到共享内存或远程服务器
    • syslogd守护进程读取所有3种格式的日志消息。syslogd在启动时读一个配置文件/etc/syslog.conf,该文件决定了不同种类消息应该送往何处。如一个紧急消息可在控制台上打印,而警告信息记录到一个文件中。
  • syslog设施的接口函数
    void openlog(const char *ident, int option, int facility);
    void syslog(int priority, const char *format, ...);
    void closelog(void);
    int setlogmask(int mask);
    
    • 调用openlog是可选择的。如果不调用openlog,则在第一次调用syslog时,自动调用openlog

    • closelog也是可选的,因为它只是关闭曾被用于与syslogd守护进程进行通信的描述符

    • openlog函数

      • ident参数:此参数是一个字符串,将被加至每一则日志消息中。(类比perror函数)
      • option参数:指定的标志用来控制openlog()操作和syslog()的后续调用。他的值为下列值或运算的结果:
        • LOG_CONS:若日志消息不能通过UNIX域数据报套接字送至syslogd守护进程,则将该消息写至控制台
        • LOG_NDELAY:立即打开至syslogd守护进程的UNIX域数据包套接字,不要等到第一条消息已经被记录时再打开。(通常在记录第一条消息前不打开该套接字文件)
        • LOG_NOWAIT:在记录日志信息时,不等待可能的子进程的创建
        • LOG_ODELAY:在第一条消息被记录之前延迟打开至syslogd守护进程的连接
        • LOG_PERROR:除了将日志消息发送给syslogd以外,还将它写至标准错误stderr
        • LOG_PID:每条消息都包含进程PID
      • facility参数:指定记录消息程序的类型。syslogd通过指定的配置文件,将以不同的方式来处理来自不同设施的消息(即这个要与syslogd守护进程的配置文件对应,日志信息会写入syslog.conf文件的指定位置)。
      • 如果不调用openlog或者该参数值为0,则在调用syslog时,可以将facility参数作为syslogpriority参数的一部分。
        《UNUX环境高级编程》(13)守护进程_第2张图片
    • syslog函数:产生一个日志消息

      • priority参数:其priority参数是openlogfacility参数和level的组合
        《UNUX环境高级编程》(13)守护进程_第3张图片
      • format参数:其中%m字符被替换成errno值对应的出错消息字符串(strerror
    • setlogmask函数

      • 设置进程的记录优先级屏蔽字,并返回之前的屏蔽字
      • mask参数:日志优先级掩码,在该掩码中的消息才会被真正记录。该掩码是level中各个常量的按位或
  • 实例:在一个行式打印机假脱机守护进程中,可能有下列调用序列
    openlog("lpd",LOG_PID,LOG_LPR);
    syslog(LOG_ERR,"open error for %s:%m",filename);
    
    • 该程序将ident字符串设置为程序名"lpd"LOG_PID参数指定该进程ID要始终被打印,并且将系统默认的facility设定为行式打印机系统(LOG_LPR参数)。对syslog制定一个出错条件(LOG_ERR)和一个消息字符串。
    • 不调用openlog,该程序可以有第二个调用形式
      syslog(LOG_LPR|LOG_ERR,"open error for %s:%m",filename);
      
      • 其中,将priority参数指定为levelfacility的组合。

5、单实例守护进程

  • 有时候需要在任意时刻只运行该守护进程的一个副本。如果同时运行该守护进程的多个实例,则会出现错误。

  • 可以通过文件和记录锁机制,该方法保证一个守护进程只有一个副本在运行(见14.3节)。如果每一个守护进程创建一个有固定名字的文件,并在该文件的整体上加一个写锁,那么只允许创建一把这样的写锁。在此之后创建写锁的尝试都会失败,这向后续守护进程副本指明已有一个副本正在运行。

  • 文件和记录锁提供了一种方便的互斥机制。如果守护进程在一个文件的整体上得到一把写锁,那么在该是守护进程终止时,这把锁将被自动删除。这就简化了复原所需的操作。

  • 实例:以下程序说明了如何使用文件和记录锁来保证只运行守护进程的一个副本

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #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_CREAT, LOCKMODE);
    	if (fd < 0) {
    		syslog(LOG_ERR, "can't open %s: %s", LOCKFILE, strerror(errno));
    		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);
    	/*得到进程id*/
    	sprintf(buf, "%ld", (long)getpid());
    	/*将进程ID写入该文件*/
    	write(fd, buf, strlen(buf)+1);
    	return(0);
    }
    
    • 该函数会使得守护进程将自己PID写入到指定文件中。如果该文件已经加了锁,那么lockfile函数将会返回失败,errno设为EACCESEAGAINlockfile函数的具体实现见见14.3节)。函数返回1,表明该守护进程已在运行。
    • 需要将文件长度截断为0,其原因是之前的守护进程ID字符串可能长于调用此函数的当前进程的进程ID字符串。比如之前是12345,现在是9999,那么在文件中留下的就是99995,将文件截断为0就可解决该问题。

6、守护进程的惯例

  • 若守护进程使用锁文件,那么该文件通常存储在/var/run目录中/var 包括系统运行时要改变的数据)。守护进程可能需要超级用户权限才能在此目录下创建文件。锁文件的名字通常是name.pid,其中name是该守护进程或服务的名字。如cron守护进程锁文件的名字就是/var/run/crond.pid

  • 若守护进程支持配置选项,那么配置文件通常存放在/etc目录中。配置文件的名字通常是name.conf,其中name是该守护进程或服务的名字。例如syslogd守护进程的配置文件通常是/etc/syslog.conf

  • 守护进程可用命令行启动,但通常它们是由系统初始化脚本之一(/etc/rc*/etc/init.d/*)启动的。如果在守护进程终止时,应当自动地重新启动它,则我们可以在/etc/inittab中为该守护进程包括respawn记录项,这样init就重新启动该守护进程。

  • 若一个守护进程有一个配置文件,那么当该守护进程启动时会读该文件,但在之后一般就不会再查看它。若某个管理员更改了配置文件,那么该守护进程可能需要被停止,然后再启动,以使配置文件生效。为避免这种麻烦,某些守护进程将捕捉SIGHUP信号,当它们收到该信号时重新读配置文件

    • 比如通过设置SIGHUP的信号捕捉函数,当收到SIGHUP时在信号捕捉函数内进行配置文件重读;
    • 或者在一个专用线程内通过sigwait函数同步的等待SIGHUP信号阻塞,当有阻塞的SIGHUP时,sigwait函数返回,然后执行配置文件重读。
  • 实例:说明守护进程可以重读其配置文件的一种方法,该程序使用sigwait以及多线程,具体用法见12.8节。

    #include "apue.h"
    #include 
    #include 
    
    sigset_t	mask;
    
    extern int already_running(void);
    
    void
    reread(void)
    {
    	/* ... */
    }
    
    void *
    thr_fn(void *arg)
    {
    	int err, signo;
    
    	for (;;) {
    		err = sigwait(&mask, &signo);
    		if (err != 0) {
    			syslog(LOG_ERR, "sigwait failed");
    			exit(1);
    		}
    
    		switch (signo) {
    		/*当收到SIGHUP信号,该线程调用reread函数重读它的配置文件*/
    		case SIGHUP:
    			syslog(LOG_INFO, "Re-reading configuration file");
    			reread();
    			break;
    		/*当收到SIGTERM信号,会记录消息并退出*/
    		case SIGTERM:
    			syslog(LOG_INFO, "got SIGTERM; exiting");
    			exit(0);
    
    		default:
    			syslog(LOG_INFO, "unexpected signal %d\n", signo);
    		}
    	}
    	return(0);
    }
    
    int
    main(int argc, char *argv[])
    {
    	int					err;
    	pthread_t			tid;
    	char				*cmd;
    	struct sigaction	sa;
    
    	if ((cmd = strrchr(argv[0], '/')) == NULL)
    		cmd = argv[0];
    	else
    		cmd++;
    
    	/*调用13.4节中的daemonize函数来初始化守护进程*/
    	daemonize(cmd);
    
    	/*调用13.5节中的already_running函数以确保该守护进程只有一个副本在运行*/
    	if (already_running()) {
    		syslog(LOG_ERR, "daemon already running");
    		exit(1);
    	}
    
    	/*
    	 * Restore SIGHUP default and block all signals.
    	 */
    	sa.sa_handler = SIG_DFL;
    	sigemptyset(&sa.sa_mask);
    	sa.sa_flags = 0;
    	/*此时SIGHUP信号仍被忽略,所以需要恢复对信号的默认处理方式,
    	否则调用sigwait的线程绝不会见到该信号*/
    	if (sigaction(SIGHUP, &sa, NULL) < 0)
    		err_quit("%s: can't restore SIGHUP default");
    	sigfillset(&mask);
    	/*阻塞所有信号,为何要这样做?防止主线程响应信号,允许其他线程响应*/
    	if ((err = pthread_sigmask(SIG_BLOCK, &mask, NULL)) != 0)
    		err_exit(err, "SIG_BLOCK error");
    
    	/*创建一个线程去处理信号,该线程唯一工作是等待SIGHUP和SIGTERM。*/
    	err = pthread_create(&tid, NULL, thr_fn, 0);
    	if (err != 0)
    		err_exit(err, "can't create thread");
    
    	/*
    	 * Proceed with the rest of the daemon.
    	 */
    	/* ... */
    	exit(0);
    }
    
  • 实例:单线程守护进程捕捉SIGHUP并重读其配置文件

    #include "apue.h"
    #include 
    #include 
    
    extern int lockfile(int);
    extern int already_running(void);
    
    void
    reread(void)
    {
    	/* ... */
    }
    
    void
    sigterm(int signo)
    {
    	syslog(LOG_INFO, "got SIGTERM; exiting");
    	exit(0);
    }
    
    void
    sighup(int signo)
    {
    	syslog(LOG_INFO, "Re-reading configuration file");
    	reread();
    }
    
    int
    main(int argc, char *argv[])
    {
    	char				*cmd;
    	struct sigaction	sa;
    
    	if ((cmd = strrchr(argv[0], '/')) == NULL)
    		cmd = argv[0];
    	else
    		cmd++;
    
    	/*
    	 * Become a daemon.
    	 */
    	daemonize(cmd);
    
    	/*
    	 * Make sure only one copy of the daemon is running.
    	 */
    	if (already_running()) {
    		syslog(LOG_ERR, "daemon already running");
    		exit(1);
    	}
    
    	/*
    	 * Handle signals of interest.
    	 */
    	sa.sa_handler = sigterm;
    	sigemptyset(&sa.sa_mask);
    	sigaddset(&sa.sa_mask, SIGHUP);
    	sa.sa_flags = 0;
    	if (sigaction(SIGTERM, &sa, NULL) < 0) {
    		syslog(LOG_ERR, "can't catch SIGTERM: %s", strerror(errno));
    		exit(1);
    	}
    	sa.sa_handler = sighup;
    	sigemptyset(&sa.sa_mask);
    	sigaddset(&sa.sa_mask, SIGTERM);
    	sa.sa_flags = 0;
    	if (sigaction(SIGHUP, &sa, NULL) < 0) {
    		syslog(LOG_ERR, "can't catch SIGHUP: %s", strerror(errno));
    		exit(1);
    	}
    
    	/*
    	 * Proceed with the rest of the daemon.
    	 */
    	/* ... */
    	exit(0);
    }
    
    • 初始化守护进程后,我们为SIGHUPSIGTERM配置了信号处理程序。可以将重读逻辑放在信号处理程序中,也可以只在信号处理程序中设置一个标志,并由守护进程的主线程完成所有的工作。

7、客户进程-服务器进程模型

  • 守护进程通常被用作服务器进程。例如syslogd进程就是服务器进程,而用户进程(客户进程)用UNIX域数据报套接字向其发送消息。syslogd服务器进程提供的服务就是将一条出错消息记录到日志文件中。
  • 上面介绍的例子中,客户进程和服务器进程之间的通信是单向的。客户进程向服务器进程发送服务请求,服务器进程则不向客户回送任何消息。
  • 后面的章节会有很多客户进程和服务器进程之间双向通信的实例。

你可能感兴趣的:(《UNIX环境高级编程》,服务器,linux,运维)