Bootstrap

6.1进程间通信--管道(匿名管道、命名管道)

进程间通信

一、理论知识

1.1进程通信的必要性

进程通信产生的必要性:
1.单进程的,无法使用并发能力,更无法实现多进程协同
2.多进程协同,需要传输数据、同步执行流、消息通知等

进程通信的本质理解:
1.进程间通信的前提,是先让不同进程看到同一块“内存”(特定的组织结构)
2.这块进程看到的同一块“内存”,不能隶属于任何一个进程,而应该强调共享

1.2进程通信的目的

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

二、进程间通信方式分类

管道

  • 匿名管道pipe
  • 命名管道

System V IPC

  • System V 消息队列
  • System V 共享内存
  • System V 信号量

POSIX IPC

  • 消息队列
  • 共享内存
  • 信号量
  • 互斥量
  • 条件变量
  • 读写锁

三、进程间通信方式发展

3.1管道

创建进程PCB后,每个进程结构体里都有一个指向文件描述符表——struct files_struct结构体的地址变量(这个结构体在文件基础IO那章讲,这里只需知道有这个结构体即可),该表包含一个指针数组,下标就是文件描述符,每个文件描述符对应的元素存放的是每个文件的地址,从该数组中空余的文件描述符存放最近打开文件的文件地址

从磁盘打开一个文件,加载进内存,在内存中就要创建一个文件结构struct file,该结构包含文件的操作方法、文件对应的内核缓冲区(这个缓冲区在内核中的结构叫address_space)、inode属性

在这里插入图片描述
双方进程关闭自己不需要的文件描述符(类似父进程关闭读端,子进程关闭写端),这样父向管道写数据,子向管道读数据,就实现了进程的通信方式

上面是打开一个磁盘级别的文件后再创建子进程,实现的管道方案,但是这样IO效率太慢了,linux下提供了创建内存级别管道的系统接口。

创建子进程后,文件描述符表也被拷贝相同数据的一份,但是父进程的文件对象不拷贝。由于文件描述符表的数据和父进程相同,子进程打开的文件依然指向原来打开的文件,此时两个进程看到的就是同一资源,该共享文件就是管道

如果在缓冲区写入了数据,这份数据没必要写入磁盘,因为进程间通信是内存级别的通信

所以管道数据不会写入到磁盘,所有的进程通信方式都不会把数据写入磁盘

3.2管道的定义

官方定义:
管道是Unix中最古老的进程间通信的形式。
我们把从一个进程连接到另一个进程的一个数据流称为一个“管道”

在这里插入图片描述

3.3匿名管道

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

例子

例子:从键盘读取数据,写入管道,读取管道,写到屏幕
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main( void )
{
	int fds[2];
	char buf[100];
	int len;
	if ( pipe(fds) == -1 )
		perror("make pipe"),exit(1);
		// read from stdin
		while ( fgets(buf, 100, stdin) ) {
			len = strlen(buf);
			// write into pipe
			if ( write(fds[1], buf, len) != len ) {
				perror("write to pipe");
				break;
			}
			memset(buf, 0x00, sizeof(buf));
			// read from pipe
			if ( (len=read(fds[0], buf, 100)) == -1 ) {
				perror("read from pipe");
				break;
			}
		// write to stdout
		if ( write(1, buf, len) != len ) {
			perror("write to stdout");
			break;
		}
	}
}

3.3.1用fork来共享管道原理

在这里插入图片描述

pipe接口哪个进程调用,就让哪个进程以读写方式打开一个文件(该文件不需要指定文件名,该文件是纯内存级别的)

pipe函数被调用来创建一个管道,并将返回的文件描述符存储在fd数组中

管道创建流程
在这里插入图片描述

1.pipe函数创建的管道只能让具有血缘关系的进程间通信
2.管道具有访问控制,即子进程写入到显示器的时候,也会等父进程写入显示器,父进程休眠,子进程不会自己打印自己的信息。

这样验证了,父进程没有写完数据进入管道,子进程是不会从管道读取数据的。

管道内部做到了当管道没有数据时,读端一方必须等待,当管道满了,写的一方不能再写入,必须等读的一方读取数据,这就叫具有访问控制。

3.管道提供的是面向流式的通信服务(父进程写了10次,子进程可能一次就读完)

4…管道是基于文件的。如果通信双方都不通信,把文件描述符关了,那么管道会自动释放。文件的生命周期是随进程的,管道的声明周期也是随进程的

3.3.2管道读写规则

当没有数据可读时

  • 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将不再保证写入的原子性

3.3.3全双工、半双工的概念

在Linux系统编程中,“双工”是指允许双向数据传输的通信方式,而**“半双工”则是指在同一时间内只能进行单向数据传输的通信方式**。具体来说:

  • 全双工(Full Duplex): 全双工模式允许两台设备之间同时进行双向数据传输。也就是说,设备A可以发送数据给设备B,同时设备B也可以发送数据给设备A,两个方向上的数据传输可以同时进行而不会互相干扰。典型的全双工系统设备包括普通电话和手机,因为在通话时我们可以同时听到对方的声音,同时也能够向对方说话。
  • 半双工(Half Duplex): 半双工模式虽然也允许双向传输,但同一时刻只允许信号在一个方向上传输。当一个设备正在发送数据时,另一个设备必须等待直到传输完成后才能开始发送数据。半双工通信需要收发两端都具备发送和接收装置,并且通过切换来改变传输方向,因此可能会产生时间延迟。这种模式适用于会话式的终端通信。

3.4命名管道

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

mkfifo函数用于创建命名管道,也称为FIFO(First-In-First-Out),它是一个特殊的文件类型,允许不同进程之间进行数据通信。

  1. 创建命名管道mkfifo在文件系统中创建一个具有指定名称的管道文件。与匿名管道(只能在有父子关系的进程间使用)不同,命名管道在文件系统中是可见的,因此可以被任何两个或多个进程用来通信。
  2. 函数参数:该函数需要两个参数——filename(管道文件的名称)和mode(设置文件的权限)。创建的文件权限会受到umask值的影响。
  3. 半双工通信方式:通过mkfifo创建的管道支持半双工通信,即数据可以在管道中单向流动,不能同时进行读写操作。
  4. 使用场景:命名管道适用于没有血缘关系(非父子进程)的进程间通信。由于它们存在于文件系统中,可以通过常规的文件I/O函数(如openreadwrite等)进行操作。
  5. 文件系统实体:虽然ls -l命令可以显示命名管道文件,但它们实际上是特殊类型的文件,其大小始终为0,因为数据实际上存储在内核缓冲区中,而不是在磁盘上。

3.4.1创建一个命名管道

可以从命令行上创建

mkfifo filename

从程序里创建,相关函数如下

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

mkfifo函数用于创建命名管道,也称为FIFO(First-In-First-Out),它是一个特殊的文件类型,允许不同进程之间进行数据通信。

创建命名管道

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

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

  • 匿名管道由pipe函数创建并打开。
  • 命名管道由mkfifo函数创建,打开用open
  • FIFO(命名管道)与pipe(匿名管道)之间唯一的区别在它们创建与打开的方式不同,一但这些工作完成之后,它们具有相同的语义
  • 命名管道中,一个管道可以有多个读端

到这里进程间通信方式匿名管道和命名管道都讲完了(讲完了系统调用接口),匿名管道可以通过父子进程来使用,而使用命名管道,可以提前创建两个main函数源文件,把两个main函数里mkfifo的第1个参数设置为同名文件,一个main函数往这个内存级别的管道(文件)写,一个main函数往这个管道读,代码思路就是这样。

管道通信方式只是进程通信方式的一种,后续还有共享内存,以及system V消息队列等通信方式,后续我只讲共享内存的原理、系统接口,了解了共享内存,消息队列也能直接上手了。

;