Пара 4: VFS, библиотеки, память и конвейеры

90 минут · 3 курс, ML.

Содержание и результат

Соединять команды, проверять код возврата и писать небольшой повторяемый скрипт.

План занятия

0–15: stdin/stdout/stderr и pipe. 15–30: VFS, inode, dentry и открытый файл. 30–45: exec, ELF и shared libraries. 45–60: виртуальные адреса, demand paging, cache/swap и временные файлы. 60–80: конвейер и наблюдение памяти. 80–90: разбор вывода. Детали объектов VFS остаются в конспекте; не требуем реализации файловой системы.

Практика выполняется в своей учебной папке и на localhost. Подготовка окружения описана в lab/README.md.

Три стандартных потока

stdin — вход; stdout — результат; stderr — диагностика.

У процесса обычно есть стандартный вход с номером 0, выход с номером 1 и поток ошибок с номером 2. Символ > перенаправляет stdout и перезаписывает файл; >> добавляет. Ошибка может продолжать появляться в терминале, потому что идёт по stderr. Запись 2> сохраняет диагностику отдельно, 2>&1 направляет её туда, куда уже направлен stdout; порядок перенаправлений имеет значение. Конвейер | передаёт stdout одной команды на stdin следующей. Он не передаёт stderr автоматически. Разделение помогает не смешивать полезные CSV-данные с сообщениями об ошибках.

Аналогия: готовые блюда и сообщения о поломке кухни идут по разным каналам.

Конвейер обработки

Каждый инструмент выполняет небольшой понятный шаг.

Из учебного журнала выберем ошибки, выделим значение run и посчитаем повторения. Конвейер удобно читать слева направо. sort упорядочивает строки, uniq -c считает соседние одинаковые строки, поэтому перед подсчётом обычно нужен sort. Команды не знают бизнес-смысл: если формат изменится, cut может извлечь не то поле. Для JSON используем структурный парсер, а не бесконечные grep. Сейчас работаем с контролируемым простым текстом и явно обсуждаем границу.

Аналогия: конвейер сортировки посылок; плохой шаг загрязняет весь результат.

Код возврата и повторяемость

Успех команды — обычно код 0, а не отсутствие текста.

Переменная $? содержит код завершения последней команды. Операторы && и || позволяют запускать следующий шаг при успехе или неуспехе. В Bash код конвейера по умолчанию относится к последней команде; set -o pipefail позволяет увидеть неуспех более раннего звена. set -e не универсальная защита: ожидаемые неуспехи нужно обрабатывать явно. grep возвращает 1, когда совпадений нет, и это иногда нормальный результат. Переменные заключаем в двойные кавычки, не используем eval для входных данных. Скрипт должен иметь понятный выход и ошибку, которую можно объяснить.

Успешно напечатать пустой результат и успешно решить задачу — не всегда одно и то же.

Подробный разбор: Потоки — это реальные связи между процессом и ядром; VFS, загрузчик библиотек и память

Куда на самом деле пишет stdout

Shell настраивает дескрипторы перед запуском программы.

Куда на самом деле пишет stdout

У процесса есть таблица открытых файловых дескрипторов. Традиционные номера 0, 1 и 2 означают stdin, stdout и stderr, но сами числа не говорят, какой объект подключён. Это могут быть терминал, обычный файл, pipe или socket. При python demo.py >out.log 2>err.log shell открывает объекты и связывает дескрипторы будущего процесса до выполнения программы. Программа пишет в 1, не выбирая экран. После fork ребёнок наследует дескрипторы, при exec открытые дескрипторы обычно сохраняются, если не помечены close-on-exec. Важно различать таблицу дескрипторов процесса и open file description ядра: разные дескрипторы могут ссылаться на одно описание и общий offset. Для первого занятия достаточно отследить маршруты байтов. Покажите print в stdout и stderr, затем откройте оба файла отдельно.

Аналогия: трубка с номером 1 подключена к выбранному приёмнику. Дескриптор не хранит содержимое файла.

Практическое следствие: Нет. Она пишет в stdout; shell уже настроил дескриптор.

Конвейер создаёт два процесса и канал

Pipe передаёт байты и заставляет ждать при пустом или полном буфере.

Конвейер создаёт два процесса и канал

Shell создаёт pipe и организует процессы по обе стороны. Записывающий конец становится stdout первой программы, читающий — stdin второй. Это не передача имени файла и не запуск второй программы только после полного окончания первой: программы могут работать одновременно. Ядро хранит ограниченный буфер, поэтому быстрый writer может ждать медленного reader; пустой pipe заставляет reader ждать данных. После закрытия всех записывающих концов reader получает EOF. Если лишний writer-дескриптор остался открыт, читатель может продолжать ждать. В опыте printf печатает несколько строк, wc -l считает переводы строк. Просим объяснить, почему терминал видит результат wc, но не исходный поток printf. Детали dup2 даём в конспекте, код системного программирования на C не требуем.

Аналогия: две станции соединены узкой лентой. В действительности передаются байты без знания их смысловой структуры.

Практическое следствие: Когда все записывающие концы pipe закрыты и чтение возвращает EOF.

VFS даёт общий файловый интерфейс

Одинаковые open/read работают поверх разных файловых систем.

VFS даёт общий файловый интерфейс

VFS — слой внутри Linux-ядра, объединяющий файловые интерфейсы. При разрешении пути dentry связывает имя с объектом, inode представляет сведения о самом объекте, а file description хранит состояние конкретного открытия, например позицию и flags. Дескриптор процесса указывает на открытый объект. Вызов read обращается к операциям конкретной файловой системы: ext4, tmpfs, procfs, NFS и другие имеют разные источники данных. Это объясняет и единое дерево, и то, почему «всё — файл» полезно как соглашение об интерфейсах, но не является буквальным описанием любого объекта ядра. Каталог хранит связи имён, hard link создаёт ещё одну связь с inode, symlink хранит путь. Расширение .csv не проверяет содержимое. Сеть и устройства могут быть доступны через дескрипторы, но операции и свойства отличаются от обычного дискового файла.

Практическое следствие: Путь и inode не тождественны. Разные имена могут указывать на один объект, а одно имя со временем — на разные объекты.

Удаление имени и освобождение данных — не один момент

Открытый файл может продолжать существовать после rm.

Удаление имени и освобождение данных — не один момент

Unlink удаляет связь имени в каталоге. Если у объекта есть другие hard links или его держит открытый file description, данные ещё могут оставаться доступны и занимать место. Типичный инцидент: удалили большой журнал, а df не показывает свободное место; процесс продолжает писать в открытый, уже безымянный файл. Смотрят lsof +L1 или соответствующие /proc/PID/fd для доступных процессов, затем используют поддерживаемый способ переоткрытия журнала или штатный рестарт конкретной службы. Не убивают все процессы и не очищают /var/log целиком. Разница df и du также может происходить из-за mounts, недоступных каталогов, reserved space и других причин — один симптом не доказывает удалённый открытый файл. При учебной демонстрации используем маленький tempfile, который автоматически удаляется, не заполняем диск.

Практическое следствие: Для освобождения места нужно убрать удерживающие ссылки и открытия, а не повторить rm несуществующего имени.

Shared library загружает динамический linker

Библиотека содержит код; ABI задаёт правила связи с программой.

Shared library загружает динамический linker

ELF-файл может указывать interpreter через PT_INTERP. При exec ядро организует начальное отображение, а динамический linker ищет необходимые shared libraries, проверяет ожидаемые зависимости и выполняет relocations. .so — Linux-библиотека, .dylib/framework — соответствующие механизмы macOS, DLL — Windows. Read-only страницы библиотечного кода могут разделяться между процессами, тогда как изменяемые данные каждого экземпляра остаются отдельными. Не все программы динамические: статическая сборка включает зависимости иначе. Ошибка «No such file or directory» для существующего ELF может означать отсутствующий interpreter, а не отсутствующий app. ABI mismatch нельзя исправлять скачиванием случайного libc.so. Ldd не выполняют на недоверенных файлах: безопаснее сначала читать metadata через readelf или objdump. Readelf также подходит для понимания архитектуры и dependencies.

Практическое следствие: Ubuntu: file /usr/bin/ls; readelf -l /usr/bin/ls; readelf -d /usr/bin/ls. Mac: file /bin/ls; otool -L /bin/ls.

RAM нужна для исполнения, но не всё копируется заранее

Виртуальная память отображает нужные страницы по мере обращения.

RAM нужна для исполнения, но не всё копируется заранее

CPU работает с адресами памяти, а ОС и аппаратная MMU связывают виртуальные адреса с физическими страницами. Код, stack, heap и libraries составляют адресное пространство. Mapping файла не равен немедленному чтению всех его байтов в физическую RAM: demand paging позволяет загрузить нужную страницу при обращении. Page fault — механизм, который может быть нормальным; он не тождественен падению приложения. Ядро также держит page cache, чтобы повторные чтения файлов не требовали каждого обращения к диску. Поэтому занятая RAM не вся принадлежит уникальным данным приложений. VSZ описывает виртуальный размер, RSS — резидентные страницы с особенностями учёта shared mappings. Суммирование RSS всех процессов может учитывать общие страницы многократно. Не путайте RAM с диском, виртуальным адресным пространством или видеопамятью.

Практическое следствие: Исполняются доступные отображённые страницы. Большой файл модели может читаться постепенно или через mmap, а не обязательно целиком до старта.

Swap помогает вытеснять страницы, но не заменяет RAM

Cache, anonymous memory и swap участвуют в разных решениях reclaim.

Swap помогает вытеснять страницы, но не заменяет RAM

Чистую file-backed страницу ядро может удалить из RAM и затем перечитать из исходного файла. Для anonymous или изменённых страниц нужен другой способ сохранить содержимое, например swap. Swap может быть разделом, файлом или связанным с memory compression механизмом; zram и zswap не являются простыми синонимами дискового swap. Swappiness задаёт соотношение оценённой стоимости swap-I/O и файлового paging, а не процент RAM, при котором внезапно начинается swap. free показывает used/free/buff/cache/available: для оценки запаса полезнее available, учитывающий возможность reclaim. Постоянная интенсивная выгрузка/возврат страниц ухудшает отзывчивость; vmstat si/so полезны вместе с CPU, I/O и журналом. При нехватке памяти возможен OOM-kill с записью в kernel journal. Для демонстрации читаем показатели и не вызываем намеренный OOM на ноутбуке.

Практическое следствие: Ubuntu: free -h; swapon --show; vmstat 1 3. Mac: vm_stat; sysctl vm.swapusage; Activity Monitor показывает другую модель учёта.

/tmp — назначение каталога, tmpfs — тип хранения

Временное имя не гарантирует RAM и конкретный срок очистки.

/tmp — назначение каталога, tmpfs — тип хранения

/tmp используется для временной работы и не годится для единственной копии результата. Его содержимое очищается согласно политике системы; /var/tmp предназначен для более длительных временных данных, /run — для runtime state текущей загрузки. Tmpfs хранит данные в виртуальной памяти и может использовать swap согласно конфигурации. /tmp может быть tmpfs или обычной дисковой файловой системой, поэтому проверяем findmnt -T /tmp, а не делаем вывод из имени. RAM-диск и tmpfs — не всегда один механизм. На общем /tmp действует sticky bit, но это не отменяет безопасного создания файлов: mktemp выбирает уникальное имя и помогает избежать подмены предсказуемого пути. Не очищайте чужие временные файлы ради лабораторной. Свою временную директорию программа должна удалять после использования.

Практическое следствие: findmnt -T /tmp показывает реальное хранение. tmpfs может обмениваться страницами со swap; после reboot данные не являются постоянным архивом.

Опыт: связать рисунок с наблюдением

printf "A\nB\n" | wc -l
python3 -c 'import sys; print("result"); print("diagnostic", file=sys.stderr)' >out.log 2>err.log
cat out.log
cat err.log

file /usr/bin/ls
# Optional when binutils is installed
readelf -l /usr/bin/ls | grep -F interpreter
readelf -d /usr/bin/ls | grep -F NEEDED
free -h
swapon --show
vmstat 1 3
findmnt -T /tmp
python3 lab/experiments/open_deleted.py

До запуска нарисуйте FD 0/1/2 для обеих программ. После запуска объясните, какие именно байты попали в каждый файл. Команды одинаковы в Ubuntu и macOS при наличии python3.

Свяжите VFS-схему с путём, descriptor и inode маленького tempfile. Сопоставьте free и ps без выделения больших массивов. Команды binutils устанавливаются заранее; readelf не является обязательной утилитой macOS.

Источники подробного разбора

Команды и наблюдения

Окружение: Ubuntu / Bash.

cd ~/devops-course/sandbox
printf "ERROR api
OK worker
ERROR api
ERROR db
" > events.log
grep -F ERROR events.log | cut -d " " -f 2 | sort | uniq -c
ls missing-file > result.txt 2> error.txt
printf "exit=%s
" "$?"
cat error.txt

Файл result.txt может быть пустым при ошибке. Проверяем код непосредственно после команды, пока другая команда не заменила его.

Ожидаемый результат: api встречается дважды, db один раз; ls возвращает ненулевой код, диагностика находится в error.txt.

Вариант для macOS

Стандартные потоки и конвейеры переносимы. Пользовательский shell macOS часто zsh; скрипт курса запускайте явно через bash. Системный Bash на macOS может быть старее Linux-версии; не переносите новые возможности Bash без проверки bash --version.

bash summary.sh "run log.txt"
grep -F ERROR events.log | cut -d " " -f 2 | sort | uniq -c

Практика: Скрипт сводки

  1. Создайте summary.sh с #!/usr/bin/env bash и set -o pipefail.
  2. Примите путь первым аргументом; при отсутствии файла напечатайте понятную ошибку в stderr и завершитесь с кодом 2.
  3. Выведите число строк и число ERROR; отсутствие ошибок должно давать 0, а не падение.
  4. Запустите на нормальном журнале, журнале без ERROR и несуществующем файле.

Результат: summary.sh и результаты трёх запусков.

Проверка: Все три случая предсказуемы; пути с пробелами работают.

Неисправность для разбора: grep без совпадений ошибочно считается аварией скрипта.

Решение и диагностика

Пример: file=${1:-}; if [[ ! -f "$file" ]]; then printf "File not found\n" >&2; exit 2; fi; lines=$(wc -l < "$file"); errors=$(grep -c -F ERROR "$file" || true); printf "lines=%s errors=%s\n" "$lines" "$errors". Здесь || true допустим после проверки читаемого учебного файла для ожидаемого кода 1; усиление решения — проверить отдельно коды grep 1 и 2, чтобы не скрывать ошибку чтения. Запуск bash summary.sh "run log.txt" не требует executable-бита.

Основные выводы

Самостоятельная работа

Улучшить скрипт: различать отсутствие совпадений и ошибку чтения; объяснить проверку кода возврата.

Источники