Linux下IPC方式之管道(pipe,fifo)

在这里插入图片描述

1. IPC方法

  Linux环境下,进程地址空间相互独立,每个进程各自有不同的用户地址空间。任何一个进程的全局变量在另一个进程中都看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2再从内核缓冲区把数据读走,内核提供的这种机制称为进程间通信(IPC,InterProcess Communication)。
在这里插入图片描述
  在进程间完成数据传递需要借助操作系统提供特殊的方法,如:文件、管道、信号、共享内存、消息队列、套接字、命名管道等。
  随着计算机的蓬勃发展,一些方法由于自身设计缺陷被淘汰或者弃用。现今常用的进程间通信方式有:
管道 (使用最简单)
信号 (开销最小)
共享映射区 (无血缘关系)
本地套接字 (最稳定)

2. 管道

2.1 管道的概念

  管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:
1. 其本质是一个伪文件(实为内核缓冲区)
2. 由两个文件描述符引用,一个表示读端,一个表示写端。
3. 规定数据从管道的写端流入管道,从读端流出。

  管道的原理: 管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。

  管道的局限性:
① 数据一旦被读走,便不在管道中存在,不可反复读取
②由于管道采用半双工通信方式。因此,数据只能在一个方向上流动
③ 只能在有公共祖先(有血缘)的进程间使用管道。


在这里插入图片描述
  常见的通信方式有,单工通信、半双工通信、全双工通信。

2.2 pipe函数

创建管道:

int pipe(int pipefd[2]);
pipefd 读写文件描述符	 0 代表 读, 1 代表 写
返回值 	成功返回 0, 失败返回  -1.

  函数调用成功返回r/w两个文件描述符。无需open,但需手动close。规定:fd[0] → r; fd[1] → w,就像0对应标准输入1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。
  管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?通常可以采用如下步骤:
在这里插入图片描述

  1. 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]fd[1]指向管道的读端和写端。
  2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
  3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。

2.2.1 父子进程间通信简单举例

#include<stdio.h>
#include<unistd.h>
 
int main() {
	//创建文件描述符
	int fd[2];
	//创建管道
	pipe(fd);
	pid_t pid = fork();
 
	if (pid == 0) {
		//son
		sleep(3);
		write(fd[1], "hello", 5);
	}
	else if (pid > 0) {
		//father
		//现有缓冲区,然后才能读
		char buf[12] = { 0 };
		//阻塞等待,哪怕子进程sleep了,也要等
		int ret = read(fd[0], buf, sizeof(buf));
		//说明读到了
		if (ret > 0) {
			write(STDOUT_FILEND, buf, ret);//STDOUT_FILEND是1的宏定义
		}
	}
	return 0;
}

运行结果:
在这里插入图片描述

2.2.2 父子进程实现pipe通信,实现ps aux | grep bash 功能

由于之前代码我们创建的3父子进程都掌握着管道(pipe)的读写两端,因此有如下结构示意图
在这里插入图片描述
出现问题的测试代码:(我们希望子进程写入,父进程读取)

#include<stdio.h>
#include<unistd.h>
 
int main() {
	//创建文件描述符
	int fd[2];
	//创建管道
	pipe(fd);
	pid_t pid = fork();
 
	if (pid == 0) {
		//son
		//son执行ps命令
		//1、先重定向
		dup2(fd[1], STDOUT_FILENO);//标准输出重定向
		//2、execlp  NULL是哨兵,告诉命令,后面不会再有参数输入了
		execlp("ps","ps","aux", NULL);
	}
	else if (pid > 0) {
		//father
		//1、先重定向
		dup2(fd[0], STDIN_FILENO);//标准输出重定向
		//2、execlp  NULL是哨兵,告诉命令,后面不会再有参数输入了
		execlp("grep","grep","bash", NULL);
	}
	return 0;
}

出现的问题:
子进程变成僵尸进程,父进程执行grep之后一直在等待输入。
在这里插入图片描述
分析原因:
grep命令的特性:等待标准输入,如果你输入的正确,则给你一个反馈。阻塞等待,因为grep一直认为还有输入,除非是输入端的进程放弃了写入的机会。grep阻塞等待示例,如图所示
在这里插入图片描述
  上面代码出现问题,是因为虽然子进程死去了,但是父进程还掌握着管道写入端的句柄,所以grep一直认为还会有输入,虽然此时读写两端都是父进程掌握。所以我们需要在代码中关闭子进程的读端,以及父进程对于管道的写端。
在这里插入图片描述

//关闭 读端
close(fd[0]);
 
//关闭 写端
close(fd[1]);

在使用管道时,应该把读写两端都规划好。这样数据的流向才稳定。
修正之后的代码:

#include<stdio.h>
#include<unistd.h>
 
int main() {
	//创建文件描述符
	int fd[2];
	//创建管道
	pipe(fd);
	pid_t pid = fork();
 
	if (pid == 0) {
		//son
		//son执行ps命令
		//关闭 读端
		close(fd[0]);
		//1、先重定向
		dup2(fd[1], STDOUT_FILENO);//标准输出重定向
		//2、execlp  NULL是哨兵,告诉命令,后面不会再有参数输入了
		execlp("ps","ps","aux", NULL);
	}
	else if (pid > 0) {
		//father
		//关闭 写端
		close(fd[1]);
		//1、先重定向
		dup2(fd[0], STDIN_FILENO);//标准输出重定向
		//2、execlp  NULL是哨兵,告诉命令,后面不会再有参数输入了
		execlp("grep","grep","bash", NULL);
	}
	return 0;
}

运行结果:
在这里插入图片描述

2.3 管道的读写行为

读管道

  • 写端全部关闭时——read读到0,相当于读到文件末尾
  • 写端没有全部关闭时:
    1.有数据——read读到数据
    2.没有数据——read阻塞 fcntl 函数可以更改阻塞为非阻塞

写管道

  • 读端全部关闭——产生一个信号 SIGPIPE ,程序异常终止
  • 读端未全部关闭时:
    1.管道已满——write阻塞
    2.管道未满——write正常写入

2.4 管道的大小和优劣

使用命令查看当前系统中创建管道文件所对应的内核缓冲区大小

ulimit -a

在这里插入图片描述

优点:简单
缺点:

  • 只能有血缘关系之间的进程通信
  • 父子进程单方向通信,如果需要双向通信,需要创建多个管道。

2.5 FIFO通信

  FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间。但通过FIFO,不相关的进程也能交换数据,实现无血缘关系进程的通信
  FIFO是Linux基础文件类型中的一种。但,FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。

  • 创建一个管道的伪文件
    1.mkfifo myfifo 命令创建
    2.也可以用函数int mkfifo(const char *pathname, mode_t mode);

  • 内核会为fifo文件开辟一块缓冲区,操作fifo文件,可以操作缓冲区,实现进程间通信——实际上就是文件读写。

  • open函数的注意事项:打开fifo文件的时候,read端会阻塞等待write端open,write端同理,也会阻塞等待另外一端打开。

mkfifo myfifo 命令创建
在这里插入图片描述
注意:要放在linux系统文件夹里如果,如果放到windows文件夹会报错:
在这里插入图片描述

写端示例

#include<stdio.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
 
int main(int argc, char *argv[]) {
	if (argc != 2) {
		//如果运行时没有输入文件名,则会报错
		printf("./a.out fifoname\n");
		return -1;
	}
	//当前目录中存在myfifo文件
	//打开fifo文件
	int fd = open(argv[1], O_WRONLY);
	//写
	char buf[256];
	int num = 1;
	while (1) {
		memset(buf, 0x00, sizeof(buf));
		sprintf(buf, "xiaoming%04d", num++);
		write(fd, buf, strlen(buf));
		sleep(1);
	}
	//关闭描述符
	close(fd);
	return 0;
}

读端示例

#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>
 
int main(int argc, char *argv[]) {
	if (argc != 2) {
		//如果运行时没有输入文件名,则会报错
		printf("./a.out fifoname\n");
		return -1;
	}
	int fd = open(argv[1], O_RDONLY);
 
	char buf[256];
	int ret;
	while (1) {
		//循环读
		ret = read(fd, buf, sizeof(buf));
		if (ret > 0) {
			printf("read:%s\n", buf);
		}
	}
	//关闭描述符
	close(fd);
	return 0;
}

读端和写端中的open函数会阻塞,之后对端的open函数也打开了,他才会停止阻塞行为
man 2 open 中有这么一句话应证了:
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值