Другие ОС: 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.
Исходники ядра открыты и это не декорация — их реально можно читать и собирать: 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-сервис с урезанными правами.
- Аналог
epoll— kqueue (пришёл из 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 — набором процессов.
Про модель процессов вообще — Процессы, потоки и планировщики.
Семантика файлов, которая ломает кроссплатформенный код
Три отличия, на которых спотыкаются буквально все:
- Открытый файл нельзя удалить или переименовать. В Unix
unlink()убирает имя, а inode живёт, пока открыт хоть один дескриптор — на этом построены и «атомарная замена конфига», и обновление бинаря на живой системе. В NT по умолчанию файл открывается безFILE_SHARE_DELETE, и удаление даётERROR_SHARING_VIOLATION. Отсюда бесконечное «закройте программу и перезапустите установщик». С Windows 10 1709 доступна POSIX-семантика черезFileDispositionInformationEx, но старый код ею не пользуется. - Пути. Разделитель
\, лимитMAX_PATH= 260 символов (снимается префиксом\\?\или манифестом long-path-aware), зарезервированные именаCON,NUL,PRN,AUX,COM1. Файлaux.cиз вашего репозитория на Windows просто не выкачается. - Регистр. NTFS хранит регистр, но сравнивает без учёта регистра.
Ядро умеет и по-другому — включается на каталог:
fsutil file setCaseSensitiveInfo C:\repo enable. Классический баг: в Git-репозитории естьFoo.javaиfoo.java, на Linux CI зелено, на машине разработчика — один файл.
Ввод-вывод: стек драйверов и IRP
Запрос ввода-вывода в NT — объект IRP (I/O Request Packet), который проходит сверху вниз по стеку драйверов устройства, а потом снизу вверх с результатом. Стек наращивается фильтрами, и это ключ к пониманию производительности Windows.
проверяет каждое открытие"] AV --> Backup["Мини-фильтр резервного копирования"] Backup --> EDR["Мини-фильтр EDR / DLP"] EDR --> FS["ntfs.sys — файловая система"] FS --> Vol["volsnap.sys — теневые копии"] Vol --> Disk["disk.sys → storport → драйвер NVMe"] Disk --> HW[("Устройство")] HW -. "завершение IRP идёт обратно
через ВЕСЬ стек" .-> App classDef slow fill:#ef444422,stroke:#ef4444,color:#ef4444 class AV,Backup,EDR slow
Три красных блока объясняют больше, чем любые бенчмарки: корпоративный ноутбук
с антивирусом, 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: lx — branded 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.
Жизненный цикл задачи
наивысший приоритет Running --> Ready: вытеснена более приоритетной
или истёк квант при round-robin Running --> Blocked: xQueueReceive / xSemaphoreTake
с таймаутом Running --> Blocked: vTaskDelayUntil — периодическая задача Blocked --> Ready: событие пришло
или истёк таймаут Running --> Suspended: vTaskSuspend Ready --> Suspended: vTaskSuspend Blocked --> Suspended: vTaskSuspend Suspended --> Ready: vTaskResume Running --> [*]: vTaskDelete note right of Running Готовая задача с приоритетом выше вытесняет текущую НЕМЕДЛЕННО. Нет «справедливости», нет вычисления vruntime — только строгий приоритет. end note
Ключевое отличие от 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 медленнее в среднем. Вы платите средней производительностью за отсутствие хвоста.
Что с этим делать практически
- Пишете кроссплатформенную библиотеку — не абстрагируйте «файл»,
абстрагируйте операцию. Реактор и проактор не сводятся друг к другу
без потерь; берите libuv/Asio, а не пишите свой
#ifdef _WIN32. - Настраиваете CI — обязательно добавьте job на macOS и Windows, даже если продакшн на Linux: у половины команды ноутбуки не на Linux, и «работает у меня» будет ломаться именно там.
- Ловите проблемы производительности на macOS — начните с
sampleиfs_usage, они дают 80% ответов без бубна с SIP. - Ловите проблемы I/O на Windows — сначала Process Monitor и проверка исключений антивируса, только потом бенчмарки.
- Столкнулись с ZFS или DTrace — знайте, что читать: документация OpenZFS (https://openzfs.github.io/openzfs-docs/) и книга Gregg & Mauro «DTrace: Dynamic Tracing in Oracle Solaris, Mac OS X and FreeBSD».
- Делаете что-то с жёсткими дедлайнами на Linux —
PREEMPT_RT,SCHED_FIFO,mlockall, изоляция ядер,cyclictestв CI. И меряйте максимум, а не среднее, — иначе вы меряете не то.
Источники
- Windows: Russinovich, Solomon, Ionescu, Yosifovich, «Windows Internals», 7-е издание, части 1–2. Документация Native API и Sysinternals: https://learn.microsoft.com/sysinternals/.
- macOS/XNU: Amit Singh, «Mac OS X Internals: A Systems Approach» (устарела, но фундамент объясняет лучше всех); Jonathan Levin, «*OS Internals», тома I–III (https://newosxbook.com/). Исходники: https://github.com/apple-oss-distributions/xnu.
- Mach: Accetta et al., «Mach: A New Kernel Foundation for UNIX Development», USENIX 1986 — стоит прочитать оригинал, там видно, чего Mach хотел и что не вышло.
- Solaris: McDougall & Mauro, «Solaris Internals», 2-е издание; документация illumos https://illumos.org/books/; статья про DTrace https://www.usenix.org/legacy/event/usenix04/tech/general/cantrill.html.
- Plan 9: Pike et al., «Plan 9 from Bell Labs» https://9p.io/sys/doc/9.html; «The Use of Name Spaces in Plan 9» https://9p.io/sys/doc/names.html; 9front http://9front.org/.
- Реальное время: Liu & Layland, «Scheduling Algorithms for Multiprogramming
in a Hard-Real-Time Environment», JACM 20(1), 1973; документация FreeRTOS
https://freertos.org/Documentation/; Zephyr https://docs.zephyrproject.org/;
материалы Linux Foundation по
PREEMPT_RThttps://wiki.linuxfoundation.org/realtime/start. - Общий фундамент: Tanenbaum & Bos, «Modern Operating Systems», 5-е изд. — единственный учебник, где Windows и Unix разобраны параллельно и честно.
Мини-итог
Пять систем, пять разных ответов на одни и те же вопросы. XNU показывает, что микроядерную структуру можно взять без микроядерной цены, а стабильным ABI может быть библиотека, а не номера вызовов. Windows NT показывает, что «всё есть файл» — не единственный способ построить ОС, и что модель проактора жизнеспособна. Solaris показывает, что хорошие идеи переживают компанию, которая их придумала. Plan 9 показывает, что доведённая до конца идея может победить даже без своей ОС. RTOS показывает, что «предсказуемо» и «быстро» — разные оси, и выбирать приходится осознанно.
Практическая польза от этого знания появляется в момент, когда что-то ломается
на чужой платформе: вместо «Windows кривой» вы понимаете, что упёрлись
в отсутствие FILE_SHARE_DELETE, и знаете, что с этим делать.
Что дальше
Мы разобрали ядра разных семейств — теперь поднимемся на уровень выше, туда, где ОС встречается с пользователем. Следующая статья: Графический стек: X11, Wayland, оконные менеджеры, композиторы и DE — почему X11 живёт сорок лет, что именно исправил Wayland, чем композитор отличается от оконного менеджера, и почему на macOS и Windows этого спора не было вовсе.