0

0

PHP传参原理深入解析

php中文网

php中文网

发布时间:2016-07-25 09:13:20

|

1482人浏览过

|

来源于php中文网

原创

在编写php扩展时,似乎参数(即传给zend_parse_parameters的变量)是不需要free的。 举例:

  1. php_function(test)

  2. {
  3. char* str;
  4. int str_len;
  5. if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &str, &str_len) == FAILURE) {

  6. RETURN_FALSE;
  7. }
  8. php_printf(str);

  9. // 无需free(str)
  10. }
复制代码

运行正常: test("Hello World"); // 打印Hello World 这里不用担心test函数会发生内存泄露,php会自动帮我们回收这些用于保存参数的变量。

那php究竟是如何做到的呢?要解释这个问题,还是得看php是怎么传递参数的。

EG(argument_stack)简介 简单来讲,在php中的EG中保存了一个专门用于存放参数的栈,名为argument_stack。每当发生函数调用时,php会将传入的参数压进EG(argument_stack)。一旦函数调用结束,则EG(argument_stack)被清理,并且等待下一次的函数调用。

关于EG(argument_stack)的struct结构、用途,php5.2和5.3实现有一些区别。本文主要以5.2为例,5.3+的变化后面抽空再说。 php传参原理

上图是5.2中argument_stack的大概示意图,看起来简单明了。其中,栈顶和栈底固定为NULL。函数接收的参数按照从左到右的顺序,依次被压入栈。注意,最后会被额外压入一个long型的值,表示栈里的参数个数(上图中为10)。

立即学习PHP免费学习笔记(深入)”;

那被压入argument_stack的参数究竟是什么呢?其实是一个个zval类型的指针。 它们指向的zva有可能是CV变量,有可能是is_ref=1的变量,还有可能是一个常量数字,或者常量字符串。

EG(argument_stack)在php5.2中被具体实现为zend_ptr_stack类型:

  1. typedef struct _zend_ptr_stack {
  2. int top; // 栈中当前元素的个数
  3. int max; // 栈中最多存放元素的个数
  4. void **elements; // 栈底
  5. void **top_element; // 栈顶
  6. } zend_ptr_stack;
复制代码

初始化argument_stack 初始化argument_stack的工作是发生在php处理具体的请求之前,更准确说是处于php解释器的启动过程之中。

在init_executor函数里我们发现如下2行:

  1. zend_ptr_stack_init(&EG(argument_stack));
  2. zend_ptr_stack_push(&EG(argument_stack), (void *) NULL);
复制代码

这2行分别代表着,初始化EG(argument_stack),紧接着压入一个NULL。由于EG是个全局变量,因此在实际调用zend_ptr_stack_init之前,EG(argument_stack)中的所有数据全部为0。

zend_ptr_stack_init实现很简单。

  1. ZEND_API void zend_ptr_stack_init(zend_ptr_stack *stack)
  2. {
  3. stack->top_element = stack->elements = (void **) emalloc(sizeof(void *)*PTR_STACK_BLOCK_SIZE);
  4. stack->max = PTR_STACK_BLOCK_SIZE; // 栈的大小被初始化成64
  5. stack->top = 0; // 当前元素个数为0
  6. }
复制代码

一旦argument_stack被初始化完,则立即会被压入NULL。这里无须深究,这个NULL其实没有任何的含义。

NULL入栈之后,整个argument_stack的实际内存分布如下: php传参原理

参数入栈 在压入第一个NULL之后,一旦再有参数入栈,则argument_stack会发生如下动作: stack->top++; *(stack->top_element++) = 参数;

用一段简单的php代码来说明问题:

  1. function foo( $str ){
  2. print_r(123);
  3. }
  4. foo("hello world");
复制代码

上述代码在调用foo时,传入了一个字符串常量。因此,实际上被压入栈的是一个指向存储“hello world”的zval。用vld来查看编译之后的opcode:

  1. line # * op fetch ext return operands
  2. ---------------------------------------------------------------------------------
  3. 3 0 > NOP
  4. 6 1 SEND_VAL OP1[ IS_CONST (458754) 'hello world' ]
  5. 2 DO_FCALL 1 OP1[ IS_CONST (458752) 'foo' ]
  6. 15 3 > RETURN OP1[ IS_CONST (0) 1 ]
复制代码

SEND_VAL指令实际上做的事情就是将“hello world”压入argument_stack。

  1. int ZEND_SEND_VAL_SPEC_CONST_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

  2. {
  3. ……
  4. zval *valptr, *value;
  5. value = &opline->op1.u.constant;

  6. ALLOC_ZVAL(valptr);
  7. INIT_PZVAL_COPY(valptr, value);
  8. if (!0) {
  9. zval_copy_ctor(valptr);
  10. }
  11. // 入栈,valptr指向存放hello world的zval

  12. zend_ptr_stack_push(&EG(argument_stack), valptr);
  13. ……
  14. }
复制代码

入栈完成之后的argument_stack为:

php传参原理3

参数个数 前文说到,实际上并非把所有参数入栈就完事了。php还会额外压入一个数字,表示参数的个数,这个工作并非发生在SEND_XXX指令时。实际上,在真正执行函数之前,php会将参数个数入栈。

继续沿用上面的例子,DO_FCALL 指令用于调用foo函数。在调用foo之前,php会自动填入argument_stack最后一块。

  1. static int zend_do_fcall_common_helper_SPEC(ZEND_OPCODE_HANDLER_ARGS)
  2. {
  3. ……
  4. // 在argument_stack中压入2个值
  5. // 一个是参数个数(即opline->extended_value)
  6. // 一个是标识栈顶的NULL
  7. zend_ptr_stack_2_push(&EG(argument_stack), (void *)(zend_uintptr_t)opline->extended_value, NULL);
  8. ……
  9. if (EX(function_state).function->type == ZEND_INTERNAL_FUNCTION) {
  10. ……
  11. }
  12. else if (EX(function_state).function->type == ZEND_USER_FUNCTION) {
  13. ……
  14. // 调用foo函数
  15. zend_execute(EG(active_op_array) TSRMLS_CC);
  16. }
  17. else { /* ZEND_OVERLOADED_FUNCTION */
  18. ……
  19. }
  20. ……
  21. // 清理argument_stack
  22. zend_ptr_stack_clear_multiple(TSRMLS_C);
  23. ……
  24. ZEND_VM_NEXT_OPCODE();
  25. }
复制代码

压入参数个数和NULL之后,用于foo调用的整个argument_stack已然完成。 php传参原理 获取参数 继续跟进上面的例子。 深入到foo函数,看看foo的opcode是什么样子的。

  1. line # * op fetch ext return operands
  2. ---------------------------------------------------------------------------------
  3. 3 0 > RECV OP1[ IS_CONST (0) 1 ]
  4. 4 1 SEND_VAL OP1[ IS_CONST (5) 123 ]
  5. 2 DO_FCALL 1 OP1[ IS_CONST (459027) 'print_r' ]
  6. 5 3 > RETURN OP1[ IS_CONST (0) null ]
复制代码

第一条指令是RECV,从字面上理解便是用于获取栈中参数的。实际上,SEND_VAL和RECV有点对应的感觉。每次函数调用之前SEND_VAL,在函数内部进行RECV。为什么不说是完全对应,实际上RECV指令并非一定需要。只有当用户定义的函数被调用是,才会产生RECV。我们编写的扩展函数,php自带的内建函数,都不会有RECV。

需要额外指出的是,每次SEND_VAL和RECV 均只能处理一个参数。也就是说如果传参的过程中有多个参数,那么会产生若干SEND_VAL以及若干RECV。这里引出一个很有趣的话题,传入参数和获取参数的顺序是怎样的呢?

答案是,SEND_VAL会将参数从左至右的进行压栈,而RECV一样的从左至右获取参数。

  1. static int ZEND_RECV_SPEC_HANDLER(ZEND_OPCODE_HANDLER_ARGS)

  2. {
  3. ……// param拿参数的顺序是沿着栈顶-->栈底
  4. if (zend_ptr_stack_get_arg(arg_num, (void **) ¶m TSRMLS_CC)==FAILURE) {
  5. ……
  6. } else {
  7. zend_free_op free_res;
  8. zval **var_ptr;
  9. // 验证参数

  10. zend_verify_arg_type((zend_function *) EG(active_op_array), arg_num, *param TSRMLS_CC);
  11. var_ptr = get_zval_ptr_ptr(&opline->result, EX(Ts), &free_res, BP_VAR_W);
  12. // 获取参数
  13. if (PZVAL_IS_REF(*param)) {
  14. zend_assign_to_variable_reference(var_ptr, param TSRMLS_CC);
  15. } else {
  16. zend_receive(var_ptr, *param TSRMLS_CC);
  17. }
  18. }
  19. ZEND_VM_NEXT_OPCODE();

  20. }
复制代码

zend_assign_to_variable_reference 和 zend_receive 都会完成“获取参数” 。“获取参数”不太好理解,实际它究竟是做哪些事情呢?

PHP与MySQL程序设计3
PHP与MySQL程序设计3

本书是全面讲述PHP与MySQL的经典之作,书中不但全面介绍了两种技术的核心特性,还讲解了如何高效地结合这两种技术构建健壮的数据驱动的应用程序。本书涵盖了两种技术新版本中出现的最新特性,书中大量实际的示例和深入的分析均来自于作者在这方面多年的专业经验,可用于解决开发者在实际中所面临的各种挑战。 本书内容全面深入,适合各层次PHP和MySQL开发人员阅读,既是优秀的学习教程,也可用作参考手册。

下载

说到底很简单,“获取参数”就是将这个参数添加到当前函数执行期间的“符号表”中,具体对应为EG(current_execute_data)->symbol_table。本示例中,RECV完成之后,函数体内的symbol_table中有了一个符号‘str’,它的值为“hello world”。

但argument_stack并没有发生一丝变化,因为RECV仅仅是读取参数,而不会对栈产生类似pop操作。

php传参原理5

清理argument_stack foo内部的print_r也是一个函数调用,因此也会产生压栈-->清栈的操作。因此print_r执行之前的argument_stack为: php传参原理6

print_r执行之后argument_stack又回到了foo刚RECV完的状态。

具体调用print_r的过程并非本文阐述的重点。我们关心的是当调用foo结束之后,php是如何清理argument_stack的。

上面展示的do_fcall代码片段中可以看出,清理工作由zend_ptr_stack_clear_multiple完成的。

  1. static inline void zend_ptr_stack_clear_multiple(TSRMLS_D)
  2. {
  3. void **p = EG(argument_stack).top_element-2;
  4. // 取栈顶保存的参数个数
  5. int delete_count = (int)(zend_uintptr_t) *p;
  6. EG(argument_stack).top -= (delete_count+2);
  7. // 从上至下,依次清理
  8. while (--delete_count>=0) {
  9. zval *q = *(zval **)(--p);
  10. *p = NULL;
  11. zval_ptr_dtor(&q);
  12. }
  13. EG(argument_stack).top_element = p;
  14. }
复制代码

注意这里清理栈中zval指针,使用的是zval_ptr_dtor。zval_ptr_dtor会将refcount减1,一旦refcount减为0,则保存该变量的内存区域会被真正的回收掉。

在本文示例中,foo调用完毕之后,保存“hello world”的zval状态为:

  1. value "hello world"
  2. refcount 1
  3. type 6
  4. is_ref 0
复制代码

由于refcount只剩1,因此,zval_ptr_dtor会将“hello world”真正从内存中销毁。

消栈完毕之后的argument_stack内存状态为:

php传参原理7

可以看出上图中的argument_stack与刚被初始化之后是一样的。此时argument_stack真正做好了迎接下一次函数调用的准备。

回到文章刚开始的问题... 为何无需free(str)呢?弄明白了argument_stack之后就很好理解这个问题了。

因为str指向的是zval中实际存放“hello world”的内存地址。假设扩展函数如下:

  1. php_function(test)

  2. {
  3. char* str;
  4. int str_len;
  5. if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s", &str, &str_len) == FAILURE) {

  6. RETURN_FALSE;
  7. }
  8. str[0] = 'H';

  9. }
复制代码

则调用

  1. $a = "hello world";
  2. test($a);
  3. echo $a;
复制代码

会输出“Hello world”。尽管我们调用test时,并非是传$a的引用,但实际效果相当于test(&$a)。

简单来说,内存中只有一份$a,不管是CV数组中,还是在argument_stack中。而zend_parse_parameters并没有拷贝一份数据用于函数执行,事实上它也不能这么做。因此,当函数完毕之后,如果没有其他地方会用到$a,php清理argument_stack时会帮我们free。如果仍然其他代码在使用,就更加不能手动free了,否则会破坏$a的内存区域。

注意,并非写扩展函数中用到的每个变量,php都会自动回收。所以该free时,切勿手软:)



相关文章

PHP速学教程(入门到精通)
PHP速学教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
batoto漫画官网入口与网页版访问指南
batoto漫画官网入口与网页版访问指南

本专题系统整理batoto漫画官方网站最新可用入口,涵盖最新官网地址、网页版登录页面及防走失访问方式说明,帮助用户快速找到batoto漫画官方平台,稳定在线阅读各类漫画内容。

331

2026.02.25

Steam官网正版入口与注册登录指南_新手快速进入游戏平台方法
Steam官网正版入口与注册登录指南_新手快速进入游戏平台方法

本专题系统整理Steam官网最新可用入口,涵盖网页版登录地址、新用户注册流程、账号登录方法及官方游戏商店访问说明,帮助新手玩家快速进入Steam平台,完成注册登录并管理个人游戏库。

49

2026.02.25

TypeScript全栈项目架构与接口规范设计
TypeScript全栈项目架构与接口规范设计

本专题面向全栈开发者,系统讲解基于 TypeScript 构建前后端统一技术栈的工程化实践。内容涵盖项目分层设计、接口协议规范、类型共享机制、错误码体系设计、接口自动化生成与文档维护方案。通过完整项目示例,帮助开发者构建结构清晰、类型安全、易维护的现代全栈应用架构。

33

2026.02.25

Python数据处理流水线与ETL工程实战
Python数据处理流水线与ETL工程实战

本专题聚焦 Python 在数据工程场景下的实际应用,系统讲解 ETL 流程设计、数据抽取与清洗、批处理与增量处理方案,以及数据质量校验与异常处理机制。通过构建完整的数据处理流水线案例,帮助开发者掌握数据工程中的性能优化思路与工程化规范,为后续数据分析与机器学习提供稳定可靠的数据基础。

13

2026.02.25

Java领域驱动设计(DDD)与复杂业务建模实战
Java领域驱动设计(DDD)与复杂业务建模实战

本专题围绕 Java 在复杂业务系统中的建模与架构设计展开,深入讲解领域驱动设计(DDD)的核心思想与落地实践。内容涵盖领域划分、聚合根设计、限界上下文、领域事件、贫血模型与充血模型对比,并结合实际业务案例,讲解如何在 Spring 体系中实现可演进的领域模型架构,帮助开发者应对复杂业务带来的系统演化挑战。

5

2026.02.25

Golang 生态工具与框架:扩展开发能力
Golang 生态工具与框架:扩展开发能力

《Golang 生态工具与框架》系统梳理 Go 语言在实际工程中的主流工具链与框架选型思路,涵盖 Web 框架、RPC 通信、依赖管理、测试工具、代码生成与项目结构设计等内容。通过真实项目场景解析不同工具的适用边界与组合方式,帮助开发者构建高效、可维护的 Go 工程体系,并提升团队协作与交付效率。

19

2026.02.24

Golang 性能优化专题:提升应用效率
Golang 性能优化专题:提升应用效率

《Golang 性能优化专题》聚焦 Go 应用在高并发与大规模服务中的性能问题,从 profiling、内存分配、Goroutine 调度、GC 机制到 I/O 与锁竞争逐层分析。结合真实案例讲解定位瓶颈的方法与优化策略,帮助开发者建立系统化性能调优思维,在保证代码可维护性的同时显著提升服务吞吐与稳定性。

9

2026.02.24

Golang 面试题精选:高频问题与解答
Golang 面试题精选:高频问题与解答

Golang 面试题精选》系统整理企业常见 Go 技术面试问题,覆盖语言基础、并发模型、内存与调度机制、网络编程、工程实践与性能优化等核心知识点。每道题不仅给出答案,还拆解背后的设计原理与考察思路,帮助读者建立完整知识结构,在面试与实际开发中都能更从容应对复杂问题。

7

2026.02.24

Golang 运行与部署实战:从本地到云端
Golang 运行与部署实战:从本地到云端

《Golang 运行与部署实战》围绕 Go 应用从开发完成到稳定上线的完整流程展开,系统讲解编译构建、环境配置、日志与配置管理、容器化部署以及常见运维问题处理。结合真实项目场景,拆解自动化构建与持续部署思路,帮助开发者建立可靠的发布流程,提升服务稳定性与可维护性。

5

2026.02.24

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
微信小程序开发之API篇
微信小程序开发之API篇

共15课时 | 1.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号