├── 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 | The cgroups application 5 | 6 | 7 | 8 |

Modules

9 | 10 |
cgroups
11 | 12 | -------------------------------------------------------------------------------- /doc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The cgroups application 5 | 6 | 7 | 8 | 9 | 10 | 11 | <h2>This page uses frames</h2> 12 | <p>Your browser does not accept frames. 13 | <br>You should go to the <a href="overview-summary.html">non-frame version</a> instead. 14 | </p> 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/cgroups.app.src: -------------------------------------------------------------------------------- 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 | {application, cgroups, 5 | [{description, "Erlang native cgroups interface"}, 6 | {vsn, "2.0.7"}, 7 | {modules, [cgroups]}, 8 | {registered, []}, 9 | {applications, [stdlib, kernel]}, 10 | {env, [ 11 | {version_default, 2}, 12 | {version_default_required, false}, 13 | {path_v1, "/sys/fs/cgroup/"}, 14 | {path_v2, "/sys/fs/cgroup2/"}, 15 | {path_mounts, "/proc/mounts"} 16 | ]}]}. 17 | 18 | -------------------------------------------------------------------------------- /doc/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* standard EDoc style sheet */ 2 | body { 3 | font-family: Verdana, Arial, Helvetica, sans-serif; 4 | margin-left: .25in; 5 | margin-right: .2in; 6 | margin-top: 0.2in; 7 | margin-bottom: 0.2in; 8 | color: #000000; 9 | background-color: #ffffff; 10 | } 11 | h1,h2 { 12 | margin-left: -0.2in; 13 | } 14 | div.navbar { 15 | background-color: #add8e6; 16 | padding: 0.2em; 17 | } 18 | h2.indextitle { 19 | padding: 0.4em; 20 | background-color: #add8e6; 21 | } 22 | h3.function,h3.typedecl { 23 | background-color: #add8e6; 24 | padding-left: 1em; 25 | } 26 | div.spec { 27 | margin-left: 2em; 28 | background-color: #eeeeee; 29 | } 30 | a.module { 31 | text-decoration:none 32 | } 33 | a.module:hover { 34 | background-color: #eeeeee; 35 | } 36 | ul.definitions { 37 | list-style-type: none; 38 | } 39 | ul.index { 40 | list-style-type: none; 41 | background-color: #eeeeee; 42 | } 43 | 44 | /* 45 | * Minor style tweaks 46 | */ 47 | ul { 48 | list-style-type: square; 49 | } 50 | table { 51 | border-collapse: collapse; 52 | } 53 | td { 54 | padding: 3 55 | } 56 | -------------------------------------------------------------------------------- /doc/overview-summary.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | The cgroups application 6 | 7 | 8 | 9 | 10 |

The cgroups application

11 | 12 |
13 | 14 |

Generated by EDoc

15 | 16 | 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016-2023 Michael Truog 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 |

cgroups Manipulation Functions

. 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 |

Description

22 |

cgroups Manipulation Functions

23 | 24 |

Data Types

25 | 26 |

new_error_reasons()

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 |

options()

31 |

options() = [{version_default, pos_integer()} | {version_default_required, boolean()} | {path_v1, string()} | {path_v2, string()} | {path_mounts, string() | undefined}]

32 | 33 | 34 |

Function Index

35 | 40 | 44 | 48 | 50 | 52 | 54 | 56 | 59 | 61 | 63 |
create/4 36 |

Create a specific cgroup.

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).
delete/2 41 |

Delete a specific cgroup.

42 | The cgroup must not contain any OS processes for this 43 | function to succeed.
delete_recursive/2 45 |

Delete a specific cgroup and as many non-leaf cgroups as possible.

46 | The cgroup must not contain any OS processes for this 47 | function to succeed.
destroy/1 49 |

Destroy cgroups state data.

.
new/0 51 |

Create new cgroups state data.

.
new/1 53 |

Create new cgroups state data with local options.

.
shell/2 55 |

Execute a command with the default shell.

.
update/4 57 |

Update a cgroup path.

58 | May be used on the cgroup root path.
update_or_create/4 60 |

Update or create a specific cgroup.

.
version/1 62 |

The cgroup version used.

.
64 | 65 |

Function Details

66 | 67 |

create/4

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 |

Create a specific cgroup.

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 |

delete/2

78 |
79 |

delete(CGroupPath::nonempty_string(), State::#cgroups{version = pos_integer(), path = string(), mounted = boolean(), root = boolean()}) -> ok | {error, any()}

80 |

81 |

82 |

Delete a specific cgroup.

83 | The cgroup must not contain any OS processes for this 84 | function to succeed.

85 | 86 |

delete_recursive/2

87 |
88 |

delete_recursive(CGroupPath::nonempty_string(), State::#cgroups{version = pos_integer(), path = string(), mounted = boolean(), root = boolean()}) -> ok | {error, any()}

89 |

90 |

91 |

Delete a specific cgroup and as many non-leaf cgroups as possible.

92 | The cgroup must not contain any OS processes for this 93 | function to succeed.

94 | 95 |

destroy/1

96 |
97 |

destroy(Cgroups::#cgroups{version = pos_integer(), path = string(), mounted = boolean(), root = boolean()}) -> ok

98 |

99 |

100 |

Destroy cgroups state data.

101 |

102 | 103 |

new/0

104 |
105 |

new() -> {ok, #cgroups{version = pos_integer(), path = string(), mounted = boolean(), root = boolean()}} | {error, new_error_reasons()}

106 |

107 |

108 |

Create new cgroups state data.

109 |

110 | 111 |

new/1

112 |
113 |

new(Options0::options()) -> {ok, #cgroups{version = pos_integer(), path = string(), mounted = boolean(), root = boolean()}} | {error, new_error_reasons()}

114 |

115 |

116 |

Create new cgroups state data with local options.

117 |

118 | 119 |

shell/2

120 |
121 |

shell(Command::string(), Arguments::list()) -> {non_neg_integer(), [binary()]}

122 |

123 |

124 |

Execute a command with the default shell.

125 |

126 | 127 |

update/4

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 |

Update a cgroup path.

133 | May be used on the cgroup root path.

134 | 135 |

update_or_create/4

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 |

Update or create a specific cgroup.

141 |

142 | 143 |

version/1

144 |
145 |

version(Cgroups::#cgroups{version = pos_integer(), path = string(), mounted = boolean(), root = boolean()}) -> pos_integer()

146 |

147 |

148 |

The cgroup version used.

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 | --------------------------------------------------------------------------------