sys: memory info: use MemAvailable from kernel to compute used memory

The old code was wrong and overestimated the memory used because it
did not take into account things like "SReclaimable", a part of slab
(in-kernel memory allocator) describing things like caches that can be
reclaimed, plus the memory for "Active(file)" and "Inactive(file)",
and other internal kernel things that even though small for each one,
can add up quickly.

Most of these metrics are exposed and could be included in the
calculation, but this will simply become obsolete in the future as the
kernel changes how it does things and how it calculates such available
memory, as it has done many times in the past.

To solve this problem for the long term, the MemAvailable field was
added to /proc/meminfo as of kernel 3.14. It describes "the amount of
memory available for a new workload without pushing the system into
swap". While it is only an estimate, it is as good as it gets, and
since it comes from the kernel, we can always assume that it is
correct for the currently booted kernel.

So, switch over to this metric for calculating the used memory by
subtracting MemAvailable from MemTotal.

Also adds a simple test case for the parser.

This commit is based on a patch from Dietmar [1].

[0]: https://git.kernel.org/torvalds/c/34e431b0ae398fc54ea69ff85ec700722c9da773
[1]: https://lore.proxmox.com/all/20250313114535.99912-2-dietmar@proxmox.com/

Originally-by: Dietmar Maurer <dietmar@proxmox.com>
 [TL: rewrite comments and commit message from scratch]
Signed-off-by: Thomas Lamprecht <t.lamprecht@proxmox.com>
This commit is contained in:
Thomas Lamprecht 2025-04-07 20:23:09 +02:00
parent deb32a6c4a
commit 58d6e8d492

View File

@ -433,24 +433,27 @@ fn parse_proc_meminfo(text: &str) -> Result<ProcFsMemInfo, Error> {
swapused: 0,
};
let (mut buffers, mut cached) = (0, 0);
let mut mem_available = 0;
for line in text.lines() {
let mut content_iter = line.split_whitespace();
if let (Some(key), Some(value)) = (content_iter.next(), content_iter.next()) {
match key {
"MemTotal:" => meminfo.memtotal = value.parse::<u64>()? * 1024,
"MemFree:" => meminfo.memfree = value.parse::<u64>()? * 1024,
"MemAvailable:" => mem_available = value.parse::<u64>()? * 1024,
"SwapTotal:" => meminfo.swaptotal = value.parse::<u64>()? * 1024,
"SwapFree:" => meminfo.swapfree = value.parse::<u64>()? * 1024,
"Buffers:" => buffers = value.parse::<u64>()? * 1024,
"Cached:" => cached = value.parse::<u64>()? * 1024,
_ => continue,
}
}
}
meminfo.memfree += buffers + cached;
meminfo.memused = meminfo.memtotal - meminfo.memfree;
// NOTE: MemAvailable is the only metric that will actually represent how much memory is
// available for a new workload, without pushing the system into swap, no amount of calculating
// with BUFFER, CACHE, .. will get you there, only the kernel can know this.
// For details see https://git.kernel.org/torvalds/c/34e431b0ae398fc54ea69ff85ec700722c9da773
meminfo.memused = meminfo.memtotal - mem_available;
meminfo.swapused = meminfo.swaptotal - meminfo.swapfree;
@ -463,6 +466,76 @@ fn parse_proc_meminfo(text: &str) -> Result<ProcFsMemInfo, Error> {
Ok(meminfo)
}
#[test]
fn test_read_proc_meminfo() {
let meminfo = parse_proc_meminfo(
"MemTotal: 32752584 kB
MemFree: 2106048 kB
MemAvailable: 13301592 kB
Buffers: 0 kB
Cached: 490072 kB
SwapCached: 0 kB
Active: 658700 kB
Inactive: 59528 kB
Active(anon): 191996 kB
Inactive(anon): 49880 kB
Active(file): 466704 kB
Inactive(file): 9648 kB
Unevictable: 16008 kB
Mlocked: 12936 kB
SwapTotal: 3 kB
SwapFree: 2 kB
Zswap: 0 kB
Zswapped: 0 kB
Dirty: 0 kB
Writeback: 0 kB
AnonPages: 244204 kB
Mapped: 66032 kB
Shmem: 9960 kB
KReclaimable: 11525744 kB
Slab: 21002876 kB
SReclaimable: 11525744 kB
SUnreclaim: 9477132 kB
KernelStack: 6816 kB
PageTables: 4812 kB
SecPageTables: 0 kB
NFS_Unstable: 0 kB
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 16376292 kB
Committed_AS: 316368 kB
VmallocTotal: 34359738367 kB
VmallocUsed: 983836 kB
VmallocChunk: 0 kB
Percpu: 12096 kB
HardwareCorrupted: 0 kB
AnonHugePages: 0 kB
ShmemHugePages: 0 kB
ShmemPmdMapped: 0 kB
FileHugePages: 0 kB
FilePmdMapped: 0 kB
Unaccepted: 0 kB
HugePages_Total: 0
HugePages_Free: 0
HugePages_Rsvd: 0
HugePages_Surp: 0
Hugepagesize: 2048 kB
Hugetlb: 0 kB
DirectMap4k: 237284 kB
DirectMap2M: 13281280 kB
DirectMap1G: 22020096 kB
",
)
.expect("successful parsed a sample /proc/meminfo entry");
assert_eq!(meminfo.memtotal, 33538646016);
assert_eq!(meminfo.memused, 19917815808);
assert_eq!(meminfo.memfree, 2156593152);
assert_eq!(meminfo.swapfree, 2048);
assert_eq!(meminfo.swaptotal, 3072);
assert_eq!(meminfo.swapused, 1024);
}
#[derive(Clone, Debug)]
pub struct ProcFsCPUInfo {
pub user_hz: f64,