目录
文件系统
背景知识
磁盘结构
磁盘的存储结构
磁盘抽象(逻辑,虚拟)结构
BootBlock:
Super block
Data blocks
inode Table
BlcokBitmap
inode Bitmap
Group Descriptor Table
文件名和inode编号
硬链接和软链接
软链接
硬链接
取消软硬链接unlink
stderr
文件系统
背景知识
我们讲解文件系统之前还需要说一些背景知识,方便我们快速理解学习文件系统的目的是什么.
1.我们之前一直讲的是open已经打开的文件,及其对应的操作。那没有被打开的文件在哪里呢?
当然是在磁盘,被称作磁盘级文件.
2.我们学习磁盘级文件,侧重点在哪里呢?
a.单个文件角度:这个文件在哪里,这个文件多大,这个文件的其他属性是什么?
b.在系统角度:一共有多少个文件?各自属性在哪里,如何快速找到,还可以存储多少个文件?如何让快速找到指定的文件等等
对于b这一系列问题,可以转化成 如何对磁盘文件进行分门别类的存储,以便更好地进行存储。
3.我们想要了解磁盘文件,必须先要了解磁盘.
内存 --- 掉电易失存储介质
磁盘 --- 永久性存储介质 (还有SSD,U盘,光盘,磁带等)
磁盘是一个外设 + 还是我们计算机中唯一一个机械设备。 相比于其它的,这个速度是比较慢的,所以OS一定会有一些提速的方式,后面会说。
磁盘结构
可以先 参考下面的的图片
我们以光盘为例, 我们知道盘面上会存储数据, 但是计算机只认二进制,有什么东西能表示两种状态呢? 磁铁上的南北极!所以向磁盘中写入 本质上就是改变磁盘上的正负性,这个操作是由磁头来完成的.
所以我们的光盘表面上看起来很光滑,实则是由非常多的磁铁组合而成的,微观上来看非常粗糙。
磁盘的存储结构
磁盘上一个盘面那么多数据,那么磁盘是如何知道我想要寻找的数据大致在哪个位置呢?
我们先来了解一下磁盘的存储结构.
盘面上一个个同心圆便是磁道,每个同心圆被划分的一小段区域叫做扇区。扇区是磁盘存储的基本单位。多个盘面半径相同(一个磁盘有多个盘面)的同心圆连起来叫做柱面。
那么在物理上,如何把数据写入指定的扇区呢?
这个问题本质上如何找到一个扇区.
找到一个扇区大概流程如下:
1.首先要先确定在哪一个面上,对应的就是哪一磁头。
2.在哪一个磁道(柱面)
3.在哪一个扇区上.
以上的寻找方法又叫做CHS寻址.有了它,我们便能找到任意一个扇区,即磁盘上所有扇区都能找到了.
磁盘抽象(逻辑,虚拟)结构
我们小时候应该见过磁带吧,那种一拉很长的那种,那时候英语听力基本用一个录音机,然后放上磁带,练习英语听力,就像下面这样:
一个磁带原本物理结构是圆形结构,我们可以把它拉出来,相当于抽象成了一种线性结构
我们可以把这个卷起来的磁带想象成磁盘的盘片,然后把磁盘的盘面抽象成一种线性结构!
这样把物理结构抽象成这样的线性结构,此时想访问一个扇区,只要找到数组的下标,便可以找到对应的扇区,这样的方法叫做LBA寻址法,然后LBA再通过刚才的CHS就成功的把数据写到了磁盘.
例如数组有10万个元素,我们可以划分成1-10000为第一个盘面,10001-20000为第二个盘面,以此类推,在第一个盘面中,又划分成0-1000为第一个磁道,1001-2000为第二个磁道,依次推下去,就完成了抽象结构划分.
所以以上的问题都转化成了下面的:
将数据存储到磁盘 ---> 将数据存储到该数组
找打磁盘中特定扇区的位置 ---> 找到数组特定的位置
对磁盘的管理 ---> 对该数组的管理
但是假如一个磁盘500GB大小,直接管理起来非常耗时,而且复杂。所以我们为了方便管理,我们可以对这500G进行划分空间,也就是分区.
对一个小分区管理好了,其它的分区直接照样用着一套方法即可.
但是100GB还是很大,所以我们还需要继续划分.
此时划分后的每一块叫做块组。每个块组的结构又如下:
它们这些都是什么呢?有什么作用呢,我们来分别讲解:
BootBlock:
先来看第一个BootBlock:
引导块(Boot Block)是特定块组中的第一个块,用于存储 引导加载程序(Bootloader)。引导加载程序是在计算机启动过程中加载并运行操作系统的程序。
引导块的主要目的是启动操作系统,它首先加载操作系统内核或引导程序到内存中,然后传递控制权给内核或引导程序,使其能够继续系统的启动过程。
这个了解一下即可,感兴趣的话可以再深入了解.
Super block
紧接着看一下Super block.
Super block 里面是文件系统的属性信息.
超级块(Super Block)是磁盘块组中的一个特殊数据结构,包含了文件系统的元数据信息,让操作系统能够理解和操作文件系统。它记录了文件系统的整体信息,包括文件系统的类型、大小、块位图和inode位图的位置等重要信息。
超级块通常位于块组的起始位置,它在文件系统格式化时被创建并初始化。超级块记录了文件系统的整体信息,以便操作系统可以理解和操作文件系统。
所以Super block相当于是一个文件系统的"总指挥",扮演着一个非常重要的角色.
Data blocks
然后是Data blocks
这里先说一下:虽然磁盘的基本单位是扇区(512字节),但是操作系统(文件系统)和磁盘进行IO的基本单位是4KB(8*512字节).
也就是说,就算OS想从磁盘中读取一个字节的数据,那么最少也必须读取4KB的数据。至于为什么是4KB,这是经过一些科学的计算最终得出的结论,4KB是最合适的.
如果使用512KB,可能会进行更多次数的IO,造成效率降低,而且如果操作系统使用和磁盘一样的大小,万一磁盘基本大小变化了,OS的源代码也需要改变,所以OS有自己一套的规定,这样就完成了软件和硬件的解耦。
操作系统与磁盘 IO的基本单位是4KB,这也是一个block大小,所以磁盘也一般叫做块设备。
所以Data blocks可以理解为
1.多个4KB(8*扇区)大小的集合.
linux下文件内容 和 属性是 分开存储的。
而2.Data blocks里保存的都是特定文件的内容.
inode Table
接下来说inode Table.
inode是一个大小为128字节的空间,保存的是对应文件的属性。
inode Table是该块组内,所有文件的inode空间的集合,需要标识唯一性,每一个inode块,都要有一个inode编号
一般而言,一个文件,只有一个inode,和一个inode编号.
inode编号属于inode,即inode编号是文件的一个属性.
例如我们在Linux下输入ls -li 便可以看到inode编号(黄色框标识的)
BlcokBitmap
再来看BlcokBitmap.
回想刚才的data blocks,有那么多的block,我们怎么知道哪一个被占用,哪一个没有被占用呢?
这个时候便用到了BlockBitmap,它的内部是一个位图,它的每一个比特位和特定的block是一一对应的,对应的bit位位1, 表示该block已经被占用,否则表示空闲可用。
通过块位图,操作系统可以快速了解文件系统上哪些块是可用的,从而进行块的分配和释放。当需要分配一个块给新的文件或目录时,操作系统会查找位图中的空闲位,并将其设置为已分配状态,然后返回该块的地址 给请求的进程。
inode Bitmap
inode Bitmap也是同样的道理.
有那么多的inode,怎么标识哪个被占用,哪个没有被占用呢?还是用inode Bitmap
它的内部也是一个位图,每一个比特位和 特定的inode是一一对应的,如果该比特位为1,说明该inode已经被占用,否则表示空闲可用。
假设有10000+的inode,那么就有10000+的比特位,分别一一对应,然后用0和1表示未被占用和已占用的两种状态.
Group Descriptor Table
Group Descriptor Table(GDT)叫做块组描述符.
它用来记录这个块组多大,已经使用了多少,有多少个inode,已经占用了多少,还剩多少,一共有多少个block,使用了多少...等等
我们将块组分割成上面的内,并且写入相关的管理数据 ---> 每一个块组都这么干 --->整个分区就被写入了文件系统信息(格式化)!
一个文件只能有一个inode,和一个inode编号.
我们知道,DataBlocks中是通过每个block存储内容的,那么一个文件 可以有多个block吗?答案是肯定可以的,如果只有一个的话,那么每个文件只能存储4KB大小,这也不切合实际。
在文件属性inode中,会有文件的各种属性,其中就包括了一个blocks数组,里面保存了每个block的位置.
那么如果一个文件特别大,有很多block,blocks数组容不下了,此时如何找到每个block呢?
其实我们要知道,一个block块里不仅可以存储文件内容,也可以存储其它块的块号!
比如blocks[在文件属性inode中]只有15个元素大小,我们可以前12个对应的block[磁盘结构中]存储文件内容,后3个对应的block存储的其它block的位置。
一个block是4KB=4096字节大小,存储一个位置只需要4/8字节(32/64位平台),这样一个block就可以存储1000个其它block的位置,如果还是不够,可以取前997个存储内容,后3个存储block的位置,以此类推...
用图大概就是如下这样:
文件名和inode编号
通过上面的讲解,我们知道
找到文件:先找到文件inode编号 ---> 找到分区特定的blcok group ---> 找到inode --->相当于知道了属性 --->根据属性便可以找到文件内容.
可问题是我们怎么知道inode编号呢?我们平时用的都是文件名进行操作,比如创建或删除等,所以我们此时需要弄清楚文件名和inode编号之间的关系.
这里提醒一句:linux中,inode属性里面,没有文件名这一说法!只根据inode编号辨识文件.
有下面两种场景 :
1.在同一个目录下,可以保存很多的文件,但是文件名不能重复.
2.目录是文件,也有自己的inode,也有自己的data block,但问题是目录data block里面存储的是什么呢?
虽然说linux下是按inode编号来识别文件的,但是我们看到就是文件名啊,这些文件名也一定是被管理的。
这里直接说结论:目录里的data block 存储的是inode 编号 和 文件名的映射关系.
它们互为key值的,也就是说即可用inode编号做key值,也可以用文件名做key值.
这也可以立即为什么创建文件,需要目录有写权限,因为目录保存的是文件名与 inode编号映射关系,只有目录有了写权限,,才能将这个映射关系写到磁盘,才能创建成功.
硬链接和软链接
以上说了这么多,只是为了说明软硬链接,因为只有知道了以上这些知识,这个软硬链接才能更好地理解.
先直接说结论:软硬链接的本质区别是:有没有独立的inode编号!
软硬链接详细的用法后面会说,这里先来看这一种现象:
我们首先创建了三个文件, testLink.txt, testLink1.txt, testLink2.txt.
然后此时我们在软连接一个文件softLink.txt,指令如下:
ln -s testLink.txt soft.txt
然后此时我们再硬链接一个文件hardLink.txt,指令如下:
ln testLink1.txt hardLink.txt
然后此时我们ls -li查看文件的其他属性及inode编号:
我们发现:软链接的文件inode编号不同,硬链接的文件inode编号相同。
软链接有独立的inode ---> 软链接是一个独立的文件.
硬链接没有独立的inode ---> 硬链接不是一个独立的文件.
软链接
软链接,也称为符号链接,是一种特殊的文件类型。它包含了指向目标文件或目录的路径,类似于Windows中的快捷方式。软链接创建一个新的文件,其中的内容是指向目标文件或目录的路径。
当访问软链接时,实际上是通过路径引导来访问目标文件或目录。软链接可以跨越文件系统边界,在不同的位置创建链接。
指令:
ln -s 目标文件(被链接的文件) 软链接的文件
比如在Linux下,比如有一个文件在特别深的地方。每次执行它需要写很多路径,非常麻烦。
此时我们便可以为这个文件 在一个方便的路径下创建一个软链接的文件,此时每次你在你方便的路径下执行这个软链接文件便可以执行到源文件了.
同样地,在Windows下,比如我们有一个软件在桌面:
比如说qq,这个qq其实就是一个软链接的文件,然后我们右击qq,然后点击属性:
便可以发现源文件其实在和这个桌面毫不相干的一个地方存着,而桌面上的这个qq其实是它的一个软连接,即快捷方式。
硬链接
我们上面知道了,硬链接不是一个独立的文件,因为inode编号一样.
硬链接(Hard Link)是在文件系统中创建的一种链接,将一个文件关联到另一个文件,使得它们共享相同的物理数据块(数据存储内容)。通过硬链接创建的文件实际上是指向相同的inode(索引节点),因此它们在文件系统中被视为相同的文件。
硬链接之间的文件没有所谓的源文件和副本之分,它们都是平等的硬链接文件。即使原始文件被删除,由于硬链接不依赖于原始文件的存在,硬链接本身仍然可以通过其他链接访问,并且数据内容仍然保留在文件系统中,直到所有链接都被删除才会真正释放。
硬链接实则就是在指定的目录下,建立了文件名和指定inode的映射关系,仅此而已!
我们把这个testLink1删掉了,但是并不影响与之建立硬链接的hardLink.txt,因为inode编号并没有改变,说明文件也没有变化。
但是此时我们发现,第三行原来的数字由2变成1,这个数字叫做硬链接数.就是与某个文件硬链接的文件数量.
这里采用一种叫做引用计数的方法,用来计算和这个文件硬链接的个数count。
当我新增一个硬链接的时候,这个数字count便会+1.
当我删除一个硬链接的时候,并不是把这个文件的inode删除,而是将这个count--,在目录层面,只是将这个文件名与inode编号取消了映射关系。当count == 0的时候,说明此时已经没有文件再关联这个文件了,此时再把这个文件删除,
取消软硬链接unlink
当我们想取消与某个文件的软硬链接的时候(相当于就是删除了,因为取消了链接这个文件本身就没有意义了),便用到了unlink指令
用法如下:
unlink 文件名
还是刚才的例子,我们想取消软链接,于是我们unlink soft.txt:
取消链接之后,原本的链接文件也被删除了.
硬链接也是如此,就不演示了。但是硬链接也只是删除文件名和inode的映射关系,不会删除文件本身,除非像刚才上面说的与之关联的文件数为0的时候,此时便会彻底删除.
当我们创建一个目录的时候,发现它的硬链接数是2:
这是哪两个文件与它建立的硬链接呢?
1.自己的目录名 与 inode编号的映射
2.自己目录的内部 . 也与inode有一个映射. 毕竟这个 . 代表了当前路径.
当我们再在这个目录里,创建一个目录:
发现这个硬链接数变成了3,这又是为什么呢?
这是因为每个文件内部都默认有两个隐藏文件, . 和.. ,分别表示当前路径和上一级路径。
dir1的上一级路径就是dir,所以此时又与dir建立了一个硬链接,使得其数量增加了1.
关于文件系统的知识就到这里结束了,其实平常用到的也不多,但是学会这个对以后我们理解文件会有很大的帮助。
这里还有一个上一章小尾巴,就是stderr,我们再这里补充一下:
stderr
我们前面说了stdin,stdout,stderr,我们知道分别是标准输入,标准输出,以及标准错误。
前两个我们都知道,也演示了,分别代表键盘文件,显示器文件。
那这个标准错误是什么呢?
默认情况下,标准输出和标准错误都会输出到终端(通常是命令行终端),这样用户可以同时看到程序的正常输出和错误信息。
通常来说,程序的正常输出应该发送到标准输出流,而程序的错误信息应该发送到标准错误流。这样可以方便地区分正常输出和错误信息,并且可以将它们分别重定向到不同的位置,例如将错误信息保存到日志文件中而不显示在终端上。
我们看下面的代码:
int main()
{ //stdout -> 1 printf("hello,printf 1\n"); //stderr -> 2 perror("hello,perror 2"); const char* s1 = "hello,write 1\n"; write(1,s1,strlen(s1)); const char* s2 = "hello,write 2\n"; write(2,s2,strlen(s2)); //cout-> 1 std::cout << "hello,cout 1" << std::endl; //cerr-> 2 std::cerr << "hello,cerr 2" <<std::endl; return 0;
}
分别向fd = 1(标准输出)的文件和 fd = 2(标准错误)的文件输出.我们来运行一下看看结果:
这些都正常输出到了终端,没有问题。
如果我们此时重定向到log.txt文件,那么这个文件中也会是相同的结果吗?
当我们把结果重定向到文件中时,终端上直接输出了标准错误,没有标准输出;而显示文件中的内容时,只有标准输出,没有标准错误.
即重定向到文件时,只有标准输出会重定向,而标准错误不会.
这也可以说明:1和2对应的都是显示器文件,但它们两个是不同的,如同认为,同一个显示器文件,被打开了两次.
如果此时我们想把标准输出和错误 分开输出到不同的文件中,该怎么做呢?
./myfile > log.txt 2>err.txt
这句话意思是把./myfile的标准输出 重定向到log.txt文件中,再把2(标准错误) 输出到err.txt中.
此时便有了这两个文件,我们分别查看里面的内容:
这样便分别输出到了不同的文件中.
或者我就想将标准输出和错误 重定向到一个文件中该怎么做?
./myfile > log.txt 2<&1
前面./myfile > log.txt容易理解,将./myfile 的标准输出 输出到log.txt中.
2>&1
: 这是重定向标准错误输出的部分。2
表示标准错误输出的文件描述符。&1
表示标准输出的文件描述符。因此,2>&1
将标准错误输出重定向到与标准输出相同的位置.
此时我们便成功地将标准输入与输出 重定向到同一个文件中了.
那么如何利用重定向拷贝一份文件呢?
cat < log.txt > back.txt
这句代码相当于是把log.txt内容先给cat,cat再重定向到back.txt,这样本质上就完成了一次拷贝.
至此所有的内容就结束了.