UNP_13_守护进程和inetd超级服务器
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 守护进程通常由系统初始化脚本进行启动,并在系统工作时间一直运行,启动步骤如下:
- 读取配置文件,在 /etc/syslog.conf 配置文件指定守护进程收取的各种日志消息应如何处理。可能添加到一个文件中,或被写到用户的登录窗口,或被转发给另一个主机上的 syslogd 进程
- 创建 Unix 域数据报套接字,给它捆绑路径名 /var/run/log
- 创建 UDP 套接字,捆绑 514 端口,接收别的主机发送过来的日志
- 打开路径名 /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 工作流程
- 启动阶段,读取配置文件,并给文件中每个类型服务器创建一个适当的类型(TCP or UDP…)的套接字。inetd 能够处理的服务器最大个数取决于 inetd 能够创建的描述符最大个数,使用 select 对所有描述符进行集中
- 为每个套接字调用 bind,指定 IP + port。端口通过 getservbyname 获取
- 对于 TCP 套接字,调用 listen 来进行监听,UDP 不用执行
- 使用 select 对所有套接字描述符进行监听,inetd 大部分时间都花在这里
- 如果可读的是 TCP 套接字描述符,调用 accept 来进行连接
- 调用 fork 创建子进程来处理不同的请求,类似于并发服务器
- 如果第 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 守护进程处理 启动任意一个程序并将其变为守护进程步骤如下:
- 调用 fork 到后台运行
- 调用 setsid 创建一个新会话,并让前一步的子进程成为会话头进程
- 再次 fork 防止会话头进程自动获取控制终端
- 改变工作目录
- 创建模式掩码
- 关闭所有非必要描述符 许多 Unix 服务器由 inetd 守护进程启动。它处理所有守护进程需要的步骤,当启动真正的服务器时,套接字已在标准输入,标准输出,标准错误上打开。这样就不用调用 socket,bind,accept,这些步骤已经由 inetd 完成。