第五周加分题--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的学习让我掌握了很多新的知识,更深入的理解了进程,通过fork、wait、exec这三个函数的学习,对linux shell有了更深的理解。
参考文献
linux c语言 fork() 和 exec 函数的简介和用法
linux中wait系统调用
linux中fork()函数详解
Linux编程基础之进程等待(wait()函数)