多进程框架(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.父进程与子进程的关系

我们先简单了解一下父进程与子进程的关系。首先,父进程和子进程是共用代码段的(这部分不了解的话可以先查一下程序运行时的内存分配),在父进程调用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;
}
//客户端
#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;
}

实现之后

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

我们回去看一下之前发的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;
}

我们就简单分析一下,服务端先进入监听状态,然后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);

即可。


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;
} 

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

Server.CloseListen();

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

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

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

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

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

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

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

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Linux进程管理框架是指Linux操作系统中用于管理进程的一套机制和工具。它包括了进程的创建、调度、终止和资源管理等方面的功能。 在Linux中,进程由进程控制块(Process Control Block,简称PCB)来表示,PCB保存了进程的相关信息,如进程ID、父进程ID、进程状态、程序计数器等。Linux通过PCB来管理和调度进程。 Linux进程管理框架主要包括以下几个组件: 1. 进程创建:Linux通过fork()系统调用来创建新的进程。fork()会复制当前进程的PCB,并创建一个新的进程,新进程与原进程共享代码段、打开的文件描述符等资源,但有独立的PCB和运行空间。 2. 进程调度:Linux使用调度算法来决定哪些进程可以执行。常见的调度算法有先来先服务(FCFS)、时间片轮转(Round Robin)、最短作业优先(SJF)等。Linux内核提供了多种调度器,如CFS(Completely Fair Scheduler)和实时调度器,可以根据需求选择合适的调度器。 3. 进程终止:进程可以通过正常退出或异常终止来结束执行。正常退出可以通过调用exit()系统调用或从main函数返回来实现,异常终止则可能是由于出现错误或收到信号等原因导致。 4. 进程间通信:Linux提供了多种进程间通信(Inter-Process Communication,简称IPC)的机制,如管道(pipe)、信号(signal)、共享内存(shared memory)、消息队列(message queue)等,用于实现进程之间的数据交换和同步。 5. 资源管理:Linux通过进程控制块来管理进程的资源,如文件描述符、内存空间、CPU时间片等。进程可以通过系统调用来请求和释放资源,同时内核也会根据资源的使用情况进行调度和管理。 总之,Linux进程管理框架提供了一套完整的机制和工具,用于创建、调度、终止和管理进程,使得多个进程可以在操作系统中并发执行,并实现进程间的通信和资源管理。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值