{"id":21421251,"url":"https://github.com/owenliang/php-fpm-code-analysis","last_synced_at":"2026-01-03T16:08:52.237Z","repository":{"id":60242931,"uuid":"127391387","full_name":"owenliang/php-fpm-code-analysis","owner":"owenliang","description":"php-fpm源码分析","archived":false,"fork":false,"pushed_at":"2018-03-30T07:11:42.000Z","size":16,"stargazers_count":177,"open_issues_count":1,"forks_count":26,"subscribers_count":9,"default_branch":"master","last_synced_at":"2025-01-23T06:44:52.106Z","etag":null,"topics":["code-analysis","php-fpm"],"latest_commit_sha":null,"homepage":null,"language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/owenliang.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-03-30T06:37:12.000Z","updated_at":"2024-12-30T09:57:02.000Z","dependencies_parsed_at":"2022-09-27T05:00:28.548Z","dependency_job_id":null,"html_url":"https://github.com/owenliang/php-fpm-code-analysis","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/owenliang%2Fphp-fpm-code-analysis","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/owenliang%2Fphp-fpm-code-analysis/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/owenliang%2Fphp-fpm-code-analysis/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/owenliang%2Fphp-fpm-code-analysis/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/owenliang","download_url":"https://codeload.github.com/owenliang/php-fpm-code-analysis/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243926072,"owners_count":20369910,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["code-analysis","php-fpm"],"created_at":"2024-11-22T20:32:59.225Z","updated_at":"2026-01-03T16:08:52.215Z","avatar_url":"https://github.com/owenliang.png","language":null,"readme":"# PHP-FPM源码分析\n## 入口文件\n\n```\nfpm/fpm/fpm_main.c\n\n\nint main(int argc, char *argv[]) {\n\n ...\n ...\n\n\t// 初始化\n\tif (0 \u003e fpm_init(argc, argv, fpm_config ? fpm_config : CGIG(fpm_config), fpm_prefix, fpm_pid, test_conf, php_allow_to_run_as_root, force_daemon, force_stderr)) {\n\n\t\tif (fpm_globals.send_config_pipe[1]) {\n\t\t\tint writeval = 0;\n\t\t\tzlog(ZLOG_DEBUG, \"Sending \\\"0\\\" (error) to parent via fd=%d\", fpm_globals.send_config_pipe[1]);\n\t\t\tzend_quiet_write(fpm_globals.send_config_pipe[1], \u0026writeval, sizeof(writeval));\n\t\t\tclose(fpm_globals.send_config_pipe[1]);\n\t\t}\n\t\treturn FPM_EXIT_CONFIG;\n\t}\n\n\tif (fpm_globals.send_config_pipe[1]) {\n\t\tint writeval = 1;\n\t\tzlog(ZLOG_DEBUG, \"Sending \\\"1\\\" (OK) to parent via fd=%d\", fpm_globals.send_config_pipe[1]);\n\t\tzend_quiet_write(fpm_globals.send_config_pipe[1], \u0026writeval, sizeof(writeval));\n\t\tclose(fpm_globals.send_config_pipe[1]);\n\t}\n\tfpm_is_running = 1;\n\n\t// 这里父进程创建监听，进入自己的循环\n\tfcgi_fd = fpm_run(\u0026max_requests); // fcgi_id就是监听socket\n\n\t// 子进程继续向下执行\n\tparent = 0;\n```\n\n## 初始化阶段\n\n```\nfpm/fpm/fpm.c\n\n\nint fpm_init(int argc, char **argv, char *config, char *prefix, char *pid, int test_conf, int run_as_root, int force_daemon, int force_stderr) /* {{{ */\n{\n\tif (0 \u003e fpm_php_init_main()           ||\n\t    0 \u003e fpm_stdio_init_main()         ||\n\t    0 \u003e fpm_conf_init_main(test_conf, force_daemon) ||\n\t    0 \u003e fpm_unix_init_main()          ||\n\t    0 \u003e fpm_scoreboard_init_main()    ||\n\t    0 \u003e fpm_pctl_init_main()          ||\n\t    0 \u003e fpm_env_init_main()           ||\n\t    0 \u003e fpm_signals_init_main()       ||\n\t    0 \u003e fpm_children_init_main()      ||\n\t    0 \u003e fpm_sockets_init_main()       ||\n\t    0 \u003e fpm_worker_pool_init_main()   ||\n\t    0 \u003e fpm_event_init_main()) {\n\n\t\tif (fpm_globals.test_successful) {\n\t\t\texit(FPM_EXIT_OK);\n\t\t} else {\n\t\t\tzlog(ZLOG_ERROR, \"FPM initialization failed\");\n\t\t\treturn -1;\n\t\t}\n\t}\n```\n\n初始化一些程序结构：配置、记分板、工作池（监听套接字，子进程管理）、事件循环...在此不做具体展开。\n\n关键点就是创建了监听socket，后续子进程需要继承并监听。\n\n## 启动阶段\n\n \n### 整体流程\n\n```\n\nfpm/fpm/fpm.c\n\n\nint fpm_run(int *max_requests) /* {{{ */\n{\n\tstruct fpm_worker_pool_s *wp;\n\n\t/* create initial children in all pools */\n\n\t// 所有的池子\n\tfor (wp = fpm_worker_all_pools; wp; wp = wp-\u003enext) {\n\t\tint is_parent;\n\n\t\tis_parent = fpm_children_create_initial(wp);\n\n\t\tif (!is_parent) {\n\t\t\tgoto run_child;\n\t\t}\n\n\t\t/* handle error */\n\t\tif (is_parent == 2) { // 创建子进程失败\n\t\t\tfpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);\n\t\t\tfpm_event_loop(1);\n\t\t}\n\t}\n\n\t/* run event loop forever */\n\n\t// 父进程循环\n\tfpm_event_loop(0);\n\n\t// 子进程继续向下执行\nrun_child: /* only workers reach this point */\n\n\tfpm_cleanups_run(FPM_CLEANUP_CHILD);\n\n\t*max_requests = fpm_globals.max_requests;\n\treturn fpm_globals.listening_socket;\n}\n\n```\n\n### 初始化池子\n\n对于每个池子（php-fpm.conf里配置的work pool），调用fpm_children_create_initial初始化若干子进程。\n\n因为work pool有不同的进程管理策略，所以初始化进程的数量和方式各有差异。\n\n```\n\nfpm/fpm/fpm_children.c\n\n\nint fpm_children_create_initial(struct fpm_worker_pool_s *wp) /* {{{ */\n{\n\t// 按需分配的进程管理模式，实际上是父进程监听listen socket可读则认为可能需要更多的子进程来处理请求\n\tif (wp-\u003econfig-\u003epm == PM_STYLE_ONDEMAND) {\n\t\twp-\u003eondemand_event = (struct fpm_event_s *)malloc(sizeof(struct fpm_event_s));\n\n\t\tif (!wp-\u003eondemand_event) {\n\t\t\tzlog(ZLOG_ERROR, \"[pool %s] unable to malloc the ondemand socket event\", wp-\u003econfig-\u003ename);\n\t\t\t// FIXME handle crash\n\t\t\treturn 1;\n\t\t}\n\n\t\tmemset(wp-\u003eondemand_event, 0, sizeof(struct fpm_event_s));\n\t\tfpm_event_set(wp-\u003eondemand_event, wp-\u003elistening_socket, FPM_EV_READ | FPM_EV_EDGE, fpm_pctl_on_socket_accept, wp);\n\t\twp-\u003esocket_event_set = 1;\n\t\tfpm_event_add(wp-\u003eondemand_event, 0);\n\n\t\treturn 1;\n\t}\n\n\t// 其他进程管理模式直接初始化，比如static模式直接拉起指定数量的子进程，dynamic模式拉起最小数量的子进程\n\treturn fpm_children_make(wp, 0 /* not in event loop yet */, 0, 1);\n}\n\n```\n\n### 创建子进程\n\nfpm_children_make用于为池子扩容子进程数量，初始化阶段in_event_loop传0，从而只启动有限数量的子进程，相关策略在代码中有注释说明。\n\n```\nfpm/fpm/fpm_children.c\n\n\n\t// 创建N个子进程\nint fpm_children_make(struct fpm_worker_pool_s *wp, int in_event_loop, int nb_to_spawn, int is_debug) /* {{{ */\n{\n\tpid_t pid;\n\tstruct fpm_child_s *child;\n\tint max;\n\tstatic int warned = 0;\n\n\tif (wp-\u003econfig-\u003epm == PM_STYLE_DYNAMIC) {\n\t\tif (!in_event_loop) { /* starting */\n\t\t\tmax = wp-\u003econfig-\u003epm_start_servers;\n\t\t} else {\n\t\t\tmax = wp-\u003erunning_children + nb_to_spawn;\n\t\t}\n\t} else if (wp-\u003econfig-\u003epm == PM_STYLE_ONDEMAND) {\n\t\tif (!in_event_loop) { /* starting */\n\t\t\tmax = 0; /* do not create any child at startup */\n\t\t} else {\n\t\t\tmax = wp-\u003erunning_children + nb_to_spawn;\n\t\t}\n\t} else { /* PM_STYLE_STATIC */\n\t\tmax = wp-\u003econfig-\u003epm_max_children;\n\t}\n\n\t/*\n\t * fork children while:\n\t *   - fpm_pctl_can_spawn_children : FPM is running in a NORMAL state (aka not restart, stop or reload)\n\t *   - wp-\u003erunning_children \u003c max  : there is less than the max process for the current pool\n\t *   - (fpm_global_config.process_max \u003c 1 || fpm_globals.running_children \u003c fpm_global_config.process_max):\n\t *     if fpm_global_config.process_max is set, FPM has not fork this number of processes (globaly)\n\t */\n\twhile (fpm_pctl_can_spawn_children() \u0026\u0026 wp-\u003erunning_children \u003c max \u0026\u0026 (fpm_global_config.process_max \u003c 1 || fpm_globals.running_children \u003c fpm_global_config.process_max)) {\n\n\t\twarned = 0;\n\n\t\t// 创建一个child对象，分配对应的记分板槽\n\t\tchild = fpm_resources_prepare(wp);\n\n\t\tif (!child) {\n\t\t\treturn 2;\n\t\t}\n\n\t\tpid = fork();\n\n\t\tswitch (pid) {\n\n\t\t\tcase 0 : // 子进程\n\t\t\t\tfpm_child_resources_use(child);\n\t\t\t\tfpm_globals.is_child = 1;\n\t\t\t\tfpm_child_init(wp);\n\t\t\t\treturn 0;\n\n\t\t\tcase -1 :\n\t\t\t\tzlog(ZLOG_SYSERROR, \"fork() failed\");\n\n\t\t\t\tfpm_resources_discard(child);\n\t\t\t\treturn 2;\n\n\t\t\tdefault :\n\t\t\t\t// 父进程\n\t\t\t\tchild-\u003epid = pid;\n\t\t\t\tfpm_clock_get(\u0026child-\u003estarted);\n\t\t\t\tfpm_parent_resources_use(child);\n\n\t\t\t\tzlog(is_debug ? ZLOG_DEBUG : ZLOG_NOTICE, \"[pool %s] child %d started\", wp-\u003econfig-\u003ename, (int) pid);\n\t\t}\n\n\t}\n\n\tif (!warned \u0026\u0026 fpm_global_config.process_max \u003e 0 \u0026\u0026 fpm_globals.running_children \u003e= fpm_global_config.process_max) {\n               if (wp-\u003erunning_children \u003c max) {\n                       warned = 1;\n                       zlog(ZLOG_WARNING, \"The maximum number of processes has been reached. Please review your configuration and consider raising 'process.max'\");\n               }\n\t}\n\n\treturn 1; /* we are done */\n}\n```\n\n### 准备创建子进程\n \n创建子进程，需要在父进程关联一些数据结构记录其信息。\n\n另外，需要创建一个Pipe，子进程会把自己标准输出和错误输出定向到pipe[1]，这样父进程就可以捕获子进程的输出了。\n\n其中fpm_resources_prepare就是这样一个函数：\n\n```\n\nfpm/fpm/fpm_children.c\n\n\nstatic struct fpm_child_s *fpm_resources_prepare(struct fpm_worker_pool_s *wp) /* {{{ */\n{\n\tstruct fpm_child_s *c;\n\n\tc = fpm_child_alloc();\n\n\tif (!c) {\n\t\tzlog(ZLOG_ERROR, \"[pool %s] unable to malloc new child\", wp-\u003econfig-\u003ename);\n\t\treturn 0;\n\t}\n\n\tc-\u003ewp = wp;\n\tc-\u003efd_stdout = -1; c-\u003efd_stderr = -1;\n\n\tif (0 \u003e fpm_stdio_prepare_pipes(c)) {\n\t\tfpm_child_free(c);\n\t\treturn 0;\n\t}\n\n\tif (0 \u003e fpm_scoreboard_proc_alloc(wp-\u003escoreboard, \u0026c-\u003escoreboard_i)) {\n\t\tfpm_stdio_discard_pipes(c);\n\t\tfpm_child_free(c);\n\t\treturn 0;\n\t}\n\n\treturn c;\n}\n\n```\n\n### 共享内存 记分板\n\n上面代码还分配了一个scoreboard记分板，这是PHP-FPM进行进程管理非常关键的组件。\n\n每个池子都有一个scoreboard对象，里面为每个子进程准备了一个scoreboard_proc对象。\n\nscoreboard和scoreboard_proc对象在父进程中从共享内存里分配，在父子进程间共享访问，通过atomic原子变量实现spinlock自旋锁，确保多进程并发访问的安全性。\n\n```\nfpm/fpm/fpm_scoreboard.h\n\n\n\t//  每个子进程有一个小记分板\nstruct fpm_scoreboard_proc_s {\n\tunion {\n\t\tatomic_t lock; // 保护该对象的自旋锁\n\t\tchar dummy[16];\n\t};\n\tint used;\n\ttime_t start_epoch;\n\tpid_t pid;\n\tunsigned long requests;\n\tenum fpm_request_stage_e request_stage;\n\tstruct timeval accepted;\n\tstruct timeval duration;\n\ttime_t accepted_epoch;\n\tstruct timeval tv;\n\tchar request_uri[128];\n\tchar query_string[512];\n\tchar request_method[16];\n\tsize_t content_length; /* used with POST only */\n\tchar script_filename[256];\n\tchar auth_user[32];\n#ifdef HAVE_TIMES\n\tstruct tms cpu_accepted;\n\tstruct timeval cpu_duration;\n\tstruct tms last_request_cpu;\n\tstruct timeval last_request_cpu_duration;\n#endif\n\tsize_t memory;\n};\n\n\t// 每个池子一个大记分板\nstruct fpm_scoreboard_s {\n\tunion {\n\t\tatomic_t lock; // 保护大记分板的自旋锁\n\t\tchar dummy[16];\n\t};\n\tchar pool[32];\n\tint pm;\n\ttime_t start_epoch;\n\tint idle;\n\tint active;\n\tint active_max;\n\tunsigned long int requests;\n\tunsigned int max_children_reached;\n\tint lq;\n\tint lq_max;\n\tunsigned int lq_len;\n\tunsigned int nprocs;\n\tint free_proc;\n\tunsigned long int slow_rq;\n\tstruct fpm_scoreboard_proc_s *procs[]; // 池子内每个进程有一个小记分板\n};\n\n```\n\n还记得本文最开始初始化中的fpm_scoreboard_init_main吗？\n\n```\nfpm/fpm/fpm_scoreboard.c\n\n\nint fpm_scoreboard_init_main() /* {{{ */\n{\n\tstruct fpm_worker_pool_s *wp;\n\tunsigned int i;\n\n#ifdef HAVE_TIMES\n#if (defined(HAVE_SYSCONF) \u0026\u0026 defined(_SC_CLK_TCK))\n\tfpm_scoreboard_tick = sysconf(_SC_CLK_TCK);\n#else /* _SC_CLK_TCK */\n#ifdef HZ\n\tfpm_scoreboard_tick = HZ;\n#else /* HZ */\n\tfpm_scoreboard_tick = 100;\n#endif /* HZ */\n#endif /* _SC_CLK_TCK */\n\tzlog(ZLOG_DEBUG, \"got clock tick '%.0f'\", fpm_scoreboard_tick);\n#endif /* HAVE_TIMES */\n\n\n\tfor (wp = fpm_worker_all_pools; wp; wp = wp-\u003enext) {\n\t\tsize_t scoreboard_size, scoreboard_nprocs_size;\n\t\tvoid *shm_mem;\n\n\t\tif (wp-\u003econfig-\u003epm_max_children \u003c 1) {\n\t\t\tzlog(ZLOG_ERROR, \"[pool %s] Unable to create scoreboard SHM because max_client is not set\", wp-\u003econfig-\u003ename);\n\t\t\treturn -1;\n\t\t}\n\n\t\tif (wp-\u003escoreboard) {\n\t\t\tzlog(ZLOG_ERROR, \"[pool %s] Unable to create scoreboard SHM because it already exists\", wp-\u003econfig-\u003ename);\n\t\t\treturn -1;\n\t\t}\n\n\t\tscoreboard_size        = sizeof(struct fpm_scoreboard_s) + (wp-\u003econfig-\u003epm_max_children) * sizeof(struct fpm_scoreboard_proc_s *);\n\t\tscoreboard_nprocs_size = sizeof(struct fpm_scoreboard_proc_s) * wp-\u003econfig-\u003epm_max_children;\n\t\tshm_mem                = fpm_shm_alloc(scoreboard_size + scoreboard_nprocs_size);\n\n\t\tif (!shm_mem) {\n\t\t\treturn -1;\n\t\t}\n\t\twp-\u003escoreboard         = shm_mem;\n\t\twp-\u003escoreboard-\u003enprocs = wp-\u003econfig-\u003epm_max_children;\n\t\tshm_mem               += scoreboard_size;\n\n\t\tfor (i = 0; i \u003c wp-\u003escoreboard-\u003enprocs; i++, shm_mem += sizeof(struct fpm_scoreboard_proc_s)) {\n\t\t\twp-\u003escoreboard-\u003eprocs[i] = shm_mem;\n\t\t}\n\n\t\twp-\u003escoreboard-\u003epm          = wp-\u003econfig-\u003epm;\n\t\twp-\u003escoreboard-\u003estart_epoch = time(NULL);\n\t\tstrlcpy(wp-\u003escoreboard-\u003epool, wp-\u003econfig-\u003ename, sizeof(wp-\u003escoreboard-\u003epool));\n\t}\n\treturn 0;\n}\n```\n\n可见，FPM为每个池子，一次性分配了足够最多子进程用的记分板内存空间，而且是通过共享内存分配的，这样子进程可以和父进程共享这块信息：\n\n```\nfpm/fpm/fpm_shm.c\n\n\nvoid *fpm_shm_alloc(size_t size) /* {{{ */\n{\n\tvoid *mem;\n\n\tmem = mmap(0, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0);\n\n#ifdef MAP_FAILED\n\tif (mem == MAP_FAILED) {\n\t\tzlog(ZLOG_SYSERROR, \"unable to allocate %zu bytes in shared memory: %s\", size, strerror(errno));\n\t\treturn NULL;\n\t}\n#endif\n\n\tif (!mem) {\n\t\tzlog(ZLOG_SYSERROR, \"unable to allocate %zu bytes in shared memory\", size);\n\t\treturn NULL;\n\t}\n\n\tfpm_shm_size += size;\n\treturn mem;\n}\n```\n\n通过mmap的MAP_ANONY|MAP_SHARED做匿名共享内存。\n\n至于多进程访问的安全性，是依靠atomic_t原子变量与atomic_cmp_set这样的原子操作实现了自旋锁，整个函数是内联的：\n\n```\nfpm/fpm/fpm_atomic.h\n\n\nstatic inline int fpm_spinlock(atomic_t *lock, int try_once) /* {{{ */\n{\n\tif (try_once) {\n\t\treturn atomic_cmp_set(lock, 0, 1) ? 1 : 0;\n\t}\n\n\tfor (;;) {\n\n\t\tif (atomic_cmp_set(lock, 0, 1)) {\n\t\t\tbreak;\n\t\t}\n\n\t\tsched_yield();\n\t}\n\n\treturn 1;\n}\n\n```\n\nsched_yield是为了让出CPU，避免空转等锁对CPU占用过高。\n\n## 执行阶段\n\n\n### 子进程进入阻塞循环\n\nfpm_children_create_initial函数返回0表示子进程，则返回到fpm_run的调用处，也就是main函数里。\n\n```\nfpm/fpm/fpm_main.c\n\n\n\tzend_first_try {\n\t\t// accept监听套接字，获得一个连接socket\n\t\twhile (EXPECTED(fcgi_accept_request(request) \u003e= 0)) {\n\t\t\tchar *primary_script = NULL;\n\t\t\trequest_body_fd = -1;\n\t\t\tSG(server_context) = (void *) request;\n\t\t\tinit_request_info();\n\n\t\t\tfpm_request_info();\n\n\t\t\t// 初始化PHP执行环境\n\n\t\t\t....\n\t\t\t\n\t\t\t...\n```\n\n子进程是阻塞循环，同一时刻只能处理一个连接。\n\n关于PHP解释器如何初始化环境属于另外一个话题，下面是关键代码：\n\n```\nfpm/fpm/fpm_main.c\n\n\t// 打开PHP文件\n\t\t\t/* path_translated exists, we can continue ! */\n\t\t\tif (UNEXPECTED(php_fopen_primary_script(\u0026file_handle) == FAILURE)) {\n\t\t\t\tzend_try {\n\t\t\t\t\tzlog(ZLOG_ERROR, \"Unable to open primary script: %s (%s)\", primary_script, strerror(errno));\n\t\t\t\t\tif (errno == EACCES) {\n\t\t\t\t\t\tSG(sapi_headers).http_response_code = 403;\n\t\t\t\t\t\tPUTS(\"Access denied.\\n\");\n\t\t\t\t\t} else {\n\t\t\t\t\t\tSG(sapi_headers).http_response_code = 404;\n\t\t\t\t\t\tPUTS(\"No input file specified.\\n\");\n\t\t\t\t\t}\n\t\t\t\t} zend_catch {\n\t\t\t\t} zend_end_try();\n\t\t\t\t/* we want to serve more requests if this is fastcgi\n\t\t\t\t * so cleanup and continue, request shutdown is\n\t\t\t\t * handled later */\n\n\t\t\t\tgoto fastcgi_request_done;\n\t\t\t}\n\n\t\t\t// 在共享内存里更新记分板信息， 也就是request开始处理的时间之类的\n\t\t\tfpm_request_executing();\n\n\t\t\t// 执行PHP脚本\n\t\t\tphp_execute_script(\u0026file_handle);\n\n```\n\n要执行PHP文件首先要找到对应的文件，然后加载一下，最后交给php_execute_script来解释执行。\n\n在执行前有一个很重要的操作，是更新记分板信息，主要是记录该子进程什么时候开始处理的请求，请求的一些基本信息是什么。\n\n这些信息对于父进程很重要，父进程根据记分板里的信息就可以知道子进程的运行情况。\n\n```\nfpm/fpm/fpm_request.c\n\n\nvoid fpm_request_executing() /* {{{ */\n{\n\tstruct fpm_scoreboard_proc_s *proc;\n\tstruct timeval now;\n\n\tfpm_clock_get(\u0026now);\n\n\tproc = fpm_scoreboard_proc_acquire(NULL, -1, 0);\n\tif (proc == NULL) {\n\t\tzlog(ZLOG_WARNING, \"failed to acquire proc scoreboard\");\n\t\treturn;\n\t}\n\n\tproc-\u003erequest_stage = FPM_REQUEST_EXECUTING;\n\tproc-\u003etv = now;\n\tfpm_scoreboard_proc_release(proc);\n}\n```\n\n先获得该进程记分板对象的锁，然后更新状态为执行中，时间点是now，然后释放锁。\n\n因为记分板是共享内存的，父进程是可以随时去查看的。\n\n```\nfpm/fpm/fpm_main.c\n\n\n\t\t\t// 记分板更新请求结束\n\t\t\tfpm_request_end();\n\t\t\tfpm_log_write(NULL);\n\n\t\t\tefree(SG(request_info).path_translated);\n\t\t\tSG(request_info).path_translated = NULL;\n\n\t\t\t// 清理PHP执行环境的东西\n\t\t\tphp_request_shutdown((void *) 0);\n\n\t\t\t// 连续处理request超过一定数量，进程退出\n\t\t\trequests++;\n\t\t\tif (UNEXPECTED(max_requests \u0026\u0026 (requests == max_requests))) {\n\t\t\t\tfcgi_request_set_keep(request, 0);\n\t\t\t\tfcgi_finish_request(request, 0);\n\t\t\t\tbreak;\n\t\t\t}\n\n```\n\n当PHP脚本执行完成后，需要fpm_request_end更新记分板请求结束，做一些状态更新，就不展开了。\n\nphp_request_shutdown清理PHP执行环境，不需要展开。\n\n下面是判断该子进程已经累计处理的请求数量，超过配置的阀值就会break退出accept loop，退出main函数结束自己的生命。这个配置项我们一般都会使用，主要是防止扩展或者PHP自身有内存泄露之类的BUG，所以定期退出一下。\n\n### 父进程进入事件循环\n\nfpm_children_create_initial函数在初始化子进程后，父进程返回1，然后进入事件循环。\n\n通常linux事件循环基于epoll实现，这里调用fpm_event_loop函数进入循环。\n\n父进程循环主要是在对子进程进行管理，比如关闭空闲的子进程，或者启动更多的子进程。\n\n另外一方面也需要监听来自命令行管理员的一些信号，比如重新加载配置，重新启动进程等。\n\n```\nfpm/fpm/fpm_event.c\n\n\n// master事件循环\nvoid fpm_event_loop(int err) /* {{{ */\n{\n\tstatic struct fpm_event_s signal_fd_event;\n\n\t/* sanity check */\n\tif (fpm_globals.parent_pid != getpid()) {\n\t\treturn;\n\t}\n\n\t// 有个pipe注册到event loop上，每次有信号触发就会写到pipe\n\tfpm_event_set(\u0026signal_fd_event, fpm_signals_get_fd(), FPM_EV_READ, \u0026fpm_got_signal, NULL);\n\tfpm_event_add(\u0026signal_fd_event, 0);\n\n\t/* add timers */\n\tif (fpm_globals.heartbeat \u003e 0) {\n\n\t\t// 创建定时器，周期性检查子进程是否执行过慢，或者超时，杀死超时进程\n\t\tfpm_pctl_heartbeat(NULL, 0, NULL);\n\t}\n\n\tif (!err) {\n\n\t\t// 创建定时器，周期性根据策略，缩减或者扩增子进程\n\t\tfpm_pctl_perform_idle_server_maintenance_heartbeat(NULL, 0, NULL);\n\n\t\tzlog(ZLOG_DEBUG, \"%zu bytes have been reserved in SHM\", fpm_shm_get_size_allocated());\n\t\tzlog(ZLOG_NOTICE, \"ready to handle connections\");\n\n#ifdef HAVE_SYSTEMD\n\t\tfpm_systemd_heartbeat(NULL, 0, NULL);\n#endif\n\t}\n\t\n\t...\n\t...\n\n```\n\n在正式进入事件循环之前，会先对信号处理做一些筹备。\n\n因为管理员可以命令行向php-fpm发送控制信号（kill -xxx），另外子进程退出会向父进程发送SIGCHLD信号，主要就是这两个行为。\n\n这里fpm_event_set,fpm_event_add都是在操作epoll，就不展开说明了。\n\nphp-fpm在初始化时就分配了一个unix socket pair，这里把socket[0]注册在epoll上监听，socket读事件的回调函数是fpm_got_signal。\n\nphp-fpm在初始化阶段就注册了信号处理函数，当fpm父进程收到信号后不会直接处理信号，而是将信号标识写入到socket[1]里，这样就会触发epoll监听到事件。\n\n### 信号处理函数\n\nfpm在初始化时这样注册了信号处理函数：\n```\nfpm/fpm/fpm_signals.c\n\n\n\t// 父进程的信号处理注册\nint fpm_signals_init_main() /* {{{ */\n{\n\tstruct sigaction act;\n\n\t// 创建一对双向双工unix socket pair\n\n\tif (0 \u003e socketpair(AF_UNIX, SOCK_STREAM, 0, sp)) {\n\t\tzlog(ZLOG_SYSERROR, \"failed to init signals: socketpair()\");\n\t\treturn -1;\n\t}\n\n\tif (0 \u003e fd_set_blocked(sp[0], 0) || 0 \u003e fd_set_blocked(sp[1], 0)) {\n\t\tzlog(ZLOG_SYSERROR, \"failed to init signals: fd_set_blocked()\");\n\t\treturn -1;\n\t}\n\n\tif (0 \u003e fcntl(sp[0], F_SETFD, FD_CLOEXEC) || 0 \u003e fcntl(sp[1], F_SETFD, FD_CLOEXEC)) {\n\t\tzlog(ZLOG_SYSERROR, \"falied to init signals: fcntl(F_SETFD, FD_CLOEXEC)\");\n\t\treturn -1;\n\t}\n\n\tmemset(\u0026act, 0, sizeof(act));\n\tact.sa_handler = sig_handler; // 收到信号，就写到unix socket里，触发主事件循环进一步处理\n\tsigfillset(\u0026act.sa_mask);\n\n\t// 来自命令行的杀死信号，来自子进程的退出信号都是重点\n\tif (0 \u003e sigaction(SIGTERM,  \u0026act, 0) ||\n\t    0 \u003e sigaction(SIGINT,   \u0026act, 0) ||\n\t    0 \u003e sigaction(SIGUSR1,  \u0026act, 0) ||\n\t    0 \u003e sigaction(SIGUSR2,  \u0026act, 0) ||\n\t    0 \u003e sigaction(SIGCHLD,  \u0026act, 0) ||\n\t    0 \u003e sigaction(SIGQUIT,  \u0026act, 0)) {\n\n\t\tzlog(ZLOG_SYSERROR, \"failed to init signals: sigaction()\");\n\t\treturn -1;\n\t}\n\treturn 0;\n}\n```\n\n它首先创建了之前说的unix socket pair用来作为信号处理函数与epoll之间的通讯机制。\n\n之后它注册了SIGTERM,SIGINT,SIGUSR1,SIGCHLD...等等信号处理函数到同一个方法：sig_handler。\n\n\n```\nfpm/fpm/fpm_signals.c\n\n\nstatic void sig_handler(int signo) /* {{{ */\n{\n\tstatic const char sig_chars[NSIG + 1] = {\n\t\t[SIGTERM] = 'T',\n\t\t[SIGINT]  = 'I',\n\t\t[SIGUSR1] = '1',\n\t\t[SIGUSR2] = '2',\n\t\t[SIGQUIT] = 'Q',\n\t\t[SIGCHLD] = 'C'\n\t};\n\tchar s;\n\tint saved_errno;\n\n\tif (fpm_globals.parent_pid != getpid()) {\n\t\t/* prevent a signal race condition when child process\n\t\t\thave not set up it's own signal handler yet */\n\t\treturn;\n\t}\n\n\tsaved_errno = errno;\n\ts = sig_chars[signo];\n\tzend_quiet_write(sp[1], \u0026s, sizeof(s)); // 写入信号对应的标识\n\terrno = saved_errno;\n}\n```\n\n该函数根据信号的类型映射到一个1字节的内部信号标识，然后写入到socket[1]里。\n\n这一步其实有点问题在于，万一socket写满了呢？ 这里并没有关注这个问题，因为一般fpm是足够快的可以处理socket里的事件的。\n\n### 处理信号事件\n\n当socket[1]写入事件标识后，epoll回调到注册的函数fpm_got_signal中。\n\n\n\n```\nfpm/fpm/fpm_events.c\n\n\n\t// 有信号发来时的事件回调函数\nstatic void fpm_got_signal(struct fpm_event_s *ev, short which, void *arg) /* {{{ */\n{\n\tchar c;\n\tint res, ret;\n\tint fd = ev-\u003efd;\n\n\tdo {\n\t\tdo {\n\t\t\tres = read(fd, \u0026c, 1);\n\t\t} while (res == -1 \u0026\u0026 errno == EINTR);\n\n\t\tif (res \u003c= 0) {\n\t\t\tif (res \u003c 0 \u0026\u0026 errno != EAGAIN \u0026\u0026 errno != EWOULDBLOCK) {\n\t\t\t\tzlog(ZLOG_SYSERROR, \"unable to read from the signal pipe\");\n\t\t\t}\n\t\t\treturn;\n\t\t}\n\n\t\tswitch (c) {\n\n\t\t\t// 收到子进程的退出信号\n\t\t\tcase 'C' :                  /* SIGCHLD */\n\t\t\t\tzlog(ZLOG_DEBUG, \"received SIGCHLD\");\n\t\t\t\tfpm_children_bury();\n\t\t\t\tbreak;\n\t\t\tcase 'I' :                  /* SIGINT  */\n\t\t\t\tzlog(ZLOG_DEBUG, \"received SIGINT\");\n\t\t\t\tzlog(ZLOG_NOTICE, \"Terminating ...\");\n\t\t\t\tfpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);\n\t\t\t\tbreak;\n\t\t\tcase 'T' :                  /* SIGTERM */\n\t\t\t\tzlog(ZLOG_DEBUG, \"received SIGTERM\");\n\t\t\t\tzlog(ZLOG_NOTICE, \"Terminating ...\");\n\t\t\t\tfpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET);\n\t\t\t\tbreak;\n\t\t\tcase 'Q' :                  /* SIGQUIT */\n\t\t\t\tzlog(ZLOG_DEBUG, \"received SIGQUIT\");\n\t\t\t\tzlog(ZLOG_NOTICE, \"Finishing ...\");\n\t\t\t\tfpm_pctl(FPM_PCTL_STATE_FINISHING, FPM_PCTL_ACTION_SET);\n\t\t\t\tbreak;\n\t\t\tcase '1' :                  /* SIGUSR1 */\n\t\t\t\tzlog(ZLOG_DEBUG, \"received SIGUSR1\");\n\t\t\t\tif (0 == fpm_stdio_open_error_log(1)) {\n\t\t\t\t\tzlog(ZLOG_NOTICE, \"error log file re-opened\");\n\t\t\t\t} else {\n\t\t\t\t\tzlog(ZLOG_ERROR, \"unable to re-opened error log file\");\n\t\t\t\t}\n\n\t\t\t\tret = fpm_log_open(1);\n\t\t\t\tif (ret == 0) {\n\t\t\t\t\tzlog(ZLOG_NOTICE, \"access log file re-opened\");\n\t\t\t\t} else if (ret == -1) {\n\t\t\t\t\tzlog(ZLOG_ERROR, \"unable to re-opened access log file\");\n\t\t\t\t}\n\t\t\t\t/* else no access log are set */\n\n\t\t\t\tbreak;\n\t\t\tcase '2' :                  /* SIGUSR2 */\n\t\t\t\tzlog(ZLOG_DEBUG, \"received SIGUSR2\");\n\t\t\t\tzlog(ZLOG_NOTICE, \"Reloading in progress ...\");\n\t\t\t\tfpm_pctl(FPM_PCTL_STATE_RELOADING, FPM_PCTL_ACTION_SET);\n\t\t\t\tbreak;\n\t\t}\n\n\t\tif (fpm_globals.is_child) {\n\t\t\tbreak;\n\t\t}\n\t} while (1);\n\treturn;\n}\n\n```\n\n该函数对不同的信号做不同的响应。\n\n例如SIGINT/SIGTERM/SIGQUIT都是来自命令行发来的退出信号，需要清理子进程然后退出，这个细节不是很重要，就不展开了。\n\n重点在于SIGCHLD信号的处理，它表示子进程退出了或者暂停了，对父进程的子进程管理非常重要。\n\n### 处理子进程事件\n\nfpm_children_bury()用于处理子进程事件，它一方面要waitpid回收子进程的资源，防止出现僵尸进程；另一方面是更新进程管理的状态，因为少了一个子进程，后续进程管理策略就可能新建子进程。\n\n```\nfpm/fpm/fpm_children.c\n\n\n这里循环回收退出的子进程资源，一直循环到没有更多子进程可以回收为止：\n\nvoid fpm_children_bury() /* {{{ */\n{\n\tint status;\n\tpid_t pid;\n\tstruct fpm_child_s *child;\n\n\t// 循环回收子进程资源，直到没有更多\n\twhile ( (pid = waitpid(-1, \u0026status, WNOHANG | WUNTRACED)) \u003e 0) {\n\t\tchar buf[128];\n\t\tint severity = ZLOG_NOTICE;\n\t\tint restart_child = 1;\n\n\t\t// 根据子进程PID找到对应的child对象\n\t\tchild = fpm_child_find(pid);\n\n\n```\n\n根据waitpid返回的子进程pid，就可以找到对应的child对象，里面维护了描述子进程的一些信息，由父进程管理。\n\n当收到SIGCHLD信号时，我们可以根据waitpid第二个status参数获知进程是如何退出的。\n\n### 子进程正常退出\n\n```\nfpm/fpm/fpm_children.c\n\n\n\t\tif (WIFEXITED(status)) { // 正常退出\n\n\t\t\tsnprintf(buf, sizeof(buf), \"with code %d\", WEXITSTATUS(status));\n\n\t\t\t/* if it's been killed because of dynamic process management\n\t\t\t * don't restart it automaticaly\n\t\t\t */\n\t\t\tif (child \u0026\u0026 child-\u003eidle_kill) {\n\t\t\t\trestart_child = 0;\n\t\t\t}\n\n\t\t\tif (WEXITSTATUS(status) != FPM_EXIT_OK) {\n\t\t\t\tseverity = ZLOG_WARNING;\n\t\t\t}\n\n\t\t} \n\t\t\n```\n\n如果是正常退出，那么说明子进程是通过main函数return或者exit方法退出的。\n\n这种情况下，其实还需要区分是子进程自己主动退出的，还是父进程让它退出的。\n\n所以child-\u003eidle_kill做了一次判断，因为父进程若主动杀死子进程，那么会先在child对象里做一下idle_kill的标记再向子进程发送杀死信号。\n\n这个判定决定了是否要立即重启子进程，若不是父进程责令其退出，那么就是意外退出，需要立即拉起。\n\n### 子进程被信号杀死\n\n这个场景非常类似于正常退出，当进程收到某些信号时默认的行为就是退出，比如SIGKILL强制杀死，SIGSEGV段错误，SIGBUS总线错误，SIGQUIT退出 等等..\n\n这种情况下会区分一下是否是段错误等严重错误，一般预示着PHP内核或者扩展代码有问题导致coredump。\n\n```\nfpm/fpm/fpm_children.c\n\n\nelse if (WIFSIGNALED(status)) { // 被信号杀死\n\t\t\tconst char *signame = fpm_signal_names[WTERMSIG(status)];\n\t\t\tconst char *have_core = WCOREDUMP(status) ? \" - core dumped\" : \"\";\n\n\t\t\tif (signame == NULL) {\n\t\t\t\tsigname = \"\";\n\t\t\t}\n\n\t\t\tsnprintf(buf, sizeof(buf), \"on signal %d (%s%s)\", WTERMSIG(status), signame, have_core);\n\n\t\t\t/* if it's been killed because of dynamic process management\n\t\t\t * don't restart it automaticaly\n\t\t\t */\n\t\t\tif (child \u0026\u0026 child-\u003eidle_kill \u0026\u0026 WTERMSIG(status) == SIGQUIT) {\n\t\t\t\trestart_child = 0;\n\t\t\t}\n\n\t\t\tif (WTERMSIG(status) != SIGQUIT) { /* possible request loss */\n\t\t\t\tseverity = ZLOG_WARNING;\n\t\t\t}\n\t\t}\n```\n\n### 子进程暂停\n\n子进程收到SIGSTOP信号就会暂停执行，此时父进程会收到SIGCHLD信号，并且status中标识了子进程是STOP状态。\n\n\n```\nfpm/fpm/fpm_children.c\n\n\nelse if (WIFSTOPPED(status)) {\t// slowlog时ptrace子进程，导致子进程STOP暂停\n\n\t\t\tzlog(ZLOG_NOTICE, \"child %d stopped for tracing\", (int) pid);\n\n\t\t\tif (child \u0026\u0026 child-\u003etracer) {\n\t\t\t\t// 获取子进程的信息，打印到slowlog日志，然后恢复子进程\n\t\t\t\tchild-\u003etracer(child);\n\t\t\t}\n\n\t\t\tcontinue;\n\t\t}\n\t\t\n```\n\n那么谁会给子进程发送STOP信号呢？ 这里先简单提一下，就是当父进程发现子进程处理一个请求超时后，就会调用ptrace去attach到子进程，这个操作就会导致子进程STOP。\n\n一旦ptrace导致子进程STOP，那么父进程就会收到SIGCHLD，从而进入上述逻辑分支。\n\n父进程要做的，就是利用ptrace的其他能力，直接去访问子进程的地址空间，获取一些堆栈信息，从而获知子进程到底卡在哪里。\n\n而上述所说的ptrace逻辑，实际上就是为了打印slowlog，也就是当父进程发现子进程执行慢，就利用ptrace去抓子进程的栈空间，从而打印出一个调用栈到slowlog日志文件中，帮助我们分析问题，这个原理和gdb调试程序是类似的。\n\n当然，child-\u003etracer除了利用Ptrace去抓子进程的堆栈之后，会向子进程发送一个SIGCONT信号，让子进程恢复运行，相关代码在后面会提及。\n\n### 回收子进程资源\n\n如果子进程是退出而不是暂停了，那么就要在父进程里清理相关的进程信息与资源。\n\n```\nfpm/fpm/fpm_children.c\n\n\n\t// 子进程退出，那么清理父进程里关联的各种内存\n\t\tif (child) {\n\t\t\tstruct fpm_worker_pool_s *wp = child-\u003ewp;\n\t\t\tstruct timeval tv1, tv2;\n\n\t\t\tfpm_child_unlink(child);\n\n\t\t\tfpm_scoreboard_proc_free(wp-\u003escoreboard, child-\u003escoreboard_i);\n\n\t\t\tfpm_clock_get(\u0026tv1);\n\n\t\t\ttimersub(\u0026tv1, \u0026child-\u003estarted, \u0026tv2);\n```\n\n例如上述清理了记分板资源，等等...\n\n### 严重错误重启自身\n\n```\nfpm/fpm/fpm_children.c\n\n\n\t\t\tif (last_faults \u0026\u0026 (WTERMSIG(status) == SIGSEGV || WTERMSIG(status) == SIGBUS)) {\n\t\t\t\ttime_t now = tv1.tv_sec;\n\t\t\t\tint restart_condition = 1;\n\t\t\t\tint i;\n\n\t\t\t\tlast_faults[fault++] = now;\n\n\t\t\t\tif (fault == fpm_global_config.emergency_restart_threshold) {\n\t\t\t\t\tfault = 0;\n\t\t\t\t}\n\n\t\t\t\tfor (i = 0; i \u003c fpm_global_config.emergency_restart_threshold; i++) {\n\t\t\t\t\tif (now - last_faults[i] \u003e fpm_global_config.emergency_restart_interval) {\n\t\t\t\t\t\trestart_condition = 0;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// COREDUMP太多，决定重启php-fpm，也就是直接execv执行php-fpm自身\n\t\t\t\tif (restart_condition) {\n\n\t\t\t\t\tzlog(ZLOG_WARNING, \"failed processes threshold (%d in %d sec) is reached, initiating reload\", fpm_global_config.emergency_restart_threshold, fpm_global_config.emergency_restart_interval);\n\n\t\t\t\t\tfpm_pctl(FPM_PCTL_STATE_RELOADING, FPM_PCTL_ACTION_SET);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// 有一些子进程退出场景，是需要立即重新拉起新的子进程的\n\t\t\tif (restart_child) {\n\t\t\t\tfpm_children_make(wp, 1 /* in event loop */, 1, 0);\n\n\t\t\t\tif (fpm_globals.is_child) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t}\n```\n\n紧接着，如果一段时间内段错误等严重致命问题连续出现，那么可能PHP-FPM已经因为某些程序bug原因写坏了内存，进入了一种万劫不复的状态。\n\n此时，满足了restart_condition=1，那么就会标记PHP-FPM进程为RELOADING状态，也就是准备重启PHP-FPM自己。\n\n重启的方法就是定时器检测到php-fpm状态为reloading，那么直接execv再次执行php-fpm二进制即可：\n\n```\nfpm/fpm/fpm_process_ctl.c\n\n\nstatic void fpm_pctl_exec() /* {{{ */\n{\n\n\tzlog(ZLOG_NOTICE, \"reloading: execvp(\\\"%s\\\", {\\\"%s\\\"\"\n\t\t\t\"%s%s%s\" \"%s%s%s\" \"%s%s%s\" \"%s%s%s\" \"%s%s%s\"\n\t\t\t\"%s%s%s\" \"%s%s%s\" \"%s%s%s\" \"%s%s%s\" \"%s%s%s\"\n\t\t\"})\",\n\t\tsaved_argv[0], saved_argv[0],\n\t\toptional_arg(1),\n\t\toptional_arg(2),\n\t\toptional_arg(3),\n\t\toptional_arg(4),\n\t\toptional_arg(5),\n\t\toptional_arg(6),\n\t\toptional_arg(7),\n\t\toptional_arg(8),\n\t\toptional_arg(9),\n\t\toptional_arg(10)\n\t);\n\n\tfpm_cleanups_run(FPM_CLEANUP_PARENT_EXEC);\n\texecvp(saved_argv[0], saved_argv);\n\tzlog(ZLOG_SYSERROR, \"failed to reload: execvp() failed\");\n\texit(FPM_EXIT_SOFTWARE);\n}\n```\n\n另外，如果此前判定子进程是异常退出，那么restart_child=1，则会立即拉起一个新进程补充起来。\n\n### 定时器 -- 子进程健康检查\n\n对于已经创建的子进程，父进程会在事件循环中创建一个定时器，定时的进行全量的扫描。\n\n目标是发现执行过慢的请求，进行对应的处理。\n\n```\nfpm/fpm/fpm_events.c\n\n\n// master事件循环\nvoid fpm_event_loop(int err) /* {{{ */\n{\n\t...\n\t\n\t...\n\t\n\t\n\t/* add timers */\n\tif (fpm_globals.heartbeat \u003e 0) {\n\n\t\t// 创建定时器，周期性检查子进程是否执行过慢，或者超时，杀死超时进程\n\t\tfpm_pctl_heartbeat(NULL, 0, NULL);\n\t}\n\t\n\t\n\tif (!err) {\n\n\t\t// 创建定时器，周期性根据策略，缩减或者扩增子进程\n\t\tfpm_pctl_perform_idle_server_maintenance_heartbeat(NULL, 0, NULL);\n\n\t\tzlog(ZLOG_DEBUG, \"%zu bytes have been reserved in SHM\", fpm_shm_get_size_allocated());\n\t\tzlog(ZLOG_NOTICE, \"ready to handle connections\");\n\n#ifdef HAVE_SYSTEMD\n\t\tfpm_systemd_heartbeat(NULL, 0, NULL);\n#endif\n\t}\n\n```\n\n这里创建了两个定时器，先说第一个定时器fpm_pctl_heartbeat。\n\n```\nfpm/fpm/fpm_process_ctl.c\n\n\n\t// 心跳处理函数,\nvoid fpm_pctl_heartbeat(struct fpm_event_s *ev, short which, void *arg) /* {{{ */\n{\n\tstatic struct fpm_event_s heartbeat;\n\tstruct timeval now;\n\n\tif (fpm_globals.parent_pid != getpid()) {\n\t\treturn; /* sanity check */\n\t}\n\n\t// 如果是心跳回调事件, 那么进入处理流程\n\tif (which == FPM_EV_TIMEOUT) {\n\t\tfpm_clock_get(\u0026now);\n\t\tfpm_pctl_check_request_timeout(\u0026now);\n\t\treturn;\n\t}\n\n\t/* ensure heartbeat is not lower than FPM_PCTL_MIN_HEARTBEAT */\n\t// 心跳间隔\n\tfpm_globals.heartbeat = MAX(fpm_globals.heartbeat, FPM_PCTL_MIN_HEARTBEAT);\n\n\t/* first call without setting to initialize the timer */\n\t// 初始只注册一次定时器\n\tzlog(ZLOG_DEBUG, \"heartbeat have been set up with a timeout of %dms\", fpm_globals.heartbeat);\n\tfpm_event_set_timer(\u0026heartbeat, FPM_EV_PERSIST, \u0026fpm_pctl_heartbeat, NULL);\n\tfpm_event_add(\u0026heartbeat, fpm_globals.heartbeat);\n}\n```\n\n该函数既是定时器的回调函数，也是定时器的初始化注册函数。\n\n当定时器回调时，会进入if (which == FPM_EV_TIMEOUT)分支执行逻辑；否则就是第一次注册定时器。\n\n进程检测算法在fpm_pctl_check_request_timeout中实现：\n\n```\nfpm/fpm/fpm_process_ctl.c\n\n\nstatic void fpm_pctl_check_request_timeout(struct timeval *now) /* {{{ */\n{\n\tstruct fpm_worker_pool_s *wp;\n\n\t// 检查每个池子\n\tfor (wp = fpm_worker_all_pools; wp; wp = wp-\u003enext) {\n\t\tint terminate_timeout = wp-\u003econfig-\u003erequest_terminate_timeout;\n\t\tint slowlog_timeout = wp-\u003econfig-\u003erequest_slowlog_timeout;\n\t\tstruct fpm_child_s *child;\n\n\t\t// 每个池子内所有子进程\n\t\tif (terminate_timeout || slowlog_timeout) {\n\t\t\tfor (child = wp-\u003echildren; child; child = child-\u003enext) {\n\n\t\t\t\t// 检查是否请求处理超时\n\t\t\t\tfpm_request_check_timed_out(child, now, terminate_timeout, slowlog_timeout);\n\t\t\t}\n\t\t}\n\t}\n}\n```\n\n逻辑上就是遍历所有池子里的所有子进程，逐一调用fpm_request_check_time_out方法检查：\n\n```\nfpm/fpm/fpm_request.c\n\n\nvoid fpm_request_check_timed_out(struct fpm_child_s *child, struct timeval *now, int terminate_timeout, int slowlog_timeout) /* {{{ */\n{\n\tstruct fpm_scoreboard_proc_s proc, *proc_p;\n\n\t// 获得子进程的记分板\n\tproc_p = fpm_scoreboard_proc_acquire(child-\u003ewp-\u003escoreboard, child-\u003escoreboard_i, 1);\n\tif (!proc_p) {\n\t\tzlog(ZLOG_WARNING, \"failed to acquire scoreboard\");\n\t\treturn;\n\t}\n\n\t// 拷贝一份当前信息\n\tproc = *proc_p;\n\n\t// 释放子进程记分板\n\tfpm_scoreboard_proc_release(proc_p);\n\n#if HAVE_FPM_TRACE\n\tif (child-\u003eslow_logged.tv_sec) {\n\t\tif (child-\u003eslow_logged.tv_sec != proc.accepted.tv_sec || child-\u003eslow_logged.tv_usec != proc.accepted.tv_usec) {\n\t\t\tchild-\u003eslow_logged.tv_sec = 0;\n\t\t\tchild-\u003eslow_logged.tv_usec = 0;\n\t\t}\n\t}\n#endif\n\n\t// 检查子进程是否存在超时问题\n\tif (proc.request_stage \u003e FPM_REQUEST_ACCEPTING \u0026\u0026 proc.request_stage \u003c FPM_REQUEST_END) {\n\t\tchar purified_script_filename[sizeof(proc.script_filename)];\n\t\tstruct timeval tv;\n\n\t\t// 当前时间减去连接接收时间\n\t\ttimersub(now, \u0026proc.accepted, \u0026tv);\n\n#if HAVE_FPM_TRACE\n\n\t\t// 检查是否执行时间触发slow log阀值\n\t\tif (child-\u003eslow_logged.tv_sec == 0 \u0026\u0026 slowlog_timeout \u0026\u0026\n\t\t\t\tproc.request_stage == FPM_REQUEST_EXECUTING \u0026\u0026 tv.tv_sec \u003e= slowlog_timeout) {\n\n\t\t\tstr_purify_filename(purified_script_filename, proc.script_filename, sizeof(proc.script_filename));\n\n\t\t\tchild-\u003eslow_logged = proc.accepted;\n\t\t\tchild-\u003etracer = fpm_php_trace; // 当收到子进程的SIGSTOP信号后，需要通过fpm_php_trace函数来获取子进程的栈信息\n\n\t\t\t// 这里attach到子进程上，目的是获取子进程的PHP栈，需要等待子进程发出SIGSTOP信号\n\t\t\tfpm_trace_signal(child-\u003epid);\n\n\t\t\tzlog(ZLOG_WARNING, \"[pool %s] child %d, script '%s' (request: \\\"%s %s%s%s\\\") executing too slow (%d.%06d sec), logging\",\n\t\t\t\tchild-\u003ewp-\u003econfig-\u003ename, (int) child-\u003epid, purified_script_filename, proc.request_method, proc.request_uri,\n\t\t\t\t(proc.query_string[0] ? \"?\" : \"\"), proc.query_string,\n\t\t\t\t(int) tv.tv_sec, (int) tv.tv_usec);\n\t\t}\n\t\telse\n#endif\n        // 是否执行超时\n\t\tif (terminate_timeout \u0026\u0026 tv.tv_sec \u003e= terminate_timeout) {\n\t\t\tstr_purify_filename(purified_script_filename, proc.script_filename, sizeof(proc.script_filename));\n\n\t\t\t// 给子进程发SIGTERM信号杀死\n\t\t\tfpm_pctl_kill(child-\u003epid, FPM_PCTL_TERM);\n\n\t\t\tzlog(ZLOG_WARNING, \"[pool %s] child %d, script '%s' (request: \\\"%s %s%s%s\\\") execution timed out (%d.%06d sec), terminating\",\n\t\t\t\tchild-\u003ewp-\u003econfig-\u003ename, (int) child-\u003epid, purified_script_filename, proc.request_method, proc.request_uri,\n\t\t\t\t(proc.query_string[0] ? \"?\" : \"\"), proc.query_string,\n\t\t\t\t(int) tv.tv_sec, (int) tv.tv_usec);\n\t\t}\n\t}\n}\n```\n\n首先要加锁获取该子进程记分板信息的一份拷贝，然后就释放掉锁，进入检查环节。\n\n记分板里记录了子进程当前的状态，如果\u003eACCEPTING \u0026\u0026 \u003c REQUEST_END表示正在处理请求，那么就可以检查这个进程是不是处理请求花费了太久的时间。\n\n首先是判断子进程请求处理事件是否超过slowlog的阀值，那么就会调用fpm_trace_signal去attach到子进程上，内部就是调用ptrace而已。\n\n这里注意child-\u003etrace之前提到过，它具体实现在fpm_php_trace中，当父进程收到SIGCHLD并且子进程是STOP状态就会回调child-\u003etrace方法，从而从ptrace中抓取子进程的堆栈信息，这里就不展开了。\n\n接下来检测了一下请求花费时间是否过长，这种情况属于极端异常，父进程的做法就是杀死子进程，这是通过发送SIGTERM信号实现的。在发送信号前并没有标记child-\u003eidle_kill，说明子进程死后父进程希望可以立即拉起来，因为子进程只是BUG卡住了之类的。\n\n### 定时器 -- 子进程伸缩管理\n\n前一个定时检查运行中的子进程状态，而该定时器fpm_pctl_perform_idle_server_maintenance_heartbeat是判断是否有必要新增子进程，或者杀死过多的空闲子进程。\n\n```\nfpm/fpm/fpm_process_ctl.c\n\n\n// 定时器，子进程空闲杀死/新增的检查逻辑\nvoid fpm_pctl_perform_idle_server_maintenance_heartbeat(struct fpm_event_s *ev, short which, void *arg) /* {{{ */\n{\n\tstatic struct fpm_event_s heartbeat;\n\tstruct timeval now;\n\n\tif (fpm_globals.parent_pid != getpid()) {\n\t\treturn; /* sanity check */\n\t}\n\n\tif (which == FPM_EV_TIMEOUT) {\n\t\tfpm_clock_get(\u0026now);\n\t\tif (fpm_pctl_can_spawn_children()) {\n\t\t\tfpm_pctl_perform_idle_server_maintenance(\u0026now);\n\n\t\t\t/* if it's a child, stop here without creating the next event\n\t\t\t * this event is reserved to the master process\n\t\t\t */\n\t\t\tif (fpm_globals.is_child) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\treturn;\n\t}\n\n\t/* first call without setting which to initialize the timer */\n\tfpm_event_set_timer(\u0026heartbeat, FPM_EV_PERSIST, \u0026fpm_pctl_perform_idle_server_maintenance_heartbeat, NULL);\n\tfpm_event_add(\u0026heartbeat, FPM_IDLE_SERVER_MAINTENANCE_HEARTBEAT);\n}\n```\n\n每当定时器被回调进入到if (which == FPM_EV_TIMEOUT)，则调用fpm_pctl_perform_idle_server_maintenance方法进行逻辑处理。\n\n\n```\nfpm/fpm/fpm_process_ctl.c\n\n\nstatic void fpm_pctl_perform_idle_server_maintenance(struct timeval *now) /* {{{ */\n{\n\tstruct fpm_worker_pool_s *wp;\n\n\t// 遍历每个池子\n\tfor (wp = fpm_worker_all_pools; wp; wp = wp-\u003enext) {\n\t\tstruct fpm_child_s *child;\n\t\tstruct fpm_child_s *last_idle_child = NULL;\n\t\tint idle = 0;\n\t\tint active = 0;\n\t\tint children_to_fork;\n\t\tunsigned cur_lq = 0;\n\n\t\tif (wp-\u003econfig == NULL) continue;\n\n\t\t// 遍历每个子进程\n\t\tfor (child = wp-\u003echildren; child; child = child-\u003enext) {\n\t\t\t//  如果子进程空闲（等待连接中）\n\t\t\tif (fpm_request_is_idle(child)) {\n\t\t\t\t// 找出闲的最久的子进程\n\t\t\t\tif (last_idle_child == NULL) {\n\t\t\t\t\tlast_idle_child = child;\n\t\t\t\t} else {\n\t\t\t\t\tif (timercmp(\u0026child-\u003estarted, \u0026last_idle_child-\u003estarted, \u003c)) {\n\t\t\t\t\t\tlast_idle_child = child;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tidle++;\n\t\t\t} else {\n\t\t\t\tactive++;\n\t\t\t}\n\t\t}\n\n\t\t/* update status structure for all PMs */\n\t\t// 获取一下TCP的连接握手队列有几个排队\n\t\tif (wp-\u003elisten_address_domain == FPM_AF_INET) {\n\t\t\tif (0 \u003e fpm_socket_get_listening_queue(wp-\u003elistening_socket, \u0026cur_lq, NULL)) {\n\t\t\t\tcur_lq = 0;\n#if 0\n\t\t\t} else {\n\t\t\t\tif (cur_lq \u003e 0) {\n\t\t\t\t\tif (!wp-\u003ewarn_lq) {\n\t\t\t\t\t\tzlog(ZLOG_WARNING, \"[pool %s] listening queue is not empty, #%d requests are waiting to be served, consider raising pm.max_children setting (%d)\", wp-\u003econfig-\u003ename, cur_lq, wp-\u003econfig-\u003epm_max_children);\n\t\t\t\t\t\twp-\u003ewarn_lq = 1;\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\twp-\u003ewarn_lq = 0;\n\t\t\t\t}\n#endif\n\t\t\t}\n\t\t}\n\n\t\t// 把这次统计的各种信息，更新到池子的记分板上\n\t\tfpm_scoreboard_update(idle, active, cur_lq, -1, -1, -1, 0, FPM_SCOREBOARD_ACTION_SET, wp-\u003escoreboard);\n\n```\n\n该函数外层也是遍历所有池子，对于每个池子进行统计。\n\n主要是统计有多少个子进程在处理请求，有多个子进程空闲，并且找出空闲最久的那个子进程。\n\n然后调用linux api获取了一下监听套接字listen socket的tcp握手队列的堆积长度，如果排队的比较多则预示着子进程不足，来不及处理更多的请求。\n\n上述统计信息会被更新到池子对应的记分板上。\n\n### 子进程管理策略 -- ON DEMAND\n\n我们知道php-fpm有3种进程管理模型，on demand是按需分配，也就是初始化给池子里分配1个子进程，如果子进程来不及处理请求就再增加子进程。\n\n```\nfpm/fpm/fpm_process_ctl.c\n\n\n\t\t// 按需分配进程，所以如果有哪个子进程闲太久了，就干掉\n\t\tif (wp-\u003econfig-\u003epm == PM_STYLE_ONDEMAND) {\n\t\t\tstruct timeval last, now;\n\n\t\t\tzlog(ZLOG_DEBUG, \"[pool %s] currently %d active children, %d spare children\", wp-\u003econfig-\u003ename, active, idle);\n\n\t\t\tif (!last_idle_child) continue;\n\n\t\t\t// 闲最久的那个进程超过了空闲阀值，杀死\n\t\t\tfpm_request_last_activity(last_idle_child, \u0026last);\n\t\t\tfpm_clock_get(\u0026now);\n\t\t\tif (last.tv_sec \u003c now.tv_sec - wp-\u003econfig-\u003epm_process_idle_timeout) {\n\t\t\t\tlast_idle_child-\u003eidle_kill = 1;\n\t\t\t\tfpm_pctl_kill(last_idle_child-\u003epid, FPM_PCTL_QUIT);\n\t\t\t}\n\n\t\t\tcontinue;\n\t\t}\n\n```\n\n因为之前统计出空闲最久的子进程是哪个，如果这个子进程处于空闲状态超过阀值，就给它发送SIGQUIT信号杀死它，这就是收缩过程，因为流量并不大，子进程也不忙。\n\n\n### 子进程管理策略 -- STATIC\n\n静态模式，也就是固定数量的子进程，这种情况下不需要进行子进程伸缩。\n\n之前的健康检查定时器会在子进程退出后立即重新拉起，来保证子进程数量恒定不变。\n\n```\nfpm/fpm/fpm_process_ctl.c\n\n\n\t\t// 固定进程数的就此退出，不需要执行后续逻辑\n\t\tif (wp-\u003econfig-\u003epm != PM_STYLE_DYNAMIC) continue;\n\n\t\tzlog(ZLOG_DEBUG, \"[pool %s] currently %d active children, %d spare children, %d running children. Spawning rate %d\", wp-\u003econfig-\u003ename, active, idle, wp-\u003erunning_children, wp-\u003eidle_spawn_rate);\n\n```\n\n###  子进程管理策略 -- Dynamic\n\n动态模式，这种配置指定了初始子进程数量，最小空闲进程数量，最大空闲进程数量，最多进程数量，是一种规则比较复杂，但资源控制比较优秀的方法。\n\n```\nfpm/fpm/fpm_process_ctl.c\n\n\n\t\t// 空闲进程数量大于了配置中的空闲最大值，那么干掉闲最久的进程\n\t\tif (idle \u003e wp-\u003econfig-\u003epm_max_spare_servers \u0026\u0026 last_idle_child) {\n\t\t\tlast_idle_child-\u003eidle_kill = 1;\n\t\t\tfpm_pctl_kill(last_idle_child-\u003epid, FPM_PCTL_QUIT);\n\t\t\twp-\u003eidle_spawn_rate = 1;\n\t\t\tcontinue;\n\t\t}\n```\n\n如果空闲的进程数量超过了最大空闲数量限制，就杀死最闲的那个。\n\n```\nfpm/fpm/fpm_process_ctl.c\n\n\n\t\t// 空闲进程数量小于配置中的空闲最小值\n\t\tif (idle \u003c wp-\u003econfig-\u003epm_min_spare_servers) {\n\n\t\t\t// 孩子总数虽然超过了配置中的最大进程数量，但是因为空闲的进程数量不多，说明负载很高，只是打日志提示一下\n\t\t\tif (wp-\u003erunning_children \u003e= wp-\u003econfig-\u003epm_max_children) {\n\t\t\t\tif (!wp-\u003ewarn_max_children) {\n\t\t\t\t\tfpm_scoreboard_update(0, 0, 0, 0, 0, 1, 0, FPM_SCOREBOARD_ACTION_INC, wp-\u003escoreboard);\n\t\t\t\t\tzlog(ZLOG_WARNING, \"[pool %s] server reached pm.max_children setting (%d), consider raising it\", wp-\u003econfig-\u003ename, wp-\u003econfig-\u003epm_max_children);\n\t\t\t\t\twp-\u003ewarn_max_children = 1;\n\t\t\t\t}\n\t\t\t\twp-\u003eidle_spawn_rate = 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\n```\n\n如果空闲进程数量小于最小空闲进程限制，说明目前流量比较大，没有充足的空闲进程响应更多请求。 \n\n按照道理，此时应该增加更多子进程来缓解压力，但是如果进程总数量超过了最大进程数量的限制，那么是不能扩容的，此时只是打印一个日志警告而已。\n\n```\nfpm/fpm/fpm_process_ctl.c\n\n\n\t// 算一下要补充多少子进程\n\t\t\tchildren_to_fork = MIN(wp-\u003eidle_spawn_rate, wp-\u003econfig-\u003epm_min_spare_servers - idle);\n\n\t\t\t/* get sure it won't exceed max_children */\n\t\t\tchildren_to_fork = MIN(children_to_fork, wp-\u003econfig-\u003epm_max_children - wp-\u003erunning_children);\n\t\t\tif (children_to_fork \u003c= 0) {\n\t\t\t\tif (!wp-\u003ewarn_max_children) {\n\t\t\t\t\tfpm_scoreboard_update(0, 0, 0, 0, 0, 1, 0, FPM_SCOREBOARD_ACTION_INC, wp-\u003escoreboard);\n\t\t\t\t\tzlog(ZLOG_WARNING, \"[pool %s] server reached pm.max_children setting (%d), consider raising it\", wp-\u003econfig-\u003ename, wp-\u003econfig-\u003epm_max_children);\n\t\t\t\t\twp-\u003ewarn_max_children = 1;\n\t\t\t\t}\n\t\t\t\twp-\u003eidle_spawn_rate = 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\twp-\u003ewarn_max_children = 0;\n\n\t\t\t// 拉起children_to_fork个子进程\n\t\t\tfpm_children_make(wp, 1, children_to_fork, 1);\n```\n\n相反，如果此时没有达到最大进程数量限制，那么就可以通过扩容子进程缓解压力。\n\n这里做了一些规则计算，细节并不重要，总之可以新建的子进程数+现有进程数量不能超过总进程数限制。\n\n最后调用fpm_children_make方法创建这些子进程，该函数之前已经讲过。\n\n### 最后 -- 事件循环起来\n\nphp-fpm父进程负责子进程管理，通过信号的方式与子进程通讯，从而实现强控子进程的能力。\n\n父进程采取了事件循环来同时实现多个逻辑的并发处理：监测子进程的标准输出、标准错误输出，监测信号，定时器。\n\n当fpm主进程将一切准备就绪，包括1个信号管道，2个常规定时器准备就绪后，就会进入正式的epoll事件循环。\n\n### 定时器的前置处理\n\n```\nfpm/fpm/fpm_events.c\n\n\nwhile (1) {\n\t\tstruct fpm_event_queue_s *q, *q2;\n\t\tstruct timeval ms;\n\t\tstruct timeval tmp;\n\t\tstruct timeval now;\n\t\tunsigned long int timeout;\n\t\tint ret;\n\n\t\t/* sanity check */\n\t\tif (fpm_globals.parent_pid != getpid()) {\n\t\t\treturn;\n\t\t}\n\n\t\tfpm_clock_get(\u0026now);\n\t\ttimerclear(\u0026ms);\n\n\t\t/* search in the timeout queue for the next timer to trigger */\n\n\t\t// 找到最近一个要到期的定时器，作为event loop超时时间\n\t\tq = fpm_event_queue_timer;\n\t\twhile (q) {\n\t\t\tif (!timerisset(\u0026ms)) {\n\t\t\t\tms = q-\u003eev-\u003etimeout;\n\t\t\t} else {\n\t\t\t\tif (timercmp(\u0026q-\u003eev-\u003etimeout, \u0026ms, \u003c)) {\n\t\t\t\t\tms = q-\u003eev-\u003etimeout;\n\t\t\t\t}\n\t\t\t}\n\t\t\tq = q-\u003enext;\n\t\t}\n\n\t\t/* 1s timeout if none has been set */\n\t\tif (!timerisset(\u0026ms) || timercmp(\u0026ms, \u0026now, \u003c) || timercmp(\u0026ms, \u0026now, ==)) {\n\t\t\ttimeout = 1000;\n\t\t} else {\n\t\t\ttimersub(\u0026ms, \u0026now, \u0026tmp);\n\t\t\ttimeout = (tmp.tv_sec * 1000) + (tmp.tv_usec / 1000) + 1;\n\t\t}\n```\n\n所有的定时器串在一个fpm_event_queue_timer链表里，首先找到最近要到期的那个定时器，计算得到它距离现在还有多久会到期，保存在timeout里。\n\n然后将timeout作为epoll的超时事件，这样避免epoll平时没有事件触发挂起，导致定时器无法处理。这个设计是任何一款异步网络框架都会涉及的，有相关经验同学不会觉得陌生。\n\n### 调用epoll等待事件触发\n\n```\nfpm/fpm/fpm_events.c\n\n\n\t\t// 监听多个fd的事件循环, 回调fd的事件处理函数\n\t\tret = module-\u003ewait(fpm_event_queue_fd, timeout);\n\n\t\t/* is a child, nothing to do here */\n\t\tif (ret == -2) {\n\t\t\treturn;\n\t\t}\n```\n\nwait其实等价于调用epoll_wait，内部会根据发生事件的fd回调注册的函数，这里可能主要就是我们之前提到的信号unix socket pair，用于响应信号，包括子进程退出的信号。\n\n### 定时器的执行\n\n当fd的事件经过epoll_wait处理完成后，我们需要遍历所有定时器，查看哪些定时器过期需要执行，仅此而已。\n\n超时的定时器需要调用fpm_event_fire回调当时注册的方法，也就是我们之前谈到的2个常规定时器。\n\n因为上述2个定时都是常规定时器，所以如果ev-\u003eflags \u0026 FPM_EV_PERSIST非空，则表示这是一个常规定时器，需要重新注册到定时链表，等待下次调度。\n\n```\n// 遍历所有注册的定时器\n\t\tq = fpm_event_queue_timer;\n\t\twhile (q) {\n\t\t\tfpm_clock_get(\u0026now);\n\t\t\tif (q-\u003eev) {\n\n\t\t\t\t// 到期的就回调\n\t\t\t\tif (timercmp(\u0026now, \u0026q-\u003eev-\u003etimeout, \u003e) || timercmp(\u0026now, \u0026q-\u003eev-\u003etimeout, ==)) {\n\n\t\t\t\t\t// 回调用户函数\n\t\t\t\t\tfpm_event_fire(q-\u003eev);\n\n\t\t\t\t\t/* sanity check */\n\t\t\t\t\tif (fpm_globals.parent_pid != getpid()) {\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// 如果是持久化的定时器，那么再次注册回去等待下次触发\n\t\t\t\t\tif (q-\u003eev-\u003eflags \u0026 FPM_EV_PERSIST) {\n\t\t\t\t\t\tfpm_event_set_timeout(q-\u003eev, now);\n\t\t\t\t\t} else { /* delete the event */\n\t\t\t\t\t\tq2 = q;\n\t\t\t\t\t\tif (q-\u003eprev) {\n\t\t\t\t\t\t\tq-\u003eprev-\u003enext = q-\u003enext;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (q-\u003enext) {\n\t\t\t\t\t\t\tq-\u003enext-\u003eprev = q-\u003eprev;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (q == fpm_event_queue_timer) {\n\t\t\t\t\t\t\tfpm_event_queue_timer = q-\u003enext;\n\t\t\t\t\t\t\tif (fpm_event_queue_timer) {\n\t\t\t\t\t\t\t\tfpm_event_queue_timer-\u003eprev = NULL;\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\tq = q-\u003enext;\n\t\t\t\t\t\tfree(q2);\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tq = q-\u003enext;\n\t\t}\n```\n\n## 结束\n\nPHP-FPM围绕进程管理设计实现，基于共享内存的记分板实现子进程状态检测，子进程采用阻塞模型，父进程基于信号控制子进程管理，整体设计保持简单纯粹。\n\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fowenliang%2Fphp-fpm-code-analysis","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fowenliang%2Fphp-fpm-code-analysis","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fowenliang%2Fphp-fpm-code-analysis/lists"}