进程间通信之管道(匿名管道与命名管道)

简介: 首先我们先提出一个问题:进程之间为什么无法直接通信,而需要操作系统提供通信方式:经过我们上一个博客学习我们可以知道,每个进程都有独立的虚拟地址空间,一个进程在访问一个数据的时候都是通过地址来进行访问的,进过页表映射在之后访问物理内存,因此如果想要给另一个进程传递一个数据,就要把它的地址空间传递给其他进程,因为a进程的是虚拟地址,所以b进程经过页表映射也访问不了。(这个设计的初衷是:独立虚拟空间可以更稳定)

进程间通信


首先我们先提出一个问题:进程之间为什么无法直接通信,而需要操作系统提供通信方式:

经过我们上一个博客学习我们可以知道,每个进程都有独立的虚拟地址空间,一个进程在访问一个数据的时候都是通过地址来进行访问的,进过页表映射在之后访问物理内存,因此如果想要给另一个进程传递一个数据,就要把它的地址空间传递给其他进程,因为a进程的是虚拟地址,所以b进程经过页表映射也访问不了。(这个设计的初衷是:独立虚拟空间可以更稳定)


操作系统提供进程间通信方式,就是给多个需要通信的进程之间建立起一个关联:能够共同访问的一块内存,但是因为具体通信场景不同,因此有提供不同的几种方式:管道,共享内存,消息队列,信号量


管道


image.png


什么是管道


艺术来源于生活,消息传输的时候我们就流水一样,所以我们联想到了管道,因此他也有个特殊的性质半双工通信,也就是只能单向传递,同一时间不能既发送又接收管道是Unix中最古老的进程间通信的形式。我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”。

image.png

==这里我们可以看到,管道就是由操作系统管理,在内核中的一个缓冲区(内存)==所以多个进程访问同一块空间的时候就完成了通信


分类:


匿名管道:管道没有标识符,不能被其他进程找到,因此只能用于具有亲缘关系的进程间通信

命名管道:管道具有标识符,能够被其他进程找到,因此可以用于同一主机上的任意进程间通信。


管道分类——1.匿名管道


使用管道实现传输:

a进程想要进行数据传输,他先在内核空间创建了一个管道,管道向a返回文件描述符,A进程的子进程b创建,复制了a进程(父进程)的文件描述符(管道操作句柄),两个进程同时访问一块内核空间,完成数据传输(如下图)


image.png


匿名管道就像是家族财产,只有同一个家族的人才有操作句柄能操作,子进程复制父进程,会复制到同一个管道的操作句柄因此能够访问同一个管道

操作句柄:文件描述符通过文件描述符这个下标,就能找到管道的描述信息,进而找到管道进行操作

发送数据就是向管道写入数据,接收数据就是从管道读取数据

管道的本质是内核中的一块缓冲区,但是linux下一切皆文件,因此把管道也当做文件来处理注意:有些文章上说管道是-种特殊的文件(不是传统意义上的磁盘文件), 本质是内存


由下图我们可以观察,只要是同一个家族的进程,都会复制相同的操作句柄,才会进行通信,所以创建管道一定要在创建子进程之前


image.png


注意:

打开一个文件才会有句柄,创建了管道才会有句柄,但是,一个程序运行起来之后,默认就会打开三个文件:标准输入,标准输出,标准错误

#include <unistd.h>
功能:创建一无名管道
原型
int pipe(int fd[2]);
参数
fd:文件描述符数组,其中fd[0]表示读端, fd[1]表示写端
返回值:成功返回0,失败返回错误代码

image.png

匿名管道举例


实例代码(一):

image.png

运行结果:

image.png


实例代码(二):


image.png


如果管道中没有数据,read会阻塞,一直等到数据写入才会返回。

如果管道中数据满了,write会阻塞,直到有数据取出才会继续写入

管道的所有读端被关闭,则继续向管道写入数据会导致进程崩溃退出 ,其本质就是读端关闭之后,再往进去写就没有意义了,所以进程就被系统给干掉了

管道的所有写端被关闭,则read从管道读取完所有数据后,将不再阻塞,而是返回0


管道如果所有能够写入数据的描述符被关闭了,意味着管道中已经不可能有数据再进来了,因此也没有必要继续等待了,当read从管道中读取数据的时候,返回了 0 意味着这个管道已经不可能读到数据了,没必要再继续读了,因此可以通过red的返回值来决定什么时候停止从管道读取数据


管道的特点


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

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

管道提供流式服务 一般而言,进程退出,管道释放,所以管道的生命周期随进程

一般而言,内核会对管道操作进行同步与互斥 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道


image.png


举例:

ps -ef | grep pipel

|管道符: 连接两个命令,将前边命令的输出结果,交给后边命令进行处理。


思考:这个命令底层是怎么实现的?


其实执行一个指令,就是创建了一个子进程,程序替换运行了一个指令对应的程序,因此ps -ef | grep pipe1就是创建了两个进程运行了两个命令程序。因此现在要做的就是将ps进程的结果,交给grep进程ps -ef的结果:打印输出(将进程信息,写入到标准输出文件)grep pipe1的结果:捕捉标准输入的数据,进行过滤输出


实现:

1.父进程创建管道

2.创建子进程1,将子进程1的标准输出重定向到管道的写入端,程序替换为ps程序 这时候ps进程向标准输出写入数据,其实就是向管道写入数据

3.创建子进程2,将子进程2的标准输入重定向到管道的读取端,程序替换为grep程序 这时候grep程序从标准输入读取数据,其实就是从管道读取数据


实现代码:

image.png

管道分类——2.命名管道


本质:内核中的一块缓冲区,但是有自己的的名字,能够被其他进程找到,因此可以用于同一主机上任意进程的通信,所以命名管道的原理就是,一个进程创建了一个命名管道的名字,多个进程通过相同的管道名字,打开同一个管道,访问同一块内存缓冲区。


命名管道的名字:是一个可见的文件系统的管道文件

注意:命名管道文件虽然是一个文件,但是其实上是一个名字,能够让多个进程打开同一个命名管道文件,进而获取到同一个管道缓冲区的描述信息或者说操作句柄,进而访问同一块缓冲区进行通信,实质上还是内核内的缓冲区完成的,而不是文件。


image.png


管道应用的一个限制就是只能在具有共同祖先(具有亲缘关系)的进程间通信。如果我们想在不相关的进程之间交换数据,可以使用FIFO文件来做这项工作,它经常被称为命名管道。命名管道是一种特殊类型的文件


注意:一个管道的创建并不涉及缓冲区的创建因为如果管道创建了没有使用的话,缓冲区会一直存在,这样不划算,故只有在使用的时候才会涉及缓冲区创建。

创建一个命名管道


命名管道可以从命令行上创建,命令行方法是使用下面这个命令:

$ mkfifo filename

命名管道也可以从程序里创建,相关函数有:

int mkfifo(const char *filename,mode_t mode);//名称   操作权限
//成功返回0 失败返回 -1

创建命名管道:

int main(int argc, char *argv[])
{
mkfifo("p2", 0644);
return 0;
}

举例


1.写端举例:image.png

解释:不使用O_CREAT,因为如果管道文件不存在,这里也是创建了一个普通的文件,EEXIST是用来判断本来存在吗,存在返回真,不存在返回假,注意这里的fflush,刷新缓冲区写入文件


2.读端举例

image.png

注意:若使用只写的方式写入的时候,会阻塞,直到管道被任意一个程序读;若使用只读的方式读取的时候,会阻塞,直到管道被任意一个程序写入。

命名管道的打开规则


如果当前打开操作是为读而打开FIFO时

 O_NONBLOCK disable:阻塞直到有相应进程为写而打开该FIFO

 O_NONBLOCK enable:立刻返回成功

如果当前打开操作是为写而打开FIFO时

 O_NONBLOCKdisable:阻塞直到有相应进程为读而打开该FIFO

 O_NONBLOCK enable:立刻返回失败,错误码为ENXIO


匿名管道与命名管道的区别


1.匿名管道由pipe函数创建并打开。

2.命名管道由mkfifo函数创建,打开用open

3.FIFO(命名管道)与pipe(匿名管道)之间唯一的别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义。


注意:管道自带同步与互斥

互斥:通过同一时间的唯一访问, 来保证访问的安全性

 管道的读写操作在不超过PIPE_ BUF-4096字节大小时,保证原子性。——原子性:不可分割特性;原子操作: 一个操作不会被打断

同步:通过对资源的访问进行一-些条件限制, 让进程对资源的访问更加合理

 管道中没有数据,则read会阻塞;管道中数据满了 ,则write会阻塞


具体使用举例:

例子1-用命名管道实现文件拷贝


#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char* argv[])
{
  mkfifo("tp", 0644);
  int infd;
  infd = open("abc", O_RDONLY);
  if (infd == -1) ERR_EXIT("open");
  int outfd;
  outfd = open("tp", O_WRONLY);
  if (outfd == -1) ERR_EXIT("open");
  char buf[1024];
  int n;
  while ((n = read(infd, buf, 1024)) > 0)
  {
    write(outfd, buf, n);
  }
  close(infd);
  close(outfd);
  return 0;
}

读取管道,写入目标文件:


#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char* argv[])
{
  int outfd;
  outfd = open("abc.bak", O_WRONLY | O_CREAT | O_TRUNC, 0644);
  if (outfd == -1) ERR_EXIT("open");
  int infd;
  infd = open("tp", O_RDONLY);
  if (outfd == -1)
    ERR_EXIT("open");
  char buf[1024];
  int n;
  while ((n = read(infd, buf, 1024)) > 0)
  {
    write(outfd, buf, n);
  }
  close(infd);
  close(outfd);
  unlink("tp");
  return 0;
}


例子2-用命名管道实现server&client通信


# cat Makefile
.PHONY:all
all:clientPipe serverPipe
clientPipe:clientPipe.c
gcc -o $@ $^
serverPipe:serverPipe.c
gcc -o $@ $^
.PHONY:clean
clean:
rm -f clientPipe serverPipe
int main()


serverPipe.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#define ERR_EXIT(m) \
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
  umask(0);
  if (mkfifo("mypipe", 0644) < 0) {
    ERR_EXIT("mkfifo");
  }
  int rfd = open("mypipe", O_RDONLY);
  if (rfd < 0) {
    ERR_EXIT("open");
  }
  char buf[1024];
  while (1) {
    buf[0] = 0;
    printf("Please wait...\n");
    ssize_t s = read(rfd, buf, sizeof(buf) - 1);
    if (s > 0) {
      buf[s - 1] = 0;
      printf("client say# %s\n", buf);
    }
    else if (s == 0) {
      printf("client quit, exit now!\n");
      exit(EXIT_SUCCESS);
    }
    else {
      ERR_EXIT("read");
    }
  }
  close(rfd);
  return 0;
}

clientPipe.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#define ERR_EXIT(m) \
do{\
perror(m);\
exit(EXIT_FAILURE);\
}while(0)
int main()
{
  int wfd = open("mypipe", O_WRONLY);
  if (wfd < 0) {
    ERR_EXIT("open");
  }
  char buf[1024];
  while (1) {
    buf[0] = 0;
    printf("Please Enter# ");
    fflush(stdout);
    ssize_t s = read(0, buf, sizeof(buf) - 1);
    if (s > 0) {
      buf[s] = 0;
      write(wfd, buf, strlen(buf));
    }
    else if (s <= 0) {
      ERR_EXIT("read");
    }
  }
  close(wfd);
  return 0;
}

结果:

image.png


目录
相关文章
|
25天前
|
存储 Unix Linux
进程间通信方式-----管道通信
【10月更文挑战第29天】管道通信是一种重要的进程间通信机制,它为进程间的数据传输和同步提供了一种简单有效的方法。通过合理地使用管道通信,可以实现不同进程之间的协作,提高系统的整体性能和效率。
|
3月前
|
消息中间件 Unix Linux
C语言 多进程编程(二)管道
本文详细介绍了Linux下的进程间通信(IPC),重点讨论了管道通信机制。首先,文章概述了进程间通信的基本概念及重要性,并列举了几种常见的IPC方式。接着深入探讨了管道通信,包括无名管道(匿名管道)和有名管道(命名管道)。无名管道主要用于父子进程间的单向通信,有名管道则可用于任意进程间的通信。文中提供了丰富的示例代码,展示了如何使用`pipe()`和`mkfifo()`函数创建管道,并通过实例演示了如何利用管道进行进程间的消息传递。此外,还分析了管道的特点、优缺点以及如何通过`errno`判断管道是否存在,帮助读者更好地理解和应用管道通信技术。
|
3月前
|
SQL 网络协议 数据库连接
已解决:连接SqlServer出现 provider: Shared Memory Provider, error: 0 - 管道的另一端上无任何进程【C#连接SqlServer踩坑记录】
本文介绍了解决连接SqlServer时出现“provider: Shared Memory Provider, error: 0 - 管道的另一端上无任何进程”错误的步骤,包括更改服务器验证模式、修改sa用户设置、启用TCP/IP协议,以及检查数据库连接语句中的实例名是否正确。此外,还解释了实例名mssqlserver和sqlserver之间的区别,包括它们在默认设置、功能和用途上的差异。
|
4月前
|
消息中间件 Linux 开发者
Linux进程间通信秘籍:管道、消息队列、信号量,一文让你彻底解锁!
【8月更文挑战第25天】本文概述了Linux系统中常用的五种进程间通信(IPC)模式:管道、消息队列、信号量、共享内存与套接字。通过示例代码展示了每种模式的应用场景。了解这些IPC机制及其特点有助于开发者根据具体需求选择合适的通信方式,促进多进程间的高效协作。
176 3
|
4月前
|
开发者 API Windows
从怀旧到革新:看WinForms如何在保持向后兼容性的前提下,借助.NET新平台的力量实现自我进化与应用现代化,让经典桌面应用焕发第二春——我们的WinForms应用转型之路深度剖析
【8月更文挑战第31天】在Windows桌面应用开发中,Windows Forms(WinForms)依然是许多开发者的首选。尽管.NET Framework已演进至.NET 5 及更高版本,WinForms 仍作为核心组件保留,支持现有代码库的同时引入新特性。开发者可将项目迁移至.NET Core,享受性能提升和跨平台能力。迁移时需注意API变更,确保应用平稳过渡。通过自定义样式或第三方控件库,还可增强视觉效果。结合.NET新功能,WinForms 应用不仅能延续既有投资,还能焕发新生。 示例代码展示了如何在.NET Core中创建包含按钮和标签的基本窗口,实现简单的用户交互。
72 0
|
4月前
|
Linux C语言
【C语言】进程间通信之命名管道fifo
【C语言】进程间通信之命名管道fifo
46 0
|
4月前
|
C语言
【C语言】进程间通信之管道pipe
【C语言】进程间通信之管道pipe
96 0
|
4月前
|
Python
Python IPC深度探索:解锁跨进程通信的无限可能,以管道与队列为翼,让你的应用跨越边界,无缝协作,震撼登场
【8月更文挑战第3天】Python IPC大揭秘:解锁进程间通信新姿势,让你的应用无界连接
27 0
|
4月前
|
消息中间件 存储 网络协议
从零开始掌握进程间通信:管道、信号、消息队列、共享内存大揭秘
在操作系统中,进程间通信(IPC)是至关重要的,它提供了多种机制来实现不同进程间的数据交换和同步。本篇文章将详细介绍几种常见的IPC方式,包括管道、信号、消息队列、共享内存、信号量和套接字,帮助你深入理解并合理应用这些通信方式,提高系统性能与可靠性。
401 0
|
5月前
|
Unix Linux Python
`subprocess`模块是Python中用于生成新进程、连接到它们的输入/输出/错误管道,并获取它们的返回(退出)代码的模块。
`subprocess`模块是Python中用于生成新进程、连接到它们的输入/输出/错误管道,并获取它们的返回(退出)代码的模块。