Skip to content

Commit 2246207

Browse files
committed
add exec-cpu-affinity
Signed-off-by: Yusuke Sakurai <[email protected]>
1 parent bf246dd commit 2246207

File tree

12 files changed

+495
-3
lines changed

12 files changed

+495
-3
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/libcontainer/src/container/tenant_builder.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,12 @@ impl TenantContainerBuilder {
447447
process_builder = process_builder.cwd(cwd);
448448
}
449449

450+
if let Some(process) = spec.process() {
451+
if let Some(cpu_affinity) = process.exec_cpu_affinity() {
452+
process_builder = process_builder.exec_cpu_affinity(cpu_affinity.clone());
453+
}
454+
}
455+
450456
if let Some(no_new_priv) = self.get_no_new_privileges() {
451457
process_builder = process_builder.no_new_privileges(no_new_priv);
452458
}

crates/libcontainer/src/process/container_intermediate_process.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use super::container_init_process::container_init_process;
1111
use super::fork::CloneCb;
1212
use crate::error::MissingSpecError;
1313
use crate::namespaces::Namespaces;
14-
use crate::process::{channel, fork};
14+
use crate::process::{channel, cpu_affinity, fork};
1515

1616
#[derive(Debug, thiserror::Error)]
1717
pub enum IntermediateProcessError {
@@ -31,6 +31,8 @@ pub enum IntermediateProcessError {
3131
ExecNotify(#[source] nix::Error),
3232
#[error(transparent)]
3333
MissingSpec(#[from] crate::error::MissingSpecError),
34+
#[error("CPU affinity error {0}")]
35+
CpuAffinity(#[from] cpu_affinity::CPUAffinityError),
3436
#[error("other error")]
3537
Other(String),
3638
}
@@ -52,6 +54,21 @@ pub fn container_intermediate_process(
5254
let cgroup_manager = libcgroups::common::create_cgroup_manager(args.cgroup_config.to_owned())
5355
.map_err(|e| IntermediateProcessError::Cgroup(e.to_string()))?;
5456

57+
let current_pid = Pid::this();
58+
// setting CPU affinity for tenant container before cgroup move
59+
if matches!(args.container_type, ContainerType::TenantContainer { .. }) {
60+
if let Some(exec_cpu_affinity) = spec
61+
.process()
62+
.as_ref()
63+
.and_then(|p| p.exec_cpu_affinity().as_ref())
64+
{
65+
if let Some(initial) = exec_cpu_affinity.initial() {
66+
cpu_affinity::set_cpuset_affinity_from_string(current_pid, initial)?;
67+
}
68+
}
69+
}
70+
let _ = cpu_affinity::log_cpu_affinity();
71+
5572
// this needs to be done before we create the init process, so that the init
5673
// process will already be captured by the cgroup. It also needs to be done
5774
// before we enter the user namespace because if a privileged user starts a
@@ -68,6 +85,19 @@ pub fn container_intermediate_process(
6885
matches!(args.container_type, ContainerType::InitContainer),
6986
)?;
7087

88+
// setting CPU affinity for tenant container after cgroup move
89+
if matches!(args.container_type, ContainerType::TenantContainer { .. }) {
90+
if let Some(exec_cpu_affinity) = spec
91+
.process()
92+
.as_ref()
93+
.and_then(|p| p.exec_cpu_affinity().as_ref())
94+
{
95+
if let Some(cpu_affinity_final) = exec_cpu_affinity.cpu_affinity_final() {
96+
cpu_affinity::set_cpuset_affinity_from_string(current_pid, cpu_affinity_final)?;
97+
}
98+
}
99+
}
100+
71101
// if new user is specified in specification, this will be true and new
72102
// namespace will be created, check
73103
// https://man7.org/linux/man-pages/man7/user_namespaces.7.html for more
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
use nix::sched::{sched_getaffinity, sched_setaffinity, CpuSet};
2+
use nix::unistd::Pid;
3+
4+
#[derive(Debug, thiserror::Error)]
5+
pub enum CPUAffinityError {
6+
#[error("invalid CPU string: {0}")]
7+
ParseError(String),
8+
#[error("values larger than {max} are not supported")]
9+
CpuOutOfRange { cpu: usize, max: usize },
10+
#[error("failed to set CPU for CPU {cpu}: {source}")]
11+
CpuSet {
12+
cpu: usize,
13+
#[source]
14+
source: nix::Error,
15+
},
16+
#[error("failed to setaffinity")]
17+
SetAffinity(#[source] nix::Error),
18+
#[error("failed to getaffinity")]
19+
GetAffinity(#[source] nix::Error),
20+
}
21+
22+
type Result<T> = std::result::Result<T, CPUAffinityError>;
23+
24+
pub fn to_cpuset(cpuset_str: &str) -> Result<CpuSet> {
25+
let mut cpuset = CpuSet::new();
26+
let max_cpu = CpuSet::count();
27+
28+
for part in cpuset_str
29+
.trim()
30+
.split(',')
31+
.map(str::trim)
32+
.filter(|s| !s.is_empty())
33+
{
34+
match part.split_once('-') {
35+
Some((start_str, end_str)) => {
36+
let start = parse_cpu_index(start_str, max_cpu)?;
37+
let end = parse_cpu_index(end_str, max_cpu)?;
38+
if start > end {
39+
return Err(CPUAffinityError::ParseError(format!(
40+
"invalid range: {}-{}",
41+
start, end
42+
)));
43+
}
44+
for cpu in start..=end {
45+
cpuset
46+
.set(cpu)
47+
.map_err(|e| CPUAffinityError::CpuSet { cpu, source: e })?;
48+
}
49+
}
50+
None => {
51+
let cpu = parse_cpu_index(part, max_cpu)?;
52+
cpuset
53+
.set(cpu)
54+
.map_err(|e| CPUAffinityError::CpuSet { cpu, source: e })?;
55+
}
56+
}
57+
}
58+
Ok(cpuset)
59+
}
60+
61+
fn parse_cpu_index(s: &str, max_cpu: usize) -> Result<usize> {
62+
let cpu: usize = s
63+
.parse()
64+
.map_err(|_| CPUAffinityError::ParseError(s.to_string()))?;
65+
if cpu >= max_cpu {
66+
return Err(CPUAffinityError::CpuOutOfRange {
67+
cpu,
68+
max: max_cpu - 1,
69+
});
70+
}
71+
Ok(cpu)
72+
}
73+
74+
pub fn set_cpuset_affinity_from_string(pid: Pid, cpuset_str: &str) -> Result<()> {
75+
tracing::debug!(?cpuset_str, "setting CPU affinity for tenant container");
76+
sched_setaffinity(pid, &to_cpuset(cpuset_str)?).map_err(CPUAffinityError::SetAffinity)
77+
}
78+
79+
pub fn log_cpu_affinity() -> Result<()> {
80+
let cpuset = sched_getaffinity(Pid::this()).map_err(CPUAffinityError::GetAffinity)?;
81+
let mask = (0..usize::BITS as usize)
82+
.filter(|&i| cpuset.is_set(i).unwrap_or(false))
83+
.fold(0, |mask, i| mask | (1 << i));
84+
tracing::debug!("affinity: 0x{:x}", mask);
85+
Ok(())
86+
}
87+
88+
#[cfg(test)]
89+
mod tests {
90+
use super::*;
91+
92+
#[test]
93+
fn test_to_cpuset_single_values() {
94+
let cpuset = to_cpuset("0,1,2").unwrap();
95+
for cpu in [0, 1, 2] {
96+
assert!(cpuset.is_set(cpu).unwrap());
97+
}
98+
}
99+
100+
#[test]
101+
fn test_to_cpuset_range() {
102+
let cpuset = to_cpuset("3-5").unwrap();
103+
for cpu in [3, 4, 5] {
104+
assert!(cpuset.is_set(cpu).unwrap());
105+
}
106+
}
107+
108+
#[test]
109+
fn test_to_cpuset_mixed() {
110+
let cpuset = to_cpuset("0, 2-4, 6").unwrap();
111+
for cpu in [0, 2, 3, 4, 6] {
112+
assert!(cpuset.is_set(cpu).unwrap());
113+
}
114+
for cpu in [1, 5, 7] {
115+
assert!(!cpuset.is_set(cpu).unwrap_or(false));
116+
}
117+
}
118+
119+
#[test]
120+
fn test_to_cpuset_spaces_and_empty() {
121+
let cpuset = to_cpuset(" , 1 , 3 , 5-7 , ").unwrap();
122+
for cpu in [1, 3, 5, 6, 7] {
123+
assert!(cpuset.is_set(cpu).unwrap());
124+
}
125+
}
126+
127+
#[test]
128+
fn test_to_cpuset_invalid_range() {
129+
let err = to_cpuset("5-3").unwrap_err();
130+
matches!(err, CPUAffinityError::ParseError(_));
131+
}
132+
133+
#[test]
134+
fn test_to_cpuset_invalid_value() {
135+
let err = to_cpuset("a,b,c").unwrap_err();
136+
matches!(err, CPUAffinityError::ParseError(_));
137+
}
138+
139+
#[test]
140+
fn test_to_cpuset_max_allowed_cpu() {
141+
let max = CpuSet::count();
142+
let highest = max - 1;
143+
let cpuset = to_cpuset(&highest.to_string()).unwrap();
144+
assert!(cpuset.is_set(highest).unwrap());
145+
}
146+
147+
#[test]
148+
fn test_to_cpuset_exceeds_max_cpu() {
149+
let max = CpuSet::count();
150+
let result = to_cpuset(&max.to_string());
151+
assert!(matches!(
152+
result,
153+
Err(CPUAffinityError::CpuOutOfRange { .. })
154+
));
155+
}
156+
}

crates/libcontainer/src/process/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ pub mod channel;
66
pub mod container_init_process;
77
pub mod container_intermediate_process;
88
pub mod container_main_process;
9+
pub mod cpu_affinity;
910
mod fork;
1011
pub mod intel_rdt;
1112
mod message;

tests/contest/contest/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ tempfile = "3"
2525
scopeguard = "1.2.0"
2626
tracing = { version = "0.1.41", features = ["attributes"]}
2727
tracing-subscriber = { version = "0.3.19", features = ["json", "env-filter"] }
28+
regex = "1"
2829

2930
[dependencies.clap]
3031
version = "4.1.6"

tests/contest/contest/src/main.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use crate::tests::delete::get_delete_test;
1313
use crate::tests::devices::get_devices_test;
1414
use crate::tests::domainname::get_domainname_tests;
1515
use crate::tests::example::get_example_test;
16+
use crate::tests::exec_cpu_affinity::get_exec_cpu_affinity_test;
1617
use crate::tests::fd_control::get_fd_control_test;
1718
use crate::tests::hooks::get_hooks_tests;
1819
use crate::tests::hostname::get_hostname_test;
@@ -136,6 +137,7 @@ fn main() -> Result<()> {
136137
let fd_control = get_fd_control_test();
137138
let masked_paths = get_linux_masked_paths_tests();
138139
let rootfs_propagation = get_rootfs_propagation_test();
140+
let exec_cpu_affinity = get_exec_cpu_affinity_test();
139141

140142
tm.add_test_group(Box::new(cl));
141143
tm.add_test_group(Box::new(cc));
@@ -171,6 +173,7 @@ fn main() -> Result<()> {
171173
tm.add_test_group(Box::new(process_oom_score_adj));
172174
tm.add_test_group(Box::new(fd_control));
173175
tm.add_test_group(Box::new(rootfs_propagation));
176+
tm.add_test_group(Box::new(exec_cpu_affinity));
174177

175178
tm.add_test_group(Box::new(io_priority_test));
176179
tm.add_cleanup(Box::new(cgroups::cleanup_v1));

0 commit comments

Comments
 (0)