硬链接共享 inode,替换名字后关系会改变
硬链接为已有文件增加另一个目录项。两个名字都指向同一文件系统里的同一个 inode,其中没有哪个天然具有“原件”地位。内容、所有者、权限和链接数属于这个共享对象,文件名只是到达它的入口。因此,需要把目录项的变化与文件对象本身的变化分开理解,才能判断修改、删除和替换到底影响了哪些访问路径。 坏掉的样子 把硬链接当成独立备份,会产生错误的恢复边界。通过任意名字写入或截断共享 inode,另一名字读到的内容也会改变;修改模式位,同样改变的是一个对象的权限。第二个路径可以在第一个路径被删除后继续存在,却不能抵御针对共享文件的普通原地修改,也不会自动保留先前历史版本。 许多编辑器和部署工具采用另一种更新方式:先创建替代文件,再通过 rename 覆盖某个路径。该目录项此时指向新 inode,其他硬链接仍然到达旧对象。两个名字是否一起显示新内容,取决于实际更新方法。因此不能仅凭它们昨天建立过硬链接,就断言今天仍共享身份;应检查当前对象以及写入工具的真实行为。 硬链接还受文件系统挂载边界约束。即使两个目录在统一路径树里看起来相邻,link 也不能随意把不同挂载中的对象连接起来。Linux 还禁止普通目录硬链接。这些限制意味着,硬链接不能作为跨目录层次、跨挂载符号链接的通用替代品;使用前必须先确认目标对象和边界,而不是只看路径字符串。 快速验证 把脚本保存为 hardlinks.py,在一次性 Linux 容器中以 root 运行 python3 hardlinks.py。它要求 /tmp 与 /run 可写、属于不同文件系统,并在执行时检查这一条件。实测使用两个独立 tmpfs 挂载、Linux 6.12.76 和 Python 3.11.2,所有新建条目都位于临时目录中: from pathlib import Path from tempfile import TemporaryDirectory import errno,os with TemporaryDirectory(prefix='xbcatm-hardlinks-',dir='/tmp') as directory, TemporaryDirectory(prefix='xbcatm-crosslink-',dir='/run') as other: root=Path(directory);source=root/'source';alias=root/'alias' source.write_bytes(b'original');os.link(source,alias) before=source.stat();linked=alias.stat() assert (before.st_dev,before.st_ino)==(linked.st_dev,linked.st_ino) assert before.st_nlink==linked.st_nlink==2 and before.st_size==linked.st_size==8 alias.write_bytes(b'changed') assert source.read_bytes()==b'changed' print('two names share device/inode, size, and link count; in-place write changes both views') replacement=root/'replacement';replacement.write_bytes(b'new file');replacement.replace(source) assert source.stat().st_ino!=alias.stat().st_ino assert source.read_bytes()==b'new file' and alias.read_bytes()==b'changed' assert source.stat().st_nlink==alias.stat().st_nlink==1 print('atomic replacement changes one name to a new inode; the other name retains the old file') with alias.open('rb') as opened: alias.unlink() assert os.fstat(opened.fileno()).st_nlink==0 and opened.read()==b'changed' print('last unlink removes the final name but an existing open descriptor still reads the old data') assert root.stat().st_dev!=Path(other).stat().st_dev,'exercise requires two filesystem mounts' try:os.link(source,Path(other)/'cross-filesystem') except OSError as error:assert error.errno==errno.EXDEV,error else:raise AssertionError('cross-filesystem hard link unexpectedly succeeded') child=root/'directory';child.mkdir() try:os.link(child,root/'directory-link') except OSError as error:assert error.errno==errno.EPERM,error else:raise AssertionError('directory hard link unexpectedly succeeded') print('cross-filesystem link fails with EXDEV; directory hard link fails with EPERM on Linux') 样本先创建 source,写入八字节内容,再通过 os.link 添加 alias。两个名字报告相同的设备与 inode 组合、八字节大小,以及值为二的链接数。比较时必须包含设备身份,因为 inode 编号并不跨文件系统全局唯一;仅仅比较内容相同,也无法证明两个入口共享对象,独立副本同样可能有相同字节。 ...