并发程序设计1:多进程并发
在并发程序设计中,主要有三种并发方式:多进程并发,基于IO复用并发,多线程并发。本节主要介绍多进程并发。以多客户端ehco程序为例。
1. 进程
进程是具有独立功能的程序关于某个数据集合的一次运行活动,是OS为正在运行的程序建立的管理实体,是系统资源管理与分配的基本单位。一个进程有五部分:操作系统管理该进程的数据结构(PCB),内存代码,内存数据,程序状态字PSW,通用寄存器信息。一个进程在OS中有四个基本状态。如图1.1所示。
图1.1 进程四态
挂起:挂起是OS收回进程的所有资源,将其移出内存。
创建进程时,实际上OS为其建立一个进程控制块,用于保存该进程的信息。多个进程同时运行时,其在内存中的状态如图1.2所示。
图1.2 多进程的内核状态
2. 多进程并发
进程是程序运行的基本单位,对于多内核的计算机,多个进程可以在多内核上同时运行,提高程序的并发性。如对于C/S类型的模型,客户端每发起一次通信,服务器开辟一个进程于其连接。这样实现服务器同时服务多个客户端。以经典的回声服务器,客户端为例,讲解多进程并发(注:Windows系统不支持,相关代码均以Linux系统为例)。
Linux系统的每个进程都有一个标志号,称为进程ID,其值大于2(1要分配给系统启动后的首个进程,用于协助操作系统)。在Linux系统中,创建一个进程采用fork函数
#include <unistd.h> pid_t fork(void); //pid_t为返回的ID号
调用fork函数之后,子进程创建,子进程会复制父进程的所有信息,然后从fork调用之后开始执行。那么怎么让父子进程执行不同的程序路径呢?这是通过主程序判断实现的,父进程调用fork函数,返回的是子进程的ID;而子进程的fork函数返回0,通过此返回值区别父子进程,从而控制fork函数之后的执行流。
2.1 僵尸进程
父进程fork子进程后,两个进程按各自的程序执行。父子进程结束时通过以下两种操作返回值并结束。
(1) 通过调用return语句返回;
(2) 通过exit()函数返回。
此返回值会保存至OS。但是子进程结束后,其返回值返回给操作系统(OS),此时OS并不会回收分配给子进程的所有资源。所以当父进程没执行完而子进程执行完成时,子进程资源没被回收,此时的子进程即为僵尸进程。僵尸进程会造成系统资源浪费。那么什么时候子进程资源会被回收呢?
(1) 当父进程结束之后;
(2) 当父进程向OS请求子进程返回值时。
因此为了结束僵尸进程,需要父进程主动向OS请求子进程的返回值。通过以下两种方式实现:
(1)父进程结束之前调用wait()函数
#include <sys/wait.h> int status; //保存返回时的状态信息 wait(&status); if(WIFEXITED(status))//WIFEXITED()在子进程正常终止时返回真 printf("Child return:%d",WEXITSTATUS(status)); //WEXITSTATUS获取子进程的返回值
一般有父进程创建了几个子进程就需要调用几次wait函数。调用wait函数后,父进程将阻塞直到有结束的子进程。
(2) 调用waitpid()
#include <sys/wait.h> pid_t waitpid(pid_t pid,int* statloc,int options); pid:等待的进程ID,若-1,则等待任一进程终止 statloc:用于保存返回状态的变量,与wait函数的参数一致 options:一般传递WNOHANG,意为非阻塞。
waitpid为非阻塞状态。
2.2 信号触发机制
2.1的wait和waitpid虽然可以回收资源,那么何时调用最合适呢?因为不可能一直阻塞等待子进程结束。常见的方式时向OS注册信号,一旦OS得知进程结束,调用信号处理函数即可。
(1) signal函数
#include <signal.h> void (*signal(int signo,void (*func)(int)))(int); //该声明难以理解,我们直接看基于signal的调用 //信号处理函数 void zombie_handle(int sig) { if(sig==SIGCHILD) //说明是子进程结束信号 //调用wait等相关函数 } 在父进程中调用注册函数 signal(SIGCHILD,zombie_handle); signo:信号种类 SIGCHLD:注册的是子进程结束信号 SIGALRM:调用alarm()函数之后时间中断信号,类似于定时器 SIGINT:输入CTRL+C 第二个参数就是信号处理函数。
(2) sigaction函数
sigaction函数可以实现signal的所有功能,而且更丰富。在不同的UNIX系统中,signal函数可能不同,但是sigaction函数都一样,因此推荐sigaction函数
#include <signal.h> int sigaction(int signo, const struct sigaction* act, struct sigaction* oldact); signo:信号类型,与singal函数一致 struct sigaction { void (*sa_handler) (int); //信号处理函数指针 sigset_t sa_mask; //置空,用 int sa_flags; //置0 } oldact:一般置0
3. 基于多进程的回声服务器
有了以上基础,现在以回声服务器为例,讲解多进程并发操作。回声服务器客户端发送信息给服务器端,服务器端收到消息后直接返回原信息给客户端。服务器端和客户端建立连接的过程如图3.1所示。
图3.1 多进程回声服务器建立过程
首先,客户端发起连接,服务器监听到连接请求后,fork一个子进程为客户端服务。此时父进程关闭建立连接描述符clientfd,而子进程关闭用于监听的描述符。调用fork函数后,父子进程都有指向监听套接字和与客户端连接的套接字的描述符。只有指向一个套接字的所有描述符都关闭,该套接字才消失。因此父进程要关闭服务器的与客户端连接的套接字,子进程要关闭用于监听的套接字。
下面直接看代码
//client.cpp 客户端代码 #include <stdio.h> #include <stdlib.h> #include <sys/socket.h> #include <arpa/inet.h> #include <string.h> #include <unistd.h> //错误处理程序 void error_handle(const char* msg) { fputs(msg,stderr); fputc('\n',stderr); exit(1); } /*********************** 回声客户端,发送什么,接收什么 ***********************/ int main(int argc,char* argv[]) { int sock; struct sockaddr_in servaddr; if(argc!=2) error_handle("please input port number"); sock=socket(PF_INET,SOCK_STREAM,0); //1. 创建socket if(sock==-1) error_handle("socket() error"); memset(&servaddr,0,sizeof(servaddr)); servaddr.sin_family=AF_INET; servaddr.sin_addr.s_addr=htonl(INADDR_ANY); servaddr.sin_port=htons(atoi(argv[1])); //设置待连接的套接字 if((connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr))==-1)) //2. 连接 error_handle("connect error"); char message[50]; while(1) { fputs("Input your message(Q to quit):",stdout); fgets(message,50,stdin); if(!strcmp(message,"Q\n")) break; int writelen=write(sock,message,strlen(message)); //写和读都是阻塞的 int readlen=0; while(readlen<writelen) { int get=read(sock,message,50); if(get==-1) error_handle("read() error"); readlen+=get; } message[readlen]=0; printf("From Servver:%s\n",message); } close(sock); return 0; }
接着是服务器代码:
1 //服务器端代码 2 3 #include <stdlib.h> 4 #include <stdio.h> 5 #include <string.h> 6 #include <sys/socket.h> 7 #include <arpa/inet.h> 8 #include <unistd.h> 9 #include <signal.h> 10 #include <wait.h> 11 12 void error_handle(const char* msg) 13 { 14 fputs(msg,stderr); 15 fputc('\n',stderr); 16 exit(1); 17 } 18 19 void ChildPro(int sig) //回收子进程的程序 20 { 21 pid_t pid; 22 int status; 23 pid=waitpid(-1,&status,WNOHANG); 24 printf("Child %d end,return %d\n",pid,WEXITSTATUS(status)); //运行后,如果客户端退出,可以看到返回的是0 25 } 26 27 int main(int argc,char* argv[]) 28 { 29 //先注册信号 30 struct sigaction act; 31 act.sa_flags=0; 32 sigemptyset(&act.sa_mask); 33 act.sa_handler=ChildPro; 34 sigaction(SIGCHLD,&act,0); 35 36 37 //服务器建立连接 38 int servsock,clntsock; 39 struct sockaddr_in servaddr,clntaddr; 40 41 if(argc!=2) 42 error_handle("Please input port number"); 43 44 servsock=socket(PF_INET,SOCK_STREAM,0); //1.建立套接字 45 46 memset(&servaddr,0,sizeof(servaddr)); 47 servaddr.sin_family=AF_INET; 48 servaddr.sin_addr.s_addr=htonl(INADDR_ANY); //默认本机IP地址 49 servaddr.sin_port=htons(atoi(argv[1])); 50 51 if(bind(servsock,(struct sockaddr*)&servaddr,sizeof(servaddr))==-1) 52 error_handle("bind error"); //2.建立连接 53 54 if(listen(servsock,10)==-1) //3.监听建立 55 error_handle("listen() error"); 56 57 socklen_t clnt_len; 58 pid_t pid; 59 char message[50]; 60 while(1) 61 { 62 clnt_len=sizeof(clntaddr); 63 clntsock=accept(servsock,(struct sockaddr*)&clntaddr,&clnt_len); //4.接受请求 64 if(clntsock==-1) 65 continue; //接收失败 66 else 67 pid=fork(); 68 if(pid==-1) 69 error_handle("子进程创建失败"); 70 else if(pid==0) //子进程 71 { 72 close(servsock); //关闭监听套接字 73 int str_len; 74 while((str_len=read(clntsock,message,sizeof(message)))!=0) 75 write(clntsock,message,str_len); 76 close(clntsock); 77 return 0; //子进程返回值,通过wait函数调用获得返回值 78 } 79 else 80 close(clntsock); 81 } 82 close(servsock); 83 return 0; 84 }
4. 总结
以上就是基于多进程并发的一个例子。由于进程之间只共享文件表,没有共享用户地址空间。进程之间的调度开销比较大,而且进程通信必须采用显式的IPC机制。常见的就是管道。因此多进程并发耗资源,耗时,通信不方便,较少采用。