- PVSM.RU - https://www.pvsm.ru -

Вероятно, вы знаете, что если запустить ядро Linux без корневой файловой системы или файла initramfs, то оно упадет с сообщением о панике ядра.
Но возможна ли работа ядра Linux без этих, вроде бы обязательных компонентов? Ответ на вопрос — да, возможна, но использовать такие возможности в конечном продукте не стоит.
При запуске ядра ему могут передаваться параметры через командную строку. Одним из параметров является rootwait, указывающий ядру на то, что нужно подождать появление корневой файловой системы. В этом случае ядро ожидает появление корневой системы, а не завершается с ошибкой.
Формально ничто не мешает написать модуль ядра, который взаимодействует с клавиатурой и дисплеем и временно выполняет функции пользовательского приложения, пока ядро ожидает корневую файловую систему.
Пользовательским приложением может быть, например, игра Тетрис. Она из-за своей простоты в реализации и зрелищности добавляет наглядности в изучении темы и дает чувство завершенности. А мысль о том, что тетрис, работает в ядре, усиливает эффект.
Изначально, я хотел рассказать, как превратить ядро в пользовательское приложение на примере Тетриса, но потом понял, что описание самой игры отвлечет читателя. Поэтому выбрал более абстрактный учебный пример, а про Тетрис в ядре только упомянул.
Тетрис и инструкция по сборке и запуску ядра с ним находится в репозитории на GitHub [1], а в статье основное внимание уделяется внутренним компонентам ядра и внутренним интерфейсам, которыми необходимо уметь пользоваться для написания игры, работающей в контексте ядра.
В статье не рассматриваются или рассматриваются только в контексте разработки модуля ядра такие аспекты игры Тетрис как:
работа с двумерными массивами;
обработка событий и игровой цикл;
коллизии и валидация движений;
повороты матриц;
отрисовка игрового процесса;
логика удаления линий и подсчёт очков;
генерация случайных фигур.
Статья должна быть интересной и полезной для вас, если вы хотите узнать как:
получить исходный код ядра;
написать программный код, который будет выполняться в ядре Linux;
отобразить игровой процесс на экране;
обработать нажимаемые клавиши на клавиатуре;
организовать игровой цикл в модуле ядра Linux;
написать логику игры на языке C, которая не зависит от стандартной библиотеки;
определиться, с какими параметрами собирать ядро;
собрать ядро.
Написание модуля ядра не такая сложная задача, как кажется на первый взгляд. Это не значит, что нет сложностей в разработке архитектуры, выявлении ошибок и поиске нужных компонентов ядра. Я имею ввиду, что минимальный набор знаний, позволяющий начать писать модули ядра Linux, — небольшой.
Если работаете в каком-то дистрибутиве Linux, создаете директорию, например, ~/sandbox/sample_module в нее помещаете всего два файла:
файл simple_module.c
#include <linux/module.h>
#include <linux/printk.h>
static int __init simple_module_init(void)
{
pr_info("simple_module: Hello, World!n");
return 0;
}
static void __exit simple_module_exit(void)
{
pr_info("simple_moudule: Bye!n");
}
module_init(simple_module_init);
module_exit(simple_module_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("whoever");
MODULE_DESCRIPTION("simple module");
файлMakefile:
obj-m += simple_module.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
и выполняете команду:
make
Помните, что в Makefile-файлах для отступов используются табуляции.
В директории должны появиться следующие файлы:
.
├── Makefile
├── modules.order
├── Module.symvers
├── README.md
├── simple_module.c
├── simple_module.ko
├── simple_module.mod
├── simple_module.mod.c
├── simple_module.mod.o
└── simple_module.o
Если ничего не получилось, а особенно если вы не увидели среди файлов simple_module.ko и получили ошибку типа:
user@debian:~/sandbox/simple_module$ make
make -C /lib/modules/6.12.63+deb13-amd64/build M=/home/user/sandbox/simple_module modules
make[1]: Entering directory '/home/user/sandbox/simple_module'
make[1]: *** /lib/modules/6.12.63+deb13-amd64/build: No such file or directory. Stop.
make[1]: Leaving directory '/home/user/simple_module'
make: *** [Makefile:4: all] Error 2
то, скорее всего, вы не установили необходимые пакеты необходимые для сборки ядра Linux. Решается это следующей командой:
sudo apt install make build-essential linux-headers-$(uname -r)
Теперь можно загрузить модуль в ядро и увидеть его работу.
sudo insmod simple_module.ko
Убеждаемся, что модуль загрузился в ядро:
lsmod | grep simple_module
Смотрим, что вывел модуль в буфер сообщений ядра:
sudo dmesg
[115232.697183] simple_module: loading out-of-tree module taints kernel.
[115232.697187] simple_module: module verification failed: signature and/or required key missing - tainting kernel
[115232.702298] simple_module: Hello, World!
На предупреждение об ошибке верификации не обращайте внимание. Для простого учебного примера подписывание модулей не нужно.
Модуль вывел сообщение и больше ничего не делает, поэтому выгружаем модуль из ядра:
sudo rmmod simple_module
и смотрим, что он нам сообщил:
sudo dmesg
[116334.688925] simple_module: Bye!
Написанный модуль будет работать только для версии ядра, для которой его собирали. Иными словами, на Linux с отличающейся версией ядра модуль не загрузится, его нужно пересобирать. Вообще, внутренние интерфейсы ядра изменчивые, и часто исходный код модуля нужно тоже изменять, если меняется версия ядра. Разбираемый в статье пример для версий ядер 6.12.y.
Если у вас не упал Linux и все работает, страх перед написанием модуля ядра должен исчезнуть, но предупреждаю, лучше дальнейшие шаги делать не на основном компьютере.
Модуль работает, но ничего полезного не делает, а нам для игры нужно реализовать обработку клавиш и вывод символов на экран. Разберемся для начала с обработкой клавиш.
Задумывались ли когда-нибудь, что происходит от момента нажатия клавиши на клавиатуре, до момента, когда вы видите реакцию компьютера на нажатие? Постараюсь объяснить.
Электрический сигнал, который получается от замыкания контактов на клавиатуре, попадает в контроллер клавиатуры — микросхему, знающую как превратить сигнал от нажатия или отпускания определенной клавиши в последовательность байт. Эта последовательность байт называется сканкодом. Она так называется, потому что контроллер сканирует текущее состояние клавиш клавиатуры.
Возникает вопрос, а что дальше происходит со сканкодом? Сканкод нужно передать дальше компьютеру, чтобы он мог его как-то обработать. Т.е. сканкод нужно передать при помощи какого-то интерфейса. Это может быть PS/2, USB, Bluetooth, BLE. Соответственно и компьютер, и клавиатура должны иметь одинаковые интерфейсы.
Контроллер и электронные схемы клавиатуры кодируют сканкод в последовательность электрических или электромагнитных сигналов, а материнская плата компьютера эти сигналы превращает обратно в сканкод, который обрабатывается программой, выполняемой процессором (в нашем случае это ядро операционной системы Linux).
Так как сканкоды могут поступать по различным интерфейсам, в ядре Linux для каждого из интерфейсов существует драйвер, умеющий превращать сканкод в keycode — унифицированный код для клавиш на различных устройствах. Эти коды можно посмотреть в файле include/uapi/linux/input-event-codes.h [2].
В принципе нам повезло, и разбираться во всех подробностях, что происходит дальше — не нужно, как и разбираться во всем разнообразии драйверов для клавиатур. Вот где настоящая сила абстракции ядра Linux!
Нужно просто найти способ, как получить и обработать этот keycode в модуле ядра Linux. Оказалось, это несложно. В файле include/linux/keyboard.h [3] определена следующая структура:
struct keyboard_notifier_param {
struct vc_data *vc; /* VC on which the keyboard press was done */
int down; /* Pressure of the key? */
int shift; /* Current shift mask */
int ledstate; /* Current led state */
unsigned int value; /* keycode, unicode value or keysym */
};
А так же объявлены две функции для подписывания и отписывания от получения нотификаций от ядра с кодами нажатых клавиш.
extern int register_keyboard_notifier(struct notifier_block *nb);
extern int unregister_keyboard_notifier(struct notifier_block *nb);
В поле value содержит значение keycode, а поле down — информацию о том, была нажата или отжата клавиша. Интересно поле vc (виртуальная консоль) — но об этом позже.
В исходном коде модуля нужно определить функцию обработчика нотификаций от ядра с сигнатурой:
static int kbd_notifier_callback (struct notifier_block *nb,
unsigned long action, void *data)
Имя функции можно выбирать любое доступное, так как мы все-равно должны проинициализировать поле notifier_call структуры notifier_block именем нашего обработчика:
static struct notifier_block kbd_nb = {.notifier_call = kbd_notifier_callback};
Теперь нужно не забыть добавить регистрацию обработчика в функции simple_module_init и разрегистрацию в simple_module_init:
static int __init simple_module_init(void)
{
pr_info("simple_module: Hello, World!n");
return register_keyboard_notifier(&kbd_nb);
}
static void __exit simple_module_exit(void)
{
pr_info("simple_module: Bye!n");
unregister_keyboard_notifier(&kbd_nb);
}
Если хотите острых ощущений и на личном опыте ощутить, что такое не проинициализированный указатель — не помещайте
unregister_keyboard_notifierвsimple_module_init.
Все, теперь можно собрать модуль и загрузить его в ядро.
sudo apt install linux-headers-$(uname -r)
make
sudo insmod simple_module
sudo dmesg
Вы увидите в логах, какие клавиши нажимали после загрузки модуля:
[36950.903738] simple_module: KEYCODE = 67 down = down
[36951.022016] simple_module: KEYCODE = 67 down = up
[36951.266171] simple_module: KEYCODE = 67 down = down
[36951.376498] simple_module: KEYCODE = 67 down = up
[36952.194590] simple_module: KEYCODE = 6c down = down
[36952.296999] simple_module: KEYCODE = 6c down = up
А теперь про острые ощущения, упомянутые ранее. Если вы забыли добавить unregister_keyboard_notifier в simple_module_exit, то при выгрузке модуля командой rmmod:
sudo rmmod simple_module
вы полностью повесите операционную систему, и вам придётся выполнить аварийную перезагрузку.
Наш модуль может только обработать и вывести, какая клавиша была нажата или отжата, но мы приблизились к созданию Тетриса в ядре. Нужно разобраться, как вывести что-нибудь на экран монитора.
Наверное, с первого раза понять, как работает вывод на экран в Linux очень сложно. Возможно, после прочтения статьи вы приблизитесь к пониманию этой сложной темы.
Ядро Linux выводит информацию в консоль. В большинстве случаев в роли консоли выступает или последовательный порт (ttyS0...ttySn) или экран монитора (tty1..ttyN).
Для того, чтобы увидеть, что ядро выводит в последовательный порт, необходимо к этому порту подключить эмулятор терминала на другом компьютере, выбрать правильные параметры соединения по протоколу UART и наблюдать вывод. Обычно консоль на последовательном порту используется в различной встраиваемой электронике, например на роутерах.
Чтобы подключиться к такому порту, вам нужно будет разобрать роутер, найти отверстия на плате на которые выведен последовательный порт, припаять гребенку и подключить к компьютеру через USB-UART преобразователь.
А еще вам придется менять код в ядре, для того, чтобы туда поместить Тетрис. Работа с ядрами для встраиваемых устройств тема интересная, но тогда статья будет занимать узкую нишу.
Последовательный порт практически отсутствует на современных ноутбуках и довольно редко встречается на стационарных компьютерах, а там где есть, используются другие логические уровни. Одним словом — морока.
Поэтому я отбросил идею использования последовательного порта.
Экран монитора представлен в Linux виртуальными консолями, или виртуальными терминалами. Вы можете переключаться между виртуальными консолями используя сочетания клавиш Ctrl + Alt + F1 .. F7. На моем Debian 13 Ctrl + Alt + F1 и Ctrl + Alt + F2 зарезервированы для графических сессий, а при нажатии Ctrl + Alt + F3 .. F6 я могу переключаться между текстовыми виртуальными консолями.
Нужно разобраться, как эти виртуальные консоли представлены в ядре, и как с ними можно работать из модуля. После небольшого поиска информации узнаем, что обратиться ко всем виртуальным консолям можно через глобальную! переменную vc_cons, определенную в файле drivers/tty/vt/vt.c [4] (когда-то давно в институте слышал фразу «глобальные переменные — это плохо»).
struct vc vc_cons [MAX_NR_CONSOLES];
Структура vc, определенная в файле include/linux/console_struct.h [5]
struct vc {
struct vc_data *d;
struct work_struct SAK_work;
/* might add scrmem, kbd at some time,
to have everything in one place */
};
нам интереса не представляет, разве что ее поле d. Кстати, комментарий, который видим в коде, как бы намекает, мы лезем туда, где кто-то что-то может поменять в будущем. Но для нашего учебного примера это не страшно.
Смотрим определение структуры vc_data в файле include/linux/console_struct.h [6]
struct vc_data {
struct tty_port port; /* Upper level data */
struct vc_state state, saved_state;
unsigned short vc_num; /* Console number */
unsigned int vc_cols; /* [#] Console size */
unsigned int vc_rows;
unsigned int vc_size_row; /* Bytes per row */
unsigned int vc_scan_lines; /* # of scan lines */
unsigned int vc_cell_height; /* CRTC character cell height */
unsigned long vc_origin; /* [!] Start of real screen */
unsigned long vc_scr_end; /* [!] End of real screen */
unsigned long vc_visible_origin; /* [!] Top of visible window */
unsigned int vc_top, vc_bottom; /* Scrolling region */
const struct consw *vc_sw;
unsigned short *vc_screenbuf; /* In-memory character/attribute buffer */
unsigned int vc_screenbuf_size;
unsigned char vc_mode; /* KD_TEXT, ... */
/* attributes for all characters on screen */
unsigned char vc_attr; /* Current attributes */
unsigned char vc_def_color; /* Default colors */
unsigned char vc_ulcolor; /* Color for underline mode */
unsigned char vc_itcolor;
unsigned char vc_halfcolor; /* Color for half intensity mode */
/* cursor */
unsigned int vc_cursor_type;
unsigned short vc_complement_mask; /* [#] Xor mask for mouse pointer */
unsigned short vc_s_complement_mask; /* Saved mouse pointer mask */
unsigned long vc_pos; /* Cursor address */
...
}
Все определение не привожу, так как слишком длинная простыня получается. Но эта структура — то, что нам нужно. Важны поля:
vc_num — номер виртуальной консоли;
vc_col — количество столбцов;
vc_rows — количество строк;
vc_attr — текущие аттрибуты цвета и фона используемые при выводе символов на экран;
vc_screenbuf— массив содержащий коды символов с атрибутами, которые отображаются на экране.
Номер текущей виртуальной консоли хранится в глобальной переменной fg_console, определенной в файле drivers/tty/vt/vt.c [7].
Таким образом, программный код для получения структуры vc_data для текущей консоли будет выглядеть:
struct vc_data *vc;
vc = vc_cons[fg_console].d;
Вывод в текущую виртуальную консоль символа будет выглядеть:
static void console_putchar(char c, int col, int row)
{
vc->vc_screenbuf[col + row * vc->vc_cols] = c | (vc->vc_attr << 8);
}
Очистка консоли:
static void console_clear(void)
{
console_lock();
for (int i = 0; i < vc->vc_cols * vc->vc_rows; i++)
{
vc->vc_screenbuf[i] = ' ' | (vc->vc_attr << 8);
}
console_unlock();
}
Функции console_lock и console_unlock вызвал для надежности, чтобы изменения во фреймбуфере были атомарными.
Изменение значений в vc_screen_buf не означает, что вы увидите изменения на экране, так как необходимо еще вызвать функцию redraw_screen.
static void console_update(void)
{
if (vc)
{
redraw_screen(vc, 0);
}
}
Код, хоть и простой, но перестает быть уровня «Hello World», а весь листинг файла исходного кода модуля неудобно приводить в статье, поэтому я подготовил репозиторий с исходным кодом модуля [8], и для каждого этапа создал тег. Вам достаточно клонировать один раз репозиторий, и потом делать checkout нужного этапа, чтобы увидеть как выглядит весь исходный код.
git clone
git checkout output
make
Переходим в текстовую виртуальную консоль, например Ctrl + Alt + F3, заходим под своим пользователем и загружаем из нее модуль
sudo insmod simple_module.ko
Наблюдаем очистку консоли и символ посередине консоли.
На скриншоте виден промпт командной строки, но это из-за того, что наш драйвер и терминал совместно используют виртуальную консоль. Я намеренно оставил терминал за виртуальной консолью, чтобы не повесить операционную систему.
Выгружаем модуль на всякий случай.
sudo rmmod simple_module
Уже можно получать коды нажатий клавиш и отображать информацию в виртуальной консоли. Сейчас предстоит закодировать управление перемещением символом при помощи клавиш клавиатуры.
То, что мы будем сейчас делать, напомнило мне управление черепашкой лого в детстве. Думаю, что статью читают и те, кто помнит те далекие времена, когда все было в новинку, и казалось, что чуть-чуть разобраться и можно будет написать крутую программу.
Чтобы символ превратить в перо, которым мы будем рисовать в текстовой виртуальной консоли, нужно при получении нотификаций о нажатых клавишах обновлять переменную, содержащую keycode нажатой клавиши, с определенной периодичностью читать эту переменную и обновлять положение символа в vc_screenbuf. Чем меньше интервал между опросами переменной, тем быстрее будет рисовать «перо».
И тут мы приходим, к тому, что уже получается многопоточное приложение. Пишем мы в одном потоке выполнения, а читаем в другом. Переменная key_state является атомарной (не может быть случая, когда она обновится по частям), но мы в одном потоке пишем, а в другом читаем и пишем, что подразумевает использование примитивов синхронизации, сделаем из key state атомарную переменную.
static atomic_t key_state = ATOMIC_INIT(KEY_RESERVED);
...
if (action == KBD_KEYCODE && down)
{
atomic_set(&key_state, param->value);
}
...
unsigned int k = atomic_xchg(&key_state, KEY_RESERVED);
Делаем чекаут на нужный коммит из репозитория и загружаем модуль в виртуальной консоли:
git checkout interactive
Для простоты я намеренно не очищал экран и оставил одновременное взаимодействие нашего модуля и остальных подсистем Linux c консолью. Также, вывод осуществляется на текущую виртуальную консоль, когда была выполнена загрузка модуля. При создании скришота из графической сессии, я нажимал клавиши управления курсором, поэтому вы видите дополнительные артефакты, так как модуль перехватывал клавиши нажатия и в графической сессии.
Теперь мы можем рисовать в виртуальной консоли. Но каждый запуск выполняется с опаской, потому что ошибка в исходном коде модуле может полностью повесить Linux, на котором и работаем. Чем-то напоминает операцию на собственном головном , поэтому сделаем из модуля встроенный и будем запускать ядро в эмуляторе QEMU.
Начать разработку внутреннего модуля немного сложнее, так как одних заголовочных файлов ядра вам не хватит, необходим весь исходный код ядра.
Вопрос откуда взять исходный код? Есть upstream-репозиторий, который поддерживает Линус Торвальдс, есть репозиторий со стабильными ветками ядра: [https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git] [10]. Все это ванильные ядра.
Исходный код ванильных ядра кроме git-репозиториев доступны и в [архивах: https://cdn.kernel.org/pub/linux/kernel [11]].
Многие разработчики дистрибутивов и встраиваемых систем поддерживают свои версии ядер, являющиеся форками от ванильных ядер.
Можно использовать исходный код, распространяемый вместе с вашим дистрибутивом, но интереснее взять код upstream.
Размер всего Git репозитория ядра Linux со стабильными ветками уже больше 6 ГБ. Репозиторий Линуса Торвальдса — 3,5 ГБ. Распакованный архив с исходным кодом ядра стабильной ветки 6.12 — около 1,6 ГБ.
Можно использовать архив с исходниками, но без Git неудобно вносить правки. Поэтому лучше клонировать весь репозиторий или выполнить поверхностное клонирование репозитория.
Думаю, лучше всего директорию с Linux source tree разместить на том же уровне, где и проект simple_module.
Выполняем поверхностное клонирование репозитория, указав конкретный тег версии ядра v6.12.67:
git clone --branch v6.12.67 --depth 1 https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git ../linux
cd ../linux
git checkout -b simple_module-v6.12.67
Помещаем исходный код модуля в Linux source tree.
mkdir -p drivers/misc/simple_module
cp ../simple_module/simple_module.c drivers/misc/simple_module/
Изменяем и добавляем файлы Кconfig и Makefile.
Чтобы внутренний (in-tree) модуль собирался системой сборки ядра Linux, нужно в директории с модулем создать два файла Makefile и Kconfig.
Создаем файл drivers/misc/simple_module/Kconfig:
cat << EOF > drivers/misc/simple_module/Kconfig
config SIMPLE_MODULE
tristate "Simple in-tree module example"
default y
help
Sample of the user application in Linux kernel without userspace.
EOF
Создаем файл drivers/misc/simple_module/Makefile:
echo 'obj-$(CONFIG_SIMPLE_MODULE) += simple_module.o' > drivers/misc/simple_module/Makefile
Для интеграции встроенного модуля в дерево исходного кода ядра Linux тоже используются Makefile и Kconfig, но они располагаются на уровень выше, и их нужно изменять, а не добавлять.
В файл drivers/misc/Kconfig нужно добавить строку перед endmenu:
sed -i '/^endmenu>/i
source "drivers/misc/simple_module/Kconfig"'
drivers/misc/Kconfig
А в конец файл drivers/misc/Makefile:
echo 'obj-$(CONFIG_SIMPLE_MODULE) += simple_module/' >> drivers/misc/Makefile
Добавляем в файлы в git:
git add .
Смотрим изменения, которые будут вноситься:
git diff --cached
Делаем коммит, чтобы не пропал наш труд:
git commit -m "My first in-tree module"
Теперь модуль у нас встроенный в ядро. Нужно сконфигурировать и собрать все ядро.
Проще всего взять минимальную конфигурацию ядра и добавить нужные опции.
make tinyconfig
./scripts/config -e 64BIT -e PCI -e PCI_HOST_GENERIC -e TTY -e BINFMT_ELF -e DEVTMPFS -e DEVTMPFS_MOUNT -e PROC_FS -e SYSFS -e TMPFS -e PRINTK -e EARLY_PRINTK -e PRINTK_TIME -e ACPI -e EFI -e EFI_STUB -e FB -e FB_EFI -e FRAMEBUFFER_CONSOLE -e CMDLINE_BOOL --set-str CMDLINE "rootwait" -e SIMPLE_MODULE -e USB -e USB_SUPPORT -e USB_XHCI_HCD -e USB_PCI -e USB_XHCI_PCI
make olddefconfig
Собираем ядро:
make -j$(nproc)
Собранное ядро находится по пути arch/x86/boot/bzImage.
Устанавливаем QEMU, если еще он не установлен:
sudo apt install qemu-system-x86 qemu-utils
И наконец, мы добрались до заветной строки, где ядро запускается как программа, внутри которого наше приложение:
qemu-system-x86_64 -kernel arch/x86/boot/bzImage
Можно себя поздравить и выпить чашечку кофе.
Но все-таки есть часть недостатков, которые хочется исправить:
ядро продолжает логировать сообщения в консоль после запуска нашего модуля, а хочется чистый экран;
системная консоль Linux тоже обрабатывает нажатия клавиш, и мы видим это внизу экрана;
раздражает мерцающий курсор, его тоже желательно убрать;
при запуске с UEFI-загрузкой — ядро некорректно работает.
Для того, чтобы ядро прекращало писать логи в консоль нужно в console_loglevel записать 0.
Дальнейшую обработку клавиш я намеренно не отключал во внешнем модуле, так вероятность того, что вы бы повесили ядро, в котором работали была бы выше. Теперь достаточно заменить NOTIFY_OK на NOTIFY_STOP в нашем kbd_notifier_callback
Отображение курсора отключается модифицированием структуры vc_data.
static void console_hide_cursor(void) {
console_lock();
vc = vc_cons[fg_console].d;
if (vc) {
vc->vc_deccm = 0;
vc->vc_cursor_type = 0;
}
console_unlock();
}
Если внутренний модуль ядра встраивается в ядро (не является загружаемым), то функция module_exit не нужна, ее можно удалить, кроме того, чтобы корректно работала консоль при UEFI-загрузке замените module_init на late_initcall. Не буду останавливаться на том, как ядро вызывает иницилиазирующие функции для встраиваемых модулей, и почему именно при UEFI-загрузке без late_initcall не работает. Будем считать, что это ваше домашнее задание.
Вносим изменения и коммитим:
git add .
git commit -m "Fixes"
QEMU позволяет эмулировать как загрузку систем с BIOS-загрузкой, так и с UEFI-загрузкой. Для UEFI-загрузки ему необходимо передать два файла с прошивками:
OVMF_CODE_4M.fd или OVMF_CODE.fd — сама прошивка UEFI;
OVMF_VARS_4M.fd или OVMF_VARS.fd — значения NVRAM переменных UEFI.
Если вы установили QEMU, то эти файлы у вас уже есть. Чтобы найти, выполните:
find /usr/share/ -name OVMF*
user@debian:~$ find /usr/share/ -name OVMF*
/usr/share/OVMF
/usr/share/OVMF/OVMF_CODE_4M.secboot.strictnx.fd
/usr/share/OVMF/OVMF_VARS_4M.ms.fd
/usr/share/OVMF/OVMF_CODE_4M.fd
/usr/share/OVMF/OVMF_CODE_4M.snakeoil.fd
/usr/share/OVMF/OVMF_VARS_4M.snakeoil.fd
/usr/share/OVMF/OVMF_VARS_4M.fd
/usr/share/OVMF/OVMF_CODE_4M.ms.fd
/usr/share/OVMF/OVMF_CODE_4M.secboot.fd
/usr/share/ovmf/OVMF.fd
/usr/share/qemu/OVMF.fd
Можно прописывать эти пути при запуске QEMU, но я для удобства и безопасности, скопировал их в директорию на уровень выше директории, в которой запускаю QEMU
Перезапускаем в режиме UEFI-загрузки:
qemu-system-x86_64 -drive if=pflash,format=raw,readonly=on,file=../OVMF_CODE_4M.fd -drive if=pflash,format=raw,readonly=on,file=../OVMF_VARS_4M.fd -kernel arch/x86/boot/bzImage
Как передать исходный код измененного ядра, который мы разработали, другому человеку. Можно конечно, создать удаленный репозиторий и запушить туда, но можно сделать более удобнее при помощи патча.
Патчи позволяют не создавать отдельный удалённый репозиторий со всем ядром, но еще они обеспечивают масштабируемую, проверяемую и децентрализованную разработку.
git checkout -b simple_module-patch-v6.12.67
У нас два коммита:
один после создания внутреннего модуля из внешнего;
второй после правок.
Поэтому желательно два последних коммита объединить в один перед созданием патча:
git reset --soft HEAD~2
git commit -s -m "In-tree module sample v6.12.67"
git format-patch -1
mkdir -p ../simple_module/patches
cp 0001-In-tree-module-sample-v6.12.67.patch ../simple_module/patches/
Теперь изменения для добавление нашего модуля дерево исходного кода Linux хранятся в одном текстовом файле 0001-In-tree-module-sample-v6.12.67.patch со всеми необходимыми метаданными. Можно брать различные версии ядра Linux или различные ветки в репозиториях ядра Linux, пробовать применить этот патч, собирать, экспериментировать.
Созданный патч не соответствует требованиям, предъявляемым к патчам, отправляемым в upstream. Не буду рассказывать, как подготовить отправить патч для upstream, вы уже и так устали, если дочитали статью до этих слов, а Тетриса до сих пор не увидели.
Теперь можно применить патч к чистому дереву исходного кода Linux при помощи команды patch:
cd ..
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.67.tar.gz
tar xvf linux-6.12.67.tar.gz
cd linux-6.12.67/
patch -p1 < ../simple_module/patches/0001-In-tree-module-sample-v6.12.67.patch
make tinyconfig
./scripts/config -e 64BIT -e PCI -e PCI_HOST_GENERIC -e TTY -e BINFMT_ELF -e DEVTMPFS -e DEVTMPFS_MOUNT -e PROC_FS -e SYSFS -e TMPFS -e PRINTK -e EARLY_PRINTK -e PRINTK_TIME -e ACPI -e EFI -e EFI_STUB -e FB -e FB_EFI -e FRAMEBUFFER_CONSOLE -e CMDLINE_BOOL --set-str CMDLINE "rootwait" -e SIMPLE_MODULE -e USB -e USB_SUPPORT -e USB_XHCI_HCD -e USB_PCI -e USB_XHCI_PCI
make olddefconfig
make -j$(nproc)
qemu-system-x86_64 -kernel arch/x86/boot/bzImage
Также патч можно применть репозиторию c исходным кодом ядра при помощи команды git am.
Мы собрали и запустили ядро с пользовательским приложением внутри. Как и говорил вначале, в статье не рассказывается, как создать Тетрис в ядре, но рассматриваются основные аспекты его Тетриса, кроме реализации самой логики игры.
Приведенных сведений должно хватить, чтобы вы реализовали такой Тетрис сами.
Вы можете посмотреть исходный код Тетриса в ядре и запустить в эмуляторе или на реальном железе.
В этом репозитории [1] находится исходный код моего Тетриса в виде патча. Для запуска Тетриса вам нужно применить этот патч и собрать ядро с ним.
Для переработки модуля, что мы создавали в статье в Тетриса в ядре, нужно сделать малость.
найти и адаптировать исходный код Тетриса (я взял за основу следующий [12] и перевел с С++ на С, параллельно убрав код, зависящий от userspace) или написать свой;
оформить его в виде патча, чтобы его можно было удобно передать другим людям.
Расскажу еще немного о том, с чем столкнулся при написании Тетриса.
Когда вы пишете консольное приложение на языке С, в коде вы используете стандартную библиотеку C или другие библиотеки пространства пользователя. Но когда вся логика находится в ядре, этой возможности нет. Поэтому нужно или искать аналоги нужных функций в ядре или писать свои функции, предоставляющие необходимый функционал.
Для Тетриса в ядре таких функций немного:
вывод символа на экран;
генерация случайного числа;
обработка кода нажатой клавиши;
запуск кода в отдельном потоке.
Статья оказалась длинной и насыщенной, хотелось рассказать больше, но все в статью не уместишь. Но можно продолжить обсуждение в комментариях.
В статье на примере было доказано, что ядро Linux может работать как пользовательское приложение. Целесообразность подобной реализации сомнительна в продакшене, но для изучения внутренних механизмов ядра и повышения мотивации в изучении такая задача является хорошим примером.
Хотелось запускать Тетрис после того, как ядро Linux упадет с паникой, но к сожалению это не получилось, так как после паники приостанавливает свою работу планировщик и запустить поток с игровым процессом нельзя. Но можно написать патч для ядра, где не найденный файл init является не паникой, а поводом для запуска Тетриса.
Вероятно, было бы интересно добавить Тетрис в качестве патча ядра, чтобы можно скоротать время за игрой, на системах, где достаточно долго загружается Linux. Недавно на Хабре была статья, где рассказывались игры при загрузке на ретро компьютерах [14], а в нашем случае это будет более современный вариант.
Можно было бы исследовать возможность реализации подобного тетриса на системах, где нет виртуальных консолей, а только консоли, работающие на последовательном порту.
Если вы прочитали статью внимательно и параллельно осознанно выполняли описываемые действия, должны были заметить, что для написания модуля ядра владение С, git и командной строкой Linux являются обязательными. Поэтому, если вы планируете дальше разбираться с написанием модулей, стоит обновить свои знания или подучить те темы, где чувствуете пробелы.
Автор: luknix
Источник [15]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/linux/444944
Ссылки в тексте:
[1] репозитории на GitHub: https://github.com/lukovkonst/tetris-in-linux-kernel.git
[2] include/uapi/linux/input-event-codes.h: https://elixir.bootlin.com/linux/v6.12.66/source/include/uapi/linux/input-event-codes.h
[3] include/linux/keyboard.h: https://elixir.bootlin.com/linux/v6.12.66/source/include/linux/keyboard.h
[4] drivers/tty/vt/vt.c: https://elixir.bootlin.com/linux/v6.12.66/source/drivers/tty/vt/vt.c#L137
[5] include/linux/console_struct.h: https://elixir.bootlin.com/linux/v6.12.66/source/include/linux/console_struct.h#L164
[6] include/linux/console_struct.h: https://elixir.bootlin.com/linux/v6.12.66/source/include/linux/console_struct.h#L93
[7] drivers/tty/vt/vt.c: https://elixir.bootlin.com/linux/v6.12.66/source/drivers/tty/vt/vt.c#L192
[8] репозиторий с исходным кодом модуля: https://github.com/lukovkonst/simple_module.git
[9] мозге: http://www.braintools.ru
[10] стабильными ветками ядра: [https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git]: https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git
[11] https://cdn.kernel.org/pub/linux/kernel: https://cdn.kernel.org/pub/linux/kernel
[12] следующий: https://gihub.com/alakhsharma22/simple-tetris-implementation
[13] Удобный сайт с навигацией по исходному коду ядра Linux: https://elixir.bootlin.com/
[14] рассказывались игры при загрузке на ретро компьютерах: https://habr.com/ru/companies/ruvds/articles/983680/
[15] Источник: https://habr.com/ru/companies/timeweb/articles/989156/?utm_source=habrahabr&utm_medium=rss&utm_campaign=989156
Нажмите здесь для печати.