06-07 - 文件系统(上)

---- 整理自 王利涛老师 课程
实验环境:宅学部落 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. 磁盘的格式化与挂载

  • 实验:
  1. 模拟一块磁盘
  2. 格式化、将数据 dump 出来
  3. 挂载、读写数据
  4. 将读写数据 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 设备文件的读写过程

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 26
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

uuxiang

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

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

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

打赏作者

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

抵扣说明:

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

余额充值