无尘阁日记

无尘阁日记

请问下copy的时候,文件处理还没完成,php就会直接执行下一步么?不会等待磁盘完全落盘完成?
2025-12-22

一、先给结论(一定要记住)

copy() 在 PHP 返回成功时,只保证“数据已经交给操作系统内核”,
❌ 不保证“数据已经完全落盘(flush 到磁盘)”。

所以答案是:

  • PHP 会等待 copy() 结束

  • 但不会等待数据真正写到磁盘

  • 更不会等待文件成为“对其他进程稳定可读的完整体”

这正是你现在问题的根源。

二、为什么“看起来同步”,但实际上不是你想的那种同步

1️⃣ copy() 到底在等什么?

copy() 的内部逻辑(简化)是:

read(源文件) → write(目标文件) → write(目标文件) → ...

当:

  • 所有 write() 调用返回成功

  • PHP 就认为“拷贝完成”

  • 然后立刻执行下一行代码

但这里有一个关键点:

write() ≠ 写入磁盘

2️⃣ Linux / Unix 的真实世界:Page Cache

在现代操作系统里:

  • write() 实际上是:

    • 把数据写进 Page Cache(内存)

    • 标记“这个页是脏的”

  • 内核会在稍后(异步):

    • 决定什么时候真正 flush 到磁盘

    • 合并 IO

    • 调度写入

也就是说:

PHP copy() 完成
↓
数据在内存里
↓(不确定多久)
数据才真正落盘

3️⃣ 对另一个进程(C#)来说,这个文件处于“危险状态”

此时文件的状态是:

  • ✅ 路径存在

  • ✅ inode 存在

  • ✅ 文件大小可能已经是最终大小

  • ❌ 文件尾部(ZIP 的 EOCD)可能还没刷到磁盘

  • ❌ 从另一个进程读取时,可能读到“不完整视图”

于是 C#:

ZipArchive(...)

从文件尾读 EOCD → 找不到 → 报错

三、那 PHP 为什么“不帮我等落盘”?

因为:

操作系统的设计目标是“吞吐量 + 性能”,而不是“每个 write 都强制刷盘”

如果每次 write 都 fsync:

  • 性能会急剧下降

  • 所有应用都会被拖慢

所以:

  • 默认:异步刷盘

  • 强制刷盘:应用自己承担成本

而 PHP:

  • 没有暴露 fsync()

  • 也没有提供“等磁盘稳定”的 API

  • 所以你不能指望 copy() 给你这种保证

四、为什么你这个问题「不是每次都发生」

因为刷盘时机是:

  • 内核调度决定

  • 磁盘负载决定

  • 文件大小决定

  • 并发 IO 决定

这就解释了你看到的所有现象:

现象解释
偶发刷盘时机不确定
小文件更容易成功更快 flush
高并发时更容易失败IO 队列拥堵
重试成功第二次文件已稳定

五、为什么 .tmp + rename 是“唯一正确姿势”

你可能会问:

“既然 copy() 不等落盘,那 rename 就等了吗?”

关键在这里 👇

rename() 的本质不是“刷盘”,而是可见性边界

你现在的设计是:

copy → 立刻对外暴露路径

而正确设计是:

copy 到 tmp(不可见)
↓
rename(原子暴露)
↓
其他进程才允许读

即使底层还有 page cache:

  • rename 之前:C# 根本看不到目标路径

  • rename 之后

    • 文件已经具备“完整结构”

    • EOCD 已写入内存页

    • 读到“半截 ZIP”的概率被降到工程上可接受的接近 0

这就是工程设计与系统调用语义的结合。

六、用一句“工程级结论”结束

copy() 是“把数据交给内核”,不是“把数据交给磁盘”。
在跨进程文件协作中,必须显式设计“文件完成发布点”,而不能依赖 copy 的返回值。