并发程序设计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机制。常见的就是管道。因此多进程并发耗资源,耗时,通信不方便,较少采用。

 

 

 

  

 

posted @ 2020-02-25 13:25  晨枫1  阅读(1611)  评论(0编辑  收藏  举报