TASK
打造一个绝无伦比的 xxx-super-shell
(xxx
是你的名字),它能实现下面这些功能:
- 实现 管道 (也就是
|
) - 实现 输入输出重定向(也就是
> < >>
) - 要求实现 在管道组合命令的两端实现重定向运算符
# Require
cat < 1.txt | grep -C 10 abc | grep -L efd | tac >> 2.txt
# Does not require
cat < 1.txt | grep -C 10 abc > test1.txt | test2.txt > grep -L efd | tac >> 2.txt
- 实现 后台运行(也就是
&
) - 实现
cd
,要求支持能切换到绝对路径,相对路径和支持**cd -**
- 屏蔽一些信号(如
ctrl + c
不能终止) - 界面美观
- 开发过程记录、总结、发布在个人博客中
要求: - 不得出现内存泄漏,内存越界等错误
- 学会如何使用 gdb 进行调试,使用 valgrind 等工具进行检测
测试用例
xxx@xxx ~ $ ./xxx-super-shell
xxx@xxx ~ $ echo ABCDEF
xxx@xxx ~ $ echo ABCDEF > ./1.txt
xxx@xxx ~ $ cat 1.txt
xxx@xxx ~ $ ls -t >> 1.txt
xxx@xxx ~ $ ls -a -l | grep abc | wc -l > 2.txt
xxx@xxx ~ $ python < ./1.py | wc -c
xxx@xxx ~ $ mkdir test_dir
xxx@xxx ~/test_dir $ cd test_dir
xxx@xxx ~ $ cd -
xxx@xxx ~/test_dir $ cd -
xxx@xxx ~ $ ./xxx-super-shell # shell 中嵌套 shell
xxx@xxx ~ $ exit
xxx@xxx ~ $ exit
- 核心为掌握
Linux系统编程
中进程
的部分
框架主体
main()
从main函数来分析实现的整体框架
int main(){
signal(SIGINT,SIG_IGN);//屏蔽ctrl+c
signal(SIGTSTP,SIG_IGN); //屏蔽ctrl+z
while(1){
char*argv[MAX]={NULL};
printname();
char*command=readline(" ");//readline函数输出给出的字符串并读取一行输入,并为读取的输入动态分配内存,返回值为指向读取输入的指针
if (command == NULL) continue;//屏蔽ctrl+d
if (strlen(command) == 0) continue;//回车不爆栈
int argc=1;
argv[0] = strtok(command, " ");
for(int i=1;argv[i] = strtok(NULL, " ");i++) argc++;//将命令行输入分割为多个命令
analyze_cmd(argc,argv);//解析命令
do_cmd(argc,argv);//实现命令
free(command); //释放空间
clear_para();//重置参数
}
}
-
一些声明如MAX可以结合文章最后的全部代码查看
-
首先要调用
signal
函数屏蔽一些信号 -
由于shell是
交互进程
(进程分类详见此博客(超链接)),所以我们进入while(1)循环,注意,这里的argc
和argv
不是main函数的参数,而是我们自定义的参数
,意义和main函数的参数类似。 -
printname函数负责每次输入命令前和后的终端名字显示和路径显示,具体下文给出。
-
这里使用了一个动态链接库readlilne,需要我们单独下载并通过相应头文件使用,具体点击这里。功能有很多,我们这里用到的是
readline()
函数读取输入和显示历史命令
-
整体框架已经有了,下面给出各个部分的详细解释
接口详解
printname()
void printname(){
char pathname[PATHMAX];
getcwd(pathname,PATHMAX);//获取当前目录
printf(BLUE"Whosefrienda-shell"CLOSE);//打印shell名称
printf(GREEN" :%s"CLOSE,pathname);//打印路径
printf("$ ");
fflush(stdout);//清除缓冲区
}
-
具体注释有解释,(下文代码也是主要看注释,特殊的具体解释)这里只说一下这个
BLUE
和GREEN
和CLOSE
是通过宏定义
实现的,定义如下#define BLUE "\033[34m"//宏定义实现有色字体 #define GREEN "\033[32m" #define CLOSE "\033[0m"
analyze_cmd(argc,argv)()
这里用了全局变量
int cd =0;
int i_redir=0;
int o_redir=0;
int _pipe=0;
int a_o_redir=0;
int pass=0;//命令解析的参数
code
int analyze_cmd(int argc,char*argv[]){
if (argv[0] == NULL) return 0;
if (strcmp(argv[0], "cd") == 0) cd = 1;
for (int i = 0; i < argc; i++){
if (strcmp(argv[i], ">") == 0) o_redir = 1;
if (strcmp(argv[i], "|") == 0) _pipe = 1;
if (strcmp(argv[i], ">>") == 0) a_o_redir = 1;
if (strcmp(argv[i], "<") == 0) i_redir = 1;
if (strcmp(argv[i], "&") == 0){
pass = 1;
argv[i]=NULL;
}
}
}
每个参数都在``do_cmd```函数中辅助判定,从而使用不同的接口来实现命令。
void do_cmd(int argc,char*argv[])
void do_cmd(int argc,char*argv[]){
if(pass==1) argc--;
if (cd == 1) mycd(argv);
else if (strcmp(argv[0], "history") == 0) showhistory();//展示历史命令
else if (strcmp(argv[0], "exit") == 0)
{
printf("exit\n");
printf("有停止的任务\n");
exit(0);
}
else if ( i_redir== 1) iredir(argv);// <
else if ( o_redir== 1) oredir(argv);// >
else if ( a_o_redir== 1) aoredir(argv);// >>
else if ( _pipe == 1) mymulpipe(argv, argc);// | 管道放在最后判定,因为重定向中也有管道的判定
else //需要fork子进程进行执行的命令
{
if (pid < 0)
{
perror("fork");
exit(1);
}
else if (pid == 0) //子进程
{
execvp(argv[0], argv);
perror("command");
exit(1);
}
else if (pid > 0) //父进程
{
if(pass==1)
{
printf("%d\n",pid);
return;
}
waitpid(pid, NULL, 0);
}
}
}
这个接口实际上通过判定参数真假值来调用其他函数来实现命令,本身只实现没有重定向和管道等的需要fork
和execve
的简单命令
- 这里只讲一下
fork子进程
实现的命令,其他在下面的具体接口再详解 - fork返回两个
pid值
,一个是父进程的,一个是子进程的,fork后的代码会被父进程和子进程分别执行一遍,所以需要进行判定来分别编写父进程和子进程需要执行的代码 - 这里,子进程需要调用
execvp
来加载命令实现需要的代码 - 父进程则调用
waitpid
来监控子进程的进行,并且在有&
的情况下将控制权重新交给主函数,从而让子进程在后台执行
命令的同时不影响shell前台继续执行新命令
void showhistory()
void showhistory()
{
int i = 0;
HIST_ENTRY **his;
his = history_list();
while (his[i] != NULL)
printf("%-3d %s\n", i, his[i++]->line);
}
这里的HIST_ENTRY
类型和history_list
函数都在<readline/history.h>
中有定义
void mycd(char *argv[])
char lastpath[MAX];//为实现cd-而声明
void mycd(char *argv[]){
if (argv[1] == NULL)//未输入要跳转的目录的情况
{
getcwd(lastpath, sizeof(lastpath));
chdir("/home");
}
else if (strcmp(argv[1], "-") == 0)//实现cd -
{
char newlastpath[MAX];
getcwd(newlastpath, sizeof(lastpath));
chdir(lastpath);
printf("%s\n", lastpath);
strcpy(lastpath, newlastpath);
}
else if (strcmp(argv[1], "~") == 0)//跳转主目录(这里的代码是跳转到我自己的主目录wanggang)
{
getcwd(lastpath, sizeof(lastpath));
chdir("/home/wanggang");
}
else
{
getcwd(lastpath, sizeof(lastpath));
chdir(argv[1]);//跳转到输入的路径名
}
}
- 为实现
cd-
,声明了lastpath
来记录之前的路径 - 主要调用
chdir
来改变当前路径
void oredir(char *argv[])
void oredir(char *argv[]){
char *preargv[MAX] = {NULL};
int i = 0;
while (strcmp(argv[i], ">"))
{
preargv[i] = argv[i];
i++;
}
int preargc=i;//重定向前面参数的个数
i++;
int fdout = dup(1);//让标准输出获取一个新的文件描述符
int fd = open(argv[i], O_WRONLY | O_CREAT | O_TRUNC,0666); //只写模式|表示如果指定文件不存在,则创建这个文件|表示截断,如果文件存在,并且以只写、读写方式打开,则将其长度截断为0。
dup2(fd, 1);
pid_t pid = fork();
if (pid == 0) //子进程
{
if (_pipe=1) //管道'|'
{
mymulpipe(preargv, preargc);
}
else
execvp(preargv[0], preargv);
}
else if (pid > 0)//父进程
{
if(pass==1)
{
printf("%d\n",pid);
return;
}
waitpid(pid, NULL, 0);
}
dup2(fdout, 1);
}
- 定义
preargv
将重定向符之前的命令保存,并获得重定向符之后的文件描述符
(没有该文件就创建一个) - fork子进程运行preargv里保存的命令
- 最后将获得的文件描述符
重定向
到标准输出
void aoredir(char *argv[])
void aoredir(char *argv[]){
char *preargv[MAX] = {NULL};
int i = 0;
while (strcmp(argv[i], ">>"))
{
preargv[i] = argv[i];
i++;
}
int preargc=i;//重定向前面参数的个数
i++;
int fdout = dup(1);//让标准输出获取一个新的文件描述符
int fd = open(argv[i], O_WRONLY | O_CREAT | O_APPEND,0666); //只写模式|表示如果指定文件不存在,则创建这个文件|表示追加,如果原来文件里面有内容,则这次写入会写在文件的最末尾。
pid_t pid = fork();
dup2(fd, 1);
if (pid == 0) //子进程
{
if (_pipe == 1) //管道'|'
{
mymulpipe(preargv, preargc);
}
else
execvp(preargv[0], preargv);
}
else if (pid > 0)
{
if(pass==1)
{
printf("%d\n",pid);
return;
}
waitpid(pid, NULL, 0);
}
dup2(fdout, 1);
}
- 基本和前面的oredir(
output redirect
的缩写)函数一样,区别在于调用open时使用的参数不同
void iredir(char *argv[])
void iredir(char *argv[]){
char *preargv[MAX] = {NULL};
int i = 0;
while (strcmp(argv[i], "<"))
{
preargv[i] = argv[i];
i++;
}
i++;
int preargc=i;//重定向前面参数的个数
int fdin = dup(0);//让标准输入获取一个新的文件描述符
int fd = open(argv[i], O_RDONLY,0666); //只读模式
dup2(fd, 0);
pid_t pid = fork();
if (pid == 0) //子进程
{
if (_pipe == 1) //管道'|'
{
mymulpipe(preargv, preargc);
}
else
execvp(preargv[0], preargv);
}
else if (pid > 0)
{
waitpid(pid, NULL, 0);
}
dup2(fdin, 0);
}
- 代码思路和前面的标准输出重定向基本相同,不同在于使用的
文件描述符
不同
void mymulpipe(char *argv[], int argc )
void mymulpipe(char *argv[], int argc ){
pid_t pid;
int index[10];//存放每个管道的下标
int number=0;//统计管道个数
for(int i=0;i<argc;i++)
if(!strcmp(argv[i],"|")) index[number++]=i;
int cmdcount=number+1;//命令个数
char* cmd[cmdcount][10];
for(int i=0;i<cmdcount;i++)//将命令以管道分割存放组数组里
{
if(i==0)
{
int n=0;
for(int j=0;j<index[i];j++)
{
cmd[i][n++]=argv[j];
}
cmd[i][n]=NULL;
}
else if(i==number)
{
int n=0;
for(int j=index[i-1]+1;j<argc;j++)
{
cmd[i][n++]=argv[j];
}
cmd[i][n]=NULL;
}
else
{
int n=0;
for(int j=index[i-1]+1;j<index[i];j++)
{
cmd[i][n++]=argv[j];
}
cmd[i][n]=NULL;
}
}//命令已经分割好了,下面可以创建管道了。
int fd[number][2]; //存放管道的描述符
for(int i=0;i<number;i++)//循环创建多个管道
{
pipe(fd[i]);
}
int i=0;
for(i=0;i<cmdcount;i++)//父进程循环创建多个并列子进程
{
pid=fork();
if(pid==0)//子进程退出,防止创建过多进程
break;
}
if(pid==0)//子进程
{
if(number)
{
if(i==0)//第一个子进程
{
dup2(fd[0][1],1);//绑定写端`
close(fd[0][0]);//关闭读端
//其他进程读写端全部关闭
for(int j=1;j<number;j++)
{
close(fd[j][1]);
close(fd[j][0]);
}
}
else if(i==number)//最后一个子进程
{
dup2(fd[i-1][0],0);//打开读端
close(fd[i-1][1]);//关闭写端
//其他进程读写端全部关闭
for(int j=0;j<number-1;j++)
{
close(fd[j][1]);
close(fd[j][0]);
}
}
else //中间进程
{
dup2(fd[i-1][0],0);//前一个管道的读端打开
close(fd[i-1][1]);//前一个写端关闭
dup2(fd[i][1],1);//后一个管道的写端打开
close(fd[i][0]);//后一个读端关闭
//其他的全部关闭
for(int j=0;j<number;j++)
{
if(j!=i&&j!=(i-1))
{
close(fd[j][0]);
close(fd[j][1]);
}
}
}
}
execvp(cmd[i][0],cmd[i]);//执行命令
perror("execvp");
exit(1);
}
else{//父进程
for(i=0;i<number;i++)
{
close(fd[i][0]);
close(fd[i][1]);//父进程端口全部关掉
}
if(pass==1)
{
pass=0;
printf("%d\n",pid);
return;
}
for(int j=0;j<cmdcount;j++)//父进程等待子进程
wait(NULL);
}
- 关于
管道
是什么,可以点击这里。 - 这里分两大步,第一步是通过一个二维数组将各个
管道两端的命令
分隔开 - 第二步是fork出
相应数量的进程
并创建相应数量的管道
来执行命令
全部代码
#include<stdio.h>
#include<string.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<stdlib.h>
#include<unistd.h>
#include<fcntl.h>
#include<dirent.h>
#include<readline/readline.h>
#include <readline/history.h>
#include<errno.h>
#define PATHMAX 4096
#define MAX 200
#define BLUE "\033[34m"//宏定义实现有色字体
#define GREEN "\033[32m"
#define CLOSE "\033[0m"
int cd =0;
int i_redir=0;
int o_redir=0;
int _pipe=0;
int a_o_redir=0;
int pass=0;//命令解析的参数
void printname(void);
int analyze_cmd(int,char**);
void showhistory();
void mycd(char *argv[]);
void oredir(char *argv[]);
void aoredir(char *argv[]);
void iredir(char *argv[]);
void mymulpipe(char *argv[], int );
void do_cmd(int,char**);
void clear_para();
int main(){
read_history(NULL);
signal(SIGINT,SIG_IGN);//屏蔽ctrl+c
signal(SIGTSTP,SIG_IGN); //屏蔽ctrl+z
while(1){
char*argv[MAX]={NULL};
printname();
char*command=readline(" ");//readline函数输出给出的字符串并读取一行输入,并为读取的输入动态分配内存,返回值为指向读取输入的指针
if (command == NULL) {printf("\n");continue;}//屏蔽ctrl+d
if (strlen(command) == 0) continue;//回车不爆栈
int argc=1;
argv[0] = strtok(command, " ");
for(int i=1;argv[i] = strtok(NULL, " ");i++) argc++;//将命令行输入分割为多个命令
analyze_cmd(argc,argv);//解析命令
do_cmd(argc,argv);//实现命令
free(command); //释放空间
clear_para();//重置参数
}
}
void printname(){
char pathname[PATHMAX];
getcwd(pathname,PATHMAX);//获取当前目录
printf(BLUE"Whosefrienda-shell"CLOSE);//打印shell名称
printf(GREEN" :%s"CLOSE,pathname);//打印路径
printf("$ ");
fflush(stdout);//清除缓冲区
}
int analyze_cmd(int argc,char*argv[]){
if (argv[0] == NULL) return 0;
if (strcmp(argv[0], "cd") == 0) cd = 1;
for (int i = 0; i < argc; i++){
if (strcmp(argv[i], ">") == 0) o_redir = 1;
if (strcmp(argv[i], "|") == 0) _pipe = 1;
if (strcmp(argv[i], ">>") == 0) a_o_redir = 1;
if (strcmp(argv[i], "<") == 0) i_redir = 1;
if (strcmp(argv[i], "&") == 0){
pass = 1;
argv[i]=NULL;
}
}
}
void do_cmd(int argc,char*argv[]){
if(pass==1) argc--;
if (cd == 1) mycd(argv);
else if (strcmp(argv[0], "history") == 0) showhistory();//展示历史命令
else if (strcmp(argv[0], "exit") == 0)
{
printf("exit\n");
printf("有停止的任务\n");
exit(0);
}
else if ( i_redir== 1) iredir(argv);// <
else if ( o_redir== 1) oredir(argv);// >
else if ( a_o_redir== 1) aoredir(argv);// >>
else if ( _pipe == 1) mymulpipe(argv, argc);// | 管道放在最后判定,因为重定向中也有管道的判定
else //需要fork子进程进行执行的命令
{
if (strcmp(argv[0], "ls") == 0)
argv[argc++] = "--color=auto";
pid_t pid = fork();
if (pid < 0)
{
perror("fork");
exit(1);
}
else if (pid == 0) //子进程
{
execvp(argv[0], argv);
perror("command");
exit(1);
}
else if (pid > 0) //父进程
{
if(pass==1)
{
printf("%d\n",pid);
return;
}
waitpid(pid, NULL, 0);
}
}
}
void showhistory()
{
int i = 0;
HIST_ENTRY **his;
his = history_list();
while (his[i] != NULL)
printf("%-3d %s\n", i, his[i++]->line);
}
char lastpath[MAX];//为实现cd-而声明
void mycd(char *argv[]){
if (argv[1] == NULL)//未输入要跳转的目录的情况
{
getcwd(lastpath, sizeof(lastpath));
chdir("/home");
}
else if (strcmp(argv[1], "-") == 0)//实现cd -
{
char newlastpath[MAX];
getcwd(newlastpath, sizeof(lastpath));
chdir(lastpath);
printf("%s\n", lastpath);
strcpy(lastpath, newlastpath);
}
else if (strcmp(argv[1], "~") == 0)//跳转主目录(这里的代码是跳转到我自己的主目录wanggang)
{
getcwd(lastpath, sizeof(lastpath));
chdir("/home/wanggang");
}
else
{
getcwd(lastpath, sizeof(lastpath));
chdir(argv[1]);
}
}
void oredir(char *argv[]){
char *preargv[MAX] = {NULL};
int i = 0;
while (strcmp(argv[i], ">"))
{
preargv[i] = argv[i];
i++;
}
int preargc=i;//重定向前面参数的个数
i++;
//出现 echo "adcbe" > test.c 这种情况
int fdout = dup(1);//让标准输出获取一个新的文件描述符
int fd = open(argv[i], O_WRONLY | O_CREAT | O_TRUNC,0666); //只写模式|表示如果指定文件不存在,则创建这个文件|表示截断,如果文件存在,并且以只写、读写方式打开,则将其长度截断为0。
dup2(fd, 1);
pid_t pid = fork();
if (pid == 0) //子进程
{
if (_pipe=1) //管道'|'
{
mymulpipe(preargv, preargc);
}
else
execvp(preargv[0], preargv);
}
else if (pid > 0)//父进程
{
if(pass==1)
{
printf("%d\n",pid);
return;
}
waitpid(pid, NULL, 0);
}
dup2(fdout, 1);//
}
void aoredir(char *argv[]){
char *preargv[MAX] = {NULL};
int i = 0;
while (strcmp(argv[i], ">>"))
{
preargv[i] = argv[i];
i++;
}
int preargc=i;//重定向前面参数的个数
i++;
int fdout = dup(1);//让标准输出获取一个新的文件描述符
int fd = open(argv[i], O_WRONLY | O_CREAT | O_APPEND,0666); //只写模式|表示如果指定文件不存在,则创建这个文件|表示追加,如果原来文件里面有内容,则这次写入会写在文件的最末尾。
pid_t pid = fork();
dup2(fd, 1);
if (pid == 0) //子进程
{
if (_pipe == 1) //管道'|'
{
mymulpipe(preargv, preargc);
}
else
execvp(preargv[0], preargv);
}
else if (pid > 0)
{
if(pass==1)
{
printf("%d\n",pid);
return;
}
waitpid(pid, NULL, 0);
}
dup2(fdout, 1);
}
void iredir(char *argv[]){
char *preargv[MAX] = {NULL};
int i = 0;
while (strcmp(argv[i], "<"))
{
preargv[i] = argv[i];
i++;
}
i++;
int preargc=i;//重定向前面参数的个数
int fdin = dup(0);//让标准输入获取一个新的文件描述符
int fd = open(argv[i], O_RDONLY,0666); //只读模式
dup2(fd, 0);
pid_t pid = fork();
if (pid == 0) //子进程
{
if (_pipe == 1) //管道'|'
{
mymulpipe(preargv, preargc);
}
else
execvp(preargv[0], preargv);
}
else if (pid > 0)
{
waitpid(pid, NULL, 0);
}
dup2(fdin, 0);
}
void mymulpipe(char *argv[], int argc ){
pid_t pid;
int index[10];//存放每个管道的下标
int number=0;//统计管道个数
for(int i=0;i<argc;i++)
if(!strcmp(argv[i],"|")) index[number++]=i;
int cmdcount=number+1;//命令个数
char* cmd[cmdcount][10];
for(int i=0;i<cmdcount;i++)//将命令以管道分割存放组数组里
{
if(i==0)
{
int n=0;
for(int j=0;j<index[i];j++)
{
cmd[i][n++]=argv[j];
}
cmd[i][n]=NULL;
}
else if(i==number)
{
int n=0;
for(int j=index[i-1]+1;j<argc;j++)
{
cmd[i][n++]=argv[j];
}
cmd[i][n]=NULL;
}
else
{
int n=0;
for(int j=index[i-1]+1;j<index[i];j++)
{
cmd[i][n++]=argv[j];
}
cmd[i][n]=NULL;
}
}//命令已经分割好了,下面可以创建管道了。
int fd[number][2]; //存放管道的描述符
for(int i=0;i<number;i++)//循环创建多个管道
{
pipe(fd[i]);
}
int i=0;
for(i=0;i<cmdcount;i++)//父进程循环创建多个并列子进程
{
pid=fork();
if(pid==0)//子进程退出,防止创建过多进程
break;
}
if(pid==0)//子进程
{
if(number)
{
if(i==0)//第一个子进程
{
dup2(fd[0][1],1);//绑定写端`
close(fd[0][0]);//关闭读端
//其他进程读写端全部关闭
for(int j=1;j<number;j++)
{
close(fd[j][1]);
close(fd[j][0]);
}
}
else if(i==number)//最后一个子进程
{
dup2(fd[i-1][0],0);//打开读端
close(fd[i-1][1]);//关闭写端
//其他进程读写端全部关闭
for(int j=0;j<number-1;j++)
{
close(fd[j][1]);
close(fd[j][0]);
}
}
else //中间进程
{
dup2(fd[i-1][0],0);//前一个管道的读端打开
close(fd[i-1][1]);//前一个写端关闭
dup2(fd[i][1],1);//后一个管道的写端打开
close(fd[i][0]);//后一个读端关闭
//其他的全部关闭
for(int j=0;j<number;j++)
{
if(j!=i&&j!=(i-1))
{
close(fd[j][0]);
close(fd[j][1]);
}
}
}
}
execvp(cmd[i][0],cmd[i]);//执行命令
perror("execvp");
exit(1);
}
else{//父进程
for(i=0;i<number;i++)
{
close(fd[i][0]);
close(fd[i][1]);//父进程端口全部关掉
}
if(pass==1)
{
pass=0;
printf("%d\n",pid);
return;
}
for(int j=0;j<cmdcount;j++)//父进程等待子进程
wait(NULL);
}
}
void clear_para(){
cd =0;
i_redir=0;
o_redir=0;
_pipe=0;
a_o_redir=0;
}
参考资料
1.《Linux/Unix系统编程手册》
2.学长的shell