diff --git a/src/uu/lscpu/src/lscpu.rs b/src/uu/lscpu/src/lscpu.rs index 3150e53..df39810 100644 --- a/src/uu/lscpu/src/lscpu.rs +++ b/src/uu/lscpu/src/lscpu.rs @@ -6,8 +6,8 @@ use clap::{crate_version, Arg, ArgAction, Command}; use regex::RegexBuilder; use serde::Serialize; -use std::{cmp, fs}; -use sysinfo::System; +use std::{cmp, collections::HashMap, fs}; +use sysfs::CacheSize; use uucore::{error::UResult, format_usage, help_about, help_usage}; mod options { @@ -15,6 +15,8 @@ mod options { pub const JSON: &str = "json"; } +mod sysfs; + const ABOUT: &str = help_about!("lscpu.md"); const USAGE: &str = help_usage!("lscpu.md"); @@ -70,8 +72,6 @@ struct OutputOptions { pub fn uumain(args: impl uucore::Args) -> UResult<()> { let matches: clap::ArgMatches = uu_app().try_get_matches_from(args)?; - let system = System::new_all(); - let output_opts = OutputOptions { _hex: matches.get_flag(options::HEX), json: matches.get_flag(options::JSON), @@ -89,21 +89,23 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { arch_info.add_child(CpuInfo::new("Address sizes", &addr_sizes, None)) } - if let Ok(byte_order) = fs::read_to_string("/sys/kernel/cpu_byteorder") { - match byte_order.trim() { - "big" => arch_info.add_child(CpuInfo::new("Byte Order", "Big Endian", None)), - "little" => arch_info.add_child(CpuInfo::new("Byte Order", "Little Endian", None)), - _ => eprintln!("Unrecognised Byte Order: {}", byte_order), - } + if let Some(byte_order) = sysfs::read_cpu_byte_order() { + arch_info.add_child(CpuInfo::new("Byte Order", byte_order, None)); } cpu_infos.push(arch_info); - cpu_infos.push(CpuInfo::new( - "CPU(s)", - &format!("{}", system.cpus().len()), + + let cpu_topology = sysfs::CpuTopology::new(); + let mut cores_info = CpuInfo::new("CPU(s)", &format!("{}", cpu_topology.cpus.len()), None); + + cores_info.add_child(CpuInfo::new( + "On-line CPU(s) list", + &sysfs::read_online_cpus(), None, )); + cpu_infos.push(cores_info); + // TODO: This is currently quite verbose and doesn't strictly respect the hierarchy of `/proc/cpuinfo` contents // ie. the file might contain multiple sections, each with their own vendor_id/model name etc. but right now // we're just taking whatever our regex matches first and using that @@ -121,16 +123,108 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { model_name_info.add_child(CpuInfo::new("Model", &model, None)); } + let socket_count = &cpu_topology.socket_count(); + let core_count = &cpu_topology.core_count(); + + model_name_info.add_child(CpuInfo::new( + "Thread(s) per core", + &(cpu_topology.cpus.len() / core_count).to_string(), + None, + )); + + model_name_info.add_child(CpuInfo::new( + "Core(s) per socket", + &(core_count / socket_count).to_string(), + None, + )); + model_name_info.add_child(CpuInfo::new("Socket(s)", &socket_count.to_string(), None)); + + if let Some(freq_boost_enabled) = sysfs::read_freq_boost_state() { + let s = if freq_boost_enabled { + "enabled" + } else { + "disabled" + }; + model_name_info.add_child(CpuInfo::new("Frequency boost", s, None)); + } + vendor_info.add_child(model_name_info); } + cpu_infos.push(vendor_info); } + if let Some(cache_info) = calculate_cache_totals(cpu_topology.cpus) { + cpu_infos.push(cache_info); + } + + let vulns = sysfs::read_cpu_vulnerabilities(); + if !vulns.is_empty() { + let mut vuln_info = CpuInfo::new("Vulnerabilities", "", None); + for vuln in vulns { + vuln_info.add_child(CpuInfo::new(&vuln.name, &vuln.mitigation, None)); + } + cpu_infos.push(vuln_info); + } + print_output(cpu_infos, output_opts); Ok(()) } +fn calculate_cache_totals(cpus: Vec<sysfs::Cpu>) -> Option<CpuInfo> { + let mut by_levels: HashMap<String, Vec<&sysfs::CpuCache>> = HashMap::new(); + let all_caches: Vec<_> = cpus.iter().flat_map(|cpu| &cpu.caches).collect(); + + if all_caches.is_empty() { + return None; + } + + for cache in all_caches { + let type_suffix = match cache.typ { + sysfs::CacheType::Instruction => "i", + sysfs::CacheType::Data => "d", + _ => "", + }; + let level_key = format!("L{}{}", cache.level, type_suffix); + + if let Some(caches) = by_levels.get_mut(&level_key) { + caches.push(cache); + } else { + by_levels.insert(level_key, vec![cache]); + } + } + + let mut cache_info = CpuInfo::new("Caches (sum of all)", "", None); + + for (level, caches) in by_levels.iter_mut() { + // Cache instances that are shared across multiple CPUs should have the same `shared_cpu_map` value + // Deduplicating the list on a per-level basic using the CPU map ensures that we don't count any shared caches multiple times + caches.sort_by(|a, b| a.shared_cpu_map.cmp(&b.shared_cpu_map)); + caches.dedup_by_key(|c| &c.shared_cpu_map); + + let count = caches.len(); + let size_total = caches + .iter() + .fold(0_u64, |acc, c| acc + c.size.size_bytes()); + + cache_info.add_child(CpuInfo::new( + level, + &format!( + "{} ({} instances)", + CacheSize::new(size_total).human_readable(), + count + ), + None, + )); + } + + // Make sure caches get printed in alphabetical order + cache_info.children.sort_by(|a, b| a.field.cmp(&b.field)); + + Some(cache_info) +} + fn print_output(infos: CpuInfos, out_opts: OutputOptions) { if out_opts.json { println!("{}", infos.to_json()); @@ -200,6 +294,8 @@ fn find_cpuinfo_value(contents: &str, key: &str) -> Option<String> { value } +// TODO: This is non-exhaustive and assumes that compile-time arch is the same as runtime +// This is not always guaranteed to be the case, ie. you can run a x86 binary on a x86_64 machine fn get_architecture() -> String { if cfg!(target_arch = "x86") { "x86".to_string() diff --git a/src/uu/lscpu/src/sysfs.rs b/src/uu/lscpu/src/sysfs.rs new file mode 100644 index 0000000..bbbf9d7 --- /dev/null +++ b/src/uu/lscpu/src/sysfs.rs @@ -0,0 +1,301 @@ +// This file is part of the uutils util-linux package. +// +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use std::{collections::HashSet, fs, path::PathBuf}; + +pub struct CpuVulnerability { + pub name: String, + pub mitigation: String, +} + +pub struct CpuTopology { + pub cpus: Vec<Cpu>, +} + +#[derive(Debug)] +pub struct Cpu { + _index: usize, + pub pkg_id: usize, + pub core_id: usize, + pub caches: Vec<CpuCache>, +} + +#[derive(Debug)] +pub struct CpuCache { + pub typ: CacheType, + pub level: usize, + pub size: CacheSize, + pub shared_cpu_map: String, +} + +#[derive(Debug)] +pub struct CacheSize(u64); + +#[derive(Debug)] +pub enum CacheType { + Data, + Instruction, + Unified, +} + +impl CpuTopology { + pub fn new() -> Self { + let mut out: Vec<Cpu> = vec![]; + + let online_cpus = parse_cpu_list(&read_online_cpus()); + + for cpu_index in online_cpus { + let cpu_dir = PathBuf::from(format!("/sys/devices/system/cpu/cpu{}/", cpu_index)); + + let pkg_id = fs::read_to_string(cpu_dir.join("topology/physical_package_id")) + .unwrap() + .trim() + .parse::<usize>() + .unwrap(); + + let core_id = fs::read_to_string(cpu_dir.join("topology/core_id")) + .unwrap() + .trim() + .parse::<usize>() + .unwrap(); + + let caches = read_cpu_caches(cpu_index); + + out.push(Cpu { + _index: cpu_index, + pkg_id, + core_id, + caches, + }) + } + Self { cpus: out } + } + + pub fn socket_count(&self) -> usize { + // Each physical socket is represented as its own package_id, so amount of unique pkg_ids = sockets + // https://www.kernel.org/doc/html/latest/admin-guide/abi-stable.html#abi-sys-devices-system-cpu-cpux-topology-physical-package-id + let physical_sockets: HashSet<_> = self.cpus.iter().map(|cpu| cpu.pkg_id).collect(); + + physical_sockets.len() + } + + pub fn core_count(&self) -> usize { + let core_ids: HashSet<_> = self.cpus.iter().map(|cpu| cpu.core_id).collect(); + core_ids.len() + } +} + +impl CacheSize { + pub fn new(size: u64) -> Self { + Self(size) + } + + fn parse(s: &str) -> Self { + // Yes, this will break if we ever reach a point where caches exceed terabytes in size... + const EXPONENTS: [(char, u32); 4] = [('K', 1), ('M', 2), ('G', 3), ('T', 4)]; + + // If we only have numbers, treat it as a raw amount of bytes and parse as-is + if s.chars().all(char::is_numeric) { + return Self(s.parse::<u64>().expect("Could not parse cache size")); + }; + + for (suffix, exponent) in EXPONENTS { + if s.ends_with(suffix) { + let nums = s.strip_suffix(suffix).unwrap(); + let value = nums.parse::<u64>().expect("Could not parse cache size"); + let multiplier = 1024_u64.pow(exponent); + + return Self(value * multiplier); + } + } + + panic!("No known suffix in cache size string"); + } + + pub fn size_bytes(&self) -> u64 { + self.0 + } + + pub fn human_readable(&self) -> String { + let (unit, denominator) = match self.0 { + x if x < 1024_u64.pow(1) => ("B", 1024_u64.pow(0)), + x if x < 1024_u64.pow(2) => ("KiB", 1024_u64.pow(1)), + x if x < 1024_u64.pow(3) => ("MiB", 1024_u64.pow(2)), + x if x < 1024_u64.pow(4) => ("GiB", 1024_u64.pow(3)), + x if x < 1024_u64.pow(5) => ("TiB", 1024_u64.pow(4)), + _ => return format!("{} bytes", self.0), + }; + let scaled_size = self.0 / denominator; + format!("{} {}", scaled_size, unit) + } +} + +// TODO: respect `--hex` option and output the bitmask instead of human-readable range +pub fn read_online_cpus() -> String { + fs::read_to_string("/sys/devices/system/cpu/online") + .expect("Could not read sysfs") + .trim() + .to_string() +} + +fn read_cpu_caches(cpu_index: usize) -> Vec<CpuCache> { + let cpu_dir = PathBuf::from(format!("/sys/devices/system/cpu/cpu{}/", cpu_index)); + let cache_dir = fs::read_dir(cpu_dir.join("cache")).unwrap(); + let cache_paths = cache_dir + .flatten() + .filter(|x| x.path().is_dir()) + .map(|x| x.path()); + + let mut caches: Vec<CpuCache> = vec![]; + + for cache_path in cache_paths { + let type_string = fs::read_to_string(cache_path.join("type")).unwrap(); + + let c_type = match type_string.trim() { + "Unified" => CacheType::Unified, + "Data" => CacheType::Data, + "Instruction" => CacheType::Instruction, + _ => panic!("Unrecognized cache type: {}", type_string), + }; + + let c_level = fs::read_to_string(cache_path.join("level")) + .map(|s| s.trim().parse::<usize>().unwrap()) + .unwrap(); + + let size_string = fs::read_to_string(cache_path.join("size")).unwrap(); + let c_size = CacheSize::parse(size_string.trim()); + + let shared_cpu_map = fs::read_to_string(cache_path.join("shared_cpu_map")) + .unwrap() + .trim() + .to_string(); + + caches.push(CpuCache { + level: c_level, + size: c_size, + typ: c_type, + shared_cpu_map, + }); + } + + caches +} + +pub fn read_freq_boost_state() -> Option<bool> { + fs::read_to_string("/sys/devices/system/cpu/cpufreq/boost") + .map(|content| content.trim() == "1") + .ok() +} + +pub fn read_cpu_vulnerabilities() -> Vec<CpuVulnerability> { + let mut out: Vec<CpuVulnerability> = vec![]; + + if let Ok(dir) = fs::read_dir("/sys/devices/system/cpu/vulnerabilities") { + let mut files: Vec<_> = dir + .flatten() + .map(|x| x.path()) + .filter(|x| !x.is_dir()) + .collect(); + + files.sort_by(|a, b| a.file_name().cmp(&b.file_name())); + + for file in files { + if let Ok(content) = fs::read_to_string(&file) { + let name = file.file_name().unwrap().to_str().unwrap(); + + out.push(CpuVulnerability { + name: (name[..1].to_uppercase() + &name[1..]).replace("_", " "), + mitigation: content.trim().to_string(), + }); + } + } + }; + + out +} + +pub fn read_cpu_byte_order() -> Option<&'static str> { + if let Ok(byte_order) = fs::read_to_string("/sys/kernel/cpu_byteorder") { + match byte_order.trim() { + "big" => return Some("Big Endian"), + "little" => return Some("Little Endian"), + _ => eprintln!("Unrecognised Byte Order: {}", byte_order), + } + } + None +} + +// Takes in a human-readable list of CPUs, and returns a list of indices parsed from that list +// These can come in the form of a plain range like `X-Y`, or a comma-separated ranges and indices ie. `1,3-4,7-8,10` +// Kernel docs with examples: https://www.kernel.org/doc/html/latest/admin-guide/cputopology.html +fn parse_cpu_list(list: &str) -> Vec<usize> { + let mut out: Vec<usize> = vec![]; + + if list.is_empty() { + return out; + } + + for part in list.trim().split(",") { + if part.contains("-") { + let bounds: Vec<_> = part.split("-").flat_map(|x| x.parse::<usize>()).collect(); + assert_eq!(bounds.len(), 2); + for idx in bounds[0]..bounds[1] + 1 { + out.push(idx) + } + } else { + let idx = part.parse::<usize>().expect("Invalid CPU index value"); + out.push(idx); + } + } + + out +} + +#[test] +fn test_parse_cache_size() { + assert_eq!(CacheSize::parse("512").size_bytes(), 512); + assert_eq!(CacheSize::parse("1K").size_bytes(), 1024); + assert_eq!(CacheSize::parse("1M").size_bytes(), 1024 * 1024); + assert_eq!(CacheSize::parse("1G").size_bytes(), 1024 * 1024 * 1024); + assert_eq!( + CacheSize::parse("1T").size_bytes(), + 1024 * 1024 * 1024 * 1024 + ); + assert_eq!(CacheSize::parse("123K").size_bytes(), 123 * 1024); + assert_eq!(CacheSize::parse("32M").size_bytes(), 32 * 1024 * 1024); + assert_eq!( + CacheSize::parse("345G").size_bytes(), + 345 * 1024 * 1024 * 1024 + ); +} + +#[test] +fn test_print_cache_size() { + assert_eq!(CacheSize::new(1023).human_readable(), "1023 B"); + assert_eq!(CacheSize::new(1024).human_readable(), "1 KiB"); + assert_eq!(CacheSize::new(1024 * 1024).human_readable(), "1 MiB"); + assert_eq!(CacheSize::new(1024 * 1024 * 1024).human_readable(), "1 GiB"); + + assert_eq!(CacheSize::new(3 * 1024).human_readable(), "3 KiB"); + assert_eq!( + CacheSize::new((7.6 * 1024.0 * 1024.0) as u64).human_readable(), + "7 MiB" + ); +} + +#[test] +fn test_parse_cpu_list() { + assert_eq!(parse_cpu_list(""), Vec::<usize>::new()); + assert_eq!(parse_cpu_list("1-3"), Vec::<usize>::from([1, 2, 3])); + assert_eq!(parse_cpu_list("1,2,3"), Vec::<usize>::from([1, 2, 3])); + assert_eq!( + parse_cpu_list("1,3-6,8"), + Vec::<usize>::from([1, 3, 4, 5, 6, 8]) + ); + assert_eq!( + parse_cpu_list("1-2,3-5,7"), + Vec::<usize>::from([1, 2, 3, 4, 5, 7]) + ); +}