Linux 链接文件详解

配图源自 Freepik

一、前言

以 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

平常访问一个文件,可以分为几个步骤:

  1. 在当前目录文件中,根据文件名称找到对应的文件 inode 号码;
  2. 通过 inode 号码,获取到 inode 信息;
  3. 根据 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.txta-hardlink.txt 的 inode 号码是相同的,都指向 59499302 这个 inode 号码。其中有一项表示「链接个数」,记录指向该 inode 的文件名称总数。目前指向 59499302 号码的文件有 a.txta-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.txta-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。

因此,软链接本质上只是记录了某个文件路径。

注意点

  1. 创建软链接建议使用「绝对路径」,以避免所生成的软链接在移动至其他目录之后,无法访问到原始文件。
$ 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,自然就找不到了。

注意,修改原始文件名称、移动或删除原始文件,就无法通过它的软链接访问到文件了。

  1. 由于硬链接和原始文件的形式是一模一样的,因此操作系统是无法区分硬链接还是原始文件的。在文件管理器中二者看着是一样的。软链接则在文件图标上有一个箭头的图案。

  2. 硬链接无法跨文件系统。

前面也提到过,不能跨文件系统的原因是,不同文件系统的 inode 号码是会出现重复现象的。

  1. 硬链接不能链接目录文件。
$ ln src src-harklink
ln: src: Is a directory

拒绝创建目录硬链接是 ln 命令本身,而不是操作系统。因为对目录创建硬链接,可能会打破文件系统的有向无环图结构。

更多请看文章:多角度分析为什么 Linux 的硬连接不能指向目录

  1. 移动、修改文件名称,不会影响 inode 号码。

  2. 一些文件名称包含特殊字符的文件,无法正常删除。可以通过直接删除 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

你可能感兴趣的:(Linux 链接文件详解)