第五周加分题--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()函数)

20155326 第五周加分题--mybash的实现的更多相关文章

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

    第五周加分题--mybash的实现 题目要求 1.使用fork,exec,wait实现mybash 2.写出伪代码,产品代码和测试代码 3.发表知识理解,实现过程和问题解决的博客(包含代码托管链接) ...

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

    第五周 加分题-mybash的实现 使用fork,exec,wait实现mybash 产品代码 #include <stdio.h> #include <stdlib.h> # ...

  3. 2017-2018-1 20155320 第五周 加分题-mybash的实现

    2017-2018-1 20155320 第五周 加分题-mybash的实现 使用fork,exec,wait实现mybash 写出伪代码,产品代码和测试代码 发表知识理解,实现过程和问题解决的博客( ...

  4. 20155308 加分题-mybash的实现(第五周)

    20155308 加分题-mybash的实现(第五周) 实验要求 使用fork,exec,wait实现mybash 写出伪代码,产品代码和测试代码 发表知识理解,实现过程和问题解决的博客(包含代码托管 ...

  5. 2017-2018-1 20155239 《信息安全系统设计基础》第五周学习总结+mybash的实现

    2017-2018-1 20155239 <信息安全系统设计基础>第五周学习总结+mybash的实现 mybash的实现 使用fork,exec,wait实现mybash 写出伪代码,产品 ...

  6. 20155339 第七周加分项目 mybash的实现

    mybash的实现 要求 使用fork,exec,wait实现mybash 写出伪代码,产品代码和测试代码 发表知识理解,实现过程和问题解决的博客(包含代码托管链接) 学习相关知识 fork函数 查看 ...

  7. 程序设计入门—Java语言 第五周编程题 2井字棋(5分)

    2 井字棋(5分) 题目内容: 嗯,就是视频里说的那个井字棋.视频里说了它的基本思路,现在,需要你把它全部实现出来啦. 你的程序先要读入一个整数n,范围是[3,100],这表示井字棋棋盘的边长.比如n ...

  8. 20155322 2017-2018-1 《信息安全系统设计》第五周 MyBash实现

    #20155322 2017-2018-1<信息安全系统设计>第五周 MyBash实现 [博客目录] 实现要求 相关知识 bash fork exec wait 相关问题 fork返回两次 ...

  9. 第五周 mybash的实现

    第五周 mybash的实现 1. 使用fork,exec,wait实现mybash 2. 写出伪代码,产品代码和测试代码 3. 发表知识理解,实现过程和问题解决的博客(包含代码托管链接) 1. for ...

随机推荐

  1. qrcodenet二维码图片下扩展区域添加号段的操作

    总监安排了个任务,一个号码导出一个二维码图, 我实现了最终还能批量生成,结果主管说要在图片下边添加一行,和图片是一起的 最开始把控件的上的图给改了,结果保存起来没用,控件上的图跟要保存的不是一个事. ...

  2. Oracle to_char函数的使用方法

    Oracle to_char函数的功能是将数值型或者日期型转化为字符型,下面就为您详细介绍Oracle to_char函数的使用,希望对您能有所帮助. Postgres 格式化函数提供一套有效的工具用 ...

  3. Liunx cal

    1.命令格式: cal [参数][月份][年份] 2.命令功能: 用于查看日历等时间信息,如只有一个参数,则表示年份(1-9999),如有两个参数,则表示月份和年份 3.命令参数: -1 显示一个月的 ...

  4. HDU_1142(最短路 + dfs)

    Jimmy experiences a lot of stress at work these days, especially since his accident made working dif ...

  5. Windows下war包部署到Linux下Tomcat出现的问题

    最近,将Windows下开发的war包部署到Linux下的Tomcat时报了一个错误:tomcat error in opening zip file.按理说,如果正常,当把war包复制到webapp ...

  6. idea 高级调试技巧

    两年前写过一篇关于idea的高级用法,今天再来一篇关于调试方面的技巧讲解: 一.条件断点 循环中经常用到这个技巧,比如:遍历1个大List的过程中,想让断点停在某个特定值. 参考上图,在断点的位置,右 ...

  7. Eclipse新建JSP文件的默认编码

    默认情况下,Eclipse新建的JSP文件的编码是“ISO-8859-1”,不支持中文.需要手动修改为“UTF-8” 以下设置可使Eclipse生成的JSP文件的默认编码为“UTF-8” Window ...

  8. Spring 循环引用(一)一个循环依赖引发的 BUG

    Spring 循环引用(一)一个循环依赖引发的 BUG Spring 系列目录(https://www.cnblogs.com/binarylei/p/10198698.html) Spring 循环 ...

  9. SPring中quartz的配置(可以用实现邮件定时发送,任务定时执行,网站定时更新等)

    http://www.cnblogs.com/kay/archive/2007/11/02/947372.html 邮件或任务多次发送或执行的问题: 1.<property name=" ...

  10. visual studio 修改注释快捷键,和断点

    修改成alt+3和alt+4.效果不错 修改插入断点快捷键.这样按F12 就可以插入删除断点了.很爽 tab是批量加缩进 shift+tab 是批量减缩进