20165324_mybash
实验要求
- 实验要求:
- 使用fork,exec,wait实现mybash
- 写出伪代码,产品代码和测试代码
- 发表知识理解,实现过程和问题解决的博客(包含代码托管链接)
背景知识
Shell的介绍
-
shell是一个程序,可以称之为壳程序,用于用户与操作系统进行交互。用来区别与核,相当于是一个命令解析器,Shell有很多中,这里列出其中几种
- Bourne SHell(sh)
- Bourne Again SHell(bash)
- C SHell(csh)
- KornSHell(ksh)
- zsh
-
各个shell的功能都差不太多,在某些语法的下达下面有些区别,Linux预设就是bash。这里主要介绍bash
bash
- bash命令是sh命令的超集大多数sh脚本都可以在bash下运行,bash主要有如下这些功能:
- 记录历史命令:bash可以记录曾经的命令,保持在~/.bash_history文件中,只保存上次注销登录之后的命令。
- tab键自动补全:使用tab见可以自动不全命令或者目录。
- alias命令别名:可以使用
alias ll='ls -al'
来设置命令的别名。 - 工作控制:可以将某些任务放在后台去运行,这里不多种介绍。
- 程序脚本:可以执行shell脚本文件。
- 通配符:在查找相关文件或者执行相关命令时,可以使用通配符*。
- 内建命令type:可以使用type命令来查看某个命令是否为内建在bash当中的命令。
进程
- 进程的状态:
- 创建(created)
- 执行(running)
- 就绪(ready)
- 阻塞(blocked)
- 终止(terminated)
bash示例和书写流程
- 命令行输入:
gedit hello_shell.sh
- 编辑器内输入:
#!/bin/bash
for ((i=0; i<10; i++));do
echo "hello shell"
done
exit 0
- 为文件添加可执行权限:
$ chmod 755 hello_shell.sh
- 执行脚本:
$ ./hello_shell.sh
- 运行截图:
- 说明:
#!/bin/bash
:它是bash文件声明语句,表示是以/bin/bash程序执行该文件。它必须写在文件的第一行。echo "hello bash"
: 表示在终端输出“hello bash”。exit 0
: 表示返回0。在bash中,0表示执行成功,其他表示失败。
- 执行bash脚本
$ ./bash
,在终端输出“hello shell”。
fork
- fork:
- 指一个进程,包括代码、数据和分配给进程的资。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
- 一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。
- 测试代码Testfork.c
#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;
}
- 运行结果
- 代码分析: 一个进程调用fork()函数后,系统先给新的进程分配资源,例如存储数据和代码的空间。然后把原来的进程的所有值都复制到新的新进程中,只有少数值与原来的进程的值不同。相当于克隆了一个自己。 执行完fork后,进程1的变量为count=0,fpid!=0(父进程)。进程2的变量为count=0,fpid=0(子进程),这两个进程的变量都是独立的,存在不同的地址中,不是共用的,这点要注意。可以说,我们就是通过fpid来识别和操作父子进程的。
wait
- 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命令用于调用并执行指令的命令。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
- mybsah的实现代码:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <unistd.h>
#include <pwd.h>
#define PATH "/home/"
void print_info()
{
uid_t id = getuid();
char *status = "$";
if( id == 0)
{
status = "#";
}
struct passwd *p = getpwuid(id);
if(p == NULL)
{
printf("mybash>>");
fflush(stdout);
return;
}
char path[128] = {0};
getcwd(path,256);
char *q = "/";
char *s = strtok(path,"/");
while( s != NULL)
{
q = s;
s = strtok(NULL,"/");
}
char hostname[128] = {0};
gethostname(hostname,128);
printf("[%s@%s %s]%s ",p->pw_name,hostname,q,status);
}
int main()
{
while(1)
{
print_info();
char buff[128] = {0};
fgets(buff,128,stdin);
buff[strlen(buff)-1] = 0;
char *myargv[10] = {0};
char *s = strtok(buff," ");
if(s == NULL)
{
continue;
}
myargv[0] = s;
int i = 1;
while((s = strtok(NULL," ")) != NULL)
{
myargv[i++] = s;
}
if(strcmp(myargv[0],"exit") == 0)
{
exit(0);
}
else if(strcmp(myargv[0],"cd") == 0)
{
if(chdir(myargv[1]) == -1)
{
perror("chdir error");
}
continue;
}
pid_t pid = fork();
assert(pid != -1);
if(pid == 0)
{
char path[256] = {0};
if(strncmp(myargv[0],"./",2) != 0 && strncmp(myargv[0],"/",1) != 0)
{
strcpy(path,PATH);
}
strcat(path,myargv[0]);
execv(path,myargv);
perror("execv error");
exit(0);
}
else
{
wait(NULL);
}
}
}
- 运行结果