Coverage Report

Created: 2026-07-18 00:49

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/root/doris/be/src/util/cgroup_util.cpp
Line
Count
Source
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 <absl/strings/escaping.h>
21
#include <absl/strings/str_split.h>
22
23
#include <algorithm>
24
#include <fstream>
25
#include <utility>
26
#include <vector>
27
28
#include "io/fs/local_file_system.h"
29
#include "util/error_util.h"
30
#include "util/string_parser.hpp"
31
32
using std::pair;
33
34
namespace doris {
35
36
14
bool CGroupUtil::cgroupsv1_enable() {
37
14
    bool exists = true;
38
14
    Status st = io::global_local_filesystem()->exists("/proc/cgroups", &exists);
39
14
    return st.ok() && exists;
40
14
}
41
42
16
bool CGroupUtil::cgroupsv2_enable() {
43
16
#if defined(OS_LINUX)
44
    // This file exists iff the host has cgroups v2 enabled.
45
16
    auto controllers_file = default_cgroups_mount / "cgroup.controllers";
46
16
    bool exists = true;
47
16
    Status st = io::global_local_filesystem()->exists(controllers_file, &exists);
48
16
    return st.ok() && exists;
49
#else
50
    return false;
51
#endif
52
16
}
53
54
11
Status CGroupUtil::find_global_cgroupv1(const std::string& subsystem, std::string* path) {
55
11
    std::ifstream proc_cgroups("/proc/self/cgroup", std::ios::in);
56
11
    std::string line;
57
27
    while (true) {
58
27
        if (proc_cgroups.fail()) {
59
0
            return Status::CgroupError("Error reading /proc/self/cgroup: {}", get_str_err_msg());
60
27
        } else if (proc_cgroups.peek() == std::ifstream::traits_type::eof()) {
61
0
            return Status::CgroupError("Could not find subsystem {} in /proc/self/cgroup",
62
0
                                       subsystem);
63
0
        }
64
        // The line format looks like this:
65
        // 4:memory:/user.slice
66
        // 9:cpu,cpuacct:/user.slice
67
        // so field size will be 3
68
27
        getline(proc_cgroups, line);
69
27
        if (!proc_cgroups.good()) {
70
0
            continue;
71
0
        }
72
27
        std::vector<std::string> fields = absl::StrSplit(line, ":");
73
        // ":" in the path does not appear to be escaped - bail in the unusual case that
74
        // we get too many tokens.
75
27
        if (fields.size() != 3) {
76
0
            return Status::InvalidArgument(
77
0
                    "Could not parse line from /proc/self/cgroup - had {} > 3 tokens: '{}'",
78
0
                    fields.size(), line);
79
0
        }
80
27
        std::vector<std::string> subsystems = absl::StrSplit(fields[1], ",");
81
27
        auto it = std::find(subsystems.begin(), subsystems.end(), subsystem);
82
27
        if (it != subsystems.end()) {
83
11
            *path = std::move(fields[2]);
84
11
            return Status::OK();
85
11
        }
86
27
    }
87
11
}
88
89
22
static Status unescape_path(const std::string& escaped, std::string* unescaped) {
90
22
    std::string err;
91
22
    if (!absl::CUnescape(escaped, unescaped, &err)) {
92
0
        return Status::InvalidArgument("Could not unescape path '{}': {}", escaped, err);
93
0
    }
94
22
    return Status::OK();
95
22
}
96
97
Status CGroupUtil::find_cgroupv1_mounts(const std::string& subsystem,
98
11
                                        pair<std::string, std::string>* result) {
99
11
    std::ifstream mountinfo("/proc/self/mountinfo", std::ios::in);
100
11
    std::string line;
101
182
    while (true) {
102
182
        if (mountinfo.fail() || mountinfo.bad()) {
103
0
            return Status::CgroupError("Error reading /proc/self/mountinfo: {}", get_str_err_msg());
104
182
        } else if (mountinfo.eof()) {
105
0
            return Status::CgroupError("Could not find subsystem {} in /proc/self/mountinfo",
106
0
                                       subsystem);
107
0
        }
108
        // The relevant lines look like below (see proc manpage for full documentation). The
109
        // first example is running outside of a container, the second example is running
110
        // inside a docker container. Field 3 is the path relative to the root CGroup on
111
        // the host and Field 4 is the mount point from this process's point of view.
112
        // 34 29 0:28 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:15 -
113
        //    cgroup cgroup rw,memory
114
        // 275 271 0:28 /docker/f23eee6f88c2ba99fcce /sys/fs/cgroup/memory
115
        //    ro,nosuid,nodev,noexec,relatime master:15 - cgroup cgroup rw,memory
116
182
        getline(mountinfo, line);
117
182
        if (!mountinfo.good()) {
118
0
            continue;
119
0
        }
120
182
        std::vector<std::string> fields = absl::StrSplit(line, " ", absl::SkipWhitespace());
121
182
        if (fields.size() < 7) {
122
0
            return Status::InvalidArgument(
123
0
                    "Could not parse line from /proc/self/mountinfo - had {} > 7 tokens: '{}'",
124
0
                    fields.size(), line);
125
0
        }
126
182
        if (fields[fields.size() - 3] != "cgroup") {
127
66
            continue;
128
66
        }
129
        // This is a cgroup mount. Check if it's the mount we're looking for.
130
116
        std::vector<std::string> cgroup_opts =
131
116
                absl::StrSplit(fields[fields.size() - 1], ",", absl::SkipWhitespace());
132
116
        auto it = std::find(cgroup_opts.begin(), cgroup_opts.end(), subsystem);
133
116
        if (it == cgroup_opts.end()) {
134
105
            continue;
135
105
        }
136
        // This is the right mount.
137
11
        std::string mount_path, system_path;
138
11
        RETURN_IF_ERROR(unescape_path(fields[4], &mount_path));
139
11
        RETURN_IF_ERROR(unescape_path(fields[3], &system_path));
140
        // Strip trailing "/" so that both returned paths match in whether they have a
141
        // trailing "/".
142
11
        if (system_path[system_path.size() - 1] == '/') {
143
0
            system_path.pop_back();
144
0
        }
145
11
        *result = {mount_path, system_path};
146
11
        return Status::OK();
147
11
    }
148
11
}
149
150
11
Status CGroupUtil::find_abs_cgroupv1_path(const std::string& subsystem, std::string* path) {
151
11
    if (!cgroupsv1_enable()) {
152
0
        return Status::InvalidArgument("cgroup is not enabled!");
153
0
    }
154
11
    RETURN_IF_ERROR(find_global_cgroupv1(subsystem, path));
155
11
    pair<std::string, std::string> paths;
156
11
    RETURN_IF_ERROR(find_cgroupv1_mounts(subsystem, &paths));
157
11
    const std::string& mount_path = paths.first;
158
11
    const std::string& system_path = paths.second;
159
11
    if (path->compare(0, system_path.size(), system_path) != 0) {
160
0
        return Status::InvalidArgument("Expected CGroup path '{}' to start with '{}'", *path,
161
0
                                       system_path);
162
0
    }
163
11
    path->replace(0, system_path.size(), mount_path);
164
11
    return Status::OK();
165
11
}
166
167
0
std::string CGroupUtil::cgroupv2_of_process() {
168
0
#if defined(OS_LINUX)
169
0
    if (!cgroupsv2_enable()) {
170
0
        return "";
171
0
    }
172
    // All PIDs assigned to a cgroup are in /sys/fs/cgroups/{cgroup_name}/cgroup.procs
173
    // A simpler way to get the membership is:
174
0
    std::ifstream cgroup_name_file("/proc/self/cgroup");
175
0
    if (!cgroup_name_file.is_open()) {
176
0
        return "";
177
0
    }
178
    // With cgroups v2, there will be a *single* line with prefix "0::/"
179
    // (see https://docs.kernel.org/admin-guide/cgroup-v2.html)
180
    // such as 0::/user.slice/user-1005.slice/session-213906.scope this is the cgroup name
181
    // it should be combined with the default cgroup mount point to get the full path to the cgroup, e.g.
182
    // /sys/fs/cgroup/user.slice/user-1005.slice/session-213906.scope
183
0
    std::string cgroup;
184
0
    std::getline(cgroup_name_file, cgroup);
185
0
    static const std::string v2_prefix = "0::/";
186
0
    if (!cgroup.starts_with(v2_prefix)) {
187
0
        return "";
188
0
    }
189
0
    cgroup = cgroup.substr(v2_prefix.length());
190
0
    return cgroup;
191
#else
192
    return "";
193
#endif
194
0
}
195
196
0
std::optional<std::string> CGroupUtil::get_cgroupsv2_path(const std::string& subsystem) {
197
0
#if defined(OS_LINUX)
198
0
    if (!CGroupUtil::cgroupsv2_enable()) {
199
0
        return {};
200
0
    }
201
202
0
    std::string cgroup = CGroupUtil::cgroupv2_of_process();
203
    //    /sys/fs/cgroup/user.slice/user-1005.slice/session-213906.scope
204
0
    auto current_cgroup = cgroup.empty() ? default_cgroups_mount : (default_cgroups_mount / cgroup);
205
206
    // Return the bottom-most nested current memory file. If there is no such file at the current
207
    // level, try again at the parent level as memory settings are inherited.
208
0
    while (current_cgroup != default_cgroups_mount.parent_path()) {
209
0
        if (std::filesystem::exists(current_cgroup / subsystem)) {
210
0
            return {current_cgroup};
211
0
        }
212
0
        current_cgroup = current_cgroup.parent_path();
213
0
    }
214
0
    return {};
215
#else
216
    return {};
217
#endif
218
0
}
219
220
Status CGroupUtil::read_int_line_from_cgroup_file(const std::filesystem::path& file_path,
221
5
                                                  int64_t* val) {
222
5
    std::ifstream file_stream(file_path, std::ios::in);
223
5
    if (!file_stream.is_open()) {
224
1
        return Status::CgroupError("Error open {}", file_path.string());
225
1
    }
226
227
4
    std::string line;
228
4
    getline(file_stream, line);
229
4
    if (file_stream.fail() || file_stream.bad()) {
230
0
        return Status::CgroupError("Error reading {}: {}", file_path.string(), get_str_err_msg());
231
0
    }
232
4
    StringParser::ParseResult pr;
233
    // Parse into an int64_t If it overflows, returning the max value of int64_t is ok because that
234
    // is effectively unlimited.
235
4
    *val = StringParser::string_to_int<int64_t>(line.c_str(), line.size(), &pr);
236
4
    if ((pr != StringParser::PARSE_SUCCESS && pr != StringParser::PARSE_OVERFLOW)) {
237
0
        return Status::InvalidArgument("Failed to parse {} as int64: '{}'", file_path.string(),
238
0
                                       line);
239
0
    }
240
4
    return Status::OK();
241
4
}
242
243
void CGroupUtil::read_int_metric_from_cgroup_file(
244
        const std::filesystem::path& file_path,
245
5
        std::unordered_map<std::string, int64_t>& metrics_map) {
246
5
    std::ifstream cgroup_file(file_path, std::ios::in);
247
5
    std::string line;
248
144
    while (cgroup_file.good() && !cgroup_file.eof()) {
249
139
        getline(cgroup_file, line);
250
139
        std::vector<std::string> fields = absl::StrSplit(line, " ", absl::SkipWhitespace());
251
139
        if (fields.size() < 2) {
252
4
            continue;
253
4
        }
254
135
        std::string key = fields[0].substr(0, fields[0].size());
255
256
135
        StringParser::ParseResult result;
257
135
        auto value =
258
135
                StringParser::string_to_int<int64_t>(fields[1].data(), fields[1].size(), &result);
259
260
135
        if (result == StringParser::PARSE_SUCCESS) {
261
135
            if (fields.size() == 2) {
262
135
                metrics_map[key] = value;
263
135
            } else if (fields[2] == "kB") {
264
0
                metrics_map[key] = value * 1024L;
265
0
            } else {
266
0
                LOG(WARNING) << "Unknown unit in cgroup file " << file_path.string()
267
0
                             << ", line: " << line;
268
0
            }
269
135
        }
270
135
    }
271
5
    if (cgroup_file.is_open()) {
272
4
        cgroup_file.close();
273
4
    }
274
5
}
275
276
Status CGroupUtil::read_string_line_from_cgroup_file(const std::filesystem::path& file_path,
277
6
                                                     std::string* line_ptr) {
278
6
    std::ifstream file_stream(file_path, std::ios::in);
279
6
    if (!file_stream.is_open()) {
280
0
        return Status::CgroupError("Error open {}", file_path.string());
281
0
    }
282
6
    std::string line;
283
6
    getline(file_stream, line);
284
6
    if (file_stream.fail() || file_stream.bad()) {
285
0
        return Status::CgroupError("Error reading {}: {}", file_path.string(), get_str_err_msg());
286
0
    }
287
6
    *line_ptr = line;
288
6
    return Status::OK();
289
6
}
290
291
8
Status CGroupUtil::parse_cpuset_line(std::string cpuset_line, int* cpu_count_ptr) {
292
8
    if (cpuset_line.empty()) {
293
0
        return Status::CgroupError("cpuset line is empty");
294
0
    }
295
8
    std::vector<std::string> ranges;
296
8
    boost::split(ranges, cpuset_line, boost::is_any_of(","));
297
8
    int cpu_count = 0;
298
299
15
    for (const std::string& range : ranges) {
300
15
        std::vector<std::string> cpu_values;
301
15
        boost::split(cpu_values, range, boost::is_any_of("-"));
302
303
15
        if (cpu_values.size() == 2) {
304
10
            int start = std::stoi(cpu_values[0]);
305
10
            int end = std::stoi(cpu_values[1]);
306
10
            cpu_count += (end - start) + 1;
307
10
        } else {
308
5
            cpu_count++;
309
5
        }
310
15
    }
311
8
    *cpu_count_ptr = cpu_count;
312
8
    return Status::OK();
313
8
}
314
315
2
int CGroupUtil::get_cgroup_limited_cpu_number(int physical_cores) {
316
2
    if (physical_cores <= 0) {
317
0
        return physical_cores;
318
0
    }
319
2
    int ret = physical_cores;
320
2
#if defined(OS_LINUX)
321
    // For cgroup v2
322
    // Child cgroup's cpu.max may bigger than parent group's cpu.max,
323
    //      so it should look up from current cgroup to top group.
324
    // For cpuset, child cgroup's cpuset.cpus could not bigger thant parent's cpuset.cpus.
325
2
    if (CGroupUtil::cgroupsv2_enable()) {
326
0
        std::string cgroupv2_process_path = CGroupUtil::cgroupv2_of_process();
327
0
        if (cgroupv2_process_path.empty()) {
328
0
            return ret;
329
0
        }
330
0
        std::filesystem::path current_cgroup_path = (default_cgroups_mount / cgroupv2_process_path);
331
0
        ret = get_cgroup_v2_cpu_quota_number(current_cgroup_path, default_cgroups_mount, ret);
332
333
0
        current_cgroup_path = (default_cgroups_mount / cgroupv2_process_path);
334
0
        ret = get_cgroup_v2_cpuset_number(current_cgroup_path, default_cgroups_mount, ret);
335
2
    } else if (CGroupUtil::cgroupsv1_enable()) {
336
        // cpu quota, should find first not empty config from current path to top.
337
        // because if a process attach to current cgroup, its cpu quota may not be set.
338
2
        std::string cpu_quota_path = "";
339
2
        Status cpu_quota_ret = CGroupUtil::find_abs_cgroupv1_path("cpu", &cpu_quota_path);
340
2
        if (cpu_quota_ret.ok() && !cpu_quota_path.empty()) {
341
2
            std::filesystem::path current_cgroup_path = cpu_quota_path;
342
2
            ret = get_cgroup_v1_cpu_quota_number(current_cgroup_path, default_cgroups_mount, ret);
343
2
        }
344
345
        //cpuset
346
        // just lookup current process cgroup path is enough
347
        // because if a process attach to current cgroup, its cpuset.cpus must be set.
348
2
        std::string cpuset_path = "";
349
2
        Status cpuset_ret = CGroupUtil::find_abs_cgroupv1_path("cpuset", &cpuset_path);
350
2
        if (cpuset_ret.ok() && !cpuset_path.empty()) {
351
2
            std::filesystem::path current_path = cpuset_path;
352
2
            ret = get_cgroup_v1_cpuset_number(current_path, ret);
353
2
        }
354
2
    }
355
2
#endif
356
2
    return ret;
357
2
}
358
359
int CGroupUtil::get_cgroup_v2_cpu_quota_number(std::filesystem::path& current_path,
360
                                               const std::filesystem::path& default_cg_mout_path,
361
4
                                               int cpu_num) {
362
4
    int ret = cpu_num;
363
12
    while (current_path != default_cg_mout_path.parent_path()) {
364
8
        std::ifstream cpu_max_file(current_path / "cpu.max");
365
8
        if (cpu_max_file.is_open()) {
366
8
            std::string cpu_limit_str;
367
8
            double cpu_period;
368
8
            cpu_max_file >> cpu_limit_str >> cpu_period;
369
8
            if (cpu_limit_str != "max" && cpu_period != 0) {
370
5
                double cpu_limit = std::stod(cpu_limit_str);
371
5
                ret = std::min(static_cast<int>(std::ceil(cpu_limit / cpu_period)), ret);
372
5
            }
373
8
        }
374
8
        current_path = current_path.parent_path();
375
8
    }
376
4
    return ret;
377
4
}
378
379
int CGroupUtil::get_cgroup_v2_cpuset_number(std::filesystem::path& current_path,
380
                                            const std::filesystem::path& default_cg_mout_path,
381
2
                                            int cpu_num) {
382
2
    int ret = cpu_num;
383
3
    while (current_path != default_cg_mout_path.parent_path()) {
384
3
        std::ifstream cpuset_cpus_file(current_path / "cpuset.cpus.effective");
385
3
        current_path = current_path.parent_path();
386
3
        if (cpuset_cpus_file.is_open()) {
387
3
            std::string cpuset_line;
388
3
            cpuset_cpus_file >> cpuset_line;
389
3
            if (cpuset_line.empty()) {
390
1
                continue;
391
1
            }
392
2
            int cpus_count = 0;
393
2
            static_cast<void>(CGroupUtil::parse_cpuset_line(cpuset_line, &cpus_count));
394
2
            ret = std::min(cpus_count, ret);
395
2
            break;
396
3
        }
397
3
    }
398
2
    return ret;
399
2
}
400
401
int CGroupUtil::get_cgroup_v1_cpu_quota_number(std::filesystem::path& current_path,
402
                                               const std::filesystem::path& default_cg_mout_path,
403
5
                                               int cpu_num) {
404
5
    int ret = cpu_num;
405
12
    while (current_path != default_cg_mout_path.parent_path()) {
406
9
        std::ifstream cpu_quota_file(current_path / "cpu.cfs_quota_us");
407
9
        std::ifstream cpu_period_file(current_path / "cpu.cfs_period_us");
408
9
        if (cpu_quota_file.is_open() && cpu_period_file.is_open()) {
409
7
            double cpu_quota_value;
410
7
            double cpu_period_value;
411
7
            cpu_quota_file >> cpu_quota_value;
412
7
            cpu_period_file >> cpu_period_value;
413
7
            if (cpu_quota_value > 0 && cpu_period_value > 0) {
414
2
                ret = std::min(ret,
415
2
                               static_cast<int>(std::ceil(cpu_quota_value / cpu_period_value)));
416
2
                break;
417
2
            }
418
7
        }
419
7
        current_path = current_path.parent_path();
420
7
    }
421
5
    return ret;
422
5
}
423
424
3
int CGroupUtil::get_cgroup_v1_cpuset_number(std::filesystem::path& current_path, int cpu_num) {
425
3
    int ret = cpu_num;
426
3
    std::string cpuset_line = "";
427
3
    Status cpuset_ret = CGroupUtil::read_string_line_from_cgroup_file(
428
3
            (current_path / "cpuset.cpus"), &cpuset_line);
429
3
    if (cpuset_ret.ok() && !cpuset_line.empty()) {
430
3
        int cpuset_count = 0;
431
3
        static_cast<void>(CGroupUtil::parse_cpuset_line(cpuset_line, &cpuset_count));
432
3
        if (cpuset_count > 0) {
433
3
            ret = std::min(ret, cpuset_count);
434
3
        }
435
3
    }
436
3
    return ret;
437
3
}
438
439
} // namespace doris