几乎每个版本的Unix都包含bc计算器。
事实上,大多数版本的bc只分析输入,然后在内部启动了dc计算器程序,并通过管道与其通信。dc是一个基于栈的计算器,它接收逆波兰表达式,执行运算后将结果送到标准输出。
bc从连接到dc标准输出的管道上读取结果,再把结果转发给用户。
事实上,bc为我们提供了用户界面,并使用dc提供的服务。这是一个简单的客户/服务器模型。
bc/dc对被称为协同进程(coroutines)。
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进行通信。