《系统程序员成长计划》这本书中提到了外壳模式的概念。所谓的外壳模式的作用就是:不需要修改原来的应用程序,而控制它的输入和输出(即用户界面),同时应用程序也不知道外壳的存在。当然这样理解会比较麻烦,我们就举个简单的例子,比如已经存在一个应用程序,我们想给该应用程序增添一个新的用户界面,这里有两种选择:1. 根据该应用程序的内部实现重新编写一个用户界面,但是如果原来的应用程序的用户界面和内部实现没有很好的分离,那么在我们创建新的用户界面的时候就不可避免的会牵扯到旧的用户界面;2. 利用外壳模式,外壳模式需要一个前提条件:应用程序实现了基于终端的用户界面,即从标准输入中读取数据,向标准输出和标准错误输出显示结果。这时我们可以把标准输入、标准输出和标准错误输出重定向到管道上,向管道里写数据来模拟应用程序的输入和输出。这样就实现了外壳模式。
下面是一个实现自动输入的程序,通过对该程序的解析,在分析过程中会总结下该程序涉及的有关知识。
#include
#include
int main(int argc, char * argv[])
{
int n = 0;
printf("Input number: \n");
fflush(stdout);
scanf("%d", &n);
printf("Your input %d\n", n);
return 0;
}
#include
#include
int main(int argc, char * argv[])
{
int shell_to_app[2] = {0};
int app_to_shell[2] = {0};
pipe(shell_to_app);
pipe(app_to_shell);
if(fork() == 0)
{
close(shell_to_app[1]);
close(app_to_shell[0]);
dup2(shell_to_app[0], STDIN_FILENO);
dup2(app_to_shell[1], STDOUT_FILENO);
execl("./app.exe", "./app.exe", NULL);
}
else
{
FILE * in = fdopen(app_to_shell[0], "r");
FILE * out = fdopen(shell_to_app[1], "w");
char message[256] = {0};
close(shell_to_app[0]);
close(app_to_shell[1]);
fgets(message, sizeof(message), in);
printf("1: %s\n", message);
fprintf(out, "1234\n");
fflush(out);
fgets(message, sizeof(message), in);
printf("2: %s\n", message);
printf("2: %s\n", message);
fclose(in);
fclose(out);
}
return 0;
}
在讲解外壳模式的概念的时候,曾提到外壳模式是利用管道模拟标准的输入输出。首先我们建立两个管道(为什么是两个呢?在讲解管道概念是会提到),两个管道分别负责两个进程间的读写通信,其次利用dup2函数重定向子进程的标准输入输出,此时我们可以利用建立的两个管道来模拟子程序中的输入输出。
我们逐个解析上面程序用到的概念:
1. 文件描述符
文件描述符(File Discriptor)在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。习惯上把0,1,2的文件描述符定义为标准输入、标准输出和标准错误输出,POSIX 定义了 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 来代替 0、1、2。这三个符号常量的定义位于头文件 unistd.h。这也是上面程序用到的。但是除了Linux和UNIX意外,其他操作系统很难兼容文件描述符,为了解决这个问题,ANSI C规范中定义的标准库的文件I/O操作。ANSI C规范给出了一个解决方法,就是使用FILE结构体的指针。事实上,UNIX/Linux平台上的FILE结构体的实现中往往都是封装了文件描述符变量在其中。
提到文件描述符,就不得不说另外两个概念:文件描述符表,文件表和V节点。
下面用一个图可以清晰的表述他们之间的关系
每个进程都有一个进程表项(包括文件描述符表),其中每项包含了文件描述符和文件指针,未见指针指向文件表中的某一项,文件表中某一项中包含了操作该文件的标志,偏移量以及V节点,V节点指向了文件在物理内存中的具体地址。从图中也可以看出,1个V节点对应多个文件表项,一个文件表可以对应多个进程表项。
与文件描述符相关的函数:
文件描述符的生成
open(), open64(), creat(), creat64() // 通过文件路径打开或创建一个文件描述符
socket() //创建一个socket描述符
socketpair() //创建一个socket对,用于进程间的通信
pipe() // 创建管道
与单一文件描述符相关的操作
read(), write() // 对文件描述符的读写,read是指从文件读取数据到内存,write是把内存中的数据写入到文件中去。
recv(), send() // 用于tcp通信的发送和接受的函数,和read,write类似,但功能更强大
recvmsg(),sendmsg() // 用于udp通信的发送和接受函数
sendfile() //升级版的用于通信的函数,可以直接通过内核调用,减少了到应用程序读写两道操作程序,并降低了内存的使用。
lseek(), lseek64() // 定位当前文件描述符的操作指针
fstat(), fstat64() // 获取文件状态的函数
fchmod()// 改变文件rwe权限的函数
fchown()//改变文件所属用户的函数
与复数文件描述符相关的操作
select(), pselect()
poll()
与文件描述符表相关的操作
close() // 关闭文件描述符
dup() //复制一个文件描述符
dup2() // 用于重定向文件描述符
fcntl (F_DUPFD) //fcntl()用来操作文件描述符的一些特性。fcntl 不仅可以施加建议性锁,还可以施加强制锁。同时,fcntl还能对文件的某一记录进行上锁,也就是记录锁。
fcntl (F_GETFD and F_SETFD)
改变进程状态的操作
fchdir() //改变当前工作目录
mmap() // 创建共享内存
与文件加锁的操作
flock()
fcntl (F_GETLK, F_SETLK and F_SETLKW)
lockf()
与套接字相关的操作
connect()
bind()
listen()
accept()
getsockname()
getpeername()
getsockopt(), setsockopt()
shutdown()
2. 管道
管道是一种半双工的进程通信机制。在另一篇博文进程通信(一)中有提到。这也是上面提到代码中为什么要用两个管道了,因为一个管道只能实现两个进程间单方向的写或者读,使用两个管道可以实现两个进程间的互相的读写。
在回到该代码的分析上,子进程保留了app_to_shell[1],,即app到shell的写的文件描述符,保留了shell_to_app[0],即app从shell读的文件描述符。代码中的两个dup2函数实现了标准输入输出的重定向,即从shell_to_app[0]文件操作符中读取数据等同于从标准输入(STDIN_FILENO)读取数据;向标准输出(STDOUT_FILENO)写入数据等同于向app_to_shell[1]写入数据。那么这样解释应该对上面的程序有些明了的吧。execl函数的作用是用app.exe应用程序替换子进程进行执行。那么我们就逐一的分析父进程的每个设计文件描述符的语句的意思:
22行: FILE * in = fdopen(app_to_shell[0], "r"); 以只读状态打开app_to_shell[0]的文件操作符,返回一个FILE指针,为什么要用fdopen()函数呢?用fopen()函数可以吗?答案是不可以的。因为app_to_shell[0]是由pipe(管道创建函数)创建的文件描述符,它是一种特殊类型的文件,它不能用标准I/O open函数打开,这类型的文件描述符包括:从 o p e n , d u p , d u p 2 , f c n t l或p i p e函数得到此文件描述符。
23行:与22行类似
29行:从in的FILE指针指向的文件中从读取数据到message,而与in对应的文件描述符是app_to_shell[0],也就是从in中读取的数据应该是由app_to_shell[1]写入的数据,由于子进程向标准输出写入了数据,因此此时的输出应该是“Input your number: ”。
32行:向out写入数据“1234\n”,也就是向shell_to_app[1]中写入数据;又因为在子进程app.exe中我们重定向了标准输入为shell_to_app[0];也即是说app.c中的scanf等同于从shell_to_app[0]中读取数据。
34行:与29行类似,不过这里要说的是,此时在app.exe中的n的值是1234,是由32行决定的,而且子进程app.c也向标准输出写入了n,此时再从app_to_shell[0]中读取数据,可以得到数据“Your input number is : 1234”。
最终程序的运行结果是:
因为我的表述能力不够,不知道这样说大家能明白么,可以尝试的调试下程序,或者自己查查有关函数的概念,就会明白这个外壳模式demo的基本原理了。