PHP运行时术语大全的庖丁解牛
总纲:PHP运行时 = Zend引擎内核 + SAPI接口层 + 扩展层 + 用户态业务层。
SAPI是关键:不同SAPI对应不同运行模式(FPM、CLI、CGI),直接决定整套运行时行为、生命周期、内存模型。
区分两大运行时形态:请求隔离型(FPM)、常驻内存型(Swoole‑CLI/Hyperf)。
一、SAPI 层术语(最顶层入口)
- SAPI(Server Application Programming Interface)服务器应用程序编程接口
根因:Zend引擎本身不负责接收HTTP、不处理网络;SAPI是PHP对外的接入层,给Zend引擎喂入输入、输出结果。不同SAPI,生命周期完全不一样。
常见SAPI:
- fpm‑fpm:FPM Web运行模式
- cli:命令行脚本
- cgi:原始CGI
- apache2handler:老版本Apache mod_php
- swoole:Swoole自定义SAPI(CLI下实现常驻服务)
核心:PHP本身没有Web能力,全部靠SAPI实现网络/命令行交互。
SG(server_globals)
SAPI全局结构体,保存请求相关全局信息:$_SERVER、请求头、输入输出缓冲区、请求状态。
FPM:每请求重置;
Swoole:不能直接复用SG,改用协程上下文保存请求信息。PG(core_globals)
PHP全局配置,对应php.ini各项配置,进程级别,整个进程生命周期有效。请求生命周期(Request lifecycle)
FPM‑SAPI生命周期(请求隔离)
请求到来 → 初始化PHP环境 → 执行用户代码 → 请求关闭阶段RSHUTDOWN → 释放请求资源 → Worker进程不退出,等待下一个请求
进程存活,请求级资源全部销毁。
CLI普通脚本生命周期
脚本启动 → 执行全部代码 → 脚本结束,整个进程销毁退出
Swoole‑CLI常驻运行时
进程启动BOOT初始化(仅一次)→ 循环事件循环;每来一个请求新建协程;协程结束销毁协程资源;进程本身永不退出
- RINIT / RSHUTDOWN 请求初始化/请求关闭
- RINIT:每一次请求到来执行;加载ini、超全局变量、GET/_GET/GET/_POST。FPM每次请求都会跑RINIT。
- RSHUTDOWN:请求结束回调,做请求资源清理。
Hyperf/Swoole常驻模式:不会自动执行RINIT/RSHUTDOWN,需要框架自己模拟请求生命周期。
- GINIT / GSHUTDOWN 全局初始化/全局关闭
进程启动执行一次,进程销毁执行一次。
常驻内存框架,业务初始化放在GINIT阶段。
二、Zend VM 虚拟机核心术语
ZendVM 虚拟机
PHP的执行引擎,不直接执行源码;执行编译后的Opcode字节码。
工作流:PHP源码 → 编译为Opcode → ZendVM逐条解释执行。Opcode(操作码)
编译生成的指令,类似汇编;ZEND_ADD、ZEND_ECHO等。OPcache缓存Opcode,跳过重复编译。OPcache
字节码缓存扩展,把编译好的Opcode存共享内存;多个FPM Worker共享;避免每次请求编译源码消耗CPU。
opcache.validate_timestamps:开启会检测php文件修改;生产关闭,修改代码不自动生效。opcache.revalidate_freq:文件检查周期。
- Zval 内核变量结构体
PHP所有变量底层统一结构。
成员:value(存储实际数据)、u1(type类型)、refcount__gc引用计数、is_ref__gc是否引用。
PHP一切变量都是zval:int string array object resource。
- refcount 引用计数
根因:PHP内存管理基础。记录这个zval被多少变量引用;refcount降到0,内存释放。
缺陷:循环引用对象/数组,refcount无法自动回收,需要GC回收器。
GC 垃圾回收器(循环引用回收)
专门处理$a=&$b; $b=&$a这种循环引用;达到gc_threshold阈值,触发GC扫描回收。
代价:会消耗CPU,大对象多的时候会出现STW(stop‑the‑world)停顿。COW 写时复制 Copy‑On‑Write
根因:节省内存。数组赋值$b = $a,不拷贝内存,refcount+1,共用同一份zval;只有发生修改时才真正复制内存。
坑:大数组传参,函数内部修改数组触发拷贝,内存瞬间飙升。符号表(Symbol table)
存储变量名到zval的映射;分全局符号表、局部函数符号表。
- 全局符号表:
$GLOBALS;FPM每次请求重置;Swoole常驻进程全局符号表进程级别,协程并发极易污染。
EG(executor_globals)执行器全局
Zend执行器上下文,保存调用栈、异常、错误处理、当前符号表。调用栈(Call stack)
函数/方法一层层调用形成栈帧;异常回溯、debug_backtrace读取调用栈。栈帧(zend_execute_data)
每一次函数调用生成一个栈帧;保存局部变量、返回地址。函数执行结束栈帧销毁。
三、变量、超全局、资源类型术语
- 超全局变量 $_GET $_POST $_COOKIE $_SESSION $_SERVER $_REQUEST $_FILES
FPM:RINIT阶段每请求重新生成,请求隔离。
Swoole/Hyperf禁止直接使用超全局变量,多个协程共享进程全局,会发生数据错乱;框架封装Request对象从协程上下文拿数据。
Resource资源类型
内核外部句柄:mysql连接、文件句柄、socket句柄。
资源是编号,不是真正指针;请求结束FPM自动回收释放;Swoole常驻不会自动回收,必须手动close。静态变量 static $a
函数内static变量:作用域是进程级别。
- FPM:同一个Worker多次请求之间static变量保留值;
- Swoole:所有协程共享这个static变量,极易并发错乱。
全局变量 global $a
存放在全局符号表,进程存活期间保留,常驻内存风险极高。对象句柄(Object handle)
PHP对象不直接拷贝;变量存对象编号,多个变量指向同一个对象实例。
四、错误、异常、运行时配置术语
error_reporting 错误报告级别
控制哪些级别错误会被抛出;E_ERROR、E_WARNING、E_NOTICE、E_DEPRECATED。display_errors
是否把错误打印输出到页面;生产环境必须关闭。log_errors
错误写入日志文件,线上开启。Exception 异常对象
主动throw抛出,try‑catch捕获。
Error:致命错误,PHP7之后大部分致命错误转为Error异常,可以捕获。
zend错误处理回调 set_error_handler、set_exception_handler
接管错误、异常;框架用来统一记录日志。注意:部分致命错误无法捕获。php.ini
PHP运行时配置文件。
FPM:可以通过pool的php_value/php_admin_value覆盖;
CLI/Swoole:启动时读取一份php.ini,运行时修改部分配置不生效。ini_set()
运行时修改ini配置;部分配置受PHP_INI_*模式限制,运行时不可修改。
- PHP_INI_ALL:代码可修改
- PHP_INI_PERDIR:php.ini、.htaccess可改,代码不可改
- PHP_INI_SYSTEM:仅php.ini可修改
五、两大SAPI运行时模型对比(核心考点)
模型1:FPM‑SAPI(请求隔离运行时)
- Worker进程常驻,但是每一次请求执行完整RINIT、RSHUTDOWN
- 请求结束,请求级符号表、局部变量、资源全部释放
- 但是进程级静态变量、全局变量在同一个Worker内跨请求保留
- 没有用户态协程,一个进程同一时刻只能处理1个请求
- 无内置连接池,数据库、redis连接属于请求资源,请求结束关闭
坑点:很多人误以为FPM每次请求销毁进程;只是销毁请求资源,进程本身复用。
模型2:Swoole‑CLI常驻内存运行时(Hyperf底层)
- 进程启动执行一次GINIT,永远不会自动执行RINIT/RSHUTDOWN
- 没有天然的请求隔离;全部请求/协程跑在同一个进程空间。
- 协程上下文模拟请求隔离;进程全局符号表、static静态变量对所有协程可见。
- 对象、连接、数组不会请求结束自动清空,容易内存泄漏、协程污染。
- 需要框架手动管理资源:defer归还连接、协程退出清理上下文。
坑:直接使用GET/_GET/GET/_POST/global/static变量,会出现协程数据错乱。
六、扩展与钩子运行时术语
PHP扩展(Extension)
C语言编写,编译进PHP;分为内核扩展(pdo、curl)、第三方扩展(swoole、redis)。
扩展可以修改ZendVM行为,注册函数、钩子。Zend钩子(Zend hook)
内核回调钩子:opcode执行钩子、函数调用钩子;AOP、调试工具基于内核钩子实现。opcache预加载 opcache.preload
PHP7.4+常驻内存预加载;进程启动把类一次性加载到内存,不用autoload;适合Swoole项目。autoload自动加载
类找不到时触发__autoload/composer自动加载;FPM每次请求会触发;Swoole启动阶段全部加载完成,运行时几乎不再触发。__construct / __destruct 析构函数
对象销毁时触发;FPM请求结束批量触发;Swoole常驻内存,对象不释放析构不会跑。
七、运行时故障坑点术语(高频)
- 请求级资源泄漏 vs 进程级内存泄漏
- 请求级:单次请求内部内存大,RSHUTDOWN后释放,FPM不怕;
- 进程级:static数组无限追加、全局变量不断堆数据;内存只涨不跌,FPM靠max_requests重启缓解;Swoole风险极高。
循环引用内存泄漏
对象互相引用,refcount无法降为0,等待GC;GC不触发内存一直占用。写时复制陷阱
大数组传参,函数内部修改,触发COW复制,内存瞬间翻倍。超全局变量污染(Swoole特有)
直接使用$_GET $_POST,协程并发互相覆盖数据。静态变量跨协程污染
函数static变量,所有协程共享,并发请求数据错乱。RSHUTDOWN缺失
Swoole没有原生RSHUTDOWN;资源不会自动回收,连接、文件句柄忘记关闭,持续泄漏。OPcache失效问题
FPM开发环境忘记开启opcache.validate_timestamps,改代码看不到效果。STW停顿
GC大规模回收循环引用对象,ZendVM暂停执行所有PHP代码,出现请求卡顿。
八、FPM运行时 vs Swoole常驻运行时对照表
| 运行时维度 | FPM‑SAPI | Swoole‑CLI常驻内存 |
|---|---|---|
| RINIT/RSHUTDOWN | 每请求自动执行 | 不会自动执行,框架模拟 |
| 全局符号表 | 每请求重置 | 进程全局,所有协程共享 |
| static静态变量 | 同Worker跨请求保留 | 全部协程共享,高并发危险 |
| 超全局$_GET | 每请求重建,安全 | 原生超全局不可直接使用 |
| 对象/资源回收 | RSHUTDOWN自动回收请求资源 | 不会自动回收,需要手动释放 |
| Opcode加载 | 每请求autoload,OPcache缓存 | 进程启动一次性全部加载 |
| 内存泄漏缓解手段 | pm.max_requests定期重启Worker | 无自动机制,代码层面管控 |