Process Control

How to manage systems software

How do we control our systems software?

  • Starting and stopping processes, handling failure
  • Inter-process communication, exchanging data between processes
  • Configuring processes
  • Bare-metal execution without an OS

Example: cargo build

Live demo using cargo build --timings

The std::process module

use anyhow::Result;
use std::process::*;

fn main() -> Result<()> {
    let mut cmd = Command::new("ls").spawn()?;
    let status = cmd.wait()?;
    println!("ls finished with status code {status}");

    Ok(())
}

Inspecting the exit code of a child process

fn main() -> Result<()> {
    let mut cmd = Command::new("ls").spawn()?;
    let status = cmd.wait()?;

    match status.code() {
        Some(code) => {
            if status.success() {
                println!("ls finished successfully with code {code}")
            } else {
                println!("ls failed with status code {code}")
            }
        }
        None => {
            println!("ls terminated by signal")
        }
    }

    Ok(())
}

Accessing the output of ls

fn main() -> Result<()> {
    let mut cmd = Command::new("ls").spawn()?;
    cmd.wait()?;

    if let Some(mut stdout) = cmd.stdout.take() {
        // `stdout` implements `Read`, so we can redirect to *our* stdout:
        let mut buf = [0; 512];
        while let Ok(count) = stdout.read(&mut buf)
            && count != 0
        {
            std::io::stdout().write_all(&buf[..count])?;
        }
    }

    Ok(())
}

Pipes and the standard streams

  • Recall that all processes have three files assigned to them:
    • stdin for passing data from the parent to the child process
    • stdout for passing data from the child to the parent process
    • stderr for passing diagnostic information from the child to the parent process
  • These are conventions, you can e.g. write diagnostics to stdout instead of stderr
    • But most tools don’t do this!
  • The OS opens these files for every child process and lets the parent process access them
    • A form of interprocess communication

Interprocess Communication (IPC)

  • There is a whole zoo of IPC methods:

Comparing IPC approaches

Method Throughput Latency Ease of setup Structured data Sync complexity Cross-language Persistence Security / Isolation
Anonymous pipes + ++ ++ - + ++ - - +
Named pipes (FIFO) + ++ + - + ++ - +
Unix domain sockets ++ ++ + - + ++ - ++
Network sockets ++ + + - + ++ - -
Message queues (POSIX) ++ ++ - - + ++ - +
Shared memory ++ ++ - - - - - ++ - -
Memory-mapped files ++ ++ - - - - ++ + -
Files on disk - - - ++ - - ++ ++ -
RPC + + + ++ + ++ - ++
Message passing ++ + - + + ++ ++ ++
Signals - - ++ ++ - - - - + - - -
Threads ++ ++ + ++ - - - - - - -

Configuring program behavior

  • How to tell a program what to do?
    • Which file(s) should be compiled?
    • Which directory to change to?
    • Which commit message?
    • Which logging level?
  • Two standardized ways to do this for processes:
    • Command-line arguments
    • Environment variables
// POSIX function to execute a program
int execve(
    const char *pathname, 
    char *const argv[], /* command line arguments */
    char *const envp[]  /* environment variables */
)

Conventions for command line args

  • On Unix:
    • Single-letter arguments prefixed with single dash -
    • Longer arguments prefixed with double dash --
  • Types of command line args:
    • Flags: Boolean conditions (e.g. -l in ls -l)
    • Parameters: Values (e.g. the paths in cp ./source ./destination)
    • Named Parameters: Parameters identified by name (e.g. -m "message" in git commit -m "message" )

Environment variables

  • Key-value pairs: KEY=value (e.g. RUST_LOG=info)
  • Describe the environment in which a program runs (changes rarely)
    • Current user name, home path, timezone, log level, connection information (e.g. database URL)
  • Different from command line args, which describe the current execution of a program

In Rust

  • Main in Rust: fn main() {}
    • No arguments!
  • Command line arguments and environment variables are globally accessible through std::env:
    • std::env::args(): Iterator over command line args (strings)
    • std::env::vars(): Iterator over environment variables (key-value pairs)
  • For parsing command line arguments, crates such as clap are typically used