`
jishublog
  • 浏览: 868953 次
文章分类
社区版块
存档分类
最新评论

Linux进程通信之管道和FIFO

 
阅读更多

Linux进程间的通信可以简称为IPCInterprocessCommunication),前面说过的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会阻塞到管道中有数据为止,具体管道的readwrite的规则将会在后面介绍。

1.2父子进程间的双向通信

由上我们知道,一个管道只能支持亲缘进程间的单向通信即半双工通信。如果要想通过管道来支持双向通信呢,那这里就需要创建两个管道,fd1fd2;父进程中关闭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...

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

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

1.3popenpclose函数

作为关于管道的一个实例,就是标准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_IRUSRS_IWUSRS_IRGRPS_IWGRPS_IROTHS_IWOTH

mkfifo函数默认指定O_CREAT|O_EXECL方式创建FIFO,如果创建成功,直接返回0如果FIFO已经存在,则创建失败,会返回-1并且errno置为EEXIST。对于其他错误,则置响应的errno值;

当创建一个FIFO后,它必须以只读方式打开或者只写方式打开,所以可以用open函数,当然也可以使用标准的文件I/O打开函数,例如fopen来打开。由于FIFO是半双工的,所以不能够同时打开来读和写。

其实一般的文件I/O函数,如readwritecloseunlink都可用于FIFO。对于管道和FIFOwrite操作总是会向末尾添加数据,而对他们的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;关于这一点以及readwrite的操作会在后面管道和FIFO的属性部分进行介绍;

POSIX标准不仅规定了对mkfifoIPC的支持,还包括了对mkfifoshell命令的支持,所以符合POSIX标准的UNIX中都含有mkfifo命令来创建有名管道,例如下面是在Linux2.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为止。
  • 从空管道或空FIFOread,如果管道和FIFO已打开来写,在阻塞模式下,那么该操作会阻塞到管道或FIFO有数据为止,或管道或FIFO不再以写方式打开。如果管道和FIFO没有打开来写,那么该操作会返回0
  • 向管道或FIFOwrite,如果管道或FIFO没有打开来读,那么内核会产生SIGPIPE信号,默认情况下,该信号会终止该进程。

另外对于管道和FIFO还需要说明的若干规则如下:

  • 如果请求write的数据的字节数小于等于PIPE_BUFPOSIX关于管道和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_MAXPIPE_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_开头的常量;下面是在Linux2.6.18下的测试代码:

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

//运行结果为:
1024
4096

当然上面两个系统限制值的具体实现值也可以通过ulimit命令来查看,下面是在Linux2.6.18下查看的结果:

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

这两个值在LinuxLinux2.6.18下都是不允许修改的,也是没有必要修改的;


Jul 20, 2013 16:52 @library

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

分享到:
评论

相关推荐

    linux进程间通信 教程

    IPC(InterProcess Communication)是各种进程通信方式的统称,主要有下面几种类型:  管道  FIFO(命名管道)  消息队列  信号量  共享空间  套接口 前五种IPC只能用于一台主机内的进程间通信,套接口...

    命名管道(FIFO)示例代码

    Linux系统编程——进程间通信:命名管道(FIFO),相关教程链接如下: http://blog.csdn.net/tennysonsky/article/details/46326957

    进程通信.doc

    创建一个管道之后,一般情况下进程将产生一个新的进程。 系统调用:pipe(); 原型:int pipe(int fd[2]); 返回值:如果系统调用成功,返回0。如果系统调用失败返回-1: errno=EMFILE&#40;没有空亲的文件描述符) ...

    linux管道通信例程

    (1) master 和 client 分别创建一个子进程; (2) client 完成创建子进程后,其子进程等待用户从键盘输入一串字符串,并将字符串通 过 clinet 发送到 master; (3) master 完成创建子进程后,等待接收 client 发送...

    Linux 进程通信之FIFO的实现

    FIFO 有名管道,实现无血缘关系进程通信。 创建一个管道的伪文件 a.mkfifo testfifo 命令创建 b.也可以使用函数int mkfifo(const char *pathname, mode_t mode); 内核会针对fifo文件开辟一个缓冲区,操作fifo...

    嵌入式系统/ARM技术中的linux下的进程通信

     linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的。而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。...

    Linux环境C语言调用Mplayer实现音乐播放器(其中缺少了一个FIFO管道文件)

    C语言字符串处理、Linux系统编程、多进程、多线程、进程间通信、线程同步、实现的功能有暂停、快进、快退、显示歌词、单曲循环、列表循环、随机播放、声音调节等

    深刻理解Linux系统进程间通信

    Linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的,最初Unix IPC包括:管道、FIFO、信号;System V IPC包括:System V消息队列、System V信号灯、System V共享内存区;Posix IPC包括: Posix消息...

    Linux进程通信(IPC)方式简介

    linux下进程间通信的几种主要方式:管道(pipe)和有名管道(FIFO)、信号(signal)、消息队列、共享内存(shared memory)、信号量(semaphore)、套接字(socket),本文对这些做简单介绍

    linux 下socket通信中select的用法实例

    第一,struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以...

    基于linux的进程通信设计方案

    linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的。而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。...

    嵌入式系统/ARM技术中的基于linux的进程通信设计方案

    linux下的进程通信手段基本上是从Unix平台上的进程通信手段继承而来的。而对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。...

    嵌入式系统/ARM技术中的有名管道(FIFO)的用法

     有名管道又称为FIFO,是进程间通信的一种方式。FIFO具有以下特点:  1.全双工的通信模式,数据先进先出;  2.可以用于任意的进程之间,通过指定相同的管道文件进行通信;  3.文件名存在文件系统中,而管道中...

    linux 命名管道实例详解

    linux进程间通信——命名管道  FIFO(命名管道)不同于匿名管道之处在于它提供⼀个路径名与之关联,以FIFO的⽂件形式存储于⽂件系统中。命名管道是⼀个设备⽂件,因此,即使进程与创建FIFO的进程不存在亲缘关系,...

    interprocess:用于Rust的多功能跨平台进程间通信工具包

    特征实现了以下进程间通信原语: 未命名管道-匿名文件状对象,用于在一个方向上私下通信,最常用于子进程与其父进程之间的通信FIFO文件-特定于Unix的文件类型,类似于未命名管道,但存在于文件系统上,通常称为...

    linux-ipcs:Linux进程间通信(Inter-Process Communication)方式汇总

    Linux进程间通信方式汇总 目前已包含的方式 管道(PIPE) FIFO(有名管道) XSI消息队列 XSI信号量 XSI共享内存 POSIX信号量 域套接字(Domain Socket) 信号(Signal) 互斥量(Mutex) 其中信号(signal)和信号量(semaphore)...

    UNIX网络编程 卷2 进程间通信 带完整书签,完整目录

    4.7 管道和FIFO的额外属性 44 4.8 单个服务器,多个客户 46 4.9 对比迭代服务器与并发服务器 50 4.10 字节流与消息 51 4.11 管道和FIFO限制 55 4.12 小结 56 习题 57 第5章 Posix消息队列 58 5.1 概述 58 ...

    UNIX网络编程 第2卷 进程间通信 pdf

    本书从对Posix IPC和System V IPC的内部结构的综合讨论开始,具体阐述并比较了四种IPC形式:消息传递(管道、FIFO、消息队列)、同步(互斥锁、条件变量、读写锁、文件与记录锁、信号灯)、共享内存区(匿名共享内存区;...

    进程间通信

    进程间通信.涵盖信号量/共享内存段/管道/FIFO/消息队列等内容。共十页

    fifo.zip_Linux/Unix编程

    linux下的管道编程,属于进程间通信的一种机制, 可作为参考

Global site tag (gtag.js) - Google Analytics