首先需要纠正问题本身,创建硬链接是占用存储空间的,软链接(或者常说符号链接)更是占用存储空间的。所以“linux创建的硬链接既然和软连接一样不占用磁盘空间”这句话本身就是错误。当然,它们的存储方式确实和普通的文件不一样,两者本身也完全不一样。
我在专栏中的下面这篇文章里介绍了XFS文件系统存储软链接的方式:
如果想详细了解XFS存储结构的可以进一步阅读相关的系列文章,XFS属于通用文件系统,理解它可以理解很多文件系统。我们下面单独以XFS文件系统为例,对软链接和硬链接的存储方式进行说明,大家可自行扩展理解到其它文件系统。下面我们先来看软链接:
符号链接,又俗称软链接,它实际上是一个真实的数据文件,存储它的时需要存储它的目录项(文件名)以及内容。如果你连目录项是什么都不知道,最好先看看书。目录项简单说就是目录(类型的文件)的数据内容,也可以看一下下面这个回答了解一下目录项的主要作用:
比如我下面这个软链接文件:
# ln -s nothisfile symlink1 # ls -li total 0 131 lrwxrwxrwx. 1 root root 10 Jun 1 21:50 symlink1 -> nothisfile
即使是通过ls命令,也可以看到这个符号链接的size是10字节,也就是"nothisfile"的10个字节。我们称这10个字节是它的数据大小,也就是它存储了10个字节的数据,实际上它还有很多元数据也占用了存储空间:
首先是存储这个符号链接文件的目录项,目录结构需要分配空间存储目录项,关于XFS的目录存储数据的结构,想详细了解的可以参考下面的文章:
醉卧沙场:XFS的on-disk组织结构(8)——Inode datafork of Directory - short/block
醉卧沙场:XFS的on-disk组织结构(9)——Inode datafork of Directory - leaf/node
醉卧沙场:XFS的on-disk组织结构(10)——Inode datafork of Directory - B+tree
我们这里只看一下当前这个简单的叫symlink1的目录项,我把它创建在了一个文件系统的根目录下,所以我们到这个文件系统的根目录去看一下:
# xfs_db /dev/mapper/testvg-testdev xfs_db> sb 0 xfs_db> p rootino rootino = 128
首先我们从这个文件系统的superblock里知道它的根目录,也就是root inode是128,那么我们去inode号为128的这个目录下去找symlink1:
xfs_db> inode 128 xfs_db> p core.magic = 0x494e core.mode = 040755 core.version = 3 core.format = 1 (local) ... u3.sfdir3.hdr.count = 1 u3.sfdir3.hdr.i8count = 0 u3.sfdir3.hdr.parent.i4 = 128 u3.sfdir3.list[0].namelen = 8 u3.sfdir3.list[0].offset = 0x60 u3.sfdir3.list[0].name = "symlink1" u3.sfdir3.list[0].inumber.i4 = 131 u3.sfdir3.list[0].filetype = 7
我们可以看到这个目录里存储着symlink1这个目录项,以及和这个目录项相关的元数据结构,比如我们知道symlink1符号链接文件对应的inode号是131。
这些数据都是真实存在在存储空间上的,不是“虚构”的,如果逐个字节的直接读取存储器(如磁盘)上的二进制数据,是可以找到这一片存储空间的,比如:
00010000: 49 4e 41 ed 03 01 00 00 00 00 00 00 00 00 00 00 INA............. 00010010: 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00010020: 60 b6 3b 43 0e 22 02 0a 60 b6 3b 3b 0a 07 b5 ea `.;C."..`.;;.... 00010030: 60 b6 3b 3b 0a 07 b5 ea 00 00 00 00 00 00 00 16 `.;;............ 00010040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00010050: 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00010060: ff ff ff ff 02 cf cb ae 00 00 00 00 00 00 00 12 ................ 00010070: 00 00 00 01 00 00 00 b7 00 00 00 00 00 00 00 00 ................ 00010080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00010090: 60 8b ad cf 19 2d 44 48 00 00 00 00 00 00 00 80 `....-DH........ 000100a0: 7a af 6b 93 ad 7b 4d 94 91 ca 38 ba f6 74 8a d4 z.k..{M...8..t.. 000100b0: 01 00 00 00 00 80 08 00 60 73 79 6d 6c 69 6e 6b ........`symlink 000100c0: 31 07 00 00 00 83 66 69 6c 65 32 01 00 00 00 84 1.....file2..... 000100d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
这就是上面那个目录的inode里的内容(一部分),如果你逐个字节对照是可以逐一对应上的(参考上面的文章)。就算你别的看不懂,"symlink1"这几个字符对应的"73 79 6d 6c 69 6e 6b 31"十六进制数你总该能看得懂的。所以说符号链接的目录项(文件名)相关数据是需要占用空间的。而且越复杂的目录结构可能需要不同的存储格式,不是简单的一个文件名就行了,还有很多元数据信息甚至在导致目录结构变化时(如发生split btree)会需要更多的空间。
从上面symlink1的目录项结构中我们看到了symlink1这个软链接文件的inode号是131,所以我们去这个inode上去看一下:
xfs_db> inode 131 xfs_db> p core.magic = 0x494e core.mode = 0120777 core.version = 3 core.format = 1 (local) ... u3.symlink = "nothisfile" a.sfattr.hdr.totsize = 51 a.sfattr.hdr.count = 1 a.sfattr.list[0].namelen = 7 a.sfattr.list[0].valuelen = 37 a.sfattr.list[0].root = 0 a.sfattr.list[0].secure = 1 a.sfattr.list[0].name = "selinux" a.sfattr.list[0].value = "unconfined_u:object_r:unlabeled_t:s0 00"
我们可以看到sysmlink1的inode结构里存储着它所指向的那个路径的路经名"nothisfile"。虽然我并没有实际创建这个路径,但是这不妨碍符号链接文件存储这个路径名,因为符号链接文件和它所指向的文件是两个不同的文件。这些也都是真实占用存储空间的,我们可以从存储器里看到这些数据,如下(列了一部分):
00010600: 49 4e a1 ff 03 01 00 00 00 00 00 00 00 00 00 00 IN.............. 00010610: 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00010620: 60 b6 3b 43 0e 22 02 0a 60 b6 3b 3b 0a 07 b5 ea `.;C."..`.;;.... 00010630: 60 b6 3b 3b 0a 07 b5 ea 00 00 00 00 00 00 00 0a `.;;............ 00010640: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00010650: 00 00 23 01 00 00 00 00 00 00 00 00 31 1f d7 7a ..#.........1..z 00010660: ff ff ff ff 44 31 12 2b 00 00 00 00 00 00 00 04 ....D1.+........ 00010670: 00 00 00 01 00 00 00 b7 00 00 00 00 00 00 00 00 ................ 00010680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00010690: 60 b6 3b 3b 0a 07 b5 ea 00 00 00 00 00 00 00 83 `.;;............ 000106a0: 7a af 6b 93 ad 7b 4d 94 91 ca 38 ba f6 74 8a d4 z.k..{M...8..t.. 000106b0: 6e 6f 74 68 69 73 66 69 6c 65 00 00 03 00 01 00 nothisfile...... 000106c0: 00 00 00 00 00 04 00 00 00 00 00 00 53 00 00 80 ............S... 000106d0: 00 00 00 00 00 05 00 00 00 00 00 00 53 00 01 70 ............S..p 000106e0: 00 00 00 00 00 08 00 00 00 00 00 00 23 00 00 80 ............#... 000106f0: 00 00 00 00 00 09 00 00 00 00 00 00 53 00 00 80 ............S... 00010700: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ...............
凑巧的是,我当前的系统还打开了selinux,我创建的文件被自动加了selinux的扩展属性信息,这些也是要占用存储空间的。这里我让symlink1指向了一个很短的路径,如果指向一个很长的路径,那么就需要更多的存储空间,inode结构的空间可能都不够,需要再另外分配block来存储。详细的还是参考上面的文章,我这里就不展开了。
所以到这里我们知道,一个软链接(符号链接)文件的创建会占用很多空间,有为其目录项分配的空间(也可能预分配好了,但是不管怎么说它要被占用),有其inode结构占用的空间,还可能有其扩展属性和额外的存储数据的空间等。如果你阅读了关于文件系统全局元数据的知识,如:
醉卧沙场:XFS的on-disk组织结构(3)——AGF&AGFL
醉卧沙场:XFS的on-disk组织结构(4)——BNO/CNT B+tree of AGF
醉卧沙场:XFS的on-disk组织结构(5)——AGI & (F)INO B+Tree
那么你就还可以想像到创建一个符号链接的操作很可能还要伴随着管理free space,管理inode和free inode,甚至管理reflink和rmapbt的数据的变化,这些全局管理结构的变化也是可能需要更多的存储空间的。
文件系统在进行解析路径的时候,默认在碰到符号链接的时候是要follow这个符号链接的,简单来说就是读取符号链接的内容,按照符号链接所记录的路经名走下去。所以如果你删除软链接所指向的文件,软链接文件本身还在,但是其存储的内容(路径名)就是变成了一个无效的路径,按照那个路径去走就找不到要找的目标了,自然访问不到文件。
硬链接就比较简单了,可能大家耳熟能详的说法就是硬链接和其指向的文件的inode号相同。是的,那么它是怎么做到的呢?如果你想详细了解inode是怎么回事,可以参考这几篇文章:
醉卧沙场:XFS的on-disk组织结构(6)——Inode Core
醉卧沙场:XFS的on-disk组织结构(7)——Inode Datafork of regular file
这里我们就简单说一下,比如我创建下面的硬链接:
# touch file # ln file hardlink1 # ls -li total 0 132 -rw-r--r--. 2 root root 0 Jun 1 22:33 file 132 -rw-r--r--. 2 root root 0 Jun 1 22:33 hardlink1 131 lrwxrwxrwx. 1 root root 10 Jun 1 21:50 symlink1 -> nothisfile
我们看到file和hardlink1这两个文件确实是相同的inode号,而且硬链接的类型不像软链接那样是"l"(表示符号链接),而是和file一样都是"-"(表示普通文件)。如果你阅读了上面的文章,你会知道,inode号如果相同,说明这两个文件本身就是同一个,它们指向同一片物理地址。但是不同的是,它们是两个不同的目录项。也就是说它们所在的目录用两块不同的目录项来存储这两个文件名及其相关结构,如下:
xfs_db> inode 128 xfs_db> p core.magic = 0x494e core.mode = 040755 core.version = 3 core.format = 1 (local) ... u3.sfdir3.hdr.count = 3 u3.sfdir3.hdr.i8count = 0 u3.sfdir3.hdr.parent.i4 = 128 u3.sfdir3.list[0].namelen = 8 u3.sfdir3.list[0].offset = 0x60 u3.sfdir3.list[0].name = "symlink1" u3.sfdir3.list[0].inumber.i4 = 131 u3.sfdir3.list[0].filetype = 7 u3.sfdir3.list[1].namelen = 4 u3.sfdir3.list[1].offset = 0x78 u3.sfdir3.list[1].name = "file" u3.sfdir3.list[1].inumber.i4 = 132 u3.sfdir3.list[1].filetype = 1 u3.sfdir3.list[2].namelen = 9 u3.sfdir3.list[2].offset = 0x88 u3.sfdir3.list[2].name = "hardlink1" u3.sfdir3.list[2].inumber.i4 = 132 u3.sfdir3.list[2].filetype = 1
可以看到,像上面我们存储symlink1目录项一样,文件系统分别存储了file和hardlink1,它们分处在两片存储区域,但是它们指向的inumber都是132(看上面输出,别光看我:-)。这就是两个目录项指向同一个inode。同样,我们可以在存储器里直接看到它们的内容:
00010000: 49 4e 41 ed 03 01 00 00 00 00 00 00 00 00 00 00 INA............. 00010010: 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00010020: 60 b6 45 68 01 32 b8 56 60 b6 45 5e 13 b0 05 b7 `.Eh.2.V`.E^.... 00010030: 60 b6 45 5e 13 b0 05 b7 00 00 00 00 00 00 00 33 `.E^...........3 00010040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00010050: 00 00 00 02 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00010060: ff ff ff ff b2 78 76 9a 00 00 00 00 00 00 00 14 .....xv......... 00010070: 00 00 00 01 00 00 00 c7 00 00 00 00 00 00 00 00 ................ 00010080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 00010090: 60 8b ad cf 19 2d 44 48 00 00 00 00 00 00 00 80 `....-DH........ 000100a0: 7a af 6b 93 ad 7b 4d 94 91 ca 38 ba f6 74 8a d4 z.k..{M...8..t.. 000100b0: 03 00 00 00 00 80 08 00 60 73 79 6d 6c 69 6e 6b ........`symlink 000100c0: 31 07 00 00 00 83 04 00 78 66 69 6c 65 01 00 00 1.......xfile... 000100d0: 00 84 09 00 88 68 61 72 64 6c 69 6e 6b 31 01 00 .....hardlink1.. 000100e0: 00 00 84 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 000100f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
目录项也是占空间的,所以不能说创建硬链接不占空间。虽然我上面的例子中的目录项们都在一个block里,没有造成额外分配,但是不表示目录项不占用空间,随着目录项的增多增大,肯定会需要更多的blocks的。而且也可能会产生目录数据结构的变化,占用更多空间来存储元数据。
所以说,硬链接不需要额外存储数据和inode结构,但是需要额外存储目录项,这是硬链接需要的存储空间,甚至可以说硬链接就是额外的目录项不是一个额外的文件。而且在分配目录项时可能造成多种目录结构甚至全局结构的变化,也是需要使用更多空间的。
硬链接因为是两个目录项指向同一个inode,也就是指向同一个物理地址。所以当你删除其中一个,你只是删除了目录项,但是因为还有其它目录项引用这个inode的物理地址,所以这片地址仍然有效,可以通过剩下的指向这片inode的目录项继续访问。
所以,理解文件系统是真正理解硬链接和软链接的途径。即使不深入理解,也应该知道上述的内容,不能模糊的说硬链接和软链接不占用空间。我们要知道它们的哪部分不额外占用空间,哪部分占用空间。
关于上面列出的部分文章,如果有兴趣想详细了解的话,还是建议按顺序阅读,从中间读会对很多前置知识难以理解。更多内容可参考下面这个索引文章里列出的链接: