topやhtopは何を見ている?/procの仕組みを知れば自分でも作れる【シェルスクリプト実践】

Raspberry Pi 5でローカルLLMを動かすのが楽しくて仕方ありません。8GBモデルなら小さめのLLMも動きますし、何より「手元で動くAI」というロマンがあります🤩 ただ、LLMを動かしていると気になるのが 「今、どれくらいリソースを使っているんだろう?」 ということ。特にRaspberry Piは熱暴走でスロットリング(性能制限)がかかることもあるので、監視は必要です。

最初は定番のtopコマンドを使っていたのですが、正直見づらい...(みんなあれでわかるの?🙄)。そこで色々なツールを試し、最終的には、自分で作ってみることにしました!


1. 定番ツールを試してみた

top - 伝統的だけど見づらい

Linuxに最初から入っている定番ツールです。ただ、表示が素っ気なくて、パッと見てわかりません。

$ top

htop - topの進化版

topの改良版で、カラフルで見やすくなりました。CPU各コアの使用率がバーグラフで表示され、マウス操作もできます。とりあえずこれを入れておけば間違いないと感じます。 CPUのコア毎の負荷をグラフでみえるのがいいですね。マウスの操作にも対応しています。

$ sudo apt update
$ sudo apt install htop
$ htop

btop - モダンでカッコいい

見た目がとにかくカッコいい!CPU、メモリ、ディスク、ネットワークが一画面に収まっていて、実行中のプロセスの負荷状況が一目でわかります。 あとCPUの温度もちゃんとみえてますね。スロットリングの発生が気になるようであればこちらの方がいいかもしれません。 少し重いかもしれませんが、Raspberry Pi 5なら十分快適に動作します。

$ sudo apt update
$ sudo apt install btop
$ btop

glances - Python製の多機能モニター

Pythonベースのツールで、btop同様に温度も表示できるのが嬉しいツール。Pythonのツールなので、aptコマンドではなく、pipxuvx(uv)でインストールを行いましょう!

pipxでインストールをする場合

$ sudo apt install pipx
$ pipx install glances
$ glances

uvx(uv)でインストールする場合

# uvを使う方法(より高速でモダン)

# 1. まずuvをインストール
$ curl -LsSf https://astral.sh/uv/install.sh | sh
$ source ~/.local/bin/env  # パスを通す

# 2a. 一時実行する場合(インストール不要で即実行)
$ uvx glances

# 2b. 永続インストールする場合
$ uv tool install glances

# 3. 実行
$ glances

こちらはかなりグラフィカルに表示を行なってくれます。また、ネットワークのモニタリングもできるようです。

💡 なぜ apt ではなく pipx/uvx?

glancesはPython製なので、aptだとバージョンが古くなりがちです。また、最近のDebian/Ubuntu系(Raspberry Pi OSも)では PEP 668 により、システムのPythonに直接pip installすることが制限されています。pipxuvxなら隔離された仮想環境にインストールされるので、システムを汚さず最新版が使えます。


2. パフォーマンスメーターの中身はどうなってるんだろう?

これらのツールを使っているうちに、疑問が湧いてきました。「CPU使用率とかメモリ使用量って、どうやって取得しているんだろう?」🤔

調べてみると、答えは意外とシンプルでした。

その答えは /proc ファイルシステム

Linuxには /proc という特殊なディレクトリがあります。これは実際のファイルではなく、カーネルが提供する「仮想ファイルシステム となります。

試しに覗いてみましょう:

$ cat /proc/stat

おお、4つのコア(cpu0〜cpu3)の情報が出てきました!これがRaspberry Piの4コアCPUの状態です。

表示された各列の意味

列番号 名前 意味
1 user ユーザープロセスが使った時間
2 nice 優先度を変更したプロセスの時間
3 system カーネルが使った時間
4 idle 何もしていない時間
5 iowait I/O待ちの時間
6 irq ハードウェア割り込み処理の時間
7 softirq ソフトウェア割り込み処理の時間

CPU使用率の計算方法

ここで重要なのは、これらの値が 「起動時からの累積値」 となる点です。そのため、「今この瞬間」の使用率を知るには、2回測定して差分を取る必要があります。

たとえば、こんな感じの処理になります。

1回目の測定: total=1000, idle=800
2回目の測定: total=1100, idle=850

差分: diff_total=100, diff_idle=50

CPU使用率 = (diff_total - diff_idle) / diff_total × 100
         = (100 - 50) / 100 × 100
         = 50%

これなら計算できそうです。

メモリ情報も同じように取得できる

CPUと同様にメモリも取得できます。

$ cat /proc/meminfo

MemTotal:        7929624 kB
MemFree:          234567 kB
MemAvailable:    3321456 kB
Buffers:          123456 kB
Cached:          2345678 kB
...

MemTotal(全体)とMemAvailable(利用可能)がわかれば、使用率は簡単に計算できます!計算式は以下のようになります。

メモリ使用率 = (MemTotal - MemAvailable) / MemTotal × 100

👉️ MemFreeではなくMemAvailableを使うのがコツです。MemAvailableはキャッシュなども考慮した「実際に使える量」なので、より実用的な値になります。

簡単なサンプルで試してみる

では、理屈がわかったところで、実際に動かして確認してみましょう。

proc_sample.sh

#!/bin/bash
# /proc からCPU・メモリ情報を取得するサンプル

echo "=== CPU使用率 ==="
# 1回目測定
declare -a total1 idle1
i=0
while read -r line; do
    if [[ $line =~ ^cpu[0-9] ]]; then
        read -r _ u n s id io ir si _ <<< "$line"
        total1[$i]=$((u + n + s + id + io + ir + si))
        idle1[$i]=$id
        ((i++))
    fi
done < /proc/stat

sleep 1

# 2回目測定して計算
i=0
while read -r line; do
    if [[ $line =~ ^cpu[0-9] ]]; then
        read -r cpu u n s id io ir si _ <<< "$line"
        total=$((u + n + s + id + io + ir + si))
        diff_total=$((total - total1[i]))
        diff_idle=$((id - idle1[i]))
        if [ $diff_total -gt 0 ]; then
            usage=$(echo "scale=1; ($diff_total - $diff_idle) * 100 / $diff_total" | bc)
        else
            usage="0.0"
        fi
        echo "  Core $i: ${usage}%"
        ((i++))
    fi
done < /proc/stat

echo ""
echo "=== Memory ==="
grep -E '^(MemTotal|MemAvailable)' /proc/meminfo
MEM_TOTAL=$(grep MemTotal /proc/meminfo | awk '{print $2}')
MEM_AVAILABLE=$(grep MemAvailable /proc/meminfo | awk '{print $2}')
echo "使用率: $(echo "scale=1; ($MEM_TOTAL - $MEM_AVAILABLE) * 100 / $MEM_TOTAL" | bc)%"

実行結果

=== CPU使用率 ===
  Core 0: 12.5%
  Core 1: 45.2%
  Core 2: 8.0%
  Core 3: 23.1%

=== Memory ===
MemTotal:        8053568 kB
MemAvailable:    3456789 kB
使用率: 57.1%

各コアのCPU使用率とメモリ使用率が取れました!これだけでも十分使えそうです。


3. 「もうちょっとカッコよくしたい!」

サンプルで基本はわかりました。でもせっかくなら、もうちょっと見やすくしたい。バーグラフとか、色分けとか...。 さすがにそこまでいくと自分の手に余るので、LLMに依頼して、もう少しいい感じのシステムモニターを生成してもらいました。

Shellでカラーとかリフレッシュを扱うのは正直キツイです🥲

完成版: system_monitor.sh

#!/bin/bash
# System Monitor - CPU(コアごと) & メモリ監視
# Usage: ./system_monitor.sh

# 色定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
BOLD='\033[1m'

# カーソル制御
CURSOR_HOME='\033[H'    # カーソルを左上に移動
CLEAR_LINE='\033[K'     # カーソル位置から行末までクリア
HIDE_CURSOR='\033[?25l' # カーソルを非表示
SHOW_CURSOR='\033[?25h' # カーソルを表示

# 終了時にカーソルを再表示
trap 'printf "${SHOW_CURSOR}"; exit' INT TERM

# バー表示関数
draw_bar() {
    local percent=$1
    local width=30
    local filled=$(echo "$percent * $width / 100" | bc)
    local empty=$((width - filled))
    
    # 色選択
    local color=$GREEN
    if (( $(echo "$percent > 80" | bc -l) )); then
        color=$RED
    elif (( $(echo "$percent > 50" | bc -l) )); then
        color=$YELLOW
    fi
    
    printf "${color}"
    printf '█%.0s' $(seq 1 $filled 2>/dev/null)
    printf "${NC}"
    printf '░%.0s' $(seq 1 $empty 2>/dev/null)
    printf " %5.1f%%" "$percent"
}

# 前回のCPU値を保持
declare -a prev_total
declare -a prev_idle

# CPUコア数を取得
get_cpu_cores() {
    grep -c '^processor' /proc/cpuinfo
}

# 物理メモリサイズを取得(GB単位、小数点1桁)
get_physical_memory_gb() {
    local total_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')
    echo "scale=1; $total_kb / 1024 / 1024" | bc
}

# 初期化
init_cpu() {
    local i=0
    while read -r line; do
        if [[ $line =~ ^cpu[0-9] ]]; then
            read -r cpu user nice system idle iowait irq softirq _ <<< "$line"
            local total=$((user + nice + system + idle + iowait + irq + softirq))
            prev_total[$i]=$total
            prev_idle[$i]=$idle
            ((i++))
        fi
    done < /proc/stat
}

# CPU使用率計算
get_cpu_usage() {
    local -a usage
    local i=0
    while read -r line; do
        if [[ $line =~ ^cpu[0-9] ]]; then
            read -r cpu user nice system idle iowait irq softirq _ <<< "$line"
            local total=$((user + nice + system + idle + iowait + irq + softirq))
            
            local diff_total=$((total - prev_total[i]))
            local diff_idle=$((idle - prev_idle[i]))
            
            if [ $diff_total -gt 0 ]; then
                usage[$i]=$(echo "scale=1; (($diff_total - $diff_idle) * 100) / $diff_total" | bc)
            else
                usage[$i]=0
            fi
            
            prev_total[$i]=$total
            prev_idle[$i]=$idle
            ((i++))
        fi
    done < /proc/stat
    echo "${usage[@]}"
}

# メモリ情報取得
get_memory_info() {
    local total=$(grep MemTotal /proc/meminfo | awk '{print $2}')
    local available=$(grep MemAvailable /proc/meminfo | awk '{print $2}')
    local used=$((total - available))
    local percent=$(echo "scale=1; $used * 100 / $total" | bc)
    
    local total_mb=$((total / 1024))
    local used_mb=$((used / 1024))
    local available_mb=$((available / 1024))
    
    echo "$percent $used_mb $total_mb $available_mb"
}

# スワップ情報取得
get_swap_info() {
    local total=$(grep SwapTotal /proc/meminfo | awk '{print $2}')
    local free=$(grep SwapFree /proc/meminfo | awk '{print $2}')
    local used=$((total - free))
    
    if [ $total -gt 0 ]; then
        local percent=$(echo "scale=1; $used * 100 / $total" | bc)
        local total_mb=$((total / 1024))
        local used_mb=$((used / 1024))
        echo "$percent $used_mb $total_mb"
    else
        echo "0 0 0"
    fi
}

# メイン表示
main() {
    init_cpu
    sleep 0.5
    
    # システム情報を取得
    local cpu_cores=$(get_cpu_cores)
    local mem_size_gb=$(get_physical_memory_gb)
    
    # 初回のみ画面クリア、カーソル非表示
    clear
    printf "${HIDE_CURSOR}"
    
    while true; do
        # カーソルを左上に移動(clearしない)
        printf "${CURSOR_HOME}"
        
        # ヘッダー
        echo -e "${BOLD}${CYAN}╔════════════════════════════════════════════════════════╗${NC}${CLEAR_LINE}"
        echo -e "${BOLD}${CYAN}║                    System Monitor                      ║${NC}${CLEAR_LINE}"
        echo -e "${BOLD}${CYAN}╚════════════════════════════════════════════════════════╝${NC}${CLEAR_LINE}"
        echo -e "${CLEAR_LINE}"
        
        # 時刻
        echo -e "${BOLD}📅 $(date '+%Y-%m-%d %H:%M:%S')${NC}${CLEAR_LINE}"
        echo -e "${CLEAR_LINE}"
        
        # CPU セクション
        echo -e "${BOLD}${BLUE}━━━ CPU (${cpu_cores} cores) ━━━${NC}${CLEAR_LINE}"
        
        read -ra cpu_usage <<< "$(get_cpu_usage)"
        
        for ((i=0; i<cpu_cores; i++)); do
            printf "  Core %d: " "$i"
            draw_bar "${cpu_usage[$i]:-0}"
            echo -e "${CLEAR_LINE}"
        done
        
        # 全体CPU使用率
        local total_cpu=0
        for u in "${cpu_usage[@]}"; do
            total_cpu=$(echo "$total_cpu + ${u:-0}" | bc)
        done
        total_cpu=$(echo "scale=1; $total_cpu / $cpu_cores" | bc)
        printf "  ${BOLD}Total${NC} : "
        draw_bar "$total_cpu"
        echo -e "${CLEAR_LINE}"
        echo -e "${CLEAR_LINE}"
        
        # メモリセクション
        echo -e "${BOLD}${BLUE}━━━ Memory (${mem_size_gb}GB) ━━━${NC}${CLEAR_LINE}"
        
        read -r mem_percent mem_used mem_total mem_available <<< "$(get_memory_info)"
        
        printf "  RAM  : "
        draw_bar "$mem_percent"
        echo -e "${CLEAR_LINE}"
        echo -e "         ${CYAN}使用: ${mem_used}MB / ${mem_total}MB (空き: ${mem_available}MB)${NC}${CLEAR_LINE}"
        
        read -r swap_percent swap_used swap_total <<< "$(get_swap_info)"
        if [ "$swap_total" -gt 0 ]; then
            printf "  Swap : "
            draw_bar "$swap_percent"
            echo -e "${CLEAR_LINE}"
            echo -e "         ${CYAN}使用: ${swap_used}MB / ${swap_total}MB${NC}${CLEAR_LINE}"
        fi
        echo -e "${CLEAR_LINE}"
        
        # 温度セクション
        echo -e "${BOLD}${BLUE}━━━ Temperature ━━━${NC}${CLEAR_LINE}"
        local temp=$(vcgencmd measure_temp 2>/dev/null | cut -d= -f2 | cut -d\' -f1)
        if [ -n "$temp" ]; then
            local temp_color=$GREEN
            if (( $(echo "$temp > 70" | bc -l) )); then
                temp_color=$RED
            elif (( $(echo "$temp > 55" | bc -l) )); then
                temp_color=$YELLOW
            fi
            echo -e "  🌡️  CPU: ${temp_color}${temp}°C${NC}${CLEAR_LINE}"
            
            # スロットリング状態
            local throttled=$(vcgencmd get_throttled 2>/dev/null | cut -d= -f2)
            if [ "$throttled" = "0x0" ]; then
                echo -e "  ⚡ Throttle: ${GREEN}正常${NC}${CLEAR_LINE}"
            else
                echo -e "  ⚡ Throttle: ${RED}制限中 ($throttled)${NC}${CLEAR_LINE}"
            fi
        else
            echo -e "  ${YELLOW}温度情報を取得できません${NC}${CLEAR_LINE}"
        fi
        echo -e "${CLEAR_LINE}"
        
        # ロードアベレージ
        echo -e "${BOLD}${BLUE}━━━ Load Average ━━━${NC}${CLEAR_LINE}"
        local load=$(cat /proc/loadavg | awk '{print $1, $2, $3}')
        echo -e "  📊 1分: $(echo $load | cut -d' ' -f1) | 5分: $(echo $load | cut -d' ' -f2) | 15分: $(echo $load | cut -d' ' -f3)${CLEAR_LINE}"
        echo -e "${CLEAR_LINE}"
        
        echo -e "${CYAN}[Ctrl+C で終了]${NC}${CLEAR_LINE}"
        
        sleep 1
    done
}

main

実行!

$ chmod +x system_monitor.sh
$ ./system_monitor.sh

実行結果

╔════════════════════════════════════════════════════════╗
║                    System Monitor                      ║
╚════════════════════════════════════════════════════════╝

📅 2025-01-27 15:30:45

━━━ CPU (4 cores) ━━━
  Core 0: █████████████░░░░░░░░░░░░░░░░░  45.2%
  Core 1: ████████████████████████░░░░░░  78.3%
  Core 2: ██████████████████████████████  98.1%
  Core 3: ████████░░░░░░░░░░░░░░░░░░░░░░  25.0%
  Total : ████████████████████░░░░░░░░░░  61.7%

━━━ Memory (7.7GB) ━━━
  RAM  : ██████████████████░░░░░░░░░░░░  58.2%
         使用: 4521MB / 7768MB (空き: 3247MB)

━━━ Temperature ━━━
  🌡️  CPU: 52.0°C
  ⚡ Throttle: 正常

━━━ Load Average ━━━
  📊 1分: 2.15 | 5分: 1.87 | 15分: 1.45

[Ctrl+C で終了]

これですぐにCPUの各コアやメモリがどれくらい使われているか、リアルタイムで見られるようになりました🤩

今回のポイント

  • CPUコア数・メモリサイズは自動取得 … ハードコードしていないので、他のマシンでも動きます
  • 画面の点滅防止 … clearではなくカーソル移動を使うことで、スムーズに更新されます
  • 負荷に応じた色分け … 50%超で黄色、80%超で赤色に

4. 気づいたこと

シンプルな仕組みの上に成り立っている

htopbtopも、結局は /proc/stat/proc/meminfo を読んでいるだけ。派手な見た目の裏側には、とてもシンプルな仕組みがありました。

シェルスクリプトでも十分作れる

「パフォーマンスモニターなんてCで書かないと...」と思っていましたが、シェルスクリプトでも十分実用的なものが作れました。もちろん、より高機能なものを作るならPythonやGoなどが良いでしょうが、学習目的なら bashで十分です。

「理解してから使う」と見え方が変わる

今まで何気なく使っていたhtopの表示が、「あ、これは差分計算してるんだな」とわかるようになりました。ツールへの理解が深まると、トラブル時の対応力も上がる気がします。


おわりに

  • 定番ツールを探す … htopbtopは手軽で高機能。まずはこれらを使おう
  • 仕組みを知る … /procファイルシステムからテキストとして情報を取得している
  • 作ってみる … 仕組みがわかれば、シェルスクリプトでも作れる

最初は「モニタリングツールなんて難しそう...」と思っていましたが、蓋を開けてみれば意外とシンプルな仕組みでした。 最終的にはLLMを使用して完成させていますが…。とはいえ、オリジナルのツールで監視するのは、なかなか楽しいですよ!