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.
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:
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
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 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, 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
This information was surprisingly complex to get right. Let's see why on each platform:
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.
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)
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.
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.
Before talking about how much "processor time" a process used, we first need to talk about how to list all of them.
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.
Calling the proc_listallpids
returns the list of all PIDs. (Yes that's all!)
On this platform too, we "simply" need to call a function: NtQuerySystemInformation
.
We need to call kvm_getprocs
which returns an array of kinfo_proc
.
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%.
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
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
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
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
A process memory is split between "resident" memory and "virtual" memory. Let's see how it's computed.
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.
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.
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.
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
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.
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.
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!
Getting this information on Windows was a lot more complicated and still only works on 64 bits pointer width processes...
NtQueryInformationProcess
to get ProcessBasicInformation
's content.ReadProcessMemory
with the PebBaseAddress
field we got from the previous call so we can get PEB
information.ReadProcessMemory
again but with the ProcessParameters
field from the PEB
structure so we can get RTL_USER_PROCESS_PARAMETERS
information.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:
VirtualQueryEx
function to fill the MEMORY_BASIC_INFORMATION
structure (for the RegionSize
and BaseAddress
fields).RegionSize - Environment.offset_from(BaseAddress)
.usize
and return it.From this point, it's simply encoding into UTF-16 and splitting between environment variables (they are separated with \0
just like on Linux).
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).
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.