一、前言
以 macOS 为例,在 Finder 中右键可以看到「复制」、「拷贝」、「制作替身」等操作。它们之间有什么区别呢?另外,还有本文重点讨论的「软链接」、「硬链接」又是什么呢?
从操作结果看,它们都生成了一个「副本」,但有着本质上的区别。
复制与拷贝
它们的共同点是都会生成一个文件的副本,而且副本与原始文件是两个独立的文件。也就是说,修改其一不会对另一个造成任何影响。二者区别在于,复制操作会立刻在当前目录下生成一个副本,而拷贝操作则会将副本放至剪贴板,等待被粘贴。
软链接与硬链接
操作系统内置的文件管理器未直接提供相关功能,通常只能通过命令行操作。从操作结果看,它们都产生了一个“副本”(打个引号),但这个副本与原始文件是有关联的。
替身(Alias)
它是 macOS 操作系统独有的一个概念。它是结合了软链接和硬链接的优缺点的一种解决方案。
可以提前观察一下,它们之间有哪些特点。
$ ls -il
total 40
59499107 -rw-r--r-- 1 frankie staff 21 7 10 19:57 README.md
59499302 -rw-r--r-- 2 frankie staff 13 7 10 20:00 a-hardlink.txt
59499638 lrwxr-xr-x 1 frankie staff 49 7 10 20:09 a-symlink-absolute.txt -> /Users/frankie/Desktop/Web/Temp/simple-link/a.txt
59499576 lrwxr-xr-x 1 frankie staff 5 7 10 20:08 a-symlink-relative.txt -> a.txt
59499486 lrwxr-xr-x 1 frankie staff 5 7 10 20:04 a-symlink.txt -> a.txt
59499302 -rw-r--r-- 2 frankie staff 13 7 10 20:00 a.txt
59499900 -rw-r--r--@ 1 frankie staff 960 7 10 20:12 a.txt的替身
59499003 -rw-r--r-- 1 frankie staff 92 7 10 19:57 package.json
59499138 drwxr-xr-x 2 frankie staff 64 7 10 19:57 src
二、基础概念
链接
通常,人类所说的链接多指 URL,表示互联网上的网页、图像、音频、文件等资源。
但是,本文所提之「链接」是 POSIX 标准中的一个概念,是一种文件共享的方式。这种文件称为「链接文件」,目前主流操作系统都支持链接文件。
链接文件可分为「符号链接 symbolic link」和「硬链接 hard link」两种。其中符号链接又称为「软链接 soft link, symlink」。
从中文角度出发,为了对比更鲜明,下文将称为「软链接」和「硬链接」。
文件与目录
人类在管理文件时,通常会划分为「文件」和「目录」两种,后者是承载前者的一种容器,这种划分方式对人类是友好的,可便于归纳整理。但在操作系统看来,目录(directory)本身也是一种文件,下称为目录文件。
下文若无特殊说明,所提到的「文件」均从操作系统角度出发,因此它可能包含了文件和目录。
文件存储
我们的文件是存储在「硬盘」上的,硬盘上的最小存储单位是「扇区 Sector」。操作系统读写硬盘时,最小的存取单位是「块 Block」。一个扇区存储 512 个字节(0.5KB),而一个块大小最常见为 4KB,即连续的八个扇区组成了一个块。
一个文件分为数据部分和元信息两部分,其中文件数据都存储在「Block」中,文件元信息存储在一个名为「inode」的区域。
每一个文件都有对应的 inode,它包含了文件的元信息,比如创建者、创建日期、大小等信息。需要注意的是,文件名称并不包含在 inode 信息中(下文会介绍),而且 inode 也会占用硬盘空间。
文件区分
通常来说,人类是通过「文件名称」来区分不同文件的,更严谨一点应该是「文件路径」。
但是,操作系统是通过一个称为「inode 号码」(一个 inode 对应一个 inode 号码)的东西来区分不同文件的。操作系统允许不同的文件名称可以具有同一个 inode 号码。inode 号码相同的文件,其文件数据是相同的,都指向了硬盘中存储的同一份数据(自然也就只占用了一份硬盘空间)。这才是本质意义上的同一文件。
以祖国的公民身份证系统来比喻,身份证号码对应 inode 号码,姓名对应为文件名称。在全国范围内,身份证号码是唯一的,姓名则不是,因此若要确定具体的某个人,只能通过身份证去查找。姓名和文件名称一样允许重名,当然在操作系统中同一路径下文件不允许重名。
细心的同学会发现,这里面其实是有界定范围的,身份证系统中的证件号码只能确保在国内是唯一的。假设某个国家也有一套类似的身份证系统,我国的小明与他国的小花证件号码是有可能重复的,那么在全世界范围内,就无法通过这个证件号码指向具体的某个人。在操作系统上同样存在这个问题,原因是多数操作系统都支持挂载「多个」文件系统,而 inode 号码则是由文件系统进行分配。尽管同一个文件系统中 inode 号码不会重复,但是多个文件系统之间是会出现相同的 inode 号码的。可以将操作系统比喻为全世界,文件系统比喻成国家,道理是一样的。
下文若无特殊说明,所指 inode 号码均指同一文件系统内。
文件信息
通过 ls
(英文 list 的缩写)命令可以查看文件清单。本文用到参数有:
-
-a
表示列举全部文件,包括隐藏文件。 -
-l
表示列举文件细节,包括文件属性、权限、所有者、所在群组、大小、相关日期、文件名称、链接指向等。 -
-i
表示列举文件的 inode 号码。
# 列举所有文件名称
$ ls
README.md a-symlink-relative.txt a.txt的替身
a-hardlink.txt a-symlink.txt package.json
a-symlink-absolute.txt a.txt src
# 列举所有文件名称和 inode 号码
$ ls -i
59499107 README.md 59499576 a-symlink-relative.txt 59499900 a.txt的替身
59499302 a-hardlink.txt 59499486 a-symlink.txt 59499003 package.json
59499638 a-symlink-absolute.txt 59499302 a.txt 59499138 src
# 列举所有文件名称、inode 号码以及其他细节
$ ls -il
total 40
59499107 -rw-r--r-- 1 frankie staff 21 7 10 19:57 README.md
59499302 -rw-r--r-- 2 frankie staff 13 7 10 20:00 a-hardlink.txt
59499638 lrwxr-xr-x 1 frankie staff 49 7 10 20:09 a-symlink-absolute.txt -> /Users/frankie/Desktop/Web/Temp/simple-link/a.txt
59499576 lrwxr-xr-x 1 frankie staff 5 7 10 20:08 a-symlink-relative.txt -> a.txt
59499486 lrwxr-xr-x 1 frankie staff 5 7 10 20:04 a-symlink.txt -> a.txt
59499302 -rw-r--r-- 2 frankie staff 13 7 10 20:00 a.txt
59499900 -rw-r--r--@ 1 frankie staff 960 7 10 20:12 a.txt的替身
59499003 -rw-r--r-- 1 frankie staff 92 7 10 19:57 package.json
59499138 drwxr-xr-x 2 frankie staff 64 7 10 19:57 sr
以 macOS 为例,命令行中文件细节所表示如下。
不同操作系统或不同终端工具所展示内容或有不同。
三、inode
本文一些内容摘自阮一峰老师文章:理解 inode。
往下之前,需要先了解 inode 是什么,干什么用的?
inode 是 index node 的简写,译为索引节点。每一个文件都有对应的 inode,它包含了以下这些文件元信息:
- 文件字节数
- 文件所有者的 User ID
- 文件所在群组的 User ID
- 文件的读、写、执行权限
- 文件的时间戳,共有三个:ctime 指 inode 上一次更改时间,mtime 指文件内容上一次更改时间,atime 指文件上一次被访问时间。
- 链接个数,即有多少个文件名称指向这个 inode
- 文件数据 Block 的文件
是的,inode 信息里没有存储文件名称。可以先思考下:它会被存放到哪里呢?
inode 信息
使用 stat
命令可以查看文件的 inode 信息:
$ stat -x README.md
File: "README.md"
Size: 21 FileType: Regular File
Mode: (0644/-rw-r--r--) Uid: ( 501/ frankie) Gid: ( 20/ staff)
Device: 1,5 Inode: 59499107 Links: 1
Access: Sun Jul 10 19:57:53 2022
Modify: Sun Jul 10 19:57:51 2022
Change: Sun Jul 10 19:57:51 2022
Birth: Sun Jul 10 19:57:26 2022
inode 大小
inode 本身也会占用硬盘空间。操作系统会将硬盘分为两个区域:一个是「数据区」,用于存储文件数据;一个是「inode 区」,用于存放 inode 所包含的信息。
每个 inode 大小一般是 128 字节或 256 字节。通常是每 1KB 或每 2KB 就设置一个 inode。由于在硬盘格式化时,inode 总数量就已经给定。而且每个文件必须要有一个 inode,因此有可能发生 inode 已经用完,但硬盘未存满的情况,此时无法再从硬盘上创建新文件。
inode 号码
每个文件都有 inode,而每个 inode 会对应一个号码,称为「inode 号码」。在 Unix/Linux 操作系统中是通过 inode 号码来识别不同文件的。文件名称只是 inode 号码的一个「别称」,但这个别称对人类是友好的、易于记忆与区分的。
还是用我国的公民身份证系统来比喻,身份证号码对应 inode 号码,而姓名则对应文件名称。平常问候朋友总不能说「那个 4413*****3425(乱写的)吃饭了没」,显然「小明你吃饭了没」更合适。
目录文件
目录文件的结构非常简单,就是一系列目录项(dirent)的列表。
每个目录项由两部分组成:所包含文件的文件名称,以及该文件名称对应的 inode 号码。
前面提到,文件名称没有存到 inode 信息中,文件名称被存在至目录项中。
通过 ls -i
命令可以查看文件的 inode 号码:
# 列出目录文件中的所有文件名称
$ ls
README.md package.json src
# 列出目录文件中所有文件名称和 inode 号码
$ ls -i
59499107 README.md 59499003 package.json 59499138 src
# 列出目录文件中所有文件名称、inode 号码以及其他详细信息
$ ls -il
total 16
59499107 -rw-r--r-- 1 frankie staff 21 7 10 19:57 README.md
59499003 -rw-r--r-- 1 frankie staff 92 7 10 19:57 package.json
59499138 drwxr-xr-x 2 frankie staff 64 7 10 19:57 src
平常访问一个文件,可以分为几个步骤:
- 在当前目录文件中,根据文件名称找到对应的文件 inode 号码;
- 通过 inode 号码,获取到 inode 信息;
- 根据 inode 信息,找到文件数据存储的 Block,然后读出数据。
当操作系统得知 inode 号码之后,后续可以直接通过该号码对文件执行相关操作。
四、软链接与硬链接
创建链接
使用 ln
(英文 link 的缩写)命令可以为文件创建软链接、硬链接。命令较为简单,如下:
# 创建硬链接
$ ln
# 创建软链接
$ ln -s
创建软链接时,原始文件路径(即
)建议使用「绝对路径」,以避免移动软链接后无法正常访问目标文件。
硬链接
通常情况下,文件名称与 inode 号码是「一一对应」的关系,即一个 inode 对应一个文件名称。但是,主流操作系统都允许多个文件名称指向同一个 inode 号码。
换句话说,通过不同的文件名称可以访问到同样的文件内容,对文件内容的修改自然会影响所有对应的文件名称。但是,删除其中一个,不会影响到另一个的访问,这种情况称为「硬链接」。
$ touch a.txt && echo 'some text...' >> a.txt
$ ln a.txt a-hardlink.txt
$ ls -il
total 32
59499107 -rw-r--r-- 1 frankie staff 21 7 10 19:57 README.md
59499302 -rw-r--r-- 2 frankie staff 13 7 10 20:00 a-hardlink.txt
59499302 -rw-r--r-- 2 frankie staff 13 7 10 20:00 a.txt
59499003 -rw-r--r-- 1 frankie staff 92 7 10 19:57 package.json
59499138 drwxr-xr-x 2 frankie staff 64 7 10 19:57 src
可以看到 a.txt
与 a-hardlink.txt
的 inode 号码是相同的,都指向 59499302
这个 inode 号码。其中有一项表示「链接个数」,记录指向该 inode 的文件名称总数。目前指向 59499302
号码的文件有 a.txt
和 a-hardlink.txt
两个文件,因此其链接个数为 2
。
$ rm a-hardlink.txt
$ ls -il
total 24
59499107 -rw-r--r-- 1 frankie staff 21 7 10 19:57 README.md
59499302 -rw-r--r-- 1 frankie staff 13 7 10 20:00 a.txt
59499003 -rw-r--r-- 1 frankie staff 92 7 10 19:57 package.json
59499138 drwxr-xr-x 2 frankie staff 64 7 10 19:57 src
当我们删除其一时,a.txt
对应链接数将会「减一」。当链接个数减到零时,表示没有文件名称指向这个 inode 了,操作系统将会回收这个 inode 号码,以及其对应的 block 区域。
需要注意的是,创建目录时会默认生成两个目录项:.
和 ..
,分别表示当前目录的硬链接、父目录的硬链接,它们的 inode 号码也是对应的。因此,当我们创建一个空目录后其链接个数为 2
:一个是所创建目录名,一个是当前目录下的 .
目录。
因此,硬链接本质上只是新增一个文件名称到某个 inode 号码而已。
软链接
通过前面发现,硬链接与原始文件的 inode 号码是相同的。但是软链接的 inode 号码与原始文件是不一样的。
假设有文件 A 和 B,它们的 inode 号码不同,但是 B 文件的内容是 A 文件的路径。当读取 B 文件时,系统将会自动导向 A 文件,读取的都是 A 文件的内容。此时,B 文件被称为 A 文件的「软链接」。
$ ln -s a.txt a-symlink.txt
$ ls -il
total 32
59499107 -rw-r--r-- 1 frankie staff 21 7 10 19:57 README.md
59499302 -rw-r--r-- 2 frankie staff 13 7 10 20:00 a-hardlink.txt
59499486 lrwxr-xr-x 1 frankie staff 5 7 10 20:04 a-symlink.txt -> a.txt
59499302 -rw-r--r-- 2 frankie staff 13 7 10 20:00 a.txt
59499003 -rw-r--r-- 1 frankie staff 92 7 10 19:57 package.json
59499138 drwxr-xr-x 2 frankie staff 64 7 10 19:57 src
可以发现,a.txt
与 a-symlink.txt
的 inode 号码是不一样的。因此,创建软链接不会使得原始文件的链接个数发生改变。
通过创建软链接而生成的新文件,其本身拥有一个新的 inode。与原始文件不同的是,它的文件内容是原始文件的文件路径。以 a-symlink.txt
为例,其文件大小为 5 个字节,就是文件内容 a.txt
的字节数(一个英文字符占一个字节)。
由于 a-symlink.txt
文件是依赖于 a.txt
文件的,意味着删除原始文件 a.txt
后,再次访问 a-symlink.txt
文件就会报错:No such file or directory。
因此,软链接本质上只是记录了某个文件路径。
注意点
- 创建软链接建议使用「绝对路径」,以避免所生成的软链接在移动至其他目录之后,无法访问到原始文件。
$ ln -s a.txt a-symlink-relative.txt
$ ln -s ~/Desktop/Web/Temp/simple-link/a.txt a-symlink-absolute.txt
二者区别在于,原始文件的路径一个是相当路径,一个是绝对路径。接着,将这两个软链接移至其他桌面目录,然后在桌面目录下访问软链接,会发生什么呢?
$ mv a-symlink-relative.txt ~/Desktop
$ mv a-symlink-absolute.txt ~/Desktop
$ cat ~/Desktop/a-symlink-relative.txt
cat: a-symlink-relative.txt: No such file or directory
$ cat ~/Desktop/a-symlink-absolute.txt
some text...
$ ls -l
total 32
-rw-r--r-- 1 frankie staff 21 7 10 19:57 README.md
-rw-r--r-- 2 frankie staff 13 7 10 20:00 a-hardlink.txt
lrwxr-xr-x 1 frankie staff 49 7 10 20:09 a-symlink-absolute.txt -> /Users/frankie/Desktop/Web/Temp/simple-link/a.txt
lrwxr-xr-x 1 frankie staff 5 7 10 20:08 a-symlink-relative.txt -> a.txt
lrwxr-xr-x 1 frankie staff 5 7 10 20:04 a-symlink.txt -> a.txt
-rw-r--r-- 2 frankie staff 13 7 10 20:00 a.txt
-rw-r--r-- 1 frankie staff 92 7 10 19:57 package.json
drwxr-xr-x 2 frankie staff 64 7 10 19:57 src
a-symlink-relative.txt
在移动到其他目录后,就不能正常访问原始文件了,而 a-symlink-absolute.txt
则不受影响。换句话说,软链接里记录的原始文件路径可以是相当路径或绝对路径,取决于创建时使用了哪种路径(即上面箭头后面的路径)。其中 cat ~/Desktop/a-symlink-relative.txt
相当于 cat ~/Desktop/a.txt
,自然就找不到了。
注意,修改原始文件名称、移动或删除原始文件,就无法通过它的软链接访问到文件了。
由于硬链接和原始文件的形式是一模一样的,因此操作系统是无法区分硬链接还是原始文件的。在文件管理器中二者看着是一样的。软链接则在文件图标上有一个箭头的图案。
硬链接无法跨文件系统。
前面也提到过,不能跨文件系统的原因是,不同文件系统的 inode 号码是会出现重复现象的。
- 硬链接不能链接目录文件。
$ ln src src-harklink
ln: src: Is a directory
拒绝创建目录硬链接是 ln
命令本身,而不是操作系统。因为对目录创建硬链接,可能会打破文件系统的有向无环图结构。
更多请看文章:多角度分析为什么 Linux 的硬连接不能指向目录
移动、修改文件名称,不会影响 inode 号码。
一些文件名称包含特殊字符的文件,无法正常删除。可以通过直接删除 inode 的方式去删除文件。
五、替身
通过前面的介绍,可以知道软链接、硬链接都有一些缺点:
- 移动、重命名或删除原始文件后,就无法通过软链接访问到原始文件了。
- 硬链接无法链接目录,不能跨文件系统。
那么,能不能结合两者的优点,产生一种新的方案来避免两者的缺点呢?
在 macOS 上有一种类似的解决方案:「替身 Alias」。替身文件记录了原始文件的路径和 inode 号码,当访问替身文件时,系统分析替身文件,找到原始文件的路径信息,然后判断原始文件是否存在,若存在则访问它,如果不存在,就寻找有相同 inode 号码的文件,然后访问该文件。
$ osascript -e 'tell application "Finder" to make alias file to POSIX file "/Users/frankie/Desktop/Web/Temp/simple-link/a.txt" at POSIX file "/Users/frankie/Desktop/Web/Temp/simple-link/"'
alias file a.txt的替身 of folder simple-link of folder Temp of folder Web of folder Desktop of folder frankie of folder Users of startup disk
$ ls -il
total 40
59499107 -rw-r--r-- 1 frankie staff 21 7 10 19:57 README.md
59499302 -rw-r--r-- 2 frankie staff 13 7 10 20:00 a-hardlink.txt
59499638 lrwxr-xr-x 1 frankie staff 49 7 10 20:09 a-symlink-absolute.txt -> /Users/frankie/Desktop/Web/Temp/simple-link/a.txt
59499576 lrwxr-xr-x 1 frankie staff 5 7 10 20:08 a-symlink-relative.txt -> a.txt
59499486 lrwxr-xr-x 1 frankie staff 5 7 10 20:04 a-symlink.txt -> a.txt
59499302 -rw-r--r-- 2 frankie staff 13 7 10 20:00 a.txt
59499900 -rw-r--r--@ 1 frankie staff 960 7 10 20:12 a.txt的替身
59499003 -rw-r--r-- 1 frankie staff 92 7 10 19:57 package.json
59499138 drwxr-xr-x 2 frankie staff 64 7 10 19:57 src
$ cat a.txt的替身
bookmark88��$n�|=�A�lUsersfrankieDesktopWebTemp
simple-linka.txt 0@LX �~
��I������A�={l�� file:///
Macintosh H� hA���$C7FF9732-E725-402E-8FC2-AF9D4F2482C5���/3dnibtxt????����|D@4TlUlVd $ � � � � � 0 d�x� ���d��l"�0%
创建替身其 inode 号码与原始文件的不相同。通过 cat
命令查看内容时,不会重定向到原始文件,而是直接看到其内容。在 Finder 上可以看到它跟软链接一样,文件图标上都有一个箭头图案。
六、应用场景
总结一下软链接、硬链接的特点:
共同点
- 创建软链接或硬链接,都会产生一个文件副本。
- 删除硬链接或软链接,都不会对原始文件产生影响。
- 对硬链接或软链接的文件内容进行修改,实质上都是在操作原始文件的内容。因此修改任意一个,其他都会同步受影响。
不同点
- 硬链接无法链接目录,软链接是可以的。
- 硬链接无法跨文件系统,软链接是可以的。
- 删除或移动原始文件,对硬链接不会产生影响,但通过软链接再也无法访问到原始文件的内容。
- 重命名原始文件,本质上只是修改了文件路径,对 inode 无影响,自然就不会影响到硬链接,但软链接就无法访问到原始文件了。
- 创建硬链接不占用硬盘空间,而创建软链接会占用一点点的硬盘空间(需存储 inode 信息和文件内容)。
- 从视觉角度来看,软链接的文件图标有一个箭头图案(不同操作系统可能略有差别),而硬链接跟普通文件一样,如果不通过文件名称无法区分普通文件还是硬链接文件。
应用场景
在前端领域也广泛使用了软链接、硬链接,比如 npm CLI、npm link、pnpm 等工具。
以 pnpm 为例,通过 pnpm install
命令安装的依赖包都将存储在 ~/Library/pnpm/store
目录下 (7.x 版本之前全局目录是 ~/.pnpm_store
,详见 #2574) 。假设项目中安装了 jest 依赖包,会产生这样两个目录:
./node_modules/jest
./node_modules/.pnpm/[email protected]/node_modules/jest
第一个目录是第二个目录的软链接,Node 查找 jest
依赖会找到 ./node_modules/jest
,由于它是软链接,系统根据其文件内容的路径找到了 ./node_modules/.pnpm/[email protected]/node_modules/jest
目录,该目录里面的文件都是全局依赖目录下相关文件的硬链接。pnpm 利用软链接构建出更干净的 node_modules
目录,不再像 npm/yarn 那样的扁平化结构,也解决了「幽灵依赖」的问题。而且 node_modules/.pnpm
下使用了硬链接可以节省大量的硬盘空间。
除此之外,还有很多很多...
比如 Windows 的快捷方式、Finder 的侧边栏等。对于一些路径层级特别深的文件,快捷方式是一个非常高效的方法。还有,你女朋友在桌面删掉的只是软链接而已,对原始文件毫无影响...
比如照片分类。假设我们的硬盘里存储了 10GB 的照片,如果想要按拍摄时间、拍摄地点、图片类型等方式分类,我们可以利用硬链接的特点,在其他目录创建图片文件的硬链接进行分类,最重要的是它们只会占用一份硬盘空间。
比如文件共享。假设多人对同一个文件进行维护的时候,每个人可以在私人目录下创建该文件的硬链接,它的所有修改都会同步到原始文件中。最重要的是,即使某人不慎误删了文件,也不会丢失文件。
比如文件备份。最原始的方式应该是修改完目标文件后,然后拷贝一份至存档目录。这种方式除了麻烦,还会占用两份同等大小的硬盘空间。利用硬链接的特点就可以轻松解决,达到仅占用一份硬盘空间,同步修改的效果。
七、参考文章
- 理解 inode
- Linux 文件系统详解
- 「复制、拷贝、替身、软连接、硬连接」区别详解
- What's the difference between alias and link?
- Symbolic Link vs Alias