Ядро из репозитория дистрибутива должно запуститься где угодно: на старом ноутбуке, на сервере с RAID-массивом, на одноплатнике с экзотическим сенсорным экраном. Ради такой универсальности в него упакованы тысячи драйверов, из которых конкретный компьютер использует десятую долю. Утилита make localmodconfig решает задачу радикально: она фиксирует, какие модули реально загружены в работающей системе, и выключает в конфигурации всё остальное. Дальше по шагам разбирается весь цикл: установка компилятора и зависимостей, загрузка исходников, урезание конфигурации, сборка, установка и честная оценка того, что этот труд даёт на практике.

Тысячи драйверов в дистрибутивном ядре при сотне реально нужных

Мейнтейнеры Debian, Fedora и Arch не знают, что стоит на столе у пользователя, поэтому включают в сборку практически весь драйверный стек. В каталоге /lib/modules дистрибутивного ядра счёт файлов .ko идёт на тысячи, а суммарный размер приближается к половине гигабайта. Команда lsmod на обычном рабочем столе при этом выдаёт около сотни строк. Разница между этими цифрами и есть балласт: модули для Wi-Fi адаптеров, которых нет, для принтеров, которые никогда не подключались, для файловых систем, куда никто не заходил.

Каждый лишний модуль бьёт по системе несколько раз. При загрузке для него запускается modprobe, то есть отдельный процесс с поиском файла, проверкой подписи, распаковкой и записью в память. Когда таких шагов сотни, ранний этап старта растягивается на секунды и десятки секунд. Плюс место на диске в /lib/modules, плюс код, который загружен в ядро и потенциально может содержать уязвимость, плюс долгая пересборка из исходников, если однажды понадобится своё ядро.

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

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

lsmod | wc -l
du -sh /lib/modules/$(uname -r)

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

Механика localmodconfig и скрипт streamline_config.pl за ним

За сокращением конфигурации стоит скрипт scripts/kconfig/streamline_config.pl. Автор Стивен Ростедт написал его в 2005 году для собственной машины с Debian, позже скрипт вошёл в дерево ядра, и теперь его вызывают цели make localmodconfig и make localyesconfig. Логика простая и оттого надёжная. Скрипт запускает lsmod, по файлам Makefile и Kbuild сопоставляет каждое имя модуля с опцией CONFIG_*, раскрывает граф зависимостей, чтобы вместе с нужными модулями не потерять то, от чего они зависят, и записывает урезанный .config. Старая конфигурация на всякий случай переезжает в .config.old. Если хочется проследить за решениями скрипта, переменная LOCALMODCONFIG_DEBUG включает подробный отладочный вывод.

Базовую конфигурацию скрипт ищет сам, перебирая несколько мест по порядку: файл .config в каталоге исходников, затем /proc/config.gz, затем /boot/config-$(uname -r). Если ничего не нашлось, конфигурация достаётся прямо из образа vmlinuz утилитой extract-ikconfig, когда та была встроена при сборке. В выводе make help цель описана коротко: обновить текущую конфигурацию, выключив незагруженные модули.

Сестринская цель localyesconfig работает жёстче: всё, что собиралось модулем, переводится в =y, то есть драйверы вшиваются прямо в образ ядра. Об этом варианте речь пойдёт ближе к концу, а основной путь здесь модульный, потому что он гибче.

Установка инструментов сборки и загрузка исходников ядра

Для Debian и Ubuntu набор зависимостей выглядит так, команда выполняется в терминале:

sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev bc dwarves

Пакет dwarves приносит утилиту pahole, без которой ядро с поддержкой BTF не собирается: процесс обрывается на генерации BTF с сообщением о недоступном pahole. В Fedora тот же набор ставится одной строкой:

sudo dnf install make gcc flex bison bc elfutils-libelf-devel openssl-devel ncurses-devel dwarves

Требования к инструментам у ядра скромные: достаточно GCC не старее 5.1 или Clang не старее 11, точный минимум перечислен в файле Documentation/process/changes.rst внутри дерева исходников. Проверить свои версии можно так:

gcc --version | head -1
make --version | head -1

Сжатый архив исходников занимает полторы сотни мегабайт, распакованное дерево отнимает около 1,5 ГБ, значит на диске заранее нужно освободить хотя бы пару гигабайт. Ниже используется версия 6.12.116 из долгосрочной ветки 6.12: такие ветки сопровождают годами, и для домашней машины это разумный выбор. Свежие стабильные номера тоже подходят, просто их придётся менять чаще. Загрузка и распаковка выполняются в домашнем каталоге:

mkdir -p ~/kbuild && cd ~/kbuild
curl -LO https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.12.116.tar.xz
tar xf linux-6.12.116.tar.xz
cd linux-6.12.116

Базовая конфигурация берётся из работающего ядра, команда выполняется уже в каталоге исходников:

cp /boot/config-$(uname -r) .config

Если каталог /boot пуст, конфигурация часто доступна через виртуальный файл, который существует при включённой опции CONFIG_IKCONFIG_PROC:

zcat /proc/config.gz > .config

Сокращение конфигурации и защита драйверов от случайного отсечения

Подготовка закончена, время главного шага. В каталоге исходников выполняются две команды:

make olddefconfig
make localmodconfig

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

Снимок lsmod честен только в момент запуска. Если Bluetooth-адаптер выключен кнопкой, Wi-Fi донгл лежит в ящике, а NFS-каталог не смонтирован, их драйверы в списке не окажутся и будут вырезаны. Перед урезанием систему стоит подготовить:

  1. подключить по USB все периодически используемые устройства, включая Wi-Fi адаптеры и веб-камеры;
  2. включить Bluetooth и дождаться, пока служба подхватит адаптер;
  3. смонтировать сетевые каталоги NFS и CIFS, если они нужны;
  4. запустить виртуальные машины, чтобы в lsmod попали kvm и vhost_net;
  5. убедиться, что система загружена с тем ядром, конфигурация которого берётся за основу.

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

make LMC_KEEP="drivers/gpu/drm:drivers/net/wireless:fs/nfs" localmodconfig

Всё, что собирается модулями из перечисленных каталогов, останется в конфигурации независимо от снимка lsmod.

Масштаб урезания виден прямо в конфиге:

grep -c '=m' .config
grep -c '=y' .config

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

После урезания полезно пройтись по конфигурации руками:

make menuconfig

Текстовый интерфейс перемещается стрелками, выбор делается Enter и пробелом, у опции три состояния: встроено y, модулем m, выключено n. Клавиша slash открывает поиск по названиям опций и показывает, в каком меню каждая лежит, это сильно экономит время. Выход через Exit с сохранением. В разделе General setup заполняется пункт Local version, например суффиксом -lmc, чтобы собственное ядро не путалось с дистрибутивным. В разделе Enable loadable module support включается сжатие модулей ZSTD: файлы .ko.zst занимают заметно меньше места, на скорости загрузки разница почти не видна.

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

Собственно сборка занимает одну команду в каталоге исходников:

make -j$(nproc)

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

Готовое ядро переносится в систему двумя командами:

sudo make modules_install
sudo make install

modules_install раскладывает модули по каталогу /lib/modules/<версия>. При повторных экспериментах с конфигом та же версия спокойно перезаписывается новой сборкой, ручная уборка не нужна. install копирует в /boot образ vmlinuz-<версия>, таблицу символов System.map и файл конфигурации, а на большинстве дистрибутивов параллельно обновляет меню загрузчика. В Debian и Ubuntu initramfs иногда приходится генерировать вручную, точная версия берётся из вывода ls /lib/modules:

sudo update-initramfs -c -k 6.12.116-lmc
sudo update-grub

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

Проверка результата и практический выигрыш от обрезки

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

uname -r
lsmod | wc -l
du -sh /lib/modules/$(uname -r)

Первая подтверждает, что система работает на собранном ядре с заданным суффиксом. Вторая и третья показывают, во что превратился набор модулей: вместо сотен мегабайт в /lib/modules остаются десятки, а счёт загруженных модулей падает в разы по сравнению с дистрибутивным ядром. Верным признаком удачной обрезки служит состав lsmod после загрузки, почти совпадающий с тем, что фиксировался до неё.

Измерить эффект в секундах можно утилитой systemd-analyze, сначала на дистрибутивном ядре, затем после перезагрузки в новое:

systemd-analyze
systemd-analyze blame | head -20

Первая команда выдаёт общее время загрузки, вторая перечисляет самые долгие шаги старта. Арифметика простая: каждый модуль на старте означает отдельный запуск modprobe, модулей стало втрое меньше, значит и шагов втрое меньше. На системе с NVMe выигрыш скромнее, чем на диске с вращающимися пластинами, но даже на SSD старт урезанного ядра ощущается бодрее. Сюда же добавляются освободившееся место, более короткие резервные копии /boot и быстрые повторные сборки.

Существует и жёсткий вариант localyesconfig: драйверы постоянного оборудования вшиваются в образ ядра, от initramfs почти ничего не остаётся, загрузка идёт без этапа подгрузки модулей вообще. Плата за это: вырастает сам vmlinuz, а любое изменение требует полной пересборки ядра. Для рабочего стола localmodconfig остаётся золотой серединой между универсальностью и скоростью.

Сопровождение собственной конфигурации при выходе новых версий

Собственное ядро живёт вне пакетного менеджера, поэтому исправления безопасности не прилетают сами: раз в несколько недель имеет смысл проверять, не вышла ли новая версия в выбранной ветке, и пересобираться. Процедура немудрёная. Файл .config переносится в каталог новой версии, make olddefconfig подтягивает свежие параметры со значениями по умолчанию, после аппаратных изменений снова запускается make localmodconfig. Конфигурацию разумно хранить в git или хотя бы копировать в сторону: команда make mrproper стирает вместе с промежуточными файлами и .config, и восстановить её будет не из чего.

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