Linux 文件系列:深入理解文件fd,重定向,自定义shell当中重定向的模拟实现
- 一.预备知识
- 二.回顾C语言中常见的文件接口跟重定向建立联系
- 1.fopen函数的介绍
- 2.fclose函数的介绍
- 3.代码演示
- 1.以"w"(写)的方式打开
- 2.跟输出重定向的联系
- 3.以 "a"(追加)的方式打开
- 4.跟追加重定向的联系
- 三.认识并使用系统接口
- 1.open
- 1.open和fopen的联系(引出 FILE和struct file的联系)
- 2.open的进一步介绍
- 3.open函数的使用
- 1.close函数
- 2.开始使用并且看看这个fd到底是什么?
- 四.理解文件描述符fd
- 1.文件描述符fd的本质
- 2.标准输入,标准输出,标准错误
- 3.理解Linux下一切皆文件的设计理念
- 五.理解struct file内核数据结构
- 六.fd的分配规则
- 1.先抛出结论
- 2.代码演示
- 3.替换标准输出时的现象
- 七.理解重定向
- 1.重定向的本质
- 2.演示一下重定向
- 1.输出重定向
- 2.追加重定向
- 3.输入重定向
- 1.fread函数
- 2.演示
- 八.dup2函数:实现两个fd之间的重定向
- 1.dup2实现输出重定向
- 2.dup2实现追加重定向
- 3.dup2实现输入重定向
- 九.自定义shell当中重定向的模拟实现
- 1.原myshell.c代码
- 2.如何实现重定向
- 3.定义全局变量
- 4.检测是否要进行重定向的函数
- 5.创建子进程进行程序替换的函数修改
- 6.main函数的修改
- 7.修改之后myshell.c代码
- 十.stderr的作用
- 1.介绍2>&1
- 2.stderr的作用
- 3.演示
- 十一.重定向和程序替换之间是互不影响的
一.预备知识
经过刚才的分析,我们可以一个很重要的结论:
一个文件要被打开,一定要先在OS中形成被打开的文件对象
下面我们来回顾一下C语言中常见的文件接口
我们会发现重定向跟它们有所联系
二.回顾C语言中常见的文件接口跟重定向建立联系
关于C语言文件操作的详细内容,大家可以看我的这篇博客:
C语言文件操作详解
1.fopen函数的介绍
2.fclose函数的介绍
3.代码演示
1.以"w"(写)的方式打开
以"w"(写)的方式打开,如果文件不存在,就会在当前进程所在的路径当中创建它
创建成功
我们用vim写一些内容,再用w打开,看看w是否会清空之前的内容
清空成功
2.跟输出重定向的联系
我们会发现,fopen的"w"选项跟输出重定向很像啊
下面我们再来看看"a"选项的方式打开跟追加重定向的关系
3.以 “a”(追加)的方式打开
"a"也是写入,不过是从文件结尾处开始写入,是追加式写入,并不会清空文件
并没有清空原有内容
4.跟追加重定向的联系
我们会发现,fopen的"a"选项跟追加重定向很像啊
三.认识并使用系统接口
下面我们来认识并使用一下系统调用接口
首先我们达成1个共识:
C语言的文件操作接口,它的底层一定封装了系统调用接口
1.open
1.open和fopen的联系(引出 FILE和struct file的联系)
这是C语言提供的库函数:fopen:
这是系统调用接口:open:
可见,这个fd跟我们之前常用的FILE*指针很像啊,
其实它们的功能是一样的,C语言的FILE是一个结构体,这个结构体里面封装了fd
而这个fd是被打开的文件的结构体(struct file内核数据结构)中的一个属性,是用来区分不同文件的
2.open的进一步介绍
刚才我们还没有介绍第2个参数呢,下面我们来看一下
至此我们也理解了fopen是如何对open进行封装的
下面我们来使用一下open函数并且看看这个fd到底是啥啊?
3.open函数的使用
1.close函数
2.开始使用并且看看这个fd到底是什么?
现在我们有了两个问题:
- 0 1 2去哪了?
- 为什么会是 3 4 5 6?
下面就让我们借助这两个问题来深入理解一下文件描述符fd
四.理解文件描述符fd
1.文件描述符fd的本质
2.标准输入,标准输出,标准错误
在C语言的学习中我们都听说过
C语言程序(也就是进程),只要运行起来,默认就打开3个流
今天我们要说明的是:
3.理解Linux下一切皆文件的设计理念
五.理解struct file内核数据结构
六.fd的分配规则
1.先抛出结论
2.代码演示
分配规则1就不言而喻了,我们来验证分配规则2
我们先关闭stdin,然后在打开log.txt
如果该进程中log.txt被分配的fd是0,那么验证成功
验证成功
3.替换标准输出时的现象
下面我们先关闭stdout,然后再打开log.txt
为什么最后的
printf("log.txt的fd是: %d\n",fd);
没有成功打印呢?
因为stdout是标准输出流,是显示器对应的流,
我们平常printf是将字符串打印到stdout当中,但是我们在printf之前已经把stdout关掉了
所以不会打印到显示器
可是当我加了一行代码
cat log.txt之后
发现刚才printf中本来要往显示器上打印的数据现在写到了log.txt里面
这说明:
1.printf只认识stdout,也就是fd为1的文件
2.上层的fd并没有改变,但是底层fd指向的内容发生改变了
本来fd值为1的这个fd应该要指向显示器这个设备文件的
但是在这个进程当中 现在指向log.txt了
3.也就是说这个过程其实就是进行了一种类似于狸猫换太子式的指向的改变
七.理解重定向
1.重定向的本质
经由刚才的print的例子之后,我们可以发现:
由此可以得出重定向的本质:
重定向的本质,其实就是修改特定文件fd的指向
2.演示一下重定向
1.输出重定向
这是log.txt之前的数据
输出重定向成功
2.追加重定向
追加重定向成功
3.输入重定向
要进行输入重定向,我们要使用fread函数
1.fread函数
2.演示
输入重定向成功
八.dup2函数:实现两个fd之间的重定向
其实库里面给我们提供了一个函数dup2
可以实现两个fd之间的重定向
下面我们使用dup2函数再来演示一下重定向
1.dup2实现输出重定向
此时log.txt的fd是3
2.dup2实现追加重定向
实现成功
3.dup2实现输入重定向
实现成功
九.自定义shell当中重定向的模拟实现
经过上面的练习之后,下面我们修改一下我们的myshell.c代码,模拟实现一下重定向
关于myshell.c代码的实现,大家可以看我的博客当中的
Linux自定义shell的编写,里面实现了自定义shell
1.原myshell.c代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//#define DEBUG 1
#define SEP " "
char cwd[1024]={'\0'};
int lastcode=0;//上一次进程退出时的退出码
char env[1024][1024]={'\0'};
int my_index=0;
const char* getUsername()
{
const char* username=getenv("USER");
if(username==NULL) return "none";
return username;
}
const char* getHostname()
{
const char* hostname=getenv("HOSTNAME");
if(hostname==NULL) return "none";
return hostname;
}
const char* getPwd()
{
const char* pwd=getenv("PWD");
if(pwd==NULL) return "none";
return pwd;
}
//分割字符串填入usercommand数组当中
//例如: "ls -a -l" 分割为"ls" "-a" "-l"
void CommandSplit(char* usercommand[],char* command)
{
int i=0;
usercommand[i++]=strtok(command,SEP);
while(usercommand[i++]=strtok(NULL,SEP));
}
//解析命令行
void GetCommand(char* command,char* usercommand[])
{
command[strlen(command)-1]='\0';//清理掉最后的'\0'
CommandSplit(usercommand,command);
#ifdef DEBUG
int i=0;
while(usercommand[i]!=NULL)
{
printf("%d : %s\n",i,usercommand[i]);
i++;
}
#endif
}
//创建子进程,完成任务
void Execute(char* usercommand[])
{
pid_t id=fork();
if(id==0)
{
//子进程执行部分
execvp(usercommand[0],usercommand);
//如果子进程程序替换失败,已退出码为1的状态返回
exit(1);
}
else
{
//父进程执行部分
int status=0;
//阻塞等待
pid_t rid=waitpid(id,&status,0);
if(rid>0)
{
lastcode=WEXITSTATUS(status);
}
}
}
void cd(char* usercommand[])
{
chdir(usercommand[1]);
char tmp[1024]={'\0'};
getcwd(tmp,sizeof(tmp));
sprintf(cwd,"PWD=%s",tmp);
putenv(cwd);
lastcode=0;
}
int echo(char* usercommand[])
{
//1.echo后面什么都没有,相当于'\n'
if(usercommand[1]==NULL)
{
printf("\n");
lastcode=0;
return 1;
}
//2.echo $? echo $PWD echo $
char* cmd=usercommand[1];
int len=strlen(cmd);
if(cmd[0]=='$' && len>1)
{
//echo $?
if(cmd[1]=='?')
{
printf("%d\n",lastcode);
lastcode=0;
}
//echo $PWD
else
{
char* tmp=cmd+1;
const char* env=getenv(tmp);
//找不到该环境变量,打印'\n',退出码依旧为0
if(env==NULL)
{
printf("\n");
}
else
{
printf("%s\n",env);
}
lastcode=0;
}
}
else
{
printf("%s\n",cmd);
}
return 1;
}
void export(char* usercommand[])
{
//export
if(usercommand[1]==NULL)
{
lastcode=0;
return;
}
strcpy(env[my_index],usercommand[1]);
putenv(env[my_index]);
my_index++;
}
int doBuildIn(char* usercommand[])
{
//cd
if(strcmp(usercommand[0],"cd")==0)
{
if(usercommand[1]==NULL) return -1;
cd(usercommand);
return 1;
}
//echo
else if(strcmp(usercommand[0],"echo")==0)
{
return echo(usercommand);
}
//export
else if(strcmp(usercommand[0],"export")==0)
{
export(usercommand);
}
return 0;
}
int main()
{
while(1)
{
//1.打印提示符信息并获取用户的指令
printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
char command[1024]={'\0'};
fgets(command,sizeof(command),stdin);
char* usercommand[1024]={NULL};
//2.解析command字符串,放入usercommand指针数组当中
GetCommand(command,usercommand);
//3.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0
int flag=doBuildIn(usercommand);
//返回值!=0说明是内建命令,无需执行第4步
if(flag!=0) continue;
//4.创建子进程,交由子进程完成任务
Execute(usercommand);
}
return 0;
}
2.如何实现重定向
以输出重定向为例:
指令 > log.txt
输出重定向的作用其实就是把本来应该往显示器上打印的内容打印到了log.txt上
也就是说进行输出重定向的话,我们的log.txt就替代了显示器的位置
也就是说执行指令之前我们只需要执行一个
dup2(fd,1)即可
fd是log.txt的文件描述符,1是显示器的文件描述符
也就是说对于用户输入的一个完整的指令
例如:
ls -a -l > log.txt
我们要做的是:
1.检测是否需要进行重定向(检测指令当中是否有> 或者 >> 或者<)
2.如果需要,把这个指令拆分为两部分
“ls -a -l"和"log.txt”
后半部分是重定向到哪个文件当中
前半部分是真正的指令
如何拆分呢?把>改为’\0’,>>改为’\0’>,<改为’\0’即可
注意:
ls -a -l > log.txt
这样写也是可以的,因此我们要取出log.txt的时候要跳过空格
3.定义全局变量
第一步:
我们定义全局变量redir和四个宏常量,文件名和跳过空格的宏
注意:
在解析命令行之前就要检测是否要进行重定向
因为如果要进行重定向,就会对命令行进行拆分,拆分之后的指令才是真正要执行的指令
在后续执行指令时只需要根据全局变量redir是否是NoneRedir来判断是否要进行重定向
如果要进行重定向,根据redir具体的值来判断要进行输出/追加/输入重定向
进而判断filename的打开方式和dup2要覆盖显示器还是键盘
然后分类打开和覆盖即可
4.检测是否要进行重定向的函数
//跳过空格的宏
#define SKIP_SPACE(pos) do{ while(isspace(*pos)) pos++; }while(0)
//检测是否要进行重定向
void CheckRedir(char* command)
{
int len=strlen(command);
char* start=command,*end=command+len-1;
while(end>=start)
{
//输入重定向
//cat < log.txt
if(*end=='<')
{
*end='\0';
filename=end+1;
SKIP_SPACE(filename);
redir=InputRedir;
break;
}
else if(*end=='>')
{
//追加重定向
//ls -a -l >> log.txt
if(end>start && *(end-1)=='>')
{
*(end-1)='\0';
filename=end+1;
SKIP_SPACE(filename);
redir=AppendRedir;
break;
}
//输出重定向
else
{
*end='\0';
filename=end+1;
SKIP_SPACE(filename);
redir=OutPutRedir;
break;
}
}
else
{
end--;
}
}
}
在这里我们就只演示非内建命令的重定向操作了
因为只演示非内建命令就能够做到让大家很好地去理解重定向了
5.创建子进程进行程序替换的函数修改
//创建子进程,完成任务
void Execute(char* usercommand[])
{
pid_t id=fork();
if(id==0)
{
//检测是否要进行重定向
int fd=0;
//输出重定向
if(redir==OutPutRedir)
{
fd=open(filename,O_WRONLY | O_CREAT | O_TRUNC,0666);
dup2(fd,1);
}
//追加重定向
if(redir==AppendRedir)
{
fd=open(filename,O_WRONLY | O_CREAT | O_APPEND,0666);
dup2(fd,1);
}
//输入重定向
if(redir==InputRedir)
{
fd=open(filename,O_RDONLY);
dup2(fd,0);
}
//子进程执行部分
execvp(usercommand[0],usercommand);
//如果子进程程序替换失败,已退出码为1的状态返回
exit(1);
}
else
{
//父进程执行部分
int status=0;
//阻塞等待
pid_t rid=waitpid(id,&status,0);
if(rid>0)
{
lastcode=WEXITSTATUS(status);
}
}
}
6.main函数的修改
int main()
{
while(1)
{
redir=NoneRedir;
filename=NULL;
//1.打印提示符信息并获取用户的指令
printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
char command[1024]={'\0'};
fgets(command,sizeof(command),stdin);
command[strlen(command)-1]='\0';//清理掉最后的'\n'
//2.检测重定向
CheckRedir(command);
char* usercommand[1024]={NULL};
//3.解析command字符串,放入usercommand指针数组当中
GetCommand(command,usercommand);
//4.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0
int flag=doBuildIn(usercommand);
//返回值!=0说明是内建命令,无需执行第4步
if(flag!=0) continue;
//5.创建子进程,交由子进程完成任务
Execute(usercommand);
}
return 0;
}
7.修改之后myshell.c代码
模拟实现重定向的目的是为了让我们更好地去理解重定向
因此本次实现重定向只是简单的模拟实现,跟系统的重定向并不完全相同
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>
#include <sys/stat.h>
#include <fcntl.h>
//#define DEBUG 1
#define SEP " "
#define NoneRedir 0
#define OutPutRedir 1
#define AppendRedir 2
#define InputRedir 3
int redir=NoneRedir;
char* filename=NULL;
char cwd[1024]={'\0'};
int lastcode=0;//上一次进程退出时的退出码
char env[1024][1024]={'\0'};
int my_index=0;
const char* getUsername()
{
const char* username=getenv("USER");
if(username==NULL) return "none";
return username;
}
const char* getHostname()
{
const char* hostname=getenv("HOSTNAME");
if(hostname==NULL) return "none";
return hostname;
}
const char* getPwd()
{
const char* pwd=getenv("PWD");
if(pwd==NULL) return "none";
return pwd;
}
//分割字符串填入usercommand数组当中
//例如: "ls -a -l" 分割为"ls" "-a" "-l"
void CommandSplit(char* usercommand[],char* command)
{
int i=0;
usercommand[i++]=strtok(command,SEP);
while(usercommand[i++]=strtok(NULL,SEP));
}
//解析命令行
void GetCommand(char* command,char* usercommand[])
{
if(strlen(command)==0) return;
CommandSplit(usercommand,command);
#ifdef DEBUG
int i=0;
while(usercommand[i]!=NULL)
{
printf("%d : %s\n",i,usercommand[i]);
i++;
}
#endif
}
//创建子进程,完成任务
void Execute(char* usercommand[])
{
pid_t id=fork();
if(id==0)
{
//检测是否要进行重定向
int fd=0;
//输出重定向
if(redir==OutPutRedir)
{
fd=open(filename,O_WRONLY | O_CREAT | O_TRUNC,0666);
dup2(fd,1);
}
//追加重定向
if(redir==AppendRedir)
{
fd=open(filename,O_WRONLY | O_CREAT | O_APPEND,0666);
dup2(fd,1);
}
//输入重定向
if(redir==InputRedir)
{
fd=open(filename,O_RDONLY);
dup2(fd,0);
}
//子进程执行部分
execvp(usercommand[0],usercommand);
//如果子进程程序替换失败,已退出码为1的状态返回
exit(1);
}
else
{
//父进程执行部分
int status=0;
//阻塞等待
pid_t rid=waitpid(id,&status,0);
if(rid>0)
{
lastcode=WEXITSTATUS(status);
}
}
}
void cd(char* usercommand[])
{
chdir(usercommand[1]);
char tmp[1024]={'\0'};
getcwd(tmp,sizeof(tmp));
sprintf(cwd,"PWD=%s",tmp);
putenv(cwd);
lastcode=0;
}
int echo(char* usercommand[])
{
//1.echo后面什么都没有,相当于'\n'
if(usercommand[1]==NULL)
{
printf("\n");
lastcode=0;
return 1;
}
//2.echo $? echo $PWD echo $
char* cmd=usercommand[1];
int len=strlen(cmd);
if(cmd[0]=='$' && len>1)
{
//echo $?
if(cmd[1]=='?')
{
printf("%d\n",lastcode);
lastcode=0;
}
//echo $PWD
else
{
char* tmp=cmd+1;
const char* env=getenv(tmp);
//找不到该环境变量,打印'\n',退出码依旧为0
if(env==NULL)
{
printf("\n");
}
else
{
printf("%s\n",env);
}
lastcode=0;
}
}
else
{
printf("%s\n",cmd);
}
return 1;
}
int doBuildIn(char* usercommand[])
{
if(usercommand[0]==NULL) return 0;
//cd
if(strcmp(usercommand[0],"cd")==0)
{
if(usercommand[1]==NULL) return -1;
cd(usercommand);
return 1;
}
//echo
else if(strcmp(usercommand[0],"echo")==0)
{
return echo(usercommand);
}
//export
else if(strcmp(usercommand[0],"export")==0)
{
//export
if(usercommand[1]==NULL)
{
lastcode=0;
return 1;
}
strcpy(env[my_index],usercommand[1]);
putenv(env[my_index]);
my_index++;
}
return 0;
}
//跳过空格的宏
#define SKIP_SPACE(pos) do{ while(isspace(*pos)) pos++; }while(0)
//检测是否发生了重定向
void CheckRedir(char* command)
{
int len=strlen(command);
char* start=command,*end=command+len-1;
while(end>=start)
{
//输入重定向
//cat < log.txt
if(*end=='<')
{
*end='\0';
filename=end+1;
SKIP_SPACE(filename);
redir=InputRedir;
break;
}
else if(*end=='>')
{
//追加重定向
//ls -a -l >> log.txt
if(end>start && *(end-1)=='>')
{
*(end-1)='\0';
filename=end+1;
SKIP_SPACE(filename);
redir=AppendRedir;
break;
}
//输出重定向
else
{
*end='\0';
filename=end+1;
SKIP_SPACE(filename);
redir=OutPutRedir;
break;
}
}
else
{
end--;
}
}
}
int main()
{
while(1)
{
redir=NoneRedir;
filename=NULL;
//1.打印提示符信息并获取用户的指令
printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
char command[1024]={'\0'};
fgets(command,sizeof(command),stdin);
command[strlen(command)-1]='\0';//清理掉最后的'\n'
//2.检测重定向
CheckRedir(command);
char* usercommand[1024]={NULL};
//3.解析command字符串,放入usercommand指针数组当中
GetCommand(command,usercommand);
//4.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0
int flag=doBuildIn(usercommand);
//返回值!=0说明是内建命令,无需执行第4步
if(flag!=0) continue;
//5.创建子进程,交由子进程完成任务
Execute(usercommand);
}
return 0;
}
十.stderr的作用
首先先介绍一下2>&1这一语法
1.介绍2>&1
下面我们用fprintf来演示一下
如果我们现在就是想要把标准错误和标准输出都往显示器上打印呢?
./mycmd > log.txt 2>&1
又因为我们先把1重定向到log.txt中,再把2重定向到1中
因此就做到把2和1中的内容全都往log.txt中打印了
2.stderr的作用
我们平常学习编程的时候,程序写的并不大
程序运行时的错误信息和正常信息我们都统一往显示器上打印了
可是一旦程序特别大,要打印的信息特别多,此时区分显示器上的正常信息和错误信息就很麻烦了
而区分正常信息和错误信息之后就能够方便我们对错误信息进行统一排查,提高效率
因此标准输出的作用是:接收打印的正常信息
标准错误的作用是接收打印的错误信息
3.演示
还是刚才那份代码
现在我们想把正常信息重定向到log.txt中
错误信息重定向到log.txt.error中
./mycmd 1>log.txt 2>log.txt.error
把1重定向给log.txt
把2重定向给log.txt.error
注意:这样重定向时不能带空格
也就是说不能这样写:
./mycmd 1 > log.txt 2 > log.txt.error
十一.重定向和程序替换之间是互不影响的
为什么它们之间是互不影响的呢?
因为程序替换时改变的是进程结构体当中的页表中虚拟地址空间和物理地址空间的映射和进程地址空间中的相关属性
而重定向改变的是文件描述符表中fd的指向
两者互不影响
以上就是Linux 文件系列:深入理解文件fd,重定向,自定义shell当中重定向的模拟实现的全部内容,希望能对大家有所帮助!