PHP底层原理和细节分析

目录

关于php-fpm

PHP变量的底层实现

PHP的垃圾回收机制

PHP中的OPcache

PHP的反射机制

PHP的Generator特性

PHP的trait细节

PHP的SPL

PHP中使用pcntl函数创建子进程


关于php-fpm

cgi协议(通用网关接口):它允许 web 服务器通过特定的协议与应用程序通信,调用原理如下:

用户请求 -> Web 服务器接收请求 -> fork 子进程 调用程序 / 执行程序 -> 程序返回内容 / 程序调用结束 -> Web 服务器接收内容 -> 返回给用户。

fast-cgi:允许在一个进程内处理多个请求,而不是一个请求处理完毕就直接结束进程,性能上有了很大的提高。调用过程如下:

Web 服务器 Fast-CGI 进程管理器初始化 -> 预先 fork 多个进程用户请求 -> Web 服务器接收请求 ->Web 服务器将请求交给 Fast-CGI 进程管理器 ->Fast-CGI 进程管理器接收,给其中一个空闲的的 Fast-CGI 进程处理 -> 处理完成 Fast-CGI 进程变为空闲状态,等待下次请求 ->Web 服务器接收内容 -> 返回给用户。

FPM:FastCGI Process Manager,是 FastCGI 的实现,任何实现了 FastCGI 协议的 Web Server 都能够与之通信。

php-fpm:php FastCGI Process Manager,是一个 PHP 进程管理器,包含 master 进程和 worker 进程两种进程:master 进程只有一个,负责监听端口,接收来自 Web Server 的请求,而 worker 进程则一般有多个 (具体数量根据实际需要配置),每个进程内部都嵌入了一个 PHP 解释器,是 PHP 代码真正执行的进程,如下图,1个 master 进程,3个 worker 进程:

 Nginx 提供了 fastcgi 模块来将 http 请求映射为对应的 fastcgi 请求,Nginx 的 fastcgi 模块提供了 fastcgi_param 指令来主要处理这些映射关系,fastcgi_pass 指令用于指定 fpm 进程监听的地址。配置nginx.conf 支持php-fpm,当请求php文件的时候,反向代理到php-fpm:

location ~ \.php$ {       
    include /usr/local/etc/nginx/fastcgi.conf; #加载nginx的fastcgi模块
    fastcgi_intercept_errors on;       
    fastcgi_pass   127.0.0.1:9000; #nginx fastcgi进程监听的IP地址和端口
}

nginx与php-fpm的结合,完整的流程是这样的:

www.example.com
→ 请求到达 nginx
→ 路由到www.example.com/index.php
→ 加载nginx的fast-cgi模块
→ fast-cgi监听127.0.0.1:9000地址
→ www.example.com/index.php 请求到达 127.0.0.1:9000
→ php-fpm 监听127.0.0.1:9000
→ php-fpm接收到请求,启用worker进程处理请求
→ php-fpm处理完请求,返回给nginx
→ nginx将结果通过http返回给浏览器

PHP变量的底层实现

PHP是用C语言开发的,C语言是强类型的,而PHP是弱类型语言,那么底层是如何实现的?

解压 php-7.3.18 的源码包,进入Zend目录可以看到如下内容:


Zend目录是Zend虚拟的实现。包括栈、数据类型、编译器等,都在这里实现。

PHP变量是通过zval结构体来存储的,打开 zend_types.h 文件,找到 _zend_value 部分:

 上面对不同类型的变量的说明如下:

typedef union _zend_value {
        zend_long         lval; // 整型(长度8字节)
        double            dval; // 浮点型(长度8字节)    
        zend_refcounted  *counted; // 引用计数(长度8字节)
        zend_string      *str; // 字符串类型(长度8字节)
        zend_array       *arr; // 数组(长度8字节)
        zend_object      *obj; // 对象(长度8字节)
        zend_resource    *res; // 资源型(长度8字节)
        zend_reference   *ref; // 引用型(长度8字节)
        zend_ast_ref     *ast; //抽象语法树(长度8字节)
        zval             *zv; // zval类型(长度8字节)
        void             *ptr; // 指针类型(长度8字节)
        zend_class_entry *ce; // class类型(长度8字节)
        zend_function    *func; // function类型(长度8字节)
        struct {
                uint32_t w1;
                uint32_t w2;
        } ww;
} zend_value;

PHP脚本解释执行的机制:
1、php初始化执行环节,启动Zend引擎,加载注册的扩展模块
2、初始化后读取脚本文件,Zend引擎对脚本文件进行词法分析(lex),语法分析(bison),生成语法树
3、Zend 引擎编译语法树,生成opcode,
4、Zend 引擎执行opcode,返回执行结果

PHP的垃圾回收机制

PHP可以自动进行内存管理,清除不再需要的对象,主要使用了引用计数。每个对象都内含一个引用计数器,每个reference链接到对象,计数器加1,当reference离开生存空间或者被设为null,计数器减1,当某个引用计数器的对象为0时,PHP知道你将不再需要使用这个对象,释放其所占有的内存空间。

在zval结构体中定义了ref_count和is_ref , ref_count是引用计数 ,标识此zval被多少个变量引用 , 为0时会被销毁。is_ref标识是否使用的 &取地址符强制引用。

细节分析:

  • 每个变量会有refcount用来表示有 多少变量指向它,当它是0的时候那么这个变量就是垃圾;

  • 在PHP5.3 之前PHP都是通过判断 refcount 来清除垃圾对象的;

  • 在PHP5.3 之后, php又增加了算法来解决循环引用的问题,判断循环引用中refcount达到10000次就自动进行回收。

<?php

/**
 * PHP的垃圾回收机制
 */
class Test {
    public $name;
}

$start_time = microtime(true);
//循环实例化1000万次
for ($i = 0; $i < 10000000; $i++) {
    $t = new Test();
    $t->name = $t; //循环引用
    unset($t);
}
$end_time = microtime(true);
echo round(($end_time - $start_time), 4) . PHP_EOL;

运行上面的代码,几乎每次都是1.2秒左右

接下来,把上面循环引用部分注释掉,再看看运行时间

// $t->name = $t; //循环引用

可以看到,耗时几乎减少了一半!因此平时在写代码的时候,还是要尽量避免循环引用的使用,因为遇到循环引用的时候它是回收不了的,只有当触发 10000次 的条件才会集中回收,并且当回收的时候PHP是卡住的,是不会往下执行的,所以就会出现非常耗时的情况。

PHP中的OPcache

由于php是解释性语言,每次运行都会加载解析项目中的所有文件,优点是可以热更新,但同时也带来了缺点:增加CPU的负担。

PHP虽然是一个脚本语言,但不是靠解释器解释,而是 zend 虚拟机,屏蔽了操作系统的区别。php代码编译成opcode, 由zend虚拟机来执行opcode。

OPcache是通过将PHP脚本预编译的字节码存储到共享内存中来提升PHP的性能,存储预编译字节码的好处就是省去了每次加载和解析PHP脚本的开销。PHP 5.5及后续版本中已经绑定了OPcache扩展。官网文档:PHP: OPcache - Manual

使用下列推荐设置来获得较好的性能:

opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=4000
opcache.revalidate_freq=60
opcache.fast_shutdown=1
opcache.enable_cli=1 ; prior to PHP 7.2.0

php.ini 开启opcache:

opcache.enable=1
opcache.enable_cli=1
//打印phpinfo(),看到有ZEND OPcache就证明已经开启成功了

进一步分析从HTTP发起请求到php处理请求的流程:

http request ---> nginx(代理)----> php-fpm(master 进程,分配)----> php-fpm(worker处理 ) ----> php-cgi

其中php-cgi处理的步骤如下:

1.启动ZEND引擎,加载配置,载入module

2.初始化php脚本进行词法分析,语法分析,生成语法树

3.ZEND引擎编译语法树,生成可执行字节码

4.执行字节码,返回处理结果

opcache 就缓存了php脚本预编译的字节码,避免每次处理请求都重复执行 php-cgi 的1,2,3 步骤,这样可以使得php性能大大提高。

PHP的反射机制

在PHP运行时扩展分析程序,导出或提出关于类、方法、属性、参数的详细信息,这种动态获取和调用信息的功能称为反射API。反射完整的描述了一个类或者对象的原型,不仅可以用于类和对象,还可以用于函数、扩展模块、异常。需要注意,反射的消耗很大,能找到替代方案的情况下就不要滥用反射。

class A
{
    public function __construct(B $b)
    {
    }
}
class B
{
}

//获取类的反射信息(所有信息)
$reflector = new ReflectionClass('A');
//获取构造函数
$constructor = $reflector->getConstructor();
//获取构造函数参数
$dependencies = $constructor->getParameters();
//获取依赖的类名
foreach ($dependencies as $dependency){
    if(!is_null($dependency->getClass())){
        $classname = $dependency->getClass()->name;
        $p[] = new $classname();
    }
}
//从给出的参数创建一个新的类实例
$a = $reflector->newInstanceArgs($p);

使用class函数返回对象属性的关联数组

get_object_vars($obj);  //返回对象属性的关联数组
get_class($obj);  //获取对象属性列表所属的类
get_class_vars(get_class($obj));  //类属性
get_class_methods(get_class($obj));  //返回由类的方法名组成的数组

PHP的Generator特性

如果想搞清楚Generator(生成器),需要先了解Iterator接口。通常使用foreach对数组进行遍历,如果要对对象进行遍历,那么这个对象的类就必须实现 Iterator接口,并且实现Iterator接口所提供的5个方法:

Iterator extends Traversable {
    /* Methods */
    abstract public mixed current ( void )   //返回当前位置的元素
    abstract public scalar key ( void )      //返回当前元素对应的key
    abstract public void next ( void )       //移到指向下一个元素的位置
    abstract public void rewind ( void )     //倒回到指向第一个元素的位置
    abstract public boolean valid ( void )   //判断当前位置是否有效
}

Generator(生成器)是在 PHP 5.5 版本中添加的,它提供了一种简单的方法来遍历数据,而不需要在内存中构建数组。在PHP中使用协程实现协作式(非抢占式)多任务调度系统。它提供了一种方便的实现简单的Iterator(迭代器)的方式,使用Generator实现Iterator不需要创建一个类来继承Iterator接口。注意: 生成器只能在函数中使用。

<?php

function getRange1($max = 10) {
    $array = [];
    for ($i = 1; $i < $max; $i++) {
        $array[] = $i;
    }
    return $array;
}

function getRange2($max = 10) {
    for ($i = 1; $i < $max; $i++) {
        //只循环遍历值和 yield 输出。 yield 与返回值类似,因为它也是从函数返回一个值,
        //但唯一的区别是 yield 只会在需要时返回一个值,并且不会尝试将整个数据集保留在内存中。
        yield $i;
    }
}

//使用传统的for循环会报错:内存耗尽
//foreach (getRange1(PHP_INT_MAX) as $value) {
//    echo $value . PHP_EOL;
//}

//使用生成器不会报错
foreach (getRange2(PHP_INT_MAX) as $value) {
    echo $value . PHP_EOL;
}

【问】为什么要使用生成器?
【答】有时候可能需要解析一个庞大的数据集,比如一个很大的日志文件,也可能对一个大型数据库的结果集执行计算,此时不想让这些数据全部加载到内存中,应该尽可能的保存相应的内存状态。使用生成器,可以使用更少的内存尽可能快的处理数据。

PHP的trait细节

PHP中使用trait间接地实现了多继承,当前类和父类和trait中如果使用了同名方法,当前类优先,其次是trait类,最后才是继承的父类。也就是说 Trait的优先级:自身方法>trait的方法>继承的方法。如果多个trait出现同名方法会报错,解决方案:使用insteadof 或者 as

//方案1:使用insteadof
use Type1, Type2 {
    Type2::speak insteadof Type1; //如果使用这个,会把Type2的speak()方法输出
    // Type1::speak insteadof Type2; //如果使用这个,会把Type1的speak()方法输出
}

//方案2:使用 as
use Type1, Type2 {
    Type2::speak insteadof Type1;
    Type1::speak as speak_type1; //调用的时候,使用speak_type1() 方法
}

PHP的SPL

SPL的英文全称是Standard PHP Library,是指标准PHP类库;内容主要包括数据结构类、迭代器、异常类、SPL函数,还提供了一系列的接口。数据结构类主要包括栈,队,堆,数组等基本数据结构,php已经封装好了,如果你要做数据处理可以直接拿来用,很方便。
SPL函数里面有个很重要的函数 spl_autoload_register() 用来实现自动加载

function load1($className)
{
  require $className .'.php';
}
spl_autoload_register('load1');//将load1函数注册到自动加载队列中。

//需要注意的是,如果你同时使用spl_autoload_register和__autoload,__autoload会失效.
//spl_autoload_register是可以多次重复使用的,如果一个页面有多个,执行顺序是按照注册的顺序,一个一个往下找,如果找到了就停止

还有一个SplObserver,用这个内置的接口实现观察者模式很方便。参考:PHP设计模式之观察者模式_浮尘笔记的博客-CSDN博客

PHP中使用pcntl函数创建子进程

<?php

//创建socket监听
$socketserv = stream_socket_server('tcp://0.0.0.0:8000', $errno, $errstr);
//创建5个子进程
for ($i = 0; $i < 5; $i++) {
    //使用pcntl_fork()创建进程,会返回pid,如果pid==0,则表示主进程
    if (pcntl_fork() == 0) {
        //循环监听
        while (true) {
            $conn = stream_socket_accept($socketserv);
            //如果监听失败,则重新去监听
            if (!$conn) {
                continue;
            }
            //读取流信息,读取的大小 是9000
            $request = fread($conn, 9000);
            //写入响应
            $response = 'hello';
            fwrite($conn, $response);
            //关闭流
            fclose($conn);
        }
        //创建完所有的子进程,然后退出
        exit(0);
    }
}

运行后,使用ps -ef 查看进程,会看到多出了如下的5个进程

需要注意的是,PHP自带的pcntl 存在很多不足,如:

  • 没有提供进程间通信的功能
  • 不支持重定向标准输入和输出
  • 只提供了fork这样原始的接口,容易使用错误

建议使用Swoole来实现多进程和协程,参考:Swoole中的协程和子进程_浮尘笔记的博客-CSDN博客

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浮尘笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值