Старт операционной системы
В начале семестра я радостно заявил что мы будем устанавливать линукс, потому что я предполагал, что это будет нечто похожее на курс 11 года о том, как вообще этот линукс работает, но… вот сегодня мы выясним, нужен ли инсталл фест вообще. Потому что сегодняшние инсталляторы выглядят так: некст, некст, некст. Раньше я хвастался, что наши инсталляторы так выглядели...
Просто не очень представляю как он будет происходить, этот инсталл фест. В виртуалке? Вот об этом и речь. Ну, подумаем еще.
Сейчас я открою наш недоделанный планчик на сегодня и мы начнем. Пока оно открывается: главный недостаток сегодняшней лекции будет в том, смогу ли я что-то осмысленное показать.
Давайте вспомним, чем закончился разговор о том, как разворачивается система в старом юникс-лайк подходе формирования дистрибутива. У нас запускалось ядро, оно запускало процесс init, он отвечал за выведение операционной системы в штатное состояние. Что это значит: запуск каких-то действий однократных (диски проверить, базу починить, еще чего-то), и запустить несколько демонов, которые будут составлять ос с ядром. Поскольку демоны пишут разные люди - запуск демонов - отдельные процедуры, пишем шелл скрипт, обмазываем все это шеллом, получаем старт-стопный сценарий, потом .dse позволяет к сценарию присоединить префикс, когда запускаем сценарий, запускается .s с параметром старт, останавливаем - с параметром стоп.
Большой недостаток есть у такого метода, но мы к нему еще вернемся.
Как так получилось, что у нас вообще загрузилось ядро, а оно еще нашло корневую файловую систему, смонтировало, запустило процесс init...? Само собой это не происходит - включаешь компьютер, у него в мозгах нет ничего вообще. Если мы подходим к компьютеру с такой точки зрения, сначала теорию.
Она очень простая. Смотрите: вот компьютер включается, у него девственно чистый мозг, он не то что загрузится - программу никакую не может запустись. Чтобы что-то было, нужно, чтобы в этот мозг что-то попало: из какого-то пзу запускается какой-то код, который выполняет компьютер при включении.
Вот есть оперативная память, а есть постоянное запоминающее устройство, вот из него что-то копируется в мозг оперативной памяти, и запускается. То, что запускается из оперативной памяти при старте компьютера должно обладать способностями загрузки чего-нибудь. Хотя бы возможность выбрать устройство, на котором есть предположительно операционная система - железяку, и из этой железяки загрузить хоть что-нибудь.
Почему так? Потому что мы не можем гарантировать, что устройство, которое собираемся загрузить ос всегда одинаково. Более того, мы можем гарантировать, что оно всегда разное: все диски, флешки, извините, флоппи диски разные, короче говоря, мы не можем запихать - особенно так было 30 лет назад - не можем запихать в пзу возможность загрузить произвольную операционную систему с произвольного устройства.
Мы можем в пзу загрузить, какие есть устройства: первоначальная задача - выбрать с какого загружаться и загрузить с него нечто под названием загрузочный том.
Получается 2 стадии: загрузочный пзу и загрузочный блок.
Загрузочный блок - маленькая программа и загружается она с устройства, с которого запустим операционную систему. Это значит, что программа не унифицирована: для диска, флешки, остальных устройств она своя. А это в свою очередь значит, что она может обладать тайными знаниями, как с этого устройства запустить что-то более осмысленное, правда трудно ожидать, что эта маленькая программа будет знать, где лежит ядро операционной системы, как его загружать, как передавать ему управление и как делать много что еще. Со всей очевидностью мы не можем затолкать в один загрузочный блок какую-то логику, связанную непосредственно с операционной системой. Что мы можем втолкать в один загрузочный сектор ? Можем затолкать простую функциональность: загрузить с устройства большую программу и запустить ее. Все. Этого вполне достаточно. Эта большая программа - вторичный загрузчик - именно она будет загружать нашу операционную систему так, как мы этого хотим.
Эта большая программа может делать что угодно начиная от простых вещей (загрузчик ядра).
$ Qemu-system-i386
- мы наблюдаем первичный загрузчик, пытающийся найти хоть какие-то загрузочные устройства. Он их не нашел. Он пытался загрузить с флоппика, жесткого диска, тогда он загрузил первичный загрузчик сети, в сети он тоже ничего не нашел, и на это он сказал все, нет никаких устройств для загрузки.
А давайте ему предоставим какие-нибудь устройства для загрузки.
QEMU - эмулятор компьютера, под которым может запускаться произвольная операционная система, например, линукс - как вы видите архитектуры поддерживаются самые разные, при этом QEMU написан очень известным в русских кругах локальным гением Фабрисом Белларом, известным также тем, что он какой-то миллиардный знак нашел числа пи. Еще он известен тем, что после того, как ему надоело работать с QEMU, написал эмулятор, на котором можно запустить линукс, на джаваскрипте.
Итак. Вот эта вот последовательность загрузок - она имеет свои некоторые недостатки. Главный недостаток состоит вот в чем, смотрите: вроде бы все хорошо, бут ром, бут клок, бут прог, загрузчик загружает ядро и все хорошо. Проблема в том, что операционная система, которую мы собираемся загрузить - эта операционная система, наверное, должна работать на нашем компьютере. То есть ядро должно распознать все устройства нашего компьютера. Получается неприятная ситуация, когда вот в такое ядро мы должны сразу загрузить все драйвера всех критически важных устройств. Если допустим у нас используется какая-то хитрая файловая система - ее драйвер должен быть в ядре.
То есть назревают две необходимости: вот этот вторичный загрузчик должен быть хитрой программой. Он должен уметь сказать: корневая файловая система лежит там-то там-то, должен уметь передавать параметры ядру. Что еще хуже - ядро должно быть generic, либо мы говорим так: помимо ядра есть еще модули - компоненты ядра - которые вместе с ядром работают в режиме супервизора, но они компонуются во время загрузки ядра. То есть вторичный загрузчик должен уже подгрузить ядро и модули файловой системы. И консоли на всякий случай. И после этого инициализировать запуск файловой системы. Мало того, я задам следущий провокационный вопрос: итак, мы создали такой загрузчик и он называется так: «великий универсальный загрузчик». И он настолько великий, что может грузить ядра разных операционных систем, каким-то образом может грузить модули разных операционных систем и компоновать ядра. Короче говоря, мы пишем свою операционную систему, маленькую такую. Ну, мы с вами занимаемся линуксом, в линуксе хотелось бы попроще. Давайте еще раз. Мы пишем свою операционную систему. Внимание, вопрос: зачем мы это делаем? У нас есть своя операционная система . Линукс она называется.
Как загрузить линукс с помощью линукса? Очень просто. Мы должны принять решение относительно того, какие у нас устройства есть в компьютере вообще всегда и именно оттуда загружать пресловутые драйверы. Более того, мы не только драйверы должны загрузить, а еще и всю последовательность действий - подгрузка модулей и так далее - еще до того как остальные устройства будут распознаны. И простейшая идея, которая пришла в голову почему-то именно линуксоидам, следующая: у нас есть одно аппаратное устройство хранения, которое есть всегда в любом компьютере: оперативная память в компьютере.
У нас уже есть бут ром и бут блок. Нам нужен такой полуторный загрузчик, и этот первичный загрузчик раньше загружал вторичный. Окей. Давайте загрузим ядро и еще один файл, а в этом файле будет файловая система, в которой лежат все нужные нам модули для того чтобы найти корневую файловую систему, чтобы ее смонтировать в программу.
Зачем нам писать свою новую операционную систему, если у нас есть старая, и называется она линукс? В этой файловой системе ничего нет кроме драйверов и программы, которая загружает в эти модули ... давайте посмотрим, что.
ls /boot/
Вот есть ядро, даже несколько ядер и файл - initrd - initial run disk - это файловая система.
Давайте посмотрим что это такое - это просто архив. Самый обычный, и вот его содержимое - маленький-маленький линукс.
В нем вообще всего 350 каких-то объектов, включая каталоги. В нем есть немножко библиотек для запуска бинарников, какая-то красотень, которая рисует красивые картинки, красивые картинки для красотени, раз программа, два программа, три, четыре, пять, шесть, еще инит должен быть, немножко сценариев на шелле, в основном для красотени опять же, немножко программ, которые внезапно могут понадобиться, чтобы это все смонтировать, запустить, показать, перестать показывать, вот это вот все, немножко правил и какое-то количество модулей ядра. Тут будут ядра файловой системы, консоли, видео немножко разного, ну и все. Настройки, правила загрузки модулей и /dev, в которой тоже немного девайсов. Короче говоря, очень-очень маленький линукс с некоторым количеством мусора, нужный только для того, чтобы подгрузить все модули, возможно, настроить что-то, что нужно для старта, начать грузить файловую систему, из нее загрузить настоящий линукс и все вот это убить.
Получается, мы немного модифицируем эту последовательность, превращаем 2 загрузчика в один.
Вот.
Я б тут еще рассказал про всякие схемки с, собственно, загрузкой - в современном мире что используется, GRUB, да? Grand unified bootloader - короче говоря, это и есть инструмент, который в современных линуксах используется для этой организации полу-первичной загрузки. В старом варианте у него были драйверы файловых систем, они и сейчас есть. Но его задача все-таки подгрузить ядро, стартовый виртуальный диск, пнуть их и забыть про это все навсегда. Современный GRUB настроен так, что его руками даже не настроишь - это не хьюман ридабл штука (а нет ,кстати, ничего). Написан он на чем-то, похожем на шелл… не совсем это шелл.. Тут язык груба - грубый язык. Да.
Так вот, значит, это генерат, который описывает нашему загрузчику, что откуда брать, какие параметры передавать и так далее, генерируется он, по-моему, вот такой штукой: как видите, он нашел несколько установленных в нашей операционной системе ядер, нашел, что еще отдельно вместо ядра можно загрузить memtest - проверку файлов, из всего этого сформировал здоровенный конфиг, этот конфиг прописывается в первичный загрузчик, дальше грузит ядро, показывает менюшку и так дальше и так дальше. Давайте посмотрим man grub-install. Тут команды, которые, собственно, устанавливают загрузчик.
Grub-setup расписывает бут сектор, grub-install просто копирует. Но, повторяю, вся эта механика раньше работала по условно классической схеме, когда GRUB сам подгружал ядро и так далее, а сейчас все сильно упростилось - тут в основном драйверы файловых систем и всяких картинок и так далее. Шрифты. И все прочее.
Так вот. Когда мы устанавливаем наш загрузчик, мы расписываем загрузочный сектор, место, куда будет сложен вторичный загрузчик, мы передаем ему знания о том, где лежит тот самый файл настройки, и на этом, собственно, наша настройка нашей операционной системы заканчивается, потому что тогда при загрузке загрузится первичный, вторичный загрузчик, конфиг.
В настройке вместе с ядром указано и какое монтировать устройство в качестве корневого и откуда брать стартовый виртуальный диск. После чего дергается ядро и там начинается. Немножко путанный генерат, но если внимательно посмотреть, то можно все понять.
Ну, наверное, стоит еще раз отметить, что под конец происходит такая операция, как монтирование настоящего корня и выкидывание всего.
Вопрос: получается, GRUB лежит по одному и тому же адресу в любой машине?
Ответ: он лежит в мастербуте, но это не обязательно, потому что может быть еще один первичный загрузчик, который загрузит другой первичный загрузчик. Например, есть виндоус, который опасается, что в его мастербут будет записано что-то кроме виндоусовского чего-то. Виндоусовский загрузчик может загрузить еще один первичный загрузчик. Который может лежать где угодно.
Биос загружает первый сектор жесткого диска. Либо первый сектор флоппи диска. Либо определенное количество байт из сети. Там лежит следующий адрес загрузки, где лежат адреса, где первые 512 байт - первичный загрузчик. 55 аа - конец блока - сигнатура, указывающая, что сектор загрузочный.
Разговаривали бы мы с вами лет 10 назад - я бы рассказал, что такое таблица разделов, но она больше не актуальна. Недостатки этой схемы: на самом деле оч большие. Главный недостаток: все более менее стандартизованные системы загрузки насквозь полны легаси. Например, загрузочный сектор, где 14 с гаком байтов можно использовать для первичного загрузчика, какие-то странные форматы устройств... ну, в общем, очень много чего-то, что давным-давно не актуально. Ну совсем. С другой стороны, получается же, что каждый производитель операционных систем вынужден скооперировавшись с каждым производителем устройства договариваться. Сколько разных способов загрузки по сети, даже если они называются одинаково - уму непостижимо.
Разумеется, ничего общего у того, что я сейчас рассказал, с архитектурой АРМ, конечно же нет.
Еще отдельная тема состоит в том, что производители железяк, а также производители операционных систем и всего остального очень боятся, что их железо/операционная система/что-то будет использовано не по назначению, не говоря уже о том, что есть всякие решения для военных - было бы очень интересно всю эту последовательность загрузки сделать доверенной. И вот этим занимается современная схема загрузки досистемной, которая, собственно, называется UEFI, и история такая: разработана она была фирмой Intel для новых архитектур, называющихся Itanium. Легаси там не работает.
Они решили: давайте в режиме супервизора будем запускать некий кусок, но сделаем его универсальным для всего - у него будет какой-то байткод и на нем можно писать драйвера - не зависимо от процессора драйвер написанный на байткоде будет ему подходить. Будем прямо в маленькой операционной системе запускать какие-то программки и ядро операционной системой будет этой программой.
В результате мы берем и с помощью EFI настраиваем аппаратное окружение, определяем, где диск, загружаем ядро. Когда загружается ядро - EFI где-то болтается, к нему можно обратиться… это мне напоминает такое старое-старое легаси, когда то самое слово BIOS, которое тут прозвучало - это же создание биоса, который в пзу преследовал те же самые цели.
Пользоваться биосом перестало быть удобно в тот момент, когда он перестал быть базовым для input-output системы. Мы просто переходим на следующий уровень. Давайте специфицируем некий интерфейс - extensible firmware interface - EFI и обязуем каждого вендора создавать такой интерфейс и наша задача будет только в том, чтобы заставить производителей железяк писать драйверы для EFI.
Кроме того, если у нас четко поддерживается последовательность загрузки из пзу - дальше это EFI, которая прямо из под себя запускает операционную систему, можем пронизать все это (электронной) подписью. Таким образом не можем прервать — эта штука называется Secure Boot, ее все очень не любят, потому что она очень удобная для всех производителей и неудобная для всех пользователей. Когда мы хотим поставить линукс - мы отключаем эту подпись, цепочка подписей позволяет вплоть до старта операционной системы снять ответственность с производителей железа: загрузилась операционная система - все, дальше сами отвечайте.
Особенно не любят Secure Boot потому, что получение этого сертификата только в одном месте возможно и, совершенно случайно, это место не производитель железа, а производитель операционной системы виндоус, что приводит на размышления и вносит сложность в получение этого сертификата. И получается проблема, когда производитель одной операционной системы может себе эти ключи выдавать сколько хочет, а производитель другой должен пройти огромный путь, а те организации которым это нужно - они точно не пойдут к микрософту за ключом, никогда совсем. Даже в Америке. И получается «за что боролись».
Не говоря уже о том, что вся эта новая модная операционная система под называнием EFI - такое новое легаси. Уже сейчас мы страдаем от того, что нужно на жестком диске делать раздел, форматировать его в систему FAT32 и загружать туда уже весь софт.
А если я хочу загружаться с чего-то волшебного? Я предвижу, что и дальше это будет развиваться примерно так, а окажется, что некоторые устройства .. Короче говоря, мы старый легаси поменяли на новый. Который, следует признаться, работает нормально... Дело в том, что биос раньше был тоже довольно передовой системой. Идея заставить вендора реализовывать протокол, а самому ничего не делать, приводит к тому, что все эти реализации протокола разные. Сколько вы видели компьютеров - столько и EFI. Вместо того, чтобы писать самому - купи интеллектуальную собственность у того, кто уже написал - именно потому это удобная штука с точки зрения бизнеса. И, наконец, вся эта процедура оказалась довольно сложной. Какой-то раздел, какие-то программы...
Мы запускали линукс из QEMU, где все эти штуки запускаются последовательно. Главный недостаток, вокруг которого был весь сыр бор - было бы неплохо запускать все эти штуки параллельно. Ядер много, диск быстрый. Ну, и это шелл скрипт - а это долго.
Второй недостаток, связанный с этой старой схемой: не очень понятно, а чего делать, когда мы в процессе работы хотим запустить. Я беру и старт-стопный скрипт перезапускаю от рута. Означает ли это, что все стартовые сценарии запускаются от рута? Допустим, да. Мы усложняем процедуру запуска просто потому, что она не гибкая.
Туда же относится … еще одна проблема связана с демонизацией. Допустим, захотели запустить процесс в виде демона. Для этого нужно отвязать его от ввода-вывода (потому что можем прислать случайно сигнал), отвязать от процесс группы, и только тогда получится демон, который никак не связывается с нашей операционной системой… еще одна проблема связана с запуском сетевых сервисов. Допустим, хотим получить сетевой сервис. Тогда мы должны писать всю сетевую технологию заново.
Внимание, вопрос: ну они же все так работают, зачем нам все это дублировать? Может, можно написать одну программу, которая умеет все это делать, а она будет запускать все эти службы?
Еще одна проблема: как будем останавливать запущенный сервис? Ну, наверное, он запускается от имени процесса, у него есть PID файл, ему надо сказать килл. Все хорошо, а если он уже умер, а PID файл есть? Есть несколько тонких моментов, которые не совсем понятно как унифицировать. С другой стороны, что мы можем захотеть, если мы вообще забудем про это наше изобретение - давайте на шелле запустим запуск сценариев? Что бы мы хотели при работе операционной системы от самой операционной системы, как ее администратор?
Ну как обычно: запуск сервисов, их остановка, однократный запуск типа проверки диска, журналирование - кстати, еще одна проблема. Как устроено журналирование в обычном виде? Открываем файл и сами пишем, либо используем специальный сетевой сервис журналирование - для этого нужно написать кусок кода в сервис, который пишет в этот сокет.
Было бы неплохо, если бы платформа для запуска сервисов сама занималась такими вещами. Ну, допустим, мы хотим писать журнал сами. Не хотим - пишем на стандартный вывод ошибок, а за нас наша система складывает в журнал.
Работа с внешними устройствами. Мы совсем забыли о том, что есть набор внешних устройств, которые:
- нужно привести в готовность
- реагировать на появляющиеся и исчезающие устройства - создавать записи в каталоге /dev
- монтировать остальные файловые системы.
На самом деле мы не можем распилить систему на части и сказать: монтирование файловых систем - отдельный шелл скрипт. Потому что это все должно быть одновременно. Что еще? Ну, вообще, всякие внешние железяки, консоль, например, как-то ее нужно настроить, ну и так далее.
Естественно, запуск и остановка запросов со стороны администратора. То, про что я говорил с которым есть проблемы в случае, когда запускают сценарий рут, а потом кто-то будет менять права, если мы запускаем по сетевой активности - у нас присоединился клиент к какому-то порту, значит нам нужно запустить сервер. Этот запуск нужно предусмотреть - сервер по умолчанию не предусмотрен, он никому не нужен.
А если, пока мы обслуживаем один запрос, пришел второй? Что делать? Это тоже нужно управлять.
Средство, которое должно управлять профилем операционной системы, должно уметь делать такие штуки. Это называется сокет-активация. Да, конечно, тут мы вспомнили, что вся эта буча затеялась, чтобы у нас были зависимости по службам и возможность параллельно их запускать. Да, конечно, как можно меньше шелла: если можно запустить бинарник - лучше сделать так, потому что тогда у нас будет 1 контекст запуска бинарника. А не десятки и сотни.
А что касается зависимости от дерева запуска при запуске - было бы неплохо, конечно, это дерево распилить на такие шелоны, чтобы не пытаться договориться всем со всеми. Это невозможно, у нас очень много производителей и они никогда не договорятся. Было бы неплохо сделать такие чекпоинты: запущено ядро, запущен инит, запущены базовые сервисы, … просто определить и сказать: мой сервис запускается после того, как запущена сеть. Когда конкретно - в общем-то все равно. Вот такого рода не только зависимости, но и такие уровни, что ли.
Было бы неплохо также ввести какие-то средства отладки, когда вручную говорим это запускать, а это не запускать.
Ну и такой отдельный бонус состоит в том, что этим инструментам можно разрешить пользоваться и пользователям. Если мы вводим какую-то отдельную системную службу которая будет это все обрабатывать с правами обычного пользователя, почему бы пользователю этим не воспользоваться?
Вот такой массив запросов можно предъявить к современной системе на базе GNU/Linux. Давайте запомним этот список и в следующий раз я потрачу 20 минут на то, чтобы рассказать, как он устроен в операционной системе которая называется systemd, и если хватит времени - немножко поговорим про десктопные стандарты. И тогда на этом у нас закончится разговор об операционной системе, как о живом организме, и останется время поговорить про пакеты, репозитории, и на этом, собственно, семестр у нас закончится.