【Linux&操作系统】8. 文件系统

64 阅读11分钟

8 文件系统

  • 上一章节我们了解了基础IO的原理和方式,针对的其实是内存级文件,也就是对一个被打开的文件进行IO

  • 而我们知道,计算机中还有没有打开的文件,这些文件一样需要被管理起来,于是就需要我们学习本章节内容,了解文件系统的构造和原理,以及它的职责

8.1 机械硬盘原理

8.1.1 机械硬盘的各个组件
  • 这里推荐两个视频,强烈建议先看看:

  • [机械硬盘原理]

  • [机械硬盘的发展史]

  • 简单来说:机械硬盘的每个盘面由内至外被划分为一个个圆环,每个圆环又被分割为一个个类似于扇形的扇面,每个扇面包含很多个单位,每个单位都相当于一个可以磁化的小金属块,这个小金属块的磁极上下(磁极上下是比较新的技术,老技术是以左右形式)都代表着0/1两种值,一个扇区的大小因为技术更迭而可能不同,旧技术下的扇区大小为512字节,新技术下的扇区大小一般为4kb

  1. 机械硬盘拥有若干个光盘
  2. 机械硬盘的每个光盘称为盘片
  3. 每个盘片拥有正反两个盘面
  4. 每个盘面拥有若干个磁道,也就是被划分的一个个圆环
  5. 每个圆环被划分为若干个扇区
  6. 每个盘面都会有一个磁头
  7. 每个盘片都会有正反一对磁头
  8. 所有磁头的运动都是同步的
  9. 所有盘片的旋转也是同步的
  10. 盘片旋转的速度非常快,[甚至还有人将机械硬盘的盘片电机改装成电动打磨机的]
  11. 磁头通过伸出的幅度(或者说角度)来定位磁道
  12. 所有盘面的同一个位置的磁道的集合称为柱面(可以想象到,就是一个圆环柱体)
  13. 可以预见的是,机械硬盘的精度很高,[所以一旦拆了机械硬盘,这个硬盘就废了]
8.1.2 机械硬盘的寻址方式
8.1.2.1 CHS寻址
  • 所以不难发现,机械硬盘的寻址包含三个步骤

    1. 柱面:即寻找地址所在的柱面,此时所有磁头都会伸出并定位到对应位置,就是定位磁道位置
    2. 磁头:因为磁头数量有非常多,所以需要找到对应的磁头,即找到地址所在的盘面,或者说找到这个磁道集合中对应的磁道
    3. 扇区:即在找到的磁道中找到对应扇区,寻找扇区靠的是机械硬盘盘片的高速旋转
  • 所以该种方式被称为CHS寻址,即Cylinder-Head-Sector寻址,"柱面-磁头-扇区"

  • 所以我们仔细想想,每个柱面是不是都包含两个地址,即在一个柱面中,使用"磁头"和"扇区"的组合就可以找到该柱面中的扇区

  • 所以再仔细想想,硬盘包含多个柱面,所以从逻辑上来看,磁盘就是一个三维数组咯!

8.1.2.2 LBA寻址
  • 在OS看来,所有的三维数组都是由一维数组组成的,在OS的角度,磁盘的地址是纯线性的,即将三维数组全部拆开并成一个一维数组,这就是LBA寻址的机制,即Logical Block Addressing寻址,逻辑块寻址

  • 操作系统只认识LBA地址

8.1.2.3 CHS地址和LBA地址间的转换
  • 因为操作系统只认识LBA地址,但磁盘的物理设计却只能使用CHS地址,所以我们就需要将两个地址互相转化才可以正确寻址

  • 这俩地址的互相转化是磁盘来帮助我们计算的

  • 这里我们需要明确几个概念:

    1. LBA地址是从0开始的
    2. CHS地址中的柱面号是从0开始的
    3. CHS地址中的磁头号是从0开始的
    4. CHS地址中的扇区号是从1开始的
  • LBA地址 = 柱面号C * 单个柱面的扇区数 + 磁头号H * 单个磁道的扇区总数 + 扇区号S - 1

  • 柱面号C = LBA地址 // 单个柱面的扇区数 (//是除取整)

  • 磁头号H = LBA地址 % 单个柱面的扇区数

  • 扇区号S = (LBA地址 % 每磁道扇区数) + 1

  • 在操作系统的角度,磁盘就是一个一维数组,在后面的小节中我们也这么理解

8.1.3 查询当前计算机的硬盘信息
  • 查询该信息需要root权限
$ sudo fdisk -l

Disk /dev/vda: 53.7 GB, 53687091200 bytes, 104857600 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x000b2d99

   Device Boot      Start         End      Blocks   Id  System
/dev/vda1   *        2048   104856254    52427103+  83  Linux
  • 即该磁盘一共有104857600个扇区
  • 每个扇区有512字节

8.2 文件系统对于扇区(硬盘空间)的管理

  • 先前我们提到过,对于操作系统而言,硬盘就是一个一维数组

  • 每个数组的元素都是一个扇区

  • 但当今计算机的角度而言,直接对一个扇区做随机读写的效率太低了,如果要读写4kb的数据,就需要同时对8个扇区进行随机读写,一个个寻址太慢了

  • 所以我们规定了一个新的方式用于管理扇区,即同时管理连续的8个扇区,也就是管理连续的4kb,我们称这个连续的4kb空间为一个"块",也就从对512字节的空间的随机读写转变成了对4kb连续空间的随机读写了

  • 事实上如果你测试过硬盘性能的话,那其实你离"块"的概念还是非常近的

  • 我们常说的硬盘的"4k随机读写性能"其实就是指的计算机随机读写4kb块的性能,因为4kb就是实际我们常规使用计算机时能接触到的最小管理单位(或者说现代计算机系统及其文件系统能管理空间的最小单位),所以"4k随机读写性能"能更加反应极限情况下硬盘的性能(曾经的我甚至以为"4k随机读写"是对一个4k电影的处理性能哈哈)

  • 但即便将管理单位扩大到4kb,对于用户而言也是一个不小的工作量,用户不可能对每个"块"单独进行读写对吧,所以我们有了"分区"的概念,即"分区"就是"块"的集合,用于管理"块",也就是我们"给硬盘分区"的那个"分区",本质上就是通过类似于start,end的指针划分"块",然后统一管理"块"的集合

  • 当然,"分区"与"块"之间的差距还是太大了,可能达到上千上万倍甚至更大,所以我们还需要一个中间件,用于更好地在"分区"之下管理"块",于是就有了"组"(我们也可以成为"块组",是同一个东西)

  • "组"是对于"分区"的逻辑管理,而并不仅仅是划分"分区",一个组中会包含很多区域,其中就包含有专门存放数据的区域Data Blocks,存放文件属性的区域inode Table

  • 我们知道,文件的属性的数量一定是固定的,归根结底文件也就那么些属性而已,只有其值的区别,所以我们可以用一个结构体来描述一个文件的属性,所以文件的属性一定是固定的,所以有规定:一个文件的属性一定是128字节

image-18.png

  • 所以什么是文件系统,就是以上用来管理文件的各种结构,或者说管理文件的方式,不同文件系统管理文件的方式在底层也会有些不同

  • 并且我们离文件系统其实也非常近,如果你格式化过硬盘你就应该知道,格式化硬盘的时候我们需要选择硬盘的格式,诸如NTFS,FAT32,exfat这些,其实这些就是文件系统,以下是常见的文件系统:

    1. FAT系列:包括FAT12,FAT16,FAT32,是主要用于U盘的文件系统,也算是最早期的文件系统之一,缺点是最大文件大小比较小,FAT32只有4GB的最大文件大小,所以像是在U盘中拷贝高视频规格的电影就不适合这种文件系统
    2. NTFS:一般用于Windows系统的文件系统,是Windows的标准文件系统,对于Linux的适配其实比较差,所以一般Linux不用NTFS
    3. ext4:是Linux的常用文件系统,主要用于高性能服务器领域
  • 这些文件系统在某些地方可能会有一些差别,例如文件属性的大小设定,组的大小等等,但大体上的设计都是差不多的

8.3 "组"的内容细谈

8.3.1 "组"的各个区域
  • 从图中,我们已经可知"组"分为以下区域,下面我们详细解释一下各个区域的作用,并解决一些问题:
    1. Data Block:基本单位是块,所占空间是组里最大的,用于存放文件内容
    2. inode Table:基本单位也是块,用于存放文件的属性,inode Table本质上是一张表(或者说我们抽象成一张表,本质是由一个个结构体节点组成的数据结构),一个块可能会有上千个文件,所以这张表可能也会有上千行,每行都是某个文件的所有属性,因为文件的属性的字长一定是固定的,所以每一个行的大小一定是,规定是128字节,意味着一个块可以存放32个行(或者说节点)
    3. inode Bitmap:一个位图,用于判断inode Table中还有没有空余位置,同时我们也可以通过位图找到文件的属性(具体找的方式我们后面会提到的),一个位对应一个inode,所以inode Bitmap的大小也是固定的
    4. Block Bitmap:一个位图,用于判断Data Block中的空余位置,同时我们也可以通过位图找到文件的内容
    5. GDT:用于描述当前组的各个区域,并对当前组的各个区域做管理,我们后面也会谈到
    6. Super Block:"超级块",这个区域的内容用于描述整个分区,我们后面也会谈到
8.3.2 inode Table
  • 我们简单通过源码鉴赏看看一个inode结构中有哪些内容
struct inode {
	struct hlist_node	i_hash;
	struct list_head	i_list;
	struct list_head	i_sb_list;
	struct list_head	i_dentry;
    //以上结构均用于链接形成数据结构

	unsigned long		i_ino; // 即inode number
	atomic_t		i_count; // 用于引用计数
	umode_t			i_mode; // 记录了文件的权限和文件类型
	unsigned int		i_nlink; 
	uid_t			i_uid; // 代表文件的所有者
	gid_t			i_gid; // 代表文件的所属组
	dev_t			i_rdev; 
	loff_t			i_size; // 文件的大小
	struct timespec		i_atime;
	struct timespec		i_mtime;
	struct timespec		i_ctime;
	unsigned int		i_blkbits;
	unsigned long		i_blksize; 
	unsigned long		i_version;
	blkcnt_t		i_blocks; //文件所占用的块数
	unsigned short          i_bytes;
	spinlock_t		i_lock;	/* i_blocks, i_bytes, maybe i_size */
	struct mutex		i_mutex;
	struct rw_semaphore	i_alloc_sem;
	struct inode_operations	*i_op; // 操作函数表
	const struct file_operations	*i_fop;	/* former ->i_op->default_file_ops */
	struct super_block	*i_sb;
	struct file_lock	*i_flock;
	struct address_space	*i_mapping;
	struct address_space	i_data;
#ifdef CONFIG_QUOTA
	struct dquot		*i_dquot[MAXQUOTAS];
#endif
	/* These three should probably be a union */
	struct list_head	i_devices;
	struct pipe_inode_info	*i_pipe;
	struct block_device	*i_bdev;
	struct cdev		*i_cdev;
	int			i_cindex;

	__u32			i_generation;

#ifdef CONFIG_DNOTIFY
	unsigned long		i_dnotify_mask; /* Directory notify events */
	struct dnotify_struct	*i_dnotify; /* for directory notifications */
#endif

#ifdef CONFIG_INOTIFY
	struct list_head	inotify_watches; /* watches on this inode */
	struct mutex		inotify_mutex;	/* protects the watches list */
#endif

	unsigned long		i_state;
	unsigned long		dirtied_when;	/* jiffies of first dirtying */

	unsigned int		i_flags;

	atomic_t		i_writecount;
	void			*i_security;
	union {
		void		*generic_ip;
	} u;
#ifdef __NEED_I_SIZE_ORDERED
	seqcount_t		i_size_seqcount;
#endif
};
8.3.3 一些规定
  • 再次我们需要暂时了解一些规定,了解这些是为了后面的内容做一些铺垫:
    1. inode的中编号是跨组的编号的,但并不是跨分区编号的
    2. 在文件总量不变的情况下,先创建的文件inode中的编号会更小,后创建的更大,也就是按创建顺序排列
    3. 如果inode被空闲,即文件被删除,inode的分配多半不会按照第二条执行,而是优先将inode number较小的inode分配给文件(inode number是结构inode的一个成员,是文件的唯一标识符,用于区分文件)
    4. 块的编号也是跨组的,一样不跨分区编号
    5. 所有组的大小是相同的,这是为了方便快速找到文件的属性和内容
8.3.4 "组"与"块"与内存
  • 我们知道,操作系统能够控制的最小空间单位是"块",那么如果我想获取一个文件的属性,是不是就需要把连续的32个inode整个加载进内存呢?当然要!至于加载进内存之后用的文件属性相关的数据,我们能用多少就用多少,不用的就浪费了
  • 当然,多数情况下,一个目录下的文件的inode的编号多半是连续的,而组中的块也是连续的,所以哪怕是加载了一整个块的文件的属性,多数情况下,也不会仅仅只用一个文件的属性,多半情况下其他文件也需要用,所以我们一并加载,就节省了CPU开销,当然,这只是期望的情况
8.3.5 GDT
  • GDT即"Group Descriptor Table","块组描述符表"

  • 这个区域中的内容用于描述和管理当前组的区域,相当于当前组的属性

偏移量字段大小用途
0x00Block Bitmap地址4字节用于记录Block Bitmap存储的块的位置
0x04inode Bitmap地址4字节用于记录inode Bitmap存储的块的位置
0x08inode Table起始地址4字节用于记录inode Table的起始块
0x0C空闲块数2字节用于记录空闲的块的数量
0x0E空闲inode2字节用于记录inode Table空闲的inode
0x10目录数量2字节记录目录的数量
0x12保留字段14字节预留空间,用于未来可能的扩展
  • 既然说GDT是用于描述组的属性,同时也是一个结构体,意味着我们就可以将管理组转化为管理GDT,我们的计算机开机时,就会将每个组的GDT加载进内存,而GDT自身就是"先描述再组织"的产物,并统一进行管理

  • GDT是一张全局的表!

  • 这里有一个误区:GDT描述的不是当前组的情况,GDT可以看作一个表,每一行都描述着某一个块组的情况,而块组中存放的是这一整个表而不是表中描述自身块组的那一行!

  • 为什么每个块组都需要有一张完整的GDT表?这是出于数据安全的考虑,防止因为GDT表丢失而导致数据丢失(因为GDT表是用来管理所有块组的,所以可不能丢,就需要做足了备份),但注意:ext2文件系统的GDT表确实是每个块组都有的,但到了ext4文件系统,GDT表就不是所有块组都有了,具体其实是使用了Super Block的一个机制,我们到下一小节就明白了

  • 同时,因为分区的块组数量是固定的,所以GDT表中的行是固定的,所以GDT的大小其实也是固定的!

  • 我们可以来看一下源码

struct ext2_group_desc
{
	__le32	bg_block_bitmap;		/* Blocks bitmap block */
	__le32	bg_inode_bitmap;		/* Inodes bitmap block */
	__le32	bg_inode_table;		/* Inodes table block */
	__le16	bg_free_blocks_count;	/* Free blocks count */
	__le16	bg_free_inodes_count;	/* Free inodes count */
	__le16	bg_used_dirs_count;	/* Directories count */
    //下面这俩都是保留字段
	__le16	bg_pad;
	__le32	bg_reserved[3];
};
8.3.6 Super Block
  • Super Block,即"超级块"

  • Super Block用于描述当前分区的属性,或者说用来管理分区

  • 这个区域其实是相当特殊的,因为它并不在所有的组中储存

  • 我们知道,一个分区是可以相当大的,你甚至可以买一块企业级氦气盘,最大的甚至有14T的容量,假设我们只为该盘分一个分区,也就是说一个分区的大小就有整整14T,如果此时描述一个分区,管理一个分区的结构出了问题,是不是这整个分区就全部挂了???是不是就全部丢失了??!!

  • 所以因为分区可以非常大,假设Super Block只有一个,存放在分区的最前头或者是最后头什么的,一旦Super Block丢失了,可能就会造成以T为单位的数据的丢失!所以不得懈怠,我们需要为Super Block进行备份!所以会出现一个分区含有多个Super Block的情况,目的是为了数据安全!

  • 而并不是所有组都会存储Super Block,而是以"稀疏超级块策略"进行存储,大抵规则是,只有编号是2的倍数的组才会存储Super block,剩余的空间会被其他区域利用,这个策略同样也用在了ext4文件系统的GDT

  • 当然,并不是说ext2Super Block就没有"稀疏超级块策略"了,ext2一样有这个策略

  • 源码鉴赏

struct super_block {
	struct list_head	s_list;		/* Keep this first */
	dev_t			s_dev;		/* search index; _not_ kdev_t */
	unsigned long		s_blocksize;
	unsigned char		s_blocksize_bits;
	unsigned char		s_dirt;
	unsigned long long	s_maxbytes;	/* Max file size */
	struct file_system_type	*s_type;
	struct super_operations	*s_op;
	struct dquot_operations	*dq_op;
 	struct quotactl_ops	*s_qcop;
	struct export_operations *s_export_op;
	unsigned long		s_flags;
	unsigned long		s_magic;
	struct dentry		*s_root;
	struct rw_semaphore	s_umount;
	struct mutex		s_lock;
	int			s_count;
	int			s_syncing;
	int			s_need_sync_fs;
	atomic_t		s_active;
	void                    *s_security;
	struct xattr_handler	**s_xattr;

	struct list_head	s_inodes;	/* all inodes */
	struct list_head	s_dirty;	/* dirty inodes */
	struct list_head	s_io;		/* parked for writeback */
	struct hlist_head	s_anon;		/* anonymous dentries for (nfs) exporting */
	struct list_head	s_files;

	struct block_device	*s_bdev;
	struct list_head	s_instances;
	struct quota_info	s_dquot;	/* Diskquota specific options */

	int			s_frozen;
	wait_queue_head_t	s_wait_unfrozen;

	char s_id[32];				/* Informational name */

	void 			*s_fs_info;	/* Filesystem private info */

	/*
	 * The next field is for VFS *only*. No filesystems have any business
	 * even looking at it. You had been warned.
	 */
	struct mutex s_vfs_rename_mutex;	/* Kludge */

	/* Granularity of c/m/atime in ns.
	   Cannot be worse than a second */
	u32		   s_time_gran;
};
8.2.7 如何找到一个文件
  • 在文件系统ext4中,一个块组可以分配8192inode,意味着一个块组可以存放8192个文件,并且,因为inode number拥有其唯一性,所以0号块组的inode编号区间是0~8191,1号块组的编号是8192~16383,以此类推

  • 而在超级块中存在一个指针*s_fs_info,它指向一个结构struct ext4_sb_info,这个结构用于描述一个块的相关信息,其中就包括规定好的:一个组块中包含的inode总数s_inodes_per_group

  • 我们可以先看看老版本的ext4_sb_info,也就是ext2_sb_info

struct ext2_sb_info {
	unsigned long s_frag_size;	/* Size of a fragment in bytes */
	unsigned long s_frags_per_block;/* Number of fragments per block */
	unsigned long s_inodes_per_block;/* Number of inodes per block */
	unsigned long s_frags_per_group;/* Number of fragments in a group */
	unsigned long s_blocks_per_group;/* Number of blocks in a group */
	unsigned long s_inodes_per_group;/* Number of inodes in a group */
	unsigned long s_itb_per_group;	/* Number of inode table blocks per group */
	unsigned long s_gdb_count;	/* Number of group descriptor blocks */
	unsigned long s_desc_per_block;	/* Number of group descriptors per block */
	unsigned long s_groups_count;	/* Number of groups in the fs */
	struct buffer_head * s_sbh;	/* Buffer containing the super block */
	struct ext2_super_block * s_es;	/* Pointer to the super block in the buffer */
	struct buffer_head ** s_group_desc;
	unsigned long  s_mount_opt;
	uid_t s_resuid;
	gid_t s_resgid;
	unsigned short s_mount_state;
	unsigned short s_pad;
	int s_addr_per_block_bits;
	int s_desc_per_block_bits;
	int s_inode_size;
	int s_first_ino;
	spinlock_t s_next_gen_lock;
	u32 s_next_generation;
	unsigned long s_dir_count;
	u8 *s_debts;
	struct percpu_counter s_freeblocks_counter;
	struct percpu_counter s_freeinodes_counter;
	struct percpu_counter s_dirs_counter;
	struct blockgroup_lock s_blockgroup_lock;
};
  • 于是,我们一旦知道了一个文件的inode number,我们就知道了它存在那个块组中,即块组号 = inode number // 单个块组包含的inode总数

  • 然后我们还可以计算出该inode number对应的文件属性在inode Table中的位置偏移量 = inode number % 单个块组包含的inode总数,接着我们就可以根据偏移量找到该文件的属性

  • 每个文件属性中一定包含这个文件在该块组的位置,我们就可以直接找到该文件的内容(我们还会再提到的)

8.2.8 格式化
  • 所以,文件系统就是一堆用于管理文件及其目录关系,描述文件和目录结构的一堆数据结构,所以对于分区的格式化,就是初始化该分区的文件系统,也就是初始化这一堆数据结构,使得用户在格式化之后,就可以直接使用该分区!
  • 更加深入一些,如果我们将分区看作一个对象,那么格式化就相当于初始化该对象,或者说调用该对象的构造函数,又或者说调用该对象的初始化方法Init()!!!

8.4 目录的内容与管理与路径解析

  • 有几个疑点

  • 其一,在上一小节中,我们谈论的关于"如何找到一个文件"的小节中,我们谈论到找到一个文件靠的是inode number,但事实是,我们找一个文件时,几乎从来不用inode number,而是使用文件名

  • 其二,很久以前我们就说过,目录也是文件,在以下命令中,我们可以使用选项-i显示文件/目录的inode number,既然目录的本质也是文件,那么目录存放在哪里?

$ ls -li
total 24
1315238 drwxrwxr-x 2 oldking oldking 4096 Mar  1 23:54 code_25_3_1
1971434 drwxrwxr-x 2 oldking oldking 4096 Mar 10 16:06 code_25_3_10
1845720 drwxrwxr-x 2 oldking oldking 4096 Mar  4 01:43 code_25_3_3
1845725 drwxrwxr-x 2 oldking oldking 4096 Mar  8 16:52 code_25_3_5
1971430 drwxrwxr-x 2 oldking oldking 4096 Mar  8 16:46 code_25_3_8
1845728 drwxrwxr-x 2 oldking oldking 4096 Mar  8 18:03 my_stdio
1845737 -rw-rw-r-- 1 oldking oldking    0 Mar 11 10:26 test
  • 其三,文件=属性+内容,目录也有自己的内容吗??

  • 事实上,目录存放的位置,跟普通文件一模一样,都是当成普通文件存放的,属性依旧是inode Table,内容依旧是Data Block

  • 而目录中存放的内容是inode number和文件名的映射关系

  • 当我们要打开一个文件时,要么我们处于当前工作目录中,我们可以直接从当前目录的内容中找到文件名和inode number的映射关系,要么我们使用"路径+文件名"的组合同样也可以从目录中一一深入查找到该文件的inode number(即路径解析,路径解析都是从根目录开始解析的,因为目录也是文件,目录名和其inode number的映射关系也会保存在上级目录中,于是我们就可以通过绝对路径,从根目录开始一一向需要找的文件解析)

  • 同时,我们怎么知道一个"文件"到底是目录还是普通文件呢??

  • 这点我们从文件属性就能识别,inode中由一个i_mode成员,用于描述文件的类型和文件的权限

  • 一个进程想要打开一个文件,它怎么找到这个文件,有绝对路径还好说,如果是在相对路径下,就会去找当前进程的环境变量pwd,而当前进程的环境变量又是从bash来的,bash的环境变量又是从操作系统来的

  • 所以,原则上说,我们找到一个文件,就需要从根目录开始进行路径解析,配合用户提供的文件名才可以找到该文件的inode number

8.5 回顾:如何找到一个文件

  • 两种情况:

    1. 用户提供文件名,进程提供当前工作目录
    2. 用户提供绝对路径,包含路径和文件名
  • 此时我们通过目录的层层解析(路径解析)得到文件名和inode number的映射关系,同时因为我们也已知文件名,此时就可以拿到该文件的inode number,于是我们就可以通过整除和取模得到该文件存放的块组和其inodeinode Table的偏移量,就可以找到文件的属性,在通过文件属性就可以找到文件在Data Block存放的文件内容!!!

8.6 路径缓存

  • 我们每次查找文件的时候,都需要从根目录开始路径解析吗?每次做路径解析不就是对磁盘硬件进行IO吗??那对硬件IO的话,不会导致查找效率很低下吗??

  • 所以我们设计了一个缓存机制,将每次解析的路径信息以树形结构保存进内存中,内存与CPU非常近,一旦将路径信息缓存,下次再查找文件时就可以直接访问缓存,不需要一次次解析路径,此时效率就会大大提高,以下是扒出来的多叉树的节点定义的源码

struct dentry {
	atomic_t d_count;
	unsigned int d_flags;		/* protected by d_lock */
	spinlock_t d_lock;		/* per dentry lock */

    //d_inode用于记录该节点保存指向目录/文件的inode指针
	struct inode *d_inode;		/* Where the name belongs to - NULL is
					 * negative */
	/*
	 * The next three fields are touched by __d_lookup.  Place them here
	 * so they all fit in a cache line.
	 */
	struct hlist_node d_hash;	/* lookup hash list */
	
    //d_parent则保存父节点的指针
    struct dentry *d_parent;	/* parent directory */
	
    //文件名,这个结构是用于存放文件名的,存放较长文件名的话,会开辟空间在堆中
    struct qstr d_name;

    //unsigned char *name 有可能指向堆开辟的空间,也有可能指向下面的d_iname,取决于文件名长度
    //struct qstr {
	//unsigned int hash;
	//unsigned int len;
	//const unsigned char *name;
    //};


    //用于链接进其他数据结构,同时LRU list是一个链表,具体来说时使用了LRU算法的链表(Lru即"Least Recently Used",即最近最少使用算法),机制是控制链表的长度,使用次数最少的节点将会被释放淘汰,用于保证该缓存多叉树不会无限增大
	struct list_head d_lru;		/* LRU list */
	/*
	 * d_child and d_rcu can share memory
	 */
	union {
		struct list_head d_child;	/* child of parent list */
	 	struct rcu_head d_rcu;
	} d_u;
	struct list_head d_subdirs;	/* our children */
	struct list_head d_alias;	/* inode alias list */
	unsigned long d_time;		/* used by d_revalidate */
	struct dentry_operations *d_op;
	struct super_block *d_sb;	/* The root of the dentry tree */
	void *d_fsdata;			/* fs-specific data */
#ifdef CONFIG_PROFILING
	struct dcookie_struct *d_cookie; /* cookie, if any */
#endif
	int d_mounted;

    //这个数组用于存放较短文件名,被d_name指向
	unsigned char d_iname[DNAME_INLINE_LEN_MIN];	/* small names */
};

8.7 挂载分区

  • 至此,还有一个疑问我们还没有解决,即我们如何找到一个分区??

  • 因为inode number并不互通,我们必须在某个分区中查找文件才能找到正确的文件

  • 那么怎么做到正确访问分区的呢?

  • 在Linux采用了挂载分区的做法,即:将分区与目录绑定,即分区与目录挂载,这样只要有正确的路径,就可以正确地找到分区

  • 该命令可以查询当前操作系统挂载分区的情况

$ df -h
Filesystem      Size  Used Avail Use% Mounted on
devtmpfs        868M     0  868M   0% /dev
tmpfs           879M     0  879M   0% /dev/shm
tmpfs           879M  668K  878M   1% /run
tmpfs           879M     0  879M   0% /sys/fs/cgroup
/dev/vda1        50G   16G   32G  33% /
tmpfs           176M     0  176M   0% /run/user/1000
tmpfs           176M     0  176M   0% /run/user/1001
  • 当然,其他的咱们不管,/dev/vda1就非常值得注意一下了
$ ls -il vda1
9423 brw-rw---- 1 root disk 253, 1 Oct 10 21:54 vda1
$ pwd
/dev
  • 这个文件我们应该很久以前就见过了,该文件类型是"块文件",系统允许"块文件"直接物理寻址,是设备文件,即分区

  • 但实际上这个文件不能直接打开,也不能直接访问,而是需要和其他目录绑定才可以访问,这样我们只需要知道文件的路径,然后根据文件路径的前缀,就知道文件在哪个分区了

  • 例如/home/oldking/code/test这个文件,因为根目录//dev/vda1挂载,所以我们就知道该文件其实存放在第1号分区中了(vda1的末尾数字代表分区号)

8.8 操作系统级inode与文件系统级inode

  • 实际上,操作系统的inode和文件系统的inode是分开的,或者说,当我们查询一个文件的时候,这个文件存放在硬盘的属性所在的结构和这个文件的属性被加载(拷贝)进内存之后所在的结构不太一样

  • 这是因为一个操作系统中很可能有好几个不同的文件系统,如果共用一套文件系统的inode结构,会导致耦合度过高,到时候如果想更换文件系统就会很麻烦,或者说想实现一个操作系统拥有好几个不同的文件系统就会很麻烦

  • 所以在拷贝属性进内存之后,不论文件系统是什么,所有的属性都会统一存放在同一类型的inode结构中,即我们之前看的struct inode源码

  • 而在文件系统中存在的inode则是ext2_inode_info(以ext2举例)

struct ext2_inode_info {
	__le32	i_data[15];
	__u32	i_flags;
	__u32	i_faddr;
	__u8	i_frag_no;
	__u8	i_frag_size;
	__u16	i_state;
	__u32	i_file_acl;
	__u32	i_dir_acl;
	__u32	i_dtime;

	/*
	 * i_block_group is the number of the block group which contains
	 * this file's inode.  Constant across the lifetime of the inode,
	 * it is ued for making block allocation decisions - we try to
	 * place a file's data blocks near its inode block, and new inodes
	 * near to their parent directory's inode.
	 */
	__u32	i_block_group;

	/*
	 * i_next_alloc_block is the logical (file-relative) number of the
	 * most-recently-allocated block in this file.  Yes, it is misnamed.
	 * We use this for detecting linearly ascending allocation requests.
	 */
	__u32	i_next_alloc_block;

	/*
	 * i_next_alloc_goal is the *physical* companion to i_next_alloc_block.
	 * it the the physical block number of the block which was most-recently
	 * allocated to this file.  This give us the goal (target) for the next
	 * allocation when we detect linearly ascending requests.
	 */
	__u32	i_next_alloc_goal;
	__u32	i_prealloc_block;
	__u32	i_prealloc_count;
	__u32	i_dir_start_lookup;
#ifdef CONFIG_EXT2_FS_XATTR
	/*
	 * Extended attributes can be read independently of the main file
	 * data. Taking i_mutex even when reading would cause contention
	 * between readers of EAs and writers of regular file data, so
	 * instead we synchronize on xattr_sem when reading or changing
	 * EAs.
	 */
	struct rw_semaphore xattr_sem;
#endif
#ifdef CONFIG_EXT2_FS_POSIX_ACL
	struct posix_acl	*i_acl;
	struct posix_acl	*i_default_acl;
#endif
	rwlock_t i_meta_lock;
	struct inode	vfs_inode;
};

8.9 inodeData Block的映射关系

  • 既然一个inode节点结构的大小是有限的,假设一个文件的大小高达20G,那它是怎样索引到这20G的块的呢?

  • 在了解具体方式之前,我们先看一下具体是那个成员用于索引在Data Block的文件的

struct ext2_inode_info {
    //...
	__le32	i_data[15]; //其实就是这个数组
    //...
}
  • 其中,前12个元素全部会直接索引(指向)数据块,从第13个元素开始,就不一样了

  • 第13个元素并不直接指向数据块,而是指向一个大小为一个块的索引表,我们知道一个块有4kb,而一个指针只有4bit,我们称第13个元素指向的为一级间接块,这个块里头会存放1024个指针,此时我们就可以索引高达1024 + 12 = 1036个数据块了

  • 而第14个元素依然指向一个一级间接块,但不同的是,这个一级间接块的所有指针全部指向二级间接块,意味着通过第14个元素,我们就可以索引1024 * 1024个数据块

  • 第15个元素也以此类推,可以索引1024 * 1024 * 1024个数据块

  • 经过计算,我们可以保存的最大文件大小就在4TB左右

8.10 文件大小问题

  • 那既然文件大小可以达到4TB,这么大的文件一个块组怎么装得下??

  • 其实也很简单,我们知道,inode是可以在整个分区中都是独立的,所以其实我们可以让inode分块组映射文件属性和内容

8.11 软链接与硬链接

8.11.1 软链接
  • 我们先来看现象
  • 首先我写了一个打印程序
#include<stdio.h>

int main()
{
    printf("hello soft link\n");

    return 0;
}
  • 然后我们简单编译并且挪动一下可执行程序的位置
$ ll
total 20
-rwxrwxr-x 1 oldking oldking 8360 Mar 11 22:13 a.out
drwxrwxr-x 2 oldking oldking 4096 Mar 11 22:11 dir
-rw-rw-r-- 1 oldking oldking   88 Mar 11 22:12 test.c
$ mv a.out ./dir/
$ ls
dir  test.c
$ ll dir
total 12
-rwxrwxr-x 1 oldking oldking 8360 Mar 11 22:13 a.out
  • 接着我们创建一下这个可执行程序的软链接
$ ln -s ./dir/a.out exe
$ ll -i
total 8
1971475 drwxrwxr-x 2 oldking oldking 4096 Mar 11 22:14 dir
1971481 lrwxrwxrwx 1 oldking oldking   11 Mar 11 22:16 exe -> ./dir/a.out
1971478 -rw-rw-r-- 1 oldking oldking   88 Mar 11 22:12 test.c

$ ll -i dir
total 12
1971480 -rwxrwxr-x 1 oldking oldking 8360 Mar 11 22:13 a.out
  • 我们可以看到,这个软链接形成的文件是一个l类型的文件,即链接文件

  • 那么这个exe到底有什么用呢?

$ ./exe
hello soft link
  • 你会发现他居然可以像a.out一样被执行,同时它所占据的空间远远小于a.out所占据的空间

  • 其实,我们早就见过这个东西了,它其实就是我们在Windows中用的快捷方式,有些时候一个大型项目的可执行程序藏得比较深,我们就可以用这个来更方便地打开可执行程序

  • 其次,这个文件跟Windows一样,也是一个独立的文件,你单独删掉它肯定是删不了整个程序的!这点从它的inode number就可以看出来

  • 那么这个文件中到底存了什么内容呢?

  • 存的其实就是目标文件的地址

image-19.png

  • 那么如何取消一个软链接呢?
$ unlink exe
  • 当然你也可以直接删除软链接文件
8.11.2 硬链接
  • 接着我们看看硬链接
$ ln dir/a.out exe
$ ls
dir  exe  test.c
  • 为了观察方便,我们将dir中的a.out放回当前目录
$ ll -i
total 32
1971480 -rwxrwxr-x 2 oldking oldking 8360 Mar 11 22:13 a.out
1971475 drwxrwxr-x 2 oldking oldking 4096 Mar 11 23:05 dir
1971480 -rwxrwxr-x 2 oldking oldking 8360 Mar 11 22:13 exe
1971478 -rw-rw-r-- 1 oldking oldking   88 Mar 11 22:12 test.c
  • 你发现了什么有趣的现象?

  • 这个形成的链接文件的inode number竟然和目标文件一样?!

  • 从本质上说,硬链接形成的文件其实就是在当前目录的内容中创建新的映射关系,即有一组映射关系的inode一样,但文件名不一样

  • 是不是有点像CPP的引用的那种玩意!!?

  • 并且细心的你一定发现了,属性后面有一个数字跟着变了,正常文件(像是test.c)都是1,为什么exea.out2呢?

  • 答案就是,这个数字是link_count作为链接计数用的

  • 那硬链接有什么用呢?

  • 其一,我们删掉一个链接文件,或者是源文件,其属性和内容并不会被删除,除非link_count变为0,意味着我们可以通过这种方式更加方便地备份文件了

$ ll -i
total 32
1971480 -rwxrwxr-x 2 oldking oldking 8360 Mar 11 22:13 a.out
1971475 drwxrwxr-x 2 oldking oldking 4096 Mar 11 23:05 dir
1971480 -rwxrwxr-x 2 oldking oldking 8360 Mar 11 22:13 exe
1971478 -rw-rw-r-- 1 oldking oldking   88 Mar 11 22:12 test.c
$ rm a.out
$ ll -i
total 20
1971475 drwxrwxr-x 2 oldking oldking 4096 Mar 11 23:05 dir
1971480 -rwxrwxr-x 1 oldking oldking 8360 Mar 11 22:13 exe
1971478 -rw-rw-r-- 1 oldking oldking   88 Mar 11 22:12 test.c
$ ./exe
hello soft link
  • 其二,它还可以实现一个我们经常见到的东西
$ ll -i
total 20
1971475 drwxrwxr-x 2 oldking oldking 4096 Mar 11 23:05 dir
1971480 -rwxrwxr-x 1 oldking oldking 8360 Mar 11 22:13 exe
1971478 -rw-rw-r-- 1 oldking oldking   88 Mar 11 22:12 test.c
$ cd dir
$ ll -ai
total 8
1971475 drwxrwxr-x 2 oldking oldking 4096 Mar 11 23:05 .
1971449 drwxrwxr-x 3 oldking oldking 4096 Mar 11 23:13 ..
  • 你发现了么,dir中的目录.inode number竟然和dirinode number一模一样!!
  • 这不就是硬链接吗?!!!
8.11.3 软链接和硬链接的区别
软链接硬链接
软链接形成的文件是一个实际存在的文件,拥有自己的属性和内容硬链接本质上不是不是文件,而是新增了inode和文件名的映射关系

  • 如有问题或者有想分享的见解欢迎留言讨论