diff --git a/Cargo.lock b/Cargo.lock
index 5dc55c1..1a26061 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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",
diff --git a/Cargo.toml b/Cargo.toml
index f2864ca..9222a85 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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"] }
diff --git a/src/uu/lscpu/Cargo.toml b/src/uu/lscpu/Cargo.toml
index 8d45f12..bb8c86a 100644
--- a/src/uu/lscpu/Cargo.toml
+++ b/src/uu/lscpu/Cargo.toml
@@ -17,3 +17,4 @@ uucore = { workspace = true }
 clap = { workspace = true }
 serde = { workspace = true }
 serde_json = { workspace = true }
+parse-size = { workspace = true }
diff --git a/src/uu/lscpu/src/lscpu.rs b/src/uu/lscpu/src/lscpu.rs
index bc532f4..71bbe1d 100644
--- a/src/uu/lscpu/src/lscpu.rs
+++ b/src/uu/lscpu/src/lscpu.rs
@@ -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());
diff --git a/src/uu/lscpu/src/sysfs.rs b/src/uu/lscpu/src/sysfs.rs
index 8536e52..f4b1102 100644
--- a/src/uu/lscpu/src/sysfs.rs
+++ b/src/uu/lscpu/src/sysfs.rs
@@ -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"),