对于本篇文章我们采取三段论:是什么 为什么 怎么办。
目录
- 进程间为什么要通信?
- 进程间如何通信?
- 进程间怎么通信?
- 匿名管道:
- 匿名管道原理:
- 代码示例:
- 匿名管道的情况与特征:
进程间为什么要通信?
进程间也是需要某种协同的。
协同应该怎么理解呢?
那么协同的前提
必然是进行通信
。
但是通信的信息也是有类别的,比如说单纯的数据信息,或者控制信息…
了解了这个话题我们还要在阐述一个事实,进程间具有独立性:比如关闭你的QQ但是不影响微信。
:进程 = 内核数据结构 + 数据与代码
那么这两份东西每个进程都是有独一份的,数据与代码会写时拷贝,所以不影响我们这个结论。
进程间如何通信?
这里我们要明确两个共识:
a. 进程间通信是有成本的(这个会在后边的代码中有验证)
b. 进程间通信的前提:让两个进程看到同一块内存资源
我们画图进行理解一下:
进程间怎么通信?
开始这个话题之前我们需要先知道一个东西:
互联网有大量标准
的存在,否则为什么不同品牌的手机,不同大陆的手机厂商,用着不一样的硬件设备最后却能进行通信?
就是因为标准的存在!
所以我们进程间通信也有标准,这里我们只谈System V
这个标准提供了一些通信方式:
- 消息队列
- 共享内存
- 信号量
但是,人们刚接触通信时肯定是先想到尽量复用源代码进行通信的,故我们暂且不谈以上的方式(未来会谈)。
所以我们利用源代码设计出了两种方式
- 匿名管道
- 有名管道
这里我们先说匿名管道,因为他简单。
匿名管道:
匿名管道原理:
下图是一幅进程,文件与磁盘的简略图:
当对此文件以另一种方式打开时,变化如下
为什么答案是否定的呢?
那么当前进程fork()(创建子进程)会发生什么变化?
所以,现在我们就可以以普通文件为切入点
理解匿名管道了
首先父进程打开同一个文件以“w”“r”方式,然后在进行创建子进程,
子进程会继承父进程的文件系统那一套数据结构,
这样他们就有了公共的一块内存
,也就是那段内核缓冲区,
我们此时就可以一个进程向其中写入,另一个读取
但是我们匿名管道并不是普通文件,
- 第一:我们的匿名管道不需要与磁盘进行交互了,因此,设计者要重新设计出一个新的通信接口
- 第二:我们的管道只允许单向通信,因为他简单!所以我们fork之后要关闭一个进程的w和另一个进程的r
因此我们现在要理解一种现象以及两个问题:
现象:为什么我们的父子进程会向同一块显示屏打印数据
问题1:既然未来要关闭不需要的fd,那我们可不可以直接不打开呢?
问题2:可以不关闭吗?
对于这种现象是因为我们的bash父进程就已经打开了fd为0 1 2的文件描述符,所以bash的子进程们都继承了,所以他们会向同一块fd为1指向的内存进行疯狂输出并刷新到显示屏外设
对于问题1:不可以,因为这样子进程就不会继承了。
对于问题2:可以,单位了防止我们有意外操作建议关闭!
代码示例:
话说了这么多我们总得有理论实践,我们会使用pipe接口+fork来创建管道。
下段这80行的代码可以帮助我们理解匿名管道的情况与特征:
代码中会有注释:
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
const int SIZE = 1024;
std::string GetOtherMessage()
{
static int cnt = 1;
int id = getpid();
std::string MessageCnt = std::to_string(cnt++);
std::string ChildId = std::to_string(id);
return "MessageCnt: " + MessageCnt + " child id: " + ChildId + + " ";
}
子进程进行写入,但是我另外写了一段程序进行获取一段动态的信息。也就是如上的函数
void SubProcessWrite(int wfd)
{
std::string message = "I am child,";
while (true)
{
std::string info = message + GetOtherMessage();
write(wfd, info.c_str(), info.size());
sleep(1);
}
}
父进程进行读
void FatherProcessRead(int rfd)
{
char buffer[SIZE] = {0};
while (true)
{
ssize_t n = read(rfd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
std::cout << "father get message:" << buffer << std::endl;
}
}
}
int main()
{
int pipefd[2];
int ret = pipe(pipefd);
if (ret == -1)
{
std::cerr << "errno: " << errno << " errstring: " << std::strerror(errno) << std::endl;
return 1;
}
pid_t id = fork();
if (id == 0)
{
std::cout << "我是子进程,已经关闭了0了" << std::endl;
// 子进程,进行写,关闭r
close(pipefd[0]);
SubProcessWrite(pipefd[1]);
close(pipefd[1]);
exit(0);
}
std::cout << "我是父进程,已经关闭了1了" << std::endl;
// 父进程,进行读,关闭w
close(pipefd[1]);
FatherProcessRead(pipefd[0]);
close(pipefd[0]);
// 父进程进行等待
int rid = waitpid(id, nullptr, 0);
if (rid > 0)
{
std::cout << "wait sucess" << std::endl;
}
return 0;
}
匿名管道的情况与特征:
对于下边情况的验证我们都可以通过对上边的程序进行修改得到结果。
四种情况:
对于2,我们可以将上面的程序进行一下修改
同时让父进程sleep更长的时间,得到管道的大小为64kb(ubuntu22.04)
至于为什么不会阻塞是因为管道已经失效了。
首先我们要对这种情况进行一个探究,读端已经被关闭,说明写再多也没有用,OS不会允许这种浪费时空的东西存在,会直接杀掉写进程。
用什么杀死呢?
OS发送13号信号进行终止。
此时的这个管道也叫做broken pipe。
五种特征:
-
匿名管道:只能用于有血缘关系的进程之间,常用于父子进程。
-
管道之间自带进程的同步机制。
(同步理解为多执行流代码的时候,具有顺序性)
-
管道文件的生命周期是随进程的
-
管道文件再通信的时候是面向字节流的(可以认为write的次数与read的次数不是一一匹配的,就像你用的水是从自然界收集来的,自然界用了10小时收集的水,你1分钟用完了)
-
管道的通信模式是一种特殊的半双工模式
我们先来说一下全双工模式->类似于我们吵架时,我们可以同时说话
半双工就是一个人说,一个人不说,或者相反过来
而我们这个特殊的半双工就特殊在只能单向,只能一段说话,另一端不说话。
最后我们在输出最后一个结论:
我们在上文提到过:我们具有同步机制,但其实这不是一定的,当我们每次写入数据小于PIPE_BUF时才会具有这种机制,也叫作原子态(atomic)
匿名管道就结束啦!