• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
get_num_cpus() -> usize32 pub fn get_num_cpus() -> usize {
33     match cgroups_num_cpus() {
34         Some(n) => n,
35         None => logical_cpus(),
36     }
37 }
38 
logical_cpus() -> usize39 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 
get_num_physical_cpus() -> usize59 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 
cgroups_num_cpus() -> Option<usize>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 
init_cgroups()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 
load_cgroups<P1, P2>(cgroup_proc: P1, mountinfo_proc: P2) -> Option<usize> where P1: AsRef<Path>, P2: AsRef<Path>,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 {
new(version: CgroupVersion, dir: PathBuf) -> Cgroup180     fn new(version: CgroupVersion, dir: PathBuf) -> Cgroup {
181         Cgroup { version: version, base: dir }
182     }
183 
translate(mntinfo: MountInfo, subsys: Subsys) -> Option<Cgroup>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 
cpu_quota(&self) -> Option<usize>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 
quota_us(&self) -> Option<usize>218     fn quota_us(&self) -> Option<usize> {
219         self.param("cpu.cfs_quota_us")
220     }
221 
period_us(&self) -> Option<usize>222     fn period_us(&self) -> Option<usize> {
223         self.param("cpu.cfs_period_us")
224     }
225 
max(&self) -> Option<(usize, usize)>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 
param(&self, param: &str) -> Option<usize>236     fn param(&self, param: &str) -> Option<usize> {
237         let buf = some!(self.raw_param(param));
238 
239         buf.trim().parse().ok()
240     }
241 
raw_param(&self, param: &str) -> Option<String>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 {
load_cpu<P: AsRef<Path>>(proc_path: P, version: CgroupVersion) -> Option<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 
parse_line(line: String) -> Option<MountInfo>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 {
load_cpu<P: AsRef<Path>>(proc_path: P) -> Option<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 
parse_line(line: String) -> Option<Subsys>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]
test_load_mountinfo()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]
test_load_subsys()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]
test_cgroup_mount()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]
test_cgroup_cpu_quota()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]
test_cgroup_cpu_quota_divide_by_zero()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]
test_cgroup_cpu_quota_ceil()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]
test_load_mountinfo()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]
test_load_subsys()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]
test_load_subsys_multi()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]
test_cgroup_mount()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]
test_cgroup_cpu_quota()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]
test_cgroup_cpu_quota_divide_by_zero()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]
test_cgroup_cpu_quota_ceil()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