├── doc
├── erlang.png
├── edoc-info
├── modules-frame.html
├── index.html
├── stylesheet.css
├── overview-summary.html
└── cgroups.html
├── rebar.config
├── src
├── cgroups.app.src
└── cgroups.erl
├── LICENSE
├── mix.exs
└── README.markdown
/doc/erlang.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/okeuday/cgroups/HEAD/doc/erlang.png
--------------------------------------------------------------------------------
/doc/edoc-info:
--------------------------------------------------------------------------------
1 | %% encoding: UTF-8
2 | {application,cgroups}.
3 | {modules,[cgroups]}.
4 |
--------------------------------------------------------------------------------
/rebar.config:
--------------------------------------------------------------------------------
1 | %-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*-
2 | % ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod:
3 |
4 | {erl_opts,
5 | [{platform_define, "^19\.", 'ERLANG_OTP_VERSION_19'},
6 | {platform_define, "^20\.", 'ERLANG_OTP_VERSION_20'},
7 | warn_export_vars,
8 | warn_unused_import,
9 | warn_missing_spec,
10 | warnings_as_errors]}.
11 | {cover_enabled, true}.
12 | {cover_print_enabled, true}.
13 |
--------------------------------------------------------------------------------
/doc/modules-frame.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a
6 | copy of this software and associated documentation files (the "Software"),
7 | to deal in the Software without restriction, including without limitation
8 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
9 | and/or sell copies of the Software, and to permit persons to whom the
10 | Software is furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 | DEALINGS IN THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | #-*-Mode:elixir;coding:utf-8;tab-width:2;c-basic-offset:2;indent-tabs-mode:()-*-
2 | # ex: set ft=elixir fenc=utf-8 sts=2 ts=2 sw=2 et nomod:
3 |
4 | defmodule CGroups.Mixfile do
5 | use Mix.Project
6 |
7 | def project do
8 | [app: :cgroups,
9 | version: "2.0.7",
10 | language: :erlang,
11 | erlc_options: [
12 | {:d, :erlang.list_to_atom('ERLANG_OTP_VERSION_' ++ :erlang.system_info(:otp_release))},
13 | :deterministic,
14 | :debug_info,
15 | :warn_export_vars,
16 | :warn_unused_import,
17 | :warn_missing_spec,
18 | :warnings_as_errors],
19 | description: description(),
20 | package: package(),
21 | deps: deps()]
22 | end
23 |
24 | def application do
25 | [env: [
26 | version_default: 2,
27 | version_default_required: false,
28 | path_v1: '/sys/fs/cgroup/',
29 | path_v2: '/sys/fs/cgroup2/',
30 | path_mounts: '/proc/mounts']]
31 | end
32 |
33 | defp deps do
34 | []
35 | end
36 |
37 | defp description do
38 | "Erlang native cgroups interface"
39 | end
40 |
41 | defp package do
42 | [files: ~w(src doc rebar.config README.markdown LICENSE),
43 | maintainers: ["Michael Truog"],
44 | licenses: ["MIT"],
45 | links: %{"GitHub" => "https://github.com/okeuday/cgroups"}]
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/README.markdown:
--------------------------------------------------------------------------------
1 | Erlang cgroups interface
2 | ========================
3 |
4 | An interface for cgroups manipulation that handles cgroup version details
5 | (i.e., differences between [v1](https://www.kernel.org/doc/Documentation/cgroup-v1/cgroups.txt) and [v2](https://github.com/torvalds/linux/blob/master/Documentation/admin-guide/cgroup-v2.rst))
6 | and provides safe usage of the cgroups filesystem mount.
7 |
8 | Build
9 | -----
10 |
11 | rebar compile
12 |
13 | Example
14 | -------
15 |
16 | Update or create the cgroup "group1/nested1" with the OS pid 19368,
17 | then delete the cgroup path after moving the OS pid back to the root cgroup
18 | (executed as root).
19 |
20 | # erl -pz ebin
21 | 1> application:start(cgroups).
22 | 2> OSPid0 = 19368.
23 | 3> CGroupPath = "group1/nested1".
24 | 4> {ok, CGroups} = cgroups:new().
25 | 5> MemoryLimit = case cgroups:version(CGroups) of 1 -> "memory.limit_in_bytes"; 2 -> "memory.high" end.
26 | 6> cgroups:update_or_create(CGroupPath,
27 | [OSPid0],
28 | [{MemoryLimit, "10000000"}],
29 | CGroups).
30 | 7> cgroups:update("", [OSPid0], [], CGroups).
31 | 8> cgroups:delete_recursive(CGroupPath, CGroups).
32 | 9> cgroups:destroy(CGroups).
33 |
34 |
35 | Troubleshooting
36 | ---------------
37 |
38 | **`cgroups:update/4` and `cgroups:update_or_create/4` Errors:**
39 | * Either root execution of beam.smp or
40 | `setcap cap_sys_admin=+ep /path/to/beam.smp` before execution is required
41 | for most usage of cgroups
42 | * A Linux/systemd setup may have the control group setup mode set to `hybrid`
43 | due to it being the [recommended default](https://github.com/systemd/systemd/blob/v239/NEWS#L1365)
44 | for distributions. However, that mode blocks the use of cgroup controllers
45 | (the file `/sys/fs/cgroup/unified/cgroup.controllers` is empty)
46 | because the cgroup controllers can only be [mounted in one hierarchy](https://www.kernel.org/doc/html/latest/admin-guide/cgroup-v2.html#mounting)
47 | (v1 or v2). If cgroup v1 should be used, the Linux kernel argument
48 | `systemd.legacy_systemd_cgroup_controller=1` can be used.
49 | If cgroup v2 should be used, the Linux kernel argument
50 | `systemd.unified_cgroup_hierarchy=1` can be used
51 | (with systemd >= `v226` and kernel >= `4.2`) or
52 | `cgroup_no_v1=all` can be used (with kernel >= `4.6`).
53 | * Any non-root use should only use `cgroups:update/4` with more limited
54 | controllers (memory, pids, possibly cpu and/or io) and all OS pids added
55 | elsewhere as root
56 |
57 | Author
58 | ------
59 |
60 | Michael Truog (mjtruog at protonmail dot com)
61 |
62 | License
63 | -------
64 |
65 | MIT License
66 |
67 |
--------------------------------------------------------------------------------
/doc/cgroups.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Module cgroups
6 |
7 |
8 |
9 |
10 |
11 |
12 | Module cgroups
13 |
14 | .
15 | Copyright © 2016-2023 Michael Truog
16 |
17 | Version: 2.0.6 Jun 20 2023 17:46:56
18 | ------------------------------------------------------------------------
19 | Authors: Michael Truog (mjtruog at protonmail dot com ).
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | new_error_reasons() = {version_default, Version::pos_integer()} | {path_mounts, Status::pos_integer(), Output::[binary()]} | {path_v1, Status::pos_integer(), Output::[binary()]} | {path_v2, Status::pos_integer(), Output::[binary()]}
28 |
29 |
30 |
31 | options() = [{version_default, pos_integer()} | {version_default_required, boolean()} | {path_v1, string()} | {path_v2, string()} | {path_mounts, string() | undefined}]
32 |
33 |
34 |
35 | create/4
36 |
37 | With cgroups v1, files cpuset.cpus and cpuset.mems are set
38 | if they are not initialized due to cgroup.clone_children
39 | (using the root values).
40 | delete/2
41 |
42 | The cgroup must not contain any OS processes for this
43 | function to succeed.
44 | delete_recursive/2
45 |
46 | The cgroup must not contain any OS processes for this
47 | function to succeed.
48 | destroy/1
49 | .
50 | new/0
51 | .
52 | new/1
53 | .
54 | shell/2
55 | .
56 | update/4
57 |
58 | May be used on the cgroup root path.
59 | update_or_create/4
60 | .
61 | version/1
62 | .
63 |
64 |
65 |
66 |
67 |
68 |
69 |
create(CGroupPath::nonempty_string(), OSPids::[pos_integer()], CGroupParameters::[{string(), string()}], State::#cgroups{version = pos_integer(), path = string(), mounted = boolean(), root = boolean()}) -> ok | {error, any()}
70 |
71 |
72 |
73 | With cgroups v1, files cpuset.cpus and cpuset.mems are set
74 | if they are not initialized due to cgroup.clone_children
75 | (using the root values).
76 |
77 |
78 |
79 |
delete(CGroupPath::nonempty_string(), State::#cgroups{version = pos_integer(), path = string(), mounted = boolean(), root = boolean()}) -> ok | {error, any()}
80 |
81 |
82 |
83 | The cgroup must not contain any OS processes for this
84 | function to succeed.
85 |
86 |
87 |
88 |
delete_recursive(CGroupPath::nonempty_string(), State::#cgroups{version = pos_integer(), path = string(), mounted = boolean(), root = boolean()}) -> ok | {error, any()}
89 |
90 |
91 |
92 | The cgroup must not contain any OS processes for this
93 | function to succeed.
94 |
95 |
96 |
97 |
destroy(Cgroups::#cgroups{version = pos_integer(), path = string(), mounted = boolean(), root = boolean()}) -> ok
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
new() -> {ok, #cgroups{version = pos_integer(), path = string(), mounted = boolean(), root = boolean()}} | {error, new_error_reasons() }
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
new(Options0::options() ) -> {ok, #cgroups{version = pos_integer(), path = string(), mounted = boolean(), root = boolean()}} | {error, new_error_reasons() }
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
shell(Command::string(), Arguments::list()) -> {non_neg_integer(), [binary()]}
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
update(CGroupPath::string(), OSPids::[pos_integer()], CGroupParameters::[{string(), string()}], State::#cgroups{version = pos_integer(), path = string(), mounted = boolean(), root = boolean()}) -> ok | {error, any()}
130 |
131 |
132 |
133 | May be used on the cgroup root path.
134 |
135 |
136 |
137 |
update_or_create(CGroupPath::nonempty_string(), OSPids::[pos_integer()], CGroupParameters::[{string(), string()}], State::#cgroups{version = pos_integer(), path = string(), mounted = boolean(), root = boolean()}) -> ok | {error, any()}
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
version(Cgroups::#cgroups{version = pos_integer(), path = string(), mounted = boolean(), root = boolean()}) -> pos_integer()
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 | Generated by EDoc
154 |
155 |
156 |
--------------------------------------------------------------------------------
/src/cgroups.erl:
--------------------------------------------------------------------------------
1 | %-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*-
2 | % ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et:
3 | %%%
4 | %%%------------------------------------------------------------------------
5 | %%% @doc
6 | %%% ==cgroups Manipulation Functions==
7 | %%% @end
8 | %%%
9 | %%% MIT License
10 | %%%
11 | %%% Copyright (c) 2016-2023 Michael Truog
12 | %%%
13 | %%% Permission is hereby granted, free of charge, to any person obtaining a
14 | %%% copy of this software and associated documentation files (the "Software"),
15 | %%% to deal in the Software without restriction, including without limitation
16 | %%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
17 | %%% and/or sell copies of the Software, and to permit persons to whom the
18 | %%% Software is furnished to do so, subject to the following conditions:
19 | %%%
20 | %%% The above copyright notice and this permission notice shall be included in
21 | %%% all copies or substantial portions of the Software.
22 | %%%
23 | %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24 | %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25 | %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26 | %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27 | %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28 | %%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
29 | %%% DEALINGS IN THE SOFTWARE.
30 | %%%
31 | %%% @author Michael Truog
32 | %%% @copyright 2016-2023 Michael Truog
33 | %%% @version 2.0.6 {@date} {@time}
34 | %%%------------------------------------------------------------------------
35 |
36 | -module(cgroups).
37 | -author('mjtruog at protonmail dot com').
38 |
39 | %% external interface
40 | -export([create/4,
41 | delete/2,
42 | delete_recursive/2,
43 | destroy/1,
44 | new/0,
45 | new/1,
46 | shell/2,
47 | update/4,
48 | update_or_create/4,
49 | version/1]).
50 |
51 | -record(cgroups,
52 | {
53 | version :: pos_integer(),
54 | path :: string(),
55 | mounted :: boolean(),
56 | root :: boolean()
57 | }).
58 | -define(APPLICATION, cgroups).
59 |
60 | -type options() :: list({version_default, pos_integer()} |
61 | {version_default_required, boolean()} |
62 | {path_v1, string()} |
63 | {path_v2, string()} |
64 | {path_mounts, string() | undefined}).
65 | -export_type([options/0]).
66 |
67 | % for features specific to Erlang/OTP version 20.x (and later versions)
68 | -ifdef(ERLANG_OTP_VERSION_19).
69 | -else.
70 | -define(ERLANG_OTP_VERSION_20_FEATURES, true).
71 | -endif.
72 |
73 | %%%------------------------------------------------------------------------
74 | %%% External interface functions
75 | %%%------------------------------------------------------------------------
76 |
77 | %%-------------------------------------------------------------------------
78 | %% @doc
79 | %% ===Create a specific cgroup.===
80 | %% With cgroups v1, files cpuset.cpus and cpuset.mems are set
81 | %% if they are not initialized due to cgroup.clone_children
82 | %% (using the root values).
83 | %% @end
84 | %%-------------------------------------------------------------------------
85 |
86 | -spec create(CGroupPath :: nonempty_string(),
87 | OSPids :: list(pos_integer()),
88 | CGroupParameters :: list({string(), string()}),
89 | State :: #cgroups{}) ->
90 | ok |
91 | {error, any()}.
92 |
93 | create([_ | _] = CGroupPath, OSPids, CGroupParameters,
94 | #cgroups{path = Path,
95 | root = true} = State) ->
96 | CGroupPathValid = cgroup_path_valid(CGroupPath),
97 | if
98 | CGroupPathValid =:= false ->
99 | {error, {invalid_cgroup_path, CGroupPath}};
100 | true ->
101 | CGroupPathFull = Path ++ CGroupPath,
102 | case filelib:is_dir(CGroupPathFull) of
103 | true ->
104 | {error, {exists, CGroupPathFull}};
105 | false ->
106 | create_cgroup(CGroupPath, OSPids, CGroupParameters, State)
107 | end
108 | end;
109 | create(_, _, _,
110 | #cgroups{root = false}) ->
111 | {error, root_required}.
112 |
113 | %%-------------------------------------------------------------------------
114 | %% @doc
115 | %% ===Delete a specific cgroup.===
116 | %% The cgroup must not contain any OS processes for this
117 | %% function to succeed.
118 | %% @end
119 | %%-------------------------------------------------------------------------
120 |
121 | -spec delete(CGroupPath :: nonempty_string(),
122 | State :: #cgroups{}) ->
123 | ok |
124 | {error, any()}.
125 |
126 | delete([_ | _] = CGroupPath,
127 | #cgroups{path = Path,
128 | root = true}) ->
129 | CGroupPathValid = cgroup_path_valid(CGroupPath),
130 | if
131 | CGroupPathValid =:= false ->
132 | {error, {invalid_cgroup_path, CGroupPath}};
133 | true ->
134 | CGroupPathFull = Path ++ CGroupPath,
135 | case shell("rmdir \"~s\"", [CGroupPathFull]) of
136 | {0, _} ->
137 | ok;
138 | {Status, Output} ->
139 | {error, {rmdir, Status, Output}}
140 | end
141 | end;
142 | delete(_,
143 | #cgroups{root = false}) ->
144 | {error, root_required}.
145 |
146 | %%-------------------------------------------------------------------------
147 | %% @doc
148 | %% ===Delete a specific cgroup and as many non-leaf cgroups as possible.===
149 | %% The cgroup must not contain any OS processes for this
150 | %% function to succeed.
151 | %% @end
152 | %%-------------------------------------------------------------------------
153 |
154 | -spec delete_recursive(CGroupPath :: nonempty_string(),
155 | State :: #cgroups{}) ->
156 | ok |
157 | {error, any()}.
158 |
159 | delete_recursive([_ | _] = CGroupPath,
160 | #cgroups{path = Path,
161 | root = true}) ->
162 | CGroupPathValid = cgroup_path_valid(CGroupPath),
163 | if
164 | CGroupPathValid =:= false ->
165 | {error, {invalid_cgroup_path, CGroupPath}};
166 | true ->
167 | CGroupPathFull = Path ++ CGroupPath,
168 | case shell("rmdir \"~s\"", [CGroupPathFull]) of
169 | {0, _} ->
170 | _ = delete_recursive_subpath(subdirectory(CGroupPathFull),
171 | Path),
172 | ok;
173 | {Status, Output} ->
174 | {error, {rmdir, Status, Output}}
175 | end
176 | end;
177 | delete_recursive(_,
178 | #cgroups{root = false}) ->
179 | {error, root_required}.
180 |
181 | %%-------------------------------------------------------------------------
182 | %% @doc
183 | %% ===Destroy cgroups state data.===
184 | %% @end
185 | %%-------------------------------------------------------------------------
186 |
187 | -spec destroy(#cgroups{}) ->
188 | ok.
189 |
190 | destroy(#cgroups{mounted = false}) ->
191 | ok;
192 | destroy(#cgroups{path = Path,
193 | mounted = true}) ->
194 | _ = shell("umount \"~s\"", [Path]),
195 | ok.
196 |
197 | %%-------------------------------------------------------------------------
198 | %% @doc
199 | %% ===Create new cgroups state data.===
200 | %% @end
201 | %%-------------------------------------------------------------------------
202 |
203 | -type new_error_reasons() ::
204 | {'version_default', Version :: pos_integer()} |
205 | {'path_mounts', Status :: pos_integer(), Output :: list(binary())} |
206 | {'path_v1', Status :: pos_integer(), Output :: list(binary())} |
207 | {'path_v2', Status :: pos_integer(), Output :: list(binary())}.
208 |
209 | -spec new() ->
210 | {ok, #cgroups{}} |
211 | {error, new_error_reasons()}.
212 |
213 | new() ->
214 | new([]).
215 |
216 | %%-------------------------------------------------------------------------
217 | %% @doc
218 | %% ===Create new cgroups state data with local options.===
219 | %% @end
220 | %%-------------------------------------------------------------------------
221 |
222 | -spec new(Options0 :: options()) ->
223 | {ok, #cgroups{}} |
224 | {error, new_error_reasons()}.
225 |
226 | new(Options0) ->
227 | {VersionDefault,
228 | Options1} = option(version_default, Options0),
229 | {VersionDefaultRequired,
230 | Options2} = option(version_default_required, Options1),
231 | {PathV1,
232 | Options3} = option(path_v1, Options2),
233 | {PathV2,
234 | Options4} = option(path_v2, Options3),
235 | {PathMounts,
236 | OptionsN} = option(path_mounts, Options4),
237 | [] = OptionsN,
238 | true = is_integer(VersionDefault) andalso (VersionDefault > 0),
239 | true = is_boolean(VersionDefaultRequired),
240 | true = is_list(PathV1) andalso
241 | ($/ == hd(lists:reverse(PathV1))) andalso (length(PathV1) > 1),
242 | true = is_list(PathV2) andalso
243 | ($/ == hd(lists:reverse(PathV2))) andalso (length(PathV2) > 1),
244 | true = (PathMounts =:= undefined) orelse
245 | (is_list(PathMounts) andalso is_integer(hd(PathMounts))),
246 | Root = os:getenv("USER") == "root",
247 | new_state(VersionDefault, VersionDefaultRequired,
248 | PathV1, PathV2, PathMounts, Root).
249 |
250 | %%-------------------------------------------------------------------------
251 | %% @doc
252 | %% ===Execute a command with the default shell.===
253 | %% @end
254 | %%-------------------------------------------------------------------------
255 |
256 | -spec shell(Command :: string(),
257 | Arguments :: list()) ->
258 | {non_neg_integer(), list(binary())}.
259 |
260 | shell(Command, Arguments) ->
261 | Shell = erlang:open_port({spawn_executable, "/bin/sh"},
262 | [{args, ["-"]}, {cd, "/"},
263 | stream, binary, stderr_to_stdout, exit_status]),
264 | Exec = io_lib:format(Command, Arguments),
265 | true = erlang:port_command(Shell, [Exec, "\nexit $?\n"]),
266 | shell_output(Shell, []).
267 |
268 | %%-------------------------------------------------------------------------
269 | %% @doc
270 | %% ===Update a cgroup path.===
271 | %% May be used on the cgroup root path.
272 | %% @end
273 | %%-------------------------------------------------------------------------
274 |
275 | -spec update(CGroupPath :: string(),
276 | OSPids :: list(pos_integer()),
277 | CGroupParameters :: list({string(), string()}),
278 | State :: #cgroups{}) ->
279 | ok |
280 | {error, any()}.
281 |
282 | update(CGroupPath, OSPids, CGroupParameters,
283 | #cgroups{version = Version,
284 | path = Path,
285 | root = Root}) ->
286 | CGroupPathValid = cgroup_path_valid(CGroupPath),
287 | OSPidsValid = lists:all(fun(OSPid) ->
288 | is_integer(OSPid) andalso (OSPid > 0)
289 | end, OSPids),
290 | CGroupParametersValid = lists:all(fun(CGroupParameter) ->
291 | case CGroupParameter of
292 | {[_ | _] = SubsystemParameter, Value} when is_list(Value) ->
293 | quoteless(SubsystemParameter) andalso quoteless(Value);
294 | _ ->
295 | false
296 | end
297 | end, CGroupParameters),
298 | if
299 | CGroupPathValid =:= false ->
300 | {error, {invalid_cgroup_path, CGroupPath}};
301 | OSPidsValid =:= false ->
302 | {error, {invalid_os_pids, OSPids}};
303 | CGroupParametersValid =:= false ->
304 | {error, {invalid_cgroup_parameters, CGroupParameters}};
305 | true ->
306 | CGroupPathFull = Path ++ CGroupPath,
307 | case update_parameters(Version, CGroupParameters,
308 | CGroupPathFull, Path, Root) of
309 | ok ->
310 | update_pids(OSPids, CGroupPathFull, Root);
311 | {error, _} = Error ->
312 | Error
313 | end
314 | end.
315 |
316 | %%-------------------------------------------------------------------------
317 | %% @doc
318 | %% ===Update or create a specific cgroup.===
319 | %% @end
320 | %%-------------------------------------------------------------------------
321 |
322 | -spec update_or_create(CGroupPath :: nonempty_string(),
323 | OSPids :: list(pos_integer()),
324 | CGroupParameters :: list({string(), string()}),
325 | State :: #cgroups{}) ->
326 | ok |
327 | {error, any()}.
328 |
329 | update_or_create([_ | _] = CGroupPath, OSPids, CGroupParameters,
330 | #cgroups{path = Path,
331 | root = true} = State) ->
332 | CGroupPathValid = cgroup_path_valid(CGroupPath),
333 | if
334 | CGroupPathValid =:= false ->
335 | {error, {invalid_cgroup_path, CGroupPath}};
336 | true ->
337 | CGroupPathFull = Path ++ CGroupPath,
338 | case filelib:is_dir(CGroupPathFull) of
339 | true ->
340 | update(CGroupPath, OSPids, CGroupParameters, State);
341 | false ->
342 | create_cgroup(CGroupPath, OSPids, CGroupParameters, State)
343 | end
344 | end;
345 | update_or_create(_, _, _,
346 | #cgroups{root = false}) ->
347 | {error, root_required}.
348 |
349 | %%-------------------------------------------------------------------------
350 | %% @doc
351 | %% ===The cgroup version used.===
352 | %% @end
353 | %%-------------------------------------------------------------------------
354 |
355 | -spec version(#cgroups{}) ->
356 | pos_integer().
357 |
358 | version(#cgroups{version = Version}) ->
359 | Version.
360 |
361 | %%%------------------------------------------------------------------------
362 | %%% Private functions
363 | %%%------------------------------------------------------------------------
364 |
365 | new_state(1 = Version, VersionDefaultRequired,
366 | PathV1, PathV2, PathMounts, Root) ->
367 | case new_paths(PathV1, PathV2, PathMounts) of
368 | {ok, {MountedV1, NewPathV1}, {MountedV2, NewPathV2}} ->
369 | case new_state_init(Version, MountedV1, NewPathV1, Root) of
370 | {ok, _} = Success ->
371 | Success;
372 | {error, _} = Error when VersionDefaultRequired =:= false ->
373 | case new_state_init(2, MountedV2, NewPathV2, Root) of
374 | {ok, _} = Success ->
375 | Success;
376 | {error, _} ->
377 | Error
378 | end;
379 | {error, _} = Error ->
380 | Error
381 | end;
382 | {error, _} = Error ->
383 | Error
384 | end;
385 | new_state(2 = Version, VersionDefaultRequired,
386 | PathV1, PathV2, PathMounts, Root) ->
387 | case new_paths(PathV1, PathV2, PathMounts) of
388 | {ok, {MountedV1, NewPathV1}, {MountedV2, NewPathV2}} ->
389 | case new_state_init(Version, MountedV2, NewPathV2, Root) of
390 | {ok, _} = Success ->
391 | Success;
392 | {error, _} = Error when VersionDefaultRequired =:= false ->
393 | case new_state_init(1, MountedV1, NewPathV1, Root) of
394 | {ok, _} = Success ->
395 | Success;
396 | {error, _} ->
397 | Error
398 | end;
399 | {error, _} = Error ->
400 | Error
401 | end;
402 | {error, _} = Error ->
403 | Error
404 | end;
405 | new_state(Version, _, _, _, _, _) ->
406 | {error, {version_default, Version}}.
407 |
408 | new_state_init(Version, true, Path, Root) ->
409 | {ok, #cgroups{version = Version,
410 | path = Path,
411 | mounted = false,
412 | root = Root}};
413 | new_state_init(1 = Version, false, Path, Root) ->
414 | case shell("mount -t cgroup none \"~s\"", [Path]) of
415 | {0, _} ->
416 | {ok, #cgroups{version = Version,
417 | path = Path,
418 | mounted = true,
419 | root = Root}};
420 | {Status, Output} ->
421 | {error, {path_v1, Status, Output}}
422 | end;
423 | new_state_init(2 = Version, false, Path, Root) ->
424 | case shell("mount -t cgroup2 none \"~s\"", [Path]) of
425 | {0, _} ->
426 | {ok, #cgroups{version = Version,
427 | path = Path,
428 | mounted = true,
429 | root = Root}};
430 | {Status, Output} ->
431 | {error, {path_v2, Status, Output}}
432 | end.
433 |
434 | new_paths(PathV1, PathV2, undefined) ->
435 | {ok, {false, PathV1}, {false, PathV2}};
436 | new_paths(PathV1, PathV2, PathMounts) ->
437 | case shell("cat \"~s\"", [PathMounts]) of
438 | {0, Mounts} ->
439 | MountsL = split(shell_output_string(Mounts), "\n"),
440 | {MountsPathV1,
441 | MountsPathV2} = new_mounts(MountsL),
442 | ResultV1 = if
443 | MountsPathV1 =:= undefined ->
444 | {false, PathV1};
445 | is_list(MountsPathV1) ->
446 | {true, MountsPathV1}
447 | end,
448 | ResultV2 = if
449 | MountsPathV2 =:= undefined ->
450 | {false, PathV2};
451 | is_list(MountsPathV2) ->
452 | {true, MountsPathV2}
453 | end,
454 | {ok, ResultV1, ResultV2};
455 | {Status, Output} ->
456 | {error, {path_mounts, Status, Output}}
457 | end.
458 |
459 | new_mounts(MountsL) ->
460 | new_mounts(MountsL, undefined, undefined).
461 |
462 | new_mounts([], PathV1, PathV2) ->
463 | {PathV1, PathV2};
464 | new_mounts([Mount | MountsL], PathV1, PathV2) ->
465 | case split(Mount, " ") of
466 | [_, NewPathV1, "cgroup" | _] ->
467 | new_mounts(MountsL, NewPathV1 ++ "/", PathV2);
468 | [_, NewPathV2, "cgroup2" | _] ->
469 | new_mounts(MountsL, PathV1, NewPathV2 ++ "/");
470 | _ ->
471 | new_mounts(MountsL, PathV1, PathV2)
472 | end.
473 |
474 | create_cgroup(CGroupPath, OSPids, CGroupParameters,
475 | #cgroups{version = Version,
476 | path = Path} = State) ->
477 | CGroupPathFull = Path ++ CGroupPath,
478 | case shell("mkdir -p \"~s\"", [CGroupPathFull]) of
479 | {0, _} ->
480 | case create_update(Version, CGroupPathFull, Path) of
481 | ok ->
482 | update(CGroupPath, OSPids, CGroupParameters, State);
483 | {error, _} = Error ->
484 | Error
485 | end;
486 | {Status, Output} ->
487 | {error, {mkdir, Status, Output}}
488 | end.
489 |
490 | create_update(1, CGroupPathFull, Path) ->
491 | case create_update_v1_get(CGroupPathFull, "cpuset.cpus", Path) of
492 | {ok, CPUS} ->
493 | case create_update_v1_get(CGroupPathFull, "cpuset.mems", Path) of
494 | {ok, MEMS} ->
495 | if
496 | CPUS =:= undefined,
497 | MEMS =:= undefined ->
498 | ok;
499 | true ->
500 | create_update_v1_set(CGroupPathFull,
501 | CPUS, MEMS, Path)
502 | end;
503 | {error, _} = Error ->
504 | Error
505 | end;
506 | {error, _} = Error ->
507 | Error
508 | end;
509 | create_update(2, _, _) ->
510 | ok.
511 |
512 | create_update_v1_set(CGroupPathFull, CPUS, MEMS, Path) ->
513 | case create_update_v1_set_subpath(CGroupPathFull, CPUS, MEMS, Path) of
514 | ok ->
515 | CGroupSubPathFull = CGroupPathFull ++ "/",
516 | case create_update_v1_set_value(CGroupSubPathFull,
517 | "cpuset.cpus", CPUS) of
518 | ok ->
519 | case create_update_v1_set_value(CGroupSubPathFull,
520 | "cpuset.mems", MEMS) of
521 | ok ->
522 | ok;
523 | {error, _} = Error ->
524 | Error
525 | end;
526 | {error, _} = Error ->
527 | Error
528 | end;
529 | {error, _} = Error ->
530 | Error
531 | end.
532 |
533 | create_update_v1_set_subpath(CGroupPathFull, CPUS, MEMS, Path) ->
534 | case subdirectory(CGroupPathFull) of
535 | Path ->
536 | ok;
537 | CGroupSubPathFull ->
538 | case create_update_v1_set_subpath(CGroupSubPathFull,
539 | CPUS, MEMS, Path) of
540 | ok ->
541 | case create_update_v1_set_value(CGroupSubPathFull,
542 | "cpuset.cpus", CPUS) of
543 | ok ->
544 | case create_update_v1_set_value(CGroupSubPathFull,
545 | "cpuset.mems",
546 | MEMS) of
547 | ok ->
548 | ok;
549 | {error, _} = Error ->
550 | Error
551 | end;
552 | {error, _} = Error ->
553 | Error
554 | end;
555 | {error, _} = Error ->
556 | Error
557 | end
558 | end.
559 |
560 | create_update_v1_set_value(_, _, undefined) ->
561 | ok;
562 | create_update_v1_set_value(CGroupSubPathFull, SubsystemParameter, Value) ->
563 | case shell("cat \"~s~s\"", [CGroupSubPathFull, SubsystemParameter]) of
564 | {0, OldValue} ->
565 | NeedsUpdate = strip(shell_output_string(OldValue)) == "",
566 | if
567 | NeedsUpdate =:= true ->
568 | case shell("echo \"~s\" > \"~s~s\"",
569 | [Value, CGroupSubPathFull,
570 | SubsystemParameter]) of
571 | {0, _} ->
572 | ok;
573 | {Status, Output} ->
574 | ErrorReason = subsystem_parameter_error_reason(
575 | SubsystemParameter, "_set"),
576 | {error, {ErrorReason, Status, Output}}
577 | end;
578 | NeedsUpdate =:= false ->
579 | ok
580 | end;
581 | {Status, Output} ->
582 | ErrorReason = subsystem_parameter_error_reason(SubsystemParameter,
583 | "_check"),
584 | {error, {ErrorReason, Status, Output}}
585 | end.
586 |
587 | create_update_v1_get(CGroupPathFull, SubsystemParameter, Path) ->
588 | case shell("cat \"~s/~s\"", [CGroupPathFull, SubsystemParameter]) of
589 | {0, Contents} ->
590 | NeedsUpdate = strip(shell_output_string(Contents)) == "",
591 | if
592 | NeedsUpdate =:= true ->
593 | subsystem_parameter_subpath(subdirectory(CGroupPathFull),
594 | SubsystemParameter, Path);
595 | NeedsUpdate =:= false ->
596 | {ok, undefined}
597 | end;
598 | {Status, Output} ->
599 | ErrorReason = subsystem_parameter_error_reason(SubsystemParameter,
600 | "_get"),
601 | {error, {ErrorReason, Status, Output}}
602 | end.
603 |
604 | delete_recursive_subpath(Path, Path) ->
605 | ok;
606 | delete_recursive_subpath(CGroupSubPathFull, Path) ->
607 | case shell("rmdir \"~s\"", [CGroupSubPathFull]) of
608 | {0, _} ->
609 | delete_recursive_subpath(subdirectory(CGroupSubPathFull), Path);
610 | {_, _} ->
611 | ok
612 | end.
613 |
614 | update_parameters(1, _, _, _, false) ->
615 | {error, root_required};
616 | update_parameters(1, CGroupParameters, CGroupPathFull, _, true) ->
617 | update_parameters(CGroupParameters, CGroupPathFull);
618 | update_parameters(2, CGroupParameters, CGroupPathFull, Path, Root) ->
619 | Controllers = lists:usort([subsystem(SubsystemParameter)
620 | || {SubsystemParameter, _} <- CGroupParameters]),
621 | ControlAdded = ["+" ++ Controller || Controller <- Controllers],
622 | case subtree_control_add(subdirectory(CGroupPathFull, Path),
623 | lists:join($ , ControlAdded),
624 | Path, Root) of
625 | ok ->
626 | update_parameters(CGroupParameters, CGroupPathFull);
627 | {error, _} = Error ->
628 | Error
629 | end.
630 |
631 | update_parameters([], _) ->
632 | ok;
633 | update_parameters([{SubsystemParameter, Value} | CGroupParameters],
634 | CGroupPathFull) ->
635 | case shell("echo \"~s\" > \"~s/~s\"",
636 | [Value, CGroupPathFull, SubsystemParameter]) of
637 | {0, _} ->
638 | update_parameters(CGroupParameters, CGroupPathFull);
639 | {Status, Output} ->
640 | {error, {subsystem_parameter, Status, Output}}
641 | end.
642 |
643 | update_pids(OSPids, CGroupPathFull, true) ->
644 | update_pids(OSPids, CGroupPathFull);
645 | update_pids(OSPids, CGroupPathFull, false) ->
646 | case shell("cat \"~s/cgroup.procs\"",
647 | [CGroupPathFull]) of
648 | {0, OSPidsUpdated} ->
649 | OSPidsUpdatedL = split(shell_output_string(OSPidsUpdated), "\n"),
650 | OSPidsSet = sets:from_list([erlang:integer_to_list(OSPid)
651 | || OSPid <- OSPids]),
652 | Updated = sets:is_subset(OSPidsSet, sets:from_list(OSPidsUpdatedL)),
653 | if
654 | Updated =:= true ->
655 | ok;
656 | Updated =:= false ->
657 | {error, root_required}
658 | end;
659 | {Status, Output} ->
660 | {error, {procs, Status, Output}}
661 | end.
662 |
663 | update_pids([], _) ->
664 | ok;
665 | update_pids([OSPid | OSPids], CGroupPathFull) ->
666 | case shell("echo \"~w\" > \"~s/cgroup.procs\"",
667 | [OSPid, CGroupPathFull]) of
668 | {0, _} ->
669 | update_pids(OSPids, CGroupPathFull);
670 | {Status, Output} ->
671 | {error, {procs, Status, Output}}
672 | end.
673 |
674 | subsystem([_ | _] = SubsystemParameter) ->
675 | {Subsystem, _} = lists:splitwith(fun(C) -> C /= $. end, SubsystemParameter),
676 | Subsystem.
677 |
678 | subsystem_parameter_subpath(Path, SubsystemParameter, Path) ->
679 | subsystem_parameter_get(Path, SubsystemParameter);
680 | subsystem_parameter_subpath(CGroupSubPathFull, SubsystemParameter, Path) ->
681 | case subsystem_parameter_get(CGroupSubPathFull, SubsystemParameter) of
682 | {ok, ""} ->
683 | subsystem_parameter_subpath(subdirectory(CGroupSubPathFull),
684 | SubsystemParameter, Path);
685 | {ok, _} = Success ->
686 | Success;
687 | {error, _} = Error ->
688 | Error
689 | end.
690 |
691 | subsystem_parameter_get(CGroupSubPathFull, SubsystemParameter) ->
692 | case shell("cat \"~s~s\"", [CGroupSubPathFull, SubsystemParameter]) of
693 | {0, Contents} ->
694 | {ok, strip(shell_output_string(Contents))};
695 | {Status, Output} ->
696 | ErrorReason = subsystem_parameter_error_reason(SubsystemParameter,
697 | "_get"),
698 | {error, {ErrorReason, Status, Output}}
699 | end.
700 |
701 | subsystem_parameter_error_reason(SubsystemParameter, Suffix) ->
702 | erlang:list_to_atom(lists:map(fun(C) ->
703 | if
704 | C == $. ->
705 | $_;
706 | true ->
707 | C
708 | end
709 | end, SubsystemParameter) ++ Suffix).
710 |
711 | subtree_control_add(Path, Value, Path, Root) ->
712 | if
713 | Root =:= true ->
714 | subtree_control_set(Path, Value);
715 | Root =:= false ->
716 | ok
717 | end;
718 | subtree_control_add(CGroupSubPathFull, Value, Path, Root) ->
719 | case subtree_control_add(subdirectory(CGroupSubPathFull),
720 | Value, Path, Root) of
721 | ok ->
722 | subtree_control_set(CGroupSubPathFull, Value);
723 | {error, _} = Error ->
724 | Error
725 | end.
726 |
727 | subtree_control_set(CGroupSubPathFull, Value) ->
728 | case shell("echo \"~s\" > \"~scgroup.subtree_control\"",
729 | [Value, CGroupSubPathFull]) of
730 | {0, _} ->
731 | ok;
732 | {Status, Output} ->
733 | {error, {subtree_control, Status, Output}}
734 | end.
735 |
736 | subdirectory(Path, Path) ->
737 | Path;
738 | subdirectory(Path, _) ->
739 | subdirectory(Path).
740 |
741 | subdirectory(Path) ->
742 | case lists:reverse(Path) of
743 | [$/ | PathRev] ->
744 | filename:dirname(lists:reverse(PathRev)) ++ "/";
745 | _ ->
746 | filename:dirname(Path) ++ "/"
747 | end.
748 |
749 | strip(Value) ->
750 | lists:filter(fun(C) ->
751 | not (C == $ orelse C == $\t orelse C == $\n orelse C == $\r)
752 | end, Value).
753 |
754 | -ifdef(ERLANG_OTP_VERSION_20_FEATURES).
755 | split(String, SearchPattern) ->
756 | string:split(String, SearchPattern, all).
757 | -else.
758 | split(String, SearchPattern)
759 | when is_list(String) ->
760 | [erlang:binary_to_list(S)
761 | || S <- split(erlang:list_to_binary(String), SearchPattern)];
762 | split(String, SearchPattern)
763 | when is_binary(String) ->
764 | Pattern = if
765 | is_binary(SearchPattern) ->
766 | [SearchPattern];
767 | is_integer(hd(SearchPattern)) ->
768 | [erlang:list_to_binary(SearchPattern)];
769 | is_list(SearchPattern) ->
770 | [erlang:iolist_to_binary(S) || S <- SearchPattern]
771 | end,
772 | binary:split(String, Pattern, [global]).
773 | -endif.
774 |
775 | cgroup_path_valid([]) ->
776 | true;
777 | cgroup_path_valid([_ | _] = CGroupPath) ->
778 | ($/ /= hd(CGroupPath)) andalso
779 | ($/ /= hd(lists:reverse(CGroupPath))) andalso
780 | quoteless(CGroupPath).
781 |
782 | quoteless(String) ->
783 | lists:all(fun(C) ->
784 | C /= $"
785 | end, String).
786 |
787 | option(Key, Options) ->
788 | case lists:keytake(Key, 1, Options) of
789 | false ->
790 | {ok, Value} = application:get_env(?APPLICATION, Key),
791 | {Value, Options};
792 | {value, {Key, Value}, NewOptions} ->
793 | {Value, NewOptions}
794 | end.
795 |
796 | shell_output(Shell, Output) ->
797 | receive
798 | {Shell, {data, Data}} ->
799 | shell_output(Shell, [Data | Output]);
800 | {Shell, {exit_status, Status}} ->
801 | {Status, lists:reverse(Output)}
802 | end.
803 |
804 | shell_output_string(IOList) ->
805 | erlang:binary_to_list(erlang:iolist_to_binary(IOList)).
806 |
807 |
--------------------------------------------------------------------------------