1. PHP

1.1. 问题

1.1.1. PHP 内存管理

  1. 内存预分配: PHP 会预先向系统申请一大块内存, 然后由此分配一块内存给到申请者, 避免了频繁的系统调用.
  2. HashTable 在初始化时只会预先给一小块内存, 在存入取出大量数据时, 会引起内存的分配和释放.
  3. 垃圾回收(GC): refcount 减少到了0, 直接当垃圾回收; refcount 减少, 但没有到0, 会加入缓冲区链表中, 当缓冲区到达阈值时, PHP 会调用方法遍历链表, 发现是垃圾就清理.

1.1.2. 程序内存溢出怎么查

  • 是什么: 代码报错 Allowed memory size 就是出现了内存溢出的问题了
  • 为什么
    • 读取了过大的文件到内存中
    • 死循环, 导致内存溢出
    • SQL 结果集过大
    • 并发上传文件, 导致 php-fpm 进程占用内存过多
  • 调试方法
    • 怎么查上限: memory_limit
    • 获取当前使用的内存: memory_get_usage()
    • 获取峰值: memory_get_peak_usage()
  • 怎么办
    • 增加 memory_limit
    • 读大文件时流式读取
    • 死循环解决代码问题
    • 及时销毁变量和对象
    • 查询数据库时只查必须的数据, 分段查询处理数据
    • 限制上传文件大小
    • 设置 php-fpm worker 进程最大处理请求数 pm.max_requests

1.1.3. PHP 底层实现, 运行过程?

执行过程: PHP 代码 => Token => 抽象语法树 => Opcodes => 执行 具体步骤:

  • 源代码通过词法分析得到 Token

Token 是 PHP 代码被分割成有意义的标识. PHP7 一共有 137 种 Token, 在 zend_language_parser.h 文件中做了定义.

  • 基于语法分析器将 Token 转换成抽象语法树 (AST: Abstract Syntax Tree)

Token 就是一个个词块, 但是单独的词块不能表达完整的语义. 所以需要语法分析器根据语法匹配 Token, 将 Token 进行串联. 语法分析器串联完 Token 后的产物就是抽象语法树

  • 将语法树转换成 Opcode

  • 执行 Opcodes

1.1.4. PHP 如何实现多线程?

  • pthread, 需要编译 php 时使用 --enable-maintainer-zts
  • 线程安全
    • 线程安全是编程中的术语, 指某个函数,函数库在多线程环境中被调用时, 能够正确的处理多个线程之间的共享变量, 使程序能够正确完成.
    • 实现: PHP 实现的线程安全主要是使用 TSRM 机制对 全局变量和静态变量进行了隔离, 将全局变量和静态变量给每个线程都复制了一份, 各个线程使用的都是主线程的一个备份, 从而避免了变量冲突, 也就不会出现线程安全问题了.
      • 优点: 保证了线程安全
      • 缺点: 子线程一旦开始运行, 主线程便无法再对子线程运行的细节进行调整了, 线程一定程度上失去了线程之间通过全局变量进行消息传递的能力.
      • 开启 TSRM 还会带来额外的损耗.

1.1.5. PHP 语言特点? 与java最大区别?

  • PHP
    • 动态语言
    • 运行在 cli 下或者 php-fpm 下
  • Java
    • 静态语言
    • 运行在 JVM 中

1.1.6. PHP如何实现并发?

  • pthreads 多线程
  • libevent 扩展, 使用 epoll 事件通知机制
  • 使用 swoole
  • curl_multi_init
  • swoole_process

1.1.7. 如何保证client端代码安全?

1.1.8. 画抽象语法树

1.1.9. 为什么 PHP7 要抽象语法树

  1. 运行时间上的优化
  2. 语义上的优化

1.1.10. empty 作用

判断变量是否为空, 比如说 null, false, 0, '', [] , '0' , 未设置的值

1.1.11. 协程实现原理, 处理网络连接有什么优势

在用户线程中模拟操作系统线程并进行调度, 一个协程 A 调用网络写请求 write 后, 然后调用 yield 将控制权交出, 协程调度器从所有协程中获取满足唤醒条件 协程, 对其调用 resume, 使该协程继续执行.

在网络连接的处理中, 相对于线程来看优势在于, 减少了上下文切换的额外消耗, 简化了并发程序的复杂度

1.1.12. 协程相对于多线程优点

  1. 协程的执行效率很高, 因为子程序切换不是线程切换, 而是由程序自身控制, 因此没有线程切换的开销, 和多线程相比, 线程数越多, 协程的性能优势就越明显.
  2. 不需要多线程的锁机制, 因为只有一个线程, 也不存在同时写变量冲突, 在协程中控制共享资源不加锁, 只需要判断状态, 所以执行效率比线程高很多.

1.1.13. 协程如何利用多核 CPU

使用多进程+协程

1.1.14. PHP下有无实现多路复用的方法?

到多路复用是一个发展进步的过程, 这个发展过程为:

  • 多线程/多进程同步阻塞
    • 有请求到来时, 启动新进程/线程来处理请求, 请求完成后销毁进程/线程
    • 缺点: 进程/线程的创建销毁太占资源
  • 多进程/多线程复用
    • 特点是启动程序后就会创建 N 个进程, 每个子进程进入 Accept, 等待新的连接进入. 当客户端连接到服务器时, 其中一个子进程会被唤醒, 开始处理客户端请求, 并且不再接受新的 TCP 连接. 当连接关闭时, 子进程会释放, 重新进入 Accept, 参与处理新的连接.
    • 缺点: 并发严重依赖于进程/线程数量. 进程/线程切换的调度太占资源.
  • IO复用/事件循环/异步非阻塞
    • Reactor 模型: 本省不处理任何数据收发, 只是可以监视一个 socket 句柄的时间变化
      • Add: 添加一个 SOCKET 到 Reactor
      • Set: 修改 SOCKET 对应的事件, 如可读可写
      • Del: 从 Reactor 中移除
      • Callback: 事件发生后回调指定的函数
    • Reactor 四个核心操作
      • add 添加 socket 监听到 reactor, 可以是 listen socket 也可以是客户端 socket, 也可以是管道 , eventdf, 信号等.
      • set 修改事件监听, 可以设置监听的类型, 例如可读, 可写. 可读很好理解, 对于 listen socket 就是有新客户端连接到来了需要 accept. 对于客户端连接就是收到数据, 需要 recv. 可写事件比较难理解一些. 一个 socket 是有缓存区的, 如果要向客户端连接发送 2M 的数据, 一次性是发送不出去的, 操作系统默认 TCP 缓存区只有 256K, 一次性只能发 256K, 缓存区满了之后 send 就会返回 EAGAIN 错误. 这个时候就要监听可写时间, 在纯异步的编程中, 必须去监听可写才能保证 send 操作时完全非阻塞的.
      • del 从 reactor 中移除, 不再监听事件.
      • callback 就是事件发生后对应的处理逻辑, 一般在 add/set 时制定. C语言用函数指针实现, JS 可以用匿名函数, PHP可以用匿名函数, 对象方法数组, 字符串函数名.
    • Reactor 模型还可以与多线程, 多进程结合起来用, 既实现异步非阻塞 IO, 又利用到多核.
      • Nginx: 多进程 Reactor
      • Nginx + Lua: 多进程 Reactor + 协程
      • Golang: 单线程 Reactor + 多线程协程
      • Swoole: 多线程 Reactor + 多进程 Worker

1.1.15. PHP 有哪些与并发 IO 编程相关的扩展

  • Stream: PHP 内核提供的 socket 封装
  • Sockets: 对底层 Socket API的封装
  • Libevent: 对 libevent 库的封装
  • Event: 基于 Libevent 更高级的封装, 提供了面向对象接口, 定时器, 信号处理的支持
  • Pcntl/Posix: 多进程, 信号, 进程管理的支持
  • Pthread: 多线程, 线程管理, 锁的支持

1.1.16. PHP 优缺点

  • 优点
    • 脚本语言, 简单易入门
    • 功能强大, 具有丰富的标准库和扩展库, 能做到服务器编程用到的 99% 的东西
  • 缺点
    • 性能比较差, 不适合做密集运算
    • 函数命名规范差
    • 提供的数据结构和函数的接口粒度比较粗, 比如一个 array 就集合了 Map, Set, Vector, Queue, Stack, Heap 等功能

1.1.17. PHP 数组的实现

PHP 中数组实际上是一个有序映射, 映射是一种把 values 关联到 keys 的类型.

在 PHP 中, 这种映射关系是使用 散列表(HashTable) 实现的. HashTable 通过 映射函数 将一个 String Key 转换为一个普通的数字下标, 并将对应的 Value 值储存到下标对应的数组元素中.

使用两次散列, 确定在映射表的位置, 映射表的值储存数组元素的下标, 哈希表是无序的, 为了实现数组的有序, 所以采用了 映射表 + 数组元素的结构.

  • 基本实现: 散列表主要由 储存元素的数组(Bucket) 和 `散列函数两个部分构成
    • 随机读: String key 会通过算法转换成 Bucket 数组中的一个小标, 根据这个下标存取数据
    • 顺序写: HashTable 元素是无序的, PHP 中的数组是有序的. 通过 中间映射表 来实现的.
      • 中间映射表: 该表与 Bucket 大小相同, 数组中储存整形数据, 用于保存元素实际储存 Value 在 Bucket 中的下标.
    • 散列函数: 实际上是先将 hash code 映射到中间映射表中, 再由中间映射表指向实际储存 Value 的元素.
      • nIndex = key->h | nTableMask
      • 散列表的大小恒为2的幂次方, 所以散列后的值会位于 [nTableMask, -1] 之间, 即映射表之中.
    • Hash 冲突
      • PHP 采用的是 链地址法, 将冲突的 Bucket 串成链表, 这样中间映射表映射出的就不是某一个元素, 而是一个 Bucket 链表, 通过散列函数定位到对应的 Bucket 时, 需要遍历链表定位元素.
    • 查找过程
      • 使用 time 33 算法对 key 值计算得到 hash code
      • 使用散列函数计算 hash code 得到散列值 nIndex, 即元素在中间映射表的下标
      • 通过 nIndex 从中间映射表中取出元素在 Bucket 中的下标 idx
      • 通过 idx 范文 Bucket 中对应的数组元素, 该元素同时也是一个 静态链表 的头结点
      • 遍历链表, 找到想要的值
    • 扩容: 自动扩容机制: 当插入一个元素且没有空闲空间时, 会扩容后再执行插入.
      • 如果已删除的元素比例到达阈值, 则会移除已被 逻辑删除 的 Bucket, 然后将后面的 Bucket 向前补上空缺的 Bucket, 因为 Bucket 的下标发生了变动, 所以还需要更改每个元素在中间映射表中储存的实际下标值.
      • 如果未达到阈值, PHP 则会申请一个大小是原数组两倍的新数组, 并将旧数组中的数据复制到新数组, 因为数组长度发生了改变, 所以 key-value 的映射关系需要重新计算, 这个步骤为 重建索引

1.1.18. PHP解析整体流程? 如何进行性能优化?

  • 流程
    • .php 文件
    • token
    • 抽象语法树
    • opcodes
    • 执行
  • 优化
    • 尽量使用内置函数: 内置函数由 C 编写, 速度更快
    • 减少 PHP 魔术函数的使用
    • 不使用错误抑制符: @: 会在代码上下加入 Opcode, 恢复了错误等级
    • 合理使用 PHP 内存, 释放掉没用的变量
    • 少使用正则表达式, 尽量使用 PHP 内置的处理函数来替代
    • 避免循环内做重复的计算
    • 避免数据密集型计算
    • 使用 Opcode cache

1.1.19. 传值和引用的区别

  • 传值会产生一个副本, 对副本的操作不会影响到原本的值
  • 应用会使新变量指向旧变量的值

1.1.20. include 和 require 的区别

都是导入一个文件, include 导入失败会报错, require 导入失败会报警告

1.1.21. PHP是否适合做守护进程,为什么(内存管理这一块)

现在来看已经能够胜任作为守护进程了, PHP 在内存管理这一块,

1.1.22. PHP的垃圾回收机制

变量分为两个部分: 变量名(zval), 变量值(zend_value). PHP 中使用引用计数来做 GC 的, 当一个变量值的 refcount 减少到0时会直接被回收, 减少但是没有到0就会加入垃圾缓冲区链表中, 缓冲区到达阈值后启动检查函数, 是垃圾的会在这个时候被回收掉.

1.1.23. PHP7 新特性

  • 引入 Throwable 接口,. Error 和 Exception 都实现了 Throwable
  • 合并空操作符
  • 飞船操作符
  • 常量数组
  • use 组合声明
  • 一次 catch 多个异常/错误
  • 类常量增加可见性修饰符
  • 可空类型 ?int
  • void 返回类型

1.1.24. 类在实例化的时候, 发生了什么

从底层来看, 实例化一个对象分为三步

  1. 根据类名去全局类列表内查找该类是否存在, 如果存在, 则获取存储类的变量
  2. 判断类是否为普通类(非抽象类或接口); 如果是普通类则给需要创建你的对象存放的 zend_value 容器分配内存, 并设置容器类型为 IS_OBJECT
  3. 执行对象初始化操作, 将对象添加到对象列表(对象池)中

1.1.25. 获取和设置类成员变量的底层

  • 获取
    1. 获取对象的属性, 从对象的 properties 查找是否存在与名称对应的属性, 如果存在返回结果, 如果不存在, 到第二步
    2. 如果存在 __get() 魔术方法, 则从此方法获取变量, 如果不存在, 则报错
  • 设置
    1. 获取对象的属性, 从对象的 properties 查找是否存在于名称对应的属性, 如果存在且已有的值和需要设置的值相同, 则不进行任何操作, 否则执行变量赋值操作, 如果不存在, 转第二步
    2. 如果存在 __set() 魔术方法, 则调用此方法设置变量, 如果不存在, 转第三步
    3. 如果成员变量一直没有被设置过, 则直接将此变量添加到对象的 properties 字段所在的 HashTable 中

1.1.26. &&和&的差别

&& 是逻辑与运算 & 是位与运算

1.1.27. 无限递归会造成哪个区域的错误?

发生内存溢出错误: Fatal error: Allowed memory size of 536870912 bytes exhausted (tried to allocate 262144 bytes) in

函数栈溢出

1.1.28. 子类构造的时候,整个构造的过程

1.1.29. 为什么选择PHP

1.1.30. go 协程和 swoole 协程什么区别

协程是轻量级的线程, 开销很小

  • Swoole 协程客户端需要在协程的上下文环境中使用.
  • Swoole v4.3.2 版本之后, 已经支持协程 CPU 密集场景调度了
  • Go 语言层面就完全支持协程了
  • Go 协程是多线程的, Swoole 协程是单线程的. 故操作全局资源时, Swoole 不需要加锁, Go 需要
  • Go 允许同一时间多个协程去读同一个 socket, 这种情况可能会发生数据包错乱的问题. 而 Swoole 在多协程读同一个 socket 时会直接抛出致命错误: "%s has already been bound to another coroutine#%ld, reading or writing of the same socket in multiple coroutines at the same time is not allowed."

1.1.31. cli 模式下的几个生命周期

  • SAPI
    • 模块初始化阶段 (module init)
      • 主要进行 PHP 框架, zend 引擎的初始化操作
    • 请求初始化阶段 (request init)
      • fpm 来说, 是在 worker 进程 accept 一个请求并读取, 解析完请求数据后的一个阶段
    • PHP 脚本执行阶段
      • Zend 引擎接管控制权, 将 PHP 脚本代码编译成 opcodes 并顺序执行
    • 请求结束阶段 (request shutdown)
    • 模块关闭阶段 (module shutdown)

1.1.32. php-fpm 运行机制?(master 管理,worker 循环 accept)

阻塞的单线程模型, 单 master, 多 worker 结构, 同一个 worker 进程同时只能处理一个请求.

PHP-FPM 是 fast-cgi 的实现, 提供了进程管理的功能, 包含 master, worker 两种进程:

  • master 创建并监听 socket, fork 多个 worker 进程, 通过共享内存获取 worker 的状态, 进而通过信号控制 worker 进程
  • worker 自由 accept cgi 请求
  • master 通过共享内存获取 worker 进程的信息, 比如 worker 进程的当前状态, 已处理请求数等, 当 master 要杀掉一个 worker 时通过发送信号的方式通知 worker 进程.

1.1.33. php-fpm 模式下,kill -9 master-pid,会怎么样?kill matser-pid 呢?(信号机制)

kill 命令默认使用 15 (SIGTERM), kill master-pid 会立即让 master 关闭, 会影响到当前请求

kill -9 master-pid (SIGKILL), 有OS来执行杀死进程的操作, 优先级最高, 会影响到当前请求

  • SIGUSR1: 重新打开日志, master 重新打开 error_log, worker 重启后重新打开 access_log
  • SIGUSR2: 重启 fpm, 包括 master 和 worker
    • master 通过给子进程发送 SIGQUIT 信号方式, 平滑关闭所有子进程
    • 如果过了一段时间有些子进程还没有退出, 就给子进程发送 SIGTERM 信号, 强制关闭子进程
    • 如果还没有关闭, 给子进程发送 SIGKILL 信号, 强制关闭
    • 等所有的子进程退出后, master 重新启动
  • SIGQUIT: 关闭 fpm, 不影响正在处理的请求, 等处理完正在处理的请求后, 子进程才退出执行步骤
    • master 通过给子进程发送 SIGQUIT 信号方式, 平滑关闭所有子进程
    • 如果过了一段时间有些子进程还没有退出, 就给子进程发送 SIGTERM 信号, 强制关闭子进程
    • 如果还没有关闭, 给子进程发送 SIGKILL 信号, 强制关闭
    • 等所有的子进程退出后, master 退出
  • SIGTERM/SIGINT 信号: 立即强制关闭 fpm, 会影响当前请求
    • Master 通过给子进程发送 SIGTERM 信号的方式关闭子进程
    • 一段时间后有些子进程没有关闭, master 给子进程发送 SIGKILL信号, 强制关闭
    • 等所有子进程退出后, master 退出

1.1.34. 内存分配流程?为什么要这么设计?

预先申请一块内存在 PHP 内部管理, 应用在申请内存时, 会从这一部分进行申请, 释放时也是先释放回到内存管理中. 这样设计话可以避免小内存的申请释放对操作系统上的额外性能的消耗.

1.1.35. GC 的出现是为了解决什么问题?什么时候会触发 GC?说下大概流程

PHP 通常用在 PHP-fpm 中, 单个 worker 进程需要处理多个请求才会被关闭, 这种情形下如果没有垃圾回收机制可能会在一次次请求中内存逐渐被占满, 会造成内存泄漏.

zval 中的 zend_refcount 计数减少时, 如果等于0直接会被回收, 大于0时会被加入到缓冲区链表中, 当缓冲区大小到达阈值时, 会执行垃圾回收程序.

1.1.36. php 里的数组是怎么实现的?(这里要注意下 php5 和 php7 实现的区别,优化了非常多)

1.1.37. nginx 和 php-fpm 的通信机制?fast-cgi 和 cgi 区别?

Nginx 接收到请求会给 worker 进程处理, worker 通过 fast-cgi 模块转发给 php-fpm 处理, php-fpm 中的 worker 会使用 poll 模型, 直接触发可读事件, 空闲 worker 中的第一个会来处理这个请求. 也就是说, php-fpm 的 master 不会参与分配请求.

fast-cgi 和 CGI 都是后台语言的交互协议, fast-cgi 有性能上的优化.

1.1.38. php-fpm 创建 worker 进程的规则是什么?不同场景下怎么选择?

  • 初始不创建, 每当有请求到来时创建, 超过一段时间后退出
  • 初始创建指定数量worker进程, 当空闲进程到达一定范围或者小于一定范围时, 对应的会减少进程数或fork新进程
  • 创建固定数量子进程

1.1.39. php 和 mysql 的通信机制?长链接和短链接啥区别?怎么实现的?连接池要怎么实现?

  1. TCP/IP: 通过网络访问
  2. socket: 本地通过 socket 文件访问

短链接在客户端结束时连接就断开了, 而长连接会被 PHP-FPM 用来复用. 减少了数据库连接的开销时间.

独立服务多进程持有一组数据库连接, 应用的数据库请求将在连接池服务中进行转发. 每次发送会查询空闲的数据库连接进行使用, 通常的话包含初始创建连接数, 动态控制空闲进程数数量的功能.

1.1.40. 依赖注入是什么?如何实现的?能解决什么问题?(代码层面不再依赖具体实现,解耦)

指服务依赖的其他服务不通过服务自己创建的方式, 而是由外部传入的方式. 通常来说使用反射实现的. 解决了服务模块之间的解耦效果, 编写代码时不用考虑外部服务的具体实现, 只需要依据接口来使用服务即可. 最常见的例子就是缓存服务的注入, 使用缓存服务的服务不需要关注缓存服务的具体实现, 不论是 文件缓存, Redis 缓存, 内存缓存, 数据库缓存等.

1.1.41. PHP 合并数组的几种方式及比较

  • array_merge
  • array_merge_recursive
  • +

  • array_merge+ 比较

    • 覆盖条件不同, array_merge 后面的覆盖前面的, + 前面的覆盖后面的
    • 对于数字索引, array_merge 会保留所有的值, 并将所有从0重排. + 处理数字索引时与处理字符索引一致, 会以前面的数组为准, 且保留原索引.
  • array_mergearray_merge_recursive
    • 对于相同的字符串索引, array_merage_recursive 会把所有的值合并成一个数组, 而 array_merge 会以后面的数组为准.
    • 对于数字索引, 两者都会保留全部的值, 并把所有重排.

results matching ""

    No results matching ""