Figuring Out How Linux Commands Work: uptime
Tags: rust, bash, tutorial, unix
Published: 2019-11-09 06:03:03

## 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.

```sh
[delta@arcxus ~ ]$ uptime
 20:11:51 up  1:06,  2 users,  load average: 0.08, 0.17, 0.08
```

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.

```sh

[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
```

The files that are important for us from the list are
 - `/proc/uptime`  -> contains the uptime value in seconds

```sh
[delta@arcxus ~ ]$ cat /proc/uptime
13304.92 52970.68
```

- `/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

```sh
[delta@arcxus ~ ]$ cat /proc/loadavg
0.00 0.00 0.00 1/113 1739
```

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).


```rust
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))
}
```
When we run the program we get the uptime printed neatly

```sh
[delta@arcxus uptime ]$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/uptime`
uptime 2h:38m:23s
```

- 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.

```rust
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]))
}
```
The output now should start showing the load averages too.
```sh
[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

```

- 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`. 

```c
/* 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.  */
};

```
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    ................
```

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    ................
```
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    ................
```

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    ................
```
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.

```rust
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)
}

```

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

```sh
[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
```

The source code for this project is available on [GitHub](https://github.com/DeltaManiac/uptime-rs). If you found this tutorial interesting, leave me a note in the comments.

Cheers
Delta