一、进程间通信的概念
进程间通信是一个进程把自己的数据交给另一个进程,它可以帮助我们进行数据传输、资源共享、通知事件和进程控制。
进程间通信的本质是让不同的进程看到同一份资源。因此,我们要有:
1、交换数据的空间。2、这个空间不能由通信双方任意一方提供。(要有一个独立的空间)
二、匿名管道
1、匿名管道的基本使用
基于文件的,让不同进程看到同一份资源的通信方式,叫做管道。
匿名管道通常用于具有血缘关系的进程间进行通信。例如:父子进程间通信
匿名管道就是通过系统调用创建出一份管道文件,然后给调用的进程返回读端、写端的文件描述符,然后创建子进程,子进程会继承父进程的相关属性信息,也可以拿到读端和写端,然后父子进程就可以进行通信了。
例如父进程写,子进程读。只要父进程关闭读端,然后往写端写数据,子进程关闭写端,往读端读数据,就可以实现父子进程间的通信。
接口:
参数:输出型参数,传入一个大小为2的int类型数组,就会返回读端和写端的文件描述符。 例如传入的数组名位pipefd,读端的文件描述符:pipefd[0],写端的就是pipefd[1]。
返回值:成功返回 0;失败返回 -1,并设置错误码。
示例代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>void mywrite(int wfd)
{char message[1024] = {0};int i = 1;while (1){// 自定义设置写入的内容snprintf(message, sizeof(message), "send a message to father, mypid is : %d, i = %d\n", getpid(), i);++i;write(wfd, message, strlen(message));// 方便观察sleep(1);}
}void myread(int rfd)
{char message[1024] = {0};while (1){// 读ssize_t n = read(rfd, message, sizeof(message) - 1);message[n] = '\0';printf("%s", message);// 方便观察sleep(1);}
}int main()
{// 子进程写,父进程读int pipefd[2] = {0};int pret = pipe(pipefd);if (pret < 0){printf("create pipe fail, errno is %d, errinfo is %s\n", errno, strerror(errno));return errno;}pid_t id = fork();if (id == 0){// 子进程关闭读端close(pipefd[0]);mywrite(pipefd[1]);close(pipefd[1]);exit(0);}// 父进程关闭写端close(pipefd[1]);myread(pipefd[0]);close(pipefd[0]);// 等待,防止僵尸wait(NULL);return 0;
}
可以看到子进程不断写,父进程不断读,并打印。
小细节:pipe()函数必须在 fork 之前,因为如果 fork 之后再创建管道,就是父子进程都会创建管道,父子进程拿不到同一份管道资源,就无法进行通信。
2、进程池
我们可以利用匿名管道,让父进程给多个子进程派发任务,也就是父进程写任务,然后多个子进程读任务。
创建多个子进程,并用read使它们阻塞,等待父进程派发任务
// 创建 sp_num 个子进程
void CreateSubProcess(int sp_num, vector<ChildP> &ChildPs)
{for (int i = 0; i < sp_num; ++i){// 创建管道int pipefd[2] = {0};pipe(pipefd);pid_t id = fork();if (id < 0){// 创建子进程失败printf("fork fail, errno is %d, errstr is %s\n", errno, strerror(errno));}else if (id == 0){// 子进程读取任务// 关闭写端close(pipefd[1]);// 读ReadTask(pipefd[0], getpid());exit(0);}// 父进程派发任务,关闭读端close(pipefd[0]);// 父进程需要记录每个父进程的写端wfd。为了方便查看,顺便记录名字和pidstring name = "process " + to_string(i);ChildPs.push_back(ChildP(pipefd[1], id, name));}
}
读任务函数
void ReadTask(int rfd, int pid)
{while (true){char buffer[200];ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);if (n > 0){buffer[n] = '\0';printf("子进程: %d 正在执行:> %s\n", pid, buffer);}// 写端关闭,读端读到0表示结束else if (n == 0){printf("子进程: %d 退出...\n", pid);break;}// n < 0表示出错else{printf("read fail, errno is %d, errstr is %s\n", errno, strerror(errno));return;}sleep(1);}
}
记录子进程相关信息的类
class ChildP
{
public:ChildP(int wfd, pid_t pid, const string &name): _wfd(wfd), _pid(pid), _name(name){}int getwfd() { return _wfd; }pid_t getpid() { return _pid; }string getname() { return _name; }private:int _wfd; // 父进程的写端pid_t _pid; // 子进程的pidstring _name; // 子进程的名字
};
不断往不同的子进程派送任务
void WriteTask(ChildP &cp)
{char buffer[1024];static int i = 1;snprintf(buffer, sizeof(buffer) - 1, "Task %d", i);++i;write(cp.getwfd(), buffer, strlen(buffer));// 打印确认信息cout << "Aleady Send a Task to " << cp.getname() << " ,pid is " << cp.getpid() << endl;
}// 发送 TaskNum 个任务
void SendTask(vector<ChildP> &ChildPs, int sp_num, int TaskNum)
{// PNode 表示子进程在数组内的编号,为 0 - (sp_num-1)int PNode = 0;while (TaskNum--){// 指派指定的子进程执行任务WriteTask(ChildPs[PNode]);sleep(1);PNode = (PNode + 1) % sp_num;}
}
主函数:
int main()
{int sp_num = 5;vector<ChildP> ChildPs;CreateSubProcess(sp_num, ChildPs);int TaskNum = 7;SendTask(ChildPs, sp_num, TaskNum);for(auto& cp : ChildPs){// 关闭写端close(cp.getwfd());}for(auto& cp : ChildPs){// 阻塞式等待waitpid(cp.getpid(), nullptr, 0);cout << "wait successfully: " << cp.getname() << " ,pid is " << cp.getpid() << endl;}return 0;
}
运行结果:
文件描述符关闭时要注意的问题:
按照上面的代码,有多个子进程时,当我们关闭第一个子进程的写端时,正常来说写端关闭,读端就会读到0退出,但第一个子进程并不会退出。为什么呢?这是因为其他子进程还有该管道的写端并且没关。
其他子进程的为什么会有第一个子进程的写端呢?
因为在父进程创建第一个子进程后,只关闭了读端,因此,到创建第二个子进程时,子进程继承了父进程的写端,所以子进程2不仅打开了自己的读端,还打开了子进程1的写端。由此类推,子进程3打开了子进程1和子进程2的写端以及自己的读端 ......因此,当最后一个子进程的写端关闭时,才能一步步回退,把所有子进程关闭。
由于这种问题的存在,当我们只想结束某一个子进程时,如果该子进程不是最后一个,那就会出错。
因此,我们可以做出改进:在创建子进程时,保存父进程的写端,然后在创建新的子进程后关闭。
改进后的创建子进程代码:
void CreateSubProcess(int sp_num, vector<ChildP> &ChildPs)
{// 记录父进程的写端vector<int> f_wfd;for (int i = 0; i < sp_num; ++i){// 创建管道int pipefd[2] = {0};pipe(pipefd);pid_t id = fork();if (id < 0){// 创建子进程失败printf("fork fail, errno is %d, errstr is %s\n", errno, strerror(errno));}else if (id == 0){// 关闭父进程指向其他管道的写端for(int e : f_wfd){close(e);}// 子进程读取任务// 关闭写端close(pipefd[1]);// 读ReadTask(pipefd[0], getpid());exit(0);}// 父进程派发任务,关闭读端close(pipefd[0]);// 父进程需要记录每个父进程的写端wfd。为了方便查看,顺便记录名字和pidstring name = "process " + to_string(i);ChildPs.push_back(ChildP(pipefd[1], id, name));f_wfd.push_back(pipefd[1]);}
}
感谢大家观看!