diff --git a/sapi/fpm/config.m4 b/sapi/fpm/config.m4 index 4d4952eee86e7..3fe25ca163021 100644 --- a/sapi/fpm/config.m4 +++ b/sapi/fpm/config.m4 @@ -356,6 +356,8 @@ if test "$PHP_FPM" != "no"; then AC_CHECK_HEADER([priv.h], [AC_CHECK_FUNCS([setpflags])]) AC_CHECK_HEADER([sys/times.h], [AC_CHECK_FUNCS([times])]) AC_CHECK_HEADER([port.h], [AC_CHECK_FUNCS([port_create])]) + AC_CHECK_HEADER([sched.h], [AC_CHECK_FUNCS([sched_setaffinity])]) + AC_CHECK_HEADER([sys/cpuset.h], [AC_CHECK_FUNCS([cpuset_setaffinity])]) PHP_ARG_WITH([fpm-user],, [AS_HELP_STRING([[--with-fpm-user[=USER]]], diff --git a/sapi/fpm/fpm/fpm_conf.c b/sapi/fpm/fpm/fpm_conf.c index 7743d28f448dd..cfd5c7869218f 100644 --- a/sapi/fpm/fpm/fpm_conf.c +++ b/sapi/fpm/fpm/fpm_conf.c @@ -97,6 +97,9 @@ static const struct ini_value_parser_s ini_fpm_global_options[] = { { "process_control_timeout", &fpm_conf_set_time, GO(process_control_timeout) }, { "process.max", &fpm_conf_set_integer, GO(process_max) }, { "process.priority", &fpm_conf_set_integer, GO(process_priority) }, +#if HAVE_FPM_CPUAFFINITY + { "process.cpu_list", &fpm_conf_set_string, GO(process_cpu_list) }, +#endif { "daemonize", &fpm_conf_set_boolean, GO(daemonize) }, { "rlimit_files", &fpm_conf_set_integer, GO(rlimit_files) }, { "rlimit_core", &fpm_conf_set_rlimit_core, GO(rlimit_core) }, @@ -129,6 +132,9 @@ static const struct ini_value_parser_s ini_fpm_pool_options[] = { #endif { "process.priority", &fpm_conf_set_integer, WPO(process_priority) }, { "process.dumpable", &fpm_conf_set_boolean, WPO(process_dumpable) }, +#if HAVE_FPM_CPUAFFINITY + { "process.cpu_list", &fpm_conf_set_string, WPO(process_cpu_list) }, +#endif { "pm", &fpm_conf_set_pm, WPO(pm) }, { "pm.max_children", &fpm_conf_set_integer, WPO(pm_max_children) }, { "pm.start_servers", &fpm_conf_set_integer, WPO(pm_start_servers) }, @@ -634,6 +640,9 @@ static void *fpm_worker_pool_config_alloc(void) wp->config->pm_process_idle_timeout = 10; /* 10s by default */ wp->config->process_priority = 64; /* 64 means unset */ wp->config->process_dumpable = 0; +#if HAVE_FPM_CPUAFFINITY + wp->config->process_cpu_list = NULL; +#endif wp->config->clear_env = 1; wp->config->decorate_workers_output = 1; #ifdef SO_SETFIB @@ -1730,6 +1739,9 @@ static void fpm_conf_dump(void) } else { zlog(ZLOG_NOTICE, "\tprocess.priority = %d", fpm_global_config.process_priority); } +#if HAVE_FPM_CPUAFFINITY + zlog(ZLOG_NOTICE, "\tprocess.cpu_list = %s", STR2STR(fpm_global_config.process_cpu_list)); +#endif zlog(ZLOG_NOTICE, "\tdaemonize = %s", BOOL2STR(fpm_global_config.daemonize)); zlog(ZLOG_NOTICE, "\trlimit_files = %d", fpm_global_config.rlimit_files); zlog(ZLOG_NOTICE, "\trlimit_core = %d", fpm_global_config.rlimit_core); @@ -1769,6 +1781,9 @@ static void fpm_conf_dump(void) zlog(ZLOG_NOTICE, "\tprocess.priority = %d", wp->config->process_priority); } zlog(ZLOG_NOTICE, "\tprocess.dumpable = %s", BOOL2STR(wp->config->process_dumpable)); +#if HAVE_FPM_CPUAFFINITY + zlog(ZLOG_NOTICE, "\tprocess.cpu_list = %s", STR2STR(wp->config->process_cpu_list)); +#endif zlog(ZLOG_NOTICE, "\tpm = %s", PM2STR(wp->config->pm)); zlog(ZLOG_NOTICE, "\tpm.max_children = %d", wp->config->pm_max_children); zlog(ZLOG_NOTICE, "\tpm.start_servers = %d", wp->config->pm_start_servers); diff --git a/sapi/fpm/fpm/fpm_conf.h b/sapi/fpm/fpm/fpm_conf.h index 5b354a9bdecef..88d6ff8a49d36 100644 --- a/sapi/fpm/fpm/fpm_conf.h +++ b/sapi/fpm/fpm/fpm_conf.h @@ -44,6 +44,9 @@ struct fpm_global_config_s { int systemd_watchdog; int systemd_interval; #endif +#if HAVE_FPM_CPUAFFINITY + char *process_cpu_list; +#endif }; extern struct fpm_global_config_s fpm_global_config; @@ -107,6 +110,9 @@ struct fpm_worker_pool_config_s { #ifdef SO_SETFIB int listen_setfib; #endif +#if HAVE_FPM_CPUAFFINITY + char *process_cpu_list; +#endif }; struct ini_value_parser_s { diff --git a/sapi/fpm/fpm/fpm_config.h b/sapi/fpm/fpm/fpm_config.h index a637326ed767a..0c8832e4d939b 100644 --- a/sapi/fpm/fpm/fpm_config.h +++ b/sapi/fpm/fpm/fpm_config.h @@ -85,3 +85,16 @@ #else # define HAVE_FPM_LQ 0 #endif + +#if defined(HAVE_SCHED_SETAFFINITY) || defined(HAVE_CPUSET_SETAFFINITY) +/* + * to expand to other platforms capable of this granularity (some BSD, solaris, ...). + * macOS is a specific case with an api working fine on intel architecture + * whereas on arm the api and semantic behind is different, since it is about + * Quality Of Service, i.e. binding to a group of cores for high performance + * vs cores for low energy consumption. + */ +# define HAVE_FPM_CPUAFFINITY 1 +#else +# define HAVE_FPM_CPUAFFINITY 0 +#endif diff --git a/sapi/fpm/fpm/fpm_unix.c b/sapi/fpm/fpm/fpm_unix.c index b2f0e71d83314..1d564c4cdc62a 100644 --- a/sapi/fpm/fpm/fpm_unix.c +++ b/sapi/fpm/fpm/fpm_unix.c @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -36,6 +37,13 @@ #include #endif +#if defined(HAVE_SCHED_SETAFFINITY) +#include +#elif defined(HAVE_CPUSETAFFINITY) +#include +typedef cpuset_t cpu_set_t; +#endif + #include "fpm.h" #include "fpm_conf.h" #include "fpm_cleanup.h" @@ -421,6 +429,101 @@ static int fpm_unix_conf_wp(struct fpm_worker_pool_s *wp) /* {{{ */ } /* }}} */ +#if HAVE_FPM_CPUAFFINITY +static long fpm_cpumax(void) +{ + static long cpuid = LONG_MIN; + if (cpuid == LONG_MIN) { + cpu_set_t cset; +#if defined(HAVE_SCHED_SETAFFINITY) + if (sched_getaffinity(0, sizeof(cset), &cset) == 0) { +#elif defined(HAVE_CPUSET_SETAFFINITY) + if (cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, -1, sizeof(cset), &cset) == 0) { +#endif + cpuid = CPU_COUNT(&cset); + } else { + cpuid = -1; + } + } + + return cpuid; +} + +static void fpm_cpuaffinity_init(cpu_set_t *c) +{ + CPU_ZERO(c); +} + +static void fpm_cpuaffinity_add(cpu_set_t *c, int min, int max) +{ + int i; + + for (i = min; i <= max; i ++) { + if (!CPU_ISSET(i, c)) { + CPU_SET(i, c); + } + } +} + +static int fpm_cpuaffinity_set(cpu_set_t *c) +{ +#if defined(HAVE_SCHED_SETAFFINITY) + return sched_setaffinity(0, sizeof(c), c); +#elif defined(HAVE_CPUSET_SETAFFINITY) + return cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, -1, sizeof(c), c); +#endif +} + +static int fpm_setcpuaffinity(char *cpu_list) +{ + char *token, *buf, *ptr; + cpu_set_t c; + int r, cpumax, min, max; + + r = -1; + cpumax = fpm_cpumax(); + + fpm_cpuaffinity_init(&c); + ptr = estrdup(cpu_list); + token = php_strtok_r(ptr, ",", &buf); + + while (token) { + char *cpu_listsep; + + if (!isdigit(*token)) { + return -1; + } + + min = strtol(token, &cpu_listsep, 0); + if (errno || (*cpu_listsep != '\0' && *cpu_listsep != '-') || min < 0 || min > cpumax) { + efree(ptr); + return -1; + } + max = min; + if (*cpu_listsep == '-') { + if (strlen(cpu_listsep) > 1) { + char *err; + max = strtol(cpu_listsep + 1, &err, 0); + if (errno || *err != '\0' || max < min || max > cpumax) { + efree(ptr); + return -1; + } + } else { + efree(ptr); + return -1; + } + } + + fpm_cpuaffinity_add(&c, min, max); + token = php_strtok_r(NULL, ",", &buf); + } + + r = fpm_cpuaffinity_set(&c); + efree(ptr); + return r; +} +#endif + int fpm_unix_init_child(struct fpm_worker_pool_s *wp) /* {{{ */ { int is_root = !geteuid(); @@ -445,6 +548,14 @@ int fpm_unix_init_child(struct fpm_worker_pool_s *wp) /* {{{ */ zlog(ZLOG_SYSERROR, "[pool %s] failed to set rlimit_core for this pool. Please check your system limits or decrease rlimit_core. setrlimit(RLIMIT_CORE, %d)", wp->config->name, wp->config->rlimit_core); } } +#if HAVE_FPM_CPUAFFINITY + if (wp->config->process_cpu_list) { + if (0 > fpm_setcpuaffinity(wp->config->process_cpu_list)) { + zlog(ZLOG_SYSERROR, "[pool %s] failed to fpm_setcpuaffinity(%s)", wp->config->name, wp->config->process_cpu_list); + return -1; + } + } +#endif if (is_root && wp->config->chroot && *wp->config->chroot) { if (0 > chroot(wp->config->chroot)) { @@ -692,6 +803,15 @@ int fpm_unix_init_main(void) } } +#if HAVE_FPM_CPUAFFINITY + if (fpm_global_config.process_cpu_list) { + if (0 > fpm_setcpuaffinity(fpm_global_config.process_cpu_list)) { + zlog(ZLOG_SYSERROR, "failed to fpm_setcpuaffinity(%s)", fpm_global_config.process_cpu_list); + return -1; + } + } +#endif + fpm_globals.parent_pid = getpid(); for (wp = fpm_worker_all_pools; wp; wp = wp->next) { if (0 > fpm_unix_conf_wp(wp)) { diff --git a/sapi/fpm/php-fpm.conf.in b/sapi/fpm/php-fpm.conf.in index f1b48adca08f2..f9522e7d39f22 100644 --- a/sapi/fpm/php-fpm.conf.in +++ b/sapi/fpm/php-fpm.conf.in @@ -124,6 +124,19 @@ ; Default value: 10 ;systemd_interval = 10 +; Bind the master process to a cpu set. +; The value can be one cpu id, a range or a list thereof. +; +; Default Value: not set +; Valid syntaxes are: +; process.cpu_list = "cpu id" - bind master process to cpu id +; process.cpu_list = "[min cpu id]-[max cpu id]" + - bind master process from min cpu id + to max cpu id +; process.cpu_list = "[[min cpu id]-[max cpu id],[min cpu id-max cpu id],...]" + - bind master process to cpu id ranges + separated by a comma + ;;;;;;;;;;;;;;;;;;;; ; Pool Definitions ; ;;;;;;;;;;;;;;;;;;;; diff --git a/sapi/fpm/tests/cpuaffinity-fail.phpt b/sapi/fpm/tests/cpuaffinity-fail.phpt new file mode 100644 index 0000000000000..d9bb4a6fffa06 --- /dev/null +++ b/sapi/fpm/tests/cpuaffinity-fail.phpt @@ -0,0 +1,43 @@ +--TEST-- +FPM: cpu affinity test +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogError("failed to fpm_setcpuaffinity\(\d+\): Inappropriate ioctl for device \(\d+\)"); +$tester->expectLogError("FPM initialization failed"); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/cpuaffinity-fail2.phpt b/sapi/fpm/tests/cpuaffinity-fail2.phpt new file mode 100644 index 0000000000000..5b29cc5094c54 --- /dev/null +++ b/sapi/fpm/tests/cpuaffinity-fail2.phpt @@ -0,0 +1,42 @@ +--TEST-- +FPM: cpu affinity test +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogError("FPM initialization failed"); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/cpuaffinity-fail3.phpt b/sapi/fpm/tests/cpuaffinity-fail3.phpt new file mode 100644 index 0000000000000..10c6fb5fcc682 --- /dev/null +++ b/sapi/fpm/tests/cpuaffinity-fail3.phpt @@ -0,0 +1,42 @@ +--TEST-- +FPM: cpu affinity test +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogError("FPM initialization failed"); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/cpuaffinity-multi.phpt b/sapi/fpm/tests/cpuaffinity-multi.phpt new file mode 100644 index 0000000000000..3b8031940f6dd --- /dev/null +++ b/sapi/fpm/tests/cpuaffinity-multi.phpt @@ -0,0 +1,48 @@ +--TEST-- +FPM: cpu affinity test +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/cpuaffinity-range.phpt b/sapi/fpm/tests/cpuaffinity-range.phpt new file mode 100644 index 0000000000000..e08db71cc5bc6 --- /dev/null +++ b/sapi/fpm/tests/cpuaffinity-range.phpt @@ -0,0 +1,48 @@ +--TEST-- +FPM: cpu affinity test, range +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/cpuaffinity.phpt b/sapi/fpm/tests/cpuaffinity.phpt new file mode 100644 index 0000000000000..ca17e885a700c --- /dev/null +++ b/sapi/fpm/tests/cpuaffinity.phpt @@ -0,0 +1,48 @@ +--TEST-- +FPM: cpu affinity test +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/tester.inc b/sapi/fpm/tests/tester.inc index f486309085e6d..528b26c10d558 100644 --- a/sapi/fpm/tests/tester.inc +++ b/sapi/fpm/tests/tester.inc @@ -203,6 +203,19 @@ class Tester } } + static public function getCores() + { + if (str_contains(PHP_OS, 'Linux')) { + $cmd = 'nproc'; + } else if (str_contains(PHP_OS, 'FreeBSD') || str_contains(PHP_OS, 'Darwin')) { + $cmd = 'sysctl hw.ncpus'; + } else { + return 0; + } + + return intval(exec($cmd)); + } + /** * @param int $backTraceIndex * diff --git a/sapi/fpm/www.conf.in b/sapi/fpm/www.conf.in index 69df3e6630047..df125c4e5f38f 100644 --- a/sapi/fpm/www.conf.in +++ b/sapi/fpm/www.conf.in @@ -80,6 +80,19 @@ listen = 127.0.0.1:9000 ; Default Value: no set ; process.priority = -19 +; Bind the pool processes to a cpu set. +; The value can be one cpu id, a range or a list thereof. +; +; Default Value: inherits master's cpu affinity +; Valid syntaxes are: +; process.cpu_list = "cpu id" - bind pool process to cpu id +; process.cpu_list = "[min cpu id]-[max cpu id]" + - bind pool process from + min cpu id to max cpu id +; process.cpu_list = "[[min cpu id]-[max cpu id],[min cpu id-max cpu id],...]" + - bind pool process to cpu id + ranges separated by a comma + ; Set the process dumpable flag (PR_SET_DUMPABLE prctl for Linux or ; PROC_TRACE_CTL procctl for FreeBSD) even if the process user ; or group is different than the master process user. It allows to create process