Skip to content

Commit 840c0d2

Browse files
committed
fpm binding master and children processes to specific core(s).
rational is there is git snippets and similar floating around about how to achieve this via command line. So the idea is to do it "natively" especially those above mostly focus on linux whereas more platforms are cpu affinity "capable".
1 parent c8ec2ed commit 840c0d2

File tree

10 files changed

+336
-1
lines changed

10 files changed

+336
-1
lines changed

sapi/fpm/config.m4

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ PHP_ARG_ENABLE([fpm],,
77
dnl Configure checks.
88
AC_DEFUN([AC_FPM_STDLIBS],
99
[
10-
AC_CHECK_FUNCS(clearenv setproctitle setproctitle_fast)
10+
AC_CHECK_FUNCS(clearenv setproctitle setproctitle_fast cpuset_setaffinity sched_setaffinity)
11+
AC_CHECK_HEADERS([sys/cpuset.h sched.h])
1112
1213
AC_SEARCH_LIBS(socket, socket)
1314
AC_SEARCH_LIBS(inet_addr, nsl)

sapi/fpm/fpm/fpm_conf.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ static struct ini_value_parser_s ini_fpm_global_options[] = {
9999
{ "process_control_timeout", &fpm_conf_set_time, GO(process_control_timeout) },
100100
{ "process.max", &fpm_conf_set_integer, GO(process_max) },
101101
{ "process.priority", &fpm_conf_set_integer, GO(process_priority) },
102+
#if HAVE_FPM_CPUAFFINITY
103+
{ "process.cpu_list", &fpm_conf_set_string, GO(process_cpu_list) },
104+
#endif
102105
{ "daemonize", &fpm_conf_set_boolean, GO(daemonize) },
103106
{ "rlimit_files", &fpm_conf_set_integer, GO(rlimit_files) },
104107
{ "rlimit_core", &fpm_conf_set_rlimit_core, GO(rlimit_core) },
@@ -131,6 +134,9 @@ static struct ini_value_parser_s ini_fpm_pool_options[] = {
131134
#endif
132135
{ "process.priority", &fpm_conf_set_integer, WPO(process_priority) },
133136
{ "process.dumpable", &fpm_conf_set_boolean, WPO(process_dumpable) },
137+
#if HAVE_FPM_CPUAFFINITY
138+
{ "process.cpu_list", &fpm_conf_set_string, WPO(process_cpu_list) },
139+
#endif
134140
{ "pm", &fpm_conf_set_pm, WPO(pm) },
135141
{ "pm.max_children", &fpm_conf_set_integer, WPO(pm_max_children) },
136142
{ "pm.start_servers", &fpm_conf_set_integer, WPO(pm_start_servers) },
@@ -622,6 +628,9 @@ static void *fpm_worker_pool_config_alloc(void)
622628
wp->config->pm_process_idle_timeout = 10; /* 10s by default */
623629
wp->config->process_priority = 64; /* 64 means unset */
624630
wp->config->process_dumpable = 0;
631+
#if HAVE_FPM_CPUAFFINITY
632+
wp->config->process_cpu_list = NULL;
633+
#endif
625634
wp->config->clear_env = 1;
626635
wp->config->decorate_workers_output = 1;
627636
#ifdef SO_SETFIB
@@ -1733,6 +1742,9 @@ static void fpm_conf_dump(void)
17331742
} else {
17341743
zlog(ZLOG_NOTICE, "\tprocess.priority = %d", fpm_global_config.process_priority);
17351744
}
1745+
#if HAVE_FPM_CPUAFFINITY
1746+
zlog(ZLOG_NOTICE, "\tprocess.cpu_list = %s", STR2STR(fpm_global_config.process_cpu_list));
1747+
#endif
17361748
zlog(ZLOG_NOTICE, "\tdaemonize = %s", BOOL2STR(fpm_global_config.daemonize));
17371749
zlog(ZLOG_NOTICE, "\trlimit_files = %d", fpm_global_config.rlimit_files);
17381750
zlog(ZLOG_NOTICE, "\trlimit_core = %d", fpm_global_config.rlimit_core);
@@ -1772,6 +1784,9 @@ static void fpm_conf_dump(void)
17721784
zlog(ZLOG_NOTICE, "\tprocess.priority = %d", wp->config->process_priority);
17731785
}
17741786
zlog(ZLOG_NOTICE, "\tprocess.dumpable = %s", BOOL2STR(wp->config->process_dumpable));
1787+
#if HAVE_FPM_CPUAFFINITY
1788+
zlog(ZLOG_NOTICE, "\tprocess.cpu_list = %s", STR2STR(wp->config->process_cpu_list));
1789+
#endif
17751790
zlog(ZLOG_NOTICE, "\tpm = %s", PM2STR(wp->config->pm));
17761791
zlog(ZLOG_NOTICE, "\tpm.max_children = %d", wp->config->pm_max_children);
17771792
zlog(ZLOG_NOTICE, "\tpm.start_servers = %d", wp->config->pm_start_servers);

sapi/fpm/fpm/fpm_conf.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ struct fpm_global_config_s {
4444
int systemd_watchdog;
4545
int systemd_interval;
4646
#endif
47+
#if HAVE_FPM_CPUAFFINITY
48+
char *process_cpu_list;
49+
#endif
4750
};
4851

4952
extern struct fpm_global_config_s fpm_global_config;
@@ -107,6 +110,9 @@ struct fpm_worker_pool_config_s {
107110
#ifdef SO_SETFIB
108111
int listen_setfib;
109112
#endif
113+
#if HAVE_FPM_CPUAFFINITY
114+
char *process_cpu_list;
115+
#endif
110116
};
111117

112118
struct ini_value_parser_s {

sapi/fpm/fpm/fpm_config.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,16 @@
7575
#else
7676
# define HAVE_FPM_LQ 0
7777
#endif
78+
79+
#if defined(HAVE_SCHED_SETAFFINITY) || defined(HAVE_CPUSET_SETAFFINITY)
80+
/*
81+
* to expand to other platforms capable of this granularity (some BSD, solaris, ...).
82+
* macOS is a specific case with an api working fine on intel architecture
83+
* whereas on arm the api and semantic behind is different, since it is about
84+
* Quality Of Service, i.e. binding to a group of cores for high performance
85+
* vs cores for low energy consumption.
86+
*/
87+
# define HAVE_FPM_CPUAFFINITY 1
88+
#else
89+
# define HAVE_FPM_CPUAFFINITY 0
90+
#endif

sapi/fpm/fpm/fpm_unix.c

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <stdlib.h>
99
#include <unistd.h>
1010
#include <sys/types.h>
11+
#include <errno.h>
1112
#include <pwd.h>
1213
#include <grp.h>
1314

@@ -35,6 +36,15 @@
3536
#include <selinux/selinux.h>
3637
#endif
3738

39+
#ifdef HAVE_SCHED_H
40+
#include <sched.h>
41+
#endif
42+
43+
#ifdef HAVE_SYS_CPUSET_H
44+
#include <sys/cpuset.h>
45+
typedef cpuset_t cpu_set_t;
46+
#endif
47+
3848
#include "fpm.h"
3949
#include "fpm_conf.h"
4050
#include "fpm_cleanup.h"
@@ -392,6 +402,101 @@ static int fpm_unix_conf_wp(struct fpm_worker_pool_s *wp) /* {{{ */
392402
}
393403
/* }}} */
394404

405+
#if HAVE_FPM_CPUAFFINITY
406+
struct fpm_cpuaffinity_conf {
407+
cpu_set_t cset;
408+
long min;
409+
long max;
410+
};
411+
412+
static long fpm_cpumax(void)
413+
{
414+
static long cpuid = LONG_MIN;
415+
if (cpuid == LONG_MIN) {
416+
cpu_set_t cset;
417+
#if defined(HAVE_SCHED_SETAFFINITY)
418+
if (sched_getaffinity(0, sizeof(cset), &cset) == 0) {
419+
#elif defined(HAVE_CPUSET_SETAFFINITY)
420+
if (cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, -1, sizeof(cset), &cset) == 0) {
421+
#endif
422+
cpuid = CPU_COUNT(&cset);
423+
} else {
424+
cpuid = -1;
425+
}
426+
}
427+
428+
return cpuid;
429+
}
430+
431+
static void fpm_cpuaffinity_init(struct fpm_cpuaffinity_conf *c)
432+
{
433+
CPU_ZERO(&c->cset);
434+
}
435+
436+
static void fpm_cpuaffinity_add(struct fpm_cpuaffinity_conf *c)
437+
{
438+
#if defined(HAVE_FPM_CPUAFFINITY)
439+
int i;
440+
441+
for (i = c->min; i <= c->max; i ++) {
442+
if (!CPU_ISSET(i, &c->cset)) {
443+
CPU_SET(i, &c->cset);
444+
}
445+
}
446+
#endif
447+
}
448+
449+
static int fpm_cpuaffinity_set(struct fpm_cpuaffinity_conf *c)
450+
{
451+
#if defined(HAVE_SCHED_SETAFFINITY)
452+
return sched_setaffinity(0, sizeof(c->cset), &c->cset);
453+
#elif defined(HAVE_CPUSET_SETAFFINITY)
454+
return cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, -1, sizeof(c->cset), &c->cset);
455+
#endif
456+
}
457+
458+
static int fpm_setcpuaffinity(char *cpu_list)
459+
{
460+
char *token, *buf;
461+
struct fpm_cpuaffinity_conf fconf;
462+
int r, cpumax;
463+
464+
r = -1;
465+
cpumax = fpm_cpumax();
466+
467+
fpm_cpuaffinity_init(&fconf);
468+
token = php_strtok_r(cpu_list, ",", &buf);
469+
470+
while (token) {
471+
char *cpu_listsep;
472+
473+
fconf.min = strtol(token, &cpu_listsep, 0);
474+
if (errno || fconf.min < 0 || fconf.min > cpumax) {
475+
return -1;
476+
}
477+
fconf.max = fconf.min;
478+
if (*cpu_listsep == '-') {
479+
if (strlen(cpu_listsep) > 1) {
480+
char *err;
481+
fconf.max = strtol(cpu_listsep + 1, &err, 0);
482+
if (errno || *err != '\0' || fconf.max < fconf.min || fconf.max > cpumax) {
483+
return -1;
484+
}
485+
} else {
486+
return -1;
487+
}
488+
}
489+
490+
fpm_cpuaffinity_add(&fconf);
491+
492+
token = php_strtok_r(NULL, ";", &buf);
493+
}
494+
495+
r = fpm_cpuaffinity_set(&fconf);
496+
return r;
497+
}
498+
#endif
499+
395500
int fpm_unix_init_child(struct fpm_worker_pool_s *wp) /* {{{ */
396501
{
397502
int is_root = !geteuid();
@@ -416,6 +521,15 @@ int fpm_unix_init_child(struct fpm_worker_pool_s *wp) /* {{{ */
416521
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);
417522
}
418523
}
524+
#if HAVE_FPM_CPUAFFINITY
525+
if (wp->config->process_cpu_list) {
526+
527+
if (0 > fpm_setcpuaffinity(wp->config->process_cpu_list)) {
528+
zlog(ZLOG_SYSERROR, "[pool %s] failed to fpm_setcpuaffinity(%s)", wp->config->name, wp->config->process_cpu_list);
529+
return -1;
530+
}
531+
}
532+
#endif
419533

420534
if (is_root && wp->config->chroot && *wp->config->chroot) {
421535
if (0 > chroot(wp->config->chroot)) {
@@ -662,6 +776,15 @@ int fpm_unix_init_main(void)
662776
}
663777
}
664778

779+
#if HAVE_FPM_CPUAFFINITY
780+
if (fpm_global_config.process_cpu_list) {
781+
if (0 > fpm_setcpuaffinity(fpm_global_config.process_cpu_list)) {
782+
zlog(ZLOG_SYSERROR, "failed to fpm_setcpuaffinity(%s)", fpm_global_config.process_cpu_list);
783+
return -1;
784+
}
785+
}
786+
#endif
787+
665788
fpm_globals.parent_pid = getpid();
666789
for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
667790
if (0 > fpm_unix_conf_wp(wp)) {

sapi/fpm/php-fpm.conf.in

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,18 @@
125125
; Default value: 10
126126
;systemd_interval = 10
127127

128+
; Bind the master process to a cpu set.
129+
; The value can be one cpu id, a range or a list thereof.
130+
;
131+
; Default Value: not set
132+
; Valid syntaxes are:
133+
; process.cpu_list = "cpu id" - to bind to a single core
134+
; process.cpu_list = "min cpu id-max cpu id" - to bind to a core range
135+
from minimum cpu id to max
136+
; process.cpu_list = "[min cpu id-max cpu id],[min cpu id-max cpu id],..."
137+
- to bind to multiple ranges
138+
separated by a comma
139+
128140
;;;;;;;;;;;;;;;;;;;;
129141
; Pool Definitions ;
130142
;;;;;;;;;;;;;;;;;;;;

sapi/fpm/tests/cpuaffinity-fail.phpt

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
--TEST--
2+
FPM: cpu affinity test
3+
--SKIPIF--
4+
<?php include "skipif.inc";
5+
6+
if (!str_contains(PHP_OS, 'Linux') && !str_contains(PHP_OS, 'FreeBSD')) {
7+
die('skipped supported only on Linux and FreeBSD');
8+
}
9+
?>
10+
--FILE--
11+
<?php
12+
13+
require_once "tester.inc";
14+
15+
$cfg = <<<EOT
16+
[global]
17+
process.cpu_list = 1000024
18+
error_log = {{FILE:LOG}}
19+
pid = {{FILE:PID}}
20+
[unconfined]
21+
listen = {{ADDR:IPv4}}
22+
pm = dynamic
23+
pm.max_children = 1
24+
pm.start_servers = 1
25+
pm.min_spare_servers = 1
26+
pm.max_spare_servers = 1
27+
EOT;
28+
29+
$tester = new FPM\Tester($cfg);
30+
$tester->start();
31+
$tester->expectLogError("failed to fpm_setcpuaffinity\(\d+\): Inappropriate ioctl for device \(\d+\)");
32+
$tester->expectLogError("FPM initialization failed");
33+
$tester->close();
34+
35+
?>
36+
Done
37+
--EXPECT--
38+
Done
39+
--CLEAN--
40+
<?php
41+
require_once "tester.inc";
42+
FPM\Tester::clean();
43+
?>

sapi/fpm/tests/cpuaffinity-range.phpt

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
--TEST--
2+
FPM: cpu affinity test, range
3+
--SKIPIF--
4+
<?php include "skipif.inc";
5+
6+
if (!str_contains(PHP_OS, 'Linux') && !str_contains(PHP_OS, 'FreeBSD')) {
7+
die('skipped supported only on Linux and FreeBSD');
8+
}
9+
10+
if (str_contains(PHP_OS, 'Linux')) {
11+
$cmd = 'nproc';
12+
} else {
13+
$cmd = 'sysctl hw.ncpus';
14+
}
15+
16+
$nproc = intval(exec($cmd));
17+
if ($nproc < 4) {
18+
die('skipped supported only on 4 cores or more environments');
19+
}
20+
?>
21+
--FILE--
22+
<?php
23+
24+
require_once "tester.inc";
25+
26+
$cfg = <<<EOT
27+
[global]
28+
error_log = {{FILE:LOG}}
29+
pid = {{FILE:PID}}
30+
[unconfined]
31+
listen = {{ADDR:IPv4}}
32+
pm = dynamic
33+
pm.max_children = 1
34+
pm.start_servers = 1
35+
pm.min_spare_servers = 1
36+
pm.max_spare_servers = 1
37+
process.cpu_list = 0-1,2-3
38+
EOT;
39+
40+
$tester = new FPM\Tester($cfg);
41+
$tester->start();
42+
$tester->expectLogStartNotices();
43+
$tester->terminate();
44+
$tester->expectLogTerminatingNotices();
45+
$tester->close();
46+
47+
?>
48+
Done
49+
--EXPECT--
50+
Done
51+
--CLEAN--
52+
<?php
53+
require_once "tester.inc";
54+
FPM\Tester::clean();
55+
?>

0 commit comments

Comments
 (0)