深入理解linux中的文件(下)
目录
一、语言级缓冲区和内核级缓冲区
二、C语音中的FILE* fp = fopen(“./file.txt”,"w"):
四、理解磁盘结构:
物理结构
逻辑结构
五、未被打开的文件:
六、更加深入理解inode编号怎么找到文件:
七、对路径结构进行缓存:
1. 路径名解析加速
2. 更新多叉树的路径
3. 进程如何使用 dentry
(1)打开文件
(2)路径遍历
一、语言级缓冲区和内核级缓冲区
编程语言运行在操作系统之上,并利用操作系统的功能来完成各种任务。当我们提到语言级缓冲区时,它实际上是在操作系统“看不见”的应用层面上工作的。
如果没有语言级缓冲区,在C语言中每次使用fwrite
或fprintf
函数写数据时,都会直接调用系统底层的write
函数,这会导致频繁的数据传输,耗费更多时间。为了避免这种情况,我们在程序里设置了语言级缓冲区。这样,当你使用fwrite
或fprintf
时,数据首先存入这个语言级缓冲区。只有当满足一定条件时,才会一次性把大量数据传给操作系统处理。至于什么时候这些数据最终被保存到硬盘上,则由操作系统决定。
语言级缓冲区刷新的方式有三种:
- 对于显示输出(比如打印到屏幕上),每当遇到换行符
\n
就会自动刷新缓冲区。 - 对于普通文件(保存在磁盘上的文件),当缓冲区满了的时候会自动刷新。
- 使用
fflush()
函数可以手动强制刷新缓冲区,立即把数据写出去。
二、C语音中的FILE* fp = fopen(“./file.txt”,"w"):
1. 在当前目录下创建一个file.txt文件,模式是写入。
2. 返回一个FILE结构体指针
二、C语音中的fclose的作用:
-
刷新缓冲区:如果文件是以写模式(如
"w"
或"a"
)打开的,调用fclose()
会首先将缓冲区中的所有未写入数据强制写入到文件中。这确保了所有通过程序写入的数据都被正确地保存到了文件里。 -
释放资源:关闭文件后,
fclose()
会释放分配给该文件流的所有系统资源。这包括内存中的缓冲区和其他与文件操作相关的资源。这对于保持程序的效率和避免资源泄漏非常重要,尤其是在程序需要处理多个文件或长期运行的情况下。例如:销毁FILE结构体
四、理解磁盘结构:
物理结构
盘片(Platters):硬盘内部由一个或多个圆形盘片组成,这些盘片是实际存储数据的地方。每个盘片都有两面,称为记录面。
磁道(Tracks):盘片上的表面被划分为若干个同心圆,每一个这样的圆被称为磁道。所有盘片上相同位置的磁道合在一起形成柱面(Cylinder)。
扇区(Sectors):每个磁道进一步被分割成多个弧段,每个弧段即为一个扇区。标准的扇区大小通常是512字节或4KB(高级格式化技术)。扇区是磁盘读写的最小单位。
柱面(Cylinders):所有盘片上相同编号的磁道共同构成一个柱面。柱面的概念主要用于描述磁盘的三维地址空间。
磁头(Heads):用于读取和写入数据的装置。每个盘片的每一面都有一个对应的磁头。(注意:各个读写磁头不会分开移动,而是由一个移动臂控制所以的磁头,一起前进后退)
由于磁盘的三维的物理结构,把每一个扇区当成一个数组的格子,就可以把他抽象成一个三维数组。而三维数组又可以抽象成一个超大的一维数组。
逻辑结构
分区(Partition):物理磁盘可以被划分成一个或多个逻辑部分,每个部分称为分区。例如,在Windows系统中的C盘和D盘就是不同的分区。每个分区都可以有自己的文件系统,并像独立的磁盘一样使用。
分组(Block Group):在Ext4等Linux文件系统中,每个分区可以进一步划分为多个块组。这种划分有助于提高性能并简化管理。
超级块(Superblock):在linux文件系统中,超级块包含了关于文件系统的元数据,比如块大小、空闲块数量、inode信息等。(重点:Superblock是管理区的,所以不是每一个组中都含有Superblock。如果Superblock损坏了,就找不到几百个G的区了,,所以Superblock也不能只有一个,整个区中会有两三个Superblock作为备份)
组描述符表(Group Descriptor Table):包含组中的信息:
数据块位图(Block Bitmap)的位置和状态,用于跟踪哪些数据块已被使用,哪些仍然空闲。
inode位图(Inode Bitmap)的位置和状态,用于跟踪哪些inode已被分配,哪些未被使用。
- inode表(Inode Table)的位置,其中包含了该块组内所有文件和目录的inode信息。
- 数据块(Data blocks)的数量以及其他相关信息。
数据块位图 和 inode位图:使用二进制0和1表示空闲和占用的情况。
inode表:inode表中包含很多的inode结构体,通常一个inode结构体大小为128字节。
索引节点(Inode):在Unix/Linux文件系统中,每个文件都有一个对应的inode,它存储了文件的元数据(如权限、所有权、大小等)以及指向文件数据块的指针。(重点:文件名不存储到inode结构体中)
数据块:每一个数据块是4KB大小,里面存放文件的内容,当然,数据块,也会存放指针。这些指针指向其他的数据块,也就是一个数据块有1KB的指针。
一级间接块索引表指针,1KB*4KB=4MB大小,就可以存储4MB大小的文件。
二级索引:4KB * 1KB * 1KB = 4GB。
三级索引:4KB * 1KB * 1KB = 4TB。
到了三级索引就够了,一个文件不会有那么大的,太大了我们也会把他拆小。一个文件太大,你内存放不下,也打不开。
五、未被打开的文件:
未被打开的文件都存储在磁盘上。如果你想访问这些文件,你需要知道它们的具体路径。明明在linux,操作系统只认识inode编号,那么文件路径怎么帮助操作系统找到文件的确切位置?
在Linux系统中,每个目录不仅包含文件名,还存储了指向这些文件的inode信息。这意味着目录实际上保存了一个文件名与inode编号之间的映射表。
例如,如果你有一个文件路径为 /home/chen/communication/mypipe.cc
,以下是系统如何找到这个文件的过程:
- 从根目录
/
开始,系统会根据路径进入home
目录。 - 在
home
目录下,系统查找名为chen
的子目录及其对应的inode编号(比如chen:13425
)。这里的13425
是一个示例inode编号,实际编号可能不同。 - 使用这个inode编号,系统可以定位到
chen
目录。然后在chen
目录中继续查找communication
子目录及其inode信息。 - 最终,在
communication
目录中找到mypipe.cc
文件的inode编号,这样就可以读取该文件的内容了。
需要注意的是,根目录 /
的inode是固定的,这保证了系统始终可以从根目录开始导航至任何文件或目录。这种机制使得即使文件系统非常庞大,也可以高效地定位和管理文件。
通过这种方式,Linux文件系统能够有效地管理和检索文件,同时保持数据的一致性和完整性。
六、更加深入理解inode编号怎么找到文件:
当我们获得了某个文件的inode编号(例如13425),接下来需要定位并读取该inode的具体信息来访问文件的内容和属性。
-
查找组描述符表:首先,系统会根据inode编号确定该inode属于哪个块组。然后从这个块组的组描述符表中获取相关信息。组描述符表包含了关于该块组的重要信息,比如起始inode编号(例如13400)和结束inode编号(例如13700)。
-
计算相对位置:知道了块组的起始inode编号(如13400),我们可以用目标inode编号减去起始inode编号(即13425 - 13400 = 25)。这样我们就得到了目标inode在该块组内的相对位置——这里是第25个inode。
-
访问inode表:接着,系统会在该块组的inode表中查找第25号区域中的inode结构体。每个inode结构体通常包含文件的元数据(如权限、所有权、大小等)以及指向文件数据块的指针。
-
读取文件内容和属性:一旦找到了正确的inode结构体,我们就可以使用其中的信息来定位文件的数据块,进而读取文件的实际内容及其属性。
通过这种方式,即使文件系统非常庞大,操作系统也能高效地定位和访问任何文件。
七、对路径结构进行缓存:
当我们访问文件时,并不是每次都从根目录开始逐层查找,因为这样的效率非常低——实际上,大约99%的文件很少被打开。为了解决这个问题,我们使用多叉树的方式来缓存路径结构。相比直接使用目录结构体,这种多叉树结构更简单,查找速度也更快。
1. 路径名解析加速
当一个进程尝试访问某个文件时,它提供的是文件的路径名。内核需要将这个路径名转换为具体的 inode
。通过使用 dentry
结构体,内核可以快速找到对应路径的 inode
,而不需要每次都从磁盘读取信息。多叉树的每个节点都是一个 dentry
结构体,定义如下:
struct dentry {
char *name; // 节点名称
struct inode *inode; // 指向inode的指针
struct dentry *parent; // 父节点指针
struct list_head children; // 子节点链表头
};
2. 更新多叉树的路径
当访问一个新的路径时,如果该路径不存在于当前缓存中,则需要创建新的 dentry
节点,并将其添加到正确的父节点的子节点列表中。同时更新父节点的相关信息,例如增加子节点计数等。
3. 进程如何使用 dentry
(1)打开文件
当一个进程调用 open()
系统调用打开一个文件时,内核会首先通过文件结构体中的 dentry指针,进行查找dentry
缓存中查找该文件的路径。如果找到了,就可以直接获取对应的 inode
和文件描述符;如果没有找到,则需要从磁盘读取相关信息并更新缓存。
(2)路径遍历
对于任何涉及路径的操作(如 stat()
, unlink()
等),内核都会通过 dentry
结构进行路径名解析。这通常涉及到从根目录开始逐步向下遍历直到找到目标文件或目录。