Skip to main content

linuxutils_system/
lscpu.rs

1use linuxutils_common::man::ManContent;
2
3pub const MAN: ManContent = ManContent::empty();
4
5use clap::Parser;
6use cols::{Cols, print_table};
7use procfs::Current;
8use std::{
9    collections::{BTreeMap, BTreeSet},
10    fs, io,
11    process::ExitCode,
12};
13
14const CPU_SYS: &str = "/sys/devices/system/cpu";
15const NODE_SYS: &str = "/sys/devices/system/node";
16
17/// Display information about the CPU architecture.
18///
19/// Gathers CPU information from /proc/cpuinfo and sysfs, displaying
20/// architecture details, topology, caches, frequencies, and vulnerabilities.
21#[derive(Parser)]
22#[command(
23    name = "lscpu",
24    about = "Display information about the CPU architecture"
25)]
26pub struct Args {
27    /// Print sizes in bytes
28    #[arg(short = 'B', long)]
29    bytes: bool,
30}
31
32#[derive(Cols)]
33struct Row {
34    #[column(header = "", width_fixed = 25)]
35    label: String,
36
37    #[column(header = "", wrap)]
38    value: String,
39}
40
41impl Row {
42    fn new(indent: usize, label: &str, value: &str) -> Self {
43        let padding = " ".repeat(indent);
44        Self {
45            label: format!("{padding}{label}"),
46            value: value.to_string(),
47        }
48    }
49
50    fn section(title: &str) -> Self {
51        Self {
52            label: title.to_string(),
53            value: String::new(),
54        }
55    }
56}
57
58fn sysfs_read(path: &str) -> Option<String> {
59    fs::read_to_string(path).ok().map(|s| s.trim().to_string())
60}
61
62fn cpuinfo_field(
63    cpuinfo: &procfs::CpuInfo,
64    cpu: usize,
65    field: &str,
66) -> Option<String> {
67    cpuinfo
68        .get_info(cpu)
69        .and_then(|info| info.get(field).map(|s| s.to_string()))
70}
71
72fn parse_cpu_list(s: &str) -> Vec<u32> {
73    let mut result = Vec::new();
74    for part in s.split(',') {
75        let part = part.trim();
76        if let Some((start, end)) = part.split_once('-') {
77            if let (Ok(s), Ok(e)) = (start.parse::<u32>(), end.parse::<u32>()) {
78                result.extend(s..=e);
79            }
80        } else if let Ok(n) = part.parse::<u32>() {
81            result.push(n);
82        }
83    }
84    result
85}
86
87fn parse_cache_size_kb(s: &str) -> Option<u64> {
88    let s = s.trim();
89    if let Some(num) = s.strip_suffix('K') {
90        num.trim().parse().ok()
91    } else if let Some(num) = s.strip_suffix('M') {
92        num.trim().parse::<u64>().ok().map(|n| n * 1024)
93    } else {
94        s.parse().ok()
95    }
96}
97
98fn format_cache_size(total_kb: u64) -> String {
99    if total_kb >= 1024 && total_kb.is_multiple_of(1024) {
100        format!("{} MiB", total_kb / 1024)
101    } else {
102        format!("{total_kb} KiB")
103    }
104}
105
106struct CacheInfo {
107    name: String,
108    one_size_kb: u64,
109    instances: usize,
110}
111
112fn gather_caches() -> Vec<CacheInfo> {
113    let mut seen: BTreeMap<String, (u64, BTreeSet<String>)> = BTreeMap::new();
114
115    let Ok(entries) = fs::read_dir(CPU_SYS) else {
116        return Vec::new();
117    };
118
119    for entry in entries.flatten() {
120        let cpu_name = entry.file_name();
121        let cpu_str = cpu_name.to_string_lossy();
122        if !cpu_str.starts_with("cpu")
123            || cpu_str[3..]
124                .chars()
125                .next()
126                .is_none_or(|c| !c.is_ascii_digit())
127        {
128            continue;
129        }
130
131        let cache_dir = format!("{CPU_SYS}/{cpu_str}/cache");
132        let Ok(cache_entries) = fs::read_dir(&cache_dir) else {
133            continue;
134        };
135
136        for ce in cache_entries.flatten() {
137            let idx_name = ce.file_name();
138            let idx_str = idx_name.to_string_lossy();
139            if !idx_str.starts_with("index") {
140                continue;
141            }
142
143            let base = format!("{cache_dir}/{idx_str}");
144            let level =
145                sysfs_read(&format!("{base}/level")).unwrap_or_default();
146            let cache_type =
147                sysfs_read(&format!("{base}/type")).unwrap_or_default();
148            let size_str =
149                sysfs_read(&format!("{base}/size")).unwrap_or_default();
150            let shared = sysfs_read(&format!("{base}/shared_cpu_list"))
151                .unwrap_or_default();
152
153            let size_kb = parse_cache_size_kb(&size_str).unwrap_or(0);
154            let type_abbr = match cache_type.as_str() {
155                "Data" => "d",
156                "Instruction" => "i",
157                _ => "",
158            };
159            let name = format!("L{level}{type_abbr}");
160
161            seen.entry(name)
162                .or_insert_with(|| (size_kb, BTreeSet::new()))
163                .1
164                .insert(shared);
165        }
166    }
167
168    seen.into_iter()
169        .map(|(name, (one_size_kb, shared_sets))| CacheInfo {
170            name,
171            one_size_kb,
172            instances: shared_sets.len(),
173        })
174        .collect()
175}
176
177fn gather_numa_nodes() -> Vec<(u32, String)> {
178    let Ok(entries) = fs::read_dir(NODE_SYS) else {
179        return Vec::new();
180    };
181
182    let mut nodes: Vec<(u32, String)> = Vec::new();
183    for entry in entries.flatten() {
184        let name = entry.file_name();
185        let name_str = name.to_string_lossy();
186        if let Some(num_str) = name_str.strip_prefix("node")
187            && let Ok(num) = num_str.parse::<u32>()
188        {
189            let cpulist = sysfs_read(&format!("{NODE_SYS}/{name_str}/cpulist"))
190                .unwrap_or_default();
191            nodes.push((num, cpulist));
192        }
193    }
194    nodes.sort_by_key(|(n, _)| *n);
195    nodes
196}
197
198fn gather_vulnerabilities() -> Vec<(String, String)> {
199    let vuln_dir = format!("{CPU_SYS}/vulnerabilities");
200    let Ok(entries) = fs::read_dir(&vuln_dir) else {
201        return Vec::new();
202    };
203
204    let mut vulns: Vec<(String, String)> = entries
205        .flatten()
206        .filter_map(|e| {
207            let name = e.file_name().to_string_lossy().to_string();
208            let status = sysfs_read(&format!("{vuln_dir}/{name}"))?;
209            Some((name, status))
210        })
211        .collect();
212
213    vulns.sort_by(|(a, _), (b, _)| a.cmp(b));
214    vulns
215}
216
217fn pretty_vuln_name(file_name: &str) -> String {
218    let mut result = file_name.replace('_', " ");
219    if let Some(first) = result.get_mut(..1) {
220        first.make_ascii_uppercase();
221    }
222    result
223}
224
225pub fn run(_args: Args) -> ExitCode {
226    let cpuinfo = match procfs::CpuInfo::current() {
227        Ok(c) => c,
228        Err(e) => {
229            eprintln!("lscpu: failed to read /proc/cpuinfo: {e}");
230            return ExitCode::FAILURE;
231        }
232    };
233
234    let num_cpus = cpuinfo.num_cores();
235    let vendor = cpuinfo_field(&cpuinfo, 0, "vendor_id").unwrap_or_default();
236    let model_name =
237        cpuinfo_field(&cpuinfo, 0, "model name").unwrap_or_default();
238    let cpu_family =
239        cpuinfo_field(&cpuinfo, 0, "cpu family").unwrap_or_default();
240    let model = cpuinfo_field(&cpuinfo, 0, "model").unwrap_or_default();
241    let stepping = cpuinfo_field(&cpuinfo, 0, "stepping").unwrap_or_default();
242    let bogomips = cpuinfo_field(&cpuinfo, 0, "bogomips").unwrap_or_default();
243    let flags_str = cpuinfo_field(&cpuinfo, 0, "flags").unwrap_or_default();
244    let addr_sizes =
245        cpuinfo_field(&cpuinfo, 0, "address sizes").unwrap_or_default();
246
247    let arch = std::env::consts::ARCH;
248    let op_modes = match arch {
249        "x86_64" => "32-bit, 64-bit",
250        "aarch64" => "32-bit, 64-bit",
251        "x86" => "32-bit",
252        _ => arch,
253    };
254
255    let online = sysfs_read(&format!("{CPU_SYS}/online"))
256        .unwrap_or_else(|| format!("0-{}", num_cpus - 1));
257    let offline = sysfs_read(&format!("{CPU_SYS}/offline")).unwrap_or_default();
258
259    let mut sockets: BTreeSet<u32> = BTreeSet::new();
260    let mut cores_per_socket: BTreeMap<u32, BTreeSet<u32>> = BTreeMap::new();
261    let online_cpus = parse_cpu_list(&online);
262
263    for &cpu in &online_cpus {
264        let pkg = sysfs_read(&format!(
265            "{CPU_SYS}/cpu{cpu}/topology/physical_package_id"
266        ))
267        .and_then(|s| s.parse().ok())
268        .unwrap_or(0);
269        let core = sysfs_read(&format!("{CPU_SYS}/cpu{cpu}/topology/core_id"))
270            .and_then(|s| s.parse().ok())
271            .unwrap_or(0);
272        sockets.insert(pkg);
273        cores_per_socket.entry(pkg).or_default().insert(core);
274    }
275
276    let num_sockets = sockets.len().max(1);
277    let cores_per_sock = if !cores_per_socket.is_empty() {
278        cores_per_socket.values().next().unwrap().len()
279    } else {
280        num_cpus
281    };
282    let threads_per_core = if cores_per_sock > 0 && num_sockets > 0 {
283        online_cpus.len() / (num_sockets * cores_per_sock)
284    } else {
285        1
286    }
287    .max(1);
288
289    let max_mhz =
290        sysfs_read(&format!("{CPU_SYS}/cpu0/cpufreq/cpuinfo_max_freq"))
291            .and_then(|s| s.parse::<f64>().ok())
292            .map(|khz| khz / 1000.0);
293    let min_mhz =
294        sysfs_read(&format!("{CPU_SYS}/cpu0/cpufreq/cpuinfo_min_freq"))
295            .and_then(|s| s.parse::<f64>().ok())
296            .map(|khz| khz / 1000.0);
297
298    let virt = if flags_str.split_whitespace().any(|f| f == "svm") {
299        Some("AMD-V")
300    } else if flags_str.split_whitespace().any(|f| f == "vmx") {
301        Some("VT-x")
302    } else {
303        None
304    };
305
306    let mut rows: Vec<Row> = Vec::new();
307
308    // Architecture
309    rows.push(Row::new(0, "Architecture:", arch));
310    rows.push(Row::new(2, "CPU op-mode(s):", op_modes));
311    if !addr_sizes.is_empty() {
312        rows.push(Row::new(2, "Address sizes:", &addr_sizes));
313    }
314    #[cfg(target_endian = "little")]
315    rows.push(Row::new(2, "Byte Order:", "Little Endian"));
316    #[cfg(target_endian = "big")]
317    rows.push(Row::new(2, "Byte Order:", "Big Endian"));
318
319    // CPU count
320    rows.push(Row::new(0, "CPU(s):", &online_cpus.len().to_string()));
321    rows.push(Row::new(2, "On-line CPU(s) list:", &online));
322    if !offline.is_empty() {
323        rows.push(Row::new(2, "Off-line CPU(s) list:", &offline));
324    }
325
326    // Vendor / Model
327    rows.push(Row::new(0, "Vendor ID:", &vendor));
328    rows.push(Row::new(2, "Model name:", &model_name));
329    rows.push(Row::new(4, "CPU family:", &cpu_family));
330    rows.push(Row::new(4, "Model:", &model));
331    rows.push(Row::new(
332        4,
333        "Thread(s) per core:",
334        &threads_per_core.to_string(),
335    ));
336    rows.push(Row::new(
337        4,
338        "Core(s) per socket:",
339        &cores_per_sock.to_string(),
340    ));
341    rows.push(Row::new(4, "Socket(s):", &num_sockets.to_string()));
342    rows.push(Row::new(4, "Stepping:", &stepping));
343    if let Some(max) = max_mhz {
344        rows.push(Row::new(4, "CPU max MHz:", &format!("{max:.4}")));
345    }
346    if let Some(min) = min_mhz {
347        rows.push(Row::new(4, "CPU min MHz:", &format!("{min:.4}")));
348    }
349    rows.push(Row::new(4, "BogoMIPS:", &bogomips));
350    if !flags_str.is_empty() {
351        rows.push(Row::new(4, "Flags:", &flags_str));
352    }
353
354    // Virtualization
355    if let Some(v) = virt {
356        rows.push(Row::section("Virtualization features:"));
357        rows.push(Row::new(2, "Virtualization:", v));
358    }
359
360    // Caches
361    let caches = gather_caches();
362    if !caches.is_empty() {
363        rows.push(Row::section("Caches (sum of all):"));
364        for cache in &caches {
365            let total_kb = cache.one_size_kb * cache.instances as u64;
366            let size_str = format_cache_size(total_kb);
367            let value = if cache.instances > 1 {
368                format!("{size_str} ({} instances)", cache.instances)
369            } else {
370                format!("{size_str} (1 instance)")
371            };
372            rows.push(Row::new(2, &format!("{}:", cache.name), &value));
373        }
374    }
375
376    // NUMA
377    let numa_nodes = gather_numa_nodes();
378    if !numa_nodes.is_empty() {
379        rows.push(Row::section("NUMA:"));
380        rows.push(Row::new(2, "NUMA node(s):", &numa_nodes.len().to_string()));
381        for (node, cpulist) in &numa_nodes {
382            rows.push(Row::new(
383                2,
384                &format!("NUMA node{node} CPU(s):"),
385                cpulist,
386            ));
387        }
388    }
389
390    // Vulnerabilities
391    let vulns = gather_vulnerabilities();
392    if !vulns.is_empty() {
393        rows.push(Row::section("Vulnerabilities:"));
394        for (name, status) in &vulns {
395            let pretty = pretty_vuln_name(name);
396            rows.push(Row::new(2, &format!("{pretty}:"), status));
397        }
398    }
399
400    let mut table = Row::to_table(&rows);
401    table.headings_set(false);
402    let _ = print_table(&table, &mut io::stdout().lock());
403
404    ExitCode::SUCCESS
405}