zoukankan      html  css  js  c++  java
  • 20155326 第五周加分题--mybash的实现

    第五周加分题--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
    ",getpid());   
            printf("I'm the child
    ");
            count++;  
        }  
        else {  
            printf("i am the parent process, my process id is %d
    ",getpid());   
            printf("I'm the father
    ");  
            count++;  
        }  
        printf("统计结果是: %d
    ",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
    ");  
       //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
    ",i,getppid(),getpid(),fpid);  
           else  
               printf("%d father %4d %4d %4d
    ",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 
    ", getpid());
    		sleep(3);/* 睡眠3秒钟 */
    		exit(0);
    	}
    	else /* 如果是父进程 */
    	{
    		printf("Now in parent process, pid = %d/n", getpid());
    		printf("I am waiting child process to exit.
    ");
    		pr = wait(NULL); /* 在这里等待子进程结束 */
    		if ( pr > 0 ) /*子进程正常返回*/
    		printf("I catched a child process with pid of %d
    ", pr);
    		else /*出错*/
    		printf("error: %s/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()函数)

  • 相关阅读:
    HD2058The sum problem
    采用循环链表结构求解约瑟夫问题
    java线性表学习笔记(二)
    java线性表学习笔记(一)
    HD1004Let the Balloon Rise
    HD1005Number Sequence
    用c++库函数轻松解决回文问题
    accelerated C++ 中查找url(学习笔记)
    C++ 之关联容器 map
    pytorch 迁移学习[摘自官网]
  • 原文地址:https://www.cnblogs.com/lmc1998/p/8011063.html
Copyright © 2011-2022 走看看