• 【Linux】基础IO之文件操作(文件fd)——针对被打开的文件


    系列文章目录



    前言

    浅谈文件的共识

    • 1.文件 = 内容 + 属性

    • 2.文件分为打开的文件和未打开的文件

      • 1)打开的文件:进程打开的。本质上是研究进程和文件的关系。
      • 文件被打开,就必须先加载到内存中。
      • 一个进程可以打开多个文件,操作系统要对这些文件进行管理,就要先描述,再组织。在内核中,操作系统要管理好这些文件,就必须有这个文件的对象,包含很多的文件属性。
    • 2)未打开的文件有很多,操作系统要将这些文件存储好,本质上就是对这些文件进行增删查改的操作!

      • 未打开的文件,在磁盘上放着。

    本文章目标:针对被打开的文件,进行各种深入剖析。

    一、 回忆c语言对文件操作的接口

    1.fopen接口和cwd路径

    先执行一下代码:

      1 #include<stdio.h>
      2 #include<unistd.h>
      3 
      4 int main()
      5 {
      6     FILE* fp = fopen("log.txt","w");
      7     if(fp == NULL)
      8     {
      9         perror("fopen");
     10         return 1;
     11     }
     12     printf("pid: %d\n",getpid());
     13     fclose(fp);
     14     sleep(1000);                                                
     15     return 0;
     16 }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    该程序运行起来后,以"w"的方式打开log.txt文件,如果该文件不存在,则会创建一个文件。

    运行起来时可以看到该进程的pid已经被打印出来。
    ll查看能看到的确存在一个log.txt的文件。
    在这里插入图片描述
    那为什么是在当前目录下创建log.txt文件呢???
    这是因为一个叫做cwd的东西的存在。

    在根目录下的proc目录下,有该进程的当前路径。
    即通过ls /proc/进程pid -l 可以看到,该运行中的进程的cwd路径!

    在这里插入图片描述并且该cwd路径就是可执行程序所在的路径!

    cwd:current work directory——当前工作目录!

    所以,fopen以写的方式打开文件,如果文件不存在,就会在该进程的cwd路径下创建一个log.txt的文件!

    由此可以得出,如果我们自己把该进程的cwd路径改了,那么它就会在更改后的cwd路径下创建log.txt文件!

    怎么改?

    用一个接口:chdir()即可更改当前的cwd路径。

    chdir("/home/dzt/learning");   
    
    • 1

    在上面代码的基础上,在main函数开头就增加这一句代码后。

    运行起来通过查找cwd路径发现,cwd被修改了!
    在这里插入图片描述

    且在/home/dzt/learning路径下发现:
    在这里插入图片描述
    真就被创建了一个log.txt文件

    且在进程对应的工作目录中,不再有log.txt文件。
    在这里插入图片描述

    注意:1.chdir也受权限的约束,作为普通用户,不能将路径修改到/home/dzt路径下!
    2.如果fopen打开的文件带绝对路径,那就按绝对路径来,如果是相对路径,就按该进程的cwd来!

    总结:这个小节讲了复习了fopen函数,并且引入了cwd当前工作目录这个概念!

    2.fwrite接口和"w","a"方法

    在这里插入图片描述
    fwrite的使用方法是:将ptr这个字符串,以size大小,nmemb个长度写入stream文件指针指向的文件中。

    在这里插入图片描述
    w方法的特点是:如果该文件不存在,会创建一个文件。如果该文件存在,会先将该文件清空,再打开!

    注意这里的一个细节:

    6     const char* message = "Hello Linux\n";                         
    17    fwrite(message,strlen(message),1,fp);
    
    • 1
    • 2

    执行该函数fwrite时,是否需要strlen(message)+1

    答案是不需要的,+1是为了将字符串后面的’\0’也写入文件中,可是:

    字符串以’\0’结尾是c语言的规定,关文件操作什么事?!

    所以并不需要+1。

    而a方法的作用是,直接在文件的末尾追加字符串。

    由此可知,Linux中的 “>” 和 ">>"两个符号的区别一定是一个以"w"方式打开,一个以"a’方式打开的区别!!


    3.fprintf接口和三个默认打开的输入输出流(文件)

    fprintf接口比我们常见的printf函数多了一个字符f,默认情况下,printf就是向显示器打印数据。

    而Linux下一切皆文件,所以显示器也是一个文件。

    而fprintf接口,就是向指定的文件中输入数据。

    fprintf(stdout,"%s %d\n",message,123);      
    
    • 1

    在这里插入图片描述

    而我们在运行该程序时,会发现显示器中出现了这些信息,这就是被打印到了显示器文件中,而不是打印到其他文件中。

    在这里插入图片描述
    而这三个标准输入输出流,就是对应的:

    键盘文件——stdin
    显示器文件——stdout
    显示器文件——stderr

    一旦c程序运行起来,就会默认打开这三个文件。

    二、过渡到系统,认识文件调用

    文件其实是在磁盘上的,磁盘是外部设备,访问磁盘文件的本质,其实是访问硬件!

    2.1看一看文件的系统调用接口——open

    使用man 2 手册进行查找open接口的功能
    man 2 open

    该函数的功能是:打开/创建一个文件或设备。

    在这里插入图片描述

    这里多嘴一句:c语言中的fopen函数,实现也是将这个open函数进行封装得来的。

    pathname是文件路径,如果传的是相对路径,就是按进程所在的cwd路径为主。
    flags是一个标志位:
    在这里插入图片描述
    这里的标志位有三个:
    O_RDONLY:表示只读操作
    O_WRONLY:表示只写操作
    O_RDWR:表示可读可写

    为了更好地进行后面的传参,下面来讲一个比特位传参的方式

    看下面的代码:

      1 #include<stdio.h>
      2 
      3 #define ONE (1<<0)
      4 #define TWO (1<<1)
      5 #define THREE (1<<2)
      6 #define FOUR (1<<3)
      7 
      8 void show(int flags)
      9 {
     10     if(flags&ONE)   printf("hello function 1\n");
     11     if(flags&TWO)   printf("hello function 2\n");
     12     if(flags&THREE)   printf("hello function 3\n");
     13     if(flags&FOUR)   printf("hello function 4\n");
     14 
     15 }
     16 
     17 int main()
     18 {
     19     show(ONE);
     20     printf("\n");
     21     show(TWO);
     22     printf("\n");
     23     show(ONE|THREE);
     24     printf("\n");
     25     show(ONE|TWO|THREE|FOUR);                                                                                                               
     26     printf("\n");
     27 
     28     return 0;
     29 }
    
    
    • 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

    上图所示的代码定义了几个宏,分别表示(1< 传参时如果穿过来的flag是ONE,则会打印function1,
    如果传的是ONE|TWO|THREE,则传过去的flag的二进制为:111
    此时就能够匹配三个if语句,就会打印出三个function。

    通过这个例子就可以理解了,open函数中的flags作为一个标志位,未来会传很多比特位为1的宏,如果传多个,就能达到不一样的效果!

    下面看这个例子:

    1 #include<stdio.h>
      2 #include<unistd.h>
      3 #include<string.h>
      4 #include<sys/types.h>
      5 #include<sys/stat.h>
      6 #include<fcntl.h>
      7 
      8 int main()
      9 {
     10     // pathname, flags, modes
     11     int fd = open("log.txt",O_WRONLY); //采用八进制,默认权限位666
     12 
     13     if(fd < 0)
     14     {                                                                                                                                       
     15         printf("open file error\n");
     16         return 1;
     17     }
     18 
     19     return 0;
     20 }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    接下来的操作上打开一个文件,因为传的是相对路径,如果按照c语言的fopen函数,如果该文件不存在,那它就会在该进程的cwd路径下创建log.txt文件。

    运行后会发现:居然打开失败了!?

    在这里插入图片描述

    因为系统的open函数的O_WRONLY是只读的,并没有创建文件的功能!
    要想解决这个问题:只需要

    int fd = open("log.txt",O_WRONLY|O_CREAT);
    
    • 1

    增加一个比特位传参即可!
    在这里插入图片描述
    此时就创建出了一个log.txt文件!

    注意:为什么log.txt的权限那么奇怪呢?还是一些随机的权限???

    因为open函数中,第三个函数 mode是权限,我们没有传,就默认是随机的!

    int fd = open("log.txt",O_WRONLY|O_CREAT, 0666); //采用八进制,默认权限位666      
    
    • 1

    再传参之后,重新试试,结果如下:

    在这里插入图片描述
    此时就相对正确了,可是,666权限对应的权限位应该是-rw-rw-rw-
    明显不同,这是因为权限掩码的存在,默认的umask是2,根据权限掩码和权限的计算规则:

    最终权限 = 起始权限 &~umask)
    
    • 1

    最终权限就是664–>-rw-rw-r--

    如果非要将权限设置成666,就更改权限掩码:

    umask(0); 
    
    • 1

    在全局中有一个umask(2),在该进程中也有一个umask(0),所以该文件创建之后其实是听进程中的umask(0)的,因为**就近原则,局部优先,**进程的umask会影响整个进程,但不会影响全局的。
    在这里插入图片描述

    此时log.txt的权限就非常正确了


    总结:这个小节讲了open函数的三个参数:

    pathname , flags , mode

    2.2 write系统接口

    在这里插入图片描述
    write接口是向文件描述符对应的文件中写入。

    文件描述符:file descrpitor(fd),也就是open函数的返回值,这个文件描述符就是一个文件的标识。

     8 int main()
        9 {
       10     umask(0);
       11     // pathname, flags, modes
       12     int fd = open("log.txt",O_WRONLY|O_CREAT, 0666); //采用八进制,默认权限位666
       13 
       14     if(fd < 0)
       15     {
       16         printf("open file error\n");
       17         return 1;
       18     }                                                                                                                                     
       19 
       20     const char* mesg = "Hello Linux";
       21     ssize_t id = write(fd,mesg,strlen(mesg));
       22 
       23     return 0;
       24 }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    此时,向fd文件描述符对应的文件中写入Hello Linux;
    结果显而易见,就不展示了,但是当我们将字符串修改成"aaa"时,结果如下:

    20     const char* mesg = "Hello Linux";
    
    • 1

    在这里插入图片描述

    这个结果跟fwrite函数结果完全不同,fwrite函数是每次打开文件都会清空内容再写入。

    所以,只需要小小地操作:

    12     int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC, 0666); //采用八进制,默认权限位666
    
    • 1

    在这里插入图片描述

    O_TRUNC就是truncate的简写。

    通过O_WRONLY|O_CREAT|O_TRUNC选项,就实现了如果文件不存在就创建,如果文件存在就打开并先清空的逻辑!

    所以,O_APPEND,就是追加的逻辑!

    访问文件的本质

    由此可知,c语言,c++,java等任何其他语言,对文件的操作接口的底层一定是对这些open函数,write函数的封装!!!

    在这里插入图片描述

    可是还有一个问题:open系统调用的返回值是int fd,而fopen函数的返回值是FILE* fp

    这两者有什么关系?

    每次创建一个进程时,都会在内存中创建一个描述该进程的task_struct对象,包含进程中的各种信息,其中就有一个叫做struct file_struct* files的指针,该指针指向一个struct files struct数组,且该数组中的所有成员类型都是struct file*的指针。

    为什么要这样设计呢?

    来看右边:

    每次打开一个文件时,都会创建一个描述该文件的struct file文件对象,该对象存储文件的各种信息。而该文件对象的地址就恰好被进程中的一个指针数组存储着!!!

    在这里插入图片描述

    所以,为什么open函数的返回值为int fd这个文件描述符,其实就是进程中维护的指针数组存储该文件对象的下标!!

    如果该文件对象的被存储在指针数组的2号下标处,打开文件成功后就返回2!(fd)

    当我们尝试着打印该文件的fd时,发现结果是3!

    在这里插入图片描述

    这恰好证明了, 该文件的文件描述符一定是放在进程管理的文件对象指针数组的3号下标处!!!

    可是,为什么是3呢?

    因为前面说过,一个进程创建后,会默认打开三个输入输出流(文件)
    这三个输入输出流分别是:

    stdin stdout stderr

    分别对应的下标是:

    0 1 2

     10 int main()
     11 {
     12     char buffer[1024];
     13     ssize_t sz = read(0,buffer,sizeof(buffer));
     14     //sz返回读取到的个数
     15     if(sz < 0)
     16     {
     17         perror("read fail");
     18         return 1;
     19     }
     20     buffer[sz] = '\0'; // read是按字节读取,如果想把它识别成字符串,就得主动加'\0'                                                          
     21     printf("%s\n",buffer);
    
     22 }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    如上就是从0号文件中读取数据,放入到buffer数组中。

    在这里插入图片描述

    运行起来后会发现,结果就是等待输入,等待键盘文件的输入。

    read系统接口的注意事项:返回值是返回成功读取到的字符的个数。如果想将读取到的若干字符识别成字符串,需要主动添加’\0’。


    下面再看一组测试代码:

      8 int main(){
      9     close(1);
     10 
     12     const char* msg = "Hello Linux\n";
     13     write(1,msg,strlen(msg));
     14     write(2,msg,strlen(msg));                                                                                                               
     15                                       
     16 }     
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    首先close 1号文件后,运行结果只打印了一行msg代码。

    前面说过,1号文件是stdout,对应的是显示器文件,2号文件是stderr,对应的也是显示器文件。它们本质上没有区别,那为什么关闭了1号文件,也就是关闭了显示器文件后,通过2号文件仍然能向显示器中打印呢?

    1号文件和2号文件虽然都是显示器文件,但是他们对应的struct file*指针不同,也就是说,有两个指针指向显示器文件。
    关闭1号文件的本质是,让1号文件对应的指针置空,同时让显示器文件对应的引用计数减减。这个就是close函数的本质操作。

    综上:

    C语言中将fd(文件描述符)封装成了FILE的结构体,不止是c语言,在任何其他语言中,只要是文件操作的结构体,就一定封装了fd(文件描述符)


    总结

    这篇文章讲述了关于文件的基础理解。
    针对的是被打开的文件。

  • 相关阅读:
    php session 的封装 (收藏)
    当代博物馆中的3DGIS虚拟现实搭建
    Springboot框架中使用 Redis + Lua 脚本进行限流功能
    Vue.js 2 —组件(Component)化编程
    Flutter高仿微信-第22篇-支付-二维码收款(二维码)
    [iOS]LLDB调试
    Oracle数据库备份与恢复exp/imp命令
    Java之多态数组
    Python Web3.0应用开发【2022】
    Dobbo微服务项目实战(详细介绍+案例源码) - 2.用户登录
  • 原文地址:https://blog.csdn.net/w2915w/article/details/134143354