扩展编译与内存泄漏
解读
国内一线/二线互联网公司 PHP 面试中,「扩展编译」与「内存泄漏」往往被放在同一题里考察,目的有三:
- 验证候选人是否真正动手写过 C 扩展,还是只停留在“会调用扩展”层面;
- 检查候选人是否具备 Linux 下 GCC、Makefile、gdb、Valgrind 等基础定位能力;
- 观察候选人能否把“C 层资源管理”与“PHP 层生命周期”对应起来,避免线上出现随着请求数上涨内存暴涨、进程被 OOM Kill 的故障。
因此,回答时要同时覆盖“编译流程”与“泄漏定位”两条线,并给出可落地的排查命令与代码级修复示例。
知识点
- PHP 扩展编译三件套:phpize、configure、make && make install
- config.m4 常用宏:PHP_ARG_WITH、PHP_ADD_LIBRARY、PHP_NEW_EXTENSION
- 扩展的内存管理宏:emalloc/efree、pemalloc/pefree、safe_emalloc
- 请求生命周期:MINIT → RINIT → 执行 → RSHUTDOWN → MSHUTDOWN
- Zend 内存管理器(ZendMM)与跟踪选项:USE_ZEND_ALLOC=0、valgrind --leak-check=full
- 常见泄漏场景:重复 zend_string_init 未释放、持久化链表未在 MSHUTDOWN 清理、自定义资源未注册正确的 dtor
- 国内线上排障工具链:Valgrind、perf、jemalloc 堆采样、Alibaba 的 dragonfly 调试扩展、Tideways/Xhprof 内存剖析
- 编码规范:PHP 内核 C 代码必须遵循 PHP Coding Standard(K&R 派生),否则合并补丁会被拒
答案
【扩展编译】
- 源码准备
假设公司扩展名称为 myext,目录结构:myext/ ├── config.m4 ├── myext.c ├── php_myext.h └── tests/ - 编写最小 config.m4
PHP_ARG_WITH(myext, for myext support, [ --with-myext Include myext support]) if test "$PHP_MYEXT" != "no"; then PHP_NEW_EXTENSION(myext, myext.c, $ext_shared) fi - 编译安装
安装成功后会提示扩展 so 路径,如$ phpize $ ./configure --with-myext $ make && make install/usr/local/php/lib/php/extensions/no-debug-non-zts-20210902/myext.so - 载入验证
在 php.ini 追加extension=myext,执行php -m | grep myext出现即 OK。
【内存泄漏定位】
- 构造可重复请求的 CLI 脚本 leak.php,循环调用待测函数 10 000 次,保证泄漏量可观测。
- 关闭 ZendMM,使用系统 malloc 方便 Valgrind 精确定位:
$ USE_ZEND_ALLOC=0 valgrind --leak-check=full --log-file=val.log php leak.php - 查看 val.log,若出现
definitely lost: 1,200,000 bytes in 10,000 blocks,记录堆栈最顶层为myext.c:myext_concat_strings。 - 回到源码,发现:
修复:确保每块动态内存都有唯一所有者,并在合适位置 RETURN/RETVAL 或 efree。PHP_FUNCTION(myext_concat_strings) { char *s1, *s2; size_t s1_len, s2_len; if (zend_parse_parameters(ZEND_NUM_ARGS(), "ss", &s1, &s1_len, &s2, &s2_len) == FAILURE) { return; } zend_string *zs = zend_string_alloc(s1_len + s2_len, 0); memcpy(ZSTR_VAL(zs), s1, s1_len); memcpy(ZSTR_VAL(zs) + s1_len, s2, s2_len); ZSTR_VAL(zs)[s1_len + s2_len] = '\0'; RETURN_STR(zs); // 正确做法 // 漏写 RETURN_STR(zs); 导致 zs 没有任何 zval 指向,泄漏 } - 对于持久化内存(pemalloc),一定在 MSHUTDOWN 或 GSHUTDOWN 阶段 pefree,并置空指针。
- 回归验证:再次 Valgrind,确认
definitely lost: 0 bytes。
【加分项】
- 如果扩展需要常驻链表,可使用 zend_llist,内置析构函数回调,避免手写链表忘记释放。
- 线上不方便 Valgrind 时,可打开 jemalloc 的 profiling,通过
jeprof生成 pdf 火焰图,快速定位泄漏热点。
拓展思考
- 多线程 CLI 模式下的泄漏:PHP 多数运行在 ZTS(Zend Thread Safety)环境,线程局部存储(TSRM)资源未释放同样会被 Valgrind 判定为 leak,需要注册
tsrm_ls析构。 - 内存与性能权衡:频繁 emalloc/efree 小块内存会造成 heap fragmentation,可考虑一次性预分配内存池,使用自定义 memory slab。
- 国内云原生场景:Sidecar 模式跑在 128 MiB 容器的 php-fpm,泄漏 20 MiB 即会被 K8s OOMKill,扩展上线前必须做“内存上限测试”,即通过 wrk 压测 5 分钟,观察 RSS 是否收敛。
- 未来 PHP 8.4 的“Arena allocator”与“Copy-on-write string”可能改变字符串内存模型,写扩展时要关注 UPGRADING.INTERNALS,避免硬拷贝 ZendString。
- 安全合规:金融、支付公司对内存泄漏零容忍,上线流程要求“0 字节 definitely lost”报告截屏随代码 review 一起归档,面试时可主动提及此类流程,展示对国内合规要求的理解。