PHP内核 - SAPI - Fpm

PHP   PHP内核  

一、概述

  
  Fpm(FastCGI Process Manager)是 PHP FastCGI运行模式的一个进程管理器,从它的定义可以看出,Fpm的核心功能是进程管理,那么它用来管理什么进程呢?这个问题需要从FastCGI说起。

  FastCGI 是Web服务器(如 Nginx、Apache)和 处理程序之间的一种通信协议,它是与HTTP类似的一种应用层通信协议。注意:它只是一种协议!

  除了在命令行下执行脚本,能不能让 PHP 处理 HTTP 请求呢?这种场景下就涉及网络处理,需要接收请求,解析协议,处理完成后再返回处理结果。

  在网络应用场景下,PHP 并没有像 Golang 那样实现 HTTP 网络库,而是实现了FastCGI 协议,然后与Web服务器配合实现了 HTTP 的处理。Web 服务器来处理 HTTP 请求,然后将解析的结果再通过 FastCGI 协议转发给处理程序,处理程序处理完成后将结果返回给 Web 服务器,Web 服务器再返回给用户。

  PHP实现了FastCGI协议的解析,但是并没有具体实现网络处理,一般的处理模型:多进程、多线程。

  多进程模型通常是主进程只负责管理子进程,而基本的网络事件由各个子进程处理,nginx、fpm 就是这种模式;

  多线程模型与多进程类似,只是它是线程粒度,通常会由主线程监听、接收请求,然后交由子线程处理,memcached 就是这种模式,有的也是采用多进程那种模式:主线程只负责管理子线程不处理网络事件,各个子线程监听、接收、处理请求,memcached 使用 udp 协议时采用的是这种模式。

注:

  进程拥有独立的地址空间及资源,而线程则没有,线程之间共享进程的地址空间与资源,所以在资源管理上,多进程模型比较简单,而多线程模型则需要考虑不同线程之间的资源冲突,也就是线程安全。

  

二、基本实现

  Fpm 是一种多进程模型,它由一个 master 进程和多个 worker 进程组成。master 进程启动时会创建一个 socket,但是不会接受、处理请求,而是由 fork 出的worker 子进程完成请求的接收及处理。

  master 进程的主要工作室管理 worker 进程,负责 fork 或杀掉 worker 进程,比如当请求比较多,worker 进程处理不过来时,master 进程会尝试 fork 新的worker 进程进行处理,而当空闲 worker 进程比较多时,则会杀掉部分子进程,避免占用、浪费系统资源。

  worker 进程的主要工作室处理请求,每个 worker 进程会竞争的 accept 请求,接收成功后解析 FastCGI,然后执行相应的脚本,处理完成后关闭请求,继续等待新的连接,这就是一个 worker 进程的生命周期。

  从 worker 进程的生命周期可以看到,一个 worker 进程只能处理一个请求,只有将一个请求处理完成后,才会处理下一个请求。

  这与 Nginx 的事件模型有很大的区别,Nginx 的子进程通过 epoll 管理套接字,如果一个请求数据还未发送完成则会处理下一个请求,即一个进程会同时连接多个请求,它是非阻塞的模型,只处理活跃的套接字。

  Fpm 的这种处理模式大大简化了 PHP 的资源管理,使得在 Fpm 模式下不需要考虑并发导致的资源冲突。

  master 进程与 worker 进程之间不会直接进行通信,master 通过共享内存获取 worker 进程的信息,比如 worker 进程当前状态、已处理请求数等,master 进程通过发送信号的方式杀掉 worker 进程。

  Fpm 可以同时监听多个端口,每个端口对应一个 worker pool,每个pool下对应对应多个 worker 进程,类似 Nginx 中 server 的概念。这些归属不同 pool 的 worker 进程仍由一个 master 管理。
  worker pool的结构为 fpm_worker_pool_s,pool 之间构成一个链表。

  1. struct fpm_worker_pool_s {
  2. // 指向下一个worker pool
  3. struct fpm_worker_pool_s *next;
  4. // php-fpm.conf 配置:pm、max_children、start_servers...
  5. struct fpm_worker_pool_config_s *config;
  6. char *user, *home;
  7. enum fpm_address_domain listen_address_domain;
  8. // 监听的套接字
  9. int listening_socket;
  10. int set_uid, set_gid;
  11. int socket_uid, socket_gid, socket_mode;
  12. // 当前 pool 的worker链表,每个worker对应一个fpm_children_s结构
  13. struct fpm_child_s *children;
  14. // 当前pool的worker运行总数
  15. int running_children;
  16. int idle_spawn_rate;
  17. int warn_max_children;
  18. // 记录worker的运行信息,比如空闲、忙碌worker数
  19. struct fpm_scoreboard_s *scoreboard;
  20. ...
  21. };
  22. 注:代码在/sapi/fpm/fpm_worker_pool.h*

  
  在php-fpm.conf中通过[pool name] 声明一个worker pool,每个 pool 各自配置监听地址、进程管理方式、worker进程数等。

  1. [web1]
  2. listen = 127.0.0.1:9000
  3. ...
  4. [web2]
  5. listen = 127.0.0.1:9001
  6. ...

上面这个例子配置了两个worker pool,分别监听9000,、9001端口。

  

三、Fpm 的初始化

Fpm 的main函数位于文件 /sapi/fpm/fpm/fpm_main.c 中。

(1)Fpm 在启动后首先会进行SAPI的注册操作

(2)接着会进入PHP 生命周期的 module startup 阶段,在这个阶段会调用各个扩展定义的 MINT 钩子函数

(3)然后会进行一系列的初始化操作,最后 master、worker进程进入不同的处理环节。

  1. int main(int argc, char *argv[])
  2. {
  3. ...
  4. // 注册SAPI:将全局变量sapi_module设置为cgi_sapi_module
  5. sapi_startup(&cgi_sapi_module);
  6. ...
  7. // 执行 php_module_startup()
  8. if (cgi_sapi_module.startup(&cgi_sapi_module) == FAILURE) {
  9. return FPM_EXIT_SOFTWARE;
  10. }
  11. ...
  12. // 初始化
  13. fpm_init(...)
  14. fpm_is_running = 1;
  15. // 后面都是worker进程的操作,master进程不会走到下面
  16. fcgi_fd = fpm_run(&max_requests);
  17. parent = 0;
  18. ...

  

注:fpm_init() 方法中将完成以下几个关键操作
(1)fpm_conf_init_main()

  解析 php.fpm.conf 配置文件,为每个 worker pool 分配一个 fpm_worker_pool_s结构,各worker pool 的配置在解析后保存到 fpm_worker_pool_s->config中,下面是 conf 中的几个常用配置。

  1. struct fpm_worker_pool_config_s {
  2. // pool 名称,即 [pool name]
  3. char *name;
  4. // Fpm 的启动用户,配置:user
  5. char *user;
  6. // 配置:group
  7. char *group;
  8. // 监听的地址,配置:listen
  9. char *listen_address;
  10. // 进程模型:static、dynamic、ondemand
  11. int pm;
  12. // 最大worker进程数
  13. int pm_max_children;
  14. // 启动时初始化的worker数
  15. int pm_start_servers;
  16. // 最小空闲worker数
  17. int pm_min_spare_servers;
  18. // 最大空闲worker数
  19. int pm_max_spare_servers;
  20. // worker空闲时间
  21. int pm_process_idle_timeout;
  22. // worker处理的最多请求数,超过这个值,worker将被kill
  23. int pm_max_requests;
  24. ...
  25. };
  26. // 注:该方法位于/sapi/fpm/fpm/fpm_conf.h中

  

(2)fpm_scoreboard_init_main()

  分配用于记录 worker 进程运行信息的结构,此结构分配在共享内存上,master 正是通过这种方式获取 worker 的运行信息。分配时按照 worker pool 的最大 worker进程数进行分配,每个 worker pool 分配一个 fpm_scoreboard_s结构。pool下的每个 worker 进程分配一个 fpm_scoreboard_proc_s结构。

  • 注:该方法位于/sapi/fpm/fpm/fpm_scoreboard.c中

(3)fpm_signals_init_main()

  这一步会通过socketpair()创建一个管道,这个管道并不是用于 master 与worker 进程通信用的,他只在 master 进程中使用,同时设置 master 的信号处理函数为 sig_handle()。

  • 注:该文件位于/sapi/fpm/fpm/fpm_signals.c文件中*
(4)fpm_sockets_init_main()

  创建每个worker pool 的 socket 套接字,启动后 worker 将监听此 socket 接收请求。

(5)fpm_event_init_main()

  启动master的事件管理,Fpm实现了一个事件管理器用于管理 I/O、定时事件,其中 I/O 事件根据不同平台选择 kqueue、epoll、poll、select 等管理,定时事件就是定时器,一定时间后触发某个事件。

(6)fpm_run()

  在完成这些初始化操作后,接下来就是最关键的fpm_run() 操作了,此环节将fork子进程、启动进程管理器,执行后master进程将不会返回这个函数,只有各worker返回,也就是说 main() 函数中调用 fpm_run() 之后的操作均是worker进程的。

  1. int fpm_run(int *max_requests)
  2. {
  3. struct fpm_worker_pool_s *wp;
  4. /* create initial children in all pools (编译worker pool) */
  5. for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
  6. int is_parent;
  7. // 调用fpm_children_make() fork子进程
  8. is_parent = fpm_children_create_initial(wp);
  9. if (!is_parent) {
  10. // fork出的 worker进程
  11. goto run_child;
  12. }
  13. /* handle error */
  14. if (is_parent == 2) {
  15. fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);
  16. fpm_event_loop(1);
  17. }
  18. }
  19. /* run event loop forever (master进程将进入event循环,不再往下走)*/
  20. fpm_event_loop(0);
  21. /* only workers reach this point(只有worker进程会到这里)*/
  22. run_child:
  23. fpm_cleanups_run(FPM_CLEANUP_CHILD);
  24. *max_requests = fpm_globals.max_requests;
  25. // 返回监听的套接字
  26. return fpm_globals.listening_socket;
  27. }

  由此 fpm_fun() 的处理来看,在fork出worker进程后,子进程将返回 main() 中,而 master 进程进入 fpm_event_loop(),这是一个事件循环,接下来分别分析master、worker后续处理。

  

四、worker的请求处理

  worker进程返回 main() 函数后将继续向下执行,此后的流程就是 worker 进程不断 accept 请求,有请求到达后将读取并解析FastCGI协议的数据,解析完成后开始执行PHP脚本,执行完成后关闭请求,继续监听等待新的请求到达。

worker处理请求的周期如下:

(1)等待请求:worker进程阻塞在fcgi_accept_request() 中等待请求

(2)解析请求:fastcgi 请求到达后被worker接收,然后开始接收并解析请求数据,直到request数据完全到达

(3)请求初始化:执行 php_request_startup(),此阶段会调用每个扩展的PHP_RINIT_FUNCTION()

(4)执行PHP脚本:由php_execute_script()完成 PHP 脚本的编译、执行操作

(5)关闭请求:请求完成后执行 php_request_shutdown(),此阶段会调用每个扩展的 PHP_RSHUTDOWN_FUNCTION(),然后重新回到步骤(1)

具体的处理如下:

  1. int main(int argc, char *argv[])
  2. {
  3. ...
  4. // worker在调用后返回监听的socket fd
  5. fcgi_fd = fpm_run(&max_requests);
  6. parent = 0;
  7. // 初始化fastcgi请求
  8. request = fpm_init_request(fcgi_fd);
  9. // worker进程将阻塞在这里,等待请求
  10. while (EXPECTED(fcgi_accept_request(request) >= 0)) {
  11. ...
  12. SG(server_context) = (void *) request;
  13. init_request_info();
  14. // 请求开始
  15. if (UNEXPECTED(php_request_startup() == FAILURE)) {
  16. fcgi_finish_request(request, 1);
  17. SG(server_context) = NULL;
  18. php_module_shutdown();
  19. return FPM_EXIT_SOFTWARE;
  20. }
  21. ...
  22. fpm_request_executing();
  23. // 编译、执行PHP脚本
  24. php_execute_script(&file_handle);
  25. ...
  26. // 请求结束
  27. php_request_shutdown((void *) 0);
  28. ...
  29. }
  30. ...
  31. // worker进程退出,进入module_shutdown阶段
  32. php_module_shutdown();
  33. ...
  34. }
  35. // 文件位于 /sapi/fpm/fpm/fpm_mian.c

  

五、master 进程管理

1、Fpm 的三种进程管理方式:

首先介绍 Fpm 三种不同的进程管理方式,具体要使用哪种模式可以在conf配置中通过 pm 指定,例如:pm = dynamic。

(1)静态模式(static)

这种方式比较简单,在启动时 master 根据 pm.max_children 配置 fork 出相应数量的worker进程,也就是 worker 进程数是固定不变的。

(2)动态模式 (dynamic)

这种方式比较常用,也是默认方式。在Fpm启动时会根据 pm.start_servers 配置初始化一定数量的 worker。

运行期间如果 master 发现空闲 worker 数低于 pm.min_spare_servers配置数 (说明请求比较多,worker处理不过来了),则会 fork worker 进程,但总的 worker 数不会高于 pm.max_children;

运行期间如master发现空闲 worker 数超过了 pm.max_spare_servers (表示闲着的worker数太多了),则会杀掉一些 worker,避免占用过多资源。

(3)按需模式 (ondemand)

这种模式很想传统的cgi,在启动时不分配 worker 进程,等到有请求了后再通知 master 进程 fork worker进程,也就是来了请求以后再 fork 子进程进行处理。总的worker数不超过 pm.max_children,处理完成后 worker 进程不会立即退出,当空闲时间超过 pm.process_idle_timeout 后再退出。

  

2、master 进入 fpm_event_loop() 时间循环后的处理

master在调用 fpm_run() 后不再返回,而是进入一个事件循环中,此后 master 将始终围绕着几个事件进行处理。

(1)信号事件 (fpm_got_signal)

(2)进程检查定时器 (fpm_pctl_perform_idle_server_maintenance_heartbeat)

(3)执行超时检查定时器 (fpm_pctl_heartbeat)

注:如果在ondemand模式下,则还会多出一个处理,为 fpm_pctl_on_socket_accept

 
 
—- 文章引自 《PHP7 内核剖析》

 

 



Top