【Linux】一篇文章搞定 进程间通信 之 管道

前言:

Q:为什么需要进程间通信呢?

A:原因是由于进程拥有自己独立的进程虚拟地址空间,从而导致了进程的独立性,通过进程间的通信,可以让不同的进程之间进行协作。

1 管道符【|】的理解

ps aux | grep XXX

该命令的作用:是将当前正在运行的所有进程的进程名带有XXX的进程过滤打印出来

  • ps和grep是两个不同的命令,同时也是两个不同的程序
  • ps程序的执行结果,通过管道,传输给grep程序。
  • 换言之,即ps程序的输出作为grep程序的输入

管道就是在内核中开辟一块内存,也叫缓冲区
在这里插入图片描述

2 匿名管道

匿名管道概念

匿名管道是程序在内核中开辟出来的一块没有标识符内存空间

2.1 创建匿名管道:使用pipe接口

int pipe(int pipefd[2]);

参数:

  • fd是一个输出型参数,在调用函数内部进行赋值并带出函数
  • fd[2]:整形数组,有两个元素:fd[0] 和 fd[1]

返回值:0 代表成功,-1代表失败

头文件:unistd.h

程序演示:查看输出型参数fd从pipe函数中带出来了什么数据?

#include <stdio.h>
#include <unistd.h>

int main()
{
    
    
  int fd[2];
  int ret_pipe = pipe(fd);                          
  if(ret_pipe < 0)
    perror("pipe");
  else
    printf("fd[0] = %d , fd[1] = %d\n",fd[0],fd[1]);

  while(1)
  {
    
    
    sleep(1);
  }

  return 0;
}

输出结果:

fd[0] = 3 , fd[1] = 4

查看该进程使用文件描述符情况

[gongruiyang@localhost ~]$ ll /proc/3456/fd/
总用量 0
lrwx------. 1 gongruiyang gongruiyang 64 1月   1 16:16 0 -> /dev/pts/0
lrwx------. 1 gongruiyang gongruiyang 64 1月   1 16:16 1 -> /dev/pts/0
lrwx------. 1 gongruiyang gongruiyang 64 1月   1 16:16 2 -> /dev/pts/0
lr-x------. 1 gongruiyang gongruiyang 64 1月   1 16:16 3 -> pipe:[38151]
l-wx------. 1 gongruiyang gongruiyang 64 1月   1 16:16 4 -> pipe:[38151]

其中3和4这两个文件描述符软链接文件,指向内核中一块无标识符的内存

2.2 pipe参数fd数组再理解

  • fd数组当中保存两个文件描述符

  • fd[0]是缓冲区的读端,从匿名管道中读取数据

  • fd[1]是缓冲区的写端,向匿名管道中写入数据

  • 读写是相对于缓冲区中数据的
    在这里插入图片描述

2.3 程序演示:用匿名管道进行读写演示

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main()
{
    
    
  int fd[2];
  int ret_pipe = pipe(fd);
  if(ret_pipe < 0)
    perror("pipe");
  else
  {
    
    
    //向 匿名管道 中写入w_buf中的数据
    char* w_buf = "Hello World!";       
    write(fd[1],w_buf,strlen(w_buf));
    //从 匿名管道 中读取数据到r_buf中
    char r_buf[1024];
    read(fd[0],r_buf,sizeof(r_buf) - 1);
    //输出读取出来的数据
    puts(r_buf);
  }

  return 0;
}
Hello World!

2.4 管道特征

  • 管道是单双工通信:数据只能从写端流向读端
  • 管道提供流式服务:读端可以决定每次读取多少个字节,读走多少数据,管道中就减少多少数据
  • 管道的生命周期跟随进程

2.5 验证管道通信方式是单双工通信方式

辅助接口

int fcntl(int fd, int cmd, ... /* arg */ );

功能:获取对某个文件的操作权限属性

参数:

  • fd : 待查询文件描述符
  • cmd
可选项 含义
F_GETFL 获取文件描述符属性信息
F_SETFL 设置文件描述符属性信息

返回值:当时F_GETFL的时候,返回的是文件描述符的操作属性值

操作属性值 对应宏 含义
0 O_RDONLY 只读权限
1 O_WRONLY 只写权限
2 O_RDWR 读写权限

程序演示:验证管道是单双工通信

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

int main()
{
    
    
  int fd[2];
  int ret_pipe = pipe(fd);
  if(ret_pipe < 0)
    perror("pipe");
  else
  {
    
    
    int authority_r = fcntl(fd[1],F_GETFL);
    int authority_w = fcntl(fd[0],F_GETFL);
    printf("fd[1] : %d\n",authority_r);
    printf("fd[0] : %d\n",authority_w);
  }
                                              
  return 0;
}
fd[1] : 1
fd[0] : 0
  • 由于fd[1]文件描述符的操作权限值是1,对应着O_WRONLY宏,意为只写权限

  • 由于fd[0]文件描述符的操作权限值是0,对应着O_RDONLY宏,意为只读权限

  • 所以匿名管道的通信方式是单双工通信

2.6 父子进程通过匿名管道进行进程间通信

匿名管道只支持具有情缘关系的进程之间进行通信

原因:没有亲缘关系的进程之间无法共享同一个管道

父子进程通信过程:

  1. 创建匿名管道
  2. 创建子进程
  3. 一个作为写进程,一个作为读进程

程序演示:子进程向匿名管道写入数据,父进程从匿名管道中读取数据

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main()
{
    
    
  //1.创建匿名管道
  int fd[2];
  int ret_pipe = pipe(fd);
  if(ret_pipe < 0)
  {
    
    
    perror("pipe");
    return -1;
  }

  //2.创建子进程
  int pid  = fork();
  if(pid < 0)
  {
    
    
    perror("fork");
    return -1;
  }
  else if(pid == 0)
  {
    
    
    //子进程 向 管道 中写入
    close(fd[0]); // 关闭读端
    const char* w_buf = "Hello World!";
    write(fd[1],w_buf,strlen(w_buf));
  }
  else
  {
    
    
    //父进程 从 管道 中读取               
    close(fd[1]); // 关闭写端
    char r_buf[1024];
    read(fd[0],r_buf,sizeof(r_buf));
    puts(r_buf);
  }

  return 0;
}

输出

Hello World!

提醒:由于父子进程异步执行,如果父进程先进行读取,而子进程还未写入,父进程就会阻塞在读取中,等待子进程写入后再读取

2.7 匿名管道的最大写入量

程序演示:测试的出匿名管道的最大写入量

思路:一直写入并记录写入了多少

#include <stdio.h>
#include <unistd.h>

int main()
{
    
    
  int fd[2];
  int ret_pipe = pipe(fd);
  if(ret_pipe < 0 )
  {
    
    
    perror("pipe");
    return -1;
  }

  int count = 0;                    
  while(1)
  {
    
    
    write(fd[1],"h",1);
    count++;
    printf("count:%d\n",count);
  }

  return 0;
}
count:65536

所以匿名管道最大写入量为64K

  • pipe_MAXSIZE = 64K ,最多写入64K的数据
  • pipe_buf = 4K ,进程向匿名管道中写入的时候,如果写入的数据量小于于4K时,才能保证写入的原子性

写入的原子性:写入只有两种可能:

  1. 全部写入

  2. 一点没写进去

不存在只写进去一部分,这就是写入的原子性

2.8 将匿名管道读写端文件描述符设置为非阻塞属性

文件描述符的非阻塞属性:O_NONBLOCK

源码定义在/usr/include/bits/fcntl-linux.h中

#define O_NONBLOCK   04000
8进制 2进制 10进制
O_NONBLOCK 04000 00000000 00000000 00001000 00000000 2048

我们的目的是将 读写端的文件描述符 加上非阻塞属性,即

fd[0]读端:只读属性(O_RDONLY) + 非阻塞属性(O_NONBLOCK) —> O_RDONLY | O_NONBLOCK

fd[1]写端:只写属性(O_WRONLY) + 非阻塞属性(O_NONBLOCK) —> O_WRONLY | O_NONBLOCK

这里的按位或就相当于将非阻塞属性和原有属性相加

程序演示:将 匿名管道读写端加上非阻塞属性

#include <unistd.h>                                      
#include <fcntl.h>                                       
                                                         
int main()                                               
{
    
                                                            
  //创建匿名管道                                         
  int fd[2];                                             
  int ret_pipe = pipe(fd);                               
  if(ret_pipe < 0)                                       
  {
    
                                                          
    perror("pipe");                                      
    return -1;                                           
  }    
    
  //获取读写端文件描述符原有属性                         
  int authority_r = fcntl(fd[0],F_GETFL);                
  int authority_w = fcntl(fd[1],F_GETFL);  
    
  //将读写端文件描述符加上非阻塞属性                     
  fcntl(fd[0],F_SETFL,authority_r | O_NONBLOCK);         
  fcntl(fd[1],F_SETFL,authority_w | O_NONBLOCK);    
    
  //查看现在的文件属性值                                 
  int authority_r_m = fcntl(fd[0],F_GETFL);              
  int authority_w_m = fcntl(fd[1],F_GETFL);              
  printf("fd[0]-authority : %d\n",authority_r_m);  
  printf("fd[1]-authority : %d\n",authority_w_m);  
                                                   
  return 0;                                        
}            
fd[0]-authority : 2048
fd[1]-authority : 2049

按位或之后将非阻塞属性加到了 匿名管道读写端文件描述符 上

2.9 匿名管道的一些极端操作及现象解释

  • 4中情况及其现象解释:

操作一:将写端文件描述符设置为非阻塞属性,读端文件描述符仍然为阻塞属性,读端文件描述符不关闭,但是也不读取数据,写端一直写入数据

现象:会将管道写满,满之后再调用write函数写入失败返回-1,表示资源不可用

操作二:将写端文件描述符设置为非阻塞属性,读端文件描述符仍然为阻塞属性,读端文件描述符关闭,写端一直写入数据

现象:调用write的进程会收到SIGPIPE信号,导致调用write的进程退出,若是子进程退出则会变成僵尸进程,若是父进程退出则子进程变成孤儿进程

操作三:将读端文件描述符设置为非阻塞属性,写端文件描述符仍然为阻塞属性,写端文件描述符不关闭,但是也不写入数据,读端一直读取数据

现象:read函数会返回-1,表示资源不可用

操作四:将读端文件描述符设置为非阻塞属性,写端文件描述符仍然为阻塞属性,写端文件描述符关闭,读端一直读取数据

现象:read函数返回0,表示没有读取到任何内容

3 命名管道

命名管道是在内核中开辟的一段缓冲区,这段缓冲区是有标识符的

也就意味着,不同的进程通过标识符就可以找到这个缓冲区,不再要求进程之间具有亲缘关系,任意进程间都可以通过命名管道实现通信

3.1 创建命名管道的两种方式

3.1.1 命令创建命名管道

mkfifo命令:使用指定文件名创建FIFO,也成为:命名管道

mkfifo 文件名

演示实例

[gongruiyang@localhost fifoTest]$ mkfifo fifo
[gongruiyang@localhost fifoTest]$ ll
总用量 0
prw-rw-r--. 1 gongruiyang gongruiyang 0 1月   1 21:53 fifo

文件名为fifo的命名管道文件默认权限为0664,且该文件不可使用vim编辑

3.1.2 函数创建命名管道

mkfifo函数:在代码中创建命名管道

int mkfifo(const char *pathname, mode_t mode);

头文件:

  • sys/stat.h
  • sys/types.h

参数:

  • pathname : 文件名
  • mode : 权限

返回值:0 代表创建成功;-1代表创建失败

程序演示:用mkfifo创建命名管道

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

int main()
{
    
    
  int mkfifo_ret = mkfifo("./fifo_test",0664);
  if(mkfifo_ret < 0)
  {
    
    
    perror("mkfifo");
    return -1;                                
  }

  return 0;
}
[gongruiyang@localhost fifoTest]$ ll fifo_test 
prw-rw-r--. 1 gongruiyang gongruiyang 0 1月   1 22:20 fifo_test

3.2 使用命名管道实现Client和Server通信

思路:Server端创建命名管道并阻塞等待Client端打开匿名命名管道进行输入数据,数据放入管道中后,Server端进行读取并打印出来

Server端

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

int main()
{
    
    
  // 创建 命名管道
  int ret_mkfifo = mkfifo("mypipe",0664);
  if(ret_mkfifo < 0)
  {
    
    
    perror("mkpipe");
    return -1;
  }
  // sever端以 只读 方式打开 命名管道
  int ret_fd = open("mypipe",O_RDONLY);
  if(ret_fd < 0)
  {
    
    
    perror("open");
    return -1;
  }

  while(1)
  {
    
    
    char buf[1024] = {
    
     0  }; //用于存放读进来的数据
    printf("Please wait...\n");
    // 读取数据,若无数据 则阻塞等待
    ssize_t s = read(ret_fd,buf,sizeof(buf) - 1);  
    if( s > 0 )
    {
    
    
      buf[s] = 0;
      printf("client say : %s\n",buf);
    }
    else if(s == 0)
    {
    
    
      printf("Client quit,exit now!\n");
      exit(EXIT_SUCCESS);
    }
    else
      perror("read");
  }

  close(ret_fd);

  return 0;
}

Client端

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>

int main()
{
    
    
  // 客户端以只写方式打开 命名管道
  int ret_fd = open("mypipe",O_WRONLY);
  if(ret_fd < 0)
  {
    
    
    perror("open");
    return -1;
  }

  while(1)
  {
    
    
    char buf[1024] = {
    
     0 };
    printf("Please Enter Message : ");
    fflush(stdout);                                    
    // 从标准输入中读取数据放入buf中
    ssize_t ret_read = read(0,buf,sizeof(buf) - 1);
    if(ret_read > 0)
    {
    
    
      buf[ret_read] = 0;
      // 从buf中向命名管道中写入数据
      write(ret_fd,buf,strlen(buf));
    }
    else
      perror("read");
  }

  close(ret_fd);

  return 0;
}

交互演示

[gongruiyang@localhost TestPipeCS]$ ./Server 
Please wait...
client say : Hello Server!

Please wait...
client say : I'm Client!

Please wait...
Client quit,exit now!
[gongruiyang@localhost TestPipeCS]$ ./Client 
Please Enter Message : Hello Server!
Please Enter Message : I'm Client!
Please Enter Message : ^C

猜你喜欢

转载自blog.csdn.net/weixin_45437022/article/details/112094013