10.守护进程和超级服务器inetd
十 守护进程和超级服务器inetd
第10章 守护进程和超级服务器inetd 10.1 守护进程的原理
10.1.1 安全的守护进程的步骤
只要系统没有关机或者崩溃,守护进程将在系统中不间断的运行。所以必须使守护进程和其他进程运行的环境隔离开,避免由于其他进程的动作,影响守护进程的工作。问
的关键就是如何将守护进程的运行环境同其他进程隔离。
1. 第一次fork
我们考虑在shell提示符下启动一个守护进程。这个进程通常将成为当前shell所在会话进程的领头进程,也就是拥有控制终端的进程(我们在文件系统和进程系统中
的)。
Linux中的每个进程都继承了一个到终端的连接,这个终端被指定为控制终端,启动进程的用户可以使用这个控制终端对进程进行控制,比如在具有作业控制功能的shell中,用户ctrl+c将发送SIGINT给进程组的中的每个进程。所以来自终端的各种信号将可能影响这个进程的工作。我们希望将进程和它的控制终端分离开,这样来自控制终端的信号就不会影响守护进程。
调用setsid可以使进程成为新的会话过程的领头进程,setsid的另一个结果就是将控制终端留给原先的会话过程,这样调用setsid的进程实际上就与原来的控制终端分离了。进程能够调用setsid的条件是,这个进程必须不是一个进程组的领导者,但是当守护进程最初在shell中运行时,它将成为它所在进程组的领导者。
为了能够成功的调用setsid,我们将进行第一次fork,这次fork后,父进程退出,而子进程将继续运行。代码如下:
ret=fork();
if(ret<0){
fprintf(stderr,"error in first fork.\n");
exit(1);
}else if(ret!=0)
exit(0);
此时子进程肯定不再进程组的领导者,所以该进程可以安全的调用setsid。 2. 调用setsid
接着我们调用setsid,将进程同旧会话过程中分离出来,使它自身单独的在一个新的会话过程中。但是由于进程是这个新的会话过程的领头进程,所以它如果打开一个终端,则这个终端将成为它的控制终端,则进程又将可能收到新的控制终端的影响。
为了避免这一点,我们将再次进行fork,使进程不再是这个新的会话过程的领头进程, 这样即使进程打开一个终端,这个终端也不能成为这个会话过程的控制终端。进程将不再会
1
Linux网络编程
同一个控制终端相连。
但是由于此时进程是会话进程的领头进程,如果我们使用上面类似的代码,使父进程退出,而子进程继续运行,则父进程退出时,将会产生SIGHUP信号,这个信号将发送给这个会话进程中的所有进程。所以我们首先要忽略信号SIGHUP,然后再fork。下图显示了进程此时的地位和状态。
子进程不再是一个进程如果直接fork,父组的领导进程,因为它进程的退出,将是的父进程才是进程组的系统发送SIGHUP给会话过程的领头进程领导者,这为setsid创会话过程中的所有并且是一个进程组的领导进程造条件进程,子进程也将
收到旧的会话过程新的会话过程
setsidfork
忽略SIGHUP信号
fork
图10.1 守护进程的"变迁"过程(1) 3. 忽略信号SIGHUP,第2次fork
在第2次fork后,进程已经脱离了控制终端,但是,它与退出的父进程属于同一个进程组,所以发送个原来进程组的信号仍然可能发送给这个进程,所以进程将调用setpgrp(),使自身脱离原来的进程组,而成为一个新的进程组的领导者。到此为止,进程已经完全调整好自身的“位置”。我们在图10.2中完整的说明了这个过程。但是与进程相关的还有一些环境问题。进程还要继续完成一些操作。
4. 关闭所有的文件描述符
在父进程创建子进程时,它已经打开的文件或套接字描述符的副本将在子进程中继承。因此此时进程所继承的描述符集合与它是如何启动有关。
在Linux中,文件和套接字对象都使用引用技术的机制,保留描述符打开实际上是浪费资源,并且可能会干扰别的进程工作,比如进程A试图删除文件F,但是文件F在我们的服务进程中保留打开状态,则文件将没有实际的删除,而是在服务进程退出后,文件F才被从磁盘中删除。
所以服务进程必须关闭它所继承的文件描述,从而避免不必要的消耗资源。
max_fd=sysconf(_SC_OPEN_MAX);
for(i=0;i
标准I/O描述符
由于此时服务进程(守护进程)已经不再同终端相关联,所以标准输出和标准错误文件描述符已经不再是标准状态。此时任何的printf,perror等输出语句都将出错,因为进程所有的文件描述符已经被关闭。
为了避免这种情况,我们可以打开一个特殊的设备,将进程的标准输出和标准错误描述符都重新定位在这个设备上。这个设备通常称为无伤害的设备(harmless device),它实际上什么也不做。但是可以避免由于程序员的疏忽将标准输出和标准错误描述符定位在其他有用的设备或文件上,而造成错误。
open("dev/null",O_RDWR);
dup(1);
dup(2);
上面的代码将打开一个”dev/null”的空设备,如果进程试图从这个设备上输入,则总是返回文件结束符,而进程对这个设备的输出,将被设备丢弃。因此,读或写操作都不会对设备产生危害。
这样,我们基本上将进程同其他进程的环境分离开。但是这样的进程还略有缺陷,我们将在后面说明这个缺陷。
8. 使用syslog来记录守护进程的错误
由于守护进程的0、1、2描述符已经被定位在空设备上。所以我们通常将利用syslogd提供的服务来记录守护进程的错误信息。
syslogd本身就是一个守护进程,它将使用UDP 514端口。应用程序可以发送UDP报文给514端口,syslogd将根据报文的优先级别进行不同的处理。
系统中为了应用程序使用的方便,提供了syslog函数,这个函数可以代替应用程序发送UDP报文。
#include
void syslog(int priority,const char *message,„);
其中priority表示报文的优先级,message是需要记录的内容。我们将报文的优先级列在表中。
表10.1 syslog报文优先级
优先级 含义
LOG_EMERG 非常紧急,该报文应当广播给所有用户 LOG_ALART 应当立即纠正的错误
LOG_CRIT 紧急的错误,常常是硬件错误 LOG_ERR 需要注意的错误
4
十 守护进程和超级服务器inetd
LOG_WARNING 警告,表示可能存在差错 LOG_NOTICE 需要注意的情况
LOG_INFO 某种信息报文
LOG_DEBUG 程序员用于调试使用的报文
在使用syslog进行错误信息的记录之前,需要先调用openlog函数说明进程所属的类型。在进程不再使用错误日志时,可以调用closelog关闭日志。
#include
void openlog(const char *ident,int options,int facility);
void closelog(void);
ident是一个字符串,它将被添加在信息message的前边,一同被syslogd记录。facility用于指明应用进程的类型。
表10.2 应用进程的类型
设施名 使用设施的系统
LOG_KERN 操作系统核心
LOG_USER 任何用户进程,即普通的应用程序 LOG_MAIL 电子邮件系统
LOG_DAEMON 守护进程
LOG_AUTH 授权和鉴别系统
LOG_LPR 打印机缓冲系统
LOG_RPC RPC和NFS系统
LOG_LOCAL0-LOG_LOCAL7 本地保留
Optioins是syslogd将采用的处理。
表10.2 syslogd采用的处理方法
options 含义
LOG_CONS 如果不能发送给syslogd,则将错误信息发送到控制台 LOG_NDELAY 不延迟打开套接字,而是立即创建套接字,发送消息 LOG_PERROR 将消息发送到标准错误文件中,同时发送给syslogd LOG_PID 在每个消息中增加进程的进程号
10.2 编程实践
我们将编写一个简单的守护进程。这个守护进程将等待在TCP端口9999上,当客户请求到来后,它将返回一个字符串,其中包含时间信息。
1. 程序源码
#include
#include
#include
#include
#include
#include
#include
#include
5
Linux网络编程 #include #include #include #include "unixnet.h" #include "netcomm.h" #include
int init_daemon(const char *pathname,int facility)
{
struct sigaction act;
int max_fd,i,ret;
char buf[100];
/*进行第1次fork,为setsid作准备*/
ret=fork();
if(ret<0){
fprintf(stderr,"error in first fork.\n");
exit(1);
}else if(ret!=0)
exit(0);
/*调用setsid,使得进程同旧会话过程分离*/
ret=setsid();
if(ret <0)
exit(1);
/*忽略信号SIGHUP*/
act.sa_handler=SIG_IGN;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGHUP,&act,NULL);
/*
*进行第2次fork,使进程不再是会话过程的领头进程,因而不能再打开
*终端作为自己的控制终端
*/
ret=fork();
if(ret<0)
exit(1);
else if(ret!=0)
6
十 守护进程和超级服务器inetd
exit(0);
/*修改进程的当前目录*/
chdir("/");
/*清除进程的文件掩码*/
umask(0);
/*使得进程脱离原来的进程组,不再受发往原来进程组的信号的干扰*/
setpgrp();
/*关闭进程所有的文件描述符*/
max_fd=sysconf(_SC_OPEN_MAX);
for(i=0;i0){
sprintf(error_buf,"The server create a new connection from
%s",inet_ntoa(cli_addr.sin_addr));
log_error_msg(LOG_INFO,error_buf);
}
now=time(NULL);
8
十 守护进程和超级服务器inetd
sprintf(send_line,"hello,it is time %s \r\n",ctime(&now));
writen(conn_fd,send_line,strlen(send_line));
close(conn_fd);
}
}
我们可以故意删除程序中的一行:
listen(listen_fd,LISTENQ);
我们查看/var/log/messages文件中将有下面的信息:
May 2 08:27:15 localhost ./my_daemon[609]: accept socket error
my_daemon是我们在程序中指定要加在消息的前边的字符串。由于我们没有调用listen将套接字转化成倾听套接字,则在这个套接字上的accept接收连接将失败。
我们加入上行程序,重新编译运行后,并使用telnet作为客户端程序telnet ++<端口号>可以观察输出的结果:
hello,it is time May 2 08:35:16
我们再次查看/var/log/messages文件,发现增加了下面的信息:
May 2 08:35:16 localhost ./my_daemon[655]: The server create a new connection from
127.0.0.1
如果我们再次运行这个守护进程,我们将在/var/log/messages文件中,增加了下面的信息:
May 2 08:37:15 localhost ./my_daemon[684]: bind server port
May 2 08:37:29 localhost ./my_daemon[689]: bind server port <两次运行./daemon>
这是由于当第2 个守护进程的副本运行时,由于端口资源已经被第1个副本进程占用了,所以进程将向syslogd登记错误信息。
这就是我们前面提到的这样的守护进程还有的缺陷。
2. 避免守护进程本身的多个副本相互干扰
由于网络守护进程通常将占用固定的端口资源和其他一些它所需要的资源,如果系统中同时运行多个服务进程,则可能它们之间将相互干扰。
为了避免这种干扰,我们可以使用某种互斥的手段,使得系统中同时没有2个相同的守护进程在运行。一种方法是每个进程在运行前先检测某个确定的文件是否存在,如果文件存在,则说明已经有一个守护进程运行了,所以进程退出。否则,如果进程发现文件还没有被创建,则它将继续运行。当进程退出后,它将删除这个文件。图10.3显示了这个设想。
但是这个方法有一点缺陷,如果系统异常崩溃,守护进程可能没有机会删除那个文件,则要是守护进程正常运行,需要在它运行之前,将该文件删除。(使用其他的互斥资源入信号量,也有类似的问题。)
我们可以使用锁文件的方式取代给创建文件的方式,两者的思路相似,但是对文件的加锁,将在系统崩溃时,自动的释放。所以更加安全。
9
Linux网络编程
Y
文件是否存在?进程退出
N
创建文件
......
删除文件进程退出
图10.3 创建文件的方法 3. 文件和记录的锁定
Linux系统的锁是咨询锁,所谓咨询锁,就是系统将告诉询问文件锁定情况的进程,当前文件的锁定情况,但是它并不阻止进程忽略文件的锁而继续进行读、写等操作,只要进程对文件有相应的权限。所以咨询锁必须在多个进程协同合作下才能正常工作。
Linux的锁分成共享锁(shared lock)和互斥锁(exculusive lock)。当一个进程读一个文件时,它只需要给文件加上共享锁,因为一个文件可以加上多个共享锁,这样允许多个进程同时对文件进行读取。而当进程希望对文件进行写操作时,它将试图给文件加上互斥锁,如果此时文件已经被加上了共享锁或者互斥锁,则进程将不能再获取互斥锁。它必须等待其他的进程释放对文件的加锁。
Linux中提供来两种给文件上锁的方式,给整个文件上锁和给文件的某个部分上锁(或称为记录上锁)。使用flock函数可以实现对整个文件的加锁。
#include
int flock(int fd,int operation);
fd是需要加锁的文件的描述符,operation可以区下列值:
LOCK_SH: 共享锁,多个进程可以同时拥有对文件的共享锁。
LOCK_EX: 互锁锁,一个文件同时只能上一把互斥锁。
LOCK_UN: 解锁操作。
LOCK_NB: 如果进程不能获取指定的锁,函数将不阻塞,缺省时,进程将睡眠等待。
这几个控制选项可以使用或操作进行组合。
另一个函数fcntl将提供更加完备的锁定功能,它可以支持对文件的部分的锁定。函数fcntl将使用数据结构struct flock。结构flcok定义如下:
struct flock{
short l_type;
short l_whence;
off_t l_start;
off_t l_len;
pid_t l_pid;
10
十 守护进程和超级服务器inetd
};
在结构中,l_type用于指定锁的类型,它可以是F_RELCK、F_WRLCK、F_UNLCK,分别是共享锁(读锁)、互斥锁和解锁操作。
l_whence指定如何使用l_start来设置锁定的起始位置,如果l_whence取SEEK_SET,表示
从l_start指定的位置作为锁定的起始位置;如果_whence取SEEK _CUR, 表示锁定从当
前文件指针的位置加上l_start开始;如果_whence取SEEK _END, 表示锁定从当前文件
的结束位置加上l_start开始;
l_len表示锁定区域的长度。
l_pid表示进行锁定的进程的进程号。
使用fcntl进行文件加锁,通常按照下面的步骤:首先填充结构中的参数,然后以匹配的方式打开文件,最后调用fcntl进行文件加锁。下面的程序示例这个过程: struct flock file_lock;
int fd;
file_lock.l_type=F_WRLCK;
file_lock.l_whence=SEEK_SET;
file_lock.l_start=0;
file_lock.l_len=10;
fd=open("mylock.lock",O_WRONLY); fcntl*fd,F_SETLKW,&file_lock);
......
file_lock.l_type=F_UNLCK;
fcntl(fd,F_SETLK,&file_lock); 4. 在锁文件中增加进程的进程号
在锁文件加入守护进程的进程号,这个操作不是守护进程所必须的,但是,它可以为系
统管理者带来方便。因为一个负载很重的服务器运行的守护进程可能很多,当某个进程
发生异常故障时,系统管理员可能需要一段时间,才能将错误准确的定位在某号进程上,
但是如果个守护进程都将其进程号记录在锁文件中,在发生异常时,系统管理员将可以
迅速的定位问题。
5. 修改init_daemon函数
我们对函数init_daemon进行了修改,增加控制守护进程多个副本,以及记录在锁文件
中记录守护进程号的工作。
#define LOCKFILE "/root/mylock.lock"
int init_daemon(const char *pathname,int facility)
{
struct sigaction act;
int max_fd,i,ret;
int lock_fd;
11
Linux网络编程
char buf[100];
/*打开一个锁文件*/
lock_fd=open(LOCKFILE,O_RDWR|O_CREAT,0640);
/*如果不能获取这个文件,说明文件不存在并且无法创建这个文件*/
if(lock_fd<0)
exit(1); /*error occurred in opening file*/
/*试图对文件进行互斥锁定*/
ret=flock(lock_fd,LOCK_EX|LOCK_NB);
if(ret<0){
/*如果失败则退出,说明已经有一相同的守护进程在系统中运行*/
fprintf(stderr,"can not obtain the file lock.\n");
exit(0); /*could not obtain a lock*/
}
ret=fork();
if(ret<0){
fprintf(stderr,"error in first fork.\n");
exit(1);
}else if(ret!=0)
exit(0);
ret=setsid();
if(ret <0)
exit(1);
act.sa_handler=SIG_IGN;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGHUP,&act,NULL);
ret=fork();
if(ret<0)
exit(1);
else if(ret!=0)
exit(0);
chdir("/");
umask(0);
12
十 守护进程和超级服务器inetd
setpgrp();
/*在锁文件中记录下,进程的进程号,方便系统管理*/
sprintf(buf,"%6d\n",getpid());
write(lock_fd,buf,strlen(buf));
max_fd=sysconf(_SC_OPEN_MAX);
for(i=0;i设计 了一个超级服务器,之所以称为超级服务器,是因为它能够处理多种服务。典型的FTP、Telnet、tFtp、Rlogin等等服务都是由这个超级服务器来间接的提供的。
我们容易发现,上面提到的服务的初始化过程都非常的相似,一般都是创建一个套接字,然后将这个套接字绑定在某个知名的端口上,再将它转化成倾听套接字,接着在那个端口上等待。(这个端口可以是TCP或者UDP,两者的差异不大。)当于客户进程建立连接后,将创建一个服务器的子进程对其进行服务,而服务器父进程将继续在倾听套接字上等待。
我们完全可能编写一个程序,由它来实现这些服务器的初始化工作。
并且,我们还可以发现上面提到的这些服务,实际上在大部分时间中,服务器都是处于
13
Linux网络编程
睡眠等待的状态,没有必要每个服务器都对网络进行检测,而可以使用专门的服务器进行检测,当它检测到客户端的连接时,再根据接入端口的不同,来启动具体的服务器。超级服务器的正是基于这样的考虑而提出的。
10.3.2 超级服务器使用的配置文件
从上面的分析,我们知道超级服务器必须有一种方法,将它在网络接口上检测到的客户连接于一个具体的服务器联系起来的方法。在Linux中,这是通过超级服务器的配置文件来实现,我们列出了文件/etc/inted.conf中典型的几行:
ftp stream tcp nowait root /usr/bin/ftpd ftpd –l
login stream tcp nowait root /usr/bin/rlogind rlogind –s
telnet stream tcp nowait root /usr/bin/telnetd telnetd
tftp dgram udp wait nobody /usr/bin/tftpd tftpd –s
/tftpboot
我们将说明文件中个行的含义:
(1) 服务的名称:这个服务的名称必须在/etc/services文件中定义。
(2) 套接字的类型:套接字是流套接字还是倾听套接字类型。
(3) 使用的协议的类型:tftp使用UDP协议,有的服务可能同时支持两种协议。
(4) 等待状态:如果指定nowait,超级服务器将允许指定的服务并发,它将在那次
服务未完成时,启动下一次服务。否则,该项服务将不能并发。一次只能服务于
一个客户进程,下一个客户进程只能等到本次服务结束才能被服务。
(5) 用户名:服务器将以该用户名对应的权限运行服务器。因为inetd是具有root
权限的进程,而有的服务可能是某个用户自己编写的,则不应当或没有必要以
root的权限运行。Inetd将根据这个记录在exec子进程时修改进程的运行权限。
(6) 服务器的可执行文件的位置:inted使用这一项定位服务器程序的位置。
(7) 程序的参数:inted将在exec函数中使用这行参数。
10.3.3 inetd处理并发服务的过程
inetd将使用select检测在文件/etc/inetd.conf文件中说明的所有的TCP/UDP端口。当某个端口上有客户的连接时,父进程将创建子进程。子进程中关闭倾听套接字描述符,父进程关闭连接套接字,而后父进程继续检测。
inetd处理并发服务的过程同我们说明的并发服务器的过程类似,不同的只是,并发服务器调用fork来创建子进程,这个子进程是自身的副本。而inetd是服务的接入者,而不是服务的直接提供者,它在创建子进程后将使用exec装入提供具体服务的程序,来创建一个提供具体服务的子进程。
但是在exec之前,inetd必须将进程的运行权限修改成配置文件中指定的权限,而不是在fork之后的root进程权限。还有,为了便于服务进程的编写,inetd将把套接字描述符复制到进程的标准输入、标准输出、和标准错误描述符上,这样服务程序的编写,就可以象
14
十 守护进程和超级服务器inetd
在本地程序编写一样方便。
inetd将等待其子进程的终止。
21连接请求
23
UDP端口inetd父进
程
(1)inetd使用select并发检测各个TCP/UDP端口
图10.4 inet处理并发服务的过程1
inetd子进
程fork
设置权限
exec具体服务
准备文件描述符
inetd父进
程
(2)inetd使用配置文件,创建一个子进程,创建中需要进行权限修改等
图10.5 inetd处理并发服务的过程2
21客户进inetd子程进程23
UDP端口
inetd父进
程
(3)inetd子进程提供服务,inetd父进程继续检测。(对于wait的服务,父进程将把那个端口从检测集合中删除,并在服务结束后重新加
入端口因为那个服务必须等待本次服务结束后,才能接入下次服务)
图10.6 inetd处理并发服务的过程3
7. inetd对不允许并发的服务的处理
15
Linux网络编程
UDP请求21
23
UDP端口inetd父进
程
(1)inetd使用select并发检测各个TCP/UDP端口
图10.7 inetd处理非并发服务过程1
21客户进inetd子程进程23
UDP端口
inetd父进
程
(2)inetd服务器接入客户请求,但是将该UDP端口从select集合中删除
图10.8 inetd处理非并发服务过程2
查表进程号端口号
......inetd父进
程
(3)inetd父进程收到来自SIGCHLD信号子进程的SIGCHLD信号,查
inetd子表将端口号重新加入
进程select检测集合
21
23
UDP端口inetd父进
程
图10.9 inetd处理非并发服务的过程
对于在文件中标明wait的服务,父进程将在接收到一个客户间接后,把那个端口从
16
十 守护进程和超级服务器inetd
select的检测集合中删除,这样,即使有其他的客户进程对该端口发起请求,服务也不被接入。当服务结束后,inetd在收到对应子进程的SIGCHLD信号后,将查表得到该进程对应的端口号(当父进程创建子进程时,将保子进程号和对应的端口号记录在一张表中),而后在select检测集合中重新加入那个端口。
图10.7、10.8、10.9显示了这个过程,其中虚线表示并非实际的连接。
完整的超级服务器的过程如图10.10所示。如果超级服务器收到SIGHUP信号,它将读取配置文件,重新初始化。当系统管理员修改超级服务器的配置文件后,就是发送SIGHUP信号使超级服务器重新启动。
重新读取配置文件,并
重新初始化
信号SIGHUP读取配置文件
创建倾听套接字
设置需要检测的套接字描述符集合,检
测它们的读就绪
接收连接
创建子进程
复制套接字描述符到0、关闭连接套接字描述符1、2
设置执行的权限检测子进程退出
SIGCHLD
执行具体的服务程序
图10.10完整的超级服务器的工作过程
由于超级服务器已经将套接字描述符复制在文件描述符0、1、2,所以服务器的程序将很简单,可以象本地程序那样从标准输入文件描述符中输入数据,并将输出结果写向标准输出文件描述符中。
int main(int argc,char **argv)
{
char send_line[100];
17
Linux网络编程
time_t now;
now=time(NULL);
sprintf(send_line,"hello,it is time %s \r\n",ctime(&now));
writen(0,send_line,strlen(send_line)); }
我们必须在文件/etc/services中增加一项:
my_daemon 9999/tcp
并在超级服务器的配置文件中增加一行:
my_daemon stream tcp nowait /home/linyu/example/daemon/dmon dmon
本章小结
编写守护进程的关键是,要将守护进程同普通进程运行的环境分离开。这个过程入下:
(1) 进程第1次fork,为进程调用setsid作准备。
(2) 进程调用setsid,进程成为新的会话过程的领头进程。
(3) 忽略信号SIGHUP,第2次fork,使进程成为一个新的进程组的领导者。
(4) 关闭所有的文件描述符。
(5) 消除umask的影响。
(6) 修改守护进程的当前目录。
(7) 重新定位标准I/O描述符。
(8) 保证服务器的互斥运行。(在锁文件中记录进程的进程号)
(9) 使用syslog来记录守护进程的错误信息。
18