CactOS

от автора

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  ├──────────────────┤  Потолок 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

Активны

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

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


Ссылки

ссылка на оригинал статьи https://habr.com/ru/articles/1039670/