1.shell是如何运行程序的?
shell由下面的循环组成:
while (!end_of_input)
getcommand
execute command
wait for command to finish
我们可以实际用一下shell:
jiange@jiange-Inspiron:~/cpp$ls
override override.cpp test test1.cpp test.cpp test.o test.sh
jiange@jiange-Inspiron:~/cpp$ps
PID TTY TIME CMD
2457 pts/12 00:00:00 bash
3780 pts/12 00:00:00 ps
上述过程为:
sh读入ls,新建一个进程ls并执行,此时sh则等待ls的退出,ls退出后回到sh,sh继续读入ps……
因此,要编写sh,我们需要知道如何运行一个程序,创建,终止一个进程,以及一个进程如何等待另一个进程的结束。
2.如何执行一个程序
答案:
execvp(progname, arglist)
它的运作流程:
程序调用execvp;
内核从磁盘将程序载入;
内核将arglist复制到进程;
内核调用main(argc, argv)。
运行ls的例子:
/* exec1.c - shows how easy it is for a program to run a program
*/
main()
{
char *arglist[3];
arglist[0] = "ls";
arglist[1] = "-l";
arglist[2] = 0 ;
printf("* * * About to exec ls -l\n");
execvp( "ls" , arglist );
printf("* * * ls is done. bye\n");
}
执行结果:
$ ./exec1
* * * About to exec ls -l
总用量 40
-rwxrwxr-x 1 jiange jiange 7370 12月 7 22:59 exec1
-rw-rw-r-- 1 jiange jiange 262 10月 14 2007 exec1.c
-rw-rw-r-- 1 jiange jiange 406 10月 14 2007 forkdemo1.c
-rw-rw-r-- 1 jiange jiange 315 10月 14 2007 forkdemo2.c
-rw-rw-r-- 1 jiange jiange 503 10月 14 2007 forkdemo3.c
-rw-rw-r-- 1 jiange jiange 1395 10月 14 2007 psh1.c
-rw-rw-r-- 1 jiange jiange 1766 10月 14 2007 psh2.c
-rw-rw-r-- 1 jiange jiange 784 10月 14 2007 waitdemo1.c
-rw-rw-r-- 1 jiange jiange 1077 10月 14 2007 waitdemo2.c
一切好像正常运行,不过,为什么printf(“* * * ls is done. bye\n”);这一句没有执行呢?!
这就要理解下execvp的作用了:execvp将程序从磁盘载入当前进程中,替代当前进程的代码和数据,所以执行完execvp之后,进程exec1完全被替换掉了,自然不会执行后续的打印工作。
2.带提示符的shell
这个版本提示用户输入程序名和参数,然后运行。
/* prompting shell version 1 * Prompts for the command and its arguments. * Builds the argument vector for the call to execvp. * Uses execvp(), and never returns. */
#include
#include
#include
#define MAXARGS 20 /* cmdline args */
#define ARGLEN 100 /* token length */
int main()
{
char *arglist[MAXARGS+1]; /* an array of ptrs */
int numargs; /* index into array */
char argbuf[ARGLEN]; /* read stuff here */
char *makestring(); /* malloc etc */
numargs = 0;
while ( numargs < MAXARGS )
{
printf("Arg[%d]? ", numargs);
if ( fgets(argbuf, ARGLEN, stdin) && *argbuf != '\n' )
arglist[numargs++] = makestring(argbuf);
else
{
if ( numargs > 0 ){ /* any args? */
arglist[numargs]=NULL; /* close list */
execute( arglist ); /* do it */
numargs = 0; /* and reset */
}
}
}
return 0;
}
int execute( char *arglist[] )
/* * use execvp to do it */
{
execvp(arglist[0], arglist); /* do it */
perror("execvp failed");
exit(1);
}
char * makestring( char *buf )
/* * trim off newline and create storage for the string */
{
char *cp, *malloc();
buf[strlen(buf)-1] = '\0'; /* trim newline */
cp = malloc( strlen(buf)+1 ); /* get memory */
if ( cp == NULL ){ /* or die */
fprintf(stderr,"no memory\n");
exit(1);
}
strcpy(cp, buf); /* copy chars */
return cp; /* return ptr */
}
上面使用makestring函数来动态分配空间给字符串参数。
运行上面程序,我们就可以传递参数进去了。
不过一个比较严重的缺陷:运行完ls之后,整个程序就退出了,而我们期待的是,运行完ls后返回到我们的shell中,shell可以继续接收命令输入并执行。
在开头我们讲到,sh读入ls,新建一个进程ls并执行,此时sh则等待ls的退出,ls退出后回到sh。
也就是说,sh并不直接执行ls命令,而是新建了一个进程来执行它,那么问题来了:如何建立新的进程?
答案:使用fork();
我们可以使用fork来创建子进程,然后新的进程调用execvp来执行用户命令。
那么,父进程sh如何等待子进程ls的结束呢?
答案:使用 pid=wait(&status);
wait阻塞调用它的进程直到子进程结束,之后wait取得子进程结束时传给exit的值(通过status),并返回子进程的PID。
3.可以持续接收用户命令的shell
经过以上分析,我们可以设计新的shell:
获取命令-> 用fork创建新进程 ->父进程wait ->子进程exec运行新程序->在新程序的main中运行->新程序exit -> 父进程得到子进程状态->获取命令
/** prompting shell version 2 ** ** Solves the `one-shot' problem of version 1 ** Uses execvp(), but fork()s first so that the ** shell waits around to perform another command ** New problem: shell catches signals. Run vi, press ^c. **/
#include
#include
#define MAXARGS 20 /* cmdline args */
#define ARGLEN 100 /* token length */
main()
{
char *arglist[MAXARGS+1]; /* an array of ptrs */
int numargs; /* index into array */
char argbuf[ARGLEN]; /* read stuff here */
char *makestring(); /* malloc etc */
numargs = 0;
while ( numargs < MAXARGS )
{
printf("Arg[%d]? ", numargs);
if ( fgets(argbuf, ARGLEN, stdin) && *argbuf != '\n' )
arglist[numargs++] = makestring(argbuf);
else
{
if ( numargs > 0 ){ /* any args? */
arglist[numargs]=NULL; /* close list */
execute( arglist ); /* do it */
numargs = 0; /* and reset */
}
}
}
return 0;
}
execute( char *arglist[] )
/* * use fork and execvp and wait to do it */
{
int pid,exitstatus; /* of child */
pid = fork(); /* make new process */
switch( pid ){
case -1:
perror("fork failed");
exit(1);
case 0:
execvp(arglist[0], arglist); /* do it */
perror("execvp failed");
exit(1);
default:
while( wait(&exitstatus) != pid )
;
printf("child exited with status %d,%d\n",
exitstatus>>8, exitstatus&0377);
}
}
char *makestring( char *buf )
/* * trim off newline and create storage for the string */
{
char *cp, *malloc();
buf[strlen(buf)-1] = '\0'; /* trim newline */
cp = malloc( strlen(buf)+1 ); /* get memory */
if ( cp == NULL ){ /* or die */
fprintf(stderr,"no memory\n");
exit(1);
}
strcpy(cp, buf); /* copy chars */
return cp; /* return ptr */
}
关于格式控制方面,比如如何在一行中输入参数,如何用exit退出shell等等,在这里我们不做讨论。
在运行的时候,我们发现一个严重的错误:
假如我们正在运行一个子进程,这时我们键入Ctrl-C键,我们预期的应该是子进程终止,而shell仍继续运行,但事实是shell也终止了,因为键盘信号SIGINT发给了所有连接的进程!
为使得有子进程在运行时,shell不会因为中断信号而跟着终止,我觉得可以这样:只要有子进程运行,shell就将对SIGINT信号的响应设为ignore,子进程结束之后,则设置为默认方式。这样子shell就能在子进程运行的时候屏蔽中断信号。