Пара 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 настраивает дескрипторы перед запуском программы.
У процесса есть таблица открытых файловых дескрипторов. Традиционные номера 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 — слой внутри 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 задаёт правила связи с программой.
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 нужна для исполнения, но не всё копируется заранее
Виртуальная память отображает нужные страницы по мере обращения.
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.
Чистую 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 используется для временной работы и не годится для единственной копии результата. Его содержимое очищается согласно политике системы; /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
Практика: Скрипт сводки
- Создайте summary.sh с #!/usr/bin/env bash и set -o pipefail.
- Примите путь первым аргументом; при отсутствии файла напечатайте понятную ошибку в stderr и завершитесь с кодом 2.
- Выведите число строк и число ERROR; отсутствие ошибок должно давать 0, а не падение.
- Запустите на нормальном журнале, журнале без 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-бита.
Основные выводы
- uniq сравнивает соседние строки.
- Перезаписывает существующее содержимое файла.
Самостоятельная работа
Улучшить скрипт: различать отсутствие совпадений и ошибку чтения; объяснить проверку кода возврата.