Вырезаем из Linux микроядро. В прямом смысле.
MiLin — это эксперимент: мы берём настоящее ядро Linux и методично режем его, превращая монолит в микроядро. Драйверы и файловые системы уезжают в userspace, в ядре остаются только планировщик, память и IPC.
English version: README_EN.md
Linux — великий монолит. Вся система живёт в режиме ядра: каждый драйвер, каждая файловая система, каждый протокол. Микроядро — противоположная идея: в привилегированном режиме остаётся минимум, а всё остальное работает обычными процессами в userspace.
Мы решили не спорить на форумах, а попробовать самим: что останется, если из Linux вырезать всё, без чего ядро ещё может жить? Где проходит эта граница? Насколько она дальше, чем принято думать?
Отличительная черта MiLin: после каждого шага резни система продолжает грузиться. Мы не пишем новое ядро — мы оперируем живым.
flowchart TB
subgraph userspace["Userspace"]
INIT["init (супервизор серверов)"]
DRV["драйверы — UIO-серверы"]
FS["файловые системы — FUSE-серверы"]
end
subgraph kernel["Ядро MiLin"]
IPC[IPC]
SCHED[Планировщик]
MM[Память]
SYS[syscall ABI]
end
INIT --> SYS
DRV --> IPC
FS --> IPC
SYS --> SCHED
SYS --> MM
| Этап | Название | Суть |
|---|---|---|
| M1 | Базовый бут | настоящее ядро + минимальный конфиг + QEMU ✅ |
| M2 | Конфиг-диета | всё лишнее выключено, bzImage −23% ✅ |
| M3 | Резня в исходниках | физическое удаление подсистем: дерево −92% ✅ |
| M4 | Userspace-изация | драйверы → UIO-серверы, ФС → FUSE-серверы ✅ |
| M5 | Микроядерный IPC | mbus-реестр + lane-хэндлы + bragi-сообщения ✅ |
| M5.1 | Асинхронные очереди | HelQueue в разделяемой памяти + futex + creds ✅ |
| M5.2 | Манифесты + живой FUSE | сервисные манифесты вместо таблицы, FUSE_WRITE/READ + readdir ✅ |
| M6 | Отчёт | честные метрики «до/после» ✅ |
| M7 | Capability + выживание | права на lane'ах + автоперезапуск, revoke, каскад при смерти шины ✅ |
| M8 | Большие сообщения + glibc | blob-путь в helq + динамический glibc-клиент ✅ |
| M8.1 | Bulk IPC без потолка | сообщения до 128КБ цепочкой blob-слотов; фиксы flow-control, wake-гонки и геометрии региона ✅ |
| M8.2 | glibc-стек целиком | helq/mmap/futex/fork/stdio из libc (общий helq.h с шимой -DMILIN_GLIBC) ✅ |
| M9 | Kdriver-домен | драйвер «между ядром и userspace», размещение user/kernel ✅ |
Подробности и критерии готовности — в docs/roadmap.md.
| метрика | M1 | M2 | M3 | M4 | M5 | M5.1 | Δ M1→M5.1 |
|---|---|---|---|---|---|---|---|
| bzImage | 1.00 МиБ | 0.77 МиБ | 0.77 МиБ | 0.94 МиБ | 1.33 МиБ | 1.34 МиБ | +34% |
| текст-код ядра | 3.89 МиБ | 3.40 МиБ | ~3.4 МиБ | ~3.7 МиБ | ~4.2 МиБ | ~4.2 МиБ | см. M6 |
| CONFIG =y | 529 | 460 | 460 | 448 | 472 | 476 | −10% |
| дерево исходников | 1.8G | 1.8G | 139 МиБ | 154 МиБ | 164 МиБ | 164 МиБ | −90.9% |
| файлов в дереве | 94,856 | 94,856 | 12,212 | 13,054 | 13,475 | 13,475 | −85.8% |
| строк .c/.h | 37.2M | 37.2M | 3.5M | 4.0M | 4.3M | 4.3M | −88.4% |
| время бута в QEMU | 1.65 с | 1.16 с | 1.19 с | 1.30 с | 1.50 с | 1.50 с | −9% |
В M3 режется само дерево исходников (bzImage почти не меняется — M2 уже
выключил этот код из сборки). В M4 bzImage отрастает на +21.6% — это не
обратная дietetика, а цена контракта серверов: ядро возвращает не драйверы
и не файловые системы, а интерфейсы, через которые userspace ими управляет
(MULTIUSER, PCI-шина, UIO, FUSE, devtmpfs, sysfs). В M5 +41% — цена
транспорта IPC: AF_UNIX тянет за собой net/core, netlink, sched, loopback
и kernel/bpf (select-контракт v7.2). Протокольные семейства (inet и
прочее) остаются вырезанными — net/Kconfig у MiLin авторский, минимальный.
M5.1 добавляет только futex-контракт: +8 КБ (+0.6%). Метрики:
docs/metrics/m4.json, docs/metrics/m5.json, docs/metrics/m51.json.
| репозиторий | что это |
|---|---|
| mel0k1/MiLin | проект: инструменты, конфиг-конституция, CI, docs, скрипт резни |
| mel0k1/linux (форк torvalds/linux) | обрезанное дерево, ветка milin — база v7.2 минус подсистемы |
Форк сохраняет настоящую историю Linux: любой вырезанный кусок можно
посмотреть/вернуть/сравнить с апстримом. Коммит резни за коммитом:
arch →
deadtop →
subsys →
drivers →
fs/kernel/tools.
git clone https://github.com/mel0k1/MiLin.git && cd MiLin
git clone --depth 1 -b milin https://github.com/mel0k1/linux.git linux
TREE_DIR=$PWD/linux tools/build.sh # собрать build/bzImage из обрезанного дерева
tools/run-qemu.sh # бут: супервизор поднимает серверы, demo FUSE+UIOБез клонирования дерева tools/build.sh соберёт baseline из tarball
kernel.org (как в M1/M2). Зависимости: gcc make flex bison bc cpio curl qemu-system-x86. Ядро и все инструменты — x86_64.
├── tools/ # скрипты: build, carve.sh (резня), fix-dead-refs.py,
│ # mkinitramfs, run-qemu, size-report, tree-metrics
├── configs/ # milin.config — конституция конфигурации ядра
├── userspace/ # супервизор /init, серверы (mlbus, logsrv, fused,
│ # uiod), клиент, манифесты /etc/milin/*.svc, ipc.h
├── docs/ # дорожная карта, логи бута, метрики, будущий отчёт
└── .github/ # CI: сборка из обрезанного дерева + QEMU smoke-тест
После загрузки ядро не знает ни файловых систем, ни драйверов «по-настоящему»:
- супервизор
/initмонтирует devtmpfs и sysfs, привязывает PCI-устройство к userspace-драйверу (/sys/.../uio_pci_generic/new_id); uiod— userspace-драйвер: мапит BAR0 через/dev/uio0и читает регистр железа (qemu pci-testdev);fused— userspace-файловая система: монтирует/mntчерез/dev/fuseи отвечает ядру FUSE_INIT/LOOKUP/GETATTR;- клиент, созданный с
setuid(1000)(вернулся MULTIUSER), читает/mnt/READMEчерез FUSE-сервер — uid-изоляция и сервер работают вместе.
Серверы перестают быть сиротами: у них есть шина (идея mbus из Managarm, транспорт — простой IPC на AF_UNIX SOCK_SEQPACKET):
mlbus— реестр: сервер делаетREGISTER("log"), клиент —LOOKUP("log"); шина создаёт socketpair и раздаёт концы через SCM_RIGHTS — дальше общение endpoint→endpoint, шина данных не видит (аналог GetRemoteLane);- сообщения — в стиле bragi: заголовок
{proto, msg_id, err, tail_len}- inline-поля + переменный tail + hooks (хэндлы); одно сообщение = один атомарный пакет;
- клиент (uid 1000) получает lane, отправляет два WRITE и STATS;
log-сервер печатает их в UART, отвечает ack и отдаёт
served=2; - FUSE-регрессия M4 проходит: тот же клиент читает
/mnt/README; - супервизор останавливает долгоживущие серверы SIGTERM'ом, как положено микроядерному менеджеру.
Транспорт M5 честный, но медленный (два syscall'а на сообщение, ядро копирует данные). У Managarm очереди HelQueue живут в разделяемой памяти — ядро в data path не участвует. Идея пересажена на user↔user:
- пара очередей в shm-регионе (ramfs-файл → MAP_SHARED → fd по SCM_RIGHTS → unlink — без TMPFS и MEMFD_CREATE);
- чанки с progress-словом и length-prefixed элементы — потребитель читает payload прямо из общей памяти (zero copy);
- будильник — futex-слово с битами DATA/SUPPLY: атомарный OR/AND и парковка настоящим futex (аналог HelFutex);
- flow control — возврат чанков свободным списком;
- creds: SO_PASSCRED/SCM_CREDENTIALS — ядро прикладывает ucred к сообщениям, шина пересылает его серверу, лог подписывается uid=1000.
Бенчмарк в системе (QEMU TCG, rdtsc, n=256; для относительного сравнения):
RTT seqpacket 297.7K циклов → очередь+futex 181.7K (×1.64) →
поллинг+yield 161.2K (×1.85); постановка в очередь пачкой — 5.1K
циклов/сообщение (в 58 раз дешевле RTT). Цена: CONFIG_FUTEX = +8 КБ
bzImage (+0.6%). Полный разбор — docs/metrics/m51.json, лог бута —
docs/boot-m51.log.
Таблица сервисов больше не зашита в супервизор: /etc/milin/*.svc — манифесты (name/exec/type/stop/order), /init читает их getdents64'ом, сортирует по order и порождает. Служба = файл, супервизор не пересобирается; тип определяет жизнь (service — глушится SIGTERM'ом, oneshot — его статус решает судьбу бута), stop=umount — graceful через размонтирование (сервер видит EOF и выходит сам). Демо-клиент уехал из супервизора в отдельный бинарник /sbin/client.
FUSE-сервер стал полноценной ФС: OPEN/READ/WRITE (DIRECT_IO — мимо page cache), FLUSH/RELEASE, OPENDIR/READDIR (fuse_dirent с cookie-оффсетами). Клиент пишет /mnt/note.txt, читает обратно с побайтовой сверкой, перечисляет /mnt через getdents64 и проверяет, что WRITE доехал до атрибутов (stat size=69). Права решает сервер: default_permissions выключен, README на запись не открывается (EACCES при OPEN). Лог бута — docs/boot-m52.log.
RU. Восьмой этап довёл раздельные пути IPC до конца и закрыл три
реальных бага. Bulk: сообщение крупнее одного blob-слота (32КБ) теперь едет
цепочкой слотов до 128КБ — в очереди по-прежнему одна ссылка, потребитель
читает zero-copy, mlq_gather собирает цепочку в непрерывный буфер.
Исправления, найденные на этом пути: (1) pop свободной цепочки blob-слотов
терял хвост (слоты утекали на конвейерах); (2) танец битов мог потерять
пробуждение — значение для futex_wait теперь захватывается до проверки
состояния; (3) заголовок региона перекрывался с чанком №1 (116 байт при
MLQ_HDR_OFF=64) — элементы затирали ссылки blob-слотов. Плюс сервер освобождает
запрос ДО выделения ресурсов ответа — иначе конвейер запирается. Клиент
прогоняет WRITE 100КБ цепочкой и RTT 64КБ; счётчик сервера сходится до
единого сообщения (966). M8.2: gclient собран с glibc и гоняет тот же стек
очередей (общий helq.h, шима -DMILIN_GLIBC): mmap+futex RTT, bulk 100КБ,
fork+waitpid, stdio поверх FUSE.
EN. Milestone 8 finished the split-path IPC and fixed three real bugs.
Bulk: messages larger than one blob slot (32КБ) now travel as slot chains up
to 128КБ — one queue reference, zero-copy consumer, mlq_gather reassembles
into a flat buffer. Fixes found on the way: (1) popping the blob free-list
dropped the chain tail (slot leaks under pipelining); (2) the futex bit
dance could lose a wakeup — the futex_wait value is now captured before the
state check; (3) the region header overlapped chunk #1 (116 bytes vs
MLQ_HDR_OFF=64) — elements clobbered blob-slot links. Also the server now
releases the request BEFORE allocating response resources, otherwise the
pipeline deadlocks. The client runs a 100КБ chained WRITE and a 64КБ RTT;
the server counter matches to the last message (966). M8.2: gclient builds
with glibc and drives the same queue stack (shared helq.h, -DMILIN_GLIBC
shim): mmap+futex RTT, 100КБ bulk, fork+waitpid, stdio over FUSE.
GPL-2.0 — как и оригинальный Linux, из которого мы всё это вырезаем.