【Linux】进程间通信--管道(匿名管道和命名管道)

前言

进程间的通信,其实就是两个进程需要进行交流,从另一个角度上想:进程本身就是程序员给开辟的,所以进程间通信也就是程序员的通信;
我们需要理解的是:进程通信是存在协同的场景的(也就是有顺序的场景);
比如:cat test.c | wc -l 这条命令:就是把 cat进程的结果交给wc进程去执行,这就是先后顺序;


但是我们知道进程之间是独立的,那么也就是说:不同的进程是看不到对方的资源的;
这么来说既然独立,进程是如何通信的呢?
那么肯定是通过一定的媒介进行通信,而这个媒介就需要操作系统介入管理了;
也就是说,操作系统要设计出一套媒介使得进程之间可以进行通信;
也就是说:两个进程要通信,必须看到一份公共的媒介资源;
这个媒介资源需要有的功能就是保存进程之间的数据,因为进程通信就是交换数据嘛!
所以说在操作系统中,这个媒介资源就是一段内存,进程间通信是通过公共的内存媒介进行的
而这个公共资源的组织形式可以有很多种:比如以文件的形式,队列的形式,或者就是内存的形式;
这不同的组织形式也形成了进程间通信的方式不同;


在这里插入图片描述


进程间通信的目的

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

管道

管道分为匿名管道和命名管道;
匿名管道是用于父子之间通信的一个通信方式

管道就是一个文件,被打开的文件,该文件的struct_file里有个成员指向了内核缓冲区,而这个内核缓冲区,我们就把它成为管道;我们进程间就可以通过这个内核缓冲区进行数据传送;
由于被打开的文件属于操作系统管理的,不属于进程的范畴,所以我们进程之间就可以通过这个管道媒介进行通信;


在这里插入图片描述


匿名管道

操作系统提供了一个接口来让我们创建管道,让我们进行通信;
一般这个管道都是父进程创建,然后父进程fork子进程,这样父子进程就可以通过管道进行通信了;

 #include <unistd.h>
功能:创建一无名管道

原型 
int pipe(int fd[2]);

参数 
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端 
这个是一个输出型参数,只要我们给它一个数组名,他就会返回两个元素fd[0] 和fd[1] 了

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


当用户层待用了pipe函数,那么内核就会给该进程开辟一个管道文件,并且返回fd[0]和fd[1]两个文件描述符号给用户层,让用户层得到这两个文件描述符就可以操作内核管道文件了;
在这里插入图片描述


简单的看看管道返回的两个文件描述符号:

在这里插入图片描述


测试结果:很明显:因为文件描述符前三个0 1 2 都被占用了,所以返回的都是从 3 和 4开始;

在这里插入图片描述


管道特点

只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;
管道提供流式服务:

  1. 一般而言,进程退出,管道释放,所以管道的生命周期随进程,由OS回收
  2. 一般而言,内核会对管道操作进行同步与互斥;(同步在这里表示:管道没数据,读端需要等待,管道数据满了,写端需要等待)
  3. 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
  4. 管道的大小为64KB,并且当管道满的时候,假如不从管道读取数据,管道就不会再被写入数据;并且管道满时候,只有当读取管道的数据大于4KB时候,管道才会被继续写入数据;
  5. 当我们的写端关闭了,读端一直读,当读到管道结尾,然后就读完就可以了;
  6. 当我们读端关闭了,写端还一直写,此时OS就会介入管理,终止该进程继续写入;

在这里插入图片描述


站在文件描述符角度理解管道

在用户层,我们通过父进程创建一个管道文件(该文件由两个文件描述符指向,一个为读端,一个为写端);
然后通过父进程创建子进程,子进程就会复制父进程的pcb,pcb里的有个成员是指向文件描述符表的指针,也会被复制过去到子进程;
这样父子进程就有了各自PCB,并且父子进程PCB都有格子指针指向文件描述符表,父子进程的文件描述符表都指向父进程打开的文件也就是管道;
这个父进程打开的管道文件结构体又有个成员指向了内核缓冲区的结构体;
这样父子进程都可以通过各自的文件描述符表找到同一个内核缓冲区,也就是可以进行通信了;


在这里插入图片描述


匿名管道通信读写特点

完成一份代码:
父进程创建管道,父进程读取管道内容,子进程向管道写入内容,子进程每写1次,sleep(1);
特点:往管道写的慢,读得快;


#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main(){
    
    
  int pipeid[2];
  //父进程创建匿名管道
  if(pipe(pipeid) != 0){
    
    
    perror("pipe error:");
  }
  //来到这里表示管道创建成功
  printf("fd[0]:%d\n",pipeid[0]);
  printf("fd[1]:%d\n",pipeid[1]);
  //进程父子通信:父进程读取数据,子进程写入数据
  //父进程关闭写端pipeid[1],子进程关闭读端pipeid[0]
  
  if(fork() == 0){
    
    
    //子进程
    close(pipeid[0]);
   const char* buffer = "hello world\n";
    while(1){
    
    
      write(pipeid[1],buffer,strlen(buffer));
      sleep(1); 
  }
     exit(0);
  }
  
  //由于子进程运行结束后退出,所以到这里肯定是父进程执行父进程
  close(pipeid[1]);
  while(1){
    
    
    char buf[64] = {
    
    0};
    //当read返回0表示子进程关闭了文件描述符fd[1]
    ssize_t sz = read(pipeid[0],buf,sizeof(buf));
    if(sz < 0)
      break;
    else if( sz == 0 ) //在这里read返回0表示管道文件的写段关闭了
      break; 
    else{
    
     
      printf("child say to father:%s\n",buf);
    }
  }
    return 0;
}

查看结果:父进程收到了子进程不断发过来的消息,这就是完成了管道之间的通信
并且我们发现:当读完管道数据时候,管道就会等到一会,等管道有了数据才会继续读;
注意:由于上面是死程序,需要按ctrl+c终止循环;
在这里插入图片描述


这次我们修改代码:让父进程读取慢,子进程写得快;往管道读的慢,写的快
修改上面代码:也就是让父进程每隔着sleep(1)再读;注释掉子进程的sleep(1);
运行观察结果:


观察结果:
在这里插入图片描述


我们发现一个特点:
只要pipe管道里有缓冲区空间,那么就会一直写入数据;
只要pipe管道里有数据,那么就会一直读;

上面的代码并没有规定,你读取的数据,需要管道的哪儿读,只要管道有数据 ,那么它就开始读,所以你看到的代码时不规则的;
于此同时,往管道里写数据,也没有规定你必须从管道哪个位置写数据,只要管道有空位置,那么我们就往管道写数据;


这次我们尝试只让子进程一直写,每次写1个字符,统计写入的次数,父进程不读
我们看看管道的大小是多少?


#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main(){
    
    
  int pipeid[2];
  if(pipe(pipeid) != 0){
    
    
    perror("pipe error:");
  }
  //进程父子通信:父进程读取数据,子进程写入数据
  //父进程关闭写端pipeid[1],子进程关闭读端pipeid[0]
  if(fork() == 0){
    
    
    //子进程
    close(pipeid[0]);
      while(1){
    
    
      write(pipeid[1],'a',1); 
      count++;
      printf("count:%d\n",count);
  }
     exit(0);
  }
  //由于子进程运行结束后退出,所以到这里肯定是父进程执行父进程
  close(pipeid[1]);
  while(1){
    
    
	sleep(1);
  }
    return 0;
}

在这里插入图片描述


观察结果:我们发现是:65536个字节,也就是:64KB;
于此同时,当我们往管道写满时候,当我们不对管道内容读取时候,管道就不会继续再写了;


我们继续测试:当子进程往写数据使得管道被写满时候,父进程从管道读数据,到底读走多少数据,子进程才会继续向管道写入数据?

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main(){
    
    
  int pipeid[2];
  if(pipe(pipeid) != 0){
    
    
    perror("pipe error:");
  }
  //进程父子通信:父进程读取数据,子进程写入数据
  //父进程关闭写端pipeid[1],子进程关闭读端pipeid[0]
  if(fork() == 0){
    
    
    //子进程
    close(pipeid[0]);
      while(1){
    
    
      write(pipeid[1],'a',1); 
      count++;
      printf("count:%d",count);
  }
     exit(0);
  }
  //由于子进程运行结束后退出,所以到这里肯定是父进程执行父进程
  	close(pipeid[1]);
 	while(1){
    
    
  		sleep(10); //保证子进程先把缓冲区管道写满
  		char buf[1024*4+1] = {
    
    0}; //尝试从管道读取数据为4KB+1bytes
		ssize_t sz = read(pipeid[0],buf,sizeof(buf));
  }
  	close(pipe[0]); //关闭读端;
  	waitpid(-1,NULL,0);
    return 0;
}

测试发现:
只有当我们读取的数据大于PIPE_BUF = 4KB(linux下的PIPE_BUF是4kb),也就是当管道数据满的时候,读取管道的数据大于4kb时候,管道才会被继续写入;


我们继续测试:当我们关闭读端,写端还一直写,会发什么事?


#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main(){
    
    
  int pipeid[2];
  if(pipe(pipeid) != 0){
    
    
    perror("pipe error:");
  }
  //进程父子通信:父进程读取数据,子进程写入数据
  //父进程关闭写端pipeid[1],子进程关闭读端pipeid[0]
  if(fork() == 0){
    
    
    //子进程
    close(pipeid[0]);
   const char* buffer = "hello world\n";
    while(1){
    
    
      write(pipeid[1],buffer,strlen(buffer));
  }
     exit(0);
  }
  //由于子进程运行结束后退出,所以到这里肯定是父进程执行父进程
  close(pipeid[1]);
  sleep(5); //保证子进程先写入数据
  char buf[64] = {
    
    0};
 	size_t sz =read(pipeid[0],buf,sizeof(buf));
 	close(pipeid[0]);
    return 0;
}

测试发现:我们的进程会直接退出。原因是,读端关闭了,再继续往管道写数据,没有任何意义,所以说OS会直接终止该进程继续写入,发13号信号给该进程终止,也就是SIGPIPE;


命名管道

命名管道和匿名管道的最主要区别在于:
命名管道可以在任意进程之间通信,不用局限于匿名管道只能在有血缘关系的进程通信;


  1. 在Linux命令行,可以通过命令创建命名管道mkfifo myfifo ,这个表示创建一个文件名为myfifo的命名管道;
    在这里插入图片描述

创建成功观察,myfifo文件的文件类型是p,这就表示该文件是一个管道文件;

只要有命名管道,我们就可以进行通信:
我们尝试使用两个进程来命名管道进行通信:
我们往 myfifo 使用echo进程输入数据,我们使用cat进程从myfifo中读取数据:
在这里插入图片描述
很明显上面的方式完成不同的进程之间进行通信;


命名管道的原理

我们知道进程之间通信成本代价是比较高的,原因就是进程之间本身就是独立的,还要进行通信,就必须保证通信之间的进程要看到一份公共的资源,使得不同进程通过这个公共资源进行通信,而我们的匿名管道就是通过子进程继承父进程的PCB资源使得他们能够看到同一份资源(这份资源必定是操作系统维护的,因为操作系统除了要保证进程之间的独立性,也要保证进程能够通信,这份资源也就是匿名管道必须由操作系统维护)。


我们知道我们表示一个磁盘文件是通过路径+文件名标识的;这是一个认知的前提;
其次我们还需要知道,一个文件是可以被以读的方式打开,并且同时以写的方式打开,也就是一个文件可以被打开多次;
再次我们需要知道,当文件被打开时候,必定是在内存中有一份数据结构struct file 来维护这个被打开的文件的;
而我们的命名管道的工作原理就是基于上面的认知:不同的进程同时打开一个文件(命名管道文件),分别以读写的方式打开,达到不同的进程之间能够进行通信;


命名管道的创建

在操作系统上会给用户提供一份系统调用接口就是:mkfifo,让用户可以创建一个命名管道;
当我们成功创建该文件,也就是在磁盘上会有这一个管道文件;
只有当我们通过open打开这个管道文件,它才会被加载到内存中;

并且还有一个重要的特点:当我们向命名管道中写入数据时候,该数据并不会刷新到磁盘中的命名管道文件,当我们从命名管道中读取数据时候,也不会是从磁盘文件的命名管道读取,而是从打开的命名管道读取数据;
这么做就是为了效率!

在这里插入图片描述


成功返回0 失败返回-1;

注意:上面的权限是受系统的umask影响的;
在这里插入图片描述
也就是说:当你实际创建出来的管道文件的权限,是等于 你传入的mode & ~umask;(一般系统的umask默认权限为002)


通过代码简单的创建管道看看:

#include<stdio.h>
#include<sys/types.h>
#include <sys/stat.h>

#define MY_FIFO "./myfifo"
int main(){
    
    
  if(mkfifo(MY_FIFO,0664) < 0){
    
    
    perror("mkfifo:");
  }

  return 0;
}

一旦运行该代码:那么成功就会直接创建一个命名管道:myfifo;
但是当我第二次再运行该程序,那么就会创建命名管道失败,原因就是当前路径下,已经有了命名管道;
也就是说,在该路径下有了该命名管道,那么就会创建失败;


命名管道完成两个不同进程通信

我们搞一个场景:一个server 进程:读取一个cilent 进程发过来的数据;达到两个进程之间通信的目的;


首先我们完成server.c文件:
这个文件用来创建管道文件,并且读取client发数据到管道文件的数据;

#include<stdio.h>
#include<sys/types.h>
#include <sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#define MY_FIFO "./myfifo"
int main(){
    
    
  umask(0); //修改系统的默认文件权限为000
  if(mkfifo(MY_FIFO,0664) < 0){
    
    
    perror("mkfifo:");
    return 1;
  }
  //当管道文件创建成功,就可以拿这个管道文件进行通信了,
  //这个管道文件,在逻辑上可以相当于普通文件的处理方式一模一样
  
  //首先我们要打开该管道文件,我们管道文件以读的方式打开,
  //目的接收另一个进程client的发过来的数据;
  int fd = open(MY_FIFO,O_RDONLY);
  if(fd < 0){
    
    
    perror("open of read:");
    return 2;
  }
  //打开成功管道文件,这里就是进程通信的业务逻辑代码了
  //打开管道文件成功,那么我们就开始读取管道文件的数据
  while(1){
    
    
    char buffer[64] = {
    
    0};//每次读取管道文件,都清一下零
    ssize_t sz = read(fd,buffer,sizeof(buffer)-1);//减1的目的,是预留一个位置给\0存放,因为我想把该数据看成字符串

    if(sz > 0){
    
    
      //成功读到数据
      buffer[sz] = '\0'; //把读到的数据当作字符串看待
      printf("client sent to server:%s\n",buffer);
    }
    else if(sz == 0){
    
    
      //表示对方(在这是client端关闭了管道文件)关闭了管道文件
      printf("client close...\n");
      break; //客户端退出,我们服务器也退出,不然就会一直运行咯
    }
    else{
    
    
      perror("read:");
      break;
    }
  }
  

  close(fd);
  return 0;
}

再搞一个client.c:
它用来向管道文件写入数据,让服务器server读到client发送的数据;

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main(){
    
    
 //对于client端,它不需要再次创建一个管道文件了,
 //因为在server端已经创建好了,我们在client拿来用就行
 
  //client 要完成对管道文件写入操作
  //打开server创建好的管道文件,以写的方式打开
  int fd = open("./myfifo",O_WRONLY);
  if(fd < 0){
    
    
    perror("open of wirte:");
    return 1;
  }
  //打开管道文件成功
  //这里就是我们通信的业务逻辑
  
  //我们需要把数据写入到管道文件
  //那么我们就需要从键盘读取数据,写入到管道文件中
  while(1){
    
    
    char buffer[64] = {
    
    0};
    
    printf("plase input some data to server:");
    fflush(stdout);
    ssize_t sz = read(STDIN_FILENO,buffer,sizeof(buffer)-1);
    if(sz < 0){
    
    
      perror("read from stdin:");
      return 2;
    }
    //读取键盘数据成功,那么把从键盘读取的数据写到管道文件中
    buffer[sz-1] = '\0';//在sz-1位置复制\0是因为我们要抵消从键盘输入的回车键的\n 
                        //把键盘读取的数据当作字符串看待
    write(fd,buffer,strlen(buffer));

  
  }
  return 0;
}


当我们编译上面两个文件时候:
必须先启动server进程,然后再启动client进程;
然后clent就可以发数据给server了;


在这里插入图片描述


上面就完成了两个不同进程之间的通信;


注意:
假如先执行client进程,那么就会失败,因为,管道文件的创建在server进程中,你先执行client进程,都还没有管道文件,肯定会出错;
第二:server执行第一次后,再第二次执行就失败,原因是:第一次执行的管道文件还没有被删除,第二次执行就会失败;


匿名管道和命名管道区别

1.为什么匿名管道和命名管道的名字叫法有区别?

1.匿名管道,不需要给管道取名字也可以通信,是因为子进程会继承父进程的信息,这样就达到了父子进程都可看到同一份资源,这样就可以通信了;
2.命名管道,需要有名字原因是,不同进程通信,需要借助该管道的名字来达到,看到同一份资源,才可以进行通信;


猜你喜欢

转载自blog.csdn.net/m0_46606290/article/details/124259324