8 文件系统
-
上一章节我们了解了基础IO的原理和方式,针对的其实是内存级文件,也就是对一个被打开的文件进行IO
-
而我们知道,计算机中还有没有打开的文件,这些文件一样需要被管理起来,于是就需要我们学习本章节内容,了解文件系统的构造和原理,以及它的职责
8.1 机械硬盘原理
8.1.1 机械硬盘的各个组件
-
这里推荐两个视频,强烈建议先看看:
-
[机械硬盘原理]
-
[机械硬盘的发展史]
-
简单来说:机械硬盘的每个盘面由内至外被划分为一个个圆环,每个圆环又被分割为一个个类似于扇形的扇面,每个扇面包含很多个单位,每个单位都相当于一个可以磁化的小金属块,这个小金属块的磁极上下(磁极上下是比较新的技术,老技术是以左右形式)都代表着
0/1两种值,一个扇区的大小因为技术更迭而可能不同,旧技术下的扇区大小为512字节,新技术下的扇区大小一般为4kb
- 机械硬盘拥有若干个光盘
- 机械硬盘的每个光盘称为盘片
- 每个盘片拥有正反两个盘面
- 每个盘面拥有若干个磁道,也就是被划分的一个个圆环
- 每个圆环被划分为若干个扇区
- 每个盘面都会有一个磁头
- 每个盘片都会有正反一对磁头
- 所有磁头的运动都是同步的
- 所有盘片的旋转也是同步的
- 盘片旋转的速度非常快,[甚至还有人将机械硬盘的盘片电机改装成电动打磨机的]
- 磁头通过伸出的幅度(或者说角度)来定位磁道
- 所有盘面的同一个位置的磁道的集合称为柱面(可以想象到,就是一个圆环柱体)
- 可以预见的是,机械硬盘的精度很高,[所以一旦拆了机械硬盘,这个硬盘就废了]
8.1.2 机械硬盘的寻址方式
8.1.2.1 CHS寻址
-
所以不难发现,机械硬盘的寻址包含三个步骤
- 柱面:即寻找地址所在的柱面,此时所有磁头都会伸出并定位到对应位置,就是定位磁道位置
- 磁头:因为磁头数量有非常多,所以需要找到对应的磁头,即找到地址所在的盘面,或者说找到这个磁道集合中对应的磁道
- 扇区:即在找到的磁道中找到对应扇区,寻找扇区靠的是机械硬盘盘片的高速旋转
-
所以该种方式被称为
CHS寻址,即Cylinder-Head-Sector寻址,"柱面-磁头-扇区" -
所以我们仔细想想,每个柱面是不是都包含两个地址,即在一个柱面中,使用"磁头"和"扇区"的组合就可以找到该柱面中的扇区
-
所以再仔细想想,硬盘包含多个柱面,所以从逻辑上来看,磁盘就是一个三维数组咯!
8.1.2.2 LBA寻址
-
在OS看来,所有的三维数组都是由一维数组组成的,在OS的角度,磁盘的地址是纯线性的,即将三维数组全部拆开并成一个一维数组,这就是
LBA寻址的机制,即Logical Block Addressing寻址,逻辑块寻址 -
操作系统只认识
LBA地址
8.1.2.3 CHS地址和LBA地址间的转换
-
因为操作系统只认识
LBA地址,但磁盘的物理设计却只能使用CHS地址,所以我们就需要将两个地址互相转化才可以正确寻址 -
这俩地址的互相转化是磁盘来帮助我们计算的
-
这里我们需要明确几个概念:
LBA地址是从0开始的CHS地址中的柱面号是从0开始的CHS地址中的磁头号是从0开始的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字节
-
所以什么是文件系统,就是以上用来管理文件的各种结构,或者说管理文件的方式,不同文件系统管理文件的方式在底层也会有些不同
-
并且我们离文件系统其实也非常近,如果你格式化过硬盘你就应该知道,格式化硬盘的时候我们需要选择硬盘的格式,诸如
NTFS,FAT32,exfat这些,其实这些就是文件系统,以下是常见的文件系统:FAT系列:包括FAT12,FAT16,FAT32,是主要用于U盘的文件系统,也算是最早期的文件系统之一,缺点是最大文件大小比较小,FAT32只有4GB的最大文件大小,所以像是在U盘中拷贝高视频规格的电影就不适合这种文件系统NTFS:一般用于Windows系统的文件系统,是Windows的标准文件系统,对于Linux的适配其实比较差,所以一般Linux不用NTFSext4:是Linux的常用文件系统,主要用于高性能服务器领域
-
这些文件系统在某些地方可能会有一些差别,例如文件属性的大小设定,组的大小等等,但大体上的设计都是差不多的
8.3 "组"的内容细谈
8.3.1 "组"的各个区域
- 从图中,我们已经可知"组"分为以下区域,下面我们详细解释一下各个区域的作用,并解决一些问题:
Data Block:基本单位是块,所占空间是组里最大的,用于存放文件内容inode Table:基本单位也是块,用于存放文件的属性,inode Table本质上是一张表(或者说我们抽象成一张表,本质是由一个个结构体节点组成的数据结构),一个块可能会有上千个文件,所以这张表可能也会有上千行,每行都是某个文件的所有属性,因为文件的属性的字长一定是固定的,所以每一个行的大小一定是,规定是128字节,意味着一个块可以存放32个行(或者说节点)inode Bitmap:一个位图,用于判断inode Table中还有没有空余位置,同时我们也可以通过位图找到文件的属性(具体找的方式我们后面会提到的),一个位对应一个inode,所以inode Bitmap的大小也是固定的Block Bitmap:一个位图,用于判断Data Block中的空余位置,同时我们也可以通过位图找到文件的内容GDT:用于描述当前组的各个区域,并对当前组的各个区域做管理,我们后面也会谈到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 一些规定
- 再次我们需要暂时了解一些规定,了解这些是为了后面的内容做一些铺垫:
inode的中编号是跨组的编号的,但并不是跨分区编号的- 在文件总量不变的情况下,先创建的文件
inode中的编号会更小,后创建的更大,也就是按创建顺序排列 - 如果
inode被空闲,即文件被删除,inode的分配多半不会按照第二条执行,而是优先将inode number较小的inode分配给文件(inode number是结构inode的一个成员,是文件的唯一标识符,用于区分文件) - 块的编号也是跨组的,一样不跨分区编号
- 所有组的大小是相同的,这是为了方便快速找到文件的属性和内容
8.3.4 "组"与"块"与内存
- 我们知道,操作系统能够控制的最小空间单位是"块",那么如果我想获取一个文件的属性,是不是就需要把连续的32个
inode整个加载进内存呢?当然要!至于加载进内存之后用的文件属性相关的数据,我们能用多少就用多少,不用的就浪费了 - 当然,多数情况下,一个目录下的文件的
inode的编号多半是连续的,而组中的块也是连续的,所以哪怕是加载了一整个块的文件的属性,多数情况下,也不会仅仅只用一个文件的属性,多半情况下其他文件也需要用,所以我们一并加载,就节省了CPU开销,当然,这只是期望的情况
8.3.5 GDT
-
GDT即"Group Descriptor Table","块组描述符表" -
这个区域中的内容用于描述和管理当前组的区域,相当于当前组的属性
| 偏移量 | 字段 | 大小 | 用途 |
|---|---|---|---|
| 0x00 | Block Bitmap地址 | 4字节 | 用于记录Block Bitmap存储的块的位置 |
| 0x04 | inode Bitmap地址 | 4字节 | 用于记录inode Bitmap存储的块的位置 |
| 0x08 | inode Table起始地址 | 4字节 | 用于记录inode Table的起始块 |
| 0x0C | 空闲块数 | 2字节 | 用于记录空闲的块的数量 |
| 0x0E | 空闲inode数 | 2字节 | 用于记录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上 -
当然,并不是说
ext2的Super 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中,一个块组可以分配8192个inode,意味着一个块组可以存放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 回顾:如何找到一个文件
-
两种情况:
- 用户提供文件名,进程提供当前工作目录
- 用户提供绝对路径,包含路径和文件名
-
此时我们通过目录的层层解析(路径解析)得到文件名和
inode number的映射关系,同时因为我们也已知文件名,此时就可以拿到该文件的inode number,于是我们就可以通过整除和取模得到该文件存放的块组和其inode在inode 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 inode和Data 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就可以看出来 -
那么这个文件中到底存了什么内容呢?
-
存的其实就是目标文件的地址
- 那么如何取消一个软链接呢?
$ 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,为什么exe和a.out是2呢? -
答案就是,这个数字是
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竟然和dir的inode number一模一样!! - 这不就是硬链接吗?!!!
8.11.3 软链接和硬链接的区别
| 软链接 | 硬链接 |
|---|---|
| 软链接形成的文件是一个实际存在的文件,拥有自己的属性和内容 | 硬链接本质上不是不是文件,而是新增了inode和文件名的映射关系 |
- 如有问题或者有想分享的见解欢迎留言讨论