UNIX网络编程卷一 学习笔记 第十三章 守护进程和inetd超级服务器

守护进程是在后台运行且不与任何控制终端关联的进程。Unix系统通常有很多守护进程在后台运行(约20到50个的量级),执行不同的管理任务。

守护进程通常由系统初始化脚本(在开机时运行)启动,而没有控制终端是在系统初始化脚本中启动进程的副作用。但守护进程也能在某个终端由用户在shell提示符下键入命令行命令启动,这样的守护进程必须亲自脱离与控制终端的关联,从而避免与作业控制、终端会话管理、终端产生的信号等发生不期望的交互,也能避免在后台运行的守护进程非预期地输出内容到终端。

守护进程有多种启动方法:
1.在系统启动阶段,许多守护进程由系统初始化脚本启动。这些脚本通常位于/etc目录,或以/etc/rc开头的某个目录中,它们的具体位置和内容是实现相关的。由这些脚本启动的守护进程一开始就拥有超级用户特权。

有若干网络服务器通常从这些脚本中启动:inetd超级服务器、Web服务器、邮件服务器(通常是sendmail)。syslogd守护进程通常也由某个系统初始化脚本启动。

2.许多网络服务器由稍后介绍的inetd超级服务器启动。inetd监听网络请求(Telnet、FTP等),每当有一个请求到达,就启动相应的实际服务器(Telnet服务器、TCP服务器等)。

3.cron守护进程按规则定期执行一些程序,由它启动执行的程序同样作为守护进程运行。cron自身由1中的某个脚本启动。

4.at命令用于指定将来某个时刻执行程序,当这些程序的执行时刻到来时,通常由cron守护进程启动执行它们,因此这些程序也作为守护进程运行。at命令指定期望在某个时间发生的一个一次性操作,而cron可以安排重复的定期任务。

5.守护进程还能从用户终端启动(不论是前台还是后台),这么做往往是为了测试守护程序或重启因某种原因而终止的某个守护进程。

因为守护进程没有控制终端,所以发生某些事时它们需要有输出消息的方法,这些消息可能是通告性消息,也可能是需要系统管理员处理的紧急事件消息。syslog函数是输出这些消息的标准方法,该函数把消息发送给syslogd守护进程。

Unix中的syslogd守护进程通常由某个系统初始化脚本启动,且在系统工作期间一直运行。源自Berkeley的syslogd实现在启动时执行以下步骤:
1.读取配置文件,通常为/etc/syslog.conf,其中指定syslogd守护进程收取的各种日志消息应怎样处理。这些消息可能被添加到一个文件(/dev/console文件是一个特例,它把消息写到控制台上),或写到指定用户的登录窗口(若用户已登录),或转发给另一个主机上的syslogd进程。

2.创建一个Unix域套接字,给它捆绑路径名/var/run/log(有些系统上是/dev/log)。

3.创建一个UDP套接字,给它捆绑端口514(syslog服务使用的端口号)。

4.打开文件/dev/klog,该文件用于提供内核日志的访问。

此后syslogd守护进程在一个无限循环中运行:调用select等待它的3个描述符之一变得可读,读入日志消息,并按配置文件进行处理。如果守护进程收到SIGHUP信号,就重新读取配置文件。

通过创建一个Unix域套接字,我们就可以在自己的守护进程中通过syslogd绑定的路径名发送我们的消息,以达到发送日志消息的目的,但更简单的接口是下面要介绍的syslog函数。我们也可以创建一个UDP套接字,通过往环回地址和端口514发送消息达到发送日志消息的目的。

较新的syslogd实现除非管理员明确要求,否则不创建UDP套接字,这么改变的原因在于:允许任何进程往这个套接字发送UDP数据报可能会让系统遭到拒绝服务攻击,其文件系统可能被填满(通过填满日志文件),来自合法进程的日志消息可能被排挤掉(如通过溢出syslogd的套接字接收缓冲区达到目的)。

syslogd的各种实现间存在差异,例如,源自Berkeley的实现使用Unix域套接字,而System V的实现使用基于流的日志驱动程序。源自Berkeley的各种不同实现给Unix域套接字使用的路径名也不相同。如果使用syslog函数,就可以忽略这些细节。

既然守护进程没有控制终端,它们就不能把消息fprintf到stderr上,从守护进程中记录消息的常用技巧是调用syslog:
在这里插入图片描述
syslog函数最初是为BSD系统开发的,但如今几乎所有Unix厂商都提供它。RFC 3164给出了BSD上syslog协议的文档。

priority参数是级别(level)和设施(facility)两者的结合,下图是level:
在这里插入图片描述
如上图所示,日志消息的level从0到7是从高到低排列的,如果发送者未指定level,就默认为LOG_NOTICE。下图是facility:
在这里插入图片描述
facility标识发送消息的进程的类型,如发送者未指定facility,则默认为LOG_USER。

RFC 3164中还有关于priority参数的额外细节。

message参数类似printf函数的格式串,但增加了%m规范,它会被替换为当前errno值对应的出错消息。此参数末尾可有换行符,但并非必需。

举例来说,当rename函数意外失败时,守护进程可执行以下调用:

syslog(LOG_INFO | LOG_LOCAL2, "rename(%s, %s): %m", file1, file2);

facility和level的目的在于,允许在/etc/syslog.conf文件中统一配置来自同一给定设施的所有消息,或统一配置具有相同级别的所有消息。例如,可在该配置文件中配置以下2行:

kern.*         /dev/console
local7.debug   /var/log/cisco.log

第一行指定所有内核消息登记到控制台;第二行指定来自local7设施的所有debug消息添加到文件/var/log/cisco.log的末尾。

当syslog函数被应用进程首次调用时,它创建一个Unix域套接字,然后调用connect连接到由syslogd守护进程创建的Unix域数据报套接字的众所周知路径名(如/var/run/log),这个套接字一直保持打开,直到进程终止。或者,进程也可调用openlog和closelog函数来控制该套接字的打开和关闭:
在这里插入图片描述
openlog函数可在首次调用syslog前调用,closelog函数可在应用不再需要发送日志消息时调用。

ident参数是一个由syslog函数冠于每个日志消息之前的字符串,它的值通常是程序名。

options参数由以下一个或多个常值的逻辑或构成:
在这里插入图片描述
openlog函数被调用时,通常不立即创建Unix域套接字,该套接字直到首次调用syslog时才打开。LOG_NDELAY选项使该套接字在openlog被调用时就创建。

openlog函数的facility参数为没有指定设施的后续syslog调用指定一个默认值。有些守护进程通过调用openlog指定一个设施(对于一个给定守护进程,设施通常不变),然后每次调用syslog时只指定级别(因为级别可随错误性质改变)。

日志消息也可由logger命令产生,它可在shell脚本中向syslogd发送消息。

以下是自编写的daemon_init函数,通过调用它,我们能把一个普通进程转变为守护进程,该函数在所有Unix变体上都适用,但有些Unix变体(如BSD和Linux)提供名为daemon的C库函数,也可实现类似的功能:

#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,不同于进程组ID
    // 这保证了子进程不是一个进程组的首进程,这是调用setsid的必要条件
    if ((pid = Fork()) < 0) {
        return -1;
    } else if (pid) {
        _exit(0);    /* parent terminates */
    }

    /* child 1 continues... */

    // setsid是一个POSIX函数,用于创建新会话
    // 当前进程会变成新会话的会话首进程和新进程组的进程组首进程,且不再有控制终端
    if (setsid() < 0) {    /* become session leader */
        return -1;
    }

    // 忽略SIGHUP信号并再次调用fork,fork函数返回时
    // 父进程是上一次调用fork产生的子进程,它被终止掉,留下新的子进程运行
    // 再次调用fork的目的是确保本守护进程将来即使打开了一个终端设备,也不会自动获得控制终端
    // 当没有控制终端的会话头进程打开一个终端设备时(该终端当前不是其他会话的控制终端)
    // 该终端自动成为这个会话头进程的控制终端
    // 再次调用fork后,我们确保新的子进程不再是一个会话头进程,从而不能自动获得控制终端
    // 忽略SIGHUP信号的原因是,当会话头进程(即首次调用fork产生的子进程)终止时
    // 会话中的所有进程都将收到SIGHUP信号
    Signal(SIGHUP, SIG_IGN);
    if ((pid = Fork()) < 0) {
        return -1;
    } else if (pid) {
        _exit(0);    /* child 1 terminates */
    }

    /* child 2 continues... */

    // 此外部变量用于我们自定义的err_XXX函数,其值非0时告知这些函数调用syslog,而非fprintf到标准错误
    // 此变量省得我们修改程序代码,在服务器不作为守护进程运行时(如在测试服务器程序时)
    // err_XXX函数会调用某个错误处理函数(如fprintf函数),在服务器作为守护进程运行时调用syslog
    daemon_proc = 1;    /* for err_XXX() functions */

    // 把工作目录改为根目录
    // 有些守护进程另有原因需改到其他某个目录
    // 如打印机守护进程可能改到打印机的假脱机处理目录,那里是它做全部工作的地方
    // 如果守护进程产生了某个core文件,该文件就存在当前工作目录中
    // 改变工作目录的另一个理由是,如果守护进程在某个文件系统中启动
    // 则该文件系统无法卸载,除非使用破坏性的强制措施
    // 卸载文件系统需要确保该文件系统上的所有文件都已关闭
    chdir("/");    /* change working directory */

    // 关闭本守护进程从执行它的进程(通常是shell)继承来的所有打开着的描述符
    // 问题是怎样检测正在使用的最大描述符,没有现成的Unix函数提供该值
    // 我们可以检测当前进程能够打开的最大描述符,但这个限制可以是无限的
    // 我们的做法是直接关闭前64个描述符,即使其中大部分可能没有打开
    // Solaris提供了closefrom函数,可用于解决守护进程的这个问题,它接收一个文件描述符
    // 然后关闭大于等于它的所有描述符
    /* close off file descriptors */
    for (i = 0; i < MAXFD; ++i) {
        close(i);
    }

    // 重定向stdin、stdout、stderr到/dev/null,确保这些常用描述符是打开的
    // 针对它们的read调用返回0;write调用会由内核丢弃所写数据
    // 打开这些描述符的原因在于
    // 守护进程调用的那些假设能从stdin读或往stdout、stderr写的库函数,不会因这些描述符未打开而失败
    // 这种失败是一种隐患,如果一个服务器守护进程未打开这些描述符
    // 而服务器守护进程却打开了与某客户关联的一个套接字,则此套接字可能会占用stdin、stdout、stderr
    // 此时如果守护进程调用如perror之类的函数,会把非预期的数据发送给那个客户
    /* redirect stdin, stdout, and stderr to /dev/null */
    open("/dev/null", O_RDONLY);
    open("/dev/null", O_RDWR);
    open("/dev/null", O_RDWR);

    // 第一个参数通常是程序的名字,如argv[0]
    // 第二个参数表示把进程ID加到每个日志消息中
    openlog(pname, LOG_PID, facility);

    return 0;    /* success */
}

既然守护进程在没有控制终端的环境运行,它绝不会收到SIGHUP信号,因此许多守护进程把此信号作为来自系统管理员的一个通知,表示其配置文件已发生改动,守护进程应重读配置文件。守护进程同样也不会收到SIGINT和SIGWINCH信号,因此这些信号也能安全地用作系统管理员的通知手段。

修改协议无关的tcp时间获取服务器程序,它调用daemon_init函数以作为守护进程运行:

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

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

    if (argc < 2 || argc > 3) {
        err_quit("usage: daytimetcpser2 [ <host> ] <service or port>");
    }

    daemon_init(argv[0], 0);

    if (argc == 2) {
        listenfd = Tcp_listen(NULL, argv[1], &addrlen);
    } else {
        listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
    }

    cliaddr = Malloc(addrlen);

    for (; ; ) {
        len = addrlen;
		connfd = Accept(listenfd, cliaddr, &len);
		err_msg("connection from %s", Sock_ntop(cliaddr, len));
	
		ticks = time(NULL);
		snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
		Write(connfd, buff, strlen(buff));
	
		Close(connfd);
    }
}

以上程序的改动只有两个地方:
1.程序尽早调用我们的daemon_init。

2.把printf调用改为我们的err_msg函数。

我们在调用daemon_init前检查argc,并输出合适用法的消息,这么做使得启动本守护进程的用户一旦提供数目不正确的命令行参数就能立即得到反馈。调用daemon_init后,所有的后续出错消息进入syslog,不再有作为标准错误输出的控制终端可用。

以上程序中,如果我们把daemon_init调用移到检查命令行参数前,会使检查命令行参数无效时的输出输出到syslog函数。

如果先在主机linux上运行以上获取时间TCP服务器程序,再在此主机上运行获取时间的TCP客户程序连接到服务器(如连接到localhost),然后检查/var/adm/messages文件(设施为LOG_USER的消息都发到该文件),可能找到类似下面的日志消息:
在这里插入图片描述
其中日期、时间、主机名由syslogd守护进程自动冠于日志消息之前。

典型的Unix系统上可能存在许多服务器,它们等待客户请求的到达,如FTP、Telnet、Rlogin、TFTP等。4.3 BSD面世前的系统中,所有这些服务都有对应的一个进程,这些进程都是在系统自举阶段从/etc/rc文件(rc是run commands的缩写)中启动的,且每个进程执行几乎相同的任务:创建一个套接字、把本服务器的众所周知端口捆绑到该套接字、等待一个连接(TCP)或一个数据报(UDP)、派生子进程为客户服务而父进程继续等待下一个客户请求。这个模型有两个问题:
1.所有这些守护进程含有几乎相同的启动代码,如创建套接字、变成守护进程。

2.每个守护进程在进程表中占一个表项,但它们大多时间处于睡眠状态。

4.3 BSD通过提供一个因特网超级服务器inetd守护进程使上述问题得到简化,基于TCP或UDP的服务器都可以使用这个守护进程,它这样解决问题:
1.守护进程的大多数启动细节由inetd处理,简化了守护进程的编写。这使得这些服务器不用再调用类似我们的daemon_init函数。

2.inetd单个进程就能为多个服务等待外来的客户请求,以此取代一个服务一个进程的做法,减少了系统中进程总数。

inetd进程使用daemon_init函数中的技巧把自己变成一个守护进程,之后它读入并处理自己的配置文件,通常是/etc/inetd.conf,其中指定了处理哪些服务以及当一个服务请求到达时该怎么做。该配置文件中每行包含的字段如下图:
在这里插入图片描述
下面是inetd.conf文件中内容的例子:
在这里插入图片描述
当inetd调用exec执行某服务器程序时,该服务器的名字总是作为程序的第一个参数传递。

以上有关inetd.conf的内容只是一个例子,许多厂商为inetd自行增设了新特性,如他们可能增加了处理RPC通信的功能;或者在TCP和UDP协议外,添加了处理其他协议的能力。并且上例中调用exec指定的路径名和服务器的命令行参数也是实现相关的。

wait-flag字段指定由inetd启动的守护进程是否有意接管与之关联的监听套接字,UDP没有监听套接字和接收套接字之分,因此几乎总配置成wait。TCP既支持wait也支持nowait,具体取决于守护进程的开发人员,但nowait更常见。配置成wait时,表示inetd等子进程退出后再接受新连接请求(对于UDP服务器来说,inetd和由inetd启动的服务器进程使用的是同一个套接字,因此需要由inetd启动的服务器进程接管该套接字,直到服务结束),而nowait表示inetd不等子进程退出就接受新连接请求(相当于监听套接字没有被由inetd启动的服务器守护进程接管)。

IPv6与/etc/inetd.conf的交互也取决于各个厂商的实现,有些厂商使用名为tcp6或udp6的protocol字段表示为相应服务创建一个IPv6套接字,有些厂商使用名为tcp64或udp64的protocol字段表示应为相应服务创建同时支持IPv6和IPv4客户的套接字,这些特殊协议名通常不出现在/etc/protocols文件中。
在这里插入图片描述
解释上图:
1.在启动阶段,读入/etc/inetd.conf,并给文件中指定的每个服务创建一个适当类型的套接字(如字节流或数据报)。inetd能处理的服务器的最大数目取决于inetd能够创建的描述符的最大数目。新创建的每个套接字都被加入到后面select调用所使用的描述符集中。

2.为每个套接字调用bind,捆绑相应服务器的众所周知端口和通配地址。这个TCP或UDP端口号通过调用getservbyname获得,参数是相应服务器在配置文件中的service-name字段和protocol字段。

3.对于每个TCP套接字,调用listen以接受外来连接请求。对数据报套接字不执行本步骤。

4.创建完所有套接字后,调用select等待其中任何一个套接字变得可读。TCP监听套接字会在一个新连接可被接受时变为可读;UDP套接字将在有一个数据报到达时变为可读。inetd大部分时间阻塞在select调用内部,等待某套接字变得可读。

5.当select函数返回,指出某个套接字可读后,如果该套接字是TCP套接字,且其服务器的wait-flag值为nowait,就调用accept函数接受这个新连接。

6.inetd守护进程调用fork派生进程,并由子进程处理服务器请求。

子进程关闭除了要处理的套接字描述符之外的所有描述符,对TCP服务器来说,这个套接字是由accept函数返回的新的已连接套接字;对UDP服务器来说,这个套接字是父进程最初创建的UDP套接字。之后子进程调用dup2三次,把待处理的套接字描述符复制到描述符0、1、2,然后关闭原套接字描述符。子进程打开的描述符于是只有0、1、2。子进程从标准输入读实际是从所处理的套接字读,往标准输出或标准错误写实际是往所处理的套接字写。子进程根据它在配置文件中的login-name字段值,调用getpwnam获取对应的password文件表项。如果login-name不是root,子进程就调用setgid和setuid把自身改为指定用户(inetd进程以uid为0的用户运行,其子进程会继承此uid,uid值0一般在系统中是预留给root用户的,因此一般uid为0的进程有root权限,因此能变成任何用户)。

子进程然后调用exec执行由相应的server-program字段指定的程序处理请求,相应的server-program-arguments字段值则作为命令行参数传给该程序。

7.如果第5步中,如果select函数返回的是一个字节流套接字,则父进程需要关闭已连接套接字,就像标准并发服务器那样,之后父进程再次调用select,等待下一个变为可读的套接字。
在这里插入图片描述
如上图,连接请求指向TCP端口21,accept函数为它创建了一个新的已连接套接字。

调用fork后,子进程会关闭除这个已连接套接字外的所有描述符:
在这里插入图片描述
之后子进程把这个已连接套接字复制到描述符0、1、2,然后关闭原描述符:
在这里插入图片描述
子进程接着调用exec,通常情况下,所有描述符跨exec保持打开,因此exec加载的服务器程序使用描述符0、1、2与客户通信,且服务器中应只打开这些描述符。

以上是配置文件中指定了nowait的服务器的情形,对于TCP服务器这是典型的设置,意味着inetd不必等待某个子进程终止就可以接受该子进程所提供服务的另一个连接。如果对于某子进程所提供服务的另一个连接在该子进程终止前到达,则一旦父进程再次调用select,这个连接就立即返回给父进程,然后再执行前面列出的4、5、6步骤,于是派生出另一个子进程处理这个新请求。

给一个数据报服务指定wait标志导致父进程执行的步骤发生变化,wait标志要求inetd在当前服务的子进程终止后,才能将该服务的套接字加入select函数的候选套接字集。发生的变化如下:
1.fork函数返回到父进程时,父进程保存子进程的PID,这使得父进程能通过查看waitpid函数返回的值确定这个是否终止了。

2.父进程通过FD_CLR宏关闭这个套接字在select函数所用描述符集中对应的位,从而在将来的select函数调用中忽略这个套接字。这意味着子进程将接管该套接字,直到自身终止为止。

3.当子进程终止时,父进程被通知一个SIGCHLD信号,而父进程的信号处理函数将取得这个子进程的PID,然后父进程打开相应套接字在select函数所用描述符集中的位,使该套接字重新成为select函数的候选套接字。

数据报服务器必须接管其套接字直至自身终止,inetd在此期间让select不检查该套接字的可读性,这是因为每个数据报服务器只有一个套接字,而不像TCP服务器那样既有一个监听套接字,对每个客户又有一个已连接套接字。如果inetd不关闭某个数据报套接字的可读条件检查,且父进程先于服务该套接字的子进程执行,则引发本次fork的数据报仍在套接字接收缓冲区中,会导致select函数再次返回可读条件,致使inetd再次fork另一个不必要的子进程。inetd通过接收表明该子进程已终止的SIGCHLD信号来得知子进程已不再使用该套接字。

既然替一个TCP服务器调用accept函数的进程是inetd,inetd启动的服务器通常通过调用getpeername获取客户的IP和端口号。fork+exec后,服务器获取客户身份的唯一方法就是getpeername函数。

inetd通常不适用于服务密集型服务器,如邮件服务器和web服务器。sendmail通常每个客户连接的进程控制开销只是fork,而由inetd启动的TCP服务器的开销是fork+exec。而web服务器会使用多种技术把每个客户连接的进程控制开销降到最小。

在Linux等系统上,扩展式因特网服务守护进程xinetd已很常见,它提供与inetd一样的基本服务,还提供其他特性,如根据客户的地址登记、接受或拒绝连接的选项、每个服务一个配置文件等。

以下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;
    openlog(pname, LOG_PID, facility);
}

以上函数比daemon_init函数简单很多,因为所有守护进程化的工作已由inetd在其启动时执行。以上函数的任务只是为错误处理函数设置daemon_proc标志,并调用openlog。

以下是可以由inetd作为守护进程启动的TCP时间获取服务器程序:

#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);
    // 既然未曾调用自定义函数tcp_listen,因此我们不知道客户套接字地址结构大小
    // 且由于未曾调用accept,我们也不知道客户的协议地址
    // 我们于是使用sockaddr_storage给套接字地址结构分配一个缓冲区
    // 并以描述符0作为第一个参数调用getpeername
    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);
}

以上程序有两个较大改动:首先所有套接字创建代码(对tcp_listen和accept的调用都消失了)消失了,这些步骤由inetd执行,此时我们使用描述符0指代已由inetd接受的TCP连接。其次,无限的for循环也消失了,因为本服务器每次客户连接启动一次,服务完当前客户后就终止。

为了在Solaris系统上运行上例程序,我们首先赋予本服务一个名字和一个端口,将其加入/etc/services文件中:

mydaytime         9999/tcp

然后把以下行加入/etc/inetd.conf文件:

mydaytime stream tcp nowait andy /home/andy/daytimetcpsrv3 daytimetcpsrv3

把可执行文件放到指定位置后,我们给inetd发送一个SIGHUP信号,让它重新读入配置文件,接着执行netstat命令验证inetd已在TCP端口9999上创建了一个套接字:
在这里插入图片描述
然后从另一个主机上访问这个服务器:
在这里插入图片描述
/var/adm/messages文件(根据/etc/syslog.conf文件,将LOG_USER设施的消息登记到该文件)中有如下日志消息:
在这里插入图片描述
守护进程是在后台运行且没有控制终端的进程。许多网络服务器作为守护进程运行。守护进程产生的所有输出通常通过调用syslog函数发送给syslogd守护进程。系统管理员可根据发送消息的守护进程的设施类型以及消息的严重级别,选择处理方式。

任意启动一个程序并让它成为守护进程需要以下步骤:调用fork转到后台运行,调用setsid建立一个新的POSIX会话并成为会话首进程,再次调用fork避免无意中获得新的控制终端,改变工作目录和文件创建模式掩码,关闭所有非必要描述符。我们的daemon_init函数处理这些细节。

许多Unix服务器由inetd守护进程启动,inetd处理所有守护进程化所需步骤,当启动真正的服务器时,套接字已在标准输入、标准输出、标准错误打开。这样我们无需调用socket、bind、listen、accept,这些步骤已由inetd处理。

实际上,对于inetd内部处理的5个服务(echo、discard、chargen、time、daytime),每个服务各有1个TCP版本和1个UDP版本,这样总共10个服务器的实现中,TCP版本的echo、discard、chargen服务器由inetd派生出来后作为子进程运行,它们需要运行到客户终止连接为止;TCP版的time和daytime服务器不需要inetd派生子进程,因为它们的服务极易实现(即取得当前时间和日期,把它格式化后写出,再关闭连接),于是inetd直接处理。所有5个UDP服务都不需要inetd派生子进程,因为每个服务对客户的返回最多只有一个数据报,因此这5个服务由inetd直接处理。

如果我们创建一个UDP套接字,把端口7(echo服务)捆绑其上,然后在这个回射服务器上把一个UDP数据报发送到chargen服务器,会导致chargen服务器发送回一个数据报到端口7,这个数据报又会被echo服务器回射到chargen服务器,从而一直循环下去,这是一个有名的拒绝服务攻击。FreeBSD上实现的解决办法是拒绝源端口和目的端口都是内部服务的外来数据报。另一个常用的解决办法是通过inetd禁止这些内部服务,可以在主机上这么做,也可以在一个组织机构接入因特网的路由器上这么做。

Solaris 2.x的inetd有一个-t标志,它会使inetd调用syslog(所用设施为LOG_DAEMON,级别为LOG_NOTICE)登记所处理的TCP请求的客户的IP地址和端口号,该客户的IP地址和端口号是inetd通过accept函数获取的。但不能获取UDP客户的IP和端口号,原因是读入数据报的recvfrom函数是由fork+exec执行的真正服务器调用的,对于inetd,一个解决办法是使用MSG_PEEK标志获取客户IP和端口号,被窥读的数据报保持不动,留待真正的服务器读入。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值