单靠 mkdir() 不足以防竞态,因其存在 TOCTOU 问题:并发请求可能同时通过 is_dir() 检查后都执行 mkdir(),导致第二个失败(EEXIST);@mkdir 仅抑制警告,不解决根本问题。

直接用 mkdir() 创建目录时,如果多个请求几乎同时执行,可能因检查与创建之间存在时间差,导致重复创建失败或权限混乱。加 @mkdir 抑制警告只是掩盖问题,真正防竞态需要配合 is_dir() 做二次确认——不是为了“多加一层保险”,而是绕过 PHP 内部竞态窗口的实用手段。
为什么单靠 mkdir() 不足以防竞态
PHP 的 mkdir() 在底层调用系统 mkdir(2) 系统调用。但若父目录不存在且未启用递归($recursive = false),或权限被 umask 干扰,就可能失败;更关键的是:即使你先 if (!is_dir($path)) { mkdir($path); },两个并发请求仍可能同时通过 is_dir() 判断(都返回 false),然后都尝试 mkdir(),其中第二个会失败(errno=17 EEXIST)。这不是 PHP Bug,是经典 TOCTOU(Time-of-Check to Time-of-Use)问题。
@mkdir 的真实作用不是“静默成功”,而是规避警告干扰流程
@ 是错误控制运算符,它只屏蔽 mkdir() 失败时的 warning,不改变函数返回值。所以以下写法是常见误区:
✘ 错误示范(看似简洁,实则逻辑断裂):
@mkdir($path, 0755); if (is_dir($path)) { /* 继续 */ }
问题在于:如果 @mkdir() 因权限不足、磁盘满、父目录不可写等原因静默失败,is_dir($path) 仍为 false,后续逻辑可能直接崩坏。
推荐做法:用 is_dir() + mkdir() + is_dir() 三段式确认
核心思路:把“创建”当作尽力而为的操作,最终以 is_dir() 的结果为准。这样无论是否真由当前请求创建成功,只要目录最终存在且可访问,就进入下一步。
- 先用
is_dir($path)快速判断——多数情况下目录已存在,直接跳过创建 - 若不存在,调用
mkdir($path, 0755, true)尝试创建(true启用递归,避免手动建父级) - 再用
is_dir($path)确认结果——这才是可信的状态断言
示例代码:
$path = '/var/www/uploads/user_123/avatar';<br>if (!is_dir($path)) {<br> if (!mkdir($path, 0755, true) && !is_dir($path)) {<br> throw new RuntimeException("无法创建目录: $path");<br> }<br>}<br>// 此时 $path 必定是有效目录
进阶建议:结合 umask 和 web 服务器用户权限
即使代码逻辑严谨,若 web 服务器用户(如 www-data、nginx)对父目录无写权限,mkdir() 必然失败。务必确保:
- 父目录(如
/var/www/uploads)归属 web 用户,且权限至少为0755 - PHP 进程未受严格
umask限制(如umask(0002)会让0755实际变成0753) - 必要时在
mkdir()后补一句chmod($path, 0755)强制修正(尤其在共享主机环境)
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











