PHP 底层分析

Jackey PHP 3,616 次浏览 没有评论

PHP编译特点

编译型语言

对于C语言,C++,编译成机器码(二进制)来运行。
Java语言,把.java 编译成.class, 称为bytecode(字节码),由jvm来运行

解释型语言

解释器解释执行。 典型的如: linux shell

解释器逐行来执行命令

PHP执行

PHP是先编译后执行

PHP稍有特殊,虽然是一个脚本语言,但不是靠解释器解释。而是zend虚拟机执行,屏蔽了操作系统的区别。

PHP代码编译成 opcode,由zend虚拟机来执行opcode。

但是opcode ,PHP脚本一结束,opcode就清除了。

opcode 能否缓存

PHP本身不支持,但是apc,xcache等加速器,实现了这样的效果。

变量的底层实现

PHP底层是C语言来实现的,C语言是强类型,而PHP是弱类型语言,是如何实现的

PHP的源码包:

|__ ext
|__ main
|__ pear
|__ sapi
|__ tests
|__ TSRM
|__ Zend
|__ .gdbinit

最核心的是Zend,这是zend虚拟的实现。包括栈,数据类型,编译器等.
最重要的main,PHP的一些内建函数,最重要的函数都在该目录下.
最大的一个目录 ext, PHP的扩展.

PHP的大部分功能,都是以extenstion形式来完成的。
如果自身开发了一个扩展,也放入ext目录下。

弱类型语言变量的实现

  1. /* zend.h */
  2. struct _zval_struct {
  3. zvalue_value value; /* 值 */
  4. zend_uint refcount__gc;
  5. zend_uchar type; /* 活动类型 */
  6. zend_uchar is_ref__gc;
  7. }

PHP中的一个变量,zend虚拟机中,使用的是 _zval_struct 的结构体来描述,变量的值也是一个就结构体来描述.

_zval_struct的结构体是由 四个字段/域 (可以理解成关联数组)

zvalue_value value; /* 值 */

PHP变量的值,存储这个字段中。

具体存储的位置:

  1. /* value 值 是一个 联合 */
  2. /* zend.h */
  3. typedef union _zval_value {
  4. long lval; /* long value */
  5. double dval; /* double value */
  6. struct {
  7. char * val;
  8. int len;
  9. } str;
  10. HashTable *ht; /* hash table 指针 */
  11. zend_object_value obj;
  12. } zvalue_value;

Zend对变量的表示

zend实现了 zval结构体

  1. {
  2. value: [联合体] /* 联合体的内容可能是C语言中的long,double,hashtable(*ht),obj, 联合体只能是其中一种类型,是一个枚举 */
  3. type: 变量类型 , /* IS_NULL,IS_BOOL,IS_STRING, IS_LONG,IS_DOUBLE,IS_ARRAY,IS_OBJECT,IS_RESOURCE */
  4. refcount_gc
  5. is_ref_gc
  6. }

C语言中类型对应PHP中的数据类型:

long -> int
double -> double
hashtable -> array
struct -> string
obj -> object

例如:

  1. $a = 3;
  2. {
  3. value: [long lval = 3]
  4. type: IS_LONG
  5. }
  6.  
  7.  
  8. $a = 3.5;
  9. {
  10. value: [double dval = 3.5]
  11. type: IS_DOUBLE
  12. }

变量类型的实现

zend_uchar type; /* 活动类型 */

可以根据上下文环境来强制转换。
例如:需要echo 的时候 就转换成 string
需要加减运算就 转换成 int

PHP 中有8中数据类型,为什么zval->value 联合体中,只有5中 ?
1: NULL,直接 zval->type = IS_NULL, 就可以表示,不必设置 value 的值。
2:BOOL, zval->type = IS_BOOL. 再设置 zval.value.lval = 1/0; (C语言中没有布尔值,都是通过1,0,来表示)
3: resource ,资源型,往往是服务器上打开一个接口,如果 文件读取接口。 zval->type = IS_RESOURCE, zval->type.lval = 服务器上打开的接口编号。

  1. struct {
  2. char * val;
  3. int len;
  4. } str;

PHP中,字符串类型,长度是已经缓存的,调用strlen时,系统可以直接返回其长度,不需要计算。

  1. $b = 'hello';
  2.  
  3. /**
  4.  *
  5.  * {
  6.  * union_zvalue {
  7.  * // 字符串的指针
  8.  * struct{
  9.  * char: 'hello';
  10.  * len: 5
  11.  * } str;
  12.  * }
  13.  * type: IS_STRING;
  14.  * refcount_gc: 1,
  15.  * is_ref_gc: 0
  16.  * }
  17.  *
  18.  */
  19.  
  20. //在PHP中字符串的长度,是直接体现在其结构体中,所以调用strlen(); 速度非常快,时间复杂度为0(1)
  21.  
  22. echo strlen($b);

符号表

符号表symbol_table,变量的花名册

符号表是什么?

符号表示一张哈希表(哈希结构理解成关联数组)
里面存储了变量名-> 变量zval结构体的地址

  1. struct _zend_executor_globals {
  2. ...
  3. ...
  4. HashTable * active_symbol_table /* 活动符号表 */
  5. HashTable symbol_table /* 全局符号表 */
  6. HashTable included_files; /* files already included */
  7. }
  1. // 变量花名册
  2. $a = 3;
  3. $b = 1.223;
  4. $c = 'hello';
  5.  
  6. /**
  7.  *
  8.  * 生成了3个结构体
  9.  * 同时,全局符号表,中多了三条记录
  10.  *
  11.  * a ---> 0x123 ---> 结构体 { 3 }
  12.  * b ---> 0x21a ---> 结构体 { 1.223 }
  13.  * c ---> 0x1A0 ---> 结构体 { hello }
  14.  *
  15.  */
  16.  
  17. // 变量声明
  18. // 第一:结构体生成
  19. // 第二:符号表中多了记录,变量的花名册
  20. // 第三:指向结构体

传值赋值

传值赋值发生了什么

在传值赋值时:
以:$a = 3; $b = $a;为例:
并没有再次产生结构体,而是2个变量共用1个结构体
此时,2个变量,指向同1个结构体
refcount_gc 值为 2 (如果没有指针指引,会有垃圾回收机制清除)

写时复制

cow写时复制特性

  1. $a = 3;
  2. $b = $a;
  3.  
  4. /**
  5.  *
  6.  * 是否产生了2 个结构体?
  7.  * 不是,共用1个, refcount_gc = 2;
  8.  *
  9.  */
  10.  
  11. $b = 5;
  12.  
  13. echo $a, $b; // 3, 5
  14. // $a,$b 指向同一个结构体,那么,修改$b或$a,对方会不会受干扰 ? 没有干扰到对方。具有写时复制的特性

如果有一方修改,将会造成结构体的分裂

结构体一开始共用,到某一方要修改值时,才分裂。这种特性称为:COW 。Copy On Write。

引用赋值

引用赋值发生了什么

当引用赋值时,双方共用一个结构体(is_ref_gc=1)

关系图例展示:

强制分裂

  1. <?php
  2.  
  3. // 强制分裂
  4.  
  5. $a = 3;
  6. /**
  7.  * {
  8.  * value: 3;
  9.  * type: IS_LONG;
  10.  * refcount_gc: 1;
  11.  * is_ref_gc: 0;
  12.  * }
  13.  */
  14. $b = $a;
  15. /**
  16.  * {
  17.  * value: 3;
  18.  * type: IS_LONG;
  19.  * refcount_gc: 2;
  20.  * is_ref_gc: 0;
  21.  * }
  22.  */
  23. $c = &$a;
  24. // 不会按照 底下结构体变化
  25. /**
  26.  * {
  27.  * value: 3;
  28.  * type: IS_LONG;
  29.  * refcount_gc: 3;
  30.  * is_ref_gc: 1;
  31.  * }
  32.  */
  33.  
  34. // 正确的结构体变化
  35. // 如果is_ref_gc 0->1 的过程中(从0到1,表示想引用变量)。refcount_gc>1。多个变量共享一个变量值。将会产生强制分裂
  36. /**
  37.  * // $a $c 结构体
  38.  * {
  39.  * value: 3;
  40.  * type: IS_LONG;
  41.  * refcount_gc: 2;
  42.  * is_ref_gc: 1;
  43.  * }
  44.  *
  45.  * // $b 结构体
  46.  * {
  47.  * value: 3;
  48.  * type: IS_LONG;
  49.  * refcount_gc: 1;
  50.  * is_ref_gc: 0;
  51.  * }
  52.  *
  53.  */
  54.  
  55. $c = 5;
  56. // a c
  57. /**
  58.  * value: 5
  59.  * type: IS_LONG;
  60.  * refcount_gc: 2;
  61.  * is_ref_gc: 1;
  62.  */
  63.  
  64. // b
  65. /**
  66.  * value: 3
  67.  * type: IS_LONG;
  68.  * refcount_gc: 1;
  69.  * is_ref_gc: 0;
  70.  */
  71.  
  72. echo $a, $b, $c; // 5 , 3 , 5

引用数组时的一些奇怪现象

  1. // 引用数组时的怪现象
  2.  
  3. $arr = array(0, 1, 2, 3);
  4.  
  5. $tmp = $arr;
  6.  
  7. $arr[1] = 11;
  8.  
  9. echo $tmp[1]; // 1
  10.  
  11. // 数组不会比较细致的检查,多维数组存在。 因此,判断的时候,只会判断外面 一层的 结构体。

数组不会比较细致的检查

  1. // 先 引用 后 赋值
  2. $arr = array(0, 1, 2, 3);
  3.  
  4. $x = &$arr[1];
  5.  
  6. $tmp = $arr;
  7.  
  8. $arr[1] = 999;
  9.  
  10. echo $tmp[1]; // 999 . hash表中的zvalue结构体中会变成引用类型。 // 只去关注外面一层结构体,而不去关注 hash表中的值。
  11.  
  12.  
  13. echo '<br/>';
  14.  
  15. // 先赋值,后引用
  16. $arr = array(0, 1, 2, 3);
  17.  
  18. $tmp = $arr;
  19.  
  20. $x = &$arr[1];
  21.  
  22. $arr[1] = 999;
  23.  
  24. echo $tmp[1]; // 1

循环数组

循环数组时的怪现象

  1. // 循环数组时的怪现象
  2. $arr = array(0, 1, 2, 3);
  3.  
  4. foreach ( $arr as $v ) {
  5.  
  6. }
  7.  
  8. var_dump(current($arr)); // 数组指针停留在数组结尾处, 取不到值. false
  9.  
  10. echo '<br/>';
  11.  
  12. $arr = array(0, 1, 2, 3);
  13.  
  14. foreach ( $arr as $val=>$key ) { // foreach 使用的 $arr 是 $arr的副本.
  15. $arr[$key] = $val; // 修改之后,就会产生分裂。 foreach 遍历的是 $arr 的副本。 但是原数组的指针已经走了一步.
  16. }
  17.  
  18. var_dump(current($arr)); // 1
  1. $arr = array('a', 'b', 'c', 'd');
  2.  
  3. foreach ( $arr as &$val ) { // 该foreach 会导致 $val = &$arr[3];
  4.  
  5. }
  6.  
  7. foreach ( $arr as $val ) {
  8. print_r($arr);
  9. echo '<br/>';
  10. }
  11. // 两个问题:
  12. // 数组使用时,要慎用引用。
  13. // foreach 使用后,不会把数组的内部指针重置, 使用数组时,不要假想内部指针指向数组头部. 也可以在foreach 之后 reset(); 指针。

符号表与作用域

当执行到函数时,会生成函数的“执行环境结构体”,包含函数名,参数,执行步骤,所在的类(如果是方法),以及为这个函数生成一个符号表。
符号表统一放在栈上,并把active_symbol_table指向刚产生的符号表。

  1. // Zend/zend_compiles.h 文件中
  2.  
  3. // 源码:
  4. struct _zend_execute_data {
  5. struct _zend_op *opline;
  6. zend_function_state function_state;
  7. zend_op_array *op_array;
  8. zval *object;
  9. HashTable *symbol_table;
  10. struct _zend_execute_data *prev_execute_data;
  11. zval *old_error_reporting;
  12. zend_bool nested;
  13. zval **original_return_value;
  14. zend_class_entry *current_scope;
  15. zend_class_entry *current_called_scope;
  16. zval *current_this;
  17. struct _zend_op *fast_ret; /* used by FAST_CALL/FAST_RET (finally keyword) */
  18. zval *delayed_exception;
  19. call_slot *call_slots;
  20. call_slot *call;
  21. };
  1. // 简化:
  2.  
  3. struct _zend_execute_data {
  4. ...
  5. zend_op_array *op_array; // 函数的执行步骤. 如果是函数调用。是函数调用的后的opcode
  6. HashTable *symbol_table; // 此函数的符号表地址
  7. zend_class_entry *current_scope; // 执行当前作用域
  8. zval * current_this; // 对象 调用 this绑定
  9. zval * current_object; // object 的指向
  10. ...
  11. }

一个函数调用多次,会有多少个*op_array ?
一个函数产生 一个*op_array. 调用多次,会产生多个 环境结构体, 会依次入栈,然后顺序执行。
调用多少次,就会入栈多少次。不同的执行环境,靠 唯一的 *op_array 来执行。

函数什么时候调用, 函数编译后的 opcode 什么时候执行。

  1. $age = 23;
  2.  
  3. function t() {
  4. $age = 3;
  5. echo $age;
  6. }
  7.  
  8. t();
  9.  
  10. /**
  11.  * t 函数 在执行时,根据函数的参数,局部变量等,生成一个执行环境结构体。
  12.  * 结构体 入栈,函数编译后的 opcode, 称为 op_array (就是执行逻辑)。开始执行, 以入栈的环境结构体为环境来执行。
  13.  * 并生成此函数的 符号表, 函数寻找变量, 就在符号表中寻找。即局部变量。(一个环境结构体,就对应一张符号表)
  14.  *
  15.  *
  16.  * 注意: 函数可能调用多次。栈中可能有某函数的多个执行环境 入栈。但是 op_array 只有一个。
  17.  *
  18.  */

静态变量

静态变量的实现

  1. // Zend/zend_compile.h
  2. struct _zend_op_array {
  3. /* Common elements */
  4. zend_uchar type;
  5. const char *function_name;
  6. zend_class_entry *scope;
  7. zend_uint fn_flags;
  8. union _zend_function *prototype;
  9. zend_uint num_args;
  10. zend_uint required_num_args;
  11. zend_arg_info *arg_info;
  12. /* END of common elements */
  13.  
  14. zend_uint *refcount;
  15.  
  16. zend_op *opcodes;
  17. zend_uint last;
  18.  
  19. zend_compiled_variable *vars;
  20. int last_var;
  21.  
  22. zend_uint T;
  23.  
  24. zend_uint nested_calls;
  25. zend_uint used_stack;
  26.  
  27. zend_brk_cont_element *brk_cont_array;
  28. int last_brk_cont;
  29.  
  30. zend_try_catch_element *try_catch_array;
  31. int last_try_catch;
  32. zend_bool has_finally_block;
  33.  
  34. /* static variables support */
  35. HashTable *static_variables;
  36.  
  37. zend_uint this_var;
  38.  
  39. const char *filename;
  40. zend_uint line_start;
  41. zend_uint line_end;
  42. const char *doc_comment;
  43. zend_uint doc_comment_len;
  44. zend_uint early_binding; /* the linked list of delayed declarations */
  45.  
  46. zend_literal *literals;
  47. int last_literal;
  48.  
  49. void **run_time_cache;
  50. int last_cache_slot;
  51.  
  52. void *reserved[ZEND_MAX_RESERVED_RESOURCES];
  53. };
  1. // 简化
  2. struct _zend_op_array {
  3. ...
  4. HashTable *static_variables; // 静态变量
  5. ...
  6. }

编译后的 op_array 只有一份。 静态变量并没有存储在符号表(symbol_table)中.而是存放在op_array中。

  1. function t() {
  2.  
  3. static $age = 1;
  4.  
  5. return $age += 1;
  6.  
  7. }
  8.  
  9. echo t();
  10. echo t();
  11. echo t();
  12.  
  13. // 静态变量 不再和 执行的结构体, 也不再和 入栈的符号表有关。

常量

  1. // Zend/zend_constants.h
  2. // 常量结构体
  3. typedef struct _zend_constant {
  4. zval value; // 变量结构体
  5. int flags; // 标志,是否大小写敏感等
  6. char *name; // 常量名
  7. uint name_len; //
  8. int module_number; // 模块名
  9. } zend_constant;

define函数的实现

define函数当然是 调用zend_register_constant声明的常量
具体如下:Zend/zend_builtin_functions.c

// 源码:

  1. ZEND_FUNCTION(define)
  2. {
  3. char *name;
  4. int name_len;
  5. zval *val;
  6. zval *val_free = NULL;
  7. zend_bool non_cs = 0;
  8. int case_sensitive = CONST_CS;
  9. zend_constant c;
  10.  
  11. if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sz|b", &name, &name_len, &val, &non_cs) == FAILURE) {
  12. return;
  13. }
  14.  
  15. if(non_cs) {
  16. case_sensitive = 0;
  17. }
  18.  
  19. /* class constant, check if there is name and make sure class is valid & exists */
  20. if (zend_memnstr(name, "::", sizeof("::") - 1, name + name_len)) {
  21. zend_error(E_WARNING, "Class constants cannot be defined or redefined");
  22. RETURN_FALSE;
  23. }
  24.  
  25. repeat:
  26. switch (Z_TYPE_P(val)) {
  27. case IS_LONG:
  28. case IS_DOUBLE:
  29. case IS_STRING:
  30. case IS_BOOL:
  31. case IS_RESOURCE:
  32. case IS_NULL:
  33. break;
  34. case IS_OBJECT:
  35. if (!val_free) {
  36. if (Z_OBJ_HT_P(val)->get) {
  37. val_free = val = Z_OBJ_HT_P(val)->get(val TSRMLS_CC);
  38. goto repeat;
  39. } else if (Z_OBJ_HT_P(val)->cast_object) {
  40. ALLOC_INIT_ZVAL(val_free);
  41. if (Z_OBJ_HT_P(val)->cast_object(val, val_free, IS_STRING TSRMLS_CC) == SUCCESS) {
  42. val = val_free;
  43. break;
  44. }
  45. }
  46. }
  47. /* no break */
  48. default:
  49. zend_error(E_WARNING,"Constants may only evaluate to scalar values");
  50. if (val_free) {
  51. zval_ptr_dtor(&val_free);
  52. }
  53. RETURN_FALSE;
  54. }
  55.  
  56. c.value = *val;
  57. zval_copy_ctor(&c.value);
  58. if (val_free) {
  59. zval_ptr_dtor(&val_free);
  60. }
  61. c.flags = case_sensitive; /* non persistent */
  62. c.name = str_strndup(name, name_len);
  63. if(c.name == NULL) {
  64. RETURN_FALSE;
  65. }
  66. c.name_len = name_len+1;
  67. c.module_number = PHP_USER_CONSTANT;
  68. if (zend_register_constant(&c TSRMLS_CC) == SUCCESS) {
  69. RETURN_TRUE;
  70. } else {
  71. RETURN_FALSE;
  72. }
  73. }
  1. // 关键代码:
  2.  
  3. c.value = *val;
  4. zval_copy_ctor(&c.value);
  5. if (val_free) {
  6. zval_ptr_dtor(&val_free);
  7. }
  8. c.flags = case_sensitive; /* 大小写敏感 */
  9. c.name = str_strndup(name, name_len);
  10. if(c.name == NULL) {
  11. RETURN_FALSE;
  12. }
  13. c.name_len = name_len+1;
  14. c.module_number = PHP_USER_CONSTANT; /* 用户定义常量 */
  15. if (zend_register_constant(&c TSRMLS_CC) == SUCCESS) {
  16. RETURN_TRUE;
  17. } else {
  18. RETURN_FALSE;
  19. }

常量就一个符号(哈希)表. 都使用一个符号表。所以全局有效。

常量的生成

  1. int zend_register_constant(zend_constant *c TSRMLS_DC) {
  2. ...
  3. ...
  4. zend_hash_add(EG(zend_constants), name, c->name_len, (vaid*)c,sizeof(zend_constant, NULL) == FAILURE);
  5. ...
  6. ...
  7. }

对象定义常量

  1. class Dog {
  2.  
  3. public $name = 'kitty';
  4.  
  5. public function __toString () {
  6. return $this->name;
  7. }
  8.  
  9. }
  10.  
  11. $dog = new Dog();
  12.  
  13.  
  14. define('DOG', $dog);
  15.  
  16. print_r(DOG);
  17.  
  18. /**
  19.  * define 值为对象时,会把对象装成标量来存储,需要类有 __toString魔术方法
  20.  */

对象

对象的底层实现

Zend/zend.h

  1. struct _zval_struct {
  2. /* Variable information */
  3. zvalue_value value; /* value */
  4. zend_uint refcount__gc;
  5. zend_uchar type; /* active type */
  6. zend_uchar is_ref__gc;
  7. };
  8.  
  9. // zvalue
  10. typedef union _zvalue_value {
  11. long lval; /* long value */
  12. double dval; /* double value */
  13. struct {
  14. char *val;
  15. int len;
  16. } str;
  17. HashTable *ht; /* hash table value */
  18. zend_object_value obj;
  19. zend_ast *ast;
  20. } zvalue_value;
  21.  
  22. // 在 zend.h 中 查看到 `zend_object_value obj;` 是以zend_object_value 定义. 在Zend/zend_types.h 文件中继续查看
  23.  
  24. // Zend/zend_types.h

定义zend_object_value 结构体

  1. typedef struct _zend_object_value {
  2. zend_object_handle handle;
  3. const zend_object_handlers *handlers;
  4. } zend_object_value;

通过new出来的对象,返回的是什么。是zend_object_value. 并不是真正的对象,而是对象的指针。

返回的 handle再次指向对象。

每次new一个对象,对象就存入一张hash表中。(形象的称之为对象池)

对象存储时的特点:

  1. // 对象
  2.  
  3. class Dog {
  4. public $leg = 4;
  5. public $wei = 20;
  6. }
  7.  
  8. $dog = new Dog();
  9.  
  10. // $dog 是一个对象么?
  11. // 严格说,并不是对象.
  12. /**
  13.  * {
  14.  * handle --指向--> [hash表 {leg: 4, wei: 20}] // hash表中存在 对象
  15.  * }
  16.  */
  17.  
  18. $d2 = $dog;
  19.  
  20. $d2->leg = 5;
  21.  
  22. echo $dog->leg, '`', $d2->leg; // 5`5
  23.  
  24. // 对象并不是 引用赋值. 主要原因 zval 结构体 是再次指向一个hash表中的 对象池
  25. $d2 = false;
  26.  
  27. echo $dog->leg; // 5

内存分层

内存管理与垃圾回收

PHP封装了对系统内存的请求
不要直接使用malloc直接请求内存

PHP函数需要内存的时候,是通过emalloc,efree.
emalloc,efree向 mm_heap索要空间。

zend 中底层都离不开hash表。PHP中的HashTable太强大。

PHP 底层 所有的变量都是 放在 zend_mm_heap 中。 然后通过 各自的hash表来指向或跟踪。

zend虚拟机的运行原理

  • PHP语法实现
    Zend/zend_language_scanner.l
    Zend/zend_language_parser.y
  • OPcode编译
    Zend/zend.compile.c
  • 执行引擎
    Zend/zend_vm_*
    Zend/zend_execute.c

以apache模块运行时的流程

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Go