Figuring Out How Linux Commands Work: uptime

delta_maniac

Harikrishnan Menon

Posted on November 9, 2019

Figuring Out How Linux Commands Work: uptime

What is uptime ?

Linux has a few nifty little commands like cat, top, ls. One particular command I've found interesting to use is uptime. Its a simple command that outputs how long the system has been up and hence the name uptime.

Lets start by figuring out what the output of uptime means.

[delta@arcxus ~ ]$ uptime
 20:11:51 up  1:06,  2 users,  load average: 0.08, 0.17, 0.08
Enter fullscreen mode Exit fullscreen mode

The man page of uptime gives the following description

uptime gives a one line display of the following information.
The current time, how long the system has been running, how many users are currently logged on, and the system load averages for the past 1, 5, and 15 minutes.

Figuring out how it works

In order to figure out how uptime works, we find the files opened when uptime executes using a program called strace and filter for the openat syscall.

open, openat — open file relative to directory file descriptor
The open() function shall establish the connection between a file and
a file descriptor. It shall create an open file description that
refers to a file and a file descriptor that refers to that open file
description.


[delta@arcxus ~ ]$ strace "uptime" 2>&1 | rg openat

 openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
 openat(AT_FDCWD, "/usr/lib/libprocps.so.7", O_RDONLY|O_CLOEXEC) = 3
 openat(AT_FDCWD, "/usr/lib/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
 openat(AT_FDCWD, "/usr/lib/libsystemd.so.0", O_RDONLY|O_CLOEXEC) = 3
 openat(AT_FDCWD, "/usr/lib/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
 openat(AT_FDCWD, "/usr/lib/librt.so.1", O_RDONLY|O_CLOEXEC) = 3
 openat(AT_FDCWD, "/usr/lib/liblzma.so.5", O_RDONLY|O_CLOEXEC) = 3
 openat(AT_FDCWD, "/usr/lib/liblz4.so.1", O_RDONLY|O_CLOEXEC) = 3
 openat(AT_FDCWD, "/usr/lib/libgcrypt.so.20", O_RDONLY|O_CLOEXEC) = 3
 openat(AT_FDCWD, "/usr/lib/libpthread.so.0", O_RDONLY|O_CLOEXEC) = 3
 openat(AT_FDCWD, "/usr/lib/libgpg-error.so.0", O_RDONLY|O_CLOEXEC) = 3
 openat(AT_FDCWD, "/proc/self/auxv", O_RDONLY) = 3
 openat(AT_FDCWD, "/proc/sys/kernel/osrelease", O_RDONLY) = 3
 openat(AT_FDCWD, "/sys/devices/system/cpu/online", O_RDONLY|O_CLOEXEC) = 3
 openat(AT_FDCWD, "/proc/self/auxv", O_RDONLY) = 3
 openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
 openat(AT_FDCWD, "/etc/localtime", O_RDONLY|O_CLOEXEC) = 3
 openat(AT_FDCWD, "/proc/uptime", O_RDONLY) = 3
 openat(AT_FDCWD, "/var/run/utmp", O_RDONLY|O_CLOEXEC) = 4
 openat(AT_FDCWD, "/proc/loadavg", O_RDONLY) = 4
Enter fullscreen mode Exit fullscreen mode

The files that are important for us from the list are

  • /proc/uptime -> contains the uptime value in seconds
[delta@arcxus ~ ]$ cat /proc/uptime
13304.92 52970.68
Enter fullscreen mode Exit fullscreen mode
  • /var/run/utmp -> contains the information about logged in users

Since this file is binary format we cannot cat to see the contents of the file.

  • /proc/loadavg -> contains the avg load on the cpu for the last 5, 10, 15 mins
[delta@arcxus ~ ]$ cat /proc/loadavg
0.00 0.00 0.00 1/113 1739
Enter fullscreen mode Exit fullscreen mode

Now we have all the information to implement our own uptime command. Lets go ahead and do that in Rust!

Implementing our own uptime

  • Reading from /proc/uptime

This is pretty easy to accomplish, the function get_uptime reads the contents of /proc/uptime converts it and returns a tuple of (hours,minutes,seconds).

use std::fs::File;
use std::io::prelude::Read;
use std::time::Duration;

fn main() -> std::io::Result<()> {
    let uptime = get_uptime()?;
    println!("uptime {}h:{}m:{}s", uptime.0, uptime.1, uptime.2);
    Ok(())
}

fn get_uptime() -> std::io::Result<(u64, u64, u64)> {
    let mut file = File::open("/proc/uptime")?;
    let mut c = String::new();
    file.read_to_string(&mut c)?;
    let uptime = Duration::from_secs_f32(
        c.split_whitespace()
            .next()
            .unwrap_or("0")
            .parse::<f32>()
            .unwrap_or(0.0),
    );
    let h = uptime.as_secs() / 3600;
    let m = (uptime.as_secs() - h * 3600) / 60;
    let s = uptime.as_secs() - h * 3600 - m * 60;
    Ok((h, m, s))
}
Enter fullscreen mode Exit fullscreen mode

When we run the program we get the uptime printed neatly

[delta@arcxus uptime ]$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/uptime`
uptime 2h:38m:23s
Enter fullscreen mode Exit fullscreen mode
  • Reading from /proc/loadavg

This function get_load_avg reads data from the /proc/loadavg file and returns it in a nice tuple for our program to use.

use std::fs::File;
use std::io::prelude::Read;
use std::time::Duration;

fn main() -> std::io::Result<()> {
    let uptime = get_uptime()?;
    let load = get_load_avg()?;
    println!(
        "uptime: {}h:{}m:{}s, load average: {} {} {}", 
        uptime.0, uptime.1, uptime.2,load.0, load.1, load.2
    );   
    Ok(())
}

fn get_uptime() -> std::io::Result<(u64, u64, u64)> {
    let mut file = File::open("/proc/uptime")?;
    let mut c = String::new();
    file.read_to_string(&mut c)?;
    let uptime = Duration::from_secs_f32(
        c.split_whitespace()
            .next()
            .unwrap_or("0")
            .parse::<f32>()
            .unwrap_or(0.0),
    );
    let h = uptime.as_secs() / 3600;
    let m = (uptime.as_secs() - h * 3600) / 60;
    let s = uptime.as_secs() - h * 3600 - m * 60;
    Ok((h, m, s))
}

fn get_load_avg() -> std::io::Result<(f32, f32, f32)> {
    let mut file = File::open("/proc/loadavg")?;
    let mut c = String::new();
    file.read_to_string(&mut c)?;
    let avg = c
        .split_whitespace() // split at whitespace
        .take(3) // take the first 3 values
        .map(|s| s.parse::<f32>().unwrap_or(0.0)) // convert each value to f32
        .collect::<Vec<f32>>(); // collect into vector
    Ok((avg[0], avg[1], avg[2]))
}
Enter fullscreen mode Exit fullscreen mode

The output now should start showing the load averages too.

[delta@arcxus uptime ]$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.37s
     Running `target/debug/uptime`
uptime: 3h:15m:9s, load average: 0 0.08 0.09

Enter fullscreen mode Exit fullscreen mode
  • Reading from /var/run/utmp

Since the /var/run/utmp is a binary file we cannot directly read the file like we did before, instead we will try to read the bytes directly from the file and parse it and extract the data we need. The utmp struct in /usr/include/bits/utmp.h defines the content in /var/run/utmp.

/* The structure describing the status of a terminated process.  This
   type is used in 'struct utmp' below.  */
struct exit_status
  {
    short int e_termination;    /* Process termination status.  */
    short int e_exit;           /* Process exit status.  */
  };


struct utmp
{
  short int ut_type;           /* Type of login.  */
  pid_t ut_pid;                /* Process ID of login process.  */
  char ut_line[32]             /* Devicename.  */
  char ut_id[4];               /* Inittab ID.  */
  char ut_user[32]             /* Username.  */
  char ut_host[256]            /* Hostname for remote login.  */
  struct exit_status ut_exit   /* Exit status of a process marked
                                  as DEAD_PROCESS.  */
  int32_t ut_session;          /* Session ID, used for windowing.  */
  struct
  {
    int32_t tv_sec;           /* Seconds.  */
    int32_t tv_usec;          /* Microseconds.  */
  } ut_tv;                    /* Time entry was made.  */
  int32_t ut_addr_v6[4];      /* Internet address of remote host.  */
  char __glibc_reserved[20];  /* Reserved for future use.  */
};

Enter fullscreen mode Exit fullscreen mode

A single entry in the /var/run/utmp file would be the length of this struct, which is 384 bytes. In order to read the user information from the file we read chunks of 384 and convert it into the struct format for further use. Observing the changes in /var/run/utmp when users are logged on we can see that a new entry is added for each user logged in, however when the user logs out the entry is not removed instead the fields are emptied.

Using a hexdump tool lets look at the contents of the /var/run/utmp.

The first 384 bytes is from address 0x00000000 to 0x0000017F

0x is used to denote a hexadecimal number
0x0F is decimal 15
0x12 is decimal 18

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 
00000000: 02 00 00 00 00 00 00 00 7E 00 00 00 00 00 00 00    ........~.......
00000010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000020: 00 00 00 00 00 00 00 00 7E 7E 00 00 72 65 62 6F    ........~~..rebo
00000030: 6F 74 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ot..............
00000040: 00 00 00 00 00 00 00 00 00 00 00 00 35 2E 33 2E    ............5.3.
00000050: 35 2D 61 72 63 68 31 2D 31 2D 41 52 43 48 00 00    5-arch1-1-ARCH..
00000060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000070: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000090: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000000a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000000b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000000c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000000d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000000e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000000f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000110: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000120: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000130: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000140: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000150: 00 00 00 00 D2 C4 C2 5D BF 83 02 00 00 00 00 00    ....RDB]?.......
00000160: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000170: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
Enter fullscreen mode Exit fullscreen mode

This does not show much information of the current user accessing the system, so lets look at the next set of 384 bytes from 0x00000180 to 0x000002FF

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000180: 06 00 00 00 BB 01 00 00 74 74 79 31 00 74 74 79    ....;...tty1.tty
00000190: 31 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    1...............
000001a0: 00 00 00 00 00 00 00 00 74 74 79 31 4C 4F 47 49    ........tty1LOGI
000001b0: 4E 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    N...............
000001c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000001d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000001e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000001f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000200: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000210: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000220: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000230: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000240: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000250: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000260: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000270: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000280: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000290: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000002a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000002b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000002c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000002d0: BB 01 00 00 DD C4 C2 5D 1F BA 01 00 00 00 00 00    ;...]DB].:......
000002e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000002f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
Enter fullscreen mode Exit fullscreen mode

We can't find anything relating to what we are looking for here, we move on to the next set 0x00000300 to 0x0000047F

 Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
0000300: 07 00 00 00 0B 02 00 00 70 74 73 2F 30 00 00 00    ........pts/0...
00000310: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000320: 00 00 00 00 00 00 00 00 74 73 2F 30 64 65 6C 74    ........ts/0delt
00000330: 61 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    a...............
00000340: 00 00 00 00 00 00 00 00 00 00 00 00 31 39 32 2E    ............192.
00000350: 31 36 38 2E 31 2E 36 00 00 00 00 00 00 00 00 00    168.1.6.........
00000360: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000370: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000380: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000390: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000003a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000003b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000003c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000003d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000003e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000003f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000400: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000410: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000420: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000430: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000440: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000450: 00 00 00 00 5C CE C2 5D 76 E9 0C 00 C0 A8 01 06    ....\NB]vi..@(..
00000460: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000470: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
Enter fullscreen mode Exit fullscreen mode

Ah! Finally we hit gold, 0x0000032C to 0x00000330 contains the username delta that has logged in, also 0x0000034C to 0x00000356 contains the host's ip address 192.168.1.6

Lets now compare the same addresses after the user has logged out to figure out what has changed

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
00000300: 08 00 00 00 0B 02 00 00 70 74 73 2F 30 00 00 00    ........pts/0...
00000310: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000320: 00 00 00 00 00 00 00 00 74 73 2F 30 00 00 00 00    ........ts/0....
00000330: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000340: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000350: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000360: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000370: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000380: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000390: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000003a0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000003b0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000003c0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000003d0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000003e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
000003f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000400: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000410: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000420: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000430: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000440: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000450: 00 00 00 00 8C CE C2 5D A4 AA 05 00 C0 A8 01 06    .....NB]$*..@(..
00000460: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
00000470: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00    ................
Enter fullscreen mode Exit fullscreen mode

We can see that 0x0000032C to 0x00000330 which contained the username delta is now empty and addresses 0x0000034C to 0x00000356 which contained the host's ip address 192.168.1.6 is also empty.

Lets use this piece of information to calculate the number of users who are currently using the system. The code in get_users

  • skips the first 2 sets of 384 bytes
  • then skips the first 4 fields (42 bytes) of the utmp struct
  • and then first 2 bytes of the ut_user field which is /0.

Since the length of ut_host is 32 bytes and we have already skipped 2 bytes, we read the remaining into a buffer of 30 bytes and check if its still an empty buffer. If it is not empty then a user is present and we increment the count for the number of users.

use std::fs::File;
use std::io::prelude::Read;
use std::io::prelude::Seek;
use std::io::SeekFrom;
use std::time::Duration;
fn main() -> std::io::Result<()> {
    let uptime = get_uptime()?;
    let load = get_load_avg()?;
    let count = get_users()?;
    println!(
        "uptime: {}h:{}m:{}s, load average: {} {} {}, users: {}",
        uptime.0, uptime.1, uptime.2, load.0, load.1, load.2, count
    );
    Ok(())
}

fn get_uptime() -> std::io::Result<(u64, u64, u64)> {
    let mut file = File::open("/proc/uptime")?;
    let mut c = String::new();
    file.read_to_string(&mut c)?;
    let uptime = Duration::from_secs_f32(
        c.split_whitespace()
            .next()
            .unwrap_or("0")
            .parse::<f32>()
            .unwrap_or(0.0),
    );
    let h = uptime.as_secs() / 3600;
    let m = (uptime.as_secs() - h * 3600) / 60;
    let s = uptime.as_secs() - h * 3600 - m * 60;
    Ok((h, m, s))
}

fn get_load_avg() -> std::io::Result<(f32, f32, f32)> {
    let mut file = File::open("/proc/loadavg")?;
    let mut c = String::new();
    file.read_to_string(&mut c)?;
    let avg = c
        .split_whitespace() // split at whitespace
        .take(3) // take the first 3 values
        .map(|s| s.parse::<f32>().unwrap_or(0.0)) // convert each value to f32
        .collect::<Vec<f32>>(); // collect into vector
    Ok((avg[0], avg[1], avg[2]))
}

fn get_users() -> std::io::Result<u32> {
    let mut file = File::open("/var/run/utmp")?;
    let mut count = 0;
    for i in 2..=file.metadata()?.len() / 384 { // skip 2 iterations 
        file.seek(SeekFrom::Start(384 * i + 42 + 2))?; // skip all the things 
        let mut data: [u8; 30] = [0; 30];
        file.read(&mut data)?;
        if data != [0; 30] {
            count = count + 1;
        }
    }
    Ok(count)
}

Enter fullscreen mode Exit fullscreen mode

Running our program now gives us the desired output similar to the uptime command.

[delta@arcxus uptime ]$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.44s
Running `target/debug/uptime`
uptime: 0h:4m:12s, load average: 0.06 0.26 0.14, users: 1
Enter fullscreen mode Exit fullscreen mode

The source code for this project is available on GitHub. If you found this tutorial interesting, leave me a note in the comments.

Cheers
Delta

💖 💪 🙅 🚩
delta_maniac
Harikrishnan Menon

Posted on November 9, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related