Files
visor/test/util/cgroup_util.cc
Shailend ChandandgVisor bot dde347d9fc Add initial cgroup2 support.
The cgroup v2 fs is initialized early in the boot process, so that every task
in the sandbox resides in at least the root v2 cgroup. A new runsc flag
--mount-cgroup-v2 has been added: this controls whether runsc mounts a
cgroupv2 filesystem at /sys/fs/cgroup automatically (currently it always
mounts a v1 hierarchy at this path). This flag also gates whether userspace
can mount a cgroup2 fs. When this flag is false, which is the default, all
tasks in the sandbox reside forever in the root cgroup.

Like in Linux, the v2 hierarchy is deemed the default owner of a controller,
that is, until the corresponding v1 hierarchy is mounted: then, the v1 tree
steals ownership of that controller and it remains this way till as long as
a v1 mount exists. In other words, hybrid mode is supported, even if such
workloads are rare these days.

The pids controller is the only controller implemented in full: it serves
as validation for the unified hierarchy. The memory controller can be used
to learn the usage stats of a subtree: but like the current v1 implementation,
and unlike Linux, it doesn't achieve this through charging up the hierarchy
during allocation, instead it queries pgalloc for every active controller's
id. And it does no enforcing. The cpu and cpuset controllers are mere stubs,
like in the current v1 implementation.

The following are supported:
* General matters of topology: cgroup.subtree_control, cgroup.controllers,
                               cgroup.procs etc
* CLONE_INTO_CGROUP
* cgroup.kill
* cgroup.events
* A working pids controller and its events files
* A minimal memory controller that just reports mem usage
* Stubbed cpu and cpuset controllers

The following are NOT supported:
* threaded cgroups
* cgroup namespaces
* BPF_PROG_TYPE_CGROUP_DEVICE
* cgroup.freeze

Updates #13007

PiperOrigin-RevId: 943519748
2026-07-06 15:17:54 -07:00

338 lines
11 KiB
C++

// Copyright 2021 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#include "test/util/cgroup_util.h"
#include <sys/syscall.h>
#include <unistd.h>
#include "absl/strings/str_cat.h"
#include "absl/strings/str_split.h"
#include "test/util/fs_util.h"
#include "test/util/mount_util.h"
namespace gvisor {
namespace testing {
Cgroup::Cgroup(absl::string_view path, absl::string_view mountpoint)
: cgroup_path_(path), mountpoint_(mountpoint) {
id_ = ++Cgroup::next_id_;
std::cerr << absl::StreamFormat("[cg#%d] <= %s", id_, cgroup_path_)
<< std::endl;
}
PosixError Cgroup::Delete() { return Rmdir(cgroup_path_); }
PosixErrorOr<Cgroup> Cgroup::CreateChild(absl::string_view name) const {
std::string path = JoinPath(Path(), name);
RETURN_IF_ERRNO(Mkdir(path));
return Cgroup(path, mountpoint_);
}
PosixErrorOr<std::string> Cgroup::ReadControlFile(
absl::string_view name) const {
std::string buf;
RETURN_IF_ERRNO(GetContents(Relpath(name), &buf));
const std::string alias_path = absl::StrFormat("[cg#%d]/%s", id_, name);
std::cerr << absl::StreamFormat("<contents of %s>", alias_path) << std::endl;
std::cerr << buf;
std::cerr << absl::StreamFormat("<end of %s>", alias_path) << std::endl;
return buf;
}
PosixErrorOr<int64_t> Cgroup::ReadIntegerControlFile(
absl::string_view name) const {
ASSIGN_OR_RETURN_ERRNO(const std::string buf, ReadControlFile(name));
ASSIGN_OR_RETURN_ERRNO(const int64_t val, Atoi<int64_t>(buf));
return val;
}
PosixError Cgroup::WriteControlFile(absl::string_view name,
const std::string& value) const {
ASSIGN_OR_RETURN_ERRNO(FileDescriptor fd, Open(Relpath(name), O_WRONLY));
RETURN_ERROR_IF_SYSCALL_FAIL(WriteFd(fd.get(), value.c_str(), value.size()));
const std::string alias_path = absl::StrFormat("[cg#%d]/%s", id_, name);
std::cerr << absl::StreamFormat("echo '%s' > %s", value, alias_path)
<< std::endl;
return NoError();
}
PosixError Cgroup::WriteIntegerControlFile(absl::string_view name,
int64_t value) const {
return WriteControlFile(name, absl::StrCat(value));
}
PosixErrorOr<absl::flat_hash_set<pid_t>> Cgroup::Procs() const {
ASSIGN_OR_RETURN_ERRNO(std::string buf, ReadControlFile("cgroup.procs"));
return ParsePIDList(buf);
}
PosixErrorOr<absl::flat_hash_set<pid_t>> Cgroup::Tasks() const {
ASSIGN_OR_RETURN_ERRNO(std::string buf, ReadControlFile("tasks"));
return ParsePIDList(buf);
}
PosixError Cgroup::PollControlFileForChange(absl::string_view name,
absl::Duration timeout) const {
return PollControlFileForChangeAfter(name, timeout, []() {});
}
PosixError Cgroup::PollControlFileForChangeAfter(
absl::string_view name, absl::Duration timeout,
std::function<void()> body) const {
const absl::Time deadline = absl::Now() + timeout;
const std::string alias_path = absl::StrFormat("[cg#%d]/%s", id_, name);
ASSIGN_OR_RETURN_ERRNO(const int64_t initial_value,
ReadIntegerControlFile(name));
body();
// The loop below iterates quickly and results in too many save-restore
// cycles. This can prevent tasks from being scheduled and incurring the
// resource usage this function is often waiting for, resulting in timeouts.
const DisableSave ds;
std::cerr << absl::StreamFormat(
"Waiting for control file '%s' to change from '%d'...",
alias_path, initial_value)
<< std::endl;
while (true) {
ASSIGN_OR_RETURN_ERRNO(const int64_t current_value,
ReadIntegerControlFile(name));
if (current_value != initial_value) {
std::cerr << absl::StreamFormat(
"Control file '%s' changed from '%d' to '%d'",
alias_path, initial_value, current_value)
<< std::endl;
return NoError();
}
if (absl::Now() >= deadline) {
return PosixError(ETIME, absl::StrCat(alias_path, " didn't change in ",
absl::FormatDuration(timeout)));
}
}
}
PosixError Cgroup::ContainsCallingProcess() const {
ASSIGN_OR_RETURN_ERRNO(const absl::flat_hash_set<pid_t> procs, Procs());
ASSIGN_OR_RETURN_ERRNO(const absl::flat_hash_set<pid_t> tasks, Tasks());
const pid_t pid = getpid();
const pid_t tid = syscall(SYS_gettid);
if (!procs.contains(pid)) {
return PosixError(
ENOENT, absl::StrFormat("Cgroup doesn't contain process %d", pid));
}
if (!tasks.contains(tid)) {
return PosixError(ENOENT,
absl::StrFormat("Cgroup doesn't contain task %d", tid));
}
return NoError();
}
PosixError Cgroup::ContainsCallingThread() const {
ASSIGN_OR_RETURN_ERRNO(const absl::flat_hash_set<pid_t> tasks, Tasks());
const pid_t tid = syscall(SYS_gettid);
if (!tasks.contains(tid)) {
return PosixError(ENOENT,
absl::StrFormat("Cgroup doesn't contain task %d", tid));
}
return NoError();
}
PosixError Cgroup::Enter(pid_t pid) const {
return WriteIntegerControlFile("cgroup.procs", static_cast<int64_t>(pid));
}
PosixError Cgroup::EnterThread(pid_t pid) const {
return WriteIntegerControlFile("tasks", static_cast<int64_t>(pid));
}
PosixErrorOr<absl::flat_hash_set<pid_t>> Cgroup::ParsePIDList(
absl::string_view data) const {
absl::flat_hash_set<pid_t> res;
std::vector<absl::string_view> lines = absl::StrSplit(data, '\n');
for (const absl::string_view& line : lines) {
if (line.empty()) {
continue;
}
ASSIGN_OR_RETURN_ERRNO(const int32_t pid, Atoi<int32_t>(line));
res.insert(static_cast<pid_t>(pid));
}
return res;
}
int64_t Cgroup::next_id_ = 0;
PosixErrorOr<Cgroup> Mounter::MountCgroupfs(std::string mopts) {
ASSIGN_OR_RETURN_ERRNO(TempPath mountpoint,
TempPath::CreateDirIn(root_.path()));
ASSIGN_OR_RETURN_ERRNO(
Cleanup mount, Mount("none", mountpoint.path(), "cgroup", 0, mopts, 0));
const std::string mountpath = mountpoint.path();
std::cerr << absl::StreamFormat(
"Mount(\"none\", \"%s\", \"cgroup\", 0, \"%s\", 0) => OK",
mountpath, mopts)
<< std::endl;
Cgroup cg = Cgroup::RootCgroup(mountpath);
mountpoints_[cg.id()] = std::move(mountpoint);
mounts_[cg.id()] = std::move(mount);
return cg;
}
PosixErrorOr<Cgroup> Mounter::MountCgroup2fs(std::string mopts) {
ASSIGN_OR_RETURN_ERRNO(TempPath mountpoint,
TempPath::CreateDirIn(root_.path()));
ASSIGN_OR_RETURN_ERRNO(
Cleanup mount, Mount("none", mountpoint.path(), "cgroup2", 0, mopts, 0));
const std::string mountpath = mountpoint.path();
std::cerr << absl::StreamFormat(
"Mount(\"none\", \"%s\", \"cgroup2\", 0, \"%s\", 0) => OK",
mountpath, mopts)
<< std::endl;
Cgroup cg = Cgroup::RootCgroup(mountpath);
mountpoints_[cg.id()] = std::move(mountpoint);
mounts_[cg.id()] = std::move(mount);
return cg;
}
PosixError Mounter::Unmount(const Cgroup& c) {
auto mount = mounts_.find(c.id());
auto mountpoint = mountpoints_.find(c.id());
if (mount == mounts_.end() || mountpoint == mountpoints_.end()) {
return PosixError(
ESRCH, absl::StrFormat("No mount found for cgroupfs containing cg#%d",
c.id()));
}
std::cerr << absl::StreamFormat("Unmount([cg#%d])", c.id()) << std::endl;
// Simply delete the entries, their destructors will unmount and delete the
// mountpoint. Note the order is important to avoid errors: mount then
// mountpoint.
mounts_.erase(mount);
mountpoints_.erase(mountpoint);
return NoError();
}
void Mounter::release(const Cgroup& c) {
auto mp = mountpoints_.find(c.id());
if (mp != mountpoints_.end()) {
mp->second.release();
mountpoints_.erase(mp);
}
auto m = mounts_.find(c.id());
if (m != mounts_.end()) {
m->second.Release();
mounts_.erase(m);
}
}
constexpr char kProcCgroupsHeader[] =
"#subsys_name\thierarchy\tnum_cgroups\tenabled";
PosixErrorOr<absl::flat_hash_map<std::string, CgroupsEntry>>
ProcCgroupsEntries() {
std::string content;
RETURN_IF_ERRNO(GetContents("/proc/cgroups", &content));
bool found_header = false;
absl::flat_hash_map<std::string, CgroupsEntry> entries;
std::vector<std::string> lines = absl::StrSplit(content, '\n');
std::cerr << "<contents of /proc/cgroups>" << std::endl;
for (const std::string& line : lines) {
std::cerr << line << std::endl;
if (!found_header) {
EXPECT_EQ(line, kProcCgroupsHeader);
found_header = true;
continue;
}
if (line.empty()) {
continue;
}
// Parse a single entry from /proc/cgroups.
//
// Example entries, fields are tab separated in the real file:
//
// #subsys_name hierarchy num_cgroups enabled
// cpuset 12 35 1
// cpu 3 222 1
// ^ ^ ^ ^
// 0 1 2 3
CgroupsEntry entry;
std::vector<std::string> fields =
StrSplit(line, absl::ByAnyChar(": \t"), absl::SkipEmpty());
entry.subsys_name = fields[0];
ASSIGN_OR_RETURN_ERRNO(entry.hierarchy, Atoi<uint32_t>(fields[1]));
ASSIGN_OR_RETURN_ERRNO(entry.num_cgroups, Atoi<uint64_t>(fields[2]));
ASSIGN_OR_RETURN_ERRNO(const int enabled, Atoi<int>(fields[3]));
entry.enabled = enabled != 0;
entries[entry.subsys_name] = entry;
}
std::cerr << "<end of /proc/cgroups>" << std::endl;
return entries;
}
PosixErrorOr<absl::flat_hash_map<std::string, PIDCgroupEntry>>
ProcPIDCgroupEntries(pid_t pid) {
const std::string path = absl::StrFormat("/proc/%d/cgroup", pid);
std::string content;
RETURN_IF_ERRNO(GetContents(path, &content));
absl::flat_hash_map<std::string, PIDCgroupEntry> entries;
std::vector<std::string> lines = absl::StrSplit(content, '\n');
std::cerr << absl::StreamFormat("<contents of %s>", path) << std::endl;
for (const std::string& line : lines) {
std::cerr << line << std::endl;
if (line.empty()) {
continue;
}
// Parse a single entry from /proc/<pid>/cgroup.
//
// Example entries:
//
// 2:cpu:/path/to/cgroup
// 1:memory:/
PIDCgroupEntry entry;
std::vector<std::string> fields = absl::StrSplit(line, absl::ByChar(':'));
ASSIGN_OR_RETURN_ERRNO(entry.hierarchy, Atoi<uint32_t>(fields[0]));
entry.controllers = fields[1];
entry.path = fields[2];
entries[entry.controllers] = entry;
}
std::cerr << absl::StreamFormat("<end of %s>", path) << std::endl;
return entries;
}
} // namespace testing
} // namespace gvisor