Linux基础IO(三)——文件系统和软硬链接

1、磁盘

1.1、认识磁盘

在这里插入图片描述

现在我们的电脑用的都是固态硬盘了,但是在十年前,电脑用的比较多的还是机械磁盘,固态硬盘比机械磁盘读写速度要快些,但是也贵些。虽然现在我们笔记本电脑用的都是固态硬盘了,但是对于大容量的存储还是磁盘用的多,像服务器数据一般就存储在磁盘中。
磁盘主要由:盘片、磁头、磁头臂、主轴马达、磁头停靠点组成。磁头会不断地左右摆动,盘片会做高速旋转。
并且所有磁头都是一起动的,要么都不动,要么全部一起动。

1、磁头是一面一个。2、磁头和盘面不接触。
盘片作为存储的介质,盘片两面都可以存储数据,并且一个磁盘可能有多个盘片,每个盘面对应一个磁头。比如上图有三个盘片,那就有六个盘面,所以有六个磁头。

计算机只认识二进制数,那么对于其他硬件来说也是一样的,硬件也只认识二进制数,所以写入数据本质就是将二进制数写到盘片上。磁盘我们称为永久性存储介质,而内存我们称为掉电易失性存储介质,CPU需要给内存不断充电,没电后内存数据就没了。
对于吸铁石来说,有NS两极,而如果我们对它充放电,通过一些电磁特性就可以将NS两极反过来,那么对应的两种不同状态就是0和1。那么将数据写入磁盘也是类似的,磁头和盘面是不接触的,那么如何写入数据呢?我们可以把磁盘想象成由无数个小的吸铁石构成,而硬件之间是要连接起来的,所以会对磁盘进行充放电或高低脉冲,如果我要写入01序列,其实就是对这些无数个小的吸铁石进行逆置。而01序列在磁盘中表现出来的就是吸铁石的南北两极不同的两种状态。
当然在不同硬件上对于01的表示不太一样,有些设备上是用信号的有无来表示01,有些设备是用信号的强弱,有的使用波形图的疏密。但是最终都会被解释成01。


1.2、磁盘的存储构成

在这里插入图片描述

以盘片中心向外辐射,会存在很多个同心圆,这些同心圆就称为磁道,而每个磁道又由很多个扇区组成。并且内侧的扇区会比较短,外侧的扇区会比较长,但是它们存储的容量是相等的,这是通过密度来控制的。
磁盘被访问的最基本单元是扇区——512字节/4KB
我们可以把磁盘看成由无数个扇区构成的存储介质。
所以要把数据存储在磁盘中,首要问题就是如何定位一个扇区——即哪一面(用哪一个磁头),哪一个磁道,哪一个扇区。

磁头和盘片高速旋转的本质就是定位哪一个扇区,首先定位哪一个盘面,也就是要用哪个磁头,然后磁头来回摆动的过程就是定位哪一个磁道,盘片高速旋转就是定位在哪一个扇区。
所以侧面也反映了,对于磁盘来说,机械运动越少效率越高,机械运动越多效率越低。所以软件设计上,设计者一定要有意识地将相关数据放在一起。

如果将每个盘片同半径地磁道分离出来,就会形成一个柱面,如上图右侧。所以柱面就等价于磁道
我们将上面寻找磁头:Header,寻找磁道:Cylinder,寻找扇区:Sector,这种方式称为CHS寻址方式


1.3、转换为逻辑结构

在这里插入图片描述
我们小时候可能见到过磁带,如上图,磁带也是缠绕成一个一个同心圆地,但是我们可以把磁带拉直成一条直线变成线性的。所以对于磁盘来说也是一样的,磁盘是由一个一个同心圆组成,我们把它类似磁带拉直就可以变成线性的结构。

在这里插入图片描述

如上图,我们把一个一个盘面拼接在一起,然后每个盘面里面又有非常多个磁道,而到磁道里面又有非常多个扇区。所以最终线性结构上磁盘就是由无数个扇区构成的。
我们将磁盘延展开,逻辑上我们看磁盘是线性的,磁盘是由很多个扇区组成的,而每个扇区又是相邻的,所以转换为下面的结构,下面就是基于扇区的数组,任何一个扇区都有下标。

现在我们假设这个数组有100000个扇区,每个盘面有20000个扇区,所以总共有五个盘面,每个盘面有50个磁道,那么每个磁道就有400个扇区。
现在我们知道扇区的编号:28888,我们可以对他进行计算然后在磁盘中找到这个扇区。
1、28888 / 20000 = 1,所以在第二个盘面上,0对应的就是第一个盘面。
2、28888 % 20000 = 8888,8888 / 400 = 22,所以在第23个磁道上。
3、8888 % 400 = 88,所以在第88个扇区。
扇区编号我们称为扇区的逻辑地址,也就是LBA地址,那么我们就可以实现LBA地址和CHS地址之间的相互转换。


1.4、回归到硬件

在这里插入图片描述
不仅仅CPU有寄存器,其他设备(外设)也有。磁盘也有。
磁盘中有控制寄存器、数据寄存器、地址寄存器、状态寄存器。

当访问磁盘,首先向控制寄存器发送IO信息,r/w表示是要读还是要写,假设要写入,所以给控制寄存器写入w。然后将数据写入到磁盘的数据寄存器中,另外你还要告诉磁盘要往哪里写入,所以需要将地址写入磁盘的地址寄存器中,然后磁盘就会从数据寄存器中获取数据写入到对应地址。而进程要等待磁盘写入,所以还有状态寄存器,里面表示结果,操作系统时不时对状态寄存器进行检查,判断是否写入完成。


2、文件系统

文件=内容+属性,所以磁盘上存储文件=存文件的内容+存文件的属性。
文件的内容是存储在数据块中的,文件的属性是存储在inode中的。
Linux的文件在磁盘中存储,是将属性和内容分开存储的!

在这里插入图片描述
对于一个800G的磁盘来说,我们需要进行分区,分区的目的是为了更好的管理。比如上图分别分成5个区。所以对磁盘的管理转换成了对某个分区的管理,也就是说某个分区管理好了,那么其他分区也就能管理好,那么磁盘也就能管理好。
那么每个分区里面有一个BootBlock,这个和开机启动有关的,我们不谈。然后之后又把这个分区再进行划分,划分为一个一个的Block group,假设一个组为10G,那么就划分成了15个组。所以现在问题又转换为如何管理好其中的一个分组。如果其中一个分组管理好了,那么其他分组也能管理好,那么整个分区就能管理好,那么整个磁盘就能管理好。所以可以看到这个过程是一个分治的思想,就类似快排和归并的算法思想。
那么每个组里面又有:Super Block、Group Descriptor Table、Block Bitmap、inode Bitmap、inode Table、Data blocks。

Data blocks:存文件内容的区域,以块的形式呈现,常见的大小是4KB——我们称为文件系统的块大小。一般而言一个块只有自己的数据。

在这里插入图片描述

inode:存储单个文件的所有属性,大小为128字节,一般而言一个文件对应一个inode。
inode结构如上图,里面有:inode编号(唯一)、文件类型、权限、引用计数、拥有者、所属组、ACM时间,还有一个int blocks[NUM]的数组,这个数组里面存储的是Data blocks的块号。但是这个数组只有15个数据,如果存储15个块号,我们发现所能存储的文件大小并不大,所以比如下标12、13还存储了块号为1000、1001的两个块,然后这两个块里面并不存储数据,而是存储其他块的块号,其他块才存储文件内容,这12、13下标我们就称为二级索引。但是这么算下来所能存储的空间还是不够大,所以下标14所对应的块号继续存储其他块号,然后其他块号也不存储数据,其他块号再存储另外的块号,最后另外的块号才存储数据,这就是三级索引。而前面0-11的12个元素我们就称为直接索引。
所以Linux的文件属性中,并不包含文件的名称。在Linux系统里面标识文件用的是inode编号

使用ls -li查看文件inode编号:
在这里插入图片描述

inode Table:每个分组里面会有很多inode,所以就有inode Table。inode有唯一的编号。
inode Bitmap:位图结构,比特位的位置跟inode的编号映射起来,比特位的内容标识inode是否有效。
Block Bitmap:比特位的位置和块号映射起来,比特位的内容标识该块有没有被使用。
Group Descriptor Table:GDT,块组描述符,整个分区分成多个块组就应有多少个块组描述符。描述当前分组的信息。比如:在这个组中从哪里开始是inode Table,从哪里开始是Data Blocks,空闲的inode和数据块还有多少等等。

Super Block:包含的是整个分区的基本使用情况,如:一共有多少组、每个组的大小、每个组的inode数量、每个组的block数量、每个组的其实inode、文件系统的类型和名称等。Super Block不会在每个组都存在,在某些组会存在,存在多个super block是为了丢失了可以进行恢复。

删除一个文件,用不用把块清空呢?
并不需要,只要把blocks Bitmap标志位改为0,inode Bitmap标志位改为0即可。所以我们删除文件很快。

每一个分区在被使用之前,都必须提前将部分文件系统的属性信息提前设置到对应的分区中,方便我们后续使用这个分区或分组。——我们称之为格式化

inode和数据块,不能跨分区!所以在同一个分区内部,inode编号和块号都是唯一的。
在每一个分组内部,inode都是从0开始的,组1的inode编号假设为0->100000,那么组2的inode编号就是100001->200000。那么在组2中,对应inode Table和inode Bitmap就需要减去100000。

inode标识文件的所有属性,文件名并不属于inode内的属性。


1、新建一个文件,系统要做什么?
首先根据路径找到对应的磁盘分区,根据分组中的GDT信息,发现这个分组还有inode可以使用,那么就在对应分组的inode Bitmap中找到为0的位置,将该位置由0改为1,然后到inode Table中找到inode,接着将文件的信息填入inode。


2、删除一个文件,系统要做什么?
首先根据路径找到对应的磁盘分区,然后根据该文件的inode编号找到对应分组,然后将inode编号减去分组的起始inode编号,到inode Bitmap中查找对应标志位,发现是1,说明该inode有效,然后在inode Table中找到inode,然后找到inode里面的blocks数组,根据blocks数组可以知道文件内容存储的数据块块号,然后在block Bitmap中将对应数据块全部置0,这样文件内容就删除了,然后再到inode Bitmap将该inode置0,那么该inode就无效也被删除了。
所以删除只是修改block Bitmap和inode Bitmap中对应的标志位。删除并不是真正的删除,删除=允许被覆盖。


3、查找一个文件,系统要做什么?
首先根据路径找到对应的磁盘分区,然后根据该文件的inode编号找到对应分组,然后将inode编号减去分组的起始inode编号,到inode Bitmap中查找对应标志位,发现是1,说明该inode有效,再到inode Table中找到inode,就可以获取该文件的所有属性,如果要获取数据,就根据blocks找到对应的数据块,将数据块组合起来就可以获取到文件的内容。


4、修改一个文件,系统要做什么?
首先根据路径找到对应的磁盘分区,然后根据该文件的inode编号找到对应分组,然后再到inode Bitmap中将inode编号减去当前分组的起始inode编号,看对应inode Bitmap位置的数据为1,说明该inode有效,然后到inode Table中找到inode,就可以修改inode中的属性。如果要修改数据就找到inode里的blocks数组,根据数组中的块号找到对应的数据块,然后将修改数据块里的内容。
如果要写入数据,到block Bitmap中找到没有使用的块号,然后置1,接着将块号写入到blocks数组中,然后找到块将数据写入。


现在问题是,我怎么知道inode? 使用者从来没有关心过inode,用的是文件名。

如何理解目录?
目录是文件吗?——当然是,是文件,所以也有自己的inode,目录也有自己的属性和内容。
目录的数据块存什么呢?——存的是该目录下,文件名和对应文件inode的映射关系。如:f.txt:123123,他们是key/value模型的。所以同一个目录下不能有同名文件,因为key是唯一的。

分析查找文件:
所以当我们查找当前目录下的文件,就需要先找到当前目录的inode,然后根据blocks找到对应的数据块,然后根据数据块里面的文件名和对应文件inode编号映射关系,可以获取到该目录下对应文件的inode编号,然后根据这个inode编号就能找到对应的inode,那么就能获取到目标文件的属性,如果要获取目标文件的数据,就可以根据blocks找到数据块,那么就能获取到数据了。
但是,我们要如何找到当前目录的inode?
是不是得先找到上级目录的inode,然后找到上级目录的数据块,获取到上级目录的数据,然后才能根据文件名和对应文件的inode编号映射关系,找到当前目录的inode,从而才能获取到当前目录的数据。
而获取上级目录的inode,就需要先获取上上级目录的inode。
所以需要一直向上递归到根目录,好在根目录我们是知道的,所以查找当前目录下的文件,需要一路向上递归到根目录,再从根目录递归返回,找到每一个路上的目录数据块,最后才能获取到目标文件inode。

这样势必会存在效率问题,所以Linux会把你常访问的若干目录,以及路径上各种inode属性信息,在系统中把你使用过的路径信息缓存起来。——dentry缓存


3、软硬链接

3.1、建立软硬链接

建立软链接:ln -s file.txt soft-link
ln表示建立链接,不带-s表示建立硬链接,带-s表示建立软链接。由后者指向前者:由soft-link指向file.txt。

在这里插入图片描述
注意到,软链接有独立的inode,所以软链接是一个独立的文件。前面的文件类型是l,代表是软链接文件。
删除软链接使用unlink

建立硬链接:ln file.txt hard-link
在这里插入图片描述
注意到,硬链接的inode编号和指向文件的inode编号相同,并且引用计数增加,变成2。所以这个计数实际上是硬链接数。
所以硬链接不是一个独立的文件,因为它没有独立的inode。


3.2、如何理解软硬链接

如何理解软链接?
软链接是一个独立的文件,有独立的inode,也有独立的数据块,它的数据块保存的是指向文件的路径。——相当于windows中的快捷方式。

如何理解硬链接?
所谓建立硬链接,本质就是在特定目录的数据块中新增:文件名和指向文件的inode编号 的映射关系。
任何一个文件,无论是目录还是普通文件,都有inode,每一个inode内部都有一个叫做引用计数的计数器,表示有多少个文件指向我。
目录里面保存的是:文件名:inode编号 的映射关系。

所以硬链接可以理解为取别名。


3.3、软链接的应用场景

在这里插入图片描述

例如当前有个可执行程序在p5目录下,但是每次执行都需要经过很多个目录比较繁琐,所以我们直接建立软链接,然后直接执行即可。

另外我们也可以在系统的库目录下看到许多软链接:
在这里插入图片描述

另外我们可以把当前程序在/usr/bin下建立软链接,就可以像指令一样运行程序了
在这里插入图片描述

我们安装软件可以在Linux的任何路径下装,随便装,最后只要在/usr/bin下建立软链接,就可以像指令一样运行它了。


3.4、硬链接的应用场景

在这里插入图片描述
我们默认创建普通文件,它的硬链接数是1,而创建目录它的硬链接数是2,这是为什么呢?
file.txt很好理解,默认创建一个普通文件,当前就只有它自己,那么引用计数当然就是1了。

在这里插入图片描述
对于dir,这是因为Linux中还存在两个隐藏文件.和..,其中.表示当前目录,..表示上级目录,所以dir除了它本身还有它路径下的隐藏文件.指向它,所以硬链接数是2。并且我们也可以看到它们的inode编号是相同的。

在这里插入图片描述
我们再往上级路径看,发现lesson5有3个硬链接数,这是因为lesson5目录本身算一个,然后lesson5目录下的隐藏文件.算一个,还有我们刚创建的dir目录下的隐藏文件 .. 算一个,所以总共有三个。

在这里插入图片描述

另外我们看到根目录是有18个硬链接数的。根目录本身算1个,然后根目录下的目录总共有12个,所以它们每个目录里都有一个..,所以有12个硬链接,软链接不算,再加上根目录下的.算1个,所以总共18个。

所以硬链接的应用场景:通常用来进行路径定位,采用硬链接可以进行目录间切换。


但是我们发现Linux是不允许用户对目录建立硬链接的:
在这里插入图片描述
这是为什么呢??
在这里插入图片描述

如上图,如果我们在mydir目录下建立一个硬链接指向了根目录,那么当我们在使用find / -name test.c的时候,从根目录向下查找test.c文件,那么当走mydir这条路径时,最后找到mydir目录下的一个硬链接文件,那么就会再回到根目录去,所以就形成了环路问题。
有人会说,那你这不扯淡吗,mydir下面也有.和..隐藏文件,不也形成了环路问题吗,是的,所以系统在做搜索的时候不会对.和..进行搜索。


4、补充知识

4.1、页框和页帧

在这里插入图片描述
实际上物理内存是分成一个块一个块的,一块有4KB,我们称之为页框。而磁盘中的可执行程序也是由4KB划分的,我们称之为页帧。这是磁盘和内存交换数据的基本单位——4KB。
之所以这么做:
1、减少IO次数,减少访问外设的次数——硬件
2、基于局部性原理的预加载机制——软件

减少IO次数,我们知道磁盘是比较慢的,一次要4KB和要4次1KB相比,肯定是一次要4KB来的快,所以提高效率。我们执行程序的代码,那么就很有可能还会访问该代码的周围代码,这就是局部性原理,所以我索性就直接把后面的也加载进来。


4.2、操作系统如何管理内存

首先操作系统肯定是可以看到内存的,因为创建的PCB结构体,进程地址空间,页表必须是物理地址。所以能看到物理内存。那操作系统如何管理内存呢?——先描述,再组织。
在内核中通过struct page{ // page页的必要属性 }将页描述起来,以32位系统为例,内存大小为4GB,每个页是4KB,算下来就需要1048576个page来描述,也就是需要struct page mem_array[1048576]。那么对内存的管理就转换成了对数组的管理。那么每个数组下标就对应一个page,那么数组下标就是页号。

假设现在有一个物理地址位:0x11223344,我们先让0x11223344 & 0xFFFFF000 = 0x11223000,计算出的地址就是页框的起始地址,然后一个页为4KB,我们再右移12位,计算出0x11223就是页号。

我们访问一个内存,只需要直接找到这个4KB对应的page,就能在系统中找到物理页框。所以申请内存的动作,都是在访问page数组。


4.3、打开文件和文件系统相关联

在这里插入图片描述

创建一个进程,操作系统要创建一个task_struct对象,task_struct里面有struct files_struct* files,这个指针指向了struct files_struct对象,struct files_struct对象里面有文件描述符表struct file* fd_array[],指向一个一个的struct file对象。

系统启动时,文件系统中的Super Block要加载到内存,有多个分区所以就会加载多个Super Block,然后每个分组的GDT信息,包括Block Bitmap、inode Bitmap也要加载到内存中。

fopen打开一个文件,需要先创建struct file结构,同时struct file里面有指针指向struct inode对象,这个struct inode是保存文件的属性信息的,当我们打开一个文件,需要先获取到文件的inode,然后根据加载到内存中的inode Bitmap确认该inode有效,然后到inode Table获取到inode,然后将inode信息初始化struct inode对象。

另外我们之前也说过文件存在内核缓冲区,当我们fprintf先写入C语言的缓冲区,然后在合适的时候调用write写入到文件的内核缓冲区中。
所以struct file还有address_space的指针,指向address_space对象,这个对象里面有一棵struct radix_tree_root的树,我们查看树中的节点发现每个节点有一个void*的slots数组。那么树的结构就是类似我上面画的样子,每个节点有slots指针数组,每个指针指向下一个节点,当到了叶节点,slots数组指向的就是一个一个的struct page对象。这就是文件的页缓冲区。
所以我们将数据写入文件页缓冲区,就是通过struct file找到address_space的树,然后通过它们某种映射关系写入到page,然后之后进程就不再关心,操作系统会将该文件在内存中的数据刷新到磁盘上。物理内存的这些缓冲区数据会被struct request描述起来,request里面包含要写入磁盘的LBA地址。那么操作系统中会存在很多进程,每个进程都可能打开很多文件进行写入,所以就会存在一个IO request queue,而每个request写入的磁盘的位置都不相同,但是为了提高效率,我们要把它们写入位置相近的放在一起,所以就需要进行IO排序,然后再进行IO合并。

这颗树radix_tree_root我们称为基数树,在google的开源项目tcmalloc中,就是通过基数树来优化多线程释放的场景,这样多线程释放的时候就不需要竞争加锁,提高多线程释放内存块的效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值