目录
1.dentry
(1)路径缓存的原因
(2)dentry的结构
①多叉树结构
②file和dentry之间的联系
③路径概念存在的意义
2.分区
(1)为什么要确认分区
(2)挂载
①进入分区
②被挂载的目录文件
(3)模拟创建分区
①创建特定大小的文件
②模拟创建分区
③挂载目录
④卸载分区
(4)总结
1.dentry
(1)路径缓存的原因
定位文件了实际上就是根据文件路径的逆向解析,从根目录(根目录的inode编号、名称等都是特殊固定的)开始一直按解析的路径依次打开目录文件,最终找到我们要访问的文件。每个文件都要有路径,而路径是进程告诉系统的(如cwd可以协助提供绝对路径)。但如果我们对同级目录的文件多次访问,每次访问都会不断重新从根目录打开文件,这样效率太低了。因此我们需要路径缓存
(2)dentry的结构
①多叉树结构
每个dentry都含有一个inode指针,指向内存中缓存的inode结构体。当系统启动的时候,根目录的dentry被最先创建。
每个文件都最多有一个dentry,整个系统的dentry链接为一棵多叉树
当进行任何路径访问时,会根据我们要访问的路径进入内存级dentry树里面找,如果找到了就直接根据inode信息打开文件;如果找不到,则会从能找到的结点(目录)开始,读取该节点内容到内存中,根据文件名找到对应的inode编号,再去创建file、缓存inode、创建dentry结点到内存中,并按树状连接到根,进入这个新的结点,以此类推直到找到文件。当找到路径最后指向的文件后就终止。此时我们可以得到fd并进行文件操作。
注意,path的存在是有意义的,它记录了根目录的dentry指针等信息,保证每次创建dentry时能够正确地连接到同一颗dentry多叉树里面,并对当前结点dentry进行管理。
我们不需要担心多叉树越来越大,因为Linux的内核链表允许dentry中的结点链入其它链表中。
②file和dentry之间的联系
我们的文件系统在硬件侧的逻辑就是fd -> file -> path -> dentry -> inode -> 根据inode结合GDT、超级块等读取Data blocks数据到内存 -> 根据内存已有数据决定新修改内容如何存储 -> 结合inode、GDT、超级块以数据块为最小单位写回磁盘 -> 更新管理信息。
其中dentry的作用就是帮助我们快速找到文件,当我们想要从fd访问文件时,我们只需要在dentry里面进行内存级访问、查找文件inode即可。第一次查找时可能会慢,因为要一直读取目录文件的内容(硬盘级操作),根据上级目录逐级创建dentry、inode结点,但第二次、第三次访问就可以很快的从根dentry找到自己重复访问的目录文件的inode(内存级操作),进一步快速打开文件。
③路径概念存在的意义
通过上面的学习,我们发现Linux需要对路径结构进行缓存。事实上,在磁盘上并没有路径的概念,也不存在目录文件和普通文件的区别,它只需要按照自己的结构存数据即可。路径的概念是操作系统建立的,路径本质就是一个针对硬盘存储进行管理的结构,Linux任何对路径的操作本质都是针对dentry的查找,构建等操作,dentry再和文件直接打交道。
一句话总结就是:硬盘的物理结构不存在路径,但系统的逻辑结构构建出了路径,dentry是提高路径访问效率的协助者。
2.分区
(1)为什么要确认分区
我们的dentry讲解中存在一个巨大的漏洞,那就是文件的inode是以分区为界限的。同一个操作系统能访问不同分区,这就使得inode编号的唯一性被打破了,我们又如何能使用inode的唯一性来查找文件呢?进一步讲,进程的路径真的只有我们理解的那样层层目录文件包含的关系吗?
问题的根源来自分区的确定,我们只要确定了分区,上面的问题就都能解释了。
(2)挂载
认识一下分区的查看
对于整个操作系统而言,尽管存在多个分区,但只有一个分区包含根目录,系统最开始就要加载根目录到内存中,创建dentry,后续的访问也都是根据根目录文件的内容来逐渐建立dentry多叉树的。但是这就再次陷入了刚才的问题,因此就必须引入挂载的概念。
①进入分区
如果不能进入分区,这块分区相当于不可用,因为根目录所在分区不能直接访问其它分区,这会导致inode混乱。我们要如何进入一个分区?
我们已经知道路径访问文件的途中本质就是不断打开目录文件,那么我们是否可以将分区和目录文件联系起来呢?可以的,我们称为把一个分区挂载到目录上。什么意思呢?就是说当我们将一个分区挂载到目录上后,这个目录会特殊处理,当我们打开这个目录文件时,系统不会真的去打开这个目录文件,而是直接进入了挂载的分区的根目录。
我们可以查看磁盘的挂载情况(Mounted on),其中我们发现/dev/cda1挂载到了根目录下,这也就意味着当我们打开根目录时,会直接进入dev1分区的根目录。进一步讲,当系统启动时,就会将系统文件所在分区挂载到根目录下。当访问根目录时,理所应当的就进入了根目录分区了。
②被挂载的目录文件
我们可以很简单地认为被挂载的目录文件就是很普通的目录文件,它有实体、有大小、有属性、inode也没什么特别的。就是因为它被我们用分区挂载了,所以它被识别为了一个挂载点,当我们访问时系统会特殊处理,不进入目录文件,而是进入挂载分区的根目录。
通过上面的知识,我们可以知道我们所见的根目录其实也是被分区挂载了一次、跳转了的,实际上的根目录并不在任何分区里,只不过这里涉及到的内核最底层,我们了解即可
当被挂载后,原来的目录文件不会有任何影响,只不过我们没办法打开它了,因为系统相当于为这个目录文件做了掩护,针对挂载点特殊处理了。我们看似是访问了这个目录文件,实际上是访问到了另外一个分区的根目录。这也提醒我们最好使用空目录来挂载分区,因为挂载后原来目录的内容会被隐藏。当然如果我们取消挂载,这个目录文件的内容依然可以正常显示,不会受到任何影响。
(3)模拟创建分区
我们可以通过模拟创建一个分区来加深对挂载的认识
①创建特定大小的文件
我们先创建一个普通文件,用来模拟一个分区。这里介绍一个指令dd,它可以快速构建一个特定大小的文件。它的主要功能是块级别复制,也就是说它会直接按照硬盘的数据块(自己指定,不一定是4KB)复制数据,而不会兼顾文件系统。这就会导致不同文件系统之间使用dd会存在兼容性问题,因为不同文件系统的管理、解读数据方式不同。这也就意味着dd适合磁盘备份、启动盘备份等操作,文件级别的复制还是cp更合适。
②模拟创建分区
我们得到一个文件后,要对它进行初始化,按照特定文件系统ext的格式将里面的GDT、blocks、bitmap等进行初始化,让它长得像一个分区
mkfs.ext可以快速帮我们格式化一个ext文件系统,会按照ext2的格式结合我们文件的大小自动将分区分为不同的块,初始化超级块等操作。
③挂载目录
我们可以进一步查看分区状况
之后我们便能正常使用这个模拟分区
④卸载分区
我们可以使用umount卸载分区,当用户正在使用分区时,或当用户的工作路径包含这个分区时,我们不能卸载这个分区,只有完全退出才可以操作。这也能解释我们永远无法卸载根目录,因为根目录永远在我们的cwd中,我们无法退出。
如果我们再挂载一次,会发现原来模拟分区里面的内容一点都没有修改,原来的文件和目录都还在
这进一步证明了挂载和删除挂载点都只是系统层面为解决不同分区访问的特殊处理,挂载操作本身不会对文件做出任何处理。当没有挂载时,分区存在,数据存在,只不过只有挂载之后,我们才有机会从Linux系统层面对它从根目录进行访问。
(4)总结
当我们cd进入某一个目录里时,有可能我们根本没有进入对应的目录文件(目录文件真实存在),而实际上进入了一个分区,这是通过挂载点进行特殊实现的。只有当分区挂载到某个路径,我们才能通过路径的形式访问分区。
路径 = 挂载点(分区) + 该分区创建的目录,Shell进程的属性保存这些信息,即时判断当前的分区,针对性的解读inode或是根据文件系统进行不同操作即可。
所有路径都是从根目录开始挂载的,甚至我们看到的根目录也是经过一层挂载的。