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)

Cact Kernel 1.0.0
Cact Kernel 1.0.0

Архитектура: почему гибридный монолит

Ядро гибридное в том смысле, что все подсистемы работают в одном адресном пространстве (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() (один стек, прерывания запрещены)

  1. Парсинг структуры Multiboot2 — карта памяти, тег фреймбуфера, модули GRUB

  2. cctkfs stagingpci_modblob_load() копирует GRUB-модуль cctkfs из физической памяти в .bss ядра до включения пейджинга. Это принципиально: после включения страничной трансляции физический адрес модуля, переданный GRUB, становится недоступен

  3. Инициализация фреймбуфера; если тег отсутствует или размер равен нулю — halt

  4. Проверка магического числа 0x36D76289

  5. Вызов kernel_setup_hardware() (фаза B)

  6. Создание задачи kernel_bootstrap_main — отложенная работа, требующая планировщик

  7. 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 (первая настоящая задача)

Этот поток создан специально для операций, которые нельзя выполнять на сыром загрузочном стеке:

  1. pci_driver_probe_deferred_all() — подключает PCI-драйверы, небезопасные на этапе голой инициализации

  2. mntfs_init — парсит таблицу монтирования, монтирует ext4 на NVMe/AHCI. Может заблокироваться на семафоре в ожидании IRQ от дискового контроллера. На загрузочном стеке это невозможно: прерывания всё ещё глобально запрещены (cli), планировщик не запущен, и обработчик IRQ никогда не выполнит sema_up — мёртвая блокировка гарантирована. Поэтому mntfs_init вынесен в отдельный поток ядра kernel_bootstrap_main, который стартует уже после sti и инициализации планировщика.

  3. 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, образ ядра, статические таблицы страниц) помечены как занятые навсегда.

Ключевые константы:

Символ

Значение

Смысл

MEM_START

0x00100000

Нижняя граница загрузки ядра

PCI_HOLE_START

0xE0000000

Первый адрес, не отдаваемый PMM (MMIO/PCI)

TOTAL_PAGES

~917 504

Количество 4K-фреймов

BITMAP_SIZE

~112 КиБ

Размер битовой карты

RESERVED_END

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/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

Активны

pipe() интегрирован в таблицу файловых дескрипторов

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-стеке

  • Портирование инструментов: компилятор, редактор


Ссылки

Комментарии (13)


  1. MasterMentor
    26.05.2026 14:15

    Двумя руками поддерживаю: статья/карма/подписка +/+/+.

    Дружеский совет: уходите с rust в POD на С++98 пока не поздно. Потом - не выберетесь. Если чо - могу дровишек подбросить. (Есть скромное мнение и по GUI и по многому из того, что у вас запланировано.) :)

    Тем более:

    C 66.0%

    Rust 30.5%

    Assembly 1.1%


    1. brumbrum
      26.05.2026 14:15

      C++98?! Совет точно дружеский?


      1. MasterMentor
        26.05.2026 14:15

        Более чем. С учётом того, кто организовал травлю С/С++ и как "продвигает" Питушманыч с Растом. Спойлер: CISA, АНБ (NSA), ФБР (FBI).

        Глубоко не убеждён ни в продвигаемых этими организациями "инициативах", ни в методах их "продвижения", ни в их "гарантиях" и бережной заботе о "конечном потребителе".

        Хронология "продвижения" здесь: https://habr.com/ru/articles/1029848/comments/#comment_30028238

        ( Warning! "Нейрослоп", эпилептикам и гражданам со слабой психикой ссылку не открывать. )

        PS Вообще никого не удивляет, что "вдруг" начавшийся в СМИ психоз в отношении С/С++ граничит с психозом 2019-го вокруг COVID? :)


    1. DanielKross
      26.05.2026 14:15

      А почему? Мне кажется это из серии вредных советов 8) Наверное в C++98 все более предсказуемо, потому что есть только указатели и железо, но в расте компилятор ловит состояние гонки и ошибки памяти за вас. Искать баги неделями, ну такое себе, имхо, лучше обнаруживать на этапе компиляции, все, что можно, и то, это не гарантия. Я только начал изучать раст, для себя, с++ изучал только в институте, лет 20 назад. Насколько я понимаю, способов выстрелить себе в ногу в расте на порядок меньше, для того он и создан. Думаю не стоит бояться нового, тем более если у нового есть хорошие плюшки, хотя конечно, инструмент выбирается под задачи. Но насколько я помню раст уже в ядре линукса присутствует и его доля будет увеличиваться.


    1. QwaYer Автор
      26.05.2026 14:15

      Спасибо за отклик! Проекты вроде CactOS — это отличная площадка для экспериментов, поэтому и получился такой гибрид Си и Rust (последний здорово спасает от глупых багов памяти).

      Предложение по GUI очень актуально: я как раз начал писать композитор для графической подсистемы. С удовольствием посмотрю на ваши исходники и идеи, обмен опытом сейчас будет супер полезен!


  1. Dhwtj
    26.05.2026 14:15

    Красивое

    Технологические решения хорошо расписаны, выглядят как обоснованные

    Я так понял, ядро Rust, многозадачность. Сетевая часть и файловые системы взято что-то готовое на Си. Разумно...

    Впрочем, я не настоящий сварщик©


    1. QwaYer Автор
      26.05.2026 14:15

      Спасибо! Проект действительно собирался как конструктор из лучших доступных элементов, чтобы не изобретать велосипеды там, где это не нужно.

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


  1. 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- для обработной совместимости.


    1. QwaYer Автор
      26.05.2026 14:15

      Да, абсолютно согласен насчет PAT и MSR 0x277. Использование MTRR здесь — это осознанный временный костыль для быстрого старта прототипа. Мне нужно было гарантированно завести burst mode на реальном железе — стареньком ноутбуке с процессором AMD R425, на котором я все тестировал. Вообще, чтобы быстрее выкатить рабочую версию ядра, я использовал много легаси-решений. На материнской плате до сих пор висят старые контроллеры по типу PIT, PIC/IRQ и древняя конфигурация PCI, которые я тоже планирую переписать в рамках перехода на современную архитектуру APIC/MSI-X. Просто эти задачи пока сдвинуты на второй план и выполнять их сейчас нецелесообразно.


      1. rastoltsev
        26.05.2026 14:15

        Что вы несете? MSR слоты для caching mode'а появились еще в 3 пентиуме в 1999 году, а ваш ноут явно не времен 90'ых, вы это оправдываете как "временный костыль, гарантированно завести на моем стареньком ноуте", какие боже распаенные PIC, PIT контроллеры в ноуте 2010 года... Они были во временах 8086-80386 позже внедрены в кристал. APIC/MSIX на Легаси? Что?


        1. QwaYer Автор
          26.05.2026 14:15

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


          1. rastoltsev
            26.05.2026 14:15

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


            1. QwaYer Автор
              26.05.2026 14:15

              Выбор делался на старте проекта. С тем, что PAT архитектурно лучше, я согласился еще два комментария назад. Развивать этот спор по кругу не вижу смысла. Похоже, вы читаете комментарии избирательно и отвечаете только на те части, что вам выгодно.