Операционные системы Другие ОС: macOS и XNU, Windows NT, Solaris/illumos, Plan 9, RTOS
0%

Другие ОС: macOS и XNU, Windows NT, Solaris/illumos, Plan 9, RTOS

Другие ОС: macOS и XNU, Windows NT, Solaris/illumos, Plan 9, RTOS

Если весь ваш опыт — Linux, то легко принять его решения за законы природы. fork(), /proc, epoll, «всё есть файл», «открытый файл можно удалить», «номера системных вызовов стабильны» — всё это не свойства вычислительной техники, а конкретные инженерные выборы конкретной системы. Стоит открыть соседнюю ОС — и половина этих «законов» перестаёт работать.

Практический интерес тут не академический. Вы почти наверняка пишете код, который собирается на macOS у половины команды. Вы почти наверняка однажды услышите «у нас на Windows тест падает». Вы точно встретите ZFS и DTrace, придуманные в Solaris. Вы каждый день пишете на Go, чья модель конкурентности пришла из потомка Plan 9. И если вы когда-нибудь коснётесь железа сложнее ESP32, вы упрётесь в вопрос «а почему тут не Linux, а FreeRTOS».

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

Карта: откуда что взялось

Обратите внимание на структуру: это не «Linux и его подражатели». Это пять независимых ответов на один и тот же набор вопросов, и Linux — шестой. Общая архитектурная рамка разобрана в статье Архитектуры ядер; историю раскола Unix смотрите в История ОС.


macOS и XNU: два ядра в одном

Откуда взялась эта странная конструкция

XNU (шутливо расшифровывают как «X is Not Unix») — не микроядро и не монолит, а сшивка трёх независимых кодовых баз в одном адресном пространстве:

  • Mach 3.0 из Университета Карнеги — Меллона: задачи, потоки, порты, IPC, виртуальная память;
  • слой BSD, исторически из 4.4BSD, потом подтянутый по FreeBSD 5: POSIX-процессы, VFS, сокеты, сигналы, UID/GID;
  • IOKit — драйверная подсистема на подмножестве C++, унаследованная от NeXT.

Почему так? NeXT в 1989-м хотел объектную ОС быстро, а Mach давал готовую хорошую виртуальную память и IPC. Но Mach 3.0 в чистом виде — микроядро, и BSD-сервер в нём жил бы отдельным процессом. NeXT (а потом Apple) втянул BSD-слой внутрь ядра: получилась микроядерная структура без микроядерной цены. Это классический гибрид, ровно как Windows NT.

Устройство XNU и Windows NT рядом

Исходники ядра открыты и это не декорация — их реально можно читать и собирать: https://github.com/apple-oss-distributions/xnu.

Двойная система вызовов

Самое необычное свойство XNU: системных вызовов два семейства, и они различаются знаком номера. На x86-64 в номер вшит класс вызова (биты 24–27):

// BSD-вызовы: положительные номера, класс 2 (SYSCALL_CLASS_UNIX)
#define SYS_write        4          // 0x2000004 в регистре rax

// Mach traps: отрицательные номера, класс 1 (SYSCALL_CLASS_MACH)
#define MACH_msg_trap  (-31)        // 0x100001f в регистре rax
$ uname -a
Darwin mbp.local 24.5.0 Darwin Kernel Version 24.5.0: root:xnu-11417.121.6~2/RELEASE_ARM64_T6041 arm64

# посмотреть таблицу BSD-вызовов конкретной сборки
$ grep -n 'AUE_WRITE' /usr/include/sys/syscall.h | head -3
#define SYS_write          4
#define SYS_writev       121
#define SYS_write_nocancel 397

Важное практическое следствие: напрямую вызывать syscall на macOS нельзя. Apple явно объявила единственным стабильным ABI библиотеку libSystem.dylib. Номера меняются между релизами, а на Apple silicon бинарь без подписи вообще не запустится. Языки, привыкшие обходить libc (Go долго так делал), на macOS обязаны линковаться с системной библиотекой — именно поэтому в Go CGO_ENABLED=0 на Darwin всё равно тянет libSystem.

Порты Mach: настоящий IPC системы

Под POSIX-фасадом macOS живёт совсем другая модель межпроцессного взаимодействия. Единица — порт Mach: защищённая ядром односторонняя очередь сообщений. На порт бывают права: receive right (владелец, ровно один) и send right (сколько угодно). Права передаются в самих сообщениях — это ёмкостная модель (capability), а не «имя в глобальном пространстве».

Что тут важно программисту:

  • Отладчики на macOS работают не через ptrace. PT_ATTACH урезан; LLDB захватывает exception port процесса и получает исключения как сообщения Mach. Отсюда же требование com.apple.security.get-task-allow в entitlements — без него task_for_pid() вам порт не отдаст, и отладка чужого бинаря невозможна.
  • Крэш-репортер — тот же механизм: он держит exception port и ловит падения раньше, чем срабатывают BSD-сигналы.
  • XPC — не «ещё один RPC», а рекомендованный способ разделить приложение на процессы с разными правами. Sandbox-профиль назначается на процесс, поэтому разбор недоверенного файла выносят в отдельный XPC-сервис с урезанными правами.
  • Аналог epollkqueue (пришёл из FreeBSD 4.1). Он умеет не только дескрипторы: EVFILT_PROC, EVFILT_VNODE, EVFILT_TIMER, EVFILT_MACHPORT — один цикл событий на все виды источников. Подробнее про модели событий — в Системные вызовы и IPC.

Чего в macOS нет, а вы на это рассчитываете

Это самый частый источник «у меня локально не собирается»:

Вы привыкли На macOS
/proc/<pid>/... нет вообще; sysctl kern.proc, libproc, proc_pidinfo()
epoll kqueue/kevent
sem_init() — POSIX-семафоры без имени возвращает ENOSYS; нужен dispatch_semaphore или sem_open
pthread_barrier_t отсутствует — часть POSIX, объявленная опциональной
PTHREAD_MUTEX_ROBUST нет
LD_PRELOAD DYLD_INSERT_LIBRARIES, и SIP запрещает его для системных бинарей
ldd otool -L, dyld_info
strace dtruss (нужен root и частичное отключение SIP), ktrace, fs_usage
/lib/libc.so как файл системных .dylib на диске нет — всё в dyld shared cache
clock_gettime был всегда появился только в 10.12; раньше mach_absolute_time()

Последний пункт стоит развернуть: начиная с Big Sur системные библиотеки физически не существуют отдельными файлами. Всё склеено в dyld shared cache — один заранее слинкованный образ, отображаемый во все процессы. Это экономит десятки миллисекунд старта, но ломает любой инструмент, который ожидал найти /usr/lib/libc.dylib на диске.

Память и наблюдаемость

# macOS не свопит "по-старому": сначала он СЖИМАЕТ страницы в памяти
$ vm_stat | head -8
Mach Virtual Memory Statistics: (page size of 16384 bytes)
Pages free:                              105432.
Pages active:                            892310.
Pages inactive:                          701844.
Pages speculative:                        41290.
Pages throttled:                              0.
Pages wired down:                        318776.
Pages purgeable:                          12933.
"Translation faults":                 891244310.

$ sysctl vm.swapusage vm.compressor_mode
vm.swapusage: total = 4096.00M  used = 812.75M  free = 3283.25M  (encrypted)
vm.compressor_mode: 4

# размер страницы 16 КиБ на Apple silicon, а не 4 КиБ — ломает предположения аллокаторов
$ getconf PAGESIZE
16384

Две ловушки в этом выводе. Первая — страница 16 КиБ: код, который хардкодит 4096 (выравнивание буферов, ручной mmap-аллокатор, расчёт fragmentation), на Apple silicon ведёт себя иначе. Вторая — сжатие памяти вместо свопа: приложение может «не свопиться» по метрикам и при этом упираться в процессор на декомпрессии. Общая теория — в Управление памятью.

Инструменты наблюдаемости у Apple свои и очень хорошие:

$ sample Safari 5 -file /tmp/safari.txt   # 5 секунд профилирования по стекам
$ spindump -notarget 10                   # системный снимок при зависании
$ fs_usage -w -f filesys nginx            # аналог strace по файловым операциям
$ log stream --predicate 'subsystem == "com.apple.network"' --level debug
$ xcrun xctrace record --template 'Time Profiler' --launch ./myapp

DTrace в macOS формально есть (Apple портировала её из Solaris), но SIP закрывает трассировку системных процессов. Про инструменты подробно — в Наблюдаемость и производительность.


Windows NT: другая вселенная, а не «Unix с другим синтаксисом»

Объекты вместо файлов

Главное расхождение с Unix проходит здесь. В Unix базовая абстракция — файл, и почти всё приводится к нему. В NT базовая абстракция — объект под управлением Object Manager: файл, процесс, поток, событие, мьютекс, ключ реестра, порт ALPC, секция памяти — всё это объекты с именами в едином пространстве имён, счётчиком ссылок, дескриптором безопасности и маской доступа. Дескриптор (HANDLE) — это индекс в таблице процесса, как файловый дескриптор, но применимый ко всему.

# пространство имён объектов реально есть - его видно утилитой WinObj из Sysinternals
PS> Get-Item \\.\C:                       # \??\C: -> \Device\HarddiskVolume3
PS> (Get-Process nginx).Handles
1247
PS> handle.exe -p nginx.exe               # Sysinternals: что за объекты держит процесс
  1C: File  (RW-)   C:\nginx\logs\access.log
  24: Section        \Sessions\1\BaseNamedObjects\nginx_shm
  30: Mutant         \Sessions\1\BaseNamedObjects\nginx_master

Следствие для кода: симметрии Unix тут нет. select() в Winsock работает только с сокетами, а не с любым дескриптором. Ждать процесс, событие и таймер одним вызовом можно — но через WaitForMultipleObjects, и лимит 64 объекта. Единый цикл событий «файлы + сокеты + процессы», привычный по epoll/kqueue, на Windows собирается иначе — вокруг IOCP (I/O completion ports).

Реактор против проактора

Linux epoll и BSD kqueueреактор: ядро говорит «дескриптор готов», вы сами делаете read(). Windows IOCP — проактор: вы заранее отдаёте ядру буфер и говорите «прочитай туда», ядро выполняет операцию и кладёт уведомление о завершении в очередь. Разница не косметическая:

// Windows: операция начинается сразу, буфер принадлежит ядру до завершения
OVERLAPPED ov = {0};
ReadFile(hFile, buf, sizeof buf, NULL, &ov);   // возвращается немедленно
// ...
GetQueuedCompletionStatus(hIocp, &bytes, &key, &pov, INFINITE);
// buf уже заполнен; трогать buf ДО этого момента нельзя

Именно поэтому кроссплатформенные библиотеки (libuv, Boost.Asio, .NET, Go runtime) внутри держат две разные реализации, а не одну с #ifdef в трёх местах. И именно поэтому io_uring в Linux ощущается знакомо всем, кто писал под Windows: это тоже проактор.

Модель процессов: почему сборка на Windows медленнее

В NT нет fork(). Процесс создаётся «с нуля» вызовом CreateProcess, который делает всё сразу: новое адресное пространство, загрузка образа, инициализация подсистемы Win32, запуск загрузчика DLL. Стоимость — порядка нескольких миллисекунд против сотен микросекунд у fork()+exec() в Linux.

Практический вывод: любые сценарии «запусти тысячу мелких процессов» — конфигурирование autotools, shell-скрипты, make с рекурсией — на Windows работают в разы медленнее не из-за «плохой ОС», а из-за архитектурного выбора. Зато потоки в NT были первоклассными с 1993 года, тогда как в Linux clone() появился только в 1996-м. Отсюда культурная разница: в мире Windows привычно решать задачи пулом потоков, в мире Unix — набором процессов.

Про модель процессов вообще — Процессы, потоки и планировщики.

Семантика файлов, которая ломает кроссплатформенный код

Три отличия, на которых спотыкаются буквально все:

  1. Открытый файл нельзя удалить или переименовать. В Unix unlink() убирает имя, а inode живёт, пока открыт хоть один дескриптор — на этом построены и «атомарная замена конфига», и обновление бинаря на живой системе. В NT по умолчанию файл открывается без FILE_SHARE_DELETE, и удаление даёт ERROR_SHARING_VIOLATION. Отсюда бесконечное «закройте программу и перезапустите установщик». С Windows 10 1709 доступна POSIX-семантика через FileDispositionInformationEx, но старый код ею не пользуется.
  2. Пути. Разделитель \, лимит MAX_PATH = 260 символов (снимается префиксом \\?\ или манифестом long-path-aware), зарезервированные имена CON, NUL, PRN, AUX, COM1. Файл aux.c из вашего репозитория на Windows просто не выкачается.
  3. Регистр. NTFS хранит регистр, но сравнивает без учёта регистра. Ядро умеет и по-другому — включается на каталог: fsutil file setCaseSensitiveInfo C:\repo enable. Классический баг: в Git-репозитории есть Foo.java и foo.java, на Linux CI зелено, на машине разработчика — один файл.

Ввод-вывод: стек драйверов и IRP

Запрос ввода-вывода в NT — объект IRP (I/O Request Packet), который проходит сверху вниз по стеку драйверов устройства, а потом снизу вверх с результатом. Стек наращивается фильтрами, и это ключ к пониманию производительности Windows.

Три красных блока объясняют больше, чем любые бенчмарки: корпоративный ноутбук с антивирусом, DLP и агентом EDR прогоняет каждое открытие файла через три пользовательских решения. npm install на такой машине медленнее не потому, что NTFS хуже ext4, а потому, что на пути каждого из 40 000 файлов стоят три фильтра. Первое, что стоит проверить при жалобах на «медленную сборку» — исключения антивируса для рабочих каталогов.

Планировщик и IRQL

Планировщик NT — вытесняющий, 32 уровня приоритета: 0–15 динамические (с «бустами» за завершение I/O и за фокус окна), 16–31 — реального времени, там бустов нет. Квант на клиентских редакциях короткий (~2 тика, десятки мс), на серверных — длинный (12 тиков), чтобы меньше переключать контекст.

Уникальная для NT концепция — IRQL (Interrupt Request Level), приоритет не потока, а текущего кода на процессоре: PASSIVE_LEVEL (0) — обычный код, APC_LEVEL (1), DISPATCH_LEVEL (2) — планировщик отключён, страничные отказы запрещены, выше — обработчики прерываний. Правило «на DISPATCH_LEVEL нельзя трогать выгружаемую память» — источник половины BSOD в самописных драйверах. Ближайший аналог в Linux — atomic context, где нельзя спать.

WSL: две принципиально разные штуки

  • WSL1 — трансляция: lxss.sys/lxcore.sys реализовывали Linux-системные вызовы поверх NT-примитивов, а процессы Linux были pico-процессами — минимальными процессами NT без подсистемы Win32. Инженерно красиво, но воспроизвести всё поведение Linux (особенно fork(), inotify, права на файлы) не вышло.
  • WSL2 — настоящее ядро Linux в лёгкой виртуальной машине Hyper-V. Совместимость полная, ценой границы ВМ. Файлы Windows видны через /mnt/c по протоколу 9P — да, тому самому из Plan 9. Отсюда известная проблема: сборка в /mnt/c в разы медленнее, чем в домашнем каталоге на ext4 внутри ВМ. Про механику — Виртуализация и контейнеры.

Канонический источник по внутренностям — Russinovich, Solomon, Ionescu, «Windows Internals», 7-е издание, части 1 и 2 (https://learn.microsoft.com/sysinternals/resources/windows-internals).


Solaris и illumos: система, из которой всё украли

Solaris сегодня — нишевая ОС. Но идеи, придуманные в Sun Microsystems между 2001 и 2007 годами, вы используете каждый день, часто не зная об этом.

Что именно Sun изобрела

Немного истории и лицензий

SunOS 4 была BSD-системой. В 1992-м Sun переписала её на System V Release 4 — получился Solaris 2, и заодно вся культура SVR4: /proc как структурированный двоичный интерфейс (а не текстовый, как в Linux), truss вместо strace, пакеты pkgadd.

В 2005-м Sun открыла код как OpenSolaris под лицензией CDDL — и это решение определило многое. CDDL несовместима с GPLv2, поэтому ZFS не может быть влита в mainline Linux: OpenZFS живёт отдельным модулем (zfs-dkms), и это вечная головная боль при обновлении ядра. Некоторые считают, что несовместимость была осознанной — доказательств нет, но эффект налицо.

В 2010-м Oracle купила Sun и закрыла разработку. Сообщество форкнуло последний открытый снимок — так появился illumos. Живые потомки:

Система Чем интересна
OmniOS CE серверная, консервативная, LTS-релизы
OpenIndiana десктоп и общее назначение, ближе всего к OpenSolaris
SmartOS от Joyent: грузится с ISO в память, зоны + KVM, база облака Triton
Tribblix ретро-минимализм, для экспериментов
Oracle Solaris 11.4 закрытая, но живая; поддержка заявлена до 2037

DTrace: почему это до сих пор эталон

DTrace (Cantrill, Shapiro, Leventhal, USENIX ATC 2004, https://www.usenix.org/legacy/event/usenix04/tech/general/cantrill.html) решала задачу, которая до неё считалась невозможной: трассировка продакшн-системы без перекомпиляции, без перезагрузки и с гарантией, что вы её не уроните. Ноль накладных расходов при выключенных зондах, язык D без циклов и без указателей (поэтому зонд заведомо завершается), десятки тысяч точек инструментации.

# сколько системных вызовов какого типа делает каждый процесс — 10 секунд
# dtrace -n 'syscall:::entry { @[execname, probefunc] = count(); } tick-10s { exit(0); }'
  postgres    read                                    18422
  postgres    lseek                                    9211
  nginx       epoll_wait                               4102

# распределение задержек чтения с диска, гистограмма по степеням двойки
# dtrace -n 'io:::start { self->t = timestamp; }
            io:::done /self->t/ { @ = quantize((timestamp - self->t)/1000); self->t = 0; }'
           value  ------------- Distribution ------------- count
             128 |@@@@@@@@@@@@@@@@@@@@@@@@                 8921
             256 |@@@@@@@@@@                               3702
            2048 |@                                         412   <- хвост, вот он

В Linux эту нишу занял eBPF — и это не совпадение: Brendan Gregg, написавший главные книги по DTrace, написал и «BPF Performance Tools». bpftrace синтаксически почти копирует D. Разница в подходе: DTrace — целостная система с одним языком, eBPF — универсальная виртуальная машина в ядре, поверх которой строят всё подряд, включая сеть и безопасность.

Зоны: контейнеры за десять лет до Docker

$ zoneadm list -cv
  ID NAME       STATUS     PATH                  BRAND    IP
   0 global     running    /                     ipkg     shared
   3 web01      running    /zones/web01          ipkg     excl
   7 legacy     running    /zones/legacy         lx       excl   # <- линуксовые бинари

$ zonecfg -z web01 info
zonename: web01
zonepath: /zones/web01
capped-memory:
        physical: 2G
dedicated-cpu:
        ncpus: 2

Обратите внимание на BRAND: lxbranded zones позволяли запускать неизменённые Linux-бинарники поверх ядра illumos, транслируя системные вызовы. Та же идея, что в WSL1, реализованная на десятилетие раньше.

Зона в Solaris — цельная абстракция, заданная одним конфигом. Контейнер в Linux — композиция независимых механизмов (namespaces + cgroups + capabilities + seccomp), которую собирает runtime. Solaris-подход проще в эксплуатации, Linux-подход гибче и позволил Docker появиться «сбоку» без изменений ядра. Детали — в Безопасность и изоляция.

Про ZFS как файловую систему — в Файловые системы, про SMF в ряду других систем инициализации — в Системы инициализации.


Plan 9: Unix, доведённый до конца

Plan 9 из Bell Labs — самая интеллектуально честная ОС в этом списке. Её сделали те же люди, что и Unix (Пайк, Томпсон, Ричи, Пресотто, Уинтерботтом), и сделали именно потому, что считали Unix недоделанным. Три идеи:

1. Всё есть файл — теперь по-настоящему

В Unix «всё есть файл» — лозунг с исключениями: сокеты не файлы, процессы не файлы, ioctl — свалка для того, что в модель не влезло. В Plan 9 исключений нет. Сеть — это каталог:

# Plan 9: TCP-соединение без единого системного вызова для сокетов
% ls /net/tcp
/net/tcp/clone
/net/tcp/0
/net/tcp/1

% cat /net/tcp/clone          # открытие clone создаёт новое соединение
3                             # вернулся номер выделенного канала
% echo 'connect 198.51.100.7!80' > /net/tcp/3/ctl
% echo 'GET / HTTP/1.0

' > /net/tcp/3/data
% cat /net/tcp/3/data
HTTP/1.0 200 OK
...
% cat /net/tcp/3/status
tcp/3 0 Established connect

Ни socket(), ни connect(), ни setsockopt() — только open, read, write. Это значит, что любая программа, умеющая работать с файлами, умеет работать с сетью, а сетевой стек другой машины монтируется к вам так же легко, как каталог.

2. 9P: один протокол для всего

Доступ ко всем ресурсам идёт через протокол 9P — девять типов сообщений (Tversion, Tattach, Twalk, Topen, Tread, Twrite, Tclunk, Tstat, Tremove). Локальный диск, удалённая машина, драйвер устройства и пользовательский демон — все говорят на 9P, и клиент не различает их. Файловый сервер — обычная программа в userspace, писать её проще, чем модуль ядра.

9P не умер вместе с Plan 9. Он живёт в:

  • v9fs — драйвер в Linux mainline (mount -t 9p);
  • QEMU/KVM — проброс каталогов хоста в гостя (-virtfs);
  • WSL2 — обмен файлами между Windows и Linux;
  • Docker Desktop до перехода на virtiofs.

3. Приватные пространства имён

Самая важная идея. В Unix есть одно дерево файлов на систему. В Plan 9 у каждого процесса своё дерево, которое он строит себе сам вызовами bind и mount, и никакого root для этого не нужно:

% bind /arm/bin /bin              # заменить /bin на бинари для ARM
% bind -a /usr/glenda/bin /bin    # ДОБАВИТЬ каталог в /bin — union directory
% bind -b /n/remote/lib /lib      # то же, но новый каталог ищется первым
% mount /srv/net.remote /net      # взять сетевой стек ЧУЖОЙ машины как свой

Последняя строка не метафора: процесс реально начинает ходить в сеть через другую машину — потому что сеть это каталог, а каталог можно примонтировать.

Отсюда пришло почти всё, что вы знаете про изоляцию:

  • Linux namespaces — прямой потомок этой идеи, урезанный до глобальных «видов» вместо честной композиции у каждого процесса;
  • clone() в Linux — идейная копия plan9-шного rfork() с флагами; FreeBSD вообще взяла имя и реализовала rfork(2) дословно;
  • OverlayFS — бедный родственник union directories;
  • /proc в Linux — заимствование plan9-шного /proc, но только на чтение; в Plan 9 отладчик управлял процессом, записывая в /proc/<pid>/ctl.

Настоящий экспорт Plan 9

Если считать по влиянию, Plan 9 — самая успешная «неудачная» ОС в истории:

  • UTF-8. Придуман Кеном Томпсоном и Робом Пайком в 1992 году в закусочной Нью-Джерси именно для Plan 9. Сегодня — кодировка 98% веба.
  • Go. Пайк, Томпсон и Гризмер сделали Go, и родословная видна: каналы и горутины — это CSP из языка Limbo в ОС Inferno, наследнице Plan 9. Ассемблерный синтаксис Go до сих пор — plan9 asm, а компиляторы изначально назывались 6g/8g по схеме именования Plan 9. Если пишете на Go — см. Go: обзор трека.
  • Идея «файловый сервер вместо API». FUSE, /sys в Linux, sysfs-подобные интерфейсы в systemd — всё это возврат к plan9-шному мышлению.

Пощупать вживую проще всего через 9front (http://9front.org/) — активный форк, запускается в QEMU за пару минут. Исходники Plan 9 с 2021 года под лицензией MIT: https://p9f.org/. Классическая статья Пайка и соавторов «Plan 9 from Bell Labs»: https://9p.io/sys/doc/9.html.


RTOS: когда «быстро» и «вовремя» — разные вещи

Определение, которое всё меняет

Система реального времени — не быстрая система, а предсказуемая. Корректность здесь зависит не только от результата, но и от момента его получения. ОС общего назначения оптимизирует среднюю пропускную способность; RTOS оптимизирует худший случай.

Разница ощущается на примере: Linux может выполнить обработку за 5 мкс в 99.99% случаев и за 40 мс в оставшихся 0.01% — отличный средний результат и катастрофа для управления инвертором мотора. FreeRTOS выполнит её за 30 мкс всегда — хуже в среднем, но гарантированно.

  • Hard real-time: пропуск дедлайна = отказ системы. Airbag, ABS, управление реактором.
  • Firm real-time: результат после дедлайна бесполезен, но не фатален. Кадр в видеокодеке.
  • Soft real-time: качество деградирует. Аудиобуфер, игровой рендер.

Метрики, которые реально измеряют: задержка прерывания, время переключения контекста, джиттер (разброс, а не среднее) и WCET — worst-case execution time.

Жизненный цикл задачи

Ключевое отличие от Linux CFS/EEVDF: никакой справедливости. Если задача с приоритетом 10 не блокируется, задачи с приоритетом 9 не выполнятся никогда — и это правильное поведение, а не баг. Инженер обязан доказать, что расписание выполнимо, а не надеяться на планировщик.

Теория расписаний в одном абзаце

Liu и Layland (JACM, 1973) доказали два факта, которыми пользуются до сих пор. Для n независимых периодических задач с дедлайном, равным периоду:

  • Rate-monotonic (приоритет тем выше, чем короче период) — оптимальное назначение фиксированных приоритетов. Достаточное условие выполнимости: сумма загрузок Σ(Cᵢ/Tᵢ) ≤ n·(2^(1/n) − 1), что при n → ∞ даёт ln 2 ≈ 0.693. То есть при фиксированных приоритетах гарантия есть только до 69% загрузки.
  • EDF (Earliest Deadline First, приоритет динамический) выдерживает загрузку до 100%, но при перегрузке деградирует непредсказуемо — «валится» лавиной.

Практическое правило индустрии: закладывать 50–70% загрузки процессора и не верить в средние значения. В Linux EDF доступен как SCHED_DEADLINE с алгоритмом Constant Bandwidth Server.

Инверсия приоритетов: самый знаменитый баг в истории RTOS

Июль 1997 года, марсоход Pathfinder начал самопроизвольно перезагружаться на Марсе. Причина: низкоприоритетная задача метеоданных взяла мьютекс шины, была вытеснена среднеприоритетной задачей связи, а высокоприоритетная задача управления шиной ждала тот же мьютекс — и сторожевой таймер, не дождавшись её, перезагружал систему.

Инверсия приоритетов и наследование

Мьютексы VxWorks поддерживали наследование приоритетов, но флаг был выключен ради производительности. Команда JPL воспроизвела баг на земной копии, включила флаг и загрузила патч на другую планету. Разбор от Гленна Ривза, ведущего инженера ПО Pathfinder, стоит прочесть целиком: https://www.rapitasystems.com/blog/what-really-happened-software-mars-pathfinder-spacecraft.

Вывод для вашего кода — не только для космоса. В POSIX это включается так:

#include <pthread.h>
// мьютекс с наследованием приоритета — обязателен, если есть SCHED_FIFO-потоки
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);  // или PRIO_PROTECT
pthread_mutex_t m;
pthread_mutex_init(&m, &attr);

Без этой строки любой смешанный по приоритетам код на Linux с SCHED_FIFO имеет ту же уязвимость, что Pathfinder. В FreeRTOS наследование встроено в xSemaphoreTake для мьютексов (но не для бинарных семафоров — частая ошибка).

Как выглядит код под FreeRTOS

#include "FreeRTOS.h"
#include "task.h"

// Периодическая задача управления: строго каждые 2 мс, без накопления дрейфа
static void vControlTask(void *pv) {
    TickType_t xLast = xTaskGetTickCount();
    const TickType_t xPeriod = pdMS_TO_TICKS(2);
    for (;;) {
        read_sensors();
        compute_pid();
        drive_pwm();
        // ВАЖНО: DelayUntil, а не Delay — иначе время выполнения тела
        // прибавляется к периоду и частота "уплывает"
        vTaskDelayUntil(&xLast, xPeriod);
    }
}

// Обработчик прерывания: минимум работы, разбудить задачу и выйти
void UART_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    uint8_t c = UART->DR;
    xQueueSendFromISR(xRxQueue, &c, &xHigherPriorityTaskWoken);
    // если разбуженная задача приоритетнее текущей — переключиться
    // ПРЯМО ПРИ ВЫХОДЕ из прерывания, не дожидаясь тика
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

int main(void) {
    xTaskCreate(vControlTask, "ctrl", 512, NULL, configMAX_PRIORITIES - 1, NULL);
    xTaskCreate(vTelemetryTask, "tlm", 1024, NULL, 2, NULL);
    vTaskStartScheduler();   // не возвращается
    for (;;) {}
}

Три вещи, которые сразу видно: нет malloc в горячем пути, нет неограниченных ожиданий, размер стека задачи задаётся явно. Динамическое выделение памяти — главный враг предсказуемости: время работы аллокатора зависит от истории фрагментации, а значит WCET не оценить. В сертифицируемых системах (DO-178C для авиации, ISO 26262 для авто) динамическая память после инициализации попросту запрещена.

Кто есть кто на рынке

RTOS Модель Лицензия Где встречается
FreeRTOS библиотека-планировщик, ~9 КБ кода MIT, поддерживает AWS ESP32, STM32, миллиарды устройств
Zephyr модульная ОС, драйверы + сеть + BLE Apache 2.0, Linux Foundation носимые устройства, IoT, новые проекты
QNX Neutrino настоящее микроядро, драйверы в userspace коммерческая автомобили, медтехника
VxWorks монолит с жёстким RT коммерческая Mars rovers, JWST, авионика
RTEMS одно адресное пространство, POSIX-API BSD-подобная космические аппараты ESA и NASA
ThreadX крошечная, детерминированная Eclipse ThreadX, MIT модемы, накопители, IoT-SoC
NuttX POSIX и почти-Linux API на МК Apache 2.0 автопилот PX4, дроны

Отдельная строка — QNX: единственный коммерчески успешный микроядерный проект. Файловая система, сетевой стек и драйверы — обычные процессы, падение драйвера не роняет машину. За это платят IPC-накладными расходами, но в автомобиле отказоустойчивость дороже пары микросекунд. Подробнее о компромиссе — Архитектуры ядер.

Linux в реальном времени

С версии 6.12 (2024) патчсет PREEMPT_RT, живший вне дерева двадцать лет, влит в mainline. Что он делает: превращает почти все спин-блокировки ядра в rt-мьютексы с наследованием приоритета, выносит обработчики прерываний в потоки, убирает длинные некритические секции с запрещённым вытеснением.

$ grep PREEMPT /boot/config-$(uname -r)
CONFIG_PREEMPT_RT=y
CONFIG_PREEMPT_COUNT=y

# cyclictest из пакета rt-tests: замер задержки пробуждения таймера
$ sudo cyclictest --mlockall --priority=99 --interval=200 --distance=0 -t4 -D 60
T: 0 ( 4231) P:99 I:200 C: 300000 Min:      2 Act:    3 Avg:    3 Max:      18
T: 1 ( 4232) P:99 I:200 C: 300000 Min:      2 Act:    4 Avg:    3 Max:      21
# Max — единственное число, которое имеет значение. 21 мкс на ванильном ядре
# легко превращается в 3000 мкс.

Чек-лист настройки Linux под low-latency — то, что реально делают в проде для торговых систем и обработки звука:

# 1. Изолировать ядра от планировщика общего назначения (параметры загрузки)
#    isolcpus=2,3 nohz_full=2,3 rcu_nocbs=2,3 intel_pstate=disable idle=poll

# 2. Убрать прерывания с изолированных ядер
$ echo 3 | sudo tee /proc/irq/*/smp_affinity_list   # всё остальное на CPU 0-1

# 3. Запретить троттлинг RT-задач (по умолчанию 95% на период)
$ sysctl -w kernel.sched_rt_runtime_us=-1

# 4. Запустить с реальным приоритетом и привязкой
$ sudo chrt -f 80 taskset -c 2 ./trading-engine

# 5. В самом коде: заранее забрать память и запретить её вытеснение
#include <sys/mman.h>
#include <sched.h>
// без этого первый же page fault добавит сотни микросекунд в худший момент
mlockall(MCL_CURRENT | MCL_FUTURE);
// прогреть стек: коснуться страниц заранее
{ char warm[512 * 1024]; memset(warm, 0, sizeof warm); }
struct sched_param sp = { .sched_priority = 80 };
sched_setscheduler(0, SCHED_FIFO, &sp);

Важная честность: даже с PREEMPT_RT Linux — это soft/firm real-time. SMI-прерывания прошивки, кэш-промахи, миграция страниц и сам объём кода не позволяют доказать WCET. Для сертификации по DO-178C level A берут RTEMS или VxWorks, а не Linux, — и не потому, что Linux хуже, а потому что доказать корректность двадцати миллионов строк невозможно.


Сводка: одни и те же вопросы, разные ответы

Вопрос Linux XNU / macOS Windows NT illumos Plan 9 FreeRTOS
Базовая абстракция файл + дескриптор файл + порт Mach объект + хендл файл + дескриптор файл, без исключений задача + очередь
Создание процесса fork + exec fork/posix_spawn CreateProcess fork + exec rfork с флагами процессов нет
Модель событий epoll — реактор kqueue — реактор IOCP — проактор event ports чтение из файлов очереди и семафоры
Изоляция namespaces + cgroups sandbox + entitlements job objects + токены зоны приватные namespaces MPU, если есть
Трассировка eBPF, ftrace, perf DTrace, ktrace, Instruments ETW DTrace сама ФС и есть интерфейс SEGGER SystemView
Стабильный ABI номера syscall навсегда только libSystem только ntdll libc 9P компилируется вместе
Конфигурация текст в /etc XML plist реестр SMF-манифесты XML файлы #define в заголовке

Строка «стабильный ABI» — самая недооценённая. Linux гарантирует, что бинарь 2005 года запустится сегодня, потому что номера системных вызовов неизменны: можно выпускать статические бинари и обходить libc. macOS и Windows такой гарантии не дают — их граница проходит по библиотеке. Это не мелочь, а причина, по которой Go пришлось отказаться от прямых syscall на Darwin, и почему статическая линковка на Windows требует особой аккуратности.

Типичные заблуждения

«macOS — это BSD». Только фасад. Ядро — Mach, драйверы — IOKit, IPC — порты и XPC, init — launchd, файловая система — APFS, безопасность — entitlements и SIP. Из BSD взяты POSIX-слой и часть userland. Код, работающий на FreeBSD, на macOS зачастую не собирается.

«Windows медленный». Планировщик NT и его менеджер памяти инженерно сильные, а IOCP годами были лучшей моделью асинхронного I/O. Медленны обычно конкретные вещи: создание процессов, файловые операции под стеком фильтров, и Win32-совместимость, которую тянут с 1993 года.

«DTrace умерла, есть eBPF». DTrace жива в illumos и FreeBSD, а eBPF превзошёл её по гибкости и уступает по цельности: чтобы получить в Linux то, что DTrace давала одной строкой, часто нужен bpftrace плюс несколько экспериментов с версией ядра.

«Plan 9 — провалившийся эксперимент». UTF-8, namespaces, clone(), /proc, 9P в WSL2 и QEMU, каналы Go — счёт по влиянию на пользователя у Plan 9 огромный. Провалилась дистрибуция, а не идеи.

«RTOS — просто маленькая ОС». Размер ни при чём. RTOS — это система с доказуемым худшим случаем. Урезанный Linux не становится системой реального времени, а FreeRTOS с включённым heap_4 и malloc в цикле перестаёт ею быть.

«Реальное время значит быстро». Наоборот: почти всегда RTOS медленнее в среднем. Вы платите средней производительностью за отсутствие хвоста.

Что с этим делать практически

  1. Пишете кроссплатформенную библиотеку — не абстрагируйте «файл», абстрагируйте операцию. Реактор и проактор не сводятся друг к другу без потерь; берите libuv/Asio, а не пишите свой #ifdef _WIN32.
  2. Настраиваете CI — обязательно добавьте job на macOS и Windows, даже если продакшн на Linux: у половины команды ноутбуки не на Linux, и «работает у меня» будет ломаться именно там.
  3. Ловите проблемы производительности на macOS — начните с sample и fs_usage, они дают 80% ответов без бубна с SIP.
  4. Ловите проблемы I/O на Windows — сначала Process Monitor и проверка исключений антивируса, только потом бенчмарки.
  5. Столкнулись с ZFS или DTrace — знайте, что читать: документация OpenZFS (https://openzfs.github.io/openzfs-docs/) и книга Gregg & Mauro «DTrace: Dynamic Tracing in Oracle Solaris, Mac OS X and FreeBSD».
  6. Делаете что-то с жёсткими дедлайнами на LinuxPREEMPT_RT, SCHED_FIFO, mlockall, изоляция ядер, cyclictest в CI. И меряйте максимум, а не среднее, — иначе вы меряете не то.

Источники

Мини-итог

Пять систем, пять разных ответов на одни и те же вопросы. XNU показывает, что микроядерную структуру можно взять без микроядерной цены, а стабильным ABI может быть библиотека, а не номера вызовов. Windows NT показывает, что «всё есть файл» — не единственный способ построить ОС, и что модель проактора жизнеспособна. Solaris показывает, что хорошие идеи переживают компанию, которая их придумала. Plan 9 показывает, что доведённая до конца идея может победить даже без своей ОС. RTOS показывает, что «предсказуемо» и «быстро» — разные оси, и выбирать приходится осознанно.

Практическая польза от этого знания появляется в момент, когда что-то ломается на чужой платформе: вместо «Windows кривой» вы понимаете, что упёрлись в отсутствие FILE_SHARE_DELETE, и знаете, что с этим делать.

Что дальше

Мы разобрали ядра разных семейств — теперь поднимемся на уровень выше, туда, где ОС встречается с пользователем. Следующая статья: Графический стек: X11, Wayland, оконные менеджеры, композиторы и DE — почему X11 живёт сорок лет, что именно исправил Wayland, чем композитор отличается от оконного менеджера, и почему на macOS и Windows этого спора не было вовсе.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов