使用inetd守护进程
运行在Unix下在的第一个服务器通常都会提供一个作为单独进程运行的服务。然而,当要提供的服务数量变得很大时,这会成为系统的一个负担。这是因为资源必须与每一个正在运行的服务器进程相关联,甚至是对当前正在提供的服务并没有请求时也是如此。
另外,我们可以观察到大多数据的服务器程序使用通常的进程来创建,绑定,监听,与接受新的客户连接。对于无连接的服务器操作与是相似的观察结果。
在这一章,我们将会了解以下内容:
什么是inetd守护进程?
inetd如何解决资源使用问题?
inetd如何简化服务器编写?
大多数服务器的通常步骤
如果我们回忆一下第8章,服务器的面向连接协议,我们就会回忆起一个面向连接的服务器用来建立与一个客户的连接的基本步骤如下:
1 创建一个套接口
2 将套接口绑定到一个已知的地址
3 监听客户端连接
4 接受客户端连接
现 在考虑一下两个不同的服务器,也就是telnet客户的telnetd,与ftp客户的ftpd。对于这两个服务器步骤1到步骤4有什么不同吗?答案是对 两者几乎相同。我们将会了解到inetd守护进程可以为任何的面向连接的服务器执行初始化步骤,节省了服务器编写者为这些步骤编写与调试代码的时间。 ineted守护进程的思想也可以扩展到处理无连接服务器的情况。
inetd简介
当我们的Linux系统第一次启动时,inetd守护进程是由一个启动脚本来启动的。
当inetd守护进程第一次启动时,他必须知道他要监听的网络服务,以及当请求到达时应将请求发送到哪个服务器。这是在启动文件/etc/inetd.conf文件是进行配置的。
/etc/inetd.conf配置文件
/etc/inetd.conf的文件而已组织为一个文本文件,每一个文本行代表一个记录,这个记录描述了一个网络服务。以#开始的行为注释。
文件描述如下表所示:
/etc/inetd.conf配置记录
域 描述 例子
1 网络服务名 telnet
2 套接口类型 stream,dgram
3 协议 tcp或udp
4 标记 nowait或wait
5 使用的用户ID root或nobody
6 可执行的路径名 /usr/sbin/in.telnetd
7 服务器参数 in.telnetd
网络服务名域
/etc/inetd.confi记录的网络服务名域只是/etc/services文件中一个简单的网络服务名,这个文件我们在第7章,客户端的面向无连接的协议,中进行了讨论。我们可以快速的查看一下/etc/services文件中的内容:
# grep telnet /etc/services
telnet 23/tcp
rtelnet 107/tcp # Remote Telnet
rtelnet 107/udp
#
在这里我们可以看,标记为telnet的服务配置为在端口号23上的一个tcp服务。这就是inetd守护进程如何来决他必须在哪个端口上进行监听。
相应的,我们也可以简单的指定一个端口号。我们将会在这一章的后面看到一个这样的例子。
套接口类型域
尽管Linux inetd守护进程可以接受许多的套接口类型,但是为了简单起见,我们在这里只讨论stream或是dgram。对于那些感兴趣的读者,inetd man手册页同时也列出raw,rdm以及seqpacket等其他可能的套接口类型。
stream类型对应着socket函数调用中的SOCK_STREAM类型。而dgram则对应着SOCK_DGRAM套接口类型。
协议域
正如我们所想到的,这会为套接口选择所使用的协议。这个值必须是出现在/etc/protocols文件的一个可用的实体。两个常用到的选择为:
TCP协议的tcp
UDP协议的udp
也存在其他的可能协议,但是这两个是最常用到的。
标记域
这个域只为数据报套接口可用。非数据报套接口(例如stream tcp)必须将这个值定为nowait。
面向数据报的服务器可以为两种类型。他们是:
持续读取UDP数据包直到超时退出的服务器(为这些类型指定wait)
读取一个数据包然后退出的服务器(为这些类型指定nowait)
这些信息是inetd所需要的,因为dgram的通信处理要面向流协议的处理复杂得多。这有助于守护进程确定当某一服务的服务器正在运行时如何处理以后的dgram连接。这一点我们会在后面进行详细的讨论。
用户ID域
inetd在root帐户下运行。如果需要,他可以将其标识改为其他帐户。为了安全的目的,推荐使用为完成工作所需权限最小的帐户来运行服务器。从而,服务器通常服务器运行在一个更为受限的用户ID下,如nobody。
然而,一些服务器必须以root用户来运行,所以有时我们会看到用户ID指定这种方式。
路径名域
这个域只是简单的通知inetd可执行文件的路径名是什么。这是守护进程调用fork之后由exec来执行的可执行文件。
服务器参数域
/etc/inetd.conf配置文件其余的域指定了要由exec所调用的服务器的命令行参数。一个常会引起迷惑的地方就是这些参数是由参数argv[0]来启动的。这会使得命令名与路径名相区别。当一个可执行文件依据其名字来限制不同的特性时,这是十分用的。
inetd服务器的设计参数
使 用inetd作为服务器前端的一个优点就是可以简化服务器编写者的工作。例如,对于stream tcp服务器不再有相同的socket,bind,listen,accpet调用的负担。相似的代码节省也可以用于dgram udp服务器。那么,当一个进程启动后,inetd服务器是如何处理连接到服务器进程的套接口的呢?
Unix简单优雅的作法是,启动的服务器在下面的文件单元(文件描述符)上处理客户端套接口:
文件单元0作为标准输入的客户端套接口
文件单元1作为标准输出的客户端套接口
文件单元2作为标准错误的客户端套接口
使用这样的设计,服务器就可以不需要单一的套接口函数调用。所有的服务器I/O可以全部在通常的标准输入,标准输出,标准错误文件单元上来执行。在后面,我们将会用一个程序来演示如何用这种方式来使用标准输出。
实现一个简单的stream tcp服务器
我们也许会回忆起第8章所介绍的一个小程序。现在我们稍做休息来回忆一下这个程序。下面是这个相同服务器的新代码,所不同的是使用inetd守护进程来设计的。
/*
* inetdserv.c
*
* Example inetd daytime server:
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <time.h>
#include <sys/types.h>
/*
* This function reports the error and
* exits back to the shell:
*/
static void bail(char *on_what)
{
if( erron != 0 )
{
fputs(strerror(errno),stderr);
fputs(": ",stderr);
}
fputs(on_what,stderr);
fputc('/n',stderr);
exit(1);
}
int main(int argc,char **argv)
{
int z;
int n;
time_t td; /* Current date&time */
char dtbuf[128]; /* Date/Time info */
/*
* Generate a time stamp:
*/
time(&td);
n = (int) strftime(dtbuf,sizeof dtbuf,
"%A %b %d %H:%M:%S %Y/n",
localtime(&td));
/*
* Write result back to the client:
*/
z = write(1,dtbuf,n);
if(z==-1)
bail("write(2)");
return 0;
}
我们可以注意到,与第8章的程序相比,这个程序是多么的简单。注意下面的几点不同:
不再需要套接口的include文件
不再需要套接口地址结构
不再需要套接口调用。注意这个程序可以立即开始工作。
因为这个程序不再使用套接品函数,所以可以很容易的由Shell来进行测试:
$ make inetdserv
gcc -c -D_GNU_SOURCE -Wall -Wreturn-type inetdserv.c
gcc inetdserv.o -o inetdserv
$ ./inetdserv
Tuesday Nov 02 16:29:45 1999
$
这与13号端口上的daytime服务相类似:
$ telnet 192.168.0.1 13
Trying 192.168.0.1 . . .
Connected to 192.168.0.1.
Escape character is '^]'.
Tue Nov 2 16:31:09 1999
Connection closed by foreign host.
$
格式上唯一的真正的不同就在于我们的程序显示的全部的星期名字。现在是时候来演示这个程序如何来使用inetd守护进程。
配置/etc/inetd.conf来调用一个新的服务器
为了使得我们的简单的新服务器有用(或者至少与daytime作用相似),我们必须修改为inetd守护进程所用的配置文件。如果需要,我们可以回顾一下上面所介绍的配置表的内容。
创建可执行文件
现在这里我们假设我们在前面已经编译了inetdserv程序。为了简化步骤,在这里我们输入下面的命令:
$ cp inetdserv /tmp/inetdserv
$ chmod a+rx /tmp/inetdserv
前面的两个步骤将服务器拷贝到一个已知的地方,并且保证他是可执行的。
创建服务
为了这个测试,在/etc/inetd.conf文件中添加一行(将其添加到最后一行)。我们可以使用vi或是其他的编辑器来完成这个操作。其内容如下:
$ tail -1 /etc/inetd.conf
9099 stream tcp nowait root /tmp/inetdserv inetdserv
下面让我们来回顾一下,这一行对于inetd意味着什么:
因为我们的新服务并不拥有一个在/etc/services文件中存在的名字,第一个域只是简单的包含我们希望使用的端口号。在这里我们选择9099。
在第二个域包含stream,所以会使用流式套接口。
在第三个域中包含tcp,来表明我们希望一个TCP流,而不是其他的协议流。
第四个域指定为nowait,这是TCP流所必需的。
第五个域指定为root。我们也可以使用通常的用户ID(但是必须保证有合适的权限来执行/tmp/inetdserv)。
路径名/tmp/inetdserv指定为第六个域。这是连接到达套接口时将会执行的可执行文件的路径。
第七个域指定为inetdserv。在这个例子中,我们的服务器程序并不会关注argv[0]的值,所以这里可以是任何值。
现在,在我们实际连接到这个服务之前,执行一步额外的测试来确认一切正常:
$ /tmp/inetdserv
Tuesday Nov 02 16:52:33 1999
$
如果我们没有得到输出结果,检查来确认我们是否使用了正确的文件名来拷贝文件。另外,确认程序有正确的可执行权限。在完成这些演示的功能之后,我们已经准备好让inetd知道我们已经对其配置文件做了更改。
通知inetd配置更改
要通知inetd发生了更改,我们必须切换到root并且执行下面的操作:
# ps -ax | grep inetd
314 ? S 0:00 inetd
# kill -HUP 314
#
我们的进程ID也许不是314,事实上,很有可能不同。所需要用到的步骤如下:
1 使用ps命令并使用grep命令过滤列出inetd守护进程的进程ID。
2 向inetd发送一个SIGHUP信号来通知他重新读取/etc/inetd.conf配置文件。
这并没有结束进程。
在通知inetd之后,我们也许希望检测我们的配置更改是否被接受。检测的一个办法就是执行下面的命令:
# lsof -i
COMMAND PID USER FD TYPE DEVICE SIZE NODE NAME
portmap 238 root 3u inet 369 UDP *:sunrpc
portmap 238 root 4u inet 370 TCP *:sunrpc (LISTEN)
inetd 314 root 4u inet 474 TCP *:ftp (LISTEN)
inetd 314 root 5u inet 475 TCP *:telnet (LISTEN)
inetd 314 root 6u inet 476 TCP *:login (LISTEN)
inetd 314 root 8u inet 477 TCP *:exec (LISTEN)
inetd 314 root 10u inet 478 TCP *:auth (LISTEN)
inetd 314 root 11u inet 1124 TCP *:9099 (LISTEN)
inetd 314 root 12u inet 1163 TCP *:daytime (LISTEN)
named 342 root 4u inet 531 UDP *:1024
. . .
在输入显示行中的TCP *:9099表明新的服务已经添加到新的服务器中。注意,左边显示inetd是在端口9099上监听的进程。TCP *:9099告诉我们TCP端口9099可以接受来自任何端的连接。
测试新服务
我们可以通过localhost地址来测试我们新的inetd服务:
$ telnet localhost 9099
Trying 127.0.0.1 . . .
Connected to localhost.
Escape character is '^]'.
Tuesday Nov 02 17:10:37 1999
Connection closed by foreign host.
$
我们会回忆起通常我们将127.0.0.1配置为我们的本地回环地址。如查我们有一个以太网卡,我们可以使用这个接口地址。我们的输出也许如下所示:
$ telnet 192.168.0.1 9099
Trying 192.168.0.1 . . .
Connected to 192.168.0.1.
Escape character is '^]'.
Tuesday Nov 02 17:13:28 1999
Connection closed by foreign host.
$
这个输出确认了允许由任何接口进行连接的事实。现在可以与我们已经存在的daytime服务相比。不要忘记在命令行添加端口13作为参数:
$ telnet 192.168.0.1 13
Trying 192.168.0.1 . . .
Connected to 192.168.0.1.
Escape character is '^]'.
Tue Nov 2 17:16:57 1999
Connection closed by foreign host.
$
我们可以注意到,与我们新服务器相比,这里忽略了星期名字。
禁止新服务器
现在切换到root用户,从/etc/inetd.conf文件中移除我们自定义的服务器实体。然后,重新通知inetd守护进程:
# ps -ax | grep inetd
314 ? S 0:00 inetd
# kill -HUP 314
#
使用inetd的数据报服务器
到目前为止,这一章一直在关注使用inetd的TCP 流式套接口的使用。当通过inetd创建数据报服务器端口时,就会添加额外的考虑。这是由我们在前面所谈到的wait与nowait标记值来提示的。
让我们来回顾一下应用到UDP服务器上所使用的inetd步骤:
1 inetd服务器在我们的UDP服务器器将会接受请求的UDP端口上进行监听。
2 inetd使用select调用来表明一个数据报已经到达套接口(注意,inetd并不读取数据报)
3 inetd服务器调用fork与exec来启动我们的UDP服务器。
4 我们的UDP服务器使用文件单元0(标准输入)来读取一个UDP数据包。
步骤1到4与我们的TCP流式是相同的。然而,在处理完步骤4所接收到的UDP数据包之后,UDP服务器有两个基本的选择:
退出(结束)
等待更多的UDP数据包(在超时时退出)
一 个小心的提示,如果UDP数据包频繁到达,为每一个UDP数据包启动一个新的进程对于系统来说是一沉重的开销。由于这个原因,一些UDP服务器在接收第一 个数据包这后会回环,并试着读取接下来的UDP数据包,而不是立即退出。使用超时,从而在没有更多数据包到达时进程会退出。当出现这种情况下,inetd 守护进程会为新的UDP数据包持续监听。
理解wait与nowait
只是简单的处理一个数据报然后退出的数据报服务器应使用nowait标记值。这会通知inetd守护进程当有额外的数据报到时会启动另外的服务器进程。这是必须的,因为启动的第一个进程将会只处理一个数据报。
对 于其他的试着读取更多数据报的数据报服务器,我们应使用wait标记值。这是必须的,inetd所启动的服务器进程将会处理接下来的数据报直到退出。 wait标记值会通知inetd不要为这个端口启动额外的服务器,直到wait系统调用通知inetd(使用SIGCHLD信号)我们的数据报服务器已经 结束。否则,inetd就会启动不必要的额外服务器进程。现在我们从系统有角度来重新描述这个过程:
1 inetd为将要到达的UDP数据包启动回环UDP服务器进程
2 inetd等待依赖于其配置文件的其他的不相关事件:他将会忽略当前的UDP端口,因为我们的数据报服务器已经启动来处理这些。注意,这个将动作将会使用,因为服务是使用wait标记值进行配置的(inetd不能确定可执行代表的是哪种类型的服务器)。
3 我们的数据报服务器完成第一个UDP数据报的处理。
4 我们的数据报服务器试着由标准输入读取额外的UDP数据报(数据报套接口)。
5 在我们的数据报服务器上发生超时,因为不再有数据报到达--我们的数据报服务器进程通过调用exit退出。
6 在inetd中发出SIGCHLD信号(记住,inetd是我们服务器的父进程)。
7 inetd服务器调用wait来确定进程ID以及我们服务器进程的结束状态。
8 inetd守护进程注意到由wait返回的进程ID属于我们的数据报服务器。他就会注意到这样的事实:他必须监视新的数据报,因为当前并没有进程在等待服务他们。
当定义数据报服务时我们要记住关于inetd的几下几点:
inetd守护进程并不能确定配置的数据报服务器中否需要wait或是nowait参数。我们必须清楚这些,并且为服务器提供正确的标记值。
wait标记值意味着不会启动另外的服务器进程,除非前面启动的服务器进程已结束。
为一个wait数据报服务器指定nowait会引起不必要的重复的服务器进程。
为一个nowait数据报服务器指定wait将会降低特定服务的性能。发生这样的情况是因为不会启动额外的进程直到父进程结束。
同时要记住,stream服务应总是使用nowait标记值。这会允许多个客户可以同时得到服务(每个连接的客户一个服务器进程)。如果为流式服务指定了wait标记值,则一次只可以服务一个客户连接。