之间我们学习了基础IO中有关文件,动静态库等知识,后面我们将讲述进程间通信的内容,在本文中就将来展示匿名管道。
进程间通信
进程间通信的目的
- 数据传输:一个进程需要将它的数据发送给另一个进程
- 资源共享:多个进程之间共享同样的资源。
- 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
- 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变
进程间通信发展
- 管道
- System V进程间通信
- POSIX进程间通信
我们知道进程是具有独立性的,因此想要进程间进行通信需要增加通信的成本。为了解决进程间通信的问题,首要的就是要让两个进程看到同一份资源。然后让一方进行读取,另一方进行写入,完成通信。
管道
什么是管道
管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。之前我们在学习Linux的基本指令的时候就使用过 | 这个指令,这被我们称为管道,例如这个指令 who | wc -l 就是将本来应该打印到显示器文件的信息传入到了wc文件中,再由wc文件进行处理输出。
匿名管道
#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]); // int fd[2] 这是一个输出型参数用管道的时候需要有读端和写端的记录
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码
匿名管道原理
进程在内存中创建PCB,然后通过进程所对应的文件描述符表打开对应的文件,匿名管道就是OS提供的一个内存级文件,进程的文件描述附表以读写的方式打开这个文件。然后进行fork()创建子进程,子进程会复制父进程相关的数据结构包括文件描述符表,不会打开父进程曾经打开的文件,这样父子进行就都会指向同一个文件,目标文件就会被多个进程看到。未来如果父进程向文件写入信息,子进程就可以接收到相关的信息,另外这种管道只支持单向通信,因此需要确定数据的流向,关闭不需要的fd。
简单的小例子
创建管道
// 让不同的进程看到同一份资源 // 任何一种进程间通信,一定要 先 保证不同的进程看到同一份资源 int pipefd[2] = {0}; // 1、创建管道 int n = pipe(pipefd); if (n < 0) { std::cout << "pipe error," << errno << ":" << strerror(errno) << std::endl; return 1; } std::cout << "pipefd[0]:" << pipefd[0] << std::endl; // 读端 std::cout << "pipefd[1]:" << pipefd[1] << std::endl; // 写端
可以看到创建管道之后文件描述符3和4确实连接到了匿名管道文件的读端与写端。
子进程写入
// 2、创建子进程 pid_t id = fork(); assert(id != -1); // 正常应该使用判断,这里使用断言:意料之外使用if,意料之中使用assert if (id == 0) { // Scene1(pipefd); // 正常通信场景 // Scene2(pipefd); // 让子进程不断地去写入,但是父进程每隔10秒读一次 // Scene3(pipefd); // 让子进程写慢一点,父进程读取不受限制 // Scene4(pipefd); // 关闭写端 // Scene5(pipefd); // 关闭读端 }
void Scene1(int pipefd[]) { // child // 3、关闭不需要的文件描述符,让父进程进行读取,子进程进行写入 close(pipefd[0]); // 4、开始通信 -- 结合某种场景 const std::string namestr = "hello, 我是子进程"; int cnt = 1; char buffer[1024]; while (true) { snprintf(buffer, sizeof(buffer), "%s, 计数器: %d, 我的PID: %d\n", namestr.c_str(), cnt++, getpid()); write(pipefd[1], buffer, strlen(buffer)); sleep(1); } close(pipefd[1]); exit(0); } void Scene2(int pipefd[]) { // child // 3、关闭不需要的文件描述符,让父进程进行读取,子进程进行写入 close(pipefd[0]); // 4、开始通信 -- 结合某种场景 const std::string namestr = "hello, 我是子进程"; int cnt = 1; char buffer[1024]; while (true) { char x = 'X'; write(pipefd[1], &x, 1); std::cout << "Cnt:" << cnt++ << std::endl; } close(pipefd[1]); exit(0); } void Scene3(int pipefd[]) { // child // 3、关闭不需要的文件描述符,让父进程进行读取,子进程进行写入 close(pipefd[0]); // 4、开始通信 -- 结合某种场景 const std::string namestr = "hello, 我是子进程"; int cnt = 1; char buffer[1024]; while (true) { snprintf(buffer, sizeof(buffer), "%s, 计数器: %d, 我的PID: %d", namestr.c_str(), cnt++, getpid()); write(pipefd[1], buffer, strlen(buffer)); sleep(10); } close(pipefd[1]); exit(0); } void Scene4(int pipefd[]) { // child // 3、关闭不需要的文件描述符,让父进程进行读取,子进程进行写入 close(pipefd[0]); // 4、开始通信 -- 结合某种场景 int cnt = 0; while (true) { char x = 'X'; write(pipefd[1], &x, 1); std::cout << "Cnt:" << cnt++ << std::endl; sleep(1); break; } close(pipefd[1]); exit(0); } void Scene5(int pipefd[]) { // child // 3、关闭不需要的文件描述符,让父进程进行读取,子进程进行写入 close(pipefd[0]); // 4、开始通信 -- 结合某种场景 int cnt = 0; while (true) { char x = 'X'; write(pipefd[1], &x, 1); std::cout << "Cnt:" << cnt++ << std::endl; sleep(2); } close(pipefd[1]); exit(0); }
父进程读取
// parent // 3、关闭不需要的文件描述符,让父进程进行读取,子进程进行写入 close(pipefd[1]); // 4、开始通信 -- 结合某种场景 char buffer[1024]; int cnt = 0; while (true) { // sleep(10); // Scene2 父进程每隔10秒进行读取 65535 2^16 // sleep(1); // Scene5 int n = read(pipefd[0], buffer, sizeof(buffer) - 1); // 最多读1023个字符,需要预留出一个空位来赋给'\0' if (n > 0) { buffer[n] = '\0'; std::cout << "我是父进程: child give me message: " << buffer << std::endl; } else if (n == 0) { std::cout << "我是父进程, 读到了文件结尾"<< std::endl; break; } else { std::cout << "我是父进程, 读异常了" << std::endl; break; } // if (cnt++ > 3) break; // Scene5 读端读一次就退出 } close(pipefd[0]); int status = 0; waitpid(id, &status, 0); std::cout << "sig:" << (status & 0x7F) << std::endl; // 获取信号编号
关闭写端场景
关闭读端场景
匿名管道的特点
1、单向通信 -- 半双工
2、管道的本质是文件,因为fd的生命周期随进程,管道的生命周期也是随进程的
3、管道通信,通常用来进行具有血缘关系的进程,进行进程间通信。通常用与父子通信 -- pipe打开管道,并不清楚管道的名字,匿名管道
4、在管道通信中写入的次数,和读取的次数,不是严格匹配的 读写次数多少没有强相关 --- 表现 --- 字节流
5、具有一定的协同能力,让reader和writer能够按照一定的步骤进行通信 --- 自带同步机制
匿名管道通信的4种场景:
1、如果我们read读取完毕了所有的管道数据,如果对方不发,我们就只能等待
2、如果我们writer将管道写满了,我们还能写吗?不能
3、如果我们关闭了写端,读取完毕管道数据,再读就会read返回0
4、写端一只写读端关闭,会发生什么? 没有意义。OS不会维护无意义,低效率,或者浪费资源的事情。OS会杀死一直在写入的进程。OS会通过信号来终止进程 13)SIGPIPE