进程间通信(4) - 管道(pipe)

1. 前言

本篇文章的所有例子,基于RHEL6.5平台。本篇只介绍管道(匿名管道/普通管道),命名管道在后续文章中会介绍。

2.管道特性

管道是Linux支持的最初Unix IPC形式之一,具有以下特点:

**管道是半双工的,数据只能向一个方向流动,一端输入,另一端输出。需要双方通信时,需要建立起两个管道。

**管道分为普通管道和命名管道。普通管道位于内存,只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程)。命名管道位于文件系统,没有亲缘关系的进程间只要知道管道名也可以通讯。

**管道也是文件。管道大小为4096字节。

**单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在于内存中。

**数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。

**管道满时,写阻塞;管道空时,读阻塞。

管道只能在有亲缘关系的进程间使用。这是由于管道没有名字的原因,所以不能跨进程的地址空间进行使用。这里这句话不是绝对的,因为从技术上可以在进程间传递管道的描述符,所以是可以通过管道实现无亲缘进程间的通信的。但尽管如此,管道还是通常用于具有共同祖先的进程间的通信。

3.建立管道pipe

#include <unistd.h>

int pipe(int filedest[2])    //成功返回0,失败返回-1

pipe函数用来创建一个管道,fd是传出参数,用于保存返回的两个文件描述符,该文件描述符用于标识管道的两端,fd[0]只能用于读,fd[1]只能用于写。

那么如果我们往fd[0]端写数据会是什么样的结果呢?

下面是测试代码:

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <errno.h>

int main()
{
    int fd[2];  

    if (pipe(fd) < 0)
    {
        std::cout<<"create pipe failed."<<std::endl;
        return -1;
    }  

    char *temp = "hello world";  

    if (write(fd[0], temp, strlen(temp) + 1) < 0)
    {
        std::cout<<"write pipe failed:"<<strerror(errno)<<std::endl;
    }

    return 0;
}

输出结果:

write pipe failed:Bad file descriptor

从这个结果可以看出,内核对于管道的fd[0]描述符打开的方式是以只读方式打开的,同理fd[1]是以只写方式打开的,所以管道只能保证单向的数据通信。

下图显示的是一个进程内的管道的数据流程:

从上图我们可以看到位于内核中的管道,进程通过两个文件描述符进行数据的传输,当然单个进程内的管道是没有必要的,上面只是为了更形象的表明管道的工作方式,一般管道的使用方式都是:父进程创建一个管道,然后fork产生一个子进程,由于子进程拥有父进程的副本,所以父子进程可以通过管道进程通信。这种使用方式如下图所示:

如上图所示,当父进程通过fork创建子进程后,父子进程都拥有对管道操作的文件描述符,此时父子进程关闭对应的读写端,使父子进程间形成单向的管道。关闭哪个端要根据具体的数据流向决定。

4.父子进程单向通信

上面说了父进程通过fork创建子进程后,父子进程间可以通过管道通信,数据流的方向根据具体的应用决定。我们都知道在shell中,管道的数据流向都是从父进程流向子进程,即父进程关闭读端,子进程关闭写端。如下图所示:

测试代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
    int pfd[2]; //保存打开管道后的两个文件描述符
    pid_t cpid; //保存子进程标识符
    char buf;
    if(argc != 2)//判断命令行参数是否符合
    {
        fprintf(stderr,"Usage: %s <string>\n",argv[0]);
        exit(0);
    }
    if (pipe(pfd) == -1)//建立管道
    {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
    cpid = fork();
    if (cpid == -1)
    {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    if (cpid == 0) //子进程
    {
        close(pfd[1]);          //关闭管道写,引用计数-1
        while (read(pfd[0], &buf, 1) > 0)   //从管道循环读取数据
            write(STDOUT_FILENO, &buf, 1);  //输出读到的数据
        write(STDOUT_FILENO, "\n", 1);      //输出从管道读取的数据
        close(pfd[0]);         //关闭管道读,引用计数-1
        exit(0);
    }
    else  <span style="font-family: 宋体;">//父进程</span>
    {
        close(pfd[0]);        //关闭管道读
        write(pfd[1], argv[1], strlen(argv[1]));//向管道写入命令行参数1
        close(pfd[1]);
        wait(NULL);           //等待子进程退出
        exit(0);
    }
}

说明:每调用一次fork  都要关闭一次进程描述符

执行此行命令:#./a.out   www

程序输出:   WWW

上述代码流程是,子进程等待父进程通过管道发送过来的数据,然后输出接收到的数据,代码中的read会阻塞到管道中有数据为止。

5.父子进程双向通信

由上我们知道,一个管道只能支持亲缘进程间的单向通信即半双工通信。如果要想通过管道来支持双向通信呢,那这里就需要创建两个管道,fd1,fd2;父进程中关闭fd1[0],fd2[1],子进程中关闭fd1[1],fd2[0]。这种通信模式如下图所示:

下面是双向通信的测试代码:

#include <iostream>
#include <unistd.h>  

int main()
{
    int fd1[2], fd2[2];  

    if (pipe(fd1) < 0 || pipe(fd2) < 0)
    {
        std::cout<<"create pipe failed."<<std::endl;
        return -1;
    }  

    char buf[256];
    char *temp = "hello world";  

    if (fork() == 0)
    {
        close(fd1[1]);
        close(fd2[0]);  

        read(fd1[0], buf, sizeof(buf));
        std::cout<<"child:receive message from pipe 1: "<<buf<<std::endl;  

        write(fd2[1], temp, strlen(temp) + 1);
        exit(0);
    }  

    close(fd1[0]);
    close(fd2[1]);  

    write(fd1[1], temp, strlen(temp) + 1);
    read(fd2[0], buf, sizeof(buf));
    std::cout<<"parent:receive message from pipe 2: "<<buf<<std::endl;  

    return 0;
}

代码的执行结果如下:

child:receive message from pipe 1: hello world

parent:receive message from pipe 2: hello world

其中代码的流程是父进程创建了两个管道,用fd1,fd2表示,管道fd1负责父进程向子进程发送数据,fd2负责子进程向父进程发送数据。进程启动后,子进程等待父进程通过管道fd1发送数据,当子进程收到父进程的数据后,输出消息,并通过管道fd2回复父进程,然后子进程退出,父进程收到子进程的响应后,输出消息并退出。

前面已经提到过对管道的read会阻塞到管道中有数据为止,具体管道的read和write的规则将会在后面介绍。

6.popen和pclose函数

作为管道的一个实例,就是标准I/O函数库提供的popen函数,该函数创建一个管道,并fork一个子进程,该子进程根据popen传入的参数,关闭管道的对应端,然后执行传入的shell命令,然后等待终止。

调用进程和fork的子进程之间形成一个管道。调用进程和执行shell命令的子进程之间的管道通信是通过popen返回的FILE*来间接的实现的,调用进程通过标准文件I/O来写入或读取管道。

#include <stdio.h>

FILE *popen(const char *command, const char *type);    //成功返回标准文件I/O指针,失败返回NULL

command:该传入参数是一个shell命令行,这个命令是通过shell处理的。

type:该参数决定调用进程对要执行的command的处理,type有如下两种情况:

type = “r”,调用进程将读取command执行后的标准输出,该标准输出通过返回的FILE*来操作;

type = “w”,调用进程将写command执行过程中的标准输入;

int pclose(FILE *stream);  //成功返回shell的终止状态,失败返回-1

pclose函数会关闭由popen创建的标准I/O流,等待其中的命令终止,然后返回shell的执行状态。

下面是关于popen的测试代码1:

#include <iostream>
#include <cstdio>
#include <unistd.h>  

using namespace std;  

int main()
{
    char *cmd = "ls /usr/include/sys*.h";  

    FILE *p = popen(cmd, "r");
    char buf[256];  

    while (fgets(buf, 256, p) != NULL)
    {
        cout<<buf;
    }    

    pclose(p);  

    return 0;
}

输出:

/usr/include/syscall.h   /usr/include/sysexits.h   /usr/include/syslog.h

程序的执行流程如下:调用进程执行popen时,会创建一个管道,然后fork生成一个子进程,子进程执行popen传入的"ls /usr/local/bin" shell命令,子进程将执行结果通过管道传递给调用进程,调用进程通过标准文件I/O来读取管道中的数据,并输出显示。

测试代码2:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int main( void )
{
    FILE   *stream;//文件流
    char   buf[1024];//读写缓冲区
    memset( buf, '\0', sizeof(buf) );//清空
    stream = popen( "ls /usr/include/sys*.h", "w" );
    for(;;)
    {
        memset(buf,0x00,sizeof(buf));
        scanf("%s",buf);//接受输入
        if(strcmp(buf,"k") == 0)//如果是k就退出
        {
            break;
        }
        fprintf(stream,"%s\n",buf);//写入
    }
    pclose( stream );//关闭
    return 0;
}

输出:

[[email protected] csdnblog]# ./a.out

/usr/include/syscall.h /usr/include/sysexits.h /usr/include/syslog.h

7.管道的读写规则

管道两端可分别用描述字fd[0]以及fd[1]来描述,需要注意的是,管道的两端是固定了任务的。即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。一般文件的I/O函数都可以用于管道,如close、read、write等等。

从管道中读取数据:

--如果管道的写端不存在,则认为已经读到了数据的末尾,读函数返回的读出字节数为0;

--当管道的写端存在时,如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数,如果请求的字节数目不大于PIPE_BUF,则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。注:(PIPE_BUF在include/linux/limits.h中定义,不同的内核版本可能会有所不同。Posix.1要求PIPE_BUF至少为512字节,red hat 7.2中为4096)。

向管道中写入数据:

--向管道中写入数据时,linux将不保证写入的原子性,管道缓冲区一有空闲区域,写进程就会试图向管道写入数据。如果读进程不读走管道缓冲区中的数据,那么写操作将一直阻塞。

注意:只有在管道的读端存在时,向管道中写入数据才有意义。否则,向管道中写入数据的进程将收到内核传来的SIFPIPE信号,应用程序可以处理该信号,也可以忽略(默认动作则是应用程序终止)。

对管道的写规则的验证1:写端对读端存在的依赖性

#include <unistd.h>
#include <sys/types.h>
int main()
{
	int pipe_fd[2];
	pid_t pid;
	char r_buf[4];
	char* w_buf;
	int writenum;
	int cmd;

	memset(r_buf, 0, sizeof(r_buf));
	if (pipe(pipe_fd)<0)
	{
		printf("pipe create error\n");
		return -1;
	}

	if ((pid = fork()) == 0)
	{
		close(pipe_fd[0]);
		close(pipe_fd[1]);
		sleep(10);
		exit();
	}
	else if (pid>0)
	{
		sleep(1);  //等待子进程完成关闭读端的操作
		close(pipe_fd[0]);//write
		w_buf = "111";
		if ((writenum = write(pipe_fd[1], w_buf, 4)) == -1)
			printf("write to pipe error\n");
		else
			printf("the bytes write to pipe is %d \n", writenum);

		close(pipe_fd[1]);
	}
	return 0;
}

输出:

Broken pipe

原因就是该管道以及它的所有fork()产物的读端都已经被关闭。如果在父进程中保留读端,即在写完pipe后,再关闭父进程的读端,也会正常写入pipe。因此,在向管道写入数据时,至少应该存在某一个进程,其中管道读端没有被关闭,否则就会出现上述错误(管道断裂,进程收到了SIGPIPE信号,默认动作是进程终止)。

对管道的写规则的验证2linux不保证写管道的原子性验证

#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
int main(int argc, char**argv)
{
	int pipe_fd[2];
	pid_t pid;
	char r_buf[4096];
	char w_buf[4096 * 2];
	int writenum;
	int rnum;
	memset(r_buf, 0, sizeof(r_buf));
	if (pipe(pipe_fd)<0)
	{
		printf("pipe create error\n");
		return -1;
	}

	if ((pid = fork()) == 0)
	{
		close(pipe_fd[1]);
		while (1)
		{
			sleep(1);
			rnum = read(pipe_fd[0], r_buf, 1000);
			printf("child: readnum is %d\n", rnum);
		}
		close(pipe_fd[0]);

		exit();
	}
	else if (pid>0)
	{
		close(pipe_fd[0]);//write
		memset(r_buf, 0, sizeof(r_buf));
		if ((writenum = write(pipe_fd[1], w_buf, 1024)) == -1)
			printf("write to pipe error\n");
		else
			printf("the bytes write to pipe is %d \n", writenum);
		writenum = write(pipe_fd[1], w_buf, 4096);
		close(pipe_fd[1]);
	}
	return 0;
}

输出:

[[email protected] csdnblog]# ./a.out

the bytes write to pipe is 1024

child: readnum is 1000

child: readnum is 1000   //注意,此行输出说明了写入的非原子性

child: readnum is 1000

child: readnum is 1000

child: readnum is 1000

child: readnum is 120    //注意,此行输出说明了写入的非原子性

child: readnum is 0

child: readnum is 0

child: readnum is 0

child: readnum is 0

child: readnum is 0

.........

结论:

写入数目小于4096时写入是非原子的!

如果把父进程中的两次写入字节数都改为5000,则很容易得出下面结论:

写入管道的数据量大于4096字节时,缓冲区的空闲空间将被写入数据(补齐),直到写完所有数据为止,如果没有进程读数据,则一直阻塞。

8.dup

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main()
{
int pfds[2];
if ( pipe(pfds) == 0 )
{
    if ( fork() == 0 )//子进程
    {
        close(1);//关闭标准输出
        dup2( pfds[1], 1 );//管道的写文件描述符复制到进程的输出
        close( pfds[0] );//关闭管道读
        execlp( "ls", "ls","-l", NULL );//执行ls -l 输出写入管道
    }
    else
    {
        close(0);
        dup2( pfds[0], 0 );//管道的读文件描述符复制到进程的输入
        close( pfds[1] );
        execlp( "wc", "wc", "-l", NULL );//执行wc -l 将管道读取数据作为wc命令的输入
     }
}
return 0;
}

[[email protected] csdnblog]# ls -a

.  ..  a.out  pipe.c

执行a.out后,输出如下:

[[email protected] csdnblog]# a.out

3

其中Linux execlp函数相当于执行

# ls -l | wc -l

统计当前目录下文件数量

时间: 2024-08-06 16:08:21

进程间通信(4) - 管道(pipe)的相关文章

【IPC第二个进程间通信】管道Pipe

IPC进程间通信+管道Pipe         IPC(Inter-Process Communication,进程间通信).         管道用于进程间共享数据,事实上质是共享内存.经常使用IPC之中的一个.管道不仅能够用于本机进程间通信,还可实现跨网络进程间通信,如同Socket通信,管道相同封装计算机底层网络实现,提供一个良好的API接口.                1.管道(Pipe):        管道分为匿名管道和命名管道.        匿名管道仅仅能用于父子进程间通信

Linux进程间通信之管道(pipe)、命名管道(FIFO)与信号(Signal)

整理自网络 Unix IPC包括:管道(pipe).命名管道(FIFO)与信号(Signal) 管道(pipe) 管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信: 实现机制: 管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条.管道的一端连接一个进程的输出.这个进程会向管道中放入信息.管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息.一个缓冲区不需要很大,它被设计成为环形的数据结构,以便管

Linux 进程间通信之管道(pipe),(fifo)

 无名管道(pipe) 管道可用于具有亲缘关系进程间的通信,有名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信: 定义函数: int pipe(int filedes[2]) filedes[0]为管道里的读取端 filedes[1]则为管道的写入端. 实现机制: 管道是由内核管理的一个缓冲区,相当于我们放入内存中的一个纸条.管道的一端连接一个进程的输出.这个进程会向管道中放入信息.管道的另一端连接一个进程的输入,这个进程取出被放入管道的信息.一个缓

进程间通信之管道--pipe和fifo使用

匿名管道pipe 函数原型: #include <unistd.h> int pipe(int fildes[2]); 参数说明 fildes是我们传入的数组,也是一个传出参数.fildes[0]是读端,fildes[1]是写端. 返回值 成功调用返回0. 失败调用返回-1且设置errno. 实例 现在实现一个用父进程读,子进程写的管道例子. int main(int argc, char const *argv[]) { int pipefd[2]; pipe(pipefd); pid_t

简述Linux进程间通信之管道pipe(下)

上篇文章的简述,我相信大家对管道的概念有了模糊的认识,本文通过代码实例来强化对管道的理解. 创建管道主要用到pipe函数,pipe的原型如下: 一.函数原型 #include <unistd.h> int pipe(int pipefd[2]); 参数:一个整型数组,管道创建成功后,pipefd[0]表示管道的读端,pipefd[1]表示管道的写端. 成功返回0,失败返回-1,同时errno被设置. 二.父子进程通信 上文中,我们描述了父子进程通过管道来进行通信的整个过程,现在我们用C语言来实

进程间通信之管道(pipe、fifo)

我们先来说说进程间通信(IPC)的一般目的,大概有数据传输.共享数据.通知事件.资源共享和进程控制等.但是我们知道,对于每一个进程来说这个进程看到属于它的一块内存资源,这块资源是它所独占的,所以进程之间的通信就会比较麻烦,原理就是需要让不同的进程间能够看到一份公共的资源.所以交换数据必须通过内核,在内核中开辟?块缓冲区,进程1把数据从?户空间 拷到内核缓冲区,进程2再从内核缓冲区把数据读?,内核提供的这种机制称为进程间通信.一般我们采用的进程间通信方式有 管道(pipe)和有名管道(FIFO)

进程间通信IPC—匿名管道(pipe)和命名管道(fifo)

管道内部如何实现-大小,组织方式,环形队列? 一.进程间通信有多种方式,本文主要讲解对管道的理解.管道分为匿名管道和命名管道. (1)管道( pipe ):又称匿名管道.是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用.进程的亲缘关系通常是指父子进程关系. (2)命名管道 (named pipe或FIFO) : 有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信. 二.管道 1. 管道的特点: (1)管道是半双工的,数据只能向一个方向流动:双方通信时,需要

进程间通信 管道 (pipe,FiFO)

管道的运行原理 管道是一种最基本的IPC机制,由pipe函数创建 #include <unistd.h> int pipe(int _pipe[2]); 调用pipe函数时在内核中开辟一块缓冲区用于通信,它有一个读端和一个写端,通过filedes参数传出给程序两个文件描述符,filedes[0]指向管道的读端,filedes[1]指向管道的写端.管道就像一个打开的文件,通过read(filedes[0]);或者write(filedes[1]):向这个文件读写数据,其实是在读写内核缓冲区.pi

进程间通信 之 管道

一 无名管道: 特点: 具有亲缘关系的进程间通信,但不仅仅指父子进程之间哦. (1)无名管道的创建 int pipe(int pipefd 参数: pipefd  数组的首地址 返回值: 成功返回0,失败返回-1 注意: 无名管道存在内核空间,创建成功会给用户空间两个文件描述符,fd[0]:读管道 fd[1]:写管道 思考:为什么无名管道只能用于亲缘关系间进程通信? 因为只有具有亲缘关系的进程存在数据拷贝 [拷贝文件描述符] 二.有名管道 特点: (1)任意进程间通信 (2)文件系统中存在文件名