使用VS Code调试PHP7的源码

我是PHP的爱好者,大一下半年开始接触PHP到现在,已经有了几个年头,但是我不敢说自己“精通PHP”,因为我连PHP内核源码都还没看全,现在顶多停留在“熟悉”的层面。

我2018年买了一本《PHP7的内核剖析》,但是只是看了一些罢了。因为里面很多原理不太明白。如果真的要读懂一本书,尤其是技术相关,一定要手把手操作才能真正的理解!!!

但是自从装了几次PHP源码失败之后,就没有再尝试下去的决心了。书,也就仍在了一边,没有再看。

但是上次一个朋友分享了一篇文章《程序员装逼被怼,决定用面试证明自己,结果…… 》,还是多少有一些小触动的。

所以,就再次拿起来书本,开始“啃骨头”。

1.安装PHP7源码

源码下载容易安装难!

自从上次几次失败之后,也多少总结一点小经验。

$ ./configure --prefix=/usr/local/php7 --enable-debug --enable-fpm #生成Makefile

出现了报错

checking for libiconv... no
configure: error: Please specify the install prefix of iconv with --with-iconv=<DIR>
$ brew install libiconv
$ ./configure --prefix=/usr/local/php7 --enable-debug --enable-fpm --with-iconv=/usr/local/opt/libiconv
...
Generating files
configure: creating ./config.status
creating main/internal_functions.c
creating main/internal_functions_cli.c
+--------------------------------------------------------------------+
| License:                                                           |
| This software is subject to the PHP License, available in this     |
| distribution in the file LICENSE.  By continuing this installation |
| process, you are bound by the terms of this license agreement.     |
| If you do not agree with the terms of this license, you must abort |
| the installation process at this point.                            |
+--------------------------------------------------------------------+

Thank you for using PHP.

config.status: creating php7.spec
config.status: creating main/build-defs.h
config.status: creating scripts/phpize
config.status: creating scripts/man1/phpize.1
config.status: creating scripts/php-config
config.status: creating scripts/man1/php-config.1
config.status: creating sapi/cli/php.1
config.status: creating sapi/fpm/php-fpm.conf
config.status: creating sapi/fpm/www.conf
config.status: creating sapi/fpm/init.d.php-fpm
config.status: creating sapi/fpm/php-fpm.service
config.status: creating sapi/fpm/php-fpm.8
config.status: creating sapi/fpm/status.html
config.status: creating sapi/cgi/php-cgi.1
config.status: creating ext/phar/phar.1
config.status: creating ext/phar/phar.phar.1
config.status: creating main/php_config.h
config.status: executing default commands
$ make #编译
$ make install #安装

2. 配置lnmp

配置lnmp的步骤就不再多说了

3. 更改www.conf

安装源码之后的bin文件,会都保存到/usr/local/php7文件夹下面,PHP-fpm的配置文件也是一样。

$ ll
total 472
-rwxrwxrwx  1 root  wheel   1271 Feb 21 22:44 pear.conf
-rwxrwxrwx  1 root  wheel   4465 Feb 21 23:25 php-fpm.conf
-rwxrwxrwx  1 root  wheel   4465 Feb 21 22:44 php-fpm.conf.default
drwxrwxrwx  4 root  wheel    128 Feb 24 00:27 php-fpm.d
-rwxrwxrwx@ 1 root  wheel  69724 Feb 22 08:07 php.ini
-rwxrwxrwx@ 1 root  wheel  69692 Feb 22 08:06 php.ini-development
-rwxrwxrwx@ 1 root  wheel  69724 Feb 22 08:06 php.ini-production

我们是想要通过web的方式,调试源码的运行过程。

大家知道,PHP-fpm是基于多进程的,我们在使用VS Code进行调试的时候,会需要选择挂起的进程号(下面会后响应的步骤说明)。所以,我们最好能够只有一个php-fpm的子进程,这样,就能确保选择的进程,能正常停顿到断点位置。

....

;pm = dynamic
pm = static

.....

pm.max_children = 1

4. 配置调试文件

添加配置,使用attach的方式

{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [{
            "name": "(lldb) Attach",
            "type": "cppdbg",
            "request": "attach",
            "program": "/usr/local/php7/sbin/php-fpm",
            "processId": "${command:pickProcess}",
            "MIMode": "lldb"
        }
    ]
}

5. 运行测试

我就以数组的PHP代码文件为例,进行测试。

在nginx的项目目录里面创建一个测试文件。比如 a.php

<?php
$arr = [1,2,3,4,5];
array_push($arr, 10, 20);
var_dump($arr);

我们在源码文件 ext/standard/array.c里面加个断点

开始进行调试

我们再选择进程号的时候,需要选择大的进程号,因为小进程号主要是php-fpm主进程,主进程是用来管理子进程的,子进程才是正在进行执行代码的进程

然后访问a.php文件

http://localhost/a.php

侧边栏显示了调用的堆栈信息,以及传入的参数信息。

PHP7内存管理之垃圾回收

回收过程

在自动GC机制中,在zval断开value指向的时候如果发现refcount=0的时候,则会直接释放value,这就是自动回收GC的过程。发生断开的两种情况为修改变量与函数返回的时候,修改变量的时候,会断开原有的value指向,函数返回的时候,则会释放局部变量,也就是把所有局部变量的refcount计数-1。
此外,当使用unset函数的时候,也会主动销毁这个变量。

垃圾回收

虽然有了自动GC机制,但是有一种情况是没办法解决的,那就是因为变量因为循环引用而无法回收造成的内存泄露,这种情况通常是循环引用。简单来讲,循环引用就是引用自身,这种情况一般只会发生在数组或者对象的身上。比如定义了$a = array() ,插入一个新元素,这个元素对数组自身进行引用$a[] = &$a,当所有的外部引用都断开了,但是数据的refcount仍然大于0而得不到释放,但是事实上,这个变量没有在使用的价值了。

<?php
$a = array();
$a[] = &$a;
unset($a);
?>

在unset之前,变量a是有两次引用的,一个来自$a,一个来自$a[1]

unset($a)之后,减少了一次引用的recount,这个时候,已经没有了外部的引用,但是还有一个内部还有一个元素指向该引用。

像这种因为循环指向没办法释放的变量称之为垃圾。PHP引入了另外的一种机制来进行垃圾回收。
– 如果一个变量的value的refcount减少到0,说明这个value可以释放,那么这就不属于垃圾
– 如果一个变量的value减少之后大于0,那么这个value还不能被释放,那么这个value就是垃圾。
所以,判断一个变量是不是垃圾,要看value的refcount是否减少到了0。

目前垃圾回收只会出现在array和object两种类型中,当一个value被视为垃圾的时候,PHP会将这个value收集起来,等到打到了规定的数量,启动垃圾回收机制,进行统一的释放。

回收的时机

前面说了,PHP垃圾回收并不是产生一个垃圾value,就进行释放,而是把value收集起来统一释放,以为value的分析和释放,也是有性能消耗的。
在php.ini中,zend.enable_gc用来设置是否启动垃圾回收机制。绝大多数都是默认开启的,因为每个都有可能在写程序的时候,出现内存垃圾,如果把这个配置关闭了,那么就有可能造成所谓的垃圾泄露。
除了zend.enable_gc以为,还会配合zend/zend_gc.c里面的变量GC_ROOT_BUFFER_MAX_ENTRIES实现垃圾回收,默认GC_ROOT_BUFFER_MAX_ENTRIES的值是10001,GC_ROOT_BUFFER_MAX_ENTRIES[0]是用来保存一些header的数据,GC_ROOT_BUFFER_MAX_ENTRIES[1]~GC_ROOT_BUFFER_MAX_ENTRIES[10000]用来收集垃圾的数据。如果你想强制执行垃圾回收,也可使用函数gc_collect_cycles()实现。

参考文献

  • PHP7内核剖析
  • PHP手册

PHP7内存管理之写时复制

其实PHP的内存管理是包含引用计数和写时复制两部分,这篇文章主要是介绍写时复制。

简要介绍

其实写时复制在计算机中有很多应用,它只在必要的时候才会进行深拷贝,也就是把保存的值连同内存一块拷贝一份,可以很好的节省效率。比如,Linux在fork子进程的时候,不会立刻复制父进程的地址空间,而是和父进程共享一个地址空间,只有在必要写入的时候,才会复制地址空间,和父进程进行分离。简单来讲,资源的复制是只有需要写入的时候,再回进行,再次之前,都是以只读的方式进行共享。

PHP的写时复制

PHP的写时复制原理是一样的。当变量要修改value的结构的时候,这个时候,就会对之前共享的内存资源进行复制一份进行修改,同事断开原来的指向,指向复制后的内存地址。
举个例子:

<?php
$a = array(1, 2);
$b = $a;
$c = $b;
echo xdebug_debug_zval('a');
?>

PHP7
a: (refcount=3, is_ref=0)=array (0 => (refcount=0, is_ref=0)=1, 1 => (refcount=0, is_ref=0)=2)

<?php
$a = array(1, 2);
$b = $a;
$c = $b;
//进行分离
$c[] = 3;
echo xdebug_debug_zval('a');
?>

PHP7
a: (refcount=2, is_ref=0)=array (0 => (refcount=0, is_ref=0)=1, 1 => (refcount=0, is_ref=0)=2)

运行结果很明显,当变量c新插入了一个元素,对那么就没有在继续引用变量a,而是独立复制了一份。

然而并不是所有的类型都是支持写时复制,比如对象、资源就无法进行复制,也就是无法进行分离,如果多个变量指向同一个对象,当其中一个变量修改对象的时候,其修改将会反应到所有对象上面。事实上只有string和array两种支持分离。
举个例子:

<?php
class test {
        public $c = 123;
}

$a = new test();
$b = $a;
$c = $b;
echo xdebug_debug_zval('a');
$c->c = 456;
echo $a->c;
echo "\n";
echo xdebug_debug_zval('a');
?>

PHP7
a: (refcount=3, is_ref=0)=class test { public $c = (refcount=0, is_ref=0)=123 }
456
a: (refcount=3, is_ref=0)=class test { public $c = (refcount=0, is_ref=0)=456 }

同样,变量a实例化了一个新的对象,然后依次进行赋值给其他变量,使用xdebug_debug_zval的时候,打印出来了变量a的3次引用计数,然后对变量c进行赋值,咦?居然发现变量a的引用计数没有变化,所以object的类型是不支持写时复制的。

支持复制的value类型:

type copyable
simple types N
string Y
interned string N
array Y
immutable array N
object N
resource N
reference N

参考文献

《PHP7内核剖析》

PHP7内存管理之引用计数

C/C++的内存管理

C/C++想要在堆上面分配内存,需要手动进行内存的分配和释放,变量管理非常的麻烦和繁琐,稍有不慎,就可能会造成内存上的错误使用。现在的一些高级语言,都普遍实行自动GC机制。

自己的意淫

我们自己先思考下实行自动GC的方法,当我们定义一个变量的时候,给变量分配一块内存,用于保存zval和value的值,等到函数返回的时候,再讲内存回收。如果将变量赋值给其他变量的时候,再进行内存的复制,变量之间相互独立,互不影响。

PHP的内存管理

PHP的内存管理肯定不会像是我们想象的那么简单,如果那么简单,那该要浪费多少内存。PHP的内存管理是采用:引用计数+写时复制 的方法。

引用计数

引用计数是指会有多少个zval指向同一个zend_value。当把变量赋值给一个新的变量的时候,引用计数就会+1。PHP7是将引用计数保存到了zval的结构中。

//php7
typedef union _zend_value {
    zend_long         lval;             /* long value */
    double            dval;             /* double value */
    zend_refcounted  *counted;  // 引用计数
    zend_string      *str;
    zend_array       *arr;
    zend_object      *obj;
    zend_resource    *res;
    zend_reference   *ref;
    zend_ast_ref     *ast;
    zval             *zv;
    void             *ptr;
    zend_class_entry *ce;
    zend_function    *func;
    struct {
        uint32_t w1;
        uint32_t w2;
    } ww;
} zend_value;

PHP5是把引用计数放到了zend_struct里面

struct _zval_struct {
    /* Variable information */
    zvalue_value value;     /* value */
    zend_uint refcount__gc; //引用计数
    zend_uchar type;    /* active type */
    zend_uchar is_ref__gc;
};

PHP5不是这篇文章的重点,暂且不说。

下面我们看下PHP7 zend_refcounted具体的结构

typedef struct _zend_refcounted_h {
    uint32_t         refcount;          /* reference counter 32-bit */
    union {
        struct {
            ZEND_ENDIAN_LOHI_3(
                zend_uchar    type,
                zend_uchar    flags,    /* used for strings & objects */
                uint16_t      gc_info)  /* keeps GC root number (or 0) and color */
        } v;
        uint32_t type_info;
    } u;
} zend_refcounted_h;

struct _zend_refcounted {
    zend_refcounted_h gc;
};

很明显,refcount字段使用了进行计数操作的。
举个例子来看看:

<?php
$a = array();
echo xdebug_debug_zval('a');
$b = $a;
echo xdebug_debug_zval('a');
$c = $b;
echo xdebug_debug_zval('a');
unset($c);
echo xdebug_debug_zval('a');
?>

运行结果如下:
a: (refcount=1, is_ref=0)=array ()
a: (refcount=2, is_ref=0)=array ()
a: (refcount=3, is_ref=0)=array ()
a: (refcount=2, is_ref=0)=array ()

就像代码运行结果一样,首先定义了一个变量a,引用给数组分配了一块空间,引用计数为1,然后把a赋值给变量b,引用计数+1,然后赋值给变量c,继续+1,然后把变量c释放,引用计数-1。

但是,并不是所有的变量都会使用引用计数。比如整形,浮点型,布尔型,NULL,他们的值是直接保存在zval中,所以他们的引用计数是0。这个也是PHP5和PHP7的一个不同点。

举例说明:

<?php
$a = 123;
echo xdebug_debug_zval('a');
$b = $a;
echo xdebug_debug_zval('a');
?>

PHP5
a: (refcount=1, is_ref=0)=123
a: (refcount=2, is_ref=0)=123

PHP7
a: (refcount=0, is_ref=0)=123
a: (refcount=0, is_ref=0)=123

特殊情况

在PHP7中还有两种特殊的情况

举例说明:

<?php
$a = "hi";
$b = $a;
$c = $a;
xdebug_debug_zval('a');
?>

PHP7
a: (refcount=0, is_ref=0)=’hi’

<?php
$a = "hi".time();
$b = $a;
$c = $a;
xdebug_debug_zval('a');
?>

PHP7
a: (refcount=3, is_ref=0)=’hi1516202718′

wtf,为什么这两个是不一样的?这就是另外的特殊情况了。

  • 在PHP中,函数名、类名、变量名、静态字符串等这种类型,比如第一个例子$a = "hi",后面的字符串是唯一不变的,这等同于C语言中的char *a = "hi",这些字符串是整个请求周期,请求结束后,同意销毁,自然不用引用计数来进行管理。
  • 不可变数组,这是opcache的一种优化类型,这里不做详细说明。

总结

引用计数算是PHP7和PHP5的一个重要的变更,这个也是内存的一个优化的地方。
在PHP5中,引用计数是在zval中,而不是在zend_value中,这样一来,导致变量复制的时候要复制两个结构(zval和zend_value),PHP7将引用计数放到zend_value中,这样就可以进行公用,设计也更加合理。

参考文献

  • 《PHP7内核剖析》

PHP源码分析之cli模式执行的过程

众所周知,PHP在web上应用很广泛。接近80%的web网站都是使用PHP+MySQL,虽然越来越多的新语种崛起,但是现在PHP依然是中小型web系统的首选。PHP除了在web上有很多应用,也经常被用作脚本工具,虽然没有原生shell效率高,但是起点比较低。今天就和大家分享下PHP cli模式的执行过程。

Read More »