【Linux】——管道

文章目录

  • 1、管道
    • 1.1引言
    • 1.2有名管道
    • 1.3无名管道

1、管道

1.1引言

1.首先我们来聊一聊为什么要引入进程间的通讯。
在我们之前的学习当中,我们知道进程都是独立的个体,每个进程之间数据是不共享的,要实现进程之间数据的通讯,我们可以使用fork。因为父子进程可以共享fork之前打开的文件描述符。
例子:父进程需要给子进程发送一个“hello world”的字符来说,按照fork的方式实现如下:

int main()
{
	int fd = open("a.txt",O_RDWR);
	pid_t pid = fork();
	if(pid == 0)
	{
		sleep(1);
		lseek(fd,0,SEEK_SET);
		read(fd,buff,127);
	}
	else
	{
		write(fd,"hello world",127);
	}
}

2.在这里我们来分析一下这段代码的特殊处理。
1、为什么要让子进程睡眠一秒?
原因就是保证父进程已经将字符串写到了文件中。
2、为什么在读取之前,需要把fd的读写偏移量偏移到起始位置?
因为父子进程共享的文件描述符中f_pos成员就是文件读写偏移量。在父进程的write完成后是在文件的末尾,所以在子进程的读取之前要将读写偏移量移到起始位置

3.上述处理方式的问题

  1. sleep是不可靠的,如果在wirte之前父进程还做了很多工作,这些操作做了多久无法预估。在真实的产品当中是无法实现的。
  2. 因为对数据的读写是操作文件,文件的存储空间是在磁盘上的,所以在写入数据时要执行一次I/O操作在读取文件的时候也要进行一次I/o操作,但是I/O操作效率不高另外,只是要求数据的传递,并没有要求永久保存,但是对文件的操作要把数据保存
  3. 只能做到有关系的进程之间数据的传递,而没有办法做到任意两个进程之间

基于上述进程间通信的不足,我们推出了进程间的通讯方式——管道、消息队列、信号量、共享内存。这篇文章我们来了解一下管道和消息队列。

1.2有名管道

1、概念
在磁盘上有个管道文件标识,但是这个管道文件只会占据一个innode结点,但是这个管道文件任何时候不会占据block块(也就是说数据不会存储到磁盘中)。数据在传递过程中缓存在内存上。
简单点来说,管道文件的作用仅仅是为了使得不同的进程(有权限操作的)能够共享

2、创建和打开
方法一:
通过mkfifo filename命令。操作如下:
在这里插入图片描述
方法二:
通过库方法int mkfifo()。该函数详情如下:
在这里插入图片描述
第一个参数代表路径,第二个参数代表权限值。
打开还是按照文件的一系列方式open read write

3、具体实现
实现在mainA.c中让终端写入数据,在mainB.c中读取终端写入的数据。其代码实现如下:
mainA.c

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

//写入数据
int main()
{
	int fd = open("./FIFO",O_WRONLY);//只需要只写的方式打开管道文件
	assert(fd != -1);

	printf("Write open fifo success\n");

	//获取操作者输入
	while(1)
	{
		char buff[128] = {0};
		printf("input: ");
		fgets(buff,127,stdin);

		if(strncmp(buff,"end",3) == 0)
		{
			break;
		}

		write(fd,buff,strlen(buff)-1);//将用户输入的数据写入到管道文件当中
	}
	close(fd);
}

mainB.c

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

int main()
{
	int fd = open("./FIFO",O_RDONLY);//以只读的方式打开管道文件
	assert(fd != -1);

	printf("Read open fifo success\n");

	while(1)
	{
		char buff[128] = {0};

		int n = read(fd,buff,127);//从管道文件中读取文件放到buff中
		if(n <= 0)
		{
			break;
		}

		printf("Read: %s\n",buff);//将读到的数据显示出来即可
	}
	close(fd);//将内存空间释放掉
}

测试1:只执行进程A
在这里插入图片描述
我们会发现,程序没有再继续往下执行。其实就是阻塞到open这个方法的。
因为我们操作的是管道文件,对于管道文件而言,这是以只写的方式,当没有数据读的时候打开是没有意义的。

测试2:写一个数据,再读出数据
【Linux】——管道_第1张图片
在这里插入图片描述
操作完成后管道文件还是为0,所以不存在数据往磁盘中存的问题。

4、实现原理
我们可以花下面的图示来表示整个数据共享的过程。
【Linux】——管道_第2张图片
从图中我们可以看到,磁盘中的inode区域指向了内核开辟的一块内存空间,而这个空间就是用来传递数据的,A,B两个进程一个是以读的方式一个是以写的方式打开,都存在一个读写偏移量,都指向该内存的起始位置。通过两个进程在磁盘中是共享的去访问FIFO的inode结点,通过inode结点去指向内核中开辟的这块空间,去写入和读取数据
这也可以解释为什么open会阻塞,因为只以一种(比如说只写)方式去打开他,如果没有读的话,其实开辟这块内存空间就没有意义了

总结如下:

  1. open以一种方式打开管道文件会阻塞,直到进程以另一种方式打开此管道文件

  2. 如果管道对应的内存空间中没有数据,则read会阻塞,直到1内存中有数据,2写端关闭

  3. 如果管道对应的内存空间已满,则write就会阻塞,直到1内存中有空间2所有读端关闭

5、补充

  1. 如果两个进程都是rw的方式打开,会存在A读取数据是让B进行写的,结果成了A进程刚刚从终端读取数据写入管道,结果从从管道中把数据读取了这样的问题。由此,可以看出管道是一种半双工通信机制
  2. 内核对管道的内存空间的管理方式是以循环的方式

1.3无名管道

1、概念
无名管道是借助父子进程共享fork之前打开的文件描述符来实现进程间的通讯。由父进程创建的子进程将会赋值父进程包括文件在内的一些资源。

如果父进程创建子进程之前创建了一个文件,那么这个文件的描述符就会被父进程在随后所创建的子进程所共享。也就是说,父、子进程可以通过这个文件进行通信。如果通信的双方一方只能进行读操作,而另一方只能进行写操作,那么这个文件就是一个只能单方向传送消息的管道,如下图所示:
【Linux】——管道_第3张图片
2、无名管道的创建
进程可以通过调用函数==pipe()==创建一个管道。函数pipe()的原型如下:

int pipe(int fildes[2]);
  1. pipe()函数的功能就是创建一个内存文件
  2. 函数pipe()将在参数fildes中为进程返回这个文件的两个文件描述符fildes[0]和fildes[1]。其中,fildes[0]是一个具有“只读”属性的文件描述符,fildes[1]是一个具有“只写”属性的文件描述符
  3. 这个文件一头专门用来输入数据,另一头专门用来输出数据,只能用于亲属进程间的通信

3、通信原理
3.1 当一个进程调用函数pipe()创建一个管道后,管道的连接方式如下所示:
【Linux】——管道_第4张图片

3.2 如果父进程创建一个管道之后,又创建了一个子进程,那么由于子进程继承了父进程的文件资源,于是管道在父子进程中的连接情况就变成如下图一样的情况了:【Linux】——管道_第5张图片
3.3 在确定管道的传输方向之后,在父进程中关闭(close())文件描述符fildes[0],在子进程中关闭(close())文件描述符fildes[1],于是管道的连接情况就变成如下情况的单向传输管道:
【Linux】——管道_第6张图片
有了以上的通信原理,我们可以实现如下的代码实现数据间的通讯。

#include
#include
#include
#include
#include

int main()
{
	int fds[2] = {-1,-1};
	//  pipe必须在fork之前调用
	int res = pipe(fds); //  创建并打开一个无名管道,fd[0]为读端,fd[1]为写端
	assert(res != -1);
	
	pid_t n = fork();
	assert(n != -1);

	if(n == 0)
	{
		close(fds[1]);//子进程直接关闭管道的写端
		while(1)//读取数据放到buff里面
		{
			char buff[128] = {0};
			int n = read(fds[0], buff, 127);
			if(n <= 0 || 0 == strncmp(buff, "end", 3))
			{
				break;
			}

			printf("child: %s\n", buff);
		}
		close(fds[0]);
	}
	else
	{
		close(fds[0]);//父进程中直接关闭管道的读端
		while(1)
		{
			printf("please input:");
			char buff[128] = {0};
			fgets(buff, 128, stdin);

			write(fds[1], buff, strlen(buff) - 1);

			if(strncmp(buff, "end", 3) == 0)
			{
				break;
			}
		}

		close(fds[1]);
	}
}

测试:
在这里插入图片描述
为什么子进程打印的数据在input后面?
父进程里面把数据写入过后直接就printf(“please input:”);,他不会阻塞等待子进程去读。相应的在子进程中也是读到数据过后就直接打印。

4、特点

  1. 由于这种管道没有其他同步措施,所以为了不产生混乱,它只能是半双工的,即数据只能向一个方向流动。如果需要双方互相传递数据,则需要建立两个管道
  2. 只能在父子进程或兄弟进程这些具有亲缘关系的进程之间进行通信
  3. 对于管道两端的进程而言,就是一个只存在于内存的特殊文件

你可能感兴趣的:(Linux)