CactOS: гибридное ядро на C и Rust с нуля. Архитектура, подсистемы, загрузка за 4 секунды
Дисклеймер: Эта статья — не руководство по написанию ОС и не туториал. Это срез архитектуры работающего ядра, которое прошло путь от вечных Page Faults и Segmentation Faults (в ring 3) до системы с 95 системными вызовами, сетевым стеком, COW и MLFQ-планировщиком. Все исходники открыты под GPLv3.
Введение
CactOS — гибридное монолитное ядро для архитектуры i686 (32-битный x86, protected mode). Проект стартовал как исследование низкоуровневого программирования и за 5-6 месяцев превратился в полноценную операционную систему, способную загружаться на реальном железе за менее чем 4 секунды — быстрее, чем BIOS проходит POST на некоторых машинах.
Ключевые цифры:
95 системных вызовов
73 — предыдущая стабильная версия, текущая — 95 (так же стабильная)
2 языка: C + Assembly (низкоуровневый код, драйверы) + Rust (менеджер памяти, планировщик, синхронизация, TCP/IP-стек)
4 уровня MLFQ-планировщика
32 одновременные точки монтирования VFS
~40 КиБ in-tree реализация ext4 (чтение/запись)
~32 КиБ xHCI-стек (USB 3.x)

Архитектура: почему гибридный монолит
Ядро гибридное в том смысле, что все подсистемы работают в одном адресном пространстве (ring 0). Однако критические компоненты вынесены в изолированные Rust-крейты с жёсткими границами FFI:
cact_mm — PMM, VMM, куча, slab-аллокатор, mmap, COW, swap, разделяемая память
sched — MLFQ-планировщик, очереди сна, таймеры
sync — спинлоки, IRQ-безопасные спинлоки, мьютексы, семафоры
cact_net — TCP/UDP/DHCP/DNS на базе smoltcp
Это даёт дисциплину внутри монолитного ядра(хотя и не обсолютную): каждый крейт компилируется отдельно, тестируется изолированно и не имеет доступа к чужим внутренностям, кроме публичного API. Такой подход упрощает рефакторинг и минимизирует гонки данных.
Загрузка: три фазы за 4 секунды
Процесс загрузки разбит на три фазы, каждая из которых решает строго ограниченный круг задач.
Фаза A — init() (один стек, прерывания запрещены)
Парсинг структуры Multiboot2 — карта памяти, тег фреймбуфера, модули GRUB
cctkfs staging —
pci_modblob_load()копирует GRUB-модуль cctkfs из физической памяти в.bssядра до включения пейджинга. Это принципиально: после включения страничной трансляции физический адрес модуля, переданный GRUB, становится недоступенИнициализация фреймбуфера; если тег отсутствует или размер равен нулю — halt
Проверка магического числа
0x36D76289Вызов
kernel_setup_hardware()(фаза B)Создание задачи
kernel_bootstrap_main— отложенная работа, требующая планировщикsti— загрузочный поток становится idle-задачей (HLT-цикл), таймер запускает вытеснение
Фаза B — kernel_setup_hardware() (порядок важен)
# |
Подсистема |
Почему именно здесь |
|---|---|---|
1 |
GDT → PMM → VMM → kmalloc → пейджинг |
База всего |
2 |
Slab-аллокатор + page fault handler |
COW, demand zero, swap-маркеры |
3 |
PIC + IDT + COM1 serial |
Часть kprint/klog дублируется на хост |
4 |
Фреймбуфер + MTRR write-combining |
Опциональный shadow buffer с batched blit |
5 |
PS/2 клавиатура и мышь |
Предупреждения при 0xFF на порту 0x64 |
6 |
PIT 100 Гц |
Таймер до сканирования PCI (нужен GDD для таймаутов) |
7 |
blkdev_init → PCI scan → usb_init (xHCI) |
Блочные устройства до PCI, чтобы AHCI/NVMe могли зарегистрироваться |
8 |
Page cache + swap |
Swap-раздел опционален, ошибка не фатальна |
9 |
vfs_init + net_init |
knetd-поток на семафоре, net_poll / stack_poll |
10 |
task_init + init_scheduler |
MLFQ на Rust |
Фаза C — kernel_bootstrap_main (первая настоящая задача)
Этот поток создан специально для операций, которые нельзя выполнять на сыром загрузочном стеке:
pci_driver_probe_deferred_all()— подключает PCI-драйверы, небезопасные на этапе голой инициализацииmntfs_init— парсит таблицу монтирования, монтирует ext4 на NVMe/AHCI. Может заблокироваться на семафоре в ожидании IRQ от дискового контроллера. На загрузочном стеке это невозможно: прерывания всё ещё глобально запрещены (cli), планировщик не запущен, и обработчик IRQ никогда не выполнитsema_up— мёртвая блокировка гарантирована. Поэтомуmntfs_initвынесен в отдельный поток ядраkernel_bootstrap_main, который стартует уже послеstiи инициализации планировщика.create_elf_task("bin/init")— первый userspace-процесс, загружаемый через binfs (ext4/bin+ cctkfs-оверлей)
Результат на последовательном порту / фреймбуфере:
text
Cact Kernel 1.0.0 -------------------------- [VER] commit=<Hash Commit> built=<Built data> Kernel is ready. Launching init...
Менеджер памяти: что под капотом
PMM адресует все 4 ГиБ физического адресного пространства под PCI-дырой как индексируемые фреймы. Фреймы внутри младших 32 МиБ (BIOS, образ ядра, статические таблицы страниц) помечены как занятые навсегда.
Ключевые константы:
Символ |
Значение |
Смысл |
|---|---|---|
|
0x00100000 |
Нижняя граница загрузки ядра |
|
0xE0000000 |
Первый адрес, не отдаваемый PMM (MMIO/PCI) |
|
~917 504 |
Количество 4K-фреймов |
|
~112 КиБ |
Размер битовой карты |
|
0x02000000 (32 МиБ) |
Нижняя память никогда не отдаётся ни ядру, ни пользователю |
Пользовательское виртуальное пространство (упрощённо):
text
0xC0000000 ┌──────────────────┐ Только ядро (ring 0) 0xBF000000 ├──────────────────┤ Дно пользовательского стека │ user stack ↓ │ 0xBEFFF000 ├──────────────────┤ sigreturn trampoline (int 0x80) 0xB0000000 ├──────────────────┤ Потолок SHM 0xA0000000 ├──────────────────┤ База SHM 0x80000000 ├──────────────────┤ Потолок пользовательской кучи 0x40000000 ├──────────────────┤ mmap + brk (до 256 регионов) 0x08048000 ├──────────────────┤ Типичная база ELF PT_LOAD 0x00000000 └──────────────────┘ NULL/guard
Флаги страниц: PAGE_PRESENT, PAGE_RW, PAGE_USER, PAGE_COW (0x200), PAGE_DEMAND (0x400 — demand-filled / zero-on-first-touch), PAGE_ZERO (0x800 — заполнение нулями по требованию), PAGE_SWAPPED (0x008 при PRESENT=0 — страница выгружена в swap), PDE_PRIVATE (0x200 в PDE — «эта таблица страниц своя у процесса», тег для fork/COW).
Планировщик: 4-уровневая MLFQ
Планировщик полностью написан на Rust. 4 уровня очередей:
Уровень |
Название |
Квант |
Назначение |
|---|---|---|---|
0 |
Real-time |
5 тиков |
Наивысший приоритет |
1 |
Interactive |
1 тик |
Цель для boost (latency-sensitive) |
2 |
Normal |
2 тика |
По умолчанию для новых задач |
3 |
Background |
4 тика |
CPU-bound batch |
Правила:
Anti-starvation boost каждые 50 тиков: задачи с Normal и ниже поднимаются к Interactive
Voluntary block bonus: если задача блокируется дольше половины кванта, она может получить повышение при пробуждении
Sleep queue + alarms / setitimer обрабатываются на каждом тике
SCHEDULE_IN_PROGRESS — защита от вложенного входа в планировщик
Состояния задач: TASK_READY, TASK_RUNNING, TASK_SLEEPING, TASK_ZOMBIE, TASK_WAITING.
Сетевой стек: smoltcp + C-обвязка
Сетевой стек реализован как гибрид C и Rust. Легаси-путь на C владеет Ethernet-демультиплексированием, ARP, частями IPv4/ICMP и временем жизни skb. TCP/UDP-сокеты для системных вызовов работают через smoltcp внутри крейта cact_net.
Ключевые компоненты:
stack_poll()— драйвер интерфейсаDHCPv4 обновляет runtime IPv4 + IP DNS-сервера
SYS_DNS_RESOLVE— блокирующий A-запрос через UDP/53SYS_PING_ECHO— ICMP echo requestknetd — выделенный поток ядра: спит на семафоре, просыпается по RX прерыванию NIC, вызывает
net_poll → stack_poll()
Ограничения: нет IPv6, нет TLS внутри ядра, NIC по умолчанию — virtio-net в QEMU, флаги send/recv могут игнорироваться в libc, DNS-резольвер — только A-записи.
Логические TCP-состояния (C-метаданные / VFS-представление; ingress TCP обрабатывается smoltcp):
text
CLOSED → LISTEN → SYN_SENT → SYN_RECEIVED → ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT → CLOSE_WAIT → LAST_ACK → CLOSED
Драйверы и файловые системы
Драйверы (in-tree)
Область |
Компоненты |
|---|---|
Блочные устройства |
AHCI, NVMe, blkdev, page cache |
USB |
xHCI, HID, hub |
Ввод |
PS/2 клавиатура и мышь |
Видео |
Linear FB 32 bpp, шрифт 8×8 с масштабированием ×2, MTRR WC + shadow |
PCI |
Сканирование конфигурации, таблица драйверов, GDD, загрузчик ET_REL-модулей |
Сеть |
virtio-net (по умолчанию в QEMU) |
Внешние PCI-драйверы (например, Marvell Yukon) живут в отдельных *-for-Cact репозиториях, собираются как .cctk-модули и упаковываются в cctkfs.img.
Файловые системы
ФС |
Статус |
Примечания |
|---|---|---|
ext4 |
Активна |
Компактная in-tree реализация: чтение/запись, inode-операции (~40 КиБ) |
VFS |
Активна |
До 32 одновременных точек монтирования, symlink-пул с защитой от ELOOP, биты rwx |
devfs, procfs, mntfs, etcfs, tmpfs |
Активны |
Полноценные реализации |
binfs, sbinfs, libfs, varfs |
Активны |
cctkfs-оверлей поверх ext4 |
pipes |
Активны |
|
btrfs, exFAT, ramfs |
Заглушки |
Placeholder-заголовки |
Системные вызовы: 95 и counting
Авторитетный список — Cact/kernel/core/syscalls/syscalls.h, который должен побайтово совпадать с syscall.h в CactLib. Многие syscall'ы принимают struct syscall_frame* (полный снимок регистров) в диспетчере.
Группы:
Процессы: fork, exec, exit, waitpid, sleep, getpid/getppid
Сигналы: kill, signal, sigaction, sigprocmask, sigreturn, sigpending, sigsuspend, alarm, setitimer
Память: brk, mmap, munmap, mprotect, shmget/shmat/shmdt/shmctl
Сеть: socket, bind, connect, listen, accept, send/recv, sendto/recvfrom + PING_ECHO, NETCFG_SET, DNS_RESOLVE
Файлы: open/read/write/close, stat/fstat, getdents, rename, mkdir, rmdir, symlink, readlink, link/unlink
Система: mount, umount, reboot, uname, module_load/module_unload
Kernel panic и обработка ошибок
Ring 0 — полный дамп регистров, сообщение, cli; hlt:
text
=== KERNEL PANIC === Exception: 14 (#PF) Error code: 0x00000003 EIP: 0xC010A3F2 CS: 0x00000008 EAX: 0x00000000 EBX: 0xDEADBEEF ECX: 0x00000001 EDX: 0x00000000 ESP: 0xC01FF9E0 EBP: 0xC01FFA10 System halted.
Ring 3 — исключения CPU преобразуются в Unix-подобные сигналы для задачи-нарушителя:
Исключение |
Сигнал |
Типичная причина |
|---|---|---|
#DE (vector 0) |
SIGFPE |
Целочисленное деление на ноль |
#MF (vector 16) |
SIGFPE |
Ошибка x87 FPU |
#GP (vector 13) |
SIGSEGV |
General protection fault |
Остальные |
SIGKILL |
Неподдерживаемый путь обработки |
Планы: v2.0.0 и v3.0.0
v2.0.0 — Графический интерфейс, Модернизация
Уход от устаревших интерфейсов общения с "железом"
Переход от текстового фреймбуфера к графическому режиму
Оконный менеджер с поддержкой мыши
Собственный набор виджетов
v3.0.0 — Максимальная отказоустойчивость
Каждая подсистема (менеджер памяти, планировщик, VFS, сетевой стек, драйверы) — изолированный модуль
Веб-сервер на собственном TCP/IP-стеке
Портирование инструментов: компилятор, редактор
Ссылки
Лицензия: GNU General Public License v3.0
Комментарии (13)

Dhwtj
26.05.2026 14:15Красивое
Технологические решения хорошо расписаны, выглядят как обоснованные
Я так понял, ядро Rust, многозадачность. Сетевая часть и файловые системы взято что-то готовое на Си. Разумно...
Впрочем, я не настоящий сварщик©

QwaYer Автор
26.05.2026 14:15Спасибо! Проект действительно собирался как конструктор из лучших доступных элементов, чтобы не изобретать велосипеды там, где это не нужно.
Сейчас этот гибрид Си и Rust проходит проверку на прочность — я как раз начал писать графический композитор для GUI и портировал TCC. Надеюсь, в следующей статье архитектура покажет себя так же обоснованно!

rastoltsev
26.05.2026 14:15Непонятно только зачем вы используете устаревшие MTRR для WC burst mode'а и так ограниченные до 8 регистров в ядре со страничной адресацией, это времена когда на пейдж атрибуты выделяли 2 бита а не 3 без extended'а, caching mode можно настраивать через слоты в MSR регистре IA32_CR_PAT (0x277), и так и топологически вернее: MTRR для физ адресов / физ адресов но поверх виртуальных, а MSR сугубо для виртуальных, топология virt on phy.
Вспоминается еще UC- для обработной совместимости.

QwaYer Автор
26.05.2026 14:15Да, абсолютно согласен насчет PAT и MSR
0x277. Использование MTRR здесь — это осознанный временный костыль для быстрого старта прототипа. Мне нужно было гарантированно завести burst mode на реальном железе — стареньком ноутбуке с процессором AMD R425, на котором я все тестировал. Вообще, чтобы быстрее выкатить рабочую версию ядра, я использовал много легаси-решений. На материнской плате до сих пор висят старые контроллеры по типу PIT, PIC/IRQ и древняя конфигурация PCI, которые я тоже планирую переписать в рамках перехода на современную архитектуру APIC/MSI-X. Просто эти задачи пока сдвинуты на второй план и выполнять их сейчас нецелесообразно.
rastoltsev
26.05.2026 14:15Что вы несете? MSR слоты для caching mode'а появились еще в 3 пентиуме в 1999 году, а ваш ноут явно не времен 90'ых, вы это оправдываете как "временный костыль, гарантированно завести на моем стареньком ноуте", какие боже распаенные PIC, PIT контроллеры в ноуте 2010 года... Они были во временах 8086-80386 позже внедрены в кристал. APIC/MSIX на Легаси? Что?

QwaYer Автор
26.05.2026 14:15Если говорить прямо, выбор MTRR, а также использование интегрированных в южный мост эмуляций PIT и PIC(да я не верно выразился про то что они впаяны, когда их давно нет) — это результат того, что база ядра собиралась по классическим OSDev-материалам на этапе раннего прототипирования. Система успешно завелась на тестовом железе в такой конфигурации, и приоритеты сместились на другие задачи. Переход на PCIe, ACPI/MCFG, APIC и табличный PAT — это очевидный технический долг, который зафиксирован в планах по развитию ядра, но на данном этапе эти задачи были отложены как нецелесообразные для релиза графического стека. А в случае ноутбука это все обратно совместимо. Именно по этому я оставил так как есть, можно сказать в "долгий ящик". На самом деле, проект разросся в настоящую махину, и одному тащить всё железо и графику дальше тяжело. Раз вы настолько глубоко знаете матчасть и спецификации x86 — прошу помощи с этими задачами.(если конечно не трудно :) )

rastoltsev
26.05.2026 14:15На современной MSR топологии PAT реализовать гораздо легче, и занимает почти в 2 раза меньше строк кода, чем на устаревшой MTRR топологии, выбор изначально нерационален и бессмысленен, вы не во временах 95 винды ядро пишете

QwaYer Автор
26.05.2026 14:15Выбор делался на старте проекта. С тем, что PAT архитектурно лучше, я согласился еще два комментария назад. Развивать этот спор по кругу не вижу смысла. Похоже, вы читаете комментарии избирательно и отвечаете только на те части, что вам выгодно.
MasterMentor
Двумя руками поддерживаю: статья/карма/подписка +/+/+.
Дружеский совет: уходите с rust в POD на С++98 пока не поздно. Потом - не выберетесь. Если чо - могу дровишек подбросить. (Есть скромное мнение и по GUI и по многому из того, что у вас запланировано.) :)
Тем более:
brumbrum
C++98?! Совет точно дружеский?
MasterMentor
Более чем. С учётом того, кто организовал травлю С/С++ и как "продвигает" Питушманыч с Растом. Спойлер: CISA, АНБ (NSA), ФБР (FBI).
Глубоко не убеждён ни в продвигаемых этими организациями "инициативах", ни в методах их "продвижения", ни в их "гарантиях" и бережной заботе о "конечном потребителе".
Хронология "продвижения" здесь: https://habr.com/ru/articles/1029848/comments/#comment_30028238
( Warning! "Нейрослоп", эпилептикам и гражданам со слабой психикой ссылку не открывать. )
PS Вообще никого не удивляет, что "вдруг" начавшийся в СМИ психоз в отношении С/С++ граничит с психозом 2019-го вокруг COVID? :)
DanielKross
А почему? Мне кажется это из серии вредных советов 8) Наверное в C++98 все более предсказуемо, потому что есть только указатели и железо, но в расте компилятор ловит состояние гонки и ошибки памяти за вас. Искать баги неделями, ну такое себе, имхо, лучше обнаруживать на этапе компиляции, все, что можно, и то, это не гарантия. Я только начал изучать раст, для себя, с++ изучал только в институте, лет 20 назад. Насколько я понимаю, способов выстрелить себе в ногу в расте на порядок меньше, для того он и создан. Думаю не стоит бояться нового, тем более если у нового есть хорошие плюшки, хотя конечно, инструмент выбирается под задачи. Но насколько я помню раст уже в ядре линукса присутствует и его доля будет увеличиваться.
QwaYer Автор
Спасибо за отклик! Проекты вроде CactOS — это отличная площадка для экспериментов, поэтому и получился такой гибрид Си и Rust (последний здорово спасает от глупых багов памяти).
Предложение по GUI очень актуально: я как раз начал писать композитор для графической подсистемы. С удовольствием посмотрю на ваши исходники и идеи, обмен опытом сейчас будет супер полезен!