---- 整理自 王利涛老师 课程
实验环境:宅学部落 www.zhaixue.cc
文章目录
1. 什么是文件系统?设备端
1.1 数据的存储
磁盘:磁道、扇区
NAND Flash:Page、Block
1.2 纯数据区和元数据区
- 元信息 metadata:文件的 时间、owner、权限、……
- 纯数据
1.3 inode table
1.4 inode bitmap 和 data bitmap
1.5 superblock
- superblock:记录文件系统的整体性信息
1.6 小结
1.7 minix 文件系统
2. 磁盘的格式化与挂载
- 实验:
- 模拟一块磁盘
- 格式化、将数据 dump 出来
- 挂载、读写数据
- 将读写数据 dump 出来
# -n 参数分别是:分区号:起始地址:终止地址。
# 分区号为0:代表使用第一个可用的分区号;第二个0代表分区的开始地址,为0则表示为第一个可用地址;第三个0代表结束地址,为0则表示磁盘末尾
sgdisk -n 0:0:0 test.disk
# 打印分区列表
sgdisk -p test.disk
losetup
命令用来设置循环设备。循环设备可把文件虚拟成块设备,借此来模拟整个文件系统,让用户可以将其视为硬盘驱动器、光驱或软驱等设备,并挂入当作目录来使用。
3. 什么是文件系统?主机端
3.1 内核中的文件系统
3.2 File Model
- file_system_type
- super_operations
- inode_operations
- dentry_operations
- file_operations
4. 文件系统核心数据结构:super_block
- minix 的超级块
- 分析一下之前创建的 minix 文件系统:
super block
struct minix_super_block {
__u16 s_ninodes; // 0x0560=1376,一共1376个inode节点。可以观察图片
__u16 s_nzones; // 0x1000=4096,一共4096个data blocks。可以观察图片
__u16 s_imap_blocks; // 0x0001,inode位图占据1个block
__u16 s_zmap_blocks; // 0x0001,data block位图占据1个block
__u16 s_firstdatazone; // 0x002f=47,第一个data block的编号为47。可以观察图片
__u16 s_log_zone_size; // 一个data block的大小为2^0,即1KB
__u32 s_max_size; // 0x10081c00,文件最大长度为0x10081c00字节
__u16 s_magic;
__u16 s_state;
__u32 s_zones;
};
5. 文件系统核心数据结构:inode
- 当用户使用 shell 命令 touch 或者 open 系统调用创建一个文件时,文件系统中会使用 唯一的一个 inode 来标识这个文件的相关信息 。
struct minix_inode {
__u16 i_mode; // 权限
__u16 i_uid; // 文件所属用户
__u32 i_size; // 文件大小
__u32 i_time; // 文件时间戳
__u8 i_gid; // 文件所属组
__u8 i_nlinks; // 文件的引用计数
__u16 i_zone[9]; // 文件数据存储在data block上的位置
};
5.1 inode bitmap
- 所有文件的 inode 都保存在磁盘上的 inode table 上,文件系统使用 inode bitmap 来记录 inode table 中 inode 的使用情况。先看 inode bitmap 数据,磁盘刚刚格式化后的数据如下:
inode bitmap
磁盘刚刚初始化,一共 1376 个 inode,所以在 inode bitmap 要用 1376 个 bit 位来表示这些 inode,从地址 0x0000 0800 到 0x0000 08ab,一共(0xab + 1=172)个 字节,每个 bit 代表 1 个 inode,则一共表示 172*8=1376 个 inode。inode bitmap 的开头第一个字节 0x03=0000 0011,其中第一个位图不用(1376-1=1375 个 inode),但是置为 1(还有一个 1 标识的是 根目录项 的 inode),再补充算上 0x0000 08ac 上的 0xfe 上的一个 bit 位(1375+1=1376 个 inode),正好是 1376 个比特位,也就是有 1376 个 inode。
5.2 data bitmap
- 所有文件的纯数据都保存在 data block 上,文件系统使用 data block bitmap 来记录 data blocks 的使用情况。磁盘刚刚格式化之后,data block bitmap 数据如下:
data bitmap
磁盘格式化后,一共有 4096 个 block,每个 block 的大小是 1KB。因为前面的元数据区占据了 47 个block,还剩下的 data block 数量为 4049 个。data bitmap 中需要 4049 个 bit 位来表示这些 block 的使用情况。从 0x0000 0c00 到 0x0000 0df9 一共 506 个字节(506*8=4048bit),再加上 0x0000 0dfa 上的数据 0xfc 的 2 个 bit 位,再减去起始位置的 1 个 bit 位,一共是 4096,和我们的实验相符。
5.3 inode table
- 在 minix 文件系统中,第一个数据块的起始地址是 block 47,第一个数据块的数据其实是根目录的数据区。经过格式化后的文件系统里面什么都没有:没有文件、没有用户创建的目录,但是 会存在有一个根目录。
- 目录项在文件系统中也是一个文件,也会用唯一的 inode 来标识。
inode
- 0x002f=47,文件数据存储在第 47 个 data block 上,每个 block 的大小是 1KB,47*1024=0xbc00。
struct minix_inode {
__u16 i_mode; // 0x41ed,八进制为040755,权限
__u16 i_uid; // 0x0000,用户为root,文件所属用户
__u32 i_size; // 0x0000 0040,文件大小64字节
__u32 i_time; // 文件时间戳
__u8 i_gid; // 文件所属组
__u8 i_nlinks; // 文件的引用计数0x02,父目录和当前目录都指向根目录
__u16 i_zone[9]; // 0x002f=47,文件数据存储在第47个data block上
};
5.4 data
一个目录文件的内容是该目录下的文件的文件名或者子目录名,每个数据块的大小是 1KB,在偏移地址 0xbc00(47*1024)上,我们可以看到第一个数据块的内容:
data
数据区中的 0x2e 转换为 ASCII 就是 “ .
”,所以在根目录下我们可以看到,除了一个“点”和“点点”分别指向 当前目录和父目录,就没有其它数据了。inode 的编号从 1 开始,Minix 文件系统的根目录的inode number 是 1,它是 inode table 这个数组里的 index。如果把 inode table 看做一个数据 inode[ ]的话,通过 inode number 索引就可以获得 inode 节点的信息。
- 实验中的接下来,我们往磁盘里写了数据,在磁盘上创建文件和目录,接着分析 minix.data:
而在根目录的数据块(也即第一个数据块)内容上,我们可以看到多了新的内容:新建的文件名 hello.c 和目录 dir_test。
- 那文件系统是如何根据文件名和对应的 inode 节点建立关联的呢?
5.5 小结:整体示意图 - minix_origin.data vs. minix.data
6. 文件系统核心数据结构:dentry
- 在文件系统模型中,目录也是一个文件,也会使用一个唯一 inode 来标识。
- 目录文件的内容 包括:该目录下的子文件、子目录、一个父指针点点、一个当前指针点 等。
- 每个目录项都是路径名的一部分,一个目录项一旦被加载到内存,它就会被 VFS 转换为一个 dentry 对象。
- 目录项的 核心功能 就是 将 用户定义的文件名 和 文件系统分配的 inode 关联起来。将上层应用程序对文件名的操作转换为对 inode 的操作。通过关联的文件名找到对应的 inode,通过 inode 就可以找到文件在磁盘上的存储位置。
- 即:应用程序 ⇒ 文件名 ⇒ inode
struct minix_dir_entry {
__u16 inode;
char name[0];
};
我们在文件系统的根目录下创建了一个子目录 dir_test,在 dir_test 子目录下创建了一个文件 hello.h,加上创建的 hello.c,此时 inode table 中就有了 4 个 inode 数据,分别标识 4 个文件:根目录、hello.c、dir_test、hello.h。
*
// inode 1 -- 根目录
00001000 ed 41 00 00 80 00 00 00 e4 ac f1 65 00 03 2f 00 |.A.........e../.|
00001010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
// inode 2 -- hello.c
00001020 a4 81 00 00 0d 00 00 00 da ac f1 65 00 01 30 00 |...........e..0.|
00001030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
// inode 3 -- dir_test
00001040 ed 41 00 00 60 00 00 00 ef ac f1 65 00 02 31 00 |.A..`......e..1.|
00001050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
// inode 4 -- hello.h
00001060 a4 81 00 00 13 00 00 00 ff ac f1 65 00 01 32 00 |...........e..2.|
00001070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
在 0x0000 1040 处 inode 3 标识的是 dir_test 子目录:
// inode 3 -- dir_test
00001040 ed 41 00 00 60 00 00 00 ef ac f1 65 00 02 31 00 |.A..`......e..1.|
00001050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
struct minix_inode {
__u16 i_mode; // 0x41ed,八进制为040755,权限
__u16 i_uid; // 0x0000,用户为root,文件所属用户
__u32 i_size; // 0x0000 0060,文件大小96字节
__u32 i_time; // 文件时间戳
__u8 i_gid; // 文件所属组 0x00,属于用户组:root
__u8 i_nlinks; // 文件的引用计数0x02
__u16 i_zone[9]; // 0x0031=49,文件数据存储在第49个data block上
};
dir_test 目录项的文件内容在第 49 个 data block(一个块 1KB,49*1024=0xc400)上,也就是偏移 0xc400 处,可以看到 dir_test 目录项的文件内容:
// 因为创建的是目录,生成了一个点和一个点点
struct minix_dir_entry {
__u16 inode;
char name[0];
};
其中,0x0004 是 inode 编号,后面的零长度数组 name 指向的是文件名:hello.h。根据 inode 编号 4,我们可以在 inode table 中找到头文件 hello.h 对应的 inode(也就是下面的 inode 4 – hello.h,再根据 minix_inode 结构体,找到数据的存储位置在第 0x0032=50 个 block 上):
*
// inode 1 -- 根目录
00001000 ed 41 00 00 80 00 00 00 e4 ac f1 65 00 03 2f 00 |.A.........e../.|
00001010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
// inode 2 -- hello.c
00001020 a4 81 00 00 0d 00 00 00 da ac f1 65 00 01 30 00 |...........e..0.|
00001030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
// inode 3 -- dir_test
00001040 ed 41 00 00 60 00 00 00 ef ac f1 65 00 02 31 00 |.A..`......e..1.|
00001050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
// inode 4 -- hello.h
00001060 a4 81 00 00 13 00 00 00 ff ac f1 65 00 01 32 00 |...........e..2.|
00001070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
struct minix_inode {
__u16 i_mode; // 0x81a4,八进制为100644,权限
__u16 i_uid; // 0x0000,用户为root,文件所属用户
__u32 i_size; // 0x0000 0013,文件大小19字节
__u32 i_time; // 文件时间戳
__u8 i_gid; // 文件所属组 0x00,属于用户组:root
__u8 i_nlinks; // 文件的引用计数0x01
__u16 i_zone[9]; // 0x0032=50,文件数据存储在第50个data block上
};
当然,在主机程序端,会遍历 dir_test 目录下的所有 inode 对象,通过文件名查找到 hello.h 对应的 inode,这一步主机文件系统中实现。最后在第 50 个 data block 上,也就是偏移 0xc800 处,我们就可以看到 hello.h 文件的真正纯数据:
*
0000c800 54 68 69 73 20 69 73 20 61 20 2a 2e 68 20 66 69 |This is a *.h fi|
0000c810 6c 65 0a 00 00 00 00 00 00 00 00 00 00 00 00 00 |le..............|
0000c820 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
7. 文件系统核心数据结构:file
8. 虚拟文件系统:VFS
8.1 虚拟文件系统的作用
- VFS:文件系统抽象层
Virtual Filesystem Switch - 连接操作系统内核和具体文件系统的桥梁
- 向下的接口:与具体文件系统交互,实现回调
- 向上的接口:抽象统一系统调用接口
- 缓存数据:超级块信息、链表、inode……
- 初始化、挂载、路径名查找……
8.2 VFS 核心数据结构
- VFS super_block
- VFS inode
- VFS dentry
- VFS file
- file_system_type
- vfsmount
- 全局数组、变量
9. 文件系统的注册
链表:file_systems
函数:register_filesystem
// kernel\linux-5.10.4\fs\filesystems.c
int register_filesystem(struct file_system_type * fs);
int unregister_filesystem(struct file_system_type * fs);
- 注册 / 注销文件系统 内核源码分析
// kernel\linux-5.10.4\fs\minix\inode.c
static struct file_system_type minix_fs_type = {
.owner = THIS_MODULE,
.name = "minix",
.mount = minix_mount,
.kill_sb = kill_block_super,
.fs_flags = FS_REQUIRES_DEV,
};
MODULE_ALIAS_FS("minix");
static int __init init_minix_fs(void)
{
int err = init_inodecache();
if (err)
goto out1;
err = register_filesystem(&minix_fs_type);
if (err)
goto out;
return 0;
out:
destroy_inodecache();
out1:
return err;
}
static void __exit exit_minix_fs(void)
{
unregister_filesystem(&minix_fs_type);
destroy_inodecache();
}
- 因为每个文件系统的 super block 超级块的格式不同,所以每种文件系统需要向 VFS 注册文件系统类型 file_system_type,实现 mount 方法来 读取和解析超级块 super block。
cat /proc/filesystems
可以用来查看已经注册的文件系统类型。
10. 文件系统的挂载
-
每次挂载文件系统,虚拟文件系统 VFS 就会创建一个 挂载描述符:mount 结构体。挂载描述符用来描述文件系统的一个挂载实例,同一个存储设备上的文件系统可以多次挂载,每次挂载到不同的目录下。
-
核心数据结构
mount、vfsmount、dentry、inode、super_block
注:一个 mount 代表一个挂载实例,记录这次挂载的信息。读取文件系统信息初始化如下的结构体信息。
- 几个重要的全局变量
不同的文件系统挂载多次
- 挂载后的文件系统全景图
- 父文件系统:核心数据结构体之间的关联
- 子文件系统:核心数据结构体之间的关联
- 父、子文件系统的关联
- 路径名的解析:二元组<mount, dentry>
- 思考:
- 多个文件挂载同一个目录,底层操作是怎样的?
- 为什么多次挂载之后,只显示最后挂载得到内容?
- 挂载过程分析
// kernel\linux-5.10.4\fs\namespace.c
SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name,
char __user *, type, unsigned long, flags, void __user *, data)
|--> do_mount
|--> path_mount // 将用户空间的路径名转换为内核空间的路径表示
|--> do_new_mount
|--> vfs_get_tree // 调用minix_fs_type的mount回调函数,读取超级块信息,初始化super block
do_new_mount_fc // 创建挂载实例:vfsmount对象
|--> do_add_mount
|--> graft_tree
|--> attach_recursive_mnt
|--> propagate_mnt // 将mount添加到全局数组mnt_hash中
mnt_set_mountpoint // 将子mount和父mount建立关联
commit_tree // 将挂载点dentry和父vfsmount通过hash计算保存到mount_hashtable[]。路径解析时,lookup_mnt会通过父vfsmount和挂载点dentry找到子mount
|--> __attach_mnt // 将挂载的dentry和vfsmount生成hash数组保存mount_hashtable[]
11. 文件打开过程分析
open("/mnt/zhaixue.c")
- 内核中的核心数据结构及其关联
- 用户空间路径名到内核空间路径的转换
- 用户空间 pathname 转换为 path 结构体 <vfsmount, dentry>
- 路径的转换:
用户空间的路径:由 “ / ” 链接的一个字符串
内核空间:超级块、inode、dentry、vfsmount
内核空间的路径 path:vfsmount + dentry
路径查找上下文:struct nameidata nd;
- 路径解析、有挂载点的路径解析
- 文件描述符 fd 和 file 对象的关联
- file 和 inode、file_operations 的关联
- 文件 I/O 回调函数是怎样被调用的?
- 普通文件
- 设备文件:字符设备、块设备
- 管道文件
- 链接文件
- 源码分析
// fs/open.c
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
|--> do_sys_open
|--> do_sys_openat2
|--> get_unused_fd_flags // 申请文件描述符:file description
|--> do_filp_open // 创建file对象表示打开的文件,并初始化
|--> path_openat // 创建file对象并初始化
|--> do_open
|--> vfs_open
|--> do_dentry_open
|--> f->f_op = fops_get(inode->i_fop); // file->file_operations = inode->file_operations
open = f->f_op->open; // 调用具体的open回调函数:普通文件一般为NULL,字符、块设备在驱动中实现。VFS通过回调,转向不同的具体文件系统或设备驱动中...
|--> fd_install // 将file和fd关联起来,file对象添加到fd_array数组中,最后返回fd给用户空间
12. 文件创建过程分析
12.1 文件的分类
- 普通磁盘文件:open、close
- 目录文件:mkdir、rmdir
- 链接文件:symlink、unlink
- 字符设备文件:mknod、unlink
- 块设备文件:mknod 、unlink
- 管道文件: mknod、unlink
- 套接字文件: mknod、unlink
12.2 文件创建过程分析
- 用户层:create\open\touch
- 系统调用:do_sys_open
- VFS层的操作:inode
- 具体文件系统:minix
- inode table、inode bitmap
// fs/open.c
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
|--> do_sys_open
|--> do_sys_openat2
|--> get_unused_fd_flags // 申请文件描述符:file description
|--> do_filp_open // 创建file对象表示打开的文件,并初始化
|--> path_openat // 创建file对象并初始化
|--> // 判断文件是否存在?
while (!(error = link_path_walk(s, nd)) && (s = open_last_lookups(nd, file, op)) != NULL)
|--> dentry = lookup_open(nd, file, op, got_write); // 若指定的文件不存在,则创建一个文件
|--> if (!dentry->d_inode && (open_flag & O_CREAT)) // 没有找到指定的文件,创建一个文件
|--> // 调用如minix的inode的minix_create
error = dir_inode->i_op->create(dir_inode, dentry, mode, open_flag & O_EXCL);
|--> minix_create
|--> minix_mknod
|--> do_open
|--> ... // 步骤和文件打开过程相同
13.文件读写过程
13.1 地址空间与页缓存
- page cache
- buffer cache
13.2 地址空间的概念
- radix tree
- address_space:从进程角度来看,是通过
fd->file->address_space->radix_tree->page
的逻辑,找到文件对应的 page cache。 - address_space_operations
- 结构体关联
13.3 read 内核流程分析
- 系统调用传参:文件描述符 + 文件位置偏移
- 定位:
fd --> file --> inode --> address_space --> page
- 若找到对应的 page,数据拷贝到用户空间
- 若没找到 page,内核新建一个 page 加入页缓存树中,并将数据从磁盘读到 page cache
- 将 page 上的数据拷贝到用户空间
- 演示:minix 文件系统内核源码分析
14. 设备文件
- 设备节点(文件)
从文件系统的角度看设备文件 - 字符设备是什么?
- 块设备是什么?
- 设备节点是什么?
- 设备节点是怎么生成的?
生成设备节点的三种方式:udev/mdev/mknod/内核创建 - 文件系统:devtmpfs、tmpfs、/dev
14.1 设备文件创建过程分析
- 设备文件创建:
mknod /dev/rtc1 c 255 1
- mknod 流程分析
- inode 默认的 file_operation
- 创建过程中设备文件和普通文件的差异
14.2 设备文件打开过程分析
inode->i_fop = &def_chr_fops
inode->i_rdev = rdev
打开字符设备时会更新file->f_op
域。
回到开头的位置:
14.3 设备文件的读写过程