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