xinetd超级服务器进程

晨曦之光 发布于 2012/04/10 15:00
阅读 405
收藏 0

xinetd的实质和inetd差不多,只不过增加了访问控制功能,更加安全了。实际上xinetd就是一个超级服务器,是个把门的守护者,在linux 丰富的网络应用中实现单点验证,它继承了unix的传统工作方式,只提供机制,而将策略留给用户,正如unix的一个很重要的特性所说,一切都是可以配置 的。它的策略配置方式就是修改配置文件,在linux中不出意外的话,任何程序都要有一个配置文件,在这个配置文件中,用户可以自己定义该程序的行为,这 样的话,用户的技术门槛降低了,但是又不至于过低,于是管理linux的过程也就成了管理配置文件的过程,配置文件大多都是标准的文本格式,提供了最好的 兼容性,时隔几十年,只要最基本的编码体制不变,这些文件都保证是可读的,不像那些商业软件中,比如word,如果没有特定的程序,文件是无法被以可读形 式打开的。当然xinetd也有其配置文件,在/etc目录下,本文不讲xinetd的用法,需要的话,可以查阅相关手册。
     xinetd的工作方式很简单,就是从其配置文件中得到它要代理的网络程序,得到该网络应用比如telnet需要监听的端口,然后代替该应用监听此端口, 当然不仅仅是例子中的telnet,任何需要它代理的网络应用都可以配置进xinetd,然后由它帮忙,今天就以telnet为例。telnet的监听端 口是23,当xinetd监听到23号端口有连接的时候,就会fork出一个子进程,将监听得到的套接字描述符传递给子进程,然后在父进程关闭该套接字, 在子进程中清除掉该套接字的close-on-exec标志,最终exec真正的telnetd进程,这个telnetd进程负责与客户端的套接字通信, 这个telnetd其实很多时候就是in.telnetd进程,该进程不负责监听(debug模式除外),它只负责从xinetd接受xinetd传来的 套接字,然后通过这个套接字与客户端通信,如果你把xinetd杀了,那么除了当前已经连接的telnet客户端外,别的客户端就再也连接不上服务器了, 因为没有进程监听23号端口了,xientd怎么知道在监听到23号端口后要exec的进程是in.telnetd呢?当然是配置文件的作用 了,xinetd在启动的时候会将配置文件的配置信息都读进来的。
     linux的fork和exec机制可以非常简单的处理掉如何传递套接字的难题,套接字其实就是一个文件描述符罢了,在linux中,进程打开的文件描述 符并不是进程的内禀属性(见《fork进程时资源的深拷贝和浅拷贝》),因此它们并非和进程死死绑在一起,多个进程之间完全可以共享之,另外linux的 fork时的拷贝机制使得一切成为了可能,拷贝的时候就连文件描述符一块给拷贝了,注意文件描述符是浅拷贝,因为父子不同进程间将共享相同的文件描述符, 共享不会造成同步问题吗?仔细想想,因为父进程只负责监听,在fork子进程之后,监听accept得到的描述符就没有用了,完全可以关闭 了,xinetd的作用就到此为止了。进一步,exec只是替换进程的地址空间,对进程打开的文件描述符丝毫没有影响,原来打开的描述符只要没有 close-on-exec标志,在exec后依然存在,因此使得xinetd作为监听代理,一旦得到新的连接,立马exec新的服务进程并且将连接的套 接字传递给exec的子进程是可行并且非常简单的。这里可以看出,linux的细粒度设计又一次使得用户捞到了实惠,在windows下,很多事情都是 Win32API给你做好的,现成的,当然方便是方便了,可是你即便想改也改不了了,所以说有人说linux可以玩,而windows只可以用。玩 linux我们玩的是积木,用windows,我们用的是成形的不能拆卸的产品,当然对于linux的商业用户可能不会这么认为,我所针对的仅仅是 linux的玩家。
原理明白了以后,我们看一下xinetd的源代码,在xinetd的main函数中最终要进入一个无限循环,其实就是一个select的无限循 环,select的对象就是所有需要监听的套接字描述符,当发现有连接的时候会调用svc_request函数:(分析代码为了节省篇幅,省略错误处理, 毕竟错误处理只在运行时才有用)
void svc_request( struct service *sp )
{
   connection_s *cp ;
   status_e ret_code;
   cp = conn_new( sp ) ;  //得到一个连接,内部调用了accept函数,结构体connection_s包含了一个套接字描述符
...
   else
      ret_code = svc_generic_handler(sp, cp);  //普通的连接
...
}
status_e svc_generic_handler( struct service *sp, connection_s *cp )
{
   if ( svc_parent_access_control( sp, cp ) == OK ) {
      return( server_run( sp, cp ) ) ;   //实际执行真正的服务器进程
   }
   return( FAILED ) ;
}
status_e server_run( struct service *sp, connection_s *cp )
{
   struct server    server ;
   struct server   *serp = NULL;
   const char      *func = "server_run" ;
   CLEAR( server ) ;
   server.svr_sp = sp ;
   server.svr_conn = cp ;
...
   serp = server_alloc( &server ) ; //分配一个服务器结构体,该结构体用于传输参数
...
   if ( server_start( serp ) == OK )
   {
      if( !SVC_WAITS(sp) )
         CONN_CLOSE( cp ) ;  //关闭cp描述符
      return( OK ) ;
   }
...
}
status_e server_start( struct server *serp )
{
   struct service   *sp = SERVER_SERVICE(serp) ;
   const char       *func = "server_start" ;
   SERVER_LOGUSER(serp) = SVC_LOGS_USERID_ON_SUCCESS( sp ) ;
   SERVER_PID(serp) = do_fork() ;  //fork子进程
   switch ( SERVER_PID(serp) )
   {
      case 0:
         ps.rws.env_is_valid = FALSE ;
         child_process( serp ) ;  //子进程实际处理这个连接,注意父进程accept的套接字已经传输给了子进程
         _exit( 0 ) ;
...
         return( OK ) ;
   }
}
void child_process( struct server *serp )
{
   struct service          *sp  = SERVER_SERVICE( serp ) ;
   connection_s            *cp  = SERVER_CONNECTION( serp ) ; //得到套接字
   struct service_config   *scp = SVC_CONF( sp ) ;
   const char              *func = "child_process" ;
   signal_default_state();
...
   if ( ! SC_IS_INTERNAL( scp ) )
   {
...
      else
      {
         exec_server( serp ) ;  //exec真正的服务器。
      }
   }
...
   _exit( 0 ) ;
}
void exec_server( const struct server *serp )
{
   const struct service_config *scp = SVC_CONF( SERVER_SERVICE( serp ) ) ;
   struct rlimit rl ;
   int fd ;
   int descriptor = SERVER_FD( serp ) ; //descriptor就是真正的套接字描述符,一点也没有封装
   const char *server = SC_SERVER( scp ) ;
   const char *func = "exec_server" ;
...
   for ( fd = 0 ; fd <= MAX_PASS_FD ; fd++ )
   {
      if ( dup2( descriptor, fd ) == -1 ) //重定向0,1,2文件描述符,因为linux的进程中,在守护化之前0,1,2这三个是必然打开的,这里用到了一个小技巧,因为在xinetd服务器 中,accept的套接字描述符的号码是不确定的,要直接用这个描述符的话就要想办法将这个描述符用进程间通信的方式传给exec的真正服务器,这样二者 的偶合性就剧增了,于是约定用标准输入/输出来重定向套接字描述符,反正它们都是守护进程,用不到这三个描述符。
...
   }
...
   (void) Sclose( descriptor ) ;//重新定向过了,原来的就没有用了
   (void) execve( server, SC_SERVER_ARGV( scp ), env_getvars( SC_ENV( scp )->env_handle ) ) ;
...
   _exit( 0 ) ;
}
以上这样就是xinetd的大体过程,在它最终exec的in.telnet中就会将文件描述符0作为套接字描述符,正是因为前面重定向的结果,在in.telnet的debug打开的情况下,它本身也还是可以监听23号端口的。
     linux中的xinetd的过程其实很简单,就是fork+exec中间传递一些套接字文件描述符,这个描述符传递的工作实际上是很透明的。它的意义就 是实现了单点验证,对于安全性,它确实安全了很多,同时也减少了系统的开销,比如没有telnet客户的时候,服务器就没有必要起一个telnetd守护 进程,也没有必要启动什么ssh之类的,若干的监听进程如今只变成了一个xinetd,大大减少了服务器的进程数量,但是是否会出现单点故障呢?当然会出 现了。是否在大负载下会损失性能呢?当然会了。这些问题都比较好解决,谁让linux很灵活呢?我们完全可以引入热备份,负载均衡的概念来解决之,在较简 单的情况之下,简单的多线程就可以解决问题。
于是不要害怕linux在某些情况之下也会有问题,是系统就会出问题,linux细粒度设计带给我们的是,一旦出现了一个问题,那么就会有十倍之多的解决办法。


原文链接:http://blog.csdn.net/dog250/article/details/5303657
加载中
返回顶部
顶部