复制和移动命令对inode的影响

Copy and move's command effect on inode

我将 inode 解释为指向文件实际存储位置的指针。

但是我理解有问题:

  1. 如果我在 file2 已经存在 的地方使用 cp file1 file2,inode 不会改变。并且如果最初有一个 hard-link 到 file2,它们现在都指向刚刚复制到这里的新文件。

    • 我能想到的唯一原因是 Linux 将此解释为 修改 文件而不是删除并创建一个新文件。不明白为什么要这样设计?
  2. 但是当我使用mv file1 file2时,inode变成了file1的inode。

索引节点是文件元数据的集合,即在 Unix/类 Unix 文件系统中关于文件的信息。它包括权限数据、上次访问/修改时间、文件大小等。

值得注意的是,文件名/路径 不是 索引节点的一部分。文件名只是 inode 的人类可读标识符。一个文件可以有一个或多个名称,其数量在 inode 中由其 "links"(hard links)的数量表示。与 inode 关联的编号,inode 编号,我相信您将其解释为它在磁盘上的物理位置,它只是 inode 的唯一标识符。一个 inode 确实 包含文件在磁盘上的位置,但那不是 inode 编号。

所以知道这一点后,您看到的区别在于 cpmv 的功能。当您 cp 一个文件时,您正在创建一个具有新名称的新 inode 并将旧文件的内容复制到磁盘上的新位置。当您 mv 一个文件时,您所做的只是更改其中一个名称。如果新名称已经是另一个文件的名称,则该名称与旧文件断开关联(并且旧文件的 link 计数减 1)并与新文件关联。

您可以阅读有关 inode 的更多信息 here

您说 cp 将修改文件而不是删除和重新创建文件是正确的。

这是 strace 看到的底层系统调用的视图(strace cp file1 file2 的部分输出):

open("file2", O_WRONLY|O_TRUNC)         = 4
stat("file2", {st_mode=S_IFREG|0664, st_size=6, ...}) = 0
stat("file1", {st_mode=S_IFREG|0664, st_size=3, ...}) = 0
stat("file2", {st_mode=S_IFREG|0664, st_size=6, ...}) = 0
open("file1", O_RDONLY)                 = 3
fstat(3, {st_mode=S_IFREG|0664, st_size=3, ...}) = 0
open("file2", O_WRONLY|O_TRUNC)         = 4
fstat(4, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
fadvise64(3, 0, 0, POSIX_FADV_SEQUENTIAL) = 0
read(3, "hi\n", 65536)                  = 3
write(4, "hi\n", 3)                     = 3
read(3, "", 65536)                      = 0
close(4)                                = 0
close(3)                                = 0

如您所见,它检测到 file2 存在 (stat returns 0),但随后打开它进行写入 (O_WRONLY|O_TRUNC),而没有先执行一个 unlink.

例如 POSIX.1-2017,其中 specifies 目标文件只能在无法打开进行写入的地方进行 unlink 编辑 -f 使用:

A file descriptor for dest_file shall be obtained by performing actions equivalent to the open() function defined in the System Interfaces volume of POSIX.1-2017 called using dest_file as the path argument, and the bitwise-inclusive OR of O_WRONLY and O_TRUNC as the oflag argument.

If the attempt to obtain a file descriptor fails and the -f option is in effect, cp shall attempt to remove the file by performing actions equivalent to the unlink() function defined in the System Interfaces volume of POSIX.1-2017 called using dest_file as the path argument. If this attempt succeeds, cp shall continue with step 3b.

这意味着如果目标文件存在,如果 cp 进程对其具有写权限(不一定 运行作为拥有该文件的用户),即使它对包含的目录没有写权限。相比之下,unlinking 和重新创建需要目录的写权限。我推测这就是标准保持原样的原因。

GNU cp 上的 --remove-destination 选项将使它执行您认为应该是默认设置的操作。

这里是strace cp --remove-destination file1 file2输出的相关部分。这次注意unlink

stat("file2", {st_mode=S_IFREG|0664, st_size=6, ...}) = 0
stat("file1", {st_mode=S_IFREG|0664, st_size=3, ...}) = 0
lstat("file2", {st_mode=S_IFREG|0664, st_size=6, ...}) = 0
unlink("file2")                         = 0
open("file1", O_RDONLY)                 = 3
fstat(3, {st_mode=S_IFREG|0664, st_size=3, ...}) = 0
open("file2", O_WRONLY|O_CREAT|O_EXCL, 0664) = 4
fstat(4, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
fadvise64(3, 0, 0, POSIX_FADV_SEQUENTIAL) = 0
read(3, "hi\n", 65536)                  = 3
write(4, "hi\n", 3)                     = 3
read(3, "", 65536)                      = 0
close(4)                                = 0
close(3)                                = 0

当您使用 mv 并且源路径和目标路径在同一文件文件系统上时,它将执行 rename,这将具有取消 linking 的效果目标路径中的任何现有文件。这是 strace mv file1 file2.

输出的相关部分
access("file2", W_OK)                   = 0
rename("file1", "file2")                = 0

在任何一种情况下,目标路径未被link编辑(无论是 unlink()cp --remove-destination 中明确调用,还是作为 rename() 效果的一部分正如从 mv 中调用的那样),它指向的 inode 的 link 计数将减少,但如果 link 计数仍然 >0 或如果任何进程有打开的文件句柄。此 inode 的任何其他(硬)links(即它的其他目录条目)将保留。

使用 ls -i

进行调查

ls -i 将显示 inode 编号(与 -l 组合时作为第一列),这有助于说明正在发生的事情。

带有默认 cp 操作的示例

$ rm file1 file2 file3 

$ echo hi > file1
$ echo world > file2
$ ln file2 file3

$ ls -li file*
49 -rw-rw-r-- 1 myuser mygroup    3 Jun 13 10:43 file1
50 -rw-rw-r-- 2 myuser mygroup    6 Jun 13 10:43 file2
50 -rw-rw-r-- 2 myuser mygroup    6 Jun 13 10:43 file3

$ cp file1 file2 
$ ls -li file*
49 -rw-rw-r-- 1 myuser mygroup    3 Jun 13 10:43 file1
50 -rw-rw-r-- 2 myuser mygroup    3 Jun 13 10:43 file2   <=== exsting inode
50 -rw-rw-r-- 2 myuser mygroup    3 Jun 13 10:43 file3   <=== exsting inode

(注意现有 inode 50 现在的大小为 3)。

示例--remove-destination

$ rm file1 file2 file3
$ echo hi > file1
$ echo world > file2
$ ln file2 file3

$ ls -li file*
49 -rw-rw-r-- 1 myuser mygroup    3 Jun 13 10:46 file1
50 -rw-rw-r-- 2 myuser mygroup    6 Jun 13 10:46 file2
50 -rw-rw-r-- 2 myuser mygroup    6 Jun 13 10:46 file3

$ cp --remove-destination file1 file2
$ ls -li file*
49 -rw-rw-r-- 1 myuser mygroup 3 Jun 13 10:46 file1
55 -rw-rw-r-- 1 myuser mygroup 3 Jun 13 10:47 file2   <=== new inode
50 -rw-rw-r-- 1 myuser mygroup 6 Jun 13 10:46 file3   <=== existing inode

(注意新的 inode 55 的大小为 3。未修改的 inode 50 的大小仍然为 6。)

示例 mv

$ rm file1 file2 file3
$ echo hi > file1
$ echo world > file2
$ ln file2 file3

$ ls -li file*
49 -rw-rw-r-- 1 myuser mygroup 3 Jun 13 11:05 file1
50 -rw-rw-r-- 2 myuser mygroup 6 Jun 13 11:05 file2
50 -rw-rw-r-- 2 myuser mygroup 6 Jun 13 11:05 file3

$ mv file1 file2 
$ ls -li file*
49 -rw-rw-r-- 1 myuser mygroup 3 Jun 13 11:05 file2  <== existing inode
50 -rw-rw-r-- 1 myuser mygroup 6 Jun 13 11:05 file3  <== existing inode

@alaniwi 的回答涵盖了正在发生的事情,但这里也有一个隐含的为什么

cp 以这种方式工作的原因是提供了一种替换具有多个名称的文件的方法,让所有这些名称都引用新文件。当 cp 的目标是一个已经存在的文件时,可能通过硬链接或软链接具有多个名称,cp 将使所有这些名称都指向新文件。将不会有 'orphan' 对遗留下来的旧文件的引用。

给定此命令,很容易获得 'just change the file for one name' 行为——首先取消链接文件。考虑到作为一个原语,实现 'change all references to point to the new contents' 行为将非常困难。

当然,执行 rm+cp 有一些竞争条件问题(它是两个命令),这就是为什么 install 命令被添加到 BSD unix 中的原因——它基本上只是执行 rm + cp,以及在极少数情况下进行一些检查以使其成为原子,两个人尝试同时安装到同一路径,以及有人从您尝试安装的文件中读取的更严重的问题(普通 cp 的问题)。然后 GNU 版本添加了选项来备份旧版本和各种其他有用的簿记。