1. 初识进程间通信
1.1进程间通信的目的:
1、数据传输:一个进程需要将它的数据发送给另一个进程
2、资源共享:多个进程之间共享同样的资源
3、通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止 时要通知父进程)4、进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另 一个进程的所有陷入和异常,并能够及时知道它的状态改变
1.2 为什么要有进程间通信
为了实现两个或者多个进程实现数据层面的交互,因为进程独立性的存在,导致进程通信的成本比较高 很多场景下需要多个进程协同工作来完成要求。如下:
- 这条命令首先使用 cat 读取 log.txt 的内容,然后通过管道 (
|
) 将输出传递给 grep 命令。grep 用于搜索指定的字符串。 - grep Hello
:
这个命令搜索包含 "Hello" 的行。
1.3进程间通信的方式
管道(通过文件系统通信)
- 匿名管道pipe
- 命名管道
System V IPC (聚焦在本地通信)
- System V 消息队列
- System V 共享内存
- System V 信号量
POSIX IPC (让通信可以跨主机)
- 消息队列
- 共享内存
- 信号量
- 互斥量
- 条件变量
- 读写锁
注意:
- System V 标准需要重新构建操作系统代码来实现进程通信,比较繁琐。
- 在 System V 标准出现之前,而「管道通信」是直接复用现有操作系统的代码
- 现在本地通信已经被网络通信取代,所以进程间通信方式只重点介绍管道通信和共享内存通信
知识补充:
(1)进程间通信的本质:必须让不同的进程看到同一份“资源”(资源:特定形式的内存空间)
(2)这个资源谁提供?一般是操作系统
- 为什么不是我们两个进程中的一个呢?假设一个进程提供,这个资源属于谁?
- 这个进程独有,破坏进程独立性,所以要借用第三方空间
(3)我们进程访问这个空间,进行通信,本质就是访问操作系统!
- 进程代表的就是用户,资源从创建,使用(一般),释放--系统调用接口!
2.匿名管道
2.1.什么是管道
进程可以通过 读/写 的方式打开同一个文件,操作系统会创建两个不同的文件对象 file,但是文件对象 file 中的内核级缓冲区、操作方法集合等并不会额外创建,而是一个文件的文件对象的内核级缓冲区、操作方法集合等通过指针直接指向另一个文件的内核级缓冲区、操作方法集合等。
- 这样以读方式打开的文件和以写方式打开的文件共用一个 内核级缓冲区
- 进程通信的前提是不同进程看到同一份共享资源
所以根据上述原理,父子进程可以看到同一份共享资源:被打开文件的内核级缓冲区。父进程向被打开文件的内核级缓冲区写入,子进程从被打开文件的内核级缓冲区读取,这样就实现了进程通信!
- 这里也将被打开文件的内核级缓冲区称为 「 管道文件」,而这种由文件系统提供公共资源的进程间通信,就叫做「 管道 」
注意:
此外,管道通信只支持单向通信,即只允许父进程传输数据给子进程,或者子进程传输数据给父进程。
- 当父进程要传输数据给子进程时,就可以只使用以写方式打开的文件的管道文件,关闭以读方式打开的文件,
- 同样的,子进程只是用以读方式打开的文件的管道文件,关闭掉以写方式打开的文件。
- 父进程向以写方式打开的文件的管道文件写入,子进程再从以读方式打开的文件的管道文件读取,从而实现管道通信。如果是要子进程向父进程传输数据,同理即可。
管道特点总结:
- 一个进程将同一个文件打开两次,一次以写方式打开,另一次以读方式打开。此时会创建两个struct file,而文件的属性会共用,不会额外创建
- 如果此时又创建了子进程,子进程会继承父进程的文件描述符表,指向同一个文件,把父子进程都看到的文件,叫管道文件
管道只允许单向通信
管道里的内容不需要刷新到磁盘
2.2 创建匿名管道
匿名管道:没有名字的文件(struct file)
匿名管道用于父子间通信,或者由一个父创建的兄弟进程(必须有“血缘“)之间进行通信
#include <unistd.h>
原型:int pipe(int fd[2]);功能:创建匿名管道
参数 fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
使用如下:
int main()
{// 1. 创建管道int fds[2] = {0};int n = pipe(fds); // fds: 输出型参数if(n != 0){std::cerr << "pipe error" << std::endl;return 1;}std::cout << "fds[0]: " << fds[0] << std::endl;std::cout << "fds[1]: " << fds[1] << std::endl;return 0;
}// 运行如下:
fds[0]: 3
fds[1]: 4
-
输出型参数:文件的描述符数字带出来,让用户使用-->3,4,因为0,1,2分别被stdin,stdout,stderr占用。
2.3 匿名管道通信案例(父子通信)
注意:匿名管道需要在创建子进程之前创建,因为只有这样才能复制到管道的操作句柄,与具有亲缘关系的进程实现访问同一个管道通信
情况一:管道为空 && 管道正常(read 会阻塞【read 是一个系统调用】)
具体代码演示如下:(子进程写入,父进程读取)
#include <iostream>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstdlib>// 父进程 -- 读取
// 子进程 -- 写入
void write(std::string &info, int cnt)
{info += std::to_string(getpid());info += ", cnt: ";info += std::to_string(cnt);info += ')';
}int main()
{// 1. 创建管道int fds[2] = {0};int n = pipe(fds); // fds: 输出型参数if (n != 0){std::cerr << "pipe error" << std::endl;return 1;}// 2. 创建子进程pid_t id = fork();if (id < 0){std::cerr << "fork error" << std::endl;return 2;}else if (id == 0){// 子进程// 3. 关闭不需要的 fd, 关闭 readint cnt = 0;while (true){close(fds[0]);std::string message = "(hello linux, pid: ";write(message, cnt);::write(fds[1], message.c_str(), message.size());cnt++;sleep(2);}exit(0);}else{// 父进程// 3. 关闭不需要的 fd, 关闭 writeclose(fds[1]);char buffer[1024];while(true){ssize_t n = ::read(fds[0], buffer, 1024);if(n > 0){buffer[n] = 0;std::cout << "child->father, message: " << buffer << std::endl;}}// 记录退出信息pid_t rid = waitpid(id, nullptr, 0);std::cout << "father wait chile success" << rid << std::endl;}return 0;
}
从上面可以知道:
- 子进程写入的信息是变化的信息
- 父进程打印信息的时间间隔和子进程一样,那么子进程没传入信息的时候,父进程处于阻塞 --> (IPC 本质:先让不同的进程,看到同一份资源,可以保护共享资源)
情况二:管道为满 && 管道正常(write 会阻塞【write 是一个系统调用】)
如下对代码做点修改(红框内的代码)
管道有上限,Ubuntu -> 64 KB
如果我们让父进程正常读取,那么结果又是怎样的呢?
当我们到 65536 个字节时,管道已满,父进程读取了管道数据,子进程会继续进行写入,然后进行继续读取,就有点数据溢出的感觉
情况三:管道写端关闭 && 读端继续(读端读到0,表示读到文件结尾)
代码修改如下:
else if (id == 0)
{int cnt = 0, total = 0;while (true){close(fds[0]);std::string message = "h";// fds[1]total += ::write(fds[1], message.c_str(), message.size());cnt++;std::cout << "total: " << total << std::endl; // 最后写到 65536 个字节sleep(2);break; // 写端关闭}exit(0);
}
else
{// 父进程// 3. 关闭不需要的 fd, 关闭 writeclose(fds[1]);char buffer[1024];while (true) {sleep(1);ssize_t n = ::read(fds[0], buffer, 1024);if (n > 0) {buffer[n] = 0;std::cout << "child->father, message: " << buffer << std::endl;}else if (n == 0) {std::cout << "n: " << n << std::endl;std::cout << "child quit??? me too " << std::endl;break;}std::cout << std::endl;}pid_t rid = waitpid(id, nullptr, 0);std::cout << "father wait chile success" << rid << std::endl;
}
结论:如果写端关闭,读端读完管道内部数据,再读取就会读取到返回值 0,表示对端关闭,也表示读到文件结尾
情况四:管道写端正常 && 读端关闭(OS 会直接杀掉写入进程)
情况二:
如何杀死呢?
a. OS 会给 目标进程发送信号:13) SIGPIPE
b. 证明如下;
else if (id == 0)
{int cnt = 0, total = 0;while (true){close(fds[0]);std::string message = "h";// fds[1]total += ::write(fds[1], message.c_str(), message.size());cnt++;std::cout << "total: " << total << std::endl; // 最后写到 65536 个字节sleep(2);}exit(0);
}
else
{close(fds[1]);char buffer[1024];while (true){sleep(1);ssize_t n = ::read(fds[0], buffer, 1024);if (n > 0){buffer[n] = 0;std::cout << "child->father, message: " << buffer << std::endl;}else if (n == 0){std::cout << "n: " << n << std::endl;std::cout << "child quit??? me too " << std::endl;break;}close(fds[0]); // 读端关闭break;std::cout << std::endl;}// 记录退出信息int status = 0;pid_t rid = waitpid(id, &status, 0);std::cout << "father wait chile success: " << rid << " exit code: " <<((status << 8) & 0xFF) << ", exit sig: " << (status & 0x7F) << std::endl;
}
小结
🦋 管道读写规则
- 当没有数据可读时
- read 调用阻塞,即进程暂停执行,一直阻塞等待
- read 调用返回-1,errno值为EAGAIN。
- 当管道满的时候
- write 调用阻塞,直到有进程读走数据
- 调用返回-1,errno值为 EAGAIN
- 如果所有管道写端对应的文件描述符被关闭,则read返回0
- 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号
2.4 匿名管道特性
-
匿名管道:只用来进行具有血缘关系的进程之间,进行通信,常用于父子进程之间通信
-
管道文件的生命周期是随进程的
-
管道内部,自带进程之间同步的机制(多执行流执行代码的时候,具有明显的顺序性)
4.管道文件在通信的时候,是面向字节流的。(写的次数和读取的次数不是一一匹配的)
-
管道的通信模式,是一种特殊的半双工模式,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道