Skip to content

Exploring Linux

Time: 90 min | Prerequisites: Any Linux host (Section 0); SSH Login (Sections 1--6 on the RPi)

The Linux Layered Stack and "Everything Is a File"

An embedded Linux system is built from three layers. At the bottom is the hardware -- the SoC, RAM, and peripherals. Above it sits the kernel, which manages hardware access, scheduling, and memory protection. On top is user space, where your applications, services, and shell sessions run. All communication between user space and hardware passes through the kernel via system calls.

One of the most powerful abstractions in Linux is that everything is a file. Hardware devices appear as entries in /dev/ (e.g., /dev/i2c-1 for the I2C bus). Kernel runtime information is exposed through virtual filesystems: /proc/ provides per-process data and system statistics (CPU info, memory, uptime), while /sys/ exposes hardware attributes (sensor readings, GPIO states, thermal zones). None of these "files" exist on disk -- the kernel generates their contents on the fly when you read them.

This means the same tools you use for regular files (cat, echo, ls) work for inspecting and controlling hardware. Reading a sensor can be as simple as cat /sys/class/thermal/thermal_zone0/temp. This uniform interface is what makes shell scripting so powerful on embedded Linux.

For deeper reading see the Embedded Linux Overview and Linux Fundamentals reference pages.


Learning Objectives

By the end of this lab you will be able to:

  • Navigate the Linux filesystem and explain the role of /dev/, /sys/, /proc/
  • Inspect running processes, identify resource usage, and understand scheduling
  • Read and interpret file permissions and apply them to device access
  • Use pipes, redirects, and shell tools to explore and debug an embedded system

0. Explore Your Host (No RPi Needed)

Info

This section runs on any Linux machine — your lab PC, a laptop with Ubuntu, or WSL on Windows. It covers the same core commands you will later use on the Raspberry Pi. If the RPis are not available yet, start here.

Your Identity and Environment

whoami          # Who am I?
id              # User ID, group memberships
echo $SHELL     # Which shell am I running?
echo $PATH      # Where does the shell look for programs?
env | head -10  # First 10 environment variables

System Information

uname -a                # Kernel version, architecture, hostname
cat /proc/cpuinfo | head -20   # CPU details
cat /proc/meminfo | head -5    # Memory details
free -h                 # Human-readable memory summary
df -h                   # Disk usage
lsmod | head -10        # Loaded kernel modules
Tip

Compare uname -a output on your host vs. the RPi later. The architecture will be different (x86_64 vs aarch64), but the commands are identical — that is the power of a standardized OS.

Files and Directories

ls -la                  # List all files (including hidden) with details
file ~/.bashrc          # What type of file is this?
mkdir ~/lab_practice    # Create a directory
cd ~/lab_practice
touch myfile.txt        # Create an empty file
ls -la                  # See ownership, permissions, timestamps
cd ~/lab_practice
echo "hello" > original.txt
ln -s original.txt soft_link    # Symbolic (soft) link
ln original.txt hard_link       # Hard link
ls -la                          # Compare: soft link shows →, hard link has same inode
rm original.txt
cat soft_link                   # Broken — target gone
cat hard_link                   # Still works — data persists

Redirects and Pipes

echo "Hello world" > ~/lab_practice/greet.txt        # Write (overwrite)
echo "Hello again" >> ~/lab_practice/greet.txt       # Append
cat ~/lab_practice/greet.txt                          # Read
grep Hello ~/lab_practice/greet.txt                   # Search
history | grep touch                                  # Pipe: filter command history

Background Jobs and Job Control

watch -n1 date     # Live-updating clock
# Press Ctrl+Z to suspend it
fg                 # Bring it back to foreground
# Press Ctrl+C to stop it
sleep 60 &         # Run in background
jobs               # List background jobs
kill %1            # Kill job #1

Network and Time

ip link             # Network interfaces (or: ip l)
ip address          # Network addresses (or: ip a)
hostname            # Machine hostname
date                # Current date and time
date "+%s"          # Unix epoch (seconds since 1970-01-01)

Shell Navigation Tricks

These keyboard shortcuts work in any Bash session and will save you a lot of typing:

# Ctrl+R     → reverse search history (type a fragment, keep pressing Ctrl+R to cycle)
# Ctrl+A / E → jump to beginning / end of line
# Ctrl+W     → delete word backward
# Ctrl+L     → clear screen (faster than typing 'clear')
# Ctrl+T     → swap last two characters (quick typo fix)

# Re-run last command with sudo prepended
sudo !!

# Grab the last argument of the previous command
ls /sys/class/thermal/thermal_zone0/
cd !$                   # cd /sys/class/thermal/thermal_zone0/

# Go back to previous directory
cd /etc
cd /var/log
cd -                    # back to /etc
Tip

Ctrl+R is the single most useful shortcut. Instead of pressing ↑ fifty times to find a command, just press Ctrl+R and type a few letters of what you remember.

Brace Expansion and Quick Copies

# Create multiple directories at once
mkdir -p project/{src,include,build,docs}

# Create a timestamped backup of a file
cp myfile.conf{,.bak.$(date +%F)}   # creates myfile.conf.bak.2026-02-19

# Generate sequences
echo {1..5}             # 1 2 3 4 5
echo file{A,B,C}.txt    # fileA.txt fileB.txt fileC.txt

Checkpoint 0a

Question Your Answer
Your username (whoami)
CPU architecture (uname -m)
Total RAM (free -h)
Root filesystem usage (df -h /)
Soft link after deleting target — what happens?

Your First Script

Everything above was interactive — type a command, see output. Real embedded work requires scripts: saved sequences of commands that run unattended.

Create a scripts directory and your first script — copy-paste the entire block:

mkdir -p ~/scripts
cat << 'EOF' > ~/scripts/sysinfo.sh
#!/bin/bash
set -euo pipefail

# Variables and command substitution
HOSTNAME=$(hostname)
KERNEL=$(uname -r)
ARCH=$(uname -m)
TIMESTAMP=$(date +%Y-%m-%dT%H:%M:%S)
MEM_TOTAL=$(grep MemTotal /proc/meminfo | awk '{print $2}')
MEM_FREE=$(grep MemAvailable /proc/meminfo | awk '{print $2}')
PROC_COUNT=$(ps aux | wc -l)

echo "=== System Report ==="
echo "Time:      $TIMESTAMP"
echo "Host:      $HOSTNAME"
echo "Kernel:    $KERNEL ($ARCH)"
echo "Memory:    ${MEM_FREE} kB free of ${MEM_TOTAL} kB"
echo "Processes: $PROC_COUNT"
EOF
chmod +x ~/scripts/sysinfo.sh
Tip

cat << 'EOF' > file is a heredoc — it writes everything between << 'EOF' and EOF into the file. The single quotes around 'EOF' prevent the shell from expanding $variables while writing. This is the fastest way to create files from copy-paste — no editor needed. You can also use vim ~/scripts/sysinfo.sh or nano ~/scripts/sysinfo.sh to create and edit scripts interactively.

Run it:

~/scripts/sysinfo.sh

You should see output like this (with your machine's values):

=== System Report ===
Time:      2026-02-19T10:30:15
Host:      lab-pc
Kernel:    6.1.0-28-amd64 (x86_64)
Memory:    3215420 kB free of 8108240 kB
Processes: 187

Verify: does the kernel version match uname -r? Does the hostname match hostname?

Tip

#!/bin/bash is the shebang — it tells the kernel which interpreter to use for this file.

Note

What does set -euo pipefail mean?

set is a shell built-in that changes how bash behaves. Each flag turns on a safety net:

Flag Meaning Without it...
-e Exit on error — stop the script immediately if any command fails The script silently continues after errors, potentially doing damage
-u Undefined variables are errors — stop if you use a variable that was never set $TYPO silently expands to an empty string, causing subtle bugs
-o pipefail Pipe failures are errors — a pipeline fails if any command in it fails, not just the last one failing_cmd \| grep ok reports success because grep succeeded

Try it yourself — see the difference:

# Without -e: the script continues after the error
bash -c 'ls /nonexistent; echo "still running"'

# With -e: the script stops at the error
bash -c 'set -e; ls /nonexistent; echo "never reached"'

# Without -u: typo in variable name silently gives empty string
bash -c 'echo "Hello $NAEM"'

# With -u: typo is caught
bash -c 'set -u; echo "Hello $NAEM"'

Always use set -euo pipefail as the first line after the shebang. It is the difference between a script that fails loudly (so you can fix it) and one that fails silently (so you debug for hours).

Edit Your Script

The heredoc created the file — now open it and make a change. Use whichever editor you prefer:

vim ~/scripts/sysinfo.sh        # or: nano ~/scripts/sysinfo.sh

Task: Add a new line that prints the system uptime. Add this line before the final echo "Processes: line:

UPTIME=$(cat /proc/uptime | awk '{printf "%.0f min", $1/60}')

and a matching echo:

echo "Uptime:    $UPTIME"

If you are using vim: press /Processes to search for the line, press O (capital O) to open a new line above, type the two lines, press Esc, then :wq to save.

If you are using nano: use Ctrl+W to search for "Processes", type the new lines above it, then Ctrl+O to save and Ctrl+X to exit.

Run the script again to see your change:

~/scripts/sysinfo.sh

You should now see a new Uptime: line in the output. If it is missing, open the file again and check that the new lines are in the right place — before the echo "Processes:" line.

Tip

This is the typical embedded workflow: paste a script to create it quickly, then edit it to adapt. On a minimal system without clipboard support, you may need to type changes by hand — that is where knowing vim pays off.

Conditionals and Loops

Add a second script that checks for files and iterates:

cat << 'EOF' > ~/scripts/check_and_count.sh
#!/bin/bash
set -euo pipefail

# Conditional: does a file exist?
if [[ -f /proc/cpuinfo ]]; then
    CORES=$(grep -c "^processor" /proc/cpuinfo)
    echo "CPU cores: $CORES"
else
    echo "Cannot read CPU info" >&2
fi

# Loop: iterate over thermal zones (if any exist)
FOUND=0
for zone in /sys/class/thermal/thermal_zone*/temp; do
    [[ -f "$zone" ]] || continue
    TEMP=$(cat "$zone")
    NAME=$(basename "$(dirname "$zone")")
    echo "$NAME: $(( TEMP / 1000 ))°C"
    FOUND=$(( FOUND + 1 ))
done

if [[ $FOUND -eq 0 ]]; then
    echo "No thermal zones found (normal on some hosts/VMs)"
fi

# Loop: top 5 memory-consuming processes
echo ""
echo "=== Top 5 Memory Users ==="
ps aux --sort=-%mem | head -6 | awk 'NR>1 {printf "  %-6s %5s%%  %s\n", $2, $4, $11}'
EOF
chmod +x ~/scripts/check_and_count.sh

Run it:

~/scripts/check_and_count.sh

You should see the CPU core count, any thermal zones with temperatures, and the top 5 memory users. If you see "No thermal zones found" — that is normal on some hosts and VMs; it will work on the RPi.

Arguments and Exit Codes

Scripts become much more useful when they accept arguments from the command line:

cat << 'EOF' > ~/scripts/check_device.sh
#!/bin/bash
set -euo pipefail

# $1 = first argument, $2 = second, $# = number of arguments
if [[ $# -eq 0 ]]; then
    echo "Usage: $0 <device_path> [device_path2 ...]" >&2
    exit 1          # exit with error code 1
fi

# Loop over all arguments with "$@"
for DEV in "$@"; do
    if [[ -e "$DEV" ]]; then
        PERMS=$(ls -la "$DEV" | awk '{print $1}')
        echo "[OK]   $DEV  ($PERMS)"
    else
        echo "[MISS] $DEV"
    fi
done

exit 0              # exit with success code 0
EOF
chmod +x ~/scripts/check_device.sh

Try it with different arguments:

~/scripts/check_device.sh                              # no args → error
~/scripts/check_device.sh /dev/null                    # one device
~/scripts/check_device.sh /dev/null /dev/zero /dev/i2c-1   # multiple devices

Check the exit code of the last command:

~/scripts/check_device.sh /dev/null
echo $?             # 0 = success

~/scripts/check_device.sh
echo $?             # 1 = error (no arguments given)
Tip

Exit codes are how scripts communicate success or failure. 0 means success, anything else means error. Every command sets $? — try ls /nonexistent; echo $? to see a failing command's exit code.

While Loops and Reading Input

for loops iterate over a list. while loops repeat until a condition is false — most commonly used to read lines from files or command output:

# Read /proc/mounts line by line, print filesystem types
while read -r _ mountpoint fstype _rest; do
    echo "$mountpoint$fstype"
done < /proc/mounts
# Process command output line by line with a pipe
ps aux | while read -r user pid _rest; do
    [[ "$user" == "root" ]] && echo "Root process: PID $pid"
done | head -5
Tip

read -r splits each line into variables by whitespace. The _ and _rest variables are throwaway — a convention for fields you do not need.

Text Processing with grep and awk

These are the two tools you will use most for log analysis and debugging:

# grep: filter lines matching a pattern
dmesg | grep -i "error"              # Find errors in kernel log
ps aux | grep bash                    # Find bash processes
grep -c "^processor" /proc/cpuinfo   # Count CPU cores

# awk: extract and format fields
df -h | awk 'NR>1 {print $6, $5}'                # Mount point + usage%
ps aux --sort=-%mem | awk 'NR<=6 {print $4, $11}' # Memory% + command
free -m | awk '/^Mem:/ {printf "%.0f%% used\n", $3/$2*100}'

Useful Combos

# Count unique items — sort first, then count
ps aux | awk '{print $1}' | sort | uniq -c | sort -rn   # processes per user

# tee: see output AND save to file at the same time
dmesg | grep -i error | tee ~/errors.txt

# column: format messy output into aligned columns
mount | column -t

# Find the 10 largest files in a directory tree
du -ah ~/  2>/dev/null | sort -rh | head -10

# Simple stopwatch — measures how long any command takes
time sleep 2

Aliases — Teach Your Shell Shortcuts

Tired of typing the same long commands? Create aliases:

# Try these in your terminal right now (temporary — gone after logout)
alias ll='ls -lah'
alias psmem='ps aux --sort=-%mem | head -10'
alias ports='ss -tlnp'
alias dmesg='dmesg --color=always'

To make them permanent, append them to ~/.bashrc:

cat << 'EOF' >> ~/.bashrc

# My aliases
alias ll='ls -lah'
alias la='ls -A'
alias ..='cd ..'
alias ...='cd ../..'
alias psmem='ps aux --sort=-%mem | head -10'
alias pscpu='ps aux --sort=-%cpu | head -10'
alias ports='ss -tlnp'
alias myip='hostname -I'
alias h='history | tail -20'
EOF
source ~/.bashrc
Warning

Note the double >> — this appends to the file. A single > would overwrite your entire .bashrc. You can also open it with vim ~/.bashrc or nano ~/.bashrc if you prefer to edit manually.

Tip

On a minimal embedded system you might not have ~/.bashrc. Use ~/.profile instead — it works on any POSIX shell. You can also use alias inside scripts, but functions (shown below) are usually more flexible.

vim Survival Guide

On your lab PC you have nano, but on embedded systems vim (or its minimal version vi) is often the only editor available. vim is friendlier than plain vi — it has syntax highlighting, better undo, and visual feedback. On most Linux distributions vim is already installed or aliased to vi.

Check which one you have:

vim --version | head -1     # "VIM - Vi IMproved" = you have vim
vi --version 2>&1 | head -1 # on minimal systems this may be BusyBox vi

Open a file (creates it if it does not exist):

vim ~/lab_practice/notes.txt

vim has two main modes:

Mode Purpose How to enter
Normal Navigate, delete, copy, paste Press Esc (you start here)
Insert Type text — you see -- INSERT -- at the bottom Press i, a, or o

The essential commands:

── Entering Insert Mode ──────────────────
i       Insert before cursor
a       Insert after cursor
o       Open new line below and insert
O       Open new line above and insert
Esc     Back to Normal mode

── Navigation (Normal mode) ──────────────
h j k l     Left, Down, Up, Right (arrow keys also work)
gg          Go to first line
G           Go to last line
0           Go to beginning of line
$           Go to end of line
w           Jump to next word
b           Jump to previous word
/pattern    Search forward (press n for next, N for previous)

── Editing (Normal mode) ─────────────────
dd          Delete (cut) entire line
yy          Copy (yank) entire line
p           Paste below current line
u           Undo (vim supports multiple undo levels, vi only one)
Ctrl+R      Redo (vim only)
x           Delete character under cursor
dw          Delete word
cw          Change word (deletes and enters Insert mode)

── Save and Quit ─────────────────────────
:w          Save (write)
:q          Quit (fails if unsaved changes)
:wq         Save and quit
:q!         Quit without saving (discard changes)
ZZ          Save and quit (shortcut for :wq)

── Useful extras (vim only) ──────────────
:set number     Show line numbers
:set nonumber   Hide line numbers
:syntax on      Enable syntax highlighting
:%s/old/new/g   Replace all occurrences of "old" with "new"

Practice — try editing a file:

vim ~/lab_practice/notes.txt
# Press i, type some text, press Esc
# Press dd to delete a line, press u to undo
# Press /text to search, n to jump to next match
# Type :wq to save and quit
Tip

The most common mistake: typing text while in Normal mode. If things go wrong, press Esc first, then u to undo. When in doubt: Esc Esc :q! gets you out without saving.

Note

On BusyBox-based embedded systems (Buildroot) you only get minimal vi — no syntax highlighting, no multi-level undo, no Ctrl+R redo. The core commands (insert, navigate, save, quit) work the same on both.

Putting It Together — A Reusable Function

cat << 'EOF' > ~/scripts/report_lib.sh
#!/bin/bash
# Library: source this from other scripts

system_summary() {
    echo "[$(date +%H:%M:%S)] $(hostname) | kernel $(uname -r) | $(ps aux | wc -l) procs | $(free -m | awk '/^Mem:/ {print $7}') MB available"
}

file_exists() {
    if [[ -e "$1" ]]; then
        echo "  [OK]   $1"
        return 0
    else
        echo "  [MISS] $1"
        return 1
    fi
}
EOF

Test it:

source ~/scripts/report_lib.sh
system_summary
file_exists /proc/cpuinfo
file_exists /dev/i2c-1
Tip

source (or .) runs a script in the current shell so its functions become available. This is how you build reusable libraries in bash — the Shell Scripting tutorial uses this pattern for sensor libraries on the RPi.

Your First Python Script

Bash is great for quick automation, but for anything more complex — data processing, networking, hardware interaction — Python is the standard tool on embedded Linux. (Historically, Perl and Tcl filled this role, and you will still find Perl scripts in many Linux system tools — but Python has largely replaced them for new development.) Let's see how it works on the same system.

Check that Python is available:

python3 --version
which python3           # where is the binary?

Create a Python script that reads system info — the same task as sysinfo.sh, but in Python:

cat << 'EOF' > ~/scripts/sysinfo.py
#!/usr/bin/env python3
"""System information script — Python version."""

import os
import sys
from datetime import datetime
from pathlib import Path


def read_file(path):
    """Read a file and return its contents, or None if it doesn't exist."""
    try:
        return Path(path).read_text().strip()
    except (FileNotFoundError, PermissionError):
        return None


def get_memory_kb(field):
    """Extract a memory field from /proc/meminfo."""
    meminfo = read_file("/proc/meminfo")
    if meminfo is None:
        return 0
    for line in meminfo.splitlines():
        if line.startswith(field):
            return int(line.split()[1])
    return 0


def get_cpu_temp():
    """Read CPU temperature from sysfs (returns °C or None)."""
    raw = read_file("/sys/class/thermal/thermal_zone0/temp")
    if raw is None:
        return None
    return int(raw) / 1000


def main():
    print("=== System Report (Python) ===")
    print(f"Time:      {datetime.now():%Y-%m-%dT%H:%M:%S}")
    print(f"Host:      {os.uname().nodename}")
    print(f"Kernel:    {os.uname().release} ({os.uname().machine})")

    mem_total = get_memory_kb("MemTotal")
    mem_free = get_memory_kb("MemAvailable")
    print(f"Memory:    {mem_free} kB free of {mem_total} kB")

    temp = get_cpu_temp()
    if temp is not None:
        print(f"CPU Temp:  {temp:.1f} °C")

    # Command-line argument: optional path to check
    if len(sys.argv) > 1:
        path = sys.argv[1]
        exists = "exists" if Path(path).exists() else "NOT found"
        print(f"Device:    {path} → {exists}")


if __name__ == "__main__":
    main()
EOF
chmod +x ~/scripts/sysinfo.py

Run it:

~/scripts/sysinfo.py
~/scripts/sysinfo.py /dev/i2c-1        # with an optional argument

You should see the same system information as the bash version, plus a CPU Temp: line if thermal zones are available. The second run adds a Device: line showing whether the path exists.

Compare the output with the bash version:

~/scripts/sysinfo.sh
~/scripts/sysinfo.py
Tip

Notice how Python reads /proc/ and /sys/ as regular files — the same virtual filesystems you explored earlier. This works because on Linux everything is a file, and Python's standard open() / Path.read_text() can access kernel interfaces directly. No special library needed.

Note

#!/usr/bin/env python3 is the portable shebang for Python — it finds python3 wherever it is installed, which may differ between your host and the RPi. The bash equivalent is #!/bin/bash.

Checkpoint 0b

Question Your Answer
What does set -euo pipefail do?
Output of your sysinfo.sh — kernel version?
CPU cores found by check_and_count.sh?
What does >&2 mean?
What does $? return after a successful command?
How do you save and quit in vim?
sysinfo.sh vs sysinfo.py — same output? Any differences?

Connect to the Raspberry Pi

Info

Everything above works identically on the Raspberry Pi. The difference: the RPi also has /dev/i2c*, /dev/spi*, GPIO, and other hardware-specific devices that you cannot explore on your host. Sections 1–6 below run on the RPi.

If you have not set up SSH access yet, complete the SSH Login tutorial first, then come back here.

Find your Pi's IP address — it is on the sticker on the device in the lab. Then connect:

ssh linux@192.168.x.y

Replace 192.168.x.y with your Pi's actual IP address. The username is linux and the lab password is passwd.

If this is your first connection, SSH will ask you to accept the host key fingerprint — type yes:

The authenticity of host '192.168.28.248 (192.168.28.248)' can't be established.
ED25519 key fingerprint is SHA256:Yvs56UL1A153xUwu/WxbxWBoSNm6+L7p+VdKfPxeI5s.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes

After login you should see the Pi's prompt:

linux@eslinux:~ $
Tip

Connection refused or timeout? Check that the Pi is powered (green LED blinks during boot), verify the IP with ping 192.168.x.y, and make sure you are on the same network.

Verify you are on the Pi:

uname -m        # should show aarch64 (not x86_64)
hostname        # should show the Pi's hostname
Tip

Try running your scripts on the Pi too — copy them over with scp ~/scripts/*.sh linux@192.168.x.y:~/scripts/ or just re-create them with the heredocs. Compare the output: different architecture, different kernel, same scripts.


1. Processes — Who Is Running?

Every program on Linux is a process with its own memory, permissions, and priority. On a microcontroller you have one program; here you have dozens.

Step 1: List All Processes

ps aux

Count them:

ps aux | wc -l
Tip

On stock Raspberry Pi OS you will see 80-120 processes. On a Buildroot minimal image, this drops to 5-10. The difference is all the services you did not ask for.

Step 2: See the Process Tree

pstree -p

Find PID 1. A PID (Process ID) is a unique number assigned by the kernel to each running process. PID 1 is always the first user-space process — systemd, the init process that starts everything else. Every other process is a descendant of PID 1.

Step 3: Interactive View

htop

Observe: - CPU bars at the top — your Pi has 4 cores - MEM bar — how much RAM is in use - PRI and NI columns — PRI is the kernel's internal scheduling priority, and NI is the nice value (range -20 to 19, default 0). A lower nice value means higher priority — a process with nice -20 gets more CPU time than one with nice 19. Regular users can only increase nice (lower their priority); only root can decrease it.

Note

Press F6 in htop to sort by different columns. Sort by %MEM to find the biggest memory user.

Bonus: Quick Process Tricks

# Which process is using a specific port?
sudo ss -tlnp | grep :22                   # who is listening on SSH port?

# Kill a process by name instead of PID
pkill -f "sleep 300"                        # kills all matching processes

# Run a command and measure its execution time
time ls -lR /sys/class/ > /dev/null

# Watch process count change live
watch -n 2 'ps aux | wc -l'

Checkpoint 1

Fill in:

Question Your Answer
Total process count
PID 1 process name
Biggest memory user
Number of CPU cores shown

2. Filesystem — Where Is Everything?

Linux organizes everything in a single directory tree. Three special directories are critical for embedded work — and none of them exist on disk. They are virtual (pseudo) filesystems: the kernel creates them in memory at boot and populates them dynamically. They consume no disk space, and their contents change as hardware and processes come and go.

Step 4: Explore /dev/ (Devices)

ls /dev/ | head -20
ls /dev/i2c*
ls /dev/spi*
ls /dev/fb*

Each entry is a device node — a file that represents a piece of hardware. When you cat /dev/i2c-1, you are talking to the I2C bus through the kernel.

Step 5: Explore /sys/ (Kernel Attributes)

ls /sys/class/
ls /sys/class/thermal/thermal_zone0/
cat /sys/class/thermal/thermal_zone0/temp

The temperature is in millidegrees Celsius. Divide by 1000:

echo "scale=1; $(cat /sys/class/thermal/thermal_zone0/temp) / 1000" | bc
Tip

When you write a kernel driver later in this course, you will create entries in /sys/. This is how user-space applications will read your sensor data.

Step 6: Explore /proc/ (Process and System Info)

cat /proc/cpuinfo | head -10
cat /proc/meminfo | head -5
cat /proc/uptime
ls /proc/ | head -20

The numbered directories in /proc/ are per-process directories. Pick a PID and look inside:

ls /proc/1/
cat /proc/1/cmdline

Step 7: Compare the Three

Filesystem Mount Point Content Created By
devfs /dev/ ? ?
sysfs /sys/ ? ?
procfs /proc/ ? ?

Fill in the "Content" and "Created By" columns based on what you observed.

Bonus: Quick Edits on the Pi

On Raspberry Pi OS, both nano and vim are available. Try editing a file on the Pi:

vim /tmp/test_edit.txt       # practice here — safe location
# Press i, type "Hello from the Pi", press Esc, type :wq

On a minimal Buildroot image later in the course, nano will not be installed — vi is your only option. The core commands are the same.

Bonus: RPi Hardware Diagnostics

The Raspberry Pi has a built-in diagnostic tool called vcgencmd:

vcgencmd measure_temp           # GPU temperature
vcgencmd measure_volts core     # core voltage
vcgencmd get_throttled          # throttling status (0x0 = OK)
vcgencmd get_mem arm            # RAM allocated to CPU
vcgencmd get_mem gpu            # RAM allocated to GPU
vcgencmd measure_clock arm      # current CPU clock speed
Warning

If get_throttled returns anything other than 0x0, your power supply is insufficient. Common flags: 0x50005 = actively throttled + under-voltage. This causes random crashes and SD card corruption — fix it before continuing.

# I2C tools: scan the bus for connected devices
sudo i2cdetect -y 1                     # scan I2C bus 1
sudo i2cget -y 1 0x18 0x05 w           # read a register from a device
sudo i2cdump -y 1 0x18                  # dump all registers of a device

# See which kernel modules are loaded for your hardware
lsmod | grep -E "i2c|spi|gpio"

# What type of filesystem is on each partition?
lsblk -f

# Live kernel log — like tail -f for hardware events
dmesg -w

Checkpoint 2

Question Your Answer
Number of entries in /dev/ (ls /dev/ \| wc -l)
CPU temperature (°C)
System uptime (hours)
RAM total / RAM free

3. Permissions — Who Can Access What?

Step 8: Read Device Permissions

ls -la /dev/i2c*
ls -la /dev/mem
ls -la /dev/gpiomem

Compare the permissions: - /dev/i2c-1 — who owns it? What group? Can your user read it? - /dev/mem — why is this restricted? - /dev/gpiomem — different from /dev/mem?

Step 9: Permission Denied

Try accessing a restricted device:

cat /dev/mem

You should get "Permission denied." Now try with sudo:

sudo hexdump -C /dev/mem | head -5
Warning

/dev/mem gives raw access to physical memory. This is why it is restricted to root — a bug could crash the entire system. Kernel drivers exist precisely to avoid this.

Step 10: Understanding Permission Bits

ls -la /etc/hostname
-rw-r--r-- 1 root root 12 Jan 15 10:00 /etc/hostname

Decode this: - Owner (root): read + write - Group (root): read only - Others: read only

Now check your own home directory:

ls -la ~/

Checkpoint 3

Question Your Answer
/dev/i2c-1 owner:group
/dev/i2c-1 permission bits
Can your user read /dev/mem without sudo?
Your username (whoami)

4. Services — What Starts at Boot?

Step 11: List Running Services

systemctl list-units --type=service --state=running

Count them:

systemctl list-units --type=service --state=running | grep -c ".service"

Step 12: Inspect a Service

systemctl status ssh

Observe: - Active: running (since when?) - Main PID: the process ID - CGroup: the control group (resource container)

Step 13: Find Unnecessary Services

systemctl list-units --type=service --state=running | grep -E "bluetooth|avahi|cups|ModemManager"

These services are useful on a desktop but unnecessary on an embedded appliance. In the Boot Timing Lab, you will disable them and measure the boot time improvement.

Bonus: Journalctl — The Better Log Viewer

# Follow system log in real time (like tail -f)
journalctl -f

# Show only SSH-related logs
journalctl -u ssh

# Show logs from this boot only
journalctl -b

# Show only errors and worse
journalctl -p err -b

# Show logs from the last 10 minutes
journalctl --since "10 min ago"

Checkpoint 4

Question Your Answer
Running services count
SSH service PID
Unnecessary services found

5. Shell Tools — Pipes, Redirects, and Watching

Step 14: Pipes

Chain commands to filter information:

# Find SPI-related kernel messages
dmesg | grep -i spi

# Find I2C devices
dmesg | grep -i i2c

# Top 5 memory users
ps aux --sort=-%mem | head -6

Step 15: Redirects

Save output to a file:

# Save kernel log
dmesg > ~/boot_log.txt
ls -lh ~/boot_log.txt

# Append to a log
echo "Lab started at $(date)" >> ~/lab_notes.txt
echo "Processes: $(ps aux | wc -l)" >> ~/lab_notes.txt
cat ~/lab_notes.txt

Step 16: Watch

Monitor a changing value in real time:

# Watch CPU temperature every 2 seconds
watch -n 2 'echo "scale=1; $(cat /sys/class/thermal/thermal_zone0/temp) / 1000" | bc'

Press Ctrl+C to stop. Now try watching process count:

watch -n 5 'ps aux | wc -l'

Step 17: Environment Variables

# See all environment variables
env | head -10

# Check PATH
echo $PATH

# Set a variable for one command
MY_SENSOR=bmi160 echo $MY_SENSOR

Bonus: Network Quick Checks

# See all listening ports and which process owns them
sudo ss -tlnp

# See all active connections
ss -tunap

# Quick DNS lookup
nslookup google.com

# Monitor interrupt counts in real time (useful for I2C/SPI debugging)
watch -n 1 'cat /proc/interrupts | head -20'

# Compare /dev/ entries between host and RPi (run from host)
diff <(ls /dev/) <(ssh pi@raspberrypi ls /dev/) | head -30

Checkpoint 5

Question Your Answer
SPI entries in dmesg (count)
Size of boot_log.txt
CPU temperature trend (rising/stable/falling)

6. Scheduling — Who Gets the CPU?

Step 18: Check Process Priorities

ps -eo pid,ni,pri,cls,comm --sort=-pri | head -15
  • NI: nice value (-20 highest priority → 19 lowest)
  • PRI: kernel priority
  • CLS: scheduling class (TS = normal, FF = FIFO real-time, RR = round-robin)

Step 19: Experiment with Nice Values

Start a background task with default priority:

sleep 300 &
SLEEP_PID=$!
ps -p $SLEEP_PID -o pid,ni,pri,cls,comm

Now start one with low priority:

nice -n 19 sleep 300 &
NICE_PID=$!
ps -p $NICE_PID -o pid,ni,pri,cls,comm

Compare the NI and PRI values.

Step 20: Check Scheduling Policy

chrt -p $SLEEP_PID

You should see SCHED_OTHER (the default fair scheduler). Real-time processes use SCHED_FIFO or SCHED_RR — you will encounter these in the Jitter Measurement lab.

Clean up:

kill $SLEEP_PID $NICE_PID

Checkpoint 6

Question Your Answer
Default nice value
Nice value of your low-priority sleep
Default scheduling policy

Summary Table

Fill in this comparison based on everything you explored:

Aspect Microcontroller (e.g., Pi Pico) Linux (Raspberry Pi)
Programs running
File system
Hardware access
Memory protection
Who manages startup
User permissions

What Just Happened?

You explored the Linux system running on your Raspberry Pi — the same system you will build kernel drivers for, write display applications on, and eventually replace with a custom Buildroot image. Every concept from this lab will be used in later tutorials:

  • /dev/ → you will create entries here with your kernel drivers
  • /sys/ → your drivers will expose sensor data through sysfs
  • Permissions → your udev rules will set access for device nodes
  • Services → you will create a systemd service for your level display
  • Scheduling → you will set real-time priority for sensor threads
  • Pipes and redirects → you will use these in every debugging session

Challenge

Write a one-page "system report" script (system_report.sh) that outputs: 1. Hostname and kernel version 2. CPU model and core count 3. Total / free RAM 4. Running process count 5. Top 3 memory-consuming processes 6. List of I2C and SPI devices in /dev/ 7. CPU temperature 8. Number of running services

Use pipes, redirects, and command substitution. The script should save its output to ~/system_report.txt.


Next Steps

Now that you understand how Linux works from the inside, you're ready to put pixels on screen: Tutorial: Framebuffer Basics


Back to Course Overview