文件上传是 Web 应用中最常见的功能之一,但大多数开发者对 PHP 底层如何处理上传文件知之甚少。本文将深入 PHP 内核源码,完整剖析从 HTTP 请求体解析到临时文件管理的全流程,帮助你建立系统级的认知。
一、HTTP 请求体与 multipart/form-data 编码
当浏览器提交包含文件的表单时,请求头中会携带 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXXXX。boundary 是分隔符,用于在请求体中将不同字段隔开。
一个典型的请求体结构如下:
------WebKitFormBoundaryXXXX
Content-Disposition: form-data; name="avatar"; filename="photo.jpg"
Content-Type: image/jpeg
<二进制文件数据>
------WebKitFormBoundaryXXXX
Content-Disposition: form-data; name="username"
zhangsan
------WebKitFormBoundaryXXXX--
PHP 需要在接收请求时,逐字节解析这个结构,将文件字段与普通字段分离。这一过程由 PHP 的 SAPI(如 php-fpm)和内核共同完成。
二、rfc1867 解析器:PHP 的上传核心
PHP 处理 multipart 上传的核心实现在 main/rfc1867.c 中,这是根据 RFC 1867 标准实现的表单上传解析器。其工作流程大致如下:
- 读取请求头:SAPI 层(如
sapi/fpm/fpm/fpm_main.c)读取Content-Type,提取 boundary。 - 初始化解析器:调用
SAPI_POST_HANDLER,最终进入rfc1867_post_handler。 - 逐块读取请求体:通过
sapi_read_post_block以流式方式读取数据,避免一次性加载整个文件到内存。 - 状态机解析:解析器维护一个状态机,识别 boundary、头部字段和正文数据。
- 写入临时文件:当遇到文件字段时,PHP 会创建一个临时文件,并将文件数据流式写入其中。
关键点在于:PHP 不会将上传文件读入内存,而是直接写入临时文件。这通过 php_stream 抽象层实现,临时文件默认位于 upload_tmp_dir 指定的目录(未设置则使用系统临时目录)。
三、临时文件的创建与管理
在 rfc1867.c 中,临时文件的创建逻辑如下:
- 调用
php_open_temporary_file在upload_tmp_dir中生成一个唯一文件名,通常形如phpXXXXXX。 - 使用
php_stream_open_wrapper以写入模式打开该文件。 - 每次读取到文件数据块时,调用
php_stream_write写入临时文件。 - 解析完成后,文件信息被注册到
$_FILES超全局数组中。
$_FILES 数组的每个条目包含:
name:客户端原始文件名type:客户端声明的 MIME 类型(不可信)tmp_name:服务器上的临时文件路径error:错误码size:文件大小
值得注意的是,临时文件在请求结束时会被自动删除,除非你将其移动或重命名。这是通过 PHP 的请求关闭函数(php_request_shutdown)实现的,它会调用 php_free_uploaded_file 清理所有未处理的临时文件。
四、错误处理与配置约束
PHP 在上传过程中会检查多个配置项,任何一个不满足都会导致上传失败,并在 $_FILES['file']['error'] 中返回对应错误码:
| 错误码 | 常量 | 含义 |
|---|---|---|
| 0 | UPLOAD_ERR_OK | 成功 |
| 1 | UPLOAD_ERR_INI_SIZE | 超过 upload_max_filesize |
| 2 | UPLOAD_ERR_FORM_SIZE | 超过 MAX_FILE_SIZE |
| 3 | UPLOAD_ERR_PARTIAL | 仅部分上传 |
| 4 | UPLOAD_ERR_NO_FILE | 未选择文件 |
| 6 | UPLOAD_ERR_NO_TMP_DIR | 临时目录不存在 |
| 7 | UPLOAD_ERR_CANT_WRITE | 写入临时文件失败 |
| 8 | UPLOAD_ERR_EXTENSION | 被 PHP 扩展中断 |
相关配置项包括:
file_uploads:是否允许上传,默认 Onupload_max_filesize:单个文件最大尺寸,默认 2Mpost_max_size:整个 POST 请求最大尺寸,必须大于 upload_max_filesizeupload_tmp_dir:临时文件目录max_file_uploads:单请求最大文件数,默认 20
当 post_max_size 被超出时,$_POST 和 $_FILES 都会为空,这一点常被开发者忽略。
五、从临时文件到持久化存储
由于临时文件在请求结束后即被删除,开发者必须使用 move_uploaded_file() 将其移动到目标位置。该函数不仅执行移动操作,还会验证源文件确实是合法的上传临时文件(通过检查 is_uploaded_file),防止路径遍历攻击。
if ($_FILES['avatar']['error'] === UPLOAD_ERR_OK) {
$tmp = $_FILES['avatar']['tmp_name'];
$dest = __DIR__ . '/uploads/' . basename($_FILES['avatar']['name']);
if (move_uploaded_file($tmp, $dest)) {
echo "上传成功";
}
}
move_uploaded_file 底层调用 VCWD_RENAME,在跨文件系统时可能退化为复制+删除。移动后,PHP 会将该临时文件从待清理列表中移除,避免请求结束时被误删。
六、安全考量与最佳实践
理解内核流程后,可以总结出以下安全要点:
- 绝不信任
$_FILES['type']:MIME 类型由客户端提供,应使用finfo_file()检测真实类型。 - 重命名文件:不要直接使用客户端文件名,应生成随机名或哈希名,防止覆盖和路径注入。
- 限制目录权限:上传目录应禁止 PHP 执行,可通过 Nginx 配置
location ~* /uploads/.*\.php$ { deny all; }。 - 校验文件大小与扩展名:结合白名单策略。
- 隔离临时目录:确保
upload_tmp_dir不可被 Web 直接访问。
七、总结
PHP 文件上传的完整链路可以概括为:SAPI 读取请求 → rfc1867 解析器流式解析 multipart → 写入临时文件 → 注册到 $_FILES → 请求结束自动清理。开发者通过 move_uploaded_file 完成持久化。理解这一内核流程,不仅能帮助你写出更安全的上传代码,还能在排查上传失败、超时、内存溢出等问题时快速定位根因。
掌握底层,方能驾驭上层。
未经允许不得转载:任鹏个人博客 » PHP 文件上传内核剖析:从 multipart 解析到临时文件管理的完整流程

