进程间通信之管道

Posted 两片空白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进程间通信之管道相关的知识,希望对你有一定的参考价值。

目录

前言

进程间通信的目的:

一.管道

        1.1 匿名管道

        1.1.1 匿名管道的使用场景      

        1.1.2 匿名管道实现通信的原理

         1.1.3 怎么创建管道

        1.1.4 怎么实现进程间通信

         1.1.5 管道通信额四种情况

        1.1.6 匿名管道的特征

        1.2 命名管道

         1.2.1 命名管道的原理

         1.2.2 创建命名管道

         1.2.3 命名管道和匿名管道的区别

        1.2.4 实现进程间通信

        1.3 总结


前言

        我们知道进程之间具有独立性,进程之间互不影响。但是,进程间通信回事两个进程之间关联起来,一个进程的状态可能会影响另一个进程。但是着并不说明进程之间就不具备独立性了。

        我们要实现进程间通信最重要的一点就是,要让通信的进程指向同一块资源(内存)。

进程间通信的目的:

  • 数据传输:一个进程需要将它的数据发售那个给另外一个进程。
  • 资源共享:多个进程之间需要共享同样的资源。
  • 通知事件:一个进程需要向另外一个或者一组进程发送消息,通知它们发生了某种事件(比如:进程终止时要通知父进程)
  • 进程控制:有些进程希望完全控制另一个进程的执行,此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时告知它的状态改变。

一.管道

        管道本身是一个文件。用管道实现进程间的通信,实际上是通过文件来实现进程间的通信。

        管道又分为匿名管道和命名管道,顾名思义,匿名管道,创建的管道名字是不知道的,命名管道,创建管道的名字是知道的。

        注意:管道实现通信只能进程单向通信。一个进程读,一个进程写。

        1.1 匿名管道

        1.1.1 匿名管道的使用场景      

        匿名管道通常使用于有亲缘关系的进程之间,常用于父子进程。看完原理再来理解一下这句话。

        有亲缘关系的进程,会继承同一个祖先进程的部分内容。其中files_struct是继承祖先进程的。可以使两亲缘关系的进程指向同一文件。

所以为什么子进程会默认打开stdin,stdout,stderr,但是子进程并没有指向open操作?

只需要祖先进程打开了stdin,stdout,stderr就好了,子进程会拷贝父进程内容。

        1.1.2 匿名管道实现通信的原理

  • 管道也是一个文件,站在文件的角度来理解管道实现通信的原理。

        管道是一个文件,当一个进程以读和写的方式打开一个管道再创建一个子进程,子进程会以父进程为模板,拷贝父进程的部分内容。此时file_strcut里的数组(文件描述符与文件的映射关系)会是父进程的拷贝。此时,父子进程都指向了管道文件(同一块空间),并且子进程也是以读写方式打开的该文件(因为子进程会继承父进程代码,父进程再创建子进程之前以读写方式打开的文件),如果将一个进程对文件进行写,一个进程对文件进行读,由于来给你进程指向同一空间,所以读进程拿到的数据就是写进程写进去的数据。此时就完成了对文件的通信。

  • 站在内核角度,理解管道实现进程间通信的原理

        父子进程指向同一个文件,该文件在内存中占用空间,于是两进程指向了同一块空间。

文件加载带内存需要开辟空间,一个进程如何找到文件的内存进行读写的?

        task_struct有一个files_struct指针,可以找到files_struct,files_struct里有一个数组,数组下标对应文件描述符,可以找到对应文件struct_file。这样就找到文件了。

        struct_file中有一个struct_path,可以找到对应目录,目录中保存文件名和inode的对应关系,就可以找到文件的inode。文件inode中有一个struct address_space,进入里面有struct radix_tree_root    page_tree,就可以找到对应内存空间。

        再调用struct_file里的const struct file_operations    *f_op指针,调用对应的读写函数。就实现了进程间的通信 

         1.1.3 怎么创建管道

使用int pipe(int fd[2]),系统调用。

//头文件
#include <unistd.h>

//pipe系统调用是以读写的方式打开一个管道文件
int pipe(int pipefd[2]);
/*返回值:打开成功0,失败返回异常信号
参数:pipefd[2]是两个输出型参数,保存的是文件描述符
    fd[0]读打开的管道文件描述符,fd[1]写打开管道的文件描述符*/
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 int main(){
  5   int fd[2]={0};                                                                                                                                         
  6   //以读写方式打开管道
  7   int res=pipe(fd);
  8   if(res==0){
  9     printf("fd[0]:%d,fd[1]:%d\\n",fd[0],fd[1]);
 10 
 11   }
 12   else{
 13     perror("pipe error");
 14     exit(1);
 15   }
 16 
 17   return 0;
 18 }

        1.1.4 怎么实现进程间通信

        父进程创建一个全局变量,再创建子进程,可以实现进程间通信吗?

        答案是不能,因为只有父子进程对全局变量有写入动作,就会进行写时拷贝,父子进程全局变量的内存空间就不是同一块空间了。

         代码实现子进程写,父进程读。

        子进程先关闭以都打开管道的文件描述符,使用write系统调用往管道写入数据。

        父进程关闭以写打开的管道的文件描述符,使用read系统调用从管道读取数据。

注意:父子进程共享一个管道文件,管道文件只有一个,是因为打开方式不同所以有多个文件描述符。

        pipe返回的文件描述符fd父子进程是共享的,因为父子进程没有对fd进行修改不会进行写时拷贝。

 1 #include<stdio.h>
  2 #include<string.h>
  3 #include<unistd.h>
  4 #include<stdlib.h>
  5 int main(){
  6   int fd[2]={0};
  7   //以读写方式打开管道
  8   int res=pipe(fd);
  9   if(res==0){
 10     pid_t id = fork();
 11     if(id==0){
 12       //子进程,写
 13       close(fd[0]);//关闭读
 14       const char *buf="i am child...\\n";
 15       while(1){//不断写
 16         write(fd[1],buf,strlen(buf));
 17       }                                                                                                                                                  
 18        
 19     }
 20     else if(id>0){
 21       //父进程,读
 22       close(fd[1]);//关闭读
 23       char buf[256];       
 24       while(1){//不断读
 25         ssize_t n=read(fd[0],buf,sizeof(buf)-1);
 26         if(n>0){//读到数据                      
 27           buf[n]=0;//最后加一个'\\0'
 28           printf("%s",buf);        
 29         }   
 30 
 31       }
 32     }
 33     else{
 34       //不成功
 35       perror("fork error");
 36       exit(1);
 37     }
 38   }
 39   else{
 40     perror("pipe error");
 41     exit(1);
 42   }
 43 
 44   return 0;
 45 }                          

         1.1.5 管道通信额四种情况

  • 当读进程进行读操作时,当读条件不满足,读进程进入阻塞状态。

        读条件不满足:管道里没有数据或者说写端没有往管道写数据。

        读进程进入阻塞状态:PCB的状态设置为S,该进程从运行队列进入等待队列,等待管道中有数据。

改变上面代码:子进程写数据时延时5s,父进程读数据没有时间限制

 由于写进程比读进程慢,读进程在读时,大部分时间,管道是空的,此时读进程会进入阻塞状态,等待管道中有数据。

  • 当写进程进行读操作时,当写条件不满足,写进程进入阻塞状态。

        写条件不满足:管道满了的时候

        写进程进入阻塞状态:PCB的状态设置为S,该进程从运行队列进入等待队列,等待管道中可以写入数据。

 改变上面代码:子进程写数据没有时间限制,父进程读数据延时5s。

此时写进程块,读进程慢。一开始写进程往管道了写了很多数据并且将道写满了。
此时已经不符合写的条件,子进程进入阻塞状态。父进程来读管道里的数据,
待管道可以写数据时,子进程再进入运行状态,往管道写数据。

        进程间同步的概念:一个进程快导致另一个进程也快,一个进程慢导致另一个进程也慢。一个进程受到控制,另外一个进程也受到控制,着就叫进程间同步。

  •  如果写进程关闭写文件描述符,读进程会一直读到管道结尾。

这里介绍一下read系统调用的返回值,如果返回0,读到文件结尾,返回值不为0,表示实际读到值的个数。

再改一下上面的代码:

        将写进程先往管道了写一些数据后关闭写文件描述符,再读进程进行读数据

读进程读数据也不是一行一行读的,读到缓冲区后,缓冲区满了再刷新出来。 

这里说明,如果将写文件描述符关闭,读进程最终一定会读到管道的结尾。因为已经没有进程往文件中写入数据了。

  • 如果读进程关闭读文件描述符,写进程可能会被系统直接杀死,系统会传入13好信号SIGPIPE,杀死进程,进程异常退出。

改一下上面的代码:

通过监控进程状态发现:

         读进程退出读文件描述符,系统传13号信号杀死写进程。并不是将写进程变成僵尸状态,上面是因为写进程是子进程,但是父进程没有退出。

所以一般先将写进程关闭写文件描述符,再关闭读进程的读文件描述符。

 总结一下四种情况:

        读写进程都不关闭文件描述符,但是如果一个进程快一个进程慢,需要快的进程以阻塞方式等慢的进程。

        读写进程其中一个关闭文件描述符,关闭写文件描述符,读进程会读到管道结尾。关闭读文件描述符,系统可能会传13号信号杀死写进程。

        1.1.6 匿名管道的特征

  •  只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常一个管道由一个进程创建,然后进程调用fork,父子进程之间就可以进程通信了。
  • 管道提供流式服务,就是一个数据一个数据传,相比较数据包,多个数据构成数据包,以整个数据包为单位。
  • 进程退出管道释放,管道声明周期随进程的
  • 内核对管道的操作式同步和互斥的。同步一个进程快,另外一个也快,一个慢,另外一个也慢。互斥是,一个再写,另外一个不会正好读,一个在读,不会另外一个进程正好写。
  • 管道是半双工的,数据只能一个方向流动,需要双方通信时,需要建立两个管道。

        1.2 命名管道

        从上面我们知道匿名管道只能用于具有同一祖先(具有亲缘关系)的进程。

        命名管道可以用于不相关的两进程之间进行通信。

        命名管道是一个特殊的管道

        先说明,上面关于管道的使用时的四种情况在这里一样适用,特点也符合。

                1.2.1 命名管道的原理

        其实原理和匿名管道差不多,只是需要先创建一个命名管道再一个进程以读或者写的方式来打开该管道文件,再另外一个进程不需要创建管道,只需要以写或者读的方式来打开管道文件。再调用读写系统调用来往文件写或者读,来进行进程间通信。

        两进程分别对同一管道文件分别用读或写的方式打开,两进程看到同一文件(资源)。

        不需要创建子进程,可以是两个不相关的进程。

                1.2.2 创建命名管道

  • 使用命令行 mkfifo 文件名

  • 使用函数int mkfifo(const char *filename,mode_t mode)

//头文件
#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
//返回值:成功返回0,失败返回-1
//参数:filename 路径+文件名
//      mode:权限

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<sys/types.h>
  4 #include<sys/stat.h>
  5 
  6 int main(){
  7   umask(0);//设置掩码为0
  8   int res=mkfifo("./fifo",0644);                                                                                                                         
  9     if(res==-1){
 10       perror("mkfifo error");
 11       exit(2);
 12     }
 13   return 0;
 14 }

         1.2.3 命名管道和匿名管道的区别

        匿名管道只能用于具有亲缘关系的进程,命名管道剋用于不相关的进程之间。

        匿名管道用pipe创建并且会以读写方式打开匿名管道文件。

        命名管道用mkfifo来创建命名管道文件再用open来通过读或者写的方式打开文件

        1.2.4 实现进程间通信 

        用命令行实现进程间的通信:

        用命名管道系统调用实现client和server进程的通信。

client.c文件代码:

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<string.h>
  4 #include<fcntl.h>
  5 #include<stdlib.h>
  6 #include<sys/types.h>
  7 #include<sys/stat.h>
  8 
  9 int main(){
 10 
 11     int fd=open("./fifo",O_WRONLY);//写的方式打开进程
 12     if(fd==-1){
 13       perror("open error");
 14       exit(-1);
 15     }
 16     char str[60];
 17     while(1){
 18       printf("please enter message #");
 19       fflush(stdout);
 20       ssize_t n=read(0,str,sizeof(str)-1);//从标准输入中获取字符串
 21       if(n>0){
 22         str[n]=0;
 23         write(fd,str,n);//写道管道中
 24       }                                                                  
 25     }
 26 
 27   return 0;
 28 }

server.c文件代码

服务器读端怎么退出?

        管道的生命周期是随进程的,用户端(client)写端进程退出,服务器端(server)读端最终会读到管道的结尾。然后再退出进程即可。

1 #include<stdio.h>                                                        
  2 #include<unistd.h>
  3 #include<string.h>
  4 #include<fcntl.h>
  5 #include<stdlib.h>
  6 #include<sys/types.h>
  7 #include<sys/stat.h>
  8 
  9 int main(){
 10     umask(0);//设置掩码为0
 11     int res=mkfifo("./fifo",0644);//创建一个命名管道
 12       if(res==-1){
 13         perror("mkfifo error");
 14         exit(2);
 15       }
 16 
 17     int fd=open("./fifo",O_RDONLY);//只读的方式打开管道
 18     if(fd==-1){
 19       perror("open errord");
 20       exit(-1);
 21     }
 22     while(1){
 23       char str[256]={0};
 24       ssize_t n=read(fd,str,sizeof(str)-1);//读管道里的内容
 25       if(n>0){
 26         str[n]=0;
 27         printf("%s",str);
 28       }
 29       else if(n==0){//写进程退出,管道里的数据读完,退出进程
 30         printf("client quit,me too\\n");
 31         break;
 32       }
 33       else{
 34 
 35       }
 36     }
 37 
 38   return 0;
 39 }

        服务器不仅可以将用户输入的数据往显示器上打,还可以往文件上打,首先将要打入的文件打开,适用dup2(1,打开文件的文件描述符) ,就会将信息往文件里打了。

#include<stdio.h>                                                                                                                                        
  2 #include<unistd.h>
  3 #include<string.h>
  4 #include<fcntl.h>
  5 #include<stdlib.h>
  6 #include<sys/types.h>
  7 #include<sys/stat.h>
  8 
  9 int main(){
 10     umask(0);//设置掩码为0
 11     int res=mkfifo("./fifo",0644);//创建一个命名管道
 12       if(res==-1){
 13         perror("mkfifo error");
 14         exit(2);
 15       }
 16 
 17     int fd=open("./fifo",O_RDONLY);//只读的方式打开管道
 18     int od=open("./test.txt",O_WRONLY| O_CREAT, 0644);//打开一个文件
 19     if(fd==-1){
 20       perror("open errord");
 21       exit(-1);
 22     }
 23     dup2(od,1);//重新向
 24     close(od);
 25     while(1){
 26       char str[256]={0};
 27       ssize_t n=read(fd,str,sizeof(str)-1);//读管道里的内容
 28       if(n>0){                                                             
 29         str[n]=0;         
 30 
 31         printf("%s",str);
 32       }
 33       else if(n==0){//写进程退出,管道里的数据读完,退出进程
 34         printf("client quit,me too\\n");
 35         break;
 36       }
 37       else{
 38 
 39       }
 40     }
 41 
 42   return 0;
 43 }

 命名管道文件到底是什么?

实际上命名管道fifo只是一个标志,实际系统会在内存开辟一段空间给管道,往管道里写入就是往这块内存中写入,从管道里读,就是从内存中读。

因为如果是真的在硬盘上创建一个文件,再从往硬盘文件写和读,就是往外设写和读,就是系统的I/O了。这样的时间效率回很低。 

        1.3 总结

        匿名管道还是命名管道都是通过文件的方式,来让两个进程看到同一份资源。通过一个进程对文件进行写操作,一个文件进程读操作,来实现进程间的通信。

        管道实现进程通信是单向的。同步和互斥的。

        匿名管道适用于具有血缘关系的进程,命名管道可以用于不相关的进程。

        理解一个命令:who | wc - l

中间的'|'是一个匿名管道,who和wc是两个进程,'|'系统通过pipe创建一个匿名管道,bash创建子进程who,bash再创建子进程wc,who和wc是兄弟进程。who和wc都会继承bash的匿名管道文件。who和wc看到统一资源,who往管道写数据,wc从管道读数据。往后数据写完退出,wc读数据到最后也退出了。

以上是关于进程间通信之管道的主要内容,如果未能解决你的问题,请参考以下文章

Linux之进程间通信

多进程编程之进程间通信-管道和消息队列

Linux之进程间通信

Linux进程通信之匿名管道

linux之进程间通信——管道

简述Linux进程间通信之管道pipe(下)