lscpu
: Parse CPU cache topology from sysfs
This commit is contained in:
parent
e689bee771
commit
99c751bd1d
13
Cargo.lock
generated
13
Cargo.lock
generated
@ -271,7 +271,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -513,6 +513,12 @@ dependencies = [
|
||||
"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]]
|
||||
name = "parse_datetime"
|
||||
version = "0.7.0"
|
||||
@ -783,7 +789,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.4.15",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -923,7 +929,7 @@ dependencies = [
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix 0.38.44",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1113,6 +1119,7 @@ name = "uu_lscpu"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"parse-size",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -46,6 +46,7 @@ dns-lookup = "2.0.4"
|
||||
libc = "0.2.152"
|
||||
linux-raw-sys = { version = "0.7.0", features = ["ioctl"] }
|
||||
nix = { version = "0.29", default-features = false }
|
||||
parse-size = "1.1.0"
|
||||
phf = "0.11.2"
|
||||
phf_codegen = "0.11.2"
|
||||
rand = { version = "0.9.0", features = ["small_rng"] }
|
||||
|
@ -17,3 +17,4 @@ uucore = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
parse-size = { workspace = true }
|
||||
|
@ -6,7 +6,7 @@
|
||||
use clap::{crate_version, Arg, ArgAction, Command};
|
||||
use regex::RegexBuilder;
|
||||
use serde::Serialize;
|
||||
use std::{cmp, fs};
|
||||
use std::{cmp, collections::HashMap, fs};
|
||||
use uucore::{error::UResult, format_usage, help_about, help_usage};
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
let cpu_topology = sysfs::read_cpu_topology();
|
||||
|
||||
cpu_infos.push(arch_info);
|
||||
cpu_infos.push(CpuInfo::new(
|
||||
"CPU(s)",
|
||||
&format!("{}", cpu_topology.cpus.len()),
|
||||
|
||||
let cpu_topology = sysfs::read_cpu_topology();
|
||||
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
|
||||
@ -132,6 +136,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
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);
|
||||
@ -146,6 +154,53 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
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) {
|
||||
if out_opts.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 name: String,
|
||||
@ -9,49 +10,127 @@ pub struct CpuTopology {
|
||||
pub cpus: Vec<Cpu>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Cpu {
|
||||
_index: usize,
|
||||
_caches: Vec<CpuCache>,
|
||||
pub _pkg_id: usize,
|
||||
pub caches: Vec<CpuCache>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CpuCache {
|
||||
_index: usize,
|
||||
_typ: String,
|
||||
_level: String,
|
||||
_size: String,
|
||||
pub typ: CacheType,
|
||||
pub level: usize,
|
||||
pub size: u64,
|
||||
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 {
|
||||
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>`
|
||||
// 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
|
||||
let enabled_cpus = match fs::read_to_string("/sys/devices/system/cpu/enabled") {
|
||||
Ok(content) => {
|
||||
let parts: Vec<_> = content
|
||||
.trim()
|
||||
.split("-")
|
||||
.flat_map(|part| part.parse::<usize>())
|
||||
.collect();
|
||||
assert_eq!(parts.len(), 2);
|
||||
(parts[0], parts[1])
|
||||
}
|
||||
Err(e) => panic!("Could not read sysfs: {}", e),
|
||||
};
|
||||
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 physical_pkg_id = fs::read_to_string(cpu_dir.join("topology/physical_package_id"))
|
||||
.unwrap()
|
||||
.trim()
|
||||
.parse::<usize>()
|
||||
.unwrap();
|
||||
|
||||
let caches = read_cpu_caches(cpu_index);
|
||||
|
||||
for cpu_index in enabled_cpus.0..(enabled_cpus.1 + 1) {
|
||||
out.push(Cpu {
|
||||
_index: cpu_index,
|
||||
_caches: vec![],
|
||||
_pkg_id: physical_pkg_id,
|
||||
caches,
|
||||
})
|
||||
}
|
||||
|
||||
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> {
|
||||
match fs::read_to_string("/sys/devices/system/cpu/cpufreq/boost") {
|
||||
Ok(content) => Some(content.trim() == "1"),
|
||||
|
Loading…
x
Reference in New Issue
Block a user