Linux进程通信之管道和FIFO

  • Post author:
  • Post category:linux


Linux

进程间的通信可以简称为


IPC





InterprocessCommunication


),前面说过的


Linux


的同步工具也是属于


IPC


的一部分,这里我想说的是通常意义的进程间的实际数据通。

1

管道

管道是最早的

UNIXIPC


,所有的


UNIX


系统都支持这个


IPC


通信机制。我们最常见到使用它的位置就是


shell


中使用的管道命令。管道


IPC

有两个特性:



  • 管道仅提供半双工的数据通信,即只支持单向的数据流




  • 管道只能在有亲缘关系的进程间使用


    。这是由于管道没有名字的原因,所以不能跨进程的地址空间进行使用。

    这里这句话不是绝对的

    ,因为从技术上可以在进程间传递管道的描述符,所以是可以通过管道实现无亲缘进程间的通信的。但尽管如此,管道还是通常用于具有共同祖先的进程间的通信。

管道的接口定义如下:

#include <unistd.h>
int pipe(int filedes[2]);
                       //成功返回0,失败返回-1

pipe

函数用来创建一个管道,


fd


是传出参数,用于保存返回的两个文件描述符,该文件描述符用于标识管道的两端,


fd[0]


只能由于读,


fd[1]


只能用于写。

那么如果我们往

fd[0]


端写数据会是什么样的结果呢


?


下面是测试代码:

#include <iostream>
#include <cstring>

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

using namespace std;

int main()
{
    int fd[2];

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

    char *temp = "yuki";

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

    return 0;
}

代码的执行结果如下:

 write pipe failed:Bad file descriptor

从这个结果可以看出,内核对于管道的

fd[0]


描述符打开的方式是以只读方式打开的,那么同理


fd[1]


是以只写方式打开的,所以管道只能保证单向的数据通信。

下图

1


显示的是一个进程内的管道的模样:






1


单个进程内管道的模样

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

fork


产生一个子进程,由于子进程拥有父进程的副本,所以父子进程可以通过管道进程通信。这种使用方式如下图


2


所示:




2


父子进程间的管道

如上图所示,当父进程通过

fork


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

1.1

父子进程间的单向通信

上面说了父进程通过

fork


创建子进程后,父子进程间可以通过管道通信,数据流的方向根据具体的应用决定。我们都知道在


shell


中,管道的数据流向都是从父进程流向子进程,即父进程关闭读端,子进程关闭写端。如下图


3


所示:






3


父子进程间的单向管道

上图的测试代码如下:

#include <iostream>

#include <unistd.h>

using namespace std;

int main()
{
    int fd[2];

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

    char buf[256];

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

        read(fd[0], buf, sizeof(buf));
        cout<<"receive message from pipe:"<<buf<<endl;

        exit(0);
    }

    close(fd[0]);

    char *temp = "I have liked yuki...";
    write(fd[1], temp, strlen(temp) + 1);

    return 0;
}

代码的执行结果如下:

receive message from pipe:I have liked yuki...

其中代码流程是,子进程等待父进程通过管道发送过来的数据,然后输出接收到的数据,代码中的

read


会阻塞到管道中有数据为止,具体管道的


read





write


的规则将会在后面介绍。

1.2

父子进程间的双向通信

由上我们知道,一个管道只能支持亲缘进程间的单向通信即半双工通信。如果要想通过管道来支持双向通信呢,那这里就需要创建两个管道,

fd1





fd2


;父进程中关闭


fd1[0]





fd2[1]


,子进程中关闭


fd1[1]





fd2[0]


。这种通信模式如下图所示:






4


父子进程间的双向通信

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

#include <iostream>

#include <unistd.h>

using namespace std;

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

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

    char buf[256];
    char *temp = "I have liked yuki...";

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

        read(fd1[0], buf, sizeof(buf));
        cout<<"child:receive message from pipe 1:"<<buf<<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));
    cout<<"parent:receive message from pipe 2:"<<buf<<endl;

    return 0;
}

代码的执行结果如下:

child:receive message from pipe 1:I have liked yuki...
parent:receive message from pipe 2:I have liked yuki...

其中代码的流程是父进程创建了两个管道,我们可以用

fd1





fd2


表示,管道


fd1


负责父进程向子进程发送数据,


fd2


负责子进程想父进程发送数据。进程启动后,子进程等待父进程通过管道


fd1


发送数据,当子进程收到父进程的数据后,输出消息,并通过管道


fd2


回复父进程,然后子进程退出,父进程收到子进程的响应后,输出消息并退出。

前面已经说了对管道的

read


会阻塞到管道中有数据为止,具体管道的


read





write


的规则将会在后面介绍。

1.3popen




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

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


command

:该传入参数是一个

shell


命令行,这个命令是通过


shell


处理的。


type

:该参数决定调用进程对要执行的

command


的处理,


type


有如下两种情况:

  • type=“r”,调用进程将读取

    command


    执行后的标准输出,该标准输出通过返回的


    FILE*


    来操作;
  • type=“w”,调用进程将写

    command


    执行过程中的标准输入;

pclose

函数会关闭由


popen


创建的标准


I/O


流,等待其中的命令终止,然后返回


shell


的执行状态。

下面是关于

popen


的测试代码:

#include <iostream>
#include <cstdio>

#include <unistd.h>

using namespace std;

int main()
{
    char *cmd = "ls /usr/local/bin ";

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

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

    pclose(p);

    return 0;
}

程序的执行结果如下所示:

ccmake
cmake
cpack
CSGMP_CG_Server
CSGMP_Start.sh
ctest
...

程序的执行流程如下:调用进程执行

popen


时,会创建一个管道,然后


fork


生成一个子进程,子进程执行


popen


传入的

“ls/usr/local/bin”shell

命令,子进程将执行结果通过管道传递给调用进程,调用进程通过标准文件


I/O


来读取管道中的数据


,


并输出显示。

2FIFO

POSIX

标准中的


FIFO


又名



有名管道或命名管道


。我们知道前面讲述的

POSIX


标准中管道是没有名称的,所以它的最大劣势是只能用于具有亲缘关系的进程间的通信。


FIFO


最大的特性就是每个


FIFO


都有一个路径名与之相关联,从而允许无亲缘关系的任意两个进程间通过


FIFO


进行通信。

所以,

FIFO

的两个特性:



  • 和管道一样,

    FIFO


    仅提供半双工的数据通信,即只支持单向的数据流





  • 和管道不同的是,

    FIFO


    可以支持任意两个进程间的通信



下面是

FIFO


的接口定义:

#include <sys/types.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
                         //成功则返回0,失败返回-1


pathname

:一个

Linux


路径名,它是


FIFO


的名字。即每个


FIFO


与一个路径名相对应。


mode

:指定的文件权限位,类似于

open


函数的第三个参数。即创建该


FIFO


时,指定用户的访问权限,有以下值:


S_IRUSR





S_IWUSR





S_IRGRP





S_IWGRP





S_IROTH





S_IWOTH



mkfifo

函数默认指定


O_CREAT|O_EXECL


方式创建


FIFO


,如果创建成功,直接返回


0






如果

FIFO


已经存在,则创建失败,会返回


-1


并且


errno


置为


EEXIST



。对于其他错误,则置响应的

errno


值;

当创建一个

FIFO


后,它必须以只读方式打开或者只写方式打开,所以可以用


open


函数,当然也可以使用标准的文件


I/O


打开函数,例如


fopen


来打开。由于


FIFO


是半双工的,所以不能够同时打开来读和写。

其实一般的文件

I/O


函数,如


read





write





close





unlink


都可用于


FIFO


。对于管道和


FIFO





write


操作总是会向末尾添加数据,而对他们的


read


则总是会从开头数据,所以不能对管道和


FIFO


中间的数据进行操作,因此对管道和


FIFO


使用


lseek


函数,是错误的,会返回


ESPIPE


错误。

mkfifo





一般使用方式


是:通过

mkfifo


创建


FIFO


,然后调用


open


,以读或者写的方式之一打开


FIFO


,然后进行数据通信。

下面是

FIFO


的一个简单的测试代码:

#include <iostream>

#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

#include <sys/stat.h>
#include <sys/types.h>

using namespace std;

#define FIFO_PATH "/home/anonym/fifo"

int main()
{
    if (mkfifo(FIFO_PATH, 0666) < 0 && errno != EEXIST)
    {
        cout<<"create fifo failed..."<<endl;
        return -1;
    }

    if (fork() == 0)
    {

        int readfd = open(FIFO_PATH, O_RDONLY);
        cout<<"child open fifo success..."<<endl;

        char buf[256];

        read(readfd, buf, sizeof(buf));
        cout<<"receive message from pipe:"<<buf<<endl;

        close(readfd);

        exit(0);
    }

    sleep(3);
    int writefd = open(FIFO_PATH, O_WRONLY);
    cout<<"parent open fifo success..."<<endl;

    char *temp = "i love you";
    write(writefd, temp, strlen(temp) + 1);

    close(writefd);
}

程序的执行结果如下:

parent open fifo success...
child open fifo success...
receive message from pipe:i love you

由上面的运行结果可以看到,子进程以读方式

open


的操作会阻塞到父进程以写方式


open


;关于这一点以及


read





write


的操作会在后面管道和


FIFO


的属性部分进行介绍;

POSIX

标准不仅规定了对


mkfifoIPC


的支持,还包括了对


mkfifoshell


命令的支持,所以符合


POSIX


标准的


UNIX


中都含有


mkfifo


命令来创建有名管道,例如下面是在


Linux

2.6.18的测试:

[root@idcserver program]# mkfifo skywalker
[root@idcserver program]# echo "I have liked yuki..." >skywalker &
[1] 28839
[root@idcserver program]# cat < skywalker
I have liked yuki...
[1]+  Done                    echo "I have liked yuki..." > skywalker

这里在第二行最后加上‘

&


’使进程转到后台运行,是因为


FIFO


以只写方式打开需要阻塞到


FIFO


以只读方式打开为止,所以必须要作为后台程序运行,否则进程会阻塞在前端,无法再进行相关输入;

1.3

管道和


FIFO


的属性

由于在

POSIX


标准中,管道和


FIFO


都是通过文件描述符来进行操作的,默认的情况下,对他们的操作都是阻塞的,当然也可以通过设置来使对他们的操作变成非阻塞的。我们都知道可以有两种方式来设置一个文件描述符为


O_NONBLOCK


非阻塞的:

  • 调用

    open


    时,指定


    O_NONBLOCK


    标志。例如:


int fd = open(FILE_NAME, O_RDONLY | O_NONBLOCK);

  • 通过

    fcntl


    文件描述符控制操作函数,对一个已经打开的描述符启用


    O_NONBLOCK


    标志。其中对于管道必须使用这种方式。示例如下:
int flag;
flag = fcntl(fd, F_GETFL, 0);

flag |= O_NONBLOCK;
fcntl(fd, F_SETFL, flag);

下图主要说明了对管道和

FIFO


的各种操作在阻塞和非阻塞状态下的不同,这张图对对于理解和使用管道和


FIFO


是非常重要的。






5


管道和


FIFO


的各种操作

从上图我们看到关于管道和

FIFO


的读出和写入的若干规则,主要需要注意的有以下几点:

  • 以只读方式

    openFIFO


    时,如果


    FIFO


    还没有以只写方式


    open


    ,那么在阻塞模式下,该操作会阻塞到


    FIFO


    以只写方式


    open


    为止。
  • 以只写方式

    openFIFO


    时,如果


    FIFO


    还没有以只读方式


    open


    ,那么在阻塞模式下,该操作会阻塞到


    FIFO


    以只读方式


    open


    为止。
  • 从空管道或空

    FIFO





    read


    ,如果管道和


    FIFO


    已打开来写,在阻塞模式下,那么该操作会阻塞到管道或


    FIFO


    有数据为止,或管道或


    FIFO


    不再以写方式打开。如果管道和


    FIFO


    没有打开来写,那么该操作会返回


    0



  • 向管道或

    FIFO





    write


    ,如果管道或


    FIFO


    没有打开来读,那么内核会产生


    SIGPIPE


    信号,默认情况下,该信号会终止该进程。

另外对于管道和

FIFO


还需要说明的若干规则如下:

  • 如果请求

    write


    的数据的字节数小于等于


    PIPE_BUF





    POSIX


    关于管道和


    FIFO


    大小的限制值),那么


    write


    操作可以保证是原子的,如果大于


    PIPE_BUF


    ,那么就不能保证了。

那么由此可知

write


的原子性是由写入数据的字节数是否小于等于


PIPE_BUF


决定的,和是不是


O_NONBLOCK


没有关系。

下面是在阻塞和非阻塞情况下,

write


不同大小的数据的操作结果:




阻塞


的情况下:

  • 如果

    write


    的字节数小于等于


    PIPE_BUF


    ,那么


    write


    会阻塞到写入所有数据,并且写入操作是原子的。
  • 如果

    write


    的字节数大于


    PIPE_BUF


    ,那么


    write


    会阻塞到写入所有数据,但写入操作不是原子的,即


    write


    会根据当前缓冲区剩余的大小,写入相应的字节数,然后等待下一次有空余的缓冲区,这中间可能会有其他进程进行


    write


    操作。




非阻塞


的情况下:

  • 如果

    write


    的字节数小于等于


    PIPE_BUF


    ,且管道或


    FIFO


    有足以存放要写入数据大小的空间,那么就写入所有数据;
  • 如果

    write


    的字节数小于等于


    PIPE_BUF


    ,且管道或


    FIFO


    没有足够存放要写入数据大小的空间,那么就会立即返回


    EAGAIN


    错误。
  • 如果

    write


    的字节数大于


    PIPE_BUF


    ,且管道或


    FIFO


    有至少


    1B


    的空间,那么就内核就会写入相应的字节数,然后返回已写入的字节数;
  • 如果

    write


    的字节数大于


    PIPE_BUF


    ,且管道或


    FIFO


    无任何的空间,那么就会立即返回


    EAGAIN


    错误。

1.4

管道和


FIFO


的限制

系统内核对于管道和

FIFO


的唯一限制为:


OPEN_MAX





PIPE_BUF;

OPEN_MAX

:一个进程在任意时刻可以打开的最大描述符数。


PIPE_BUF


标识一个管道可以原子写入管道和


FIFO


的最大字节数,并不是管道或


FIFO


的容量。

关于这两个系统限制,

POSIX


标准中都有定义的不变最小值:


POSIX_OPEN_MAX





_POSIX_PIPE_BUF


,这两个宏是


POSXI


标准定义的编译时确定的值,他们是标准定义的且不会改变的,


POSIX


标准关于这两个值的限制为:

cout<<_POSIX_OPEN_MAX<<endl;
cout<<_POSIX_PIPE_BUF<<endl;

//运行结果为:
20
512

我们都知道,关于

POSIX


的每个不变最小值都有一个具体的系统的实现值,这些是实现值由具体的系统决定,通过调用以下函数在运行时确定这个实现值:

#include <unistd.h>

long sysconf(int name);
long fpathconf(int filedes, int name);
long pathconf(char *path, int name);

                           //成功返回具体的值,失败返回-1

其中

sysconf


是用于返回系统限制值,这些值是以


_SC_


开头的常量,

pathconf和fpathconf是用于返回与文件和目录相关的运行时的限制值,这些值都是以_PC_

开头的常量;下面是在


Linux

2.6.18下的测试代码:

cout<<sysconf(_SC_OPEN_MAX)<<endl;
cout<<pathconf(FIFO_PATH, _PC_PATH_MAX)<<endl;

//运行结果为:
1024
4096

当然上面两个系统限制值的具体实现值也可以通过

ulimit


命令来查看,下面是在


Linux

2.6.18下查看的结果:

[root@idcserver program]# ulimit -a
...
open files                    (-n) 1024
pipe size            (512 bytes, -p) 8
...

这两个值在

LinuxLinux

2.6.18下都是不允许修改的,也是没有必要修改的;

Jul 20, 2013 16:52 @library

机会永远都是留给有准备的人。。。