目录
关于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博客