【Linux】进程间通信方式①——匿名管道与命名管道(附图解与代码实现)

相信大家平时没少用微信、QQ这样的聊天工具,通过这样的聊天工具通讯属于互联网通信,在往下一级便是内网通信,也就是在同一局域网内互相通信,再接着便是我们今天要讲的进程间通信了

进程间通信有以下几种方式:信号(Signal)、消息队列、管道(pipe)等

今天我们来分别讲一讲两种管道:匿名管道和命名管道

匿名管道

匿名管道的特性

既然说到了管道,大家不妨想想,管道都有什么特性呢?

相信大家都用过吸管喝过水,那么就来拿这来举个例子

  1. 吸管内的水应该具有方向性,同一时间内只能是由一端流向另一端,匿名管道内的数据也是如此,同一时间内,数据只能是从写端流向读端,也就是绝大多数时间内,访问管道单工使用
  2. 当我们掐住吸管的两头时,吸管内的水应该可以在里面存放一段时间,匿名管道也是如此。数据就好比水,掐住吸管两头就好比进程不进行数据的读写 , 匿名管道也应该有暂存数据的能力,也就是管道缓冲区,但是这个管道缓冲区很小,在Ubuntu16.0中只有4K(不同的ubuntu版本,管道缓冲区大小不同)
  3. 匿名管道只能完成亲缘进程之间的通信。为什么匿名管道会有这样的特性呢?这就需要我们了解一下匿名管道是如何创建的了

匿名管道的创建过程

假设现在有两个进程,分别是进程1和进程2,进程1中有一条信息想要让进程2读取到,接下来我想问大家一个问题:管道是建立在进程的用户层还是内核层呢?

【Linux】进程间通信方式①——匿名管道与命名管道(附图解与代码实现)_第1张图片

首先大家要知道一些概念:

  1. 一个进程的用户层是不允许其他进程访问的,一个进程的用户层是自己完全私有,只允许本进程创建数据或对用户层中的数据进行读写
  2. 同一终端下进程的内核层一般都是放在一片空间下统一管理的,系统对内核层具有操作权限 

在了解了这些之后,相信大家应该就能明白了,匿名管道是建立在内核层的,并且几乎所有的进程间通信技术,都是利用进程内核层(共享内存)完成进程通信

想要创建匿名管道,我们就要用到一个函数:pipe函数

头文件 #include
功能 创建一个匿名管道
语法 int pipe(int fds[2]);
返回值 创建成功返回0,创建失败返回-1

参数介绍:相信大家没有看懂 "int pipe(int fds[2]);"中的这个int fds[2],我们来介绍一下这个参数,这个参数里存放的是两个文件描述符,fds[0]是文件的读权限,fds[1]是文件的写权限

接着,进程1使用pipe函数创建一个管道,得到管道的读写权限,结果如下图所示:

【Linux】进程间通信方式①——匿名管道与命名管道(附图解与代码实现)_第2张图片

这时候一段有趣的对话就出现了:

进程1:大哥,我这边管道创建好了,读写的那两个文件描述符我也拿到了,你快来用这个管道啊!

进程2:老弟呀,大哥我知道你啥都搞好了,但是我这边没有那两个文件描述符来对管道进行读写啊

进程1:那还不简单,大哥你也用pipe函数创建一个管道获取文件描述符呗!

进程2:老弟呀,我要用pipe函数了,我这不就创建了一个新的管道了嘛,我还是对你创建的那个管道没有读写权限啊

进程1:那咋整啊大哥,老弟我有一大堆话想通过管道跟你说呢

进程2:老弟啊,现在就只有一个办法了!

进程1:啥办法呀大哥?

进程2:这样,你通过fork函数把你那两个文件描述符传过来,但是注意啊,一定要把pipe函数放在fork函数之前啊!否则我也会用pipe函数创建管道了

进程1:行吧,但是这样咱俩以后就得各论各的了,这样吧,以后,我管你叫哥,你管我叫爸!

进程2:我***********************

相信看完上面这段对话以后,大家也就明白为什么我们前面所说的:匿名管道只能完成亲缘进程之间的通信,fork函数创建的当然是亲缘进程了

代码实现与图解——通过匿名管道实现亲缘进程通信

我们来实现一个父写子读的功能

#include
#include
#include
#include
#include
#include

#define message "Hello mother fucker!"

int main(void)
{
	//定义一个数组存放文件描述符
	int fds[2];
	//最好不要用变量来接fds,因为你只得到了里面的数值,pipe返回的fds里面有文件描述符,
	//很可能你不并不能用接取到的变量来访问管道
	pipe(fds);
	printf("管道创建成功,这是一个父写子读的管道!\n");
	//创建父子进程
	pid_t pid;
	pid = fork();
    //父进程进入该判断
	if(pid > 0)
	{
		//关闭读权限
		close(fds[0]);
		printf("信息准备发送,已关闭父端读管道的权限\n");
		write(fds[1] , message , strlen(message));
		//数据写入,关闭写权限
		close(fds[1]);
		printf("信息已经发送,已关闭父端写管道的权限\n");
		//父进程阻塞回收子进程
		pid_t zpid;
		while(zpid = wait(NULL))
		{
			if(zpid > 0)
			{
				printf("父进程No.%d 已成功回收子进程No.%d\n" , getpid() , zpid);
				printf("父进程已成功退出!\n");
                exit(0);
			}
			else
				printf("子进程仍不可回收,父进程持续阻塞等待回收中!\n");
		}
	}
    //子进程创建成功后进入该判断
	else if(pid == 0)
	{
        //关闭写权限
		close(fds[1]);
		printf("子进程准备读取管道内的消息,已关闭子端写管道的权限\n");
        //定义信息接收缓冲区
		char buffer[1024];
		//清空缓存内的消息
		bzero(buffer ,  sizeof(buffer));
        //读取管道缓冲区内的信息放入buffer中
		read(fds[0] , buffer , sizeof(buffer));
        //关闭读权限
		close(fds[0]);
		printf("子进程已经读取到管道内的消息,消息为: %s ,已关闭子端读管道的权限\n",buffer);
		exit(0);
		printf("子进程先于父进程退出,成为僵尸态进程!\n");
	}
    //如果子进程创建失败,进入该判断
	else
	{
		perror("进程创建失败!\n");
		return 0;
	}
	return 0;
}

运行结果:

【Linux】进程间通信方式①——匿名管道与命名管道(附图解与代码实现)_第3张图片

相信有一些细心的同学发现了,在父进程向管道写入数据之前,我们关闭了父进程读的权限,在子进程读取管道数据之前,我们关闭了子进程写的权限,我们为什么这么着急关闭这两个进程用不到的权限呢?这里就涉及到一个问题:指向管道的描述符问题,也就是文件描述符

这里我们要引出一个概念,也就是管道的引用计数:每当存在一个指向管道的描述符,也就是文件描述符时,管道的引用计数加一。只有当管道的引用计数为0时,管道才能被系统释放

在很多情况下,往往不是一父一子通过匿名管道进行通信,而是很多亲缘进程通信,也就是会有很多文件描述符,但凡有一个进程某个文件描述符没关闭,管道都无法被系统释放而占用内存空间,所以使用匿名管道时要养成尽早关闭无用权限的习惯

匿名管道使用时的特殊情况

四种特殊情况 情况简介
写阻塞 管道缓冲区被写满,无法继续放入数据,从而阻塞写端
读阻塞 管道缓冲区为空,无数据可读,从而阻塞读端
写关闭 写端退出,读端读取剩余内容,管道内无内容时,读端读取0
读关闭(最特别的情况) 读端退出,此时写端写入的数据就是垃圾数据,系统发送SIGPIPE信号,杀死写进程

命名管道

管道文件的创建方法

相比匿名管道,命名管道的限制要小一些,它可以实现非亲缘进程之前的通信,但是要使用命名管道,我们要先创建管道文件,管道文件有两种创建方式:

  1. 命令创建:mkfifo 管道文件名
  2. 函数创建:mkfifo(char* 管道文件名 , int 文件权限)

命名管道使用时的特殊情况

  1. 不论是多少个进程要使用同一管道文件来进行通信,这些进程的权限加起来要满足读写权限(比如有100个进程,但他们都只有读权限,没有一个有写权限,那么管道文件就无法被使用)
  2. 当一个进程内有多个线程在同一读序列时,阻塞只对第一个线程有效,其他读线程自动被设置为非阻塞

要想理解第二种特殊情况,举个排队挂号的例子实在是再合适不过了

假设现在有一个队伍,共有五个人在排队等待挂号,第一个人已经到了挂号窗口,肯定是专心致志的等待拿到号,这就相当于阻塞状态,而还没到窗口的后面四个人肯定是干自己的事,比如玩手机什么的,这就相当于非阻塞状态,等到了窗口再专心等号

这样做的好处就是可以降低等待开销,可以让其他的线程去干一干自己的事

代码实现与图解——通过命名管道实现非亲缘进程通信

既然是非亲缘进程,自然就要写两个程序,这里我们采用函数创建管道文件的方式

process_1.c(负责写入数据)

#include
#include
#include
#include
#include
#include
#include

#define MSG "儿子,儿子,我是你爸爸!"

int main(void)
{
	//创建管道文件
	mkfifo("fifo" , 0664);
    //写打开
	int wfd = open("fifo" , O_WRONLY);
	write(wfd , MSG , strlen(MSG));
	printf("写进程已经成功发送数据\n");
	//关闭文件
	close(wfd);
	exit(0);
}

process_2.c(负责读取数据)

#include
#include
#include
#include
#include
#include
#include


int main(void)
{
    int rfd = open("fifo" , O_RDONLY);
    char buffer[1024];
	bzero(buffer , sizeof(buffer));
	read(rfd , buffer , sizeof(buffer));
	printf("读进程已成功接收到消息:%s\n" , buffer);
    //关闭文件
    close(rfd);
    exit(0);
}
  

运行结果

【Linux】进程间通信方式①——匿名管道与命名管道(附图解与代码实现)_第4张图片

命名管道的原子访问与非原子访问

  • 当我们要写入的数据大于管道大小时,就会进行非原子访问,来对数据进行裁剪来使其传入管道中,显而易见,非原子访问的优点就是速度快,管道利用率高,缺点就是要校验数据的完整性和正确性
  • 当我们要写入的数据小于管道大小时,就会进行原子访问,将数据整个放入管道中,显而易见,原子访问的优点就是不用担心数据的一致性和完整性,缺点就是速度慢,管道利用率低(因为写入数据比管道小,所以一定有空间没有利用上)

大家有什么地方没有看懂的话,可以在评论区留言给我,咱要力所能及的话就帮大家解答解答

今天的学习记录到此结束啦,咱们下篇文章见,ByeBye!

你可能感兴趣的:(Linux,linux,学习)