对带有硬链接的 cp 的行为感到惊讶

对带有硬链接的 cp 的行为感到惊讶

我非常了解硬链接的概念,并且cp多次阅读了基本工具的手册页,甚至是最近的 POSIX 规范。但我仍然惊讶地发现以下行为:

$ echo john > john
$ cp -l john paul
$ echo george > george

此时johnpaul将具有相同的 inode(和内容),并且george在两个方面都会有所不同。现在我们做:

$ cp george paul

此时我期望georgepaul不同的 inode 编号但内容相同 --- 这个期望得到满足 --- 但我预计paul现在具有与 不同的 inode 编号john,并且john仍具有内容john。这让我感到惊讶。事实证明,将文件复制到目标路径paul也会导致在共享 的paulinode 的所有其他目标路径上安装相同的文件(相同的 inode)。我以为这cp会创建一个新文件并将其移动到旧文件 以前占据的位置paul。相反,它似乎做的是打开现有文件paul,截断它,然后将george的内容写入现有文件。因此,任何具有相同 inode 的“其他”文件都会同时更新“它们的”内容。

好吧,这是一种系统行为,现在我知道会发生这种情况,我可以弄清楚如何解决它,或酌情利用它。让我困惑的是我应该在哪里看到这种行为的记录?如果没有记录的话我会感到惊讶某处在我已经看过的文件中。但显然我错过了它,现在找不到讨论这种行为的来源。

答案1

cp文档表明,如果目标文件已存在,它将覆盖目标文件。你是对的,它没有详细说明“覆盖”的含义,但它肯定说“覆盖”,而不是“替换”。如果你想学究气,你可以说“覆盖”正是这样cp做的,而你所期望的行为应该被正确地称为“替换”。

另请注意,如果cp要“替换”预先存在的目标文件,这可能会被认为是令人惊讶或不正确的,可能比“覆盖”更严重。例如:

  • 如果cp先删除旧文件,然后创建新文件,那么会有一段时间该文件不存在,这将是令人惊讶的。
  • 如果cp首先创建一个临时文件,然后将其移动到位,那么它可能应该记录这一点,因为这样具有奇怪名称的临时文件偶尔会被注意到......但事实并非如此。
  • 如果cp由于权限原因无法在与旧文件相同的目录中创建新文件,那么这将是不幸的(特别是如果它已经删除了旧文件)。
  • 如果该文件不属于正在运行的用户cp,并且运行的用户cp也不是,root那么就不可能将新文件的所有者和权限与新文件的所有者和权限相匹配。
  • 如果文件具有cp不知道的特殊属性,那么这些属性将在副本中丢失。如今, 的实现cp应该能够可靠地理解扩展属性之类的东西,但情况并非总是如此。还有其他东西,比如 MacOS 资源分叉,或者对于远程文件系统,基本上是任何东西。

总而言之:现在你知道cp真正的作用是什么了。您将永远不会再对此感到惊讶!老实说,我认为很多年前我也可能发生过同样的事情。

答案2

我看到 POSIX 2013 标准确实指定了观察到的行为。它说:

  1. 如果源文件如果是普通文件类型,则需要执行以下步骤:

    A。 ... 如果目标文件存在,应采取以下措施:

    我。如果该-i选项有效,cp实用程序应向标准错误写入提示并从标准输入读取一行。如果答复是否定的,cp则不再采取任何行动 源文件并继续处理任何剩余的文件。

    二.文件描述符目标文件open()应通过执行与 POSIX.1-2008 的系统接口卷中定义的函数等效的操作来获得,调用使用目标文件作为路径参数,并且按位OR包含O_WRONLYO_TRUNC作为奥弗拉格争论。

    三.如果尝试获取文件描述符失败并且该-f选项有效,则应尝试通过执行与 POSIX.1-2008 系统接口卷中定义的函数cp等效的操作来删除文件,调用使用unlink()目标文件作为路径参数。如果此尝试成功,cp则应继续步骤 3b。

    ...

    d.的内容源文件应写入文件描述符。任何写入错误都将导致cp将诊断消息写入标准错误并继续执行步骤 3e。

    e.文件描述符应关闭。

答案3

首先,为什么要这样做?原因之一是历史原因:事情就是这样完成的在 Unix 第一版中

文件成对进行;第一个打开以供读取,第二个创建模式 17。然后将第一个复制到第二个中。

“创造”是指creat系统调用(就是众所周知缺少一个 e),如果存在,则按给定名称截断现有文件。

这里cpUnix第二版的源代码(我找不到第一版的源代码)。您可以看到open对源文件和creat第二个文件的调用;并且,作为对第一版的改进,如果第二个文件是现有目录,则cp在该目录中创建一个文件。

但是,你可能会问,当时为什么要这样做呢? “为什么 Unix 最初这样做”的答案几乎总是简单的。cp打开其源进行读取并创建其目标 - 创建文件的系统调用会通过打开文件进行写入来覆盖现有文件,因为这允许调用者通过给定名称强加文件的内容,无论该文件是否已存在或不是。

现在,关于它的记录位置:在FreeBSD 手册页

对于每个已存在的目标文件,如果权限允许,其内容将被覆盖。除非指定 -p 选项,否则其模式、用户 ID 和组 ID 不会更改。

存在这样的措辞至少可以追溯到 1990 年(当时 BSD 是 4.3BSD)。网上也有类似的说法索拉里斯10:

如果 target_file 存在,cp 会覆盖其内容,但与其关联的模式(以及 ACL,如果适用)、所有者和组不会更改。

你的情况甚至在HP-UX 10手动的:

如果 new_file 是指向具有其他链接的现有文件的链接,则覆盖现有文件并保留所有链接。

POSIX 将其放入标准语言中。引用自单一 UNIX v2:

如果 dest_file 存在,则执行以下步骤: (…) 通过执行与使用 dest_file 作为路径参数调用的 XSH 规范 open() 函数等效的操作以及 O_WRONLY 和 O_TRUNC 的按位或运算,将获得 dest_file 的文件描述符作为 oflag 参数。

我引用的手册页和规范进一步指定,如果-f传递该选项并且尝试打开/创建目标文件失败(通常是由于没有写入文件的权限),则cp尝试删除目标并再次创建文件。这会破坏您场景中的硬链接。

您可能想要报告文档错误GNU coreutils 手册,因为它没有记录此行为。即使 的描述--preserve=links(在您的场景中会导致paul链接被删除并创建新文件)也没有明确说明如果没有--preserve=links.某种描述-f暗示了如果没有它会发生什么,但没有详细说明(“在没有此选项的情况下进行复制并且无法打开现有目标文件进行写入时,复制会失败。但是,使用 --force,...”)。

答案4

如果您可以说,“将文件复制到目标路径paul 也会将相同的文件(相同的 inode)复制到共享paulinode 的所有其他目标路径。”,我很遗憾地说您不理解以下概念硬链接很好。如果我给麦卡特尼爵士一个苹果,我就给了保罗一个苹果,给了约翰·列侬的歌曲创作伙伴一个苹果。但我还没有送出三个苹果;我给了一个有多个名字/头衔/描述的人一个苹果。

同样,当您复制george到时paul,您也不是将其复制到john.相反,您是将george数据复制到目录条目指向其 inode 的文件中paul

一步步:  当你这样做时

echo john > john

john您已经创建了一个新文件(假设该目录中还没有指定的文件)。或者,更严格地说,这是假设该目录中尚不存在具有该名称的目录条目john(因为严格来说,目录中没有文件;只有指向 inode 的目录条目)。做完之后

cp -l john paul

或者

ln john paul

您尚未创建新文件;相反,您已经为现有文件指定了一个新名称。您现在有一个具有两个名称的文件:johnpaul。当你说

cp george paul

你正在覆盖那个文件。它有两个名字这一事实是无关紧要的;它可能有 42 个名称,可能位于您甚至无法访问的位置,并且此命令不会将数据复制george\n到所有这些名称(路径);它只是将数据复制到一个文件有多个名称。

相关内容