Spyrius

类别:编程语言 点击:0 评论:0 推荐:

   Spyrius是Mark Lindner先生写的一个小程序。Mark Lindner先生曾经写过pingutil和CFL几个工具包。我是从GNU找到Mark

Lindner先生的。希望这里看客经常去GNU的人也很多,并都得到自己的收获和快乐。

   Spyrius是一个多线程的、通用的一个超级守护进程(SuperDaemon)。从事Linux/Unix的人员应该清楚Daemon的具体含义,模糊

的话可以想象一下inetd所做的工作。

   Spyrius在一个TCP端口进行侦听来自客户端的连接,不像一些传统的Daemons,必须产生一个新的子进程将来为每一个客户端连

接来进行服务(Service)。Spyrius是一个基于多线程的工具,利用每一个线程来处理请求。用线程替代进程的好处之一就是共享

了进程的相关堆栈资源等,避免了进程创建所带来的消耗。

   Spyrius另外的一个特点是将特定的功能服务进行模块化。譬如网络通信、解析报文、创建线程、等等被设计成为plug-in模块,

可以被独立的编写和调试。你可以通过配置文件组合自己的Spyrius。
   Plug-In模块通过一组API和Daemon来进行交互,当然,也提供了相应的API和Client来通信。

    Spyrius主程序在Spyrius.c文件中,默认指定了管理端口(SPYR_ADMIN_PORT)8381和服务端口(SPYR_SERVER_PORT)8380。主程序

从命令行读取配置参数,其中包含:
 -a 指定管理端口,即SPYR_ADMIN_PORT
 -p 指定服务端口,即SPYR_SERVER_PORT
 -t 指定任务运行超时时间
 -w 指定最大工作的Worker线程数量。
 -c 读取相应的配置文件。
   当然,如果没有使用命令行参数的话,Spyrius带有默认缺省指定的。
   87-174行使用getopt处理完成命令行参数后,程序开始创建守护进程,进程首先fork,父进程退出程序,子进程调用

spyr_daemon_init来完成一些操作变成守护进程。
193:  if(!spyr_daemon_init(sp, ap))
   我们进入spyr_daemon_init里面,此时文件变成Daemon.c,从名字也可以看出这个文件代码的主要功能。在93行是

spyr_daemon_init函数。第一步,调用openlog,开启log功能,为后续的信息输出做好准备。因为程序变为守护进程后,没有控制

终端,所以无法使用stdout,stderr来进行输出。创建守护进程的步骤,Stevens先生已经提到,相信各位看官也熟悉,我这里也不

再絮叨。
   接下来,spyr_daemon_init函数进行服务端口和管理端口的创建。分别是spyr_socket_create和spyr_socket_create调用。
完成这两步后,进行了进程的信号MASK设置和安装信号处理器,相关代码如下(Daemon.c:125-138):
main.c
125: /* set up signal mask (will be inherited by other threads) */
126:
127:  sigfillset(&(act.sa_mask));
128:  pthread_sigmask(SIG_BLOCK, &(act.sa_mask), NULL);
129:
130:  act.sa_handler = spyr_fault;
131:
132:  for(i = 0; i < SPYR_MAX_SIGS; i++)
133:    sigaction(spyr_daemon_sigs[i].sig, &act, NULL);
134:
135:  /* start sig listener */
136:
137:  pthread_create(&spyr_daemon_sigthread, NULL, spyr_daemon_sigwait,
138:   NULL);
其中,137-138代码部分创建了一个线程Listener,它去执行spyr_daemon_sigwait函数代码,目的就是去等待
信号,并进行信号判断,如果信号值是SIGHUP,那么将系统重新启动,如果是SIGTERM,那么将系统关闭。这就是所有

spyr_daemon_int完成的功能。列表如下:
1、调用openlog启动syslog功能
2、创建服务端口、管理端口
3、安装信号处理程序,并制定一个线程(spyr_daemon_sigthread)来处理相关信号
   完成spyr_daemon_init后,程序运行spyr_worker_int来进行worker结构资源的申请。worker_t的结构如下:
typedef struct worker_t
  {
  int active;  ///<此线程worker目前的状态
  pthread_t thread;
  pthread_attr_t attr; ///<此线程worker属性结构
  int sd;  ///<此线程worker操作的连接描述字
  time_t when; ///<此线程worker本次任务开始执行时间
  char addr[SPYR_MAX_ADDRLEN]; ///<此线程worker应答的客户端地址
  char label[SPYR_MAX_LABELLEN];
  int timeout; ///<此线程worker任务超时时间值
  int module;
  int logtype;
  FILE *logendpoint;
  } worker_t;
   申请完成worker资源后,调用spyr_socket_init(),目前此函数并没有实质代码。然后是调用spyr_module_int。
   看官老爷注意,在spyr_module_init中,Spyrius完成了动态plug-in的功能。在spyr_module_init中,调用

spyr_module_parseconfig来对命令行-c指定的配置文件进行格式化处理,当然,这部分少不了文件IO和字符串处理,不过
我觉得Mark Lindner先生字符处理的部分写的有些乱,呵呵~
   在这里,大致的意思是要为plug-in的模块进行设置,相关结构如下:
typedef struct module_t
  {
  int enabled; ///<此模块是否启动
  char name[12]; ///<模块名称
  char login[12]; ///<模块是否需要注册名称
  char passwd[12]; ///<模块名称匹配的密码
  char file[128]; ///<模块所在的文件(动态库文件)
  char funcname[40]; ///<模块暴露的函数名称(dlsym要挂接的函数名)
  void *handle; ///< dlopen后返回的值
  char *env; ///<运行所需的env
  int (*func)(worker_t *); ///<dlsym返回函数指针
  pthread_mutex_t mutex; ///<线程的互斥对象
  } module_t;
  可以看到,每一个设置的plug-in,都被填充到一个modult_t结构中。这就是spyr_module_init和spyr_module_parseconfig所作

的工作。
   回到main.c中,此时已经完成module_int,接下来是spyr_admin_init。这是完成管理线程部分的工作的代码。
main.c
00217:   if(!spyr_admin_init())
   管理线程其实也是用worker_t结构,不过创建线程时,指定的任务是spyr_admin。参看:
Admin.c
0090  pthread_create(&(admin.thread), &(admin.attr), (void *)spyr_admin, NULL);
   在spyr_admin中,有大量的代码是用来和CLIENT来通信的。我们可以通过连接到管理端口来查看当前配置的plug-in列表,同时

可以指定某一个plug-in的当前运行的线程状态、是否重新加载、是否停止等。这都是在spyr_admin中完成的。看官老爷注意的一点

是,当要对某一线程进行操作时,代码使用spyr_worker_locktable和spyr_worker_unlocktable来对线程进行锁定,以免此过程出

现不同步的错误。
   spyr_admin完成后,main.c中就剩下最后一个spyr_listen函数,此时Spyrius主进程也就绪,进入服务端口accept状态。
Listen.c
0101 void spyr_listen()
0102   {
0103   int cs;
0104
0105   for(;;)
0106     {
0107     if((cs = spyr_socket_accept(ms)) < 0)
0108      continue;
0109
0110     else if(spyr_worker_create(cs) < 0)
在spyr_socket_accept中使用一个accept来进入阻塞状态,如0184行代码所示。
Socket.c
0178 int spyr_socket_accept(socket_t *s)
0179   {
1080   int cs;
0181  
0182   /* accept a connection */
0183
0184   if((cs = accept(s->sd, (struct sockaddr *)&(s->sin), &(s->slen))) < 0)
0185     {
0186    syslog(LOG_WARNING, "accept(): %s", strerror(errno));
0187    return(-1);
0188     }
0189
0190   spyr_socket_unblock(cs);
0191  
0192   return(cs);
0193  }
看官老爷注意0190行的spyr_socket_unblock函数,此函数将accept返回的cs进行Unblock处理,也就是将描述字设置为非阻塞状
态,此过程虽然简单,但在网络服务器中,使用非阻塞IO(Non-Block IO),相比阻塞IO,效果却是非常明显。尤其是同时要处理多

个连接时。我曾经写过一个非阻塞IO的的程序测试,Server接受Client的大数据传送信息,同时将收到的信息进行写文件操作,利

用单缓冲来做的,将Socket描述字和文件描述字设置为非阻塞+Select后,比用之前时间减少了数倍。
     spyr_listen完成accept后(同时设置端口为Non-Block),spyr_worker_create创建worker线程。我们跟进去看一下:
Worker.c
0096 int spyr_worker_create(int sd)
  {
  int worker;

  spyr_worker_locktable();

  if(nworkers == config.workers)
    {
    spyr_worker_unlocktable();
    return(-1);
    }

  /* find free worker slot */

  for(worker = 0; worker < config.workers; worker++)
    if(!(workers[worker].active)) break;

  /* set up worker entry */

  workers[worker].sd = sd;
  workers[worker].active = 1;
  workers[worker].when = time(NULL);
  workers[worker].timeout = SPYR_DFL_TIMEOUT;
  workers[worker].logtype = SPYR_LOG_SYSLOG;

  if(!spyr_socket_getpeername(sd, workers[worker].addr, SPYR_MAX_ADDRLEN - 1))
    strcpy(workers[worker].addr, "<unresolved>");

  strcpy(workers[worker].label, "-");

  pthread_attr_init(&(workers[worker].attr));
  pthread_attr_setdetachstate(&(workers[worker].attr),
                              PTHREAD_CREATE_DETACHED);

  /* spawn worker */

  pthread_create(&(workers[worker].thread), &(workers[worker].attr),
   (void *)spyr_server,
   (void *)&(workers[worker]));
 
  nworkers++;
 
  spyr_worker_unlocktable();

  return(worker);
0155  }
   由于篇幅太长,我就没有标记行号。我们看到,首先,spyr_worker_locktable加锁,防止全局变量config.workers变化,判断

当前workers数目nworkers是否达到配置指定的最大的数目。如果达到,那么此连接将被关闭,返回信息"Too Busy"。没办法,当前

worker都在忙,呵呵。当还有空闲的worker资源可用时,对workers所有的“槽位”进行扫描。用“槽位”来描述,看官老爷觉得形

象么?当找到空闲的“槽位”(workers.active为false),则进行相关设置,譬如Socket描述字,超时时间等等。接下来新创建的线

程放到这个“槽位”中,并且被指定任务为spyr_server。当然,此时的nworkers要++。然后解锁,这个锁的时间够长:)
   来看spyr_server:
Listen.c
0124 void *spyr_server(worker_t *self)
   我是越来越偷懒了,代码都懒得贴了,呵呵。 继续说,spyr_server代码伊始,对当前工作的线程进行取消点(cancellation)相

关设置,有两个函数:
0132  pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &x);
0133  pthread_setcanceltype(PTHREAD_CANCEL_DEFERRED, &x);
   对取消点(cancellation)不了解的老爷们可以看看PosixThread手册。
接着,进行线程退出时的回调设置:
0137  pthread_cleanup_push((void *)spyr_worker_cleanup, (void *)self);
   好,马上看一下spyr_worker_cleanup做了些什么:
Worker.c
0170 void spyr_worker_cleanup(void *arg)
0171  {
0172  worker_t *w = (worker_t *)arg;
0173
0174  spyr_worker_locktable();
0175
0176  w->active = 0;
0177  spyr_socket_shutdown(w->sd);
0178  nworkers--;
0179
0180  pthread_attr_destroy(&(w->attr));
0181
0181  spyr_worker_unlocktable();
0182  }
   呵呵,当线程工作完成后,将所占的worker“槽位”设置为0(w->active = 0;),表示空闲,然后将Socket端口关闭

(spyr_socket_shutdown),将nworkers--,保证有新的请求来的时候,不会判断出错(还记得spyr_worker_create开始的判断么?)

,最后将所占的“槽位”信息进行destoryp(thread_attr_destroy)。
   好!到这里,我们已经看清了一个worker线程的创建和消亡过程。
   spyr_server剩下的部分,大致是执行动态加载的plug-in部分的代码和进行Socket描述字IO了,不在进行描述。在函数的最后部

分使用
0336  pthread_cleanup_pop(1);
0338  pthread_exit(NULL);
将此线程作了个善终。
   至此,Spyrius代码处理框架已经完毕。
   Spyrius一个特点是利用线程代替子进程来处理来自客户端的连接,此一个好处是可以避免进程间通讯所带来的消耗,其次是避

免创建新进程产生所带来的消耗。但缺点也是明显。任何的一个worker线程的不稳定,将导致整个Spyrio的Collapse。尤其是当

worker线程执行用户代码时,很难预料到客户程序员会写出什么样的代码(指plug-in部分引起的错误和不稳定)。
   Spyrius另一个缺点是,没有预先Spawn一批worker线程。(当然,线程的创建也许速度是很快的。)如果能开始就Spawn一些

Worker线程,那么系统的效率将会更高。
   同样,在worker线程的退出上,考虑也不是很完善。应该考虑以下问题:是否完成当前任务后,worker线程就应该退出?还是达

到某些条件后在退出(超时?低于固定数量的worker)?所有这些,也许应该用一个“线程池(ThreadPool)”来描述,Spyrius缺乏一

个好的线程池。
   和Mark Lindner有过几次E-mail交流,自己也曾说过要完善这几个地方,可惜一年多的光景过去了,不但一点代码没有写,

Spyrius我几乎都要忘记了,写这篇文章,做些纪念吧!:(
链接:
Spyrius在FreshMeat: http://freshmeat.net/projects/spyrius/

原文来自我的BLogDriver:http://manari.blogdriver.com

本文地址:http://com.8s8s.com/it/it27742.htm