实验三 实现带参数的简单Shell
1. 实验内容
利用课本第9页程序1-5的框架,实现允许输入命令带参数的简单shell。原来的实现是不能够带参数的。输入命令所能带的参数个数,只受
到系统键盘输入缓冲区长度(以及shell输入缓冲区长度)的限制,该缓冲区的缺省长度是4096个字节。
实现时要解决的主要问题有:
**1.1正确理解并使用系统调用fork(),execve()和waitpid(),特别是execve()函数。**fork()函数创建一个新的进程。新进程就是所谓的子
进程,它是执行fork()函数的进程(父进程)的“克隆”,也就是说,子进程执行的程序与父进程的完全一样。当fork()函数返回值为0时表示处
于子进程中;而返回值大于0时表示处于父进程中,此时的返回值是子进程的进程id。因此,fork()的返回值可以用来划分仅仅适合父进程
和子进程执行的程序段。fork()函数返回值为-1时表示出错。
如果子进程只是运行与父进程完全一样的程序,那用处是很有限的。要让子进程运行不同于父进程的程序,就必须调用execve函数,它是
所有其他exec函数的基础。execve函数把调用它的进程的程序,替换成execve函数的参数所指定的程序。运行execve函数成功后,进程
将开始运行新的程序,也就是execve函数的参数所指定的程序。
execve函数原型:int execve(const char *path, const char *argv[],const char *envp[]);
其中:
- path:要执行的程序路径名,比如“/bin/ls”,“cd”,“/usr/bin/gcc”等等。
- argv:参数表,比如ls命令中可带的命令行参数-l,-a等。注意,argv的第一个元素必须是要执行的程序(命令)的路径名。
- envp:环境变量表,供要执行的命令使用。实参数用NULL或系统环境变量environ均可。注意,因为environ由系统提供,属于外部变量,所以说明时必须用“extern”修饰。
例子:
char *argv[] = {“gcc”, “-g”, “-c”, “hello.c”, NULL};// 编译程序“hello.c”execve(“/bin/ls”, argv1, NULL);
char *argv1[] = {“/bin/ls”, “-l”, “-a”, NULL}; // 执行命令“ls –l –a”execve(“/usr/ls”, argv1, NULL);
execve(“/usr/bin/gcc”, argv, environ); // 出错,因为目录/usr/下没有ls程序。// 注意,在argv1 的第一个字符串“/bin/ls”中,只有ls是有用的。
系统调用waitpid()用于等待子进程结束、获取子进程的运行状态,详细说明在第八章。本实验仅仅用它使父进程等待子进程结束,因此维持程序1-5的用法即可。
1.2 根据简单shell的输入,构造execve函数的参数。
根据程序1-5,数组buf保存用户的输入,包括命令和参数。由于shell命令的命令名和各参数之间是用空格分开,因此可以用空格作为分界符。通过一个循环可以把buf数组中的命令和各个参数依次分离开来,并赋给数组argv的各元素适当的指针值。argv数组的最后一个指针必须是NULL。接着就可以调用execve(argv[0],argv, environ)来执行用户输入的命令。
提示:argv数组中各指针所指向的字符串,可以直接利用buf的存储空间,不需要另外分配内存。
2. 实验设计与实现
2.1功能概述
支持常用unix环境下命令**(pwd,ls,cd,vi,touch,rm,构成简单的shell)**
支持带任意参数命令如ls -lh ,rm -rf,cd ~ ,文件重定向等等
2.2代码框架
头文件:
#include "apue.h"
#include <errno.h>
#include <fcntl.h>
apue.h: strcmp
字符串比较、strlen
取字符串长度、strtok
字符串分割函数;
基本io函数以及dup2
、close
、chdir
、getcwd
获得当前目录、execvp
进程运行参数替换程序、fork
创建进程函
数fgets
、waitpid
(系统调用,用于等待子进程结束、获取子进程的运行状态,本实验仅仅用它使父进程等待子进程结束)、exit
退出函数
fcntl.h: 用到文件权限位、文件打开函数
errno.h:
用到errno
、strerror
出错函数、出错标志处理;
- 缓冲区长度(也可以有sysconf函数得出),最大参数个数,提示符最大长度按实验要求,均设置为MAX=4096
2.3提示字:
[Testshell 绝对路径]$,仿unix shell风格
组织提示字函数:
char pre[MAX+10]="[Testshell ";
void printpre(char* s)
{
if(getcwd(s+11,MAX)==NULL)
{
printf("getcwd error: %s\n", strerror(errno));
exit(1);
}
strcat(s, "]$");
}
很简单的思路,调用getcwd得到当前目录存在s+11位置之后,("[Testshell "是11个字符),最后再使用strcat将后半部分提示字拼接到后面~
2.4 构建argv
ISO C标准规定的string.h
头文件中,包括了strtok
这一函数,因此我们完全可以直接调用该函数对字符串进行切分,而无需手动操作,但如果不调用strtok
函数,需要进行复杂的字符串模拟,可以编写如下:
//Filter commands for extra Spaces
int argc=0,len,i;
while(cmd[i] == ' ')i++;//Remove the leading space
argv[argc++]=cmd+i; //first position of space
for(len=strlen(cmd),i=0;i<len;i++)
{
if (cmd[i]==' ')cmd[i] = 0;
else
{
//Fill in the command parameters
if((i-1>=0)&&(!buf[i-1]==0)) argv[argc++] = cmd+i;
}
}
argv[argc] = NULL;//last argv is NULL
调用strtok
,很方便就可以提取出输入指令参数:
char *token = strtok(cmd, " ");
while (token != NULL)
{
...
token = strtok(NULL, " ");
}
由于本Shell还具有输入输出重定向功能,因此需要处理< filename
和> filename
的情况。因此在token
为<
或>
时,设置相应的flag
:0为正常参数,1代表输入重定向,2代表输出重定向。之后,下一个读入的参数token
会根据flag
的值设置重定向的输入文件名rfile
和重定向的输出文件名wfile
。
将上述功能封装成construct_argv
函数,完整代码如下:
void construct_argv(char *cmd, char **argv, char **rfile, char **wfile)
{
int flag = 0; // 1 for rfile, 2 for wfile
*rfile = NULL;
*wfile = NULL;
cmd[strlen(cmd) - 1] = '\0';
int argc = 0;
char *token = strtok(cmd, " ");
while (token != NULL)
{
if (flag == 1)
{
*rfile = token;
flag = 0;
}
else if (flag == 2)
{
*wfile = token;
flag = 0;
}
else if (strcmp(token, "<") == 0)
flag = 1;
else if (strcmp(token, ">") == 0)
flag = 2;
else
argv[argc++] = token;
token = strtok(NULL, " ");
}
argv[argc] = NULL;
}
2.5 exec函数族:
1.execl int execl(const char *path, const char *arg, ...);
2.execlp int execlp(const char *file, const char *arg, ...);
3.execv int execv(const char *path, char *const argv[]);
4.execvp int execvp(const char *file, char *const argv[]);
5.execle int execle(const char *path, const char *arg,
..., char * const envp[]);
6.execvpe int execvpe(const char *file, char *const argv[],
char *const envp[]);
**path:**可执行文件的路径名字
**arg:**可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束。
**file:**如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件
excel 、execv都是需要给出可执行文件名的绝对路径,execlp、execvp则不需要,它们两者的区别是execvp函数参数是一个argv参数表,而execlp是一项一项给出参数,这些函数族最终都是调用execve系统调用。
后缀名总结:
l:表示list,即每个命令行参数都说明为一个单独的参数
v:表示vector,命令行参数放在数组中
e:调用者提供环境表
p:表示通过环境变量PATH,查找执行文件
f:表示以文件描述符为第一个参数
一般命令都可以由fork
+execvp
执行,由fork
创建一个子进程,调用一种exec函数时,该进程执行的程序完全替换为新程序
而新程序则从其main函数开始执行; 但要注意exec并不创建新进程,所以前后的进程ID并未改变,exec只是用一个全新的程序替换了
当前进程的正文、数据、堆和栈段。
因此,我们可以使用execvp
函数,仅需传入之前构造的argv
参数,从而间接执行系统调用execve
:
if ((pid=fork())<0)printf("fork error: %s\n", strerror(errno));
else if(pid==0) // child
{
redirect_stdin(rfile);
redirect_stdout(wfile);
execvp(argv[0], argv);
printf("execvp error: %s\n", strerror(errno));
exit(1);
}
else if((pid=waitpid(pid,&status,0))<0) // parent
printf("waitpid error: %s\n", strerror(errno));
代码中先调用fork创建子进程若出错则打印出错信息,pid=0表示在子进程中,若有重定向输入输出,则在redirect_stdin或
redirect_stdout中处理,execvp填入可执行文件参数,子进程开始执行,若出错才会执行下面的execvp error打印错误语句,waitpid等
待特定fork后子进程号结束,若出错则同样做出错打印信息处理
2.6 cd命令
对于一个自制shell如果没有cd命令就不算一个合格的shell,因为cd是shell内部命令,如果用execve系统调用,fork出子进程改变的是子
进程的目录,父进程的目录仍然没有发生改变。所以本实验中如果不做特殊处理,cd命令不会成功运行,需要手动编写一个简单函数,思
路也很简单,对于一般的cd 路径名
,我们可以采用chdir函数切换到相应目录,注意到一般shell有cd
、cd ~
,两种形式,我们可以特
判将参数argv[1]等于使用getenv("HOME")
获取家目录的环境变量:
if (!strcmp(argv[0],"cd")) // cd command
{
if (argv[1]==NULL||!strcmp(argv[1],"~"))argv[1]=getenv("HOME");
if(chdir(argv[1])<0)printf("chdir error: %s\n", strerror(errno));
}
2.7 输入输出重定向
在执行其他命令时,调用了自己写的redirect_stdin
和redirect_stdout
两个函数。这两个函数通过open
命令,将之前获取的rfile
和wfile
文件打开,获取File descriptor后,再使用dup2
函数重定向STDIN_FILENO
和STDOUT_FILENO
,open函数采用权限位为644:
void redirect_stdin(char *rfile)
{
int readfd;
if (rfile!=NULL)
{
if((readfd=open(rfile,O_RDONLY))<0)
{
printf("open error: %s\n", strerror(errno));
exit(1);
}
if(dup2(readfd,STDIN_FILENO)<0)
{
printf("dup2 error: %s\n", strerror(errno));
exit(1);
}
close(readfd);
}
}
void redirect_stdout(char *wfile)
{
int writefd;
if (wfile!=NULL)
{
if ((writefd=open(wfile, O_WRONLY|O_CREAT|O_TRUNC,644)) < 0)
{
printf("open error: %s\n", strerror(errno));
exit(1);
}
if (dup2(writefd,STDOUT_FILENO)<0)
{
printf("dup2 error: %s\n", strerror(errno));
exit(1);
}
close(writefd);
}
}
3.实验结果
3.1编译运行:
gcc shell.c -o shell
编译成功、提示字正常显示
3.2 测试常见命令
cd
、pwd
、who
、mkdir
、rm -r
、vi
、cd
带参数、cat
、ls
带参数:
可以看到,这个自制shell基本能够处理大多数命令,能够成功解析出参数,包括输入输出重定向等更复杂的命令,完成了本实验需要做的基本操作,其中拓展了cd命令以及重定向输入输出命令。
4.实验不足与心得
实验不足:毕竟是套壳的shell,没有正宗shell支持tab补全、回溯上一条命令,复制粘贴、退出输入输出、特定类型高亮等快捷方式。
实验心得:哪怕是一个小小的shell黑窗体,开发难度都不容小觑,要考虑到多种情况,比如用户的输入输出、随着支持功能的拓展,复杂性激增,需要团队协作、科学的软件工程理念指导才能开发出可移植性强、功能丰富的shell。
附,shell.c:
//
// main.cpp
// shell
//
// Created by apple on 2020/11/5.
//
#include "apue.h"
#include <errno.h>
#include <fcntl.h>
#define MAX 4096
char pre[MAX+10]="[Testshell ";
char buf[MAX];
char *argv[MAX];
char *rfile, *wfile;
int status;
pid_t pid;
void readgrgv(char *buf, char **argv, char **rfile, char **wfile);
void printpre(char* s);
void redirect_stdin(char *rfile);
void redirect_stdout(char *wfile);
int main()
{
printpre(pre);
printf("%s",pre);
while(fgets(buf,MAX-1,stdin)!=NULL)
{
readgrgv(buf,argv,&rfile,&wfile);
if (!strcmp(argv[0],"cd")) // cd command
{
if (argv[1]==NULL||!strcmp(argv[1],"~"))
{
argv[1]=getenv("HOME");
}
if(chdir(argv[1])<0)printf("chdir error: %s\n", strerror(errno));
}
else // other command
{
if ((pid=fork())<0)printf("fork error: %s\n", strerror(errno));
else if(pid==0) // child
{
redirect_stdin(rfile);
redirect_stdout(wfile);
execvp(argv[0], argv);
printf("execvp error: %s\n", strerror(errno));
exit(1);
}
else if((pid=waitpid(pid,&status,0))<0) // parent
printf("waitpid error: %s\n", strerror(errno));
}
printpre(pre);
printf("%s",pre);
}
return 0;
}
void printpre(char* s)
{
if(getcwd(s+11,MAX)==NULL)
{
printf("getcwd error: %s\n", strerror(errno));
exit(1);
}
strcat(s,"]$");
}
void readgrgv(char *buf, char **argv, char **rfile, char **wfile)
{
int flag = 0; // 1 for rfile, 2 for wfile
*rfile=NULL;
*wfile=NULL;
buf[strlen(buf)-1] = '\0';
int argc = 0;
char *token = strtok(buf, " ");
while(token!=NULL)
{
if(flag==1)
{
*rfile=token;
flag = 0;
}
else if (flag==2)
{
*wfile=token;
flag=0;
}
else if (strcmp(token,"<")==0)
flag = 1;
else if (strcmp(token, ">") == 0)
flag = 2;
else
argv[argc++] = token;
token = strtok(NULL, " ");
}
argv[argc]=NULL;
}
void redirect_stdin(char *rfile)
{
int readfd;
if (rfile!=NULL)
{
if((readfd=open(rfile,O_RDONLY))<0)
{
printf("open error: %s\n", strerror(errno));
exit(1);
}
if(dup2(readfd,STDIN_FILENO)<0)
{
printf("dup2 error: %s\n", strerror(errno));
exit(1);
}
close(readfd);
}
}
void redirect_stdout(char *wfile)
{
int writefd;
if (wfile!=NULL)
{
if ((writefd=open(wfile, O_WRONLY|O_CREAT|O_TRUNC,644)) < 0)
{
printf("open error: %s\n", strerror(errno));
exit(1);
}
if (dup2(writefd, STDOUT_FILENO) < 0)
{
printf("dup2 error: %s\n", strerror(errno));
exit(1);
}
close(writefd);
}
}