本文主要是介绍PHP面经,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
- CORS(cross origion resource sharing)跨域资源共享,可以允许跨站请求资源。客户端需要用特定的方法去请求资源(如xmlhttprequest)同时需要带上特定的报文头信息,服务端也要支持跨域的资源请求
memcache和redis的异同点
- memcache可以利用多线程,吞吐量高,适合大访问量
- memcache只支持简单的key/value结构
- memcache无法将数据持久化,且没法备份,只能用于缓存,重启后数据丢失
- 由于memcache支持多线程操作,所以要考虑数据一致性的问题,使用的是cas(check and set)乐观锁
- redis由于是单线程,所以可以保证操作的有序性
- redis支持多种数据结构,key/value,list,set,zset,hash等
- redis数据可以持久化存储,可以将数据存储在磁盘中,再次重启时可以读取出来
- redis支持数据备份,即master-slave模式的数据备份
web项目高并发解决方案
- html静态化,将常用但是更新很少的数据静态化
- 图片服务器分离,避免影响应用服务器,同时可以针对图片服务器进行单独的配置优化,缓存设置等
- 数据库集群,库表散列
- 数据库集群,可以实现读写分离,提高数据库响应速度,一般为一主多从,或者x主y从。比如写少读多时,一个服务器专门用于写操作,别的服务器用于读操作,这样可以避免读写锁的影响。不过写操作完成后,数据需要同步。
- 数据库集群还有个好处是,当一处数据库服务器宕机,别的服务器上也还有完整的数据
- 分布式数据库,系统中有多个节点,每个节点完成不同的功能,某个节点挂掉,那么相应的功能就没法完成了。
- 数据库集群和分布式数据库的区别。假如一个任务一个节点需要1个小时完成,现在有10个这样的任务,以及10个节点。分布式数据库可把任务拆分称10个任务,每个节点完成不同的任务,不考虑子任务的依赖,一个小时后所有任务完成。数据库集群中每个节点都可以完成完整的任务,它可以将10个任务平均到每个节点上,这样也是一个小时后所有任务完成。
- sql优化
- 表内数据过多,则可以考虑将表拆分,再用hash映射
- 索引优化
- 缓存
- 镜像,用于提高访问速度
- 负载均衡,解决高并发和大量访问问题
- CDN,让用户访问最近的cdn服务器,获得最快速的响应
- 数据压缩
PHP
魔术方法
- __set/__get 处理类中不存在的属性
- __call/_callStatic 调用类中不存在的方法/静态方法时会触发。__callStatic本身也得声明成静态方法
- __toString 将对象转换成字符串输出
- __invoke 把对象当成函数去执行
require和include的区别
- include函数:会将指定的文件读入并且执行里面的程序;
- require函数:会将目标文件的内容读入,并且把自己本身代换成这些读入的内容;
- include_once 函数:在脚本执行期间包含并运行指定文件。此行为和 include 语句类似,唯一区别是如果该文件中已经被包含过,则不会再次包含。如同此语句名字暗示的那样,只会包含一次;
- require_once 函数:和 require 语句完全相同,唯一区别是 PHP 会检查该文件是否已经被包含过,如果是则不会再次包含。
- 使用include_once或者require_once会使得程序效率降低,因为加载文件前会先去检索已加载文件表里是否已加载该文件
- include一个不存在的文件时只会产生告警,require不存在的文件时会直接抛出致命错误,脚本停止
- include是有条件包含函数,require是无条件包含函数。即,如果include或require外层有if条件,当if为false时,include不会包含,只有为真时才会包含文件,而require无论真假都会包含文件进来
return时为何不能带括号
- return只是个语言结构,不是函数,没必要将返回值用括号括起来,括起来反而会降低效率
- 如果返回值不提供参数,此时返回null,此时一定不能带括号。如果带括号会造成解析错误
- 当返回变量的引用的时候一定不能带括号。否则会变成返回引用的值,而不是引用本身
PHP弱类型变量机制
- PHP的执行是通过Zend Engine(下面简称ZE),ZE是使用C编写,在底层实现了一套弱类型机制。ZE的内存管理使用写时拷贝、引用计数等优化策略,减少再变量赋值时候的内存拷贝。
PHP的所有变量,都是以结构体zval来实现,在Zend/zend.h中我们能看到zval的定义:
typedef struct _zval_struct {zvalue_value value; /* 变量的值 */zend_uint refcount__gc; /* 引用计数器 */zend_uchar type; /* 类型 */zend_uchar is_ref__gc; /* 是否是引用 */
} zval;
php变量类型一共有8种
标准类型:布尔boolen, 整型integer, 浮点float, 字符string
复杂类型:数组array, 对象object
特殊类型:资源resource,null
其中refcount__gc和is_ref__gc表示变量是否是一个引用。type字段标识变量的类型,type的值可以是:IS_NULL, IS_BOOL, IS_LONG, IS_FLOAT, IS_STRING, IS_ARRAY, IS_OBJECT, IS_RESOURCE
。PHP根据type的类型,来选择如何存储到zvalue_value。 zvalue_value能够实现变量弱类型的核心,定义如下:
typedef union _zvalue_value {long lval; /* long value */double dval; /* double value */struct { char *val;int len; /* this will always be set for strings */} str; /* string (always has length) */HashTable *ht; /* an array */zend_object_value obj; /* stores an object store handle, and handlers */
} zvalue_value;
union 维护足够的空间来置放多个数据成员中的“一种”,而不是为每一个数据成员配置空间
- 布尔型,zval.type=IS_BOOL,会读取zval.value.lval字段,值为1/0。
- 如果是字符串,zval.type=IS_STRING,会读取zval.value.str,这是一个结构体,存储了字符串指针和长度。
C语言中,用”\0”作为字符串结束符。也就是说一个字符串”Hello\0World”在C语言中,用printf来输出的话,只能输出hello,因为”\0”会认为字符已经结束。PHP中是通过结构体的_zval_value.str.len来控制字符串长度,相关函数不会遇到”\0”结束。所以PHP的字符串是二进制安全的只关心二进制化的字符串,不关心具体格式.只会严格的按照二进制的数据存取。不会对某种特殊格式解析数据
。 - 如果是NULL,只需要zval.type=IS_NULL,不需要读取值。 通过对zval的封装,PHP实现了弱类型,对于ZE来说,通过zval可以存取任何类型。
- 数组是PHP语言中非常强大的一个数据结构,分为索引数组和关联数组,zval.type=IS_ARRAY。在关联数组中每个key可以存储任意类型的数据。PHP的数组是用Hash Table实现的,数组的值存在zval.value.ht中。
- 对象类型的zval.type=IS_OBJECT,值存在zval.value.obj中
源类型是个很特殊的类型,zval.type=IS_RESOURCE,在PHP中有一些很难用常规类型描述的数据结构,比如文件句柄,对于C语言来说是 一个指针,不过PHP中没有指针的概念,也不能用常规类型来约束,因此PHP通过资源类型概念,把C语言中类似文件指针的变量,用zval结构来封装。资 源类型值是一个整数,ZE会根据这个值去资源的哈希表中获取。资源类型的定义:
typedefstruct_zend_rsrc_list_entry
{void *ptr;int type;int refcount;
}zend_rsrc_list_entry;
其中,ptr是一个指向资源的最终实现的指针,例如一个文件句柄,或者一个数据库连接结构。type是一个类型标记,用于区分不同的资源类型。refcount用于资源的引用计数。
内核中,资源类型是通过函ZEND_FETCH_RESOURCE数获取的。
ZEND_FETCH_RESOURCE(con, type, zval *, default, resource_name, resource_type);
变量转换
变量的类型依赖于zval.type字段指示,变量的内容按照zval.type存储到zval.value。当PHP中 需要变量的时候,只需要两个步骤:把zval.value的值或指针改变,再改变zval.type的类型。不过对于PHP的一些高级变量 Array/Object/Resource,变量转换要进行更多操作
- 标准类型的互相转换,按照上述步骤即可
- 标准类型与资源类型转换
资源类型可以理解为是int,比较方便转换标准类型。转换后资源会被close或回收
<? php
$var = fopen('/tmp/aaa.txt', 'a'); // 资源 #1
$var = (int) $var;
var_dump($var); // 输出1
?>
- 标准类型与复杂类型转换
array转int/float会返回数组内元素的个数,array转bool会返回数组是否为空,array转string会返回‘Array’并抛出warning - 复杂类型相互转换
array和object可以相互转换,其他类型转换成object类型会变成stdClass,实际上array转object也会变成stdClass
变量符号表与作用域
PHP的变量符号表与zval值的映射,是通过HashTable(哈希表,又叫做散列表,下面简称HT),HashTable在ZE中广泛使用,包括常量、变量、函数等语言特性都是HT来组织,在PHP的数组类型也是通过HashTable来实现。
举个例子:<? php $var = 'Hello World';?>
var的变量名会存储在变量符号表中,代表var的类型和值的zval结构存储在哈希表中。内核通过变量符号表与zval地址的哈希映射,来实现PHP变量的存取。
为什么要提作用域呢?因为函数内部变量保护。按照作用域PHP的变量分为全局变量和局部变量,每种作用域PHP都会维护一个符号表的HashTable。当在PHP中创建一个函数或类的时候,ZE会创建一个新的符号表,表明函数或类中的变量是局部变量,这样就实现了局部变量的保护–外部无法访问函数内部的变量。 当创建一个PHP变量的时候,ZE会分配一个zval,并设置相应type和初始值,把这个变量加入当前作用域的符号表,这样用户才能使用这个变量。如果只是单纯的声明了一个变量,并没有赋值或初始化,此时var_dump会提示变量undefined,以及值为null
为了更好的理解变量的哈希表与作用域,举个简单的例子:
<? php
$temp = 'global';
function test() {$temp = 'active';
}
test();
var_dump($temp);
?>
创建函数外的变量temp,会把这个它加入全局符号表,同时在全局符号表的HashTable中,分配一个字符类型的zval,值为‘global‘。创 建函数test内部变量$temp,会把它加入属于函数test的符号表,分配字符型zval,值为’active’ 。
php数组的实现
CGI
CGI(Common Gateway Interface)全称是“通用网关接口”,WEB 服务器与WEB应用进行“交谈”的一种工具,其程序须运行在网络服务器上。CGI可以用任何一种语言编写,只要这种语言具有标准输入、输出和环境变量。如php、perl、tcl等。
WEB服务器会传哪些数据给PHP解析器呢?URL、查询字符串、POST数据、HTTP header都会有。所以,CGI就是规定要传哪些数据,以什么样的格式传递给后方处理这个请求的协议。
也就是说,CGI就是专门用来和 web 服务器打交道的。web服务器收到用户请求,就会把请求提交给cgi程序(如php-cgi),cgi程序根据请求提交的参数作应处理(解析php),然后输出标准的html语句,返回给web服服务器,WEB服务器再返回给客户端,这就是普通cgi的工作原理。
CGI的好处就是完全独立于任何服务器,仅仅是做为中间分子。提供接口给apache和php。他们通过cgi搭线来完成数据传递。这样做的好处了尽量减少2个的关联,使他们2变得更独立。
但是CGI有个蛋疼的地方,就是每一次web请求都会有启动和退出过程,也就是最为人诟病的fork-and-execute模式,这样一在大规模并发下,就死翘翘了。
fast_cgi
从根本上来说,FastCGI是用来提高CGI程序性能的。类似于CGI,FastCGI也可以说是一种协议。
FastCGI像是一个常驻(long-live)型的CGI,它可以一直执行着,只要激活后,不会每次都要花费时间去fork一次。它还支持分布式的运算, 即 FastCGI 程序可以在网站服务器以外的主机上执行,并且接受来自其它网站服务器来的请求。
FastCGI是语言无关的、可伸缩架构的CGI开放扩展,其主要行为是将CGI解释器进程保持在内存中,并因此获得较高的性能。众所周知,CGI解释器的反复加载是CGI性能低下的主要原因,如果CGI解释器保持在内存中,并接受FastCGI进程管理器调度,则可以提供良好的性能、伸缩性、Fail- Over特性等等
- Web Server启动时载入FastCGI进程管理器(Apache Module或IIS ISAPI等)
- FastCGI进程管理器自身初始化,启动多个CGI解释器进程(可建多个php-cgi),并等待来自Web Server的连接
- 当客户端请求到达Web Server时,FastCGI进程管理器选择并连接到一个CGI解释器。Web server将CGI环境变量和标准输入发送到FastCGI子进程php-cgi
- FastCGI子进程完成处理后,将标准输出和错误信息从同一连接返回Web Server。当FastCGI子进程关闭连接时,请求便告处理完成。FastCGI子进程接着等待,并处理来自FastCGI进程管理器(运行在Web Server中)的下一个连接。 在CGI模式中,php-cgi在此便退出了
FastCGI与CGI特点:
- 对于CGI来说,每一个Web请求PHP都必须重新解析php.ini、重新载入全部扩展,并重新初始化全部数据结构。而使用FastCGI,所有这些都只在进程启动时发生一次。一个额外的好处是,持续数据库连接(Persistent database connection)可以工作。
- 由于FastCGI是多进程,所以比CGI多线程消耗更多的服务器内存,php-cgi解释器每进程消耗7至25兆内存,将这个数字乘以50或100就是很大的内存数
php_cgi
PHP-CGI就是PHP实现的自带的FastCGI管理器。 虽然是php官方出品,但是这丫的却一点也不给力,性能太差,而且也很麻烦不人性化,主要体现在:
- php-cgi变更php.ini配置后,需重启php-cgi才能让新的php-ini生效,不可以平滑重启。
- 直接杀死php-cgi进程,php就不能运行了
php_fpm
FPM (FastCGI 进程管理器)是一个 PHP 进程管理器,包含 master 进程和 worker 进程两种进程:master 进程只有一个,负责监听端口,接收来自 Web Server 的请求,而 worker 进程则一般有多个 (具体数量根据实际需要配置),每个进程内部都嵌入了一个 PHP 解释器,是 PHP 代码真正执行的地方
从 FPM 接收到请求,到处理完毕,其具体的流程如下:
- FPM 的 master 进程接收到请求
- master 进程根据配置指派特定的 worker 进程进行请求处理,如果没有可用进程,返回错误,这也是我们配合 Nginx 遇到502错误比较多的原因。
- worker 进程处理请求,如果超时,返回504错误
- 请求处理结束,返回结果
主要优点有:
- 支持平滑重启。原理是新的worker采用新的php.ini配置文件,旧的worker处理完后自动销毁,生成新的worker。原理类似与nginx的平滑重启。
php5 php7区别
php7新增内容
- 标量类型和返回类型声明
- 更多的Error变为可捕获的Exception
- AST(abstract syntax tree)抽象语法树
- 作用为替换原来直接从解释器吐出opcode的方式,让解释器(parser)和编译器(compliler)解耦,可以减少一些Hack代码,同时,让实现更容易理解和可维护
- php5:php code -> parser(词法语法分析) -> opcode (中间字节代码)-> execute
- php7: php code -> parser -> ast -> opcode ->execute
- Native TLS(native thread local storage)原生线程本地存储
- 支持64位
- JIT(just in time)即时编译
- zval的改变
- php数组的变化
opcode
编译原理的中间过程会产生一种中间代码(语言),PHP由Zend引擎(C语言编写)编译后的中间代码为Opcode然后再交由Zend引擎处理,如同C语言编译后汇编代码然后再交由汇编编译处理一样(也可以直接设置编译成二进制文件然后转为机器码),不是不可以直接生成机器码让计算机去执行,而是通过这个过程将复杂的问题分步进行,并且可以根据当前系统环境的不同而对Opcode做进一步的优化,一步一步地去解析和进行
编译原理知识回顾
- 词法分析 输入源程序,对构成源程序的字符串进行扫描和分解,识别出一个个的单词(如基本字、标识 符、常量、运算符、标点符、左右括号等)
描述词法规则通常用:正规式 和 有限自动机
依循的原则:词法规则。。。线性分析。。。 - 语法分析 在词法分析的基础上,根据语言的语法规则,把单词符号串分解成各类语法单位(语法范畴) (如,“短语”、“子句”、“句子”、“程序段”等)
描述语法规则通常用:上下文无关文法
依循的原则:语法规则。。。层次结构分析。。。。 - 语义分析与中间代码生成 对语法分析所识别出的各类语法范畴,分析其含义,并进行初步翻译(产生中间代码)。
描述语义规则通常用:属性文法
依循的原则:语义规则。。。 - 优化 对前段产生的中间代码进行加工变换,以便在最后阶段能产生出更高效的目标代码
依循的原则:程序的等价不变换规则 - 目标代码生成 把中间代码(或经优化处理后)变换成特定机器上的低级语言代码。这一阶段实现了最后的翻译
PHP 垃圾回收
内存泄漏
$a = [123];
$a[] = &$a;
unset($a);
上面就是最简单的内存泄露的例子。当a自身引用的时候,会使得自身的refcount = 2,而当unset的时候,只会将refcount减为1,但此时已经没有符号指向这个zval了,可由于php认为这个zval还在被引用,因为它的refcount不为0,所以不会释放掉这块内存。这就造成了这块内存没法访问,同时又没有释放掉的问题。
php 5.3中引入的垃圾回收,就是为了解决这种问题的。(之前只有说refcount为0时进行内存释放)
对于scalar(标量)来说没有自身引用的问题,所以不用考虑垃圾回收。只需要对array和object类型进行考虑。参考源码将zval放入buffer的时候,也是会先判断是否为array或者object类,才将节点放入buffer中
垃圾的定义
- 当一个变量我们不想用的时候,同时我们没有unset掉这个变量,那么这个变量不叫‘垃圾‘。因为此时还有符号指向这个变量。
- 当一个变量的refcount减为0的时候,这个时候它也不叫‘垃圾‘,因为它立马会被free掉
- 当一个变量的refcount不为0,但是没有符号指向这个变量的时候,它就是需要清除掉的垃圾
GC的算法
- 如果一个zval的refcount在增加,那说明这个zval还在被使用,那么不属于垃圾
- 如果一个zval的refcount减少到了0,那么这个zval可以被释放掉,它也不属于垃圾
- 如果一个zval的refcount减少后大于0,那个这个zval还不能被释放掉,那么它可能属于垃圾
只有在3这种情况下,GC才会把zval收集起来,然后判断这个zval是否为垃圾。下面就是 它具体的算法。
简单来说,gc会对gc_root_buffer中的zval的refcount进行减一的操作,如果减一后refcount为0,则该zval为垃圾。
详细步骤如下:
- 为了避免每次变量的refcount减少的时候都调用GC的算法进行垃圾判断,此算法会先把所有前面准则3情况下的zval节点放入一个节点(root)缓冲区(root buffer),并且将这些zval节点标记成紫色,同时算法必须确保每一个zval节点在缓冲区中之出现一次。
当缓冲区被节点塞满的时候
,GC才开始开始对缓冲区中的zval节点进行垃圾判断 - 当缓冲区满了之后,算法以深度优先对每一个节点所包含的zval进行减1操作,为了确保不会对同一个zval的refcount重复执行减1操作,一旦zval的refcount减1之后会将zval标记成灰色。需要强调的是,这个步骤中,
起初节点zval本身不做减1操作
,但是如果节点zval中包含的zval又指向了节点zval(环形引用),那么这个时候需要对节点zval进行减1操作(php是如何进行环形引用的判别的
) - 算法再次以深度优先判断每一个节点包含的zval的值,如果zval的refcount等于0,那么将其标记成白色(代表垃圾),如果zval的refcount大于0,那么将对此zval以及其包含的zval进行refcount加1操作,这个是对非垃圾的还原操作,同时将这些zval的颜色变成黑色(zval的默认颜色属性)
这里应该和上面一样,当是环形引用的时候,需要对黑色节点再进行refcount+1
- 遍历zval节点,将C中标记成白色的节点zval释放掉
这个总结不错
对于一个包含环形引用的数组,对数组中包含的每个元素的zval进行减1操作,之后如果发现数组自身的zval的refcount变成了0,那么可以判断这个数组是一个垃圾。
这个道理其实很简单,假设数组a的refcount等于m, a中有n个元素又指向a,如果m等于n,那么算法的结果是m减n,m-n=0,那么a就是垃圾,如果m>n,那么算法的结果m-n>0,所以a就不是垃圾了
m=n代表什么? 代表a的refcount都来自数组a自身包含的zval元素,代表a之外没有任何变量指向它,代表用户代码空间中无法再访问到a所对应的zval,代表a是泄漏的内存,因此GC将a这个垃圾回收了
PHP中GC是默认开启的,同时gc_root_buffer大小为10000个节点
参考文章:https://blog.csdn.net/phpkernel/article/details/5734743
摘除几个有意思的点:
- 垃圾回收只对array和object类型进行,buffer中只会有array和object两类zval
- 只有gc_root_buffer满的时候才会进行垃圾回收,否则只是将zval加入buffer
(unset后refcount不为0则进入buffer,需要注意zval在buffer中只会出现一次,所以会判断是否已出现)
,所以当unset的时候刚好buffer满了,那么这个unset会用时比较久,因为还要进行gc的处理 - 节点放入buffer的时间:在我们调用unset的时候,会从当前符号的哈希表中删除变量名对应的项,并对该项调用一个析构函数,所以这个refcount减少的操作发生在这个析构函数中
- buffer没满则不会进行垃圾分析,只有buffer满的时候才会进行垃圾分析
- gc_root_buffer中并没有颜色的标识位。这里GC运用了一个小的技巧:利用节点指针的低两位来标识颜色属性。可能读者会有疑问,用指针中的位来保存颜色属性,那么设置颜色后,指针不就变化了吗,那么还能查找到指针对应的结构吗? 这个还真能查到! 为什么? 这个和malloc分配的内存地址属性有一定的关系,glib的malloc分配的内存地址都会有一定的对齐,这个对齐值为2 * SIZE_SZ,在不同位的机器上这个值是不一样的,但是可以确保的是分配出来的指针的最低两位肯定是0,然后看看颜色相关的宏,GC_COLOR为0x03, 3只需要两个二进制位就能够保存,所以拿指针的最低两位来保存颜色值是没有任何问题的,但是在使用指针的时候一定要先把指针最低的两位还原成0,否则指针指向的值是错误的
PHP的浅复制和深复制
先上结论,PHP中的clone为浅复制
$a = new A;
$b = $a;
这个时候我们知道,对a进行修改,b也会被修改,因为他们使用的是同一个zval。
这时有人就会用clone方法:
$a = new A;
$b = clone $a;
此时修改a的值,b的确不会受影响。但是如果是这样:
$a = new A;
$a->c = new C;
$b = $a;
你会发现非引用变量都不会受影响,但是c却还是和a的c一样。这就是clone的弊端,当a中有对其他对象的引用时(new返回的是引用),clone不会复制个新值,而是还和原引用一致,仍指向原来的对象
PHP中可以通过两种方式来实现深复制。第一种是__clone魔术方法:
public function __clone()
{$this->workExperience = new WorkExperience();
}
深复制涉及深的层次,通过clone魔术方法实现需要知道有几层然后对每一层依次实现。
但这种方法会比较麻烦,因为需要手动对引用再进行clone
还有一种是可以通过序列化对象的方式,先将对象序列化之后再反序列化,如:
$ResumeB = unserialize(serialize($ResumeA));
此方案会触发被复制对象和所有被引用对象的__sleep和__wakeup魔术方法,所以这些情况都需要被考虑
这篇关于PHP面经的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!