重定向与缓冲区理解

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的打印。

上一篇:爬虫学习3


下一篇:Sealos 基础教程:Sealos Devbox 的架构原理解析