lscpu: Parse CPU cache topology from sysfs

This commit is contained in:
alxndrv
2025-02-14 12:55:50 +02:00
parent e689bee771
commit 99c751bd1d
5 changed files with 177 additions and 34 deletions

13
Cargo.lock generated

@ -271,7 +271,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -513,6 +513,12 @@ dependencies = [
"unicode-width 0.2.0", "unicode-width 0.2.0",
] ]
[[package]]
name = "parse-size"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "487f2ccd1e17ce8c1bfab3a65c89525af41cfad4c8659021a1e9a2aacd73b89b"
[[package]] [[package]]
name = "parse_datetime" name = "parse_datetime"
version = "0.7.0" version = "0.7.0"
@ -783,7 +789,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys 0.4.15", "linux-raw-sys 0.4.15",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -923,7 +929,7 @@ dependencies = [
"getrandom", "getrandom",
"once_cell", "once_cell",
"rustix 0.38.44", "rustix 0.38.44",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@ -1113,6 +1119,7 @@ name = "uu_lscpu"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"clap", "clap",
"parse-size",
"regex", "regex",
"serde", "serde",
"serde_json", "serde_json",

@ -46,6 +46,7 @@ dns-lookup = "2.0.4"
libc = "0.2.152" libc = "0.2.152"
linux-raw-sys = { version = "0.7.0", features = ["ioctl"] } linux-raw-sys = { version = "0.7.0", features = ["ioctl"] }
nix = { version = "0.29", default-features = false } nix = { version = "0.29", default-features = false }
parse-size = "1.1.0"
phf = "0.11.2" phf = "0.11.2"
phf_codegen = "0.11.2" phf_codegen = "0.11.2"
rand = { version = "0.9.0", features = ["small_rng"] } rand = { version = "0.9.0", features = ["small_rng"] }

@ -17,3 +17,4 @@ uucore = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
parse-size = { workspace = true }

@ -6,7 +6,7 @@
use clap::{crate_version, Arg, ArgAction, Command}; use clap::{crate_version, Arg, ArgAction, Command};
use regex::RegexBuilder; use regex::RegexBuilder;
use serde::Serialize; use serde::Serialize;
use std::{cmp, fs}; use std::{cmp, collections::HashMap, fs};
use uucore::{error::UResult, format_usage, help_about, help_usage}; use uucore::{error::UResult, format_usage, help_about, help_usage};
mod options { mod options {
@ -92,15 +92,19 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
arch_info.add_child(CpuInfo::new("Byte Order", byte_order, None)); arch_info.add_child(CpuInfo::new("Byte Order", byte_order, None));
} }
let cpu_topology = sysfs::read_cpu_topology();
cpu_infos.push(arch_info); cpu_infos.push(arch_info);
cpu_infos.push(CpuInfo::new(
"CPU(s)", let cpu_topology = sysfs::read_cpu_topology();
&format!("{}", cpu_topology.cpus.len()), 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, None,
)); ));
cpu_infos.push(cores_info);
// TODO: This is currently quite verbose and doesn't strictly respect the hierarchy of `/proc/cpuinfo` contents // 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 // 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 // we're just taking whatever our regex matches first and using that
@ -132,6 +136,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
cpu_infos.push(vendor_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(); let vulns = sysfs::read_cpu_vulnerabilities();
if !vulns.is_empty() { if !vulns.is_empty() {
let mut vuln_info = CpuInfo::new("Vulnerabilities", "", None); let mut vuln_info = CpuInfo::new("Vulnerabilities", "", None);
@ -146,6 +154,53 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
Ok(()) 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);
cache_info.add_child(CpuInfo::new(
level,
// TODO: Format sizes using `KiB`, `MiB` etc.
&format!("{} bytes ({} instances)", size_total, 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) { fn print_output(infos: CpuInfos, out_opts: OutputOptions) {
if out_opts.json { if out_opts.json {
println!("{}", infos.to_json()); println!("{}", infos.to_json());

@ -1,4 +1,5 @@
use std::fs; use parse_size::parse_size;
use std::{fs, path::PathBuf};
pub struct CpuVulnerability { pub struct CpuVulnerability {
pub name: String, pub name: String,
@ -9,49 +10,127 @@ pub struct CpuTopology {
pub cpus: Vec<Cpu>, pub cpus: Vec<Cpu>,
} }
#[derive(Debug)]
pub struct Cpu { pub struct Cpu {
_index: usize, _index: usize,
_caches: Vec<CpuCache>, pub _pkg_id: usize,
pub caches: Vec<CpuCache>,
} }
#[derive(Debug)]
pub struct CpuCache { pub struct CpuCache {
_index: usize, pub typ: CacheType,
_typ: String, pub level: usize,
_level: String, pub size: u64,
_size: String, pub shared_cpu_map: String,
}
#[derive(Debug)]
pub enum CacheType {
Data,
Instruction,
Unified,
}
// 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()
}
// 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![];
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
} }
// TODO: This should go through each CPU in sysfs and calculate things such as cache sizes and physical topology
// For now it just returns a list of CPUs which are enabled
pub fn read_cpu_topology() -> CpuTopology { pub fn read_cpu_topology() -> CpuTopology {
let mut out: Vec<Cpu> = vec![]; let mut out: Vec<Cpu> = vec![];
// NOTE: All examples I could find was where this file contains a CPU index range in the form of `<start>-<end>` let online_cpus = parse_cpu_list(&read_online_cpus());
// Theoretically, there might be a situation where some cores are disabled, so that `enabled` cannot be represented
// as a continuous range. For now we just assume it's always `X-Y` and use those as our bounds to read CPU information for cpu_index in online_cpus {
let enabled_cpus = match fs::read_to_string("/sys/devices/system/cpu/enabled") { let cpu_dir = PathBuf::from(format!("/sys/devices/system/cpu/cpu{}/", cpu_index));
Ok(content) => {
let parts: Vec<_> = content let physical_pkg_id = fs::read_to_string(cpu_dir.join("topology/physical_package_id"))
.trim() .unwrap()
.split("-") .trim()
.flat_map(|part| part.parse::<usize>()) .parse::<usize>()
.collect(); .unwrap();
assert_eq!(parts.len(), 2);
(parts[0], parts[1]) let caches = read_cpu_caches(cpu_index);
}
Err(e) => panic!("Could not read sysfs: {}", e),
};
for cpu_index in enabled_cpus.0..(enabled_cpus.1 + 1) {
out.push(Cpu { out.push(Cpu {
_index: cpu_index, _index: cpu_index,
_caches: vec![], _pkg_id: physical_pkg_id,
caches,
}) })
} }
CpuTopology { cpus: out } CpuTopology { cpus: out }
} }
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 = parse_size(size_string.trim()).unwrap();
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> { pub fn read_freq_boost_state() -> Option<bool> {
match fs::read_to_string("/sys/devices/system/cpu/cpufreq/boost") { match fs::read_to_string("/sys/devices/system/cpu/cpufreq/boost") {
Ok(content) => Some(content.trim() == "1"), Ok(content) => Some(content.trim() == "1"),