赞
踩
关于多进程,我们就需要了解一个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
}
我们先简单了解一下父进程与子进程的关系。首先,父进程和子进程是共用代码段的(这部分不了解的话可以先查一下程序运行时的内存分配),在父进程调用fork()函数以后,子进程就复制一份父进程的堆区、栈区、数据区到内存中,成为一个新的进程。
在此之后,父进程和子进程就各自存在,接受调度,不存在联系了。
在此就简单说一下,运行这个程序,会发生什么。
首先,父进程执行到语句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。
1.我之前一直以为,新开一个进程,为什么不是从代码开头运行,而是从创建它的fork()语句后开始执行?例如父进程调用了语句1的fork()生成子进程1以后,子进程1就直接从语句2开始执行了。
而原因,我看了这篇博文fork后子进程从哪里开始执行以后,就明白了。简单总结一下,操作系统对进程管理,会有一张进程表,其中保存了每个进程执行到哪条指令,在将子进程添加到进程表的时候,保存的信息就是它从fork()的语句开始执行。否则如果每次都从程序开头执行,那么就会一直调用fork(),无限产生子进程,这是不可能的。
利用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:
首先,我们实现了多个客户端同时通信,并且客户端关闭以后,最开始的服务端还是没有结束,仍然可以继续运行,这就实现了一个简单的多进程框架。
在结束了几个客户端之后,我去查看系统的进程状态,发现了如下的情况
<defunct>后缀即是僵尸进程,我直接去百度了一下僵尸进程的概念:
子进程结束了,但是资源并没有全部释放,仍然保留一些信息需要返回信息给父进程,这就变成了僵尸进程。解决僵尸进程最简单的办法就是结束父进程,父进程结束一个,子进程的资源也会被回收。
但是通常情况下,父进程会一直保持监听而不是结束,那么随着客户端的通信越来越多,僵尸进程越来越多,占用资源而不释放,会造成危害。
那么接下来就有两种办法去解决。
如果父进程不关心子进程的退出状态,就应该将父进程对SIGCHLD的处理函数设置为SIG_IGN,或者在调用sigaction函数时设置SA_NOCLDWAIT标志位,告诉内核自己不关心子进程的信息,这样子进程的资源就会由系统回收,不需要父进程调用wait()或者waitpid(),也不会产生僵尸进程。
而有的时候,父进程需要得知子进程退出状态,就需要调用wait()函数,如果子进程变成僵尸进程,父进程就会调用wait()去"收尸",而如果子进程没有结束,父进程就会在wait()函数调用处挂起,直到子进程结束父进程才能继续。
显然,第二种方法会影响到多进程的并发性能,所以我们大多采用第一种方法,第一种方法的实现也非常简单,我们只需要在服务端main()函数开头加上一句
signal(SIGCHLD,SIG_IGN);
即可。
哪里来的多余的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();
我们知道,父进程是用于持续监听的然后生成子进程的,那么当我们想让他退出的时候,应该怎么办呢?
运行在终端的时候,ctrl+C;运行在后台,kill或者killall命令将其终止。
我知道,当程序正常运行结束,析构函数不用我们调用,它也会运行。而上述的ctrl+C还是kill、killall,并没有让程序正常结束,这个时候程序并不会正常地调用析构函数,那么这个时候,我们这个进程所占用的资源,很可能没有被完全释放。
包括调用exit(0)这类语句,也是直接终止程序,不作其他操作的话,是不会调用析构函数释放资源的。
所以我们就要对程序作一些改进,使得它能够"体面"地结束。比较简单的做法,就是我们在main()函数中使用signal()函数,在收到SIGINT和SINTERM等信号的时候,调用一些我们自定义的函数,在自定义的函数里面释放完资源后,再结束程序。
而kill -9是没有办法被捕获的,所以我们还是尽量少用kill -9去强行结束一个进程。
这个第五大点的内容呢,其实跟多进程处理业务的关系已经不是很大了,主要是完善程序的方面有所涉及。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。