Linux第八课---------文件系统

作者前言

🎂 ✨✨✨✨✨✨🍧🍧🍧🍧🍧🍧🍧🎂

​🎂 作者介绍: 🎂🎂

🎂 🎉🎉🎉🎉🎉🎉🎉 🎂

🎂作者id:老秦包你会, 🎂

简单介绍:🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂🎂

喜欢学习C语言、C++和python等编程语言,是一位爱分享的博主,有兴趣的小可爱可以来互讨 🎂🎂🎂🎂🎂🎂🎂🎂

🎂个人主页::小小页面🎂

🎂gitee页面:秦大大🎂

🎂🎂🎂🎂🎂🎂🎂🎂

🎂 一个爱分享的小博主 欢迎小可爱们前来借鉴🎂


文件系统

文件

文件 = 文件内容+ 属性, 文件被打开会在内存进行存储,而未打开的文件是在磁盘上的,当我们要在磁盘上找到文件,是要根据目录去找的,这个目录是一个树结构, 当我们输入一个目录,那么这个文件系统就要帮我们找到这个文件

硬件

磁盘

机械磁盘是计算机中唯⼀的⼀个机械设备 ,它是一个外设,容量大,便宜。

磁盘的存储结构如下:

一个盘片是由许多的磁道组成,一个磁道并不是一个连续的轨道,而是由许多的扇区组成,扇区之间是有间隙的,每一个扇区存储的存储512个字节,磁盘存储数据的一个基本单位是扇区 也就是512字节,而不是一个字节。

一个磁盘可以有多个盘片。一个盘片有两个磁头 ,分别是一个盘片的正反面,控制盘片转动的是主轴 ,所有磁头在磁头壁共同操作, 在同一柱面进行写入(柱面 = 所有盘片上、同一半径的磁道组成的立体圆柱,也就是所有盘片表面上、相同编号的磁道集合),也就是主轴控制当前磁道的扇区的位置,而机械臂控制磁头放在哪个磁道。

当我们想要哪一个扇区: 选择柱面(磁道),选择该扇区的磁头,在对应的扇区开始写入或者读取,这个也就是CHS地址定位

我们可以查看当前的云服务的磁盘:

bash 复制代码
fdisk -l

如图:

磁头(head)数:每个盘⽚⼀般有上下两⾯,分别对应1个磁头,共2个磁头

• 磁道(track)数:磁道是从盘⽚外圈往内圈编号0磁道,1磁道...,靠近主轴的同⼼圆⽤于停靠磁

头,不存储数据

• 柱⾯(cylinder)数:磁道构成柱⾯,数量上等同于磁道个数

• 扇区(sector)数:每个磁道都被切分成很多扇形区域,每道的扇区数量相同

• 圆盘(platter)数:就是盘⽚的数量

• 磁盘容量=磁头数 × 磁道(柱⾯)数 × 每道扇区数 × 每扇区字节数

##################################################################

磁盘的逻辑结构:

磁盘相当于一条长长的磁带,基本单位是扇区,变成看一个线性结构,以物理结构想象(假的):

这样每个扇区都有一个编号也就是数组下标(这个下标从1开始),这种地址叫做 LBA,我们用户先要找到某个扇区,很难获取到CHS地址,但是LBA却是可以获取的,操作系统可以根据LBA转换成CHS地址。

我们知道一个磁盘,有多个柱面,也就是每个盘片相同编号的磁道的集合, 一个磁道展开如下:

而柱面:

我们也展开来看:

变成了一个二维数组,一个磁盘有多个柱面,那么这个磁盘我们可以当作是有多个柱面组成的,也就是拥有多个二维数组的一个表 ,也就是一个三维数组

那么这个CHS就是这样的一个坐标(C, H , S),但是本质仍然是一个一维数组, 跟C++的数组是一样的,只不过是逻辑想象成多维,存储的只是一维。

如图:

这个就是磁盘的逻辑存储结构(真实),以柱面为主。LBA就是一个扇区的下标,注意: 扇区的从1开始计算,但是LBA从0开始计算,也就是CHS转换为LBA要减一。 LBA转换为CHS要+1, 每个柱面的扇区是一样的

具体规则如下

CHS转成LBA:

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

• 磁头号H = (LBA % (磁头数*每磁道扇区数)) // 每磁道扇区数

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

• "//": 表⽰除取整, "%":表示取余

​那么我们只需要使用LBA地址,磁盘内部进行转换,就可以找到对应的扇区了。

磁盘就是⼀个 元素为扇区 的⼀维数组,数组的下标就是每⼀个扇区的LBA地址。OS使⽤磁盘,就可以⽤⼀个数字访问磁盘扇区了。

文件系统的 数据存储

"块"概念

硬盘是典型的"块"设备,操作系统读取硬盘数据的时候,其实是不会⼀个个扇区地读取,这样效率太低,⽽是⼀次性连续读取多个扇区,即⼀次性读取⼀个"块"(block)。

硬盘的每个分区是被划分为⼀个个的"块"。⼀个"块"的⼤⼩是由格式化的时候确定的,并且不可

以更改,最常⻅的是4KB,即连续⼋个扇区组成⼀个 "块"。"块"是⽂件存取的最⼩单位。

也就是OS访问磁盘不以扇区为单位进行访问,而是以块为单位,也就是4kb(可以调整), 对应着连续的8个扇区

每个扇区都有LBA,那么8个扇区⼀个块,每⼀个块的地址我们也能算出来。

• 知道LBA:块号 = LBA/8

• 知道块号:LAB=块号*8 + n. (n是块内第⼏个扇区, 从0 开始)。

文件系统使用的单位是块,而不是扇区

假设有磁盘有100GB, 100* 1024 *1024 / 4 = 26214400(块)

"分区"概念(磁盘有,文件系统没有)

其实磁盘是可以被分成多个分区(partition)的,以Windows观点来看,你可能会有⼀块磁盘并且将它分区成C,D,E盘。那个C,D,E就是分区。分区从实质上说就是对硬盘的⼀种格式化。但是Linux的设备都是以⽂件形式存在。

磁盘只要记得分区的起始位置和结束位置们就可以知道一个分区的大小

文件系统的载体是分区

分组(文件系统有,磁盘没有)

文件系统怎么进行管理的呢 。假设100GB通过分区, 每个分区50GB, 只要管理好其中一个分区,然后再拷贝这个分区的方法给其他分区,这样就可以管理好其他分区,而分区里面又会有多个分组 组成, 假设每个分区有10个分组,每个组就会有5GB,而只要管理好一个组,把这个组的管理方法拷贝到其他的组,这样据可以管理好一个分区。

文件系统的认识

如下:

注意: 分组的基本单位: 块

Data Block(存放文件内容和目录内容) 和inode table(存放目录的inode和文件的inode)

在Linux下, 文件的属性和内容是分开存储,文件 = 文件内容 + 属性,其中文件的内容存储在分组的Data Blocks中, 在这个Data Blocks中有许多的小格子,每个格子是4kb,也就是一个块,以4kb为单位 ,把文件内容存储就在这里。而文件的属性( 如 ⽂件⼤⼩,所有者,最近修改时间等)就存储在inode Table中,也是以4kb为单位的。

在Linux中,任何文件都要有自己的属性集合inode,这个集合就是一个结构体,如下:

c 复制代码
struct inode
{
	int size; 
	int type;//是文件还是目录
	int inode_number;//文件编号
	.........

}

这个结构体的大小是固定的,里面也包含有文件的编号。 一般情况下是128字节,一个块(4kb)可以保存有32个文件属性集合,并且每个文件都有对应的编号,保证文件的唯一性

查看文件的编号:

bash 复制代码
ls -li two.txt

每⾏包含7列(编号不算):

• 模式

• 硬链接数

• ⽂件所有者

• 组

• ⼤⼩

• 最后修改时间

• ⽂件名

注意: 文件名不会在这个结构体中

在Linux中,目录有属于自己的inode结构体和数据块,那么一个文件名会保存在当前文件所属目录的数据内容中(也就数据块中)

小知识:

inode编号以分区为单位,整体划分,不可跨分区,比如块组1 的inode使用编号1 - 100,块组2就使用101 - 200,编号唯一性,在同一个分区里,不能出现相同的编号。

文件 inode和data block映射(弱化)

知道文件的inode,那怎么找到文件的内容的呢?

答案: inode结构体里面包含有i_blockEXT2_N_BLOCKS

源码如下:

c 复制代码
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 */
};

其中__le32 i_blockEXT2_N_BLOCKS中的EXT2_N_BLOCKS =15

c 复制代码
#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)

到这里就很困惑,假设一个下标指向一个数据块,那么这个文件的大小并不就是60kb,不太可能

我们现在的很多文件 都是大于60kb的,如图:

据说前12下标存储的是一个个数据块的指针,然后后面下标为13的存储的是一个一级间接块索引指针,这个一级间接块索引大小本身就是一个块,4kb大小,一个指针大小为4b,一个一级间接块索引表可以保存有1024个地址,如果是三级的话就有10241024 102441024 字节也就是4TB。

注意,如果一个文件内容太大的话,是可以跨分组进行保存的,

Block Bitmap 和 Inode Bitmap

在众多的文件面前, 我们怎么知道 data block 和inode table的哪些块是有数据的,哪些块没有?答案就是Block Bitmap 和 Inode Bitmap。

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

假如 Data Block里面有100000个块, 那么Block Bitmap就会有100000个比特位,也就是3个块,

当哪一块的Data Block里面的块被占用,对应的比特位就会变成1, 否则就为0。

inode Bitmap: 记录着inode Table中哪个数据块已经被占⽤,哪个数据块没有被占⽤。

和Block Bitmap是一样的效果

删除的本质就是: 这两个结构的bit位都被重置为0。

GDT(描述一个分组的情况)

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

Super Block(描述一个分区的情况)

存放⽂件系统本⾝的结构信息,描述整个分区的⽂件系统信息。记录的信息主要有:bolck 和 inode的总量,未使⽤的block和inode的数量,⼀个block和inode的⼤⼩,最近⼀次挂载的时间,最近⼀次写⼊数据的时间,最近⼀次检验磁盘的时间等其他⽂件系统的相关信息。Super Block的信息被破坏,可以说整个⽂件系统结构就被破坏了。

超级块在每个块组的开头都有⼀份拷⻉(第⼀个块组必须有,后⾯的块组可以没有)。 为了保证⽂件系统在磁盘部分扇区出现物理问题的情况下还能正常⼯作,就必须保证⽂件系统的super block信息在这种情况下也能正常访问。所以⼀个⽂件系统的super block会在多个block group中进⾏备份,这些super block区域的数据保持⼀致。

格式化的本质: 写入文件系统的管理信息,也就是块组的Block Bitmap 和 Inode Bitmap和inode table和data block为空全部写入到文件系统的每一个块组中,然后把所有块的信息全部写入到磁盘的分区中。

文件系统是存在于磁盘(更准确说是:存在于磁盘的分区)中的

电脑启动,操作系统要对磁盘进行管理,就会把相关信息加载到内存里, 对文件系统的管理,转变成对链表的增删查改。

文件inode和目录inode的获取

Linux系统通过一个文件名在指定的分区,找到文件的内容,原理如下:

  1. 通过路径+文件名, 查找当前目录下的数据块内容里面的文件名,通过文件名和对应文件的inode的映射关系,找到对应文件inode,然后查找文件,哪个文件系统下的块组。获取对应的inode和数据块内容
  2. 由目录本身也是一个文件,也是需要先找到上一层目录的数据块内容,获取对应的文件名,所以可以理解根目录是Linux系统启动后自动打开的(/目录下形成操作系统),查找一个文件,会从根目录下一层层的进行解析,获取下一层的文件名,直到获取到自己想要的文件inode和数据内容。目录内容保存的是:⽂件名和Inode号的映射关系。
    都要从根⽬录开始,依次打开每⼀个⽬录,根据⽬录名,依次访问每个⽬录下指定的⽬录,直到访问到test.c。这个过程叫做Linux路径解析。
  3. 路径是进程提供的,因为我们在系统上的操作一定是转换为进程在操作,而文件名是我们提供的,操作系统获取路径+文件名进行解析,磁盘是没有目录的,目录是内存的,磁盘只有⽂件。只保存⽂件属性+⽂件内容 ,我们在xshell访问任何文件。本质就是一个操作系统通过磁盘里面的数据(磁盘IO),加载到内存里,通过路径解析,形成一颗多叉树,这个就是一个路径缓存,这个结构体是一个内核级的叫做struct dentry,如下:
c 复制代码
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 */
};

里面包含有对应文件的inode,获取到文件的inode,就可以找到对应的数据内容了。

目录和文件名之间的映射关系

目录和文件名之间的关系,代码如下:

cpp 复制代码
#include<iostream>
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<dirent.h>
#include<string.h>
int main(int argc, char* argv[], char* env[])
{
  close(2);
  umask(0);
  int fd1 = open("./stderr.txt",O_WRONLY | O_CREAT | O_TRUNC, 0666);
  dup2(fd1, 1);
  if(argc != 2)
  {
    fprintf(stderr,"%s, error\n", argv[1]);
    exit(1);
  }
 DIR* id = opendir(argv[1]);//打开一个目录
 if(!id)
 {
   perror("open dir error\n");
   exit(1);
 }
struct dirent *entry = nullptr;
while((entry = readdir(id)) != nullptr)//每次读取一个文件的inode
{
  if(strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
  {
    continue;
  }
  printf("filename : %s, fileinode: %lu\n", entry->d_name, entry->d_ino);
}
 closedir(id);
  close(fd1);

  
  return 0;
}

效果如下:

通过打开当前目录,然后读取目录的数据内容,查看对应的文件名和inode的映射关系

挂载

通过一个文件的inode,怎么知道在哪一个分区,

1.inode编号是不能跨分区的,

原因如下:分区和某一个目录进行关联,如果进入到这个目录,就会默认进入到这个分区,这个就叫挂载

分区写⼊⽂件系统,⽆法直接使⽤,需要和指定的⽬录关联,进⾏挂载才能使⽤。

总结

一个进程打开一个文件的过程大致如下,

  1. 操作系统通过路径缓存生成的多叉树,找到文件的inode,如果没有找到,就会从磁盘重新获取,然后从根目录开始进行目录解析,找到对应的文件inode,因为某一个目录肯定跟分区进行挂载了,所以知道分区了,
  2. 找到文件的inode,创建 strcut file对象,创建好文件缓冲区,通过文件的inode把文件的属性加载到内存里进行补充到file对象,把文件内容加载到缓冲区中,然后 在strcut files_struct对象里增加这个file对象,分配好文件描述符。

文件系统的总结

一个进程的PCB拥有进程的相关信息,其中拥有struct fs_struct *fs和struct files_struct *flies,

  1. 其中fs指针指向的结构体里面包含有该进程的根目录、当前路径等结构struct path,这个结构体里面包含有对应dentry结构体对象,这样就可以找到对应的inode。
  2. file这个文件描述符表里面struct file* fd_array数组里面保存有对应文件的flie对象,struct file结构体里面包含有struct path对象,这个struct path里面包含有 struct dentry指针,通过struct dentry对象就可以找到相关文件的inode。

所有,我们通过使用文件名找文件,本质就是通过找到文件名和文件的inode的映射关系 找到文件的inode,在磁盘找到对应的inode和数据块,

软硬链接

软链接(相当于文件的快捷键)

bash 复制代码
ln -s 源文件或目录 软链接名(随便取)

如图:

通过链接名软链接对应的文件,如果往stderr.txt文件写入,然后就会在软连接stderr文件中查看到相同的内容如图:

如果查看对应的文件编号,如下:

两者则是不同的文件,编号不一样,文件inode就不一样,

所以说,软链接是一个独立的文件 ,软链接就相当于应用的快捷键,不用找到软件,直接启动就行,哪怕删除软链接也不影响链接的文件。

文件 = 内容 + 属性, 软链接的文件内容保存的是目标文件的路径

硬链接

bash 复制代码
ln 源文件 硬链接名

如图:

可以看到,硬链接srderr_hard的编号和stderr.txt编号是一样的,所显示的硬链接数是2.

可以总结:硬链接不是一个独立的文件。没有独立的inode,**本质就是新文件名和目标文件的编号的映射,**文件的inode是有一个引用计数的。

硬链接的用途:

  1. 可以用作一个备份,哪怕删除目标文件,也可以通过硬链接找到对应的文件内容。
  2. 如图:

    作为目录下的.和...的硬链接