第五周加分题--mybash的实现

题目要求

1.使用fork,exec,wait实现mybash

2.写出伪代码,产品代码和测试代码

3.发表知识理解,实现过程和问题解决的博客(包含代码托管链接)

bash是什么

在百度中搜索bash查看它是什么,得知bash 是一个为GNU计划编写的Unix shell。bash 指的就linux常用的shell脚本语言,这个常见于脚本第一行 : #!/bin/bash或者 #!/bin/sh
这种shell脚本很简单,就和你在终端输入命令一样,一行一行执行。

通过man -f pwd直接运行命令,可以了解pwd的大致功能。

要进一步了解pwd的用法,需要借助联机帮助manpages,输入man 1 pwd:

重点看总览(SYNOPSIS)部分,这是命令的用法说明,包括命令格式、参数(arguments)和选项(Option)列表。

描述(DESCRIPTION)部分是关于命令功能的详细阐述,根据命令和平台的不同,描述的内容也不同,有的简洁、精确,有的包含了大量的例子。不管怎么样,它描述了命令的所有功能,而且是这个命令的权威性解释。

mybash令是如何实现的?

由题可知可通过fork,exec,wait来实现mybash。

那么我们一个个的开始学习。

fork是什么

  • 一个进程,包括代码、数据和分配给进程的资源。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。

  • 一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。

  • 通过一个小例子,可以对fork产生一个基本的认识:


#include <unistd.h>  
#include <stdio.h>   
int main ()   
{   
    pid_t fpid; //fpid表示fork函数返回的值  
    int count=0;  
    fpid=fork();   
    if (fpid < 0)   
        printf("error in fork!");   
    else if (fpid == 0) {  
        printf("i am the child process, my process id is %d\n",getpid());   
        printf("I'm the child\n");
        count++;  
    }  
    else {  
        printf("i am the parent process, my process id is %d\n",getpid());   
        printf("I'm the father\n");  
        count++;  
    }  
    printf("统计结果是: %d\n",count);  
    return 0;  
}  


  • 运行结果如下:

  • 这里可以看出在语句fpid=fork()之前,只有一个进程在执行这段代码,但在这条语句之后,就变成两个进程在执行了,这两个进程的几乎完全相同。fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

      1)在父进程中,fork返回新创建子进程的进程ID;
      2)在子进程中,fork返回0;
      3)如果出现错误,fork返回一个负值;
    
  • 在fork函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是子进程,一个是父进程。在子进程中,fork函数返回0,在父进程中,fork返回新创建子进程的进程ID。我们可以通过fork返回的值来判断当前进程是子进程还是父进程。

  • fork出错可能有两种原因:

      1)当前的进程数已经达到了系统规定的上限,这时errno的值被设置为EAGAIN。
      2)系统内存不足,这时errno的值被设置为ENOMEM。
    
  • 创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。

  • 接下来我们对fork进行深一步的理解,运行下面代码:



#include <unistd.h>  
#include <stdio.h>  
int main(void)  
{  
   int i=0;  
   printf("I son/fa ppid pid  fpid\n");  
   //pid指当前进程的父进程pid  
   //pid指当前进程的pid,  
   //fpid指fork返回给当前进程的值  
   for(i=0;i<2;i++){  
       pid_t fpid=fork();  
       if(fpid==0)  
           printf("%d son  %4d %4d %4d\n",i,getppid(),getpid(),fpid);  
       else  
           printf("%d father %4d %4d %4d\n",i,getppid(),getpid(),fpid);  
   }  
   return 0;  
}  


  • 运行结果是:

  • 分析代码我们可以得到下面的过程图:

  • 这个程序最终产生了3个子进程,执行过6次printf()函数。

  • 第一步:在父进程中,指令执行到for循环中,i=0,接着执行fork,fork执行完后,系统中出现两个进程,分别是p3224和p3225(后面我都用pxxxx表示进程id为xxxx的进程)。可以看到父进程p3224的父进程是p2043,子进程p3225的父进程正好是p3224。我们用一个链表来表示这个关系:
    p2043->p3224->p3225

  • 第一次fork后,p3224(父进程)的变量为i=0,fpid=3225(fork函数在父进程中返向子进程id),代码内容为:

      for(i=0;i<2;i++){  
      pid_t fpid=fork();//执行完毕,i=0,fpid=3225  
      if(fpid==0)  
      printf("%d child  %4d %4d %4d/n",i,getppid(),getpid(),fpid);  
      else  
      printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid);  
      }  
      return 0;  
    
  • p3225(子进程)的变量为i=0,fpid=0(fork函数在子进程中返回0),代码内容为:

      for(i=0;i<2;i++){  
      pid_t fpid=fork();//执行完毕,i=0,fpid=0  
      if(fpid==0)  
         printf("%d child  %4d %4d %4d/n",i,getppid(),getpid(),fpid);  
      else  
         printf("%d parent %4d %4d %4d/n",i,getppid(),getpid(),fpid);  
      }  
      return 0;  
    
  • 所以打印出结果:
    0 parent 2043 3224 3225
    0 child 3224 3225 0

  • 第二步:假设父进程p3224先执行,当进入下一个循环时,i=1,接着执行fork,系统中又新增一个进程p3226,对于此时的父进程,p2043->p3224(当前进程)->p3226(被创建的子进程)。

  • 对于子进程p3225,执行完第一次循环后,i=1,接着执行fork,系统中新增一个进程p3227,对于此进程,p3224->p3225(当前进程)->p3227(被创建的子进程)。从输出可以看到p3225原来是p3224的子进程,现在变成p3227的父进程。父子是相对的,这个大家应该容易理解。只要当前进程执行了fork,该进程就变成了父进程了,就打印出了parent。

  • 所以打印出结果是:
    1 parent 2043 3224 3226
    1 parent 3224 3225 3227

  • 第三步:第二步创建了两个进程p3226,p3227,这两个进程执行完printf函数后就结束了,因为这两个进程无法进入第三次循环,无法fork,该执行return 0;了,其他进程也是如此。

  • 以下是p3226,p3227打印出的结果:
    1 child 1 3227 0
    1 child 1 3226 0

  • 在p3224和p3225执行完第二个循环后,main函数就该退出,也即进程该死亡了,因为它已经做完所有事情了。p3224和p3225死亡后,p3226,p3227就没有父进程了,这在操作系统是不被允许的,所以p3226,p3227的父进程就被置为p1了,p1是永远不会死亡的。

wait

  • 由于fork产生的父子进程的执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略,所以要实现mybash必须使用wait函数。如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID.

  • wait的函数原型是:

      #include <sys/types.h> /* 提供类型pid_t的定义 */
      #include <sys/wait.h>
      pid_t wait(int *status);
    

返回值: 如果执行成功则返回子进程识别码(PID),如果有错误发生则返回-1。失败原因存于errno中。

  • 进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经 退出,如果让它找到了这样一个已经变成僵尸的子进程, wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

  • 参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就象下面这样:
    pid = wait(NULL);
    如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

  • 下面运行一个例子来理解wait调用:



#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
 
int main()
{
	pid_t pc, pr;
	pc = fork();
	if ( pc < 0 ) /* 如果出错 */
	{
		printf("create child prcocess error: %s/n", strerror(errno));
		exit(1);
	}
	else if ( pc == 0) /* 如果是子进程 */
	{
		printf("I am child process with pid %d \n", getpid());
		sleep(3);/* 睡眠3秒钟 */
		exit(0);
	}
	else /* 如果是父进程 */
	{
		printf("Now in parent process, pid = %d/n", getpid());
		printf("I am waiting child process to exit.\n");
		pr = wait(NULL); /* 在这里等待子进程结束 */
		if ( pr > 0 ) /*子进程正常返回*/
		printf("I catched a child process with pid of %d\n", pr);
		else /*出错*/
		printf("error: %s/n.\n", strerror(errno));
	}
	exit(0);
}


  • 运行结果:

  • 设定的让子进程睡眠3s的时间,只有子进程从睡眠中苏醒过来,它才能正常退出,也就才能被父进程捕捉到。这里不管设定子进程睡眠的时间有多长,父进程都会一直等待下去。

exec

  • exec命令用于调用并执行指令的命令。exec命令通常用在shell脚本程序中,可以调用其他的命令。如果在当前终端中使用命令,则当指定的命令执行完毕后会立即退出终端。

通过man -k exec来寻找相关信息,找到了符合要求的几个函数:

  • 通过帮助手册,找到了所有的语法格式:

  • int execl(const char *pathname, const char arg0, ... / (char *)0 *);
    execl()函数用来执行参数path字符串所指向的程序,第二个及以后的参数代表执行文件时传递的参数列表,最后一个参数必须是空指针以标志参数列表为空.

  • int execv(const char *path, char *const argv[]);

      execv()函数函数用来执行参数path字符串所指向的程序,第二个为数组指针维护的程序参数列表,该数组的最后一个成员必须是空指针。
    
  • int execlp(const char *filename, const char arg0, ... / (char *)0 */ );

      execlp()函数会从PATH环境变量所指的目录中查找文件名为第一个参数指示的字符串,找到后执行该文件,第二个及以后的参数代表执行文件时传递的参数列表,最后一个参数必须是空指针.
    
  • int execvp(const char *file, char *const argv[]);

      execvp()函数会从PATH环境变量所指的目录中查找文件名为第一个参数指示的字符串,找到后执行该文件,第二个及以后的参数代表执行文件时传递的参数列表,最后一个成员必须是空指针。
    
  • 由于exec函数会取代执行它的进程, 一旦exec函数执行成功, 它就不会返回了, 进程结束。但是如果exec函数执行失败, 它会返回失败的信息, 而且进程继续执行后面的代码。
    所以通常exec会放在fork() 函数的子进程部分, 来替代子进程执行, 执行成功后子程序就会消失, 但是执行失败的话, 必须用exit()函数来让子进程退出。

mybash的实现

  • 学习了fork、wait、exec后,我们可以编写一个伪代码来模拟mybash的实现:

    while(!命令结束)
    {
    取命令
    命令通过exec运行
    使用fork建立一个进程
    使用wait等待命令执行完
    }

  • mybash代码

mybash代码

  • 运行结果如下

实践感想

这次mybash的学习让我掌握了很多新的知识,更深入的理解了进程,通过fork、wait、exec这三个函数的学习,对linux shell有了更深的理解。

参考文献

linux c语言 fork() 和 exec 函数的简介和用法
linux中wait系统调用
linux中fork()函数详解
Linux编程基础之进程等待(wait()函数)