articles

sysinfo: how to extract systems' information

The sysinfo crate aims to provide systems' information.

This blog post will explain how it's done to allow you to get a better understanding on how things like CPU usage are computed. Please note that it will be mostly technical and is not specific to how sysinfo works but rather how it extracts systems' information.

It doesn't cover all the information provided by the sysinfo crate but only the things I found interesting to talk about. If enough people are interested, I will write other blog posts on this topic later on.

Table of content

System CPU usage

Let's start with the system CPU usage computation (not the CPU usage per process, we will explain this one below!). A CPU can execute up to a given amount of instructions in a (clock) cycle, so the more instructions it executes in a cycle, the higher its usage.

A very important thing to note though: this information can only be computed over a given amount of time. Now you might wonder why this limitation? The CPU time only grows over time (like the unix timestamp). So if you try to compute the CPU usage with only the current time, you will actually get the CPU usage from the boot of your computer (to simplify things), which wouldn't be very useful...

So if you want to compute the CPU usage more accurately, you have to do it over a given span of time. First, you get the CPU time and CPU information you need, then you wait for some time (like a second), get this information again and then compute the CPU usage by subtracting the "old" information from the "new" one.

Anyway, let's now check how to get this information:

Linux

On Linux, the information is in /proc/stat. The file looks like this:

cpu  7032633 6839 1618386 246526491 110129 0 1392 0 0 0
cpu0 441666 276 94030 15411157 7027 0 208 0 0 0
cpu1 521796 1252 92976 15336333 6958 0 86 0 0 0

The first line is the "global" usage (so all processors). If we want the details for each processor, we have to look at the other lines. If you're interested about what each value is, I recommend you to take a look at man proc and to look for /proc/stat (this manual is really great!).

To put it simply, we sum all values to get the "total time" of the processor which is composed of "idle time" (time spent not doing anything) and "work time". Then a simple computation and we have its usage:

cpu_usage = (new_work_time - old_work_time) / (new_total_time - old_total_time) * 100

macOS

On mac, we first need to get the number of processors by using sysctl. Then we call the host_processor_info function which fills an array of i32.

This array can be seen as an array of "chunk" where each "chunk" contains data for each processor. To go to next chunk, we add libc::CPU_STATE_MAX to our current position. Then we get the values we want. Pseudo-code time:

offset = 0
from 0 to nb_processors:
    value_we_want = cpu_info[offset + index_of_value_we_want]
    offset += libc::CPU_STATE_MAX

So in the end, we need to compute/get two things: the "in use" time and the "idle" time, then a simple computation:

total = idle + in_use
cpu_usage = in_use / total * 100

And that's it! As for the "global" CPU usage, we simply add all of them and divide the result by the number of processors.

Windows

Windows API provide information very differently as you'll see. To get the CPU usage, we need to use a "query engine" which is completely opaque. So firstly, we create a "connection" to the query engine using PdhOpenQueryA, then we create the query (which looks like "% Processor Time"), then we translate it (because otherwise it might not be found depending on the local language) using PdhLookupPerfNameByIndexW. Finally, we run the query using PdhGetFormattedCounterValue. That's it!

FreeBSD

FreeBSD, just like macOS, relies a lot on sysctl calls to retrieve system information. To get the system CPU usage, you will need to call sysctl with kern.cp_time and kern.cp_times. The first one needs an array of CPUSTATES elements whereas the seconds needs an array of CPUSTATES * nb_processors elements. Both are arrays of c_ulong types.

Then, it's the same for both once again: for each CPU (so cp_time and each CPU inside cp_times), you can compute the CPU usage as follows:

You go through the values and add all of them into a variable. You subtract the "old" values from this variable to get how much time your CPU used. Now to get how much of this time was used to compute things, you subtract the cpu[CP_IDLE] (both old and new!) from it.

Then the computation is:

cpu_usage = total_time_without_idle / total_time * 100

System memory information

This information was surprisingly complex to get right. Let's see why on each platform:

Linux

To get both swap and RAM usage, the information is in /proc/meminfo. The file looks like this:

MemTotal:       32499128 kB
MemFree:         3013464 kB
MemAvailable:    7337880 kB

So pretty easy to parse to get the values we need. Now we can start with the "funny" part.

First, all these values are not in kB but in KiB contrary to what the manual and the file says and sysinfo API returns kB so a conversion is needed. The discussion about this happened in this issue in case you want to look at the details.

Then you need to know which values to actually use to compute the used RAM. The computation is the following (I use the name from the /proc/meminfo file):

MemTotal - MemFree - Buffers - Cached - SReclaimable

For the other memory values, it's much easier. MemTotal for the total amount of RAM, MemAvailable for the available memory (which is different that the "free" memory!). Same for the swap: SwapTotal and SwapFree.

And of course, we need convert all of them from KiB to KB.

macOS

For the swap, no problem. We use sysctl with the VM_SWAPUSAGE argument. Almost the same goes for the total amount of RAM, we use sysctl as well but with the HW_MEMSIZE argument.

For these 2 values, they are returned as "bytes" so we need to divide by 1000 to convert them to KB.

The problem, once again, is the computation for the used RAM. To get it, we use the host_statistics64 function with the HOST_VM_INFO64 argument which fills the vm_statistics64 struct. It contains a lot of fields and they are all expressed in "page numbers", so to get the usage in KB, simply needs to multiply this number with the page size (which you can get using sysconf).

To get the free memory, nothing complicated, you only need to multiply the free_count field by the page size.

For the available memory however, it's a bit more tricky:

TotalRAM - (active_count + inactive_count + wire_count + speculative_count - purgeable_count)

Windows

Calling GlobalMemoryStatusEx gives you the total and available memory numbers. Please note that sysinfo considers the available memory as the free memory on Windows.

For the swap, we need to call GetPerformanceInfo and then we make the following computations (the PageSize is returned by GetPerformanceInfo):

swap_total = (CommitLimit - PhysicalTotal) * PageSize
swap_used  = (CommitTotal - PhysicalTotal) * PageSize

And finally, as all 4 values are in bytes, we divide them by 1000.

FreeBSD

For the next ones, we will need to get the "page size". You can get it by calling sysctl with vm.stats.vm.v_page_size.

To get the total memory available, we call sysctl with vm.stats.vm.v_page_count to get the number of pages available, then we multiply it by the page size (and divide it by 1000 to convert it to KB).

To get the used memory, we need vm.stats.vm.v_active_count (active memory) and vm.stats.vm.v_wire_count (wired memory). Then it's as follows:

used_memory = (active_memory * page_size) + (wired_memory * page_size) / 1000

To be noted that we should subtract "ZFS ARC" from the "wired memory" because it should belongs to cache but the kernel reports it as "wired memory" instead...

To get the free memory, we need vfs.bufspace, vm.stats.vm.v_inactive_count (inactive memory), vm.stats.vm.v_free_count (free memory) and vm.stats.vm.v_cache_count (cached memory). Then the computation is:

free_memory = buffers_mem
  + (inactive_memory * page_size)
  + (cached_memory * page_size)
  + (free_memory * page_size);

Then divide free_memory by 1000 to convert it to KB.

Another thing to note: the available memory doesn't seem to be provided by FreeBSD. In sysinfo, it's considered the same as the free memory.

And finally the swap. For this one, we need to call kvm_getswapinfo. To do so, we create an array of kvm_swap types of length 16. Why 16? It's a magic number coming from htop. When reading the source code of kvm_getswapinfo, I wasn't able to figure out how they got it.

kvm_getswapinfo returns how much entries it updated. Then you just need to iterate through the entries and add all ksw_used fields to get the used swap and all ksw_total to get the total swap.

Listing running processes

Before talking about how much "processor time" a process used, we first need to talk about how to list all of them.

Linux

The processes are all listed in /proc. All the folders being a number are actually a process. So the folder /proc/1 contains information about the process with the PID (for Process ID) 1. As simple as that.

macOS

Calling the proc_listallpids returns the list of all PIDs. (Yes that's all!)

Windows

On this platform too, we "simply" need to call a function: NtQuerySystemInformation.

FreeBSD

We need to call kvm_getprocs which returns an array of kinfo_proc.

Process CPU usage

We saw how to get the system CPU usage and how to list the running processes, now let's see how to get how much time a process used. An important thing to note is that contrary to a processor, a process CPU usage can be more than 100% because of multi-threading. If you have two threads running at 100%, then the total usage of the process is 200%.

Linux

As a reminder: for system CPU usage computation, we needed the divide the work time by the total time. To know how much a process used the CPU, we will divide the process time by the total time. The biggest question at this point is how to get a process' time.

You'll find it into /proc/[THE PROCESS PID]/stat. It is composed of the utime (user time) and the stime (system time). The file looks like this:

1 (systemd) S 0 1 1 0 -1 4194560 335815 45130177 1981 1535413 14596 10132 91899 33291 20 0 1 0 24 172781568 2878 18446744073709551615 1 1 0 0 0 0 671173123 4096 1260 0 0 0 17 0 0 0 36 0 0 0 0 0 0 0 0 0 0

Not very easy to read for humans, but great for parsing! (man proc if you want to learn what each value is.)

utime is the 13th value and stime is the 14th value.

Then the computation is as follows:

cpu_usage = ((utime - old_utime) + (stime - old_stime)) * nb_processors * 100 / total_time

macOS

On mac, there are two ways to compute a process CPU usage. For some reason, since the new macbook with M1 processors, the "old" one isn't accurate anymore. We'll only describe the "new" one here.

Before doing anything, we need to get the "system time interval". We get it by retrieving the "ticks" count from host_processor_info and then convert it to nanoseconds so we can use it afterward. This is a lot of not very interesting technical details but if you're interested anyway, you can take a look at how we do it here.

Then, we need to get the proc_taskinfo struct from the proc_pidinfo function. Then it's just a simple computation (pti is the proc_taskinfo struct):

cpu_usage = ((pti.stime + pti.utime) - (old_stime + old_utime)) / system_time_interval * 100

Windows

Once you have the process ID (or "process handle" as it's called on Windows), you need to get the process times (using GetProcessTimes) and the system times (using GetSystemTimes). Then this is (again) just a simple computation:

denominator = (global_kernel_time - old_global_kernel_time) + (global_user_time - old_global_user_time)
((kernel_time - old_kernel_time) + (user_time - old_user_time)) / denominator * 100

FreeBSD

The computation was surprisingly easy on FreeBSD: first we need to call sysctl with kern.fscale to get the "fscale". Then for each process, the kinfo_proc struct has a ki_pctcpu field. Then the computation is just:

100 * kinfo_proc.ki_pctcpu / fscale

Process memory information

A process memory is split between "resident" memory and "virtual" memory. Let's see how it's computed.

Linux

Once again, this information is available in /proc/[THE PROCESS PID]/stat (take a look at Process CPU usage for more information). The "virtual" memory is the 22nd and the "resident" memory is the 23rd entry.

And that's it.

macOS

In here, not very complicated either. First we need to call the proc_pidinfo function and we give it the PID, the PROC_PIDTASKINFO constant and the proc_taskinfo structure. Then we simply get the values from the fields: pti_resident_size for the resident memory and pti_virtual_size for the virtual memory.

Windows

When iterating on content returned by NtQuerySystemInformation, the information is stored in the SYSTEM_PROCESS_INFORMATION structure fields: WorkingSetSize for "resident" memory and VirtualSize for "virtual" memory.

FreeBSD

The kinfo_proc has all we need. To get the virtual memory used by the process:

virtual_memory = kinfo_proc.ki_size / 1000

And to get the memory:

memory = (kinfo_proc.ki_rssize * page_size) / 1000

Process environment variables

Getting the environment variables for a process was actually quite tricky. The support on Windows was actually merged very recently (you can see the pull request here). Anyway, let's check it out.

Linux

The information is in /proc/[THE PROCESS PID]/environ. Each environment variable ends with a \0, so it's pretty easy to transform it into a Vec<String> by splitting on this character.

macOS

This implementation is very tricky. We use sysctl with KERN_PROCARGS2 to get the process run arguments. A little extra explanation here: the main function in C looks like this:

int main(int argc, char **argv) {}

argc tells you how many arguments you received and argv contains these arguments. So for example:

ls -l some_folder

In here, argv will be (simplified): ["ls", "-l", "some_folder"] and argc will be 3.

But actually, the main function can receive a third argument:

int main(int argc, char **argv, char **env) {}

It's not always the case (old POSIX doesn't allow it), but to simplify things, we'll say it does (you can use extern environ; in both old and "less old" POSIX to get access to environment variables too).

So now you might wonder why I'm giving all this information? The answer is simply because thanks to the sysctl call, we have access to all the program's arguments, including the third one!

So first, we skip argc and argv to then extract the information we are looking for in env. Little drawing (also present in the sysinfo source code!):

/---------------\ 0x00000000
| ::::::::::::: |
|---------------| <-- Beginning of data returned by sysctl() is here.
| argc          |
|---------------| <-- First "argv" element.
| exec_path     |
|---------------|
| 0             |
|---------------|
| arg[0]        |
|---------------|
| 0             |
|---------------|
| arg[n]        |
|---------------|
| 0             |
|---------------| <-- First environment variable.
| env[0]        |
|---------------|
| 0             |
|---------------|
| env[n]        |
|---------------|
| ::::::::::::: |
|---------------| <-- Top of stack.
:               :
:               :
\---------------/ 0xffffffff

So just like I said: tricky!

Windows

Getting this information on Windows was a lot more complicated and still only works on 64 bits pointer width processes...

At this point, we're almost done. Since we have the RTL_USER_PROCESS_PARAMETERS structure filled, we can access its Environment field. But before going any further, we need to know the length of this field. To do so:

From this point, it's simply encoding into UTF-16 and splitting between environment variables (they are separated with \0 just like on Linux).

FreeBSD

We need to call the kvm_getenvv which returns an array of strings. That's it. The only thing to be careful of is that this function is not thread-safe (it uses static variables).

Conclusion

I think there is already a lot of information so let's stop here for today. As you can see, each operating system has very different API, which is both very interesting and complicated to handle correctly and why crates such as sysinfo are so convenient!

Like said at the beginning, if people want to know more about this, I'll write other blog posts covering other parts like the network interfaces or how to get a process disk I/O.

Posted on the 06/09/2021 at 11:30 by @GuillaumeGomez

Next article

sysinfo: version 0.22 and FreeBSD support

Previous article

Improvements for #[doc] attributes in Rust
Back to articles list
RSS feedRSS feed