您的位置:首页 > 编程语言

《unix高级环境编程》进程间通信——管道和FIFO

2014-11-16 20:12 141 查看
管道是早期 UNIX 系统的 IPC 机制,并且所有的 UNIX 系统都提供了管道通信机制。管道的一个显著性特点就是:当一个管道建立后,将获的两个文件描述符,分别用于对管道读取和写入,通常将其称为管道的写入端和管道的读取端,从写入端写入管道的任何数据都可以从读取端读取。对于一个进程来说,管道的写入和读取操作与写入和读取一个普通文件没有区别,只是在内核中通过这种机制来实现进程间通信。

管道 IPC 有以下两个特性:

管道只提供半双工数据通信方式,即只允许单方向传输数据;
管道只能在具有亲缘关系的进程间通信(只能用于两个进程间通信,而不能用于多个进程),由于管道没有名字,所以不能跨进程的地址空间进行使用;

管道的操作

创建一个管道的系统调用函数如下:

/* 管道 */
/*
 * 函数功能:创建一个管道;
 * 返回值:若成功则返回0,若出错则返回-1;
 * 函数原型:
 */
#include <unistd.h>
int pipe(int filedes[2]);
/*
 * 说明:
 * 该函数的参数是一个二元整数数组,用于存放调用该函数所创建管道的两个文件描述符;
 * filedes[0]为读而打开,存放管道的读取端的文件描述符;
 * filedes[1]为写而打开,存放管道的写入端的文件描述符;
 * filedes[1]的输出是filedes[0]的输入;
 * 内核对于管道的filedes[0]以只读方式打开的,filedes[1]是以只写方式打开的,所以管道只能保证单向的数据通信;
 */
调用 pipe 函数创建一个管道后,还不能实现通过管道在两个进程间通信,因为此时管道的读取端和写入端的文件描述符同属于一个进程。通常是在调用 pipe 函数完成后,调用 fork 函数创建一个子进程,需要时调用 exec 函数族使子进程执行所需程序。然后根据数据传输的方向分别关闭父进程和子进程中的一个文件描述符。例如:要实现父进程向子进程的数据传输,则需要关闭父进程的读取端文件描述符和子进程的写入端文件描述符。下面是进程之间的管道关系:





父进程经过管道向子进程传输数据:

父进程调用 pipe 创建管道,得到两个文件描述符指向管道的两端。
父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信

下面的程序是父进程向子进程传输数据:

#include "apue.h"

int main(void)
{
    int fd[2];
    int n;
    pid_t pid;
    char *str = "the data form parent process.\n";
    char buf[MAXLINE];
    if(pipe(fd) == -1)
        err_quit("pipe error");
    if((pid = fork()) < 0)
        err_quit("fork error");
    else if(pid == 0)
    {
        close(fd[1]);
        n = read(fd[0], buf, sizeof(buf));
        printf("Recevied the messages:\n");
        write(STDOUT_FILENO, buf, n);
    }
    else
    {
        close(fd[0]);
        write(fd[1], str, strlen(str)+1);
    }
    exit(0);
}
输出结果:上面的例子是创建一个从父进程到子进程的管道,并且父进程经由管道向子进程传输数据。

$ ./pipe 
Recevied the messages:
the data form parent process.

下面使用两个管道实现父进程与子进程的同步通信:

#include "apue.h"

int main(void)
{
    int fd1[2], fd2[2];
    int n;
    pid_t pid;
    char *str = "the data form parent process.\n";
    char *dtr = "the data form child process.\n";
    char buf[MAXLINE];
    if(pipe(fd1) == -1)
        err_quit("pipe error");
    if(pipe(fd2) == -1)
        err_quit("pipe error");
    if((pid = fork()) < 0)
        err_quit("fork error");
    else if(pid == 0)
    {
        close(fd2[0]);
        close(fd1[1]);
        n = read(fd1[0], buf, sizeof(buf));
        printf("In child, Recevied the messages:\n");
        write(STDOUT_FILENO, buf, n);

        write(fd2[1], dtr, strlen(dtr)+1);
    }
    else
    {
        close(fd2[1]);
        close(fd1[0]);
        write(fd1[1], str, strlen(str)+1);
        n = read(fd2[0], buf, sizeof(buf));
        printf("In parent, Recevied the messages:\n");
        write(STDOUT_FILENO, buf, n);
    }
    exit(0);
}
输出结果:

./pipe 
In child, Recevied the messages:
the data form parent process.
In parent, Recevied the messages:
the data form child process.
上面例子是父进程创建了两个管道,用 fd1,fd2 表示,管道 fd1 负责父进程向子进程发送数据,fd2 负责子进程向父进程发送数据。进程启动后,子进程等待父进程通过管道fd1 发送数据,当子进程收到父进程的数据后,输出消息,并通过管道 fd2 回复父进程,父进程收到子进程的响应后,输出消息并退出。

popen 和 pclose 函数

常见的操作是创建一个管道链接到另一个进程,然后读其输出或向其输入端发送数据。标准 I/O 库提供了两个函数 popen 和 pclose 函数,这两个函数实现的操作是:创建一个管道,调用 fork 创建一个子进程,关闭管道的不使用端,执行一个 shell 以运行命令,然后等待终止。

/* popen 和 pclose 函数 */
/*
 * 函数功能:创建一个管道链接到另一个进程,实现管道数据传输;
 * 函数原型:
 */
#include <stdio.h>
FILE *popen(const char *cmdstring, const char *type);//返回值:若成功则返回文件指针,若出错则返回NULL;

int pclose(FILE *fp);//返回值:cmdstring的终止状态,若出错则返回-1;
/*
 * 说明:
 * cmdstring是要执行的 shell 命令;
 * type参数有如下取值:
 * (1)type = "r"  文件指针连接到cmdstring标准输出;
 * (2)type = "w"  文件指针连接到cmdstring标准输入;
测试程序:

#include "apue.h"

int main(void)
{
    char *cmd = "ls ./";

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

    while (fgets(buf, 256, p) != NULL)
    {
        if(fputs(buf, stdout) == EOF)
            err_sys("fputs error");
    }

    pclose(p);

    exit(0);
}
输出结果:

./pipe 
imgs
pipe
pipe2.c
pipe3.c
pipe.c
pipe.h
上面例子是调用进程执行 popen 时,会创建一个管道,然后 fork 生成一个子进程,子进程执行 popen 传入的"ls ./" shell 命令,子进程将执行结果通过管道传递给调用进程,调用进程通过标准文件 I/O 来读取管道中的数据,并输出显示。

FIFO(命名管道)

前面介绍的管道有局限性,只能在具有亲缘关系的进程间通信,而 FIFO (命名管道)克服了管道没有名字的限制。因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。FIFO 最大的特性就是每个 FIFO 都有一个路径名与之相关联,从而允许无亲缘关系的任意两个进程间通过 FIFO 进行通信。

/* FIFO 命名管道 */
/*
 * 函数功能:功能和管道类似;
 * 返回值:若成功则返回0,若出错则返回-1;
 * 函数原型:
 */
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);
/*
 * 说明:
 * 参数mode和open函数的mode参数一样;
 * pathname:一个 Linux 路径名,它是 FIFO 的名字。即每个 FIFO 与一个路径名相对应。
 * mode:指定的文件权限位,类似于 open 函数的第三个参数。
 * 即创建该 FIFO 时,指定用户的访问权限,可以取以下值:
 * S_IRUSR,S_IWUSR,S_IRGRP,S_IWGRP,S_IROTH,S_IWOTH。
 * 该函数创建一个FIFO之后,就可用open函数打开;
 */


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

当创建一个 FIFO 后,它必须以只读方式或只写方式打开,所以可以用 open 函数,当然也可以使用标准的文件 I/O 打开函数。由于 FIFO 是半双工的,所以不能够同时以读、写方式打开。一般的文件I/O函数,如 read,write,close,unlink 都可用于 FIFO。对于管道和 FIFO 的 write 操作总是会向末尾添加数据,而对他们的 read 则总是会从开头数据,所以不能对管道和 FIFO 中间的数据进行操作,因此对管道和 FIFO 使用 lseek 函数,是错误的,会返回
ESPIPE 错误。

测试程序:

#include "apue.h"
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>

#define FIFO_PATH "./Fifo"

int main(void)
{
    int fd;
    pid_t pid;
    char buf[MAXLINE];
    char str[] = "hello FIFO, doing..\n";

    if(mkfifo(FIFO_PATH, 0666) < 0 && errno != EEXIST)
        err_quit("mkfifo error");
    if((pid = fork()) == 0)
    {
        fd = open(FIFO_PATH, O_RDONLY);
        read(fd, buf, MAXLINE);
        printf("the buf is: %s", buf);

        close(fd);
        exit(0);
    }
    sleep(2);
    fd = open(FIFO_PATH, O_WRONLY);

    write(fd, str, strlen(str)+1);
    close(fd);

    waitpid(pid, NULL, 0);
    exit(0);
}
输出结果:

./pipe 
the buf is: hello FIFO, doing..

参考资料:

《UNIX高级环境编程》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: