• 【Linux】基础IO(万字详解) —— 系统文件IO | 文件描述符fd | 重定向原理


    🌈欢迎来到Linux专栏~~基础IO


    • (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
    • 目前状态:大三非科班啃C++中
    • 🌍博客主页:张小姐的猫~江湖背景
    • 快上车🚘,握好方向盘跟我有一起打天下嘞!
    • 送给自己的一句鸡汤🤔:
    • 🔥真正的大师永远怀着一颗学徒的心
    • 作者水平很有限,如果发现错误,可在评论区指正,感谢🙏
    • 🎉🎉欢迎持续关注!
      在这里插入图片描述

    请添加图片描述

    请添加图片描述

    0.感性认识一切皆文件

    linux认为,一切皆文件。
    对文件而言:

    曾经理解的文件:read 、 write
    显示器:printf/cout ——》 一种write
    键盘:scanf/cin ——》一种read
    
    • 1
    • 2
    • 3

    在这里插入图片描述 🧐什么叫做文件呢?

    • 站在系统的角度,能够被input读取,或者能够被output写出设备就叫文件!

    侠义上的文件:普通的磁盘文件
    广义上的文件:显示器,键盘,网卡,磁盘,显卡,声卡,几乎所有的外设,都可以称为文件

    1. 回顾C中的文件操作

    🥑C读写文件

    文件操作:

    • 首先要打开文件:打开成功返回文件指针;打开失败,返回NULL
    • 最后要关闭文件
    FILE *fopen(const char *path, const char *mode);//路径 + 打开方式
    int fclose(FILE *fp);
    
    • 1
    • 2

    💦C写文件
    们可以fputs/fgets以字符串形式读写;也可以fprintf/fscanf格式化读写

    int fputs(const char *s, FILE *stream);  向特定文件流写入字符串
    
    • 1
    int fprintf(FILE *stream, const char *format, ...);
    
    • 1

    什么叫做当前路径

    • 当一个进程运行起来的时候,每个进程都会记录自己当前所处的工作路径

    在这里插入图片描述

    如果以w模式打开文件,默认把原始内容清掉,再写入(类似输入重定向)

    在这里插入图片描述

    如果要以追加方式写,则要以"a" append模式打开文件
    在这里插入图片描述

    💢细节提问: 此处的strlen要不要+1?

       //进行文件操作
       const char* s1 = "hello fwrite\n";
       fwrite(s1, strlen(s1), 1, fp);
    
    • 1
    • 2
    • 3

    不要!\0结尾是C语言的规定,文件用遵守吗?文件保存的是有效数据!
    否则就会出现乱码

    在这里插入图片描述

    tips:快速清空文件

    >log.txt  //输入前已经清空文件,输入的又为空白
    
    • 1

    💦C读文件
    fgets从特定文件流中按行读取,内容放在缓冲区。读取成功返回字符串起始地址,读失败返回NULL

    char *fgets(char *s, int size, FILE *stream); //size:为缓冲区大小
    
    • 1

    在这里插入图片描述

    🥑关于stdin stdout stderr

    C语言默认会打开三个输入输出流:stdin、stdout、stderr, 它们的类型都是FILE*,这三个东西是什么呢?

    • C语言把它们当做文件看待;站在系统角度,stdin对应的硬件设备是键盘stdout对应显示器stderr对应显示器,本质上我们最终都是访问硬件。C++中也有cin、cout、cerr,几乎所有语言都提供标准输入、标准输出、标准错误

    既然fputs是向文件写入,stdout也是FILE*类型,我们是不是可以向显示器标准输出打印了?这说明显示器被看做文件(有那味了) 喊出那句话:Linux下,一切皆文件

    2.系统文件 I / O

    通过之前的学习,这些文件操作最终都是访问硬件(显示器、键盘、磁盘)。众所周知,OS是硬件的管理者。所有语言上对“文件”的操作,都必须贯穿操作系统。然而OS不相信任何人,访问操作系统,就必须要通过系统接口!!

    open/fclose,fread/fwrite,fputs/fgets,fgets/fputs 等库函数一定需要使用OS提供的系统调用接口,接下来我们就来学习文件的系统调用接口,才能做到万变不离其宗!!

    在这里插入图片描述

    🌈open & close

    💢通过手册查找 man 2 open

    #include 
    #include 
    #include 
    
    int open(const char *pathname, int flags);//路径 + 选项
    int open(const char *pathname, int flags, mode_t mode);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    参数说明

    pathname: 要打开或创建的目标文件文件名
    flags:    打开方式。传递多个标志位,下面的一个或者多个常量进行“或”运算,构成flags.
                 O_RDONLY: 只读打开
                 O_WRONLY: 只写打开
                 O_RDWR  : 读写打开
              以上这三个常量,必须指定一个且只能指定一个
                 O_CREAT : 若文件不存在,则创建它。同时需要使用mode选项,来指明新文件的访问权限
                 O_APPEND: 追加写
    mode: 	  设置默认权限信息 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    返回值说明:

    return the new file descriptor, or -1 if an error occurred (in which case, errno is set appropriately).
         成功: 新打开的文件描述符 
         失败: -1
    
    • 1
    • 2
    • 3

    💢man 2 close

    #include 
    
    int close(int fd);
    
    • 1
    • 2
    • 3

    话不多说,用起来。open如果以写入方式打开且文件不存在,需要或|上O_CREAT,这与C中以"w"模式打开完全一样 (为什么是|呢,继续看下去吧)。如果我们先不带第三个参数去实现

    在这里插入图片描述
    发现open并没有帮我创建新文件,因为我刚刚用的是C语言的接口,C语言的接口创建难道在系统也一样?

    在这里插入图片描述
    可以看见文件的权限为什么是这样子的?有这个文件,要创建它,系统层面就必须指定权限是多少!,也是要用到我们的第三个参数了,我们采用权限设置的八进制方案——

    • 我们在应用层看到一个很简单的动作,在系统接口层面甚至OS层面,可能做了非常多的动作! 我根本就不关心什么只写、创建、权限这些与系统相关的概念。语言为我们做了封装,我用就好了。
    • 也就是:哪有什么岁月静好,有人负重前行罢了
    fopen("./log.txt", "w");
    int fd = open("./log.txt", O_WRONLY | O_CREAT, 0666);//umask过滤权限,可以把umask设为0
    
    • 1
    • 2

    💢那第二个参数flags(int)为什么要把模式|在一起呢?

    • 这是一种用户层给内核传递标志位的常用做法。
    • int有32个比特位,不重复的一个bit,就可以表示不同状态,就可以传递多个标志位且位运算效率较高。这些O_RDONLYO_WRONLYO_RDWR都是只有一个比特位是1的宏,并且相互不重复,这样|在一起,就能传递多个标志位
    //用int中的不重复的一个bit,就可以表示不同状态
    #define ONE 0x1     //0000 0001
    #define TWO 0X2     //0000 0010
    #define THREE 0X4   //0000 0100                                                                                
    
    //系统内部用 & 来验证标志位是否为1
    void show(int flags)//0000 0011
    {
      if(flags & ONE)  printf("hello one\n");//0000 0011 & 0000 0001 为真
      if(flags & TWO)  printf("hello two\n");
      if(flags & THREE) printf("hello three\n");
    }
    
    int main()
    {
      show(ONE);
      show(ONE | TWO); //0000 0001 | 0000 0010
      show(ONE | TWO | THREE);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    进入fcntl-linux.h此文件,可以看到

    在这里插入图片描述

    🌈read & write

    找辣个男人问问 哈哈哈
    💦man 2 write

    #include 
    
    ssize_t write(int fd, const void *buf, size_t count);
    参数:
        buf: 用户缓冲区
        count: 期望写的字节数
    返回值:实际写入的字节数
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述

    又一次写入时,我们发现:

    在这里插入图片描述

    O_TRUNC: 打开文件的时候直接清空文件

    O_TRUNC:
        如果文件已经存在并且是一个常规文件,并且开放模式允许写入(即是0_RDNRor O_MwRONLY),那么它将被截断为长度为0(也就是清空文件)
    
    • 1
    • 2

    在这里插入图片描述

    O_APPEND: 追加文件

    在这里插入图片描述

    注意注意:写入文件的过程中,不需要写入\0!因为\0是C语言层面上规定字符串的结束标志,而写入文件关心的是字符串的内容,文件和语言不要搞混了

    💦 man 2 read

    #include 
    
    ssize_t read(int fd, void *buf, size_t count);
    参数:
        buf: 读到的内容放在用户层缓冲区中,也就是自己定义缓冲区
        count: 期望读多少个字节
    返回值:实际读多少个字节
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    读文件的前提:文件已经存在,不涉及创建及权限的问题,那么用两个参数的open打开文件即可

    在这里插入图片描述

    3.文件描述符(fd)

    通过上面的练习,我发现每次成功open的fd都是3
    在这里插入图片描述

    接下来,我们连续的打开文件,观察fd。我们知道打开文件失败返回-1,那么012去哪了呢?012消失的原因,要么是不让用,要么是被别人占用

    在这里插入图片描述

    事实上,当我们的程序运行起来变成进程,默认情况下,OS会帮助我们打开三个标准输入输出,012其实分别对应的就是标准输入、标准输出、标准错误

    实践出真知:

    在这里插入图片描述
    c语言上的stdin标准输入、stdout标准输出、stderr标准错误,对应硬件设备也是键盘、显示器、显示器,这有什么关联呢?

    话说回来,我们还是一直没搞懂FILE*是什么东西,FILE*open

    • FILE其实是一个struct结构体!是C语言库提供的,一般内部有多种成员
    • C文件 库函数内部 一定要调用 系统调用
    • 在系统角度,只认识fd
    • 所以FILE结构体里面,必定封装了fd

    怎么样证明FILE结构体里面,必定封装了fd?

    在这里插入图片描述

    🎨file descriptor(fd文件描述符)

    所有的文件操作都是进程执行对应的函数,即本质上是进程对文件的操作

    🔸 如果一个文件没有被打开,这个文件是在磁盘上。如果我创建一个空文件,该文件也是要占用磁盘空间的,因为文件的属性早就存在了(包括名称、时间、类型、大小、权限、用户名所属组等等),属性也是数据,所谓“空文件”是指文件内容为空

    🥑文件 = 内容 + 属性。对文件的操作也是分成两类的:对文件内容的操作 + 对文件属性的操作

    🔸 要操作文件,必须打开文件(C语言fopen、系统上open),本质上,就是文件相关的属性信息从磁盘加载到内存的过程

    文件:被进程打开的文件(内存文件),没有被打开的文件(磁盘文件)

    操作系统中存在大量进程,一个进程可以打开多个文件:进程:文件 = 1:1。系统中会存在大量的被打开的文件!所以OS要不要把如此之多的文件在内存中也管理起来呢? 必须管理! 先描述再组织

    我们操作系统是C语言写的,OS内部要为了管理每一个被打开的文件,构建struct file

    //创建struct file 对象,充当一个被打开的文件
    struct file
    {
        struct file * next;
        struct file * prev;
        //包含了一个被打开的文件的几乎所有的内容(不仅仅包含属性,权限,缓冲区)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 🌍文件描述符:01234567,从0开始,连续的小整数,会让我们联想到数组下标!

    打开的这么多文件,怎么知道哪些是我们进程的呢?操作系统为了让进程和文件之间产生关联,进程在内核创建struct files_struct 的结构,这个结构包含了一个数组 struct file* fd_array[] ,也就是一个指针数组,把表述文件的结构体地址填入到特定下标中。

    在这里插入图片描述

    🥑那么现在就能解释了为什么打开文件返回的是3:

    • 新打开一个文件本质是内核会为我们描述struct file结构,再把struct file地址填入到fd_array[]数组下标去,因为012已经被占用了,于是填到3号下标,对应的数组下标3返回给用户,这样就能通过fd从而找到了文件对象

    这也解释了为什么write和read这样的系统调用接口为什么一定要传入文件描述符fd:执行系统调用接口是进程执行的,通过进程PCB,找到自己打开的文件列表,通过fd索引数组找到对应的文件,从而对文件进行操作

    ✅ 结论:文件描述符fd,本质是内核中进程和打开文件关联数组下标

    🍈接下来我们看看源代码:

    在这里插入图片描述

    刚好对应我们的猜测:
    在这里插入图片描述
    我们理一下逻辑:

    • 🔥调用fopen -> 底层是调用open -> 得到fd -> 封装成FILE -> 返回FILE*给给open,传到用户手里
    • 🔥:用户调用fwrite() -> FILE* -> 包含了fd -> 内部封装write -> 最后是write(fd, …)-> 自己执行操作系统内部的write -> 找到进程的task_struct -> 再找到*fs 指针 -> 找到文件描述符表 files_ struct -> fd_ array[ fd ] -> struct _file -> 找到了内存文件

    🎨理性认识一切皆文件

    一切皆文件是linux设计哲学,体现在操作系统的软件设计层面

    Linux是C语言写的!那如何用C语言实现面向对象,甚至多态?

    • 我们知道:类是由成员函数 + 成员方法组成,c语言里的struct就能实现
    struct file
    {
    	int size:
    	mode_t mode;
    	int user;
    	int group;
    	......
    	//函数指针
    	int (*readp)(int fd, void * buffer, int len);
    	int (*writep)(int fd, void * buffer, int len);
    	.....
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    对于键盘显示器等等这些外设,一定都有比如像read、write读写方法,因为由冯诺依曼体系结构知,外设是要和内存打交道的。这可能有些奇怪,比如键盘能读我知道,但能写吗注意,我们有统一的读写方法,但不代表非要每一个都实现,比如键盘就可以没有写方法,即方法为空
    在这里插入图片描述

    • 底层不同的硬件,一定对应的是不同的操作方法!!
    • 上面所述的外设,每一个设备的核心访问函数,都可以是read,write (IO)

    所有的设备都可以有自己的readwrite,但是代码的实现方法一定是不一样的

    那又是如何做到一切皆文件的呢?Linux中做了软件的虚拟层vfs(虚拟文件系统),会统一维护每一个打开文件的结构体struct file. 上层的struct file操作系统OS实行维护

    我们在每个struct file当中包含上一大堆的函数指针,这样,在struct file上层看来所有的文件都是调用统一的接口;在底层我们通过函数指针指向不同硬件的方法。
    在这里插入图片描述

    说白了就是,我上层不管你具体是什么鸡鸭鹅,都统一被我看成了动物类,类里面有具体的辨别方法,鹅的话就调用鹅的辨别方法,鸡就调用鸡的方法,这样就是一切皆动物的思维了,可以理解为C++的多态是漫长的软件开发摸索中实现“一切皆…”的高级版本/语言版本

    在源代码中,struct file就有这样一个结构体指针,指向底层各种实现方法

    在这里插入图片描述

    这就是面面向对象的手法

    🎨文件描述符的分配规则

    观察如下代码,可以看到,我把0关掉后,再打开文件是分配的文件描述符就是0 ~

    在这里插入图片描述在这里插入图片描述
    ⚡我们得出文件描述符的分配规则:每次给新文件分配的fd,是从fd_array[]中找一个最小的未被使用的作为新的fd.

    其实很好理解,就是从0开始遍历数组中找一个未被使用的下标,并填入文件地址

    4. 重定向原理

    🌍输出重定向

    有没有细心的同学,上面我们唯独没有关闭1,我们现在上手试一下。按照文件描述符的规则,再打开就是打印我们刚刚关闭的1

    在这里插入图片描述

    惊奇的发现,居然没有打印出来,而是全部打印到文件中呢?

    在这里插入图片描述

    本来应该显示到显示器中,却被打印到文件内部,这种行为我们早就知道叫做输出重定向。咱们无意之间居然完成了一次重定向操作,为什么是这样呢?

    💢这是因为:我们以上来就close(1), 断开了和显示器文件的关系,相当于置NULL,对于新打开的log.txt,根据文件分配规则,1是指向log.txt的

    在这里插入图片描述
    思考printf底层是在做什么?事实上,它本质是向标准输出(stdout)打印 ——

    int fprintf(FILE *stream, const char *format, ...);
    stdout -> FIEL{fileno = 1} -> log.txt// stdout只认识1,只对1输入输出
    
    • 1
    • 2

    这就是重定向的本质:在OS内部,更改fd对应的内容的指向!!

    🌍追加重定向

    追加重定向与输出重定向唯一的差别就是在打开方式上不要O_TRUNC 清空,增加O_APPEND选项。

    在这里插入图片描述

    🌍输入重定向

    输入重定向就是把本来应该从键盘获取内容变成从文件中获取

    char *fgets(char *s, int size, FILE *stream); 
    
    • 1

    在这里插入图片描述

    🌍dup2

    以上情况都是先关闭了文件然后再打开文件这样重定向,但是情况不会总是这样子

    🍑看文档得知:dup2是对指针做拷贝

    #include 
    
    int dup2(int oldfd, int newfd); //oldfd->newfd
    
    dup2() makes newfd be the copy of oldfd, closing newfd first if necessary, but note the following:
    *  If oldfd is not a valid file descriptor, then the call fails, and newfd is not closed.
    *  If oldfd is a valid file descriptor, and newfd has the same value as oldfd, then dup2() does nothing, and returns newfd.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    这里有点绕的,copy后最后要和谁一样?,嘿嘿我刚学也绕晕了

    • 要跟oldfd的一样

    💢假设:输出重定向 显示器(1)-> log.txt, dup2()的参数该怎么样填呢?

    • 最终的目的:是让1不再指向显示器,而是指向log.txt
    • 所以1的地址xxxx要换成yyyy
    • 3的内容要拷贝到1里面, 最终要和3内容一致——> 也就是和oldfd一样
    • 所以1是newfd ,3是oldfd 。dup2(3,1)

    在这里插入图片描述

    ➰输出重定向

    dup2(fd, 1);  本来应该显示到显示器的内容,写入到文件
    
    • 1

    在这里插入图片描述
    注意,系统层面,open打开文件时带了选项O_TRUNC,以清空原来内容。而在C语言中"w"也会先把原始文件清空,说明上层封装了这个选项

    ➰追加重定向

    只需在输出只写的基础上添加O_APPEND选项

    在这里插入图片描述

    ➰输入重定向

    dup2(fd, 0);  原本从键盘读,现在从文件中读。
    
    • 1

    在这里插入图片描述

    5.课后练习

    Linux下两个进程可以同时打开同一个文件,这时如下描述错误的是:

    A.两个进程中分别产生生成两个独立的fd
    B.两个进程可以任意对文件进行读写操作,操作系统并不保证写的原子性
    C.进程可以通过系统调用对文件加锁,从而实现对文件内容的保护
    D.任何一个进程删除该文件时,另外一个进程会立即出现读写失败
    E.两个进程可以分别读取文件的不同部分而不会相互影响
    F.一个进程对文件长度和内容的修改另外一个进程可以立即感知

    • A选项正确,进程数据独有,各自有各自的文件描述信息表,因此各自打开文件会有自己独立的描述信息添加在各自信息表的不同位置,因此fd各自也相互独立

    • B选项正确,两个进程打开同一个文件,但是各有各的文件描述信息以及读写位置,互不影响,因此多个进程同时读写有可能会造成穿插覆盖的情况(原子性操作,被认为是一次性完成的操作,操作过程中间不会被打断,通常以此表示操作的安全性

    • C选项正确,文件锁就是用于保护对文件当前的操作不会被打断,就算时间片轮转,因为已经对文件加锁,其他的进程也无法对文件内容进行操作,从而保护在本次文件操作过程是安全的。

    • D选项错误,删除文件实际上只是删除文件的目录项,文件的数据以及inode并不会立即被删除,因此若进程已经打开文件,文件被删除时,并不会影响进程的操作,因为进程已经具备文件的描述信息(可以编写代码进行尝试,在文件打开后,外界删除文件,然后看进程中是否还可以继续写入或读取数据)

    • E选项正确,如果仅仅是读取文件内容,两个不同进程其实都有自己各自的描述信息和读写位置,因此可以同时读取文件数据而不会受到对方的影响。

    • F选项正确,因为文件内容的修改是直接反馈至磁盘文件系统中的,因此当文件内容被修改,其他进程因为也是针对磁盘数据的操作,因此可以立即感知到(可以写代码尝试一个进程打开文件后,等其他进程修改了内容后然后再读取文件数据进行测试)

    以下描述正确的是 [多选]

    A.程序中打开文件所返回的文件描述符, 本质上在PCB中是文件描述符表的下标
    B.多个文件描述符可以通过dup2函数进行重定向后操作同一个文件
    C.在进程中多次打开同一个文件返回的文件描述符是一致的
    D.文件流指针就是struct _IO_FILE结构体, 该结构体当中的int _fileno 保存的文件描述符, 是一对一的关系

    解析:
    A和D不用多说,都是正确的,重点我们来看B我当时没选):不同信息表数组下标的位置可以存放相同的文件描述信息结构指针,dup2重定向的本质原理,就是改变对应位置的文件信息而改变操作对象文件的

    答案是ABD

    bash中,需要将脚本demo.sh的标准输出和标准错误输出重定向至文件demo.log,以下哪些用法是正确的 [多选]

    A.bash demo.sh &>demo.log
    B.bash demo.sh >&demo.log
    C.bash demo.sh >demo.log 2>&1
    D.bash demo.sh 2>demo.log 1>demo.log

    题目解析:
    比较典型的方式是:bash demo.sh 1>demo.log 2>&1

    • 先将标准输出重定向到demo.log文件,然后将标准错误重定向到标准输入(这时候的标准输入已经是指向文件了,所以也就是将标准错误重定向到文件)

    A command &> file 表示将标准输出stdout和标准错误输出stderr重定向至指定的文件file中。

    B 与A选项功能雷同

    C 比较典型的写法,将标准输出和标准错误都重定向到文件, >demo.log是一种把前边的标准输出1忽略的写法

    D 比较直观的一种写法,不秀技,直观的将标准输入和标准错误分别重定向到文件

    答案是ABCD

    以下代码的功结果是

       void func() {
         int fd = open("./tmp.txt", O_RDWR|O_CREAT, 0664);
         if (fd < 0) {
         return -1;
         }
         dup2(fd, 1);
         printf("hello bit");
         return 0;
       }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    A.将hello bit打印到终端显示
    B.将hello bit 写入到tmp.txt中
    C.将hello bit 打印到终端显示并且写入tmp.txt文件中
    D.既不打印,也没有写入到文件中

    解析:
    选错选了C,忘记了printf是默认向1中打印,因为dup2,标准输出已经被重定向,因此数据会被写入文件中,而不是直接打印

    答案选B ,粗心大意

    📢写在最后

    最近为瑞幸打call,双十一囤了很多咖啡

    请添加图片描述

  • 相关阅读:
    ETCD数据库源码分析——服务端PUT流程
    Java图片验证码的实现方法
    训练好的神经网络怎么用,神经网络训练电脑配置
    HTML+CSS大作业 环境网页设计与实现(垃圾分类) web前端开发技术 web课程设计 网页规划与设计
    关于操作系统
    承接vue2->vue3的一些变动
    【控制】滑模控制,滑模面的选择
    ifconfig看不见自己外网地址?
    typora操作手册
    产业园区中工业厂房的能源综合配置
  • 原文地址:https://blog.csdn.net/qq_42996461/article/details/127811244