给你一套完整的 PHP 底层C 扩展开发实战从原理到完整可编译代码全程大白话。我不创建文件直接把代码贴给你你照着敲就能跑。---一、先搞懂 PHP 底层到底是啥大白话)你平时写的.php 文件是给 Zend 引擎 执行的。Zend 引擎是用 C 语言写的。 所谓PHP 底层开发 / 原生扩展开发就是你用 C 语言写一个.soLinux或.dllWindows的动态库挂到 PHP 引擎上让 PHP 里能直接调用你用 C 写的函数。 为什么要这么干-C 比 PHP 快几十倍上百倍。算法热点比如加密、图像处理、海量计算用 C 写。-省内存。C 你自己管内存没有 PHP 那层胖胖的封装。-调系统底层/第三方 C 库比如某个只有 C 接口的 SDK。 核心概念就一个词zval。PHP 里所有变量$a1、$babc在底层都是一个叫 zval 的 C 结构体。你做底层开发本质就是在 C 里操作这些 zval。---二、环境准备大白话装编译工具)PHP 扩展必须在 Linux 下开发最舒服Windows 编译扩展非常痛苦建议用 WSL 或 Docker。#Ubuntu/Debiansudo apt update sudo apt install php-dev gcc make autoconf#CentOS/RHELsudo yum install php-devel gcc make autoconf # 验证这个命令是扩展开发的灵魂工具 phpize--version php-config--version-php-dev/php-devel里面带了写扩展要的头文件和 phpize 工具。-phpize自动帮你生成编译扩展需要的一堆配置脚本。---三、完整扩展开发流程一个能跑的真实例子)我们写一个扩展叫 myperf里面提供两个函数1.myperf_add($a,$b)——两个数相加演示参数传递。2.myperf_sum_array($arr)——对数组求和演示遍历、性能优化。 一个扩展最少需要3个文件config.m4编译配置、php_myperf.h头文件、myperf.c核心代码。1.config.m4 ——告诉编译器怎么编 dnl config.m4 ——dnl 开头是注释 dnl PHP_ARG_ENABLE注册一个--enable-myperf 编译开关PHP_ARG_ENABLE([myperf],[whether to enable myperf support],[AS_HELP_STRING([--enable-myperf],[Enable myperf support])],[no])dnl 如果开关打开了就把 myperf.c 编译成扩展iftest$PHP_MYPERF!no;thenAC_DEFINE(HAVE_MYPERF,1,[Have myperf support])PHP_NEW_EXTENSION(myperf,myperf.c,$ext_shared)fi 大白话这个文件就是编译说明书。PHP_NEW_EXTENSION(myperf,myperf.c,...)这行是关键——告诉它扩展名叫myperf源码是 myperf.c。2.php_myperf.h ——头文件声明)/* php_myperf.h */#ifndefPHP_MYPERF_H#definePHP_MYPERF_Hexternzend_module_entry myperf_module_entry;#definephpext_myperf_ptrmyperf_module_entry#definePHP_MYPERF_VERSION1.0.0#ifdefZTS#includeTSRM.h#endif#endif/* PHP_MYPERF_H */大白话头文件就是个目录告诉别的代码我这扩展长啥样、版本几号。ZTS 是线程安全相关的先不用管细节。3.myperf.c ——核心代码重点全在这)/* myperf.c */#ifdefHAVE_CONFIG_H#includeconfig.h#endif#includephp.h#includeext/standard/info.h#includephp_myperf.h/* * 函数 1myperf_add($a, $b) ——两数相加 * */PHP_FUNCTION(myperf_add){/* zend_long 是 PHP 底层的整数类型相当于 PHP 的 int */zend_long a,b;/* 解析 PHP 传进来的参数。 * ll 表示我要两个 long 类型参数。 * a, b 是用来接收的变量地址。 * 如果用户传参不对这个宏会自动报错并 return。 */ZEND_PARSE_PARAMETERS_START(2,2)/* 最少2个参数最多2个 */Z_PARAM_LONG(a)Z_PARAM_LONG(b)ZEND_PARSE_PARAMETERS_END();/* RETURN_LONG把一个整数作为返回值还给 PHP */RETURN_LONG(ab);}/* * 函数 2myperf_sum_array($arr) ——数组求和 * 这里演示性能优化的核心直接遍历底层 HashTable * 不经过任何 PHP 层的封装速度飞快。 * */PHP_FUNCTION(myperf_sum_array){zval*arr;/* 接收传进来的数组 */zval*val;/* 遍历时每个元素 */zend_long sum0;/* 累加结果 */ZEND_PARSE_PARAMETERS_START(1,1)Z_PARAM_ARRAY(arr)/* a 类型我要一个数组 */ZEND_PARSE_PARAMETERS_END();/* ZEND_HASH_FOREACH_VAL这是底层遍历数组最快的宏。 * Z_ARRVAL_P(arr) 拿到数组底层的 HashTable。 */ZEND_HASH_FOREACH_VAL(Z_ARRVAL_P(arr),val){/* 只对整数和浮点数求和做个类型判断 */if(Z_TYPE_P(val)IS_LONG){sumZ_LVAL_P(val);/* 取整数值 */}elseif(Z_TYPE_P(val)IS_DOUBLE){sum(zend_long)Z_DVAL_P(val);/* 取浮点值 */}}ZEND_HASH_FOREACH_END();RETURN_LONG(sum);}/* * 参数信息arginfo——PHP7.2 强制要求 * 大白话声明每个函数的参数长啥样给反射/类型检查用 * */ZEND_BEGIN_ARG_INFO_EX(arginfo_myperf_add,0,0,2)ZEND_ARG_INFO(0,a)ZEND_ARG_INFO(0,b)ZEND_END_ARG_INFO()ZEND_BEGIN_ARG_INFO_EX(arginfo_myperf_sum_array,0,0,1)ZEND_ARG_INFO(0,arr)ZEND_END_ARG_INFO()/* * 函数注册表 ——把 C 函数和 PHP 函数名挂钩 * */staticconstzend_function_entry myperf_functions[]{PHP_FE(myperf_add,arginfo_myperf_add)PHP_FE(myperf_sum_array,arginfo_myperf_sum_array)PHP_FE_END/* 结束标记必须有 */};/* * 扩展信息phpinfo 里会显示 * */PHP_MINFO_FUNCTION(myperf){php_info_print_table_start();php_info_print_table_header(2,myperf support,enabled);php_info_print_table_row(2,Version,PHP_MYPERF_VERSION);php_info_print_table_end();}/* * 模块入口 ——整个扩展的身份证 * */zend_module_entry myperf_module_entry{STANDARD_MODULE_HEADER,myperf,/* 扩展名 */myperf_functions,/* 上面的函数表 */NULL,/* MINIT模块加载时执行整个进程一次 */NULL,/* MSHUTDOWN模块卸载时执行 */NULL,/* RINIT每个请求开始时执行 */NULL,/* RSHUTDOWN每个请求结束时执行 */PHP_MINFO(myperf),/* phpinfo 信息 */PHP_MYPERF_VERSION,STANDARD_MODULE_PROPERTIES};/* 让 PHP 能动态加载这个扩展 */#ifdefCOMPILE_DL_MYPERFZEND_GET_MODULE(myperf)#endif4.编译安装测试完整命令)# 在三个文件所在目录执行 phpize # 生成编译脚本./configure--enable-myperf # 配置 make # 编译生成 modules/myperf.so sudo make install # 安装到 PHP 扩展目录 # 临时测试不改 php.ini php-d extensionmyperf.so-r echomyperf_add(3,5),\n;// 输出 8echomyperf_sum_array([1,2,3,4]),\n;// 输出 10 # 永久启用在 php.ini 加一行#extensionmyperf.so到这里你的第一个 C 扩展就跑通了 。---四、性能优化底层视角的大白话)1.zval 的写时复制Copy On WriteCOW PHP 里 $b$a 时不会真的复制数据只是让 $b 和 $a 指向同一块内存引用计数1。只有当你改其中一个时才真正复制一份。 优化要点在 C 扩展里读数组/字符串时绝对不要无脑复制。能用引用就用引用/* 错误示范无意义的复制浪费内存和 CPU */zval copy;ZVAL_COPY(copy,original);/* 引用计数1大数组开销大 *//* 正确只读场景直接用指针啥都不复制 */zval*valoriginal;if(Z_TYPE_P(val)IS_STRING){char*strZ_STRVAL_P(val);/* 直接拿底层指针零拷贝 */size_tlenZ_STRLEN_P(val);}2.字符串用 zend_string别用裸char*PHP7的字符串是 zend_string里面缓存了长度和哈希值比 C 的char*每次还得 strlen快得多。/* 创建一个 zend_string智能字符串 */zend_string*strzend_string_init(hello,sizeof(hello)-1,0);/* 第3个参数 0 请求级内存请求结束自动释放1 持久内存 *//* 用完手动释放引用计数减1 */zend_string_release(str);/* 把 zend_string 作为返回值还给 PHP零拷贝转移所有权 */RETURN_STR(str);3.遍历数组用宏别手动操作 HashTable 上面 myperf_sum_array 用的 ZEND_HASH_FOREACH_VAL 是内联展开的宏没有函数调用开销比老的 zend_hash_get_current_data 循环快很多。这是数组性能优化的标准写法。4.预分配避免反复扩容 要返回一个大数组时一次性把容量开够别让它一边塞一边自动扩容扩容要重新哈希很贵PHP_FUNCTION(myperf_make_array){zend_long n;ZEND_PARSE_PARAMETERS_START(1,1)Z_PARAM_LONG(n)ZEND_PARSE_PARAMETERS_END();array_init_size(return_value,n);/* 关键预分配 n 个槽位避免反复扩容 */for(zend_long i0;in;i){add_next_index_long(return_value,i*i);/* 往数组尾部塞元素 */}/* return_value 就是返回值不用 RETURN_xxx */}5.把热点循环整个搬到 C 里 最大的性能提升不是优化单个函数而是把整个计算密集的循环搬进 C。// PHP 写法1000 万次循环慢$sum0;for($i0;$i10000000;$i){$sum$i;}// C 扩展写法一个函数调用搞定快几十倍$summyperf_loop_sum(10000000);对应的 C 函数PHP_FUNCTION(myperf_loop_sum){zend_long n,i,sum0;ZEND_PARSE_PARAMETERS_START(1,1)Z_PARAM_LONG(n)ZEND_PARSE_PARAMETERS_END();for(i0;in;i){sumi;/* 纯 C 循环没有 PHP 引擎的 opcode 开销 */}RETURN_LONG(sum);}记住这条铁律PHP↔C的来回调用本身有成本。所以要减少调用次数、加大每次调用的工作量——别在PHP 里循环1000万次每次调一个 C 函数而是调一次 C 函数让它在内部循环1000万次。---五、内存优化大白话)1.分清两种内存请求内存 vs 持久内存 这是 PHP 底层内存管理的核心区别 ┌──────────┬───────────────────┬────────────────┬────────────────────────────────────┐ │ 类型 │ 分配函数 │ 释放函数 │ 大白话 │ ├──────────┼───────────────────┼────────────────┼────────────────────────────────────┤ │ 请求内存 │ emalloc │ efree │ 请求结束后引擎自动全部回收最常用 │ ├──────────┼───────────────────┼────────────────┼────────────────────────────────────┤ │ 持久内存 │pemalloc(size,1)│pefree(ptr,1)│ 跨请求存活你不释放就泄漏慎用 │ └──────────┴───────────────────┴────────────────┴────────────────────────────────────┘/* 99% 的情况用这个请求级内存 */char*bufemalloc(1024);/* 分配 *//* ... 用 buf ... */efree(buf);/* 释放即使忘了请求结束也会自动清 *//* 只有需要跨请求保存的全局数据才用持久内存 */char*globalpemalloc(1024,1);/* 第二个参数 1 持久 *//* 必须在 MSHUTDOWN 里手动释放否则永久泄漏 */pefree(global,1);优化要点能用 emalloc 就别用 pemalloc。请求内存有引擎兜底几乎不会泄漏持久内存全靠你自觉。2.用引擎自带的内存调试工具抓泄漏 PHP 的 emalloc 系列自带泄漏检测。编译一个 debug 版 PHP跑完会自动报告哪一行内存没释放 # 编译 debug 版 PHP开发时用./configure--enable-debug makemake install # 跑你的代码如果有泄漏结束时会打印 #[Thu]Script:-Freeing0x...(1024bytes),allocated at myperf.c:42...大白话debug 版 PHP 会盯着你每一次 emalloc结束时谁没 efree直接报出文件名和行号比 valgrind 还方便。3.引用计数该加加该减减别失衡 zval 内部有引用计数。你在 C 里持有一个 zval 时计数要1放手时要-1。失衡就是泄漏多了或崩溃少了。 zval*val...;/* 我要长期持有它 →计数1 */Z_TRY_ADDREF_P(val);/* 我不要了 →计数-1归零时引擎自动释放 */Z_TRY_DELREF_P(val);只读用一下、马上就还回去的场景根本不用动引用计数——这也是省内存省CPU 的关键。4.字符串/数组的写时复制也是省内存利器 前面说的 COW 不光省 CPU更省内存。一个1MB 的字符串赋值100次只要没人改内存里只有1份。在 C 扩展里把握住只读不复制原则就能享受这个红利。---六、完整工作流程总结一张图说清)1.写代码 →config.m4php_xxx.hxxx.c2.phpize →生成编译环境3.configure →./configure--enable-xxx4.make →编译出 xxx.so5.install →make install 装到扩展目录6.启用 →php.ini 加 extensionxxx.so7.测试 →php-r调用你的函数8.调优 →用 debug 版 PHP 抓内存泄漏用 ZEND_HASH 宏提速9.上线 →编译 release 版去掉--enable-debug---七、给你的实战建议大白话忠告)1.先确定瓶颈再写扩展。用 xhprof/perf 找到真正慢的那5%代码只把那部分搬到 C。不要为了底层而底层。2.参数解析必用 ZEND_PARSE_PARAMETERS别手写容易出安全漏洞。3.内存优先 emalloc持久内存是泄漏重灾区。4.开发期一定用 debug 版 PHP泄漏当场暴露。5.减少 PHP↔C调用次数把循环搬进 C 内部。6.现代替代方案如果你不想啃 C可以看看 FFIPHP7.4直接在 PHP 里调 C 库零编译或 Rust 的 ext-php-rs用 Rust 写扩展比 C 安全。但要榨干性能手写 C 扩展仍是天花板。