num_cpus/
linux.rs

1use std::collections::HashMap;
2use std::fs::File;
3use std::io::{BufRead, BufReader, Read};
4use std::mem;
5use std::path::{Path, PathBuf};
6use std::sync::atomic::{AtomicUsize, Ordering};
7use std::sync::Once;
8
9use libc;
10
11macro_rules! debug {
12    ($($args:expr),*) => ({
13        if false {
14        //if true {
15            println!($($args),*);
16        }
17    });
18}
19
20macro_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
32pub fn get_num_cpus() -> usize {
33    match cgroups_num_cpus() {
34        Some(n) => n,
35        None => logical_cpus(),
36    }
37}
38
39fn 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
59pub 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)]
108static CGROUPS_CPUS: AtomicUsize = ::std::sync::atomic::ATOMIC_USIZE_INIT;
109
110fn 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
125fn 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
146fn load_cgroups<P1, P2>(cgroup_proc: P1, mountinfo_proc: P2) -> Option<usize>
147where
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)]
158enum CgroupVersion {
159    V1,
160    V2,
161}
162
163struct Cgroup {
164    version: CgroupVersion,
165    base: PathBuf,
166}
167
168struct MountInfo {
169    version: CgroupVersion,
170    root: String,
171    mount_point: String,
172}
173
174struct Subsys {
175    version: CgroupVersion,
176    base: String,
177}
178
179impl 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
252impl 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
307impl 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)]
350mod 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}