Linux文件操作:文件描述符fd

前言:

​ 现在我们对进程的总体概念也有了了解,下面我们进入新的模块学习。关于Linux如何操作文件。其关的操作也与进程有关。

回顾一下文件

​ 我们以前就使用C语言对文件进行读和写的操作,甚至说是追加append。下面代码就是一个最简单的使用C语言操作文件的代码:

#include <stdio.h>

int main()
{
    FILE* fp = fopen("log.txt", "w"); // 以写方式打开log.txt文件
    if(fp == NULL)
    {
        perror("fopen");
        return 1;
    }
    fprintf(fp, "hello file");
    fclose(fp);
    return 0;
}

​ 最后的结果就是在当前目录文件夹下生成了对应的log.txt文件,这些我们在之前学习C语言的时候是有了解过的,我在这里也不必多说。但是我们现在是要从操作系统的角度来理解文件的,因此我们需要学习操作系统关于文件管理的操作。

​ 值得注意的是,这里以写方式操作文件
​ 1、如果文件不存在,就在当前路径下,新建指定文件。
​ 2、默认打开文件的时候,就会把里面的数据全部清空

提炼一下关于文件的理解:

文件 == 属性 + 内容

  • 首先,从语言层面(c语言,c++)来讲,我们是无法真正理解文件的。

    ——这是因为各个语言对应文件管理的接口不一样。因此我们要从操作系统的角度来学习。
    我们要进行文件操作,前提是我们的程序跑起来了,文件的打开和关闭,本质是CPU在执行我们的代码。

  • 操作文件,本质是进程在操作文件

    1. 文件在没有被打开的时候是存在于磁盘中的。
    2. 一个进程可以打开多个文件

    在很多情况下,操作系统内部一定存在大量被打开的文件,因此操作系统必须对打开的文件进行管理!!!
    谈及管理永远六个字:“先描述,在组织

    因此我们猜测,未来估计会有一个类似于task_struct的结构体对文件进行管理

理解文件:

  • 操作文件,本质是进程在操作文件。

  • 文件最是存在于磁盘之中,是外设硬件

  • 向文件写入 => 向硬件中写入

    单用户没有权限直接写入,因为OS是硬件的管理者,OS必须给我们提供系统调用(OS不相信任何人),但是对于fprintf / fscanf 等等C库函数,我们却可以向显示器 / 磁盘 等硬件中写入 / 读取。
    本质:我们用的C / C++都是对系统调用的封装(后面再谈封装)

通过系统调用操作文件:

理解标志位传参:

​ 我们先来看一份代码:

#include <stdio.h>

#define ONE 1             //  1   0000 0001
#define TWO (1 << 1)      //  2   0000 0010
#define FOUR (1 << 2)     //  4   0000 0100

void print(int num)
{
    if(num & ONE)
    {
        printf("1 ");
    }
    if(num & TWO)
    {
        printf("2 ");
    }
    if(num & FOUR)
    {
        printf("4 ");
    }
}

int main()
{
    print(ONE);
    printf("\n");
    
    print(TWO);
    printf("\n");
    
    print(FOUR);
    printf("\n");
    
    print(ONE | TWO);
    printf("\n");
    
    print(ONE | FOUR);
    printf("\n");
    
    print(TWO | FOUR);
    printf("\n");

}

最后的运行结果为:
image-20240822203053894

这需要我们理解按位或的操作,才能理解上述操作,接下来我们就来介绍系统调用函数

打开文件 open

#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

int open(
	const char* pathname,  // 表示要打开的文件的所在路径,如果只有文件名则默认在当前路径
	int flags, 			  // 表示打开文件的方式
	mode_t mode            // 给文件赋予权限,可不提供该参数 (新文件权限随机,旧文件不变)
	);			

​ 我们在上述介绍的位图的概念,其实是为我们的参数做铺垫。对于第一个参数pathname不难理解,怎对于int flag,本质是用比特位来进行标志位的传递(OS设计了很多系统调用接口的常见方法)。传递方式就如同上述所讲的一致。

  • O_WRONLY -> 以写方式打开文件
  • O_CREAT -> 不存在则创建文件
  • O_TRUNC -> 如果文件已经存在,将原有的内容清空(截断)
  • O_APPEND -> 追加信息
  • O_CREAT -> 如果文件不存在,则创建它。需要第三个参数 mode 来指定新文件的权限。
  • O_EXCL -> 和 O_CREAT 一起使用时,如果文件已存在,则 open 失败,确保文件是新建的。
  • O_TRUNC -> 如果文件已存在且以写方式打开,则将文件内容清空。
  • O_APPEND -> 以追加的方式打开文件,写入的数据会添加到文件末尾。
  • O_NONBLOCK -> 对于设备文件,以非阻塞方式打开
  • O_SYNC -> 将写操作同步到磁盘。
  • O_DSYNC -> 类 O_SYNC,但只同步写入操作,不包括元数据的更新

代码演示:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

int main()
{
    int fd = open("log.txt", O_WRONLY | O_CREAT, 0270); // 已权限270的方式创建
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    return 0;
}

image-20240822212004029

写入信息 write

#include <unistd.h>

ssize_t write(
	int fd,           // 文件描述符,表示要写入的是哪个文件
	const void *buf,  // 要写入到文件中的字符串的起始地址
	size_t count      // 要写入到文件当中的字节数
);

代码演示:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

int main()
{
    int fd = open("log.txt", O_WRONLY | O_CREAT, 0270); // 已权限270的方式创建
    if(fd < 0)
    {
        perror("open");
        return 1;
    }
    
    const char* message = "hello system call -> write!\n";
    write(fd, message, strlen(message));
    
    return 0;
}

image-20240822212544165

在这里肯定也存在close关闭文件的,在这里我不做过多的赘述,不要忘记就好了。

理解文件描述符:

对于open的返回值:

​ 先看代码:

#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

int main()
{
    int fda = open("loga.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fdb = open("logb.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    int fdc = open("logc.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    
    printf("fda -> %d\n", fda);
    printf("fdb -> %d\n", fdb);
    printf("fdc -> %d\n", fdc);

    close(fda);
    close(fdb);
    close(fdc);
    return 0;
}

image-20240822213428312

​ 诶,在这里我们发现为什么我们创建新的文件后,对于open返回后的值是从3开始的呢?
不是都说程序员都是从0开始数数的吗,为什么不是从0,而是从3呢?

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

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

  • 你说的嘛,0 && 1 && 2是默认打开的,那么我们可以直接去找对对应的显示1号,直接写入,最后肯定也会直接打印在显示器上:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

int main()
{
    const char* message = "hello No.1 -> Monitor!\n";
    write(1, message, strlen(message));
    return 0;
}

image-20240822214814456

​ 结果也确实如此,这也说明了我们后面创建的文件标识符都是从3开始的。

文件描述fd的本质是什么呢?

问题就是凭什么我们可以直接对一个整形1写入,就可以在显示器显示?

image-20240822222245468

​ 而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包涵一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件描述符,就可以找到对应的文件 。

原理说白了就是:先描述,再组织

简单来说,open是在干嘛呢?

  1. 创建struct file
  2. 开辟文件缓冲区的空间,加载文件数据(这个过程可以延后)
  3. 查看进程的文件描述符
  4. 将struct file地址填入对应的表下标中
  5. 返回下标

所以,本质是文件映射关系的数组的下标。无论读写,都必须在合适的时候让OS把文件的内容读到文件缓冲区中。

如何理解Linux中一切皆文件?

​ 现在我们只是知道0 && 1 && 2 代表着键盘和显示器,可是这些是硬件啊,我们又该如何向这些硬件中写入和读取呢?

  • 如何使用C语言创建类?

    ​ 了解过C++的都知道,C++是一门面向对象的编程语言,而在C++中一切皆对象已经熟的不能再熟。为什么我们使用C++那么爱”类“?无非就是方便管理,正如老板肯定想的是如何赚大钱,那赚大钱的基础肯定是需要管理好手下的员工。正如使用C++目的是为了创建维护良好的项目,基础就是将数据代码管理好,因此也是需要管理!谈及管理,永远绕不开的六个字 —— 先描述,再组织
    ​ 类的出现就完美的实现了这六个字,不同于C语言创建的struct 对象,C++通过类创建出来的class 对象拥有灵活的“组织“功能,关键就是类能“描述”函数,函数就是方法,每个对象都有自己对应的方法!
    ​ 所以知道区别后,C语言想要实现C++的类,需要的正是“描述”函数,来创建动作。
    ​ 对于不同struct对象的函数,我们便可以使用函数指针来找到对应的函数,从而实现类的操作。

  • 深入剖析:

    image-20240822235724252

    ​ 对于每个硬件来说,都有自己独特的函数,例如read和write方式,但是硬件与硬件之间,甚至说型号与型号之间的read函数肯定不一样,然而我们并不需要关系这些区别,我们只负责使用就好!至于read函数是如何实现的,不必关心,因为在每个硬件都会收到对应的struct file进行管理,每个struct file内部都会有对应硬件的函数的函数指针,通过函数指针就能直接使用专门对应的函数

    不必关系底层实现,只负责使用。便是一切皆文件的意义!

    ​ 而我们又会发现,以上这种通过函数指针调用不同函数的方式,不就是我们在C++学习的多态吗!!!!

打通系统调用和C语言函数

​ 现在我们知道,系统在访问文件时,OS只认文件标识符fd。
image-20240823000619243

  • 那对于系统调用和C语言的库函数有什么关系吗?

    我们在使用C语言操作文件时,打开文件是使用函数fopen的,而函数fopen的返回值确实FILE*。
    FILE 本质是一个结构体,内部是会封装文件标识符的。

    #include <stdio.h>
    #include <unistd.h>
    #include <fcntl.h>
    #include <sys/stat.h>
    #include <sys/types.h>
    
    int main()
    {
        FILE* fp = fopen("log.txt", "w");
        printf("stdin-> %d\n", stdin->_fileno);
        printf("stdout-> %d\n", stdout->_fileno);
        printf("stderr-> %d\n", stderr->_fileno);
        printf("fp-> %d\n", fp->_fileno);
        fclose(fp);
        return 0;
    }
    

    image-20240823001610356

    FILE 本质是一个结构体,内部是会封装文件标识符的。而这个文件标识符就是_fileno。

    因为stdin、stdout和stderr的类型如下:

    extern FILE* stdin;
    extern FILE* stdout;
    extern FILE* stderr;
    

    所以我们也能打印他们对于的文件标识符,也和上面讲的一样,默认为0、1、2

  • 为什么要进行封装?

    ​ C语言的这些函数代码,是在我们配置环境时存在于自动下载的库当中,而这个库就是我们
    熟知的——C标准库。

    ​ 我们当然可以不用C语言的方式而使用系统调用来操作文件,但是不同平台具有不同的系统调用。windows有自己的一套,Linux有自己的,mac当然也有自己的。所以为了保证代码跨平台性我们建议使用C语言的方式来操作文件
    ​C语言在封装的时候,会使用条件编译来区分操作系统的类别:

    fopen()
    {
    #if windows
    	[...windows...]
    #elif mac
    	[...mac...]
    #elif linux
    	[...linux...]
    }
    

    然后通过这一份代码,针对不同的操作系统生成不同的C标准库,即可完成跨平台性!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

无双@

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值