文件系统

总览

定义:文件系统是负责管理和存储文件信息的软件(一般由OS提供),它会在==存储介质中建立一种特定的组织结构==(不同文件系统不同)包含操作系统引导区,inode区、目录,文件等

这样看这个概念可能比较晦涩难懂,说了跟没说一样,下面举个例子:

之前在使用W25Q64这个Flash芯片的时候,由于没有使用文件系统,它有一下这些缺点:

  • 往里写数据、读数据的时候都得指定Flash上的具体的地址
  • 有效数据的位置不方便记录,如果想使用存进去的数据,还得额外手记什么数据存到哪个地址里了
  • 并且数据都是以page(256 Byte)为最小的读写单位,如果读写时发生跨页的话,那就更麻烦了
  • 数据类型多变,读取数据的时候不知道以什么方式读,不像Windows系统,有诸如.exe.cpp.txt之类的文件格式

因此需要一种更加高效的管理文件的方式——文件系统。

使用文件系统时,数据都以文件的形式存储。写入新文件时先在目录中创建一个文件索引,它指示了文件存放的物理地址,再把数据存储到该地址中。当需要读取数据时,可以从目录中找到该文件的索引,进而在相应的地址中读取出数据。具体还涉及到逻辑地址、簇大小、不连续存储等一系列辅助结构或处理过程。

文件系统的存在使我们在存储数据时,不再是简单的向某物理地址直接读写,而是要遵循它的读写格式。如经过逻辑转换,一个完整的文件可能被分开成多段存储不连续的物理地址,使用目录或者链表的方式来获知下一段的位置

分类

在Windows下,一般有FAT、NTFS、exFAT之类的文件系统,Linux下常用ext2、ext3、ext4、NFS之类的文件系统,但也支持Windows的FAT等

  • 目前有许多存储介质:硬盘、U盘、SD卡、NAND Flash、NOR Flash、网络存储设备…
  • 不同的存储介质一般要使用不同的文件系统,比如管理NAND Flash要用YAFFS文件系统,管理硬盘、SD卡的话可以使用ext文件系统等等
  • 不同文件系统的区别在于数据在磁盘中的==布局不同==,比如ext文件系统下,数据可能被按照某种格式组织起来,另一个文件系统又是不同的组织形式

在格式化硬盘的时候,其实就是创建文件系统的过程

image-20231213092227520

挂载

定义:将文件系统连接到某个目录,使得用户可以通过构造出文件的绝对路径和相对路径,以便访问文件

  • 只有在文件系统挂载后,才能对其进行访问,不然他就是只是一个设备节点比如/dev/sda

  • 比如把U盘插到电脑上,电脑上突然多了个G盘,这就是把U盘挂载到了电脑上

在Windows系统下,文件系统挂载是其内部完成的,而在Linux系统中,需要手动把一个存储设备挂载到某个目录下才可以访问,可以通过以下命令实现:

1
2
3
4
mount [-选项] [设备] [挂载点]

mount /dev/sdb1 /mnt/usb # 把U盘挂载到/mnt/usb目录下
mount -t nfs 192.168.1.100:/share /mnt/nfs # 挂载一个nfs文件系统

可以通过以下命令查看当前文件系统的挂载情况:

1
df -T -h <path_to_dir>

image-20231213092939886

由此可以看到各个存储设备的文件系统的格式以及挂载到了哪个目录里面

  • 第一列虽然叫FileSystem,但他这里应该指的是存储介质,这里写/dev/sda是因为文件系统存在该介质中
  • tmpfs是一种虚拟文件系统,这里有多个应该是因为内存中有好几个分区都是用了该文件系统类型

分区

定义:将一个存储介质分为多个区域,每个分区都得有独立的文件系统

在Linux下,一个目录可能是一个独立的分区,也有可能是另一个分区的一个文件夹,比如/bin这个目录就是根文件系统的一个目录,而/mnt又是一个独立的文件系统

如何看有哪些存储设备,有哪些分区?

使用ls /dev/sd*可以看出有哪些存储设备、哪些分区

image-20231213093444980

其中有sda和sdb2个存储设备,sda有sda1、sda2、sda5三个分区、sdb有sdb1一个分区

Linux内核文件子系统

虚拟文件系统

Linux的文件系统中实现了个VFS抽象层,来定义各个文件系统均需要的一些属性和操作

具体地,VFS根据文件系统的共性需求(元数据管理、路径解析、进程隔离)抽象出了4个对象(对应内核中4个数据结构):

VFS 对象 Linux中的结构体 抽象意义
超级块 struct super_block 文件系统的元数据容器
inode struct inode 文件/目录的元数据
目录项 struct dentry 路径解析的中间缓存层
文件对象 struct file 进程与文件的交互接口

对于这4个对象,VFS对每个对象又封装了个操作函数表,里面全是函数指针。每个具体的文件系统都得实现操作函数表里的各个API(有点类似驱动开发里的bus_type和各个具体总线的实现):

  • super_operations对象,其中包括内核针对特定文件系统所能调用的方法,比如write_inode()和sync_fs()等方法
  • inode_operations对象,其中包括内核针对特定文件所能调用的方法,比如create(), link()等方法
  • dentry_operations对象,其中包括内核针对特定目录所能调用的方法,比如dcompare(), delete()等方法
  • file_operations对象,其中包括进程针对已打开文件所能调用的方法,比如read(), write()等方法

例如超级块对象的操作函数表定义如下:

1
2
3
4
5
6
7
8
9
10
struct super_operations{
struct inode *(*alloc inode)(struct super block *sb);
void{*destroy inode)(struct inode *);
void(*dirty inode)(struct inode *);
int(*write inode)(struct inode *,int)
void(*drop inode)(struct inode *);
void(*delete inode)(struct inode *);
void(*put super)(struct super block *);
....
}

特殊文件系统

在Linux的根目录下,还挂载了很多特殊文件系统,他们不管理磁盘数据,而是提供内核与用户空间的交互接口或实现特定功能

这些文件系统是由内核创建并维护在内存中的,所以通常不允许直接在虚拟文件系统里面创建文件

类型 文件系统 挂载点 核心功能
内核信息 sysfs /sys 设备、驱动、内核对象管理
调试 debugfs /sys/kernel/debug 内核调试接口
资源控制 cgroupfs /sys/fs/cgroup 控制组(cgroups)资源隔离
临时存储 tmpfs /dev/shm, /run 内存中的临时文件
安全 selinuxfs /sys/fs/selinux SELinux 策略管理
容器支持 overlayfs / (容器内) 联合挂载,支持镜像分层

对于不同类型的文件,open(), read()等系统调用的作用都不尽相同,这究竟是怎么实现的呢?

image-20241008102459371

当我们得到一个文件描述符fd时,实际上内核中会创建一个文件对象(struct file),文件对象不仅包含文件的基本信息(如文件偏移量、状态标志),还指向一个与该文件类型关联的文件操作表(file_operations 结构体)

1
2
3
4
5
6
7
struct file_operations {
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
int (*release) (struct inode *, struct file *);
// 其他文件操作函数
};

Linux内核在创建struct file时,首先会根据inode查看它是什么类型的文件,接着在依据其文件类型,填充struct file_operations,这样就实现了文件操作系统调用的多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
struct inode {
umode_t i_mode;
unsigned short i_opflags;
kuid_t i_uid;
kgid_t i_gid;
unsigned int i_flags;

const struct inode_operations *i_op;
struct super_block *i_sb;
struct address_space *i_mapping;

unsigned long i_ino;
/* ... 其他成员 ... */

/* 文件系统特定的数据 */
union {
struct hlist_node i_hash;
struct {
struct list_head i_lru;
struct list_head i_sb_list;
struct list_head i_wb_list;
};
struct rcu_head i_rcu;
};

atomic_t i_count;
unsigned int i_nlink;
dev_t i_rdev;
loff_t i_size;

struct timespec64 i_atime;
struct timespec64 i_mtime;
struct timespec64 i_ctime;

spinlock_t i_lock; /* 保护以下成员 */
struct list_head i_dentry;

/* ... 其他成员 ... */
};

在inode的源码中,我们主要关注2个成员变量:

  • i_mode:此文件属于什么类型:目录、普通文件、符号链接、网络、FIFO、字符设设备、块设备
  • i_op:表示与inode相关的文件系统操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct inode_operations {
int (*create) (struct inode *, struct dentry *, umode_t, bool);
struct dentry * (*lookup) (struct inode *, struct dentry *, unsigned int);
int (*link) (struct dentry *, struct inode *, struct dentry *);
int (*unlink) (struct inode *, struct dentry *);
int (*symlink) (struct inode *, struct dentry *, const char *);
int (*mkdir) (struct inode *, struct dentry *, umode_t);
int (*rmdir) (struct inode *, struct dentry *);
int (*mknod) (struct inode *, struct dentry *, umode_t, dev_t);
int (*rename) (struct inode *, struct dentry *,
struct inode *, struct dentry *, unsigned int);
void (*truncate) (struct inode *);
int (*setattr) (struct dentry *, struct iattr *);
int (*getattr) (const struct path *, struct kstat *, u32, unsigned int);
/* ... 其他操作 ... */
};

stat系统调用

​ 为了让用户便于查看某文件的元信息(inode),Linux系统为我们提供了stat这个系统调用,通过该系统调用,内核首先会找到该文件对应的inode,接着从inode中提取重要的信息到struct stat中,并展示给用户

image-20240815092624584