Досистемная загрузка

Программа login, регистрирующая пользователей в системе, запускается только тогда, когда сама система уже приведена в полную готовность и работает в обычном режиме. Происходит это далеко не сразу после включения компьютера: Linux -- довольно сложная система, объекты которой попадают в оперативную память не сами собой, а в процессе загрузки. Сама загрузка -- процесс ступенчатый: поведение компьютера на различных этапах загрузки определяется совершенно разными людьми, от разработчиков аппаратной составляющей до системного администратора. Предъявляемые к системе требования гибкости, возможности изменять её настройку в зависимости от аппаратной составляющей, необходимость решать разные задачи с помощью одного и того же компьютера тоже делают процесс загрузки ступенчатым: сначала определяется профиль будущей системы, а затем этот профиль реализуется.

Начальный этап вообще не зависит от того, какая операционная система установлена на компьютере, для некоторых этапов в каждой операционной системе предлагаются свои решения -- по большей части, взаимозаменяемые. Эту стадию (начальную) назовём досистемной загрузкой. Начиная с определённого этапа загрузка компьютера уже управляется самим Linux, используются утилиты, сценарии и т. п. Эту стадию (завершающую) назовём системной загрузкой.

Загрузчик в ПЗУ

Сразу после включения, оперативная память компьютера классической архитектуры девственно чиста. Для того, чтобы начать работать, процессору необходима хоть какая-то программа. Эта программа автоматически загружается в память из постоянного запоминающего устройства, ПЗУ (или ROM, read-only memory), в которое она вписана раз и навсегда в неизменном виде(1). В специализированных компьютерах -- например, в дешёвых игровых приставках -- всё, что нужно пользователю, записывается именно на ПЗУ (часто сменное), и запуском программы оттуда загрузка заканчивается.

Обычно в компьютерах общего назначения программа из ПЗУ пользователю ничем полезна не бывает: она невелика, да и делает всегда одно и то же. Слегка изменить поведение программы из ПЗУ можно, оперируя данными, записанными в энерго-независимую память (иногда её называют CMOS, иногда -- NVRAM). Объём энерго-независимой памяти очень невелик, а данные из неё сохраняются после выключения компьютера за счёт автономного электропитания (как правило, от батарейки вроде часовой).

Что должна уметь эта начальная программа? Распознавать основные устройства, на которых может быть записана другая -- нужная пользователю -- программа, уметь загружать эту программу в память и передавать ей выполнение, а также поддерживать интерфейс, позволяющий менять настройки в NVRAM. Собственно, это даже не одна программа, а множество подпрограмм, занимающихся взаимодействием с разнообразными устройствами ввода-вывода -- как с теми, на которых могут храниться программы (жёсткие и гибкие диски, магнитные ленты и даже сетевые карты), так и теми, посредством которых можно общаться с пользователем (последовательные порты передачи данных -- если есть возможность подключить консольный терминал, системная клавиатура и видеокарта -- для простых персональных рабочих станций). Этот набор подпрограмм в ПЗУ обычно называется BIOS (basic input-output system).

BIOS

Сокращение от "Basic Input-Ooutput System", набор подпрограмм в ПЗУ, предназначенных для простейшего низкоуровневого доступа ко внешним устройствам компьютера. В современных ОС используется только в процессе начальной загрузки.

Этот этап загрузки системы можно назвать нулевым, так как ни от какой системы он не зависит. Его задача -- определить (возможно, с помощью пользователя), с какого устройства будет идти загрузка, загрузить оттуда специальную программу-загрузчик и запустить его. Например, выяснить, что устройство для загрузки -- жёсткий диск, считать самый первый сектор этого диска и передать управление программе, которая находится в считанной области.

Загрузочный сектор и первичный загрузчик

Чаще всего размер первичного дискового загрузчика -- программы, которой передаётся управление после нулевого этапа, -- весьма невелик. Это связано с требованиями универсальности подобного рода программ. Считывать данные с диска можно секторами, размер которых различается для разных типов дисковых устройств (от половины килобайта до восьми или даже больше). Кроме того, если считать один, первый, сектор диска можно всегда одним и тем же способом, то команды чтения нескольких секторов на разных устройствах могут выглядеть по-разному. Поэтому-то первичный загрузчик занимает обычно не более одного сектора в самом начале диска, в его загрузочном секторе.

Если бы первичный загрузчик был побольше, он, наверное, и сам мог бы разобраться, где находится ядро операционной системы и смог бы самостоятельно считать его, разместить в памяти, настроить и передать ему управление. Однако ядро операционной системы имеет довольно сложную структуру -- а значит, и непростой способ загрузки; оно может быть довольно большим, и, что неприятнее всего, может располагаться неизвестно где на диске, подчиняясь законам файловой системы (например, состоять из нескольких частей, разбросанных по диску). Учесть всё это первичный загрузчик не в силах. Его задача скромнее: определить, где на диске находится "большой" вторичный загрузчик, загрузить и запустить его. Вторичный загрузчик прост, и его можно положить в заранее определённое место диска, или, на худой конец, положить в заранее определённое место карту размещения, описывающую, где именно искать его части (размер вторичного загрузчика ограничен, поэтому и возможно построить такую карту).

карта размещения

Представление области с необходимыми данными (например, вторичным загрузчиком или ядром системы) в виде списка секторов диска, которые она занимает.

В случае IBM-совместимого компьютера размер загрузочного сектора составляет всего 512 байтов, из которых далеко не все приходятся на программную область. Загрузочный сектор IBM PC, называемый MBR (master boot record), содержит также таблицу разбиения диска, структура которой описана в лекции Работа с внешними устройствами. Понятно, что программа такого размера не может похвастаться разнообразием функций. Стандартный для многих систем загрузочный сектор может только считать таблицу разбиения диска, определить т. н. загрузочный раздел (active partition), и загрузить программу, расположенную в начале этого раздела. Для каждого типа диска может быть своя программная часть MBR, что позволяет считывать данные из любого места диска, сообразуясь с его типом и геометрией. Однако считывать можно всё же не более одного сектора: неизвестно, для чего используются установленной на этом разделе операционной системой второй и последующие сектора. Выходит, что стандартная программная часть MBR -- это некий предзагрузчик, который считывает и запускает настоящий первичный загрузчик из первого сектора загрузочного раздела.

Существуют версии предзагрузчика, позволяющие пользователю самостоятельно выбрать, с какого из разделов выполнять загрузку(2). Это позволяет для каждой из установленных операционных систем хранить собственный первичный загрузчик в начале раздела и свободно выбирать среди них. В стандартной схеме загрузки Linux используется иной подход: простой первичный загрузчик записывается прямо в MBR, а функция выбора передаётся вторичному загрузчику.

первичный загрузчик

Первая стадия загрузки компьютера: программа, размер и возможности которой зависят от аппаратных требований и функций BIOS. Основная задача -- загрузить вторичный загрузчик.

Загрузчик ядра

В задачу вторичного загрузчика входит загрузка и начальная настройка ядра операционной системы. Как правило, ядро системы записывается в файл с определённым именем. Но как вторичному загрузчику прочитать файл с ядром, если в Linux эта операция и есть функция ядра? Эта задача может быть решена тремя способами.

Во-первых, ядро может и не быть файлом на диске. Если загрузка происходит по сети, достаточно попросить у сервера "файл с таким-то именем", и в ответ придёт цельная последовательность данных, содержащая запрошенное ядро. Все файловые операции выполнит сервер, на котором система уже загружена и работает. В других случаях ядро "загоняют" в специально выделенный под это раздел, где оно лежит уже не в виде файла, а таким же непрерывным куском, размер и местоположение которого известны. Однако в Linux так поступать не принято, так как места для специального раздела на диске, скажем, IBM-совместимого компьютера, может и не найтись.

Во-вторых, можно воспользоваться описанной выше картой размещения: представить ядро в виде набора секторов на диске, записать этот набор в заранее определённое место, а загрузчик заставить собирать ядро из кусков по карте. Использование карты размещения имеет два существенных недостатка: её создание возможно только под управлением уже загруженной системы, а изменение ядра должно обязательно сопровождаться изменением карты. Если по какой-то причине система не загружается ни в одной из заранее спланированных конфигураций, единственная возможность поправить дело -- загрузиться с внешнего носителя (например, с лазерного диска). А система может не загружаться именно потому, что администратор забыл после изменения ядра пересобрать карту: в карте указан список секторов, соответствовавших старому файлу с ядром, и после удаления старого файла в этих секторах может содержаться какой угодно мусор.

В-третьих, можно научить вторичный загрузчик распознавать структуру файловых систем, и находить там файлы по имени. Это заметно увеличит его размер и потребует "удвоения функций", ведь точно такое же, даже более мощное, распознавание будет и в самом ядре. Зато описанной выше тупиковой ситуации можно избежать, если, скажем, не удалять старое ядро при установке нового, а переименовывать его. Тогда, если загрузка системы с новым ядром не удалась, можно загрузиться ещё раз, вручную указав имя файла (или каталога) со старым ядром, под чьим управлением всё работало исправно.

Вторичный загрузчик может не только загружать ядро, но и настраивать его. Чаще всего используется механизм настройки ядра, похожий на командную строку shell: в роли команды выступает ядро, а в роли параметров -- настройки ядра. Настройки ядра нужны для временного изменения его функциональности: например, чтобы выбрать другой графический режим виртуальных консолей, чтобы отключить поддержку дополнительных возможностей внешних устройств (если аппаратура их не поддерживает), чтобы передать самому ядру указания, как загружать систему и т. п.

Очень часто конфигурация вторичного загрузчика предусматривает несколько вариантов загрузки, начиная от нескольких вариантов загрузки одного и того же ядра с разными настройками (например, стандартный профиль и профиль с отключёнными расширенными возможностями), и заканчивая вариантами загрузки разных ядер и даже разных операционных систем. Это требует от самого загрузчика некоторого разнообразия интерфейсных средств. С одной стороны, он должен уметь работать в непритязательном окружении, например, обмениваться с пользователем данными через последовательный порт, к которому подключена системная консоль. С другой стороны, если есть стандартные графические устройства ввода/вывода, хотелось бы, чтобы загрузчик использовал и их. Поэтому все загрузчики имеют универсальный текстовый интерфейс (зачастую с довольно богатыми возможностями) и разнообразный графический (чаще в виде меню).

Особенная ситуация возникает в случае, когда на компьютере установлено несколько операционных систем (например, если персональный компьютер используется также и для компьютерных игр, строго привязанных к определённой системе). В этом случае не стоит надеяться на "универсальность" вторичного загрузчика: даже если он способен различать множество файловых систем и несколько форматов загрузки ядер, невозможно знать их все. Однако, если в загрузочном секторе раздела операционной системы записан первичный загрузчик, можно просто загрузить его, как если бы это произошло непосредственно после работы MBR. Таким образом, вторичный загрузчик может выступать в роли предзагрузчика, передавая управление "по цепочке" (chainloading). К сожалению, чем длиннее цепочка, тем выше вероятность её порвать: можно, например, загрузить по цепочке MS-DOS, удалить с его помощью раздел Linux, содержавший вторичный загрузчик, переразметить этот раздел, чем и привести компьютер в неработоспособное состояние.

вторичный загрузчик

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

Досистемная загрузка Linux

Несмотря на то, что досистемная загрузка не зависит от типа операционной системы, которая начинает работу после неё, большинство систем предоставляют собственные средства по её организации. В Linux наиболее популярны подсистемы загрузки LILO (LInux LOader) и GRUB (GRand Unified Bootloader). Обе эти подсистемы имеют текстовый и графический варианты интерфейса, предоставляющего пользователю возможность выбрать определённый заранее настроенный тип загрузки.

LILO

Подсистема загрузки LILO использует и для первичного, и для вторичного загрузчика схему с картой размещения. Это делает работу с LILO занятием, требующем повышенной аккуратности, так как изменение процедуры загрузки не атомарно: сначала пользователь изменяет ядро или его модули, потом -- редактирует файл /etc/lilo.conf, в котором содержатся сведения обо всех вариантах загрузки компьютера, а затем -- запускает команду lilo, которая собирает таблицы размещения для всех указанных ядер и вторичного загрузчика и записывает первичный и вторичный загрузчик вместе с картами в указанное место диска. Первичный загрузчик LILO (он называется LI) можно записывать и в MBR, и в начало раздела Linux.

Простейший файл lilo.conf может выглядеть так:

boot=/dev/hda
map=/boot/map
image=/boot/vmlinuz-up
        root=/dev/hda1

Простейшая настройка LILO: пример файла lilo.conf

Такая настройка LILO определяет ровно один вариант загрузки: первичный загрузчик записывается в начало первого жёсткого диска (строчка boot=/dev/hda), карту размещения утилита lilo записывает в файл /boot/map, ядро добывается из файла /boot/vmlinuz-up, а запись root=/dev/hda1 указывает ядру, что корневая файловая система находится на первом разделе первого диска.

Одна из машин, за которыми случалось работать Мефодию, использовалась иногда для запуска единственной программы, написанной для MS-DOS. Исходные тексты этой программы давно потерялись, автор -- тоже, поэтому на машине пришлось устанавливать и MS-DOS и Linux. В результате lilo.conf оказался таким:

[root@localhost root]# cat /etc/lilo.conf
 boot=/dev/hda
 map=/boot/map
 default=linux-up
 prompt
 timeout=50
 image=/boot/vmlinuz-up
         label=linux-up
         root=/dev/hda5
         initrd=/boot/initrd-up.img
         read-only
 image=/boot/vmlinuz-up
         label=failsafe
         root=/dev/hda5
         initrd=/boot/initrd-up.img
         vga=normal
         append=" failsafe noapic nolapic acpi=off"
         read-only
 other=/dev/hda1
         label=dos
 other=/dev/fd0
         label=floppy
         unsafe

Настройка LILO на двухсистемной машине

Здесь Linux был установлен на пятый раздел диска (о нумерации разделов в IBM-совместимых компьютерах рассказано в лекции Работа с внешними устройствами), а на первом находится MS-DOS. Кроме загрузки MS-DOS предусмотрено два варианта загрузки Linux и ещё один -- любой операционной системы с дискеты. Каждый вариант загрузки помечен строкой label=вариант. При старте LILO выводит простейшее(3) окошко, в котором перечислены все метки (в данном случае -- "linux-up", "failsafe", "dos" и "floppy"). Пользователь с помощью "стрелочек" выбирает нужный ему вариант и нажимает Enter. При необходимости пользователь может вручную дописать несколько параметров, они передадутся ядру системы. Если пользователь ничего не трогает, по истечении тайм-аута выбирается метка, указанная в поле default.

Ещё несколько пояснений. Метки linux-up и failsafe в примере используют одно и то же ядро (vmlinuz-up), но во втором случае перенастраивается режим графической карты и добавляются параметры, отключающие поддержку необязательных для загрузки аппаратных расширений (многопроцессорность, автоматическое управление электропитанием и т. п.). Строчку, стоящую после append= пользователь мог бы ввести и самостоятельно, это и есть параметры ядра. Поле initrd= указывает, в каком файле находится стартовый виртуальный диск (ему посвящён раздел Стартовый виртуальный диск и модули ядра этой лекции), а внушающая некоторые опасения надпись "unsafe" (для метки floppy) означает всего лишь, что дискета -- съёмное устройство, поэтому бессмысленно во время запуска lilo проверять правильность её загрузочного сектора и составлять карту.

Наконец, записи вида other=устройство говорят о том, что LILO неизвестен тип операционной системы, находящейся на этом устройстве, а значит, загрузить ядро невозможно. Зато ожидается, что в первом секторе устройства будет обнаружен ещё один первичный загрузчик, LILO загрузит его и передаст управление по цепочке. Так и загружается MS-DOS на этой машине: первичный загрузчик берётся (по метке dos) из начала первого раздела первого диска.

GRUB

Подсистема загрузки GRUB устроена более сложно. Она также имеет первичный загрузчик, который записывается в первый сектор диска или раздела, и вторичный загрузчик, располагаемый в файловой системе. Однако карта размещения в GRUB обычно используется только для т. н. "полуторного" загрузчика ("stage 1.5") -- по сути дела, драйвера одной определённой файловой системы. Процедура загрузки при этом выглядит так. Первичный загрузчик загружает полуторный по записанной в него карте размещения. Эта карта может быть очень простой, так как обычно полуторный загрузчик размещается непосредственно после первичного подряд в нескольких секторах(4), или в ином специально отведённом месте вне файловой системы. Полуторный загрузчик умеет распознавать одну файловую систему и находить там вторичный уже по имени (обычно /boot/grub/stage2). Наконец, вторичный загрузчик, пользуясь возможностями полуторного, читает из файла /boot/grub/menu.lst меню, в котором пользователь может выбирать варианты загрузки так же, как и в LILO. Таким образом, обновление и перенастройка установленного GRUB не требует пересчёта карт размещения и изменения чего-то, кроме файлов в каталоге /boot/grub.

По требованию Мефодия, Гуревич установил на двухсистемную машину GRUB. При этом файл /boot/grub/menu.lst получился таким:

[root@localhost root]# cat /boot/grub/menu.lst
 default 0
 timeout 50

 title linux-up
 kernel (hd0,4)/boot/vmlinuz-up root=/dev/hda5
 initrd (hd0,4)/boot/initrd-up.img

 title failsafe
 kernel (hd0,4)/boot/vmlinuz-up root=/dev/hda5 failsafe noapic nolapic acpi=off
 initrd (hd0,4)/boot/initrd-up.img

 title floppy
 root (fd0)
 chainloader +1

 title dos
 root (hd0,0)
 chainloader +1

Настройка GRUB на двухсистемной машине

Разница между lilo.conf только в синтаксисе, да ещё в том, что жёсткие диски и разделы на них GRUB именует по-своему, в виде (hdномер_диска,номер_раздела), причём нумеровать начинает с нуля. Метки ("title") тоже нумеруются с нуля, так что запись default 0 означает, что по истечении тайм-аута будет загружена самая первая конфигурация (по имени "linux-up").

Изучая руководство по GRUB, Мефодий обнаружил гораздо более важное отличие от LILO. Оказывается, в GRUB не только параметры, но и сами файлы (ядро, стартовый виртуальный диск и т. п.) распознаются и загружаются в процессе работы. Вместо пунктов меню можно выбрать режим командной строки, подозрительно похожий на bash, в котором заставить GRUB загрузить какое-нибудь другое, не предписанное конфигурацией, ядро, посмотреть содержимое каталогов файловой системы, распознаваемой полуторным загрузчиком, и даже содержимое этих файлов, невзирая ни на какие права доступа: система-то ещё не загружена. Мало того, можно по-своему перенастроить загрузчик и записать результаты настройки. Так и не успев сполна насладиться неожиданной свободой, Мефодий в один прекрасный день обнаружил, что выход в командную строку защищён паролем.

Действия ядра Linux в процессе начальной загрузки

Итак, досистемная загрузка проходит в три этапа.

  1. Загрузчик из ПЗУ определяет, с каких устройств можно грузиться и, возможно, предлагает пользователю выбрать одно из них. Он загружает с выбранного устройства первичный загрузчик и передаёт ему управление.
  2. Первичный загрузчик определяет (а чаще всего -- знает), где находится вторичный загрузчик -- большая и довольно умная программа. Ему это сделать проще, чем программе из ПЗУ: во-первых, потому что для каждого устройства первичный загрузчик свой, а во-вторых, потому что его можно легко изменять при изменении настроек загружаемой системы. В схеме, предлагаемой LILO и GRUB, первичный загрузчик не вступает в разговоры с пользователем, а немедленно загружает вторичный и передаёт ему управление.

  3. Вторичный загрузчик достаточно умён, чтобы знать, где находится ядро системы (возможно, не одно), предлагать пользователю несколько вариантов загрузки на выбор, и даже, в случае GRUB, разрешает задавать собственные варианты загрузки. Его задача -- загрузить в память ядро и всё необходимое для старта системы (иногда -- модули, иногда -- стартовый виртуальный диск), настроить всё это и передать управление ядру.

Ядро -- это и мозг, и сердце Linux. Все действия, которые нельзя доверить отдельной подзадаче (процессу) системы, выполняются ядром. Доступом к оперативной памяти, сети, дисковым и прочим внешним устройствам заведует ядро. Ядро запускает и регистрирует процессы, управляет разделением времени между ними. Ядро реализует разграничение прав и вообще определяет политику безопасности, обойти которую, не обращаясь к нему, нельзя просто потому, что в Linux больше никто не предоставляет подобных услуг.

Ядро работает в специальном режиме, т. н. "режиме супервизора", позволяющем ему иметь доступ сразу ко всей оперативной памяти и аппаратной таблице задач. Процессы запускаются в "режиме пользователя": каждый жёстко привязан ядром к одной записи таблицы задач, в которой, в числе прочих данных, указано, к какой именно части оперативной памяти этот процесс имеет доступ. Ядро постоянно находится в памяти, выполняя системные вызовы -- запросы от процессов на выполнение этих подпрограмм.

ядро

Набор подпрограмм, используемых для организации доступа к ресурсам компьютера, для обеспечения запуска и взаимодействия процессов, для проведения политики безопасности системы и для других действий, которые могут выполняться только в режиме полного доступа (т. н. "режиме супервизора").

Работа ядра после того, как ему передано управление, и до того, как оно начнёт работать в штатном режиме, выполняя системные вызовы, сводится к следующему.

Сначала ядро определяет аппаратное окружение. Одно и то же ядро может быть успешно загружено и работать на разных компьютерах одинаковой архитектуры, но с разным набором внешних устройств. Задача ядра -- определить список внешних устройств, составляющих компьютер, на который оно угодило, классифицировать их (определить диски, терминалы, сетевые устройства и т. п.) и, если надо, настроить. При этом на системную консоль (обычно первая виртуальная консоль Linux) выводятся диагностические сообщения (впоследствии их можно просмотреть утилитой dmesg).

Затем ядро запускает несколько процессов ядра. Процесс ядра -- это часть ядра Linux, зарегистрированная в таблице процессов. Такому процессу можно послать сигнал и вообще пользоваться средствами межпроцессного взаимодействия, на него распространяется политика планировщика задач, однако никакой задаче в режиме пользователя он не соответствует, это просто ещё одна ипостась ядра. Команда ps -ef показывает процессы ядра в квадратных скобках, кроме того, в Linux принято (но не обязательно), чтобы имена таких процессов начинались на "k": [kswapd], [keventd] и т. п.

Далее ядро подключает (монтирует) корневую файловую систему в соответствии с переданными параметрами (в наших примерах -- root=/dev/hda5). Подключение это происходит в режиме "только для чтения" (read-only): если целостность файловой системы нарушена, этот режим позволит, не усугубляя положение, прочитать и запустить утилиту fsck (file system check). Позже, в процессе загрузки, корневая файловая система подключится на запись.

Наконец, ядро запускает из файла /sbin/init первый настоящий процесс. Идентификатор процесса (PID) у него равен единице, он -- первый в таблице процессов, даже несмотря на то, что до него там были зарегистрированы процессы ядра. Процесс init -- очень, очень старое изобретение, он чуть ли не старше самой истории (истории UNIX, конечно), и с давних пор его идентификатор равен 1.

Загрузка системы

С запуска init начинается загрузка самой системы. В давние времена, во времена молодости Linux и ранее, в этом месте никаких подводных камней не наблюдалось. Если ядро содержало подпрограммы для работы со всеми необходимыми устройствами (т. н. "драйверы"), оно загружалось и запускало init. Если ядру недоставало каких-то важных драйверов (например, поддержки дискового массива, с которого и шла загрузка) -- не загружалось и не запускало. Из положения выходили просто: в ядро старались включить как можно больше драйверов. Такое ядро называлось базовым (generic), и имело довольно внушительный размер. Загрузивши систему с базовым ядром, администратор обычно пересобирал его: выбрасывал из специального файла-профиля драйверы всех отсутствующих в системе устройств, быть может, добавлял новые (те, что не нужны для загрузки, но нужны для работы, например, звуковые), и компилировал из исходных текстов новое, профильное ядро.

Стартовый виртуальный диск и модули ядра

Пересборка ядра нынче требуется очень редко. Во-первых, в Linux поддерживается несметное число различных внешних устройств, драйверы которых (особенно похожих, но разных) вполне могут помешать друг другу работать, если их использовать одновременно. Пришлось бы собирать множество разных ядер, без возможности указать пользователю, какое из них годится. Во-вторых, выяснением того, какой именно драйвер необходим найденному устройству, занимаются сейчас специальные программы, в распоряжении которых есть целые базы данных -- ядру такую работу делать неудобно, да и незачем. Это делает процедуру пересборки ядра почти что обязательной (пока не загружено базовое ядро, непонятно, какие драйверы добавлять в профильное). А в-третьих, пресборка ядра требует весьма высокой квалификации того, кто его пересобирает. Этот процесс нельзя ни автоматизировать, ни упростить. Утилита linuxconf, устроенная именно для этого на основе окон и меню, даёт на выходе работоспособное ядро в трёх случаях: (1) в руках профессионала, (2) при чётком следовании полной инструкции и (3) по случайности.


Не надо вручную пересобирать ядро, даже если учебник по Linux это советует!


Совсем другие времена настали, когда изобрели и активно внедрили в Linux загружаемые модули ядра. Модуль ядра -- это часть ядра Linux, которую можно добавлять и удалять во время работы системы. Модуль ядра -- не процесс, он работает в режиме супервизора и в таблице процессов не регистрируется: это набор подпрограмм для работы с определённым устройством, которые добавляются к возможностям ядра(5). При загрузке в память модуль компонуется с ядром, образуя с ним одно целое. Просмотреть список загруженных модулей можно командой lsmod, а подгрузить модуль в память, добавив его к ядру, и удалить его оттуда -- командами insmod и rmmod соответственно.

# lsmod
Module                  Size  Used by    Not tainted
usb-uhci               21676   0  (unused)
usbcore                58464   1  [usb-uhci]
af_packet              12392   1  (autoclean)
pcnet32                15140   1  (autoclean)
mii                     2544   0  (autoclean) [pcnet32]
crc32                   2880   0  (autoclean) [pcnet32]
floppy                 48568   0  (autoclean)
subfs                   4296   4  (autoclean)
ac                      1792   0 
rtc                     6236   0  (autoclean)
ext3                   62288   2 
jbd                    37852   2  [ext3]

Получение списка загруженных модулей

Изменилось и базовое ядро: теперь оно включает в себя только устройства, необходимые для загрузки системы: главным образом диски и графическую консоль. Остальные устройства определятся уже самой системой, тогда можно будет и распознать экзотическую аппаратуру, и модуль для неё подгрузить. Однако полностью перевести драйверы всех внешних устройств в модули мешает вот какое соображение: что, если загрузка системы происходит именно с того устройства, чей модуль ещё не загружен в ядро, например, с дискового массива (RAID). Вторичный загрузчик и ядро можно, недолго думая, разместить на другом носителе (например, на лазерном диске) или добыть с дискового массива средствами BIOS (карты размещения позволяют не обращать внимания на логическую структуру RAID). Но как добыть модуль работы с RAID, тот самый, что распознает эту логическую структуру?

модуль ядра

Необязательная часть ядра, расширяющая его функциональность. Модуль можно загрузить в память или удалить оттуда в процессе работы системы.

Подсистема загрузки GRUB умеет разбираться в файловых системах и даже подключать модули к ядру, однако для того, чтобы сделать процесс загрузки более или менее универсальным, пришлось бы обучить GRUB всем видам логики RAID, и всем способам подключения модулей. И то, и другое постоянно изменяется, и успевать за этими изменениями означает поддерживать собственную, параллельную Linux, дисковую подсистему.

Вдумаемся. Для того, чтобы средствами Linux подключить модуль ядра, работающий с дисковым устройством, необходимо загрузить Linux с этого же устройства. Так ли это невозможно? Ведь если можно прочесть оттуда "ядро", то, наверное, можно прочесть и "Linux"? Более точно, вдобавок к одной области данных, соответствующей ядру, надо прочитать вторую, соответствующую некоторой уменьшенной до предела установке Linux, в которой содержатся только нужные программы и модули, загрузить оттуда "маленький Linux", который настроит и подключит злополучный RAID и запустит процесс загрузки полноценной системы оттуда.

Предельно сжатый вариант Linux есть, это проект busybox, используемый во встроенных системах, где дорог каждый байт. Разместить файловую систему в памяти тоже легко, этим, например, занимается модуль tmpfs, который можно включить в базовое ядро (подробнее о типах файловых систем рассказано в лекции Работа с внешними устройствами). Осталось только обучить подсистемы загрузки GRUB и LILO считывать не одну, а две области данных -- ядро и образ файловой системы. Ядру при этом передаётся параметр "пользуйся виртуальным диском", чтобы оно подключило загруженный образ в качестве временной корневой файловой системы. Можно также потребовать, чтобы память, занимаемая временной файловой системой, освобождалась в процессе дальнейшей загрузки.

Такой механизм называется initrd (initial ram ddisk, где "ram" -- это не "баран", а random access memory, то есть оперативная память) или стартовым виртуальным диском. Стартовый виртуальный диск собирается по команде mkinitrd в соответствии с профилем компьютера, и записывается на диск по тем же правилам, что и ядро. В примере двухсистемной машины, за которой работал Мефодий, также был стартовый виртуальный диск, причём довольно маленький:

[root@localhost root]# ls -lg /boot
drwxr-xr-x  2 root   4096 Nov 20 21:08 grub
-rw-------  1 root 205374 Nov  9 01:33 initrd-2.4.26-std-up.img
lrwxrwxrwx  1 root     29 Nov  9 01:33 initrd-up.img -> initrd-2.4.26-std-up.img
-rw-------  1 root  45056 Nov 20 19:07 map
-rw-r--r--  1 root 935892 Aug  3 21:59 vmlinuz-2.4.26-std-up
lrwxrwxrwx  1 root     26 Nov  9 01:33 vmlinuz-up -> vmlinuz-2.4.26-std-up

Размеры и наименование файлов с ядром и стартовым виртуальным диском

Как видно из примера, ядро в четыре раза превосходит размером стартовый виртуальный диск. Стоит заметить, что и ядро, и образ диска упакованы с помощью утилиты gzip (причём ядро умеет распаковываться в памяти самостоятельно), поэтому их действительный размер больше. В файле map хранится карта размещения LILO, а упомянутые в lilo.conf и menu.lst файлы vmlinuz-up и initrd-up.img оказались символьными ссылками на файлы с более говорящими именами. Никаких требований к названиям ядер в Linux нет, это дело авторов дистрибутива. В этом случае в имени ядра и образа диска встречается версия ядра (2.4.26), тип сборки std (по-видимому, "standard") и тип архитектуры up (uniprocessor, т. е. однопроцессорная).

стартовый виртуальный диск

Минимальный набор программ и модулей Linux, необходимый для обеспечения загрузки системы. Представляет собой виртуальную файловую систему в оперативной памяти. Загружается вторичным загрузчиком вместе с ядром.

Отец всех процессов

Если в параметрах не сказано иного, ядро считает, что init называется /sbin/init. В стартовом виртуальном диске это обычно некоторый простейший сценарий, а в полноценной системе у init другая задача: он запускает все процессы. Если не он сам, то его потомки, так что все процессы Linux, кроме ядерных, происходят от init, как весь род людской -- от Адама.

Первым делом init разбирает собственный конфигурационный файл -- /etc/inittab. Файл этот имеет довольно простую структуру: каждая строка (если она не комментарий) имеет вид "id: уровни: действие: процесс", где id -- это некоторая двух- или однобуквенная метка, уровни -- это слово, каждая буква которого соответствует уровню выполнения, (об уровнях выполнения будет рассказано далее), действие -- это способ запуска процесса. Например, запись 4:2345:respawn:/sbin/mingetty tty4 означает, что меткой "4" помечен запуск /sbin/mingetty tty4(6) на уровнях выполнения 2, 3, 4 и 5 по алгоритму "respawn" (запустить в фоне, а когда процесс завершится, запустить заново). Помимо "respawn" существуют методы "once" (запустить в фоне однократно), "wait" (запустить интерактивно, при этом никаких других действий не выполняется, пока процесс не завершится) и множество других, включая даже "ctrlaltdel" -- процесс, запускаемый, когда пользователь нажимает на консоли Ctrl+Alt+Del(7).

Наконец-то Мефодий до конца понял, отчего getty ведёт себя так непохоже на остальные процессы: не просто запускает из-под себя login, а дожидается окончания его работы, отсутствуя при этом в таблице процессов. На самом деле дожидается не getty, а init, используя метод "respawn": порождается (в фоне) процесс getty с определённым PID, а init бездействует до тех пор, пока существует процесс с этим PID getty, login, стартовый командный интерпретатор или программа, запущенная из него с помощью exec); когда же процесс, наконец, умирает, порождается новый getty.

Запуск системных служб

Полноценно загруженная Linux-система -- не только login на виртуальной консоли. Системе есть чем заняться и помимо идентификации пользователей. Даже если компьютер не работает WWW-, FTP- или почтовым сервером для "внешнего мира", себе самой и своим пользователям система предоставляет множество услуг: отсылка заданий на печать и обеспечения их очереди, запуск заданий по расписанию, проверка целостности и т. п. Набор утилит и системных программ, предназначенных для предоставления таких услуг, принято называть подсистемами или службами.

Чему служат демоны?

Как правило, системная служба организована так. Во время начальной загрузки запускается в фоновом режиме программа, которая всё время работы системы находится в таблице процессов, однако большей частью бездействует, ожидая, когда её о чем-нибудь попросят. Для того, чтобы попросить эту программу об услуге, которую она предоставляет, используются утилиты, взаимодействующие с ней по специальному протоколу. По аналогии с сократовским "даймонионом", который незримо присутствует, по своей инициативе не делает ничего, не даёт совершать плохое и потворствует хорошему, такую программу стали называть "daemon". Не знакомые с творчеством Платона программисты-любители частенько переименовывали её в "demon" (демон); к сожалению, именно в такой, слегка инфернальной форме, daemon и вошёл в русскоязычную терминологию. Выходит, что в Linux услуги пользователям предоставляют... демоны!

демон

Запускаемая в фоне программа, длительное время пребывающая в таблице процессов. Обычно демон активизируется по запросу пользовательской программы, по сетевому запросу или по наступлению какого-либо системного события.

В ранних версиях UNIX всё, что нужно было запускать при старте системы, вписывалось в inittab. Было довольно удобно в одном файле указывать, какие именно демоны должны работать в системе, и в каком порядке их запускать. Само поведение демона при запуске явно рассчитано на использование в inittab по методу "wait": классический демон запускается интерактивно, проверяя правильность конфигурационных файлов и прочие условия работы, а затем самостоятельно уходит в фон (попросту делая fork() и завершая родительский процесс). Таким образом, init ждёт, пока демон работает интерактивно, а когда служба, возглавляемая этим демоном, готова к работе, переходит к следующей строке inittab. Однако часто бывает так, что автор демона знать не знает, какие именно условия разработчики той или иной версии Linux сочтут пригодными для запуска. Для этого ими создаётся стартовый сценарий, в котором запрограммирована логика запуска и останова службы. Кроме того, если на каждом из уровней выполнения запускается много различных служб, попытка записывать их запуск в inittab делает его громоздким и совсем неочевидным.

Гуревич посоветовал Мефодию отложить на время изучение загрузки системы, а сначала посмотреть, как управлять стартовыми сценариями.

Стартовый сценарий системной службы

Стартовый сценарий -- программа (обычно написанная на shell), управляющая включением или выключением какого-нибудь свойства системы. Это может быть запуск и остановка HTTP-сервера, активизация и деактивизация сетевых настроек, загрузки модулей и настройка звуковой подсистемы и т. п. Простейший стартовый сценарий обязан принимать один параметр, значение которого может быть словом "start" для запуска (включения) и "stop" для остановки (выключения). Если в определённом дистрибутиве Linux принято решение, что стартовые сценарии должны понимать и другие параметры, например "restart" (обычно "stop" + "start", но не всегда) и "status" (для опроса состояния), это требование распространяется на все стартовые сценарии. Единообразие позволяет, например, легко запускать и останавливать демоны, не выясняя, каков PID останавливаемого процесса и какой именно сигнал ему следует послать. Достаточно запуск написать так, чтобы PID процесса откладывался в специальный файл (обычно /var/run/имя_службы), а в остановку вписать что-то вроде kill -правильный_сигнал `cat /var/run/имя_службы`.

Все стартовые сценарии служб, которыми может воспользоваться система, принято хранить в каталоге /etc/rc.d/init.d (в некоторых дистрибутивах, для совместимости со старыми версиями UNIX, используется /etc/init.d, иногда это -- просто символьная ссылка на /etc/rc.d/init.d). Запустить или остановить службу можно, просто вызвав соответствующий сценарий с параметром "start" или "stop". Часто ту же самую задачу выполняет и специальная команда service -- которая проверяет, есть ли указанный стартовый сценарий, и запускает его(8).

[root@localhost root]# lsmod > old
[root@localhost root]# /etc/rc.d/init.d/sound stop
 Saving OSS mixer settings: [ DONE ]
 Unloading sound module (es1371): [ DONE ]
[root@localhost root]# lsmod > nosound
[root@localhost root]# service sound start
 Loading sound module (es1371): [ DONE ]
 Loading OSS mixer settings: [ DONE ]
[root@localhost root]# lsmod > new
[root@localhost root]# diff3 old new nosound
 ====3
 1:2,5c
 2:2,5c
  es1371                 25608   0 
  ac97_codec             11880   0  [es1371]
  soundcore               3652   4  [es1371]
  gameport                1628   0  [es1371]
 3:1a

Перезапуск звуковой подсистемы

Здесь Мефодий сначала остановил, а потом снова активизировал звуковую подсистему. Остановка привела к выгрузке звуковых модулей, а повторный запуск -- к загрузке, полностью аналогичной исходной. В этом Мефодий убедился, сравнив с помощью утилиты diff3 три списка модулей: old (до остановки звуковой подсистемы), new (после повторного запуска) и nosound (между остановкой и повторным запуском). Файлы old и new полностью одинаковы, а от nosound оба отличаются тем, что со второй строки по пятую содержат названия тех самых модулей ядра (среди которых затесался gameport, отвечающий за джойстик).

Схема ". d"

Итак, имеется способ единообразно и гибко управлять запуском и остановкой каждой системной службы в отдельности (или включением и выключением одного свойства системы). Однако задача целиком организовать загрузку системы, от запуска init до полноценной работы, этим ещё не решается. Первая из возникающих задач такова: чаще всего нужно загружать не все из размещённых в /etc/rc.d/init.d сценариев, потому что некоторые из установленных в системе служб администратор решил не использовать. Удалять оттуда не используемые при загрузке сценарии -- значит, лишать администратора возможности запускать эти сценарии вручную.

Создателям ранних версий UNIX пришла в голову простая мысль: написать один большой сценарий по имени /etc/rc, в который и заносить только нужные для запуска команды вида /etc/init.d/сценарий start. Можно даже занести туда все имеющиеся стартовые сценарии, но строки, запускающие те, что не используются, закомментировать. Такая (монолитная) схема имела существенный недостаток: добавление и удаление службы в систему (например, добавление и удаление пакета, содержащего исполняемые файлы службы) требовало редактирования этого файла. Если учесть, что порядок запуска служб весьма важен (например, бессмысленно запускать сетевые демоны до активизации сетевых настроек), становится ясно, что автоматическое изменение такого файла не может гарантировать нормальную загрузку системы, а значит, недопустимо.

На помощь пришла тактика, известная под именем "схема. d". Суть её в следующем. Пусть некоторый процесс управляется конфигурационным файлом, содержимое которого зависит от наличия и активации других служб системы (таким процессом может быть демон централизованной журнализации syslogd, ведущий журнал всех событий системы, или сетевой метадемон inetd, принимающий сетевые запросы и превращающий сетевой поток данных в обыкновенный посимвольный ввод-вывод). Тогда, чтобы избежать постоянного редактирования конфигурационного файла, его превращают в каталог (например, вдобавок к файлу /etc/xinetd.conf заводится каталог /etc/xinetd.d). Каждый файл в таком каталоге соответствует настройке одной службы: при добавлении её в систему файл появляется, при удалении -- исчезает. Остаётся только обучить тот же xinetd читать настройки не из одного xinetd.conf, но и из всех файлов в xinetd.d, и задача решена.

На случай запускаемых сценариев схема ". d" распространяется с двумя дополнениями. Во-первых, как уже было сказано, стартовые сценарии можно запускать, а можно и не запускать. Поэтому сам init.d не годится на роль ". d"-каталога. Впрочем, достаточно организовать ещё один каталог, скажем, rc.d, и создать там ссылки (для наглядности лучше символьные) на те сценарии из init.d, которые планируется запускать при старте системы. Общий стартовый сценарий rc как раз и будет заниматься запуском стартовых сценариев из rc.d.

Во-вторых, необходимо обеспечить строгий порядок запуска этих сценариев. Теоретически это совсем просто: отсортировать их по алфавиту, как это делает ls, и запускать подряд. Практически же такое требование накладывает ограничение на имя ссылки в ". d"-каталоге, поэтому принято, чтобы в начале имени стояло двузначное число. Тогда, запуская подряд все сценарии, отсортированные алфавитно, rc будет в первую очередь руководствоваться этим номером, а уж потом -- названием службы, которое после него стоит.

Уровни выполнения

В Linux схема начальной загрузки слегка сложнее, чем обычная ". d". Связано это с тем, что одну и ту же систему в разных случаях бывает необходимо загружать с разным набором служб. Если, скажем, использование сети нежелательно, удобнее сказать что-то вреде "Система! Загружайся без сети!", чем вручную удалять стартовые сценарии всех предположительно сетевых служб их ". d"-каталога. Необходимость выбора возникает также, если компьютер используется в качестве рабочей станции с запуском графической среды и всего с нею связанного или в качестве стоечного сервера, управлять которым лучше с системной консоли.

Поэтому в Linux предусмотрено несколько вариантов начальной загрузки, называемых уровни выполнения (run levels). Уровни выполнения нумеруются с 0 до 9:

  • Уровень 1 соответствует однопользовательскому режиму загрузки системы. При загрузке на уровень 1 не запускается никаких служб, и даже системная консоль, как правило, бывает доступна только одна, так что в системе может работать не более одного пользователя. В однопользовательском режиме изредка работает администратор -- исправляет неполадки системы, изменяет ключевые настройки, обслуживает файловые системы.

  • Уровень 2 соответствует многопользовательскому режиму загрузки системы с отключённой сетью. В этом режиме не запускаются никакие сетевые службы, что, с одной стороны, соответствует строгим требованиям безопасности, а с другой стороны, позволяет запускать службы и настраивать сеть вручную.

  • Уровень 3 соответствует многопользовательскому сетевому режиму загрузки системы. Сеть при загрузке на этот уровень настроена, и все необходимые сетевые службы запущены. На этом уровне обычно работают компьютеры-серверы.

  • Уровень 5 соответствует многопользовательскому графическому режиму загрузки системы. На этом уровне обычно работают рабочие станции, предоставляя пользователям возможность работать с графической подсистемой X11. Сеть на этом уровне настроена, а вот список запущенных сетевых служб может быть меньше, так как рабочая станция не всегда выполняет серверные функции (хотя, безусловно, может).

  • Уровни 0 и 6 -- специальные. Они соответствуют останову и перезагрузке системы. В сущности, это удобные упрощения для действий, обратных загрузке на уровень: все службы останавливаются, диски размонтируются. В случае останова даже электропитание можно отключать программно, если аппаратура позволяет, а в случае перезагрузки система идёт на повторную загрузку.

Остальные уровни никак специально в Linux не описаны, однако администратор может использовать и их, определяя особый профиль работы системы. Переход с уровня на уровень происходит очень просто: по команде init номер_уровня. На какой уровень загружаться при старте системы, написано в inittab (в поле действие должно быть написано "initdefault", а в поле уровни -- только одна цифра). Узнать текущий уровень выполнения можно с помощью команды runlevel:

[root@localhost root]# grep initdefault /etc/inittab 
 id:3:initdefault:
[root@localhost root]# runlevel
 N 3

Задание и просмотр уровня выполнения

уровень выполнения

Сохранённый профиль загрузки системы. В Linux реализован выполнением всех сценариев остановки и запуска служб из подкаталога rc.уровеньd каталога /etc или /etc/rc.d

Схема ". d" легко учитывает уровни выполнения. В каталоге /etc/rc.d(9) заводится несколько ". d"-подкаталогов, соответствующих каждому уровню выполнения: /etc/rc.d/rcуровень.d. Именно оттуда их запускает стартовый сценарий /etc/rc.d/rc.

[root@localhost root]# ls -F /etc/rc.d
 init.d/  rc.powerfail*  rc0.d/  rc2.d/  rc4.d/  rc6.d/
 rc*      rc.sysinit*    rc1.d/  rc3.d/  rc5.d/  scripts/
[root@localhost root]# ls /etc/rc2.d
 K10power       K75netfs    S15random   S31klogd     S37gpm      S54sshd
 K44rawdevices  K95kudzu    S30sound    S32hotplug   S40crond    S98splash
 K50xinetd      S10network  S30syslogd  S35keytable  S41anacron  S99local
[root@localhost root]# ls -l /etc/rc2.d/ K75netfs
 lrwxrwxrwx  1 root root 15 Nov  9 01:16 /etc/rc2.d/K75netfs -> ../init.d/netfs

Содержимое каталогов /etc/rc.d и /etc/rc.d/rc2.d

Переход с уровня на уровень должен сопровождаться не только запуском, но и остановкой служб. Это касается не только уровней 0 и 6, но и любых других. Например, при переходе с уровня 3 на уровень 2 необходимо остановить все сетевые службы. Поэтому схема ". d" была расширена: сначала с параметром "stop" запускаются сценарии, имена которых начинаются на "K" (Kill), а затем, с параметром "start" -- те, имена которых начинаются на "S" (Start). В приведённом примере при переходе на уровень 2 останавливаются несколько служб, в том числе сетевой метадемон (K50xinetd) и монтирование по сети удалённых файловых систем (K75netfs). Если при переходе с уровня на уровень некой службе не требуется менять своего состояния, сценарий не запускается вовсе. Так, при переходе с уровня 3 на уровень 2 сетевые настройки остаются активными, поэтому соответствующий сценарий (S10network), скорее всего, запущен не будет.

Долгое время считалось, что определение порядка загрузки -- дело системного администратора, поэтому расставлять символьные ссылки в каталоги rc*.d приходилось вручную. Однако одно из другого не следует: можно предусмотреть и более лёгкий способ наполнения этих каталогов ссылками. Один из способов такой: поставить в стартовый сценарий комментарий особого вида, в котором и описать, на каких уровнях служба должна быть активизирована, и какой по порядку должна быть запущена и остановлена:

[root@localhost root]# grep chkconfig /etc/init.d/netfs
 # chkconfig: 345 25 75
[root@localhost root]# chkconfig --list netfs
 netfs           0:off   1:off   2:off   3:on    4:on    5:on    6:off
[root@localhost root]# ls /etc/rc.d/rc*.d/*netfs
 /etc/rc.d/rc0.d/K75netfs  /etc/rc.d/rc3.d/S25netfs  /etc/rc.d/rc6.d/K75netfs
 /etc/rc.d/rc1.d/K75netfs  /etc/rc.d/rc4.d/S25netfs
 /etc/rc.d/rc2.d/K75netfs  /etc/rc.d/rc5.d/S25netfs

Управление порядком выполнения стартовых сценариев

Здесь Мефодий использовал утилиту chkconfig, которая ищет в стартовом сценарии комментарий вида chkconfig: уровни вкл выкл, и самостоятельно проставляет ссылки в соответствии с этими полями: во всех каталогах, упомянутых в уровнях соответствующий netfs сценарий имеет вид Sвклnetfs, а во всех остальных -- Kвыклnetfs. Эта же утилита позволяет добавлять и удалять службу на каждом уровне в отдельности или запрещать её вовсе.

Загрузка типичной системы на уровень выполнения 5

Итак, что же происходит после запуска init?

[root@localhost root]# grep rc /etc/inittab
 si::sysinit:/etc/rc.d/rc.sysinit
 l0:0:wait:/etc/rc.d/rc 0
 l1:1:wait:/etc/rc.d/rc 1
 l2:2:wait:/etc/rc.d/rc 2
 l3:3:wait:/etc/rc.d/rc 3
 l4:4:wait:/etc/rc.d/rc 4
 l5:5:wait:/etc/rc.d/rc 5
 l6:6:wait:/etc/rc.d/rc 6
[root@localhost root]# grep initdefault /etc/inittab 
 id:5:initdefault:

Стартовые сценарии в /etc/inittab

Метод "sysinit" в inittab означает, что процесс запускается во время начальной загрузки системы, до перехода на какой-нибудь уровень выполнения. Следовательно, первым запускается сценарий /etc/rc.d/rc.sysinit. Он настраивает аппаратуру дисковых массивов, проверяет и монтирует дисковые файловые системы, инициализирует область подкачки, межсетевой экран -- словом, делает всё, без чего дальнейшая полноценная загрузка системы невозможна. Далее из строчки с "initdefault" init узнаёт, что уровень выполнения по умолчанию -- пятый (многопользовательский графический), и выполняет все строки из inittab, в поле уровни которых есть 5. В частности, запускается сценарий rc с параметром 5 (l5:5:wait:/etc/rc.d/rc 5), который и выполняет необходимые действия из ". d"-каталога /etc/rc.d/rc5.d. Метод запуска rc -- "wait", так что init ждёт, пока не выполнятся все стартовые сценарии, а потом продолжает разбор inittab.

[root@localhost root]# ls /etc/rc.d/rc5.d/
 K10acpid    S10network  S30syslogd     S37gpm      S50xinetd
 K20nfs      S13portmap  S31klogd       S40crond    S54sshd
 K65apmd     S15random   S32hotplug     S41anacron  S56rawdevices
 K86nfslock  S25netfs    S35keytable    S44xfs      S64power
 S05kudzu    S30sound    S36update_wms  S45dm       S98splash

Профиль системы на уровне выполнения 5

Мефодий заметил, сценарий K20nfs (с параметром "stop") не выполнился: соответствующего сообщения на системной консоли не появилось. Беглый просмотр /etc/rc.d/init.d/nfs показал, что этот сценарий предназначен для запуска и остановки сервера сетевой файловой системы (NFS). Сервер используется на уровне 3, а на уровне 5 -- нет, поэтому при переходе с 3 на 5 его следует останавливать. Поскольку во время начальной загрузки останавливать нечего, сценарий не выполнился.

Из служб, запускаемых именно на пятом уровне, примечателен шрифтовый сервер, под номером 44, (the X font server, xfs) -- программа, у которой графическая подсистема получает шрифты (нередко по сети, тогда такой сервер может быть один на несколько рабочих станций), и экранный диспетчер(10), под номером 45, (the X display manager, xdm) -- программа, регистрирующая пользователя на манер login, с той разницей, что регистрация и запуск графических приложений могут происходить по сети с удалённого компьютера. Тут разрешилась ещё одна загадка: вместо обычной виртуальной консоли и login-а, Мефодий нередко наблюдал окно графической подсистемы с надписью "Login:" и "Password:", а кое-где даже "Логин:", "Пароль:" и портрет самого пользователя! Оказывается, это были различные версии xdm. Дабы не забивать себе голову разрозненными сведениями, Мефодий решил до поры (до лекции Графический интерфейс (X11)) не использовать графическую среду и нажал Ctrl+Alt+F1, переключившись в текстовую консоль.

Текстовая консоль на пятом уровне доступна: записи вида 1:2345:respawn:/sbin/mingetty tty1 обычно включают 5 в поле уровни.

Останов системы

Как уже говорилось, операция, обратная загрузке системы -- останов -- устроена в Linux как специальный уровень выполнения: 0 -- если требуется выключить систему, и 6 -- если требуется перезагрузка. Соответствующие каталоги rc0.d и rc6.d будут состоять почти сплошь из ссылок вида K*, но как минимум один сценарий, killall, будет запущен с параметром "start". Этот сценарий остановит все процессы, которые не были остановлены K-сценариями: программы пользователей, демоны, запущенные администратором вручную, и т. п.

Нечего и говорить, что отключение электропитания в разгар работы системы -- операция очень рискованная. Даже в самом удачном случае при повторной загрузке rc.sysinit увидит, что файловые системы не были размонтированы и станет проверять их целостность. В не самом удачном случае эта целостность будет нарушена: некоторые открытые на запись и не закрытые файлы окажутся в странном, недописанном состоянии, появятся индексные дескрипторы, не связанные ни с каким каталогом и т. п. Как правило, такие ошибки исправляются программой восстановления файловых систем fsck: с одной стороны, за счёт дополнительных свойств файловой системы (журнализация, сводящая вероятность порчи к минимуму, логически упорядоченная запись и т. п.), с другой -- за счёт некоторых предположений, которые делает сама утилита fsck. Однако надеяться на неё нельзя: очень редко, но бывают неразрешимые противоречия в лишённой цельности файловой системе, и тогда fsck обращается за помощью к администратору, требуя подтверждения действий (например, для удаления испорченного файла, который точно раньше был), или выполняя эти рискованные действия автоматически. В первом случае всё время взаимодействия с администратором система будет работать в однопользовательском режиме, причём администратору предстоит разбираться с тем, что получилось, а во втором есть нешуточная вероятность того, что система испортится, а замечено это будет слишком поздно.

Останов системы может занимать больше времени, чем загрузка: например, процессы, выполняющие системный вызов (скажем, чтения с дискеты), не завершаются по сигналу TERM сразу, а получив его, могут некоторое время заниматься обработкой (дописыванием в файл и т. п.). Остановка службы, особенно сетевой, тоже может длиться долго: например, когда требуется сообщить о закрытии сервиса каждому клиенту. Однако только в этом случае можно быть уверенным, что все процессы завершились нормально, и что после перезагрузки они продолжат нормально работать.

В экстренных случаях (например, когда при сбое электропитания демон, обслуживающий устройство бесперебойного снабжения, сообщает, что ресурсы на исходе), безопаснее всё-таки быстро поостанавливать процессы, чем дожидаться отключения питания на работающей системе. Для этого можно послать всем процессам сначала TERM, а короткое время спустя -- KILL. Для обработки таких ситуаций в inittab есть методы, начинающиеся со слова "power", а в /etc/rc.d -- специальный сценарий rc.powerfail. На самый крайний случай существуют команды halt и reboot с ключом -f, однако их почти мгновенное действие практически эквивалентно внезапной потере питания, и использовать их не рекомендуется.

Для останова или перезагрузки системы можно выполнять команды init 0 и init 6. Они вполне справятся с оповещением и остановкой активных в системе программ, что займёт минуту-две. А вот с пользователями, работающими в системе, всё сложнее. Как правило, для завершения работы требуется хотя бы минут пять, а лучше -- десять. Поэтому вежливые администраторы пользуются утилитой shutdwon, которая запускается за несколько минут до времени перезагрузки, каждую минуту предупреждая пользователей о грядущем событии в работе системы, после чего уже запускает init:

[root@localhost root]# shutdown -r +3 "Sorry, we need to reboot"
 Broadcast message from root (ttyS0) (Sun Nov 28 14:05:41 2004):
 Sorry, we need to reboot 
 The system is going DOWN to maintenance mode in 3 minutes!
  . . .
 Broadcast message from root (ttyS0) (Sun Nov 28 14:06:41 2004):
 Sorry, we need to reboot 
 The system is going DOWN to maintenance mode in 2 minutes!
  . . .
 Broadcast message from root (ttyS0) (Sun Nov 28 14:07:41 2004):
 Sorry, we need to reboot 
 The system is going DOWN to maintenance mode in 1 minute!
  . . .
 Broadcast message from root (ttyS0) (Sun Nov 28 14:08:41 2004):
 Sorry, we need to reboot 
 The system is going down to maintenance mode NOW!
 INIT: Switching to runlevel: 6
  . . .

Использование shutdown

Остаётся заметить, что у shutdown есть обязательный параметр -- время начала останова, в примере он равен "+3", то есть "через три минуты", и необязательный -- "-r" (reboot, перезагрузка) или "-h" (halt, останов). Без необязательных параметров выполняется переход на первый уровень выполнения, причём запускается стартовый командный интерпретатор суперпользователя, а после его завершения система вновь переходит на уровень выполнения по умолчанию (используется, например, для профилактических действий в системе). Нажатие Ctrl+Alt+Del или кнопки выключения питания (в системах, где эта кнопка ничего не выключает, а лишь посылает соответствующий аппаратный сигнал) приводит к запуску именно shutdown -r или shutdown -h.


(1) Современные компьютеры используют программируемые ПЗУ, содержимое которых можно изменять, однако такое изменение всегда считается ситуацией нештатной: например, запись новой версии содержимого ПЗУ, в которой исправлены ошибки (upgrade).

(2) Например, BOOTACTV из пакета pfdisk или стандартный для FreeBSD предзагрузчик boot0, которые, в силу их досистемности, можно применять где угодно.

(3) Если установлен графический вариант интерфейса, то -- сколь угодно изукрашенное.

(4) Т. е. на нулевой дорожке нулевого цилиндра, начиная с сектора 2. Эта область диска часто не используется под файловые системы (см. лекцию Работа с внешними устройствами).

(5) Этим он скорее похож на динамическую библиотеку.

(6) mingetty -- упрощённый аналог getty, работающий только на виртуальных консолях.

(7) Понятно, что Ctrl+Alt+Del -- это не reset, а обычное сочетание клавиш. Для удобства пользователя его специально распознаёт клавиатурный драйвер, а ядро сообщает об этом init-у.

(8) В некоторых дистрибутивах Linux такая команда может называться invoke-rc.d, а команда, аналогичная описанному ниже chkconfig -- update-rc.d.

(9) В некоторых дистрибутивах -- в каталоге /etc/

(10) Не путать с диспетчером окон, описанным в лекции Графический интерфейс (X11).