Files

35 KiB
Raw Permalink Blame History

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 за один-два прохода.

Проверь себя
  1. Скрипт с set -e содержит if grep -q pattern file; then echo found; fi, и grep не находит pattern (возвращает 1). Остановится ли скрипт на этой строке?

    ОтветНет — команда внутри условия `if` не подчиняется `-e`, даже если она вернула ненулевой код; это одно из исключений `-e`, скрипт продолжит выполнение дальше.
  2. grep pattern missing_file.txt | wc -l при отсутствующем файле напечатает 0, а $? пайплайна без pipefail будет 0. Как обнаружить, что реальная проблема — отсутствующий файл, а не отсутствие совпадений?

    ОтветВключить `set -o pipefail` — тогда код возврата пайплайна станет кодом первой упавшей команды (`grep` на несуществующем файле), а не только `wc -l`; либо проверить `${PIPESTATUS[0]}` явно после пайплайна.
  3. На файле в миллион строк нужно посчитать число запросов на каждый 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` — это фиксированное малое число процессов (по одному на каждый элемент конвейера), каждый из которых делает один линейный проход по всему потоку целиком.
  4. Процесс с ulimit -c 0 неожиданно падает по SIGSEGV под нагрузкой, воспроизвести падение по требованию не получается. Что нужно было сделать заранее, чтобы разобрать причину постфактум?

    ОтветВыставить `ulimit -c unlimited` до запуска процесса — тогда при падении ядро сохранит core dump со снимком памяти на момент сбоя, и его можно будет открыть позже (`gdb prog core`), не дожидаясь повторного воспроизведения краша.

Материалы