【Linux取经路】文件系统之被打开的文件——文件描述符的引入

在这里插入图片描述

文章目录

  • 一、明确基本共识
  • 二、C语言文件接口回顾
    • 2.1 文件的打开操作
    • 2.2 文件的读取写入操作
    • 2.3 三个标准输入输出流
  • 三、文件有关的系统调用
    • 3.1 open
      • 3.1.1 比特位级别的标志位传递方式
    • 3.2 write
      • 3.2.1 模拟实现 w 选项
      • 3.2.2 模拟实现 a 选项
    • 3.3 read
  • 四、访问文件的本质
    • 4.1 再来认识 FILE
    • 4.2 再来理解关闭文件
  • 五、结语

一、明确基本共识

  • 文件等于内容加属性,内容和和属性都是数据,不管是内容还是属性都要在磁盘中保存。

  • 文件分为打开的文件和没打开的文件。

  • 打开的文件本质是进程打开的,要研究打开的文件,本质是研究进程和文件的关系。

  • 对文件的所有操作(打开文件、读取文件、向文件写入)等,都是通过代码来实现的,而代码最终是由 CPU 去执行的,根据冯诺依曼结构体系,CPU 不能直接和外设打交道,因此一个被打开的文件,第一步一定是先将其加载到内存。

  • 一个进程能够打开多个文件,所以在操作系统内部一定存在大量的被打开的文件,操作系统还是通过先描述,再组织的方式对打开的文件进行管理。每个被打开的文件都必须有自己的文件打开对象,其中一一定包含了文件的很多属性,将这些文件对象以某种特殊的数据结构组织起来,最终对文件的管理,就变成了对某种数据结构的维护(增删查改)。

  • 没打开的文件一般都是在磁盘上放着,对于没打开的文件,由于没打开的文件非常多,所以对于没打开的文件我们最关心文件如何被分门别类的放置好,分门别类的放置好是为了快速的进行增删查改。

二、C语言文件接口回顾

2.1 文件的打开操作

// 文件打开接口
FILE *fopen(const char *path, const char *mode);

第一个参数 path ,表示要打开的文件路径,或者文件名。如果只有文件名前面没写路径,表示打开当前路径下的文件。这里又涉及到当前路径,在前一篇文章中实现 cd 指令的时候就讲过什么是当前路径。总的来说,当前工作路径是一个进程 PCB 中维护的一个属性。一个可执行程序在被加载到内存成为进程创建出对应的 PCB 对象的时候,PCB 对象中就维护了一个叫做 cwd 的属性,该属性就表示进程当前的工作路径。

在这里插入图片描述
如果 fopen 函数的第一个参数只传递了文件名,最终在打开文件的时候,操作系统会去 cwd 指向的工作路径下查找该文件。

第二个参数 mode,这个参数有很多可选项,今天只介绍个别选项,关于所有选项的详细介绍请看我之前的文章【C语言进阶】文件操作。

  • w选项:只要是以 w 选项打开的文件,在写入之前都会对文件做清空处理,然后从头开始写入。

  • a选项:在文件结尾进行追加写。

小Tips:我们之前介绍的重定向,> 本质上就对应使用的是 w 选项,>> 本质上就对应使用的是 a 选项。

2.2 文件的读取写入操作

和文件读取写入的相关接口,以及使用方法,今天也不过多介绍,详细介绍请看我之前写的文章【C语言进阶】文件操作。今天只想通过 fwrite 接口跟大家明确一件事情。

// fwrite 接口声明
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
int main()
{
    FILE *fp = fopen("log.txt", "w");
    if (fp == NULL)
    {
        // 打开失败
        perror("fopen");
        return errno;
    }
    // 打开成功,对文件进行相关的操作
    // ...
    char* str = "Hello Linux!";
    fwrite(str, strlen(str), 1, fp);
    // 操作结束,关闭文件
    fclose(fp);
    return 0;
}

fwrite 接口的第二个参数 size 表示每一个要写入的对象的大小。在向文件写入字符串的时候,该参数是字符串的长度还是字符串的长度加一呢?因为 strlen 计算出来的字符串长度是不包含结尾的 \0,加一的小伙伴觉得要把 \0 也写到文件里面,但是 \0 真的需要写入文件嘛?其实 \0 并不需要写入文件中,因为字符串以 \0 结尾只是 C 语言这么规定的,我们把一个字符串写入文件后,可能通过其它的语言去读取该文件,我们并不希望读到与该字符串无关的内容 \0。下面是加一的结果:

在这里插入图片描述
\0 也是字符,只不过不可显,在被写入到文件后,vim 编辑器会把它识别成 ^@,对 Hello Linux 来说,^@ 就是多余的无用字符。我们不希望它在文件中出现。

2.3 三个标准输入输出流

C程序在启动时候,默认会打开三个标准流文件:

  • stdin:标准输入流——键盘文件

  • stdout:标准输出流——显示器文件

  • stderr:标准错误流——显示器文件

三、文件有关的系统调用

文件最初是在磁盘上的,磁盘是外部设备,访问磁盘文件其实是访问硬件,在计算机层状结构中,硬件是处于最底层的,操作系统帮我们把这些硬件管理起来,并且操作系统是不相信用户的,因此操作系统不允许我们直接去访问硬件,而是给我们提供了系统调用接口,几乎所有的库只要是访问硬件设备,必定要封装系统调用。也就是说我们平时在C语言里面使用的 fopenprintffprintffscanf等函数都一定是封装了系统调用。

3.1 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:标志位选项。O_RDPPNLY:只读打开;O_WRONLY:只写打开;O_RDWR:读,写打开。这三个常量,必须指定一个且只能指定一个。O_CREAT:若文件不存在,则创建它。需要使用 mode 参数,来指明新文件的访问权限。O_APPEND:追加写;O_TRUNC:文件打开的时候先清空。

  • 第三个参数 mode:新创建文件的默认权限,要考虑权限掩码,可以配合 umask 系统调用接口来设置自己想要的效果。umask 系统调用产生的效果就只对当前进程创建的文件有关。

  • 返回值:成功,返回新打开的文件描述符,关于文件描述符是什么,将在后文为大家介绍;失败,返回-1。

小Tipsopen 函数具体使用哪个,和具体的应用场景有关,如目标文件不存在,需要 open 创建,则第三个参数表示创建文件的默认权限。如果不需要创建新文件,使用两个参数的 open

3.1.1 比特位级别的标志位传递方式

#define ONE (1<<0) // 1
#define TWO (1<<1) // 2
#define FOU (1<<2) // 4
#define EIG (1<<3) // 8

void show(int flags)
{
    if(flags & ONE) printf("function1\n");
    if(flags & TWO) printf("function2\n");
    if(flags & FOU) printf("function3\n");
    if(flags & EIG) printf("function4\n");

    return;
}

int main()
{
    printf("--------------------------------------\n");
    show(ONE);
    printf("--------------------------------------\n");
    show(ONE | TWO);
    printf("--------------------------------------\n");
    show(ONE | TWO | FOU );
    printf("--------------------------------------\n");
    show(ONE | TWO | FOU | EIG);
    printf("--------------------------------------\n");
    return 0;
}

在这里插入图片描述
小Tips:这种比特位级别的标志位传递方式,使用户可以在函数调用的时候采用按位或的方式传递多个选项实现不同的功能。open 函数的第二个参数就是采用这种方式就是这样。

3.2 write

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
  • 第一个参数 fd:表示待写入文件的文件描述符。

  • 第二个参数 buf:指向待写入的文件内容。

  • 第三个参数 count:待写入内容的大小,单位是字节。

  • 返回值:实际上写入的字节数。

3.2.1 模拟实现 w 选项

int main()
{
    umask(0); // 将权限掩码设置成全0
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666); // 以读的方式打开,若文件不存在就创建,打开文件时清空
    if(fd < 0)
    {
        printf("open file\n");
        return errno;
    }

    const char* str = "aaa";
    ssize_t ret = write(fd, str, strlen(str));

    close(fd);

    return 0;
}

在这里插入图片描述

3.2.2 模拟实现 a 选项

int main()
{
    umask(0); // 将权限掩码设置成全0
    int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 以读的方式打开,若文件不存在就创建,以追加的方式进行写入
    if(fd < 0)
    {
        printf("open file\n");
        return errno;
    }

    const char* str = "aaa";
    ssize_t ret = write(fd, str, strlen(str));

    close(fd);

    return 0;
}

在这里插入图片描述

3.3 read

#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
  • 第一个参数 fd:要读取文件的文件描述符。

  • 第二个参数 buf:指向一段空间,该空间用来存储读取到的内容。

  • 第三个参数 count:参数二指向空间的大小。

四、访问文件的本质

在这里插入图片描述

总结:一个被打开的文件,加载到内存,会为该文件创建一个 struct file 结构体对象,操作系统对文件的管理本质上就是对 struct file 结构体对象的管理,操作系统会将当前所有被打开文件的 struct file 对象以双链表的形式组织起来。进程的 PCB 对象中有一个 struct files_struct 类型的指针,指向该类型的一个对象,该类型对象里面记录了当前进程所打开的所有文件新信息,其中中维护了一个 struct file* 类型的数组,数组的内容就指向了当前进程所打开的文件结构体对象,简言之就是指向了当前进程打开的文件。我们将这个数组就叫做文件描述符表,数组的下标就叫做文件描述符(因此文件描述符一定大于0)。open 函数的返回值其实就是文件描述符,即只要当前进程打开一个新文件,操作系统就会按照从前往后的顺序从该进程的文件描述符表中分配一个数组下标,该下标对应的内存空间中存储的就是该文件结构的地址。此后要对该文件进行任何操作,只需要知道它对应的数组下标即可。

int main()
{
    umask(0); // 将权限掩码设置成全0
    int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666); // 以读的方式打开,若文件不存在就创建,以追加的方式进行写入
    int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
    printf("fd1: %d\n", fd1);
    printf("fd2: %d\n", fd2);
    printf("fd3: %d\n", fd3);
    printf("fd4: %d\n", fd4);

    return 0;
}

在这里插入图片描述
小Tips:通过结果可以看出,进程新打开的文件,其下标只能从3,开始,这是因为 C 程序在运行起来的时候操作系统会默认帮我们打开三个流,标准输入流 stdin 对应键盘文件,下标为0;标准输出流 stdout 对应显示器文件,下标为1;标准错误流 stderr 对应显示器文件,下标为2。从这里可以的出一个结论,默认打开三个标准输入输出流并不是 C 语言的特性,而是操作系统的特性,所有语言编写的程序运行起来后都会打开。操作系统为什么要帮我们打开呢?因为电脑在开机的时候,键盘和显示器就已经被打开了,我们在编程的时候,一般都会用键盘输入和通过显示器查看结果。

文件描述符对应的分配规则:从0下标开始,寻找最小的没有使用的数组位置,它的下标就是新打开文件的文件描述符。

4.1 再来认识 FILE

FILE 是 C 语言库中自己封装的一个结构体,在 C 语言中,通过 FILE 对象去描述文件。可以确定,FILE 中一定封装了文件描述符。如下面代码,FILE 中的 _fileno 属性就是文件描述符。

int main()
{
    printf("stdin->fd: %d\n", stdin->_fileno); // 标准输入
    printf("stdout->fd: %d\n", stdout->_fileno); //标准输出
    printf("stderr->fd: %d\n", stderr->_fileno); // 标准错误
    return 0;
}

在这里插入图片描述

4.2 再来理解关闭文件

一个文件可以被多个进程同时打开,最常见的比如键盘文件,显示器文件。在 struct file 对象中有一个 f_count 字段,叫做当前文件的引用计数,记录了当前文件被多少个进程打开了,在进程视角关闭文件就是调用 close 系统调用,将对应下标里面的内容置为 NULL,这是进程系统需要执行的工作。置空后操作系统会把该文件描述对应文件结构体对象中的 f_count 字段减减,然后判断 f_count 是否为0,如果不为0就什么也不干,如果为0,操作系统才将对应的 struct file 对象回收,这是文件系统执行的工作。从这儿可以看出,文件描述符表的存在,将进程系统和文件系统进行了完美的解藕。这不禁让我想起了前面的虚拟地址(进程地址空间)和页表的存在将进程系统和内存系统进行解藕。Linux 操作系统的设计真的让人拍案叫绝!

int main()
{
    close(1); // 将 stdout 关闭
    int ret = printf("stdin->fd: %d\n", stdin->_fileno);
    printf("stdout->fd: %d\n", stdout->_fileno);
    printf("stderr->fd: %d\n", stderr->_fileno);

    fprintf(stderr, "printf ret: %d\n", ret);
    return 0;
}

在这里插入图片描述
代码分析close(1) 表示将标准输出关闭,1下标指向显示器文件,printf 就是向标准输出中进行写入,关闭后,三条 printf 函数都没有将内容成功打印到显示器上。根据上面的分析,虽然把标准输出关了,但是标准错误也指向显示器,所以在调用 fprintf 向标准错误中写入时,我们可以在显示器上看到打印结果。其次,printf 执行成功,返回值表示写入的字符个数,可以看出虽然我们通过系统调用直接把标准输出给关了,但是 printf 还是认为它写入成功。

五、结语

今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/389620.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

STM32,嵌入式系统中的I2C协议

I2C协议——读写EEPROM 关注我&#xff0c;共同交流&#xff0c;一起成长 前言一、协议简介二、I2C特性及架构三、通信过程 前言 这是一种主要用于集成电路和集成电路&#xff08;IC&#xff09;通信&#xff0c;计算机中复杂的问题大多数就是用分层来进行解决&#xff0c;这个…

k8s-项目部署案例

一、容器交付流程 在k8s平台部署项目流程 在K8s部署Java网站项目 DockerFile 如果是http访问&#xff0c;需要在镜像仓库配置可信任IP 三、使用工作负载控制器部署镜像 建议至少配置两个标签 一个是声明项目类型的 一个是项目名称的 继续配置属性 资源配额 健康检查 五、使…

积分(二)——复化Simpson(C++)

前言 前言 simpson积分 simpson积分公式 ∫ a b f ( x ) d x ≈ b − a 6 [ f ( a ) f ( b ) 4 f ( a b 2 ) ] \int_{a}^{b}f(x)dx \approx \frac{b-a}{6}[f(a)f(b)4f(\frac{ab}{2})] ∫ab​f(x)dx≈6b−a​[f(a)f(b)4f(2ab​)] 与梯形积分类似&#xff0c;当区间[a,b]较…

Java 和 JavaScript 的奇妙协同:语法结构的对比与探索(下)

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

卷积神经网络的基本结构

卷积神经网络的基本结构 与传统的全连接神经网络一样&#xff0c;卷积神经网络依然是一个层级网络&#xff0c;只不过层的功能和形式发生了变化。 典型的CNN结构包括&#xff1a; 数据输入层&#xff08;Input Layer&#xff09;卷积层&#xff08;Convolutional Layer&#x…

社区商铺开什么店最好?从商业计划书到实际运营

在社区商铺开店&#xff0c;选择适合的业态是成功的关键。作为一名开店 5 年的资深创业者&#xff0c;我想分享一些关于社区店的干货和见解。 这篇文章&#xff0c;我用我的项目给大家举例子&#xff01; 鲜奶吧作为一种新兴的业态&#xff0c;以提供新鲜、健康的乳制品为主&…

vue3 之 倒计时函数封装

理解需求 编写一个函数useCountDown可以把秒数格式化为倒计时的显示xx分钟xx秒 1️⃣formatTime为显示的倒计时时间 2️⃣start是倒计时启动函数&#xff0c;调用时可以设置初始值并且开始倒计时 实现思路分析 安装插件 dayjs npm i dayjs倒计时逻辑函数封装 // 封装倒计时…

C++类和对象-多态->多态的基本语法、多态的原理剖析、纯虚函数和抽象类、虚析构和纯虚析构

#include<iostream> using namespace std; //多态 //动物类 class Animal { public: //Speak函数就是虚函数 //函数前面加上virtual关键字&#xff0c;变成虚函数&#xff0c;那么编译器在编译的时候就不能确定函数调用了。 virtual void speak() { …

流量主小程序/公众号h5开源代码 源码分享

小程序开源代码合集 1、网课搜题小程序源码/小猿题库多接口微信小程序源码自带流量主 搭建教程 1、微信公众平台注册自己的小程序 2、下载微信开发者工具和小程序的源码 3、上传代码到自己的小程序 界面截图&#xff1a; 开源项目地址&#xff1a;https://ms3.ishenglu.com…

python 人脸检测器

import cv2# 加载人脸检测器 关键文件 haarcascade_frontalface_default.xml face_cascade cv2.CascadeClassifier(haarcascade_frontalface_default.xml)# 读取图像 分析图片 ren4.png image cv2.imread(ren4.png) gray cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)# 进行人脸…

php基础学习之函数

基本概念 是一种语法结构&#xff0c;将实现某一个功能的代码块封装到一个结构中&#xff0c;从而实现代码的重复利用 php函数的定义语法 &#xff08;与C/Java很类似&#xff0c;区别在于没有数据类型&#xff0c;因为php是弱类型语言&#xff09; function 函数名(参数){ //…

波奇学Linux:文件系统打开文件

从文件系统来看打开文件 计算机系统和磁盘交互的大小是4kb 物理内存的4kb&#xff0c;磁盘的4kb文件叫做页帧 磁盘数据块的以4kb为单位。 减少IO的次数&#xff0c;减少访问外设的次数--硬件 基于局部性的原理&#xff0c;预加载机制--软件 操作系统管理内存 操作系统对…

leetcode hot 100最小花费爬楼梯

本题和之前的爬楼梯类似&#xff0c;但是需要考虑到花费的问题&#xff01;**注意&#xff0c;只有在爬的时候&#xff0c;才花费体力&#xff01;**那么&#xff0c;我们还是按照动态规划的五部曲来思考。 首先我们要确定dp数组的含义&#xff0c;那么就是我们爬到第i层所花费…

[嵌入式AI从0开始到入土]14_orangepi_aipro小修补含yolov7多线程案例

[嵌入式AI从0开始到入土]嵌入式AI系列教程 注&#xff1a;等我摸完鱼再把链接补上 可以关注我的B站号工具人呵呵的个人空间&#xff0c;后期会考虑出视频教程&#xff0c;务必催更&#xff0c;以防我变身鸽王。 第1期 昇腾Altas 200 DK上手 第2期 下载昇腾案例并运行 第3期 官…

DDR简单了解

DDR全称为 double data rate Synchronous Dynamic Random Access Memory 既DDR SDRAM。 顾名思义需要依次了解这些名词DRAM, SDRAM, DDR, DDR2, DDR3, DDR4。因为这些名词代表DRAM发展的不同阶段&#xff0c;它们是内存的同一条技术路线&#xff0c;核心都是使用一个晶体管和一…

debug - 打补丁 - 浮点数加法

文章目录 debug - 打补丁 - 浮点数加法概述笔记demo用CE查看汇编(x64debug)main()update_info()快捷键 - CE中查看代码时的导航打补丁的时机 - 浮点数加法补丁代码补丁效果浮点数寄存器组的保存END debug - 打补丁 - 浮点数加法 概述 在cm中, UI上显示的数值仅仅用来显示, 改…

开启AI新篇章:全新GPT-4订阅方案! ChatGPTPlus(GPT4)支付渠道! 付费充值!

1. GPT-4订阅价格 以每月仅20美元的价格&#xff0c;引领您进入GPT-4的强大数字体验世界。作为前沿的语言模型&#xff0c;GPT-4为您的工作和创造带来了无与伦比的生产力提升&#xff0c;彻底改变您的工作和创造方式。 GPT-4不仅具有卓越的自然语言处理能力&#xff0c;还引入…

kafka如何保证消息不丢?

概述 我们知道Kafka架构如下&#xff0c;主要由 Producer、Broker、Consumer 三部分组成。一条消息从生产到消费完成这个过程&#xff0c;可以划分三个阶段&#xff0c;生产阶段、存储阶段、消费阶段。 产阶段: 在这个阶段&#xff0c;从消息在 Producer 创建出来&#xff0c;…

【汇总】解决IndexedDB报Failed to execute ‘transaction‘ on ‘IDBDatabase‘

问题发现 再学习HTML5中&#xff0c;有介绍到 Web 存储&#xff0c;当代码编写完成后&#xff0c;运行报错 Failed to execute ‘transaction’ on ‘IDBDatabase’: One of the specified object stores was not found. 示例代码如下&#xff1a; <!DOCTYPE html> <…

【后端高频面试题--Nginx篇】

&#x1f680; 作者 &#xff1a;“码上有前” &#x1f680; 文章简介 &#xff1a;后端高频面试题 &#x1f680; 欢迎小伙伴们 点赞&#x1f44d;、收藏⭐、留言&#x1f4ac; 后端高频面试题--Nginx篇 往期精彩内容什么是Nginx&#xff1f;为什么要用Nginx&#xff1f;为…
最新文章