Linux管道学习(无名管道)

目录

1、概述

2、管道的创建

3、管道读写行为

3.1、管道读

3.2、管道写

4、管道用于兄弟进程之间的通讯


        在linux中管道有两种,一是无名管道(匿名管道),第二种是有名管道;无名管道主要用于有血缘关系的父子进程间通信,有名管道则不受该限制,可用于任意进程之间的通信;这里我们主要学习无名管道。

1、概述

        创建无名管道的函数如下:

#include <unistd.h>
int pipe(int pipefd[2]);

        调用失败返回-1,成功返回0;调用成功时该函数会创建一个单向的管道用于进程之间的通讯,返回的管道包含读端和写端,其中fd[0]用于读,fd[1]用于写,写到fd[1]的数据会被内核保存到缓冲区,直到fd[0]读走数据。

2、管道的创建

        代码如下:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>

int main(int argc, char *argv[]) {
	pid_t fd[2];
	int ret;

	ret = pipe(fd);	
	if (-1 == ret) {
		perror("pipe error!\n");	
		exit(1);
	}

	printf("Hello pipe, fd[0] = %d, fd[1] = %d\n", fd[0], fd[1]);


	return 0;
}

        很简单,直接调用pipe并传入一个文件描述符数组,如果出错则退出,调用成功则返回两个可用的文件描述符,其中fd[0]用于读,fd[1]用于写。

        gcc编译:gcc pipe.c -o pipe,运行./pipe得出如下运行结果:

3、管道读写行为

        由于管道是单向通讯的,所以在使用的时候会有一些限制。

  • 管道读

如果管道中有数据,则读取实际能读到的数据;

如果管道中无数据,此时有两种情况:一是有管道写端,此时会阻塞直到写端写入数据;二是没有管道写端(写端被关闭),因为没有写端,永远不会有数据写入,此时返回0。

  • 管道写

如果管道满了,则写阻塞,直到能写入。

如果管道未满,此时有两种情况:一是没有管道读端,此时异常终止(SIGPIPE导致);二是有读端,则返回实际写入的字节数。

        下面简单列举几个例子,主要是父子进程之间通讯。

3.1、管道读

         代码如下:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>

int main(int argc, char *argv[]) {
	pid_t fd[2];
	int ret;
	char buf[1024];
	char *info = "Hello Pipe(from read)\n";

	ret = pipe(fd);	
	if (-1 == ret) {
		perror("pipe error!\n");	
		exit(1);
	}
	ret = fork();

	if (0 == ret) {
		printf("I'm child\n");
		close(fd[1]);
		
		while ((ret = read(fd[0], &buf, 1024)) > 0) {
			write(STDOUT_FILENO, &buf, ret);
		}
		close(fd[0]);
	} else if (ret > 0) {
		printf("I'm parent\n");
		close(fd[0]);

		write(fd[1], info, strlen(info));
		
		close(fd[1]);
	} else {
		perror("fork error!\n");
		exit(1);
	}


	return 0;
}

         首先调用pipe创建了读写管道的读端和写端,然后调用fork创建了一个子进程,如果ret 为0,说明是在子进程,如果ret大于0,说明是在父进程,如果ret小于0则说明fork出错,退出程序。

        代码中申明了一个字符串"Hello Pipe(from read)",由于父进程用于写,子进程用于读,所以父进程首先关闭了fd[0],然后用fd[1]往管道中写入"Hello Pipe(from read)",之后关闭fd[1]。

        由于子进程用于读,所以一开始关闭了管道的写端fd[1],然后循环从fd[0]中读入数据存储到buf中并打印到屏幕,直到读完管道中的数据,最后关闭管道读端fd[0]。

        运行结果如下:

        上面演示的是管道有数据的情况,立刻就读出了管道中的数据;下面考虑以下两种情况

  1. 管道无数据 有写端
  2. 管道无数据 无写端

        第一种情况,管道将会读阻塞,可以运行如下实例看看:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>

int main(int argc, char *argv[]) {
	pid_t fd[2];
	int ret;
	char buf[1024];
	char *info = "Hello Pipe(from read)\n";

	ret = pipe(fd);	
	if (-1 == ret) {
		perror("pipe error!\n");	
		exit(1);
	}
	ret = fork();

	if (0 == ret) {
		printf("I'm child\n");
		close(fd[1]);
		
		while ((ret = read(fd[0], &buf, 1024)) > 0) {
			write(STDOUT_FILENO, &buf, ret);
		}
		close(fd[0]);
		printf("After read\n");
	} else if (ret > 0) {
		printf("I'm parent\n");
		close(fd[0]);

		//write(fd[1], info, strlen(info));
		
		sleep(10);
		printf("After sleep\n");
		close(fd[1]);
	} else {
		perror("fork error!\n");
		exit(1);
	}


	return 0;
}

        代码结构和之前一样,为了构造管道无数据的情况,父进程中没有写入数据,而是sleep(10),然后10s后输出"After sleep"且关闭管道写端fd[1];子进程保持不变,由于管道中一直没有数据,会导致子进程管道的读端一直阻塞直到父进程关闭管道写端fd[1]。

        程序输出如下:

        第二种情况,管道无数据无写端,此时读管道将直接返回0,因为确实没有数据可以读;其实第一种情况演示里面也包含了这种情况,管道关闭的时候,也就是没有写端,读端就返回了。

3.2、管道写

        代码如下:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>

int main(int argc, char *argv[]) {
	pid_t fd[2];
	int ret;
	char buf[1024];
	char *info = "Hello Pipe(from read)\n";

	ret = pipe(fd);	
	if (-1 == ret) {
		perror("pipe error!\n");	
		exit(1);
	}
	ret = fork();

	if (0 == ret) {
		printf("I'm child\n");
		close(fd[1]);
		
		while ((ret = read(fd[0], &buf, 1024)) > 0) {
			write(STDOUT_FILENO, &buf, ret);
		}
		close(fd[0]);
		printf("After read\n");
	} else if (ret > 0) {
		printf("I'm parent\n");
		close(fd[0]);

		int n = write(fd[1], info, strlen(info));
		
		printf("parent write: %d byte\n", n);
		close(fd[1]);
	} else {
		perror("fork error!\n");
		exit(1);
	}


	return 0;
}

        逻辑结构与管道读一致,这里仅仅是在写管道的时候返回写入的管道字节数,运行结果如下:

        上面演示了管道未满,且有读端的情况;那么没有读端的时候表现如何呢?前面我们说过管道未满,没有读端的时候写入管道将会报错,现在来看看这种情况,将代码改为如下:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>

int main(int argc, char *argv[]) {
	pid_t fd[2];
	int ret;
	char buf[1024];
	char *info = "Hello Pipe(from read)\n";

	ret = pipe(fd);	
	if (-1 == ret) {
		perror("pipe error!\n");	
		exit(1);
	}
	ret = fork();

	if (0 == ret) {
		printf("I'm child\n");
		close(fd[1]);
		/*	
		while ((ret = read(fd[0], &buf, 1024)) > 0) {
			write(STDOUT_FILENO, &buf, ret);
		}
		*/
		close(fd[0]);
		printf("After close\n");
	} else if (ret > 0) {
		printf("I'm parent\n");
		close(fd[0]);
		sleep(2);

		int n = write(fd[1], info, strlen(info));
		
		printf("parent write: %d byte\n", n);
		close(fd[1]);
	} else {
		perror("fork error!\n");
		exit(1);
	}
	printf("end of process\n");

	return 0;
}

        子进程一开始就关闭了管道的读端和写端,父进程则一开始关闭读端,睡眠2s等待子进程关闭管道读端后再写入数据,此时就营造了有写端没读端的情况,在命令行模式输出如下:

        只输出了一行"end of process",明显是有一个进程没有运行完整就退出了,我们在gdb下运行看看,输出如下:

         在gdb下以及把进程的错误输出了,SIGPIPE导致了进程退出,其实也可以理解,因为没读端,此时写入的数据相当于是无用数据,内核干脆把问题抛了出来;开发者其实可以捕获这个信号然后进行自定义处理,此处不再展开。 

        管道写还有一种情况是,管道已经满了,这种情况如何演示呢?很遗憾,没法演示这种情况,因为内核会在管道快要满的时候动态扩容,此时管道会恢复到正常状况,对于应用来说就是管道可以一直写入,所以我们看不到这种情况。

4、管道用于兄弟进程之间的通讯

        前面介绍的是用于父子进程之间的通讯,现在来看看用于兄弟进程之间如何通讯,也是在前面例子的基础上,我们在父进程中创建两个子进程,然后在两个子进程之间使用管道进行通讯,修改后的代码如下:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>

int main(int argc, char *argv[]) {
	pid_t fd[2];
	int ret, i;
	char buf[1024];
	pid_t status;
	char *info = "Hello Pipe(from read)\n";

	ret = pipe(fd);	
	if (-1 == ret) {
		perror("pipe error!\n");	
		exit(1);
	}
	for (i = 0; i < 2; i++) {
		ret = fork();
		if (0 == ret) {
			break;
		} else if (ret < 0) {
			perror("fork error!\n");
			exit(1);
		}
	}

	if (0 == i) { // child 1
		printf("I'm child 1\n");
		close(fd[1]);
		
		while ((ret = read(fd[0], &buf, 1024)) > 0) {
			write(STDOUT_FILENO, &buf, ret);
		}

		close(fd[0]);
	} else if (1 == i) { // child 2
		printf("I'm child 2\n");
		close(fd[0]);

		int n = write(fd[1], info, strlen(info));
		close(fd[1]);
	} else if (2 == i) {
		printf("I'm parent\n");
		//close(fd[0]);
		//close(fd[1]);

		for (int j = 0; j < 2; j++) {
			status = wait(NULL);
			printf("parent wait %d\n", status);
		}
	}

	printf("end of process\n");

	return 0;
}

        子进程2写入,子进程1读出,所以子进程2一开始先关闭管道读端fd[0],然后往管道的写端fd[1]写入数据;子进程负责1读取数据,所以子进程1刚开始就关闭管道写入端fd[1],然后从管道读入端fd[0]读取数据,下面是运行结果:

        从运行结果来看是有问题的,两个子进程加上一个父进程,应该输出3次"end of process",而这里只有一次,同时父进程也只回收了一个子进程;代码在父子进程通讯的时候正常,变成兄弟进程之后就不正常了,为什么会这样呢?

         前面我们说过管道数据是单向流动的,从写端到读端;在当前我们的这个场景下,其实出除了两个子进程之间从子进程2向子进程1的数据流动,父进程同样也持有了管道的读写端,从而破坏了管道的单向流动,所以我们需要关闭父进程中的管道读写端,保证数据的单向流动,修改后的代码如下:

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>

int main(int argc, char *argv[]) {
	pid_t fd[2];
	int ret, i;
	char buf[1024];
	pid_t status;
	char *info = "Hello Pipe(from read)\n";

	ret = pipe(fd);	
	if (-1 == ret) {
		perror("pipe error!\n");	
		exit(1);
	}
	for (i = 0; i < 2; i++) {
		ret = fork();
		if (0 == ret) {
			break;
		} else if (ret < 0) {
			perror("fork error!\n");
			exit(1);
		}
	}

	if (0 == i) { // child 1
		printf("I'm child 1\n");
		close(fd[1]);
		
		while ((ret = read(fd[0], &buf, 1024)) > 0) {
			write(STDOUT_FILENO, &buf, ret);
		}

		close(fd[0]);
	} else if (1 == i) { // child 2
		printf("I'm child 2\n");
		close(fd[0]);

		int n = write(fd[1], info, strlen(info));
		close(fd[1]);
	} else if (2 == i) {
		printf("I'm parent\n");
		close(fd[0]);
		close(fd[1]);

		for (int j = 0; j < 2; j++) {
			status = wait(NULL);
			printf("parent wait %d\n", status);
		}
	}

	printf("end of process\n");

	return 0;
}

        运行结果如下:

        如结果所示,此时所有进程都能正常终止了。 

Linux 无名管道(也称为FIFO,即File In/Out Channel)是一种半双工通信机制,它允许两个进程通过创建一个特殊类型的文件来进行通信,而无需预设的命名路径。这种通信是在内核级进行的,因此效率较高。以下是基本的无名管道操作的代码分析: ```c #include <stdio.h> #include <unistd.h> #include <fcntl.h> #define READ_END 0 #define WRITE_END 1 int main(int argc, char *argv[]) { int pipe_fd[2]; // 创建一个管道句柄数组 char message[] = "Hello, Pipe!"; if (pipe(pipe_fd) == -1) { // 如果创建失败,返回错误 perror("Pipe creation failed"); return 1; } pid_t child_pid = fork(); // 创建进程 if (child_pid < 0) { perror("Fork failed"); return 1; } else if (child_pid == 0) { // 子进程 close(pipe_fd[WRITE_END]); // 子进程关闭端 write(pipe_fd[READ_END], message, strlen(message)); // 子进程管道写入数据 printf("Child process wrote to pipe and exited.\n"); exit(0); } else { // 父进程 close(pipe_fd[READ_END]); // 父进程关闭读端 char buffer[1024]; read(pipe_fd[WRITE_END], buffer, sizeof(buffer)); // 父进程管道读取数据 printf("Parent process read from pipe: %s\n", buffer); wait(NULL); // 等待子进程结束 } return 0; } ``` 在这个例子中, - 父进程首先创建了一个管道,并将其两端分别保存在`pipe_fd`数组的`READ_END`和`WRITE_END`位置。 - `fork()`函数用于创建进程,新生成的子进程会拥有相同的`pipe_fd`副本。 - 子进程负责将消息写入管道(`write`),然后结束。 - 父进程则关闭读端,从管道读取数据(`read`),并打印出来。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值