赞
踩
经过刚才的分析,我们可以一个很重要的结论:
一个文件要被打开,一定要先在OS中形成被打开的文件对象
下面我们来回顾一下C语言中常见的文件接口
我们会发现重定向跟它们有所联系
关于C语言文件操作的详细内容,大家可以看我的这篇博客:
C语言文件操作详解
以"w"(写)的方式打开,如果文件不存在,就会在当前进程所在的路径当中创建它
创建成功
我们用vim写一些内容,再用w打开,看看w是否会清空之前的内容
清空成功
我们会发现,fopen的"w"选项跟输出重定向很像啊
下面我们再来看看"a"选项的方式打开跟追加重定向的关系
"a"也是写入,不过是从文件结尾处开始写入,是追加式写入,并不会清空文件
并没有清空原有内容
我们会发现,fopen的"a"选项跟追加重定向很像啊
下面我们来认识并使用一下系统调用接口
首先我们达成1个共识:
C语言的文件操作接口,它的底层一定封装了系统调用接口
这是C语言提供的库函数:fopen:
这是系统调用接口:open:
可见,这个fd跟我们之前常用的FILE*指针很像啊,
其实它们的功能是一样的,C语言的FILE是一个结构体,这个结构体里面封装了fd
而这个fd是被打开的文件的结构体(struct file内核数据结构)中的一个属性,是用来区分不同文件的
刚才我们还没有介绍第2个参数呢,下面我们来看一下
至此我们也理解了fopen是如何对open进行封装的
下面我们来使用一下open函数并且看看这个fd到底是啥啊?
现在我们有了两个问题:
下面就让我们借助这两个问题来深入理解一下文件描述符fd
在C语言的学习中我们都听说过
C语言程序(也就是进程),只要运行起来,默认就打开3个流
今天我们要说明的是:
分配规则1就不言而喻了,我们来验证分配规则2
我们先关闭stdin,然后在打开log.txt
如果该进程中log.txt被分配的fd是0,那么验证成功
验证成功
下面我们先关闭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.也就是说这个过程其实就是进行了一种类似于狸猫换太子式的指向的改变
经由刚才的print的例子之后,我们可以发现:
由此可以得出重定向的本质:
重定向的本质,其实就是修改特定文件fd的指向
这是log.txt之前的数据
输出重定向成功
追加重定向成功
要进行输入重定向,我们要使用fread函数
输入重定向成功
其实库里面给我们提供了一个函数dup2
可以实现两个fd之间的重定向
下面我们使用dup2函数再来演示一下重定向
此时log.txt的fd是3
实现成功
实现成功
经过上面的练习之后,下面我们修改一下我们的myshell.c代码,模拟实现一下重定向
关于myshell.c代码的实现,大家可以看我的博客当中的
Linux自定义shell的编写,里面实现了自定义shell
#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; }
以输出重定向为例:
指令 > 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的时候要跳过空格
第一步:
我们定义全局变量redir和四个宏常量,文件名和跳过空格的宏
注意:
在解析命令行之前就要检测是否要进行重定向
因为如果要进行重定向,就会对命令行进行拆分,拆分之后的指令才是真正要执行的指令
在后续执行指令时只需要根据全局变量redir是否是NoneRedir来判断是否要进行重定向
如果要进行重定向,根据redir具体的值来判断要进行输出/追加/输入重定向
进而判断filename的打开方式和dup2要覆盖显示器还是键盘
然后分类打开和覆盖即可
//跳过空格的宏 #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--; } } }
在这里我们就只演示非内建命令的重定向操作了
因为只演示非内建命令就能够做到让大家很好地去理解重定向了
//创建子进程,完成任务 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); } } }
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; }
模拟实现重定向的目的是为了让我们更好地去理解重定向
因此本次实现重定向只是简单的模拟实现,跟系统的重定向并不完全相同
#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; }
首先先介绍一下2>&1这一语法
下面我们用fprintf来演示一下
如果我们现在就是想要把标准错误和标准输出都往显示器上打印呢?
./mycmd > log.txt 2>&1
又因为我们先把1重定向到log.txt中,再把2重定向到1中
因此就做到把2和1中的内容全都往log.txt中打印了
我们平常学习编程的时候,程序写的并不大
程序运行时的错误信息和正常信息我们都统一往显示器上打印了
可是一旦程序特别大,要打印的信息特别多,此时区分显示器上的正常信息和错误信息就很麻烦了
而区分正常信息和错误信息之后就能够方便我们对错误信息进行统一排查,提高效率
因此标准输出的作用是:接收打印的正常信息
标准错误的作用是接收打印的错误信息
还是刚才那份代码
现在我们想把正常信息重定向到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当中重定向的模拟实现的全部内容,希望能对大家有所帮助!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。