35 KiB
D5, часть 3. Bash, awk/sed, /proc (урок)
Это не проверка, а урок: сначала разбираем механизм, потом решаешь задачи. Код в разборах можно копировать и запускать — это образец, а не готовый ответ на задачу.
1. Пайплайны и код возврата
cmd1 | cmd2 | cmd3 запускает три отдельных процесса одновременно, соединённых анонимными
каналами (pipe) — cmd2 начинает читать данные, ещё пока cmd1 их пишет, ничего не
буферизуется целиком в памяти между шагами. $? после пайплайна — это код возврата
последней команды (cmd3), а не всего конвейера: если cmd1 упал с ошибкой, а cmd3
завершилась успешно, $? покажет 0 — ошибка cmd1 потеряется молча.
Чтобы увидеть код возврата каждой команды пайплайна отдельно, есть массив PIPESTATUS:
echo "${PIPESTATUS[@]}" после cmd1 | cmd2 печатает, например, 1 0 — cmd1 упал,
cmd2 отработал нормально, хотя $? был бы 0.
Факты для карточек
- base | Чей код возврата хранит
$?послеcmd1 | cmd2 | cmd3? — только последней команды (cmd3) - core | Как узнать код возврата каждой команды пайплайна отдельно? — массив
PIPESTATUS(${PIPESTATUS[@]})
Почему дальше: раз $? по умолчанию скрывает ошибки середины пайплайна, логично разобрать
режимы set, которые меняют это поведение по умолчанию.
2. set -euo pipefail: что каждая буква включает и где ломается
-e— скрипт немедленно завершается, если любая команда вернула ненулевой код.-u— обращение к неопределённой переменной — ошибка, а не пустая строка.-o pipefail— код возврата пайплайна становится кодом первой упавшей команды в нём, а не только последней (закрывает дыру из раздела 1).
-e не так надёжен, как кажется, — есть места, где он не срабатывает:
- Команда внутри условия (
if cmd; then,while cmd; do, после&&/||) — код возврата там ожидаем и проверяется явно,-eэту команду не трогает даже при ошибке. - Команда в подстановке
$(cmd), присвоенной переменной без немедленного использования результата в условии — ошибка внутри подстановки сама по себе не всегда останавливает внешний скрипт, поведение зависит от контекста использования результата. - Любая команда как часть пайплайна, кроме последней, — без
pipefail-eреагирует только на код последней команды пайплайна (та же дыра, что и в разделе 1).
Ловушки
- Полагаться на
-eвнутриif some_check; then ... fiдля остановки скрипта при ошибкеsome_check→ скрипт не остановится, потому что команда — часть условия → ошибка проглатывается и обнаруживается заметно позже, в неожиданном месте. - Забыть
pipefail→grep pattern file | sortпри отсутствующемfileвернёт 0 (потому чтоsortпустого потока отрабатывает успешно) → скрипт продолжит работу, как будто файл найден.
Факты для карточек
- base | Что делает
-eвset -euo pipefail? — скрипт завершается сразу при ненулевом коде возврата любой команды - base | Что делает
-u? — обращение к неопределённой переменной — ошибка вместо пустой строки - core | Что делает
pipefailи какую проблему из раздела 1 это закрывает? — код возврата пайплайна = код первой упавшей команды, а не только последней; закрывает потерю ошибок середины пайплайна - core | В каком месте
-eне остановит скрипт при ошибке команды? — если команда — часть условия (if,while, после&&/||) или не последняя команда пайплайна безpipefail
Почему дальше: set -u ловит неопределённые переменные, но не спасает от другой частой
причины сюрпризов — как bash разбивает строку на слова при подстановке без кавычек.
3. Кавычки, word splitting, IFS, массивы
Без двойных кавычек bash после подстановки переменной делает word splitting — разбивает
результат на отдельные слова по символам из IFS (по умолчанию пробел, таб, перевод строки) и
затем globbing — раскрывает символы *, ?, [...] как маски файлов. "$var" в двойных
кавычках отключает оба шага — переменная передаётся как одна строка целиком, что почти всегда
и нужно.
file="my file.txt"
rm $file # word splitting: rm воспринимает это как rm "my" "file.txt" — два аргумента
rm "$file" # правильно: один аргумент "my file.txt"
IFS можно временно переопределить, чтобы разбить строку по нужному разделителю:
IFS=',' read -ra parts <<< "a,b,c" # parts=(a b c)
Массивы: arr=(a b c), обращение по индексу ${arr[1]}, все элементы — ${arr[@]} (каждый
элемент — отдельное слово при развёртывании в кавычках, "${arr[@]}"), длина — ${#arr[@]}.
Ловушки
- Забыть кавычки вокруг переменной с путём, содержащим пробел, → word splitting режет путь на несколько аргументов → «No such file or directory» на файле, который на самом деле есть.
${arr[@]}без кавычек при перебореfor x in ${arr[@]}→ элементы с пробелами внутри сами разбиваются на несколько слов → цикл обрабатывает не те элементы, что были в массиве.
Факты для карточек
- base | Что по умолчанию входит в IFS? — пробел, таб, перевод строки
- core | Что отключают двойные кавычки вокруг
"$var"? — word splitting и globbing (*/?/[...]не раскрываются) - core | Как правильно перебрать массив с элементами, содержащими пробелы? —
for x in "${arr[@]}"(с кавычками)
Почему дальше: помимо кода возврата команды и переменных окружения, у самого запущенного
скрипта и его процессов есть ещё несколько специальных переменных и способ отреагировать на
сигнал — trap.
4. $?, $!, $$, trap
$?— код возврата последней выполненной команды (см. раздел 1 про пайплайны).$!— PID последнего запущенного в фоне процесса (cmd &затемpid=$!), нужен, чтобы потом сделатьwait $pidилиkill $pid.$$— PID самого текущего скрипта/шелла, часто используется для уникальных временных файлов:tmpfile="/tmp/out.$$".trap— регистрирует обработчик на сигнал или на выход из скрипта:trap 'rm -f "$tmpfile"' EXITгарантированно удалит временный файл при любом завершении скрипта — обычном, по ошибке (-e) или по сигналу (если сигнал перечислен), без ручного дублированияrmв каждой точке выхода.
Факты для карточек
- base | Что хранит
$!? — PID последнего фонового процесса - base | Что хранит
$$? — PID текущего скрипта/шелла - core | Зачем
trap ... EXITиспользуют для временных файлов? — гарантирует очистку при любом пути завершения скрипта (успех, ошибка, сигнал), без дублирования кода в каждой точке выхода
Почему дальше: $$ в имени временного файла — это про один процесс; когда данные для команды
нужно передать через аргументы, а не через имя файла, и данных много (список файлов, список
PID), на сцену выходит xargs.
5. xargs -0
xargs берёт поток строк со стандартного ввода и подставляет их как аргументы в конец
указанной команды, разбивая по частям, если список слишком длинный для одного вызова
(ограничение ОС на длину аргументов). По умолчанию xargs, как и bash, разбивает вход по
пробельным символам — имена файлов с пробелами или переводами строк внутри (редко, но
бывает) ломают такой разбор.
-0 — вход разделён не пробелами/переводами строк, а нулевыми байтами (\0), которые не
могут встретиться внутри легального имени файла в Unix. Источник таких нулевых разделителей —
find ... -print0:
find . -name '*.tmp' -print0 | xargs -0 rm -f
Это единственная безопасная пара для случая, когда имена файлов заранее не гарантированы «простыми» (без пробелов, кавычек, переводов строк).
Ловушки
find ... | xargs rmбез-print0/-0на именах файлов с пробелами → имя разбивается на несколько «аргументов» →rmпытается удалить несуществующие файлы или удаляет не то.
Факты для карточек
- base | Чем
-0вxargs -0отличается от поведения по умолчанию? — вход разделяется нулевыми байтами\0вместо пробелов/переводов строк - core | С какой опцией
findобычно комбинируютxargs -0? —-print0
Почему дальше: xargs передаёт список аргументов другой команде; сами команды для поиска и
агрегации данных в потоке — отдельный набор утилит с конкретными флагами.
6. Поиск и агрегация: grep, sort, uniq
grep -c PATTERN— печатает не совпавшие строки, а их количество.grep -o PATTERN— печатает только совпавшую часть строки, а не строку целиком (удобно вместе сwc -l, чтобы посчитать число вхождений, а не строк с хотя бы одним вхождением).grep -v PATTERN— инвертирует фильтр, печатает строки, которые не совпали.sort -k N— сортирует по N-му полю (по умолчанию разделитель — пробел), а не по всей строке целиком.sort -n— числовая сортировка (10идёт после9, а не перед2, как при лексикографической сортировке строк по умолчанию).sort -u— сортировка с одновременным удалением дубликатов (эквивалентsort | uniqв один проход).uniq -c— схлопывает соседние повторяющиеся строки, добавляя счётчик повторов перед каждой строкой; работает только на уже отсортированном входе — несмежные повторыuniqне видит.
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -3 # топ-3 IP по числу запросов
Ловушки
- Вызвать
uniq -cбез предварительногоsort→ повторы, не идущие подряд в исходном файле, не схлопнутся → счётчики занижены относительно реального числа повторов. sortбез-nна числовых полях (порты, счётчики) →10 < 9лексикографически → строки идут не в том порядке, который ожидался.
Факты для карточек
- base | Что печатает
grep -c? — количество совпавших строк, а не сами строки - base | Почему
uniq -cнужно применять послеsort? —uniqсхлопывает только соседние одинаковые строки, несмежные повторы не видит - core | Чем
sort -uэквивалентен по результату? —sort | uniq, но за один проход
Почему дальше: grep/sort/uniq работают со строками целиком или по одному полю через
внешний ключ; когда нужна произвольная логика по колонкам с накоплением состояния (суммы,
счётчики по ключу), нужен awk.
7. awk: поля, NR/NF, ассоциативные массивы
awk читает вход построчно и автоматически разбивает каждую строку на поля по разделителю
(FS, по умолчанию — пробел/таб): $1, $2, … — поля, $0 — строка целиком, NR — номер
текущей строки от начала потока, NF — число полей в текущей строке.
awk '{sum[$1] += $2} END {for (k in sum) print k, sum[k]}' data.txt
sum здесь — ассоциативный массив: ключ $1 (первое поле), значение — накапливаемая сумма
второго поля. awk — полноценный язык с переменными, циклами и такими массивами, а не просто
фильтр, поэтому в нём можно агрегировать данные по ключу за один проход, без промежуточных
файлов.
Почему awk на файле в 10⁶ строк быстрее bash-цикла построчного чтения с внешними командами
внутри: awk — один процесс, который читает поток и делает линейный проход по строкам целиком
внутри себя. Bash-цикл вида while read -r line; do grep ... <<< "$line"; done < file
порождает (форкает) отдельный новый процесс на каждую строку — на миллионе строк это
миллион fork+exec, каждый из которых на порядки дороже одной итерации внутреннего цикла
awk. Разница — не в разы, а на порядки по времени выполнения.
Ловушки
- Обрабатывать большой лог циклом
for/whileс построчным чтением и вызовомgrep/awkвнутри цикла → минуты вместо секунд на файле в миллионы строк → видно поtimeна запуске скрипта; правильный подход — один проходawk/sortна весь поток целиком.
Факты для карточек
- base | Что означает
$1в awk? — первое поле текущей строки - base | Что содержит
NR? — номер текущей строки от начала потока - core | Почему awk на 1e6 строк быстрее bash-цикла с grep внутри? — awk — один процесс с линейным проходом по строкам, bash-цикл форкает новый процесс на каждую итерацию (миллион fork+exec против одного процесса)
Почему дальше: awk агрегирует и печатает; когда нужно не посчитать, а точечно
отредактировать текст по шаблону прямо в потоке, для этого — sed.
8. sed
sed применяет команды редактирования к каждой строке потока и печатает результат — потоковый
редактор, не интерактивный. Самая частая команда — замена по шаблону:
sed 's/OLD/NEW/' (заменяет первое вхождение в строке), sed 's/OLD/NEW/g' (все вхождения в
строке, флаг g = global). Без флага -i sed не меняет исходный файл, а печатает результат
в stdout — sed -i 's/OLD/NEW/g' file редактирует файл на месте.
sed -n '5,10p' file # напечатать только строки с 5 по 10 (-n подавляет вывод по умолчанию)
sed '/^#/d' file # удалить строки, начинающиеся с #
Факты для карточек
- base | Что делает флаг
gвs/OLD/NEW/g? — заменяет все вхождения в строке, а не только первое - core | Чем
sed -iотличается отsedбез флага? — редактирует файл на месте вместо печати результата в stdout
Почему дальше: grep/awk/sed работают с текстовыми потоками из файлов и команд; отдельный
источник текстовых данных в Linux — это /proc, где ядро публикует состояние процессов и
ресурсов в виде текстовых файлов.
9. /proc: что там реально лежит
/proc — виртуальная файловая система: файлы в ней не лежат на диске, ядро генерирует их
содержимое в реальном времени по запросу чтения. Ключевые пути:
/proc/<pid>/status— читаемая человеком сводка о процессе: состояние, использование памяти (VmRSS и другие), UID/GID, число потоков./proc/<pid>/cmdline— команда запуска процесса с аргументами, разделёнными нулевыми байтами\0(не пробелами — тот же принцип, что уfind -print0, аргумент с пробелом внутри не спутать с границей между аргументами)./proc/<pid>/fd/— каталог, где каждый файл — символическая ссылка на открытый этим процессом файловый дескриптор (обычный файл, сокет, pipe); число ссылок в этом каталоге — реальное число открытых дескрипторов процесса прямо сейчас./proc/<pid>/maps— карта отображений памяти процесса: диапазоны адресов, права доступа (r/w/x), какому файлу или сегменту (куча, стек, конкретная библиотека) принадлежит каждый диапазон./proc/cpuinfo— параметры процессора (модель, число ядер, флаги поддерживаемых инструкций)./proc/meminfo— распределение оперативной памяти: занято, свободно, в кэше, в буферах (источник данных дляfree -h)./proc/<pid>/stat— машинно-читаемая (не для человека) строка с числовыми полями состояния процесса, включая, например, время в user/kernel-режиме; источник данных дляps/top.
Утилиты вроде ps, top, free, iostat — это не отдельный механизм сбора данных, а тонкая
обёртка над чтением этих же файлов /proc; ядро уже держит эти данные готовыми, специально
«собирать» их не нужно.
Ловушки
- Читать
/proc/<pid>/cmdlineкак обычную строку с пробелами в качестве разделителей аргументов → аргумент вида"two words"неотличим от двух отдельных аргументов → нужен именно разбор по\0.
Факты для карточек
- base | Что такое /proc с точки зрения хранения данных? — виртуальная файловая система, содержимое генерируется ядром на лету, не хранится на диске
- base | Чем разделены аргументы в /proc//cmdline? — нулевыми байтами
\0 - core | Что можно узнать из /proc//maps? — диапазоны адресов памяти процесса, права доступа (r/w/x) и к какому файлу/сегменту относится каждый диапазон
- core | Откуда
free -hберёт данные о памяти? — из /proc/meminfo
Почему дальше: /proc/<pid>/fd/ показывает текущее число открытых дескрипторов процесса;
верхнюю границу на это число задаёт ulimit.
10. ulimit: дескрипторы, core, стек
ulimit задаёт лимиты ресурсов для текущего шелла и процессов, запущенных из него:
ulimit -n— максимальное число одновременно открытых файловых дескрипторов на процесс; упереться в этот лимит на нагруженном сервере — типичная причина ошибки «Too many open files».ulimit -c— максимальный размер core dump файла; по умолчанию часто0(core dump отключён), поэтому перед тем как ловить редкий краш, выставляютulimit -c unlimited— иначе программа упадёт, а файла с состоянием памяти на момент падения просто не будет.ulimit -s— размер стека потока; именно этот лимит определяет, на какой глубине рекурсии процесс упадёт с переполнением стека (см. риск глубокой рекурсии при мемоизации в теме ДП).
Ловушки
- Ждать редкий краш под нагрузкой без
ulimit -c unlimitedзаранее → программа падает, core dump не создаётся (лимит 0 по умолчанию) → единственный шанс поймать причину падения упущен, воспроизвести заново может быть нечем.
Факты для карточек
- base | Что ограничивает
ulimit -n? — максимальное число открытых файловых дескрипторов на процесс - core | Почему перед отладкой редкого краша выставляют
ulimit -c unlimited? — по умолчанию размер core dump часто ограничен нулём, без этого файл с состоянием памяти на момент падения не создастся
Почему дальше: лимиты ulimit — это про ресурсы процесса; отдельная система ограничений — про
то, кому вообще разрешено читать, писать и исполнять конкретный файл.
11. Права: chmod/chown, umask
Права файла — три триады бит (владелец/группа/остальные) × (чтение r / запись w / выполнение
x), каждая триада кодируется одной восьмеричной цифрой: r=4, w=2, x=1, сумма даёт цифру для
триады. 755 = rwxr-xr-x: владелец — 7 (4+2+1, полный доступ), группа — 5 (4+1, чтение и
выполнение без записи), остальные — 5 (то же самое). chmod 755 file выставляет эти права
явно; chown user:group file меняет владельца и группу файла.
umask — маска, которая вычитается из прав по умолчанию при создании нового файла/каталога
(обычно каталоги создаются с базовым 777, файлы — с 666, дальше действует umask): при
umask 022 новый файл получает 666 & ~022 = 644 (rw-r--r--), новый каталог — 777 & ~022 = 755. umask не меняет права существующих файлов — только права по умолчанию для новых.
Факты для карточек
- base | Расшифровка прав 755? — rwxr-xr-x (владелец: полный доступ, группа и остальные: чтение+выполнение)
- base | Какие числа кодируют r, w, x? — r=4, w=2, x=1
- core | Что делает
umask 022с правами нового файла по умолчанию (666)? — вычитает 022, получается 644 (rw-r--r--)
Почему дальше: права управляют доступом к файлам статически; когда нужно понять, что процесс делает с файлами/сетью прямо сейчас, в динамике, нужны отдельные диагностические утилиты.
12. strace, lsof, ss как инструменты
strace -p PID(илиstrace command) — трассирует системные вызовы процесса построчно: какой syscall, с какими аргументами, что вернул. Способ увидеть, например, что процесс зависает именно наread()конкретного файлового дескриптора, а не где-то в логике приложения.lsof -p PID— список файлов (в широком unix-смысле — включая сокеты, pipe), открытых процессом; тот же смысл, что и/proc/<pid>/fd/, но в удобном для чтения формате с дополнительной информацией.ss -tan— состояние TCP-сокетов (замена устаревшемуnetstat): адреса, порты, состояние соединения (ESTABLISHED,TIME_WAITи так далее — см. состояния TCP).
Факты для карточек
- base | Что показывает
strace? — системные вызовы процесса с аргументами и результатом, построчно - core | Чем
lsof -p PIDи/proc/<pid>/fd/пересекаются по смыслу? — оба показывают список файловых дескрипторов, открытых процессом
Ссылки на задачи этого дня: tasks/08_bash — разбор access-лога (TOTAL/TOP/5XX) на
файле до миллиона строк; построчный bash-цикл не проходит по времени (раздел 7), нужен
awk/sort за один-два прохода.
Проверь себя
-
Скрипт с
set -eсодержитif grep -q pattern file; then echo found; fi, иgrepне находитpattern(возвращает 1). Остановится ли скрипт на этой строке?Ответ
Нет — команда внутри условия `if` не подчиняется `-e`, даже если она вернула ненулевой код; это одно из исключений `-e`, скрипт продолжит выполнение дальше. -
grep pattern missing_file.txt | wc -lпри отсутствующем файле напечатает0, а$?пайплайна безpipefailбудет0. Как обнаружить, что реальная проблема — отсутствующий файл, а не отсутствие совпадений?Ответ
Включить `set -o pipefail` — тогда код возврата пайплайна станет кодом первой упавшей команды (`grep` на несуществующем файле), а не только `wc -l`; либо проверить `${PIPESTATUS[0]}` явно после пайплайна. -
На файле в миллион строк нужно посчитать число запросов на каждый IP-адрес. Почему решение через
while read -r line; do ...; done < access.logс вызовомawk/grepвнутри цикла будет на порядки медленнее, чемawk '{print $1}' access.log | sort | uniq -c?Ответ
Цикл на bash с внешней командой внутри порождает (fork+exec) новый процесс на каждую из миллиона строк; связка `awk | sort | uniq -c` — это фиксированное малое число процессов (по одному на каждый элемент конвейера), каждый из которых делает один линейный проход по всему потоку целиком. -
Процесс с
ulimit -c 0неожиданно падает по SIGSEGV под нагрузкой, воспроизвести падение по требованию не получается. Что нужно было сделать заранее, чтобы разобрать причину постфактум?Ответ
Выставить `ulimit -c unlimited` до запуска процесса — тогда при падении ядро сохранит core dump со снимком памяти на момент сбоя, и его можно будет открыть позже (`gdb prog core`), не дожидаясь повторного воспроизведения краша.
Материалы
- man7.org,
proc(5)— https://man7.org/linux/man-pages/man5/proc.5.html - man7.org,
getrlimit(2)— https://man7.org/linux/man-pages/man2/getrlimit.2.html - man7.org,
ptrace(2)— https://man7.org/linux/man-pages/man2/ptrace.2.html - man7.org,
chmod(2)— https://man7.org/linux/man-pages/man2/chmod.2.html - man7.org,
umask(2)— https://man7.org/linux/man-pages/man2/umask.2.html - man7.org,
signal(7)— https://man7.org/linux/man-pages/man7/signal.7.html