Практикум Linux: три уровня с проверяемыми решениями
Автор: Казачкин Даниил Михайлович · Обновлено
Практикум рассчитан на Bash и GNU-утилиты. Первые два уровня работают в новом каталоге без sudo; системные задания третьего уровня предназначены для отдельной VM или собственного…
Практикум рассчитан на Bash и GNU-утилиты. Первые два уровня работают в новом каталоге без sudo; системные задания третьего уровня предназначены для отдельной VM или собственного ограниченного контейнера. После каждого упражнения сравнивайте содержимое и код завершения с ожидаемым результатом.
Подготовить небольшой набор данных
practice_dir=$(mktemp -d)
cd "$practice_dir"
mkdir files logs restore
printf 'alpha\n' > 'files/first note.txt'
printf 'beta\n' > files/second.txt
truncate -s 2048 files/large.bin
touch -d '3 days ago' files/second.txt
printf 'port=8080\nmode=test\n' > app.conf
cat > metrics.csv <<'CSV'
team,value
red,10
blue,6
red,20
blue,10
CSV
cat > logs/requests.log <<'LOG'
10 / 200 120
10 /api 500 40
11 /api 502 20
11 / 200 140
LOGФормат журнала здесь специально простой: час, путь, статус, байты. Он не выдаётся за произвольный access log Nginx. CSV не содержит кавычек и запятых внутри полей: для общего CSV нужен полноценный CSV-парсер.
Уровень 1: имена, права, поиск и архив
Найдите файл с пробелом в имени, измените его режим на 640, затем покажите результат через stat. Решение:
chmod 640 'files/first note.txt'
stat -c '%a %n' 'files/first note.txt'
find files -type f -size +1k -print
find files -type f -mtime +1 -printОжидаются режим 640, large.bin по размеру и second.txt по возрасту. GNU find округляет размер до единиц выбранного суффикса, а mtime считает полные сутки; пограничные значения исследуйте отдельно. Права каталога отвечают за поиск имени и доступ к записи каталога, поэтому режим файла не описывает весь путь.
Создайте tar -czf files.tar.gz files, просмотрите tar -tzf files.tar.gz, извлеките tar -xzf files.tar.gz -C restore и сравните diff -r files restore/files. Пустой diff подтверждает одинаковое содержимое; режим проверьте отдельным stat. Задание усложнения: добавьте имя, начинающееся с дефиса, и объясните назначение -- там, где утилита поддерживает завершение опций.
Уровень 2: счётчики и средние
Посчитайте запросы по статусам, байты и среднее value по командам:
awk '{count[$3]++; bytes+=$4} END {for (s in count) print s,count[s]; print "bytes",bytes}' logs/requests.log | sort
awk -F, 'NR>1 {sum[$1]+=$2; n[$1]++} END {for (g in sum) print g,sum[g]/n[g]}' metrics.csv | sort
awk '$3>=500 {count[$2]++} END {for (u in count) print u,count[u]}' logs/requests.log
awk '{count[$1]++} END {for (h in count) print h,count[h]}' logs/requests.log | sortПроверка: 200 встречается дважды, 500 и 502 — по одному разу, сумма байтов 320. Среднее blue равно 8, red — 15. У /api две ошибки 5xx; в каждый час записано два запроса. Для рейтинга всех URL сгруппируйте поле 2 и отсортируйте числовой счётчик по убыванию. Порядок обхода ассоциативного массива awk не гарантирован, поэтому вывод для сравнения сортируют.
Измените только строку port, сохранив копию: sed -i.bak 's/^port=8080$/port=9090/' app.conf, затем diff -u app.conf.bak app.conf. У diff код 1 здесь ожидаем: различие обнаружено. Не трактуйте каждый ненулевой код как одинаковую аварию. Дополнительная задача: добавьте пустое значение CSV и явно выберите политику — ошибка данных или пропуск строки; молчаливое превращение в ноль меняет среднее.
Уровень 3: скрипт с контрактом
Создайте check-disk.sh:
#!/usr/bin/env bash
set -euo pipefail
limit=${1:-80}
path=${2:-.}
[[ $limit =~ ^(0|[1-9][0-9]?|100)$ ]] || exit 2
if ! report=$(LC_ALL=C df -P -- "$path"); then exit 2; fi
used=$(awk 'NR==2 {gsub(/%/, "", $5); print $5}' <<< "$report")
[[ $used =~ ^[0-9]+$ ]] || exit 2
printf 'used=%s threshold=%s\n' "$used" "$limit"
if (( used >= limit )); then exit 1; fiПосле chmod u+x check-disk.sh порог 0 должен дать код 1, нечисловой аргумент — 2. Порог 100 обычно даёт 0, но на полностью заполненном filesystem верно 1. Запуск с отсутствующим путём возвращает 2, а не ложное сообщение о свободном диске. Если терминал использует errexit, проверяйте ожидаемый неуспех через if ./check-disk.sh 0; then ...; else ...; fi.
В пользовательском systemd создайте lab-disk.service с Type=oneshot и ExecStart, содержащим абсолютный путь к скрипту. Чтобы предупреждение с кодом 1 не считалось неисправностью запуска, укажите SuccessExitStatus=1. В lab-disk.timer задайте OnCalendar=hourly, Persistent=true и WantedBy=timers.target. Проверьте units через systemd-analyze verify, календарь через systemd-analyze calendar hourly, включите свой timer и найдите запуск через journalctl --user -u lab-disk.service. После упражнения отключите только этот timer.
Ограничение памяти и объяснение отказа
В собственной VM с Docker и cgroup v2 запустите конечную пробу:
docker run --name lab-memory --memory=64m --memory-swap=64m \
python:3.13-alpine python -c 'x=bytearray(128*1024*1024); print(len(x))'
docker inspect --format '{{.State.ExitCode}} {{.State.OOMKilled}}' lab-memory
docker rm lab-memoryОжидается отказ выделения или завершение процесса, но конкретный код зависит от среды. Подтверждайте OOMKilled и события cgroup; число 137 само по себе не доказывает OOM. Не переносите эксперимент на процесс без установленного лимита.
Наконец, выполните strace -e trace=%file -o trace.log cat absent.conf внутри practice_dir и найдите строку absent.conf с ENOENT. Объясните отличие отсутствующего файла от отказа EACCES. Итоговый результат практикума — воспроизводимые файлы, ожидаемые значения и объяснение каждой ошибки, а не список команд, которые однажды завершились.