【Linux】ext 文件系统

目录

  • [一、ext 文件系统](#一、ext 文件系统)
    • [1.1 在磁盘角度,如何创建、删除、修改、查看一个文件呢?](#1.1 在磁盘角度,如何创建、删除、修改、查看一个文件呢?)
    • [1.2 理解目录](#1.2 理解目录)
    • [1.3 关于 inode 编号和块号](#1.3 关于 inode 编号和块号)
    • [1.4 关于路径解析](#1.4 关于路径解析)
    • [1.5 关于路径缓存](#1.5 关于路径缓存)
    • [1.6 关于路径](#1.6 关于路径)
    • [1.7 存储大文件](#1.7 存储大文件)
    • [1.8 如何知道你的文件在哪个文件系统中?](#1.8 如何知道你的文件在哪个文件系统中?)

个人主页:矢望

个人专栏:C++LinuxC语言数据结构Coze-AI

一、ext 文件系统

1.1 在磁盘角度,如何创建、删除、修改、查看一个文件呢?

创建 :首先在一个磁盘的分组中会先在inode BitMap位图中申请一个没有被占用的位置,然后将其标记为1,这样就有了inode编号,之后查找这个位置在inode Table中所对应的块号,这样再将文件的所有属性写入到这个块中,由于初始时文件中没有内容,所以这样就完成了文件的创建。而当你向文件中写入1字节内容时,此时就会向Block BitMap申请一个没有被占用的位置,将其标记为1,然后查找这个位置在Data Blocks中对应的块号,再将要写的内容写入到这个块中,就好了。

删除 :知道了inode编号,那么我们就可以通过inode编号去查找inode BitMap中这个编号对应的1,找到这个1,就可以找到inode Table中所使用的块号,这样就找到了文件的inode,文件的所有属性都在这个结构体中,而这所有的属性里面有i_block[],这个里面记录着文件内容所使用的块号,通过这个数组就可以查找Block BitMap中该文件内容所占用的1,然后将这些1置为0,再利用inode编号将inode BitMap中该文件的1置为0,这样我们就删除了这个文件。

所以rm并没有真正清除数据。这也是为什么我们在日常删文件的时候会这么快。如果想要恢复这个文件做删除的反操作即可。如果你误删了文件并且它很重要,想要恢复,接下来就不要做任何操作,尤其是创建操作,如果会恢复就自己恢复,不会就去找专业人员。

修改 :修改一个文件无非修改一个文件的内容或者文件的属性,操作流程都是相同的读,改,写。我们知道inode编号,就像上面那样就可以找到它们对应的数据块,接下来就是将文件内容或者属性对应的数据块加载到内存中,然后在内存中做自己的修改,然后再将内存中修改好的数据写入到磁盘中。

查看 :通过文件的inode编号就可以先查询inode BitMap查看编号是否有效,然后找到对应的数据块号,这样就找到了文件的所有属性,然后通过inode结构体中的i_block[]就可以通过Block BitMap查询到文件内容所对应的数据块号,这样也就看到了文件内容。

1.2 理解目录

Linux下一切皆文件,所以目录也是文件,所以目录也有inode编号。文件 = 内容 + 属性,目录文件的属性保存在inode结构体里,那么目录文件的内容是什么呢? 目录文件的内容记录的是所包含文件的文件名和该文件inode编号的映射表。

文件名和该文件的inode编号都是数据,所以它们都保存在目录内容所在的Data Blocks的数据块里。

所以在磁盘和文件系统角度上,存储目录和存储普通文件没有任何区别,都是存储数据。我们之前说去除目录文件的r权限,你就不能查看目录中的内容,而去掉目录文件的w权限,你就不能修改目录中的数据。这也是因为目录内容路面保存的是当前目录下的文件名和inode编号之间的映射关系,去除rw权限就是不让你对目录文件在磁盘中的数据块内容查看或修改。

我们日常访问文件都是用的文件名啊,没有使用inode编号呀。从上面铺垫那么多,我们很容易就可以得到答案,当前目录下放着文件名,通过文件名就可以映射找到inode编号,这样就可以对文件进行访问了所以,访问文件必须要知道当前工作目录,本质是必须能打开当前工作目录文件,查看目录文件的内容!

那么如何通过目录使用文件名获取文件的inode编号呢?下面这个程序的功能:读取指定目录中的所有条目,显示每个文件的文件名和对应的inode编号。

cpp 复制代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <unistd.h>

/**
 * 程序功能:显示指定目录中所有文件的文件名和inode编号
 * 使用方法:./a.out <目录路径>
 * 示例:./a.out /home/user
 */

int main(int argc, char *argv[]) 
{
    // ==== 1. 检查命令行参数 ====
    if (argc != 2) {
        fprintf(stderr, "Usage: %s <directory>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    
    // ==== 2. 打开目录 ====
    // DIR *opendir(const char *name) - 打开目录,返回目录流指针
    // 这是一个系统调用,会进入内核空间读取目录文件的数据块
    DIR *dir = opendir(argv[1]);
    if (!dir) {
        perror("opendir");  // 打印错误原因:权限不够?目录不存在?
        exit(EXIT_FAILURE);
    }
    
    // ==== 3. 读取目录内容 ====
    struct dirent *entry;
    // readdir() - 每次调用返回一个目录项,读到末尾返回NULL
    // 它读取的是目录文件数据块中的目录项结构
    while ((entry = readdir(dir)) != NULL) {
        
        // 跳过两个特殊目录项:
        // "."  - 当前目录(inode指向自己)
        // ".." - 父目录(inode指向上一层)
        if (strcmp(entry->d_name, ".") == 0 || 
            strcmp(entry->d_name, "..") == 0) {
            continue;
        }
        
        // ==== 4. 显示文件名和inode号 ====
        // d_name: 文件名(用户看到的)
        // d_ino:  inode号(内核使用的)
        // 这正是我们之前讨论的:目录 = 文件名 ↔ inode 的映射表!
        printf("Filename: %s, Inode: %lu\n", 
               entry->d_name, 
               (unsigned long)entry->d_ino);
    }
    
    // ==== 5. 关闭目录 ====
    closedir(dir);
    return 0;
}

测试结果

如上获取了文件名和文件inode编号的映射关系。所以在同一个目录下文件名是不能重复的,因为它要作为键值查询inode编号

1.3 关于 inode 编号和块号

inode编号和块号不是组内有效的,而是在整个分区内唯一的。在一个分区内部,一个文件系统内部,有多少inode,有多少数据块都是固定的,都是提前设计好的。我们通过inode编号,数据块号就可以找到对应的组的编号。假如一个组有10000inode,那么inode编号为10230的文件,通过简单的运算就可以知道它在组1里面。

inode和数据块之间的比例常见的有1:100,这个可以调节,存在inode编号使用完但块号没有使用完的情况,它们之间的匹配关系不是很完美。所以你才会在Windows上看到磁盘空间利用率这一说。

1.4 关于路径解析

如果我们要访问当前目录下的文件内容或者属性,我们首先要做的一件事就是打开当前目录,访问到当前目录的数据块,而访问当前目录的数据块就需要知道当前目录的inode编号。我们知道目录可以查找到当前目录下文件的inode编号,而目录文件也是文件,所以要访问当前目录文件的inode编号,就需要从上级目录中找!

如上图,为了访问当前目录下的一个文件的内容和属性,我就需要先知道当前目录的inode编号,而想要知道这个编号就需要递归似的向前找,一直找的根目录。所以在Linux开机的时候根目录就是固定知道的,可访问的,我们可以找到根目录下的所有文件的inode编号。

我们发现当我们要访问任何文件的时候,Linux内核都要为我们从根目录/开始做路径解析操作。因此这也变相说明,访问文件必须要有路径。

1.5 关于路径缓存

如果我们要访问同一个文件一百次,Linux就要做一百次路径解析吗?这也太麻烦也太慢了吧。所以Linux就需要管理我们所访问过的打开过的路径节点。Linux中缓存过的路径结构呈树状,是一棵多叉树。Linux中,在内核中维护树状路径结构的内核结构体叫做:struct dentry

所以我们会发现,当ls -al展示这个目录下的文件时,第一次会比较慢,因为多叉树的路径节点还没有完善,而下一次再看就几乎是瞬间完成的,因为路径已经缓存好了。

这棵多叉树会动态变化的,当你有一些路径很长时间都没有被访问就会去除掉。另外任何文件都有自己的dentry,包括普通文件。如果使用的是相对路径,它会先看当前路径在不在路径树形结构里,如果在,就会从当前路径的节点找到上一个路径节点,接着就是继续解析的工作;如果不在就是从根目录开始解析路径的过程。

cpp 复制代码
struct dentry {
    /* 引用计数和访问控制 */
    atomic_t d_count;           /* 使用计数:0=可回收,>0=正在使用 */
    unsigned int d_flags;       /* DCACHE_* 标志位 */
    spinlock_t d_lock;          /* 保护dentry内部字段的自旋锁 */

    /* 与inode的关联 */
    struct inode *d_inode;      /* 指向这个文件名对应的inode,NULL表示负缓存 */

    /* 哈希表相关 - 用于快速查找 */
    struct hlist_node d_hash;   /* 全局dentry哈希表节点 */
    struct dentry *d_parent;    /* 父目录的dentry */
    struct qstr d_name;         /* 文件名(包含hash、长度、名字) */

    /* LRU链表 - 用于缓存回收 */
    struct list_head d_lru;     /* LRU链表节点,系统根据这个回收不用的dentry */

    /* 父子关系链表 */
    union {
        struct list_head d_child;   /* 在父目录的子节点链表中的位置 */
        struct rcu_head d_rcu;      /* RCU回调,用于无锁访问 */
    } d_u;
    struct list_head d_subdirs; /* 本目录的子节点链表头 */

    /* 别名链表 - 用于硬链接 */
    struct list_head d_alias;   /* 指向同一个inode的所有dentry链表 */

    /* 时间戳和验证 */
    unsigned long d_time;       /* 用于d_revalidate的时间戳(NFS等使用) */

    /* 操作函数表 */
    struct dentry_operations *d_op;  /* dentry操作函数 */

    /* 超级块关联 */
    struct super_block *d_sb;   /* 所属文件系统的超级块 */

    /* 文件系统私有数据 */
    void *d_fsdata;             /* 具体文件系统的私有数据 */

    /* 性能分析相关 */
#ifdef CONFIG_PROFILING
    struct dcookie_struct *d_cookie;  /* 用于内核profiling */
#endif

    /* 挂载点计数 */
    int d_mounted;              /* 如果是挂载点,记录挂载的文件系统数 */

    /* 短文件名内联存储 */
    unsigned char d_iname[DNAME_INLINE_LEN_MIN];  /* 短文件名直接存这里 */
};

如上就是dentry,里面有指向父路径节点的指针struct dentry *d_parent;,所以可以cd ..切换到上级目录。

还记得我们文件操作时的系统调用int fd = open("log.txt", XXX);吗?打开这个文件我们是需要路径的,也就是会进行路径解析与缓存操作,下面是我们的struct file结构体中的部分内容。

cpp 复制代码
struct file {
    /*
     * 链表和引用计数
     */
    struct list_head        f_list;        /* 所有已打开文件的链表 */
    struct dentry          *f_dentry;      /* 指向这个文件对应的dentry */
    struct vfsmount        *f_vfsmnt;      /* 指向这个文件所在的挂载点 */
    struct file_operations *f_op;          /* 文件操作函数表 */
    atomic_t                f_count;       /* 引用计数(有多少进程在使用) */
    unsigned int            f_flags;       /* 打开文件时指定的标志(O_RDONLY, O_NONBLOCK等) */
    mode_t                  f_mode;        /* 文件打开模式(读/写) */
    
    // ...
};

看到结构体中的struct dentry *f_dentry; 了吗?通过这个就可以找到自己这个文件所对应的dentry,通过结构体中的struct inode *d_inode;,就可以找到自己文件的inode编号了,这样文件的属性和文件的内容就都找到了。所以struct file 是整个I/O操作的中枢,连接着用户态的fd和内核态的dentry/inode

1.6 关于路径

Linux下访问任何文件都需要Linux内核进行路径解析和路径缓存的,那么这些路径由谁提供呢?

Linux为什么要有根目录,根目录下为什么要有那么多缺省目录?你为什么要有家目录,你自己可以新建目录?

上面所有行为:本质就是在磁盘文件系统中,新建目录文件。而我们新建的任何文件,都在我们或者系统指定的目录下新建,这就天然有了路径。

你访问一个文件,都是使用指令进行访问,所以本质是进程进行访问,进程有自己的工作路径CWD,所以这是进程提供了路径。而当你使用open打开文件时,此时你又提供了路径。所以系统加用户共同构建了Linux的路径结构。路径 = 系统基础设施 + 用户创建的命名空间 + 进程运行时的上下文

1.7 存储大文件

我们之前说过,文件的内容存储在对应inode结构体的i_block[]中。

cpp 复制代码
#define EXT2_NDIR_BLOCKS 12
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)

// inode 结构体中的 i_block 数组
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */

如上,我们看到EXT2_N_BLOCKS = 15,而一个块是4KB,那么4 * 15 = 60KB,也就是说Linux中最大的文件只有这么大? 不是的,Linux早就设计好了,如下图。

如上图,i_block数组的前12个位置是直接存储数据的,而后面三个存储的是指向块号的信息。例如一级索引表指针,它的4KB的数据会存储它所指向的块号,如果4字节存储一个块号,那么它4KB的数据就可以存储1024个块,也就是4096KB。二级、三级能存储数据的数量就更庞大了。

  • 各层次的数据量
指针类型 计算公式 数据块数量 数据量
直接块 12个直接指针 12个块 12 × 4KB = 48KB
间接块 1 × 1024 1024个块 1024 × 4KB = 4MB
二次间接 1 × 1024 × 1024 1,048,576个块 1,048,576 × 4KB = 4GB
三次间接 1 × 1024 × 1024 × 1024 1,073,741,824个块 1,073,741,824 × 4KB = 4TB

如上表,Linux下绝对能够存储大文件。

假如一个分组20GB,但要存储的文件大于20GB,该如何存储呢? 我们知道块号是整个分区有效的,使用完这个分组的块号之后,就会占用其它分组的块号,然后将使用的块号记录在i_block数组中。所以块内不止可以存储文件自己的数据,也可以存储自己文件存储时用的更多的块号

1.8 如何知道你的文件在哪个文件系统中?

首先磁盘要被使用就需要先进行分区,分区之后还不行,接下来就需要进行格式化。格式化的本质是在写入管理信息,这部分管理信息叫做文件系统(磁盘级)

那么分区格式化之后,这个分区或者文件系统就可以直接被使用了吗? 并不是,现在还不能像访问一个目录一样去访问它,所以还要把你的分区或者文件系统挂载到指定的目录下!

如上,ls /dev/vd*:列出 /dev 目录下所有以 vd 开头的设备文件,/dev/vda:表示第一块虚拟磁盘(整个磁盘),/dev/vda1:表示第一块磁盘上的第一个分区。(vd 前缀表示这是虚拟磁盘,通常是云服务器或虚拟机环境)。df -h 命令用于查看磁盘空间使用情况。

cpp 复制代码
/dev/vda1    40G    4.7G   33G    13% /

如上这个分区/dev/vda1就挂载到了根目录/下。一旦这个分区挂载到了指定目录下,就可以使用我们平时使用的文件操作对这个分区做操作了,我们进入这个目录,touch、rm都是对这个分区做操作

挂载分区

接下来,我们来演示一下如何挂载分区。

cpp 复制代码
dd if=/dev/zero of=./disk.img bs=1M count=5 

上述命令是在当前目录创建一个 5MB 的空白文件 disk.img,内容全是 \0

如上,我们执行了这个命令,现在我们就有了这个比较大的文件,我们可以将它视为一个分区,有了分区之后,我们下一步要做的工作就是格式化这个分区

cpp 复制代码
mkfs.ext4 disk.img # 格式化写入文件系统

如上,分区格式化已完成,接下来的工作就是将分区挂载到指定目录下。

我们可以在当前目录下创建一个空目录然后将我们的分区挂载到这个目录下。

cpp 复制代码
sudo mount -t ext4 ./disk.img /XXX # 将分区挂载到指定的⽬录

如上,我们的分区挂载到了当前目录的disk目录下,df -h 查看也能看到,只不过它被识别成了那个名字,这个我们不管。

接下来就可以使用这个分区了。

如上,就可以正常使用这个分区了,我们也能看到使用之后它的使用空间变化了。

cpp 复制代码
sudo umount /XXX # 卸载分区

如上就成功卸载分区了,接着将目录和那个分区文件删除即可。

回归正题,如何知道你的文件在哪个文件系统中? 要访问你的文件是必须要有路径的,例如./disk/test.c,其中./disk不就是指明在哪个分区下了吗?分区也有自己的路径名

完整过程

cpp 复制代码
路径 /home/wuhu/study/day_35/disk/test.c
        ↓
系统逐级查找挂载点
        ↓
发现 /home/wuhu/study/day_35/disk 是一个挂载点
        ↓
查询挂载表 → 这个挂载点对应哪个设备
        ↓
找到 /dev/loop0 (即 disk.img)
        ↓
确认:test.c 在 /dev/loop0 这个文件系统上

总结图

总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~

相关推荐
Edward111111114 分钟前
4月28日防火墙问题
linux·运维·服务器
子琦啊43 分钟前
【算法复习】字符串 | 两个底层直觉,吃透高频题
linux·运维·算法
AOwhisky2 小时前
Kubernetes 学习笔记:集群管理、命名空间与 Pod 基础
linux·运维·笔记·学习·云原生·kubernetes
小龙在慢慢变强..2 小时前
目录结构(FHS 标准)
linux·运维·服务器
2035去旅行2 小时前
嵌入式开发,如何选择C标准库
linux·arm开发
刘延林.2 小时前
win11系统下通过 WSL2 安装Ubuntu 24.04 使用RTX 5080 GPU
linux·运维·ubuntu
CodeOfCC4 小时前
Linux 嵌入式arm64安装openclaw
linux·运维·服务器
宵时待雨5 小时前
linux笔记归纳3:linux开发工具
linux·运维·笔记
magrich5 小时前
安装NoMachine并解决无外接显示器桌面黑屏
linux·运维·服务器
fish_xk5 小时前
Linus基础指令
linux·服务器