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 ├──────────────────┤ Потолок SHM0xA0000000 ├──────────────────┤ База SHM0x80000000 ├──────────────────┤ Потолок пользовательской кучи0x40000000 ├──────────────────┤ mmap + brk (до 256 регионов)0x08048000 ├──────────────────┤ Типичная база ELF PT_LOAD0x00000000 └──────────────────┘ 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/53 -
SYS_PING_ECHO— ICMP echo request -
knetd — выделенный поток ядра: спит на семафоре, просыпается по 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: 0x00000003EIP: 0xC010A3F2 CS: 0x00000008EAX: 0x00000000 EBX: 0xDEADBEEF ECX: 0x00000001 EDX: 0x00000000ESP: 0xC01FF9E0 EBP: 0xC01FFA10System 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
ссылка на оригинал статьи https://habr.com/ru/articles/1039670/