开发者

Linux文件重定向&&文件缓冲区解读

目录
  • 一、C文件接口
  • 二、系统文件I/O
    • 2.1认识系统文件I/O
    • 2.2系统文件I/O
    • 2.3系统调用和库函数
    • 2.4open( )的返回值--文件描述符
    • 2.5访问文件的本质
  • 三、文件重定向
    • 3.1认识文件重定向
    • 3.2文件重定向的本质
    • 3.3在shell中添加重定向功能
    • 3.4stdout和stderr
    • 3.5如何理解“linux下一切皆文件” --以对外设的IO操作为例
  • 四、文件缓冲区
    • 4.1认识FILE
    • 4.2文件缓冲区引入
    • 4.3文件缓冲区的原理
    • 4.4解释现象
  • 总结

    一、C文件接口

    stdin & stdout & stderr

    C默认会打开三个输入输出流,分别是stdin, stdout, stderr

    仔细观察发现,这三个流的类型都是phpFILE*, fopen返回值类型,文件指针

    • fwrite向指定文件写入内容
    • fread从指定文件读取内容

    fprintf根据指定的format(格式)发送信息(参数)到由stream(流)指定的文件,fprintf可以使得信息写入到指定的文件

    调用C文件接口,以w的形式打开,若文件不存在,会在php当前目录下新建文件,当前路径就是进程的当前路径cwd,如果改变了进程的cwd就可以在其他目录下新建文件

    w写入前都会对文件进行清空,a在文件结尾追加写,两者都是写入

    C默认打开的三个输入输出流不是C语言的特性,而是操作系统的特性,进程会默认打开键盘,显示器,显示器

    Linux文件重定向&&文件缓冲区解读

    二、系统文件I/O

    2.1认识系统文件I/O

    • 文件其实是在磁盘上的,磁盘是外设,对文件进行访问,就是对硬件进行访问
    • 任何用户都不能直接访问硬件的数据 ,而必须通过系统调用
    • 几乎所有的库只要是访问硬件设备,必须封装系统调用
    • C文件接口就是一种库函数,是对系统调用的封装

    2.2系统文件I/O

    open( )

    #include <sys/types.h>
    
    #include <sys/stat.h>
    #include <fcntl.h>
    
    int open(const char *pathname, int flags);
    int open(const char *pathname, int flags, mode_t mode);
    • pathname: 要打开或创建的目标文件
    • flags: 打开文件时,可以传入多个参数选项,用下面的一个或者多个常量进行 “ 或 ” 运算,构成 flags

    参数 :

    • O_RDONLY: 只读打开
    • O_WRONLY: 只写打开
    • O_RdwR : 读写打开
    • O_CREAT : 若文件不存在,则创建它,需要使用 mode(例0666) 选项,来指明新文件的访问权限
    • O_APPEND: 追加写
    • O_TRUNC: 每一次写入都清空文件

    返回值:

    • 成功:新打开的文件描述符
    • 失败:-1

    代码示例:

    umask( )可以用来设置掩码的值

    Linux文件重定向&&文件缓冲区解读

    Linux文件重定向&&文件缓冲区解读

    Linux文件重定向&&文件缓冲区解读

    Linux文件重定向&&文件缓冲区解读

    比特方位式的标志位传递方式通过位运算来实现

    Linux文件重定向&&文件缓冲区解读

    Linux文件重定向&&文件缓冲区解读

    Linux文件重定向&&文件缓冲区解读

    2.3系统调用和库函数

    上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)

    open close read write lseek 都属于系统提供的接口,称之为系统调用接口

    可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发。

    2.4open( )的返回值--文件描述符

    Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2

    0,1,2对应的物理设备一般是:键盘,显示器,显示器

    linux下文件描述符的分配规则:从0下标开始,寻找最小没有被使用过的数组位置,它的下标就是新文件的文件描述符--结合访问文件的本质来说明

    代码示例:

    • 因为C库函数是对系统接口的封装,系统接口下只认识文件描述符,所以C库自己提供的FILE结构体中必定也包含着文件描述符,用_fileno记录

    Linux文件重定向&&文件缓冲区解读

    Linux文件重定向&&文件缓冲区解读

    如果关闭了1号文件,printf就无法向1号文件(显示器)写入了 ,但可以向3号文件写入,所以我们打印就只能看到n的值

    Linux文件重定向&&文件缓冲区解读

    Linux文件重定向&&文件缓冲区解读

    2.5访问文件的本质

    任何一个被打开的文件在内存中都要被管理起来,操作系统如果管理被打开的文件?----先描述再组织

    当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件--file结构体(直接或间接包含如下属性:文件的基本属性,文件的内核缓冲区信息,引用计数,struct file*next,在磁盘的什么位置),表示一个已经打开的文件对象而进程执行open系统调用,所以必须让进程和文件关联起来,每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!

    所以,本质上,文件描述符就是该数组的下标,只要拿着文件描述符,就可以找到对应的文件

    Linux文件重定向&&文件缓冲区解读

    • 当一个进程open()一个文件时,操作系统会在struct_file的指针数组中从下标为0的地方在开始寻找一个没有被使用过的数组位置,填入要打开文件的struct file*,再将数组下标返回给open( )调用,作为该文件的文件描述符fd
    • 当一个进程要向某个文件写入的时候,操作系统只认识文件描述符,根据文件描述符找到对应的数组下标,根据数组下标位置里的内容找到所对应的文件再写入
    • close关闭文件本质上是清空对应fd数组下标位置的内容,再将该fd内容指向的文件的引用计数--,引用计数为0才释放销毁相应的struct_ file

    三、文件重定向

    3.1认识文件重定向

    关闭1号文件再打开新文件 ,向1号文件写入内容

    Linux文件重定向&&文件缓冲区解读

    Linux文件重定向&&文件缓冲区解读

    可以看到,原来要向1号文件(显示屏)打印的信息,被写入到了新打开的文件,其中,fd=1。这种现象叫做输出重定向

    常见的重定向有:>输出重定向, >>追加重定向, <输入重定向

    追加重定向

    Linux文件重定向&&文件缓冲区解读

    Linux文件重定向&&文件缓冲区解读

    输入重定向

    Linux文件重定向&&文件缓冲区解读

    Linux文件重定向&&文件缓冲区解读

    3.2文件重定向的本质

    • 文件重定向的本质:将1号文件描述符在指针数组中对应位置的内容,用log.txt文件描述符在指针数组中对应位置的内容进行覆盖,原本数组内的指向1号文件的文件指针就被替换成log.txt的文件指针,当我们再向1号文件描述符写入内容的时候,就是向文件指针指向的log.txt内写入而不再写到标准输出
    • dup2系统调用
    • 原本向显示屏打印的内容被写入到log.txt文件中

    Linux文件重定向&&文件缓冲区解读

    Linux文件重定向&&文件缓冲区解读

    Linux文件重定向&&文件缓冲区解读

    Linux文件重定向&&文件缓冲区解读

    Linux文件重定向&&文件缓冲区解读

    3.3在shell中添加重定向功能

    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/wait.h>
    #include<assert.h>
    #include<ctype.h>
    #include<fcntl.h>
    
    #define LEFT "["
    #define RIGHT "]"
    #define LABLE "#"
    #define DELIM " \t"
    #define LINE_SIZE 1024
    #define ARGV_SIZE 32
    
    #define NONE -1
    #define IN_RDIR     0
    #define OUT_RDIR    1
    #define APPEND_RDIR 2
    
    extern char** environ;
    char commandline[LINE_SIZE];
    char* argv[ARGV_SIZE];
    char pwd[LINE_SIZE];
    char myenv[LINE_SIZE];
    
    int lastcode=0;
    int quit=0;
    
    char *rdirfilename = NULL;
    int rdir = NONE;
    
    const char* getuser()
    {
        return getenv("USER");
    }
    
    const char* gethostname()
    {
        return getenv("HOSTNAME");
    }
    
    void getpwd()
    {
        getcwd(pwd,sizeof(pwd));
    }
    
    void check_redir(char *cmd)
    {
    
        // ls -al -n
        // ls -al -n >/</>> filename.txt
        char *pos = cmd;
        while(*pos)
        {
            if(*pos == '>')
            {
                if(*(pos+1) == '>'){
                    *pos++ = '\0';
                    *pos++ = '\0';
                    while(isspace(*pos)) pos++;
                    rdirfilename = pos;
                    rdir=APPEND_RDIR;
                    break;
                }
                else{
                    *pos = '\0';
                    pos++;
                    while(isspace(*pos)) pos++;
                    rdirfilename = pos;
                    rdir=OUT_RDIR;
                    break;
                }
            }
            else if(*pos == '<')
            {
                *pos = '\0'; // ls -a -l -n < filename.txt
                pos++;
                while(isspace(*pos)) pos++;
                rdirfilename = pos;
                rdir=IN_RDIR;
                break;
            }
            else{
                //do nothing
            }
            pos++;
        }
    }
    
    void interact(char* cline,int size)
    {
        getpwd();
        printf(LEFT"%s@%s %s"RIGHT""LABLE" ",getuser(),gethostname(),pwd);
        char* s=fgets(cline,size,stdin);
        assert(s);
        (void)s;
        cline[strlen(cline)-1]='\0';
    
        //printf("echo : %s",cline);
        //ls -a -l > myfile.txt
        check_redir(cline);
    }
    
    int splitstring(char cline[],char* _argv[])
    {
        int i=0;
        _argv[i++]=strtok(cline,DELIM);
        while(_argv[i++]=strtok(NULL,DELIM));
    
        return i-1;
    }
    
    void normalexcute(char* _argv[])
    {
        pid_t id=fork();
        if(id<0)
        {
            perror("fork");
            //continue;
            return ;
        }
        else if(id==0)
        {
    
            int fd = 0;
    
            // 后面我们做了重定向的工作,后面我们在进行程序替换的时候,难道不影响吗???
            if(rdir == IN_RDIR)
            {
                fd = open(rdirfilename, O_RDONLY);
                dup2(fd, 0);
            }
            else if(rdir == OUT_RDIR)
            {
                fd = open(rdirfilename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
                dup2(fd, 1);
            }
            else if(rdir == APPEND_RDIR)
            {
                fd = open(rdirfilename, O_CREAT|O_WRONLY|O_APPEND, 0666);
                dup2(fd, 1);
            }
            //子进程执行指令
            //execvpe(argv[0],argv,environ);
            execvp(argv[0],argv);
        }
        else{
            int status=0;
            pid_t rid=waitpid(id,&status,0);
            if(rid==id)
            {
                lastcode=WEXITSTATUS(status);
            }
        }
    }
    
    int buildcommand(char* _argv[],int _argc)
    {
        if(_argc==2&&strcmp(_argv[0],"cd")==0)
        {
            chdir(_argv[1]);
            getpwd();
            sprintf(getenv("PWD"),"%s",pwd);
            return 1;
        }
        else if(_argc==2&&strcmp(_argv[0],"export")==0)
        {
            strcpy(myenv,_argv[1]);
            putenv(myenv);
            return 1;
        }
        else if(_argc==2&&strcmp(_argv[0],"echo")==0)
        {
            if(strcmp(_argv[1],"$?")==0)
            {
                printf("%d\n",lastcode);
                lastcode=0;
            }
            else if(*_argv[1]=='$')
            {
                char* s=getenv(_argv[1]+1);
                if(s) printf("%s\n",s);
            }
            else{
                printf("%s\n",_argv[1]);
            }
    
            return 1;
    
        }
    
        //特殊处理ls
        if(_argc==2&&strcmp(_argv[0],"ls")==0)
        {
            _argv[_argc++]="--color";
            _argv[_argc]=NULL;
        }
    
        return 0;
    
    }
    
    int main()
    {
        while(!quit)
        {
            //交互问题,获得命令行参数
            interact(commandline,sizeof commandline);
    
            //字符串分割,解析命令行参数
            int argc = splitstring(commandline,argv);
            if(argc==0) continue;
    
            //指令的判断
            int n=buildcommand(argv,argc);        
    
            //普通指令的执行
            if(!n)normalexcute(argv);
        }
            return 0;
    }
    

    Linux文件重定向&&文件缓冲区解读

    • 进程历史打开的文件以及文件的重定向关系,并不会被程序替换所影响!!进程程序替换之后影响页表右边的物理地址所指向的内容,虚拟地址并左边的部分并不会受到影响
    • 程序替换并不会影响文件访问

    3.4stdout和stderr

    • stdout和stderr对应的硬件设备都是显示屏,访问的都是同一个文件(引用计数)
    • 在重定向的时候,默认只对stdout的fd进行重定向

    代码示例:

    Linux文件重定向&&文件缓冲区解读

    Linux文件重定向&&文件缓冲区解读

    如果对1号和2号文件都要进行重定向呢?

    示例:./mytest 1> log.txt 2>err.txt

    Linux文件重定向&&文件缓冲区解读

    示例:./mytest > log.txt 2>&1

    Linux文件重定向&&文件缓冲区解读

    3.5如何理解“linux下一切皆文件” --以对外设的IO操作为例

    • 不同的外设在进行IO操作时都有自己对应的读写方法,放在struct device里
    • 这些读写方法如何被找到?--由struct operation_func来对读写方法进行管理,该结构体里存在指向对应读写法的函数指针
    • 如何找到struct operation_func?--由struct file来对struct operation_func进行管理,file结构体存在指向struct operation_func的指针,基于struct file之上的被称为虚拟文件系统(VFS)--一切皆文件
    • 当我们打开一个文件的时候,通过进程的pcb数据结构找到struct struct_file,操作系统根据文件描述符的分配规则,在struct struct_file的指针数组中为该文件分配一个fd;当我们要访问一个外设的时候,根据该外设文件fd对应的数组下标内容找到该外设文件的struc编程客栈t file,根据file结构体找到对应的struct operation_func,由于访问的外设的不同,在struct operation_func中根据函数指针找到对应的读写方法,就可以对外设进行访问了

    四、文件缓冲区

    4.1认识FILE

    因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的

    所以C库当中的FILE结构体内部,必定封装了fd

    4.2文件缓冲区引入

    • 对比有无fork( )的代码

    Linux文件重定向&&文件缓冲区解读

    我们发现 printf 和 fwrite (库函数)都输出了 2 次,而 write 只输出了一次(系统调用),为什么呢?肯定和 fork有关!

    再来验证一个现象:

    不加'\n'并且在最后close(1)

    Linux文件重定向&&文件缓冲区解读

    代码运行的结果是:只有系统调用接口写入的内容被打印出来了

    Linux文件重定向&&文件缓冲区解读

    加上'\n',结果又不一样了

    Linux文件重定向&&文件缓冲区解读

    4.3文件缓冲区的原理

    C语言会提供一个缓冲区,我们调用C文件接口写入的数据会被暂存在这个缓冲区内,缓冲区的刷新方式有三种:

    1. 无缓冲:直接刷新,一般我们使用的fflush( )就是无缓冲的刷新方式
    2. 行缓冲:遇到'\n'才刷新,一般对应显示器
    3. 全缓冲:缓冲区满了才刷新,一般对应普通文件的写入
    4. 特殊说明:进程结束的时候会自动刷新缓冲区

    在操作系统的内核中也存在一个内核级别的缓冲区,目前认为,只要将数据刷新到了内核,数据就可以到硬件了,内核缓冲区也有自己的刷新方式

    为什么要有C层面的缓冲区?

    1. 用户不需要一步一步将数据写入到硬件中,而是可以直接调用C库为我们提供的读写方法,将数据交给库函数来处理,解决用户的效率问题
    2. 我们真正存到文件里的都是一个个的字符,调用C库的读写方法,可以在放入缓冲区之前将我们的数据格式化成字符串,再刷新到内核中进而写入文件,C层面的缓冲区可以配合格式化的工作

    C为我们提供的缓冲区在FILE结构体里,FILE里面有相关缓冲区的字段和维护信息,FILE属于用户层面,而不属于操作系统

    文件写入的过程:

    1. 首先,在文件写入之前,进程会打开一个文件,通过对各种内核数据结构的访问和操作,获得该文件的文件描述符
    2. 如果使用系统调用接口来对文件进行写入,数据直接通过write和fd写入对应的内核级别缓冲区,默认最后都会刷新到硬件中
    3. 如果使用fwrite等库函数来对文件进行写入,首先,在语言层面会malloc出一个FILE结构体,FILE里面有对应的缓冲区信息以及文件的fd,然后内容会先被暂存在C层面的缓冲区,如果是无缓冲,数据直接被刷新到内核中,如果是行缓冲,遇到'\n'就会被刷新到内核中,如果是全缓冲,等缓冲区满了就被刷新到内核中
    4. 由于库函数是对系统调用接口的封装,用户通过write和fd将数据刷新到对应的文件的内核缓冲区内,再由该内核缓冲区刷新到外设

    4.4解释现象

    为什么不加'\n'并且close(1)的时候,使用库函数写入的内容不会被显示?

    不加'\n',调用库函数写入的数据都会被暂存在C层面的缓冲区

    close(1)后,即使进程退出后缓冲区会自动刷新,但是此时已经找不到1号文件的fd了,缓冲区内的数据也无法被写入到内核中,最后也不会显示到显示器上

    加了'\n'即使最后close(1),遇到'\n'缓冲区就会立马将数据刷新到内核中,就会显示到显示器上

    为什么fork()之后重定向C接口会被调用两次?

    1. 重定向后,缓冲区的刷新方式会从行缓冲变成全缓冲,也就说,数据要么等到缓冲区满了再被刷新,要么等待进程结束后再刷新,所以我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后
    2. fork( )之后,创建子进程,子进程会继承父进程的内核数据结构对象的内容,父子进程在一开始的时候数据和代码是共享的,缓冲区也属于数据
    3. 进程退出后,要对缓冲区的数据进行统一刷新,刷新就是对数据进行访问写入,此时父子数据会发生写时拷贝,所以当父进程准备刷新的时候,子进程也就有了同样的一份数据,随即产生两份数据
    4. 由于write没有所谓的缓冲区,write()写入的数据直接在内核中,所以write( )的数据只有一份

    总结

    printf fwrite 库函android数会自带缓冲区,而 write 系统调用没有带缓冲区。这里所说的缓冲区, 都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区

    那这个用户级缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统 调用的“封装&rdwww.devze.comquo;,但是 write 没有缓冲区,而 printf fwrite 有,说明该缓冲区是二次加上的,由C标准库提供

    Linux文件重定向&&文件缓冲区解读

    以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

    0

    上一篇:

    下一篇:

    精彩评论

    暂无评论...
    验证码 换一张
    取 消

    最新运维

    运维排行榜