进程间通信-----管道

管道

首先了解一下进程通信是什么?
  • Linux 环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程 1 把数据从用户空间拷到内核缓冲区,进程 2 再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。
    在这里插入图片描述
  • 在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方式有:
    ① 管道 (使用最简单)
    ② 信号 (开销最小)
    ③ 共享映射区 (无血缘关系)
    ④ 本地套接字 (最稳定)
管道的概念:
  • 管道是一种最基本的 IPC 机制,作用于有血缘关系的进程之间,完成数据传递。调用 pipe 系统函数即可创建一个管道。有如下特质:
    1. 其本质是一个伪文件(实为内核缓冲区)
    2. 由两个文件描述符引用,一个表示读端,一个表示写端。
    3. 规定数据从管道的写端流入管道,从读端流出。
  • 管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。
  • 管道的局限性:
    ① 数据不能进程自己写,自己读。
    ② 管道中数据不可反复读取。一旦读走,管道中不再存在。
    ③ 采用半双工通信方式,数据只能在单方向上流动。
    ④ 只能在有公共祖先的进程间使用管道。
  • 常见的通信方式有,单工通信、半双工通信、全双工通信。
管道可以分为匿名管道和命名管道

匿名管道

#include <unistd.h>
功能:创建无名管道
原型    int pipe(int fd[2]);
参数   fd:⽂文件描述符数组,其中 fd[0] 表⽰示读端, fd[1] 表⽰示写端
返回值:成功返回0,失败返回错误代码
  • 来看一个从键盘中输入,写入管道,读取管道,再从管道中读取到屏幕上
  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #include <unistd.h>
  5 int main( void )
  6 {
  7     int fds[2];
  8     char buf[100];
  9     int len;
 10     if ( pipe(fds) == -1 )
 11         perror("make pipe"),exit(1);
 12         //read from stdin
 13     while ( fgets(buf, 100, stdin) ) {
 14         len = strlen(buf);
 15         // write into pipe
 16         if ( write(fds[1], buf, len) != len ) {
 17             perror("write to pipe");
 18             break;
 19         }
 20         memset(buf, 0x00, sizeof(buf));
 21         // read from pipe
 22         if ( (len=read(fds[0], buf, 100)) == -1 ) {
 23             perror("read from pipe");
 24             break;
 25         }
 26         // write to stdout
 27         if ( write(1, buf, len) != len ) {
 28             perror("write to stdout");
 29             break;
 30         }
 31     }
 32 }

在这里插入图片描述

  • 我们在站在文件描述符角度,添加进程看看管道。
  • 函数调用成功返回 r/w 两个文件描述符。无需open,但需手动close。规定:fd[0] → r; fd[1] → w,就像0对应标准输入,1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。
  • 管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?通常可以采用如下步骤:
    在这里插入图片描述
  1. 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。
  2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
  3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
  • 我们来看代码,程序创建一个从父进程到子进程的管道,并且父进程经由该管道向子进程传送数据。
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<string.h>
  4 int main()
  5 {
  6     int fd[2];
  7     int pid;
  8 
  9     int ret = pipe(fd);
 10     if(ret < 0)
 11     {
 12         perror("pipe error");
 13         return -1;
 14     }
 15     pid = fork();
 16     if(pid < 0)
 17     {
 18         perror("fork error");
 19         return -1;
 20     }else if(pid > 0)
 21     {
 22         close(fd[0]);
 23         char* buf = "hello world!!\n";
 24         write(fd[1], buf, strlen(buf));
 25     }else
 26     {
 27         close(fd[1]);
 28         int line[1024] = {0};
 29         int n = read(fd[0],line,1024);
 30         write(STDOUT_FILENO,line,n);
 31     }
 32 
 33     return 0;
 34 }

在这里插入图片描述

  • 在上面的例子中,直接对管道描述符调用了read和write。更有趣的是将管道描述符复制到了标准输入或标准输出上。通常会在此执行另一个程序,该程序或者标准输入(已经创建的管道)读数据,或者将数据写至其标准输出(该管道)。
管道读写规则
  • 当没有数据可读时
    • 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,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务。(数据的流式服务:体现的是数据的发送和接收的灵活性,但是数据没有边界,容易沾包(多条数据连接到一起了,接收方没有办法分辨数据界限))。
  • 一般⽽言,进程退出,管道释放,所以管道的生命周期随进程的结束而结束。
  • 一般⽽言,内核会对管道操作进行同步与互斥
    • 同步:保证一个操作访问的时序性(我操作完了,你再操作)
    • 互斥:保证操作的同一时间唯一访问(我操作的时候,你不能操作)
  • 管道是半双工的,数据只能向⼀个⽅向流动;需要双⽅通信时,需要建立起两个管道。
我在演示一下 ps -ef | grep ssh 这条命令中的管道
  1 #include<stdio.h>
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<string.h>
  4 
  5 
  6 int main()
  7 {
  8     int pid = -1;
  9     int pipefd[2] = {0};
 10 
 11     if(pipe(pipefd) < 0)
 12     {
 13         perror("pipe error");
 14         return -1;
 15     }
 16     pid = fork();
 17     if(pid < 0)
 18     {
 19         perror("fork error");
 20         return -1;
 21     }
 22     else if(pid == 0)
 23     {
 24         dup2(pipefd[0],0);
 25         close(pipefd[1]);
 26         execl("/bin/grep","grep","ssh",NULL);
 27     }
 28     else
 29     {
 30         dup2(pipefd[1],1);
 31         close(pipefd[0]);
 32         execl("/bin/ps","ps","-ef",NULL);
 33     }
 34     return 0;
 35 }

在这里插入图片描述

命名管道

  • 管道应⽤的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想在不相关的进程之间交换数据,可以使用FIFO⽂件来做这项工作,它经常被称为命名管道。命名管道是⼀种特殊类型的⽂件。
  • 命名管道可以从命令行上创建,命令行方法是使用下面这个命令
  • mkfifo filename
    在这里插入图片描述
  • 大家可以看到创建出来的是管道文件。
    在这里插入图片描述
    上图是我们将“hello world”写入到管道中,在打开一个shell,我们就可以将管道中的数据拿到。
    在这里插入图片描述
  • 系统也提供了创建命名管道的函数
    int mkfifo(const char *filename,mode_t mode);
int main(int argc, char *argv[])
{
	mkfifo("test", 0644);
	return 0;
}

在这里插入图片描述

命名管道和匿名管道的区别
  • 匿名管道由pipe函数创建并打开。
  • 命名管道由 mkfifo 函数创建,打开用open
  • FIFO(命名管道)与pipe(匿名管道)之间唯⼀一的区别在它们创建与打开的⽅方式不同,一但这些⼯作完成之后,它们具有相同的语义。
命名管道打开规则
  • 如果当前打开操作是为读而打开 FIFO 时
    • O_NONBLOCK disable:阻塞直到有相应进程为写而打开该 FIFO
    • O_NONBLOCK enable:立刻返回成功
  • 如果当前打开操作是为写而打开FIFO时
    • O_NONBLOCK disable:阻塞直到有相应进程为读而打开该FIFO
    • O_NONBLOCK enable:立刻返回失败,错误码为ENXIO
  • 输入管道数据的一方
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<errno.h>
  4 #include<fcntl.h>
  5 #include<string.h>
  6 
  7 int main()
  8 {
  9     char* file = "./test.fifo";
 10     umask(0);
 11     if( mkfifo(file,0664) < 0)
 12     {
 13         if(errno == EEXIST)
 14         {
 15             printf("fifo exit!!\n");
 16         }
 17         else
 18         {
 19             perror("mkfifo");
 20             return -1;
 21         }
 22     }
 23 
 24     int fd = open(file, O_WRONLY);
 25     if(fd < 0)
 26     {
 27         perror("open error");
 28         return -1;
 29     }
 30     printf("open fifo success!!\n");
 31     while(1)
 32     {
 33         char buf[1024] = {0};
 34         printf("please input:");
 35         fflush(stdout);
 36         scanf("%s",buf);
 37         write(fd,buf,strlen(buf));
 38     }
 39     return 0;
 40 }

  • 读取管道数据的一方
  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<errno.h>
  4 #include<fcntl.h>
  5 #include<string.h>
  6 
  7 int main()
  8 {
  9     char* file = "./test.fifo";
 10     umask(0);
 11     if( mkfifo(file,0664) < 0)
 12     {
 13         if(errno == EEXIST)
 14         {
 15             printf("fifo exit!!\n");
 16         }
 17         else
 18         {
 19             perror("mkfifo");
 20             return -1;
 21         }
 22     }
 23 
 24     int fd = open(file, O_RDONLY);
 25     if(fd < 0)
 26     {
 27         perror("open error");
 28         return -1;
 29     }
 30     printf("open fifo success!!\n");
 31 
 32     while(1)
 33     {
 34         char buff[1024];
 35         memset(buff,0x00,1024);
 36         int ret = read(fd,buff,1024);
 37         if(ret > 0)
 38         {
 39             printf("peer say:%s\n",buff);
 40         }
 41     }
 42     close(fd);
 43 
 44     return 0;
 45 }

在这里插入图片描述
上图是往管道中输入数据的一方,我们在打开一个shell,下图就是从管道中读取数据的一方。

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值