我们在之前讲过 "进程之间是具有独立性" 的,如果进程间想交互数据,成本会非常高!
因为独立性之本质即 "封闭",进程们你封闭你的我封闭我的,那么进程间的交流可谓是窒碍难行。
进程间的通信说白了就是 "数据交互",我们需要多进程进行协同处理一件事情。
进程间通信的必要性:
进程间通信的方式 也有一些标准
1.Linux原生能提供 - 管道-->匿名 命名
2.Systemy ---多进程 -- 单机通信
共享内存
消息队列(不常用)
信号量(不讲 - 原理)
3.posix-- 多线程----网络通信?
标准更多在我们使用者看来,都是接口上具有一定的规律
何为管道?管道是??系统中最古老的 IPC 形式,
将一个进程连接到另一个进程的数据流称为管道 (Pipe)。
?
?下面我们先来讲解 匿名管道 (Anonymous Pipe) !
?匿名管道是计算机进程间的一种 单工 先进先出通信机制,全双工通信 通常需要两个匿名管道。
举个例子:假设内存中有两个独立的进程??和 ,我们想让 ?之间进行进程间通信。
* 令??先把数据拷贝到磁盘上,再让??去读取该数据,如下图所示:
?我们可以通过这个例子明白一个道理:通信之前,要让不同的进程看到同一份资源。?
现阶段我们要学的进程间通信,不是如何通信,而是先去关注它们是如何看到同一份资源的。
那么在进程通信之前,如何做到让进程 "先看到同一份资源" 呢?
资源的不同,决定了不同种类的通信方式! 而管道,就是提供共享资源的一种手段。
我们知道,文件在内存和磁盘之间来回切换是非常耗时的,因此进程间通信大多都是内存级别的。
即在内存内部重建一块 "小区域" 进行通信,示意图如下:
对我们来说,我们 echo 一个 hello,写到文件中,实际上这就算通信了
但是我们要讨论的不是这种通信!我们讨论的是内存级的通信!
?我们在前几章中学了文件描述符 (fd) 的知识点,我们将其系统中存在的匿名管道相结合:
?首先,一个进程维护自己进程对应的文件描述符表file_struct,而 file_struct 中有对应的数组。
数字里存的是 struct file* fd_array[],这里面存的就是打开文件的文件指针。
其中 0,1,2 被默认占用,这个在之前我们也做过讲解,对应 stdin, stdout, stdin,这里不再赘述。
如果我们今天打开一个文件,OS 为了管理文件,需要将磁盘中的文件的属性信息加载到内存里。
对该文件形成 struct file,包含了文件的所有属性,对应了文件的:
如果我们让该进程 fork 创建一个子进程,
在做拷贝时是不需要将 struct file 本身给子进程拷贝一份的。
创建子进程 task_struct 和 file_struct 是需要被拷贝的,但是 struct file 是不需要的。
????????????????????????????????"创建进程,和我文件有什么关系?"
这也就是为什么我们创建 fork 子进程之后,让父子打印时,都会像同一个显示器打印的原因。
?结论:struct file 一定能找到对应缓冲区的操作方法和 file 自己内部缓冲区。
?如何做到让不同的进程,看到同一份资源的呢?---fork让子进程继承的能够让具有血缘关系的进程进行进程间通信- 常用于父子进程
输出型参数,期望通过调用它,得到被打的文件fd
特点:
其中上面第二点理解:
管道是一个文件 - -读取 -- 具有访问控制
显示器也是一个文件,父子同时往显示器写入的时候,有没有说·个会等另·个的情况呢?
--缺乏访问控制?
Linux 给我们提供了 pipe?接口,只需调一下 pipe?就会在底层自动把文件以读和写的方式打开。
你会得到两个 fd,并且会被写进 pipefd[2]?数组中:
#include <unistd.h>
int pipe(int pipefd[2]); // 数组中分别存储第一次 O_RDONLY 和 O_WRONLY
你可以理解为:pipe?内部封装了?open,并且它 open 了两次:
最后,把读写 fd?分别放在 pipefd?数组的 0?下标和 1?下标中,这就帮你创建了一个共享文件。
并且别忘了 pipe?可是系统调用,创建文件时就在内核中将文件类型初始化 i_pipe,
让它指向的是一个管道文件,指向管道信息,也就不用和磁盘产生关联了。
当父进程没有写入的时候,子进程在等,所以父进程写入之后,
子进程才能 read(会返回)到数据,子进程打印读取数据要以父进程的节奏为主。
思考:父进程和子进程读写的时候(向显示器写入也是文件),是有一定顺序性的。父子进程各自 printf 的时候,会有顺序吗?
答案是不会。管道内部没有数据,reader 就必须阻塞等待(read),管道内部如果数据被写满,此时 writer 就必须阻塞等待(write),等管道有数据。
完全乱序的地方就是缺乏访问控制,管道内部自带访问控制机制。
最后帮助大家理解管道,准备了一个程序,可以自行尝试理解:
makefile:
mypipe:mypipe.cc
g++ -o $@ $^ -std=c++11 #-DDEBUG
.PHONY:clean
clean:
rm -f mypipe
?mypipe.cc:
#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
// 为什么不定义全局buffer来进行通信呢?? 因为有写时拷贝的存在,无法更改通信!
int main()
{
// 1. 创建管道
int pipefd[2] = {0}; // pipefd[0(嘴巴,读书)]: 读端 , pipefd[1(钢笔,写)]: 写端
int n = pipe(pipefd);
assert(n != -1); // debug assert, release assert
(void)n;
#ifdef DEBUG
cout << "pipefd[0]: " << pipefd[0] << endl; // 3
cout << "pipefd[1]: " << pipefd[1] << endl; // 4
#endif
// 2. 创建子进程
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
//子进程 - 读
// 3. 构建单向通信的信道,父进程写入,子进程读取
// 3.1 关闭子进程不需要的fd
close(pipefd[1]);
char buffer[1024 * 8];
while (true)
{
// sleep(20);
// 写入的一方,fd没有关闭,如果有数据,就读,没有数据就等
// 写入的一方,fd关闭, 读取的一方,read会返回0,表示读到了文件的结尾!
ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1);
if (s > 0)
{
buffer[s] = 0;
cout << "child get a message[" << getpid() << "] Father# " << buffer << endl;
}
else if(s == 0)
{
cout << "writer quit(father), me quit!!!" << endl;
break;
}
}
// close(pipefd[0]);
exit(0);
}
//父进程 - 写
// 3. 构建单向通信的信道
// 3.1 关闭父进程不需要的fd
close(pipefd[0]);
string message = "我是父进程,我正在给你发消息";
int count = 0;
char send_buffer[1024 * 8];
while (true)
{
// 3.2 构建一个变化的字符串
snprintf(send_buffer, sizeof(send_buffer), "%s[%d] : %d",
message.c_str(), getpid(), count++);
// 3.3 写入
write(pipefd[1], send_buffer, strlen(send_buffer));
// 3.4 故意sleep
sleep(1);
cout << count << endl;
if (count == 5){
cout << "writer quit(father)" << endl;
break;
}
}
close(pipefd[1]);
pid_t ret = waitpid(id, nullptr, 0);
cout << "id : " << id << " ret: " << ret <<endl;
assert(ret > 0);
(void)ret;
return 0;
}
运行结果:
感谢阅读!!!!!