【Linux】—— 进程间的通信之匿名管道

进程间的通信

一、进程间通信的概念

  • 进程是操作系统的概念,每当我们执行一个程序时,对于操作系统来讲就创建了一个进程,在这个过程中,伴随着资源的分配和释放。可以认为进程是一个程序的一次执行过程。
  • 进程的地址空间是相互独立的,一般而言是不能相互访问的,若读者不了解进程地址空间相关概念,可参考博客 进程地址空间 。但很多情况下进程间需要互相通信,来完成系统的某项功能。进程通过与内核及其它进程之间的互相通信来协调它们的行为。
  • 我们之前也提到过进程之间是具有独立性的,所以要两个进程之间实现通信是有难度的,所以想要两个进程之前实现通信的前提是:让不同进程看到同一份资源(通常指的是某一块内存)。
    进程间的通信

二、进程间通信的目的

  • 数据传输:一个进程需要将它的数据传输给另一个进程
  • 资源共享:多个进程共享同样的资源,多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。
  • 通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
  • 资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。
  • 进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

三、进程间的通信方式之管道

管道

一、什么是管道

  • 管道是Unix中最古老的进程间的通信方式
  • 我们把从一个进程连接到另一个进程的一个数据流称为"管道"。
    管道

二、管道简介

管道包括三种:

  • 普通管道pipe: 通常有两种限制,一是单工,只能单向传输;二是只能在具有亲缘关系的进程之间,常用于父子或者兄弟进程间使用.
  • 流管道s_pipe: 去除了第一种限制,为半双工,只能在父子或兄弟进程间使用,可以双向传输.
  • 命名管道:name_pipe(fifo):去除了第二种限制,可以在许多并不相关的进程之间进行通信.

三、匿名管道

  • 匿名管道的本质:内核提供的一段内存(队列),通过内存借助这段内存,完成进程间通信。然后将管道这段内存抽象成文件。通过访问文件描述符的形式,来读写这块内存中的数据,只能应用于具有亲缘关系的进程之间,常用于父子进程和兄弟进程
  • 创建匿名管道函数原型int pipe(int fd[2]);
  • 参数:fd[2]:文件描述符数组,其中f [0] 表示读端, f [1]表示写端
  • 返回值:成功返回0,失败返回错误码

匿名管道pipe

四、用fork来共享管道的原理

  • 我们之前在讲 进程的控制时详细介绍了fork函数,我们提到了父进程通过fork创建子进程时,子进程是和父进程拥有一样的信息,那此时父子进程就都能看到管道,并且对管道都有读和写的功能,因为匿名管道是半双工的,因此需要父子进程关闭各自不用的文件描述符,之后变可以进程各自的读或写操作而实现通信。
    fork

五、文件描述符-深入理解匿名管道

文件描述符理解管道

六、站在内核角度- 管道的本质

  • 在 Linux 中,管道的实现并没有使用专门的数据结构,而是借助了文件系统的file结构和VFS的索引节点inode。通过将两个 file 结构指向同一个临时的 VFS 索引节点,而这个 VFS 索引节点又指向一个物理页面而实现的。如下图: 内核理解管道
  • 有两个 file 数据结构,但它们定义文件操作例程地址是不同的,其中一个是向管道中写入数据的例程地址,而另一个是从管道中读出数据的例程地址。这样,用户程序的系统调用仍然是通常的文件操作,而内核却利用这种抽象机制实现了管道这一特殊操作。
  • 所以,看待管道,就如同看待文件一样,管道的使用和文件一致,迎合了我们之前在 深入理解Linux文件系统中提到过的"Linux下一切皆文件的思想"

接下来我们看看实例代码

  • 子进程向管道中写数据,父进程读数据
    匿名管道的创建

  • 父子进程通信成功父进程读取到子进程在管道中写入的数据,运行结果如下:
    进程间的通信

七、管道读写规则

我们在讲管道读写规则之前我们先来了解一下几个概念:

  • 临界资源:进程间通信的本质是让两个进程看到同一份资源,多进程间共享的资源称为临界资源
  • 临界区:进程访问临界资源的那部分代码称为临界区
  • 互斥:任何一个时刻只允许一个进程进入临界区访问临界资源的情况称为互斥
  • 同步:在保证数据安全的前提下(通常是互斥),让多进程访问临界资源具有一定的顺序性,称为同步(目标:协同进程步调,避免饥饿问题)
  • 原子性:通常访问临界资源时只有两种状态:不访问和访问完,我们将此称为原子性

在了解完上面几个概念之后我们再来看看管道的读写规则

  • 当没有数据可读时
    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将不再保证写入的原子性。

我们分别举例说明一下上面提到的几种规则

情况1

  • 管道被写满的情况,子进程一直在往管道中写数据,但父进程不从管道中读取数据,而是仅仅只在wait子进程,这就会出现阻塞的情况,直到有进程读走数据。
    情况1
    情况2
  • 子进程一直写,父进程不读且关闭其对应的读操作符,这是write操作就会常数信号SIGPIPE,进而可能导致write的进程也就是子进程退出
    在这里插入图片描述

情况3

  • 子进程写了一条数据之后,直接退出,并且关闭对应的写的文件操作符,此时父进程读取了一条数据之后就无数据可读,此时read调用返回-1,errno的值为ESGAIN。情况3

情况4

  • 子进程写了一条数据后退出,但不关闭对应的写文件操作符,此时父进程就会一直阻塞等待,一直到有数据来为止。
    情况4

八、管道特点

  • 只能用于具有共同祖先的进程(具有亲缘关系的进程)之间进行通信;通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。
  • 管道提供流式服务
  • 一般而言,进程退出,管道释放,所以管道的生命周期随进程
  • 一般而言,内核会对管道操作进行同步与互斥
  • 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道
发布了167 篇原创文章 · 获赞 175 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/chenxiyuehh/article/details/90759128