现在的位置: 首页 > 综合 > 正文

Linux环境进程间通信(一):管道

2014年01月24日 ⁄ 综合 ⁄ 共 5458字 ⁄ 字号 评论关闭

转自:https://www.ibm.com/developerworks/cn/linux/l-ipc/part1/

1 管道相关的关键概念

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

1> 管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;

2> 只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);

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

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

2 管道的创建:

管道创建可通过pipe()函数来创建;

包含头文件:

#include <unistd.h>

函数原型:

int pipe(int fd[2])

参数:

fd[0] 是管道的读取端;fd[1] 是管道的写入端;

返回值:

如果创建管道成功,则返回0;否则,返回-1

可能的错误原因:(为防止翻译不准确,故用英文)

EFAULT pipefd is not valid.

EINVAL (pipe2()) Invalid value in flags.

EMFILE Too many file descriptors are in use by the process.

ENFILE The  system  limit  on  the  total number of open files has been reached.

该函数创建的管道的两端处于一个进程中间,在实际应用中没有太大意义,因此,一个进程在由pipe()创建管道后,一般再fork()一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在亲缘关系,这里的亲缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。

3 管道的读写规则:

管道两端可分别用描述字fd[0]以及fd[1]来描述

需要注意的是,管道的两端是固定了任务的:即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端

如果试图从管道写端读取数据,或者向管道读端写入数据都将导致错误发生。

一般文件的I/O函数都可以用于管道,如close、read、write等等。

1> 从管道中读取数据:

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

当管道的写端存在时:

        如果请求的字节数目大于PIPE_BUF,则返回管道中现有的数据字节数;

        如果请求的字节数目不大于PIPE_BUF,则返回管道中现有数据字节数(此时,管道中数据量小于请求的数据量);或者返回请求的字节数(此时,管道中数据量不小于请求的数据量)。

注:(PIPE_BUF在include/linux/limits.h中定义,不同的内核版本可能会有所不同。Posix.1要求PIPE_BUF至少为512字节,red hat 7.2中为4096)。

关于管道的读规则验证:

/**************
*管道的读取测试
**************/
#include <unistd.h>
#include <sys/types.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

int main()
{
    int pipe_fd[2];
    pid_t pid;
    char r_buf[32]={0};
    char w_buf[32]={0};
    char* p_wbuf;
    int r_num;
    int cmd;

    p_wbuf=w_buf;

    //创建管道
    if(pipe(pipe_fd)<0)
    {
        printf("pipe create error\n");
        return -1;
    }

    //创建子进程
    if((pid=fork())==0)
    {
        //子进程操作
        printf("\n");
        //关闭写端
        close(pipe_fd[1]);
        //确保父进程关闭读端
        sleep(3);
        //从管道中读取信息
        r_num=read(pipe_fd[0], r_buf, 100);
        printf( "读取到%d字符\n信息为:%s\n", r_num, r_buf);
        //关闭读端
        close(pipe_fd[0]);
        exit(0);
    }
    //父进程操作
    else if(pid>0)
    {
        //关闭读端
        close(pipe_fd[0]);//read
        strcpy(w_buf,"hello, this is BYH");
        if(write(pipe_fd[1], w_buf, 100)!=-1)
        {
            printf("父进程向管道中写入了数据\n");
        }

        //关闭写端
        close(pipe_fd[1]);
        printf("父进程关闭了写端\n");
        sleep(10);
    } 
}

程序输出结果:

父进程向管道中写入了数据
父进程关闭了写端

读取到100字符
信息为:hello, this is BYH

附加结论:

管道写端关闭后,写入的数据将一直存在,直到读出为止.

2> 向管道中写入数据

向管道中写入数据时,linux将不保证写入的原子性

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

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

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

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

int main()
{
    //用来存放管道的两端(读端和写端)
    int pipe_fd[2];
    pid_t pid;
    char r_buf[32]={0};
    char w_buf[32]={0};
    int writenum;

    //创建管道
    if(pipe(pipe_fd)<0)
    {
        printf("创建管道失败\n");
        return -1;
    }

    //创建子进程(子进程会继承父进程的管道)
    if((pid=fork())==0)
    {
        //关闭读端和写端
        close(pipe_fd[0]);
        close(pipe_fd[1]);
        sleep(10); 
        exit(0);
    }
    //父进程操作
    else if(pid>0)
    {
        //等待子进程完成关闭读端的操作
        sleep(1); 
        close(pipe_fd[0]);

        strcpy(w_buf, "Hello, this is BYH");
        if((writenum=write(pipe_fd[1], w_buf, 32))<0)
        { 
            printf("写入到管道的写端中失败\n");
        }       
        else 
        {
            printf("向管道的写端中写入%d字符\n", writenum);
        }
        close(pipe_fd[1]);
    } 
}

则输出结果为: Broken pipe,原因就是该管道以及它的所有fork()产物的读端都已经被关闭。

如果在父进程中保留读端,即在写完pipe后,再关闭父进程的读端,也会正常写入pipe,读者可自己验证一下该结论。

因此,在向管道写入数据时,至少应该存在某一个进程,其中管道读端没有被关闭,否则就会出现上述错误(管道断裂,进程收到了SIGPIPE信号,默认动作是进程终止)

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

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

int main(int argc,char**argv)
{
    int pipe_fd[2];
    pid_t pid;
    char r_buf[4096]={0};
    char w_buf[4096*2]={0};
    int writenum;
    int rnum;

    //创建管道
    if(pipe(pipe_fd)<0)
    {
        printf("创建管道失败\n");
        return -1;
    }

    //子进程操作
    if((pid=fork())==0)
    {
        //关闭写端
        close(pipe_fd[1]);
        //只要有数据写入管道,就一直读取
        while(true)
        {
            sleep(1); 
            rnum=read(pipe_fd[0], r_buf, 1000);
            printf("子进程读取了%d字节\n",rnum);
        }

        //关闭读端
        close(pipe_fd[0]);
        exit(0);
    }
    
    //父进程操作
    else if(pid>0)
    {
        //关闭读端
        close(pipe_fd[0]);
 
        if((writenum=write(pipe_fd[1], w_buf, 1024))==-1)
        {
            printf("写入信息到管道写端失败\n");
        }
        else
        { 
            printf("向管道写端写入%d字节\n", writenum);
        }
        //再次写入数据
        writenum=write(pipe_fd[1], w_buf, 4096);
        close(pipe_fd[1]);
    }
}

输出结果:

向管道写端写入1024字节

子进程读取了1000字节
子进程读取了1000字节
子进程读取了1000字节
子进程读取了1000字节
子进程读取了120字节
子进程读取了0字节

.........

结论:

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

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

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

4 管道应用实例:

shell命令的重定向:

管道可用于输入输出重定向,它将一个命令的输出直接定向到另一个命令的输入。

比如,当在某个shell程序(Bourne shell或C shell等)键入who│wc -l后,相应shell程序将创建who以及wc两个进程和这两个进程间的管道。考虑下面的命令行:

$kill -l 运行结果见 附一。

$kill -l | grep SIGRTMIN 运行结果如下:

30) SIGPWR 31) SIGSYS 32) SIGRTMIN 33) SIGRTMIN+1

34) SIGRTMIN+2 35) SIGRTMIN+3 36) SIGRTMIN+4 37) SIGRTMIN+5
38) SIGRTMIN+6 39) SIGRTMIN+7 40) SIGRTMIN+8 41) SIGRTMIN+9
42) SIGRTMIN+10 43) SIGRTMIN+11 44) SIGRTMIN+12 45) SIGRTMIN+13

46) SIGRTMIN+14 47) SIGRTMIN+15 48) SIGRTMAX-15 49) SIGRTMAX-14

5  管道的局限性

管道的主要局限性正体现在它的特点上:

1> 只支持单向数据流;

2> 只能用于具有亲缘关系的进程之间;

3> 没有名字;

4> 管道的缓冲区是有限的(管道制存在于内存中,在管道创建时,为缓冲区分配一个页面大小);

5> 管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式,比如多少字节算作一个消息(或命令、或记录)等等;

6 小结

管道常用于两个方面:

(1)在shell中时常会用到管道(作为输入输入的重定向),在这种应用方式下,管道的创建对于用户来说是透明的;

(2)用于具有亲缘关系的进程间通信,用户自己创建管道,并完成读写操作。

附1:

kill -l 的运行结果,显示了当前系统支持的所有信号:

1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN
22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO
30) SIGPWR 31) SIGSYS 32) SIGRTMIN 33) SIGRTMIN+1
34) SIGRTMIN+2 35) SIGRTMIN+3 36) SIGRTMIN+4 37) SIGRTMIN+5
38) SIGRTMIN+6 39) SIGRTMIN+7 40) SIGRTMIN+8 41) SIGRTMIN+9
42) SIGRTMIN+10 43) SIGRTMIN+11 44) SIGRTMIN+12 45) SIGRTMIN+13
46) SIGRTMIN+14 47) SIGRTMIN+15 48) SIGRTMAX-15 49) SIGRTMAX-14
50) SIGRTMAX-13 51) SIGRTMAX-12 52) SIGRTMAX-11 53) SIGRTMAX-10
54) SIGRTMAX-9 55) SIGRTMAX-8 56) SIGRTMAX-7 57) SIGRTMAX-6
58) SIGRTMAX-5 59) SIGRTMAX-4 60) SIGRTMAX-3 61) SIGRTMAX-2

62) SIGRTMAX-1 63) SIGRTMAX 

【上篇】
【下篇】

抱歉!评论已关闭.