当前位置:   article > 正文

多进程框架(Linux)_多进程通信框架

多进程通信框架

以下多进程框架均在三丰云服务器上完成,特此感谢。

1.fork()函数

关于多进程,我们就需要了解一个fork()函数,它在头文件unistd.h中。

当我们在一个程序中调用fork()函数的时候,这个进程就会创建出一个新的进程(我们称为子进程),当前这个程序就被称为父进程。并且,父进程使用fork(),还会返回子进程的进程号,而子进程中,返回值是0。

例如这么一个程序,我们来分析一下

#include<bits/stdc++.h>
#include<unistd.h>
using namespace std;
int main(){
	int pid = fork();//1
	cout<< pid <<endl;//2
	pid = fork();//3
	cout<< pid <<endl;//4
	return 0;//5
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

1.父进程与子进程的关系

我们先简单了解一下父进程与子进程的关系。首先,父进程和子进程是共用代码段的(这部分不了解的话可以先查一下程序运行时的内存分配),在父进程调用fork()函数以后,子进程就复制一份父进程的堆区、栈区、数据区到内存中,成为一个新的进程。

在此之后,父进程和子进程就各自存在,接受调度,不存在联系了。

2.简单分析

在此就简单说一下,运行这个程序,会发生什么。

首先,父进程执行到语句1,调用fork(),复制了自身的堆栈区和数据区,生成一个新进程,我们暂且称为子进程1,对于父进程而言,语句1fork()返回值为子进程1的进程号;对于子进程1而言,语句1fork()返回值为0.

此时内存中有:父进程、子进程1(来自父进程)

之后,父进程和子进程1都会进行到语句3,分别生成子进程2和子进程3,对于父进程和子进程1而言,语句3fork()的返回值都是其生成子进程的进程号;而对于子进程1、2而言,语句3的返回值都是0。

此时内存中有:父进程、子进程1(来自父进程)、子进程2(来自父进程)、子进程3(来自子进程1)

在我的单核处理器下运行结果如下:
在这里插入图片描述
根据,这里没有父进程的进程号,再加上这是一个单核处理器,所以子进程1、2、3的进程号分别为431257、431258、431259。

3.一点问题

1.我之前一直以为,新开一个进程,为什么不是从代码开头运行,而是从创建它的fork()语句后开始执行?例如父进程调用了语句1的fork()生成子进程1以后,子进程1就直接从语句2开始执行了。

而原因,我看了这篇博文fork后子进程从哪里开始执行以后,就明白了。简单总结一下,操作系统对进程管理,会有一张进程表,其中保存了每个进程执行到哪条指令,在将子进程添加到进程表的时候,保存的信息就是它从fork()的语句开始执行。否则如果每次都从程序开头执行,那么就会一直调用fork(),无限产生子进程,这是不可能的。


2.实现多进程(Linux)

利用UNIX下有的fork()函数,我们就可以实现多进程了。

实现之前

我们先来看看在使用多进程之前存在的问题。
我是写了一个很简陋的输入json串、进行解析再返回的通信。

服务端:
在这里插入图片描述
客户端1:
在这里插入图片描述
客户端2:
在这里插入图片描述
很显然,服务端和客户端的通信目前只能一对一地进行,客户端2完全没有办法和服务端通信。

先挂上当前服务端和客户端的代码:

//服务端
#include <bits/stdc++.h>
#include <rapidjson/prettywriter.h>
#include <rapidjson/document.h>
#include "yzz_server.h"
#include <unistd.h>
using namespace std;

int main(int argc,char* argv[]){
        TcpServer Server;
        if(argc == 2)Server.Init(atoi(argv[1]));
        else if(argc == 3)Server.Init(argv[1],atoi(argv[2]));
        else {
            printf("error\n");
            return 0;
        }
        if(Server.Bind() == 1)return 0;
        Server.Listen();
        Server.Accept();
        while(true){
                std::string s;
                if( Server.Recv(s) <= 0 || s == "bye"){
                        printf("通信结束\n");
                        break;
                }
        
                rapidjson::StringBuffer buffer;
                rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
                rapidjson::Document doc;
                doc.Parse(s.data());
                doc.Accept(writer);
                s = buffer.GetString();
                
                if( Server.Send(s) <= 0 ){
                    printf("通信结束\n");
                    break;
                }
        }
        return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
//客户端
#include<bits/stdc++.h>

#include"yzz_client.h"

using namespace std;

int main(int argc,char* argv[]){

    TcpClient Client;
    if(argc == 3)
        Client.Init(argv[1],atoi(argv[2]));
    else {
        printf("error\n");
        return 0;
    }
    Client.Connect();
    while(true){
        string s;
        printf("请输入字符串:\n");
        cin>>s;
        Client.Send(s);
        if(s == "bye"){
            printf("通信结束\n");
            break;
        }
        Client.Recv(s);
        printf("解析后Json串为:\n%s\n",s.data());
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

实现之后

只需要对现在的程序作出一些修改,就可以完成多进程了。

我们回去看一下之前发的TCP三次握手那篇文章,Accept()是从全连接队列中取一个客户端,申请一个connect_fd,来进行通信。

所以我们父进程只负责监听,生成connect_fd,之后产生新进程,交给新进程去connect_fd对应的客户端通信即可。

修改以后服务端代码如下:

#include <bits/stdc++.h>
#include <rapidjson/prettywriter.h>
#include <rapidjson/document.h>
#include "yzz_server.h"
#include <unistd.h>
using namespace std;

int main(int argc,char* argv[]){
        TcpServer Server;
        if(argc == 2)Server.Init(atoi(argv[1]));
        else if(argc == 3)Server.Init(argv[1],atoi(argv[2]));
        else {
            printf("error\n");
            return 0;
        }
        if(Server.Bind() == 1)return 0;
        Server.Listen();
        while(true){//1
            if(0 == Server.Accept() && fork() > 0)continue;//如果Accept取到连接,并且能创建新进程,那么通信的事情就交给新进程完成了,下面的代码就和最初的服务端关系不大了
            cout<<"创建新进程"<<endl;
            while(true){//2
                std::string s;
                if( Server.Recv(s) <= 0 || s == "bye"){
                    printf("通信结束\n");
                    break;
                }

                rapidjson::StringBuffer buffer;
                rapidjson::PrettyWriter<rapidjson::StringBuffer> writer(buffer);
                rapidjson::Document doc;
                doc.Parse(s.data());
                doc.Accept(writer);
                s = buffer.GetString();

                if( Server.Send(s) <= 0 ){
                    printf("通信结束\n");
                    break;
                }
            }//2
            exit(0);//新进程完成通信以后就可以结束它的任务了。
        }//1
        return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

我们就简单分析一下,服务端先进入监听状态,然后Accept()函数负责取连接,当连接队列为空的时候,会阻塞(socket原本的accept()函数就存在这个功能),那么我们if语句的第一关就通不过。
取到连接之后,我们调用fork()创建新进程,去完成通信,而如果fork()>0说明这是父进程,它只负责监听,生成connect_fd,不去完成客户端的通信。

这就是修改后程序的大概流程。

最终结果

服务端:
在这里插入图片描述

客户端1:
在这里插入图片描述
客户端2:
在这里插入图片描述
首先,我们实现了多个客户端同时通信,并且客户端关闭以后,最开始的服务端还是没有结束,仍然可以继续运行,这就实现了一个简单的多进程框架。


3.僵尸进程

在结束了几个客户端之后,我去查看系统的进程状态,发现了如下的情况
在这里插入图片描述
<defunct>后缀即是僵尸进程,我直接去百度了一下僵尸进程的概念:
在这里插入图片描述
子进程结束了,但是资源并没有全部释放,仍然保留一些信息需要返回信息给父进程,这就变成了僵尸进程。解决僵尸进程最简单的办法就是结束父进程,父进程结束一个,子进程的资源也会被回收。

但是通常情况下,父进程会一直保持监听而不是结束,那么随着客户端的通信越来越多,僵尸进程越来越多,占用资源而不释放,会造成危害。

那么接下来就有两种办法去解决。

1.父进程不关心子进程的退出状态

如果父进程不关心子进程的退出状态,就应该将父进程对SIGCHLD的处理函数设置为SIG_IGN,或者在调用sigaction函数时设置SA_NOCLDWAIT标志位,告诉内核自己不关心子进程的信息,这样子进程的资源就会由系统回收,不需要父进程调用wait()或者waitpid(),也不会产生僵尸进程。

2.父进程关心子进程退出状态

而有的时候,父进程需要得知子进程退出状态,就需要调用wait()函数,如果子进程变成僵尸进程,父进程就会调用wait()去"收尸",而如果子进程没有结束,父进程就会在wait()函数调用处挂起,直到子进程结束父进程才能继续。

显然,第二种方法会影响到多进程的并发性能,所以我们大多采用第一种方法,第一种方法的实现也非常简单,我们只需要在服务端main()函数开头加上一句

signal(SIGCHLD,SIG_IGN);
  • 1

即可。


4.关闭多余socket

哪里来的多余的socket呢?

是在fork()产生子进程的时候,由于把子进程listen_fd和connect_fd都复制了一份,而对于父进程来说,connect_fd是它不需要的;而对于子进程来说,listen_fd是它不需要,这就有了多余的socket。

为什么需要关闭呢?先给大家看一个我查到的信息:
在这里插入图片描述
简而言之,就是父进程有一份listen_fd的引用,子进程也有一份引用,要这些进程都关闭引用,才能fd才能彻底释放。

所以我们采取的措施有二:
一、在父进程fork()创建子进程后立即close通信的fd

if(0 == Server.Accept() && fork() > 0){ 
                cout<<"创建新进程"<<endl;
                Server.CloseConnect();                                                                                                  
                continue;
} 
  • 1
  • 2
  • 3
  • 4
  • 5

二、在子进程需要运行的部分加上如下语句(最好是子进程部分的开头)

Server.CloseListen();
  • 1

5.服务程序的退出和资源的释放

我们知道,父进程是用于持续监听的然后生成子进程的,那么当我们想让他退出的时候,应该怎么办呢?

运行在终端的时候,ctrl+C;运行在后台,kill或者killall命令将其终止。

我知道,当程序正常运行结束,析构函数不用我们调用,它也会运行。而上述的ctrl+C还是kill、killall,并没有让程序正常结束,这个时候程序并不会正常地调用析构函数,那么这个时候,我们这个进程所占用的资源,很可能没有被完全释放。

包括调用exit(0)这类语句,也是直接终止程序,不作其他操作的话,是不会调用析构函数释放资源的。

所以我们就要对程序作一些改进,使得它能够"体面"地结束。比较简单的做法,就是我们在main()函数中使用signal()函数,在收到SIGINT和SINTERM等信号的时候,调用一些我们自定义的函数,在自定义的函数里面释放完资源后,再结束程序

而kill -9是没有办法被捕获的,所以我们还是尽量少用kill -9去强行结束一个进程。

这个第五大点的内容呢,其实跟多进程处理业务的关系已经不是很大了,主要是完善程序的方面有所涉及。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/122761?site
推荐阅读
相关标签
  

闽ICP备14008679号