多进程多线程并发服务器.ppt
- 文档编号:18851451
- 上传时间:2024-01-30
- 格式:PPT
- 页数:69
- 大小:417KB
多进程多线程并发服务器.ppt
《多进程多线程并发服务器.ppt》由会员分享,可在线阅读,更多相关《多进程多线程并发服务器.ppt(69页珍藏版)》请在冰点文库上搜索。
第5章并发服务器,目录服务器分类进程与线程多进程服务器多线程服务器,并发服务器,服务器分类,按连接类型分类面向连接的服务器(如tcp)面向无连接的服务器(如udp)按处理方式分类迭代服务器并发服务器,迭代服务器vs.并发服务器,绑定地址,监听连接,接收连接,处理连接,断开连接,接收请求,处理请求,返回响应,绑定地址,监听连接,接收连接,创建子进程,关闭连接套接字,处理连接,关闭连接套接字,终止子进程,关闭监听套接字,服务器主进程,服务器子进程,TCP迭代服务器,TCP并发服务器,“进程”基本概念,进程定义了一个计算的基本单元,可以认为是一个程序的一次运行。
它是一个动态实体,是独立的任务。
它拥有独立的地址空间、执行堆栈、文件描述符等。
每个进程拥有独立的地址空间,进程间正常情况下,互不影响,一个进程的崩溃不会造成其他进程的崩溃。
当进程间共享某一资源时,需注意两个问题:
同步问题和通信问题。
创建进程,#include#includepid_tfork(void)返回:
父进程中返回子进程的进程ID,子进程返回0,-1出错fork后,子进程和父进程继续执行fork()函数后的指令。
子进程是父进程的副本。
子进程拥有父进程的数据空间、堆栈的副本。
但父、子进程并不共享这些存储空间部分。
如果代码段是只读的,则父子进程共享代码段。
如果父子进程同时对同一文件描述字操作,而又没有任何形式的同步,则会出现混乱的状况;父进程中调用fork之前打开的所有描述字在函数fork返回之后子进程会得到一个副本。
fork后,父子进程均需要将自己不使用的描述字关闭,有两方面的原因:
(1)以免出现不同步的情况;
(2)最后能正常关闭描述字,#include#includemain()inti,sum;sum=0;for(i=0;i=2;i+)sum=sum+i;printf(“i=%dn,i);printf(sum=%dn,sum);,intmain(void)pid_tpid;intstatus;if(pid=fork()=0)sleep
(2);printf(childrunning.n);printf(childsleeping.n);sleep
(2);printf(childdead.n);exit(0);elseif(pid0)printf(parentrunning.n);printf(parentexitn);exit(0);elseprintf(forkerror.n);exit
(1);,创建进程(cont.),#include#includepid_tvfork(void);在BSD3.0中开始出现,主要为了解决fork昂贵的开销。
它是完全共享的创建,新老进程共享同样的资源,完全没有拷贝。
两者的基本区别在于当使用vfork()创建新进程时,父进程将被暂时阻塞,而子进程则可以借用父进程的地址空间。
这个奇特状态将持续直到子进程退出或调用execve()函数,至此父进程才继续执行。
intmain(void)pid_tpid;intstatus;if(pid=vfork()=0)sleep
(2);printf(childrunning.n);printf(childsleeping.n);sleep
(2);printf(childdead.n);exit(0);elseif(pid0)printf(parentrunning.n);printf(parentexitn);exit(0);elseprintf(forkerror.n);exit
(1);,运行的结果:
终止进程,进程的终止存在两个可能:
父进程先于子进程终止(init进程领养)子进程先于主进程终止对于后者,系统内核为子进程保留一定的状态信息:
进程ID、终止状态、CPU时间等;当父进程调用wait或waitpid函数时,获取这些信息;(什么叫“僵尸进程”?
)当子进程正常或异常终止时,系统内核向其父进程发送SIGCHLD信号;缺省情况下,父进程忽略该信号,或者提供一个该信号发生时即被调用的函数。
终止进程(续),#includevoidexit(intstatus);本函数终止调用进程。
关闭所有子进程打开的描述符,向父进程发送SIGCHLD信号,并返回状态。
获取子进程终止信息,#include#includepid_twait(int*stat_loc);返回:
终止子进程的ID成功;-1出错;stat_loc存储子进程的终止状态(一个整数);如果没有终止的子进程,但是有一个或多个正在执行的子进程,则该函数将堵塞,直到有一个子进程终止或者wait被信号中断时,wait返回。
当调用该系统调用时,如果有一个子进程已经终止,则该系统调用立即返回,并释放子进程所有资源。
获取子进程终止信息,在SIGCHLD信号处理函数中使用wait()函数可能会出现一个问题,SIGCHLD,由于linux信号不排队,在SIGCHLD信号同时到来后,信号处理程序中调用了wait函数,其只执行一次,这样将留下2个僵尸进程。
可以使用waitpid函数解决这个问题。
获取子进程终止信息,pid_twaitpid(pid_tpid,int*stat_loc,intoptions);返回:
终止子进程的ID成功;-1出错;stat_loc存储子进程的终止状态;当pid=-1,option=0时,该函数等同于wait,否则由参数pid和option共同决定函数行为,其中pid参数意义如下:
-1:
要求知道任何一个子进程的返回状态(等待第一个终止的子进程);0:
要求知道进程号为pid的子进程的状态;-1:
waitforanychildprocesswhoseprocessgroupIDisequaltotheabsolutevalueofpid.Options最常用的选项是WNOHANG,它通知内核在没有已终止进程时不要堵塞。
获取子进程终止信息(cont.),调用wait或waitpid函数时,正常情况下,可能会有以下几种情况:
阻塞(如果其所有子进程都还在运行);获得子进程的终止状态并立即返回(如果一个子进程已终止,正等待父进程存取其终止状态);出错立即返回(如果它没有任何子进程),pid_tpid;intstat;while(pid=waitpid(-1,waitpid函数用法,pid_tpid;if(pid=fork()0)/*parentprocess*/intchild_status;waitpid(pid,程序例子:
#include#include#includeintmain(void)pid_tpid;intstatus;if(pid=fork()=0)printf(childrunning.n);sleep
(1);printf(childsleeping.n);printf(childdead.n);exit(0);,elseif(pid0)printf(parentrunning.n);waitpid(pid,多进程并发服务器状态图,服务器,客户,connect()函数,listenfd,客户/服务器状态图(调用accept函数时),连接请求,多进程并发服务器状态图(cont.),服务器,客户,connect()函数,listenfd,客户/服务器状态图(调用accept函数后),connfd,连接建立,多进程并发服务器状态图(cont.),服务器(父进程),客户,connect()函数,listenfd,客户/服务器状态图(调用fork函数后),connfd,连接建立,服务器(子进程),listenfd,connfd,fork()函数,多进程并发服务器状态图(cont.),服务器(父进程),客户,connect()函数,listenfd,客户/服务器状态图(父进程关闭连接套接字,子进程关闭监听套接字),连接建立,服务器(子进程),connfd,多进程并发服务器模板,intmain(void)intlistenfd,connfd;pid_tpid;intBACKLOG=5;if(listenfd=socket(AF_INET,SOCK_STREAM,0)=-1)perror(“Createsocketfailed.”);exit
(1);bind(listenfd,);listen(listenfd,BACKLOG);while
(1)if(connfd=accept(sockfd,NULL,NULL)=-1)perror(“Accepterror.”);exit
(1);,多进程并发服务器模板,if(pid=fork()0)/*parentprocess*/close(connfd);.continue;elseif(pid=0)/*childprocess*/close(lisetenfd);.exit(0);elseprintf(“forkerrorn”);exit
(1);,一点说明,从以上模板看出,产生新的子进程后,父进程要关闭连接套接字,而子进程要关闭监听套接字,主要原因是:
关闭不需要的套接字可节省系统资源,同时可避免父子进程共享这些套接字可能带来的不可预计的后果;另一个更重要的原因,是为了正确地关闭连接。
和文件描述符一样,每个套接字描述符都有一个“引用计数”。
当fork函数返回后,listenfd和connfd的引用计数变为2,而系统只有在某描述符的“引用计数”为0时,才真正关闭该描述符。
多进程并发服务器实例,该实例包括服务器程序和客户程序,具体功能如下:
服务器等待客户连接请求,连接成功后显示客户地址,并接收该客户的名字并显示,然后接收来自客户的信息(字符串)并显示,然后将该字符串反转,并将结果送回客户,之后,继续等待接收该客户的信息直至该客户关闭连接。
要求服务器具有同时处理多个客户的能力。
客户首先与服务器连接,接着接收用户输入客户的名字,并将该名字发送给服务器,接收用户输入的字符串,发送给服务器,接收服务器返回的经处理后的字符串,并显示之。
之后,继续等待用户输入直至用户输入Ctrl+C,终止连接并退出。
多进程并发服务器服务器,#include#definePORT1234#defineBACKLOG10#defineMAXDATASIZE1000voidprocess_cli(intconnectfd,structsockaddr_inclient);intmain(void)intlistenfd,connectfd;pid_tpid;structsockaddr_intserver,client;intsin_size;/*CreateTCPSocket*/,if(listenfd=socket(AF_INET,SOCK_STREAM,0)=-1)perror(Createsocketfailed);exit(-1);intopt=SO_REUSEADDR;setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,sin_size=sizeof(structsockaddr_in);while
(1)if(connectfd=accept(listenfd,(structsockaddr*)/*closelistenfd*/,多进程并发服务器服务器,voidprocess_cli(intconnectfd,structsockaddr_inclient)intnum;charrecvbufMAXDATASIZE,sendbufMAXDATASIZE,cli_nameMAXDATASIZE;printf(“Yougotaconnectionfrom%s.n”,inet_ntoa(client.sin_addr);num=recv(connectfd,cli_name,MAXDATASIZE,0);if(num=0)close(connectfd);printf(“cllientdisconnected.n”);return;cli_namenum=0;printf(“Clientnameis%s.n”,cli_name);:
多进程并发服务器服务器,while(num=recv(connectfd,recvbuf,MAXDATASIZE,0)recvbufnum=0;printf(“Receivedclient(%s)message:
%sn”,cli_name,recvbuf);for(inti=0;inum;i+)sendbufi=recvbufnum-i-1;sendbufi=0;send(connectfd,sendbuf,strlen(sendbuf),0);close(connectfd);,多进程并发服务器-运行结果,rootshengsheng#./multiproserverTheserverisrunningYougotaconnectionfrom222.18.113.153.ClientnameisshengReceivedclient(sheng)message:
howayYougotaconnectionfrom127.0.0.1.ClientnameislimingReceivedclient(liming)message:
abcdReceivedclient(sheng)message:
12345可以看出,服务器能同时为多个客户服务。
(可以思考如果采用迭代服务器,结果如何?
),多个客户同时请求处理,多进程服务器的问题,传统的网络服务器程序大都在新的连接到达时,fork一个子进程来处理。
虽然这种模式很多年使用得很好,但fork有一些问题:
fork是昂贵的。
fork时需要复制父进程的所有资源,包括内存映象、描述字等;目前的实现使用了一种写时拷贝(copy-on-write)技术,可有效避免昂贵的复制问题,但fork仍然是昂贵的;fork子进程后,父子进程间、兄弟进程间的通信需要进程间通信(IPC)机制,给通信带来了困难;多进程在一定程度上仍然不能有效地利用系统资源;系统中进程个数也有限制。
“线程”基本概念,线程是进程内的独立执行实体和调度单元,又称为“轻量级”进程(lightwightprocess);创建线程比进程快10100倍。
一个进程内的所有线程共享相同的内存空间、全局变量等信息(这种机制又带来了同步问题)。
而且它们还共享以下信息:
共享信息私有信息进程指令线程ID大多数数据寄存器集合(包括程序计数器和栈指针)打开的文件描述字栈(用于存放局部变量等)信号处理程序和信号处置errno当前工作目录信号掩码用户ID和组ID优先级,线程调用函数
(1),#includeintpthread_create(pthread_t*tid,constpthread_attr_t*attr,void*(*func)(void*),void*arg);返回:
成功时为0;出错时为正的Exxx值当一个程序开始运行时,系统会创建一个初始线程或主线程的单个线程。
额外线程由上述函数创建;新线程由线程id标识:
tid,新线程的属性attr包括:
优先级、初始栈大小、是否应该是守护线程等等。
线程的执行函数和调用参数分别是:
func和arg;由于线程的执行函数的参数和返回值类型均为void*,因此可传递和返回指向任何类型的指针;常见的返回错误值:
EAGAIN:
超过了系统线程数目的限制。
ENOMEN:
没有足够的内存产生新的线程。
EINVAL:
无效的属性attr值。
例:
创建一个线程#include#includepthread_ttid;void*ex()printf(thisisathread);main()pthread_create(,线程函数调用
(2),#inlcudeintpthread_join(pthread_ttid,void*status);返回:
成功时为0;出错时为正的Exxx值,不设置error该函数类似与waitpid函数,但必须指定等待线程的ID,该函数不能等待任意一个线程结束;被等待线程必须是当前进程的成员,并且不是分离的线程和守护线程。
pthread_tpthread_self(void);返回:
调用线程的线程id;,线程函数调用(3),#includeintpthread_detach(pthread_ttid)返回:
成功时为0;出错时为正Exxx值;线程或者是可汇合的(joinable)(默认),或者是脱离的(detached)。
当可汇合的线程终止时,其线程id和退出状态将保留,直到另外一个线程调用pthread_join。
脱离的线程则像守护进程,当它终止时,释放所有资源,我们不能等待它终止。
该函数将指定的线程变为脱离的。
pthread_detach(pthread_self();,线程函数调用(4),#includevoidpthread_exit(void*status);无返回值;如果线程为可汇合的,将保留线程id和退出状态供pthread_join()函数调用;指针status:
指向线程的退出状态。
不能指向一个局部变量,因为线程终止时其所有的局部变量将被撤销;还有其他两种方法可使线程终止启动线程的函数(pthread_create的第3个参数)返回。
其返回值便是线程的终止状态;如果进程的main函数返回,或者当前进程中,任一线程调用了exit()函数,将终止该进程中所有线程。
线程函数调用(5),includeintpthread_once(pthread_once_t*once_control,void(*init_routine)(void)成功返回0,否则返回错误码如果本函数中,once_control变量使用的初值为PTHREAD_ONCE_INIT,可保证init_routine()函数在本进程执行序列中仅执行一次。
一般在init_routine函数中完成一些初始化工作。
LinuxThreads使用互斥锁和条件变量保证由pthread_once()指定的函数执行且仅执行一次,而once_control则表征是否执行过。
pthread_once函数例子,#include#includepthread_once_tonce=PTHREAD_ONCE_INIT;voidonce_run(void)printf(once_runinthread%dn,pthread_self();void*child1(void*arg)inttid=pthread_self();printf(thread%dentern,tid);pthread_once(,void*child2(void*arg)inttid=pthread_self();printf(thread%dentern,tid);pthread_once(,线程函数调用(6),intpthread_cancel(pthread_ttid);返回值:
成功时为0,失败为非0;线程可以利用“取消机制”来终止另外一个线程;线程通过向另外一个线程发送取消请求,接收到取消请求的线程根据其设置状态,作出:
1)忽略该请求;2)立即终止自己;3)延迟一段时间终止自己;,线程的例子1,/*thread1.c*/#include#include#includevoid*thread_function(void*arg)inti;for(i=0;i5;i+)printf(Threadsayshi!
n);sleep
(1);returnNULL;intmain(void)pthread_tmythread;,if(pthread_create(/*gccthread1.c-othread1-lpthread*/,线程的例子2,/*thread2.c*/#include#includeintmyglobal=0;void*thread_function(void*arg)inti,j;for(i=0;i5;i+)j=myglobal;j=j+1;printf(.);fflush(stdout);sleep
(1);myglobal=j;returnNULL;,intmain(void)pthread_tmythread;inti;if(pthread_create(,主线程和新线程都将myglobal加15次。
结果应是10,为什么是5?
答案:
新线程覆盖了主线程所做的修改。
互斥锁,在linux系统中,提供一种基本的线程同步机制互斥锁,可以用来保护线程代码中共享数据的完整性。
操作系统将保证同时只有一个线程能成功完成对一个互斥锁的加锁操作。
如果一个线程已经对某一互斥锁进行了加锁,其他线程只有等待该线程完成对这一互斥锁解锁后,才能完成加锁操作。
互斥锁函数,pthread_mutex_lock(pthread_mutex_t*mptr)返回:
成功0,否则返回错误码mptr:
指向互斥锁的指针。
该函数接受一个指向互斥锁的指针作为参数并将其锁定。
如果互斥锁已经被锁定,调用者将进入睡眠状态。
函数返回时,将唤醒调用者。
如果互斥锁是静态分配的,就将它初始化为常值PTHREAD_MUTEX_INITIALIZER。
pthread_mutex_unlock(pthread_mutex_t*mptr)用于互斥锁解锁操作。
返回:
成功0,否则返回错误码,改进的程序,#include#include#include#includeintmyglobal;pthread_mutex_tmymutex=PTHREAD_MUTEX_INITIALIZER;void*thread_function(void*arg)inti,j;for(i=0;i5;i+)pthread_mutex_lock(,intmain(void)pthread_tmythread;inti;if(pthread_create(,多线程并发服务器模板,void*start_routine(void*arg);intmain(void)intlistenfd,connfd;pthread_ttid;typearg;/*CreateTCPsocket*/*Bindsockettoaddress*/*Listen*/while
(1)/*Acceptconnection*/if(pthread_create(&tid,NULL,start_routine,(void*)&arg)/*handleexception*/,给新线程传递参数,由于同一个进程内的所有线程共享内存和变量,因此在传递参数时需作特殊处理,下面参考如下几种方法:
传递参数的普通方法通过指针传递参数通过分配arg的空间来传递参数还可以通过加锁等同步设施来实现传递参数;,通过指针传递参数,这种方法首先将要传递的数据转换成通用指针类型,然后传递给新线程,新线程再将其还原成原数据类型:
void*start_routine(void*arg);intmain(void)intconnfd;pthread_create(这种方法虽然简单,但却有很大的局限性。
如:
要求arg的类型必须能被正确地转换成通用指针类型,而且可传递的参数只有一个。
传递参数的普通方法,由于线程创建函数只允许传递一个参数,因此当需要传递多个数据时,应首先将这些数据封装在一个结构中。
void*start_routine(void*arg);structARGintconnfd;intother;intmain()structARGarg;While
(1)if(connfd=accept(sockfd,NULL,NULL)=-1)arg.co
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 进程 多线程 并发 服务器