• 【1++的Linux】之文件(二)


    👍作者主页:进击的1++
    🤩 专栏链接:【1++的Linux】

    一,文件描述符

    我们先来看一段代码:

      #include
      2 #include<stdio.h>
      3 #include <sys/types.h>
      4 #include <sys/stat.h>
      5 #include <fcntl.h>
      6 int main()
      7 {
      8     int fd1=open("log1.txt1",O_WRONLY|O_CREAT,0666);
      9     int fd2=open("log1.txt2",O_WRONLY|O_CREAT,0666);
     10     int fd3=open("log1.txt3",O_WRONLY|O_CREAT,0666);
     11     printf("%d\n",fd1);
     12     printf("%d\n",fd2);                                                                                     
     13     printf("%d\n",fd3);
     14     return 0;
     15 }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    返回值fd是什么?为什么是345连续的,并且012哪去了呢?
    并且当我们在用C库函数提供的文件函数时,FILE*又是什么呢?
    下面我们依次来进行解释:
    首先,我们现在是对文件进行读写操作,文件要被访问要被加载到内存中去,因此我们现在所说的文件都是内存级文件。
    我们的进程要打开文件进行操作,一个进程可以打开多个文件,我们在上述代码中已经验证过。
    一个进程打开多个文件,多个进程就能打开更多的文件,那么我们要不要将这些文件管理起来呢?
    要的!!!怎么管理???先描述,后组织,这是操作系统进行各种管理的最重要的手段。
    因此在OS内部,为了方便管理,OS会创建一个struct file结构体用来描述被打开的文件,创建一个
    文件对象,并用双链表将对象链接起来,文件对象里面包含了文件的所有内容。
    每个进程用fils_struct来记录文件描述符的使用情况,称为用户打开文件表。在这个结构中又有一个指针数组,用来存放文件对象的地址。我们的文件描述符就是数组下表,不同下表可以对应同一个文件对象,我们的标准输出和标准错误就是这样的。我们的C语言会默认打开三个文件:标准输入,标准输出,标准错误(即stdin,stdout,stderror),因此对应的0,1,2下表也就分给了他们,所以我们新建的文件下表只能从3开始了。file_struct则由我们的PCB:task_struct进行管理。
    在这里插入图片描述

    在这里插入图片描述

    接下来我们再来谈谈FILE*

    FILE是什么呢?是一个结构体,那么在这个结构体中有什么呢?
    我们来看看源码:

    struct _IO_FILE {
    int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
    #define _IO_file_flags _flags
    
    /* The following pointers correspond to the C++ streambuf protocol. */
    /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
    char* _IO_read_ptr; /* Current read pointer */
    char* _IO_read_end; /* End of get area. */
    char* _IO_read_base; /* Start of putback+get area. */
    char* _IO_write_base; /* Start of put area. */
    char* _IO_write_ptr; /* Current put pointer. */
    char* _IO_write_end; /* End of put area. */
    char* _IO_buf_base; /* Start of reserve area. */
    char* _IO_buf_end; /* End of reserve area. */
    /* The following fields are used to support backing up and undo. */
    char *_IO_save_base; /* Pointer to start of non-current get area. */
    char *_IO_backup_base; /* Pointer to first valid character of backup area */
    char *_IO_save_end; /* Pointer to end of non-current get area. */
    
    struct _IO_marker *_markers;
    
    struct _IO_FILE *_chain;
    
    int _fileno;
    #if 0
    int _blksize;
    #else
    int _flags2;
    #endif
    _IO_off_t _old_offset; /* This used to be _offset but it's too small. */
    
    #define __HAVE_COLUMN /* temporary */
    /* 1+column number of pbase(); 0 is unknown. */
    unsigned short _cur_column;
    
    signed char _vtable_offset;
    char _shortbuf[1];
    
    /* char* _save_gptr; char* _save_egptr; */
    
    _IO_lock_t *_lock;
    #ifdef _IO_USE_OLD_IO_FILE
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    在我们的文件系统调用中,文件的读写都离不开文件描述符,那么C库函数的文件操作函数中也必定离不开文件描述符,因为C库的文件操作函数的都是经过将系统函数经过封装的。那么我们在使用库函数的时候怎么没有见过文件描述符呢?答案是:我们都间接的使用过,它在FILE结构体中int _fileno;
    我们在上述的源码当中,还看到了一些由指针维护的空间,这是什么呢?
    我们想一想在学习C语言时提到了缓冲区,那么这个缓冲区是在哪呢?由谁维护呢?
    我们来看下面这段代码:

    int main()
    {
       // FILE* pf=fopen("test.txt","w");
        fwrite("hellow",1,strlen("hellow"),stdout);
        write(1,"hhh\n",3);
        //ffluh(stdout);
        fork();    
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述**

    加fflush后
    在这里插入图片描述
    我们通过结果可以看到加fflush前后结果是不同的。这是为什么呢?接下来我们进行分析。
    我们直到父子进程的数据是读时共享,写时拷贝,那么缓冲区在在读时也是共享的,所以,当我们调用C库提供的文件接口时,其数据先会刷新到缓冲区中,fork之后父子共享缓冲区的数据,所以最后子进程要冲刷缓冲区时进行写时拷贝,创建自己的数据区,因此就会有两份“hellow",那么为啥”hhh“只有一份呢?因为其使用的是系统调用,数据在内核的缓冲区中,所以只有一份。
    因此我们所了解的缓冲区也就只能由C库提供。在打开一个文件时,其也会被创建,并且由我们FILE*中的两个代表起始地址的指针维护。

    那为什么要有缓冲区呢?
    当没有缓冲区时,我们要像文件中多次写入少量数据,那么每一次写入,都得需要将这分数据写到磁盘上才能够继续写入(写的过程还包括了打开磁盘,关闭磁盘等)这样效率就会大大的降低,用户的响应速度也会很低。我们将这种模式称为写透模式。
    若我们有缓冲区,此时我们就可以将要写的数据直接放到缓冲区中(写入时,最耗时的其实是机械操作(磁盘的寻道等这样的动作),所以我们先将数据放到缓冲区中,根据缓冲区的刷新策略刷新到磁盘中,这样就减少了IO过程,从而提高了整机的效率与用户响应速度。这样的方式其实类似于我们生活中的发快递的过程。这样的模式称为写回模式。
    缓冲区的刷新策略有哪几种呢?

    1. 立即刷新
    2. 行刷新 aaaaa\n
      3.满刷新(全缓冲)
      特殊情况:
      用户强制刷新(fflush)
      进程退出

    下面是我们模拟实现的一个缓冲区:

    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    
    struct MyFile_
    {
        int fd;
        char buff[1024];
        int end;
    
    };
    typedef struct MyFile_ MyFile;
    
    MyFile* fopen_(const char *pathname,const char *mode)
    {
        assert(pathname);
        assert(mode);
        MyFile* fp=NULL;
        if(strcmp(mode,"r")==0)
        {
    
        }
        else if(strcmp(mode,"w")==0)
        {
            int fd=open(pathname,O_WRONLY|O_TRUNC|O_CREAT,0666);
            if(fd>0)
            {
                fp=(MyFile*)malloc(sizeof(MyFile));
                assert(fp);
                memset(fp,0,sizeof(MyFile));
                fp->fd=fd;
            }
    
        }
        else if(strcmp(mode,"w+")==0)
        {
    
        }
        else{
    
        }
    
        return fp;
    
        
    }
    
    void fputs_(char *message,MyFile* fp)
    {
        assert(fp);
        assert(message);
        strcpy(fp->buff+fp->end,message);
        fp->end+=strlen(message);
        if(fp->fd==0)
        {
    
        }
        else if(fp->fd==1)
        {
            if(fp->buff[fp->end-1]=='\n')
            {
                fprintf(stderr,"%s",fp->buff);
                write(fp->fd,fp->buff,fp->end);
                fp->end=0;
    
            }
    
        }
        else if(fp->fd==2)
        {
    
        }
        else{
    
        }
    
    }
    
    void fflush_(MyFile* fp)
    {
        assert(fp);
        if(fp->end!=0)
        {
          write(fp->fd,fp->buff,fp->end);
          syncfs(fp->fd);//将数据写到磁盘
          fp->end=0;
        
        }
    }
    
    void fclose_(MyFile* fp)
    {
        assert(fp);
        fflush_(fp);
        close(fp->fd);
        free(fp);
        fp=NULL;
    
    }
    int main()
    {
        close(1);
        MyFile* fp=fopen_("log.txt","w");
        if(fp==NULL)
        {
            printf("open error\n");
            return 1;
        }
        fputs_("hellow world\n",fp);
       // fflush_(fp);
        fputs_("hyp",fp);
        fputs_("zkn\n",fp);
       // fflush_(fp);
       // fork();
        fclose_(fp);
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123

    二,重定向

    先来看一段代码:

    #include
    #include
    #include
    #include
    #include
    #include
    #include
    #include
    
    struct MyFile_
    {
        int fd;
        char buff[1024];
        int end;
    
    };
    typedef struct MyFile_ MyFile;
    
    MyFile* fopen_(const char *pathname,const char *mode)
    {
        assert(pathname);
        assert(mode);
        MyFile* fp=NULL;
        if(strcmp(mode,"r")==0)
        {
    
        }
        else if(strcmp(mode,"w")==0)
        {
            int fd=open(pathname,O_WRONLY|O_TRUNC|O_CREAT,0666);
            if(fd>0)
            {
                fp=(MyFile*)malloc(sizeof(MyFile));
                assert(fp);
                memset(fp,0,sizeof(MyFile));
                fp->fd=fd;
            }
    
        }
        else if(strcmp(mode,"w+")==0)
        {
    
        }
        else{
    
        }
    
        return fp;
    
        
    }
    
    void fputs_(char *message,MyFile* fp)
    {
        assert(fp);
        assert(message);
        strcpy(fp->buff+fp->end,message);
        fp->end+=strlen(message);
        if(fp->fd==0)
        {
    
        }
        else if(fp->fd==1)
        {
            if(fp->buff[fp->end-1]=='\n')
            {
                fprintf(stderr,"%s",fp->buff);
                write(fp->fd,fp->buff,fp->end);
                fp->end=0;
    
            }
    
        }
        else if(fp->fd==2)
        {
    
        }
        else{
    
        }
    
    }
    
    void fflush_(MyFile* fp)
    {
        assert(fp);
        if(fp->end!=0)
        {
          write(fp->fd,fp->buff,fp->end);
          syncfs(fp->fd);//刷新内核的缓冲区//将数据写到磁盘
          fp->end=0;
        
        }
    }
    
    void fclose_(MyFile* fp)
    {
        assert(fp);
        fflush_(fp);
        close(fp->fd);
        free(fp);
        fp=NULL;
    
    }
    int main()
    {
        close(1);
        MyFile* fp=fopen_("log.txt","w");
        if(fp==NULL)
        {
            printf("open error\n");
            return 1;
        }
        fputs_("hellow world\n",fp);
       // fflush_(fp);
        fputs_("hyp",fp);
        fputs_("zkn\n",fp);
       // fflush_(fp);
       // fork();
        fclose_(fp);
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123

    在这里插入图片描述
    若我们直接执行该程序,则屏幕上会输出“hellow world” 但若我们将命令改为./test>log.txt后,本应该输出的内容却被写到了log.txt中,这是为什么呢?
    文件的写入读取都要靠文件描述符去找到所对应的文件,既然本应该向屏幕中写入的内容,写到了其他的文件中,那么一定与文件描述符有关,我们前面说过,文件描述符是数组的下表,这个数组存储的是文件对象的指针,那么就好理解了,发生上述现象的原因,就是该下标中的内容发生了改变,使得原本指向屏幕文件,被替换为了Log.txt这个文件对象的指针。所以就被写入到了log.txt这个文件中。
    那么 它具体是怎么实现的呢?
    来看一段代码:

    int main()
    {
      int fd=open("./log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
      dup2(fd,1);
      printf("hellow world\n");
      return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述

    dup函数会将参数一所所指向的内容拷贝到参数二中。本质就是更改了文件描述符对应的内容的指向。

    三,理解Linux下一切皆文件

    感性的认识:
    站在系统的角度,我们将屏幕输出可以认为是一种写的过程,键盘输入是一种读的过程,那么我们就可以定义广义的文件概念:只要能进行读和写的设备就叫做文件。将所有设备都看作是文件,方便了我们对他们的操作变得统一和方便。

    理性的认识:
    在OS的软件设计层面,我们将文件的成员属性和方法都放在结构体中,其方法我们使用文件指针的形式,统一了方法的接口,但是不同的对象去调用,会有不同的结果。其实上述这种也就是用C语言实现面向对象和多态的一种方法。这种方法使得看待所有的文件的方式都一样,也就没有了硬件间的差别了。

  • 相关阅读:
    百度松果菁英班——机器学习实践一:海量文件遍历
    06公共方法
    【开始刷题啦——Leetcode《初级算法》(Go语言)】
    编译原理复习——语法分析(自顶向下)2
    [篇五章五]-如何禁用 Windows Defender-我的创作纪念日
    Java_封装
    帅爆! 赛博朋克特效实现
    Python编程指南:利用HTTP和HTTPS适配器实现智能路由
    Windows:Arm,我们不合适
    公众号微信网页授权
  • 原文地址:https://blog.csdn.net/m0_63135219/article/details/133688660