🔥 本文专栏:Linux
🌸作者主页:努力努力再努力wz

★★★ 本文前置知识:
文件系统以及文件系统调用接口
用c语言简单实现一个shell外壳程序
内容回顾
那么在此前的学习中,我们对于Linux的文件系统已经有了一个基本的概念,那么我们先来来做一个简单的回顾,那么Linux上的一切事物都可以视作文件,那么我们对于Linux上各种文件可以按照状态将其分为两类分别是打开的文件和未打开的文件,那么对于打开的文件,那么它的元数据会被加载到内存中,我们操作系统会为其定义一个file的结构体来记录其各种属性,那么管理这些文件就是管理这些文件对应的file结构体所组织而成的双链表的数据结构,那么我们要打开一个文件就得通过相应的代码来打开,那么意味着我们打开文件只能通过我们的进程来打开,那么打开文件的对象必定就是进程,而单个进程与文件之间的关系是一对多的,那么必然我们进程对应的task_struct结构体要维护一个指针数组,也就是文件描述表,其中该数组的每一个元素都指向一个打开的文件,而其中会默认打开三个标准输入输出文件,也就是stdin和stdout和stderr,那么分别对应指针数组下标0,1,2这三个位置,那么该数组的下标就是我们的文件描述符,那么我们可以用我们open系统调用接口来打开一个文件,那么open系统接口会为奇创建一个file结构体,并且在指针数组中分配一个位置的指针来指向该file结构体,并且返回该位置的数组下标也就是文件描述符来为后续比如write以及read系统接口来使用,那么这就是我们上一篇文章主要讲述的内容,如果对着部分内容还是感到有点陌生的话,那么可以去看我们的上一篇文章
引入
那么在此前我们学习Linux指令的时候,我们知道会有这样一条指令:
echo "hello Linux" > log.txt
echo "hello Linux" >>log.txt
而我们的echo指令本身是获取之后的字符串“hello Linux",然后将该字符串打印在我们的终端也就是显示器上,但是添加了之后的大于符号后,那么它会将“hello Linux”这个字符串给写入到log.txt文件中,而这就是所谓的输出重定向的现象,而如果后面是两个大于符号,那么则是在之前文件末尾继续写入,那么也就是追加重定向。
< log.txt
而小于符号则是本应该从键盘文件中读取内容,替换为读取小于符号后面的目标文件的文件内容,那么这就是输入重定向,那么输入重定向我们相对比较陌生,而输出以及追加重定向我们则比较熟悉,那么在引入了文件系统以及相关的系统调用接口之后,那么输入以及输出重定向的原理本质上就是对文件进行操作,那么我们就可以根据输入以及输出重定向的原理,结合系统调用接口来自己实现输入以及输出重定向,并且完善我们之前的shell外壳程序
输出以及输入重定向的原理以及实现
那么我们知道进程对应的task_struct结构体内部会有一个指针指向一个指针数组,那么该指针数组的每一个元素则指向该进程已经打开的文件的结构体,那么我们知道像我们的echo和pwd以及ls指令,那么这些指令在执行过后,都会在显示器终端打印信息,那么它们既然能够在显示器打印相应的内容,那么必然他们是要将它们要打印的内容给写入到显示器文件中,而我们知道我们进程会默认打开三个标准输入输出文件,那么其中下标为1的位置就是我们的显示器文件,那么他们必然就是往文件描述符下标为1的文件中做写入,而所谓的输出重定向就是原本我们要将显示器文件中写入的内容给替换写入到另一个目标文件中,所以当我们运行ehco “hello Linux” > log.txt时,我们发现我们终端不会打印“hello Linux" 字符串,而是将该字符串写入到了目标文件log.txt当中,那么这就是我们的输出重定向
那么要做到输出重定向的话,那么这里我们就得引入一个关键的系统调用接口,那么就是dup系统调用接口
dup
- 头文件:<unistd.h>
- 函数原型:
int dup(int fd)
- 原理:
那么dup接口会接受一个文件描述符fd,那么该文件描述符fd所指向的file结构体就是要复制的对象,那么接下来dup接口会从前往后线性的扫描整个指针数组,直到找到一个空的位置也就是没有指向任何结构体的指针的位置,然后将该位置的指针复制之前文件描述符fd所指向的结构体,并返回该数组下标,而如果分配复制失败则返回-1,而由于这个接口由于要线性扫描整个指针数组,所以一般不推荐使用dup这个接口
dup2
- 头文件:<unistd.h>
- 函数原型:
int dup2(int oldfd, int newfd);
- 原理:
那么dup2会接受两个参数分别是oldfd以及newfd,那么我们的oldfd就会是被复制的对象,而我们的newfd则是被替换的对象,那么dup2首先会先close掉我们的指针数组newfd下标所对应的位置,那么其中就会让其指向的结构体的引用计数减一,如果该结构体引用计数为0,那么操作系统则会回收清理该文件对应的结构体以及在内存中的元数据,那么引用计数不为0,那么则是只是将位置的指针给置空,那么close完指针数组中的newfd位置后,那么下一步就是将oldfd指针所指向的结构体给拷贝到newfd,那么此时newfd和oldfd就指向了同一个file结构体,该接口调用成功的返回值就是newfd,失败则返回-1
所以我们输出重定向就是调用该dup2接口,那么我们假设输出重定向的目标文件是log.txt,那么我们首先就是先调用open接口打开我们的log.txt,然后获得了log.txt的文件描述符,那么接着我们在调用dup2,那么将我们原本要向显示器文件也就是文件描述符为1的位置中写入的内容给写入到log.txt中,所以接下来我们就调用dup2接口,那么oldfd就是目标文件log.txt的文件描述符,而newfd则是显示器文件的文件描述符也就是1,那么这样就是实现了所谓的输出重定向,而我们知道我们open的时候可以指定我们打开该文件要进行的行为或者说模式,也就是通过第二个参数来指定,那么如果是输出重定向,那么我们每次输出都是会清空之前的文本内容,从文本开始处写入,所以我们需要或上O_TRUNC的宏定义,而追加重定向则是从之前的文本末尾处接着写入,所以追加重定向打开目标文件的open的第二个参数不能或上O_TRUNC,而是或上O_APPPEND,那么这就是我们输出以及追加重定向的原理实现
而输入重定向的话则是我们打开的模式要设置为只读打开,第二个参数要或上O_RDONLY,那么我们用open打开我们的目标文件假设为log.txt,然后得到其描述符,那么这里我们接着调用dup2替换的文件也就是键盘文件,那么对应的文件描述符就是0,所以第二个参数newfd就是0,那么这样就实现了我们的输入重定向
完善shell外壳程序
那么之前我们的shell外壳程序只是简单实现了获取用户输入的指令,那么判断指令是内置指令还是外部命令,如果是内置指令的话就交给父进程也就是shell外壳程序来执行,而如果是外部指令的话,那么则交给子进程来执行,那么子进程执行流中就会被进程替换为要执行的指令的进程的上下文,而父进程的执行流则是等待子进程的退出,获取子进程的退出码,那么这就是之前我们shell外壳程序所实现的功能
那么在此基础上,我们shell外壳程序无法进行输出以及输入重定向,那么在本文学习了输出以及输入重定向之后,我们就可以完善我们的shell外壳程序这部分功能
1.获取重定向内容
那么我们知道我们的shell外壳程序的第一个环节就是获取用户输入的指令,本质上就是获取用户输入的字符串然后将其保存在临时字符数组temp中,而用户输入指令时会手动用空格分割隔开指令部分和各个参数部分,所以下一个环节就是解析指令部分与参数部分,利用strtok函数利用空格作为分隔符将其分割的各个参数部分的字符串保存在argv字符指针数组的各个位置中,那么在解析的这一步骤中,我们用户可能有输入或者输出重定向的情况出现,那么在解析完字符串的各个参数部分后,那么我们就得从后往前线性扫描每一个元素所指向的字符串中是否出现重定向的符号也就是"<",">",">>",我们可以定义一个int类型的全局变量check_redir来追踪重定向的情况,如果出现了重定向,那么该变量则不为0,那么如果是输出重定向我们则设置为1,追加重定向设置为2,输入重定向则设置为3,然后再定义一个全局属性字符指针,因为如果出现重定向之后,那么该重定向符号的下一个位置就一定还是目标文件的文件名,所以我们得用指针来保存
实现细节:
细节1:.由于这里我们的重定向的符号以及目标文件名并不是有用的指令部分以及参数部分,所以我们接得保存完该文件名之后,就得将其分别设置为NULL,因为在进程替换的时候,会扫描我们该字符指针数组argv,直到遇到NULL结束,而重定向符号如“>”以及文件名如log.txt不是有命令行参数,所以得设置为空
细节2:这里由于我们字符指针数组argv的最后一个元素是NULL,而我们每一个位置都要调用strcmp函数来匹配,所以这里匹配的开始一定是从字符指针数组的倒数第二个位置开始匹配
细节3:这里由于我们的重定向符号以及之后的文件名不是命令行参数,而我们之前解析命令行参数argc是将其计入了的,所以我们就得重新设置我们的返回的命令行参数的个数,这是一个细节
细节4:我们这里定义了两个全局属性的变量分别追踪重定向的情况以及保存文件名,所以在每一次外部的while循环之后,我们都得将其给重新设置
代码实现:
int getString(char temp[],char* argv[])
{
int len=strlen(temp);
if(len>0&&temp[len-1]=='\n')
{
temp[len-1]='\0';
len--;
}
int argc=0;
char* toke=strtok(temp," ");
while(toke!=NULL&&argc<length-1)
{
argv[argc++]=toke;
toke=strtok(NULL," ");
}
argv[argc]=NULL;
for(int i=argc-1;i>=0;i--)
{
if(strcmp(argv[i],">")==0)
{
check_redir=1;
filename=argv[i+1];
argc=i;
argv[i]=NULL;
argv[i+1]=NULL;
break;
}
if(strcmp(argv[i],">>")==0)
{
check_redir=2;
filename=argv[i+1];
argc=i;
argv[i]=NULL;
argv[i+1]=NULL;
break;
}
if(strcmp(argv[i],"<")==0)
{
check_redir=3;
filename=argv[i+1];
argc=i;
argv[i]=NULL;
argv[i+1]=NULL;
break;
}
}
return argc;
}
2.内置命令的重定向
那么这里我们注意对于内置命令来说,那么由于我们对于一些要往显示器写入的内置命令要做重定向,所以这里我们注意由于内置命令的执行是在父进程中执行的,那么我们关闭比如显示器文件的话就一定会影响父进程,意味着之后我们还得再重新打开,因为下一个命令的执行有可能需要向显示器文件写入,所以这里我个人在实现的时候,我是的思路首先是先open打开目标文件,得到该文件的文件描述符,然后我直接将该指令要向显示器文件中写入的内容用一个数组来保存,然后利用write函数将数组的内容写入该文件,这样就避免了关闭显示器文件,这里由于我自己在实现shell外壳程序的时候,只有cd和pwd两个内置指令,所以这里就只实现了pwd内置命令的输出重定向
代码实现:
void ordercomplete(int argc,char* argv[])
{
if(strcmp(argv[0],"cd")==0)
{
if(argc==2)
{
if(chdir(argv[1])==0)
{
char cwd[1024];
getcwd(cwd,sizeof(cwd));
setenv("pwd",cwd,1);
}
else{
perror("chdir");
}
}else
{
printf("error: expected argument for 'cd'\n");
}
}
if(strcmp(argv[0],"pwd")==0)
{
char cwd[length]; // 定义一个足够大的缓冲区来存储路径
if (getcwd(cwd, sizeof(cwd)) != NULL) {
if(check_redir!=0)
{
int fd;
int flag=O_CREAT;
if(check_redir==1)
{
flag|=O_WRONLY|O_TRUNC;
}
if(check_redir==2)
{
flag|=O_APPEND|O_WRONLY;
}
fd=open(filename,flag,0666);
if(fd<0)
{
perror("open");
return;
}
int m=write(fd,cwd,strlen(cwd));
if(m<0)
{
perror("write");
close(fd);
return;
}
close(fd);
}
else{
printf("Current working directory: %s\n", cwd);
}
}
else {
perror("getcwd failed"); // 输出错误信息
}
}
3.外部命令的重定向
而对于外部命令的重定向,那么我们知道外部命令的执行则是创建一个子进程来执行,而我们知道创建一个子进程的本质就是拷贝父进程的task_struct结构体得到子进程的一份task_struct结构体以及父子进程共享同一个物理内存页,而其中拷贝父进程的task_struct结构体意味着就会拷贝一份父进程所对应的文件描述表,所以父子进程各自有一份独立的文件描述表,那么这里我们对于子进程的重定向,那么就无需关心关闭显示器文件所带来的影响,因为子进程对应的文件描述表中的显示器文件也就是下标为1的位置被关闭了,那么它不会影响父进程的文件描述表,所以我们可以在子进程中直接调用我们的dup2系统接口,至于进程替换,由于进程替换只会影响进程的地址空间以及页表的映射,不会影响文件描述符表,所以我们就得在进程替换之前,调用dup2系统接口来完成我们外部命令的重定向,而其中的实现就是我们上文介绍输出以及输入重定向的原理实现
代码实现:
int id=fork();
if(id==0)
{
if(check_redir!=0)
{
int fd;
int flag=O_CREAT;
int exchange;
if(check_redir==1)
{
flag|=O_WRONLY|O_TRUNC;
exchange=1;
}
if(check_redir==2)
{
flag|=O_WRONLY|O_APPEND;
exchange=1;
}
if(check_redir==3)
{
flag|=O_RDONLY;
exchange=0;
}
fd=open(filename,flag,0664);
if(fd<0)
{
perror("open");
exit(1);
}
int m=dup2(fd,exchange);
if(m<0)
{
perror("dup2");
close(fd);
exit(2);
}
close(fd);
}
execvp(argv[0],argv);
perror("execvp");
exit(EXIT_FAIL);
}
4.完整实现
完整代码:
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<fcntl.h>
#include<stdlib.h>
#include<stdbool.h>
#define length 1000
#define EXIT_FAIL 40
const char* order[]={"cd","pwd",NULL};
int check_redir=0;
char* filename;
int getString(char temp[],char* argv[])
{
int len=strlen(temp);
if(len>0&&temp[len-1]=='\n')
{
temp[len-1]='\0';
len--;
}
int argc=0;
char* toke=strtok(temp," ");
while(toke!=NULL&&argc<length-1)
{
argv[argc++]=toke;
toke=strtok(NULL," ");
}
argv[argc]=NULL;
for(int i=argc-1;i>=0;i--)
{
if(strcmp(argv[i],">")==0)
{
check_redir=1;
filename=argv[i+1];
argc=i;
argv[i]=NULL;
argv[i+1]=NULL;
break;
}
if(strcmp(argv[i],">>")==0)
{
check_redir=2;
filename=argv[i+1];
argc=i;
argv[i]=NULL;
argv[i+1]=NULL;
break;
}
if(strcmp(argv[i],"<")==0)
{
check_redir=3;
filename=argv[i+1];
argc=i;
argv[i]=NULL;
argv[i+1]=NULL;
break;
}
}
return argc;
}
bool check(char* argv[])
{
for(int i=0;order[i]!=NULL;i++)
{
if(strcmp(argv[0],order[i])==0)
{
return true;
}
}
return false;
}
void ordercomplete(int argc,char* argv[])
{
if(strcmp(argv[0],"cd")==0)
{
if(argc==2)
{
if(chdir(argv[1])==0)
{
char cwd[1024];
getcwd(cwd,sizeof(cwd));
setenv("pwd",cwd,1);
}
else{
perror("chdir");
}
}else
{
printf("error: expected argument for 'cd'\n");
}
}
if(strcmp(argv[0],"pwd")==0)
{
char cwd[length]; // 定义一个足够大的缓冲区来存储路径
if (getcwd(cwd, sizeof(cwd)) != NULL) {
if(check_redir!=0)
{
int fd;
int flag=O_CREAT;
if(check_redir==1)
{
flag|=O_WRONLY|O_TRUNC;
}
if(check_redir==2)
{
flag|=O_APPEND|O_WRONLY;
}
fd=open(filename,flag,0666);
if(fd<0)
{
perror("open");
return;
}
int m=write(fd,cwd,strlen(cwd));
if(m<0)
{
perror("write");
close(fd);
return;
}
close(fd);
}
else{
printf("Current working directory: %s\n", cwd);
}
}
else {
perror("getcwd failed"); // 输出错误信息
}
}
}
int main()
{
int argc;
char* argv[length];
char temp[length];
while(1)
{
printf("[%s@%s %s]$",getenv("USER"),getenv("HOSTNAME"),getenv("PWD"));
check_redir=0;
filename=NULL;
if(fgets(temp,sizeof(temp),stdin)==NULL)
{
perror("fgets");
continue;
}
argc=getString(temp,argv);
if(argc==0)
{
continue;
}
if(check(argv))
{
ordercomplete(argc,argv);
continue;
}
int id=fork();
if(id==0)
{
if(check_redir!=0)
{
int fd;
int flag=O_CREAT;
int exchange;
if(check_redir==1)
{
flag|=O_WRONLY|O_TRUNC;
exchange=1;
}
if(check_redir==2)
{
flag|=O_WRONLY|O_APPEND;
exchange=1;
}
if(check_redir==3)
{
flag|=O_RDONLY;
exchange=0;
}
fd=open(filename,flag,0664);
if(fd<0)
{
perror("open");
exit(1);
}
int m=dup2(fd,exchange);
if(m<0)
{
perror("dup2");
close(fd);
exit(2);
}
close(fd);
}
execvp(argv[0],argv);
perror("execvp");
exit(EXIT_FAIL);
}else
{
int status;
int m=waitpid(id,&status,0);
if(m<0)
{
perror("waitpid");
}else
{
if(WIFEXITED(status))
{
if(WEXITSTATUS(status)==40)
{
printf("子进程替换失败\n");
}
if(WEXITSTATUS(status)==1)
{
printf("open调用失败\n");
}
if(WEXITSTATUS(status)==2)
{
printf("dup2系统调用失败\n");
}
}
}
}
}
return 0;
}
Linux上的运行截图:
结语
那么本篇文章就详细解析了我们Linux下的输入以及输出重定向,那么先介绍了输入以及输出重定向的原理以及实现,然后再完善了我们的shell外壳程序,那么下一篇文章我将会解析缓冲区的内容
我会持续更新,希望你能多多关照,那么如果本文对你有所帮组的话,还请多多三连加关注哦,你的支持就是我创作的最大的动力!