Леннарт Поттеринг, возглавляющий разработку системного менеджера systemd, прокомментировал ситуацию, сложившуюся вокруг ошибки 1073433 в Upstart, приводящей к повреждению корневой файловой системы в процессе выключения компьютера. При этом он рассказал о причинах и механизмах возникновения проблемы, указал на метод ее решения, и предупредил о вопросах, которые в обозримом будущем останутся нерешенными из-за ограничений архитектуры Upstart.
Чтобы обеспечить корректное состояние корневой файловой системы на момент выключения компьютера, система инициализации должна предварительно смонтировать ее в режиме «только для чтения». Чтобы ядро позволило выполнить данную операцию, на корневом разделе не должно оставаться ни одного файла, открытого на запись. Традиционно, это требование обеспечивается путем предварительной остановки всех процессов, кроме самого init, который сам по себе не держит открытых для записи файлов. Однако в некоторых ситуациях такой подход оказывается неэффективен — несмотря на то, что в системе остается только процесс init, не имеющий открытых на запись файлов, ядро все равно не позволяет перемонтировать корень только для чтения.
Анализ реализаций алгоритма остановки ОС в различных системах инициализации
Дело в том, что существует еще одно ограничение, действующее даже для файлов, открытых только для чтения. Если в то время, когда файл оставался открытым на чтение для некоторого процесса, этот файл был перезаписан целиком (удален и создан заново), процесс продолжает работать со старой версией файла, и файловая система будет ждать завершения этого процесса, чтобы окончательно удалить старый файл. На данном этапе, перемонтировать систему только для чтения нельзя. Подобная ситуация происходит в том числе и при обновлении исполняемого файла init, а также библиотек и других файлов, которые используются этим процессом.
Наиболее очевидное и, казалось бы, действенное решение — перезапускать процесс init (командой «telinit u») после каждого обновления соответствующих пакетов. Однако оно далеко не всегда является эффективным — существуют ситуации, когда выявить зависимости процесса init от библиотек весьма проблематично (например, при использовании NSS или аналогичных API для плагинов). Кроме того, проблема не ограничивается только библиотеками — например, она может затронуть и файлы системной локали. И наконец, далеко не всегда перезапись файлов связана с обновлением — например, процесс prelink тоже перезаписывает файлы библиотек.
Корректным решением проблемы является перезапуск процесса init при каждом выключении, непосредственно перед попыткой перемонтирования корня. Именно по этой причине, еще во времена SysV init в RHEL и Fedora, в их скриптах выключения присутствовала команда «telinit u». Таким же путем Поттеринг рекомендует пойти и разработчикам Upstart (которые пренебрегли его советом, всецело положившись на перезапуск init только при обновлении библиотек — недостатки такого шага описаны выше).
К слову сказать, в systemd данная проблема решена более изящно — вместо повторного запуска полноценного init со всеми сопутствующими библиотеками, запускается простой и компактный бинарный файл systemd-shutdownd, не использующий дополнительных зависимостей и действующий по примитивному, но надежному алгоритму: убиваются оставшиеся процессы, отмонтируются/перемонтируются оставшиеся файловые системы, отключаются loop-устройства, отключаются устройства подкачки, останавливаются устройства Device Mapper (RAID, LVM, LUKS и т.д.).
Разумеется, могут существовать целые стеки таких технологий, не позволяющие закончить за один проход (например, файловая система или раздел подкачки в loop-устройстве, использующем файл на одном из обычных разделов), поэтому shutdownd повторяет цикл снова и снова, пока остаются файловые системы, примонтированные для записи. Такой подход позволяет оставить систему гарантированно консистентной, независимо от сложности используемых технологий хранения.
Кроме того, существует еще одна проблема, связанная со сложными технологиями хранения. Дело в том, что даже примонтированная только на чтение корневая файловая система не позволяет корректно остановить блочное устройство, на котором она расположена. Если это обычный раздел на диске, такое и не требуется. Однако существуют и более сложные технологии, например, RAID и iSCSI. Несоблюдение правил корректной остановки сложного и/или сетевого устройства может привести к потере данных или повреждению метаданных. Но как отмонтировать корень, если с него запущен процесс, отвечающий за размонтирование?
Это ограничение можно обойти, если на финальной стадии запускать код для отмонтирования не с корня, а из виртуальной файловой системы, например, из образа initrd, антисимметрично тому, как это происходит при запуске системы (когда init из initrd монтирует корень и передает управление процессу init из него). Подобный подход, позволяющий корректно остановить все блочные устройства, в настоящее время реализован пока только в связке systemd и dracut (dracut — модульный конструктор initrd, используемый в Fedora и openSUSE).
Примечание: Интересно, что проблема перемонтирования корневой файловой системы, похоже, присутствует не только в Upstart, но и в OpenRC — там она выражается в зависании процесса выключения с сообщением «failed because we are using /», например, после запуска prelink. Несмотря на то, что разработчики OpenRC отмечают такие ошибки, как UNCONFIRMED, опытные пользователи Gentoo знают о проблеме, и рекомендуют добавлять команду «telinit u» в скрипт выключения ОС — /etc/init.d/mount-ro.