Linux 进程间通信(二)管道通信

版权声明:转载请声明 https://blog.csdn.net/qq_40732350/article/details/82082115

1 管道简介

      管道是 Linux 中进程间通信的一种方式, 它把一个程序的输出直接连接到另一个程序的输入。 Linux 的管道主要包括两种: 无名管道和有名管道。


1. 无名管道
无名管道是 Linux 中管道通信的一种原始方法, 如图(左) 所示, 它具有如下特点:
1.它能用于具有亲缘关系的进程之间的通信(也就是父子进程或者兄弟进程之间)。
 2.它是一个
半双工的通信模式, 具有固定的读端和写端,也就是一个进程写,另一个进程读,不能同时读和写。
 3.管道也可以看成是一种特殊的文件, 对于它的读写也
可以使用普通的 read()、 write()等函数。 但是它不是普通的文件, 并不属于其他任何文件系统, 并且只存在于内存中。


2. 有名管道(FIFO)
有名管道是对无名管道的一种改进, 如图(右) 所示, 它具有如下特点:
1.它可以使不具有亲缘关系的两个进程实现彼此通信。
 2.该管道可以通过
路径名来指出, 并且在文件系统中是可见的。 在建立了管道之后,两个进程就可以把它当做普通文件一样进行读写操作, 使用非常方便。
 3.严格地遵循
先进先出规则, 对管道及 FIFO 的读总是从开始处返回数据, 对它们的写则是把数据添加到末尾, 它们不支持如 lseek()等文件定位操作。

2 无名管道系统调用

2.1. 管道创建与关闭说明
     管道是基于文件描述符的通信方式, 当一个管道建立时, 它会创建两个文件描述符 fd[0]和 fd[1], 其中 fd[0]固定用于读管道, 而 fd[1]固定用于写管道, 如图所示, 这样就构成了一个半双工的通道。

管道关闭时只需将这两个文件描述符关闭即可, 可使用普通的 close()函数逐个关闭各个文件描述符。


2.2 管道创建函数
创建管道可以通过调用 pipe()来实现。 表列出了 pipe()函数的语法要点。

2.3 管道读写说明
     用 pipe()函数创建的管道两端处于一个进程中, 由于管道是主要用于在不同进程间通信的, 因此在实际应用中没有太大意义。 实际上, 通常先是创建一个管道, 再调用 fork()函数创建一个子进程, 该子进程会继承父进程所创建的管道, 这时, 父子进程管道的文件描述符
对应关系如图所示。

      此时的关系看似非常复杂, 实际上却已经给不同进程之间的读写创造了很好的条件,父子进程分别拥有自己的读写通道。

扫描二维码关注公众号,回复: 2921501 查看本文章

但是为了防止

)进程写了后,)进程还没来得及读(),)进程就把内容读了

)进程读了后,)进程还没来得及写(),)进程就把内容写了

必需把无关的读端或写端的文件描述符关闭。

例如, 在图中将父进程的写端 fd[1]和子进程的读端 fd[0]关闭。此时, 父子进程之间就建立起了一条“子进程写入父进程读取” 的通道。

同样, 也可以关闭父进程的 fd[0]和子进程的 fd[1], 这样就可以建立一条“父进程写入子进程读取” 的通道。

另外, 父进程还可以创建多个子进程, 各个子进程都继承了相应的fd[0]和 fd[1]。 这时, 只需关闭相应端口就可以建立其各子进程间的通道。


2.4管道读写注意点
管道读写需注意以下几点:
 只有在管道的读端存在时, 向管道写入数据才有意义。 否则, 向管道写入数据的进程将收到内核传来的 SIGPIPE 信号(通常为 Broken pipe 错误)。
 向管道写入数据时, Linux 将不保证写入的原子性, 管道缓冲区一有空闲区域, 写进程就会试图向管道写入数据。 如果读进程不读取管道缓冲区中的数据, 那么写操作将会一直阻塞。
 父子进程在运行时, 它们的先后次序并不能保证。 因此, 为了保证父子进程已经关闭了相应的文件描述符, 可在两个进程中调用 sleep()函数。 当然这种调用不是很好的解决方法, 在后面学到进程之间的同步机制与互斥机制后, 请读者自行修改本小节的实例程序。
 

2.5使用实例
在本例中, 首先创建管道, 之后父进程使用 fork()函数创建子进程, 最后通过关闭父进程的读描述符和子进程的写描述符, 建立起它们之间的管道通信。
 

/* pipe.c */
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_DATA_LEN 256
#define DELAY_TIME 1
int main()
{
	pid_t pid;
	int pipe_fd[2];
	char buf[MAX_DATA_LEN];
	const char data[] = "Pipe Test Program";
	int real_read, real_write;
	memset((void*)buf, 0, sizeof(buf));
	if (pipe(pipe_fd) < 0) /* 创建管道 */
	{
		printf("pipe create error\n");
		exit(1);
	}
	
	if ((pid = fork()) == 0) /* 创建一个子进程 */
	{
		/* 子进程关闭写描述符, 并通过使子进程暂停 3秒 等待父进程已关闭相应的读描述符 */
		close(pipe_fd[1]);
		sleep(DELAY_TIME * 3);
		/* 子进程读取管道内容 */
		if ((real_read = read(pipe_fd[0], buf, MAX_DATA_LEN)) > 0)
		{
			printf("%d bytes read from the pipe is '%s'\n", real_read, buf);
		} 
		close(pipe_fd[0]); /* 关闭子进程读描述符 */
		exit(0);
	}
	else if (pid > 0)
		{
			/* 父进程关闭读描述符, 并通过使父进程暂停 1秒 等待子进程已关闭相应的写描述符 */
			close(pipe_fd[0]);
			sleep(DELAY_TIME);
			if((real_write = write(pipe_fd[1], data, strlen(data))) != -1)
			{
				printf("Parent wrote %d bytes : '%s'\n", real_write, data);
			} 
			close(pipe_fd[1]); /* 关闭父进程写描述符 */
			waitpid(pid, NULL, 0); /* 收集子进程退出信息 */
			exit(0);
		}
}

将该程序编译, 运行结果如下:
$ ./pipe
Parent wrote 17 bytes : 'Pipe Test Program'
17 bytes read from the pipe is 'Pipe Test Program

 

3 标准流管道

3.1 标准流管道函数说明
      与 Linux 的文件操作中有基于文件流的标准 I/O 操作一样, 管道的操作也支持基于文件流的模式。 这种基于文件流的管道主要是用来创建一个连接到另一个进程的管道, 这里的“另一个进程” 也就是一个可以进行一定操作的可执行文件, 例如, 用户执行“ls -l” 或者自己编写的程序“./pipe” 等。 由于这类操作很常用, 因此标准流管道就将一系列的创建过程合并到一个函数 popen()中完成。 它所完成的工作有以下几步:
 创建一个管道。
 fork()一个子进程。
 在父子进程中关闭不需要的文件描述符。
 执行 exec 函数族调用。
 执行函数中所指定的命令。
     这个函数的使用可以大大减少代码的编写量, 但同时也有一些不利之处。 例如, 它不如前面管道创建的函数那样灵活多样, 并且用 popen()创建的管道必须使用标准 I/O 函数进行操作, 但不能使用前面的 read()、 write()一类不带缓冲的 I/O 函数。

与之相对应, 关闭用 popen()创建的流管道必须使用函数 pclose()。 该函数关闭标准 I/O流, 并等待命令执行结束。


3.2函数格式
popen()和 pclose()函数语法要点如表 4.2 和表 4.3 所示。

3.3 使用实例
在该实例中, 使用 popen()来执行“ps -ef” 命令。 可以看出, popen()函数的使用能够使程序变得短小精悍。

/* standard_pipe.c */
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#define BUFSIZE 1024
int main()
{
	FILE *fp;
	char *cmd = "ps -ef";
	char buf[BUFSIZE];
	if ((fp = popen(cmd, "r")) == NULL) /* 调用 popen()函数执行相应的命令 */
	{
		printf("Popen error\n");
		exit(1);
	}
	while ((fgets(buf, BUFSIZE, fp)) != NULL)
	{
		printf("%s",buf);
	}
	pclose(fp);
	exit(0);
}


面是该程序在目标板上的执行结果。
$ ./standard_pipe
PID TTY Uid Size State Command
1 root 1832 S init
2 root 0 S [keventd]

74 root 1284 S ./standard_pipe
75 root 1836 S sh -c ps -ef
76 root 2020 R ps –ef
 

4 有名管道(FIFO)

       有名管道的创建可以使用函数 mkfifo(), 该函数类似于文件中的 open()操作, 可以指定管道的路径和打开的模式。 用户还可以在命令行使用“mknod 管道名 p” 来创建有名管道。

     在创建管道成功后, 就可以使用 open()、 read()和 write()这些函数了。 与普通文件的开发设置一样, 对于为读而打开的管道可在 open()中设置 O_RDONLY, 对于为写而打开的管道可在 open()中设置 O_WRONLY, 在这里与普通文件不同的是阻塞问题。 由于普通文件在读写时不会出现阻塞问题, 而在管道的读写中却有阻塞的可能, 这里的非阻塞标志可以在open()函数中设定为 O_NONBLOCK。 下面分别对阻塞打开和非阻塞打开的读写进行讨论。
对于读进程:
 若该管道是阻塞打开, 且当前 FIFO 内没有数据, 则对读进程而言将一直阻塞到有数据写入。
 若该管道是非阻塞打开, 则不论 FIFO 内是否有数据, 读进程都会立即执行读操作。即如果 FIFO 内没有数据, 则读函数将立刻返回 0。
对于写进程:
 若该管道是阻塞打开, 则写操作将一直阻塞到数据可以被写入。
 若该管道是非阻塞打开而不能写入全部数据, 则读操作进行部分写入或者调用失败。
列出了 mkfifo()函数的语法要点。

再对 FIFO 相关的出错信息进行归纳, 以方便用户查错。

    下面的实例包含两个程序, 一个用于读管道, 另一个用于写管道。 其中在读管道的程序中创建管道, 并且作为 main()函数里的参数由用户输入要写入的内容; 读管道的程序会读出用户写入到管道的内容。 这两个程序采用的是阻塞式读写管道模式。
写管道的程序如下:

/* fifo_write.c */
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#define MYFIFO "/tmp/myfifo" /* 有名管道文件名 */
#define MAX_BUFFER_SIZE PIPE_BUF /* 定义在 limits.h 中 */
int main(int argc, char * argv[]) /* 参数为即将写入的字符串 */
{
	int fd;
	char buff[MAX_BUFFER_SIZE];
	int nwrite;
	if(argc <= 1)
	{
		printf("Usage: ./fifo_write string\n");
		exit(1);
	}
	sscanf(argv[1], "%s", buff);
	/* 以只写阻塞方式打开 FIFO 管道 */
	fd = open(MYFIFO, O_WRONLY);
	if (fd == -1)
	{
		printf("Open fifo file error\n");
		exit(1);
	} /* 向管道中写入字符串 */
	if ((nwrite = write(fd, buff, MAX_BUFFER_SIZE)) > 0)
	{
		printf("Write '%s' to FIFO\n", buff);
	}
	close(fd);
	exit(0);
}

读管道程序如下:
 

/* fifo_read.c */
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#define MYFIFO "/tmp/myfifo" /* 有名管道文件名 */
#define MAX_BUFFER_SIZE PIPE_BUF /* 定义在 limits.h 中 */
int main()
{
	char buff[MAX_BUFFER_SIZE];
	int fd;
	int nread;
	/* 判断有名管道是否已存在, 若尚未创建, 则以相应的权限创建 */
	if (access(MYFIFO, F_OK) == -1)
	{
		if ((mkfifo(MYFIFO, 0666) < 0) && (errno != EEXIST))
		{
			printf("Cannot create fifo file\n");
			exit(1);
		}
	}
	/* 以只读阻塞方式打开有名管道 */
	fd = open(MYFIFO, O_RDONLY);
	if (fd == -1)
	{
		printf("Open fifo file error\n");
		exit(1);
	}
	while (1)
	{
		memset(buff, 0, sizeof(buff));
		if ((nread = read(fd, buff, MAX_BUFFER_SIZE)) > 0)
		{
			printf("Read '%s' from FIFO\n", buff);
		}
	} 
	close(fd);
	exit(0);
}

终端一:
$ ./fifo_read
Read 'FIFO' from FIFO
Read 'Test' from FIFO
Read 'Program' from FIFO

终端二:
$ ./fifo_write FIFO
Write 'FIFO' to FIFO
$ ./fifo_write Test
Write 'Test' to FIFO
$ ./fifo_write Program
Write 'Program' to FIFO

猜你喜欢

转载自blog.csdn.net/qq_40732350/article/details/82082115
今日推荐