【Linux】文件系统:文件fd

Alt

🔥个人主页Quitecoder

🔥专栏linux笔记仓

Alt

01.回顾C文件接口

#include<stdio.h>

int main()
{
    FILE *fp=fopen("log.txt","w");
    if(fp==NULL)
    {
        perror("fopen");
        return 1;
    }
    fclose(fp);
    return 0;
}

我们进行文件操作,前提是我们的代码跑起来了,文件的打开和关闭,是cpu在执行我们的代码

打开文件:本质是进程打开文件

文件没有被打开的时候,存储在磁盘中

一个进程可以打开多个文件,系统中可以启动很多进程,OS内部一定存在大量被打开的文件,操作系统对这些文件进行管理(先描述再组织,类似PCB)

文件=属性+内容

FILE *fp=fopen("log.txt","w");
  1. 如果不存在,则在当前路径下,新建指定的文件
  2. 默认打开文件的时候,会把目标文件清空

在这里插入图片描述
在这里插入图片描述
> 可以用来新建文件,清空文件,先清空再写入

输出重定向一定是文件操作!

打开文件的方式:

 r Open text file for reading. 
 The stream is positioned at the beginning of the file.
 
 r+ Open for reading and writing.
 The stream is positioned at the beginning of the file.
 
 w Truncate(缩短) file to zero length or create text file for writing.
 The stream is positioned at the beginning of the file.
 
 w+ Open for reading and writing.
 The file is created if it does not exist, otherwise it is truncated.
 The stream is positioned at the beginning of the file.
 
 a Open for appending (writing at end of file). 
 The file is created if it does not exist. 
 The stream is positioned at the end of the file.
 
 a+ Open for reading and appending (writing at end of file).
 The file is created if it does not exist. The initial file position
 for reading is at the beginning of the file, 
 but output is always appended to the end of the file.
模式描述文件指针位置文件是否会创建文件是否会被截断(清空)
r只读模式,文件必须存在开始位置
r+读写模式,文件必须存在开始位置
w写入模式,文件不存在时会创建,存在时会清空文件开始位置
w+读写模式,文件不存在时会创建,存在时会清空文件开始位置
a追加模式,文件不存在时会创建,存在时内容追加到文件末尾文件末尾
a+读写追加模式,文件不存在时会创建,存在时内容追加到文件末尾开始位置(读取)/文件末尾(写入)

02.系统文件I/O

文件->磁盘->外设->硬件

向文件中写入,本质是向硬件中写入,用户没有权利直接写入,通过OS写入,OS必须给我们提供系统调用

fopen/fwrite/fread/fprintf/scanf/printf/cin/cout 我们用的c/c++都是对系统调用接口的封装

02.1 open

在这里插入图片描述

open 是 Unix/Linux 系统中用于打开或创建文件的系统调用,位于 fcntl.h 头文件中。它用于以不同的模式访问文件,如只读、写入、追加等。

1. open 函数原型
在 C 语言中,open 的函数原型如下:

#include <fcntl.h>  // 文件控制选项
#include <sys/types.h>  // 类型定义
#include <sys/stat.h>  // 文件属性
#include <unistd.h>  // 通用 API

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

2. 参数解析

  • pathname:要打开的文件路径。
  • flags:指定文件的访问模式(只读、读写等)和其他选项(创建文件、截断等)。
  • mode:用于设置新文件的权限(仅当 O_CREAT 选项被使用时有效),通常采用 06440666 形式。

3. 返回值

  • 成功:返回一个文件描述符(非负整数),用于后续的 readwriteclose 操作
  • 失败:返回 -1,并设置 errno 变量来指示错误类型。

flags 参数(文件打开模式)标记位传参

在系统调用(如 open())中,flags 是一个 位掩码(bitmask),用于控制函数的行为。例如,在 open() 中,flags 用于指定文件的打开模式(只读、只写、读写等)以及额外的选项(创建、追加、非阻塞等)。

位掩码 是指使用二进制位的不同组合来表示多种选项,这样可以通过按位或 (|) 来组合多个标志,而不会相互影响

1. 访问模式(必须指定一个)

访问模式决定了文件的读写权限,这三者不能同时使用,否则会报错。

标志值(十六进制)说明
O_RDONLY0x0000只读模式(Read Only)
O_WRONLY0x0001只写模式(Write Only)
O_RDWR0x0002读写模式(Read & Write)

示例

int fd = open("file.txt", O_RDONLY);  // 以只读模式打开文件
int fd = open("file.txt", O_WRONLY);  // 以只写模式打开文件
int fd = open("file.txt", O_RDWR);    // 以读写模式打开文件

2. 额外控制标志(可选,可组合使用)

这些标志可以与访问模式组合使用,以改变 open() 的行为。

标志值(十六进制)说明
O_CREAT0x0040若文件不存在,则创建新文件(需要 mode 参数)
O_EXCL0x0080O_CREAT 组合,文件必须不存在,否则失败
O_TRUNC0x0200若文件已存在,则清空文件内容
O_APPEND0x0400追加模式,写入时自动跳到文件末尾
O_NONBLOCK0x0800非阻塞模式打开文件(常用于设备文件、网络通信)
O_SYNC0x101000使 write 操作同步到磁盘,保证数据立即写入
O_NOFOLLOW0x20000如果文件是符号链接,则 open() 失败
3. mode 参数(仅 O_CREAT 生效)

flags 包含 O_CREAT 时,需要提供 mode 参数,表示新建文件的权限,例如:

open("newfile.txt", O_WRONLY | O_CREAT, 0644);

其中 0644 代表:

  • 0:八进制数前缀
  • 6110):用户(拥有者)可读可写
  • 4100):组(Group)只读
  • 4100):其他人(Others)只读

如何组合 flags
由于 flags位掩码(bitmask),可以使用按位或 | 运算符来组合多个标志。例如:

int fd = open("file.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
  • O_RDWR(读写模式)
  • O_CREAT(如果文件不存在,则创建)
  • O_TRUNC(如果文件已存在,则清空内容)

等价于:

flags = 0x0002 | 0x0040 | 0x0200;  // 计算出的十六进制值

flags 具体的二进制表示
假设 flags 组合如下:

int flags = O_RDWR | O_CREAT | O_APPEND; 

如果 O_RDWR = 0x0002O_CREAT = 0x0040O_APPEND = 0x0400,那么它们的二进制表示如下:

O_RDWR    = 0000 0000 0000 0010  (0x0002)
O_CREAT   = 0000 0000 0100 0000  (0x0040)
O_APPEND  = 0000 0100 0000 0000  (0x0400)
--------------------------------
组合后    = 0000 0100 0100 0010  (0x0442)

open() 处理 flags 时,它会解析这个二进制值,并执行相应的操作。

  • flagsopen()核心参数,用于控制文件的访问模式和特殊行为。
  • 访问模式(O_RDONLYO_WRONLYO_RDWR必须指定一个
  • 可以通过按位或 | 组合多个控制标志(如 O_CREATO_TRUNCO_APPEND)。
  • mode 参数用于 O_CREAT,指定新文件的权限,如 0644
  • open() 返回文件描述符(fd),失败时返回 -1 并设置 errno

这种flags的传递位图标记位的方法在OS系统调用接口中很常见,我们可以自己设计一个传递位图标记位的函数:

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

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


void print(int flag)
{
    if(flag&ONE)
        printf("one\n");
    if(flag&TWO)
        printf("two\n");
    if(flag&THREE)
        printf("three\n");
    if(flag&FOUR)
        printf("four\n");
}

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

    print(THREE);
    printf("\n");
    
    print(ONE|TWO);
    printf("\n");
    
    print(ONE|THREE);
    printf("\n");
    
    print(ONE|TWO|THREE|FOUR);
    printf("\n");
    
    
    return 0;
}

演示了如何使用位运算来检查不同的标志(flag)。程序中的关键部分是 print 函数,它检查传入的 flag 参数,并根据不同的位值输出相应的文本。

让我们逐步分析每个部分。


宏定义

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

这部分定义了四个宏,每个宏的值都代表一个特定的二进制位:

  • ONE = 1 :二进制 0000 0001,表示第 0 位(从右向左数)。
  • TWO = 2 :二进制 0000 0010,表示第 1 位。
  • THREE = 4 :二进制 0000 0100,表示第 2 位。
  • FOUR = 8 :二进制 0000 1000,表示第 3 位。

使用 1 << n 来左移 1 位 n 次,从而生成一个表示 2 的幂的二进制值。


print 函数

void print(int flag)
{
    if(flag&ONE)
        printf("one\n");
    if(flag&TWO)
        printf("two\n");
    if(flag&THREE)
        printf("three\n");
    if(flag&FOUR)
        printf("four\n");
}

print 函数通过位运算 &(按位与运算)检查给定 flag 中每一位是否设置为 1。如果某一位为 1,就会输出相应的文本。

  • flag & ONE:检查 flag 的第 0 位(ONE)。
  • flag & TWO:检查 flag 的第 1 位(TWO)。
  • flag & THREE:检查 flag 的第 2 位(THREE)。
  • flag & FOUR:检查 flag 的第 3 位(FOUR)。

输出说明:

  1. print(ONE) 传入 ONE(即 1,二进制 0000 0001),因此检查到第 0 位为 1,输出 one
  2. print(TWO) 传入 TWO(即 2,二进制 0000 0010),因此检查到第 1 位为 1,输出 two
  3. print(THREE) 传入 THREE(即 4,二进制 0000 0100),因此检查到第 2 位为 1,输出 three
  4. print(ONE | TWO) 传入 ONE | TWO(即 1 | 2 = 3,二进制 0000 0011),因此第 0 位和第 1 位都为 1,输出 onetwo
  5. print(ONE | THREE) 传入 ONE | THREE(即 1 | 4 = 5,二进制 0000 0101),因此第 0 位和第 2 位都为 1,输出 onethree
  6. print(ONE | TWO | THREE | FOUR) 传入 ONE | TWO | THREE | FOUR(即 1 | 2 | 4 | 8 = 15,二进制 0000 1111),因此所有四个标志位都为 1,输出 onetwothreefour

one

two

three

one
two

one
three

one
two
three
four

这个例子很有用,展示了如何使用位操作进行标志位管理,尤其是在处理多个选项或功能时


4. openfopen 的区别

对比项open(系统调用)fopen(标准库函数)
头文件<fcntl.h><stdio.h>
返回值文件描述符(intFILE* 指针
模式需要手动指定 flags类似 "r", "w", "a"
缓冲无缓冲,直接访问内核有缓冲,性能更优
适用场景底层 I/O 操作高级文件读写操作
  • 如果只是进行简单的文件读写,建议使用 fopen,因为它提供了缓冲,效率更高
  • 如果需要进行更底层的文件控制(如非阻塞、文件锁定等),则使用 open

02.2 open函数返回值fd(文件描述符)

write函数原型:

#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);
int main()
{
    umask(0);
    int fd=open("file.txt",O_WRONLY | O_CREAT,0666);
    if(fd<0)
    {
        perror("open");
        return 1;
    }
    const char *message = "hello linux\n";
    write(fd,message,strlen(message));
    close(fd);
    
    return 0;
}

在这里插入图片描述
此时如果我们重新写入aaaaa:

在这里插入图片描述
这里的文件内容老的没有被清空,这里跟open那里fd传入flags参数有关,我们这里传入O_TRUNC,每次打开文件时清空即可

在这里插入图片描述

int fd=open("file.txt",O_WRONLY | O_CREAT | O_TRUNC,0666);

写方式打开,不存在就创建,存在就先清空!

在认识返回值之前,先来认识一下两个概念: 系统调用 和 库函数

上面的 fopen fclose fread fwrite 都是C标准库当中的函数,我们称之为库函数(libc)。
而, open close read write lseek 都属于系统提供的接口,称之为系统调用接口

在这里插入图片描述

所以,可以认为,f#系列的函数,都是对系统调用的封装,方便二次开发

接下来看fd

fd是open的返回值,也是文件描述符,我们这里设置代码来打印出它的值

int main()
{
    int fda=open("loga.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
    printf("fda:%d\n",fda);
    int fdb=open("logb.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
    printf("fdb:%d\n",fdb);
    int fdc=open("logc.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
    printf("fdc:%d\n",fdc);
    int fdd=open("logd.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
    printf("fdd:%d\n",fdd);
    return 0;
}

在这里插入图片描述
文件描述符数字为3 4 5 6

Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器

C 语言的标准流与操作系统的文件描述符是关联的。

  • 标准输入(stdin):与文件描述符 0 相关联。
  • 标准输出(stdout):与文件描述符 1 相关联。
  • 标准错误(stderr):与文件描述符 2 相关联

这些文件描述符在程序启动时由操作系统自动分配并打开,且在大多数情况下它们指向终端或控制台。如果没有显式地进行文件重定向(例如使用freopen 或命令行中的重定向符号),这些文件描述符就会继续指向终端

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

那么我们可以直接往显示器上写入:

const char *message = "aaaaa\n";
write(1,message,strlen(message));

在这里插入图片描述

03.fd理解

在这里插入图片描述
系统中有进程被task_struct管理,有很多被打开的文件,struct file 是操作系统用来表示打开文件的主要数据结构。每当一个文件被打开时,操作系统会创建一个 struct file 对象,并将其与文件描述符关联,对象之间通过双向链表连接

struct file {
    struct file_operations *f_op;  // 文件操作的指针
    void *private_data;            // 私有数据,用于存储文件的额外信息(如文件特定的数据结构)
    unsigned long f_flags;         // 文件标志,描述文件的访问模式(如只读、只写、追加等)
    struct inode *f_inode;         // 文件的 inode 节点,包含文件的元数据
    off_t f_pos;                   // 文件指针,记录当前文件的读写位置
};

struct file有指向它的缓存,磁盘中文件,属性来初始化struct file,内容加载到缓存中,未来想读修改写入直接在缓存总写入,最后刷新到磁盘中,所以对文件的管理转化为对内核struct file进行管理

那么系统中有很多的进程,右边有很多被打开的文件,那么哪些文件对应哪些进程呢?

进程是可以打开多个文件的,操作系统在PCB里存在一个属性,这个属性叫做struct files_struct *files

在这里插入图片描述

struct files_struct 包含一个数组,数组有对应的下标,进程需要找到一个文件,最终只需要把数组的下标返回给上层,上层只要拿着int fd就可以访问文件了!

所以文件描述符fd的本质是内核的进程的,文件映射关系数组的下标

所以文件一旦打开,我们发现,write,read,close都需要参数fd,一旦fd传入,操作系统就能知道你要访问当前系统的哪个文件

所以读文件就是把缓存中的内容拷贝到应用层,无论读写都必须在合适的时候让0S把文件的内容读到文件缓冲区中,哪怕只修改一个字节,也需要把文件内容读到缓冲区中,再刷到磁盘中

open在干什么呢?

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

我们已经理解了什么是fd,我们前面提到0 1 2是默认打开了,分别对应键盘,显示器,显示器,三个硬件,如何理解它呢?

我们需要理解,linux,一切皆为文件

在这里插入图片描述
在linux层面上他是怎么做到的呢?

对于每一种设备,我们需要关心的是它的属性和操作方法

最重要的是关注它的操作方法,对于键盘来讲,需要有自己的读方法,写方法,对于显示器,也要有自己的读方法,写方法。冯诺依曼体系中,键盘作为外设,那么这个写方法设置为空函数,显示器对应的读方法也没办法实现

不同的设备,每一种设备的操作方法一定是不一样的,这一层是由驱动层来完成的

每一个被打开的设备,在操作系统层面,为设备构建struct file,虽然底层的方法不同,但是我可以把参数和返回值设为类似的,这里OS设置函数指针。所以如果我们想要访问键盘,找到键盘的struct file,调用read,从struct file视角往上看,上层看到所有的设备一切皆文件

这里是c语言实现的多态技术,进程统一认为struct file

这里的文件层,我们成为虚拟文件系统

虚拟文件系统(VFS,Virtual File System)是操作系统中的一个抽象层,它为不同类型的文件系统(如ext4、NTFS、FAT、NFS 等)提供了一个统一的接口。通过VFS,操作系统能够以一致的方式访问和管理不同类型的文件系统,无论是本地磁盘文件系统还是网络文件系统。这使得文件系统的实现与用户或应用程序的使用方式解耦,增强了操作系统的灵活性和可扩展性

在这里插入图片描述
在操作系统内访问文件时,系统只认文件描述符

那么如何理解c语言通过FILE*访问文件呢?

FILE*:是 C 标准库提供的文件操作接口,它是一个指向 FILE 结构的指针。C 标准库通过 FILE* 为程序员提供了一套更为友好的文件操作函数,如 fopen、fread、fwrite、fclose 等。FILE* 实际上是对文件描述符的封装,提供了缓冲区管理、错误处理等功能

在这里插入图片描述
c语言为什么要这么做?

对于系统调用,不同系统调用接口不一样,这样的代码不具有跨平台性。所有的语言都想有跨平台性,所有的语言对不同平台系统的系统调用进行封装,不同语言封装的时候,文件接口就有差别了

在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

QuiteCoder

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

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

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

打赏作者

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

抵扣说明:

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

余额充值