编写命令解释器sh(一)

来源:互联网 发布:eclipse查看源码插件 编辑:程序博客网 时间:2024/05/29 17:17

之前由于终于了结了一段心事,荒废了半个月没写博客,浑浑噩噩。果然代码是有这种魔性,长久不写手痒痒。之后还是得坚持更新,也把这几天粗粗学的python整理一下补上,下面言归正传。

shell我们都很熟悉,我们输入一些命令(如ls、cat、touch等),得到结果。ls等命令实际上都对应于磁盘上的某个程序。shell运行这些程序,得到运行结果。Unix将运行中的程序定义为进程,它对于于计算机内实际的实体:内存中的一些字节。
那么当我们输入ls时,shell实际做了什么呢?1、用户键入ls。2、shell建立一个新的进程来运行这个程序(注意,这里的新的进程中的是shell程序)3、shell将程序从磁盘中载入4、程序在进程中运行直到结束
这里面的第二步就有点疑问了,为什么是shell建立一个自己的进程来运行程序而不是其他的方式?这就与shell的实现基于的系统调用有关了。
shell本质上来说也就是一个程序,那如何让一个程序来运行另一个程序呢?这就要基于系统调用execvp。result = execvp(const char *file, const char *argv[])。函数的第一个参数是程序的名称,会在环境变量PATH中寻找,第二个参数是程序运行的参数。我们在shell程序中调用execvp,就可以运行file程序了。

execvp函数
函数的第一个参数不必多说,但是第二个储存程序参数的argv数组应注意两点,1、数组的第一个元素应置为程序名称2、数组应以NULL结尾
我们先来测试一下:

#include "unistd.h"#include "stdio.h"int main(int argc, char const *argv[]){     char *arglist[3];     arglist[0] = "ls";     arglist[1] = "-l";     arglist[2] = NULL;     printf("*** About to exec ls -l\n");     execvp("ls", arglist);     printf("*** ls is done. bye\n");     return 0;}

程序运行结果:
这里写图片描述
execvp正常运行了,但是execvp之后的输出却没有了。这就是execvp的特性,运行execvp之后,内核会将程序载入到当前进程,并且会替换掉当前进程的代码和数据,当前的进程就再也不存在了且不能恢复。
知道这一点我们就可以开始写一个自己的sh了。

psh.c version1
Function: 提示用户输入argv数组中的各个元素,以用户输入回车作为结束符,运行完指定的程序后,整个sh退出。
Bugs: 只能运行一次命令中程序

#include "stdio.h"#include "signal.h"#include "string.h"#include "unistd.h"#include "stdlib.h"#define MAXARGS 20#define ARGLEN  100void execute(char *arglist[]);char *makestring(char *buf);int main(int argc, char const *argv[]){     char     *arglist[MAXARGS + 1];     int          numargs;     char      argbuf[ARGLEN];     numargs = 0;     while(numargs < MAXARGS){          printf("Arg[%d] ", numargs);          if(fgets(argbuf, ARGLEN, stdin) && *argbuf != '\n')               arglist[numargs++] = makestring(argbuf);          else          {               if (numargs > 0)               {                    arglist[numargs] = NULL;                    execute(arglist);                    numargs = 0;               }          }     }     return 0;}void execute(char *arglist[]){     execvp(arglist[0], arglist);     perror("execvp failed");     exit(1);}char *makestring(char *buf){     char *cp;     buf[strlen(buf) - 1] = '\0';     cp = malloc(strlen(buf) + 1);     if (cp == NULL)     {          fprintf(stderr, "no memory\n");          exit(1);     }     strcpy(cp, buf);     return cp;}

其中构造argvlist数组特别重要,第一个元素是程序名,最后一个是NULL,需特别注意。
那么上面提到的BUG如何解决呢?就要用到另一个系统调用fork

fork系统调用
pid_t result = fork(void)
这个函数理解起来很简单,就是复制/克隆一个一模一样的自己进程。当使用fork函数时,内核会如何操作呢?1、分配新的内存块和内核数据结构。2、复制原来的进程到新的进程3、向运行进程集添加新的进程4、将控制返回给两个进程。进程对应的实际上就是一些内存,赋值内存和内核数据结构到新的进程,实际上就是赋值了一个完完全全一模一样的进程。那么问题就来了,如何分辨出哪个是原来的进程,哪个是新的进程?
我们知道UNIX系统以pid来标识每个进程,一个pid唯一对应于一个进程。父进程及子进程调用fork之后返回值是不一样的,父进程会返回子进程的pid,而子进程返回0。通过fork返回值可以分辨出父子进程。然后父子进程分别从fork之后开始运行下去。

借助于fork我们的sh不必运行完一个程序就退出了。回想一下shell的处理方式:输入命令,等待程序运行完后返回结果,并等待下一个命令的输入。父进程是sh,子进程运行程序时父进程挂起,父进程等待子进程运行结束后解除挂起状态并等待用户再次输入命令。这里需要另一个函数wait

wait系统调用
pid_t result = wait(int *statusptr)
wait函数是专门为父进程设计的,它可以将进程挂起直到其子进程结束,因此sh程序就被抽象为父进程wait,子进程execvp。
这个wait函数很牛,通过fork的介绍我们知道父子进程在fork之后完全分家了,父进程没法控制子进程,而挂起之后的这段时间也不知道用户对子进程干了什么事,这对sh的处理很不利,所以wait函数的statusstr整型指针专门设计出来,供子进程反馈给父进程这段时间到底发生了什么。statusptr指向的整数保存子进程退出状态或者信号序号(return/exit/signal等),此整型变量是宏。而wait返回值返回子进程pid,这样父进程就可以监控子进程如何返回并做出响应了。所以wait就有两个非常重要的功能:1、阻塞调用它的进程直到子进程结束2、返回子进程pid及子进程的退出情况。

结合execvp, fork, wait就写出了以下程序:

#include "stdio.h"#include "signal.h"#include "string.h"#include "unistd.h"#include "stdlib.h"#define MAXARGS 20#define ARGLEN  100void execute(char *arglist[]);char *makestring(char *buf);int main(int argc, char const *argv[]){     char     *arglist[MAXARGS + 1];     int          numargs;     char      argbuf[ARGLEN];     numargs = 0;     while(numargs < MAXARGS){          printf("Arg[%d]", numargs);          if(fgets(argbuf, ARGLEN, stdin) && *argbuf != '\n')               arglist[numargs++] = makestring(argbuf);          else          {               if (numargs > 0)               {                    arglist[numargs] = NULL;                    execute(arglist);                    numargs = 0;               }          }     }     return 0;}void execute(char *arglist[]){     int pid, exitstatus;     pid = fork();     switch(pid){          case -1:               perror("fork failed");               exit(1);          case 0:               execvp(arglist[0], arglist);               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){     char *cp;     buf[strlen(buf) - 1] = '\0';     cp = malloc(strlen(buf) + 1);     if (cp == NULL)     {          fprintf(stderr, "no memory\n");          exit(1);     }     strcpy(cp, buf);     return cp;}
0 0