一.进程间通信
进程间通信三问:是什么,为什么(图片右上角通信起来),怎么办

二.进程间通信实现
2.1 原理

相关知识:
-
首先进程间通信的本质是需要让不同的进程,看到同一份资源,而管道它可以充当这个资源。
-
文件操作不管写入还是读取都是要先加载到内存(冯诺依曼体系决定的)
-
内存级文件:不会再磁盘刷新,而是直接就存在内存里面
-
如果子父进程一方关闭了影响另一方吗,不影响有引用计数
-
但是管道有一个坑:父进程只能读或写打开文件,但是子进程也只能继承父进程的读和写,因此要么同时读要么同时写

如何解决同时以写打开和读打开(占用两个fd)
-
其实同时具备读写是很尴尬的,因为读和写本质是一个指针来控制,如果我写完那么指针就应该跑到最后一个字符如果此时我在读还是读不到我写了什么内容(基于文件系统)
-
不过我们也有解决办法,父进程先创建管道打开读写功能,然后父进程创建子进程,最后父进程删一个子进程删另一个,从而实现单向通信。

-
这个方法只适合父子进程,兄弟进程,爷孙进程(有血缘关系)
-
这种管道也叫匿名管道,因为我们之所以能访问同一个文件靠的不是路径而是继承
至此,通信了吗???没有。建立通信信道 ------ 为什么这么费劲??------ 进程具有独立性,通信是有成本的!!!
2.2 匿名管道使用方法
cpp
// 创建一个匿名管道,返回两个文件描述符:一个用于读,一个用于写
#include <unistd.h>
int pipe(int pipefd[2]);
// 参数:
// pipefd - 长度为 2 的整型数组:
// pipefd[0] 为读端(只能读)
// pipefd[1] 为写端(只能写)
// 返回值:
// 成功:返回 0
// 失败:返回 -1,并设置 errno
2.3 管道性质和匿名管道的特性
匿名管道特性:
- 具有血缘关系的进程进行进程间通信
- 匿名管道它是内核中的缓冲区实现,生命周期随相关进程
管道的性质:
- 管道只能单向通信
- 管道是面向字节流的**(会在网络详细讲)**
- 管道的读写具有同步语义:读空则阻塞,写满则阻塞。
2.4 匿名管道的几种情况
- 读写端正常,管道如果为空,读端就要阻塞
- 读写端正常,管道如果被写满,写端就要阻塞
- 读端正常读,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾,不会被阻塞
- 写端是正常写入,读端关闭了。操作系统就要杀掉正在写入的进程。如何干掉?通过信号杀掉(13号信号)

三.匿名管道实现
3.1 函数演示
1,2,3,4,5(不演示,自能麻烦大家回去以前的地方观看)
6. memset
cpp
// 将内存区域的前 n 个字节设置为指定的值(按字节赋值)
#include <string.h>
void *memset(void *s, int c, size_t n);
// 参数:
// s - 指向目标内存区域的指针
// c - 要设置的值(以 int 形式传入,但只使用低 8 位,即一个字节)
// n - 要设置的字节数
// 返回值:
// 返回 s 的原始值(即传入的指针)
7. snprintf
cpp
// 将格式化字符串安全地写入指定大小的缓冲区,不会溢出
#include <stdio.h>
int snprintf(char *str, size_t size, const char *format, ...);
// 参数:
// str - 目标缓冲区指针
// size - 缓冲区的最大字节数(包括结尾的 '\0')
// format - 格式控制字符串(如 "%s %d")
// ... - 可变参数,对应 format 中的占位符
// 返回值:
// 返回'期望写入的字符数(不包括结尾 '\0')':
// - 若返回值 < size:成功,字符串已完整写入并以 '\0' 结尾
// - 若返回值 >= size:表示缓冲区太小,输出被截断(但仍保证 '\0' 结尾)
// - 若出错:返回负值(极少见,通常只在底层编码错误时发生)
8. write
cpp
// 将数据从缓冲区写入到文件描述符
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
// 参数:
// fd - 文件描述符
// buf - 指向要写入的数据缓冲区
// count - 要写入的字节数
// 返回值:
// 成功:返回实际写入的字节数(可能小于 count)
// 失败:返回 -1,并设置 errno
9. read
cpp
// 从文件描述符中读取数据到缓冲区
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
// 参数:
// fd - 文件描述符
// buf - 指向用于存放读取数据的缓冲区
// count - 最多读取的字节数
// 返回值:
// 成功:返回实际读取的字节数(可能小于 count);返回 0 表示已到达文件末尾
// 失败:返回 -1,并设置 errno
3.2 代码实现
cpp
#include <string.h>
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
#define N 2 // 管道有两个文件描述符:[0]读端,[1]写端
#define NUM 1024 // 缓冲区大小
using namespace std;
// 子进程:持续向管道写入消息
void Writer(int wfd)
{
string s = "hello, I am child";
pid_t self = getpid(); // 获取当前子进程的 PID(5)
int number = 0; // 序号
char buffer[NUM]; // 缓冲区
while (true)
{
// 清空缓冲区
memset(buffer, 0, sizeof(buffer));//(6)
// 格式化发送内容:包含字符串、PID 和序号
snprintf(buffer, sizeof(buffer), "%s-%d-%d", s.c_str(), self, number++);//(7)
// 向管道写入数据(不包含 '\0')
write(wfd, buffer, strlen(buffer));//(8)
// 注意:若父进程关闭读端,write 会失败(触发 SIGPIPE 或返回 -1)
// 子进程将退出
}
}
// 父进程:从管道读取消息,最多读 5 次
void Reader(int rfd)
{
char buffer[NUM];
int cnt = 0;
while (cnt < 5) // 只读 5 条就退出,用于演示
{
// 清空缓冲区
memset(buffer, 0, sizeof(buffer));
// 从管道读取数据
ssize_t n = read(rfd, buffer, sizeof(buffer));//(9)
if (n > 0)
{
buffer[n] = 0; // 手动添加字符串结束符
cout << "father get a message[" << getpid() << "]# " << buffer << endl;
cnt++;
}
else if (n == 0)
{
// 读到 EOF:说明写端已全部关闭
printf("father read file done!\n");
break;
}
else
{
// read 出错(如被信号中断等)
break;
}
}
}
int main()
{
// 定义一个数组用于接收 pipe() 返回的两个文件描述符:
// pipefd[0] 将作为读端,pipefd[1] 将作为写端
int pipefd[N] = {0};
// 用pipefd这个数组去创建匿名管道:pipefd[0] 为读端,pipefd[1] 为写端(1)
int n = pipe(pipefd);
if (n < 0)
return 1; // 创建失败(管道)
// 创建子进程
pid_t id = fork();//(2)
if (id < 0)
return 2; // 创建失败(子进程)
if (id == 0)
{
// 子进程:关闭不用的读端
close(pipefd[0]);//(3)
// 开始写数据到管道
Writer(pipefd[1]);
// 正常退出前关闭写端(实际可能因 SIGPIPE 提前终止)
close(pipefd[1]);
exit(0);
}
// 父进程:关闭不用的写端
close(pipefd[1]);
// 从管道读取数据
Reader(pipefd[0]);
// 等待子进程结束,回收资源
pid_t rid = waitpid(id, nullptr, 0);//(4)
if (rid < 0)
return 3; // 回收失败
// 关闭读端(虽然 Reader 返回后已无用,但显式关闭更规范)
close(pipefd[0]);
return 0;
}
好了,关于匿名管道的内容就先到这里结束吧,我每次实现代码都会先把用到的函数贴出来,这些代码绝大部分都是我们学过的,不过学过不代表记得,我每次贴一次大家看一次加深大家的影响,这是我的初心,至于代码实现大家可以看着我的注释理解,我每一句话基本都有注释相信大家看懂应该是没啥问题的!