Linux操作系统进程间通信方式:管道(Pipeline)

什么是管道

管道是Linux进程间的一种通信方式,两个进程可以通过一个共享内存区域来传递信息,并且管道中的数据只能是单向流动的,也就是说只能有固定的写进程和读进程。

管道可以分为两种类型:匿名管道命名管道


匿名管道

匿名管道只能在父子进程间进行通信,其具体读写规则有:

  1. 管道内无数据时,读端会发生阻塞直到有数据可读
  2. 管道数据满时,写端会发生阻塞,直到读端开始读取数据
  3. 如果写端对应的文件描述符被关闭,read函数返回0,但可以将数据读完
  4. 如果读端对应的文件描述符被关闭,在执行write函数时会产生SIGPIPE信号,其默认行为会导致当前进程终止。

匿名管道中的数据是存储在内存中的。
可通过函数pipe创建一个匿名管道,匿名管道相关的函数定义于头文件unistd.h中:

int pipe (int fd[2]);

该函数需要传入一个长度为2的数组,函数执行完成后,如果返回0则代表管道创建成功,其中fd[0]为只读属性的文件描述符,fd[1]为只写属性的文件描述符,分别对应管道的读端和写端操作:
在这里插入图片描述
从上图中可以看出,如果我们将数据写入fd[1],那么从fd[0]就可以获得对应的数据:

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {
	int fd[2];
	if (pipe(fd) == -1) {
		fprintf(stdout, "Can not create pipe.\n");
		_exit(1);
	}

	char buf[512];

	while (1) {
		fprintf(stdout, "write data to fd[1]->");
		fflush(stdout);
		fscanf(stdin, "%s", buf);  //从键盘读入数据
		
		size_t len = strlen(buf);
		write(fd[1], buf, len);  //写入到管道

		memset(buf, 0, sizeof(buf)); //清空buf缓冲区

		read(fd[0], buf, len);  //从管道读取数据
		fprintf(stdout, "read data from fd[0]->%s\n", buf);
	}

    return 0;
}

gcc编译上述程序并运行:

[root@localhost Debug]# ./test-app
write data to fd[1]->message
read data from fd[0]->message
write data to fd[1]->abc
read data from fd[0]->abc
write data to fd[1]->^C

单纯在一个进程中使用管道的写端和读端没什么意义,因为管道本身就是为父子进程的通信设计的,fork出的进程继承了父进程打开的文件描述符,所以我们可以利用这一点来使用管道进行父子进程通信:

#include <stdio.h>
#include <unistd.h>
#include <string.h>

int main() {
	int fd[2];
	if (pipe(fd) == -1) {
		fprintf(stdout, "Can not create pipe.\n");
		_exit(1);
	}

	pid_t pid = fork();
	if (pid < 0) {
		fprintf(stdout, "Fork failure\n");
	} else if (pid == 0) {  //子进程
		close(fd[0]);  //子进程关闭读端
		char str[] = "message";  //将"message"传递给父进程
		write(fd[1], str, strlen(str));
	} else {   //父进程
		close(fd[1]);  //父进程关闭写端
		char str[100];
		read(fd[0], str, 100);
		fprintf(stdout, "Parent: read data from pipeline (%s)", str);
	}

    return 0;
}

gcc编译上述程序并执行:

[root@localhost Debug]# ./test-app
Parent: read data from pipeline (message)

可以发现父进程通过管道获得了子进程向管道写入的"message"字符串,此时两个进程的管道示意图如下:
在这里插入图片描述
如果在fork进程之后不关闭管道,那么示意图如下:

在这里插入图片描述


命名管道

命名管道本质上是一个管道文件,它基于文件系统来实现进程间的通信,其读写端进程可以不是父子进程的关系,只需要进程有权限访问该管道文件即可。需要注意的是,命名管道中的数据实际上是存储在内存中,管道文件在文件系统中相当于是一个标记。

创建命名管道有两种方式:

  1. 在命令行界面通过命令mkfifo filename创建命名管道文件,可以指定其文件名
  2. 在程序内部调用mkfifo函数(定义于头文件sys/stat.h中),参数filename为管道文件路径,mode为管道文件读写权限:
int mkfifo(const char *filename, mode_t mode);

命名管道的读写机制和匿名管道相似。
只不过在使用前我们需要调用open函数来打开管道文件,通过其返回的文件描述符来读写管道文件。

下面两个程序展示了命名管道的用法:
写端:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main() {
	mkfifo("pipe", 0644);  //创建一个管道文件pipe,权限为644
	int fd = open("pipe", O_WRONLY);  //以只写的方式打开管道文件,返回该管道的文件描述符
	if (fd == -1) {
		fprintf(stderr, "can not open pipe file\n");
		_exit(1);
	}

	char buf[1024];
	while (1) {
		fprintf(stdout, "Enter message:");
		fflush(stdout);
		if (fscanf(stdin, "%s", buf) == EOF) {  //从键盘读入一串字符串到buf
			break;
		}
		write(fd, buf, strlen(buf));  //将buf写入到管道
	}
	close(fd);
	return 0;
}

读端:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main() {
	int fd = open("pipe", O_RDONLY);  //以只读方式打开管道文件pipe
	if (fd == -1) {
		fprintf(stdout, "can not open pipe file\n");
		_exit(1);
	}

	char buf[1024];
	while (1) {
		ssize_t len = read(fd, buf, sizeof(buf));  //从管道读入数据到buf
		if (len <= 0) {
			break;
		}
		fprintf(stdout, "receive:%s\n", buf);  //打印buf中的数据
	}
	close(fd);
	return 0;
}

将上述两个源代码文件放在同一个目录下,并使用gcc编译,程序名为readwrite。同时打开两个终端,前者运行read程序,后者运行write程序。在运行write程序的终端用键盘输入一串消息并按下回车,此时在另外一个终端立刻就能收到这个消息:

[root@localhost Debug]# ./write
Enter message:abcdef
Enter message:abncd
Enter message:sdasda
Enter message:asdadsa

另外一个终端:

[root@localhost Debug]# ./read
receive:abcdef
receive:abncdf
receive:sdasda
receive:asdadsa
发布了117 篇原创文章 · 获赞 96 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/abc123lzf/article/details/101355944