Line data Source code
1 : /* The cpuset stage creates a cgroup v2 "isolated" cpuset partition
2 : over the Firedancer tile CPUs, at /sys/fs/cgroup/<name> where <name>
3 : is the instance [name] from the configuration (default "fd1"),
4 : allowing multiple Firedancer instances on one host to own distinct
5 : partitions.
6 :
7 : An isolated partition removes its CPUs from the scheduler domains of
8 : the rest of the system: no load balancing onto them, and processes
9 : outside the cgroup cannot be scheduled (or set affinity) onto them
10 : at all. This is the runtime-configurable equivalent of the
11 : `isolcpus=` boot parameter, and the strongest tool available for
12 : keeping foreign tasks off tile CPUs.
13 :
14 : IMPORTANT INTERACTION: once the partition exists, ONLY processes
15 : inside the cgroup may run on tile CPUs. The tile launcher
16 : (execve_tile in run.c) joins the cgroup before setting a fixed
17 : tile's affinity. Setting affinity to a partitioned CPU from
18 : outside the cgroup fails with EINVAL; run `configure fini cpuset`
19 : to remove the partition if that happens with tooling that predates
20 : this stage.
21 :
22 : Notes:
23 : - Requires cgroup v2 (unified hierarchy) with the cpuset controller,
24 : and kernel support for the "isolated" partition type (5.15+ for
25 : root-adjacent partitions; probed at init by the partition write).
26 : - The cgroup directory is created directly under the cgroup root.
27 : systemd tolerates foreign cgroups there, but external cleanup may
28 : remove it; the check stage detects this and reports unconfigured.
29 : - Sibling exclusivity: the partition's CPUs must not appear in any
30 : sibling cgroup's explicit cpuset.cpus. Typical systemd slices
31 : leave cpuset.cpus empty (meaning "parent's effective"), which
32 : does not conflict. If an operator has explicitly assigned tile
33 : CPUs to another cgroup, the partition write fails and init
34 : reports it. */
35 :
36 : #include "configure.h"
37 : #include "fd_cpu_isolation.h"
38 :
39 : #include <errno.h>
40 : #include <fcntl.h>
41 : #include <unistd.h>
42 : #include <sys/stat.h>
43 :
44 0 : #define NAME "cpuset"
45 :
46 : #define CGROUP_ROOT "/sys/fs/cgroup"
47 : #define SUBTREE_CONTROL CGROUP_ROOT "/cgroup.subtree_control"
48 :
49 : /* cgroup_path formats the per-instance cgroup path or one of its files
50 : into buf. file may be NULL for the cgroup directory itself. */
51 :
52 : static char const *
53 : cgroup_path( char buf[ static PATH_MAX ],
54 : config_t const * config,
55 0 : char const * file ) {
56 0 : FD_TEST( fd_cstr_printf_check( buf, PATH_MAX, NULL, CGROUP_ROOT "/%s%s%s",
57 0 : config->name, file ? "/" : "", file ? file : "" ) );
58 0 : return buf;
59 0 : }
60 :
61 : /* write_cstr writes val to the file at path. Returns 1 on success,
62 : 0 on open/write failure with errno set; callers decide severity
63 : (typically FD_LOG_ERR with call-site context). */
64 :
65 : static int
66 : write_cstr( char const * path,
67 0 : char const * val ) {
68 0 : int fd = open( path, O_WRONLY );
69 0 : if( FD_UNLIKELY( fd<0 ) ) return 0;
70 0 : ulong val_len = strlen( val );
71 0 : int err = 0;
72 0 : if( FD_UNLIKELY( write( fd, val, val_len )!=(long)val_len ) ) err = errno;
73 0 : if( FD_UNLIKELY( close( fd ) ) ) FD_LOG_ERR(( "close(%s) failed (%i-%s)", path, errno, fd_io_strerror( errno ) ));
74 0 : if( FD_UNLIKELY( err ) ) {
75 0 : errno = err; /* preserve write() errno across close() */
76 0 : return 0;
77 0 : }
78 0 : return 1;
79 0 : }
80 :
81 : /* read_cstr reads the first line of path into buf. Returns 1 on
82 : success, 0 if the file does not exist (ENOENT). Any other error is
83 : fatal: these are kernel-provided files, and an unexpected failure
84 : reading them means something is wrong that we should not paper
85 : over. */
86 :
87 : static int
88 : read_cstr( char const * path,
89 : char * buf,
90 0 : ulong buf_sz ) {
91 0 : int fd = open( path, O_RDONLY );
92 0 : if( FD_UNLIKELY( fd<0 ) ) {
93 0 : if( FD_LIKELY( errno==ENOENT ) ) return 0;
94 0 : FD_LOG_ERR(( "open(%s) failed (%i-%s)", path, errno, fd_io_strerror( errno ) ));
95 0 : }
96 0 : long n = read( fd, buf, buf_sz-1UL );
97 0 : if( FD_UNLIKELY( n<0L ) ) FD_LOG_ERR(( "read(%s) failed (%i-%s)", path, errno, fd_io_strerror( errno ) ));
98 0 : if( FD_UNLIKELY( close( fd ) ) ) FD_LOG_ERR(( "close(%s) failed (%i-%s)", path, errno, fd_io_strerror( errno ) ));
99 0 : buf[ n ] = '\0';
100 0 : char * nl = strchr( buf, '\n' );
101 0 : if( FD_LIKELY( nl ) ) *nl = '\0';
102 0 : return 1;
103 0 : }
104 :
105 : static int
106 0 : enabled( config_t const * config ) {
107 0 : (void)config;
108 : /* cgroup v2 unified hierarchy with the cpuset controller. On v1 or
109 : hybrid systems the unified root has no cgroup.controllers file (or
110 : no cpuset in it) and this stage cannot work. Only genuine absence
111 : disables the stage; any other failure reading the file is fatal
112 : (in read_cstr). */
113 0 : char controllers[ 256 ];
114 0 : if( FD_UNLIKELY( !read_cstr( CGROUP_ROOT "/cgroup.controllers", controllers, sizeof(controllers) ) ) ) return 0;
115 0 : return NULL!=strstr( controllers, "cpuset" );
116 0 : }
117 :
118 : static void
119 : init_perm( fd_cap_chk_t * chk,
120 0 : config_t const * config ) {
121 0 : char path[ PATH_MAX ];
122 0 : static char reason[ PATH_MAX+64UL ];
123 0 : FD_TEST( fd_cstr_printf_check( reason, sizeof(reason), NULL, "create and configure the cgroup `%s`",
124 0 : cgroup_path( path, config, NULL ) ) );
125 0 : fd_cap_chk_root( chk, NAME, reason );
126 0 : }
127 :
128 : static void
129 0 : init( config_t const * config ) {
130 0 : FD_CPUSET_DECL( part_cpus );
131 0 : fd_cpu_isolation_partition_cpus( part_cpus, &config->topo );
132 :
133 0 : char cgroup[ PATH_MAX ]; cgroup_path( cgroup, config, NULL );
134 :
135 0 : if( FD_UNLIKELY( !fd_cpuset_cnt( part_cpus ) ) ) {
136 0 : FD_LOG_WARNING(( "no fixed tile CPUs in topology; not creating `%s`", cgroup ));
137 0 : return;
138 0 : }
139 :
140 0 : FD_CPUSET_DECL( host );
141 0 : fd_cpu_isolation_host_cpus( host );
142 0 : FD_CPUSET_DECL( housekeeping );
143 0 : fd_cpuset_subtract( housekeeping, host, part_cpus );
144 0 : if( FD_UNLIKELY( !fd_cpuset_cnt( housekeeping ) ) )
145 0 : FD_LOG_ERR(( "all host CPUs are assigned to Firedancer tiles; cannot isolate them all (the kernel needs at "
146 0 : "least one housekeeping CPU)" ));
147 :
148 0 : char list[ FD_CPU_ISOLATION_LIST_MAX ];
149 0 : fd_cpu_isolation_format_list( list, sizeof(list), part_cpus );
150 :
151 : /* The parent must delegate the cpuset controller before a child can
152 : use it. "+cpuset" is idempotent. */
153 0 : FD_LOG_NOTICE(( "%sRUN: `echo \"+cpuset\" > " SUBTREE_CONTROL "`%s", fd_log_style_dim(), fd_log_style_normal() ));
154 0 : if( FD_UNLIKELY( !write_cstr( SUBTREE_CONTROL, "+cpuset" ) ) )
155 0 : FD_LOG_ERR(( "could not enable the cpuset controller in `" SUBTREE_CONTROL "` (%i-%s)", errno, fd_io_strerror( errno ) ));
156 :
157 0 : FD_LOG_NOTICE(( "%sRUN: `mkdir %s`%s", fd_log_style_dim(), cgroup , fd_log_style_normal() ));
158 0 : if( FD_UNLIKELY( mkdir( cgroup, 0755 ) && errno!=EEXIST ) )
159 0 : FD_LOG_ERR(( "mkdir(%s) failed (%i-%s)", cgroup, errno, fd_io_strerror( errno ) ));
160 :
161 0 : char path[ PATH_MAX ];
162 0 : cgroup_path( path, config, "cpuset.cpus" );
163 0 : FD_LOG_NOTICE(( "%sRUN: `echo \"%s\" > %s`%s", fd_log_style_dim(), list, path , fd_log_style_normal() ));
164 0 : if( FD_UNLIKELY( !write_cstr( path, list ) ) )
165 0 : FD_LOG_ERR(( "write(%s,\"%s\") failed (%i-%s)", path, list, errno, fd_io_strerror( errno ) ));
166 :
167 0 : cgroup_path( path, config, "cpuset.cpus.partition" );
168 0 : FD_LOG_NOTICE(( "%sRUN: `echo \"isolated\" > %s`%s", fd_log_style_dim(), path , fd_log_style_normal() ));
169 0 : if( FD_UNLIKELY( !write_cstr( path, "isolated" ) ) )
170 0 : FD_LOG_ERR(( "write(%s,\"isolated\") failed (%i-%s). The kernel may be too old for isolated cpuset "
171 0 : "partitions, or a sibling cgroup may have explicitly claimed one of the CPUs `%s`",
172 0 : path, errno, fd_io_strerror( errno ), list ));
173 0 : }
174 :
175 : static int
176 : fini( config_t const * config,
177 0 : int pre_init ) {
178 0 : (void)pre_init;
179 :
180 0 : char cgroup[ PATH_MAX ]; cgroup_path( cgroup, config, NULL );
181 0 : if( FD_UNLIKELY( access( cgroup, F_OK ) ) ) return 0;
182 :
183 : /* Downgrade to a regular member cgroup first so the CPUs return to
184 : the system scheduler domains even if rmdir fails (e.g. because
185 : Firedancer is still running in it). ENOENT is tolerated here and
186 : below: the access() check above races with concurrent removal (a
187 : second `configure fini`, or systemd cleaning up foreign cgroups),
188 : and someone else deleting the cgroup is this stage's goal state,
189 : not an error. */
190 0 : char path[ PATH_MAX ];
191 0 : cgroup_path( path, config, "cpuset.cpus.partition" );
192 0 : if( FD_UNLIKELY( !write_cstr( path, "member" ) && errno!=ENOENT ) )
193 0 : FD_LOG_ERR(( "write(%s,\"member\") failed (%i-%s)", path, errno, fd_io_strerror( errno ) ));
194 :
195 0 : FD_LOG_NOTICE(( "%sRUN: `rmdir %s`%s", fd_log_style_dim(), cgroup , fd_log_style_normal() ));
196 0 : if( FD_UNLIKELY( rmdir( cgroup ) && errno!=ENOENT ) ) {
197 0 : if( FD_LIKELY( errno==EBUSY ) ) {
198 0 : FD_LOG_ERR(( "Removal of the CPU isolation cgroup `%s` failed because processes are still inside it, "
199 0 : "likely because Firedancer is still running. The partition was downgraded so its CPUs are "
200 0 : "returned to the system, but the cgroup itself could not be removed. Stop the validator and "
201 0 : "run `%s configure fini cpuset` again, or remove it manually with `rmdir %s`",
202 0 : cgroup, FD_BINARY_NAME, cgroup ));
203 0 : } else {
204 0 : FD_LOG_ERR(( "rmdir(%s) failed (%i-%s)", cgroup, errno, fd_io_strerror( errno ) ));
205 0 : }
206 0 : }
207 0 : return 1;
208 0 : }
209 :
210 : static configure_result_t
211 : check( config_t const * config,
212 0 : int check_type ) {
213 0 : (void)check_type;
214 :
215 0 : FD_CPUSET_DECL( part_cpus );
216 0 : fd_cpu_isolation_partition_cpus( part_cpus, &config->topo );
217 0 : if( FD_UNLIKELY( !fd_cpuset_cnt( part_cpus ) ) ) CONFIGURE_OK();
218 :
219 0 : char cgroup[ PATH_MAX ]; cgroup_path( cgroup, config, NULL );
220 0 : if( FD_UNLIKELY( access( cgroup, F_OK ) ) )
221 0 : NOT_CONFIGURED( "`%s` does not exist", cgroup );
222 :
223 0 : char path[ PATH_MAX ];
224 0 : cgroup_path( path, config, "cpuset.cpus" );
225 0 : char cpus[ FD_CPU_ISOLATION_LIST_MAX ];
226 0 : if( FD_UNLIKELY( !read_cstr( path, cpus, sizeof(cpus) ) ) )
227 0 : NOT_CONFIGURED( "`%s` does not exist", path ); /* cgroup removed concurrently */
228 :
229 0 : FD_CPUSET_DECL( current );
230 0 : if( FD_UNLIKELY( !fd_cpu_isolation_parse_list( current, cpus ) ) )
231 0 : FD_LOG_ERR(( "failed to parse `%s` (\"%s\")", path, cpus ));
232 :
233 0 : if( FD_UNLIKELY( !fd_cpuset_eq( current, part_cpus ) ) ) {
234 0 : char expected[ FD_CPU_ISOLATION_LIST_MAX ];
235 0 : fd_cpu_isolation_format_list( expected, sizeof(expected), part_cpus );
236 0 : PARTIALLY_CONFIGURED( "`%s` is \"%s\", expected \"%s\"", path, cpus, expected );
237 0 : }
238 :
239 0 : cgroup_path( path, config, "cpuset.cpus.partition" );
240 0 : char partition[ 64 ];
241 0 : if( FD_UNLIKELY( !read_cstr( path, partition, sizeof(partition) ) ) )
242 0 : NOT_CONFIGURED( "`%s` does not exist", path ); /* cgroup removed concurrently */
243 :
244 : /* An invalid partition reads as e.g. "isolated invalid (...)". */
245 0 : if( FD_UNLIKELY( strcmp( partition, "isolated" ) ) )
246 0 : PARTIALLY_CONFIGURED( "`%s` is \"%s\", expected \"isolated\"", path, partition );
247 :
248 0 : CONFIGURE_OK();
249 0 : }
250 :
251 : configure_stage_t fd_cfg_stage_cpuset = {
252 : .name = NAME,
253 : .always_recreate = 0,
254 : .enabled = enabled,
255 : .init_perm = init_perm,
256 : .fini_perm = init_perm,
257 : .init = init,
258 : .fini = fini,
259 : .check = check,
260 : };
261 :
262 : #undef NAME
263 : #undef CGROUP_ROOT
264 : #undef SUBTREE_CONTROL
|