Как написать ядро линукс
Перейти к содержимому

Как написать ядро линукс

  • автор:

Записки программиста

Если помните, не так давно мы научились собирать из исходников ядро FreeBSD. Спрашивается, почему бы не научиться делать то же самое и с ядром Linux? Причины собирать ядро Linux из исходников, в общем то, те же — получение самой свежей версии ядра, срочное применение security-патчей, оптимизация под конкретные задачи и конкретное железо, а также желание принятие участие в разработке ядра.

Важно! Следование инструкциям из этого поста может привести к потере ваших данных. Делайте бэкапы и помните, что делаете вы все исключительно на свой страх и риск. Все описанное ниже было проверено на Ubuntu 14.04 LTS. Но на других версиях Ubuntu, а также других дистрибутивах Linux, отличия должны быть минимальными.

Настраиваем загрузчик

Правим /etc/default/grub примерно таким образом:

После правки говорим:

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

Ставим зависимости

Нам понадобятся как минимум следующие пакеты:

На многих системах все они, впрочем, уже будут присутствовать.

Получаем исходники

Исходники ядра можно скачать по ссылке на главной странице kernel.org. На момент написания этих строк последней стабильной версией ядра была 4.6.4:

Или, если вам нужен самый-самый свежак, можно взять исходники прямо из Git:

Судя по тому, что тэга v4.6.4 в Git’е мне найти не удалось, релизы ядра Linux оформляются исключительно в форме сжатых tar-архивов.

Если же вместо ванильного ядра вы хотели бы собрать ядро с патчами от компании Canonical:

По своему опыту скажу, что если вы пользуетесь Ubuntu, то можете смело использовать ванильное ядро. Вряд ли у вас возникнут с ним какие-то проблемы.

Примечание: Интересно, что из существующих сравнительно популярных дистрибутивов Linux ядро без собственных патчей, похоже, используют только Gentoo, Slackware и Arch Linux.

Так или иначе, теперь у вас есть исходники.

Собираем и устанавливаем ядро

Выбираем опции, с которыми будет собрано ядро:

В случае необходимости меняем настройки, жмем Save, затем Exit. В результате будет создан файл .config , содержащий выбранные нами параметры.

При обновлении ядра (вы же по-любому уже используете какое-то ядро?) удобно взять конфиг текущего ядра, и выставить новым опциям значения по умолчанию:

Собирается ядро довольно долго. На моем ноутбуке сборка заняла 1 час 15 минут. Однако из этого времени большая часть тратится на сборку гигантского пакета ядра с отладочными символами. Сборку этого пакета можно отключить, закомментировав в конфиге параметр CONFIG_DEBUG_INFO. Только учтите, что этот пакет требуется SystemTap и другим полезным инструментам.

Помимо самого ядра также можно собрать и документацию:

По окончании сборки в дочернем каталоге видим что-то вроде:

Ставим все deb-пакеты кроме dbg-версии ядра обычным sudo dpkg -i и перезагружаемся. После перезагрузки смотрим на вывод команды uname -a . Убеждаемся, что действительно загрузились с новым ядром. Если с новым ядром что-то не так, в загрузчике просто выбираем то, с которым система загружалась до этого. После загрузки со старым ядром поскорее удаляем пакеты нового ядра, и вуаля — система вернулась к прежнему состоянию.

Дополнительно после загрузки нового ядра можно прогнать на нем тесты:

Поздравляю, теперь вы умеете собирать ядро Linux!

Заключение

Материалы по внутреннему устройству ядра Linux:

    , сайт для новичков в ядре Linux со своим списком рассылки, IRC-каналом, форумом, вики, и так далее; , сайт для комфортного чтения мейлинг листа разработчиков ядра Linux через браузер. Будьте внимательны, он нередко портит форматирование писем! В этом смысле архив рассылки на marc.info намного лучше; , сайт для комфортного чтения кода ядра Linux через браузер;
  • Книга Linux Kernel Development, 3rd Edition является наиболее свежей книгой по ядру Linux на английском языке (2010 год, ядро 2.6.x), есть перевод на русский язык;
  • Книга Linux-Treiber entwickeln, 4th Edition более свежая (2015 год, ядро 4.x), но написана на немецком и не имеет перевода;
  • Бесплатная книга Linux Insides за авторством Alexander Kuleshov a.k.a @0xAX выглядит наиболее доступной и актуальной, но на данный момент еще находится в процессе написания;
  • Иногда что-то полезное удается найти в Linux Kernel HTML Documentation, официальной документации из исходников ядра Linux; , которые пытаются поддерживать в актуальном состоянии, плюс исходники к 4-му изданию, которое на данный момент еще пишется;

А собираете ли вы ядро из исходников, и если да, то зачем? 🙂

Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.

HOW-TO: Сборка ядра Linux

В данном пошаговом руководстве вы узнаете, как правильно собрать и установить ядро ветвей >2.6 в семействе ОС Ubuntu.

Шаг 1. Получение исходного кода ядра

Исходники ядра Ubuntu можно получить двумя способами:

150 Мб в текущую папку. Чтобы получить исходники ядра, версия которого установлена на компьютере выполните команду:

Или вместо `uname -r` можно указать конкретную версию из имеющихся в репозитории.

Не забудьте включить общий доступ к исходникам в репозитории ( Параметры системы → Программы и обновления → Программное обеспечение Ubuntu → Исходный код ). Из консоли это сделать можно раскомментировав строки начинающиеся с deb-src в файле /etc/apt/sources.list, а затем выполнить обновление командой: «sudo apt-get update».

Где <release codename> — имя релиза, например:

Другие ядра

Также существуют ядра, работоспособность которых в Ubuntu не гарантируется. Например, известна проблема с рядом популярных системных приложений (в частности драйвера NVidia, VirtualBox), которые при своей установке компилируются под установленное ядро. Поэтому для их установки на ядро, нестандартное для данной версии Ubuntu (например, Ubuntu 16.04 идёт с ядром 4.4.0), может потребоваться их отдельная компиляция вручную или специальные патчи, а последние версии ядер с kernel.org приложение может вообще не поддерживать.

Распакуйте полученный архив, используя команды:

Или в случае с linux-source:

Шаг 2. Получение необходимых для сборки пакетов

Выполните следующие команды для установки основных пакетов:

Далее всё зависит от того, каким способом вы хотите произвести конфигурацию ядра. Это можно сделать несколькими способами.

В случае, если вы хотите использовать config, oldconfig, defconfig, localmodconfig или localyesconfig, вам больше не нужны никакие дополнительные пакеты. В случае же с оставшимися тремя вариантами необходимо установить также дополнительные пакеты.

Для установки пакетов, необходимых для использования menuconfig выполните следующую команду:

Для установки пакетов, необходимых для использования gconfig выполните следующую команду:

Для установки пакетов, необходимых для использования xconfig выполните следующую команду:

Шаг 3. Применение патчей

Официальные патчи уже наложены на исходники, если ядро получалось описанной выше командой:

Если вы никогда до этого не применяли патчей к исходному коду, то выполните следующую команду:

Эта команда установит программу patch, необходимую для, как можно догадаться, применения патчей. Теперь скачайте файл патча в папку, куда вы распаковали ядро. Это может быть либо архивный файл (напр. Bzip2 или Gzip), либо несжатый patch-файл.

На данный момент подразумевается, что вы уже сохранили файл в ту папку, куда ранее распаковали ядро, и установили программу patch.
Если скачанный вами файл был в формате Gzip (*.gz), тогда выполните следующую команду для распаковки содержимого архива:

Если скачанный вами файл был в формате Bzip2 (*.bz2), тогда выполните следующую команду для распаковки содержимого архива:

где 2.6.x.y — версия патча ядра. Соответствующие команды распакуют файл патча в папку с исходным кодом ядра. Прежде чем применить патч, необходимо удостовериться, что он заработает без ошибкок. Для этого выполните команду:

где 2.6.x.y — версия патча ядра. Эта команда сымитирует применение патча, не изменяя сами файлы.

Если при её выполнении не возникнет ошибок, то изменения можно смело внедрять в сами файлы. Для этого выполните команду:

где 2.6.x.y — версия патча ядра. Если не было никаких ошибок, значит к исходному коду был успешно применён патч.

где 2.6.x.y — версия патча и ваших исходников

Шаг 4. Конфигурация будущей сборки ядра

Перейдите в папку, куда вы распаковали ядро, выполнив команду

где 2.6.x.y — версия загруженного вами ядра.

На данный момент вы уже должны были определиться с методом конфигурации ядра (если нет, то ознакомьтесь с ними в разделе «Получение необходимых для сборки пакетов». В зависимости от этого, выполните следующую команду для запуска выбранного вами способа конфигурации:

После вызова соответствующая программа конфигурации будет запущена. Произведите необходимые настройки в соответствии с вашими потребностями, сохраните файл конфигурации и переходите к следующему шагу.

Шаг 5. Сборка ядра

Итак, приготовления завершены. Теперь можно запустить процесс сборки ядра. Чтобы это сделать, выполните команду:

Шаг 6. Установка образов и заголовков ядра

Когда сборка ядра подошла к концу, в вашей домашней папке появятся два deb-пакета. Их и необходимо установить. Для этого выполните команды:

где 2.6.x.y — версия собранного ядра, arc — архитектура процессора (i386 — 32-бит, amd64 — 64-бит).
Если вы не знаете точного названия пакета, выведите список файлов в домашнем каталоге командой

и найдите эти самые два пакета.

Шаг 7. Генерация начального RAM-диска

Для корректной работы Ubuntu требует наличия образа начального RAM-диска. Чтобы его создать, выполните команду:

где 2.6.x.y — версия собранного ядра.

Шаг 8. Обновление конфигурации загрузчика GRUB

Для того, чтобы новая версия ядра была доступна для выбора при загрузке компьютера, выполните следующую команду:

Файл menu.lst (для GRUB версии 1) или grub.cfg (для GRUB версии 2) обновится в соответствии с наличием установленных операционных систем и образов ядер.

Шаг 9. Проверка ядра

Сборка и установка ядра успешно выполнены! Теперь перезагрузите компьютер и попробуйте загрузить систему с новым ядром. Чтобы удостовериться, что система запущена с новым ядром, выполните команду

Она выведет на экран используемую версию ядра.

Если всё сделано правильно, то вы можете удалить архивы с исходным кодом и весь каталог linux-2.6.x.y в вашей домашней папке. Это освободит около 5 ГБ на вашем жёстком диске (размер освобождаемого пространства зависит от параметров сборки).

На этом процесс сборки и установки завершён, поздравляю!

ядро Linux. Описание процесса разработки

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

Где взять исходный код ядра

Исходный программный код последней версии ядра Linux всегда доступен как в виде полного архива в формате .tar (т.е. в виде архивного файла, созданного утилитой tar ), так и в виде инкрементальной заплаты по адресу

Если нет необходимости по той или иной причине работать со старыми версиями ядра, то всегда нужно использовать самую последнюю версию. Хранилище kernel.org — это то место, где можно найти как само ядро, так и заплаты к нему от ведущих разработчиков.

Несколько лет назад ведущие разработчики ядра Linux под руководством Линуса Торвальдса начали использовать новую систему контроля версий для управления исходным кодом ядра. При создании этой системы, которую Линус назвал Git , во главу угла ставилась скорость работы. В отличие от традиционных систем контроля версий, таких как CVS , Git является распределенной системой. По этой причине рабочий процесс на ее основе и алгоритм использования оказались совершенно незнакомы большинству разработчиков. Тем не менее я настоятельно рекомендую использовать Git для загрузки исходного кода ядра Linux и управления им.

Чтобы получить с помощью Git копию последней зафиксированной версии дерева ядра Linux, введите приведенную ниже команду.

$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git

После загрузки исходного кода нужно обновить дерево до состояния последнего обновления, которое сделал Линус. Для этого введите следующее:

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

Инсталляция исходного кода ядра

Архив .tar исходного кода ядра распространяется в сжатых форматах GNU zip (gzip) и bzip2. Формат bzip2 предпочтительнее, поскольку он обеспечивает больший коэффициент сжатия по сравнению с форматом gzip . Архив ядра в формате bzip2 имеет имя linux- x . y . z .tar.bz2 , где x . y . z — номер соответствующей версии исходного кода ядра. После загрузки файла его нужно распаковать с помощью простой команды. Если .tar -архив сжат с помощью утилиты bzip2 , введите такую команду:

$ tar xvjf linux- x . y . z .tar.bz2

Если сжатие выполнено с помощью gzip , то команда должна иметь следующий вид:

$ tar xvzf linux- x . y . z .tar.gz

Обе эти команды позволяют разархивировать и развернуть дерево исходных кодов ядра в каталог linux- x . y . z . При использовании Git для получения исходного кода ядра и управления им вам не нужно загружать архив в формате .tar . Просто введите команду git clone , как было описано в предыдущем разделе, и она сама загрузит и распакует самую последнюю версию дерева исходного кода ядра.

Где лучше всего инсталлировать и изменять исходный код

Исходный код ядра обычно инсталлируется в каталог /usr/src/linux . Заметим, что это дерево исходного кода нельзя использовать для собственных разработок. Версия ядра,

с которой была скомпилирована ваша библиотека C , обычно привязана к этому дереву ката! логов. Кроме того, чтобы вносить изменения в ядро, не обязательно иметь права пользовате! ля root , вместо этого лучше работать в собственном рабочем каталоге и использовать права пользователя root только для инсталляции ядра. Даже при инсталляции нового ядра каталог /usr/src/linux должен оставаться без изменений.

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

Начальные сведения о ядре Linux 39

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

$ patch –p1 < ../patch- x . y . z

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

Дерево каталогов исходных кодов ядра

Дерево исходных кодов ядра содержит ряд каталогов, большинство из которых также содержит подкаталоги. Каталоги, которые находятся в корне дерева исходных кодов, и их описание приведены в табл. 2.1.

Таблица 2.1. Дерево исходных кодов ядра

Исходный код, специфичный для аппаратной платформы

Слой блочных операций ввода-вывода

Документация исходного кода ядра

Микропрограммы устройств, необходимые для использования некоторых драйверов

Подсистема VFS и отдельные файловые системы

Заголовочные файлы ядра

Коды для загрузки и инициализации ядра

Код межпроцессного взаимодействия

Основные подсистемы ядра, такие как планировщик

Подсистема управления памятью и поддержка виртуальной памяти

Примеры и демонстрационный код

Сценарии компиляции и построения ядра

Модуль безопасности Linux

Вспомогательные утилиты для разработки Linux

Код инициализации пространства пользователя ( initramfs )

Ряд файлов, находящихся в корне дерева исходных кодов, также заслуживают отдельного внимания. Файл COPYING — это лицензия ядра (GNU GPL v2). Файл CREDITS — это список разработчиков, которые внесли большой вклад в разработку ядра. Файл MAINTAINERS — список людей, которые занимаются поддержкой подсистем и драйверов ядра. И наконец, Makefile — это основной файл построения ядра.

Сборка ядра достаточно проста. Это может показаться удивительным, но она даже более проста, чем компиляция и инсталляция других системных компонентов, таких, как, например, библиотеки glibc . В ядрах серии 2.6 встроена новая система конфигурации и построения, которая позволяет сделать эту задачу еще проще и является долгожданным улучшением по сравнению с ранними выпусками ядер.

Поскольку исходный код ядра Linux доступен, то это означает, что есть возможность сконфигурировать ядро перед компиляцией. В результате у вас появляется возможность скомпилировать в ядро поддержку только необходимых драйверов и функций. Конфигурирование ядра — необходимый этап перед его компиляцией. Поскольку в ядре существует бесчисленное количество функций и вариантов поддерживаемого аппаратного обеспечения, возможностей по конфигурации, мягко говоря, много . Процесс конфигурации выполняется с помощью указания ряда параметров конфигурации в виде CONFIG_ ВОЗМОЖНОСТЬ . Например, поддержка симметричной многопроцессорной обработки (Symmetric multiprocessing, SMP) устанавливается с помощью параметра CONFIG_SMP . Если этот параметр установлен, то поддержка функций SMP включена. Если не установлен, то функции поддержки SMP отключены. Параметры конфигурации используются как для определения того, какие файлы должны быть скомпилированы во время сборки ядра, так и для управления процессом компиляции через директивы препроцессора.

Параметры конфигурации, которые управляют процессом сборки ядра, бывают двух типов: логические ( boolean ) и с тремя состояниями ( tristate ). Логические параметры могут принимать значение yes или no . Как правило, параметры конфигурации ядра наподобие CONFIG_PREEMPT являются логическими. Параметры конфигурации с тремя состояниями могут принимать значение yes , no или module . Значение module означает, что данный параметр конфигурации установлен, но соответствующий ему код должен быть скомпилирован в виде модуля (т.е. как отдельный объект, который загружается динамически). В случае параметров конфигурации с тремя состояниями значение yes явно указывает на то, что соответствующий код должен быть скомпилирован в основной файл образа ядра, а не в виде модуля. Драйверы устройств обычно определяются параметрами конфигурации с тремя состояниями.

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

Ядра, которые включаются в поставки ОС Linux такими производителями, как Canonical (для Ubuntu) или Red Hat (для Fedora), компилируются как часть дистрибутива. В таких ядрах обычно имеется большой набор различных функций и практически полный набор всех драйверов устройств в виде загружаемых модулей. Это позволяет получить хорошее базовое ядро и поддержку широкого спектра оборудования в виде дополнительных модулей. Однако в любом случае разработчикам ядра нужно компилировать собственные ядра и самим решать, какие модули включать, а какие нет.

К счастью, в ядре поддерживается несколько средств, с помощью которых можно выполнитьконфигурацию. Самоепростоесредство — этотекстоваяутилитакоманднойстроки:

Начальные сведения о ядре Linux 41

Эта утилита просматривает все параметры один за другим и интерактивно запрашивает у пользователя, какое значение соответствующего параметра установить — yes , no или module (для переменной с тремя состояниями). Эта операция требует длительного времени, и если у вас не почасовая оплата, то лучше использовать графическую утилиту на основе интерфейса ncurses :

Имеется также и более удобная графическая утилита, использующая библиотеку gtk+ :

В этих трех утилитах все параметры конфигурации делятся на категории наподобие Processor Type and Features (Типы и свойства процессора). При этом пользователь имеет возможность перемещаться по категориям, просматривать параметры конфигурации ядра и, разумеется, изменять их значения.

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

И хотя значения этих параметров установлены достаточно произвольно (ходят слухи, что для аппаратной платформы i386 используется конфигурация Линуса), они являются хорошей отправной точкой, если ранее вам никогда не приходилось заниматься конфигурацией ядра. Чтобы ускорить процесс конфигурации, запустите эту команду, а потом проверьте, включена ли поддержка всех нужных вам аппаратных средств.

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

Эту команду вам также нужно запустить перед сборкой ядра.

Параметр конфигурации CONFIG_IKCONFIG_PROC позволяет сохранить все текущие параметры конфигурации ядра в виде сжатого файла config.gz и поместить его в каталог /proc . Это позволит вам легко создать клон текущей конфигурации при сборке нового ядра. Если этот параметр конфигурации установлен в вашем текущем ядре, то просто скопируйте файл config.gz из каталога /proc и воспользуйтесь им для создания нового ядра, как показано ниже.

$ zcat /proc/config.gz > .config $ make oldconfig

После того как будет сконфигурировано ядро (как бы там ни было, вы должны были сделать это!), можно выполнить сборку с помощью команды

В отличие от предыдущих серий ядер, в серии 2.6 больше нет необходимости перед сборкой ядра выполнять команду make dep , так как дерево зависимостей создается автоматически. Также больше не нужно указывать конкретный тип сборки, например bzImage ,

или отдельно собирать модули, как делалось в более ранних версиях. Стандартное правило, записанное в файле Makefile , в состоянии обработать все!

Уменьшение количества выводимых сообщений

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

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

В результате весь огромный объем бессмысленных сообщений будет перенаправлен

в “большую сточную канаву” /dev/null .

Порождение нескольких параллельных задач сборки

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

По умолчанию утилита make запускает только одну задачу, так как часто файлы сборки пользователей содержат некорректную информацию о зависимостях. При этом несколько заданий могут начать “наступать друг другу на пятки”, что приведет к ошибкам в процессе построения. Разумеется, в файле сборки ядра Makefile таких ошибок нет, поэтому порождение нескольких задач не приведет к ошибкам построения. Для построения ядра с использованием нескольких параллельных задач сборки выполните команду

где число n определяет количество порождаемых заданий. Как правило, на один процессор запускается одно или два задания. Например, на 16-процессорной машине можно воспользоваться приведенной ниже командой.

$ make -j32 > /dev/null

Используя такие великолепные утилиты, как distcc и ccache , можно еще больше ускорить время сборки ядра.

Инсталляция нового ядра

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

Начальные сведения о ядре Linux 43

образ ядра и как сделать его загружаемым, обратитесь к руководству по используемому системному загрузчику. При этом всегда сохраняйте копии одного-двух старых ядер, которые гарантированно работоспособны! Это на случай, если новое ядро не будет нормально работать или вовсе откажется загружаться.

Например, для платформы x86 при использовании системного загрузчика grub скопируйте загружаемый образ ядра из файла arch/i386/boot/bzImage в каталог /boot и переименуйте его в, скажем, vmlinuz- версия . После этого отредактируйте файл /boot/grub/grub.conf , добавив в него новую запись, которая соответствует новому ядру. В системах, где используется загрузчик LILO, необходимо соответственно отредактировать файл /etc/lilo.conf и запустить утилиту lilo .

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

В результате все скомпилированные модули будут инсталлированы в их корректные домашние каталоги, находящиеся в иерархии каталога /lib/modules .

В процессе построения ядра в корневом каталоге дерева исходного кода также создается файл System.map , в котором содержится таблица соответствия символов ядра их

начальным адресам в памяти. Эта таблица используется при отладке для перевода адресов памяти в имена функций и переменных.

Отличия от обычных приложений

Ядро имеет некоторые уникальные отличительные особенности по сравнению с обычными пользовательскими приложениями. И хотя эти отличия не всегда приводят к усложнениям при разработке кода ядра по сравнению с пользовательскими приложениями, все же они делают сам процесс разработки немного другим .

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

Ядро не имеет доступа к библиотеке функций и к стандартным заголовкам языка C.

Код ядра написан с использованием компилятора GNU С.

В ядре нет такой защиты памяти, как в режиме пользователя.

В ядре нельзя также просто использовать вычисления с плавающей точкой, как в пользовательских приложениях.

Ядро использует стек небольшого фиксированного размера для каждого процесса.

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

Переносимость кода ядра очень важна.

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

Отсутствие библиотеки libc и стандартных заголовков

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

Однако этот факт не должен приводить вас в замешательство, поскольку многие из функций библиотеки языка С реализованы непосредственно в ядре. Например, обычные функции работы со строками находятся в файле lib/string.c . Чтобы воспользоваться ими, нужно лишь включить в исходный код заголовочный файл <linux/string.h >.

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

В файлы исходного кода ядра нельзя включать заголовочные файлы, расположенные вне этого дерева каталогов, а также использовать внешние библиотеки.

Основные файлы находятся в подкаталоге include/ , расположенном в корне дерева исход! ного кода ядра. Например, заголовочный файл <linux/inotify.h> расположен в подката! логе include/linux/inotify.h дерева исходных кодов.

Набор заголовочных файлов, зависимых от аппаратной платформы, расположен в подката! логе arch/< платформа >/include/asm дерева исходных кодов ядра. Например, при

сборке ядра для платформы x86 все аппаратно!зависимые заголовочные файлы находятся в подкаталоге arch/x86/include/asm . Для включения этих файлов в исходный код используется префикс asm/ , например <asm/ioctl.h> .

Что касается отсутствующих функций, то самая известная из них — printf() . Ядро не имеет доступа к функции printf() , однако в нем реализована функция printk() , которая работает ничем не хуже своего аналога. Функция printk() копирует отформатированную строку в буфер системных сообщений ядра (kernel log buffer), который обычно считывается с помощью программы syslog . Использование этой функции аналогично использованию printf() :

printk(«Привет, всем! Строка ‘%s’ и целое число ‘%d’\n», str, i);

Одно важное отличие между функциями printf() и printk() состоит в том, что в функции printk() можно использовать флаг важности вывода (priority flag). Этот флаг используется программой syslog для того, чтобы определить, где именно нужно отображать сообщения ядра. Ниже приведен пример использования этого флага.

printk(KERN_ERR «this is an error!\n»);

Обратите внимание на то, что между KERN_ERR и выводимым сообщением отсутствует запятая. Это сделано преднамеренно. Флаг важности вывода определен с помощью директивы препроцессора как строковый литерал, который во время компиляции автоматически добавляется к выводимому сообщению. Функция printk() будет использоваться на протяжении всей книги.

Начальные сведения о ядре Linux 45

Компилятор GNU C

Как и все “уважающие себя” ядра Unix, ядро Linux написано на языке С. Может быть, это покажется странным, но ядро Linux написано не на чистом языке С стандарта ANSI С. Там, где это только возможно, разработчики ядра используют различные расширения языка, реализованные в компиляторе gcc (GNU Compiler Collection — это набор компиляторов GNU, в котором содержится компилятор С, используемый для компиляции ядра, а также всего остального программного обеспечения, написанного на языке C для системы Linux).

Разработчики ядра используют как расширения языка ISO C99 1 , так и расширения GNU C. Все это привело к тому, что ядро Linux было привязано к компилятору gcc . Несмотря на это, для компиляции ядра Linux можно также использовать любой другой современный компилятор, например Intel C, в котором достаточно хорошо реализована поддержка функций компилятора gcc . Самой старой поддерживаемой версией компилятора gcc является 3.2, однако настоятельно рекомендуется использовать версию 4.4 или более позднюю. В исходном коде ядра Linux не используются какие-либо особенные расширения стандарта C99. Кроме того, поскольку стандарт С99 является официальной редакцией языка С, эти расширения постепенно начинают использоваться и в других частях кода. Более интересные и, возможно, менее знакомые отклонения от стандарта языка ANSI C связаны с расширениями GNU С. Рассмотрим некоторые наиболее интересные расширения, которые могут встретиться в исходном коде ядра. Именно они отличают исходный код ядра от исходного кода других программных проектов, в которых, возможно, вам приходилось участвовать.

В стандарте С99 определены так называемые встраиваемые функции (inline functions), которые поддерживает компилятор GNU C. Как следует из названия, исполняемый код функции автоматически помещается компилятором во все места программы, где указан вызов этой функции. Это позволяет избежать дополнительных потерь времени процессора, связанных с вызовом функции и возвратом из нее (сохранение и восстановление регистров), а также позволяет потенциально повысить уровень оптимизации программы, поскольку компилятор теперь может одновременно оптимизировать коды и вызывающей, и вызываемой функции. Обратной стороной такой подстановки (ничто в этой жизни не дается даром!) является увеличение объема кода, увеличение используемой памяти и уменьшение эффективности использования процессорной кеш-памяти команд. Поэтому разработчики ядра используют подстановку для небольших функций, критичных ко времени выполнения. Настоятельно не рекомендуется использовать эту же возможность для больших функций, особенно когда они вызываются больше одного раза или не слишком критичны ко времени выполнения.

Встраиваемые функции объявляются с помощью ключевых слов static и inline , которые указываются при определении функции, как показано ниже.

static inline void wolf(unsigned long tail_size);

1 Стандарт ISO C99 — это последняя основная версия стандарта ISO C. По сравнению с предыдущей редакцией стандарта ISO C90 стандарт С99 был существенно расширен и дополнен. Так,

в нем появились выделенные инициализаторы ( designated initializers ), позволяющие выполнять поименную инициализацию отдельных полей структур, массивы переменной длины, комментарии

в стиле языка C++, типы long long и complex . Тем не менее при написании ядра Linux исполь-

зуется только ограниченный набор возможностей стандарта C99.

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

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

Компилятор gcc позволяет встраивать инструкции языка ассемблера в обычные функции языка С. Эта возможность, конечно, должна использоваться только в тех частях ядра, которые уникальны для определенной аппаратной платформы.

Для встраивания ассемблерного кода используется директива компилятора asm() . Например, приведенная ниже встроенная директива ассемблера выполняет команду rdtsc процессора x86, которая возвращает значение счетчика времени (TSC — Time Stamp Counter) работы процессора, прошедшего с момента последнего сброса.

unsigned int low, high;

asm volatile(«rdtsc» : «=a» (low), «=d» (high));

/* Переменные low и high теперь содержат значение старших и младших 32-разрядов 64-разрядного регистра tsc */

Ядро Linux написано на смеси языков ассемблера и С. Язык ассемблера используется в низкоуровневых подсистемах и на участках кода, где нужна большая скорость выполнения. Б о льшая часть кода ядра написана на языке программирования С.

Компилятор gсс имеет встроенные директивы, позволяющие оптимизировать различные ветви условных операторов, выполнение которых наиболее или наименее вероятно. Компилятор использует эти директивы для оптимизации кода соответствующих веток. В ядре эти директивы заключены в макросы likely() и unlikely() , которые легко использовать.

Например, рассмотрим приведенный ниже оператор if .

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

/* Мы предполагаем, что переменная ‘error’ практически всегда будет равна нулю. */

И наоборот, чтобы отметить приведенную ниже ветку как наиболее вероятную, нужно записать так:

/* Мы предполагаем, что переменная ‘success’ практически всегда не будет равна нулю. */

Как написать ядро линукс

Введение в написание модулей ядра Linux.
Valerie Henson
14 февраля 2008 12:33

Версия для печати (откроется в новом окне)

С давних пор, первым шагом в изучении нового языка программирования, является написание программы, которая печатает «Hello, world!». В этой статье мы используем такой подход при изучении написания простых модулей ядра Linux и драйверов устройств. Мы изучим, как напечатать «Hello, world!» из модуля ядра тремя различными способами: printk(), из /proc файла и из устройства в /dev.

Приготовления.

Модуль ядра — это часть кода ядра, которая может быть динамически загружена и выгружена во время работы ядра. Так как он запускается как часть ядра и ему необходимо близко взаимодействовать с ядром, модуль ядра не может быть скомпилирован в вакууме. Ему нужны, по крайней мере, заголовочные файлы и настройки для ядра, в которое он будет загружен. Компилирование модуля также требует набор средств разработки, такие как компилятор. Для простоты, мы сжато опишем как установить требуемые вещи для создания модуля ядра для Debian, Fedora и «ванильного» ядра Linux из тарболла. Во всех случаях, вы должны скомпилировать свой модуль относительно исходного кода работающего на вашей системе ядра.

Исходные коды ядра обычно находятся в директории /usr/src/linux, права доступа к которой имеет только root. В настоящее время, рекомендуется размещать исходные коды ядра в домашней директории, права на которую будут иметь не-root пользователи. Комманды в этой стстье запускаются не из-под root, используя sudo для временного получения привелегий root-а только тогда, когда это необходимо. Для установки sudo, прочитайте sudo(8), visudo(8) и sudoers(5). Вы можете запускать все комманды и из-под root, если пожелаете. В этом случае, вам потребуются права root-а для того, чтобы следовать инструкциям из этой статьи.

Приготовления для компилирования модулей ядра в Debian.

Пакет module-assistant для Debian настраивает систему для сборки внешних модулей ядра. Установите его с помощью:

Это все. Теперь вы можете скомпилировать модуль ядра. Дополнительно, вы можете прочитать Debian Linux Kernel Handbook, чтобы иметь более глубокое представление о работе с ядром в Debian.

Приготовления для компилирования модулей в Fedora

Пакет kernel-devel в Fedora имеет все необходимые заголовочные файлы ядра и инструменты для сборки внешних модулей ядра. Установите его с помощью:

И снова, это все, что нужно, — теперь вы можете компилировать модули ядра. Документацию по теме можно найти в Fedora release notes.

«Ванильное» ядро и его настройки.
Если вы решили использовать ванильное ядро Linux, то сначала вы должны его сконфигурировать, скомпилировать, установить и перезагрузиться с вашим новым ванильным ядром. Эти действия не так уж и просты, но эта статья покрывает только базовые знания по работе с ванильным ядром.

Исходные коды стандартного ядра Linux размещаются на http://kernel.org/. Ссылка на последний стабильный релиз находится на главной странице. Скачайте полностью весь релиз, а не патч. Например, на момент написания статьи, последний стабильный релиз был расположен на http://kernel.org/pub/linux/kernel/v2.6/linux-2.6.21.5.tar.bz2. Для более быстрой загрузки, найдите ближайшее к вам зеркало из списка зеркал и скачивайте с него. Используйте wget, это самый простой способ скачать исходные коды в режиме докачки. HTTP часто блокируется и если скачивание прервется, оно продолжится с того момента, на котором остановилась:

Распакуйте исходные коды ядра:

Теперь ваше ядро расположено в linux- /. Войдите в эту директорию с начните конфигурацию:

Существует набор параметров для make, чтобы автоматически собрать и установить ядро в различной форме: пакет Debian, пакет RPM, тарболл и т.д. Чтобы узнать их всех, наберите:

Параметр, который будет работать почти на каждом дистрибутиве:

Когда сборка завершится, установите новое ядро с помощью:

Затем создайте ссылку на дерево исходных кодов ядра в его стандартном расположении:

Теперь искходный код ядра готов для компилирования внешних модулей. Перезагрузитесь в свое новое ядро прежде чем загружать модули, основанные на этом дереве исходных кодов.

«Hello, World!» с помощью printk()

Для нашего первого модуля, чтобы напечатать «Hello, world!», мы будем использовать модуль, который использует возможность ядра показа сообщений. В общем-то, printk() является printf() для ядра. Сообщения printk() направляются в буффер сообщений ядра и копируются /var/log/messages.

Он содержит два файла: Makefile, который содержит инструкции для сборки модуля, и hello_printk.c, исходный код модуля. Для начала, осмотрим Makefile:

obj-m — это список модулей ядра для сборки. .o будет автоматически собран из соответствующего .c файла (нет необходимости указывать список исходных файлов).

KDIR — это расположение исходного кода ядра. Текущий стандарт это ссылка на соответствующее дерево исходных кодов, содержащее скомпилированные модули.

PWD — это текущая директория и расположения наших исходных кодов модуля.

default это стандартный параметр make. До тех пор, пока не будет указан другой параметр, make будет исполнять правила для этого параметра. Правило, указанное здесь запускает make с рабочей директорией, содержащей исходные коды ядра и компилирует модули только в $(PWD) (локальной) директории. Это позволяет нам использовать все правила для компилирования модулей, определенные в главном дереве исходного кода ядра.

А сейчас, давайте пройдемся по коду hello_printk.c.

Эти включения заголовочных файлов, предоставляемых ядром, требуются для всех модулей. Они включают такие вещи, как определение макроса module_init(), который мы увидим позже.

Это функция инициализации модуля, которая выполняется при его первой загрузке. Ключевое слово __init указывает компилятору, что этот код будет выполнен единожды, когда модуль загрузится. Строка, содержащая printk() запишет строку «Hello, world!» в буффер сообщений ядра. Формат аргументов printk(), в большинстве случаев, идентичен формату printf(3).

Макрос module_init() сообщает ядру, какую функцию выполнить при загрузке модуля. Все остальное, происходящее внутри ядра — результат установок функции инициализации модуля.

Проще говоря, функция выхода запускается один раз, когда модуль выгружается, а макрос module_exit() идентифицирует функцию выхода. Ключевое слово __exit указывает ядру, что этот код нужно выполнить единожды, во время выгрузки модуля.

MODULE_LICENSE() сообщает ядру, под какой лицензией распространяется исходный код модуля, что влияет на то, к каким символам (функциям, переменным и т.д.) он может получить доступ в главном ядре. Модуль под лицензией GPLv2 (как на примере) имеет доступ ко всем символам. Другие лицензии модуля предупредят ядро о том, что был загружен закрытый модуль, или модуль, которому нельзя доверять. Модули без MODULE_LICENSE() распознаются ядром, как модули, выпущенные не под GPLv2. Макросы MODULE_*() полезны для идентификации информации о модуле в стандартной форме.

Теперь скомпилируем и запустим код. Войдите в директорию с кодом и соберите модуль:

Затем, загрузите модуль с помощью insmod и проверьте, что он печатает свое сообщение, используя dmesg, программу, которая выводит на экран содержимое буффера сообщений ядра:

Вы должны увидеть «Hello, world!». А теперь выгрузим модуль, используя rmmod, и проверим сообщение о выходе:

Вы успешно скомпилировали и установили модуль ядра!

«Hello, World!» с помощью /proc

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

Для наших целей, мы создадим файл в /proc, который выведет «Hello, world!» при чтении. Мы будем использовать /proc/hello_world. Скачайте и распакуйте тарболл hello_proc модуля. Заглянем в код hello_proc.c:

На этот раз, мы добавили заголовочный файл для procfs, который включает поддержку работы с /proc.

Следующая функция вызовется, когда процесс выполнит read() на созданный нами файл в /proc. Это проще, чем реализовывать это полностью на системном вызове read(), так как нам надо разрешить считать строку «Hello, world!» только один раз.

Аргументы этой функции нуждаются в подробном объяснении. buffer это указатель на буффер ядра, в который мы пишем выходные данные из read(). start используется для более сложных /proc файлов; здесь мы его игнорируем. offset указывает на то место, с которого надо начинать считывание внутри «файла»; здесь мы передадим 0 для простоты. size это размер буффера в байтах; мы должны удостовериться в том, что мы не пишем данные после того, как буффер закончится. eof указывает на конец файла. Аргумент data, опять же, для более сложных файлов и здесь игнорируется.

А теперь, тело функции:

Затем, нам надо зарегистрировать подсистему /proc в функции инициализации нашего модуля:

И отменить эту регистрацию, когда модуль выгружен (если мы этого не сделаем, то когда процесс попробует считать /proc/hello_world, файловая система /proc попробует выполнить функцию, которая больше не существует, что вызовет kernel panic).

Теперь мы готовы скомпилировать и загрузить модуль:

Появится файл под именем /proc/hello_world, который будет выводить «Hello, world!» при чтении:

Вы можете создать много других /proc файлов для одного и того же драйвера, добавить функции, позваляющие запись в /proc файлы, создавать директории, полные /proc файлов, и так далее. Для чего-то более сложного, чем этот драйвер, проще и безопаснее использовать вспомогательные функции seq_file при написании интерфейсных функции для /proc. Для расширения своих знаний в этой области, прочтите Driver porting: The seq_file interface.

«Hello, World!» с помощью /dev/hello_world

Теперь мы реализуем «Hello, world!» с помощью файла устройства в /dev, /dev/hello_world. Раньше файл устройства был специальным файлом, создаваемым запуском хитроумного шелл-скрипта под названием MAKEDEV, который вызывал комманду mknod для создания любого возможного файла в /dev, невзирая на то, запущено ли это устройство в системе. Потом был devfs, который создавал /dev файлы, когда к ним впервые обращались, что вызывало много интересных проблем блокировки и множество попыток открыть файл устройства, чтобы проверить его существование. Сейчас с /dev работает udev, который связывает /dev с программами пользовательского окружения. Когда модули ядра регистрируют устройсва, они появляются в файловой системе sysfs, смонтированной в /sys. Программа из пользовательского окружения, udev, следит за изменениями в /sys и динамечески создает записи в /dev, исходя из настроек, обычно размещаемых в /etc/udev.

Как мы видим из списка необходимых заголовочных файлов, для создания устройства требуется немного больше поддержки ядра, чем в предыдущих методах. fs.h включает в себя определения структур для операций над файлами, которые мы должны использовать, применительно к нашему /dev файлу. miscdevice.h включает поддержку регистрации различных файлов устройств. asm/uaccess.h влючает функции для тестирования на чтение или запись в памяти пользовательского окружения, без нарушения прав.

hello_read() — функция, вызываемая процессом системным вызовом read() к /dev/hello. Она выведет «Hello, world!» в буффер, передаваемый вызовом read().

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

Теперь создадим структуру, содержащую информацию для регистрации стороннего устройства в ядре:

Как обычно, мы регистрируем устройство в функции инициализации модуля:

И не забываем выгружать устройство в функции выхода:

Скомпилируем и загрузим модуль:

В системе появилось устройство с именем /dev/hello, которое выводит «Hello, world!» в момент считывания root-ом:

Но оно не может считаться обычным пользователем:

Это происходит при стандартном правиле udev, которое, при появлении нового устройства, создает файл под названием /dev/ и присваивает ему права на чтение 0660. Нам необходимо создать вместо устройства ссылку, читаемую обычными пользователями, с именем /dev/hello_world. Для того, чтобы это сделать, напишем правило udev.

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

Разобьем это правило на части и более подробно разберем каждую из них.

KERNEL==»hello» приводит к выполнению правила, когда устройство с именем в строке (== это оператор сравнения) появляется в /sys. Устройство hello появляется когда мы вызываем misc_register() со структура, содержащей имя устройства «hello». Вот результат в /sys:

SYMLINK+=»hello_world» приказывает добавить hello_world к списку ссылок, которые должны создаваться при появлении устройства. В нашем случае, мы знаем, что список будет состоять из одной ссылки, но другие устройства могут иметь много udev правил, которые создают другие ссылки, так что было бы неплохо это сделать.

MODE=»0444″ назначает права доступа к оригинальному файлу устройства на 0444, что позволяет получить доступ на чтение к этому файлу.

Очень важно использовать правильный оператор (==, += или =), во избежание появления неожиданного результата.

Теперь, когда мы понимаем, что делает это правило, давайте установим его в директорию /etc/udev. Файлы правил udev раположены в той же манере, что и скрипты инициализации System V в /etc/init.d/. Udev выполняет каждый скрипт из директории правил udev, /etc/udev/rules.d, в алфавитном/номерном порядке. Также, как и скрипты инициализации System V, файлы в /etc/udev/rules.d являются обычными ссылками на реально существующие файлы, так что правила будут выполнятся в правильном порядке.

Скопируйте файл hello.rules из директории hello_dev в /etc/udev и создайте ссылку к нему, которая будет выполнятся до других файлов правил:

Теперь, перезагрузите драйвер hello world и посмотрите на записи в /dev:

Теперь у нас есть /dev/hello_world! Наконец, проверьте, что вы можете считывать устройства «Hello, world!», как обычный пользователь:

За более подробной информацией по написанию правил udev, обращайтесь к Writing udev rules, написанную Дэниэлом Дрэйком.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *