/root/doris/be/src/util/cgroup_util.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | // Licensed to the Apache Software Foundation (ASF) under one |
2 | | // or more contributor license agreements. See the NOTICE file |
3 | | // distributed with this work for additional information |
4 | | // regarding copyright ownership. The ASF licenses this file |
5 | | // to you under the Apache License, Version 2.0 (the |
6 | | // "License"); you may not use this file except in compliance |
7 | | // with the License. You may obtain a copy of the License at |
8 | | // |
9 | | // http://www.apache.org/licenses/LICENSE-2.0 |
10 | | // |
11 | | // Unless required by applicable law or agreed to in writing, |
12 | | // software distributed under the License is distributed on an |
13 | | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
14 | | // KIND, either express or implied. See the License for the |
15 | | // specific language governing permissions and limitations |
16 | | // under the License. |
17 | | |
18 | | #include "util/cgroup_util.h" |
19 | | |
20 | | #include <algorithm> |
21 | | #include <fstream> |
22 | | #include <utility> |
23 | | #include <vector> |
24 | | |
25 | | #include "gutil/stringprintf.h" |
26 | | #include "gutil/strings/escaping.h" |
27 | | #include "gutil/strings/split.h" |
28 | | #include "gutil/strings/substitute.h" |
29 | | #include "io/fs/local_file_system.h" |
30 | | #include "util/error_util.h" |
31 | | #include "util/string_parser.hpp" |
32 | | |
33 | | using strings::CUnescape; |
34 | | using strings::Split; |
35 | | using strings::SkipWhitespace; |
36 | | using std::pair; |
37 | | |
38 | | namespace doris { |
39 | | |
40 | 8 | bool CGroupUtil::cgroupsv1_enable() { |
41 | 8 | bool exists = true; |
42 | 8 | Status st = io::global_local_filesystem()->exists("/proc/cgroups", &exists); |
43 | 8 | return st.ok() && exists; |
44 | 8 | } |
45 | | |
46 | 14 | bool CGroupUtil::cgroupsv2_enable() { |
47 | 14 | #if defined(OS_LINUX) |
48 | | // This file exists iff the host has cgroups v2 enabled. |
49 | 14 | auto controllers_file = default_cgroups_mount / "cgroup.controllers"; |
50 | 14 | bool exists = true; |
51 | 14 | Status st = io::global_local_filesystem()->exists(controllers_file, &exists); |
52 | 14 | return st.ok() && exists; |
53 | | #else |
54 | | return false; |
55 | | #endif |
56 | 14 | } |
57 | | |
58 | 7 | Status CGroupUtil::find_global_cgroupv1(const string& subsystem, string* path) { |
59 | 7 | std::ifstream proc_cgroups("/proc/self/cgroup", std::ios::in); |
60 | 7 | string line; |
61 | 63 | while (true) { |
62 | 63 | if (proc_cgroups.fail()) { |
63 | 0 | return Status::CgroupError("Error reading /proc/self/cgroup: {}", get_str_err_msg()); |
64 | 63 | } else if (proc_cgroups.peek() == std::ifstream::traits_type::eof()) { |
65 | 0 | return Status::CgroupError("Could not find subsystem {} in /proc/self/cgroup", |
66 | 0 | subsystem); |
67 | 0 | } |
68 | | // The line format looks like this: |
69 | | // 4:memory:/user.slice |
70 | | // 9:cpu,cpuacct:/user.slice |
71 | | // so field size will be 3 |
72 | 63 | getline(proc_cgroups, line); |
73 | 63 | if (!proc_cgroups.good()) { |
74 | 0 | continue; |
75 | 0 | } |
76 | 63 | std::vector<string> fields = Split(line, ":"); |
77 | | // ":" in the path does not appear to be escaped - bail in the unusual case that |
78 | | // we get too many tokens. |
79 | 63 | if (fields.size() != 3) { |
80 | 0 | return Status::InvalidArgument( |
81 | 0 | "Could not parse line from /proc/self/cgroup - had {} > 3 tokens: '{}'", |
82 | 0 | fields.size(), line); |
83 | 0 | } |
84 | 63 | std::vector<string> subsystems = Split(fields[1], ","); |
85 | 63 | auto it = std::find(subsystems.begin(), subsystems.end(), subsystem); |
86 | 63 | if (it != subsystems.end()) { |
87 | 7 | *path = std::move(fields[2]); |
88 | 7 | return Status::OK(); |
89 | 7 | } |
90 | 63 | } |
91 | 7 | } |
92 | | |
93 | 14 | static Status unescape_path(const string& escaped, string* unescaped) { |
94 | 14 | string err; |
95 | 14 | if (!CUnescape(escaped, unescaped, &err)) { |
96 | 0 | return Status::InvalidArgument("Could not unescape path '{}': {}", escaped, err); |
97 | 0 | } |
98 | 14 | return Status::OK(); |
99 | 14 | } |
100 | | |
101 | 7 | Status CGroupUtil::find_cgroupv1_mounts(const string& subsystem, pair<string, string>* result) { |
102 | 7 | std::ifstream mountinfo("/proc/self/mountinfo", std::ios::in); |
103 | 7 | string line; |
104 | 70 | while (true) { |
105 | 70 | if (mountinfo.fail() || mountinfo.bad()) { |
106 | 0 | return Status::CgroupError("Error reading /proc/self/mountinfo: {}", get_str_err_msg()); |
107 | 70 | } else if (mountinfo.eof()) { |
108 | 0 | return Status::CgroupError("Could not find subsystem {} in /proc/self/mountinfo", |
109 | 0 | subsystem); |
110 | 0 | } |
111 | | // The relevant lines look like below (see proc manpage for full documentation). The |
112 | | // first example is running outside of a container, the second example is running |
113 | | // inside a docker container. Field 3 is the path relative to the root CGroup on |
114 | | // the host and Field 4 is the mount point from this process's point of view. |
115 | | // 34 29 0:28 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:15 - |
116 | | // cgroup cgroup rw,memory |
117 | | // 275 271 0:28 /docker/f23eee6f88c2ba99fcce /sys/fs/cgroup/memory |
118 | | // ro,nosuid,nodev,noexec,relatime master:15 - cgroup cgroup rw,memory |
119 | 70 | getline(mountinfo, line); |
120 | 70 | if (!mountinfo.good()) { |
121 | 0 | continue; |
122 | 0 | } |
123 | 70 | std::vector<string> fields = Split(line, " ", SkipWhitespace()); |
124 | 70 | if (fields.size() < 7) { |
125 | 0 | return Status::InvalidArgument( |
126 | 0 | "Could not parse line from /proc/self/mountinfo - had {} > 7 tokens: '{}'", |
127 | 0 | fields.size(), line); |
128 | 0 | } |
129 | 70 | if (fields[fields.size() - 3] != "cgroup") { |
130 | 42 | continue; |
131 | 42 | } |
132 | | // This is a cgroup mount. Check if it's the mount we're looking for. |
133 | 28 | std::vector<string> cgroup_opts = Split(fields[fields.size() - 1], ",", SkipWhitespace()); |
134 | 28 | auto it = std::find(cgroup_opts.begin(), cgroup_opts.end(), subsystem); |
135 | 28 | if (it == cgroup_opts.end()) { |
136 | 21 | continue; |
137 | 21 | } |
138 | | // This is the right mount. |
139 | 7 | string mount_path, system_path; |
140 | 7 | RETURN_IF_ERROR(unescape_path(fields[4], &mount_path)); |
141 | 7 | RETURN_IF_ERROR(unescape_path(fields[3], &system_path)); |
142 | | // Strip trailing "/" so that both returned paths match in whether they have a |
143 | | // trailing "/". |
144 | 7 | if (system_path[system_path.size() - 1] == '/') { |
145 | 0 | system_path.pop_back(); |
146 | 0 | } |
147 | 7 | *result = {mount_path, system_path}; |
148 | 7 | return Status::OK(); |
149 | 7 | } |
150 | 7 | } |
151 | | |
152 | 7 | Status CGroupUtil::find_abs_cgroupv1_path(const string& subsystem, string* path) { |
153 | 7 | if (!cgroupsv1_enable()) { |
154 | 0 | return Status::InvalidArgument("cgroup is not enabled!"); |
155 | 0 | } |
156 | 7 | RETURN_IF_ERROR(find_global_cgroupv1(subsystem, path)); |
157 | 7 | pair<string, string> paths; |
158 | 7 | RETURN_IF_ERROR(find_cgroupv1_mounts(subsystem, &paths)); |
159 | 7 | const string& mount_path = paths.first; |
160 | 7 | const string& system_path = paths.second; |
161 | 7 | if (path->compare(0, system_path.size(), system_path) != 0) { |
162 | 0 | return Status::InvalidArgument("Expected CGroup path '{}' to start with '{}'", *path, |
163 | 0 | system_path); |
164 | 0 | } |
165 | 7 | path->replace(0, system_path.size(), mount_path); |
166 | 7 | return Status::OK(); |
167 | 7 | } |
168 | | |
169 | 0 | std::string CGroupUtil::cgroupv2_of_process() { |
170 | 0 | #if defined(OS_LINUX) |
171 | 0 | if (!cgroupsv2_enable()) { |
172 | 0 | return ""; |
173 | 0 | } |
174 | | // All PIDs assigned to a cgroup are in /sys/fs/cgroups/{cgroup_name}/cgroup.procs |
175 | | // A simpler way to get the membership is: |
176 | 0 | std::ifstream cgroup_name_file("/proc/self/cgroup"); |
177 | 0 | if (!cgroup_name_file.is_open()) { |
178 | 0 | return ""; |
179 | 0 | } |
180 | | // With cgroups v2, there will be a *single* line with prefix "0::/" |
181 | | // (see https://docs.kernel.org/admin-guide/cgroup-v2.html) |
182 | 0 | std::string cgroup; |
183 | 0 | std::getline(cgroup_name_file, cgroup); |
184 | 0 | static const std::string v2_prefix = "0::/"; |
185 | 0 | if (!cgroup.starts_with(v2_prefix)) { |
186 | 0 | return ""; |
187 | 0 | } |
188 | 0 | cgroup = cgroup.substr(v2_prefix.length()); |
189 | 0 | return cgroup; |
190 | | #else |
191 | | return ""; |
192 | | #endif |
193 | 0 | } |
194 | | |
195 | 0 | std::optional<std::string> CGroupUtil::get_cgroupsv2_path(const std::string& subsystem) { |
196 | 0 | #if defined(OS_LINUX) |
197 | 0 | if (!CGroupUtil::cgroupsv2_enable()) { |
198 | 0 | return {}; |
199 | 0 | } |
200 | | |
201 | 0 | std::string cgroup = CGroupUtil::cgroupv2_of_process(); |
202 | 0 | auto current_cgroup = cgroup.empty() ? default_cgroups_mount : (default_cgroups_mount / cgroup); |
203 | | |
204 | | // Return the bottom-most nested current memory file. If there is no such file at the current |
205 | | // level, try again at the parent level as memory settings are inherited. |
206 | 0 | while (current_cgroup != default_cgroups_mount.parent_path()) { |
207 | 0 | if (std::filesystem::exists(current_cgroup / subsystem)) { |
208 | 0 | return {current_cgroup}; |
209 | 0 | } |
210 | 0 | current_cgroup = current_cgroup.parent_path(); |
211 | 0 | } |
212 | 0 | return {}; |
213 | | #else |
214 | | return {}; |
215 | | #endif |
216 | 0 | } |
217 | | |
218 | | Status CGroupUtil::read_int_line_from_cgroup_file(const std::filesystem::path& file_path, |
219 | 5 | int64_t* val) { |
220 | 5 | std::ifstream file_stream(file_path, std::ios::in); |
221 | 5 | string line; |
222 | 5 | getline(file_stream, line); |
223 | 5 | if (file_stream.fail() || file_stream.bad()) { |
224 | 1 | return Status::CgroupError("Error reading {}: {}", file_path.string(), get_str_err_msg()); |
225 | 1 | } |
226 | 4 | StringParser::ParseResult pr; |
227 | | // Parse into an int64_t If it overflows, returning the max value of int64_t is ok because that |
228 | | // is effectively unlimited. |
229 | 4 | *val = StringParser::string_to_int<int64_t>(line.c_str(), line.size(), &pr); |
230 | 4 | if ((pr != StringParser::PARSE_SUCCESS && pr != StringParser::PARSE_OVERFLOW)) { |
231 | 0 | return Status::InvalidArgument("Failed to parse {} as int64: '{}'", file_path.string(), |
232 | 0 | line); |
233 | 0 | } |
234 | 4 | return Status::OK(); |
235 | 4 | } |
236 | | |
237 | | void CGroupUtil::read_int_metric_from_cgroup_file( |
238 | | const std::filesystem::path& file_path, |
239 | 5 | std::unordered_map<std::string, int64_t>& metrics_map) { |
240 | 5 | std::ifstream cgroup_file(file_path, std::ios::in); |
241 | 5 | std::string line; |
242 | 144 | while (cgroup_file.good() && !cgroup_file.eof()) { |
243 | 139 | getline(cgroup_file, line); |
244 | 139 | std::vector<std::string> fields = strings::Split(line, " ", strings::SkipWhitespace()); |
245 | 139 | if (fields.size() < 2) { |
246 | 4 | continue; |
247 | 4 | } |
248 | 135 | std::string key = fields[0].substr(0, fields[0].size()); |
249 | | |
250 | 135 | StringParser::ParseResult result; |
251 | 135 | auto value = |
252 | 135 | StringParser::string_to_int<int64_t>(fields[1].data(), fields[1].size(), &result); |
253 | | |
254 | 135 | if (result == StringParser::PARSE_SUCCESS) { |
255 | 135 | if (fields.size() == 2) { |
256 | 135 | metrics_map[key] = value; |
257 | 135 | } else if (fields[2] == "kB") { |
258 | 0 | metrics_map[key] = value * 1024L; |
259 | 0 | } |
260 | 135 | } |
261 | 135 | } |
262 | 5 | if (cgroup_file.is_open()) { |
263 | 4 | cgroup_file.close(); |
264 | 4 | } |
265 | 5 | } |
266 | | |
267 | | } // namespace doris |