目录

1.理解硬件

1.1 磁盘、服务器、机柜、机房

1.2 磁盘的物理结构

1.3 磁盘的存储结构

1.4 磁盘的逻辑结构

1.4.1 理解过程

1.4.2 真实过程

1.5 CHS && LBA地址

2. 引入文件系统

2.1 引入 "块" 概念

2.2 引入 "分区" 概念

2.3 引入 "inode" 概念

3. ext2 文件系统

3.1 宏观认识

3.2 Block Group

3.3 块组内部结构

3.3.1 超级块 (Super Block)

3.3.2 GDT (Group Descriptor Table)

3.3.3 块位图(Block Bitmap)

3.3.4 inode位图(Inode Bitmap)

3.3.5 i节点表(Inode Table)

3.3.6 Data block

​编辑

3.4 inode 和 Datablock的映射

3.5 目录与文件名

3.6 路径缓存

3.7 挂载分区

3.7.1 一个实验

3.7.2 一个结论:

3.8 文件系统总结

4. 软硬连接

4.1 软链接

4.2 硬链接

4.3 软硬链接的区别

4.3 软硬链接的应用场景(重点)


1.理解硬件

1.1 磁盘、服务器、机柜、机房

小公司也要用网呀!也要有自己的网站,所以有了大公司提供的云服务!!

磁盘如何存储数据??

计算机只认识二进制,有可能用电信号的有无来表示0、1,在传输数据时,有可能用电信号的高低电频来表示0、1,在磁盘中,为什么叫磁盘?简单的理解:计算机只认识二进制,往磁盘中存储的数据都是二进制的,但是二进制也是我们加工处理之后的说法。什么又叫做二进制??磁盘的光滑的盘面是由上百亿个磁铁拼在一起,打磨光滑,叫做磁盘。规定磁铁南极为0,北极为1。在磁盘中写入1,在特定的小磁铁由南极变为北极,清0就由北极变为南极。用磁头修改磁盘上的数据就是更改磁盘中的某块南北极朝向的问题。

扔磁盘的时候必须将数据销毁了,才可以被淘汰。要保证数据安全。只是将磁盘的数据清空是不够的,厂家可以恢复出来,深度清理磁盘是通过火烧!!还有就是进行对磁盘整盘格式化,全写0或全写1。

1.2 磁盘的物理结构

磁盘在高速旋转的同时,磁头也是在左右来回摆动的,但是磁盘和磁头是没有进行任何的接触的!!!

1.3 磁盘的存储结构

扇区是OS访问磁盘设备的基本单位:一般为512字节!!磁盘被称为是块设备!!

60%的磁盘最内道的和最外道的磁道上的扇区个数是一样的。还有40%是新技术。本篇文章认为扇区个数是一样的,方便计算!!可以理解为最内道和最外道的疏密程度不一样,所以导致扇区个数一样。所以可以将高频访问的数据放在磁盘的内侧,数据更密集。访问的数据不怎么高频可以放在外侧。

一个盘片两面,所以上面的图是三盘六面。正反都有数据。

等下会谈到将存储结构进行一个抽象,不再采用CHS这样的寻址方式,这样的寻址方式太老了,要求OS做的工作太多了。

文件 = 内容 + 属性,都是数据,无非就是占据哪几个扇区的问题!所以能定位多个扇区!!!

  • 扇区是从磁盘读出和写入信息的最小单位,通常大小为 512 字节。
  • 磁头(head)数:每个盘片⼀般有上下两⾯,分别对应1个磁头,共2个磁头
  • 磁道(track)数:磁道是从盘片外圈往内圈编号0磁道,1磁道...,靠近主轴的同⼼圆⽤于停靠磁头,不存储数据
  • 柱⾯(cylinder)数:磁道构成柱⾯,数量上等同于磁道个数
  • 扇区(sector)数:每个磁道都被切分成很多扇形区域,每道的扇区数量相同
  • 圆盘(platter)数:就是盘片的数量
  • 磁盘容量=磁头数 × 磁道(柱⾯)数 × 每道扇区数 × 每扇区字节数
  • 细节:传动臂上的磁头是共进退的(这点比较重要,后面会说明)

柱面(cylinder),磁头(head),扇区(sector),显然可以定位数据了,这就是数据定位(寻址)方式之⼀,CHS寻址方式。

CHS寻址:


对早期的磁盘非常有效,知道用哪个磁头,读取哪个柱⾯上的第几扇区就可以读到数据了。但是CHS模式⽀持的硬盘容量有限,因为系统⽤8bit来存储磁头地址,用10bit来存储柱面地址址,用6bit来存储扇区地址,而⼀个扇区共有512Byte,这样使用CHS寻址⼀块硬盘最大容量为256 * 1024 * 63 * 512B = 8064 MB(1MB = 1048576B)(若按1MB=1000000B来算就是8.4GB)

1.4 磁盘的逻辑结构

1.4.1 理解过程

磁带:

磁带上面的磁带条就是存储数据的介质,我们可以把磁带 "拉直",形成线性结构

那么磁盘本质上虽然是硬质的,但是逻辑上我们可以把磁盘想象成为卷在一起的磁带,那么磁盘的逻辑存储结构我们也可以类似于:

当代磁盘用的都是LBA地址,给磁盘LBA地址,磁盘内部会将LBA地址转为CHS,将数据写到一个扇区上,只要能写到一个扇区上就能够写到100个扇区里。磁盘的每个扇区大小如果为512的话,OS开机的时候只需要识别到磁盘的总容量是多少,瞬间就能够计算出一共有多少个LBA,每一个扇区的LBA地址是多少?

1.4.2 真实过程

一个细节:传动臂上的磁头是共进退的

柱面是一个逻辑上的概念,其实就是每一面上,相同半径的磁道逻辑上构成柱面。

所以,磁盘物理上分了分多面(纵向的柱面),但是在我们看来,逻辑上,磁盘整体是由 “柱面”卷起来的。有点向山楂卷:

所以,磁盘的真实情况是:

磁道:

某一盘面的某一个磁道展开:

即:一维数组

柱面:

柱面上的每个磁道,扇区个数是一样的

这就是二维数组

磁盘:

磁盘,就是一个三维数组(sector array[ ][ ][ ])!!!所以定位磁盘上的任意一个扇区是通过3个下标,C,H,S  -> 三维数组的下标(顺序也是如此)!!!即:sector array[ 柱面 ][ 磁头 ][ 扇区 ]

即:

sector array[ C ][ H ][ S ]   !!!  

重要结论1:

磁盘的本质是一个三维数组!!!

将整个磁盘想象成一个线性结构,每一个扇区都有一个下标,对应的地址是LBA地址,其实就是线性地址!!!将LBA地址转化为CHS地址,本质就是将一维数组的下标,转换成为3个数字!!!

OS只需要使用LBA就可以了!!!LBA地址转换成CHS地址,CHS如何转换成为LBA地址,这个工作是磁盘自己来做的!

重要结论2:

磁盘,就是一个以sector为单位的一维数组!!!

1.5 CHS && LBA地址

LBA转成CHS:

  • 柱⾯号C = LBA // (磁头数*每磁道扇区数)【就是单个柱⾯的扇区总数
  • 磁头号H = (LBA % (磁头数*每磁道扇区数)) // 每磁道扇区数扇区号S = (LBA % 每磁道扇区数) + 1
  • "//": 表示除取整

CHS转成LBA:

  • 磁头数*每磁道扇区数 = 单个柱⾯的扇区总数
  • LBA = 柱⾯号C*单个柱⾯的扇区总数 + 磁头号H*每磁道扇区数 + 扇区号S - 1
  • 即:LBA = 柱面号C*(磁头数*每磁道扇区数) + 磁头号H*每磁道扇区数 + 扇区号S - 1
  • 扇区号通常是从1开始的,而在LBA中,地址是从0开始的柱面和磁道都是从0开始编号的
  • 总柱面,磁道个数,扇区总数等信息,在磁盘内部会⾃动维护,上层开机的时候,会获取到这些参数。

2. 引入文件系统

2.1 引入 "块" 概念

访问磁盘时的基本单位是512个字节,磁盘是典型的块设备,OS读取磁盘时,OS不会按照硬件的扇区个数为单位来读。

理由一:扇区512字节为单位太小了,磁盘和内存之间IO时效率较低

理由二:OS不想硬编码,不想每次读磁盘都读512字节,OS规定访问磁盘的时候,读取磁盘的时候,必须把磁盘划分为一个一个的块,一个个“块”是由格式化的时候决定的,并且不可以更改,最常见的大小是4KB,即连续8个扇区组成一个 "块","块"是文件存取的最小单位。

一般的文件系统和文件交互时都是以4KB为单位的。4KB就是8个扇区组成的一个 “块”。

注意:

  • 磁盘就是⼀个三维数组,我们把它看待成为⼀个"⼀维数组",数组下标就是LBA,每个元素都是扇区
  • 每个扇区都有LBA,那么8个扇区⼀个块,每⼀个块的地址我们也能算出来。
  • 知道LBA:块号 = LBA/8
  • 知道块号:LAB=块号*8 + n. (n是块内第几个扇区)

所以:

OS看待磁盘,就认为磁盘是一个块设备,每一个块都有下标!!!

文件系统的角度(以4KB为单位):磁盘当作 block array[N] ---> 磁盘是块设备 !!!

块号是由8个LBA地址构成的。磁盘被当作是一个块设备,由n个块构成的。磁盘总容量 / 4KB,就知道整个磁盘有多少数据块,然后拿着块号,想上找,找到LBA地址,再次向上找CHS,就可以访问到一个或多个扇区。

物理内存/外设,看起来是外设,但是OS和外设之间进行沟通的时候,也有他的寄存器,可以通过它所对应的寄存器,写入它的控制命令,地址,数据硬件电路自动会进行对设备的访问。外设比较慢尤其是磁盘,是因为数据寄存器、地址寄存器数量有点少,内部又是机械设备,进行IO的时候,很多时候都要等磁盘就绪,这就是进程发起IO后,进程会处于短暂的D状态。

2.2 引入 "分区" 概念

其实磁盘是可以被分成多个分区(partition)的,windows上C盘,D盘,其实是一块物理盘对应的分区!!!

柱面是分区的最小单位,柱面大小一致,扇区个位⼀致,那么其实只要知道每个分区的起始和结束柱面号,知道每⼀个柱面多少个扇区,那么该分区多大,其实和解释LBA是多少也就清楚了。

2.3 引入 "inode" 概念

之前我们说过 文件=数据+属性 ,我们使⽤ ls -l 的时候看到的除了看到文件名,还能看到文件元数据(属性)。

每行包含7列:

  • 模式
  • 硬链接数
  • 文件所有者
  • 大小
  • 最后修改时间
  • 文件名

其实这个信息除了通过这种⽅式来读取,还有⼀个stat命令能够看到更多信息

到这我们要思考⼀个问题,文件数据都储存在”块”中,那么很显然,我们还必须找到⼀个地方储存文件的元信息(属性信息),比如文件的创建者、⽂件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为”索引节点”。

所以⼀个文件的属性inode长什么样子呢?

/*
* Structure of an inode on the disk
*/
struct ext2_inode {
	__le16 i_mode; /* File mode */
	__le16 i_uid; /* Low 16 bits of Owner Uid */
	__le32 i_size; /* Size in bytes */
	__le32 i_atime; /* Access time */
	__le32 i_ctime; /* Creation time */
	__le32 i_mtime; /* Modification time */
	__le32 i_dtime; /* Deletion Time */
	__le16 i_gid; /* Low 16 bits of Group Id */
	__le16 i_links_count; /* Links count */
	__le32 i_blocks; /* Blocks count */
	__le32 i_flags; /* File flags */
	union {
		struct {
			__le32 l_i_reserved1;
		} linux1;
		struct {
			__le32 h_i_translator;
		} hurd1;
		struct {
			__le32 m_i_reserved1;
		} masix1;
	} osd1; /* OS dependent 1 */
	__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
	__le32 i_generation; /* File version (for NFS) */
	__le32 i_file_acl; /* File ACL */
	__le32 i_dir_acl; /* Directory ACL */
	__le32 i_faddr; /* Fragment address */
	union {
		struct {
			__u8 l_i_frag; /* Fragment number */
			__u8 l_i_fsize; /* Fragment size */
			__u16 i_pad1;
			__le16 l_i_uid_high; /* these 2 fields */
			__le16 l_i_gid_high; /* were reserved2[0] */
			__u32 l_i_reserved2;
		} linux2;
		struct {
			__u8 h_i_frag; /* Fragment number */
			__u8 h_i_fsize; /* Fragment size */
			__le16 h_i_mode_high;
			__le16 h_i_uid_high;
			__le16 h_i_gid_high;
			__le32 h_i_author;
		} hurd2;
		struct {
			__u8 m_i_frag; /* Fragment number */
			__u8 m_i_fsize; /* Fragment size */
			__u16 m_pad1;
			__u32 m_i_reserved2[2];
		} masix2;
	} osd2; /* OS dependent 2 */
};
/*
* Constants relative to the data blocks
*/
#define EXT2_NDIR_BLOCKS 12
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)
备注:EXT2_N_BLOCKS = 15

再次注意:

  • 文件名属性并未纳入到inode数据结构内部
  • inode的大小一般是128字节或者256,我们后面统⼀128字节
  • 任何文件的内容大小可以不同,但是属性大小⼀定是相同的

问题:

  1. 我们已经知道硬盘是典型的“块”设备,操作系统读取硬盘数据的时候,读取的基本单位是”块”。“块”又是硬盘的每个分区下的结构,难道“块”是随意的在分区上排布的吗?那要怎么找到“块”呢?还有就是上⾯提到的存储⽂件属性的inode,又是如何放置的呢?
  2. 文件系统就是为了组织管理这些的!!

3. ext2 文件系统

3.1 宏观认识

所有的准备⼯作都已经做完,是时候认识下文件系统了。我们想要在硬盘上储⽂件,必须先把硬盘格式化为某种格式的⽂件系统,才能存储⽂件。文件系统的目的就是组织和管理硬盘中的⽂件。在Linux 系统中,最常见的是 ext2 系列的⽂件系统。其早期版本为 ext2,后来又发展出 ext3 和 ext4。ext3 和 ext4 虽然对 ext2进行了增强,但是其核心设计并没有发生变化,我们仍是以较老的 ext2 作为演示对象。

ext2⽂件系统将整个分区划分成若干个同样大小的块组 (Block Group),如下图所示。只要能管理⼀个分区就能管理所有分区,也就能管理所有磁盘文件。

上图中启动块(Boot Block/Sector)的大小是确定的,为1KB,由PC标准规定,⽤来存储磁盘分区信息和启动信息,任何文件系统都不能修改启动块。启动块之后才是ext2文件系统的开始。

3.2 Block Group

ext2文件系统会根据分区的大小划分为数个Block Group。而每个Block Group都有着相同的结构组成。

3.3 块组内部结构

3.3.1 超级块 (Super Block)

存放文件系统本身的结构信息,描述整个分区的文件系统信息。记录的信息主要有:block 和 inode 的总量,未使用的 block 和 inode 的数量,一个block 和 inode 的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其它文件系统的相关信息。Super Block 的信息被破坏,可以说整个文件系统结构就被破坏了。

Super Block 描述的是整个文件系统所对应的宏观管理信息;GDT 管理一个组:

/*
* Structure of the super block
*/
struct ext2_super_block {
	__le32 s_inodes_count; /* Inodes count */
	__le32 s_blocks_count; /* Blocks count */
	__le32 s_r_blocks_count; /* Reserved blocks count */
	__le32 s_free_blocks_count; /* Free blocks count */
	__le32 s_free_inodes_count; /* Free inodes count */
	__le32 s_first_data_block; /* First Data Block */
	__le32 s_log_block_size; /* Block size */
	__le32 s_log_frag_size; /* Fragment size */
	__le32 s_blocks_per_group; /* # Blocks per group */
	__le32 s_frags_per_group; /* # Fragments per group */
	__le32 s_inodes_per_group; /* # Inodes per group */
	__le32 s_mtime; /* Mount time */
	__le32 s_wtime; /* Write time */
	__le16 s_mnt_count; /* Mount count */
	__le16 s_max_mnt_count; /* Maximal mount count */
	__le16 s_magic; /* Magic signature */
	__le16 s_state; /* File system state */
	__le16 s_errors; /* Behaviour when detecting errors */
	__le16 s_minor_rev_level; /* minor revision level */
	__le32 s_lastcheck; /* time of last check */
	__le32 s_checkinterval; /* max. time between checks */
	__le32 s_creator_os; /* OS */
	__le32 s_rev_level; /* Revision level */
	__le16 s_def_resuid; /* Default uid for reserved blocks */
	__le16 s_def_resgid; /* Default gid for reserved blocks */
		/*
		* These fields are for EXT2_DYNAMIC_REV superblocks only.
		*
		* Note: the difference between the compatible feature set and
		* the incompatible feature set is that if there is a bit set
		* in the incompatible feature set that the kernel doesn't
		* know about, it should refuse to mount the filesystem.
		*
		* e2fsck's requirements are more strict; if it doesn't know
		* about a feature in either the compatible or incompatible
		* feature set, it must abort and not try to meddle with
		* things it doesn't understand...
		*/
	__le32 s_first_ino; /* First non-reserved inode */
	__le16 s_inode_size; /* size of inode structure */
	__le16 s_block_group_nr; /* block group # of this superblock */
	__le32 s_feature_compat; /* compatible feature set */
	__le32 s_feature_incompat; /* incompatible feature set */
	__le32 s_feature_ro_compat; /* readonly-compatible feature set */
	__u8 s_uuid[16]; /* 128-bit uuid for volume */
	char s_volume_name[16]; /* volume name */
	char s_last_mounted[64]; /* directory where last mounted */
	__le32 s_algorithm_usage_bitmap; /* For compression */
	/*
	* Performance hints. Directory preallocation should only
	* happen if the EXT2_COMPAT_PREALLOC flag is on.
	*/
	__u8 s_prealloc_blocks; /* Nr of blocks to try to preallocate*/
	__u8 s_prealloc_dir_blocks; /* Nr to preallocate for dirs */
	__u16 s_padding1;
	/*
	* Journaling support valid if EXT3_FEATURE_COMPAT_HAS_JOURNAL set.
	*/
	__u8 s_journal_uuid[16]; /* uuid of journal superblock */
	__u32 s_journal_inum; /* inode number of journal file */
	__u32 s_journal_dev; /* device number of journal file */
	__u32 s_last_orphan; /* start of list of inodes to delete */
	__u32 s_hash_seed[4]; /* HTREE hash seed */
	__u8 s_def_hash_version; /* Default hash version to use */
	__u8 s_reserved_char_pad;
	__u16 s_reserved_word_pad;
	__le32 s_default_mount_opts;
	__le32 s_first_meta_bg; /* First metablock block group */
	__u32 s_reserved[190]; /* Padding to the end of the block */
};

3.3.2 GDT (Group Descriptor Table)

块组描述符表,描述块组属性信息,整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储一个块组的描述信息,如在这个块组中从哪里开始是inode Table,从哪里开始时Data Blocks,空闲的 inode 和数据块还有多少个等等。块组描述符在每个块组的开头都有一份拷贝。

// 磁盘级blockgroup的数据结构
/*
* Structure of a blocks group descriptor
*/
struct ext2_group_desc
{
	__le32 bg_block_bitmap; /* Blocks bitmap block */
	__le32 bg_inode_bitmap; /* Inodes bitmap */
	__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];
};

3.3.3 块位图(Block Bitmap)

Block Bitmap 中记录着Data Block 中哪个数据块已经被占用,哪个数据块没有被占用

3.3.4 inode位图(Inode Bitmap)

每个 bit 表示一个 inode 是否空闲可用。

3.3.5 i节点表(Inode Table)

  • 存放文件属性 如:文件大小,所有者,最近被修改的时间等等
  • 当前分组所有Inode属性的集合
  • inode编号以分区为单位,整体划分,不可跨分区

3.3.6 Data block

数据区:存放文件内容,也就是⼀个⼀个的Block。根据不同的⽂件类型有以下几种情况:

  1. 对于普通文件,文件的数据存储在数据块中。
  2. 对于目录,该目录下的所有⽂件名和⽬录名存储在所在⽬录的数据块中,除了文件名外,ls -l命令看到的其它信息保存在该⽂件的inode中。
  3. Block 号按照分区划分,不可跨分区

文件的内容如何在一个分组中进行保存?

文件的属性如何在一个分组中进行保存?

有了上面的知识,下面进行知识的串联:

3.4 inode 和 Datablock的映射

每一个文件都有自己对应的inode,有了对应的inode,将来拿着inode数字,就可以得到相关文件的属性,文件也有对应的内容,所以inode里还存在了一张表,inode和数据块之间的映射表:i_block

/*
* Structure of an inode on the disk
*/
struct ext2_inode {
	__le16 i_mode; /* File mode */
	__le16 i_uid; /* Low 16 bits of Owner Uid */
	__le32 i_size; /* Size in bytes */
	__le32 i_atime; /* Access time */
	__le32 i_ctime; /* Creation time */
	__le32 i_mtime; /* Modification time */
	__le32 i_dtime; /* Deletion Time */
	__le16 i_gid; /* Low 16 bits of Group Id */
	__le16 i_links_count; /* Links count */
	__le32 i_blocks; /* Blocks count */
	__le32 i_flags; /* File flags */
	union {
		struct {
			__le32 l_i_reserved1;
		} linux1;
		struct {
			__le32 h_i_translator;
		} hurd1;
		struct {
			__le32 m_i_reserved1;
		} masix1;
	} osd1; /* OS dependent 1 */
	__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
	__le32 i_generation; /* File version (for NFS) */
	__le32 i_file_acl; /* File ACL */
	__le32 i_dir_acl; /* Directory ACL */
	__le32 i_faddr; /* Fragment address */
	union {
		struct {
			__u8 l_i_frag; /* Fragment number */
			__u8 l_i_fsize; /* Fragment size */
			__u16 i_pad1;
			__le16 l_i_uid_high; /* these 2 fields */
			__le16 l_i_gid_high; /* were reserved2[0] */
			__u32 l_i_reserved2;
		} linux2;
		struct {
			__u8 h_i_frag; /* Fragment number */
			__u8 h_i_fsize; /* Fragment size */
			__le16 h_i_mode_high;
			__le16 h_i_uid_high;
			__le16 h_i_gid_high;
			__le32 h_i_author;
		} hurd2;
		struct {
			__u8 m_i_frag; /* Fragment number */
			__u8 m_i_fsize; /* Fragment size */
			__u16 m_pad1;
			__u32 m_i_reserved2[2];
		} masix2;
	} osd2; /* OS dependent 2 */
};
#define EXT2_NDIR_BLOCKS 12
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)
//inode 的⼤⼩通常是 128 字节 或 256 字节
__le32 i_block[EXT2_N_BLOCKS];/* Pointers to blocks */

数组可以理解为是一个无符号整数,里面存放的该文件所对应的数据块的块号。所以一个文件要被访问,直接拿着inode table中的映射关系直接在当前的data block中去找,对应的块号是多少,直接找对应的块就好了。

其实,EXT2_N_BLOCKS =15一个块大小是4KB,只能建立15组的映射关系的话,总共大小是60KB,而在linux中一个文件最大不止60KB,所以,该怎么办?

但是一个块组内所对应的数据块个数不够的话,inode的编号在整个分区中是有效的,数据块也是在整个分区内全局有效的,实际上inode table中建立和数据块对应的映射时,不仅仅仅限于自己坐在组的块,本组内的块不够了,再在文件系统层面上的其它的组内去要,块也是可以在文件系统内跨组的。去抢别人的块号如何抢?因为每一个块组大小都是一样的,inode个数固定的,block个数固定,所以去其他组,起始编号能够算出来的,inode table和data block是可以进行跨组的,在分区内有效。 

所以,打开一个文件Linux系统从根目录开始做路径解析,把所有的目录打开在内存里创建dentry缓存/dentry树,打开对应的每一个目录的文件名和inode的映射关系,路径的每一个节点都有文件名。找到当前目录,找到要访问的文件inode,根据inode/每一个组inode个数,就可以得到在哪个组,inode的属性就找到了,找到对应的块,要的内容就拿到了,文件的属性和内容就全部拿到了。

但是还是会有一个问题:

3.5 目录与文件名

  • 目录也是⽂件,但是磁盘上没有目录的概念,只有文件属性+文件内容的概念。
  • 目录的属性不用多说,内容保存的是:文件名和Inode号的映射关系
  • 所以,访问文件,必须打开当前目录,根据文件名,获得对应的inode号,然后进行文件访问
  • 所以,访问⽂件必须要知道当前⼯作⽬录,本质是必须能打开当前⼯作⽬录⽂件,查看⽬录⽂件的内容!

代码证明:文件名和inode在指定目录中确实是存在映射关系的!!!

//redir.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <dirent.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char* argv[]) {
	if (argc != 2) {
		fprintf(stderr, "Usage: %s <directory>\n", argv[0]);
		exit(EXIT_FAILURE);
	}
	DIR* dir = opendir(argv[1]); // 系统调⽤,⾃⾏查阅
	if (!dir) {
		perror("opendir");
		exit(EXIT_FAILURE);
	}
	struct dirent* entry;
	while ((entry = readdir(dir)) != NULL) { // 读取目录,系统调⽤,⾃⾏查阅
		// Skip the "." and ".." directory entries
		if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..")
			== 0) {
			continue;
		}
		printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned
			long)entry->d_ino);
	}
	closedir(dir);
	return 0;
}

dirent 与 dentry 类似

d_name:当前对应的文件名

//打开一个目录,访问这个目录下的所有的文件名和inode
struct dirent* entry;
	while ((entry = readdir(dir)) != NULL) { // 读取目录,系统调⽤,⾃⾏查阅
		// Skip the "." and ".." directory entries
		if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..")
			== 0) {
			continue;
		}

运行结果:

3.6 路径缓存

问题1:Linux磁盘中,存在真正的目录吗?

答案:不存在,只有文件。只保存文件属性+文件内容

问题2:访问任何文件,都要从 / 根目录开始进行路径解析?

答案:原则上是,但是这样太慢,所以Linux回缓存历史路径结构

问题3:Linux目录的概念,怎么产生的?

答案:打开的文件是目录的话,由OS自己在内存中进行路径维护

Linux中,在内核中维护树状路径结构的内核结构体叫做:struct dentry,struct dentry 表明当前的叶子节点

struct dentry {
	atomic_t d_count;
	unsigned int d_flags; /* protected by d_lock */
	spinlock_t d_lock; /* per dentry lock */
	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 */
	struct dentry* d_parent; /* parent directory */
	struct qstr d_name;
	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;
	unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */
};

注意:

  • 每个文件其实都要有对应的dentry结构,包括普通⽂件。这样所有被打开的文件,就可以在内存中形成整个树形结构
  • 整个树形节点也同时会隶属于LRU(Least Recently Used,最近最少使用)结构中,进行节点淘汰
  • 整个树形节点也同时会⾪属于Hash,⽅便快速查找
  • 更重要的是,这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何文件,都在先在这棵树下根据路径进行查找,找到就返回属性inode和内容,没找到就从磁盘加载路径,添加dentry结构,缓存新路径

3.7 挂载分区

3.7.1 一个实验

拿到inode在磁盘中找的时候,怎么知道是在哪一个分区下的呢?

Linux中,一般磁盘设备在 /dev下:

在这里(本篇文章)将vda当作物理盘,但这块物理盘不能被直接使用,使用这块物理盘得先分区,格式化,当前系统磁盘只有一块,分区也只分了一块,叫做vda1,如果将磁盘分为多块,就是vda2,vda3,vda4这样的磁盘的盘符。所以 /dev/vda 是物理磁盘,能看见的,/dev/vda1 是磁盘中的分区。而未来新建文件、删除文件、打开文件、文件操作,对应的分区,把文件系统写好,进行格式化,这个分区不能被直接使用,而是将分区挂载才能使用的!!!

如何查明挂载??

/dev/vda1 被挂载到了根目录下,所以我们只要在根目录下所作的所有的文件增删查改的操作都在vda1这个分区下进行的,如果要真正的使用vda1这个分区的话,是需要将/dev/vda1挂载到指定的一个目录下的。但是我们系统上面只有一个分区,还怎么观察呀?但是自己可以模拟出来一个分区:dd命令,就是二进制写入,形成一个大文件,通常用dd命令:

dd if=/dev/zero of=./disk.img bs=1M count=5 #制作⼀个⼤的磁盘块,就当做⼀个分区
mkfs.ext4 disk.img # 格式化写⼊⽂件系统

mkdir /mnt/mydisk # 建⽴空⽬录
df -h # 查看可以使⽤的分区

dd if =……,dd命令中的参数 if 表示的是import表示的是输入设备,从dev/zero,zero是Linux系统中提供的临时的设备文件,of是outputfile,将if中读到的数据写入到什么地方,形成一个大文件,bs=1M表示写入的块是1兆,一次写一兆,一共写5次,构建出一个5兆的大小。此时将形成的大文件也是磁盘的一个区域,当作一个磁盘块。

相当于磁盘上面有对应的数据块了,将这个小的磁盘块当作小的分区

写入文件系统:

mkfs.ext4 disk.img # 格式化写⼊⽂件系统

//向刚刚构建的disk.img磁盘里面构建文件系统

得挂载起来才能使用:

$ sudo mount -t ext4 ./disk.img /home/sy/linux/lesson25/hello

其实在/home/sy/linux/lesson25/hello这个文件中给所作的所有操作都是在 disk.img这块盘中做操作。因为我们是模拟出来的分区,Linux识别是把他当成dev/loop0,一种循环的测试设备,如果挂载的是真正的分区,/dev/vda1这个名字是不变的

模拟出来的磁盘分区:disk.img 比较小,dd命令想弄一个10MB的数据,将分区写满就没再弄了。只写了3.6MB的大小,部分的inode占用的,inode还在,但是数据却被写满了。

分区要被使用就得被挂载,挂载在指定的目录下,进入这个目录才能访问对应的指定的分区了。

如果不想用的话,用umount命令:

sudo umount /home/sy/linux/lesson25/hello

此时,关联关系就没有了:

3.7.2 一个结论:

  • 分区写入文件系统,无法直接使用,需要和指定的目录关联,进行挂载才能使用。
  • 所以,可以根据访问目标文件的"路径前缀"准确判断我在哪⼀个区。(最多理解到这个层面)
  • linux是可以存在很多个分区的,每一个分区可以存在多个文件系统,每一个文件系统都可以挂载到指定的目录下,所以OS怎么会知道它有多少个文件系统呢?多少个分区呢?每一个分区分别挂载到哪一个目录下呢?文件系统是多个的,因为分区是多个的,每一个文件系统加载到目录下也是多个的。所以,在OS内要对文件系统做管理,先描述,在组织!!!在Linux内核中,对应的文件系统数据结构是 struct SB,就是super block,其中一个文件系统就有一个挂载点,同时也是对挂载点有管理的,在Linux内核中struct mount数据结构,对应的struct mount数据结构和文件系统是Linux中的一些全局对象,新增一个文件系统,新增一个挂载点,就会将mount数据结构和文件数据关联起来,后续打开对应的文件,打开对应的dentry结构:

3.8 文件系统总结

主要想从不同角度说明:

4. 软硬连接

4.1 软链接

硬链接时通过inode引用另外一个文件,软链接时通过名字引用另外一个文件,但实际上,新的文件和被引用的文件的inode不同,应用创建上可以想象成一个快捷方式。

ln -s redir.c redir-soft   # 建立软链接,用后者链接前者

4.2 硬链接

真正找到磁盘上文件的并不是文件名,而是inode。其实在linux中可以让多个文件名对应于同一个inode。

ln redir.c redir-hard  #建立硬链接,用后者链接前者

从运行的结果中,可以发现:

4.3 软硬链接的区别

4.3 软硬链接的应用场景(重点)

软链接的应用场景之一就是快捷方式

之前有环境变量来解决找不到路径的问题,但是一般发布一款软件,一款软件有项目结构

redir不在当前路径下,但是就是想在当前路径下执行对应的程序

以此,用快捷方式直接访问目标程序了,这个是软链接最典型的应用之一。

软链接的应用场景:让用户无感知的进行软件升级

升级的时候将exe给替换掉,就升级成功了。相当于就是OS和用户之间增加了一个软链接,这样的话就多了一层软件层。对底层做修改、更新,软链接都不变,对软甲进行无感知的升级。

硬链接典型的应用场景:对文件进行备份!!

将 redir.c 删掉了,此时的 redir-hard是不会被删掉的。因为inode的还存在,inode存在,属性在,属性在,内容就还在

与当前 redir.c 文件的 inode 号一致:

说明两者是同一个文件。将 redir.c 删掉,当前目录下是没有了,但是这个文件是没有被删掉的,因为我们做了备份:

所以,今天进行备份的时候是不会占用两份对应的内容和属性的空间,备份文件只是在指定目录下建立了文件名和inode映射关系。文件属性和文件内容永远都只有一份,而在指定目录下删文件时,只是删了当前目录下的对应的文件名和inode的映射关系。

此时文件的引用计数变为0,将该文件删除了。

创建出普通文件、目录,为什么对应默认的引用计数不一样??

普通文件所对应的文件名和inode的映射关系只有一组,所以引用计数就是1,但是为什么空目录的引用计数是2呢?因为自己的dir名字和inode是一组映射关系,因为dir目录下还有一个 .  ,点也是一个文件名,指向的也是 dir:

所以,终于理解为什么 表示的也是当前目录了,因为 和 当前目录dir指向同一个 inode,因为 是当前目录的硬链接,又因为任意一个目录永远都有自己的目录名和  ,所以一个空目录的引用计数是2。

在dir目录下新建另一个目录:

此时,在查看dir的引用计数时,显示的是3:

这是因为otherdir目录下有 .. 指向的是上级目录,就是dir目录

查看根目录的inode为19:

根目录下有多少个子目录?(直接引用计数-2 = 子目录的个数)

自身占了2个,一个是 . ,一个是/ ,就有17个子目录:

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐