目录
一、文件系统的基本组成
文件系统是操作系统中负责管理持久数据的子系统,说简单点,就是负责把用户的文件存到磁盘硬件中,因为即使计算机断电了,磁盘里的数据并不会丢失,所以可以持久化的保存文件。
文件系统的基本数据单位是文件,它的目的是对磁盘上的文件进行组织管理,那组织的方式不同,就会形成不同的文件系统。
Linux 最经典的一句话是:「一切皆文件」,不仅普通的文件和目录,就连块设备、管道、socket 等,也都是统一交给文件系统管理的。
文件 = 文件内容 + 属性,对文件的操作无外乎对内容和对属性。
文件在磁盘上放着,向磁盘(硬件)中写入,只有OS系统有权利,所以普通用户想向磁盘中写入数据必须通过OS,由OS提供的文件类的系统调用接口。
我们访问文件,先写代码 - 编译 - exe - 运行成为进程来访问文件。
文件分为被打开的文件(内存文件),和在磁盘中没有被打开的文件(磁盘文件)。
二、被打开的文件(被加载到内存中)
2.1 系统调用接口
open,close,read,write
2.1.1 open
mode用来设置新打开的文件的权限
2.1.2 close
2.1.3 write
2.1.4 read
2.2 文件描述符fd
文件要是想要被访问,前提是加载到内存中,才能被直接访问。
一个进程可以打开多个文件,那么OS肯定也需要把文件管理起来。
在内核中,OS为了管理每一个被打开的文件构建struct file结构体
struct file
{
struct file* next;
struct file* prev;
// 包含了一个被打开的文件的几乎所有内容(不仅仅包含属性)
}
创建struct file对象,充当一个被打开的文件,如果有很多呢?再用双链表组织起来。
那么进程是如何找到这些被加载到内存的文件的呢?内存文件有很多,进程怎么知道自己可以访问哪些文件呢?
由图可知,进程被创建后OS会给进程创建PCB结构体,即task_struct结构体,task_struct结构体里有一个struct files_struct类型的指针。
该类型结构体里有struct file * fd_array[NR_OPEN_DEFAULT]指针数组,该指针数组里储存的就是内存文件的struct file的地址,所以可以分析出文件描述符fd就是这个指针数组的数组下标。
所以,拿到文件描述符就能找到对应的文件。进程执行open时,将文件与进程关联起来。
files_struct结构体里的属性和方法
- 文件指针:系统跟踪上次读写位置作为当前文件位置指针,这种指针对打开文件的某个进程来说是唯一的;
- 文件打开计数器:文件关闭时,操作系统必须重用其打开文件表条目,否则表内空间不够用。因为多个进程可能打开同一个文件,所以系统在删除打开文件条目之前,必须等待最后一个进程关闭文件,该计数器跟踪打开和关闭的数量,当该计数为 0 时,系统关闭文件,删除该条目;
- 文件磁盘位置:绝大多数文件操作都要求系统修改文件数据,该信息保存在内存中,以免每个操作都从磁盘中读取;
- 访问权限:每个进程打开文件都需要有一个访问模式(创建、只读、读写、添加等),该信息保存在进程的打开文件表中,以便操作系统能允许或拒绝之后的 I/O 请求;
2.2.1 文件描述符的分派规则
文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
2.2.2 重定向
常见的重定向:输出重定向 > ,追加重定向 >> ,输入重定向 <。
输出重定向:本应该输出到显示器的变成输出到文件里
输入重定向:本应该从键盘中输入变成从文件中输入
那么重定向的本质是什么?
重定向的系统调用函数
文件流,cout cin - 类 - 必须包含 1.fd 2.buffer
cout << -> operator << () 运算符重载就是把()里的缓冲区拷贝到cout类内的buffer里,再定期刷新
2.3 如何理解一切皆文件
Linux的设计哲学,体现在操作系统软件设计的方面
struct file
{
int size;
mode_t mode;
int user;
int group;
// ....
// 函数指针
int (*readp) (int fd,void* buffer,int len);
int (*writep) (int fd,void* buffer,int len);
}
2.4 缓冲区
先提出两个问题
1.什么是缓冲区,就是有一段内存空间(这个空间谁提供的?语言)
2.为什么要有缓冲区
首先介绍两种模式,写透模式(write through),写回模式(write back)
写透模式:数据同时写入缓存和内存,即数据直接写入磁盘,如果频繁的写入数据,会导致频繁的与外设进行IO,降低效率。
写回模式:数据先会写入缓存,当条件满足再统一从缓存刷新到内存。即数据先放入缓冲区,当缓冲区刷新条件满足时再统一发送到磁盘。
缓冲区的刷新策略
1.立即刷新
2.行刷新(行缓冲) \n
3.满缓冲(全缓冲)
特殊情况
1.用户强制刷新(fflush)
2.进程退出
关于缓冲区的认识,一般而言,行缓冲设备-显示器,全缓冲设备-磁盘文件。
所有设备,永远都倾向于全缓冲 -- 缓冲区满了才刷新 -- 需要更少次的IO操作 -- 更少次的外设访问
和外部设备IO的时候,数据量的大小不是主要矛盾,你和外设预备IO的过程是最浪费时间的。
其他刷新情况,是结合其他情况做的妥协
显示器:直接给用户看的,一边要照顾效率,一边要照顾用户体验。
文件操作的标准库是可以实现数据的缓存,那么根据「是否利用标准库缓冲」,可以把文件 I/O 分为缓冲 I/O 和非缓冲 I/O:
- 缓冲 I/O,利用的是标准库的缓存实现文件的加速访问,而标准库再通过系统调用访问文件。
- 非缓冲 I/O,直接通过系统调用访问文件,不经过标准库缓存。
2.5 C标准库封装的FILE结构体
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。
所以C库当中的FILE结构体内部,必定封装了fd。
printf("%d\n",fileno(stdout));
// C语言提供的
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
const char *s = "hello fputs\n";
fputs(s, stdout);
// OS提供的
const char *ss = "hello write\n";
write(1, ss, strlen(ss));
// fflush(stdout);
fork();
运行结果
1
hello printf
hello fprintf
hello fputs
hello write
对进程实现输出重定向 ./test > myfile
运行结果
hello write
1
hello printf
hello fprintf
hello fputs
1
hello printf
hello fprintf
hello fputs
我们发现 printf 和 fwrite 和 fputs(库函数)都输出了2次,而 write 只输出了一次(系统调用)。为什么呢?肯定和fork有关!上面的测试,并不影响系统接口,说明我们说的缓冲区不是由OS维护的,不然我们的代码表现应该是一样的,所以我们得出缓冲区是由C标准库维护的。
1.如果是向显示器打印,那么对应的刷新策略是行刷新,那么最后执行fork的时候,函数已经执行完了,数据已经被刷新了,fork没有起作用。
2.如果你的程序进行了重定向,要向磁盘文件打印,隐形的刷新策略就变成了全缓冲,\n就没有意义了。
fork执行的时候,一定函数已经执行完了,但是数据还没有刷新,在当前进程对应的C标准库中的缓冲区中,这部分数据是父进程的数据。
在fork后生成子进程时,进程结束时缓冲区会进行刷新,在刷新时,子进程会对缓冲区的数据进行写时拷贝。
其实调用 write 接口也不是直接将数据直接就写入外设中,而是内核缓冲区中,至于什么时候刷新内核缓冲区由操作系统自主决定!但是有些信息是非常重要的,需要马上刷新内核缓冲区写入到磁盘文件中。那么此时就需要借助fsync
接口了,该接口可以直接刷新内核缓冲区并将数据写入外设中。
直接与非直接 I/O
我们都知道磁盘 I/O 是非常慢的,所以 Linux 内核为了减少磁盘 I/O 次数,在系统调用后,会把用户数据拷贝到内核中缓存起来,这个内核缓存空间也就是「页缓存」,只有当缓存满足某些条件的时候,才发起磁盘 I/O 的请求。
那么,根据是「否利用操作系统的缓存」,可以把文件 I/O 分为直接 I/O 与非直接 I/O:
- 直接 I/O,不会发生内核缓存和用户程序之间数据复制,而是直接经过文件系统访问磁盘。
- 非直接 I/O,读操作时,数据从内核缓存中拷贝给用户程序,写操作时,数据从用户程序拷贝给内核缓存,再由内核决定什么时候写入数据到磁盘。
如果你在使用文件操作类的系统调用函数时,指定了 O_DIRECT
标志,则表示使用直接 I/O。如果没有设置过,默认使用的是非直接 I/O。
如果用了非直接 I/O 进行写数据操作,内核什么情况下才会把缓存数据写入到磁盘?
以下几种场景会触发内核缓存的数据写入磁盘:
- 在调用
write
的最后,当发现内核缓存的数据太多的时候,内核会把数据写到磁盘上; - 用户主动调用
sync
,内核缓存会刷到磁盘上; - 当内存十分紧张,无法再分配页面时,也会把内核缓存的数据刷到磁盘上;
- 内核缓存的数据的缓存时间超过某个时间时,也会把数据刷到磁盘上;
2.6 自己实现一个缓冲区
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#define NUM 1024
struct MyFILE_
{
int fd;
char buffer[NUM];
int end; // 当前缓冲区结尾
};
typedef struct MyFILE_ MyFILE;
MyFILE* fopen_(const char* pathname,const char* mode)
{
assert(pathname);
assert(mode);
MyFILE* fp = NULL;
if(strcmp(mode,"r") == 0)
{
}
else if(strcmp(mode,"r+") == 0)
{
}
else if(strcmp(mode,"w") == 0)
{
int fd = open(pathname,O_CREAT | O_WRONLY | O_TRUNC,0666);
if(fd >= 0)
{
fp = (MyFILE*)malloc(sizeof(MyFILE));
memset(fp,0,sizeof(MyFILE));
fp->fd = fd;
}
}
else if(strcmp(mode,"w+") == 0)
{
}
else if(strcmp(mode,"a") == 0)
{
}
else if(strcmp(mode,"a+") == 0)
{
}
return fp;
}
// 是不是应该是C标准库中的实现
void fputs_(const char* message,MyFILE* fp)
{
assert(message);
assert(fp);
strcpy(fp->buffer+fp->end,message);
fp->end += strlen(message);
// for debug
// printf("%s\n",fp->buffer);
// 暂时没有刷新,刷新策略是由谁来执行的呢?用户通过执行C标准库中的代码逻辑,来完成刷新动作
// 这里效率提高体现在哪里?因为C提供了缓冲区,那么我们就通过策略,减少了IO执行次数(不是数据量 )
if(fp->fd == 0)
{
}
else if(fp->fd == 1)
{
if(fp->buffer[fp->end - 1] == '\n')
{
fprintf(stderr,"%s",fp->buffer);
write(fp->fd,fp->buffer,fp->end);
fp->end = 0;
}
}
else if(fp->fd == 2)
{
}
else
{
}
}
void fflush_(MyFILE* fp)
{
assert(fp);
if(fp->end != 0)
{
// 暂且认为刷新了--其实是把数据写到了内核里 sync把数据刷到磁盘
write(fp->fd,fp->buffer,fp->end);
syncfs(fp->fd);// 将数据写到磁盘
fp->end = 0;
}
}
void fclose_(MyFILE* fp)
{
assert(fp);
fflush_(fp);
close(fp->fd);
free(fp);
}
int main()
{
close(1);
MyFILE* fp = fopen_("log.txt","w");
fputs_("hello one",fp);
sleep(1);
fputs_("hello two\n",fp);
sleep(1);
fputs_("hello three\n",fp);
sleep(1);
fputs_("hello four\n",fp);
sleep(1);
fclose_(fp);
return 0;
}
三、没有被打开的文件(磁盘文件) - 文件系统
3.1 磁盘
内存:掉电易失存储介质
磁盘:永久性存储介质 - SSD,U盘,flash卡,光盘
磁盘结构:磁盘盘片,磁头,伺服系统,音圈马达
盘面上会储存数据 - 计算机只认二进制 - 磁头向磁盘写入,本质是改变磁盘上的正负极
在物理上,如何把数据写入到指定扇区?
CHS寻址
1.C - cylinder 在哪一个柱面上
2.H - head 在哪一个面上(对应在哪一个磁头上)
3.S - sector 在哪一个扇区上
磁盘存储数据的基本单位 - 硬件上的 512KB,一个扇区的大小
3.2 虚拟文件系统(VFS)
文件系统的种类众多,而操作系统希望对用户提供一个统一的接口,于是在用户层与文件系统层引入了中间层,这个中间层就称为虚拟文件系统(Virtual File System,VFS)。
VFS 定义了一组所有文件系统都支持的数据结构和标准接口,这样程序员不需要了解文件系统的工作原理,只需要了解 VFS 提供的统一接口即可。
在 Linux 文件系统中,用户空间、系统调用、虚拟文件系统、缓存、文件系统以及存储之间的关系如下图:
Linux 支持的文件系统也不少,根据存储位置的不同,可以把文件系统分为三类:
- 磁盘的文件系统,它是直接把数据存储在磁盘中,比如 Ext 2/3/4、XFS 等都是这类文件系统。
- 内存的文件系统,这类文件系统的数据不是存储在硬盘的,而是占用内存空间,我们经常用到的
/proc
和/sys
文件系统都属于这一类,读写这类文件,实际上是读写内核中相关的数据。 - 网络的文件系统,用来访问其他计算机主机数据的文件系统,比如 NFS、SMB 等等。
文件系统首先要先挂载到某个目录才可以正常使用,比如 Linux 系统在启动时,会把文件系统挂载到根目录。
3.3 ext2文件系统
Linux ext2文件系统,上图为磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,硬盘分区被划分为一个个的block。一个block的大小是由格式化的时候确定的,并且不可以更改。例如mke2fs的-b选项可以设定block大小为1024、2048或4096字节。而上图中启动块(Boot Block)的大小是确定的。
size表示390字节。blocks是以512字节为单位的块的数量。IO Block是文件系统的最小寻址单元,操作系统(文件系统)和磁盘进行IO的基本单位,4KB(8*512byte)。
为什么操作系统的最小寻址单元不以512字节为单位呢?
1.太小了,有可能会导致多次IO,导致效率降低。
2.如果操作系统和磁盘使用一样的大小,万一磁盘的大小变了呢?操作系统的最小寻址单元是不是也要改,操作系统的源代码也要改?不是一样的就可以实现硬件和软件的解耦。
3.4 文件系统的结构
Linux管理磁盘文件,是将文件内容和文件属性分开管理的。
BOOT BLOCK:启动块,其实只有一份就够了,但事实上在每个分区都可能有一份,这是为了备份,防止磁盘被刮花导致无法启动。电脑蓝屏修复就是把其他地方有备份的数据写过来.
Super Block:超级块,包含的是文件系统的重要信息,比如 inode 总个数、块总个数、已使用与未使用inode和块的数量,每个块组的 inode 个数、每个块组的块个数等等。
Group Descriptor Table:块组描述符,包含文件系统中各个块组的状态,比如块组中空闲块和 inode 的数目等,每个块组都包含了文件系统中「所有块组的组描述符信息]。就是记录block bitmap和inode bitmap。
Block Bitmap:用于表示对应的数据块是空闲的,还是被使用中。inode Bitmap:用于表示对应的inode 是空闲的,还是被使用中。
inode Table:inode 列表,包含了块组中所有的 inode,inode 用于保存文件系统中与各个文件和目录相关的所有元数据。inode是一个大小为128字节的空间,保存的是对应文件的属性,该块组内,所有文件的inode空间的集合,需要标识唯一性,每一个inode块,都要有一个inode编号!一般而言一个文件,一个inode,一个inode编号
Data blocks:数据块,包含文件的有用数据。多个4KB(扇区*8)大小的集合,保存的都是特定文件的内容。
块组被分割成了上面的内容,并且写入相关的管理数据 -> 每一个块组都这么干 -> 整个分区就被写入了文件系统信息,这就是格式化。
你可以会发现每个块组里有很多重复的信息,比如超级块和块组描述符表,这两个都是全局信息,而且非常的重要,这么做是有两个原因:
- 如果系统崩溃破坏了超级块或块组描述符,有关文件系统结构和内容的所有信息都会丢失。如果有冗余的副本,该信息是可能恢复的。
- 通过使文件和管理数据尽可能接近,减少了磁头寻道和旋转,这可以提高文件系统的性能。
不过,Ext2 的后续版本采用了稀疏技术。该做法是,超级块和块组描述符表不再存储到文件系统的每个块组中,而是只写入到块组 0、块组 1 和其他 ID 可以表示为 3、 5、7 的幂的块组中。
3.5 文件的存储方式
非连续空间存放方式
非连续空间存放方式分为「链表方式」和「索引方式」。
我们先来看看链表的方式。
链表的方式存放是离散的,不用连续的,于是就可以消除磁盘碎片,可大大提高磁盘空间的利用率,同时文件的长度可以动态扩展。根据实现的方式的不同,链表可分为「隐式链表」和「显式链接」两种形式。
文件要以「隐式链表」的方式存放的话,实现的方式是文件头要包含「第一块」和「最后一块」的位置,并且每个数据块里面留出一个指针空间,用来存放下一个数据块的位置,这样一个数据块连着一个数据块,从链头开始就可以顺着指针找到所有的数据块,所以存放的方式可以是不连续的。
隐式链表的存放方式的缺点在于无法直接访问数据块,只能通过指针顺序访问文件,以及数据块指针消耗了一定的存储空间。隐式链接分配的稳定性较差,系统在运行过程中由于软件或者硬件错误导致链表中的指针丢失或损坏,会导致文件数据的丢失。
如果取出每个磁盘块的指针,把它放在内存的一个表中,就可以解决上述隐式链表的两个不足。那么,这种实现方式是「显式链接」,它指把用于链接文件各数据块的指针,显式地存放在内存的一张链接表中,该表在整个磁盘仅设置一张,每个表项中存放链接指针,指向下一个数据块号。
对于显式链接的工作方式,我们举个例子,文件 A 依次使用了磁盘块 4、7、2、10 和 12 ,文件 B 依次使用了磁盘块 6、3、11 和 14 。利用下图中的表,可以从第 4 块开始,顺着链走到最后,找到文件 A 的全部磁盘块。同样,从第 6 块开始,顺着链走到最后,也能够找出文件 B 的全部磁盘块。最后,这两个链都以一个不属于有效磁盘编号的特殊标记(如 -1 )结束。内存中的这样一个表格称为文件分配表(File Allocation Table,FAT)。
由于查找记录的过程是在内存中进行的,因而不仅显著地提高了检索速度,而且大大减少了访问磁盘的次数。但也正是整个表都存放在内存中的关系,它的主要的缺点是不适用于大磁盘。
比如,对于 200GB 的磁盘和 1KB 大小的块,这张表需要有 2 亿项,每一项对应于这 2 亿个磁盘块中的一个块,每项如果需要 4 个字节,那这张表要占用 800MB 内存,很显然 FAT 方案对于大磁盘而言不太合适。
接下来,我们来看看索引的方式。
链表的方式解决了连续分配的磁盘碎片和文件动态扩展的问题,但是不能有效支持直接访问(FAT除外),索引的方式可以解决这个问题。
索引的实现是为每个文件创建一个「索引数据块」,里面存放的是指向文件数据块的指针列表,说白了就像书的目录一样,要找哪个章节的内容,看目录查就可以。
另外,文件头需要包含指向「索引数据块」的指针,这样就可以通过文件头知道索引数据块的位置,再通过索引数据块里的索引信息找到对应的数据块。
创建文件时,索引块的所有指针都设为空。当首次写入第 i 块时,先从空闲空间中取得一个块,再将其地址写到索引块的第 i 个条目。
索引的方式优点在于:
- 文件的创建、增大、缩小很方便;
- 不会有碎片的问题;
- 支持顺序读写和随机读写;
由于索引数据也是存放在磁盘块的,如果文件很小,明明只需一块就可以存放的下,但还是需要额外分配一块来存放索引数据,所以缺陷之一就是存储索引带来的开销。
如果文件很大,大到一个索引数据块放不下索引信息,这时又要如何处理大文件的存放呢?我们可以通过组合的方式,来处理大文件的存。
先来看看链表 + 索引的组合,这种组合称为「链式索引块」,它的实现方式是在索引数据块留出一个存放下一个索引数据块的指针,于是当一个索引数据块的索引信息用完了,就可以通过指针的方式,找到下一个索引数据块的信息。那这种方式也会出现前面提到的链表方式的问题,万一某个指针损坏了,后面的数据也就会无法读取了。
还有另外一种组合方式是索引 + 索引的方式,这种组合称为「多级索引块」,实现方式是通过一个索引块来存放多个索引数据块,一层套一层索引,像极了俄罗斯套娃是吧。其他的索引数据块是由data block扮演的,data block不只能存储文件数据,还能存储其他块的块号。
一个文件只能对应一个inode属性节点。
struct inode {
struct hlist_node i_hash; /* 哈希表 */
struct list_head i_list; /* 索引节点链表 */
struct list_head i_dentry; /* 目录项链表 */
unsigned long i_ino; /* 节点号 */
atomic_t i_count; /* 引用记数 */
umode_t i_mode; /* 访问权限控制 */
unsigned int i_nlink; /* 硬链接数 */
uid_t i_uid; /* 使用者id */
gid_t i_gid; /* 使用者id组 */
kdev_t i_rdev; /* 实设备标识符 */
loff_t i_size; /* 以字节为单位的文件大小 */
struct timespec i_atime; /* 最后访问时间 */
struct timespec i_mtime; /* 最后修改(modify)时间 */
struct timespec i_ctime; /* 最后改变(change)时间 */
unsigned int i_blkbits; /* 以位为单位的块大小 */
unsigned long i_blksize; /* 以字节为单位的块大小 */
unsigned long i_version; /* 版本号 */
unsigned long i_blocks; /* 文件的块数 */
unsigned short i_bytes; /* 使用的字节数 */
spinlock_t i_lock; /* 自旋锁 */
struct rw_semaphore i_alloc_sem; /* 索引节点信号量 */
struct inode_operations *i_op; /* 索引节点操作表 */
struct file_operations *i_fop; /* 默认的索引节点操作 */
struct super_block *i_sb; /* 相关的超级块 */
struct file_lock *i_flock; /* 文件锁链表 */
struct address_space *i_mapping; /* 相关的地址映射 */
struct address_space i_data; /* 设备地址映射 */
struct dquot *i_dquot[MAXQUOTAS]; /* 节点的磁盘限额 */
struct list_head i_devices; /* 块设备链表 */
struct pipe_inode_info *i_pipe; /* 管道信息 */
struct block_device *i_bdev; /* 块设备驱动 */
unsigned long i_dnotify_mask; /* 目录通知掩码 */
struct dnotify_struct *i_dnotify; /* 目录通知 */
unsigned long i_state; /* 状态标志 */
unsigned long dirtied_when; /* 首次修改时间 */
unsigned int i_flags; /* 文件系统标志 */
unsigned char i_sock; /* 可能是个套接字吧 */
atomic_t i_writecount; /* 写者记数 */
void *i_security; /* 安全模块 */
__u32 i_generation; /* 索引节点版本号 */
union {
void *generic_ip; /* 文件特殊信息 */
} u;
我们再继续思考一个问题:一个inode大小是 128bytes , 而记录一个 block 号码要花掉 4byte(int) ,假设我一个文件有 400MB 且每个 block 为 4K 时, 那么至少也要十万笔 block 号码的记录呢!这点数据空间是远远不够讲inode和十万笔data blk号相关联的。因此,操作系统讲inode的数据块指针做如下划分:数据块指针数组共有 15 项,前 12 个为直接块指针,后 3 个分别为“一次 间接块指针”、“二次间接块指针”、“三次间接块指针”。
所谓“直接块”,是指该块直接用来存储文件的数据,而“一次间接块”是指该块不存 储数据,而是存储直接块的地址,同样,“二次间接块”存储的是“一次间接块”的地址。 这里所说的块,指的都是物理块。
所以,这种方式能很灵活地支持小文件和大文件的存放:
- 对于小文件使用直接查找的方式可减少索引数据块的开销;
- 对于大文件则以多级索引的方式来支持,所以大文件在访问数据块时需要大量查询;
这个方案就用在了 Linux Ext 2/3 文件系统里,虽然解决大文件的存储,但是对于大文件的访问,需要大量的查询,效率比较低。
为了解决这个问题,Ext 4 做了一定的改变,具体怎么解决的,本文就不展开了
3.6 空闲空间管理
位图法
位图是利用二进制的一位来表示磁盘中一个盘块的使用情况,磁盘上所有的盘块都有一个二进制位与之对应。
当值为 0 时,表示对应的盘块空闲,值为 1 时,表示对应的盘块已分配。它形式如下:
1111110011111110001110110111111100111 ...
在 Linux 文件系统就采用了位图的方式来管理空闲空间,不仅用于数据空闲块的管理,还用于 inode 空闲块的管理,因为 inode 也是存储在磁盘的,自然也要有对其管理。
3.7 目录的存储
在前面,我们知道了一个普通文件是如何存储的,但还有一个特殊的文件,经常用到的目录,它是如何保存的呢?
基于 Linux 一切皆文件的设计思想,目录其实也是个文件,你甚至可以通过 vim
打开它,它也有 inode,inode 里面也是指向一些块。
和普通文件不同的是,普通文件的块里面保存的是文件数据,而目录文件的块里面保存的是目录里面一项一项的文件信息。
在目录文件的块中,最简单的保存格式就是列表,就是一项一项地将目录下的文件信息(如文件名、文件 inode、文件类型等)列在表里。
列表中每一项就代表该目录下的文件的文件名和对应的 inode,通过这个 inode,就可以找到真正的文件。
通常,第一项是「.
」,表示当前目录,第二项是「..
」,表示上一级目录,接下来就是一项一项的文件名和 inode。
如果一个目录有超级多的文件,我们要想在这个目录下找文件,按照列表一项一项的找,效率就不高了。
于是,保存目录的格式改成哈希表,对文件名进行哈希计算,把哈希值保存起来,如果我们要查找一个目录下面的文件名,可以通过名称取哈希。如果哈希能够匹配上,就说明这个文件的信息在相应的块里面。
Linux 系统的 ext 文件系统就是采用了哈希表,来保存目录的内容,这种方法的优点是查找非常迅速,插入和删除也较简单,不过需要一些预备措施来避免哈希冲突。
目录查询是通过在磁盘上反复搜索完成,需要不断地进行 I/O 操作,开销较大。所以,为了减少 I/O 操作,把当前使用的文件目录缓存在内存,以后要使用该文件时只要在内存中操作,从而降低了磁盘操作次数,提高了文件系统的访问速度。
目录文件的数据块中,储存的是[文件名:inode]编号的映射关系,互为key值。回答了为什么linux中访问文件必须采用绝对路径或者相对路径,关键是找到要访问文件的目录。
为什么系统里还有空间但是创建文件失败了?
inode是固定的,data block是固定的。就会出现inode还有但是data block不存在了和data block还有但是inode不存在的情况。
删除文件时,只是将inode bitmap 和 block bitmap上的二进制位置为0
3.8 软连接和硬链接
有时候我们希望给某个文件取个别名,那么在 Linux 中可以通过硬链接(Hard Link) 和软链接(Symbolic Link) 的方式来实现,它们都是比较特殊的文件,但是实现方式也是不相同的。
硬链接是多个目录项中的「索引节点」指向一个文件,也就是指向同一个 inode,但是 inode 是不可能跨越文件系统的,每个文件系统都有各自的 inode 数据结构和列表,所以硬链接是不可用于跨文件系统的。由于多个目录项都是指向一个 inode,那么只有删除文件的所有硬链接以及源文件时,系统才会彻底删除该文件。
软链接相当于重新创建一个文件,这个文件有独立的 inode,但是这个文件的内容是另外一个文件的路径,所以访问软链接的时候,实际上相当于访问到了另外一个文件,所以软链接是可以跨文件系统的,甚至目标文件被删除了,链接文件还是在的,只不过指向的文件找不到了而已。