UNP_13_守护进程和inetd超级服务器

Posted on Mar 13, 2020

13.1 概述

  守护进程:是在后台运行且不与任何控制终端关联的进程。Unix 系统通常有很多守护进程在后台运行(20~50)执行不同的管理任务
  守护进程没有终端,通常是因为他们由开机时的脚本进行启动。但是守护进程也可能从某个终端由用户在 shell 提示符下键入命令行进行启动,这样的守护进程必须亲自脱离与控制终端的关联,从而避免与作业控制,终端会话管理,终端产生信号等发生不希望的交互,也防止后台的守护进程输出到终端

守护进程的启动方式:

  • 在系统阶段进行启动,许多守护进程由系统初始化脚本进行启动,脚本通常位于 /etc 等目录,这些脚本启动的守护进程开始就拥有超级用户权限(inetd,Web,sendmail,syslogd 等等)
  • 许多网络服务器由 inetd 超级服务器进行启动。Inetd 监听网络请求,每当有一个请求到达,启动相应的实际服务器(Telnet,FTP…)
  • cron 守护进程按规则定期执行一些程序。这些程序的时刻到来时,corn 执行的程序通常也是守护进行的方式运行
  • at 命令用于指定将来某个时刻的程序执行,时间到达时,通常使用 corn 来进行执行
  • 守护进程还可以从用户的终端在前台或者后台进行启动。这么做往往是测试守护进程或者重启关闭的守护进程。

因为守护进程没有终端,所以他们的消息使用 syslog 进行处理,即使用 syslog 函数,将消息发送给 syslogd 进程

13.2 syslodg 守护进程

syslogd 守护进程通常由系统初始化脚本进行启动,并在系统工作时间一直运行,启动步骤如下:

  1. 读取配置文件,在 /etc/syslog.conf 配置文件指定守护进程收取的各种日志消息应如何处理。可能添加到一个文件中,或被写到用户的登录窗口,或被转发给另一个主机上的 syslogd 进程
  2. 创建 Unix 域数据报套接字,给它捆绑路径名 /var/run/log
  3. 创建 UDP 套接字,捆绑 514 端口,接收别的主机发送过来的日志
  4. 打开路径名 /dev/klog。来自内核的任何出错消息从这个设备输入   syslog 使用 select 来监听上面 2,3,4 步的描述符来接受日志,并按照配置文件进行处理。如果守护进程读取 SIGHUP 信号,就重新读取配置文件   最新的系统不建议开启 514 端口,会遭到攻击

13.3 syslog 函数

守护进程没有终端,所以不能把消息 fprintf 到 stderr 上。从守护进程中登记消息的常用技巧是调用 syslog 函数

#include <syslog.h>
void syslog(int priority, const char * message, ...);

参数解析:

  • priority:级别和设施两者的组合体
  • message:类似 printf 格式串,增加了 %m 规范代表当前 errno 值

  当 syslog 被应用进程首次调用时,它创建一个 Unix 域数据报套接字,然后调用 connect 连接到由 syslogd 守护进程创建的 Unix 域数据报套接字的众所周知的路径名。这个套接字一直打开,直到进程终止关闭

logger 命令在 shell 脚本中以向 syslogd 发送消息

13.4 daemon_init 函数

编写一个守护进程的创建函数,有些系统提供 daemon 函数用来创建守护进程,和本程序类似

#include	"unp.h"
#include	<syslog.h>

#define	MAXFD	64

extern int	daemon_proc;	/* defined in error.c */

int
daemon_init(const char *pname, int facility)
{
	int		i;
	pid_t	pid;
    // 调用 fork 创建子进程,然后直接终止父进程,留下子进程继续执行。如果是在 shell 中执行的程序,父进程终止,shell 会认为程序已经结束了,子进程就可以在后台执行了
    // 子进程继承父进程的进程组 ID,但它有自己的进程 ID,这就保证了子进程不是一个进程组的头进程,这是接下来调用 setsid 的必要条件
	if ( (pid = Fork()) < 0)
		return (-1);
	else if (pid)
		_exit(0);			/* parent terminates */

	/* child 1 continues... */
    // setsid 用来创建一个新的会话。当前进程变为新会话的会话头进程以及新进程组的进程组头进程,从而不再有控制终端
	if (setsid() < 0)			/* become session leader */
		return (-1);
    // 忽略 SIGHUP 信号,并再次调用 fork。该函数返回时,同样只使用子进程,父进程返回
    // 再次 fork 是为了确保本守护进程将来即使打开一个新的终端,也不会自动获得控制终端。当没有终端的一个会话头进程打开终端时,该终端自动成为这个头进程的控制终端。再次调用 fork,产生的子进程不是会话头进程,就不会自动获得一个控制终端。这里必须忽略 SIGHUP 信号,当会话头进程终止时,所有会话子进程都会收到 SIGHUP 信号
	Signal(SIGHUP, SIG_IGN);
	if ( (pid = Fork()) < 0)
		return (-1);
	else if (pid)
		_exit(0);			/* child 1 terminates */

	/* child 2 continues... */
    // 把全局变量 daemon_proc 设置为非 0 值,这个变量由 err_XXX 函数使用,不为 0 是为了告诉他们将 fprintf 输出替换为调用 syslog 函数
	daemon_proc = 1;			/* for err_XXX() functions */
    // 改变工作目录到根目录
	chdir("/");				/* change working directory */

	/* close off file descriptors */
    // 关闭所有打开的描述符,直接关闭前 64 个,这里不考虑太多
	for (i = 0; i < MAXFD; i++)
		close(i);

	/* redirect stdin, stdout, and stderr to /dev/null */
    // 将 stdin stdout stderr 重定向到 /dev/null
	open("/dev/null", O_RDONLY);
	open("/dev/null", O_RDWR);
	open("/dev/null", O_RDWR);
    // 使用 syslogd 处理错误
	openlog(pname, LOG_PID, facility);

	return (0);				/* success */
}

  守护进程在没有终端的环境下运行,不会接收 SIGHUP 信号。许多守护进程把这个信号可以当作系统发送的通知,表示配置文件发送了变化,应重新读取配置文件,类似的还有 SIGINT SINGWINCH 信号

13.5 inetd 守护进程

  通过 inetd 简化一系列服务器进程的启动流程,这些服务器程序流程相似,并且大部分时间处于睡眠,交给 inetd 守护进程来进行处理,既可以简化编写的代码,又可以对每次客户端的请求单独响应,并不需要每个服务器一直等待客户端,因为他们大多数时间都处于休眠状态。只需要 inetd 循环等待客户端的请求即可,来了请求,为对应的客户创建需要的服务器子进程即可。

当使用 inetd 调用一个程序时,程序名作为第一个参数进行传递。

inetd 工作流程

  1. 启动阶段,读取配置文件,并给文件中每个类型服务器创建一个适当的类型(TCP or UDP…)的套接字。inetd 能够处理的服务器最大个数取决于 inetd 能够创建的描述符最大个数,使用 select 对所有描述符进行集中
  2. 为每个套接字调用 bind,指定 IP + port。端口通过 getservbyname 获取
  3. 对于 TCP 套接字,调用 listen 来进行监听,UDP 不用执行
  4. 使用 select 对所有套接字描述符进行监听,inetd 大部分时间都花在这里
  5. 如果可读的是 TCP 套接字描述符,调用 accept 来进行连接
  6. 调用 fork 创建子进程来处理不同的请求,类似于并发服务器
  7. 如果第 5 步返回字节流套接字,父进程要关闭已连接套接字,就是 accept 的套接字,类似于 TCP 并发服务器

13.6 daemon_inetd 函数

该函数可以用在 inetd 启动的服务器程序中

#include	"unp.h"
#include	<syslog.h>

extern int	daemon_proc;	/* defined in error.c */

void
daemon_inetd(const char *pname, int facility)
{
	daemon_proc = 1;		/* for our err_XXX() functions */
	openlog(pname, LOG_PID, facility);
}

所有的步骤已经由 inetd 在启动时执行完毕,本函数仅仅处理错误函数设置 daemon_proc 标志,并调用 openlog 函数

由 inetd 作为守护进程启动时间获取服务器程序

#include	"unp.h"
#include	<time.h>

int
main(int argc, char **argv)
{
	socklen_t		len;
	struct sockaddr	*cliaddr;
	char			buff[MAXLINE];
	time_t			ticks;

	daemon_inetd(argv[0], 0);

	cliaddr = Malloc(sizeof(struct sockaddr_storage));
	len = sizeof(struct sockaddr_storage);
	Getpeername(0, cliaddr, &len);
	err_msg("connection from %s", Sock_ntop(cliaddr, len));

    ticks = time(NULL);
    snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
    Write(0, buff, strlen(buff));

	Close(0);	/* close TCP connection */
	exit(0);
}

小结

  守护进程是在后台运行并独立与所有终端的进程,许多网络服务器作为守护进程运行。守护进程所产生的输出调用 syslog 函数交给 syslogd 守护进程处理 启动任意一个程序并将其变为守护进程步骤如下:

  1. 调用 fork 到后台运行
  2. 调用 setsid 创建一个新会话,并让前一步的子进程成为会话头进程
  3. 再次 fork 防止会话头进程自动获取控制终端
  4. 改变工作目录
  5. 创建模式掩码
  6. 关闭所有非必要描述符 许多 Unix 服务器由 inetd 守护进程启动。它处理所有守护进程需要的步骤,当启动真正的服务器时,套接字已在标准输入,标准输出,标准错误上打开。这样就不用调用 socket,bind,accept,这些步骤已经由 inetd 完成。