前言
我们在基础 IO 专题都是围绕打开后的文件展开讨论,下面我们要认识一下文件打开之前是怎样的,会围绕以下几个问题展开: 1、为什么打开文件要带文件路径? 2、打开文件时操作系统做了什么? 3、没被打开的文件在哪里?如何存放的? 我们目前可以回答第三个问题,没被打开的文件一定在磁盘这样的存储设备上。(Ext 系列文件系统是专为磁盘等持久化块存储设备设计的文件系统)
Linux Ext 系列文件系统基于磁盘块设备设计,将数据划分为扇区、块、分区及块组。核心通过 inode 记录文件属性与权限,通过 data block 存储内容,利用 bitmap 管理空闲资源。路径解析依赖目录树映射文件名与 inode,支持软硬链接机制。系统维护超级块与组描述符表保障元数据完整性,结合路径缓存优化访问效率。

我们在基础 IO 专题都是围绕打开后的文件展开讨论,下面我们要认识一下文件打开之前是怎样的,会围绕以下几个问题展开: 1、为什么打开文件要带文件路径? 2、打开文件时操作系统做了什么? 3、没被打开的文件在哪里?如何存放的? 我们目前可以回答第三个问题,没被打开的文件一定在磁盘这样的存储设备上。(Ext 系列文件系统是专为磁盘等持久化块存储设备设计的文件系统)
机械磁盘是计算机中唯一的机械设备 磁盘—外设 慢 容量大,价格便宜
正因为磁盘价格便宜,容量大,所以如今的大型互联网公司都青睐用磁盘来存数据。
我们知道计算机只认识二进制,这个二进制的程序员规定出来的,在不同的设备可能用电的有无或者高低电平表示 0、1,而磁盘中有无数细小磁铁,所以磁盘是用小磁铁的正负极来表示 0、1 的。
上面是磁盘的物理结构示意图,我们只用知道主轴马达会一直高速旋转,磁头会高速左右摆动就够了。
扇区:是磁盘存储数据的基本单位,也是操作系统访问数据的基本单位,512 字节,它是块设备。
我们一般认为一个磁盘中各个半径不同的磁道所含的扇区数目是一样的,原因是不同磁道扇区的疏密程度不同,一般需要高频访问的数据在内侧,不太高频访问的数据就在外侧。

我们看上图的磁盘结构: 1、盘面和磁头是一对一对应的。 2、并且一个磁盘的所有磁头是共进退的,如上所示,同一时间六个磁头都是访问的是对应盘面同一半径的磁道,我们把这六个磁头所在的六个相同半径的磁道合为一体称为柱面。
下面我们让磁盘动起来: 1、磁头左右摆动的本质是在定位哪个磁道 2、盘面旋转的的本质是在确定某个磁道后,定位该磁道(柱面)上的某一个扇区。
如何定位⼀个扇区呢? 1、可以先定位磁头(header)。 2、确定磁头要访问哪⼀个柱⾯ (磁道)(cylinder)。 3、定位⼀个扇区 (sector)。 4、这就叫做 CHS 地址定位,但是现代磁盘一般是用 LBA 地址定位,后面再讲。 5、一般磁头、磁道编号从 0 开始,扇区编号从 1 开始。
总结: 1、扇区是从磁盘读出和写⼊信息的最⼩单位,通常⼤⼩为 512 字节。 2、磁头(head)数:每个盘⽚⼀般有上下两⾯,分别对应 1 个磁头,共 2 个磁头。 3、磁道(track)数:磁道是从盘⽚外圈往内圈编号 0 磁道,1 磁道…,靠近主轴的同⼼圆⽤于停靠磁头,不存储数据。 4、柱⾯(cylinder)数:磁道构成柱⾯,数量上等同于磁道个数。 5、扇区(sector)数:每个磁道都被切分成很多扇形区域,每道的扇区数量相同。 6、圆盘(platter)数:就是盘⽚的数量。 7、磁盘容量=磁头数 × 磁道 (柱⾯) 数 × 每道扇区数 × 每扇区字节数。 8、细节:传动臂上的磁头是共进退的 (这点⽐较重要,后⾯会说明)。 9、柱⾯(cylinder),磁头(head),扇区(sector),显然可以定位数据了,这就是数据定位 (寻址) ⽅式之⼀,CHS 寻址⽅式。
磁盘本质上虽然是硬质的,但是逻辑上我们可以把磁盘想象成为卷在⼀起的磁带,那么磁盘的逻辑存储结构我们也可以类似于:

这样每⼀个扇区,就有了⼀个线性地址 (其实就是数组下标),sector array[N],这种地址叫做 LBA(Logic Block Address)
我们在前面理解磁盘展开的时候是以盘面为单位展开的,但磁盘的真实展开,是以柱面为单位展开的。
某一个盘面的其中一个磁道是一维数组,磁盘中该磁道所处的柱面是二维数组,那么整个磁盘本质就是一个三维数组。 所有,寻址⼀个扇区:先找到哪⼀个柱⾯ (Cylinder) ,在确定柱⾯内哪⼀个磁道 (其实就是磁头位置,Head),在确定扇区(Sector),所以就有了 CHS,而 CHS 这正好对应磁盘三维数组的下标:sector array[C][H][S]。
不管是二维数组还是三维数组,本质还是一维数组,因为数组元素在内存中都是连续存放的,下面是三维数组降维后的演示图:
所以从今天开始我们可以把磁盘当作一个以 sector 为单位的线性一维数组(操作系统内部并没有定义这个一维数组,所以一维数组只是一个抽象概念),数组的每个扇区元素的下标,就是 LBA 地址。 所以未来操作系统只用使用 LBA 地址就可以访问磁盘了,磁盘自己会做 LBA 和 CHS 之间的转化。
CHS 转成 LBA: 1、磁头数每个磁道扇区数 = 单个柱⾯的扇区总数。 2、LBA = 柱⾯号 C单个柱⾯的扇区总数 + 磁头号 H每磁道扇区数 + 扇区号 S - 1。 3、即:LBA = 柱⾯号 C(磁头数每磁道扇区数) + 磁头号 H每磁道扇区数 + 扇区号 S - 1。 4、扇区号通常是从 1 开始的,⽽在 LBA 中,地址是从 0 开始的。 5、柱⾯和磁道都是从 0 开始编号的。 6、总柱⾯,磁道个数,扇区总数等信息,在磁盘内部会⾃动维护,上层开机的时候,会获取到这些参数。
LBA 转成 CHS: 1、柱⾯号 C = LBA // (磁头数每磁道扇区数)【就是单个柱⾯的扇区总数】。 2、磁头号 H = (LBA % (磁头数每磁道扇区数)) // 每磁道扇区数。 3、扇区号 S = (LBA % 每磁道扇区数) + 1。 4、'//': 表⽰除取整。
所以:从此往后,在磁盘使⽤者看来,根本就不关⼼ CHS 地址,⽽是直接使⽤ LBA 地址,磁盘内部⾃⼰转换。 所以:从现在开始,磁盘就是⼀个 元素为扇区 的⼀维数组,数组的下标就是每⼀个扇区的 LBA 地址。OS 使⽤磁盘,就可以⽤⼀个数字访问磁盘扇区了。
其实硬盘是典型的'块'设备,操作系统读取硬盘数据的时候,其实是不会⼀个个扇区地读取,这样效率太低,⽽是⼀次性连续读取多个扇区,即⼀次性读取⼀个'块'(block)。 硬盘的每个分区是被划分为⼀个个的'块'。⼀个'块'的⼤⼩是由格式化的时候确定的,并且不可以更改,最常⻅的是 4KB,即连续⼋个扇区组成⼀个 '块'。'块'是操作系统访问⽂件的最⼩单位。
注意: 1、磁盘就是⼀个三维数组,我们把它看待成为⼀个"⼀维数组",数组下标就是 LBA,每个元素都是扇区。 2、每个扇区都有 LBA,那么 8 个扇区⼀个块,每⼀个块的地址我们也能算出来。 3、知道 LBA:块号 = LBA/8。 4、知道块号:LAB=块号*8 + n. (n 是块内第⼏个扇区)。
所以现在我们站在操作系统的角度看待磁盘,就认为磁盘是一个块设备,每个块都有下标,从文件系统的角度,对磁盘的文件进行访问都是以块为单位的。
至此我们已经完成了对磁盘的完整建模过程,磁盘本质就是: block array[N] 数组,是块设备。
其实不止 CPU 有寄存器,各种硬件外设也有寄存器,只不过数量较少。 我们磁盘为例,磁盘中会有 dir 寄存器,指示操作系统要对硬盘做什么操作,例如是读还是写。addr 寄存器,指示操作系统要对磁盘的哪一个扇区进行访问,一般会对 addr 寄存器内写入 LBA 地址。data 寄存器,用于存储操作系统的读写数据,所以操作系统对外设的控制本质就是对外设的特定寄存器写入对应数据,然后外设内部的硬件电路会自动进行相应的访问。
其实磁盘是可以被分成多个分区(partition)的,以 Windows 观点来看,你电脑会有⼀块磁盘并且将它分区成 C,D,E 盘。如果没有给电脑添加硬盘那么 C,D,E 就是电脑自带的一块机械硬盘(磁盘)的分区。分区从实质上说就是对硬盘的⼀种格式化。但是 Linux 的设备都是以⽂件形式存在,那是怎么分区的呢? 分区可以类似治理国家,国家很大,所以需要甚至省政府,市政府…而一块磁盘空间也很大,也需要分区治理,一块磁盘会被分为多个区,而一个区还会被分为多个组,所以我们只用研究如何把一个组管理好,就可以把整个磁盘管理好。下面我们就需要来研究一个磁盘的一个组内部有哪些要素。

文件 = 内容 + 属性,磁盘中文件的内容和属性都需要存储。这里先输出两个结论: 1、Linux 中,文件的内容和属性是分开存储的。 2、OS 文件系统中,OS 和磁盘进行 IO 的基本单位是 4kb。
Data Blocks 是用来存储文件内容的,以 4kb 数据块为单位,它占据了绝大部分磁盘的分组空间。Block Bitmap 用来表示数据块的整体使用情况,它记录了 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用,每一个比特位的 0、1 用来表示其中一个数据块是否被占用。
在 linux 中,我们一般用结构体 struct inode 表示文件属性,下面是 ext2 文件系统的 inode 完整代码:
/* * Structure of an inode on the disk */
struct ext2_inode {
__le16 i_mode; /* 文件类型 + 权限(如截图中 `-rw-rw-r--`) */
__le16 i_uid; /* 所有者 UID 的低 16 位 */
__le32 i_size; /* 文件大小(字节),截图中 touch 创建的空文件为 0 */
__le32 i_atime; /* 最后访问时间(时间戳) */
__le32 i_ctime; /* inode 元数据最后修改时间(时间戳) */
__le32 i_mtime; /* 文件内容最后修改时间(时间戳,对应截图中 `10:55` 等) */
__le32 i_dtime; /* 文件删除时间(时间戳,未删除时无效) */
__le16 i_gid; /* 所属组 GID 的低 16 位 */
__le16 i_links_count; /* 硬链接数(截图中每个文件为 1) */
__le32 i_blocks; /* 文件占用的磁盘块总数 */
__le32 i_flags; /* 文件标志(如是否为特殊文件、日志标记等) */
union{
struct{
__le32 l_i_reserved1;
} linux1;
struct{
__le32 h_i_translator;
} hurd1;
struct{
__le32 m_i_reserved1;
} masix1;
} osd1; /* 操作系统相关字段(兼容 Linux、Hurd、Masix) */
__le32 i_block[EXT2_N_BLOCKS]; /* 数据块指针数组(直接/间接块,EXT2_N_BLOCKS 通常为 15) */
__le32 i_generation; /* 文件版本号(用于 NFS 等网络文件系统) */
__le32 i_file_acl; /* 文件 ACL(访问控制列表)的块指针 */
__le32 i_dir_acl; /* 目录 ACL 的块指针(若为目录时有效) */
__le32 i_faddr; /* 碎片地址(若文件启用碎片存储时使用) */
union{
struct{
__u8 l_i_frag; /* 碎片编号 */
__u8 l_i_fsize; /* 碎片大小 */
__u16 l_i_pad; /* 填充(字节对齐) */
__le16 l_i_uid_high; /* 所有者 UID 的高 16 位(扩展 UID 范围) */
__le16 l_i_gid_high; /* 所属组 GID 的高 16 位(扩展 UID 范围) */
__le32 l_i_reserved2; /* 保留字段 */
} linux2;
struct{
__u8 h_i_frag; /* 碎片编号(Hurd 系统用) */
__u8 h_i_fsize; /* 碎片大小(Hurd 系统用) */
__le16 h_i_mode_high; /* 高 16 位模式(Hurd 系统用) */
__le16 h_i_uid_high; /* 高 16 位 UID(Hurd 系统用) */
__le16 h_i_gid_high; /* 高 16 位 GID(Hurd 系统用) */
__le32 h_i_author; /* 作者标识(Hurd 系统用) */
} hurd2;
struct{
__u8 m_i_frag; /* 碎片编号(Masix 系统用) */
__u8 m_i_fsize; /* 碎片大小(Masix 系统用) */
__u16 m_pad1; /* 填充(字节对齐) */
__le32 m_i_reserved2; /* 保留字段(Masix 系统用) */
} masix2;
} osd2; /* 操作系统相关字段(第二部分,兼容多系统) */
};
既然 inode 是结构体,说明 inode 的大小是固定的,所以每个 inode 所包含的成员变量类型和数目是一样的,但不同文件之间 inode 的成员变量的具体内容一般不同。文件的 inode 根据文件系统的不同,一般是 128 字节或者 256 字节。OS 若读取 inode,因为一次读取 4kb,所以 OS 一次可以最多可以读取 32 个 inode。
一般一个文件对应一个 inode,一个文件可能对应 0 个或多个 Data Block。这里会引出两个结论: 1、inode 会有一个 int 类型变量 inode,用来表示文件的唯一性。 2、linux 中,文件名不能也不在 inode 中存储。
下面介绍文件属性在磁盘中的存储情况,磁盘的分组中会有一个 inode Table 用来存放文件属性内容,结构如下图所示,innode Bitmap 类似 Block Bitmap,表示 innode Table 的整体使用情况。

下面我们上手实践一下,看我们创建的文件的 inode number 是多少,可以通过 -i 选项查看文件的 inode number :

现在我们拿到了文件的 inode number,就可以拿到文件的属性:struct inode,那如何通过文件属性找到文件的内容呢?实际上文件的 struct inode 中会有一个 inode 到 block 的映射关系表:
__le32 i_block[EXT2_N_BLOCKS];
总结:我们可以通过 inode 编号访问 struct inode,然后根据 struct inode 内部包含的 inode 和数据块的映射关系,进一步找到文件的内容。
inode 中不仅有文件的属性信息,内部还维护了一张表 i_block 存储了 inode 和文件数据 data block 的映射关系。 一个 inode 只能映射 15 组 data block 数据,前 12 组是映射到的数据块中的数据就是文件内容,而 13-15 是映射的数据块中的数据是其他数据块的块号,也就是索引,详情见下表,13、14、15 依次的一级索引、二级索引、三级索引。
一个组内的 inode 是可能映射到同一分区其他组的 data block 的,因为 data block 是全区整体编号的。
块组描述符表,描述块组属性信息(包含块组的区域划分情况),整个分区分成多个块组就对应有多少个块组描述符。每个块组描述符存储⼀个块组 的描述信息,如在这个块组中从哪⾥开始是 inode Table,从哪⾥开始是 Data Blocks,空闲的 inode 和数据块还有多少个等等。块组描述符在每个块组的开头都有⼀份拷⻉。
// 磁盘级 blockgroup 的数据结构
/* * Structure of a blocks group descriptor */
struct ext2_group_desc {
__le32 bg_block_bitmap; /* Blocks bitmap block */
__le32 bg_inode_bitmap; /* Inodes bitmap */
__le32 bg_inode_table; /* Inodes table block*/
__le16 bg_free_blocks_count; /* Free blocks count */
__le16 bg_free_inodes_count; /* Free inodes count */
__le16 bg_used_dirs_count; /* Directories count */
__le16 bg_pad;
__le32 bg_reserved[3];
};
存放⽂件系统本⾝的结构信息,描述整个分区⽂件系统信息。等于说 GDT 管理它所在的一个组,Super Block 管理它所在分区的所有组。(一般来说磁盘的每个分区都有各自独立的文件系统) 这里会有一个问题,既然 Super Block 是管理一整个分区,为什么它不在分区的开头,而是在一个分组的开头? 这其实是一种数据容灾的备份处理:Super Block 不仅仅只在一个组里,它可能同时在多个组里存在,但是不一定整个分区中的所有组都有,那么当其中一个分组的 Super Block 出于某种原因损坏后,不会导致整个分区直接崩掉,并且损坏的数据可以通过其他分组中的 Super Block 恢复,分组的数据量不是很大,所以分组的 GDT 只有一份。记录的信息主要有:bolck 和 inode 的总量,未使⽤的 block 和 inode 的数量,⼀个 block 和 inode 的⼤⼩,最近⼀次挂载的时间,最近⼀次写⼊数据的时间,最近⼀次检验磁盘的时间等其他⽂件系统的相关信息。Super Block 的信息被破坏,可以说整个⽂件系统结构就被破坏了。 (补充:还能通过超级块中的成员数据间接找到文件所在扇区的 LBA 地址)
1、格式化分区是指给特定分区写入管理信息,即写入文件系统和分组分区的相关管理数据,文件数据可以暂时不写入。所以当我们新建一个分区时会就会将该区格式化。 2、删除文件不用释放掉文件 inode Table 和 Data Block 中的数据,只用把它在 inode Bitmap 和 Block Bitmap 中的对应位置置为 0 就行了。这其实可以解释我们平时在下载文件需要很长时间,但是删除文件只需要 1、2 秒的原因。 这里可以引申出一个结论:计算机删除文件,只用设置数据无效就行了。 3、要访问一个文件,只能通过该文件的 inode,所以我们要拿到它的 inode 编号,因为只有 inode 编号可以在一个分区内表示文件的唯一性。 4、inode 编号和 data block 编号是全区统一分配的,不是只在一个组内有效。所以 super block 中会维护该区中每个组的中有多少个 block 和 inode。不同分区的 inode 编号和 data block 编号就互不影响,可以重复了。

5、我们之前谈过,文件名不在文件的 inode struct 中,那文件名应该在哪里?其实普通文件的文件名在它所在的目录的文件内容中,因为目录也是文件,所以目录也有文件内容和文件属性。但普通文件的文件名并不是单独的存在目录内容中,而是以文件名和它对应的 inode 的映射关系形式存储的。所以我们平时用的文件名本质是指定目录下的映射关系。这也解释了为什么同一目录下文件名不能重复。 6、从今天开始,在文件系统的角度,普通文件和目录文件的存储方式没有任何区别,无非就是存储 inode 和数据块,只不过内容不同。区分普通文件和目录文件是用文件的 struct inode 中的权限成员变量 i_mode 的第一个比特位:'-'表示普通文件,'d"表示目录文件。
// readdir.c
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<dirent.h>
#include<sys/types.h>
#include<unistd.h>
int main(int argc, char* argv[]){
if(argc !=2){
fprintf(stderr,"Usage: %s <directory>\n", argv[0]);
exit(EXIT_FAILURE);
}
DIR* dir = opendir(argv[1]); // 系统调⽤,⾃⾏查阅
if(!dir){
perror("opendir");
exit(EXIT_FAILURE);
}
struct dirent* entry;
while((entry = readdir(dir))!=NULL){ // 系统调⽤,⾃⾏查阅
// Skip the "." and ".." directory entries
if(strcmp(entry->d_name,".")==0||strcmp(entry->d_name,"..")==0){
continue;
}
printf("Filename: %s, Inode: %lu\n", entry->d_name,(unsigned long)entry->d_ino);
}
closedir(dir);
return 0;
}
1、在指定目录下新建文件的本质是将文件的文件名和 inode 的映射关系写到目录的内容 data block 里,相当于新建文件本质要对目录做写入操作,所以这里可以解释之前讲的为什么在目录下新建文件,需要目录有 w 权限。
我们已经知道要访问一个文件首先要拿到它的 inode,拿着文件名要找 inode 就需要访问它的上级目录内 data block 中的文件名-inode 映射关系,而这个上级目录也是一个文件,要访问它有需要拿到它的 inode,而它的 inode 又在更上级的目录中…直到打开根目录为止,这个过程类似递归。所以我们访问要 linux 下任意一个文件,都需要从根目录开始,对该文件上的所有目录进行解析,也就是依次打开路径上的所有目录(如 /home/fdb:打开根目录,拿着 home 文件名就能在根目录中找到 home 的 inode,就能打开 home 了…依此类推),这个过程就叫做路径解析。 这就是为什么打开一个文件、访问文件必须要有该文件路径的原因,也是为什么 PCB 中会维护 cwd(当前进程的工作路径)的原因。
我们学习了路径解析后,我相信大家和我都有一个疑问,每访问一个文件都要做路径解析,这样是不是太慢了啊?所以 linux 还会支持一个名叫路径缓存的功能。 在 linux 系统中,当用户访问指定路径下的文件时(包括路上目录,最终的目标文件在内),linux 会在进路径解析的过程中,在内核里形成目录树和路径缓存,在整个树形结构中,普通文件和空目录就是叶子结点。

1、Linux 中,在内核中维护树状路径结构的内核结构体叫做: struct dentry。 2、每个⽂件其实都要有对应的 dentry 结构,包括普通⽂件。这样所有被打开的⽂件,就可以在内存中形成整个树形结构。 3、整个树形节点也同时会⾪属于 LRU(Least Recently Used,最近最少使⽤) 结构中,进⾏节点淘汰。 4、整个树形节点也同时会⾪属于 Hash,⽅便快速查找。 5、更重要的是,这个树形结构,整体构成了 Linux 的路径缓存结构,打开访问任何⽂件,都在先在这棵树下根据路径进⾏查找,找到就返回属性 inode 和内容,没找到就从磁盘加载路径,添加 dentry 结构,缓存新路径。
文件描述符 fd 会关联 struct file,Linux 内核内存中的 struct file 会间接关联 struct dentry 结构体,struct dentry 会间接关联 inode,所以我们之前介绍 struct file 内的三大内容之一的文件属性就在 inode 中。内存中的 inode 是由磁盘的 inode 在将文件加载到内存时初始化的,我们前面介绍的 ext2_inode 就是磁盘级 inode。
总而言之,只想让大家理解一点,打开一个文件首先会对该文件进行路径解析、对每个路径结点检查 / 创建 dentry,dentry 中还会关联 inode,并将 dentry 结构挂进目录树中,之后会为其创建 struct file,并将 struct file 与刚创建的 dentry 相关联,所以我们就可以通过 struct file 访问该文件的内容和属性了(访问文件内容也需要通过 inode),文件描述符封装了 file,所以最上层就能通过文件描述符访问文件的内容和属性了。
1、当我们要对磁盘中的文件进行增删改查时需要先将文件加载到内存中,在内存中修改后再刷新回磁盘。 2、文件在内存中用文件描述符来唯一标识文件,文件在磁盘中用 inode 来唯一标识文件。 3、fd 最终绑定的是唯一的 inode,因此进程拿到一个 fd 看到的是一个唯一文件(例外场景如硬链接看似不唯一,本质仍是 inode 唯一)。
1、一个磁盘,必须分区格式化,才有使用的前提。 2、一个分区,必须挂载到指定的目录才可以被真正的使用。 3、所以,可以根据访问⽬标⽂件的"路径前缀"准确判断我在哪⼀个分区。
在 linux 中,还有两种链接文件:软连接和硬链接。在讲解原理之前,我们先认识一下操作,如何用指令创建这两种链接文件:


我们看上图会发现软链接是一个独立的文件,而硬链接不是一个独立的文件。
软链接的文件内容是该链接文件指向文件的路径字符串,例如 test-soft 的文件内容就是 test.c 文件的路径字符串。
1、硬链接本质是在指定目录下,建立新的文件名和 inode 的映射关系,并没有在系统层面创建新的文件。 2、文件的硬链接数本质是有几个文件名指向特定的 inode,相当于是一种引用计数,删除一个文件名时若该文件名映射的 inode 的引用计数减 1,若引用计数为 0 了才会真的释放掉文件的属性和内容所占用的磁盘空间。 3、软链接必须使用绝对路径,硬链接推荐使用绝对路径。
软链接: 1、快捷方式。 2、让用户无感知进行软件升级,用户拿到的是->前面的文件,升级的是->后面的文件。
硬链接: 1、对文件进行备份,并且备份后该文件只会占用一份文件的属性和内容空间,因为创建硬链接本质只是创建了一组文件名和 inode 的映射关系。
1、为什么新建的普通文件硬链接数默认为 1?因为只有一组文件名和 inode 的映射关系。
2、为什么新建的目录文件(空目录)硬链接数默认为 2?因为目录不仅有目录名和 inode 的映射关系,目录里还有一个隐藏目录文件 . 表示当前目录,所以还有一组 . 和 inode 的映射关系。当我们在该目录下再创建一个目录文件时,目录的硬链接数会变为 3,因为新建的目录文件里会有一个隐藏目录文件 .. 指向当前文件。
3、一个目录的子目录数是它的硬链接数减 2,因为它本身要占两个(目录名和 . )。 4、在用户层面,我们可以给目录、普通文件设置硬链接,但是不允许对目录设置硬链接,因为对目录设置硬链接会发生路径环路问题,如下图。当我们想对该路径进行路径解析时,打开最后一个目录 home-hard 就相当于打开了 home,所以会继续从 home 开始进行解析,形成环路问题。

那为什么系统却允许的 . 和 .. 的存在呢,它们不就是对目录的硬链接吗?这其实是系统的一种特殊处理,系统通过底层逻辑避免了环路风险。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online