请问下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 的返回值。
发表评论: