一、进程间通信的概念
进程间通信是一个进程把自己的数据交给另一个进程,它可以帮助我们进行数据传输、资源共享、通知事件和进程控制。
进程间通信的本质是让不同的进程看到同一份资源。因此,我们要有:
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。为了方便查看,顺便记录名字和pid
string 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; // 子进程的pid
string _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。为了方便查看,顺便记录名字和pid
string name = "process " + to_string(i);
ChildPs.push_back(ChildP(pipefd[1], id, name));
f_wfd.push_back(pipefd[1]);
}
}
感谢大家观看!