Зачем
Стандартные ps/top показывают RSS, который завышает реальное потребление
памяти из-за shared-страниц (общие библиотеки, mmap и т.д.). Если процесс
запущен в нескольких экземплярах (или использует много shared-памяти),
сумма RSS по всем его PID сильно преувеличивает фактическое потребление.
Более честная метрика — Pss (Proportional Set Size) из
/proc/<pid>/smaps_rollup: shared-страницы в ней учитываются пропорционально
количеству процессов, которые их используют. Сумма Pss по всем процессам
системы гораздо ближе к реальной занятой памяти.
Требование к сбору данных
Скрипт сборки статистики специально написан без единого внешнего
вызова (awk, grep, cat, sort и т.д.) — только bash-билтины
(read, [[ ]], арифметика (( ))). Причина: каждый вызов внешней
команды — это fork()+exec(), и если таких вызовов много (сотни
процессов × несколько вызовов на процесс), скрипт сам начинает заметно
шуметь в системе, которую же и измеряет. На чистом bash в горячем пути
остаётся только неизбежное чтение файлов /proc.
Обработка и форматирование вывода (сортировка, конвертация единиц,
CSV/JSON/YAML) вынесены в отдельную стадию — там уже можно спокойно
использовать awk/sort, так как на сам процесс замера это не влияет.
Скрипт
#!/bin/bash
set -euo pipefail
# ---------- defaults ----------
out_format="flat" # flat|csv|json|yaml
unit="MB" # B|KB|MB
sort_key="size" # name|size (только для flat)
sort_order="desc" # asc|desc (только для flat)
show_header=1 # только для flat
show_total=1 # только для flat
usage() {
cat <<EOF
Usage: $0 [options]
-f, --format FORMAT flat|csv|json|yaml (default: flat)
-u, --unit UNIT B|KB|MB (default: MB)
-s, --sort KEY name|size [flat only] (default: size)
-o, --order ORDER asc|desc [flat only] (default: desc)
--no-header [flat only]
--no-total [flat only]
-h, --help
EOF
}
while [[ $# -gt 0 ]]; do
case "$1" in
-f|--format) out_format=$2; shift 2 ;;
-u|--unit) unit=$2; shift 2 ;;
-s|--sort) sort_key=$2; shift 2 ;;
-o|--order) sort_order=$2; shift 2 ;;
--no-header) show_header=0; shift ;;
--no-total) show_total=0; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown option: $1" >&2; usage; exit 1 ;;
esac
done
case "$out_format" in flat|csv|json|yaml) ;; *) echo "bad format: $out_format" >&2; exit 1 ;; esac
case "$unit" in B|KB|MB) ;; *) echo "bad unit: $unit" >&2; exit 1 ;; esac
case "$sort_key" in name|size) ;; *) echo "bad sort key: $sort_key" >&2; exit 1 ;; esac
case "$sort_order" in asc|desc) ;; *) echo "bad sort order: $sort_order" >&2; exit 1 ;; esac
# ---------- сбор: только bash, минимальное влияние на систему ----------
declare -A mem_kb
for pid_dir in /proc/[0-9]*; do
comm=""
read -r comm < "$pid_dir/comm" 2>/dev/null
[[ -z "$comm" ]] && continue
pss=0
found=0
while IFS=$' \t' read -r key value _; do
if [[ "$key" == "Pss:" ]]; then
pss=$value
found=1
break
fi
done < "$pid_dir/smaps_rollup" 2>/dev/null
[[ $found -eq 1 && "$pss" =~ ^[0-9]+$ ]] || continue
mem_kb["$comm"]=$(( ${mem_kb["$comm"]:-0} + pss ))
done
# ---------- обработка: обычный режим, внешние тулы ок ----------
convert() {
# $1 = значение в KB -> строка в целевой единице
local kb=$1
case "$unit" in
B) echo $(( kb * 1024 )) ;;
KB) echo "$kb" ;;
MB) awk -v k="$kb" 'BEGIN{printf "%.2f", k/1024}' ;;
esac
}
csv_escape() {
# экранирование под RFC4180: удвоение кавычек, обёртка в кавычки
local s=${1//\"/\"\"}
printf '"%s"' "$s"
}
total_kb=0
raw_lines=()
for name in "${!mem_kb[@]}"; do
kb=${mem_kb[$name]}
total_kb=$(( total_kb + kb ))
raw_lines+=("$name"$'\t'"$kb")
done
total_val=$(convert "$total_kb")
case "$out_format" in
flat)
sort_flags=(-t $'\t')
if [[ "$sort_key" == "size" ]]; then
sort_flags+=(-k2,2 -n)
else
sort_flags+=(-k1,1)
fi
[[ "$sort_order" == "desc" ]] && sort_flags+=(-r)
sorted=$(printf '%s\n' "${raw_lines[@]}" | sort "${sort_flags[@]}")
[[ $show_header -eq 1 ]] && printf "%-25s %12s\n" "NAME" "MEM(${unit})"
while IFS=$'\t' read -r name kb; do
printf "%-25s %12s\n" "$name" "$(convert "$kb")"
done <<< "$sorted"
[[ $show_total -eq 1 ]] && printf "%-25s %12s\n" "TOTAL" "$total_val"
;;
csv)
echo "name,mem_${unit,,}"
for name in "${!mem_kb[@]}"; do
printf '%s,%s\n' "$(csv_escape "$name")" "$(convert "${mem_kb[$name]}")"
done
;;
json)
echo "{"
first=1
for name in "${!mem_kb[@]}"; do
[[ $first -eq 1 ]] || echo ","
first=0
printf ' "%s": %s' "$name" "$(convert "${mem_kb[$name]}")"
done
echo
echo "}"
;;
yaml)
for name in "${!mem_kb[@]}"; do
printf '%s: %s\n' "$name" "$(convert "${mem_kb[$name]}")"
done
;;
esac
Использование
./mem_report.sh # flat, MB, шапка+итог, сортировка по размеру убыв.
./mem_report.sh -f json -u KB
./mem_report.sh -f csv -u B
./mem_report.sh -s name -o asc --no-total
./mem_report.sh --no-header --no-total -s name
Опции
| Флаг | Значения | По умолчанию | Действует на |
|---|---|---|---|
-f, --format | flat, csv, json, yaml | flat | все |
-u, --unit | B, KB, MB | MB | все |
-s, --sort | name, size | size | только flat |
-o, --order | asc, desc | desc | только flat |
--no-header | — | шапка показана | только flat |
--no-total | — | итог показан | только flat |
Заметки на будущее
- Группировка идёт по
comm(имя процесса, обрезано ядром до 15 символов). Для точных длинных имён можно заменить источник имени наreadlink -f /proc/$pid/exeили парсингcmdline, но это дороже и не всегда доступно без прав. - Сортировка в
flatделается по сырым KB до конвертации в целевую единицу — иначе строки вида"1234.56"сортировались бы как текст. csv/json/yamlне сортируются и не имеют шапки/итога — эти опции относятся только кflat.