Skip to content

Commit db3e622

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 ca4a841 commit db3e622

File tree

9 files changed

+334
-0
lines changed

9 files changed

+334
-0
lines changed

sapi/fpm/fpm/fpm_conf.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ static const struct ini_value_parser_s ini_fpm_global_options[] = {
9797
{ "process_control_timeout", &fpm_conf_set_time, GO(process_control_timeout) },
9898
{ "process.max", &fpm_conf_set_integer, GO(process_max) },
9999
{ "process.priority", &fpm_conf_set_integer, GO(process_priority) },
100+
#if HAVE_FPM_CPUAFFINITY
101+
{ "process.cpu_list", &fpm_conf_set_string, GO(process_cpu_list) },
102+
#endif
100103
{ "daemonize", &fpm_conf_set_boolean, GO(daemonize) },
101104
{ "rlimit_files", &fpm_conf_set_integer, GO(rlimit_files) },
102105
{ "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[] = {
129132
#endif
130133
{ "process.priority", &fpm_conf_set_integer, WPO(process_priority) },
131134
{ "process.dumpable", &fpm_conf_set_boolean, WPO(process_dumpable) },
135+
#if HAVE_FPM_CPUAFFINITY
136+
{ "process.cpu_list", &fpm_conf_set_string, WPO(process_cpu_list) },
137+
#endif
132138
{ "pm", &fpm_conf_set_pm, WPO(pm) },
133139
{ "pm.max_children", &fpm_conf_set_integer, WPO(pm_max_children) },
134140
{ "pm.start_servers", &fpm_conf_set_integer, WPO(pm_start_servers) },
@@ -634,6 +640,9 @@ static void *fpm_worker_pool_config_alloc(void)
634640
wp->config->pm_process_idle_timeout = 10; /* 10s by default */
635641
wp->config->process_priority = 64; /* 64 means unset */
636642
wp->config->process_dumpable = 0;
643+
#if HAVE_FPM_CPUAFFINITY
644+
wp->config->process_cpu_list = NULL;
645+
#endif
637646
wp->config->clear_env = 1;
638647
wp->config->decorate_workers_output = 1;
639648
#ifdef SO_SETFIB
@@ -1730,6 +1739,9 @@ static void fpm_conf_dump(void)
17301739
} else {
17311740
zlog(ZLOG_NOTICE, "\tprocess.priority = %d", fpm_global_config.process_priority);
17321741
}
1742+
#if HAVE_FPM_CPUAFFINITY
1743+
zlog(ZLOG_NOTICE, "\tprocess.cpu_list = %s", STR2STR(fpm_global_config.process_cpu_list));
1744+
#endif
17331745
zlog(ZLOG_NOTICE, "\tdaemonize = %s", BOOL2STR(fpm_global_config.daemonize));
17341746
zlog(ZLOG_NOTICE, "\trlimit_files = %d", fpm_global_config.rlimit_files);
17351747
zlog(ZLOG_NOTICE, "\trlimit_core = %d", fpm_global_config.rlimit_core);
@@ -1769,6 +1781,9 @@ static void fpm_conf_dump(void)
17691781
zlog(ZLOG_NOTICE, "\tprocess.priority = %d", wp->config->process_priority);
17701782
}
17711783
zlog(ZLOG_NOTICE, "\tprocess.dumpable = %s", BOOL2STR(wp->config->process_dumpable));
1784+
#if HAVE_FPM_CPUAFFINITY
1785+
zlog(ZLOG_NOTICE, "\tprocess.cpu_list = %s", STR2STR(wp->config->process_cpu_list));
1786+
#endif
17721787
zlog(ZLOG_NOTICE, "\tpm = %s", PM2STR(wp->config->pm));
17731788
zlog(ZLOG_NOTICE, "\tpm.max_children = %d", wp->config->pm_max_children);
17741789
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
@@ -85,3 +85,16 @@
8585
#else
8686
# define HAVE_FPM_LQ 0
8787
#endif
88+
89+
#if defined(HAVE_SCHED_SETAFFINITY) || defined(HAVE_CPUSET_SETAFFINITY)
90+
/*
91+
* to expand to other platforms capable of this granularity (some BSD, solaris, ...).
92+
* macOS is a specific case with an api working fine on intel architecture
93+
* whereas on arm the api and semantic behind is different, since it is about
94+
* Quality Of Service, i.e. binding to a group of cores for high performance
95+
* vs cores for low energy consumption.
96+
*/
97+
# define HAVE_FPM_CPUAFFINITY 1
98+
#else
99+
# define HAVE_FPM_CPUAFFINITY 0
100+
#endif

sapi/fpm/fpm/fpm_unix.c

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <unistd.h>
1010
#include <sys/types.h>
1111
#include <sys/un.h>
12+
#include <errno.h>
1213
#include <pwd.h>
1314
#include <grp.h>
1415

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

40+
#ifdef HAVE_SCHED_H
41+
#include <sched.h>
42+
#endif
43+
44+
#ifdef HAVE_SYS_CPUSET_H
45+
#include <sys/cpuset.h>
46+
typedef cpuset_t cpu_set_t;
47+
#endif
48+
3949
#include "fpm.h"
4050
#include "fpm_conf.h"
4151
#include "fpm_cleanup.h"
@@ -421,6 +431,101 @@ static int fpm_unix_conf_wp(struct fpm_worker_pool_s *wp) /* {{{ */
421431
}
422432
/* }}} */
423433

434+
#if HAVE_FPM_CPUAFFINITY
435+
struct fpm_cpuaffinity_conf {
436+
cpu_set_t cset;
437+
long min;
438+
long max;
439+
};
440+
441+
static long fpm_cpumax(void)
442+
{
443+
static long cpuid = LONG_MIN;
444+
if (cpuid == LONG_MIN) {
445+
cpu_set_t cset;
446+
#if defined(HAVE_SCHED_SETAFFINITY)
447+
if (sched_getaffinity(0, sizeof(cset), &cset) == 0) {
448+
#elif defined(HAVE_CPUSET_SETAFFINITY)
449+
if (cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, -1, sizeof(cset), &cset) == 0) {
450+
#endif
451+
cpuid = CPU_COUNT(&cset);
452+
} else {
453+
cpuid = -1;
454+
}
455+
}
456+
457+
return cpuid;
458+
}
459+
460+
static void fpm_cpuaffinity_init(struct fpm_cpuaffinity_conf *c)
461+
{
462+
CPU_ZERO(&c->cset);
463+
}
464+
465+
static void fpm_cpuaffinity_add(struct fpm_cpuaffinity_conf *c)
466+
{
467+
#if defined(HAVE_FPM_CPUAFFINITY)
468+
int i;
469+
470+
for (i = c->min; i <= c->max; i ++) {
471+
if (!CPU_ISSET(i, &c->cset)) {
472+
CPU_SET(i, &c->cset);
473+
}
474+
}
475+
#endif
476+
}
477+
478+
static int fpm_cpuaffinity_set(struct fpm_cpuaffinity_conf *c)
479+
{
480+
#if defined(HAVE_SCHED_SETAFFINITY)
481+
return sched_setaffinity(0, sizeof(c->cset), &c->cset);
482+
#elif defined(HAVE_CPUSET_SETAFFINITY)
483+
return cpuset_setaffinity(CPU_LEVEL_WHICH, CPU_WHICH_PID, -1, sizeof(c->cset), &c->cset);
484+
#endif
485+
}
486+
487+
static int fpm_setcpuaffinity(char *cpu_list)
488+
{
489+
char *token, *buf;
490+
struct fpm_cpuaffinity_conf fconf;
491+
int r, cpumax;
492+
493+
r = -1;
494+
cpumax = fpm_cpumax();
495+
496+
fpm_cpuaffinity_init(&fconf);
497+
token = php_strtok_r(cpu_list, ",", &buf);
498+
499+
while (token) {
500+
char *cpu_listsep;
501+
502+
fconf.min = strtol(token, &cpu_listsep, 0);
503+
if (errno || fconf.min < 0 || fconf.min > cpumax) {
504+
return -1;
505+
}
506+
fconf.max = fconf.min;
507+
if (*cpu_listsep == '-') {
508+
if (strlen(cpu_listsep) > 1) {
509+
char *err;
510+
fconf.max = strtol(cpu_listsep + 1, &err, 0);
511+
if (errno || *err != '\0' || fconf.max < fconf.min || fconf.max > cpumax) {
512+
return -1;
513+
}
514+
} else {
515+
return -1;
516+
}
517+
}
518+
519+
fpm_cpuaffinity_add(&fconf);
520+
521+
token = php_strtok_r(NULL, ";", &buf);
522+
}
523+
524+
r = fpm_cpuaffinity_set(&fconf);
525+
return r;
526+
}
527+
#endif
528+
424529
int fpm_unix_init_child(struct fpm_worker_pool_s *wp) /* {{{ */
425530
{
426531
int is_root = !geteuid();
@@ -445,6 +550,15 @@ int fpm_unix_init_child(struct fpm_worker_pool_s *wp) /* {{{ */
445550
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);
446551
}
447552
}
553+
#if HAVE_FPM_CPUAFFINITY
554+
if (wp->config->process_cpu_list) {
555+
556+
if (0 > fpm_setcpuaffinity(wp->config->process_cpu_list)) {
557+
zlog(ZLOG_SYSERROR, "[pool %s] failed to fpm_setcpuaffinity(%s)", wp->config->name, wp->config->process_cpu_list);
558+
return -1;
559+
}
560+
}
561+
#endif
448562

449563
if (is_root && wp->config->chroot && *wp->config->chroot) {
450564
if (0 > chroot(wp->config->chroot)) {
@@ -692,6 +806,15 @@ int fpm_unix_init_main(void)
692806
}
693807
}
694808

809+
#if HAVE_FPM_CPUAFFINITY
810+
if (fpm_global_config.process_cpu_list) {
811+
if (0 > fpm_setcpuaffinity(fpm_global_config.process_cpu_list)) {
812+
zlog(ZLOG_SYSERROR, "failed to fpm_setcpuaffinity(%s)", fpm_global_config.process_cpu_list);
813+
return -1;
814+
}
815+
}
816+
#endif
817+
695818
fpm_globals.parent_pid = getpid();
696819
for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
697820
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
@@ -124,6 +124,18 @@
124124
; Default value: 10
125125
;systemd_interval = 10
126126

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

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)