【Linux进程间通信】匿名管道

【Linux进程间通信】匿名管道

目录

  • 【Linux进程间通信】匿名管道
      • 进程间通信介绍
        • 进程间通信目的
        • 进程间通信发展
        • 进程间通信分类
        • 管道
            • 用fork来共享管道原理
            • 站在文件描述符角度——深度理解管道
            • 站在内核角度——管道本质
            • 匿名管道
              • 在myshell中添加管道的实现:
              • 管道读写规则
              • 管道特点

                作者:爱写代码的刚子

                时间:2023.11.21

                前言:本篇博客将会介绍匿名管道的运用

                进程间通信介绍

                前言:因为进程独立性的存在,导致进程通信的成本比较高。为什么要进行进程间通信?基本数据,发送命令,某种协同,通知。

                进程间通信目的
                • 数据传输:一个进程需要将它的数据发送给另一个进程

                • 资源共享:多个进程之间共享同样的资源。

                • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

                • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

                  a. 进程间通信的本质:必须让不同的进程看到同一份“资源”

                  b. “资源”?特定形式的内存空间

                  c. 这个“资源”由操作系统提供,为什么不是我们两个进程中的一个呢?假设一个由一个进程提供,这个资源被这个进程独有,破坏进程独立性。

                  d. 我们进程访问这个空间,进行通信,本质就是访问操作系统!进程代表的就是用户,“资源”从创建,使用(一般),释放,都是调用系统调用接口!从底层设计,从接口设计,都要由操作系统独立设计。一般操作系统会有一个独立的通信模块——隶属于文件系统——IPC通信模块定制标准(进程间通信是有标准的)——System V &&posix

                  进程间通信发展
                  • 管道

                  • System V进程间通信

                  • POSIX进程间通信

                    进程间通信分类

                    管道

                    • 匿名管道pipe
                    • 命名管道

                      System V IPC

                      • System V 消息队列

                      • System V 共享内存

                      • System V 信号量

                        POSIX IPC

                        • 消息队列

                        • 共享内存

                        • 信号量

                        • 互斥量

                        • 条件变量

                        • 读写锁

                          管道

                          • 管道是Unix中最古老的进程间通信的形式。

                          • 我们把从一个进程连接到另一个进程的一个数据流称为一个“管道“

                            • who | wc -l统计在云端服务器中的用户个数。

                              管道打开的是内存级文件(管道就是文件),每个文件有着对应的缓冲区,不同进程打开同一个文件时存在引用计数来进行控制。

                              管道并不支持同时读写!

                              回顾文件系统:

                              无论对文件怎么读写,首先都需要将文件加载到内存中。既然如此,我们也可以创建一个内存级文件(技术上可行)。

                              注意!!!!!,如果该进程创建了子进程,子进程中的struct file*fd_arry数组中存放相同的指针,但是指向的文件相同!!!!

                              • 所以存在引用计数解决父进程关闭子进程仍然在读取文件的情况。
                                用fork来共享管道原理
                                站在文件描述符角度——深度理解管道
                                • 注意,如果两个进程间没有关系(看不到同一份资源),就不能用上述方法进行通信,如果要通信,则需要采用下面匿名管道的方法。

                                • 所以要想进行上述通信,进程间必须是父子关系,兄弟关系,爷孙关系…(血缘关系,常用于父子关系)

                                  站在内核角度——管道本质
                                  • 【问题】:管道只能单向通信,如果我们需要进行双向通信呢?(建立多个管道)

                                  • 至此进程间通信了吗?没有,只是建立了通信信道——为什么怎么费劲??——进程具有独立性,通信是有成本的!!!

                                    匿名管道

                                    #include

                                    功能:创建一无名管道

                                    原型

                                    int pipe(int fd[2]);int fd[2]为输出型参数,将文件的文件描述符数字带出来,让用户使用!!(pipefd[0]:读下标,pipefd[1]:写下标)

                                    参数

                                    fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端

                                    返回值:成功返回0,失败返回错误代码

                                    • 简单的代码示例:

                                      示例一:查看pipefd数组的值

                                      一般来说==pipefd[0]为读,pipefd[1]为写==。

                                      示例二:子进程打印数据父进程读取数据:

                                      示例代码:

                                      #include #include #include #include #include #include #include #define N 2
                                      #define NUM 1024
                                      using namespace std;
                                      void Writer(int wfd)
                                      { string s = "hello";
                                          pid_t self = getpid();
                                          int number = 0;
                                          char buffer[NUM];
                                          while(true)
                                          { buffer[0]=0;//字符串清空,将这个数组当作字符串
                                              snprintf(buffer,sizeof(buffer),"%s-%d-%d",s.c_str(),self,number++);
                                              write(wfd,buffer,strlen(buffer));//strlen(buffer)不需要加1,c语言规定的‘\0’不关文件的事,只需要文件内容即可
                                              sleep(1);
                                          }
                                      }
                                      void Reader(int rfd)
                                      { char buffer[NUM];
                                          while(true)
                                          { buffer[0] = 0;
                                              ssize_t n = read(rfd,buffer,sizeof(buffer));//注意 sizeof != strlen,管道满了怎么办?管道有面向字节流概念,之后学习网络会提到(定协议去区分)。
                                              if(n > 0)
                                              { buffer[n] = 0;// 0 == '\0'当作字符串
                                                  cout<< "父进程得到了一个消息:"<< getpid() << "]#" << buffer < int pipefd[N] = {0};
                                          int n = pipe(pipefd);
                                          if(n<0)return 1;
                                          cout << "pipefd[0]:" << pipefd[0] << ", pipefd[1]:  "< //子进程
                                              close(pipefd[0]);//关闭读
                                              //IPC code
                                              Writer(pipefd[1]);
                                              close(pipefd[1]);
                                              exit(0);
                                          }
                                          //父进程
                                          close(pipefd[1]);//关闭写
                                          //IPC code
                                          Reader(pipefd[0]);
                                          pid_t rid = waitpid(id,nullptr,0);
                                          if(rid < 0) return 3;
                                          close(pipefd[0]);  
                                          return 0;
                                      }
                                      

                                      结果:

                                      • 父子进程在对同一份数据进行访问时,这份资源是多执行流共享的,难免会出现访问冲突的问题。(临界资源竞争的问题)但是父子进程会进行协同。对于管道文件会发生同步与互斥————保护管道文件的数据安全。

                                      • 同时子进程向管道中写入的都是字符,父进程进行读取时也是字符,所以不会出换行的情况。(忽视分隔符等特殊符号,就相当于字节)(管道是面向字节流的)

                                        总结:

                                        管道的特征:

                                        1. 具有血缘关系的进程进行进程间通信
                                        2. 管道只能单向通信
                                        3. 父子进程是会进程协同的,同步与互斥——保护管道文件的数据安全(多线程)
                                        4. 管道是有固定大小的(会被写满,但是在不同的内核里大小不同)
                                        5. 管道是面向字节流的(网络)
                                        6. 管道是基于文件的,而文件的生命周期是随进程的
                                        • ulimit -a查看相关的限制(open files表示单个进程最多打开文件的个数 )
                                          • man 7 pipe查看管道大小
                                            • 官方文档中说了,如果读取的数据小于PIPE_BUF,读取操作就必须是原子的
                                              在myshell中添加管道的实现:

                                              思路:

                                              • 分析输入的命令行字符串,获取有多少个|, 命令打散多个子命令字符串

                                              • malloc申请空间,pipe先申请多个管道

                                              • 循环创建多个子进程,每一个子进程的重定向情况。最开始. 输出重定向, 1->指定的一个管道的写端

                                              • 中间:输入输出重定向, 0标准输入重定向到上一个管道的读端 1标准输出重定向到下一个管道的写端

                                              • 最后一个:输入重定向,将标准输入重定向到最后一个管道的读端

                                              • 分别让不同的子进程执行不同的命令— exec* — exec*不会影响该进程曾经打开的文件,不会影响预先设置好的管道重定向

                                                管道读写规则

                                                管道的4中情况:

                                                1. 读写端正常,管道如果为空,读端就要阻塞
                                                2. 读写端正常,管道如果被写满,写端就要阻塞
                                                3. 读端正常读,写端关闭,读端就会读到0,表明读到了文件(pipe)结尾,不会被阻塞
                                                4. 写端是正常写入,读端关闭了。操作系统就要杀掉正在写入的进程。(通过信号干掉)
                                                • 当没有数据可读时

                                                  • O_NONBLOCK disable:read调用阻塞,即进程暂停执行,一直等到有数据来到为止。
                                                  • O_NONBLOCK enable:read调用返回-1,errno值为EAGAIN。
                                                  • 当管道满的时候

                                                    • O_NONBLOCK disable: write调用阻塞,直到有进程读走数据
                                                    • O_NONBLOCK enable:调用返回-1,errno值为EAGAIN
                                                    • 如果所有管道写端对应的文件描述符被关闭,则read返回0

                                                    • 如果所有管道读端对应的文件描述符被关闭,则write操作会产生信号SIGPIPE,进而可能导致write进程退出

                                                    • 当要写入的数据量不大于PIPE_BUF时,linux将保证写入的原子性。

                                                    • 当要写入的数据量大于PIPE_BUF时,linux将不再保证写入的原子性。

                                                      管道特点

                                                      只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创

                                                      建,然后该进程调用fork,此后父、子进程之间就可应用该管道。

                                                      管道提供流式服务

                                                      一般而言,进程退出,管道释放,所以管道的生命周期随进程

                                                      一般而言,内核会对管道操作进行同步与互斥

                                                      管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道