учебники, программирование, основы, введение в,

 

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

Хотели как лучше
В выключенном состоянии компьютер не может выполнять никаких основных функций, разве что служить подставкой для кофе. На самом деле и во включенном - после нажатия тумблера "Вкл." - он будет способен адекватно отзываться на требования пользователя далеко не сразу. Прежде чем выйти на штатный режим работы, система должна загрузиться: следует разместить в памяти ядро (возможно, с дополнительными модулями) и передать ему управление, ядро должно провести опрос аппаратуры, не помешает быстрый профилактический осмотр состояния системы, затем нужно запустить необходимые для работы демоны и т. д.
Все, что происходит после включения компьютера и до входа системы в штатный режим (пока на терминале не появится "login"), называется начальной загрузкой системы. Процедуру начальной загрузки можно разделить на две стадии: досистемная - от включения до старта ядра и системная - начальная работа ядра и все, что происходит после.
Досистемная начальная загрузка, вообще говоря, системно-независима. Одни и те же действия компьютера с некоторыми подсказками от пользователя (или без них) могут завершаться загрузкой разных ядер в различном формате, а значит, и разных операционных систем. Зато досистемная загрузка аппаратно-зависима, причем работа составляющих ее процедур зависит не только от архитектуры системы, но и от конфигурации аппаратуры, которую очень часто приходится задавать в явном виде.
Поэтому рассмотрим оторванную от конкретной архитектуры, идеальную последовательность начальной загрузки, конечная цель которой - загрузить ядро и передать ему управление. В один присест это сделать не удастся (где уж компьютерному железу знать местоположение ядра неизвестно какой системы, не говоря уже о способе его загрузки!), поэтому вся последовательность разделена на уровни: если какое-то действие нельзя выполнить из-за ограничений текущего уровня, значит, необходимо ввести следующий, в котором подобных ограничений не будет. Цель каждого уровня - выбрать (или позволить пользователю выбрать) следующий.
Первые действия выполняются процедурой, записанной в ПЗУ. Задача этой процедуры - выявить устройства, с которых возможна загрузка, и попытаться загрузиться с них. Программа, записанная в ПЗУ, не может быть слишком уж велика (имеются исключения, о которых см. далее), но ее достаточно для того, чтобы воспользоваться настройками и параметрами из энергонезависимой памяти (non-volatile RAM, nvram; иногда встречается сокращение CMOS). Этот уровень называется BootROM (от ROM - ПЗУ (практически во всех современных компьютерах BootROM хранится не в ПЗУ, а в т. н. ППЗУ - программируемом ПЗУ. В случае крайней необходимости содержимое BootROM можно-таки изменить ("перешить"), однако действие это нештатное). BootROM не обязан, да и не может знать все тонкости всех внешних устройств, с которых можно загружаться, однако простейшие команды вида "считать с устройства один (первый) блок" ему доступны. Уже со вторым блоком устройства может быть не все ладно (а ну как он ровно один?), но первый-то есть всегда (скажем, сектор 0/0/1 на диске в нотации CHS, т. е цилиндр/головка/сектор).
Следующий уровень находится в том самом считанном блоке, поэтому называется он BootBlock или первичный загрузчик. Поскольку BootBlock принадлежит конкретному устройству (например, диску), процедура этого уровня уже умеет считывать любые данные из любого места - вот только размер ее не позволяет быть слишком умной. Поэтому BootBlock знает только, как загрузить следующий уровень. Если речь идет о диске, BootBlock может знать, на сколько - и каких - разделов разбит этот диск и где в них размещается вторичный загрузчик, размер которого может быть любым, лишь бы строго заданным.
Следующий уровень - вторичный загрузчик. Это уже довольно разумная программа, поэтому уровень носит название BootProg. Раз уж программа загружается с определенного раздела диска, она вполне может разбираться в структуре файловой системы, размещенной на этом разделе (да и на других, если они того же типа) и в формате загрузки ядра. BootProg может передавать ядру параметры, ему можно указать в качестве ядра любой файл в файловой системе, настройки вторичного загрузчика можно тоже хранить в файле; скорее всего, BootProg обеспечит даже простейший способ просмотреть содержимое каталога в файловой системе.
Итак, вторичный загрузчик загрузил и настроил ядро, а затем передал ему управление. В работе ядра надо предусмотреть доступ к данным, переданным именно этим типом загрузчика, а остальное будет соответствовать стандартам загружаемой операционной системы.
Достоинство этой схемы в том, что уровни не зависят друг от друга. Мы можем использовать любой тип первичного загрузчика (а так оно и бывает при загрузке с разных устройств), лишь бы он умел находить вторичный. То же можно сказать о паре BootProg - ядро. В идеале трехуровневая схема позволяет избежать обратной зависимости: изменения работы более высокого уровня не требуют исправлений на более низком. Например, если в результате обновления системы изменится BootProg, не надо перенастраивать BootBlock. Что еще важнее, вполне можно обойти и прямую зависимость: чтобы (хотя бы однократно) изменить поведение нижнего уровня, не нужно загружать верхний. Например, для того чтобы "прописать" новое ядро, в BootProg не обязательно сначала грузить старое и часть системы, а потом менять настройки вторичного загрузчика. Для постоянного изменения настройки, скорее всего, придется загружать систему, здесь важна сама возможность выбора между, допустим, старым (работающим) и новым (экспериментальным) вариантами ядра.

Получилось как всегда

Истинный порядок вещей, увы, всегда беспорядочнее теории. У последовательности начальной загрузки два врага: спешка и тяжелая наследственность (под "тяжелой наследственностью" мы понимаем то, что в компьютерном мире именуется legacy: совместимость со старыми версиями, соответствие устаревшему или изначально неудобному стандарту и т. п.). Например, нестандартные операционные системы, использующиеся на компьютерах с архитектурой i386, породили жесткие и очень неудобные требования к разметке диска. Первый сектор диска (и без того размером всего 512 байт) отведен и под загрузочную процедуру, Master Boot Record, MBR, и под таблицу разбиения диска - Hard Disk Partition Table, HDPT. Таблица разбиения диска занимает 64 байт, ключ 0xAA55 в конце сектора - еще два, на программную область остается 446, а ведь ей предстоит разбираться с тем, как размечен диск.
Мало того. В HDPT помещается только четыре записи о том, где и какие разделы есть на диске. Формат самих записей тоже страдает от тяжелой наследственности: параметры современных дисков хронически не помещаются в отведенные для них 16 байт. Чтобы диск можно было разделить более чем на четыре части, пришлось выдумывать механизм дополнительных разделов: одна из четырех записей указывает на extended partition - часть диска, в начале которой есть еще одна HDPT (еще четыре записи), описывающая разбиение этой части; если необходимо дальнейшее деление, заводится еще один дополнительный раздел и т. д. В довершение путаницы обычно используются только две из четырех записей в HDPT: первая ссылается на какую-нибудь файловую систему, вторая - на дополнительный раздел; в HDPT этого раздела снова первая запись ссылается на файловую систему, вторая - на следующий вложенный дополнительный раздел и т. д.
Пример разбиения диска на трехсистемной машине приведен. Первый раздел занят файловой системой FAT-16 (установлен FreeDOS), второй отдан FreeBSD (дальнейшее разбиение этого раздела идет уже в терминах BSD), третий и последующие разделы используются Linux для файловых систем и области подкачки.
Каждый дисковый раздел соответственно поименован в /dev, правда, способ именования для различных версий UNIX различен, а кроме того, различаются по именам и разные типы дисковых устройств. Если мы имеем дело с жестким диском стандарта ATA (на сегодня это самый распространенный тип дисковых носителей), то имена соответствующих файл-дырок в FreeBSD будут начинаться на ad, а в Linux - на hd. В обеих системах четыре раздела в HDPT из MBR нумеруются независимо от типа; они называются основными разделами (primary partitions). Для всего остального вводится сквозная нумерация: при просмотре цепочки дополнительных разделов пятым, шестым и т. д. считается очередной непустой (и не являющийся ссылкой на HDPT) раздел.
Просмотреть таблицу разделов диска можно с помощью утилит семейства fdisk. Так выглядит наш пример с точки зрения sfdisk, написанной для Linux и перенесенной под FreeBSD (ОСТОРОЖНО! Утилиты семейства fdisk предназначены в основном для изменения таблиц диска!).

freebsd# sfdisk-linux -uS -l /dev/ad0
 
Disk /dev/ad0: 30.7 GB, 30750031872 bytes
255 heads, 63 sectors/track, 3738 cylinders
Units = sectors of 512 bytes, counting from 0
 
    Device Boot   Start         End     #sectors    Id  System
    /dev/ad0s1        63     2104514     2104452     6   FAT16
    /dev/ad0s2 * 2104515    12578894    10474380    a5  FreeBSD
    /dev/ad0s3  12578895    13601951    1023057     83  Linux
    /dev/ad0s4  13601952    60058655    46456704    5   Extended
    /dev/ad0s5  13602015    15698591    2096577     82  Linux swap
    /dev/ad0s6  15698655    27986111    12287457    83  Linux

В Linux эти разделы получили бы имена от hda1 до hda6, где а означает то же, что и 0 в FreeBSD: номер диска. Стоит заметить, что ad0s6 содержится в следующем дополнительном разделе, который начинается сразу за ad0s5, но собственного имени - согласно сквозной нумерации - не имеет. Начало ad0s6 отстоит от начала дополнительного раздела на одну дорожку (63 сектора), так же как и ad0s1 начинается с первой (а не с нулевой) дорожки диска.

BSD

Структура диска получается довольно сложной, а размер первого сектора - слишком малым. В таких условиях программная часть MBR работать как полноценный BootBlock не сможет. Фактически даже алгоритм разбора вложенных HDPT уже не помещается в 446 байт. Самое большее, на что способен такой предзагрузчик (в FreeBSD он называется boot0) - загрузить вместо себя один (первый, разумеется) сектор из раздела, упомянутого в HDPT-части MBR. В FreeBSD, путем особых хитростей, умудрились впихнуть выбор из четырех имеющихся разделов.
Дальнейшее - в случае загрузки по схеме FreeBSD (напоминаем, что досистемная загрузка системно-независима, поэтому данный метод можно применить к чему угодно) - соответствует идеальной трехуровневой последовательности. В этот сектор надо положить первичный загрузчик, он загрузит вторичный - уже побольше размером, он-то найдет и загрузит ядро. Для создания файловых систем в BSD используют не HDPT, а собственную схему разбиения, не зависящую от архитектуры компьютера. Получается, что для своих нужд система выделяет один раздел, который внутри делит по своим правилам (структура разбиения диска по принципу BSD носит название disklabel):

freebsd# disklabel /dev/ad0s2
# /dev/ad0s2:
#   size     offset  fstype      [fsize bsize bps/cpg]
 a: 524288        0  4.2BSD  2048 16384  32776 
 b: 1048576  524288  swap 
 c: 10474380      0  unused     0     0  # "raw" part, don't edit
 d: 524288  1572864  4.2BSD  2048 16384  32776 
 e: 8377228 2097152  4.2BSD  2048 16384  28552 

Принято (но не требуется), чтобы корневая файловая система размещалась в подразделе "a" (в нашем примере - /dev/ad0s2a), область подкачки - в подразделе "b". Кроме того, подразел "c" соответствует всему ad0s2: его можно использовать, если не требуется разбивать диск на подразделы и записывать вторичный загрузчик. Вторичный загрузчик присоединяется к таблице BSD-подразделов. Фактически он попадает внутрь первой файловой системы - ad0s2a, поэтому в заголовках файловых систем BSD имеется 8 Кбайт свободного места. Здесь приходится бороться уже с наследственностью BSD: вторичный загрузчик оказывается тоже слишком маленьким. В него уже всего не вставишь - в частности, нельзя сделать так, чтобы вместе с ядром можно было загрузить несколько любых модулей-драйверов.
Поэтому в FreeBSD ввели еще один уровень загрузки - собственно загрузчик (он так и называется - loader). Вторичный загрузчик берет loader из файловой системы, так что он-то может быть сколь угодно сложен и состоять из многих частей. Фактически loader - это небольшая однопользовательская система, задача которой - загрузить ядро и некоторое количество модулей, настроить их, скомпоновать с ядром и передать ему управление. С точки зрения интерфейса, loader - полноценный интерпретатор языка FORTH, так что логику загрузки модулей можно выдумать какую угодно.
Два слова о FORTH-интерпретаторе: аналогичным способом устроена загрузка компьютеров фирмы Sun Microsystems, имеющих архитектуру sparc. Маленькое, но важное отличие состоит в том, что в компьютерах sparc процедура начальной загрузки целиком, включая FORTH, записана в ПЗУ. Учитывая то, что на качестве в Sun не экономят, а внешние устройства используют только строго стандартные, такое решение представляется очень удобным. Правда, дорогим.
Вот так попытка обойти legacy привела к изобретению четырех уровней загрузки. Поскольку по крайней мере три уровня - boot0, BootProg (в FreeBSD он называется boot2) и loader могут работать интерактивно, время работы каждого вполне ощутимо (от двух до десяти секунд: пользователь должен успеть набрать команду). В результате, если ничего предварительно не подстраивать, общее время досистемной загрузки растягивается, и вдобавок неосторожное нажатие (скажем, в ответ на подсказку BootProg) на клавиши может привести к тому, что система не загрузится вообще.

Linux: LILO и GRUB

Процедуру загрузки можно изрядно сократить, если не давать пользователю никакого выбора вплоть до последней стадии - "большого" загрузчика, а уже его обучить загружать ядро (возможно, не одно) или первичные загрузчики других систем, если обучение невозможно. По этой схеме действует, например, стандартный загрузчик Linux - LILO (LInux LOader). Linux на архитектуре i386 пользуется стандартными HDPT и дополнительными разделами, поэтому LI (первичным загрузчиком LILO) можно заменить программную часть MBR. LI только и умеет, что загружать LO, вторичный загрузчик, а уж он-то разбирается, передавать ли управление ядру или вызывать другой первичный загрузчик (скажем, DOS). Тогда процедура загрузки станет двухуровневой: монолит LI+LO (первичный загрузчик + вторичный) -> ядро.
К сожалению, любое упрощение "идеальной" трехуровневой загрузки провоцирует разного рода зависимости между уровнями. Во-первых, нельзя загрузить никакую систему, если что-то случилось с любой из частей LILO, ведь именно оттуда вызываются первичные загрузчики систем. Однако если использовать трехуровневую схему - MBR (Boot0 из FreeBSD или BOOTACTV из пакета pfdisk в Debian GNU Linux) -> LILO -> ядро, этой неприятности можно избежать.
Во-вторых, при стандартном разбиении диска вторичный загрузчик упрятать иногда вообще некуда, кроме как в файл на файловой системе. Стандартный, да к тому же очень маленький, первичный загрузчик не в состоянии разбираться с файловыми системами, поэтому данные о расположении LO на диске приходится "зашивать" в LI, что называется, вручную. Это значит, что при любом изменении вторичного загрузчика (перемещении или изменении файла, изменении настроек) необходимо запускать системную утилиту lilo, которая и обновит в LI карту размещения LO - в терминах самого низкого уровня, например, в нотации C/H/S. Тут тяжелая наследственность просто пляшет на обломках, потому что нынешние жесткие диски зачастую имеют такую геометрию, что функции BIOS, которыми пользуется LI, вообще не достигнут места, указанного в карте; для этого надо пользоваться новыми функциями, а их может не быть в старых версиях BIOS. Некоторые диски поддерживают преобразование геометрии, режимы работы Large Block Access и т. д., а некоторые - нет. Все это требует особого варианта LI и всем этим можно управлять только из загруженной системы; если что-то пойдет не так, повторно загрузить систему не удастся.
Хуже всего то, что, в-третьих, вторичный загрузчик в LILO тоже недостаточно умен, чтобы разбираться в файловых системах. Дело в том, что он должен быть не слишком велик (размер карты размещения в LI ограничен), но при этом достаточно универсален, чтобы уметь загружать ядро, модули (о том, как это делается, речь впереди), вызывать первичные загрузчики других систем и т. п. Для того чтобы LO мог загрузить файл, этот файл должен быть представлен в собственной карте размещения LO, т. е. опять-таки "зашит вручную". Легко вообразить, сколь неприятно это свойство LILO: любое изменение ядра или загружаемых модулей (что, в отличие от изменения вторичного загрузчика, действительно может понадобиться) непременно должно сопровождаться перестройкой всей последовательности загрузки, иначе следующий запуск системы просто не состоится, потому что карта размещения устарела, и там, куда она указывает, на законных основаниях может лежать мусор.
Несколько лучше обстоят дела с другой схемой загрузки, основанной на GRUB (GRand Unified Bootloader) - универсальным и мощным, разрабатываемым в GNU. Как и LILO, GRUB предполагает немедленный вызов вторичного загрузчика из первичного, причем расположение вторичного загрузчика, как и в LILO, должно быть известно первичному заранее. Однако вторичный загрузчик GRUB гораздо более интеллектуален: он умеет разбирать содержимое различных файловых систем, распознает несколько форматов загрузки ядер и модулей и организует настоящий интерфейс командной строки, так что пользователь может сам управлять загрузкой нужных ему файлов.
Чтобы не усложнять первичному загрузчику задачу картами размещения, вторичный вписывается в неиспользуемое пространство - в сектора на диске прямо после загрузочного. Скорее всего, эти сектора (как и вообще вся первая дорожка диска) системой не будут использоваться, и места для GRUB там наверняка хватит. Если же места мало, туда помещается "полуторный" загрузчик - драйвер файловой системы, который только и умеет, что искать вторичный. И тот и другой способ накладывают ограничения на структуру жесткого диска и оставляют проблему зависимости уровней загрузки нерешенной.

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

Задачу предварительной загрузки модулей ядра такая организация досистемной загрузки, понятно, не решает. Поэтому в Linux воспользовались технологией, которая применяется при первоначальной установке системы. Процедура установки системы тоже ведь должна выполняться под управлением какой-то ОС? Скорее всего, это будет тот же Linux, только в облегченном варианте, в котором оставлены лишь функции, необходимые для установки. Как будет показано далее, для системной начальной загрузки требуется файловая система определенной структуры с некоторыми стандартными файлами, каталогами и утилитами. Но дискового пространства под это еще не отведено, так что приходится размещать такую файловую систему в памяти. Данная технология носит название initrd (initial ram disk, стартовый виртуальный диск): вторичному загрузчику передается два файла - ядро и образ стартового диска. Загрузчик размещает образ в памяти и загружает ядро, передавая ему информацию об initrd, как о файловой системе. Утилиты, расположенные в initrd, загружают модули, находящиеся в initrd, после чего становится доступной сеть, специфические дисковые и прочие внешние устройства.
Формально в initrd можно положить все, что угодно, без ощутимого неудобства для дальнейшей работы системы, лишь бы хватило оперативной памяти: после загрузки модулей и настройки стартовая виртуальная файловая система больше не нужна, так что про нее можно забыть, а память освободить. Все, что нужно для системной загрузки, находится в действительных файловых системах. Сборка initrd гораздо проще пересборки ядра: нужно только скопировать файлы, изготовить файловую систему, содержащую стандартное дерево каталогов, и обновить карту вторичного загрузчика, если таковая имеется.
Выходит, что initrd решает свою задачу с тем же успехом, что и LILO - свою. С одной стороны, цель достигнута: ядро не обязательно пересобирать, модули и настройки можно подгружать готовые, после загрузки ядра, так что отпала необходимость в средствах разработки (вроде компилятора, компоновщика, библиотек и пр.) для изменения параметров системы. К тому же механизм стартового виртуального диска очень гибок (мини-Linux!) и строго соответствует У, поскольку не вводит ничего сверх уже известного (ни дополнительного языка программирования, ни второго механизма загрузки и компоновки модулей, как в схеме FreeBSD). С другой стороны, платой за экономию становится все та же обратная зависимость: изменение initrd возможно только при уже частично загруженной системе, т. е. после отработки содержимого самого initrd. Так что с точки зрения разделения уровней загрузки initrd мало чем отличается от самой старой схемы с пересборкой ядра.


Задачи начальной загрузки ядра
Но вот ядро загружено, скомпоновано с модулями, получило дополнительные настройки и начало работу. С этой минуты начальная загрузка системы идет под управлением самой системы.
Первым делом ядро занимается определением параметров вычислительной подсистемы компьютера: выясняет тип и быстродействие центрального процессора, объем оперативной памяти, объем и структуру кэш-памяти; делает предположение об архитектуре компьютера в целом и многое другое. На основании полученных разведданных ядро вычисляет размеры внутренних таблиц, выставляет разные таймеры и счетчики, выбирает стратегию распределения памяти и вообще планирует будущую работу системы. Может случиться так, что одно и то же ядро не просто может работать на целом семействе компьютеров с похожей архитектурой, но и различает их, выбирая соответственно оптимизированные варианты основных процедур. Так поступают программы, которым действительно важно вычислительное быстродействие. Ядро системы - тоже такая программа, потому что любое обращение к системе идет через ядро, а значит, повышение быстродействия ядра поможет повысить быстродействие работы системы.
На следующем шаге ядро определяет состав и архитектуру всего аппаратного наполнения компьютера: тип и параметры шин передачи данных и устройств управления ими (контроллеров), список внешних устройств, доступных по шинам, настройки этих устройств - диапазон портов ввода/вывода, адрес ПЗУ, занимаемое аппаратное прерывание, номер канала прямого доступа к памяти и т. п. Старые устройства (например, работающие с шиной ISA архитектуры i386) не имели процедуры самодиагностики, и об их настройках приходилось или догадываться, или разглядывать положение микропереключателей на плате контроллера; в любом случае все настройки следовало заранее сообщать ядру (возможно, для этого требовалась пересборка). Более дорогие - или более новые - устройства получают эти настройки от более разумного контроллера шины (например, PCI в i386 или sbus в Sparc), он же и рассказывает ядру о том, какие именно найдены устройства и как он их настроил (подробнее об архитектуре вычислительных систем рассказано в , а в описана архитектура i386). На этой же стадии (причем в самом начале) определяется консоль системы - терминальная линия, на которую будет выводиться вся системная диагностика.
Эти две стадии называются процедурой самонастройки ядра (autoconfiguration). По окончании самонастройки UNIX уже хорошо представляет себе архитектуру компьютера и список важнейших внешних устройств. Ядро анализирует таблицы разбиения дисков и выбирает корневой раздел - файловую систему, содержащую будущий каталог / и его подкаталоги (для системной начальной загрузки очень важен каталог /etc и его содержимое, но не менее важны и /bin, и /sbin). Корневой раздел задается настройками ядра (при сборке, загрузке или в сохраненных настройках). Корневой раздел монтируется в качестве / (подробнее о монтировании см. главу 13), и первым делом из файла /sbin/init запускается процесс init. Этот процесс всегда находится в таблице процессов, и PID его всегда равен 1; если он ни с того ни с сего завершится, система перезагрузится, потому что без него ей делать нечего: именно init запускает все остальные процессы.
Кстати, не все процессы, перечисленные в таблице процессов, запущены init: в FreeBSD команда ps ax выдаст некоторое количество процессов (в том числе и имеющий PID=0), имена которых заключены в круглые скобки (в Linux - в квадратные). Это так называемые процессы ядра, то есть части ядра, зарегистрированные как процессы. Так они могут участвовать в планировке процессов и передаче сигналов на общих основаниях. Если ядро не найдет /sbin/init - например, оттого, что неправильно указан корневой раздел, - системная начальная загрузка остановится.

 

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика