1、重定向的本质
在OS中为打开文件创建的struct files_struct中,有指向某一个打开文件struct file的指针数组(文件描述符表),每个下标对应一个struct file,重定向的本质就是在内核中改变文件描述符表特定下标的内容。
(1)实验一:关闭默认打开的文件(stdin、stdout、strerr)实现重定向
①输出重定向
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#define FILE_NAME "log.txt"
int main()
{
close(1);
int fd=open(FILE_NAME,O_WRONLY|O_TRUNC|O_CREAT,0666);
if(fd<0)
{
perror("open:");
return 1;
}
const char* msg="testredir\n";
printf("fd:%dtest redir\n",fd);
write(fd,msg,strlen(msg));
//fflush(stdout);//不刷新看不到printf打印的内容
close(fd);
return 0;
}
这里我们关闭了默认打开的显示器输出文件(close(1)),当打开log.txt文件时,发现其fd被设置成1了。由于printf默认是向显示器中打印,显示器输出本来的fd为0,但此时fd为0的文件是我们打开的log.txt文件,所以不会在显示器上打印了。但这里也并未写入log.txt文件,则是与语言级别的文件缓冲区有关,如果将fflush那一行放开,就写进去了(或者不关闭文件,这样在适当的时候OS也会将缓冲区中的内容刷新到OS内部的struct file中):
②输入重定向
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#define FILE_NAME "log.txt"
int main()
{
close(0);
int fd=open(FILE_NAME,O_RDONLY);
//int fd=open(FILE_NAME,O_WRONLY|O_TRUNC|O_CREAT,0666);
if(fd<0)
{
perror("open:");
return 1;
}
char buffer[1024];
fread(buffer,1,sizeof(buffer),stdin);
printf("%s",buffer);
// const char* msg="testredir\n";
// printf("fd:%dtest redir\n",fd);
// write(fd,msg,strlen(msg));
// fflush(stdout);
close(fd);
return 0;
}
这里buffer的内容本来从stdin(键盘)输入的,变成从log.txt文件输入了。
③追加重定向
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#define FILE_NAME "log.txt"
int main()
{
close(1);
//int fd=open(FILE_NAME,O_RDONLY);
int fd=open(FILE_NAME,O_WRONLY|O_APPEND|O_CREAT,0666);
//int fd=open(FILE_NAME,O_WRONLY|O_TRUNC|O_CREAT,0666);
if(fd<0)
{
perror("open:");
return 1;
}
//char buffer[1024];
//fread(buffer,1,sizeof(buffer),stdin);
//printf("%s",buffer);
const char* msg="testredir\n";
printf("fd:%dtest redir\n",fd);
write(fd,msg,strlen(msg));
fflush(stdout);
close(fd);
return 0;
}
(2)实验二:dup2()接口实现重定向
①dup2(int oldfd,int newfd)介绍
dup2的作用是将oldfd的内容拷贝到newfd上,使得两处的文件描述符都指向oldfd指向的内容。
扩展:dup2会引发文件关闭的问题。当oldfd与newfd都指向同一个打开的文件struct file,那么本来newfd指向的文件struct file会不会关闭?这个其实是通过引用计数实现的。也就是在每个struct file中都会有一个引用计数,表明了有几个指针指向该文件,当newfd指向的struct file发现,newfd不再指向自己,那么其引用计数就会减一,当减到0时该文件就会关闭。
②示例:输出重定向
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#define FILE_NAME "log.txt"
int main()
{
//close(1);
//int fd=open(FILE_NAME,O_RDONLY);
int fd=open(FILE_NAME,O_WRONLY|O_TRUNC|O_CREAT,0666);
//int fd=open(FILE_NAME,O_WRONLY|O_TRUNC|O_CREAT,0666);
if(fd<0)
{
perror("open:");
return 1;
}
//char buffer[1024];
//fread(buffer,1,sizeof(buffer),stdin);
//printf("%s",buffer);
dup2(fd,1);//全指向fd本来所指的文件了
const char* msg="testredir\n";
printf("fd:%dtest redir\n",fd);
fprintf(stdout,msg);
//write(fd,msg,strlen(msg));
//fflush(stdout);
close(fd);
return 0;
}
2、在shell程序中实现重定向
前提:程序替换不会影响重定向。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<error.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<string.h>
#include<ctype.h>
#include<sys/stat.h>
#include<fcntl.h>
#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)
#define SkipSpace(cmd,pos)do{while(isspace(cmd[pos])) {pos++;} }while(0)
#define NoneRedir 0
#define OutputRedir 1
#define AppendRedir 2
#define InputRedir 3
// ls -l >log.txt
//
char* filename=NULL;
int redirtype=NoneRedir;
char* GArgv[32];
int lastcode=0;
char newcwd[SIZE];//这里必须是全局变量
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* GetCwd()
{
const char* cwd=getenv("PWD");
if(cwd==NULL) return "None";
return cwd;
}
void PrintCmdInfo()
{
char line[SIZE];
const char* username=GetUserName(); //get username
const char* hostname=GetHostName();
const char* cwd=GetCwd();
SkipPath(cwd);
sprintf(line,"[%s@%s %s]",username,hostname,strlen(cwd)==1?"/":cwd+1);
printf("%s",line);
fflush(stdout);
}
int GetUserCmd(char usercommand[],size_t n)
{
//char usercommand[SIZE];
char* cmd=fgets(usercommand,n,stdin);// 由于最后一次输入的是回车,那么这里usercommand中就会多一个回车
if(cmd==NULL) return -1;
usercommand[strlen(usercommand)-1]=ZERO;
return strlen(usercommand);
}
void CheckRedir(char cmd[],size_t n)
{
for(int i=0;i<n;i++)
{
if(cmd[i]=='>')
{
if(cmd[i+1]=='>')
{
//追加重定向
cmd[i]=0;
redirtype=AppendRedir;
i+=2;
SkipSpace(cmd,i);
filename=cmd+i;
break;
}
else{
//输出重定向
//ls -a -l > log.txt
// SkipSpace(cmd,i);
cmd[i]=0;
redirtype=OutputRedir;
i++;
SkipSpace(cmd,i);
filename=cmd+i;
break;
}
}
else if(cmd[i]=='<')
{
//输入重定向
cmd[i]=0;
redirtype=InputRedir;
i++;
SkipSpace(cmd,i);
filename=cmd+i;
break;
}
}
}
void SplitCmd(char usercommand[],size_t n)
{
(void)n;//只是为了防止警告
GArgv[0]=strtok(usercommand,SEP);
int index=1;
while(GArgv[index++]=strtok(NULL,SEP))
{
//GArgv[index]=tmp;
// index++;
}
}
void ExecuteCommand()
{
int id=fork();
if(id==0)
{
//重定向设置
if(filename!=NULL)
{
if(redirtype==OutputRedir)
{
int fd=open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666);
dup2(fd,1);
}
else if(redirtype==AppendRedir)
{
//int fd=
int fd=open(filename,O_WRONLY|O_CREAT|O_APPEND,0666);
dup2(fd,1);
}
else if(redirtype==InputRedir)
{
int fd=open(filename,O_RDONLY);
dup2(fd,0);
}
}
//child
execvp(GArgv[0],GArgv);
exit(errno);
}
//father
int status=0;
int childid=waitpid(id,&status,0);//阻塞等待
// if(WIFEXITED(status))
// {
// printf("child process Exit_Code:%d",WEXITSTATUS(status));
// exit(0);
// }
lastcode=WEXITSTATUS(status);
printf("%s:%s\n",GArgv[0],strerror(lastcode));
// exit(0);//在这里直接退出
}
void Cd()
{
const char* path=GArgv[1];
if(path!=NULL)
{
chdir(path);
}
else{
chdir(getenv("HOME"));
}
//更新环境变量
char Tmp[SIZE];
getcwd(Tmp,sizeof(Tmp));
// char newcwd[SIZE];
sprintf(newcwd,"PWD=%s",Tmp);
//printf("%s\n",newcwd);
putenv(newcwd);
}
int CheckBuildIn()
{
int isbuildin=0;
const char* entercmd=GArgv[0];
if(strcmp(entercmd,"cd")==0)
{
Cd();
isbuildin=1;
//exit(0);
}
else if(strcmp(entercmd,"echo")==0 && strcmp(GArgv[1],"$?")==0)
{
isbuildin=1;
printf("%d\n",lastcode);
lastcode=0;
}
return isbuildin;
}
int main()
{
int quit=0;
while(!quit)
{
//更新filename和redirtype
filename=NULL;
redirtype=NoneRedir;
//打印命令行提示信息 user@Hostname path
PrintCmdInfo();
//获取一行内容
char usercommand[SIZE];
GetUserCmd(usercommand,SIZE);
//printf("%s\n",usercommand);
//判断是否有重定向符号
CheckRedir(usercommand,SIZE);
// printf("filename:%s,redirtype:%d\n",filename,redirtype);
//将命令按照空格分割
SplitCmd(usercommand,strlen(usercommand));
// int i=0;
// while(GArgv[i])
// {
// printf("%s ",GArgv[i]);
// i++;
// }
//
// 判断是否为内建命令
int isbuildin=CheckBuildIn();
//
// 不是内建命令,创建子进程+进程程序替换
if(!isbuildin) ExecuteCommand();
}
return 0;
}
3、缓冲区理解
(1)缓冲区及相关理论
缓冲区分为用户级缓冲区及内核级缓冲区。我们这里关注用户级缓冲区。
①缓冲区本质
一部分内存
②缓冲区的作用
a、解耦;b、给上层提供高效的IO体验,间接提高整体效率
缓冲区的作用的理解:系统调用是有成本的,为了提高效率,我们要尽可能的少调用。我们在C语言写的代码,向文件写入时,如果每次都直接写入进程中的文件,那么系统调用的次数就很多;而采用缓冲区的方式,每次写都往缓冲区里写,等到合适的时候将缓冲区中的内容刷新到进程中的file里,这样就只用一次系统调用就完成了大量内容的写入。
③缓冲区刷新策略
a、立即刷新:fflush(stdout),int fsync(int fd)
b、行刷新:显示器采用这种刷新策略(照顾用户的查看习惯)
c、全缓冲:缓冲区写满才刷新,普通文件采用这种刷新策略。
特殊情况:进程退出,系统会自动刷新缓冲区。
(2)实验
#include<stdio.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#define FILE_NAME "log.txt"
int main()
{
fprintf(stdout,"C:hello fprintf\n");
printf("C: hello printf\n");
fputs("C:hello fputs\n",stdout);
const char* str="systemcall:hello write\n";
write(1,str,strlen(str));
fork();
return 0;
}
因为显示器是按行刷新的,所以在fork之前这些内容都已经刷新到stdout中了,重定向一下(使刷新方式变为全缓冲):
可以发现只有系统调用函数被打印了一次,而其他都打印了两次;原因就是系统调用函数是直接写进OS内的文件结构体struct file中,而c语言提供的函数则是先写入语言级别的缓存区中,当fork()之后形成的两个进程,在退出时发生写时拷贝,都会将缓存区进行刷新,那么就会打印两次了。
4、缓冲区实验:封装一个简单的C库
mystdio.h:
#pragma once
#include<stdio.h>
#include<string.h>
#define SIZE 1024
#define FLUSH_NONE 1
#define FLUSH_LINE (1<<1)
#define FLUSH_ALL (1<<2)
typedef struct my_FILE
{
int flag;//刷新方式
int fileno;
int end;
//缓冲区
char cache[SIZE];
}myFILE;
myFILE* my_fopen(const char* filename,const char* opentype);
int my_fwrite(myFILE* myfile,const char* buffer,size_t len);
int my_fflush(myFILE* myfile );
int my_fclose(myFILE* myfile);
mystdio.c:
#include"mystdio.h"
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
//#define FLUSH_NONE 1
//#define FLUSH_LINE (1<<1)
//#define FLUSH_ALL (1<<2)
#define READONLY 0
#define WRITEONLY 1
#define APPEND 2
myFILE* my_fopen(const char* filename,const char* opentype)
{
myFILE* newfile=(myFILE*)malloc(sizeof(myFILE));
if(strcmp(opentype,"r")==0)
{
int fd= open(filename,O_RDONLY);
newfile->flag=FLUSH_LINE;
newfile->fileno=fd;
}
else if(strcmp(opentype,"w")==0)
{
int fd= open(filename,O_WRONLY | O_CREAT | O_TRUNC,0666);
newfile->flag=FLUSH_LINE;
newfile->fileno=fd;
}
else if(strcmp(opentype,"a")==0)
{
int fd= open(filename,O_WRONLY | O_CREAT | O_APPEND,0666);
newfile->flag=FLUSH_LINE;
newfile->fileno=fd;
}
newfile->end =0;
return newfile;
}
int my_fwrite( myFILE* myfile,const char* buffer,size_t len)
{
//拷贝到缓冲区
memcpy(myfile->cache+myfile->end,buffer,len);
myfile->end+=len;
//判断刷新条件
if((myfile->flag&FLUSH_LINE) && myfile->cache[myfile->end-1]=='\n')
{
my_fflush(myfile);
}
return 1;
}
int my_fflush(myFILE* myfile)
{
write(myfile->fileno,myfile->cache,myfile->end);
myfile->end=0;//清空
return 1;
}
int my_fclose(myFILE* myfile)
{
my_fflush(myfile);
close(myfile->fileno);
free(myfile);
return 1;
}
测试代码teststdio.c:
#include"mystdio.h"
#include<unistd.h>
int main()
{
myFILE *fp=my_fopen("log1.txt","w");
const char* msg="hello,this is my test";//运行完进程结束时刷新
//const char* msg="hello,this is my test\n"; //行刷新
int cnt=20;
while(cnt--)
{
my_fwrite(fp,msg,strlen(msg));
sleep(1);
}
my_fclose(fp);
return 0;
}
5、stderr理解
我们写的程序,本质都是在对数据进行处理(计算、存储),然而数据从哪里来到哪里去,系统为了给用户提供查看与交互机会,才有了stdin stdout stderr。
stderr与stdout指向的都是显示器,那为什么要分成两个?
我们的做法是,将stdout重定向到另一个文件中,剩下来的都是错误信息,错误信息是为了服务用户的。
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int main()
{
printf("hello printf\n");
perror("hello perror");
printf("hello printf\n");
perror("hello perror");
printf("hello printf\n");
perror("hello perror");
printf("hello printf\n");
perror("hello perror");
printf("hello printf\n");
perror("hello perror");
return 0;
}
这里的重定向默认是将1中的内容重定向到newlog.txt中,这只是重定向的简单写法。重定向的完整写法:
./程序名 fd(文件描述符) > 另一个文件。
所以说,C语言提供的库函数 perror是向文件描述符为2的文件打印,而printf是向文件描述符为1的打印。