└── README.md /README.md: -------------------------------------------------------------------------------- 1 | # PHP-FPM源码分析 2 | ## 入口文件 3 | 4 | ``` 5 | fpm/fpm/fpm_main.c 6 | 7 | 8 | int main(int argc, char *argv[]) { 9 | 10 | ... 11 | ... 12 | 13 | // 初始化 14 | if (0 > 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)) { 15 | 16 | if (fpm_globals.send_config_pipe[1]) { 17 | int writeval = 0; 18 | zlog(ZLOG_DEBUG, "Sending \"0\" (error) to parent via fd=%d", fpm_globals.send_config_pipe[1]); 19 | zend_quiet_write(fpm_globals.send_config_pipe[1], &writeval, sizeof(writeval)); 20 | close(fpm_globals.send_config_pipe[1]); 21 | } 22 | return FPM_EXIT_CONFIG; 23 | } 24 | 25 | if (fpm_globals.send_config_pipe[1]) { 26 | int writeval = 1; 27 | zlog(ZLOG_DEBUG, "Sending \"1\" (OK) to parent via fd=%d", fpm_globals.send_config_pipe[1]); 28 | zend_quiet_write(fpm_globals.send_config_pipe[1], &writeval, sizeof(writeval)); 29 | close(fpm_globals.send_config_pipe[1]); 30 | } 31 | fpm_is_running = 1; 32 | 33 | // 这里父进程创建监听,进入自己的循环 34 | fcgi_fd = fpm_run(&max_requests); // fcgi_id就是监听socket 35 | 36 | // 子进程继续向下执行 37 | parent = 0; 38 | ``` 39 | 40 | ## 初始化阶段 41 | 42 | ``` 43 | fpm/fpm/fpm.c 44 | 45 | 46 | int 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) /* {{{ */ 47 | { 48 | if (0 > fpm_php_init_main() || 49 | 0 > fpm_stdio_init_main() || 50 | 0 > fpm_conf_init_main(test_conf, force_daemon) || 51 | 0 > fpm_unix_init_main() || 52 | 0 > fpm_scoreboard_init_main() || 53 | 0 > fpm_pctl_init_main() || 54 | 0 > fpm_env_init_main() || 55 | 0 > fpm_signals_init_main() || 56 | 0 > fpm_children_init_main() || 57 | 0 > fpm_sockets_init_main() || 58 | 0 > fpm_worker_pool_init_main() || 59 | 0 > fpm_event_init_main()) { 60 | 61 | if (fpm_globals.test_successful) { 62 | exit(FPM_EXIT_OK); 63 | } else { 64 | zlog(ZLOG_ERROR, "FPM initialization failed"); 65 | return -1; 66 | } 67 | } 68 | ``` 69 | 70 | 初始化一些程序结构:配置、记分板、工作池(监听套接字,子进程管理)、事件循环...在此不做具体展开。 71 | 72 | 关键点就是创建了监听socket,后续子进程需要继承并监听。 73 | 74 | ## 启动阶段 75 | 76 | 77 | ### 整体流程 78 | 79 | ``` 80 | 81 | fpm/fpm/fpm.c 82 | 83 | 84 | int fpm_run(int *max_requests) /* {{{ */ 85 | { 86 | struct fpm_worker_pool_s *wp; 87 | 88 | /* create initial children in all pools */ 89 | 90 | // 所有的池子 91 | for (wp = fpm_worker_all_pools; wp; wp = wp->next) { 92 | int is_parent; 93 | 94 | is_parent = fpm_children_create_initial(wp); 95 | 96 | if (!is_parent) { 97 | goto run_child; 98 | } 99 | 100 | /* handle error */ 101 | if (is_parent == 2) { // 创建子进程失败 102 | fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET); 103 | fpm_event_loop(1); 104 | } 105 | } 106 | 107 | /* run event loop forever */ 108 | 109 | // 父进程循环 110 | fpm_event_loop(0); 111 | 112 | // 子进程继续向下执行 113 | run_child: /* only workers reach this point */ 114 | 115 | fpm_cleanups_run(FPM_CLEANUP_CHILD); 116 | 117 | *max_requests = fpm_globals.max_requests; 118 | return fpm_globals.listening_socket; 119 | } 120 | 121 | ``` 122 | 123 | ### 初始化池子 124 | 125 | 对于每个池子(php-fpm.conf里配置的work pool),调用fpm_children_create_initial初始化若干子进程。 126 | 127 | 因为work pool有不同的进程管理策略,所以初始化进程的数量和方式各有差异。 128 | 129 | ``` 130 | 131 | fpm/fpm/fpm_children.c 132 | 133 | 134 | int fpm_children_create_initial(struct fpm_worker_pool_s *wp) /* {{{ */ 135 | { 136 | // 按需分配的进程管理模式,实际上是父进程监听listen socket可读则认为可能需要更多的子进程来处理请求 137 | if (wp->config->pm == PM_STYLE_ONDEMAND) { 138 | wp->ondemand_event = (struct fpm_event_s *)malloc(sizeof(struct fpm_event_s)); 139 | 140 | if (!wp->ondemand_event) { 141 | zlog(ZLOG_ERROR, "[pool %s] unable to malloc the ondemand socket event", wp->config->name); 142 | // FIXME handle crash 143 | return 1; 144 | } 145 | 146 | memset(wp->ondemand_event, 0, sizeof(struct fpm_event_s)); 147 | fpm_event_set(wp->ondemand_event, wp->listening_socket, FPM_EV_READ | FPM_EV_EDGE, fpm_pctl_on_socket_accept, wp); 148 | wp->socket_event_set = 1; 149 | fpm_event_add(wp->ondemand_event, 0); 150 | 151 | return 1; 152 | } 153 | 154 | // 其他进程管理模式直接初始化,比如static模式直接拉起指定数量的子进程,dynamic模式拉起最小数量的子进程 155 | return fpm_children_make(wp, 0 /* not in event loop yet */, 0, 1); 156 | } 157 | 158 | ``` 159 | 160 | ### 创建子进程 161 | 162 | fpm_children_make用于为池子扩容子进程数量,初始化阶段in_event_loop传0,从而只启动有限数量的子进程,相关策略在代码中有注释说明。 163 | 164 | ``` 165 | fpm/fpm/fpm_children.c 166 | 167 | 168 | // 创建N个子进程 169 | int fpm_children_make(struct fpm_worker_pool_s *wp, int in_event_loop, int nb_to_spawn, int is_debug) /* {{{ */ 170 | { 171 | pid_t pid; 172 | struct fpm_child_s *child; 173 | int max; 174 | static int warned = 0; 175 | 176 | if (wp->config->pm == PM_STYLE_DYNAMIC) { 177 | if (!in_event_loop) { /* starting */ 178 | max = wp->config->pm_start_servers; 179 | } else { 180 | max = wp->running_children + nb_to_spawn; 181 | } 182 | } else if (wp->config->pm == PM_STYLE_ONDEMAND) { 183 | if (!in_event_loop) { /* starting */ 184 | max = 0; /* do not create any child at startup */ 185 | } else { 186 | max = wp->running_children + nb_to_spawn; 187 | } 188 | } else { /* PM_STYLE_STATIC */ 189 | max = wp->config->pm_max_children; 190 | } 191 | 192 | /* 193 | * fork children while: 194 | * - fpm_pctl_can_spawn_children : FPM is running in a NORMAL state (aka not restart, stop or reload) 195 | * - wp->running_children < max : there is less than the max process for the current pool 196 | * - (fpm_global_config.process_max < 1 || fpm_globals.running_children < fpm_global_config.process_max): 197 | * if fpm_global_config.process_max is set, FPM has not fork this number of processes (globaly) 198 | */ 199 | while (fpm_pctl_can_spawn_children() && wp->running_children < max && (fpm_global_config.process_max < 1 || fpm_globals.running_children < fpm_global_config.process_max)) { 200 | 201 | warned = 0; 202 | 203 | // 创建一个child对象,分配对应的记分板槽 204 | child = fpm_resources_prepare(wp); 205 | 206 | if (!child) { 207 | return 2; 208 | } 209 | 210 | pid = fork(); 211 | 212 | switch (pid) { 213 | 214 | case 0 : // 子进程 215 | fpm_child_resources_use(child); 216 | fpm_globals.is_child = 1; 217 | fpm_child_init(wp); 218 | return 0; 219 | 220 | case -1 : 221 | zlog(ZLOG_SYSERROR, "fork() failed"); 222 | 223 | fpm_resources_discard(child); 224 | return 2; 225 | 226 | default : 227 | // 父进程 228 | child->pid = pid; 229 | fpm_clock_get(&child->started); 230 | fpm_parent_resources_use(child); 231 | 232 | zlog(is_debug ? ZLOG_DEBUG : ZLOG_NOTICE, "[pool %s] child %d started", wp->config->name, (int) pid); 233 | } 234 | 235 | } 236 | 237 | if (!warned && fpm_global_config.process_max > 0 && fpm_globals.running_children >= fpm_global_config.process_max) { 238 | if (wp->running_children < max) { 239 | warned = 1; 240 | zlog(ZLOG_WARNING, "The maximum number of processes has been reached. Please review your configuration and consider raising 'process.max'"); 241 | } 242 | } 243 | 244 | return 1; /* we are done */ 245 | } 246 | ``` 247 | 248 | ### 准备创建子进程 249 | 250 | 创建子进程,需要在父进程关联一些数据结构记录其信息。 251 | 252 | 另外,需要创建一个Pipe,子进程会把自己标准输出和错误输出定向到pipe[1],这样父进程就可以捕获子进程的输出了。 253 | 254 | 其中fpm_resources_prepare就是这样一个函数: 255 | 256 | ``` 257 | 258 | fpm/fpm/fpm_children.c 259 | 260 | 261 | static struct fpm_child_s *fpm_resources_prepare(struct fpm_worker_pool_s *wp) /* {{{ */ 262 | { 263 | struct fpm_child_s *c; 264 | 265 | c = fpm_child_alloc(); 266 | 267 | if (!c) { 268 | zlog(ZLOG_ERROR, "[pool %s] unable to malloc new child", wp->config->name); 269 | return 0; 270 | } 271 | 272 | c->wp = wp; 273 | c->fd_stdout = -1; c->fd_stderr = -1; 274 | 275 | if (0 > fpm_stdio_prepare_pipes(c)) { 276 | fpm_child_free(c); 277 | return 0; 278 | } 279 | 280 | if (0 > fpm_scoreboard_proc_alloc(wp->scoreboard, &c->scoreboard_i)) { 281 | fpm_stdio_discard_pipes(c); 282 | fpm_child_free(c); 283 | return 0; 284 | } 285 | 286 | return c; 287 | } 288 | 289 | ``` 290 | 291 | ### 共享内存 记分板 292 | 293 | 上面代码还分配了一个scoreboard记分板,这是PHP-FPM进行进程管理非常关键的组件。 294 | 295 | 每个池子都有一个scoreboard对象,里面为每个子进程准备了一个scoreboard_proc对象。 296 | 297 | scoreboard和scoreboard_proc对象在父进程中从共享内存里分配,在父子进程间共享访问,通过atomic原子变量实现spinlock自旋锁,确保多进程并发访问的安全性。 298 | 299 | ``` 300 | fpm/fpm/fpm_scoreboard.h 301 | 302 | 303 | // 每个子进程有一个小记分板 304 | struct fpm_scoreboard_proc_s { 305 | union { 306 | atomic_t lock; // 保护该对象的自旋锁 307 | char dummy[16]; 308 | }; 309 | int used; 310 | time_t start_epoch; 311 | pid_t pid; 312 | unsigned long requests; 313 | enum fpm_request_stage_e request_stage; 314 | struct timeval accepted; 315 | struct timeval duration; 316 | time_t accepted_epoch; 317 | struct timeval tv; 318 | char request_uri[128]; 319 | char query_string[512]; 320 | char request_method[16]; 321 | size_t content_length; /* used with POST only */ 322 | char script_filename[256]; 323 | char auth_user[32]; 324 | #ifdef HAVE_TIMES 325 | struct tms cpu_accepted; 326 | struct timeval cpu_duration; 327 | struct tms last_request_cpu; 328 | struct timeval last_request_cpu_duration; 329 | #endif 330 | size_t memory; 331 | }; 332 | 333 | // 每个池子一个大记分板 334 | struct fpm_scoreboard_s { 335 | union { 336 | atomic_t lock; // 保护大记分板的自旋锁 337 | char dummy[16]; 338 | }; 339 | char pool[32]; 340 | int pm; 341 | time_t start_epoch; 342 | int idle; 343 | int active; 344 | int active_max; 345 | unsigned long int requests; 346 | unsigned int max_children_reached; 347 | int lq; 348 | int lq_max; 349 | unsigned int lq_len; 350 | unsigned int nprocs; 351 | int free_proc; 352 | unsigned long int slow_rq; 353 | struct fpm_scoreboard_proc_s *procs[]; // 池子内每个进程有一个小记分板 354 | }; 355 | 356 | ``` 357 | 358 | 还记得本文最开始初始化中的fpm_scoreboard_init_main吗? 359 | 360 | ``` 361 | fpm/fpm/fpm_scoreboard.c 362 | 363 | 364 | int fpm_scoreboard_init_main() /* {{{ */ 365 | { 366 | struct fpm_worker_pool_s *wp; 367 | unsigned int i; 368 | 369 | #ifdef HAVE_TIMES 370 | #if (defined(HAVE_SYSCONF) && defined(_SC_CLK_TCK)) 371 | fpm_scoreboard_tick = sysconf(_SC_CLK_TCK); 372 | #else /* _SC_CLK_TCK */ 373 | #ifdef HZ 374 | fpm_scoreboard_tick = HZ; 375 | #else /* HZ */ 376 | fpm_scoreboard_tick = 100; 377 | #endif /* HZ */ 378 | #endif /* _SC_CLK_TCK */ 379 | zlog(ZLOG_DEBUG, "got clock tick '%.0f'", fpm_scoreboard_tick); 380 | #endif /* HAVE_TIMES */ 381 | 382 | 383 | for (wp = fpm_worker_all_pools; wp; wp = wp->next) { 384 | size_t scoreboard_size, scoreboard_nprocs_size; 385 | void *shm_mem; 386 | 387 | if (wp->config->pm_max_children < 1) { 388 | zlog(ZLOG_ERROR, "[pool %s] Unable to create scoreboard SHM because max_client is not set", wp->config->name); 389 | return -1; 390 | } 391 | 392 | if (wp->scoreboard) { 393 | zlog(ZLOG_ERROR, "[pool %s] Unable to create scoreboard SHM because it already exists", wp->config->name); 394 | return -1; 395 | } 396 | 397 | scoreboard_size = sizeof(struct fpm_scoreboard_s) + (wp->config->pm_max_children) * sizeof(struct fpm_scoreboard_proc_s *); 398 | scoreboard_nprocs_size = sizeof(struct fpm_scoreboard_proc_s) * wp->config->pm_max_children; 399 | shm_mem = fpm_shm_alloc(scoreboard_size + scoreboard_nprocs_size); 400 | 401 | if (!shm_mem) { 402 | return -1; 403 | } 404 | wp->scoreboard = shm_mem; 405 | wp->scoreboard->nprocs = wp->config->pm_max_children; 406 | shm_mem += scoreboard_size; 407 | 408 | for (i = 0; i < wp->scoreboard->nprocs; i++, shm_mem += sizeof(struct fpm_scoreboard_proc_s)) { 409 | wp->scoreboard->procs[i] = shm_mem; 410 | } 411 | 412 | wp->scoreboard->pm = wp->config->pm; 413 | wp->scoreboard->start_epoch = time(NULL); 414 | strlcpy(wp->scoreboard->pool, wp->config->name, sizeof(wp->scoreboard->pool)); 415 | } 416 | return 0; 417 | } 418 | ``` 419 | 420 | 可见,FPM为每个池子,一次性分配了足够最多子进程用的记分板内存空间,而且是通过共享内存分配的,这样子进程可以和父进程共享这块信息: 421 | 422 | ``` 423 | fpm/fpm/fpm_shm.c 424 | 425 | 426 | void *fpm_shm_alloc(size_t size) /* {{{ */ 427 | { 428 | void *mem; 429 | 430 | mem = mmap(0, size, PROT_READ | PROT_WRITE, MAP_ANONYMOUS | MAP_SHARED, -1, 0); 431 | 432 | #ifdef MAP_FAILED 433 | if (mem == MAP_FAILED) { 434 | zlog(ZLOG_SYSERROR, "unable to allocate %zu bytes in shared memory: %s", size, strerror(errno)); 435 | return NULL; 436 | } 437 | #endif 438 | 439 | if (!mem) { 440 | zlog(ZLOG_SYSERROR, "unable to allocate %zu bytes in shared memory", size); 441 | return NULL; 442 | } 443 | 444 | fpm_shm_size += size; 445 | return mem; 446 | } 447 | ``` 448 | 449 | 通过mmap的MAP_ANONY|MAP_SHARED做匿名共享内存。 450 | 451 | 至于多进程访问的安全性,是依靠atomic_t原子变量与atomic_cmp_set这样的原子操作实现了自旋锁,整个函数是内联的: 452 | 453 | ``` 454 | fpm/fpm/fpm_atomic.h 455 | 456 | 457 | static inline int fpm_spinlock(atomic_t *lock, int try_once) /* {{{ */ 458 | { 459 | if (try_once) { 460 | return atomic_cmp_set(lock, 0, 1) ? 1 : 0; 461 | } 462 | 463 | for (;;) { 464 | 465 | if (atomic_cmp_set(lock, 0, 1)) { 466 | break; 467 | } 468 | 469 | sched_yield(); 470 | } 471 | 472 | return 1; 473 | } 474 | 475 | ``` 476 | 477 | sched_yield是为了让出CPU,避免空转等锁对CPU占用过高。 478 | 479 | ## 执行阶段 480 | 481 | 482 | ### 子进程进入阻塞循环 483 | 484 | fpm_children_create_initial函数返回0表示子进程,则返回到fpm_run的调用处,也就是main函数里。 485 | 486 | ``` 487 | fpm/fpm/fpm_main.c 488 | 489 | 490 | zend_first_try { 491 | // accept监听套接字,获得一个连接socket 492 | while (EXPECTED(fcgi_accept_request(request) >= 0)) { 493 | char *primary_script = NULL; 494 | request_body_fd = -1; 495 | SG(server_context) = (void *) request; 496 | init_request_info(); 497 | 498 | fpm_request_info(); 499 | 500 | // 初始化PHP执行环境 501 | 502 | .... 503 | 504 | ... 505 | ``` 506 | 507 | 子进程是阻塞循环,同一时刻只能处理一个连接。 508 | 509 | 关于PHP解释器如何初始化环境属于另外一个话题,下面是关键代码: 510 | 511 | ``` 512 | fpm/fpm/fpm_main.c 513 | 514 | // 打开PHP文件 515 | /* path_translated exists, we can continue ! */ 516 | if (UNEXPECTED(php_fopen_primary_script(&file_handle) == FAILURE)) { 517 | zend_try { 518 | zlog(ZLOG_ERROR, "Unable to open primary script: %s (%s)", primary_script, strerror(errno)); 519 | if (errno == EACCES) { 520 | SG(sapi_headers).http_response_code = 403; 521 | PUTS("Access denied.\n"); 522 | } else { 523 | SG(sapi_headers).http_response_code = 404; 524 | PUTS("No input file specified.\n"); 525 | } 526 | } zend_catch { 527 | } zend_end_try(); 528 | /* we want to serve more requests if this is fastcgi 529 | * so cleanup and continue, request shutdown is 530 | * handled later */ 531 | 532 | goto fastcgi_request_done; 533 | } 534 | 535 | // 在共享内存里更新记分板信息, 也就是request开始处理的时间之类的 536 | fpm_request_executing(); 537 | 538 | // 执行PHP脚本 539 | php_execute_script(&file_handle); 540 | 541 | ``` 542 | 543 | 要执行PHP文件首先要找到对应的文件,然后加载一下,最后交给php_execute_script来解释执行。 544 | 545 | 在执行前有一个很重要的操作,是更新记分板信息,主要是记录该子进程什么时候开始处理的请求,请求的一些基本信息是什么。 546 | 547 | 这些信息对于父进程很重要,父进程根据记分板里的信息就可以知道子进程的运行情况。 548 | 549 | ``` 550 | fpm/fpm/fpm_request.c 551 | 552 | 553 | void fpm_request_executing() /* {{{ */ 554 | { 555 | struct fpm_scoreboard_proc_s *proc; 556 | struct timeval now; 557 | 558 | fpm_clock_get(&now); 559 | 560 | proc = fpm_scoreboard_proc_acquire(NULL, -1, 0); 561 | if (proc == NULL) { 562 | zlog(ZLOG_WARNING, "failed to acquire proc scoreboard"); 563 | return; 564 | } 565 | 566 | proc->request_stage = FPM_REQUEST_EXECUTING; 567 | proc->tv = now; 568 | fpm_scoreboard_proc_release(proc); 569 | } 570 | ``` 571 | 572 | 先获得该进程记分板对象的锁,然后更新状态为执行中,时间点是now,然后释放锁。 573 | 574 | 因为记分板是共享内存的,父进程是可以随时去查看的。 575 | 576 | ``` 577 | fpm/fpm/fpm_main.c 578 | 579 | 580 | // 记分板更新请求结束 581 | fpm_request_end(); 582 | fpm_log_write(NULL); 583 | 584 | efree(SG(request_info).path_translated); 585 | SG(request_info).path_translated = NULL; 586 | 587 | // 清理PHP执行环境的东西 588 | php_request_shutdown((void *) 0); 589 | 590 | // 连续处理request超过一定数量,进程退出 591 | requests++; 592 | if (UNEXPECTED(max_requests && (requests == max_requests))) { 593 | fcgi_request_set_keep(request, 0); 594 | fcgi_finish_request(request, 0); 595 | break; 596 | } 597 | 598 | ``` 599 | 600 | 当PHP脚本执行完成后,需要fpm_request_end更新记分板请求结束,做一些状态更新,就不展开了。 601 | 602 | php_request_shutdown清理PHP执行环境,不需要展开。 603 | 604 | 下面是判断该子进程已经累计处理的请求数量,超过配置的阀值就会break退出accept loop,退出main函数结束自己的生命。这个配置项我们一般都会使用,主要是防止扩展或者PHP自身有内存泄露之类的BUG,所以定期退出一下。 605 | 606 | ### 父进程进入事件循环 607 | 608 | fpm_children_create_initial函数在初始化子进程后,父进程返回1,然后进入事件循环。 609 | 610 | 通常linux事件循环基于epoll实现,这里调用fpm_event_loop函数进入循环。 611 | 612 | 父进程循环主要是在对子进程进行管理,比如关闭空闲的子进程,或者启动更多的子进程。 613 | 614 | 另外一方面也需要监听来自命令行管理员的一些信号,比如重新加载配置,重新启动进程等。 615 | 616 | ``` 617 | fpm/fpm/fpm_event.c 618 | 619 | 620 | // master事件循环 621 | void fpm_event_loop(int err) /* {{{ */ 622 | { 623 | static struct fpm_event_s signal_fd_event; 624 | 625 | /* sanity check */ 626 | if (fpm_globals.parent_pid != getpid()) { 627 | return; 628 | } 629 | 630 | // 有个pipe注册到event loop上,每次有信号触发就会写到pipe 631 | fpm_event_set(&signal_fd_event, fpm_signals_get_fd(), FPM_EV_READ, &fpm_got_signal, NULL); 632 | fpm_event_add(&signal_fd_event, 0); 633 | 634 | /* add timers */ 635 | if (fpm_globals.heartbeat > 0) { 636 | 637 | // 创建定时器,周期性检查子进程是否执行过慢,或者超时,杀死超时进程 638 | fpm_pctl_heartbeat(NULL, 0, NULL); 639 | } 640 | 641 | if (!err) { 642 | 643 | // 创建定时器,周期性根据策略,缩减或者扩增子进程 644 | fpm_pctl_perform_idle_server_maintenance_heartbeat(NULL, 0, NULL); 645 | 646 | zlog(ZLOG_DEBUG, "%zu bytes have been reserved in SHM", fpm_shm_get_size_allocated()); 647 | zlog(ZLOG_NOTICE, "ready to handle connections"); 648 | 649 | #ifdef HAVE_SYSTEMD 650 | fpm_systemd_heartbeat(NULL, 0, NULL); 651 | #endif 652 | } 653 | 654 | ... 655 | ... 656 | 657 | ``` 658 | 659 | 在正式进入事件循环之前,会先对信号处理做一些筹备。 660 | 661 | 因为管理员可以命令行向php-fpm发送控制信号(kill -xxx),另外子进程退出会向父进程发送SIGCHLD信号,主要就是这两个行为。 662 | 663 | 这里fpm_event_set,fpm_event_add都是在操作epoll,就不展开说明了。 664 | 665 | php-fpm在初始化时就分配了一个unix socket pair,这里把socket[0]注册在epoll上监听,socket读事件的回调函数是fpm_got_signal。 666 | 667 | php-fpm在初始化阶段就注册了信号处理函数,当fpm父进程收到信号后不会直接处理信号,而是将信号标识写入到socket[1]里,这样就会触发epoll监听到事件。 668 | 669 | ### 信号处理函数 670 | 671 | fpm在初始化时这样注册了信号处理函数: 672 | ``` 673 | fpm/fpm/fpm_signals.c 674 | 675 | 676 | // 父进程的信号处理注册 677 | int fpm_signals_init_main() /* {{{ */ 678 | { 679 | struct sigaction act; 680 | 681 | // 创建一对双向双工unix socket pair 682 | 683 | if (0 > socketpair(AF_UNIX, SOCK_STREAM, 0, sp)) { 684 | zlog(ZLOG_SYSERROR, "failed to init signals: socketpair()"); 685 | return -1; 686 | } 687 | 688 | if (0 > fd_set_blocked(sp[0], 0) || 0 > fd_set_blocked(sp[1], 0)) { 689 | zlog(ZLOG_SYSERROR, "failed to init signals: fd_set_blocked()"); 690 | return -1; 691 | } 692 | 693 | if (0 > fcntl(sp[0], F_SETFD, FD_CLOEXEC) || 0 > fcntl(sp[1], F_SETFD, FD_CLOEXEC)) { 694 | zlog(ZLOG_SYSERROR, "falied to init signals: fcntl(F_SETFD, FD_CLOEXEC)"); 695 | return -1; 696 | } 697 | 698 | memset(&act, 0, sizeof(act)); 699 | act.sa_handler = sig_handler; // 收到信号,就写到unix socket里,触发主事件循环进一步处理 700 | sigfillset(&act.sa_mask); 701 | 702 | // 来自命令行的杀死信号,来自子进程的退出信号都是重点 703 | if (0 > sigaction(SIGTERM, &act, 0) || 704 | 0 > sigaction(SIGINT, &act, 0) || 705 | 0 > sigaction(SIGUSR1, &act, 0) || 706 | 0 > sigaction(SIGUSR2, &act, 0) || 707 | 0 > sigaction(SIGCHLD, &act, 0) || 708 | 0 > sigaction(SIGQUIT, &act, 0)) { 709 | 710 | zlog(ZLOG_SYSERROR, "failed to init signals: sigaction()"); 711 | return -1; 712 | } 713 | return 0; 714 | } 715 | ``` 716 | 717 | 它首先创建了之前说的unix socket pair用来作为信号处理函数与epoll之间的通讯机制。 718 | 719 | 之后它注册了SIGTERM,SIGINT,SIGUSR1,SIGCHLD...等等信号处理函数到同一个方法:sig_handler。 720 | 721 | 722 | ``` 723 | fpm/fpm/fpm_signals.c 724 | 725 | 726 | static void sig_handler(int signo) /* {{{ */ 727 | { 728 | static const char sig_chars[NSIG + 1] = { 729 | [SIGTERM] = 'T', 730 | [SIGINT] = 'I', 731 | [SIGUSR1] = '1', 732 | [SIGUSR2] = '2', 733 | [SIGQUIT] = 'Q', 734 | [SIGCHLD] = 'C' 735 | }; 736 | char s; 737 | int saved_errno; 738 | 739 | if (fpm_globals.parent_pid != getpid()) { 740 | /* prevent a signal race condition when child process 741 | have not set up it's own signal handler yet */ 742 | return; 743 | } 744 | 745 | saved_errno = errno; 746 | s = sig_chars[signo]; 747 | zend_quiet_write(sp[1], &s, sizeof(s)); // 写入信号对应的标识 748 | errno = saved_errno; 749 | } 750 | ``` 751 | 752 | 该函数根据信号的类型映射到一个1字节的内部信号标识,然后写入到socket[1]里。 753 | 754 | 这一步其实有点问题在于,万一socket写满了呢? 这里并没有关注这个问题,因为一般fpm是足够快的可以处理socket里的事件的。 755 | 756 | ### 处理信号事件 757 | 758 | 当socket[1]写入事件标识后,epoll回调到注册的函数fpm_got_signal中。 759 | 760 | 761 | 762 | ``` 763 | fpm/fpm/fpm_events.c 764 | 765 | 766 | // 有信号发来时的事件回调函数 767 | static void fpm_got_signal(struct fpm_event_s *ev, short which, void *arg) /* {{{ */ 768 | { 769 | char c; 770 | int res, ret; 771 | int fd = ev->fd; 772 | 773 | do { 774 | do { 775 | res = read(fd, &c, 1); 776 | } while (res == -1 && errno == EINTR); 777 | 778 | if (res <= 0) { 779 | if (res < 0 && errno != EAGAIN && errno != EWOULDBLOCK) { 780 | zlog(ZLOG_SYSERROR, "unable to read from the signal pipe"); 781 | } 782 | return; 783 | } 784 | 785 | switch (c) { 786 | 787 | // 收到子进程的退出信号 788 | case 'C' : /* SIGCHLD */ 789 | zlog(ZLOG_DEBUG, "received SIGCHLD"); 790 | fpm_children_bury(); 791 | break; 792 | case 'I' : /* SIGINT */ 793 | zlog(ZLOG_DEBUG, "received SIGINT"); 794 | zlog(ZLOG_NOTICE, "Terminating ..."); 795 | fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET); 796 | break; 797 | case 'T' : /* SIGTERM */ 798 | zlog(ZLOG_DEBUG, "received SIGTERM"); 799 | zlog(ZLOG_NOTICE, "Terminating ..."); 800 | fpm_pctl(FPM_PCTL_STATE_TERMINATING, FPM_PCTL_ACTION_SET); 801 | break; 802 | case 'Q' : /* SIGQUIT */ 803 | zlog(ZLOG_DEBUG, "received SIGQUIT"); 804 | zlog(ZLOG_NOTICE, "Finishing ..."); 805 | fpm_pctl(FPM_PCTL_STATE_FINISHING, FPM_PCTL_ACTION_SET); 806 | break; 807 | case '1' : /* SIGUSR1 */ 808 | zlog(ZLOG_DEBUG, "received SIGUSR1"); 809 | if (0 == fpm_stdio_open_error_log(1)) { 810 | zlog(ZLOG_NOTICE, "error log file re-opened"); 811 | } else { 812 | zlog(ZLOG_ERROR, "unable to re-opened error log file"); 813 | } 814 | 815 | ret = fpm_log_open(1); 816 | if (ret == 0) { 817 | zlog(ZLOG_NOTICE, "access log file re-opened"); 818 | } else if (ret == -1) { 819 | zlog(ZLOG_ERROR, "unable to re-opened access log file"); 820 | } 821 | /* else no access log are set */ 822 | 823 | break; 824 | case '2' : /* SIGUSR2 */ 825 | zlog(ZLOG_DEBUG, "received SIGUSR2"); 826 | zlog(ZLOG_NOTICE, "Reloading in progress ..."); 827 | fpm_pctl(FPM_PCTL_STATE_RELOADING, FPM_PCTL_ACTION_SET); 828 | break; 829 | } 830 | 831 | if (fpm_globals.is_child) { 832 | break; 833 | } 834 | } while (1); 835 | return; 836 | } 837 | 838 | ``` 839 | 840 | 该函数对不同的信号做不同的响应。 841 | 842 | 例如SIGINT/SIGTERM/SIGQUIT都是来自命令行发来的退出信号,需要清理子进程然后退出,这个细节不是很重要,就不展开了。 843 | 844 | 重点在于SIGCHLD信号的处理,它表示子进程退出了或者暂停了,对父进程的子进程管理非常重要。 845 | 846 | ### 处理子进程事件 847 | 848 | fpm_children_bury()用于处理子进程事件,它一方面要waitpid回收子进程的资源,防止出现僵尸进程;另一方面是更新进程管理的状态,因为少了一个子进程,后续进程管理策略就可能新建子进程。 849 | 850 | ``` 851 | fpm/fpm/fpm_children.c 852 | 853 | 854 | 这里循环回收退出的子进程资源,一直循环到没有更多子进程可以回收为止: 855 | 856 | void fpm_children_bury() /* {{{ */ 857 | { 858 | int status; 859 | pid_t pid; 860 | struct fpm_child_s *child; 861 | 862 | // 循环回收子进程资源,直到没有更多 863 | while ( (pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) { 864 | char buf[128]; 865 | int severity = ZLOG_NOTICE; 866 | int restart_child = 1; 867 | 868 | // 根据子进程PID找到对应的child对象 869 | child = fpm_child_find(pid); 870 | 871 | 872 | ``` 873 | 874 | 根据waitpid返回的子进程pid,就可以找到对应的child对象,里面维护了描述子进程的一些信息,由父进程管理。 875 | 876 | 当收到SIGCHLD信号时,我们可以根据waitpid第二个status参数获知进程是如何退出的。 877 | 878 | ### 子进程正常退出 879 | 880 | ``` 881 | fpm/fpm/fpm_children.c 882 | 883 | 884 | if (WIFEXITED(status)) { // 正常退出 885 | 886 | snprintf(buf, sizeof(buf), "with code %d", WEXITSTATUS(status)); 887 | 888 | /* if it's been killed because of dynamic process management 889 | * don't restart it automaticaly 890 | */ 891 | if (child && child->idle_kill) { 892 | restart_child = 0; 893 | } 894 | 895 | if (WEXITSTATUS(status) != FPM_EXIT_OK) { 896 | severity = ZLOG_WARNING; 897 | } 898 | 899 | } 900 | 901 | ``` 902 | 903 | 如果是正常退出,那么说明子进程是通过main函数return或者exit方法退出的。 904 | 905 | 这种情况下,其实还需要区分是子进程自己主动退出的,还是父进程让它退出的。 906 | 907 | 所以child->idle_kill做了一次判断,因为父进程若主动杀死子进程,那么会先在child对象里做一下idle_kill的标记再向子进程发送杀死信号。 908 | 909 | 这个判定决定了是否要立即重启子进程,若不是父进程责令其退出,那么就是意外退出,需要立即拉起。 910 | 911 | ### 子进程被信号杀死 912 | 913 | 这个场景非常类似于正常退出,当进程收到某些信号时默认的行为就是退出,比如SIGKILL强制杀死,SIGSEGV段错误,SIGBUS总线错误,SIGQUIT退出 等等.. 914 | 915 | 这种情况下会区分一下是否是段错误等严重错误,一般预示着PHP内核或者扩展代码有问题导致coredump。 916 | 917 | ``` 918 | fpm/fpm/fpm_children.c 919 | 920 | 921 | else if (WIFSIGNALED(status)) { // 被信号杀死 922 | const char *signame = fpm_signal_names[WTERMSIG(status)]; 923 | const char *have_core = WCOREDUMP(status) ? " - core dumped" : ""; 924 | 925 | if (signame == NULL) { 926 | signame = ""; 927 | } 928 | 929 | snprintf(buf, sizeof(buf), "on signal %d (%s%s)", WTERMSIG(status), signame, have_core); 930 | 931 | /* if it's been killed because of dynamic process management 932 | * don't restart it automaticaly 933 | */ 934 | if (child && child->idle_kill && WTERMSIG(status) == SIGQUIT) { 935 | restart_child = 0; 936 | } 937 | 938 | if (WTERMSIG(status) != SIGQUIT) { /* possible request loss */ 939 | severity = ZLOG_WARNING; 940 | } 941 | } 942 | ``` 943 | 944 | ### 子进程暂停 945 | 946 | 子进程收到SIGSTOP信号就会暂停执行,此时父进程会收到SIGCHLD信号,并且status中标识了子进程是STOP状态。 947 | 948 | 949 | ``` 950 | fpm/fpm/fpm_children.c 951 | 952 | 953 | else if (WIFSTOPPED(status)) { // slowlog时ptrace子进程,导致子进程STOP暂停 954 | 955 | zlog(ZLOG_NOTICE, "child %d stopped for tracing", (int) pid); 956 | 957 | if (child && child->tracer) { 958 | // 获取子进程的信息,打印到slowlog日志,然后恢复子进程 959 | child->tracer(child); 960 | } 961 | 962 | continue; 963 | } 964 | 965 | ``` 966 | 967 | 那么谁会给子进程发送STOP信号呢? 这里先简单提一下,就是当父进程发现子进程处理一个请求超时后,就会调用ptrace去attach到子进程,这个操作就会导致子进程STOP。 968 | 969 | 一旦ptrace导致子进程STOP,那么父进程就会收到SIGCHLD,从而进入上述逻辑分支。 970 | 971 | 父进程要做的,就是利用ptrace的其他能力,直接去访问子进程的地址空间,获取一些堆栈信息,从而获知子进程到底卡在哪里。 972 | 973 | 而上述所说的ptrace逻辑,实际上就是为了打印slowlog,也就是当父进程发现子进程执行慢,就利用ptrace去抓子进程的栈空间,从而打印出一个调用栈到slowlog日志文件中,帮助我们分析问题,这个原理和gdb调试程序是类似的。 974 | 975 | 当然,child->tracer除了利用Ptrace去抓子进程的堆栈之后,会向子进程发送一个SIGCONT信号,让子进程恢复运行,相关代码在后面会提及。 976 | 977 | ### 回收子进程资源 978 | 979 | 如果子进程是退出而不是暂停了,那么就要在父进程里清理相关的进程信息与资源。 980 | 981 | ``` 982 | fpm/fpm/fpm_children.c 983 | 984 | 985 | // 子进程退出,那么清理父进程里关联的各种内存 986 | if (child) { 987 | struct fpm_worker_pool_s *wp = child->wp; 988 | struct timeval tv1, tv2; 989 | 990 | fpm_child_unlink(child); 991 | 992 | fpm_scoreboard_proc_free(wp->scoreboard, child->scoreboard_i); 993 | 994 | fpm_clock_get(&tv1); 995 | 996 | timersub(&tv1, &child->started, &tv2); 997 | ``` 998 | 999 | 例如上述清理了记分板资源,等等... 1000 | 1001 | ### 严重错误重启自身 1002 | 1003 | ``` 1004 | fpm/fpm/fpm_children.c 1005 | 1006 | 1007 | if (last_faults && (WTERMSIG(status) == SIGSEGV || WTERMSIG(status) == SIGBUS)) { 1008 | time_t now = tv1.tv_sec; 1009 | int restart_condition = 1; 1010 | int i; 1011 | 1012 | last_faults[fault++] = now; 1013 | 1014 | if (fault == fpm_global_config.emergency_restart_threshold) { 1015 | fault = 0; 1016 | } 1017 | 1018 | for (i = 0; i < fpm_global_config.emergency_restart_threshold; i++) { 1019 | if (now - last_faults[i] > fpm_global_config.emergency_restart_interval) { 1020 | restart_condition = 0; 1021 | break; 1022 | } 1023 | } 1024 | 1025 | // COREDUMP太多,决定重启php-fpm,也就是直接execv执行php-fpm自身 1026 | if (restart_condition) { 1027 | 1028 | zlog(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); 1029 | 1030 | fpm_pctl(FPM_PCTL_STATE_RELOADING, FPM_PCTL_ACTION_SET); 1031 | } 1032 | } 1033 | 1034 | // 有一些子进程退出场景,是需要立即重新拉起新的子进程的 1035 | if (restart_child) { 1036 | fpm_children_make(wp, 1 /* in event loop */, 1, 0); 1037 | 1038 | if (fpm_globals.is_child) { 1039 | break; 1040 | } 1041 | } 1042 | ``` 1043 | 1044 | 紧接着,如果一段时间内段错误等严重致命问题连续出现,那么可能PHP-FPM已经因为某些程序bug原因写坏了内存,进入了一种万劫不复的状态。 1045 | 1046 | 此时,满足了restart_condition=1,那么就会标记PHP-FPM进程为RELOADING状态,也就是准备重启PHP-FPM自己。 1047 | 1048 | 重启的方法就是定时器检测到php-fpm状态为reloading,那么直接execv再次执行php-fpm二进制即可: 1049 | 1050 | ``` 1051 | fpm/fpm/fpm_process_ctl.c 1052 | 1053 | 1054 | static void fpm_pctl_exec() /* {{{ */ 1055 | { 1056 | 1057 | zlog(ZLOG_NOTICE, "reloading: execvp(\"%s\", {\"%s\"" 1058 | "%s%s%s" "%s%s%s" "%s%s%s" "%s%s%s" "%s%s%s" 1059 | "%s%s%s" "%s%s%s" "%s%s%s" "%s%s%s" "%s%s%s" 1060 | "})", 1061 | saved_argv[0], saved_argv[0], 1062 | optional_arg(1), 1063 | optional_arg(2), 1064 | optional_arg(3), 1065 | optional_arg(4), 1066 | optional_arg(5), 1067 | optional_arg(6), 1068 | optional_arg(7), 1069 | optional_arg(8), 1070 | optional_arg(9), 1071 | optional_arg(10) 1072 | ); 1073 | 1074 | fpm_cleanups_run(FPM_CLEANUP_PARENT_EXEC); 1075 | execvp(saved_argv[0], saved_argv); 1076 | zlog(ZLOG_SYSERROR, "failed to reload: execvp() failed"); 1077 | exit(FPM_EXIT_SOFTWARE); 1078 | } 1079 | ``` 1080 | 1081 | 另外,如果此前判定子进程是异常退出,那么restart_child=1,则会立即拉起一个新进程补充起来。 1082 | 1083 | ### 定时器 -- 子进程健康检查 1084 | 1085 | 对于已经创建的子进程,父进程会在事件循环中创建一个定时器,定时的进行全量的扫描。 1086 | 1087 | 目标是发现执行过慢的请求,进行对应的处理。 1088 | 1089 | ``` 1090 | fpm/fpm/fpm_events.c 1091 | 1092 | 1093 | // master事件循环 1094 | void fpm_event_loop(int err) /* {{{ */ 1095 | { 1096 | ... 1097 | 1098 | ... 1099 | 1100 | 1101 | /* add timers */ 1102 | if (fpm_globals.heartbeat > 0) { 1103 | 1104 | // 创建定时器,周期性检查子进程是否执行过慢,或者超时,杀死超时进程 1105 | fpm_pctl_heartbeat(NULL, 0, NULL); 1106 | } 1107 | 1108 | 1109 | if (!err) { 1110 | 1111 | // 创建定时器,周期性根据策略,缩减或者扩增子进程 1112 | fpm_pctl_perform_idle_server_maintenance_heartbeat(NULL, 0, NULL); 1113 | 1114 | zlog(ZLOG_DEBUG, "%zu bytes have been reserved in SHM", fpm_shm_get_size_allocated()); 1115 | zlog(ZLOG_NOTICE, "ready to handle connections"); 1116 | 1117 | #ifdef HAVE_SYSTEMD 1118 | fpm_systemd_heartbeat(NULL, 0, NULL); 1119 | #endif 1120 | } 1121 | 1122 | ``` 1123 | 1124 | 这里创建了两个定时器,先说第一个定时器fpm_pctl_heartbeat。 1125 | 1126 | ``` 1127 | fpm/fpm/fpm_process_ctl.c 1128 | 1129 | 1130 | // 心跳处理函数, 1131 | void fpm_pctl_heartbeat(struct fpm_event_s *ev, short which, void *arg) /* {{{ */ 1132 | { 1133 | static struct fpm_event_s heartbeat; 1134 | struct timeval now; 1135 | 1136 | if (fpm_globals.parent_pid != getpid()) { 1137 | return; /* sanity check */ 1138 | } 1139 | 1140 | // 如果是心跳回调事件, 那么进入处理流程 1141 | if (which == FPM_EV_TIMEOUT) { 1142 | fpm_clock_get(&now); 1143 | fpm_pctl_check_request_timeout(&now); 1144 | return; 1145 | } 1146 | 1147 | /* ensure heartbeat is not lower than FPM_PCTL_MIN_HEARTBEAT */ 1148 | // 心跳间隔 1149 | fpm_globals.heartbeat = MAX(fpm_globals.heartbeat, FPM_PCTL_MIN_HEARTBEAT); 1150 | 1151 | /* first call without setting to initialize the timer */ 1152 | // 初始只注册一次定时器 1153 | zlog(ZLOG_DEBUG, "heartbeat have been set up with a timeout of %dms", fpm_globals.heartbeat); 1154 | fpm_event_set_timer(&heartbeat, FPM_EV_PERSIST, &fpm_pctl_heartbeat, NULL); 1155 | fpm_event_add(&heartbeat, fpm_globals.heartbeat); 1156 | } 1157 | ``` 1158 | 1159 | 该函数既是定时器的回调函数,也是定时器的初始化注册函数。 1160 | 1161 | 当定时器回调时,会进入if (which == FPM_EV_TIMEOUT)分支执行逻辑;否则就是第一次注册定时器。 1162 | 1163 | 进程检测算法在fpm_pctl_check_request_timeout中实现: 1164 | 1165 | ``` 1166 | fpm/fpm/fpm_process_ctl.c 1167 | 1168 | 1169 | static void fpm_pctl_check_request_timeout(struct timeval *now) /* {{{ */ 1170 | { 1171 | struct fpm_worker_pool_s *wp; 1172 | 1173 | // 检查每个池子 1174 | for (wp = fpm_worker_all_pools; wp; wp = wp->next) { 1175 | int terminate_timeout = wp->config->request_terminate_timeout; 1176 | int slowlog_timeout = wp->config->request_slowlog_timeout; 1177 | struct fpm_child_s *child; 1178 | 1179 | // 每个池子内所有子进程 1180 | if (terminate_timeout || slowlog_timeout) { 1181 | for (child = wp->children; child; child = child->next) { 1182 | 1183 | // 检查是否请求处理超时 1184 | fpm_request_check_timed_out(child, now, terminate_timeout, slowlog_timeout); 1185 | } 1186 | } 1187 | } 1188 | } 1189 | ``` 1190 | 1191 | 逻辑上就是遍历所有池子里的所有子进程,逐一调用fpm_request_check_time_out方法检查: 1192 | 1193 | ``` 1194 | fpm/fpm/fpm_request.c 1195 | 1196 | 1197 | void fpm_request_check_timed_out(struct fpm_child_s *child, struct timeval *now, int terminate_timeout, int slowlog_timeout) /* {{{ */ 1198 | { 1199 | struct fpm_scoreboard_proc_s proc, *proc_p; 1200 | 1201 | // 获得子进程的记分板 1202 | proc_p = fpm_scoreboard_proc_acquire(child->wp->scoreboard, child->scoreboard_i, 1); 1203 | if (!proc_p) { 1204 | zlog(ZLOG_WARNING, "failed to acquire scoreboard"); 1205 | return; 1206 | } 1207 | 1208 | // 拷贝一份当前信息 1209 | proc = *proc_p; 1210 | 1211 | // 释放子进程记分板 1212 | fpm_scoreboard_proc_release(proc_p); 1213 | 1214 | #if HAVE_FPM_TRACE 1215 | if (child->slow_logged.tv_sec) { 1216 | if (child->slow_logged.tv_sec != proc.accepted.tv_sec || child->slow_logged.tv_usec != proc.accepted.tv_usec) { 1217 | child->slow_logged.tv_sec = 0; 1218 | child->slow_logged.tv_usec = 0; 1219 | } 1220 | } 1221 | #endif 1222 | 1223 | // 检查子进程是否存在超时问题 1224 | if (proc.request_stage > FPM_REQUEST_ACCEPTING && proc.request_stage < FPM_REQUEST_END) { 1225 | char purified_script_filename[sizeof(proc.script_filename)]; 1226 | struct timeval tv; 1227 | 1228 | // 当前时间减去连接接收时间 1229 | timersub(now, &proc.accepted, &tv); 1230 | 1231 | #if HAVE_FPM_TRACE 1232 | 1233 | // 检查是否执行时间触发slow log阀值 1234 | if (child->slow_logged.tv_sec == 0 && slowlog_timeout && 1235 | proc.request_stage == FPM_REQUEST_EXECUTING && tv.tv_sec >= slowlog_timeout) { 1236 | 1237 | str_purify_filename(purified_script_filename, proc.script_filename, sizeof(proc.script_filename)); 1238 | 1239 | child->slow_logged = proc.accepted; 1240 | child->tracer = fpm_php_trace; // 当收到子进程的SIGSTOP信号后,需要通过fpm_php_trace函数来获取子进程的栈信息 1241 | 1242 | // 这里attach到子进程上,目的是获取子进程的PHP栈,需要等待子进程发出SIGSTOP信号 1243 | fpm_trace_signal(child->pid); 1244 | 1245 | zlog(ZLOG_WARNING, "[pool %s] child %d, script '%s' (request: \"%s %s%s%s\") executing too slow (%d.%06d sec), logging", 1246 | child->wp->config->name, (int) child->pid, purified_script_filename, proc.request_method, proc.request_uri, 1247 | (proc.query_string[0] ? "?" : ""), proc.query_string, 1248 | (int) tv.tv_sec, (int) tv.tv_usec); 1249 | } 1250 | else 1251 | #endif 1252 | // 是否执行超时 1253 | if (terminate_timeout && tv.tv_sec >= terminate_timeout) { 1254 | str_purify_filename(purified_script_filename, proc.script_filename, sizeof(proc.script_filename)); 1255 | 1256 | // 给子进程发SIGTERM信号杀死 1257 | fpm_pctl_kill(child->pid, FPM_PCTL_TERM); 1258 | 1259 | zlog(ZLOG_WARNING, "[pool %s] child %d, script '%s' (request: \"%s %s%s%s\") execution timed out (%d.%06d sec), terminating", 1260 | child->wp->config->name, (int) child->pid, purified_script_filename, proc.request_method, proc.request_uri, 1261 | (proc.query_string[0] ? "?" : ""), proc.query_string, 1262 | (int) tv.tv_sec, (int) tv.tv_usec); 1263 | } 1264 | } 1265 | } 1266 | ``` 1267 | 1268 | 首先要加锁获取该子进程记分板信息的一份拷贝,然后就释放掉锁,进入检查环节。 1269 | 1270 | 记分板里记录了子进程当前的状态,如果>ACCEPTING && < REQUEST_END表示正在处理请求,那么就可以检查这个进程是不是处理请求花费了太久的时间。 1271 | 1272 | 首先是判断子进程请求处理事件是否超过slowlog的阀值,那么就会调用fpm_trace_signal去attach到子进程上,内部就是调用ptrace而已。 1273 | 1274 | 这里注意child->trace之前提到过,它具体实现在fpm_php_trace中,当父进程收到SIGCHLD并且子进程是STOP状态就会回调child->trace方法,从而从ptrace中抓取子进程的堆栈信息,这里就不展开了。 1275 | 1276 | 接下来检测了一下请求花费时间是否过长,这种情况属于极端异常,父进程的做法就是杀死子进程,这是通过发送SIGTERM信号实现的。在发送信号前并没有标记child->idle_kill,说明子进程死后父进程希望可以立即拉起来,因为子进程只是BUG卡住了之类的。 1277 | 1278 | ### 定时器 -- 子进程伸缩管理 1279 | 1280 | 前一个定时检查运行中的子进程状态,而该定时器fpm_pctl_perform_idle_server_maintenance_heartbeat是判断是否有必要新增子进程,或者杀死过多的空闲子进程。 1281 | 1282 | ``` 1283 | fpm/fpm/fpm_process_ctl.c 1284 | 1285 | 1286 | // 定时器,子进程空闲杀死/新增的检查逻辑 1287 | void fpm_pctl_perform_idle_server_maintenance_heartbeat(struct fpm_event_s *ev, short which, void *arg) /* {{{ */ 1288 | { 1289 | static struct fpm_event_s heartbeat; 1290 | struct timeval now; 1291 | 1292 | if (fpm_globals.parent_pid != getpid()) { 1293 | return; /* sanity check */ 1294 | } 1295 | 1296 | if (which == FPM_EV_TIMEOUT) { 1297 | fpm_clock_get(&now); 1298 | if (fpm_pctl_can_spawn_children()) { 1299 | fpm_pctl_perform_idle_server_maintenance(&now); 1300 | 1301 | /* if it's a child, stop here without creating the next event 1302 | * this event is reserved to the master process 1303 | */ 1304 | if (fpm_globals.is_child) { 1305 | return; 1306 | } 1307 | } 1308 | return; 1309 | } 1310 | 1311 | /* first call without setting which to initialize the timer */ 1312 | fpm_event_set_timer(&heartbeat, FPM_EV_PERSIST, &fpm_pctl_perform_idle_server_maintenance_heartbeat, NULL); 1313 | fpm_event_add(&heartbeat, FPM_IDLE_SERVER_MAINTENANCE_HEARTBEAT); 1314 | } 1315 | ``` 1316 | 1317 | 每当定时器被回调进入到if (which == FPM_EV_TIMEOUT),则调用fpm_pctl_perform_idle_server_maintenance方法进行逻辑处理。 1318 | 1319 | 1320 | ``` 1321 | fpm/fpm/fpm_process_ctl.c 1322 | 1323 | 1324 | static void fpm_pctl_perform_idle_server_maintenance(struct timeval *now) /* {{{ */ 1325 | { 1326 | struct fpm_worker_pool_s *wp; 1327 | 1328 | // 遍历每个池子 1329 | for (wp = fpm_worker_all_pools; wp; wp = wp->next) { 1330 | struct fpm_child_s *child; 1331 | struct fpm_child_s *last_idle_child = NULL; 1332 | int idle = 0; 1333 | int active = 0; 1334 | int children_to_fork; 1335 | unsigned cur_lq = 0; 1336 | 1337 | if (wp->config == NULL) continue; 1338 | 1339 | // 遍历每个子进程 1340 | for (child = wp->children; child; child = child->next) { 1341 | // 如果子进程空闲(等待连接中) 1342 | if (fpm_request_is_idle(child)) { 1343 | // 找出闲的最久的子进程 1344 | if (last_idle_child == NULL) { 1345 | last_idle_child = child; 1346 | } else { 1347 | if (timercmp(&child->started, &last_idle_child->started, <)) { 1348 | last_idle_child = child; 1349 | } 1350 | } 1351 | idle++; 1352 | } else { 1353 | active++; 1354 | } 1355 | } 1356 | 1357 | /* update status structure for all PMs */ 1358 | // 获取一下TCP的连接握手队列有几个排队 1359 | if (wp->listen_address_domain == FPM_AF_INET) { 1360 | if (0 > fpm_socket_get_listening_queue(wp->listening_socket, &cur_lq, NULL)) { 1361 | cur_lq = 0; 1362 | #if 0 1363 | } else { 1364 | if (cur_lq > 0) { 1365 | if (!wp->warn_lq) { 1366 | zlog(ZLOG_WARNING, "[pool %s] listening queue is not empty, #%d requests are waiting to be served, consider raising pm.max_children setting (%d)", wp->config->name, cur_lq, wp->config->pm_max_children); 1367 | wp->warn_lq = 1; 1368 | } 1369 | } else { 1370 | wp->warn_lq = 0; 1371 | } 1372 | #endif 1373 | } 1374 | } 1375 | 1376 | // 把这次统计的各种信息,更新到池子的记分板上 1377 | fpm_scoreboard_update(idle, active, cur_lq, -1, -1, -1, 0, FPM_SCOREBOARD_ACTION_SET, wp->scoreboard); 1378 | 1379 | ``` 1380 | 1381 | 该函数外层也是遍历所有池子,对于每个池子进行统计。 1382 | 1383 | 主要是统计有多少个子进程在处理请求,有多个子进程空闲,并且找出空闲最久的那个子进程。 1384 | 1385 | 然后调用linux api获取了一下监听套接字listen socket的tcp握手队列的堆积长度,如果排队的比较多则预示着子进程不足,来不及处理更多的请求。 1386 | 1387 | 上述统计信息会被更新到池子对应的记分板上。 1388 | 1389 | ### 子进程管理策略 -- ON DEMAND 1390 | 1391 | 我们知道php-fpm有3种进程管理模型,on demand是按需分配,也就是初始化给池子里分配1个子进程,如果子进程来不及处理请求就再增加子进程。 1392 | 1393 | ``` 1394 | fpm/fpm/fpm_process_ctl.c 1395 | 1396 | 1397 | // 按需分配进程,所以如果有哪个子进程闲太久了,就干掉 1398 | if (wp->config->pm == PM_STYLE_ONDEMAND) { 1399 | struct timeval last, now; 1400 | 1401 | zlog(ZLOG_DEBUG, "[pool %s] currently %d active children, %d spare children", wp->config->name, active, idle); 1402 | 1403 | if (!last_idle_child) continue; 1404 | 1405 | // 闲最久的那个进程超过了空闲阀值,杀死 1406 | fpm_request_last_activity(last_idle_child, &last); 1407 | fpm_clock_get(&now); 1408 | if (last.tv_sec < now.tv_sec - wp->config->pm_process_idle_timeout) { 1409 | last_idle_child->idle_kill = 1; 1410 | fpm_pctl_kill(last_idle_child->pid, FPM_PCTL_QUIT); 1411 | } 1412 | 1413 | continue; 1414 | } 1415 | 1416 | ``` 1417 | 1418 | 因为之前统计出空闲最久的子进程是哪个,如果这个子进程处于空闲状态超过阀值,就给它发送SIGQUIT信号杀死它,这就是收缩过程,因为流量并不大,子进程也不忙。 1419 | 1420 | 1421 | ### 子进程管理策略 -- STATIC 1422 | 1423 | 静态模式,也就是固定数量的子进程,这种情况下不需要进行子进程伸缩。 1424 | 1425 | 之前的健康检查定时器会在子进程退出后立即重新拉起,来保证子进程数量恒定不变。 1426 | 1427 | ``` 1428 | fpm/fpm/fpm_process_ctl.c 1429 | 1430 | 1431 | // 固定进程数的就此退出,不需要执行后续逻辑 1432 | if (wp->config->pm != PM_STYLE_DYNAMIC) continue; 1433 | 1434 | zlog(ZLOG_DEBUG, "[pool %s] currently %d active children, %d spare children, %d running children. Spawning rate %d", wp->config->name, active, idle, wp->running_children, wp->idle_spawn_rate); 1435 | 1436 | ``` 1437 | 1438 | ### 子进程管理策略 -- Dynamic 1439 | 1440 | 动态模式,这种配置指定了初始子进程数量,最小空闲进程数量,最大空闲进程数量,最多进程数量,是一种规则比较复杂,但资源控制比较优秀的方法。 1441 | 1442 | ``` 1443 | fpm/fpm/fpm_process_ctl.c 1444 | 1445 | 1446 | // 空闲进程数量大于了配置中的空闲最大值,那么干掉闲最久的进程 1447 | if (idle > wp->config->pm_max_spare_servers && last_idle_child) { 1448 | last_idle_child->idle_kill = 1; 1449 | fpm_pctl_kill(last_idle_child->pid, FPM_PCTL_QUIT); 1450 | wp->idle_spawn_rate = 1; 1451 | continue; 1452 | } 1453 | ``` 1454 | 1455 | 如果空闲的进程数量超过了最大空闲数量限制,就杀死最闲的那个。 1456 | 1457 | ``` 1458 | fpm/fpm/fpm_process_ctl.c 1459 | 1460 | 1461 | // 空闲进程数量小于配置中的空闲最小值 1462 | if (idle < wp->config->pm_min_spare_servers) { 1463 | 1464 | // 孩子总数虽然超过了配置中的最大进程数量,但是因为空闲的进程数量不多,说明负载很高,只是打日志提示一下 1465 | if (wp->running_children >= wp->config->pm_max_children) { 1466 | if (!wp->warn_max_children) { 1467 | fpm_scoreboard_update(0, 0, 0, 0, 0, 1, 0, FPM_SCOREBOARD_ACTION_INC, wp->scoreboard); 1468 | zlog(ZLOG_WARNING, "[pool %s] server reached pm.max_children setting (%d), consider raising it", wp->config->name, wp->config->pm_max_children); 1469 | wp->warn_max_children = 1; 1470 | } 1471 | wp->idle_spawn_rate = 1; 1472 | continue; 1473 | } 1474 | 1475 | ``` 1476 | 1477 | 如果空闲进程数量小于最小空闲进程限制,说明目前流量比较大,没有充足的空闲进程响应更多请求。 1478 | 1479 | 按照道理,此时应该增加更多子进程来缓解压力,但是如果进程总数量超过了最大进程数量的限制,那么是不能扩容的,此时只是打印一个日志警告而已。 1480 | 1481 | ``` 1482 | fpm/fpm/fpm_process_ctl.c 1483 | 1484 | 1485 | // 算一下要补充多少子进程 1486 | children_to_fork = MIN(wp->idle_spawn_rate, wp->config->pm_min_spare_servers - idle); 1487 | 1488 | /* get sure it won't exceed max_children */ 1489 | children_to_fork = MIN(children_to_fork, wp->config->pm_max_children - wp->running_children); 1490 | if (children_to_fork <= 0) { 1491 | if (!wp->warn_max_children) { 1492 | fpm_scoreboard_update(0, 0, 0, 0, 0, 1, 0, FPM_SCOREBOARD_ACTION_INC, wp->scoreboard); 1493 | zlog(ZLOG_WARNING, "[pool %s] server reached pm.max_children setting (%d), consider raising it", wp->config->name, wp->config->pm_max_children); 1494 | wp->warn_max_children = 1; 1495 | } 1496 | wp->idle_spawn_rate = 1; 1497 | continue; 1498 | } 1499 | wp->warn_max_children = 0; 1500 | 1501 | // 拉起children_to_fork个子进程 1502 | fpm_children_make(wp, 1, children_to_fork, 1); 1503 | ``` 1504 | 1505 | 相反,如果此时没有达到最大进程数量限制,那么就可以通过扩容子进程缓解压力。 1506 | 1507 | 这里做了一些规则计算,细节并不重要,总之可以新建的子进程数+现有进程数量不能超过总进程数限制。 1508 | 1509 | 最后调用fpm_children_make方法创建这些子进程,该函数之前已经讲过。 1510 | 1511 | ### 最后 -- 事件循环起来 1512 | 1513 | php-fpm父进程负责子进程管理,通过信号的方式与子进程通讯,从而实现强控子进程的能力。 1514 | 1515 | 父进程采取了事件循环来同时实现多个逻辑的并发处理:监测子进程的标准输出、标准错误输出,监测信号,定时器。 1516 | 1517 | 当fpm主进程将一切准备就绪,包括1个信号管道,2个常规定时器准备就绪后,就会进入正式的epoll事件循环。 1518 | 1519 | ### 定时器的前置处理 1520 | 1521 | ``` 1522 | fpm/fpm/fpm_events.c 1523 | 1524 | 1525 | while (1) { 1526 | struct fpm_event_queue_s *q, *q2; 1527 | struct timeval ms; 1528 | struct timeval tmp; 1529 | struct timeval now; 1530 | unsigned long int timeout; 1531 | int ret; 1532 | 1533 | /* sanity check */ 1534 | if (fpm_globals.parent_pid != getpid()) { 1535 | return; 1536 | } 1537 | 1538 | fpm_clock_get(&now); 1539 | timerclear(&ms); 1540 | 1541 | /* search in the timeout queue for the next timer to trigger */ 1542 | 1543 | // 找到最近一个要到期的定时器,作为event loop超时时间 1544 | q = fpm_event_queue_timer; 1545 | while (q) { 1546 | if (!timerisset(&ms)) { 1547 | ms = q->ev->timeout; 1548 | } else { 1549 | if (timercmp(&q->ev->timeout, &ms, <)) { 1550 | ms = q->ev->timeout; 1551 | } 1552 | } 1553 | q = q->next; 1554 | } 1555 | 1556 | /* 1s timeout if none has been set */ 1557 | if (!timerisset(&ms) || timercmp(&ms, &now, <) || timercmp(&ms, &now, ==)) { 1558 | timeout = 1000; 1559 | } else { 1560 | timersub(&ms, &now, &tmp); 1561 | timeout = (tmp.tv_sec * 1000) + (tmp.tv_usec / 1000) + 1; 1562 | } 1563 | ``` 1564 | 1565 | 所有的定时器串在一个fpm_event_queue_timer链表里,首先找到最近要到期的那个定时器,计算得到它距离现在还有多久会到期,保存在timeout里。 1566 | 1567 | 然后将timeout作为epoll的超时事件,这样避免epoll平时没有事件触发挂起,导致定时器无法处理。这个设计是任何一款异步网络框架都会涉及的,有相关经验同学不会觉得陌生。 1568 | 1569 | ### 调用epoll等待事件触发 1570 | 1571 | ``` 1572 | fpm/fpm/fpm_events.c 1573 | 1574 | 1575 | // 监听多个fd的事件循环, 回调fd的事件处理函数 1576 | ret = module->wait(fpm_event_queue_fd, timeout); 1577 | 1578 | /* is a child, nothing to do here */ 1579 | if (ret == -2) { 1580 | return; 1581 | } 1582 | ``` 1583 | 1584 | wait其实等价于调用epoll_wait,内部会根据发生事件的fd回调注册的函数,这里可能主要就是我们之前提到的信号unix socket pair,用于响应信号,包括子进程退出的信号。 1585 | 1586 | ### 定时器的执行 1587 | 1588 | 当fd的事件经过epoll_wait处理完成后,我们需要遍历所有定时器,查看哪些定时器过期需要执行,仅此而已。 1589 | 1590 | 超时的定时器需要调用fpm_event_fire回调当时注册的方法,也就是我们之前谈到的2个常规定时器。 1591 | 1592 | 因为上述2个定时都是常规定时器,所以如果ev->flags & FPM_EV_PERSIST非空,则表示这是一个常规定时器,需要重新注册到定时链表,等待下次调度。 1593 | 1594 | ``` 1595 | // 遍历所有注册的定时器 1596 | q = fpm_event_queue_timer; 1597 | while (q) { 1598 | fpm_clock_get(&now); 1599 | if (q->ev) { 1600 | 1601 | // 到期的就回调 1602 | if (timercmp(&now, &q->ev->timeout, >) || timercmp(&now, &q->ev->timeout, ==)) { 1603 | 1604 | // 回调用户函数 1605 | fpm_event_fire(q->ev); 1606 | 1607 | /* sanity check */ 1608 | if (fpm_globals.parent_pid != getpid()) { 1609 | return; 1610 | } 1611 | 1612 | // 如果是持久化的定时器,那么再次注册回去等待下次触发 1613 | if (q->ev->flags & FPM_EV_PERSIST) { 1614 | fpm_event_set_timeout(q->ev, now); 1615 | } else { /* delete the event */ 1616 | q2 = q; 1617 | if (q->prev) { 1618 | q->prev->next = q->next; 1619 | } 1620 | if (q->next) { 1621 | q->next->prev = q->prev; 1622 | } 1623 | if (q == fpm_event_queue_timer) { 1624 | fpm_event_queue_timer = q->next; 1625 | if (fpm_event_queue_timer) { 1626 | fpm_event_queue_timer->prev = NULL; 1627 | } 1628 | } 1629 | q = q->next; 1630 | free(q2); 1631 | continue; 1632 | } 1633 | } 1634 | } 1635 | q = q->next; 1636 | } 1637 | ``` 1638 | 1639 | ## 结束 1640 | 1641 | PHP-FPM围绕进程管理设计实现,基于共享内存的记分板实现子进程状态检测,子进程采用阻塞模型,父进程基于信号控制子进程管理,整体设计保持简单纯粹。 1642 | 1643 | 1644 | --------------------------------------------------------------------------------