【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进行通信。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值