万字解析文件fd,深刻理解:fd文件描述符、位图、标准输入、标准输出、标准错误、文件打开、文件关闭、Linux一切皆文件理解、进程和文件的关系、虚拟软件系统

建议全文阅读!!!

建议全文阅读!!!

建议全文阅读!!!

目录

文章概述

一、文件操作

1、什么叫当前路径

2、常见文件操作

(1)fopen函数

(2)fclose函数

(3)fprintf函数

3、理解文件:

4、常见系统调用

(1)open接口

[1]umask掩码

[2]位图

(2)close接口

(3)write接口

二、文件描述符fd

1、打开文件

2、标准输入、输出、错误

3、编程语言的本质


文章概述

什么是文件?
打开文件是什么意思?
怎么打开文件?
关闭文件又是什么意思?
怎么关闭文件?
打开的文件怎么管理?
关闭的文件又怎么管理?
为什么要管理?
如何管理?
文件路径是什么意思?
什么是位图?
我们不能仅从语言角度去理解文件,还需要站在操作系统的角度去理解才能全面。

所以,本文将会从操作系统的角度去深刻理解文件。
同时,回答上述的若干问题。

一、文件操作

1、什么叫当前路径

什么叫做当前路径?
当你运行一个程序的时候,你的程序就会变成一个进程
我们的路径,就是进程在执行的当前路径

查看当前路径:

pwd

2、常见文件操作

(1)fopen函数

FILE *fopen(const char *filename, const char *mode);

参数:

filename 参数是一个字符串,指定要打开的文件名或路径。
mode 参数是一个字符串,指定文件的打开模式,可以是:

"r"(只读)、

"w"(写入,如果文件存在则清空)、

"a"(追加)、

"r+"(读写,文件必须存在)、

"w+"(读写,如果文件存在则清空)、

"a+"(读写,追加到文件末尾)等。

返回值:
如果成功打开文件,则返回指向 FILE 类型结构的指针,该指针用于后续的文件操作。
如果打开失败,返回 NULL,并且通过检查 errno 变量可以确定失败的具体原因。

示例:

FILE *fp;
fp = fopen("example.txt", "r");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

(2)fclose函数

int fclose(FILE *stream);

参数:

stream 是一个指向 FILE 结构的指针,指定要关闭的文件。

返回值:
如果成功关闭文件,则返回 0。
如果关闭失败,则返回 EOF(通常为 -1),并且可以通过检查 errno 变量来确定关闭失败的具体原因。

示例:

FILE *fp;
fp = fopen("example.txt", "r");
if (fp == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILURE);
}

// 操作文件...

if (fclose(fp) == 0) {
    printf("File closed successfully.\n");
} else {
    perror("Error closing file");
    exit(EXIT_FAILURE);
}

(3)fprintf函数

int fprintf(FILE *stream, const char *format, ...);

参数:

stream 是一个指向 FILE 结构的指针,指定要写入的目标文件。
format 是一个格式化字符串,类似于 printf 函数中的格式化字符串,用于指定输出的格式。

返回值:

返回成功写入的字符数,如果出错则返回一个负数。


默认打开文件的时候,清空文件内容
a打开方式:append追加方式写入文件,不会清空原文件内容

3、理解文件:

(1)操作文件,本质是进程在控制文件,强调的是进程和文件的关系
(2)用户打开文件时:
找到对应磁盘->但磁盘是外部设备->把文件向磁盘写入:本质是向硬件中写入
但是用户没有权力直接写入底层硬件,因为硬件由操作系统直接管理
所以,要通过操作系统接口,系统调用
这些系统调用提供给用户进行和底层数据的控制交互

但是,我们对文件的操作都是c语言/c++等语言的函数,例如上述三个函数
我们并使用到系统调用啊?
系统调用在哪里?
然而,事实上,所有的语言函数
本质上都是对系统调用的封装
后面我们再细谈

4、常见系统调用

(1)open接口

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

两个参数的open函数,一般是打开已经存在的文件
三个参数的open函数,一般是打开没有存在的文件

参数:

pathname 是一个字符串,表示要打开的文件路径。
flags 是打开文件的标志位,例如 O_RDONLY(只读)、O_WRONLY(只写)、O_RDWR(读写)等。
mode 是文件的权限,通常与 flags 参数中的某些标志结合使用,用于指定文件的创建模式。

返回值:

如果成功,返回一个新的文件描述符,用于后续的文件操作。
如果出错,返回 -1。

[1]umask掩码

open函数的第三个参数怎么理解?

第三个参数,是设置文件创建的初始权限,例如:0666:wr_ 0777:wrx

当我们创建一个新的文件时,会有一个默认的文件权限:标红部分

初始文件权限,等于你初始设置的权限 按位与 umask掩码值

查看掩码:一般默认掩码是0022

umask

动态调整掩码:设置当前进程下的掩码,掩码使用规则是:就近原则

umask(num);
[2]位图

例如,一个整数有32位,用比特位来进行标志位的传递

什么是位图?是一种标记位传参
这些标记位都是一个一个的宏,这些宏只有一个1

例如:

1是:0000 0001

2是:0000 0010

4是:0000 0100

8是:0000 1000

...

以此类推

根据上述,我假设,

参数为1,是一个功能A

参数为2,是一个功能B

参数为3,是一个功能C

....

那么,当我同时想要A和B这两个功能时

按理来说,我就要两个参数

也就是说

要传2个参数

那么我的函数对应的就要有两个形参

那我要向同时想要ABC功能呢?
我的函数就要设计成三个相残

以此类推

不好解决

于是我怎么办呢?

传参还是一个形参int

当我想要A和B功能结合时

传参为:1 | 2

        即:0000 0001

             | 0000 0010

 结果为:0000 0011 = 3

于是,对应的函数中,我就将之0000 0011 = 3设置为A和B的功能结合

所以,我就只用一个参数,做到了两个参数的结合

这种设计方式,遇用到了比特位的运用

所以,叫做位图设计

下面我将会设计一个简单的位图程序
让你更好的理解什么是位图的设计方案

#include<stdio.h>
#define ONE 1
#define TWO ONE<<1
#define THREE TWO<<1
#define FOUR THREE<<1

void func(int flag)
{
  if(flag == ONE)
  {
    printf("NOE\n");
  }
  else if(flag == (TWO |ONE))
  {
    printf("TWO and NOE\n");
  }
  else if(flag == (THREE | TWO))
  {
   printf("THREE and TWO\n");  
  }
  else if(flag == (FOUR|THREE))
  {
   printf("FOUR and THREE\n");  
  }
  else
  {}
}
int main()
{
      func(ONE);
     func(ONE | TWO);
     func(FOUR | THREE); 

  return 0;
}

在此函数的设计思路上,去设计其他功能的函数,就是位图设计模式

那么,当有了这种设计模式后
当一个函数需要传参时,我们就不需要笨重的写多个参数
而是直接使用标记位模式,设计宏,设计代码

根据上述的理解,现在我们来理解open函数的第二个参数
这些参数就是一些宏,每个宏只有一个比特位为1
对应不同的选项
当两个不通过的选项(宏)用按位或 | 结合起来时
就会将之不同的选项功能结合起来
例如: 01 | 10 = 11
这就实现了只用一个参数,就实现了多个功能的选择组合

下列是open函数的位图宏参数列表

位图宏参数功能描述
O_RDONLY只读打开文件。
O_WRONLY只写打开文件。
O_RDWR读写打开文件。
O_CREAT如果文件不存在,则创建文件。
O_EXCLO_CREAT一同使用时,要求文件必须不存在,否则返回错误。
O_APPEND追加写入文件末尾。
O_TRUNC如果文件已存在且是普通文件,则将其长度截断为0。
O_NONBLOCK非阻塞模式打开。
O_SYNC每次write操作都要等待物理I/O完成。
O_DIRECTORY如果打开的是一个目录,则要求它是一个目录文件。
O_NOFOLLOW如果path是一个符号链接,则不会跟随符号链接。
O_CLOEXEC在进程执行exec系列函数时,自动关闭该文件描述符。

而,我们c语言的fopen以为w方式打开文件时
功能是:如果有文件,清空原文件的所有内容、没有文件就创建
你发现了什么?
你会发现,这个功能,不就是系统调用open中,O_WEONLY和O_CREAT功能的组合吗?
所以,本质上,c语言的fopen这个函数
就是对系统调用open功能函数的组合封装

运用示例:

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int fd;
    fd = open("file.txt", O_WRONLY | O_CREAT, 0644);
    if (fd == -1) {
        perror("Failed to open file");
        return 1;
    }
    close(fd);
    return 0;
}

(2)close接口

#include <unistd.h>
int close(int fd);

参数:

fd:要关闭的文件描述符。

返回值:

如果成功,返回值为0。
如果失败,返回值为-1,并设置全局变量 errno 来指示错误类型。

(3)write接口

#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);

参数:

fd:要写入的文件描述符。
buf:指向要写入数据的缓冲区。
count:要写入的字节数。


返回值:

如果成功,返回实际写入的字节数(非负值)。
如果出错,返回值为-1,并设置 errno 来指示错误类型。

上述函数的返回值都是一个int整数,叫做文件描述符fd
0:标注你输入 键盘
1:标准输出 屏幕
2:标准错误 屏幕

二、文件描述符fd

什么是fd?是一个数组的下标!

我们知道,操作系统运行时,会有很多进程在运行
同时,也会打开很多文件
在文件没有被打开之前,文件是存在磁盘中的
那么,打开文件,是谁打开的呢?
是进程打开的
当要打开一个文件时,进程将文件的数据加载到文件内核级的缓存块中
因此,打开文件:本质是进程打开文件
而一个进程能打开很多文件
同时,系统当中可以存在许多进程
所以,在操作系统内部存在大量的文件
这些文件,可能有上万、上几十万个
有些是关闭,又是打开的
那么,操作系统如何对这些大量的文件进行管理呢?
例如:
这个文件是那个进程打开的?
以什么方式打开的?
什么时候打开?
什么时候关闭?
谁来关闭?
.....
同样的,六字真言:先描述,再组织
为了更好的管理文件
所有被打开的文件,操作系统都会对其创建一个结构体struct file,在Linux内核定义如下:

struct file {
    union {
        struct llist_node fu_llist;
        struct rcu_head fu_rcuhead;
    } f_u;
    struct path f_path;
    struct inode *f_inode;  /* 文件对应的inode */
    const struct file_operations *f_op;  /* 文件操作函数表 */
    atomic_long_t f_count;
    unsigned int f_flags;  /* 文件标志 */
    fmode_t f_mode;  /* 文件的访问权限和打开模式 */
    loff_t f_pos;  /* 文件当前的读写位置 */
    struct fown_struct f_owner;
    const struct cred *f_cred;
    struct file_ra_state f_ra;
    u64 f_version;
#ifdef CONFIG_SECURITY
    void *f_security;
#endif
    /* needed for tty driver, and maybe others */
    void *private_data;

#ifdef CONFIG_EPOLL
    /* Used by fs/eventpoll.c to link all the hooks to this file */
    struct list_head f_ep_links;
    struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
    struct address_space *f_mapping;
#ifdef CONFIG_MMU
    struct vm_fault *f_vm_fault; /* fault info */
#endif
    /* Open file handle status information */
    u64 f_version;
    atomic_t f_count;
    spinlock_t f_lock;
    u32 f_flags;  /* open flags */
    fmode_t f_mode;  /* access mode */
    struct path f_path;
    struct inode *f_inode;  /* associated inode */
    const struct file_operations *f_op;  /* file operations table */
    struct device *f_owner;  /* device */
    const struct cred *f_cred;  /* credentials */
    struct file_ra_state f_ra;  /* read-ahead state */
    void *f_security;  /* security descriptor */
    void *private_data;  /* instance data */
};

我们说过,文件 = 属性 + 内容
而这个struct file结构体对象
就是用来描述一个个被打开的文件
这些被打开的文件结构体对象,就被操作系统用双向链表组织起来,成为一个双向链表
于是,操作系统对被打开文件的管理
就变成了对一个双向链表的增删查改
与此同时,在文件被打开时,
操作系统还会给对应别打开的文件,分配一个叫做文件内核级的缓存块
用于文件数据的存放,当文件被进程打开时
进程就会把文件的数据加载到该缓存块

可是,这只解决了对文件的管理
对于文件和进程之间的从属关系,如何描述组织呢?
所以,为了描述这个关系
在进程的PCB(task_struct)结构体对象中
存在一个指针变量:struct files_struct *file
这个指针变量指向一个数组:struct file* fd_array[N]
这个fd_array[N]数组内部的元素,是一个一个的file文件对象的指针
而指针,就是地址;
而地址,就是映射
所以,这些指针就指向,被该进程打开的文件
而fd_array[N]是一个数组
既然是数组,那么就必然会有下标

这个数组的下标,就对应着一个个被打开的文件的指针地址
所以,当用户想要访问一个进程下的文件
而每一个进程的PCB是唯一的,所以文件管理数组也唯一的
只要是进程打开一个文件,就会把文件的*file添加进去
我只需要返回这个进程PCB结构体下,文件管理数组的下标
是不是就可以找到对应的文件对象了?
因为只要对上提供这个下标
我就可以顺藤摸瓜,找到对文件管理数组下标对应的文件指针
即可找到文件

因此,
open函数的返回值,为int fd
这个fd到底是什么?
fd就是下标!!
即文件描述符

因此,观察下述三个系统调用函数:

 ssize_t write(int fd, const void *buf, size_t count);
 ssize_t read(int fd, void *buf, size_t count);
int close(int fd);

他们的参数,都有一个int fd
也就是说,当打开一个文件时,
要对这个文件进行写、读、操作
首先要解决对谁写?对谁读?关闭谁?
所以,清一色的,都传递open时返回的文件描述符:fd参数

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

让write、read、close这三个函数
用fd去找到对应的文件
然后执行相关操作

同时,对文件的读写等操作的数据改动
都是在文件缓存块内进行

然后由操作系统刷新到磁盘对应位置
所以,读取文件,本质上就是从文件的文件内核缓存块内加载数据到应用层
而写文件,以及是在文件缓存块进行
所以,无论是读文件还是写文件
都是内存级别的操作
然后再由操作系统在合适的时机刷新到磁盘中
所以,本质来说,read、write都是拷贝函数,就是般数据的函数
这里要注意:
磁盘中的文件数据,必须要先加载到文件缓存块中(内存)
才可以进行修改

1、打开文件

所以,当open一个文件时,到底在干什么?
1、创建file文件结构体
2、开辟文件缓存区的空间,加载文件数据(从磁盘中加载,有可能延后)
3、查看进程的文件描述符表
4、将file文件地址,填入对应的表下标中
5、返回下标,即fd

2、标准输入、输出、错误

当我们理解了上述的陈述之后
我们再接着来理解这三个东西:
0:标注输入 键盘
1:标准输出 显示器
2:标准错误 显示器

当我们打开在一个进程下只有一个文件时,我们查看该文件的fd,按理来说,一个进程下只有一个我们创建的文件,因此,进程的文件描述符号表对应的,只有一个文件,数组中文件的下标应为0,所以文件的fd应该是0,但是实际上却不是,而是3

所以,为什么是3呢?也就是说前面还有0、1、2三个文件,这三个文件是什么?

下面解释:
鼠标、键盘、显示器,都有对应的文件描述符
既然有文件描述符
说明有对应的file结构体
可是,这三个货明明是硬件
为什么就变成了文件呢?
怎么做到的呢?
我又该怎么理解呢?
例如说,是文件,就说明有读写
读键盘,我能理解
但是写键盘,是什么意思?
往键盘里写数据?
啥?
对不起,我理解不了一点

所有想要理解上述的问题,需要进一步理解Linux一切皆文件的理念
我们理解这个理念的困难,在于如何理解硬件
怎么把硬件也视为文件?

每一个硬件设备,我们关注的主要是两点:属性和操作方法
首先是属性
虽然每一个硬件的属性都不同
但是,我们可以抽象属性类别
创建硬件设备的结构体device
结构体内部有着诸如:
设备名、设备状态、厂商等属性
于是,每一个设备都对应着一个device结构对对象
于是所有的硬件设备都是属于同一个结构体类型
仅对象本身数据的不同
于是,我们就通过先描述,再组织
将硬件设备管理起来了
上述是属性

接下来是操作方法
每一个设备都有着对应的操作方法
例如,读操作和写操作
现在我们来理解键盘
键盘有读操作,这个好理解
但是,没有写操作
那怎么办呢?
我们将键盘的写操作设置为空方法

对应着的,
显示器写操作
但是没有读操作
所以,我们把显示器的读操作设置为空

所以,每一个设别都有着不同的操作方法
而每个硬件设备的操作方法,例如读写操作
在出厂商设置之前,都已经设计好了对应的接口方法
这体现在驱动层

而在Linux操作系统中
每一个驱动设备,都创建一个对应的struct file结构体
在这个结构体内部
有属性,这些属性就是设备的属性数据
还有函数指针,
所以,尽管每一个设备的操作方法不一样
但是,我可以把方法的返回值、参数设置成一样的
然后让这些函数指针指向底层的硬件设备的操作方法
所以,这个结构体还有一张操作底层指针表,指向底层的操作方法

于是,经过上述的组织之后
我要用键盘的read方法,则指向底层键盘的read方法
我要用鼠标的read方法,则指向底层鼠标的read方法
我要用屏幕的read方法,则指向底层鼠标的write方法
以此类推...
上述实现这种组织的技术
其实就是多态! 
我们再回头来看struct file这个结构体
有属性、有方法
但是c语言的结构体内不能写函数
所以我们使用函数指针
所以,这个struct file结构体,其实就是一个类
所以,从一切皆对象的理解角度来看
文件也是对象的一种

于是,站在struct file这个结构体对象之上
我们就不用再关心底层的硬件的方法到底是怎样的
当我们想调用一个硬件设备的方法时
例如要读取键盘
就是找到键盘的结构体对象
然后直接调用其中的read方法即可

于是,通过上述的组织方式
现在站在Linxu的角度来看
这些硬件,也视为文件
所以,一切皆文件!

所以对于硬件这一层,在Linux下叫做vfs,即vitural file system,虚拟软件系统

所以,当我们底层的硬件性质不一样时,我们可以在其上设计一层软件
屏蔽掉底层硬件上的差异
这样就可以以统一的视角去看待、组织、管理底层

3、编程语言的本质

在系统内,访问文件时,只认文件描述符
可是,c语言中的fopen、fclose函数等
返回值是一个文件类型指针:FILE*
哪里有文件描述符?
这个FILE是什么?
是一个c语言提供的结构体类型
而这个FILE结构体内部
封装了文件描述符!!!

所以,本质上,c语言的所有文件操作函数,底层都是对系统调用的封装


所以,我们写代码可以使用系统调用,也可以使用语言提供的方法
可以,既然已经有了系统调用,为什么还要有一个语言呢?
直接用系统调用函数不就好了吗?
c语言为什么这么做?
这是因为,系统不同,系统调用接口不同
例如,你在Linux下的系统调用,在mac、windows下就可能不一样
在Linux下,你是一个参数;mac下是2个参数;windows下是3个参数
根本没法兼容
于是,你使用Linux的系统调用写出来的代码
只能在Linux下编译运行
但是,不能拿到mac、windows下运行
也就是说,代码不具备跨平台性

所以,c语言/c++/java等语言,解决了上述的问题
在c语言本身的标准库中
对每一个操作系统的环境的系统调用都进行了封装
再用条件编译进行调整兼容
就这样,你写的同一份代码
就可以在不同的系统环境中编译运行

  • 15
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
这里提供一个简单的C语言代码示例,用于演示如何使用mvs+ef来实现文件添加: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #define CLUSTER_SIZE 512 // 簇大小为512字节 #define MAX_FILE_COUNT 100 // 最多支持100个文件 // 文件描述符结构体 typedef struct { char name[256]; // 文件名 int size; // 文件大小 int cluster_count; // 占用的簇数 int clusters[100]; // 占用的簇号 time_t create_time; // 创建时间 time_t modify_time; // 修改时间 } file_descriptor; // 位图结构体 typedef struct { int size; // 位图大小,单位为簇数 char *data; // 位图数据 } bitmap; // 文件系统结构体 typedef struct { int cluster_count; // 簇总数 int free_cluster_count; // 空闲簇数 bitmap *bitmap; // 位图 file_descriptor files[MAX_FILE_COUNT]; // 文件描述符数组 int file_count; // 文件数目 } file_system; // 初始化文件系统 void init_file_system(file_system *fs, int cluster_count) { // 初始化位图 fs->bitmap = (bitmap*)malloc(sizeof(bitmap)); fs->bitmap->size = cluster_count; fs->bitmap->data = (char*)malloc(cluster_count); memset(fs->bitmap->data, 0, cluster_count); // 初始化其他成员变量 fs->cluster_count = cluster_count; fs->free_cluster_count = cluster_count; fs->file_count = 0; } // 查找空闲簇 int find_free_cluster(file_system *fs) { for (int i = 0; i < fs->cluster_count; i++) { if (fs->bitmap->data[i] == 0) { fs->bitmap->data[i] = 1; fs->free_cluster_count--; return i; } } return -1; } // 添加文件 void add_file(file_system *fs, char *name, char *content) { // 创建文件描述符 file_descriptor fd; strcpy(fd.name, name); fd.size = strlen(content); fd.cluster_count = (fd.size + CLUSTER_SIZE - 1) / CLUSTER_SIZE; fd.create_time = time(NULL); fd.modify_time = fd.create_time; for (int i = 0; i < fd.cluster_count; i++) { int cluster = find_free_cluster(fs); fd.clusters[i] = cluster; } // 写入文件内容 int offset = 0; for (int i = 0; i < fd.cluster_count; i++) { int cluster = fd.clusters[i]; int size = (i == fd.cluster_count - 1) ? (fd.size % CLUSTER_SIZE) : CLUSTER_SIZE; memcpy(fs->bitmap->data + cluster * CLUSTER_SIZE, content + offset, size); offset += size; } // 更新文件系统信息 fs->files[fs->file_count++] = fd; } // 主函数 int main() { file_system fs; init_file_system(&fs, 1024); // 初始化文件系统,总共有1024个簇 // 添加文件 add_file(&fs, "test.txt", "hello world"); // 输出文件系统信息 printf("cluster_count=%d, free_cluster_count=%d, file_count=%d\n", fs.cluster_count, fs.free_cluster_count, fs.file_count); for (int i = 0; i < fs.file_count; i++) { file_descriptor fd = fs.files[i]; printf("name=%s, size=%d, cluster_count=%d, create_time=%ld, modify_time=%ld\n", fd.name, fd.size, fd.cluster_count, fd.create_time, fd.modify_time); } return 0; } ``` 以上代码仅仅是一个简单的示例,实际上还需要考虑并发访问、错误处理等情况。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

二十5画生

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

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

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

打赏作者

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

抵扣说明:

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

余额充值