As a user of i3 , I like to display information such as the battery level, CPU temperature or memory (RAM) usage at the bottom of my screen.

The i3status programme does not support displaying memory usage on OpenBSD, and
i3status-rust, an alternative written in Rust, is only compatible
with Linux.
So I decided to develop my own status bar
, in Rust,
and whilst developing the block that displays memory usage, I realised that calculating the
memory in use was a more difficult task than I’d anticipated. On my first
attempt, I actually miscounted 22 GiB of memory.
Memory in OpenBSD
Before going into detail about the various possible approaches to calculating
memory, it is important to have a basic understanding of how it is managed by
the OpenBSD kernel.
Firstly, we need to know whether we are referring to the total memory (hw.physmem),
or only the memory accessible to the user (hw.usermem), that is to say
hw.physmem minus the portion reserved by the kernel for its own operation. I have chosen
to base my calculations on hw.physmem, i.e. the total memory, including that reserved
by the kernel but excluding the portion retained by the firmware, to which the system
has no access.
To give an example: on a machine with 32 GiB of memory, one might imagine that hw.physmem is 31.75 GiB and hw.usermem is 31.10 GiB. This means that the machine’s firmware reserves 250 Mio and the OpenBSD kernel reserves 650 Mio.
The uvmexp structure for memory accounting
Now that we know what we’re talking about, let’s see how OpenBSD manages memory-related data. When using sysctl(2) to retrieve memory information, we get an uvmexp structure. Here are the fields we’re interested in:
pagesize: the size of a memory page, in bytes (e.g. 4096, or 4 Kio)npages: the total number of pages in memory (over 8.1 million on a machine with 31.75 GiB of RAM)free: the number of free pagesactive: the number of active pages, i.e. those in useinactive: the number of inactive pages, i.e. those that can be reclaimed if the system needs memorywired: the number of ‘wired’ pages, i.e. those reserved by the kernel or mlock(2) edvnodepages: the number of pages containing file datavtextpages: the number of pages containing executable programme data (a subset of vnodepages)
Before continuing, it may be useful to explain what the “vnode” and “vtext” caches are, and what they are used for. The principle is simple: when a file is read from the disk, it is stored in the cache to save time the next time a user wishes to read it again.
A practical example
Let’s imagine I’m using less to read a file called “file.txt”.
The system will load the contents of the less programme into the vtext cache;
if the executable is 144 KiB in size, this represents 36 pages of 4 KiB each stored
in the cache (I’m simplifying here; in reality, only the instructions are stored).
It then stores file.txt in the vnode cache; if it is 4 KiB in size, this represents
1 page stored in the cache. That was just the system part, but
file.txt is stored a second time in memory, on the heap (the memory area
where programmes dynamically allocate their data), so that less can access it,
which represents another 1 page in use.
Pages stored in the vtext cache are counted in the
vtextpages field (and in vnodepages, as vtextpages is a subset of it).
Pages stored in the vnode cache are counted in vnodepages. And
finally, pages stored on the heap are counted in active.
When less finishes running, the system reclaims the page that was marked
as active, which then becomes free, as it is available again.
However, the contents of the less binary and fichier.txt are kept in the cache.
If I need to run less again or read fichier.txt later, the
system will not need to read them from the disk again; it will be able to
access them quickly from the cache.
However, a distinction can be made: whilst less was running, the pages
in the cache were being used, but now that it has finished, the pages are
no longer in use; they are there “just in case”.
It is this “just in case” aspect that makes the measurement tricky.
Different approaches to measuring memory
This is where it gets interesting. When you want to display a simple percentage, as in the screenshot at the top of the article, to show how much memory is in use, you need to know:
- The total amount of memory.
- The amount of memory in use.
These two points raise two questions:
- Is the total amount more accurately represented by
hw.physmemorhw.usermem? There is no right or wrong answer to this question; it depends on whether the developer considers that the memory reserved by the kernel should be included or not. This is not memory that the user will be able to reclaim even if they close all their programmes, so one might be tempted to preferhw.usermem. On the other hand, the user might find it inconsistent to have a total amount of memory that is smaller, given that the kernel’s memory also belongs to them, so one might be tempted to usehw.physmem.
I have chosen, in my programme, for the total amount to behw.physmem, but that doesn’t mean it’s the right answer; it’s simply a choice. - What do we mean by ‘memory in use’? The easy part is to
consider that
freeandinactiverepresent available memory, and thatactiveandwiredrepresent memory in use. And the cache,vnodepagesandvtextpages– are these available or used memory? Technically, the cache can be reclaimed if the system needs memory, so we could consider this memory to be available; but on the other hand, part of the cache is actively in use, so we could consider the cache to be occupied memory.
Approach 1: the cache is memory in use
When I first tried to develop the block that displays memory usage in
my programme, I forgot to take the cache into account, thinking that free
included all available memory, cache or otherwise. Here is the calculation:
total_memory = npages * pagesize
free_memory = (free + inactive) * pagesize
used_memory = total_memory - free_memory
It was unintentional, but by counting only the free and inactive pages
as available, I was counting the cache as used memory. It took me
a while to realise why my programme was showing 80 per cent memory usage.
When using a lightweight system like OpenBSD and a minimalist window manager
such as i3, consuming 25 GiB of RAM with just a web browser
and two terminal emulators open seems impossible.
After digging deeper, I realised that the vnode and vtext caches
took up 22 GiB. As I didn’t have a breakdown of what was actively in use and what wasn’t,
I assumed that most of the cache was there ‘just in case’; given my usage
at the time, I couldn’t possibly have been actively using 22 GiB of files and
programmes.
As a user of my own programme, this approach did not suit me, as it gave the impression that my memory was almost full, whereas in reality the majority of the cache was certainly unused. Showing 80 per cent of memory as used was misleading.
Approach 2: the cache is available memory
As I wasn’t satisfied with the initial approach, I decided to reverse my calculation and treat the cache as available memory.
total_memory = npages * pagesize
used_memory = (active + wired) * pagesize
free_memory = total_memory - used_memory
This time, only the active and wired pages were counted as
used memory; the rest was available memory.
It should be noted that this isn’t perfect either; if I go back to my example with
less reading a file, the vnode and vtext caches whilst the
programme is running will be treated as available memory, whereas in reality
this memory is in use.
With such a small example, you don’t realise it, but if I were to use
Emacs (whose executable is ~8 Mio) to open a 1 GiB file, it would make
a real difference to memory usage, and not accounting for it is also a problem.
Conclusion
There is no right answer between these two approaches; both are flawed. I considered the second approach to be the least bad and, above all, the closest to the truth. If one day OpenBSD allows us to find out how much of the cache is actively in use, I will improve my programme.
And what about the others? How do they manage?
Waybar
, a toolbar that works with Sway (an alternative to
i3 that runs on Wayland rather than X11), takes the same approach as I do.
The programme uses hw.physmem to calculate total memory and counts
the vnode and vtext caches as available memory.
The i3status project has received a proposal
from a
contributor to manage memory on OpenBSD, and this time it’s a little different:
it considers total memory to be hw.usermem, and available memory to be
free plus the caches, excluding inactive.
This experience has enabled me to delve into the OpenBSD kernel, understand
how it manages memory, and also to finally see the amount of memory
displayed on my screen.
My programme, i3status-again
, is designed to be
portable across several operating systems; I’ll see how the others
handle this aspect of memory management.
Understanding how your system manages its resources is part of my
day-to-day work.
If your servers deserve the same level of attention,
Find out about my services
or contact me
.