在 Linux 中,如果一个目录中的 1000 个文件被移动到另一个位置,而另外 300 个文件被添加到源目录中,会发生什么?

Sha*_*mad 38 linux operating-systems filesystems

在 Linux 中,如果一个目录中的 1000 个文件被移动到另一个位置并且另外 300 个文件被添加到源目录中,同时原始的 1000 个文件被移动会发生什么。目标最终会是 1300 个文件吗?或者源文件夹中是否还有 300 个文件。

Eug*_*eck 91

这取决于您使用的工具:让我们检查一些情况:

如果你按照mv /path/to/source/* /path/to/dest/int a shell 的方式运行一些东西,你最终会移动原来的 1000 个文件,而新的 300 个文件没有受到影响。这是因为*在开始移动操作之前,shell 会展开,所以当移动正在进行时,列表已经固定了。

如果您使用 Nautilus(和其他 GUI 朋友),您将以相同的方式结束:它将根据选择的文件运行移动操作 - 这在新文件出现时不会改变。

如果您使用您自己的程序,在循环中使用系统调用glob并且只有一个 mvuntilglob保持为空,那么您最终将在新目录中拥有所有 1300 个文件。这是因为每个新glob文件都会选择同时出现的新文件。

  • @grawity:POSIX 说:_如果在最近一次调用 opendir() 或 rewinddir() 之后从目录中删除或添加文件,则对 readdir() 的后续调用是否返回该文件的条目是未指定的。_ 也, NFS 可能会对可实现的内容施加一些限制,IIRC 它使 `telldir()/seekdir()` 的实现复杂化 (24认同)
  • 根据 POSIX,`opendir()` 的结果集是稳定的。使用 PHP 的 `opendir()` 进行的快速测试证实了这一点(但我只测试了 ext4)。 (16认同)
  • 如果您 opendir() 源,然后循环 readdir() 或 getdents() 会发生什么? (7认同)
  • @db 超过 100 000 个文件,扩展的命令行将超过最大命令长度限制(ARG_MAX,通常为几个 MiB)并且 `mv` 将无法执行。 (3认同)
  • @db 如果在扩展发生时添加或删除文件 _,则 readdir 讨论适用(因为这也是扩展的方式)。 (3认同)
  • 无论文件数量如何,所有文件系统都是如此吗?我假设内核通常通过 readdir() 返回实时结果,并且不会预先缓存它们或任何东西。 (2认同)
  • 如果 dir 包含十亿个文件,其中通配符的扩展花费了相当多的时间,并且在此扩展发生时将 300 个文件移动到那里,会发生什么? (2认同)
  • 我有一个 Ninjalj 引用的同一篇文章的书签,它包含一个指向 https://lwn.net/Articles/544846/ 的链接(*规范允许应用程序每周读取一个文件名,并且仍然可以保证看到所有开始读取时存在的文件,没有重复项。*)。这当然是通过我的深入挖掘而过时的,它仅在缓冲区大小的块中表现出稳定性。 (2认同)

cho*_*oba 8

当您告诉系统移动目录中的所有文件时,它会列出所有文件,然后开始移动它们。如果目录中出现新文件,它们不会添加到要移动的文件列表中,因此它们将保留在原始位置。

当然,您可以编写一种移动文件的方法,mv该方法将定期检查源目录中的新文件。


Pet*_*des 8

内核本身不能处于“移动 1000 个文件”操作的“中间”。您需要更具体地说明您提议的操作。

一个线程一次只能使用rename(*oldpath, const char *newpath)renameat系统调用移动一个文件(并且只能在同一个文件系统1 内)。或Linuxrenameat2拥有像标志RENAME_EXCHANGE以原子交换两个路径名,或RENAME_NOREPLACE若存在替换目标。(例如,允许mv -i实现避免了statand then的竞争条件rename,这仍然会覆盖在stat. link+之后创建的文件,unlink也可以解决这个问题,因为link如果新名称存在,则会失败。)

但是这些系统调用中的每一个都只为每个系统调用重命名一个目录条目。将 POSIXrenameatolddirfdnewdirfd(以 开头)一起使用open(O_DIRECTORY)将允许您继续循环遍历目录中的文件,即使源或目标目录本身已被重命名。(使用相对路径也可以允许使用常规rename().)

无论如何,正如其他答案所说,大多数使用 rename 系统调用的程序会在执行第一个rename. (通常使用readdir(3)POSIX 库函数作为平台特定系统调用(如 Linux getdents)的包装器)。

但是,如果您正在谈论find -exec ... {} \;为每个文件运行一个命令,或者-exec {} +在一个命令行中无法容纳如此多的文件时更有效,那么您当然可以在扫描的同时进行重命名。例如

find . -name '*.txt' -exec mv -t ../txtfiles {} \;   # Intentionally inefficient
Run Code Online (Sandbox Code Playgroud)

如果您在.txt运行时创建了一些新文件,您可能会../txtfiles. 但在内部find(1)会一直使用open(O_DIRECTORY)getdents.

如果一个系统调用足以返回其中的所有目录条目.(其中 find 将一次循环一个,仅在需要-type或递归时才进行进一步的系统调用,或在匹配时使用 fork+exec),则该列表是某一时间点目录条目的快照。对目录的进一步更改不会影响find执行的操作,因为它已经有一份目录副本,列出了它将循环遍历的内容。(可能它在内部使用readdir(3),它一次返回一个条目,但在 glibc 中我们知道strace find .它使用条目getdents64的缓冲区大小进行系统调用count=32768。)

但是如果目录很大和/或内核没有填充find的缓冲区,它必须在循环第一次获得的内容后进行第二次 getdents 系统调用。因此,在进行一些重命名后,它可能会看到新条目。

但是请参阅其他答案下的评论中的讨论:内核可能已经为我们创建了快照,因为(我认为)getdents 不允许两次返回相同的文件名。不同的文件系统使用不同的排序/索引机制来使访问巨大目录中的条目比线性搜索更有效。因此,添加或删除目录可能会对剩余条目的顺序产生其他影响。嗯,可能文件系统更可能保持稳定的顺序,并且只更新实际索引(如 EXT4dir_index功能),所以目录 FD 的位置可以只是一个目录条目来恢复?我真的不知道telldir(3)库接口如何映射到lseek,或者这是否纯粹是用户空间的东西,用于循环用户空间获得的缓冲区。getdents 可能需要从一个巨大的目录中获取所有条目,因此即使不支持搜索,内核也需要能够记录当前位置。


脚注 1:

要在文件系统之间“移动”,复制和取消链接取决于用户空间。(例如,与open和任一read+writemmap+writesendfile(2)copy_file_range(2),后两者完全避免弹跳通过用户空间中的文件数据。)