第6章 网络服务器的设计模式.docx
《第6章 网络服务器的设计模式.docx》由会员分享,可在线阅读,更多相关《第6章 网络服务器的设计模式.docx(31页珍藏版)》请在冰豆网上搜索。
第6章网络服务器的设计模式
第6章网络服务器的设计模式
6-1概述
第3章和第4章中讨论的是最基本的TCP服务器和UDP服务器:
串行单用户服务器,本章将讨论服务多客户的网络服务器的设计模式。
因为大多数网络服务器都是多进程或多线程应用,因此在讨论网络服务器的设计模式之前先介绍进程和线程的基本概念、编程接口以及程序设计方法。
6-2多进程环境下的网络编程
6-2-1进程的基本概念
在传统的多任务系统中,“进程”是一个很重要的概念,在系统中进程是程序执行的实体,它由一组机器指令(也称为“正文”)、数据以及堆栈组成。
当一个进程需要由另一个实体执行某项任务时,该进程就派生(fork)一个子进程,让子进程去进行处理。
当一个程序按多任务组织时,在多任务操作系统核心的调度下,系统中许多进程在并发执行,这些进程是同一程序的不同体现。
每个进程按自己的一系列指令严格执行,各进程之间的指令空间是独立的,不用跳转到另外的进程去,它只会读写自己的数据与堆栈,不能去读写另外进程的数据和堆栈。
进程通过系统调用与其他进程和外界通信。
采用多进程的方式来实现处理多任务的程序主要有以下不足之处:
(1)用fork()创建子进程代价比较高。
操作系统需要将父进程的内存映像拷贝到子进程,同时还要复制所有打开的描述符等数据。
(2)父进程与子进程之间的信息交换需要用到进程间通信(IPC),如共享内存(SharedMemory),报文队列(MessageQueue),管道(Pipe),甚至网络通信等。
这种通信的代价要比同一地址空间内的变量共享高得多。
采用线程可以解决上面的问题。
6-2-2多进程下的网络编程
Unix系统中派生新进程的唯一的方法是调用fork()函数。
该函数的原型如下:
#include
pid_tfork(void);
fork函数的一个最重要的特性是:
调用一次,如果调用成功则返回两次:
在调用进程(父进程)中,它返回一次,返回值为新派生的子进程的进程ID号,父进程需要通过它来区别不同的子进程;在新派生的子进程中还返回一次,返回值为0,子进程可以通过getpid获取父进程的进程ID。
如果函数调用失败,则只在调用进程中返回-1。
fork函数的两个典型应用是:
(1)一个进程在处理一个操作时,同时还需要处理另一个操作,这时,进程就派生一个子进程来执行该任务。
大多数网络服务器就是这种应用的典型代表。
(2)一个进程在执行过程中,想执行另一个程序,它就通过fork产生一个自己的副本(子进程)。
再由子进程调用exec序列函数来代替自己去执行新程序。
shell程序中常采用这种方式。
在网络服务器中,最常见的使用fork函数的工作方式是:
父进程调用accept,当客户连接到达,accept成功返回时,调用fork派生一个子进程。
然后,父进程关闭accept返回的已连接描述符,继续在监听描述符上调用accept等待下一个连接请求。
而子进程则关闭父进程使用的监听描述符,使用accept返回的已连接描述符进行读写操作。
上述过程可以用程序6-1来表示。
程序6-1典型的调用fork的网络服务器程序结构。
/*其他变量定义*/
intsockfd,connfd;
pid_tpid;
sockfd=socket(...);/*创建一个新的插口*/
/*设置服务器地址结构变量代码*/
bind(sockfd,...);/*将服务器插口绑定到本地地址和端口*/
listen(sockfd,n);
for(;;)
{
connfd=accept(sockfd,...);/*阻塞等待*/
if((pid=fork())==0)/*if中的代码只在子进程中的执行*/
{
close(sockfd);/*子进程关闭父进程使用的监听插口描述符*/
/*通过connfd读写操作,任务处理*/
close(connfd);
exit(0);
}
if(pid==-1)prinf(″调用fork()派生子进程失败!
\n″);
close(connfd);/*在父进程中关闭子进程使用的已连接描述符*/
}
下面我们来讨论为什么要在父进程中关闭已连接描述符,而在子进程中要关闭监听描述符,以及这样的做法不会造成不良后果的原因。
当一个描述符正被某一进程使用,又被另一进程打开时,并不是真的创建新的内存I结点,而只是将该描述符的引用数加1。
调用fork()派生的子进程将继承父进程的所有打开的描述符(相当于子进程中又打开了一次),所以成功调用fork()后,父进程打开的所有描述符的引用数都将加1。
在程序6-1中,父进程调用close关闭子进程使用的已连接插口时,它只是将描述符connfd的访问计数值减1。
由于connfd的访问计数值仍然大于0,所以父进程中的这个close调用并没有使得内核启动TCP的四分组连接终止序列。
因而不会影响子进程中对connfd的读写操作。
同样子进程中的close调用也没有关闭父进程使用的监听描述符。
如果父进程不关闭accept返回的已连接插口,将会发生下列严重情况:
(1)父进程最终将用尽其可用描述符,因为任何进程中允许同时打开的描述符的数量是有限的。
(2)没有一个客户连接被终止。
这是因为,当子进程完成任务后调用close关闭其使用的已连接插口时,它的访问计数值只是由2变为1,连接并没有被关闭,资源仍被占用未释放。
如果子进程中不关闭父进程使用的监听描述符,则父进程调用close关闭监听描述符时也并没有真正关闭它,资源得不到释放。
在多进程应用中,如果一个进程确实要终止一个连接,但这个连接所对应的插口又被其他进程所使用,则必须通过shutdown而不是close来终止连接。
关于shutdown的使用在第2章中已经详细讨论过。
在程序6-1中,当子进程处理完所有任务时,它调用exit()退出。
但是,子进程中的exit()只能关闭所有打开的描述符以及释放其他的一些系统资源,对于系统核心中的进程控制块(PCB)表中的存放的进程信息(进程的ID、终止状态,以及子进程的资源利用信息等)却得不到释放。
这时,子进程将进入僵尸状态(Zombie),成为系统中的僵尸进程。
设置僵尸状态的目的是为了方便父进程处理子进程结束后的状态。
但是,僵尸进程占用内核空间,极端情况下将导致系统无法正常工作。
因此,父进程必须调用wait()函数,清除自己创建的子进程。
wait()函数的功能是让父进程获取子进程的结束状态信息并释放子进程在PCB表中的资源。
其函数原型如下:
#include
pid_twait(int*stat)
wait的返回值为子进程的进程号,传址参数stat被用来返回子进程退出时的状态值(exit()调用中的传入参数)。
如果子进程已退出,wait将把结束的子进程完全释放,并获得其返回值。
但是,如果子进程的执行没有结束,wait将阻塞调用它的父进程。
解决办法是在信号SIGCHLD中断处理子程序中调用wait或waitpid。
因为如果有子进程被中断,内核将给父进程发送SIGCHLD信号。
下面我们来看看调用wait函数的SIGCHLD信号中断处理子程序的程序结构,如程序6-2所示。
程序6-2调用wait的SIGCHLD信号中断处理子程序。
voidsigchld_handler(intsig)
{
pid_tpid;
intstat;
pid=wait(&stat);
/*处理stat的代码*/
return;
}
但是,上述中断处理子程序还存在着产生僵尸进程的可能性。
假定一个网络服务器同时为多个客户服务(服务器为每一个客户派生一个服务子进程),如果在某一时刻,有多个客户同时关闭连接,将导致各自的服务子进程几乎同时终止。
这些服务子进程的父进程也将几乎同时收到多个SIGCHLD信号,但是Unix信号一般是不排队的,并且系统能处理多少个几乎同时到达的信号是未知的,可能是一个,也可能来得及处理两个或更多。
正确的解决办法是调用Unix提供的另一个系统调用waitpid()而不是wait()。
waitpid的函数原型如下:
#include
pid_twaitpid(pid_tpid,int*stat,intoptions);
第一个参数pid指定所等待的子进程的进程ID,如果将其设为-1,则表示第一个终止的子进程。
第二个参数stat返回终止进程的退出状态,与wait函数中的一样。
第三个参数options指定函数waitpid等待的方式。
宏定义WNOHANG表示若无子进程结束就返回。
如果返回0,表示没有子进程终止,如果大于0则表示终止的子进程的进程ID。
修改后的SIGCHLD信号的中断处理子程序如程序6-3所示。
程序6-3调用waitpid的SIGCHLD信号中断处理子程序。
voidsigchld_handler(intsig)
{
pid_tpid;
intstat;
while((pid=waitpid(-1,&stat,WNOHANG))>0)
{/*处理stat的代码*/}
}
程序6-3将清除所有已经结束的子进程的残余部分。
因此,为了不产生僵尸进程,在多进程应用的主进程中一般要捕获SIGCHLD信号(加入语句signal(SIGCHLD,sigchld_handler)),信号处理子程序如程序6-3所示。
如果一个进程终止,且该进程有子进程处于僵尸状态,则所有僵尸子进程的父进程ID均置为1(关机进程init)。
init进程将作为这些僵尸进程的父进程负责清除它们。
总之,在多进程网络编程中,为了程序正确运行,要做到以下几点:
(1)在子进程中关闭父进程使用的插口,在父进程中关闭子进程使用的插口。
(2)在主进程中捕获SIGCHLD信号,在信号的中断处理子程序中调用waitpid函数完全释放服务子进程占用的资源。
6-3多线程环境下的应用程序设计
6-3-1线程的基本概念
线程(Thread),有时也称为轻进程(LightweightProcess),是程序中被执行的指令序列,它具备有程序计数器(PC)与保持局部变量与返回地址痕迹的堆栈等最少状态参数来控制它的执行。
线程是具有最少开销的程序实体,具有以下属性:
(1)线程是任务中程序流的控制点。
(2)任务中的线程可以访问该任务的所有资源,这些资源包括:
进程指令,大多数数据(全局数据),信号处理程序,工作环境信息(当前工作目录、用户ID和组ID等)。
同时,每个线程有自己的一些专用资源:
寄存器组(包括程序计数器和栈指针),线程堆栈(存放线程自己的局部变量和返回地址),优先级,线程ID,信号屏蔽掩码,全局错误代码errno。
(3一个线程可以与其他线程并行执行,甚至在一个任务中的所有线程都可并行执行。
(4)线程是具有最少状态的轻型实体,它的开销最低。
一般来说,创建线程要比创建进程快10~100倍。
并且线程之间的切换是在同一地址空间内进行的,因此切换开销也比较小。
在传统的多任务操作系统中,进程(process)是一个重要的概念。
在系统中,进程是程序执行的实体,它由一组机器指令、数据和堆栈组成。
各进程的指令是独立的,它只会读写自己的数据与堆栈而不能读写其他进程的数据与堆栈。
进程通过系统调用与其他进程以及外界通信。
有了线程的概念后,程序的执行单位就由传统的进程变为线程了,而进程则是作为资源的分配单位存在。
同时,同一进程内的不同线程之间的通信也变得简单了,因为它们都共享同一地址空间内的所有资源。
总之,多线程有以下优点:
改善应用响应时间,提高多处理系统的效率,改善程序结构,开销比较小,提高性能。
一般来说,根据线程所处的级别,可以将线程分为三种:
用户级线程,内核级线程和混合型线程。
用户级线程通常运行在一个已有的操作系统之上,进程中的线程对内核是不可见的。
用户级线程之间将竞争分配给进程的资源。
这些线程是通过运行时间(Runtime)系统即进程的部分代码来调度的。
使用用户级线程包的程序通常被链接到一个特殊的库中,在此库中的每一个库函数和系统调用被封装在一个外壳中,这个外壳代码能调用运行时间系统来执行线程管理。
用户级线程的优点是开销特别低,但它也有一些缺点:
CPU受限的线程几乎不执行系统调用或库调用;因为线程仅共享分配给进程的处理机资源,所以限制了可并行的线程的数量。
在内核级线程中,内核将每个线程看作一个可调度的实体。
线程在一个广阔的基础上竞争处理机资源。
内核级线程的调度如同调度进程自身一样耗时,但内核级线程可以利用多处理机。
内核级线程提供的同步和共享数据的耗时不比进程的长,但与用户级线程相比,却要长得多。
混合型线程具有用户级线程和内核级线程的优点。
它提供两级控制:
用户依照用户级线程来写程序,然后指定与进程相连的内核可调度的实体的数量。
当进程正在运行以获得并行性时,用户级线程被映射到内核可调度的实体上。
具体的映射方法依赖于实现。
pthread线程调度模型就是一种混合模型。
尽管有不同的线程机制,但是对编程人员而言,这三种方法都是透明的,可以置之不理,只需知道系统提供了哪些编程接口就可以了。
6-3-2线程的基本编程接口
本章我们介绍Posix线程,因为现有的大多数实现均支持Posix线程。
Posix线程接口也称为pthread接口,所有接口函数均以pthread_为前缀。
除了流行的pthread接口外,还有Solaris2上的thread接口(Solaris2.5以及更高的版本既支持Solaris2线程,又支持pthread线程),SunOS中的LWP接口等。
而且,不同实现上的pthread接口提供的函数的个数也可能有些差别,大多数实现不同程度上增强了Posix标准中定义的pthread函数接口。
本章只介绍常用的pthread接口,如表6-1所示。
表6-1pthread接口中的函数
类型
函数名
功能
创建与终止
pthread_create
创建一个新的线程,类似于进程接口中的fork
pthread_join
等待一个线程终止,类似于进程接口中的waitpid
pthread_self
返回线程ID,类似于进程接口中的getpid
pthread_detach
将指定的线程变为脱离的(detached),如果线程是脱离的,则当它终止时,所有的资源都将立即释放,别的线程不能等待它终止。
该函数常被想脱离自己的线程调用(即:
pthread_detach(pthread_self());)
pthread_cancel
请求指定线程终止运行,被请求终止的线程何时终止是不可知的
pthread_exit
终止线程。
还有另外三种终止线程的方法:
线程主函数(pthread_create函数中的第三个参数)返回;另一个线程调用pthread_cancel请示线程终止运行;进程的main函数返回或任何线程调用了exit,所有线程将随进程的终止而终止
特定数据处理
pthread_once
调用初始化子程序,该子程序只能被一个线程执行一次
pthread_keycreate
创建一个唯一的每线程上下文中的键值,对于一个进程内的指定键,该函数只能被调用一次
加锁解锁函数
pthread_mutex_lock
给一个未加锁的锁加锁。
如果试图为一个已被其他线程锁住的互斥锁加锁,程序将会阻塞直到该互斥锁被解锁
pthread_mutex_unlock
给一个加锁的锁解锁
pthread_mutex_trylock
加锁,但是如果其他线程已加锁,则调用线程不等待而是立即返回
条件变量函数
pthread_cond_wait
等待一个条件变量被唤醒
pthread_cond_signal
唤醒等待指定条件变量的线程
pthread_cond_timewait
在指定时间间隔内等待一个条件变量被唤醒
pthread_cond_broadcast
唤醒所有等待在指定条件变量上的线程
pthread_cond_destroy
删除一个条件变量
pthread_cond_init
创建一个条件变量
线程属性函数
pthread_attr_create
创建线程属性对象。
线程属性包括:
优先级,堆栈大小,调度级,继承的调度级等
pthread_attr_delete
删除线程属性对象
表6-2列出了表6-1中所有函数的原型及参数说明。
调用pthread编程接口必须在程序中包括线程定义头文件。
函数pthread_exit()没有返回值;函数pthread_self()的成功返回值为线程自己的ID;函数pthread_mutex_trylock调用成功时返回1,如果已经加锁则返回0,传入参数值错误则返回-1。
对于其他的函数,成功时返回0,失败则返回-1,错误代码保存在errno中。
表6-2pthread函数接口原型
函数原型
参数说明
intpthread_create(pthread_t*thread,constpthread_attr_t*attr,void*(*start_func)(void),void*arg);
thread:
线程ID,attr:
指定线程属性,start_func:
线程主函数,arg:
线程主函数的传入参数
intpthread_join(pthread_tthread,void**status);
thread:
等待线程的ID,status:
返回结束线程的返回值
intpthread_self(void)
无参数
intpthread_detach(pthread_tthread)
thread:
请求脱离的线程ID
intpthread_cancel(pthread_tthread);
thread:
请求终止的线程ID
voidpthread_exit(void*status);
status:
线程退出状态,它不能指向局部于调用线程的对象
intpthread_once(pthread_once_t*once,void(*init)(void));
once指向的值确保init函数只被调用一次
intpthread_keycreate(pthread_key_t*key,void(*destructor)(void*value));
当线程终止时,调用destructor删除key指定的键值
intpthread_mutex_lock(pthread_mutex_t*mutex)
mutex:
锁
intpthread_mutex_unlock(pthread_mutex_t*mutex);
mutex:
锁
intpthread_mutex_trylock(pthread_mutex_t*mutex);
mutex:
锁
intpthread_cond_wait(phtread_cond_t*cond,pthread_mutex_t*mutex);
cond:
条件变量,mutex:
条件变量锁
intpthread_cond_signal(pthread_cond_t*cond);
cond:
条件变量
intpthread_cond_timewait(phtread_cond_t*cond,pthread_mutex_t*mutex,structtimespec*abstime);
cond:
条件变量,mutex:
条件变量锁,abstime:
超时值
intpthread_cond_broadcast(pthread_cond_t*cond);
cond:
条件变量
intpthread_cond_destroy(pthread_cond_t*cond);
cond:
条件变量
intpthread_cond_init(pthread_cond_t*cond,pthread_condattr_t*attr);
cond:
条件变量,attr:
条件变量属性
intpthread_attr_create(pthread_attr_t*attr);
attr:
线程属性变量
intpthread_attr_delete(pthread_attr_t*attr);
attr:
线程属性变量
表6-3列出了Solaris2线程接口,并给出了对应的Posix的pthread接口。
表6-3Posix线程接口与Solaris2线程接口的比较
Posix线程接口
Solaris2线程接口
pthread_create
thr_create
pthread_exit
thr_exit
pthread_kill
thr_kill
pthread_join
thr_join
pthread_self
thr_self
pthread_mutex_init
mutex_init
pthread_mutex_destroy
mutex_destroy
pthread_mutex_lock
mutex_lock
pthread_mutex_trylock
mutex_trylock
pthread_mutex_unlock
mutex_unlock
pthread_cond_init
cond_init
pthread_cond_destroy
cond_destroy
pthread_cond_wait
cond_wait
pthread_cond_timewait
cond_timewait
pthread_cond_signal
cond_signal
pthread_cond_broadcast
cond_broadcast
6-3-3多线程程序设计
本节讨论基本的多线程程序设计,包括线程的创建和终止,线程互斥和同步,线程属性设置等内容。
1.线程的创建和终止
程序6-4显示如何创建线程以及如何向线程主函数传递参数。
例子中,主线程一共创建了5个线程,每个线程在等待10秒钟后退出。
主线程在创建所有线程后等待它们结束后退出。
程序6-4一个简单的多线程示例程序thread_test.c。
1#define_REENTRANT
2#include
3
4#defineNUM_THREADS5
5#defineSLEEP_TIME10
6
7void*sleeping(void*);
8inti;
9pthread_ttid[NUM_THREADS];
10
11main(intargc,char*argv[])
12{
13for(i=0;i14pthread_create(&tid[i],NULL,sleeping,SLEEP_TIME);
15for(i=0;i16pthread_join(tid[i],NULL);
17printf("all%dthreadshaveterminated\n",i);
18}/*main*/
1