进程间通信机制之四:管道

管道是UNIX系统IPC的最古老形式,并且所有UNIX系统都提供此种通信机制。管道有下面两种局限性:

(1)历史上,它们是半双工的。现在,某些系统提供全双工管道,但是为了最佳的可移植性,我们决不应预先假定系统使用此特性。

(2)它们只能在具有公共祖先的进程之间使用。通常,一个管道由一个进程创建,然后该进程调用fork,此后父、子进程之间就可应用该管道。

稍后将会看到FIFO没有第二种局限性,UNIX域套接字和命名流管道则没有这两种局限性。

尽管有这两种局限性,半双工管道仍是最常见的IPC形式。每当你在管道线中键入一个由shell执行的命令序列时,shell为每一条命名单独创建一进程,然后将前一条命令进程的标准输出用管道与后一条命令的标准输入相连接。对于shell命令来说,命令的连接是通过管道字符来完成的,例如:

cmd1 | cmd2

shell负责安排两个命令的标准输入和标准输出。

  • cmd1的标准输入来自终端键盘。
  • cmd1的标准输出传递给cmd2,作为它的标准输入。
  • cmd2的标准输出连接到终端屏幕。

shell所作的工作实际上是对标准输入和标准输出流进行了重新连接,使数据流从键盘输入通过两个命令最终输出到屏幕上。

一.进程管道

可能最简单的在两个程序之间传递数据的方法就是使用popen和pclose函数了,原型如下:

#include <stdio.h>

FILE *popen(const char *command, const char *open_mode);
int pclose(FILE *stream_to_close);

1.popen函数

该函数允许一个程序将另一个程序作为新进程来启动,并可以传递数据给它或者通过它接收数据。command字符串是要运行的程序名和相应的参数。open_mode必须是"r"或者"w"。

如果open_mode是"r",被调用程序的输出就可以被调用程序使用,调用程序利用popen函数返回的FILE*文件流指针,就可以通过常用的stdio库函数(如fread)来读取被调用程序的输出。如果open_mode是"w",调用程序就可以用fwrite调用向被调用程序发送数据,而被调用程序可以在自己的标准输入上读取这些数据。被调用的程序通常不会意识到自己正在从另一个进程读取数据,它只是在标准输入流上读取数据,然后做出相应的操作。

每个popen调用都必须指定"r"或者"w",在popen函数的标准实现中不支持任何其他选项。这意味着我们不能调用另一个程序并同时对它进行读写操作。popen函数在失败时返回一个空指针。如果想通过管道实现双向通信,最普通的解决方法的是使用两个管道,每个管道负责一个方向的数据流。


2.pclose函数

用popen启动的进程结束时,我们可以用pclose函数关闭与之关联的文件流。pclose调用只在popen启动的进程结束后才返回。如果调用pclose时它仍在运行,pclose调用将等待该进程的结束。

pclose调用的返回值通常是它所关闭的文件流所在进程的退出码。如果调用进程在调用pclose之前执行了一个wait语句,被调用进程的退出状态就会丢失,因为被调用进程已结束。此时,pclose将返回-1并设置error为ECHILD。

举例1:

在程序中用popen访问uname命令给出的信息。命令uname -a的作用是打印系统信息,包括计算机型号、操作系统名称、版本和发行号,以及计算机的网络名。

完成程序的初始化工作后,打开一个连接到uname命令的管道,把管道设置为可读方式并让read_fp指向该命令的输出。最后关闭read_fp指向的管道。

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main()
{
    FILE *read_fp;
    char buffer[BUFSIZ + 1];
    int chars_read;
    memset(buffer, '\0', sizeof(buffer));
    read_fp = popen("uname -a", "r");
    if (read_fp != NULL) {
        chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
        if (chars_read > 0) {
            printf("Output was:-\n%s\n", buffer);
        }
        pclose(read_fp);
        exit(EXIT_SUCCESS);
    }
    exit(EXIT_FAILURE);
}

举例2:

下面程序将输出发送到外部程序的示例程序,它将数据通过管道送往另一个程序。与上面程序不同的是,该程序将数据写入管道,而不是从管道中读取。

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main()
{
    FILE *write_fp;
    char buffer[BUFSIZ + 1];

    sprintf(buffer, "Once upon a time, there was...\n");
    write_fp = popen("od -c", "w");
    if (write_fp != NULL) {
        fwrite(buffer, sizeof(char), strlen(buffer), write_fp);
        pclose(write_fp);
        exit(EXIT_SUCCESS);
    }
    exit(EXIT_FAILURE);
}

举例3:

我们目前使用的机制都只是将所有数据通过一次fread或fwrite调用来发送或接收。有时,可能希望能以块方式发送数据,或者根本就不知道输出数据的长度。为了避免定义一个非常大的缓冲区,可以用多个fread或fwrite调用来将数据分为几部分处理。

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main()
{
    FILE *read_fp;
    char buffer[BUFSIZ + 1];
    int chars_read;

    memset(buffer, '\0', sizeof(buffer));
    read_fp = popen("ps -ax", "r");
    if (read_fp != NULL) {
        chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
        while (chars_read > 0) {
            buffer[chars_read - 1] = '\0';
            printf("Reading:-\n %s\n", buffer);
            chars_read = fread(buffer, sizeof(char), BUFSIZ, read_fp);
        }
        pclose(read_fp);
        exit(EXIT_SUCCESS);
    }
    exit(EXIT_FAILURE);
}

解析:

这个程序调用popen函数时使用了"r"参数,这与第一个程序的做法一样。这次,它连续从文件流中读取数据,直到没有数据可读为止。注意,虽然ps命令的执行要花费一些时间,但Linux会安排好进程间的调度,让两个程序在可以运行时继续运行。如果读进程popen3没有数据可读,它将被挂起直到有数据到达。如果写进程ps产生的输出超过了可用缓冲区的长度,它也会被挂起直到读进程读取了一些数据。

在程序中,可能不会看到Reading:-信息的第二次出现。如果BUFSIZ的值超过了ps命令输出的长度,这种情况就会发生。一些Linux系统将BUFSIZ设置为9102或更大的数字。为了测试程序在读取多个输出数据块时能够正常工作,可以尝试每次读取少于BUFSIZ个字符(比如BUFSIZ/10个字符)。


二、pipe调用

这个函数在两个程序之间传递数据不需要启动一个shell来解释请求的命令。它同时还提供对读写数据的更多控制。

#include <unistd.h>

int pipe(int file_descriptor[2]);
pipe函数的参数是一个由两个整数类型的文件描述符组成的数组的指针。该函数在数组中填上两个新的文件描述符后返回0,如果失败则返回-1并设置errno来表明失败的原因。Linux手册中定义了下面一些错误:

  • EMFILE:进程使用的文件描述符过多。
  • ENFILE:系统的文件表已满。
  • EFAULT:文件描述符无效。

两个返回的文件描述符以一种特殊的方式连接起来。写到file_descriptor[1]的所有数据都可以从file_descriptor[0]读回来。数据基于先进先出的原则(FIFO)进行处理,这意味着如果你把字节1,2,3写到file_descriptor[1],从file_descriptor[0]读取到的数据也会是1,2,3。这与栈的处理方式不同,栈采用后进先出的原则,简写为LIFO。

特别要注意,这里使用的是文件描述符而不是文件流,所以必须用底层的read和write调用来访问数据,而不是用文件流库函数fread和fwrite。

举例1:

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main()
{
    int data_processed;
    int file_pipes[2];
    const char some_data[] = "123";
    char buffer[BUFSIZ + 1];

    memset(buffer, '\0', sizeof(buffer));

    if (pipe(file_pipes) == 0) {
        data_processed = write(file_pipes[1], some_data, strlen(some_data));
        printf("Wrote %d bytes\n", data_processed);
        data_processed = read(file_pipes[0], buffer, BUFSIZ);
        printf("Read %d bytes: %s\n", data_processed, buffer);
        exit(EXIT_SUCCESS);
    }
    exit(EXIT_FAILURE);
}
说明:

该程序用数组file_pipes[]中的两个文件描述符创建一个管道。然后它用文件描述符file_pipes[1]向管道中写数据,再从file_pipes[0]读回数据。注意,管道有一些内置的缓冲区,它在write和read调用之间保存数据。

如果尝试用file_descriptor[0]写数据或用file_descriptor[1]读数据,其后果并未在文档中明确规定,所以其行为可能会非常奇怪,并且随着系统的不同,其行为可能会发生变化。

举例2:

跨越fork调用的管道

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main()
{
    int data_processed;
    int file_pipes[2];
    const char some_data[] = "123";
    char buffer[BUFSIZ + 1];
    pid_t fork_result;

    memset(buffer, '\0', sizeof(buffer));

    if (pipe(file_pipes) == 0) {
        fork_result = fork();
        if (fork_result == -1) {
            fprintf(stderr, "Fork failure");
            exit(EXIT_FAILURE);
        }

// We've made sure the fork worked, so if fork_result equals zero, we're in the child process.

        if (fork_result == 0) {
            data_processed = read(file_pipes[0], buffer, BUFSIZ);
            printf("Read %d bytes: %s\n", data_processed, buffer);
            exit(EXIT_SUCCESS);
        }

// Otherwise, we must be the parent process.

        else {
            data_processed = write(file_pipes[1], some_data,
                                   strlen(some_data));
            printf("Wrote %d bytes\n", data_processed);
        }
    }
    exit(EXIT_SUCCESS);
}

三、命名管道:FIFO

用FIFO文件来完成不相关进程间的数据交换,因此也被称为命名管道(named pipe)。命名管道是一种特殊类型的文件(Linux中所有事物都是文件),它在文件系统中以文件名的形式存在,但它的行为却和已经见过的没有名字的管道类似。

可以在命令行上创建命名管道,也可以在程序中创建它。命令行如下:

$ mkfifo filename
在程序中,我们可以使用两个不同的函数调用,如下:

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

int mkfifo(const char *filename, mode_t mode);
int mknod(const char *filename, mode_t mode | S_IFIFO, (dev_t) 0);
与mknod命令一样,可以用mknod函数建立许多特殊类型的文件。要想通过这个函数创建一个命名管道,唯一具有可移植性的方法是使用一个dev_t类型的值0,并将文件访问模式与S_IFIFO按位或。

举例:

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

int main()
{
    int res = mkfifo("/tmp/my_fifo", 0777);
    if (res == 0)
        printf("FIFO created\n");
    exit(EXIT_SUCCESS);
}
1.访问FIFO文件

命名管道的一个非常有用的特点是:由于它们出现在文件系统中,所以它们可以像平常的文件名一样在命令中使用。在把创建的FIFIO文件用在程序设计中之前,先要通过普通的文件命令来观察FIFO文件的行动。

(1)首先尝试读这个(空的)FIFO文件:

$ cat < /tmp/my_fifo
(2)尝试向FIFO写数据。

$ echo "Hello world" > /tmp/my_fifo
将看到cat命令产生输出。如果不向FIFO发送任何数据,cat命令将一直挂起,直到你中断它,常用的中断方式是使用Ctrl+C键。

(3)可以将第一个命令放在后台执行,这样即可以一次执行两个命令:

$ cat < /tmp/my_fifo &

$ echo "Hello world" > /tmp/my_fifo


2.使用open打开FIFO文件

打开FIFO的一个主要限制是,程序不能以O_RDWR模式打开FIFO文件进行读写操作,这样做的后果并未明确定义。但这个限制是有道理的,因为通常使用FIFO只是为了单向传递数据,所有没有必要使用O_RDWR模式。如果一个管道以读/写方式打开,进程就会从这个管道读回它自己的输出。

如果确实需要在程序之间双向传递数据,最好使用一对FIFO或管道,一个方向使用一个,或者(但并不常用)采用先关闭再重新打开FIFO的方法来明确地改变数据流的方向。打开FIFO文件和打开普通文件的另一点区别是,对open_flag的O_NONBLOCK选项的用法。使用这个选项不仅改变open调用的处理方式,还会改变对这次open调用返回的文件描述符进行的读写请求的处理方式。

O_RDONLY、O_WRONLY和O_NONBLOCK标志共有4种合法的组合方式,下面逐个介绍:

open(const char *path, O_RDONLY);
在这种情况下,open调用将阻塞,除非有一个进程以写方式打开同一个FIFO,否则它不会返回。

open(const char *path, O_RDONLY | O_NONBLOCK);
即使没有其他进程以写方式打开FIFO,这个open调用也将成功并立刻返回。

open(const char *path, O_WRONLY);
在这种情况下,open调用将阻塞,直到有一个进程以读方式打开同一个FIFO为止。

open(const char *path, O_WRONLY | O_NONBLOCK);
这个函数调用总是立刻返回,但如果没有进程以读方式打开FIFO文件,open调用将返回一个错误-1并且FIFO也不会被打开。如果确实有一个进程以读方式打开FIFO文件,那么我们就可以通过它返回的文件描述符对这个FIFO文件进行写操作。

请注意O_NONBLOCK分别搭配O_RDONLY和O_WRONLY在效果上的不同,如果没有进程以读方式打开管道,非阻塞写方式的open调用将失败,但非阻塞读方式的open调用总是成功。close调用的行为并不受O_NONBLOCK标志的影响。

举例:

// Let's start with the header files, a #define and the check that the correct number
// of command-line arguments have been supplied.

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

#define FIFO_NAME "/tmp/my_fifo"

int main(int argc, char *argv[])
{
    int res;
    int open_mode = 0;
    int i;

    if (argc < 2) {
        fprintf(stderr, "Usage: %s <some combination of\
               O_RDONLY O_WRONLY O_NONBLOCK>\n", *argv);
        exit(EXIT_FAILURE);
    }

// Assuming that the program passed the test, we now set the value of open_mode
// from those arguments.

    for(i = 1; i < argc; i++) {
        if (strncmp(*++argv, "O_RDONLY", 8) == 0)
             open_mode |= O_RDONLY;
        if (strncmp(*argv, "O_WRONLY", 8) == 0)
             open_mode |= O_WRONLY;
        if (strncmp(*argv, "O_NONBLOCK", 10) == 0)
             open_mode |= O_NONBLOCK;
     }

// We now check whether the FIFO exists and create it if necessary.
// Then the FIFO is opened and output given to that effect while the program
// catches forty winks. Last of all, the FIFO is closed.

    if (access(FIFO_NAME, F_OK) == -1) {
        res = mkfifo(FIFO_NAME, 0777);
        if (res != 0) {
            fprintf(stderr, "Could not create fifo %s\n", FIFO_NAME);
            exit(EXIT_FAILURE);
        }
    }

    printf("Process %d opening FIFO\n", getpid());
    res = open(FIFO_NAME, open_mode);
    printf("Process %d result %d\n", getpid(), res);
    sleep(5);
    if (res != -1) (void)close(res);
    printf("Process %d finished\n", getpid());
    exit(EXIT_SUCCESS);
}

3.对FIFO进行读写操作

使用O_NONBLOCK模式会影响到对FIFO的read和write调用。

对一个空的、阻塞的FIFO(即没有用O_NONBLOCK标志打开)的read调用将等待,直到有数据可以读时才继续执行。与此相反,对一个空的、非阻塞的FIFO的read调用将立刻返回0字节。

对一个完全阻塞FIFO的write调用将等待,直到数据可以被写入时才继续执行。如果FIFO不能接收所有写入的数据,它将按下面的规则执行。

  • 如果请求写入的数据的长度小于等于PIPE_BUF字节,调用失败,数据不能写入。
  • 如果请求写入的数据的长度大于PIPE_BUF字节,将写入部分数据,返回实际写入的字节数,返回值也可能是0。

FIFO的长度是需要考虑的一个很重要的因素。系统对任一时刻在一个FIFO中可以存在的数据长度是有限制的,它由#define PIPE_BUF语句定义,通常可以在头文件limits.h中找到它。在linux和许多其他类UNIX系统中,它的值通常是4096字节,但在某些系统中它可能会小到512字节。系统规定:在一个以O_WRONLY方式(即阻塞方式)打开的FIFO中,如果写入的数据长度小于等于PIPE_BUF,那么或者写入全部字节,或者一个字节都不写入。

虽然,对只有一个FIFO写进程和一个FIFO读进程的简单情况来说,这个限制并不是非常重要,但只使用一个FIFO并允许多个不同的程序向一个FIFO读进程发送请求的情况是很常见的。如果几个不同的程序尝试同时向FIFO写数据,能否保证来自不同程序的数据块不相互交错就非常关键了。也就是说,每个写操作都必须是“原子化”的。怎样才能做到这一点呢?

如果你能保证所有的写请求是发往一个阻塞的FIFO的,并且每个写请求的数据长度小于等于PIPE_BUF字节,系统就可以确保数据决不会交错在一起。通常将每次通过FIFO传递的数据长度限制为PIPE_BUF字节是个好办法,除非你只使用一个写进程和一个读进程。

举例:

使用FIFO实现进程间通信

(1)第一个程序是生产者程序,它在需要时创建管道,然后尽可能快地向管道中写入数据。

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

#define FIFO_NAME "/tmp/my_fifo"
#define BUFFER_SIZE PIPE_BUF
#define TEN_MEG (1024 * 1024 * 10)

int main()
{
    int pipe_fd;
    int res;
    int open_mode = O_WRONLY;
    int bytes_sent = 0;
    char buffer[BUFFER_SIZE + 1];

    if (access(FIFO_NAME, F_OK) == -1) {
        res = mkfifo(FIFO_NAME, 0777);
        if (res != 0) {
            fprintf(stderr, "Could not create fifo %s\n", FIFO_NAME);
            exit(EXIT_FAILURE);
        }
    }

    printf("Process %d opening FIFO O_WRONLY\n", getpid());
    pipe_fd = open(FIFO_NAME, open_mode);
    printf("Process %d result %d\n", getpid(), pipe_fd);

    if (pipe_fd != -1) {
        while(bytes_sent < TEN_MEG) {
            res = write(pipe_fd, buffer, BUFFER_SIZE);
            if (res == -1) {
                fprintf(stderr, "Write error on pipe\n");
                exit(EXIT_FAILURE);
            }
            bytes_sent += res;
        }
        (void)close(pipe_fd); 
    }
    else {
        exit(EXIT_FAILURE);        
    }

    printf("Process %d finished\n", getpid());
    exit(EXIT_SUCCESS);
}
(2)第二个程序是消费者程序,从FIFO读取数据并丢弃它们。

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

#define FIFO_NAME "/tmp/my_fifo"
#define BUFFER_SIZE PIPE_BUF

int main()
{
    int pipe_fd;
    int res;
    int open_mode = O_RDONLY;
    char buffer[BUFFER_SIZE + 1];
    int bytes_read = 0;

    memset(buffer, '\0', sizeof(buffer));
    
    printf("Process %d opening FIFO O_RDONLY\n", getpid());
    pipe_fd = open(FIFO_NAME, open_mode);
    printf("Process %d result %d\n", getpid(), pipe_fd);

    if (pipe_fd != -1) {
        do {
            res = read(pipe_fd, buffer, BUFFER_SIZE);
            bytes_read += res;
        } while (res > 0);
        (void)close(pipe_fd);
    }
    else {
        exit(EXIT_FAILURE);
    }

    printf("Process %d finished, %d bytes read\n", getpid(), bytes_read);
    exit(EXIT_SUCCESS);
}




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值