【Unix/Linux编程实践】用管道实现双向通信—以bc计算器为例

前言

几乎每个版本的Unix都包含bc计算器。

事实上,大多数版本的bc只分析输入,然后在内部启动了dc计算器程序,并通过管道与其通信。dc是一个基于栈的计算器,它接收逆波兰表达式,执行运算后将结果送到标准输出。

bc从连接到dc标准输出的管道上读取结果,再把结果转发给用户。

事实上,bc为我们提供了用户界面,并使用dc提供的服务。这是一个简单的客户/服务器模型。

bc/dc对被称为协同进程(coroutines)。

编写bc的思路

1.bc创建两个管道(使用pipe);

2.bc创建一个新进程(用于运行dc,使用fork);

3.新进程创建后,在运行exec dc之前,将其标准输入和标准输出重定向到管道;

4.运行exec dc;

5.在父进程bc中,读取并分析用户的输入,将命令通过管道传给dc,dc读取响应,并把响应通过管道传给用户。

代码实现与分析

/** tinybc.c * a tiny calculator that uses dc to do its work ** * demonstrates bidirectional pipes ** * input looks like number op number which ** tinybc converts into number \n number \n op \n p ** and passes result back to stdout ** ** +-----------+ +----------+ ** stdin >0 >== pipetodc ====> | ** | tinybc | | dc - | ** stdout <1 <== pipefromdc ==< | ** +-----------+ +----------+ ** ** * program outline ** a. get two pipes ** b. fork (get another process) ** c. in the dc-to-be process, ** connect stdin and out to pipes ** then execl dc ** d. in the tinybc-process, no plumbing to do ** just talk to human via normal i/o ** and send stuff via pipe ** e. then close pipe and dc dies ** * note: does not handle multiline answers **/

#include <stdio.h>

#define oops(m, x) { perror(m); exit(x); }

main()
{
    int pid, todc[2], fromdc[2];

    /*创建两个管道*/

    if ( pipe(todc) == -1 || pipe(fromdc) == -1 )
        oops("pipe failed", 1);

    /*fork一个新进程,此时父子进程共享以上两个管道*/

    if ( (pid = fork()) == -1 )
        oops("cannot fork", 2);

    if ( pid == 0 )     /*子进程,执行dc*/
        be_dc(todc, fromdc);
    else                /*父进程,执行bc*/
    {
        be_bc(todc, fromdc);
        wait(NULL);     /*等待子进程结束*/
    }
}

be_dc(int in[2], int out[2])
{
    /*将文件描述符in[0](读数据端)复制到文件描述符0(即stdin)上*/
    if ( dup2(in[0], 0) == -1 )
        oops("dc:cannot redirect stdin",3);
    close(in[0]);       /*关闭原文件描述符,于是读数据端只剩下fd 0*/
    close(in[1]);       /*关闭写数据端*/

    /*原理同上,将标准输出重定向到另一条管道的写数据端*/
    if ( dup2(out[1], 1) == -1 )
        oops("dc:cannot redirect stdout",4);
    close(out[1]);
    close(out[0]);

    /* now execl dc with the - option */
    execlp("dc", "dc", "-", NULL);
    oops("Cannot run dc",5);
}

be_bc(int todc[2], int fromdc[2])
{
    int num1, num2;
    char op[BUFSIZ], message[BUFSIZ], *fgets();
    FILE *fpout, *fpin, *fdopen();

    /*setup*/
    close(todc[0]);     /*只写不读*/
    close(fromdc[1]);   /*只读不写*/

    /*把一个通向管道的连接转换成FILE * 类型值,之后可以使用标准缓存的I/O操作来对其进行操作*/
    fpout = fdopen(todc[1], "w");
    fpin = fdopen(fromdc[0], "r");
    if ( fpout == NULL || fpin == NULL)
        fatal("Error convering pipes to streams");

    /*main loop*/

    /*接受用户输入*/
    while ( printf("tinybc: "), fgets(message, BUFSIZ, stdin) != NULL)
    {
        /*从message按照指定格式读取数据*/
        if (sscanf(message, "%d%[-+*/^]%d", &num1, op, &num2) != 3)
        {
            printf("syntax error\n");
            continue;
        }       

        /*写数据到管道*/
        if (fprintf(fpout, "%d\n%d\n%c\np\n", num1, num2, *op) == EOF)
            fatal("Error writing");
        fflush(fpout);

        /*从管道读数据*/
        if (fgets(message, BUFSIZ, fpin) == NULL)
            break;
        printf("%d %c %d = %s", num1, *op, num2, message);
    }
    fclose(fpout);      /*close pipe*/
    fclose(fpin);       /*dc will see EOF*/
}

fatal(char mess[])
{
    fprintf(stderr, "Error: %s\n", mess);
    exit(1);
}

结语

本代码参考自《Unix/Linux编程实践教程》第11章。

自己模仿着敲了一遍,印象加深了很多,一个例子把pipe,fork,dup,exec等知识都融入进来了。

其中一个需要注意的地方便是使用fdopen来打开文件描述符,使得可以使用fprintf和fgets来通过管道和dc进行通信。

你可能感兴趣的:(unix,通信,管道,C-S)