首页 > 代码库 > 模拟shell ( 进程函数:fork(),execvp(),wait() )

模拟shell ( 进程函数:fork(),execvp(),wait() )

      shell是一个管理进程和运行进程的程序,下面我们就通过模拟一个shell程序这个实例来更好地认识认识在Linux/Unix系统中,进程的创建和结束,以及父子进程之间的一些关系。接下来先贴上源代码的中命令的读取部分:

  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;
    }
   }
  }

       这一段代码用于读取用户输入的命令,保存在arglist这个字符指针的数组中。因为进程间的通信的参数类型为字符串,所以我们选择指向字符串的指针构成的数组作为传递的参数,并且注意要将最后一个指针置NULL。当命令读取完毕完毕之后,即调用execute函数并且将arglist数组传递给它,进行关于子进程的一些操作。接下来,我们来看看execute函数具体的实现情况。

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);
  }
}

    在execute函数中最先调用fork()函数,那fork()函数做了些什么呢?其实fork()函数创建了和当前进程基本一模一样的一个子进程。当控制转到内核中的fork代码之后,内核先分配新的内存块和内核数据结构,然后将原来的进程复制到新的进程中去。最后向运行进程中添加新的进程并且控制重新返回到进程中。开始可能觉得挺奇怪的,搞个基本差不多的进程干什么?其实看了后面的内容你就会知道,只要调用个execvp函数,子进程就变得完全不一样啦。好,现在我们就有两个进程了,并且它们的代码相同,都运行到

  pid=fork();//创建子进程
       这一步,那我们怎么判断哪个是父进程,哪个是子进程呢?其实在父进程中fork函数的返回值是子进程的进程ID,而在子进程中,fork返回的是0,所以我们通过fork的返回值就能判断父子进程了。下面进入switch部分,若fork返回-1,说明创建子进程失败,若是在子进程中,则调用execvp函数(其实execvp不是系统调用,而是一个库函数,它通过调用execve来调用内核服务)来执行指明的程序。那我们就来看看execvp这个函数干了些什么?

result=execvp(const char*file,const char*argv[])
      其中第一个参数指明了要执行的进程,如:“ls”,"ps"等等命令,而第二个参数则为指向要执行的命令及相关参数的字符串指针。通过调用execvp我们就能在一个进程中,执行像"ls"这样另外一个进程了。但是有一个问题需要注意,那就是execvp会清除当前进程,并加载由file指定的进程。也就是说,比如当"ls"执行完之后,execvp下面的那句perror是不会执行的,因为它早就被“ls”的代码替换掉了。这其实也就是我们为什么要创建子进程的原因。如果在父进程中调用execvp的话,我们做的这个shell程序就只能调用一条命令了。

      那我们就要想了,父进程这个时候在干嘛呢?其实在fork之后,父子进程是并行执行的,而我们想要的效果是父进程先等等,等子进程结束之后再继续执行。接下来的wait函数就满足了我们的愿望啦!

pid=wait(&status)
      wait函数主要做两件事,首先wait暂停调用它的进程直到子进程结束,然后wait通过status取得子进程结束时传给exit的值。wait返回结束进程的PID,如果进程没有子进程或没有得到终止状态值,则返回-1。

      这样通过不断地创建子进程,用想要执行的程序代替子进程并且让父进程等待,最后执行完毕,回到父进程,我们也就模拟了一个shell程序啦。最后来说说,结束进程的函数exit。exit的话,它会先刷新所有的流,调用一些函数,执行当前系统定义的其他和exit相关的操作。最后,调用_exit这个内核操作,来进行释放内存,关闭相关文件这些善后工作。

    就这样,通过几个函数的调用,我们就帮一个进程走过了它短暂的一生。其实仔细想想也不是那么复杂嘛,咔咔咔~

 


参考文献:《Understanding Unix/Linux Programming ----A Guide to Theory and Practice》                                                                      

模拟shell ( 进程函数:fork(),execvp(),wait() )