1 | use std::collections::HashMap; |
2 | use std::fs::File; |
3 | use std::io::{BufRead, BufReader, Read}; |
4 | use std::mem; |
5 | use std::path::{Path, PathBuf}; |
6 | use std::sync::atomic::{AtomicUsize, Ordering}; |
7 | use std::sync::Once; |
8 | |
9 | use libc; |
10 | |
11 | macro_rules! debug { |
12 | ($($args:expr),*) => ({ |
13 | if false { |
14 | //if true { |
15 | println!($($args),*); |
16 | } |
17 | }); |
18 | } |
19 | |
20 | macro_rules! some { |
21 | ($e:expr) => {{ |
22 | match $e { |
23 | Some(v) => v, |
24 | None => { |
25 | debug!("NONE: {:?}" , stringify!($e)); |
26 | return None; |
27 | } |
28 | } |
29 | }}; |
30 | } |
31 | |
32 | pub fn get_num_cpus() -> usize { |
33 | match cgroups_num_cpus() { |
34 | Some(n) => n, |
35 | None => logical_cpus(), |
36 | } |
37 | } |
38 | |
39 | fn logical_cpus() -> usize { |
40 | let mut set: libc::cpu_set_t = unsafe { mem::zeroed() }; |
41 | if unsafe { libc::sched_getaffinity(0, mem::size_of::<libc::cpu_set_t>(), &mut set) } == 0 { |
42 | let mut count: u32 = 0; |
43 | for i in 0..libc::CPU_SETSIZE as usize { |
44 | if unsafe { libc::CPU_ISSET(i, &set) } { |
45 | count += 1 |
46 | } |
47 | } |
48 | count as usize |
49 | } else { |
50 | let cpus = unsafe { libc::sysconf(libc::_SC_NPROCESSORS_ONLN) }; |
51 | if cpus < 1 { |
52 | 1 |
53 | } else { |
54 | cpus as usize |
55 | } |
56 | } |
57 | } |
58 | |
59 | pub fn get_num_physical_cpus() -> usize { |
60 | let file = match File::open("/proc/cpuinfo" ) { |
61 | Ok(val) => val, |
62 | Err(_) => return get_num_cpus(), |
63 | }; |
64 | let reader = BufReader::new(file); |
65 | let mut map = HashMap::new(); |
66 | let mut physid: u32 = 0; |
67 | let mut cores: usize = 0; |
68 | let mut chgcount = 0; |
69 | for line in reader.lines().filter_map(|result| result.ok()) { |
70 | let mut it = line.split(':' ); |
71 | let (key, value) = match (it.next(), it.next()) { |
72 | (Some(key), Some(value)) => (key.trim(), value.trim()), |
73 | _ => continue, |
74 | }; |
75 | if key == "physical id" { |
76 | match value.parse() { |
77 | Ok(val) => physid = val, |
78 | Err(_) => break, |
79 | }; |
80 | chgcount += 1; |
81 | } |
82 | if key == "cpu cores" { |
83 | match value.parse() { |
84 | Ok(val) => cores = val, |
85 | Err(_) => break, |
86 | }; |
87 | chgcount += 1; |
88 | } |
89 | if chgcount == 2 { |
90 | map.insert(physid, cores); |
91 | chgcount = 0; |
92 | } |
93 | } |
94 | let count = map.into_iter().fold(0, |acc, (_, cores)| acc + cores); |
95 | |
96 | if count == 0 { |
97 | get_num_cpus() |
98 | } else { |
99 | count |
100 | } |
101 | } |
102 | |
103 | /// Cached CPUs calculated from cgroups. |
104 | /// |
105 | /// If 0, check logical cpus. |
106 | // Allow deprecation warnings, we want to work on older rustc |
107 | #[allow (warnings)] |
108 | static CGROUPS_CPUS: AtomicUsize = ::std::sync::atomic::ATOMIC_USIZE_INIT; |
109 | |
110 | fn cgroups_num_cpus() -> Option<usize> { |
111 | #[allow (warnings)] |
112 | static ONCE: Once = ::std::sync::ONCE_INIT; |
113 | |
114 | ONCE.call_once(init_cgroups); |
115 | |
116 | let cpus = CGROUPS_CPUS.load(Ordering::Acquire); |
117 | |
118 | if cpus > 0 { |
119 | Some(cpus) |
120 | } else { |
121 | None |
122 | } |
123 | } |
124 | |
125 | fn init_cgroups() { |
126 | // Should only be called once |
127 | debug_assert!(CGROUPS_CPUS.load(Ordering::SeqCst) == 0); |
128 | |
129 | // Fails in Miri by default (cannot open files), and Miri does not have parallelism anyway. |
130 | if cfg!(miri) { |
131 | return; |
132 | } |
133 | |
134 | if let Some(quota) = load_cgroups("/proc/self/cgroup" , "/proc/self/mountinfo" ) { |
135 | if quota == 0 { |
136 | return; |
137 | } |
138 | |
139 | let logical = logical_cpus(); |
140 | let count = ::std::cmp::min(quota, logical); |
141 | |
142 | CGROUPS_CPUS.store(count, Ordering::SeqCst); |
143 | } |
144 | } |
145 | |
146 | fn load_cgroups<P1, P2>(cgroup_proc: P1, mountinfo_proc: P2) -> Option<usize> |
147 | where |
148 | P1: AsRef<Path>, |
149 | P2: AsRef<Path>, |
150 | { |
151 | let subsys = some!(Subsys::load_cpu(cgroup_proc)); |
152 | let mntinfo = some!(MountInfo::load_cpu(mountinfo_proc, subsys.version)); |
153 | let cgroup = some!(Cgroup::translate(mntinfo, subsys)); |
154 | cgroup.cpu_quota() |
155 | } |
156 | |
157 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
158 | enum CgroupVersion { |
159 | V1, |
160 | V2, |
161 | } |
162 | |
163 | struct Cgroup { |
164 | version: CgroupVersion, |
165 | base: PathBuf, |
166 | } |
167 | |
168 | struct MountInfo { |
169 | version: CgroupVersion, |
170 | root: String, |
171 | mount_point: String, |
172 | } |
173 | |
174 | struct Subsys { |
175 | version: CgroupVersion, |
176 | base: String, |
177 | } |
178 | |
179 | impl Cgroup { |
180 | fn new(version: CgroupVersion, dir: PathBuf) -> Cgroup { |
181 | Cgroup { version: version, base: dir } |
182 | } |
183 | |
184 | fn translate(mntinfo: MountInfo, subsys: Subsys) -> Option<Cgroup> { |
185 | // Translate the subsystem directory via the host paths. |
186 | debug!( |
187 | "subsys = {:?}; root = {:?}; mount_point = {:?}" , |
188 | subsys.base, mntinfo.root, mntinfo.mount_point |
189 | ); |
190 | |
191 | let rel_from_root = some!(Path::new(&subsys.base).strip_prefix(&mntinfo.root).ok()); |
192 | |
193 | debug!("rel_from_root: {:?}" , rel_from_root); |
194 | |
195 | // join(mp.MountPoint, relPath) |
196 | let mut path = PathBuf::from(mntinfo.mount_point); |
197 | path.push(rel_from_root); |
198 | Some(Cgroup::new(mntinfo.version, path)) |
199 | } |
200 | |
201 | fn cpu_quota(&self) -> Option<usize> { |
202 | let (quota_us, period_us) = match self.version { |
203 | CgroupVersion::V1 => (some!(self.quota_us()), some!(self.period_us())), |
204 | CgroupVersion::V2 => some!(self.max()), |
205 | }; |
206 | |
207 | // protect against dividing by zero |
208 | if period_us == 0 { |
209 | return None; |
210 | } |
211 | |
212 | // Ceil the division, since we want to be able to saturate |
213 | // the available CPUs, and flooring would leave a CPU un-utilized. |
214 | |
215 | Some((quota_us as f64 / period_us as f64).ceil() as usize) |
216 | } |
217 | |
218 | fn quota_us(&self) -> Option<usize> { |
219 | self.param("cpu.cfs_quota_us" ) |
220 | } |
221 | |
222 | fn period_us(&self) -> Option<usize> { |
223 | self.param("cpu.cfs_period_us" ) |
224 | } |
225 | |
226 | fn max(&self) -> Option<(usize, usize)> { |
227 | let max = some!(self.raw_param("cpu.max" )); |
228 | let mut max = some!(max.lines().next()).split(' ' ); |
229 | |
230 | let quota = some!(max.next().and_then(|quota| quota.parse().ok())); |
231 | let period = some!(max.next().and_then(|period| period.parse().ok())); |
232 | |
233 | Some((quota, period)) |
234 | } |
235 | |
236 | fn param(&self, param: &str) -> Option<usize> { |
237 | let buf = some!(self.raw_param(param)); |
238 | |
239 | buf.trim().parse().ok() |
240 | } |
241 | |
242 | fn raw_param(&self, param: &str) -> Option<String> { |
243 | let mut file = some!(File::open(self.base.join(param)).ok()); |
244 | |
245 | let mut buf = String::new(); |
246 | some!(file.read_to_string(&mut buf).ok()); |
247 | |
248 | Some(buf) |
249 | } |
250 | } |
251 | |
252 | impl MountInfo { |
253 | fn load_cpu<P: AsRef<Path>>(proc_path: P, version: CgroupVersion) -> Option<MountInfo> { |
254 | let file = some!(File::open(proc_path).ok()); |
255 | let file = BufReader::new(file); |
256 | |
257 | file.lines() |
258 | .filter_map(|result| result.ok()) |
259 | .filter_map(MountInfo::parse_line) |
260 | .find(|mount_info| mount_info.version == version) |
261 | } |
262 | |
263 | fn parse_line(line: String) -> Option<MountInfo> { |
264 | let mut fields = line.split(' ' ); |
265 | |
266 | // 7 5 0:6 </> /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:7 - cgroup cgroup rw,cpu,cpuacct |
267 | let mnt_root = some!(fields.nth(3)); |
268 | // 7 5 0:6 / </sys/fs/cgroup/cpu,cpuacct> rw,nosuid,nodev,noexec,relatime shared:7 - cgroup cgroup rw,cpu,cpuacct |
269 | let mnt_point = some!(fields.next()); |
270 | |
271 | // Ignore all fields until the separator(-). |
272 | // Note: there could be zero or more optional fields before hyphen. |
273 | // See: https://man7.org/linux/man-pages/man5/proc.5.html |
274 | // 7 5 0:6 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:7 <-> cgroup cgroup rw,cpu,cpuacct |
275 | // Note: we cannot use `?` here because we need to support Rust 1.13. |
276 | match fields.find(|&s| s == "-" ) { |
277 | Some(_) => {} |
278 | None => return None, |
279 | }; |
280 | |
281 | // 7 5 0:6 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:7 - <cgroup> cgroup rw,cpu,cpuacct |
282 | let version = match fields.next() { |
283 | Some("cgroup" ) => CgroupVersion::V1, |
284 | Some("cgroup2" ) => CgroupVersion::V2, |
285 | _ => return None, |
286 | }; |
287 | |
288 | // cgroups2 only has a single mount point |
289 | if version == CgroupVersion::V1 { |
290 | // 7 5 0:6 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:7 - cgroup cgroup <rw,cpu,cpuacct> |
291 | let super_opts = some!(fields.nth(1)); |
292 | |
293 | // We only care about the 'cpu' option |
294 | if !super_opts.split(',' ).any(|opt| opt == "cpu" ) { |
295 | return None; |
296 | } |
297 | } |
298 | |
299 | Some(MountInfo { |
300 | version: version, |
301 | root: mnt_root.to_owned(), |
302 | mount_point: mnt_point.to_owned(), |
303 | }) |
304 | } |
305 | } |
306 | |
307 | impl Subsys { |
308 | fn load_cpu<P: AsRef<Path>>(proc_path: P) -> Option<Subsys> { |
309 | let file = some!(File::open(proc_path).ok()); |
310 | let file = BufReader::new(file); |
311 | |
312 | file.lines() |
313 | .filter_map(|result| result.ok()) |
314 | .filter_map(Subsys::parse_line) |
315 | .fold(None, |previous, line| { |
316 | // already-found v1 trumps v2 since it explicitly specifies its controllers |
317 | if previous.is_some() && line.version == CgroupVersion::V2 { |
318 | return previous; |
319 | } |
320 | |
321 | Some(line) |
322 | }) |
323 | } |
324 | |
325 | fn parse_line(line: String) -> Option<Subsys> { |
326 | // Example format: |
327 | // 11:cpu,cpuacct:/ |
328 | let mut fields = line.split(':' ); |
329 | |
330 | let sub_systems = some!(fields.nth(1)); |
331 | |
332 | let version = if sub_systems.is_empty() { |
333 | CgroupVersion::V2 |
334 | } else { |
335 | CgroupVersion::V1 |
336 | }; |
337 | |
338 | if version == CgroupVersion::V1 && !sub_systems.split(',' ).any(|sub| sub == "cpu" ) { |
339 | return None; |
340 | } |
341 | |
342 | fields.next().map(|path| Subsys { |
343 | version: version, |
344 | base: path.to_owned(), |
345 | }) |
346 | } |
347 | } |
348 | |
349 | #[cfg (test)] |
350 | mod tests { |
351 | mod v1 { |
352 | use super::super::{Cgroup, CgroupVersion, MountInfo, Subsys}; |
353 | use std::path::{Path, PathBuf}; |
354 | |
355 | // `static_in_const` feature is not stable in Rust 1.13. |
356 | static FIXTURES_PROC: &'static str = "fixtures/cgroups/proc/cgroups" ; |
357 | |
358 | static FIXTURES_CGROUPS: &'static str = "fixtures/cgroups/cgroups" ; |
359 | |
360 | macro_rules! join { |
361 | ($base:expr, $($path:expr),+) => ({ |
362 | Path::new($base) |
363 | $(.join($path))+ |
364 | }) |
365 | } |
366 | |
367 | #[test] |
368 | fn test_load_mountinfo() { |
369 | // test only one optional fields |
370 | let path = join!(FIXTURES_PROC, "mountinfo" ); |
371 | |
372 | let mnt_info = MountInfo::load_cpu(path, CgroupVersion::V1).unwrap(); |
373 | |
374 | assert_eq!(mnt_info.root, "/" ); |
375 | assert_eq!(mnt_info.mount_point, "/sys/fs/cgroup/cpu,cpuacct" ); |
376 | |
377 | // test zero optional field |
378 | let path = join!(FIXTURES_PROC, "mountinfo_zero_opt" ); |
379 | |
380 | let mnt_info = MountInfo::load_cpu(path, CgroupVersion::V1).unwrap(); |
381 | |
382 | assert_eq!(mnt_info.root, "/" ); |
383 | assert_eq!(mnt_info.mount_point, "/sys/fs/cgroup/cpu,cpuacct" ); |
384 | |
385 | // test multi optional fields |
386 | let path = join!(FIXTURES_PROC, "mountinfo_multi_opt" ); |
387 | |
388 | let mnt_info = MountInfo::load_cpu(path, CgroupVersion::V1).unwrap(); |
389 | |
390 | assert_eq!(mnt_info.root, "/" ); |
391 | assert_eq!(mnt_info.mount_point, "/sys/fs/cgroup/cpu,cpuacct" ); |
392 | } |
393 | |
394 | #[test] |
395 | fn test_load_subsys() { |
396 | let path = join!(FIXTURES_PROC, "cgroup" ); |
397 | |
398 | let subsys = Subsys::load_cpu(path).unwrap(); |
399 | |
400 | assert_eq!(subsys.base, "/" ); |
401 | assert_eq!(subsys.version, CgroupVersion::V1); |
402 | } |
403 | |
404 | #[test] |
405 | fn test_cgroup_mount() { |
406 | let cases = &[ |
407 | ("/" , "/sys/fs/cgroup/cpu" , "/" , Some("/sys/fs/cgroup/cpu" )), |
408 | ( |
409 | "/docker/01abcd" , |
410 | "/sys/fs/cgroup/cpu" , |
411 | "/docker/01abcd" , |
412 | Some("/sys/fs/cgroup/cpu" ), |
413 | ), |
414 | ( |
415 | "/docker/01abcd" , |
416 | "/sys/fs/cgroup/cpu" , |
417 | "/docker/01abcd/" , |
418 | Some("/sys/fs/cgroup/cpu" ), |
419 | ), |
420 | ( |
421 | "/docker/01abcd" , |
422 | "/sys/fs/cgroup/cpu" , |
423 | "/docker/01abcd/large" , |
424 | Some("/sys/fs/cgroup/cpu/large" ), |
425 | ), |
426 | // fails |
427 | ("/docker/01abcd" , "/sys/fs/cgroup/cpu" , "/" , None), |
428 | ("/docker/01abcd" , "/sys/fs/cgroup/cpu" , "/docker" , None), |
429 | ("/docker/01abcd" , "/sys/fs/cgroup/cpu" , "/elsewhere" , None), |
430 | ( |
431 | "/docker/01abcd" , |
432 | "/sys/fs/cgroup/cpu" , |
433 | "/docker/01abcd-other-dir" , |
434 | None, |
435 | ), |
436 | ]; |
437 | |
438 | for &(root, mount_point, subsys, expected) in cases.iter() { |
439 | let mnt_info = MountInfo { |
440 | version: CgroupVersion::V1, |
441 | root: root.into(), |
442 | mount_point: mount_point.into(), |
443 | }; |
444 | let subsys = Subsys { |
445 | version: CgroupVersion::V1, |
446 | base: subsys.into(), |
447 | }; |
448 | |
449 | let actual = Cgroup::translate(mnt_info, subsys).map(|c| c.base); |
450 | let expected = expected.map(PathBuf::from); |
451 | assert_eq!(actual, expected); |
452 | } |
453 | } |
454 | |
455 | #[test] |
456 | fn test_cgroup_cpu_quota() { |
457 | let cgroup = Cgroup::new(CgroupVersion::V1, join!(FIXTURES_CGROUPS, "good" )); |
458 | assert_eq!(cgroup.cpu_quota(), Some(6)); |
459 | } |
460 | |
461 | #[test] |
462 | fn test_cgroup_cpu_quota_divide_by_zero() { |
463 | let cgroup = Cgroup::new(CgroupVersion::V1, join!(FIXTURES_CGROUPS, "zero-period" )); |
464 | assert!(cgroup.quota_us().is_some()); |
465 | assert_eq!(cgroup.period_us(), Some(0)); |
466 | assert_eq!(cgroup.cpu_quota(), None); |
467 | } |
468 | |
469 | #[test] |
470 | fn test_cgroup_cpu_quota_ceil() { |
471 | let cgroup = Cgroup::new(CgroupVersion::V1, join!(FIXTURES_CGROUPS, "ceil" )); |
472 | assert_eq!(cgroup.cpu_quota(), Some(2)); |
473 | } |
474 | } |
475 | |
476 | mod v2 { |
477 | use super::super::{Cgroup, CgroupVersion, MountInfo, Subsys}; |
478 | use std::path::{Path, PathBuf}; |
479 | |
480 | // `static_in_const` feature is not stable in Rust 1.13. |
481 | static FIXTURES_PROC: &'static str = "fixtures/cgroups2/proc/cgroups" ; |
482 | |
483 | static FIXTURES_CGROUPS: &'static str = "fixtures/cgroups2/cgroups" ; |
484 | |
485 | macro_rules! join { |
486 | ($base:expr, $($path:expr),+) => ({ |
487 | Path::new($base) |
488 | $(.join($path))+ |
489 | }) |
490 | } |
491 | |
492 | #[test] |
493 | fn test_load_mountinfo() { |
494 | // test only one optional fields |
495 | let path = join!(FIXTURES_PROC, "mountinfo" ); |
496 | |
497 | let mnt_info = MountInfo::load_cpu(path, CgroupVersion::V2).unwrap(); |
498 | |
499 | assert_eq!(mnt_info.root, "/" ); |
500 | assert_eq!(mnt_info.mount_point, "/sys/fs/cgroup" ); |
501 | } |
502 | |
503 | #[test] |
504 | fn test_load_subsys() { |
505 | let path = join!(FIXTURES_PROC, "cgroup" ); |
506 | |
507 | let subsys = Subsys::load_cpu(path).unwrap(); |
508 | |
509 | assert_eq!(subsys.base, "/" ); |
510 | assert_eq!(subsys.version, CgroupVersion::V2); |
511 | } |
512 | |
513 | #[test] |
514 | fn test_load_subsys_multi() { |
515 | let path = join!(FIXTURES_PROC, "cgroup_multi" ); |
516 | |
517 | let subsys = Subsys::load_cpu(path).unwrap(); |
518 | |
519 | assert_eq!(subsys.base, "/" ); |
520 | assert_eq!(subsys.version, CgroupVersion::V1); |
521 | } |
522 | |
523 | #[test] |
524 | fn test_cgroup_mount() { |
525 | let cases = &[ |
526 | ("/" , "/sys/fs/cgroup/cpu" , "/" , Some("/sys/fs/cgroup/cpu" )), |
527 | ( |
528 | "/docker/01abcd" , |
529 | "/sys/fs/cgroup/cpu" , |
530 | "/docker/01abcd" , |
531 | Some("/sys/fs/cgroup/cpu" ), |
532 | ), |
533 | ( |
534 | "/docker/01abcd" , |
535 | "/sys/fs/cgroup/cpu" , |
536 | "/docker/01abcd/" , |
537 | Some("/sys/fs/cgroup/cpu" ), |
538 | ), |
539 | ( |
540 | "/docker/01abcd" , |
541 | "/sys/fs/cgroup/cpu" , |
542 | "/docker/01abcd/large" , |
543 | Some("/sys/fs/cgroup/cpu/large" ), |
544 | ), |
545 | // fails |
546 | ("/docker/01abcd" , "/sys/fs/cgroup/cpu" , "/" , None), |
547 | ("/docker/01abcd" , "/sys/fs/cgroup/cpu" , "/docker" , None), |
548 | ("/docker/01abcd" , "/sys/fs/cgroup/cpu" , "/elsewhere" , None), |
549 | ( |
550 | "/docker/01abcd" , |
551 | "/sys/fs/cgroup/cpu" , |
552 | "/docker/01abcd-other-dir" , |
553 | None, |
554 | ), |
555 | ]; |
556 | |
557 | for &(root, mount_point, subsys, expected) in cases.iter() { |
558 | let mnt_info = MountInfo { |
559 | version: CgroupVersion::V1, |
560 | root: root.into(), |
561 | mount_point: mount_point.into(), |
562 | }; |
563 | let subsys = Subsys { |
564 | version: CgroupVersion::V1, |
565 | base: subsys.into(), |
566 | }; |
567 | |
568 | let actual = Cgroup::translate(mnt_info, subsys).map(|c| c.base); |
569 | let expected = expected.map(PathBuf::from); |
570 | assert_eq!(actual, expected); |
571 | } |
572 | } |
573 | |
574 | #[test] |
575 | fn test_cgroup_cpu_quota() { |
576 | let cgroup = Cgroup::new(CgroupVersion::V2, join!(FIXTURES_CGROUPS, "good" )); |
577 | assert_eq!(cgroup.cpu_quota(), Some(6)); |
578 | } |
579 | |
580 | #[test] |
581 | fn test_cgroup_cpu_quota_divide_by_zero() { |
582 | let cgroup = Cgroup::new(CgroupVersion::V2, join!(FIXTURES_CGROUPS, "zero-period" )); |
583 | let period = cgroup.max().map(|max| max.1); |
584 | |
585 | assert_eq!(period, Some(0)); |
586 | assert_eq!(cgroup.cpu_quota(), None); |
587 | } |
588 | |
589 | #[test] |
590 | fn test_cgroup_cpu_quota_ceil() { |
591 | let cgroup = Cgroup::new(CgroupVersion::V2, join!(FIXTURES_CGROUPS, "ceil" )); |
592 | assert_eq!(cgroup.cpu_quota(), Some(2)); |
593 | } |
594 | } |
595 | } |
596 | |