问题现象
当前有个目录 a,需要对其创建一个软链接 b。
代码语言:javascript复制 tree
.
`-- a
|-- a1.txt
`-- a2.txt
1 directory, 2 files
第一次执行ln -s a b
成功执行,符合预期
代码语言:javascript复制tree
.
|-- a
| |-- a1.txt
| `-- a2.txt
`-- b -> a
2 directories, 2 files
通过软链能正确访问到 a1.txt
代码语言:javascript复制ls -l b/a1.txt
-rw-r--r-- 1 root root 0 Aug 17 02:08 b/a1.txt
第二次执行ln -s a b
成功执行,不符合预期
代码语言:javascript复制tree
.
|-- a
| |-- a -> a
| |-- a1.txt
| `-- a2.txt
`-- b -> a
2 directories, 3 files
此时软链接 b 已存在,我的预期是执行失败,或者覆盖软链接 b,但实际上在 a 下创建了一个软链接 a,这是第一个问题
第二个问题,为什么新创建的软链接文件名是 a,而不是 b
第三个问题,通过新创建的软链,无法访问到 a1.txt
代码语言:javascript复制ls -l a/a/a1.txt
ls: cannot access 'a/a/a1.txt': Too many levels of symbolic links
第三次执行ln -s a b
执行失败,提示软链接 b/a 已存在。
代码语言:javascript复制ln -s a b
ln: failed to create symbolic link 'b/a': File exists
第四个问题,为什么第二次执行都没报错,第三次却报错了?到底能不能重复执行?
软链接原理
先介绍下文件系统。
磁盘布局
在 VSFS(Very SimpleFile System,简单文件系统)中
superblock
超级块里存储了文件系统信息,inode 数量,数据块数量,inode 区域的开始位置等信息。
inode bitmap
用 bitmap 来储存 inode 区域的分配情况。当写入一个新文件时,需要创建一个 inode,保存在 inode 空闲区域。如果直接扫描 inode 区域,寻找空闲位置,是非常耗费 IO 性能的。而通过 inode bitmap,只需要一次 IO,就能知道整个 inode 区域的分配情况。再在内存中遍历下,得到一个空闲的 inode 号。通过 inode 号,就能找到 inode 区域中对应位置。
data bitmap
和 inode bitmap 功能类似。
inode
inode 是index node(索引节点)的缩写,因为inode号用于索引磁盘上的inode数组,以便查找该inode号对应的inode。
inode 用于保存文件的元数据,例如文件类型(例如,常规文件、目录等)、大小、分配给它的块数、保护信息(如谁拥有该文件以及谁可以访问它)、一些时间信息(包括文件创建、修改或上次访问的时间文件下),以及有关其数据块驻留在磁盘上的位置的信息(如某种类型的指针)。
注意,inode 里没有文件名,在文件系统中,文件的路径是由目录文件构成的。每个目录在数据块区域是一个目录文件,包含这个目录下的文件名、文件的 inode 号、这条记录的长度、文件名字符串的长度等信息。所以通过硬链接或软链接,可以使用不同的路径访问到同一个文件。 延伸想想同一个文件系统下,mv 命令是如何工作的。
使用 df -i 命令查看 inode 的使用量
代码语言:javascript复制df -i
Filesystem Inodes IUsed IFree IUse% Mounted on
overlay 3907584 119123 3788461 4% /
tmpfs 1003656 17 1003639 1% /dev
shm 1003656 1 1003655 1% /dev/shm
/dev/vda1 3907584 119123 3788461 4% /etc/hosts
tmpfs 1003656 1 1003655 1% /sys/firmware
小文件太多,可能造成磁盘没满,但是 inode 用完了,导致无法创建文件。可通过上诉命令检查。
拿上面的目录举例
代码语言:javascript复制tree
.
`-- a
|-- a1.txt
`-- a2.txt
1 directory, 2 files
下图左边部分在磁盘的 inode 区域,右边部分在磁盘的数据块区域。
执行软链命令后
ln -s a b
tree
.
|-- a
| |-- a1.txt
| `-- a2.txt
`-- b -> a
2 directories, 2 files
创建软链接执行了下列动作
- 修改当前目录对应的目录文件,增加软链接 b 的信息
- 创建软链接 b 的 inode 注意,没有磁盘指针,软链接是不会在数据块区域分配空间。 另外指向目标路径是个相对路径,和创建软链接的方式有关。
软链接相对路径问题
准备 case
代码语言:javascript复制echo `date` > c.txt
mkdir d
ln -s c.txt d/c.txt
查看目录结果
代码语言:javascript复制tree
.
|-- c.txt
`-- d
`-- c.txt -> c.txt
查看软链报错
代码语言:javascript复制cat d/c.txt
cat: d/c.txt: Too many levels of symbolic links
使用 stat 查看软链信息
代码语言:javascript复制stat d/c.txt
File: d/c.txt -> c.txt
Size: 5 Blocks: 0 IO Block: 4096 symbolic link
Device: 3bh/59d Inode: 533547 Links: 1
Access: (0777/lrwxrwxrwx) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2024-08-17 08:27:32.335451007 0000
Modify: 2024-08-17 08:27:12.832303012 0000
Change: 2024-08-17 08:27:12.832303012 0000
Birth: -
d/c.txt -> c.txt
这是一个相对路径,相对于软链本身,那么这个软链指向的是自己,所以报错:“Too many levels of symbolic links”。
ln 命令 help 信息里也有说明相对路径的情况。
代码语言:javascript复制ln --help
Usage: ln [OPTION]... [-T] TARGET LINK_NAME
or: ln [OPTION]... TARGET
or: ln [OPTION]... TARGET... DIRECTORY
or: ln [OPTION]... -t DIRECTORY TARGET...
In the 1st form, create a link to TARGET with the name LINK_NAME.
In the 2nd form, create a link to TARGET in the current directory.
In the 3rd and 4th forms, create links to each TARGET in DIRECTORY.
Create hard links by default, symbolic links with --symbolic.
By default, each destination (name of new link) should not already exist.
When creating hard links, each TARGET must exist. Symbolic links
can hold arbitrary text; if later resolved, a relative link is
interpreted in relation to its parent directory.
如何避免此问题?
- 使用绝对路径。
- 创建软链时,先进入到即将创建的软链的所在目录,然后被软链文件使用相对于当前目录的路径。
解释问题现象
第一个问题
为什么第二次执行ln -s a b
时,没有覆盖软链接 b,而是在 a 下创建了一个软链接 a?
第一次创建软链后,生成了软链 b。在第二次创建软链时,会判断目标路径是否是存在的目录,此时目标路径是 b,它指向了 a,所以目标路径是存在的 a,那么就在 a 目录下创建软链。也就是上面ln --help
信息里的第三种和第四种用法。
第二个问题
为什么新创建的软链接文件名是 a,而不是 b?
创建软链的目标路径是目录时,就在该目录下创建自身的同名软链,指向自己。
第三个问题
通过新创建的软链,无法访问到 a1.txt。
原因是上文提到的软链相对路径问题,检查 a 目录下的软链 a,看看它指向哪里。
代码语言:javascript复制stat a/a
File: a/a -> a
Size: 1 Blocks: 0 IO Block: 4096 symbolic link
Device: 3bh/59d Inode: 533417 Links: 1
Access: (0777/lrwxrwxrwx) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2024-08-17 03:18:11.949193005 0000
Modify: 2024-08-17 03:18:10.232193004 0000
Change: 2024-08-17 03:18:10.232193004 0000
Birth: -
发现它指向的是自己,而不是预期的父目录,所以访问它时报错:“Too many levels of symbolic links”。
第四个问题
为什么第二次执行都没报错,第三次却报错了?到底能不能重复执行?
原因是软链目录是存在的目录时,就在该目录下创建自身的同名软链,指向自己。而第二次执行时已经创建了这个软链 a/a,再次创建时就会报错:“ File exists”。
解决
方案一
创建软链前先检查软链是否存在,存在的话检查是否是指向预期的源文件。
方案二
使用 ln 命令的选项 n。
-n, --no-dereference treat LINK_NAME as a normal file if it is a symbolic link to a directory
达到的效果是在第二次创建软链时,因为软链 b 已存在,报错。而不是在 b 软链指向的 a 目录下,再去创建一个软链指向自己。
代码语言:javascript复制ln -sn a b
ln: failed to create symbolic link 'b': File exists
参考
- 《操作系统导论》
- https://linuxconfig.org/how-to-fix-too-many-levels-of-symbolic-links-error
- https://unix.stackexchange.com/questions/588021/why-does-ln-s-create-a-directory-if-soft-link-exists?rq=1