这里先接着《基础IO 上》中的缓冲区的内容作些补充,这里主要补充 dup2 接口。
✔ 测试用例一:
#include
#include
#include
#include
#include
{close(1);int fd &#61; open("log.txt", O_WRONLY|O_CREAT, 0644);if(fd < 0){perror("open");return 1; }fprintf(stdout, "hello world!: %d\n", fd);close(fd);return 0;
}
close 1 后&#xff0c;1 就不再表示显示器文件&#xff0c;而 open log.txt 后&#xff0c;1 就表示 log.txt 文件&#xff0c;所以 fprintf 并不会往显示器上输出&#xff0c;而是会往 log.txt 里输出&#xff0c;可是 log.txt 中没有内容。通常数据流动过程是&#xff1a;先把语言上的数据写到用户层的缓冲区 ➡ 然后数据通过文件描述符在操作系统内核中&#xff0c;找到自己的 task_struct ➡ 然后通过 task_struct 中的 struct files_struct* files 指针找到 struct files_struct 中 struct files* fd_array[] 以文件描述符为下标的位置 ➡ 然后再通过下标的内容找到要写的 struct_file&#xff0c;并把用户层缓冲区的数据拷贝到内核层缓冲区 ➡ 操作系统再由自己的刷新策略和时机通过磁盘驱动刷新到磁盘设备。注意因为用的是 C&#xff0c;所以这里的用户层缓冲区是 C 提供的&#xff0c;如果是其它语言&#xff0c;那么用的缓冲区就是其它语言提供的。所以之所以操作系统没有由用户层把数据刷新到内核层是因为现在 1 指向的是磁盘文件。显示器是行刷新策略&#xff0c;磁盘是全缓冲策略&#xff0c;这两种策略既可以被用户层采纳&#xff0c;也可以被内核层采纳。
为什么语言都要在用户层提供一个缓冲区&#xff0c;printf 直接把数据刷新到内核缓冲区不行吗 ❓
上层只要把数据写到用户层缓冲区中就不用管了&#xff0c;剩下的就由操作系统来完成&#xff0c;所以对用户来讲&#xff0c;就完成了用户层和内核层之间的完全解耦。而用户要自己拷贝数据到内核层&#xff0c;还需要提升权限&#xff0c;效率太低。
所以用户层中存在缓冲区可以让用户和底层之间的差异屏蔽掉&#xff0c;以此来提升效率。同理内核层中存在缓冲区也有着解耦、提高效率的意义。
✔ 测试用例二&#xff1a;
#include
#include
#include
#include
#include
#include
{//c callprintf("hello printf\n");fprintf(stdout, "hello fprintf\n");fputs("hello fputs\n", stdout);//system callconst char* msg &#61; "hello write\n";write(1, msg, strlen(msg));fork();return 0;
}
这里的 fork 看起来没有什么价值&#xff0c;也不会影响到什么&#xff0c;fork 之后&#xff0c;父子进程马上就退出了。
但是当我们重定向后&#xff0c;就会出现很诡异的现象。
我们发现往显示器上输出的结果是合理的&#xff0c;但是往普通文件上输出的结果却很诡异&#xff0c;它输出了 7 条信息&#xff0c;且使用 C 语言接口的都输出了两次&#xff0c;使用系统调用接口的输出了一次。毫无疑问这种现象是和随手写的 fork 是相关联的&#xff0c;因为去掉 fork 再重定向是合理的。
这里先考虑 C 语言接口&#xff0c;我们都知道&#xff0c;这里输出的三条数据并不会直接写到操作系统里&#xff0c;而是先写到 C 语言的缓冲区中&#xff0c;fork 之后&#xff0c;程序就分流了&#xff0c;但不幸的是&#xff0c;程序马上要退出了&#xff0c;而 C 语言缓冲区中的数据也要被刷新&#xff0c;此时父子进程谁先运行&#xff0c;谁就先刷新&#xff0c;而缓冲区中的数据也是数据&#xff0c;即使是 C 语言的数据&#xff0c;也不能凌驾于进程之上&#xff0c;当父或子想刷新时&#xff0c;那么立马要发生写时拷贝。至此&#xff0c;我们就能理解重定向后&#xff0c;刷新策略由行刷新变为全缓冲&#xff0c;也就是说 fork 时&#xff0c;数据还在 C 缓冲区中&#xff0c;所以重定向后&#xff0c;C 接口的数据输出了两份&#xff1b;而向显示器输出时&#xff0c;因为显示器的刷新策略是行刷新&#xff0c;且这里的每条数据都有 \n&#xff0c;所以每执行完 printf&#xff0c;数据就立马刷新出来&#xff0c;最后 fork 时便无意义了。
而重定向后&#xff0c;系统接口没有受影响的原因是 write 会绕过语言层缓冲区&#xff0c;写到内核层缓冲区&#xff0c;而其实只要是数据都要写时拷贝&#xff0c;但大部分情况只针对用户数据&#xff0c;对于内核数据&#xff0c;数据属于操作系统不会写时拷贝&#xff0c;属于进程会写时拷贝&#xff0c;但这种情况很少考虑&#xff0c;现在我们就认为写时拷贝主要拷贝的是用户数据。
通常我们不建议所语言接口和系统接口混合使用&#xff0c;因为可能会出现一些难以理解的现象。
✔ 测试用例三&#xff1a;
#include
#include
#include
#include
#include
#include
int main01()
{int fd &#61; open("log.txt", O_WRONLY|O_CREAT, 0644);if(fd < 0){perror("open");return 1;}dup2(fd, 1);//此时再写入就不是标准输出,而是fd const char* msg &#61; "hello dup2->output\n";int i &#61; 0;while(i < 5){write(1, msg, strlen(msg));i&#43;&#43;;}close(fd);return 0;
}//输入重定向
int main02()
{int fd &#61; open("log.txt", O_RDONLY);if(fd < 0){perror("open");return 1;}dup2(fd, 0);//此时再读就不是从标准输入读,而是fdchar buffer[1024];ssize_t sz &#61; read(0, buffer, sizeof(buffer) - 1);if(sz > 0) {buffer[sz] &#61; 0;printf("%s", buffer);}close(fd);return 0;
}//追加重定向
int main03()
{int fd &#61; open("log.txt", O_WRONLY|O_APPEND);if(fd < 0){perror("open");return 1;}dup2(fd, 1);//此时再写入就不是标准输出,而是fdconst char* msg &#61; "hello dup2->append\n";int i &#61; 0;while(i < 5){write(1, msg, strlen(msg));i&#43;&#43;;} close(fd);return 0;
}
我们之前自己实现重定向时是先 close 被重定向的文件&#xff0c;再 open 想重定向的文件。有没有可以不 close 被重定向的文件&#xff0c;直接重定向&#xff0c;此时系统提供了类似的接口 dup2 来解决 close 多此一举的行为。
使用 dup2&#xff0c;需要包含 unistd 头文件&#xff0c;
要输出的文件描述符是 1&#xff0c;而要重定向的目标文件描述符是 fd(echo “hello” > log.txt)&#xff0c;dup2 应该怎么传参 —— dup2(1, fd) || dup2(fd, 1) ❓
很明显&#xff0c;依靠函数原型&#xff0c;我们就能认为 dup2(1, fd)&#xff0c;因为 1 是先打开的&#xff0c;而 fd 是后打开的&#xff0c;可实际上并不是这样的。文档中说 newfd 是 oldfd 的一份拷贝&#xff0c;这里拷贝的是文件描述符对应数组下标的内容&#xff0c;所以数组内容&#xff0c;最终应该和 oldfd 一致。换言之&#xff0c;这里就是想把 1&#xff0c;不要指向显示器了&#xff0c;而指向 log.txt&#xff0c;fd 也指向 log.txt。所以这里的 oldfd 对应 fd&#xff0c;newfd 对应 1&#xff0c;所以应该是 dup2(fd, 1)。
运行结果
输出重定向
输入重定向
追加重定向
所以现在我们就明白了&#xff1a;
echo "hello world" > log.txt
—— echo 是一个进程&#xff1b;“hello world” 默认是调用 printf 或 write 往显示器上输出&#xff1b;log.txt 是调用 open 使用 O_WRONLY|O_CREAT 打开&#xff1b;> 是调用 dup2&#xff0c;将默认标准输出 1 的内容改为 log.txt&#xff1b;
<
就是 dup2(fd, 0)&#xff0c;且 open 文件的方式是 O_RDONLY&#xff1b;
>>
同 >&#xff0c;都是 dup2(fd, 1)&#xff0c;只不过它打开文件的方式是 O_WRONLY|O_APPEND&#xff1b;
进程替换时&#xff0c;是否会干扰重定向对应的数据结构 ❓
换言之&#xff0c;将来 fork&#xff0c;创建子进程&#xff0c;子进程会以父进程的大部分数据为模板&#xff0c;子进程进行程序替换时&#xff0c;并不会影响曾经打开的文件&#xff0c;也就不会影响重定向对应的数据结构。
从头到位我们都在说打开的文件&#xff0c;磁盘中包含了上百万个文件&#xff0c;肯定不可能都是以打开的方式存在&#xff0c;其实文件包含打开的文件和普通的未打开的文件&#xff0c;接下来我们重点谈未打开的文件。我们知道打开的文件是通过操作系统被进程打开&#xff0c;一旦打开&#xff0c;操作系统就要维护多个文件&#xff0c;所以它是需要被操作系统管理的&#xff0c;也就是说这种方式&#xff0c;磁盘上和内存上都有这个文件&#xff0c;它们不是完全一样的&#xff0c;内存中的文件更强调的是属性和方法&#xff0c;磁盘中的文件更强调的是数据&#xff0c;它们是通过缓冲区关联的&#xff1b;而普通的未打开的文件在磁盘上&#xff0c;未被加载到内存中&#xff0c;它当然也要被管理&#xff1b;其中管理打开的文件和管理未打开的文件在操作系统中有一个功能模块叫做文件系统。之前我们谈过进程 vs 程序&#xff0c;一个被打开的程序就是进程&#xff0c;只不过我们在解释进程时不是严格把它当作文件来解释&#xff0c;需要明白的是进程是要被加载到内存的&#xff0c;程序就是一个磁盘文件&#xff0c;打开的文件是进程&#xff0c;而普通未打开的文件是程序。
ls -l 可以看到当前路径下文件的元数据&#xff0c;也就是文件的属性。其中这里的硬链接数我们还没有谈过&#xff0c;一会我们会谈。
这里在命令行上输入 ls -l&#xff0c;bash 解析 ls -l&#xff0c;fork 创建子进程&#xff0c;让子进程通过进程替换执行 ls -l&#xff0c;ls -l 会在当前路径下把文件的属性通过磁盘读到内核&#xff0c;再由内核读到用户空间显示出来。stat 命令还可以查看更详细的信息。
如果不深入的话&#xff0c;这块也没啥价值&#xff0c;谁不知道文件在磁盘上&#xff0c;谁不知道 ls -l 读取当前路径下文件的属性&#xff0c;所以我们还要研究它的原理。但是在此之前&#xff0c;我们需要先认识磁盘&#xff0c;关于磁盘这个话题&#xff0c;还会在数据库中再谈一次。
众所周知&#xff0c;磁盘分为机械硬盘 (HDD) 和固态硬盘 (SSD)&#xff0c;现在很多的电脑都是机械硬盘和固态硬盘组合使用&#xff0c;但服务器上大多都是固态硬盘&#xff0c;只有一些高效率的存储集群会用到固态硬盘&#xff0c;机械硬盘和固态硬盘在存储技术上肯定是不同的&#xff0c;而我们主要了解机械硬盘&#xff0c;因为它多用于服务器上&#xff0c;其次虽然固态硬盘要比机械硬盘快不少&#xff0c;但在 CPU 看来&#xff0c;都很慢&#xff0c;所以我们就了解最慢的。
如下图&#xff0c;虽然磁盘的盘面看起来很光滑&#xff0c;但是它上面有一些同心圆&#xff0c;这些同心圆用圆白线划分&#xff0c;每一圈叫做磁道&#xff0c;数据写在这些有颜色的区域上。实际上你并不是把一圈的空间都用完&#xff0c;所以这里还使用了一些直白线划分&#xff0c;被圆白线和直白线划分出来的区域叫做扇区。所以当盘片在旋转、磁头摆动就可以找到这个盘面的任何一个扇区进行读写。
盘面是有两面的&#xff0c;且两面都是同心圆&#xff0c;根据配置不同&#xff0c;有些磁盘可能还有多组盘片&#xff0c;我们可以从上至下的分为不同的盘面&#xff0c;也叫做你是第几个盘面。
虽然在 C 语言中我们知道访问内存的基本单位是字节&#xff0c;但是在操作系统的角度认为内存的基本单位一般是 4kb&#xff0c;在操作系统看来&#xff0c;内存就是一个数组&#xff0c;每一个元素是 4kb&#xff0c;之前在谈进程地址空间时也说过它叫做页框&#xff0c;4kb 是页帧&#xff0c;所以操作系统申请内存时是按 4kb 为单位进行分配和加载的&#xff0c;语言层面上并不关心底层是怎么做的。磁盘存储也有基本单位&#xff0c;一个基本单位是一个扇区&#xff0c;它是磁盘读取的最小单元&#xff0c;大部分磁盘的一个扇区是 512byte&#xff0c;你会发现虽然这里好像越靠近圆心&#xff0c;扇区越小&#xff0c;其实它们都是 512byte&#xff0c;原因是越靠近圆心的虽然扇区越小&#xff0c;但是比特位也相对外圈更密集。内存和磁盘之间也是有交互的&#xff0c;它们之间的交互我们称为 output、input&#xff0c;也叫做 IO&#xff0c;一般内存和磁盘之间 IO 交互时&#xff0c;不是纯硬件级别的交互&#xff0c;而是要通过文件系统完成&#xff0c;也就是通过操作系统。这里用户和内存之间交互的基本单元大小是 1byte&#xff0c;一般内存和磁盘之间交互时的基本单元大小是 4kb&#xff0c;所以文件系统在往磁盘读数据时&#xff0c;要读 8 个扇区&#xff0c;这就是数据由磁盘加载到内存的过程。
其中我们再看 stat 中展示的信息&#xff0c;我们把内存和磁盘之间交互时的基本单元大小 4kb 叫 Blocks&#xff0c;这里的 IO Block:4096 就是 8 × 512。
一般像这样的机械硬盘&#xff0c;物理上是圆状&#xff0c;操作系统很难去管理它&#xff0c;因为操作系统如果不对它进行抽象化处理&#xff0c;那么操作系统中的代码可能就是 read(盘面&#xff0c;磁道&#xff0c;扇区)&#xff0c;操作系统需要知道这三个参数的话&#xff0c;那么一定要在操作系统读取磁盘的代码中以硬编码的形式写到操作系统中。如果有一天&#xff0c;你给自己的电脑加了一块固态硬盘&#xff0c;你要对固态硬盘进行读操作&#xff0c;就不能再用以前的方法了&#xff0c;因为固态硬盘与机械硬盘的结构不一样&#xff0c;它没有盘面、磁道、扇区&#xff0c;所以操作系统中曾经设计好的代码就得修改。很显然&#xff0c;这样的设计导致它们之间出现了强耦合&#xff0c;这是很不合理的。
所以我们需要对磁盘抽象化处理&#xff0c;将圆状结构的磁盘空间抽象成线性结构的磁盘空间&#xff0c;很多人就纳闷了&#xff0c;这里举两个例子方便理解&#xff0c;a) 其实在 C 语言中我们见过的 int arr[3][4] 二维数组就是把线性的数据结构抽象成了好理解的有行有列的结构。 b) 曾经风靡一时的磁带是把数据存储于那条黑色的带子上&#xff0c;可能是为了空间的原因&#xff0c;把带子卷起来形成一个圆状&#xff0c;所以磁带在物理上&#xff0c;既可以是圆状&#xff0c;也可以是线状。
同样的&#xff0c;也能把磁盘抽象成线性结构。把磁盘上的磁道抽象成线性形状&#xff0c;比如磁盘的所有磁道被我们抽象成了一条 500GB 的线性空间&#xff0c;我们可以把它看作一个很大的数组 —— 扇区 array[NUM]&#xff0c;其中每一个元素是 512byte&#xff0c;操作系统要申请 4kb&#xff0c;那就给数组的 8 个元素。所以将磁盘抽象后&#xff0c;操作系统就摆脱盘面、磁道、扇区的束缚了&#xff0c;操作系统只关心你想访问的哪个下标&#xff0c;这里的地址我们称为逻辑区块地址(Logical Block Address, LBA)&#xff0c;这里抽象出来的数组下标是和机械硬盘中盘面、磁道、扇区构成映射关系的&#xff0c;这里的映射关系是由对应的机械磁盘驱动维护的&#xff0c;操作系统想往 2 下标处写数据&#xff0c;最终 2 下标一定是能对应到具体磁盘中某个扇区上。如果要往固态硬盘中写数据&#xff0c;也是把它抽象成线性的数组&#xff0c;它也有自己的固态硬盘驱动维护数组下标和固态硬盘之间的映射关系。至此&#xff0c;通过抽象的方法&#xff0c;就完成了操作系统和磁盘之间的解耦。所以最终操作系统对磁盘的管理&#xff0c;转换成了对数组的管理。
500G 的磁盘空间抽象成每个元素是 512byte 的数组&#xff0c;那样非常大&#xff0c;不易管理&#xff0c;所以操作系统还要对这 500G 的数组进行拆分&#xff0c;比如这里拆分成了 100G、100G、150G、150G&#xff0c;所以这里只要管理好了第一个 100G 的空间&#xff0c;然后把管理的方法复制到其它空间&#xff0c;其它的空间也能被管理好。这里我们把拆分的过程叫做分区&#xff0c;这也就是我们的电脑上为什么会有 C 盘、D 盘、E 盘。至此我们仅仅是对空间进行划分&#xff0c;要把空间管理好&#xff0c;还需要写入相关的管理数据&#xff0c;比如把中国 960 万平方公里&#xff0c;划分了不同大小的省份&#xff0c;你要管理好一个省&#xff0c;我们不考虑地质差异等因素&#xff0c;只要一个领导、一个团队他们把一个省管理好了&#xff0c;那么他们的管理方法就可以复制到其它省&#xff0c;同样的&#xff0c;刚刚我们分区的工作只是把中国划分成不同的省份&#xff0c;接下来我们还要分配每个省的省长、省中每个市的市长、市中每个镇的镇长等&#xff0c;以此来管理一个省。这里我们把分配的过程叫做格式化过程&#xff0c;所谓的格式化在计算机中就是写入文件系统&#xff0c;也就是说我们要把文件系统写入某个分区中&#xff0c;这个文件系统的核心包括数据 &#43; 方法&#xff0c;数据就类似这个省有多少人口、粮食等&#xff0c;方法就类似这个省有生育政策、耕种政策等。同样文件系统包含的数据就是文件系统的类型等&#xff0c;方法就是操作各种文件的方法。
当然不同的分区当然可以使用不同的文件系统&#xff0c;Linux 下就使用五六种不同的文件系统&#xff0c;Linux 可以支持多种文件系统&#xff0c;包括 Ext2、Ext3、fs、usb-fs、sysfs、proc。这就好比&#xff0c;各个省份需要因地制宜的分配不同的团队。我们今天谈的都是 Ext 系列的文件系统&#xff0c;另外也不谈其它的文件系统如何&#xff0c;我们就认为磁盘上不同分区的文件系统是一样的。
因为一个省也很大&#xff0c;为了更好的管理&#xff0c;还要分配市长、镇长等&#xff0c;同样的分区后的 100G 空间还要再划分&#xff0c;比如这里划分了 10 组 10G 的空间&#xff0c;然后把它看作成一个一个的块组(Block group)&#xff0c;一个块组中又有多个 4kb 空间&#xff0c;而磁盘存储是有块基本单位的&#xff0c;文件系统认为一块是 4kb&#xff0c;我们只要把一个块组管好&#xff0c;整个文件系统内的块组就能管好&#xff0c;所以问题又转换为怎么把这 10G 的空间管好&#xff0c;所以接下来划分的才是文件系统写入的相关细节&#xff0c;也是我们要研究的&#xff0c;这个区域的信息&#xff0c;大家都有&#xff0c;可能略有差异。
这里 Linux 文件系统以 Ext 系列的为话题&#xff0c; 因为不同的文件系统可能略有差异。在块组之前&#xff0c;有一个 Boot Block&#xff0c;它是启动的意思&#xff0c;一般一个磁盘的 0 号分区的 0 号块组上的第一扇区存储着一些启动信息&#xff0c;这里不是重点。这里我们重点谈一个块组细分下来的后四个信息&#xff1a;
A) Super Block 是文件系统的核心结构&#xff0c;用于描述文件系统的属性&#xff0c;包括文件系统名、文件系统版本、块组中有哪些使用和未使用&#xff0c;一般计算机启动时&#xff0c;Super Block 会被加载到操作系统&#xff0c;其中每一块组好像都有一个 Super Block&#xff0c;但实际可能 10 个块组中只有两三个有 Super Block。
B) Group Descriptor Table 是块组描述符表&#xff0c;Super Block 描述的是整个块组相关的信息&#xff0c;这里描述的是一组的信息&#xff0c;每一个块组都必需要有一个 Group Descriptor Table。
C) 我们说过文件 &#61; 内容 &#43; 属性。这里的内容和属性采用分离存储&#xff0c;属性放在 inode Table 中。一个组中可以放多少个 inode 是一定的&#xff0c;基本上&#xff0c;一个文件或目录一个 inode&#xff0c;inode 是一个文件的所有属性集合&#xff0c;属性也是数据&#xff0c;也要占用空间&#xff0c;所以即便是一个空文件&#xff0c;它也要占用空间&#xff0c;这里的属性集合包含文件权限、大小等&#xff0c;但不包含文件名&#xff0c;这个下面再说。
内容放在 Date blocks 中。比如这里的块组是 10G&#xff0c;那么inode Table 占 1G&#xff0c;Date blocks 占了 8G&#xff0c; 一个 inode 是 512byte&#xff0c;粗略的算一下&#xff0c;1G 大概 42 亿多字节&#xff0c;除以 512 大概也有几千万&#xff0c;所以这样一个块组能保存几千万文件的 inode 信息&#xff0c;这里 inode Table 和 Data blocks 的划分可能会出现你用完了&#xff0c;我没用完&#xff0c;你没用完了&#xff0c;我用完了的情况&#xff0c;这种情况并没有有效的方法解决。
Date blocks 相当于一个数据块集合&#xff0c;以 4k 为单位&#xff0c;对应的数据块属于哪些文件&#xff0c;是由 Data Blocks 和 inode Table 维护的。如下图&#xff0c;inode Table 包含了若干大小相同的块&#xff0c;这些块有不同的编号&#xff0c;对应就是文件的属性&#xff0c;Data blocks 也包含了若干大小相同的块&#xff0c;这些块也有不同的编号&#xff0c;对应就是文件的内容。此时新建文件或目录&#xff0c;就给文件申请 1 号 inode&#xff0c;并把文件的各种属性写入到 1 号 inode&#xff0c;1 号 inode 中包含了一个数组 block b[32]&#xff0c;比如 1 号 inode 需要 2 个数据块&#xff0c;所以 [0] &#61; 2&#xff0c;[1] &#61; 3&#xff0c;所以 1 号 inode 就可以找到对应的数据块。换言之&#xff0c;要在磁盘上查找一个文件&#xff0c;我们只需要知道这个文件的 inode 是多少&#xff0c;至此&#xff0c;我们知道真正标识文件的不是文件名&#xff0c;而是文件的 inode 编号。既然 inocde 大小是确定的&#xff0c;万一文件是 10 个 T&#xff0c;此时数据块就不够了&#xff0c;文件系统的处理策略是数据块不仅可以保存数据的内容&#xff0c;还可以保存其它数据块的编号&#xff0c;它类似于 b&#43; 树。换言之&#xff0c;对于保存较大的文件&#xff0c;可能就需要多级索引的形式。
这里 ls - i 就可以查看文件或目录对应的 inode 了&#xff0c;可以看到这里的 inode 并不是连续申请的&#xff0c;它依然能看到文件名&#xff0c;是因为我们需要识别。
如何理解目录 ❓
我们知道程序员定位一个文件&#xff0c;是通过绝对路径或相对路径定位的&#xff0c;但不管是绝对路径还是相对路径最终一定是要有一个目录。目录当然是一个文件&#xff0c;也有独立的 inode&#xff0c;也有自己的数据块&#xff0c;目录中的 block 数组能找到对应的数据块&#xff0c;目录的数据块维护的是文件名和 inode 的映射关系。换言之&#xff0c;在目录下创建文件时&#xff0c;除了给文件申请 inode、数据块之外&#xff0c;还要把文件名和申请创建成功之后文件的 inode 编号写到目录的数据块中。所以现在就能理解为什么大多数操作系统下同一个目录中不允许存在同名文件。所以只要我们找到了目录就可以找到文件名&#xff0c;根据映射然后可以找到文件 inode&#xff0c;通过 inode 读取文件的属性&#xff0c;也可以通过 inode 中的数组读取文件的内容。所以 ls -l 时就可以读到文件的属性信息&#xff0c;它是在当前目录对应的 inode 下找到对应数据块中文件名和文件名映射的 inode&#xff0c;再去找对应文件的 inode&#xff0c;此时就看到文件的属性了。所以 echo “hello world” > file.txt 是先启动进程&#xff0c;这个进程当然知道自己所在的目录&#xff0c;所以它就可以拿着 file.txt 文件名找它对应的 inode&#xff0c;把数据追加到对应的数据块中。所以我们说 inode 不存储文件名&#xff0c;只是往目录的数据块中写入文件名和文件对应的 inode。
D) Block Bitmap 和 inode Bitmap 是位图&#xff0c;就是用比特位 0 1 来表示。Block Bitmap 用来标识数据块的使用情况&#xff0c;inode Bitmap 用来标识 inode 的使用情况&#xff0c;每个比特位都对应一个块。换言之&#xff0c;当你新建文件时&#xff0c;它并不是遍历 inode 区域&#xff0c;这样太慢了&#xff0c;它只需要在系统启动时&#xff0c;将 Blok Bitmap 和 inode Bitmap 预加载到系统中&#xff0c;你要新建文件&#xff0c;就把 inode Bitmap 的比特位由 0 至 1&#xff0c;文件需要多少数据块&#xff0c;就把 Block Bitmap 的比特位由 0 至 1。所以我们可以通过位图&#xff0c;可以快速的完成 inode 的申请和释放&#xff0c;同时也能确认当前磁盘的使用情况。但是位图依然还是需要去遍历哪些使用和未使用&#xff0c;以及做位操作等&#xff0c;所以这里通过 Group Descriptor Table 来管理。
如何理解删除文件 ❓
之前我们说过&#xff0c;计算机中删除一个文件并不是真正的删除&#xff0c;而是把那块空间标识为无效&#xff0c;就像房子上的 “ 拆 ” 字。而现在理解的是不用把 inode 属性清空&#xff0c;不用把 inode 对应的数据块清空&#xff0c;只要把两个位图中对应的比特位由 1 到 0&#xff0c;再把所在的目录下中的对应的映射关系去掉&#xff0c;此时空间就是无效的&#xff0c;下一次再新建文件时&#xff0c;就可以直接把无效的空间覆盖。
按上面这样说&#xff0c;删除后的文件当然可以恢复&#xff0c;Windows 下的回收站就是一个目录&#xff0c;当你删除时就是把文件移动到回收站目录下&#xff0c;移动时就是把其它目录下数据块中的映射关系移动到回收站目录下的数据块中。Windows 下就算把回收站的内容删除也是能恢复的&#xff0c;Linux 下&#xff0c;如果要恢复删除的文件是有一些恢复工具的&#xff0c;但有可能在恢复过程中&#xff0c;创建各种临时文件&#xff0c;可能就会把想恢复的文件的信息覆盖掉&#xff0c;你想自己恢复删除的文件&#xff0c;就需要更深入的了解文件系统原理。
ln -s file.txt soft_link
给 file.txt 建立软链接 soft_link。 file.txt 的 inode 是 790929&#xff0c;soft_link 的 inode 是 790955&#xff0c;也就是说软链接 soft_link 就是一个普通的正常文件&#xff0c;有自己独立的 inode&#xff0c;soft_link 中的数据块中保存着它指向的文件 file.txt 的路径&#xff0c;就类似于 Winodws 下的快捷方式&#xff0c;比如桌面看到的软件保存的是其它的路径&#xff0c;在系统中可能你要运行的可执行程序在一个很深的目录下&#xff0c;就可以在较上层的目录中建立软链接。
ln file.cpp hard_link
给 file.cpp 建立硬链接 hard_link。硬链接和它链接的文件的 inode 是一样的&#xff0c;硬链接没有独立的 inode&#xff0c;所以严格来说硬链接不是一个文件&#xff0c;硬链接本质就是在 file.cpp 文件所在目录的数据块中重新创建一个映射关系&#xff0c;也就是给 file.cpp 的 inode 重新起了一个别名&#xff0c;我们发现了链接后的 file.cpp 的有一个属性信息由 1 变为 2&#xff0c;所以这里 ls -l 显示的这一列数据表示的不是软链接&#xff0c;而是硬链接。
硬链接的应用 ❓
为什么创建普通目录的硬链接是 2 &#xff1f;创建普通文件的硬链接是 1 &#xff1f;—— 普通文件是 1 好理解&#xff0c;因为当前目录中只包含一组 file 和 file 的 inode&#xff1b;
而普通目录是 2 的原因是因为除了当前目录下包含了 dir 和 dir 的 inode&#xff0c;还有 dir 目录下中隐藏的 " . "&#xff0c;这个点叫做当前路径&#xff0c;此时我们发现这个点的 inode 和 dir 的 inode 是一样的&#xff0c;所以 dir 的 inode 编号是 2。 这个点就是 dir 的别名&#xff0c;因为当前路径的使用频率很高&#xff0c;所以它是为了方便我们对当前路径的索引&#xff0c;如果没有这个别名&#xff0c;那就只能是 " dir/xxx/… "&#xff0c;完全没有 " ./xxx/… " 方便。
我们再在 dir 下建立一个目录 other&#xff0c;此时 dir 的硬链接数就变成了 3&#xff0c;other 的硬链接数就是 2。—— other 的是 2 能理解&#xff0c;因为 other inode 和 . inode&#xff1b;
而 dir 之所以是 3&#xff0c;是因为要 " cd … "&#xff0c;所以 other 下还有一个点点&#xff0c;它是 dir 的别名。
所以硬链接最典型的应用场景就是方便进行路径转换。