
理解硬链接,关键仍然要回到 inode、目录项和链接数之间的对应关系。它并不是生成另一份独立文件,而是在同一文件系统内,让多个目录项指向同一个 inode。
基于这一点,硬链接既具有共享同一份底层数据的特征,也受到明确约束:不能跨文件系统、不能链接目录,删除时关注的是链接数变化,而不是“源文件”和“副本”的区分。
硬链接是同一个文件系统内,多个目录项(文件名)指向同一个 inode。这些文件名对文件系统来说是完全等价的,没有"源文件"和"链接文件"之分。
# 创建硬链接ln /data/mysql/backup.sql /backup/mysql_backup.sql# 验证:两个文件指向同一个 inodels -li /data/mysql/backup.sql /backup/mysql_backup.sql# 131085 -rw-r--r-- 2 mysql mysql 104857600 May 15 10:00 /data/mysql/backup.sql# 131085 -rw-r--r-- 2 mysql mysql 104857600 May 15 10:00 /backup/mysql_backup.sql# inode 相同,链接数都是 2# 验证:两个文件内容完全相同md5sum /data/mysql/backup.sql /backup/mysql_backup.sql# 相同的 MD5 值
硬链接有以下硬性限制,理解这些限制有助于在工作中正确选择链接类型:
限制一:不能跨文件系统
每个文件系统有独立的 inode 表,跨文件系统的 inode 编号是独立的。A 文件系统的 inode 131082 和 B 文件系统的 inode 131082 是完全不同的数据。硬链接需要两个路径指向同一个 inode,所以不能跨文件系统。
# 假设 /data 是独立的挂载点(/dev/sdb1)# 尝试在 /data 目录下的文件和根分区之间创建硬链接ln /data/mysql/backup.sql /root/backup_copy.sql# ln: failed to create hard link '/root/backup_copy.sql': Invalid cross-device link# 验证两个路径在不同文件系统df -h /data/mysql/backup.sql /root/backup_copy.sql 2>/dev/null# /dev/sdb1 500G 320G 180G 64% /data# /dev/sda1 100G 45G 55G 45% /
限制二:不能链接目录
如果允许目录硬链接,会导致目录树中出现环(A 是 B 的子目录,B 又硬链接回 A),这会让rm等命令陷入无限递归。Linux 文件系统禁止对目录创建硬链接。
mkdir -p /data/appln /data/app /data/app_link# ln: /data/app: hard link not allowed for directory
补充知识:目录的链接数来源于子目录的..入口。例如/data目录初始链接数为 2(.和父目录的引用),每在/data下创建一个子目录,该子目录的..目录项就会使/data的链接数加 1。
ls -lai /data# drwxr-xr-x 3 root root 4096 May 15 10:00 .# drwxr-xr-x 4 root root 4096 May 15 09:00 ..# 在 /data 下创建子目录mkdir /data/logs# /data 的链接数从 3 变成 4(多了 logs 子目录的 .. 引用)ls -lai /data# drwxr-xr-x 3 root root 4096 May 15 10:00 . ← inode# ↑ Links: 4(包含 .、.. 和 logs/..)
限制三:不能对不存在的文件创建硬链接
硬链接需要指向一个已存在的 inode,所以目标文件必须存在。
ln /nonexistent /backup/link# ln: /nonexistent: No such file or directory
限制四:不能对特殊文件(设备、socket、管道)创建硬链接
这些文件不是普通数据文件,不应该被链接。
ln /dev/null /tmp/mynull# ln: /dev/null: hard link not allowed for special file
场景一:文件热备(不改原文件名)
# 每日备份脚本:复制一份文件到备份目录,不改原文件名ln /data/www/uploads/avatar_20260515.jpg /backup/uploads/avatar_20260515.jpg# 优点:备份立即生效,不占用额外磁盘空间(同一份数据块)# 缺点:原文件和备份共享数据,改一个两个都变(对备份来说不是问题,因为备份通常只读)
场景二:多版本代码共存(不改原路径引用)
# 新版本上线前,在同一目录下创建硬链接ln /data/app/v1.2.3 /data/app/v1.2.3_old# 然后替换 v1.2.3 的内容# 如果新版本有问题,v1.2.3_old 仍然指向旧版本数据
场景三:日志文件轮转后的无缝衔接
# 假设日志文件 /var/log/nginx/access.log 被 logrotate 移动到 access.log.1# 但某个服务还在写 /var/log/nginx/access.log(通过文件描述符)# 如果用硬链接,删除旧文件后链接数减 1,数据块不会被立即释放# 服务继续写旧的 inode,新文件(access.log.1)不受影响
# 删除硬链接:unlink 或 rmunlink /backup/mysql_backup.sqlrm /backup/mysql_backup.sql# 真正的删除发生在链接数变为 0 时# 进程持有打开文件句柄时,即使 rm 删除了目录项,数据块也不会立即释放
一个经典问题:为什么rm之后 df 还显示磁盘占用不释放?
# 进程 A 打开了一个大文件 file.txt(持有文件描述符)# 管理员 rm 了 file.txtrm /path/to/file.txt# 此时:# - 目录项已被删除,ls 看不到 file.txt# - 但进程 A 仍然持有文件描述符,inode 和数据块没有被释放# - df -h 看到磁盘使用率没有变化# 查看被删除但未释放的文件lsof +L1# 输出示例:# COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME# python 12345 app 3r REG 8,02 104857600 0 131085 /path/to/file.txt (deleted)# 解决方法:# 1. 联系应用团队,让进程正常关闭文件(最佳方案)# 2. kill 进程(需要评估影响)# 3. 重启进程
硬链接最关键的特征,不在于命令形式,而在于“多个目录项指向同一个 inode”。链接数变化、删除行为和内容共享,都是这一前提的直接结果。
如果需求发生在同一文件系统内,并且希望为同一份数据保留多个访问入口,硬链接是合适的方案;一旦涉及跨分区、目录切换或路径别名,适用的就不再是硬链接,而是路径引用方式。
欢迎「长按」下方图片👇,关注我们在公众号上的专业知识分享。