Операционные системы Семейство BSD: FreeBSD, OpenBSD, NetBSD, DragonFly — философия и различия
0%

Семейство BSD: FreeBSD, OpenBSD, NetBSD, DragonFly — философия и различия

Семейство BSD: FreeBSD, OpenBSD, NetBSD, DragonFly — философия и различия

Вы почти наверняка каждый день пользуетесь кодом BSD, даже если никогда не устанавливали ни одну из этих систем. Когда вы открываете терминал на macOS — вы работаете в userland, значительная часть которого пришла из FreeBSD. Когда вы делаете ssh куда угодно с чего угодно — вы запускаете OpenSSH, написанный командой OpenBSD. Когда ваш код вызывает socket(), bind(), listen(), accept() — вы используете API, придуманный в Беркли в 1983 году и с тех пор не менявшийся принципиально. Когда вы смотрите Netflix — ваш поток отдаёт FreeBSD-машина. Когда вы играете на PlayStation — под вами ядро, выросшее из FreeBSD.

Но «BSD» — не одна система. Это четыре независимых операционных системы с общим предком, которые за тридцать лет разошлись настолько, что различаются сильнее, чем Debian и Fedora. У каждой есть одна главная идея, которой она подчиняет всё остальное, включая удобство. Понять BSD — значит понять эти четыре идеи и цену, которую каждая система за свою идею платит.

Для прикладного программиста это не музейная экскурсия. BSD — это место, где родились и до сих пор живут вещи, которых в Linux либо нет, либо они устроены иначе: kqueue вместо зоопарка epoll/inotify/timerfd/signalfd, pledge/unveil вместо seccomp-BPF, jails до всякого Docker, ZFS как штатная корневая ФС. Знание BSD — это ещё и калибровка: когда вы видите, как одну и ту же задачу решили четырьмя способами, вы перестаёте считать привычный способ единственно возможным.

История линии Unix → BSD разобрана подробно в статье История ОС; здесь мы начнём с момента раскола.

Откуда взялись четыре системы

Ключевая точка — 1991–1994 годы. Berkeley Software Distribution была не операционной системой, а набором дополнений к AT&T Unix: чтобы легально запустить BSD, требовалась лицензия AT&T. Группа CSRG в Беркли методично выпиливала последний AT&T-код, и в 1991-м вышел Networking Release 2 (Net/2) — почти полная система, свободная от кода AT&T, но без шести файлов ядра.

Уильям и Линн Джолиц дописали недостающее и в 1992-м выпустили 386BSD — первую свободную BSD для дешёвого железа. Дальше произошло то, что определило всё: Джолицы не успевали интегрировать поток патчей от сообщества. Накопленный неофициальный «patchkit» породил две параллельные попытки продолжить работу — NetBSD и FreeBSD, обе в 1993 году.

Параллельно AT&T через дочернюю USL судилась с BSDi и Университетом Калифорнии. Иск урегулировали в начале 1994-го: несколько файлов удалили, несколько переписали, и вышел 4.4BSD-Lite — юридически чистая база, на которую перебазировались обе молодые системы. Эти два года неопределённости — важнейший «что если» в истории ОС: пока BSD судилась, Linux рос без юридических рисков и собрал критическую массу разработчиков.

В 1995-м Тео де Раадт после конфликта в NetBSD форкнул её и основал OpenBSD с явно сформулированной целью — корректность и безопасность важнее всего остального. В 2003-м Мэтт Диллон, не согласный с выбранным во FreeBSD 5.x подходом к SMP, форкнул FreeBSD 4.8 и создал DragonFly BSD.

Главное отличие от Linux: base system

Если запомнить из статьи одну вещь — пусть это будет она. BSD — это операционная система целиком, разрабатываемая одной командой в одном репозитории. Ядро, libc, компилятор, ls, ssh, ifconfig, установщик, man-страницы, файрвол — всё лежит в одном дереве /usr/src, версионируется вместе и релизится вместе. Это называется base system.

Linux — ядро. Всё остальное в вашей системе — независимые проекты, которые кто-то (Debian, Red Hat, Arch) взял на себя труд собрать вместе и подружить. Дистрибутив — это слой интеграции.

Base system в BSD против собранного из пакетов Linux-дистрибутива

Из этого различия растут абсолютно все остальные:

Документация. В BSD man-страница — часть исходников и обязательна для любого изменения. Патч, меняющий поведение утилиты, но не правящий её man, в OpenBSD не примут. Поэтому man tcp, man pf.conf, man 9 uvm — это реально исчерпывающие тексты, а не заглушка «see info page». Практический совет: на BSD сначала читайте man, а не Stack Overflow — там обычно уже есть ответ. man -k (он же apropos) ищет по всем разделам.

Граница «система/приложение» физическая. Всё из base лежит в /bin, /sbin, /usr/bin, /usr/sbin. Всё, что вы поставили пакетами — в /usr/local. Не «по договорённости», а жёстко: pkg физически не может писать в /usr/bin. Отсюда практическая привычка: в скриптах #!/usr/bin/env bash, а не #!/bin/bash — bash в base нет ни в одной BSD, он лежит в /usr/local/bin/bash.

Согласованность ядра и userland. Ядро и libc собираются вместе и меняются вместе. Отсюда — важнейшее следствие, которое ломает интуицию линуксоидов: в BSD нет стабильного syscall ABI. Прямые системные вызовы в обход libc — не поддерживаемый контракт. В Linux наоборот: syscall ABI свят, а libc — просто библиотека, поэтому Go долгие годы вызывал syscalls напрямую. На FreeBSD/OpenBSD так делать нельзя, и Go на этих платформах ходит через libc; OpenBSD c msyscall(2) вообще убивает процесс, если syscall пришёл не из объявленного региона libc.

Апгрейд. freebsd-update fetch install или sysupgrade в OpenBSD обновляют всю базовую систему как единое целое — комбинация «ядро + userland» тестировалась именно в таком составе. Обратная сторона — меньшая гибкость: нельзя взять новое ядро и старый userland.

Про архитектуру самих ядер — все четыре BSD монолитные с загружаемыми модулями — см. Архитектуры ядер.

Лицензия: чем BSD отличается от GPL на практике

BSD-лицензия (2-clause) требует одного: сохранить копирайт. Можно взять код, изменить, встроить в закрытый продукт и не публиковать ничего. GPL требует открыть производные работы.

Это не абстракция, а причина, по которой BSD-код везде: сетевой стек в Windows долгое время нёс следы BSD; macOS взяла userland и часть ядра; Junos (Juniper), NetApp ONTAP, PlayStation 4/5, Nintendo Switch — всё это использует FreeBSD-код. Компании выбирают BSD именно потому, что не обязаны ничего возвращать.

Философский спор здесь честный: сторонники GPL говорят, что BSD-лицензия позволяет «забрать и не отдать», и это ослабляет проект; сторонники BSD — что настоящая свобода включает свободу использовать код как угодно, и что широкое проникновение важнее принудительного возврата. Практический вывод для вас: если пишете библиотеку и хотите, чтобы её взяли в коммерческий продукт, BSD/MIT/Apache — правильный выбор; если хотите гарантировать, что улучшения вернутся, нужна GPL/AGPL.

Четыре системы, четыре главные идеи

FreeBSD — «нам нужна работающая полная система, и быстрая»

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

Что даёт FreeBSD прикладному программисту прямо сейчас:

# Версия ядра и userland — они по определению совпадают
$ freebsd-version -kru
14.2-RELEASE-p3   # ядро
14.2-RELEASE-p3   # userland
14.2-RELEASE-p3   # с учётом установленных патчей

$ uname -a
FreeBSD db01 14.2-RELEASE-p3 FreeBSD 14.2-RELEASE-p3 GENERIC amd64

# ZFS — корневая ФС по умолчанию в установщике
$ zfs list
NAME                 USED  AVAIL  REFER  MOUNTPOINT
zroot                8.9G   421G    96K  /zroot
zroot/ROOT           2.1G   421G    96K  none
zroot/ROOT/default   2.1G   421G   2.1G  /
zroot/usr/home       6.2G   421G   6.2G  /usr/home

# Снапшот перед рискованным деплоем — мгновенный и почти бесплатный
$ zfs snapshot zroot/ROOT/default@before-upgrade
$ zfs rollback zroot/ROOT/default@before-upgrade   # если всё сломалось

# Пакеты
$ pkg install -y nginx postgresql16-server
$ pkg info -l nginx | head -3      # что именно поставилось и куда
/usr/local/etc/nginx/nginx.conf
/usr/local/sbin/nginx

Jails — контейнеры до контейнеров, появившиеся в FreeBSD 4.0 (2000). В отличие от Linux, где изоляция собирается из ортогональных namespaces + cgroups + seccomp, jail — это единый примитив ядра: одним вызовом jail(2) процесс получает свой корень ФС, свой набор адресов, свой hostname и запрет на всё, что затрагивает хост:

# /etc/jail.conf
web {
    host.hostname = "web.internal";
    path = "/jails/web";
    ip4.addr = "10.0.0.10";
    exec.start = "/bin/sh /etc/rc";
    exec.stop  = "/bin/sh /etc/rc.shutdown";
    mount.devfs;
    allow.raw_sockets = 0;   # запрет ping/raw-сокетов внутри
    persist;
}
$ service jail start web
$ jls                    # список запущенных jail'ов
   JID  IP Address      Hostname        Path
     1  10.0.0.10       web.internal    /jails/web
$ jexec web /bin/sh      # войти внутрь

Разница в философии заметна: jail безопасен по умолчанию — вы явно разрешаете отдельные возможности. Linux-контейнер по умолчанию делится с хостом почти всем, и безопасным его делает конфигурация рантайма. Подробный разбор обеих моделей — в Виртуализация и контейнеры и Безопасность и изоляция.

Capsicum — второй механизм изоляции FreeBSD, ортогональный jail’ам: capability-режим для процесса. После cap_enter() процесс теряет доступ к глобальным пространствам имён — он больше не может ни open() по пути, ни резолвить PID’ы; работать можно только с уже открытыми дескрипторами, права которых дополнительно сужены:

#include <sys/capsicum.h>

int fd = open("/var/log/app.log", O_WRONLY | O_APPEND);

cap_rights_t rights;
cap_rights_init(&rights, CAP_WRITE, CAP_FSTAT);
if (cap_rights_limit(fd, &rights) < 0) err(1, "cap_rights_limit");

if (cap_enter() < 0) err(1, "cap_enter");   // обратной дороги нет

write(fd, "ok\n", 3);                        // работает
open("/etc/passwd", O_RDONLY);               // ECAPMODE — глобальных путей больше нет

Логика та же, что у Linux seccomp, но выражена не в терминах «какие syscalls разрешены», а в терминах «какими объектами разрешено оперировать». Это чище концептуально, но требует переписывать программу под модель «всё через переданные дескрипторы».

Производительность сети. FreeBSD — платформа Netflix Open Connect: одна машина отдаёт сотни гигабит в секунду шифрованного видео. Ключи к этому — sendfile(2), интегрированный с VM-подсистемой, kernel TLS (шифрование в ядре, без копирования в userspace) и NUMA-aware выделение буферов. Инженерные отчёты Дрю Гэллэтина с EuroBSDcon — лучшее чтение о том, где на самом деле упирается сетевой стек: people.freebsd.org/~gallatin.

DTrace во FreeBSD — полноценный, включая пробы в ядре и USDT в приложениях. Про сравнение с eBPF — Наблюдаемость и производительность.

OpenBSD — «пусть будет медленнее, но правильно»

OpenBSD — это проект с одним категорическим приоритетом: корректность кода. Не «безопасность» в смысле фич, а именно корректность — потому что большинство уязвимостей это обычные баги. Отсюда всё остальное: непрерывный аудит исходников, беспощадное удаление сложности, отказ от кода, который нельзя понять целиком.

Адресное пространство процесса OpenBSD и включённые по умолчанию защиты

Самый ценный для прикладного программиста подарок OpenBSD — pledge(2). Идея гениально проста: программа сама заявляет ядру, что ей нужно, и всё остальное отключается необратимо.

#include <unistd.h>
#include <err.h>

int main(void)
{
    /* Фаза 1: инициализация — нужно всё */
    int sock = setup_listener(8080);
    load_config("/etc/myapp.conf");

    /* Фаза 2: работа — сети и stdio достаточно, файлы больше не открываем */
    if (pledge("stdio inet", NULL) == -1)
        err(1, "pledge");

    serve_forever(sock);   /* любой open() отсюда = SIGABRT и core dump */
}

Обратите внимание на три решения, каждое из которых спорно и каждое обосновано:

  1. Категории, а не отдельные syscalls. "stdio" — это ~60 вызовов, включая read, write, close, sigaction, clock_gettime. Не потому, что лень, а потому, что списком из 300 номеров syscall никто не сможет пользоваться правильно. Сравните с профилем seccomp-BPF на двести строк, который вы копируете из интернета, не понимая.
  2. Нарушение убивает процесс, а не возвращает EPERM. Если программа делает то, чего не обещала — это баг, и он обязан быть громким. EPERM бы утонул в логах.
  3. Только сужение. Повторный pledge может лишь убрать права. Захваченный процесс не вернёт себе возможности.

unveil(2) — вторая половина: ограничение видимой части ФС без root и без namespaces.

unveil("/var/www", "r");        /* только чтение */
unveil("/var/log/app", "rwc");  /* чтение, запись, создание */
unveil(NULL, NULL);             /* закрыть список — больше ничего не добавить */

/* с этого момента остальная файловая система для процесса не существует:
   open("/etc/passwd") вернёт ENOENT, а не EACCES — путь просто не виден */

Сравните трудозатраты: chroot требует root, копирования библиотек и /dev; namespaces требуют unshare, монтирований и понимания propagation; SELinux требует политики. unveil — это три строки в main(). Именно поэтому в OpenBSD им покрыты почти все программы base system.

Остальные механизмы включены по умолчанию, без вашего участия: W^X по всему адресному пространству, ASLR + PIE, guard-страницы и junk-заполнение в malloc, RETGUARD (криптографическая защита адресов возврата), KARL — пересборка ядра со случайным порядком объектных файлов при каждой загрузке, msyscall(2) — запрет syscall’ов откуда угодно, кроме региона libc.

Проверить работу malloc-защит легко:

# Включить максимальную диагностику для одного запуска
$ MALLOC_OPTIONS=CFGJU ./myapp
# C — canary у чанков, F — освобождённые страницы недоступны,
# G — guard pages, J — junk-заполнение, U — free unmap
# use-after-free падает сразу и в точке использования, а не через час где-то ещё

Практический совет, который стоит того, чтобы запомнить: прогоняйте тесты своей C/C++/Rust-программы на OpenBSD, даже если деплоите на Linux. Комбинация agressive-malloc, guard pages и junk fill ловит use-after-free и переполнения буфера там, где glibc молча продолжает работать. Это дешёвый и очень эффективный фаззинг «бесплатно».

Что OpenBSD за это платит: производительность заметно ниже FreeBSD и Linux на многоядерных нагрузках — распараллеливание ядра идёт медленно и осторожно; часть популярного софта требует патчей; JIT-движки (JVM, Node.js, браузеры) конфликтуют с W^X и требуют раздела, смонтированного с wxallowed; Linux-эмуляции нет вообще (удалена в 2016-м). Проект считает эту цену приемлемой и не собирается её снижать.

Отдельно стоит отметить, сколько софта OpenBSD подарила остальному миру: OpenSSH (стоит буквально везде), LibreSSL (форк OpenSSL после Heartbleed — с выкинутыми 200k строк), pf (файрвол с человекочитаемым синтаксисом, портированный во FreeBSD, NetBSD и macOS), tmux, doas (замена sudo на 300 строк вместо 30k), mandoc, OpenBGPD, OpenNTPD, arc4random(3), strlcpy/strlcat.

NetBSD — «оно должно работать везде»

Девиз «Of course it runs NetBSD» — не шутка, а следствие архитектурного принципа: жёсткое разделение машинно-независимого (MI) и машинно-зависимого (MD) кода. В дереве sys/ есть sys/kern, sys/uvm, sys/net — код, не знающий об архитектуре, — и sys/arch/<arch>/ с минимально необходимым машинным слоем. Портирование на новую архитектуру означает написание MD-части, а не правку ядра повсюду.

Результат: NetBSD работает на полусотне аппаратных платформ — от amd64 и arm64 до VAX, SPARC, m68k-Amiga и Sega Dreamcast. Для встраиваемых систем и странного железа это единственный реальный вариант из BSD.

Самое интересное для программиста в NetBSD — rump kernels (Антти Кантее). Идея «anykernel»: драйверы ядра написаны так, что их можно собрать и запустить как обычную библиотеку в userspace. Это значит, что ФС, TCP/IP-стек и драйверы NetBSD можно отлаживать обычным gdb, фаззить обычным AFL и юнит-тестировать без виртуалки:

# Поднять TCP/IP-стек NetBSD как процесс в userspace
$ rump_server -lrumpnet -lrumpnet_net -lrumpnet_netinet unix:///tmp/rumpnet

# Обычные утилиты, но говорящие с ядром-процессом, а не с настоящим ядром
$ export RUMP_SERVER=unix:///tmp/rumpnet
$ rump.ifconfig -a
$ rump.netstat -rn

Это же превращает NetBSD-драйверы в переносимые компоненты: rump-ядра запускали внутри Xen как unikernel (проект Rumprun). Академическая ценность здесь огромна — диссертация Кантее «Flexible Operating System Internals: The Design and Implementation of the Anykernel and Rump Kernels» — одна из самых интересных работ по архитектуре ОС за последние пятнадцать лет: book.rumpkernel.org.

Второй вклад NetBSD — pkgsrc: система портов, которая работает не только на NetBSD, а на Linux, macOS, illumos, FreeBSD и десятке других систем. По сути, предшественник Homebrew и Nix по идее «единое дерево рецептов для разных ОС».

DragonFly BSD — «SMP надо делать иначе»

В начале 2000-х перед всеми BSD стоял один вопрос: как перейти от «большого блокирующего ядро замка» (Giant lock) к настоящей многоядерности. FreeBSD выбрала путь мелкогранулярных блокировок: разбивать структуры данных на всё более мелкие области, защищённые своими мьютексами. Путь рабочий, но трудный: каждая новая блокировка — потенциальный deadlock и точка конкуренции.

Мэтт Диллон считал, что это тупик, и в 2003-м форкнул FreeBSD 4.8. Ставка DragonFly: минимизировать разделяемое состояние, а не защищать его блокировками. Ключевые механизмы:

  • LWKT (Light Weight Kernel Threads) — потоки ядра, привязанные к своему CPU. Планировщик каждого ядра работает со своей очередью и не трогает чужие; миграция между CPU — явная и редкая операция.
  • Асинхронные сообщения между CPU вместо блокировок: вместо того чтобы взять мьютекс на чужую структуру, поток отправляет сообщение владельцу CPU.
  • Токены вместо мьютексов. Токен можно удерживать, засыпая, и он автоматически отпускается при переключении контекста. Это делает deadlock структурно почти невозможным — ценой того, что критическая секция может быть прервана.

По-настоящему известной систему сделала другая вещь — HAMMER2: файловая система с криптографическими контрольными суммами, мгновенными снапшотами, онлайн-дедупликацией, сжатием и поддержкой multi-master репликации. Философски она ближе к ZFS/btrfs, но заметно проще; про устройство copy-on-write ФС — Файловые системы.

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

Честная оценка: DragonFly — самая маленькая из четвёрки по сообществу и охвату железа. Её ценность скорее исследовательская — это живая демонстрация альтернативного подхода к SMP. Для продакшна общего назначения выбирают FreeBSD.

Сравнение: что выбрать под задачу

Критерий FreeBSD OpenBSD NetBSD DragonFly
Главный приоритет производительность, полнота корректность и безопасность портируемость модель SMP
Релизный цикл ~2 года на мажор + патчи строго раз в 6 месяцев по готовности по готовности
Поддержка релиза 5 лет (STABLE-ветка) 12 месяцев (2 релиза) долгая короткая
Файловая система ZFS, UFS2 FFS2 (без ZFS) FFS, LFS, ZFS (базовая) HAMMER2, UFS
Виртуализация bhyve vmm/vmd NVMM нет своей
Контейнеры jails (зрелые) chroot + pledge/unveil нет аналога jail нет
Файрвол pf + ipfw pf (родина) npf, pf pf
Пакеты pkg / ports / poudriere pkg_add / ports pkgsrc pkg / DPorts
Многоядерность отличная скромная, растёт хорошая своя модель
DTrace да нет частично нет
Linux-совместимость linuxulator, зрелый нет вообще есть ограниченно
Где применяют серверы, storage, сеть firewall, DNS, VPN, bastion embedded, экзотика нишевые сервисы

Практическая эвристика:

  • Файловый/медиа-сервер, база данных, высоконагруженная сеть, storage-appliance → FreeBSD. ZFS + jails + bhyve + отличный сетевой стек.
  • Пограничный firewall, VPN-концентратор, DNS, bastion-хост, всё смотрящее в интернет → OpenBSD. pf + relayd + httpd + iked в base, релизы по расписанию, минимум атакуемой поверхности.
  • Странное железо, встроенные системы, исследование ОС → NetBSD.
  • Интерес к архитектуре SMP и HAMMER2 → DragonFly.
  • Обычный сервер приложений с Docker/Kubernetes → всё-таки Linux, и это нормально: экосистема контейнеров живёт там.

Что реально ломается при портировании кода с Linux

Здесь начинается прикладная часть. Ниже — то, на чём спотыкается практически каждый проект.

1. make — это BSD make, а не GNU make. Синтаксис .if/.for несовместим с ifeq/foreach. Ставьте gmake и вызывайте его явно.

2. Утилиты — BSD-версии, а не GNU coreutils. Список ловушек, которые надо знать наизусть:

# sed -i требует суффикс бэкапа (можно пустой, но аргумент обязателен)
sed -i ''  's/foo/bar/' file    # BSD/macOS
sed -i     's/foo/bar/' file    # GNU

# даты
date -v +1d          # BSD
date -d '+1 day'     # GNU

# рекурсивное разрешение симлинков
realpath path        # есть в BSD
readlink -f path     # GNU (во FreeBSD есть, в OpenBSD нет)

# сортировка, xargs, stat, ps — отличаются флагами
stat -f '%z' file    # BSD: размер
stat -c '%s' file    # GNU

# спасение: поставить GNU-версии, они с префиксом g
pkg install coreutils gsed gawk
gsed -i 's/foo/bar/' file

3. /proc может отсутствовать. В OpenBSD его нет вообще (procfs удалена). Во FreeBSD он объявлен устаревшим и обычно не смонтирован. Код вида «прочитаю /proc/self/status, чтобы узнать RSS» на BSD не работает. Правильный способ — sysctl:

/* FreeBSD: путь к собственному исполняемому файлу — аналог /proc/self/exe */
#include <sys/sysctl.h>

char path[PATH_MAX];
size_t len = sizeof(path);
int mib[4] = { CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1 };
sysctl(mib, 4, path, &len, NULL, 0);
# То, что в Linux читают из /proc, в BSD спрашивают у sysctl
$ sysctl hw.ncpu hw.physmem vm.stats.vm.v_free_count
hw.ncpu: 16
hw.physmem: 68719476736
vm.stats.vm.v_free_count: 1204877

$ sysctl -a | wc -l        # весь /proc/sys живёт здесь
1847

# аналоги привычных инструментов
$ procstat -v $$           # карта памяти процесса (≈ /proc/PID/maps)
$ sockstat -4l             # открытые сокеты (≈ ss -ltn)
$ top -m io                # ввод-вывод по процессам
$ vmstat -i                # прерывания

4. Нет epoll/inotify/timerfd/signalfd/eventfd. Есть kqueue, и он один заменяет все пять. Об этом ниже отдельно.

5. Другие libc-расширения. Нет memfd_create, pipe2 (есть во FreeBSD, нет кое-где ещё), accept4 (во FreeBSD есть), gettid, sched_setaffinity (аналог — cpuset_setaffinity). Зато strlcpy, strlcat, arc4random_buf, reallocarray, pledge — родные (в Linux их дают только libbsd или musl).

6. SO_REUSEPORT означает другое. В Linux он балансирует входящие соединения между процессами. В классическом BSD это про multicast. FreeBSD добавила отдельный SO_REUSEPORT_LB (12.0+) с Linux-подобной семантикой; в OpenBSD аналога нет. Многопроцессные серверы, полагающиеся на SO_REUSEPORT-балансировку, надо править.

7. Пути и линковка. Заголовки и библиотеки пакетов — в /usr/local/include и /usr/local/lib, компилятор туда сам не смотрит:

$ cc -I/usr/local/include -L/usr/local/lib -o app app.c -lpq
# либо через pkg-config, который знает про префикс
$ cc $(pkg-config --cflags --libs libpq) -o app app.c

8. Docker нет. Есть jails, есть podman на FreeBSD (развивается), есть OCI-рантайм runj (экспериментальный). Если ваша схема поставки — исключительно образы OCI, BSD пока неудобен.

9. Компилятор по умолчанию — clang (в FreeBSD и OpenBSD; в NetBSD исторически gcc). Код, завязанный на расширения GCC, может не собраться.

10. Шелл /bin/sh — не bash. Это POSIX-совместимый ash (FreeBSD) или pdksh (OpenBSD). Bash-измы ([[ ]], массивы, source) в скриптах с #!/bin/sh молча ломаются.

kqueue: почему это самая красивая часть BSD

Если вы пишете сетевые сервисы — это раздел ради которого стоит читать статью. В Linux, чтобы дождаться разнородных событий, приходится собирать зоопарк: epoll для сокетов, inotify для файлов, timerfd для таймеров, signalfd для сигналов, eventfd для пробуждения из другого потока, waitid/pidfd для детей. Пять разных API, каждый со своими правилами, и всё сводится в один epoll через файловые дескрипторы.

В BSD есть один механизм — kqueue(2), введённый Джонатаном Лемоном во FreeBSD 4.1 (USENIX 2001, «Kqueue: A Generic and Scalable Event Notification Facility»). Событие описывается фильтром: EVFILT_READ, EVFILT_WRITE, EVFILT_TIMER, EVFILT_SIGNAL, EVFILT_VNODE (изменения файла), EVFILT_PROC (события процесса), EVFILT_USER (пробуждение вручную), EVFILT_AIO.

Рабочий скелет event loop:

#include <sys/types.h>
#include <sys/event.h>
#include <sys/time.h>
#include <err.h>

int kq = kqueue();
if (kq == -1) err(1, "kqueue");

struct kevent changes[4];
int n = 0;

/* сокет: данные на чтение, edge-triggered */
EV_SET(&changes[n++], listen_fd, EVFILT_READ, EV_ADD | EV_CLEAR, 0, 0, conn);

/* таймер на 5 секунд — без отдельного timerfd */
EV_SET(&changes[n++], 1, EVFILT_TIMER, EV_ADD, NOTE_MSECONDS, 5000, NULL);

/* сигнал SIGHUP — без signalfd и без обработчика сигналов */
signal(SIGHUP, SIG_IGN);   /* обязательно: kqueue не заменяет диспозицию */
EV_SET(&changes[n++], SIGHUP, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL);

/* смерть дочернего процесса — без SIGCHLD и waitpid в обработчике */
EV_SET(&changes[n++], child_pid, EVFILT_PROC, EV_ADD, NOTE_EXIT, 0, NULL);

struct kevent events[64];
for (;;) {
    /* changes применяются и события забираются одним системным вызовом */
    int nev = kevent(kq, changes, n, events, 64, NULL);
    n = 0;
    if (nev == -1) { if (errno == EINTR) continue; err(1, "kevent"); }

    for (int i = 0; i < nev; i++) {
        struct kevent *e = &events[i];
        if (e->flags & EV_ERROR) { /* ошибка регистрации — в e->data */ continue; }

        switch (e->filter) {
        case EVFILT_READ:
            /* e->data — сколько байт доступно ДО чтения. Можно сразу
               выделить буфер нужного размера, а не читать вслепую. */
            handle_read(e->udata, (size_t)e->data);
            break;
        case EVFILT_TIMER:  on_tick();                  break;
        case EVFILT_SIGNAL: on_reload();                break;  /* безопасный контекст! */
        case EVFILT_PROC:   reap((pid_t)e->ident);      break;
        }
    }
}

Три вещи, которые kqueue делает лучше epoll и которые стоит понять:

  1. Регистрация и ожидание — один системный вызов. В epoll на каждое изменение интереса нужен отдельный epoll_ctl. При тысячах соединений с постоянным переключением read/write это заметный поток syscalls. Цена перехода в ядро разобрана в Системные вызовы и IPC.
  2. udata — произвольный указатель. epoll даёт 64-битное поле в union, и общепринятая практика — класть туда указатель, но kqueue делает это явной частью контракта, вместе с отдельным ident.
  3. data несёт полезную нагрузку. Для EVFILT_READ это количество доступных байт, для слушающего сокета — длина очереди backlog, для EVFILT_VNODE — флаги изменения. epoll сообщает только факт готовности.

Хорошая новость: писать это руками почти никогда не нужно. libevent, libev, libuv, Go runtime, tokio (через mio), Erlang/BEAM — все умеют kqueue и выбирают его автоматически. Читайте man 2 kqueue — это образцовая man-страница.

Как устроена работа с системой: rc.d и ports

Инициализация в BSD — это rc.d: shell-скрипты с зависимостями, разрешаемыми rcorder(8) по ключевым словам REQUIRE/PROVIDE/BEFORE. Подробно — в Системы инициализации; здесь минимум для работы:

# Включить сервис (запись в /etc/rc.conf) и запустить
$ sysrc nginx_enable=YES          # FreeBSD
$ service nginx start
$ service nginx status

$ rcctl enable nginx              # OpenBSD — один инструмент на всё
$ rcctl start nginx
$ rcctl set nginx flags "-c /etc/nginx/custom.conf"

Установка софта из портов, когда нужны нестандартные опции сборки:

# FreeBSD: дерево портов — это Makefile'ы, а не готовые бинарники
$ pkg install -y nginx            # быстро, бинарный пакет
# либо со своими опциями:
$ cd /usr/ports/www/nginx
$ make config                     # интерактивное меню опций
$ make install clean

# Промышленный способ: собрать свой репозиторий пакетов с нужными опциями
$ pkg install poudriere
$ poudriere jail -c -j 14amd64 -v 14.2-RELEASE
$ poudriere bulk -j 14amd64 -f mylist.txt

Обновление системы:

# FreeBSD — бинарные обновления
$ freebsd-update fetch install
$ pkg upgrade

# OpenBSD — патчи безопасности и переход на следующий релиз
$ syspatch                        # бинарные патчи текущего релиза
$ sysupgrade                      # переход на следующий релиз, перезагрузка
$ pkg_add -u                      # обновление пакетов

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

«BSD — это такой Linux-дистрибутив». Нет. Это независимая система с собственным ядром, собственной libc и собственной родословной. Общее с Linux — только POSIX-стандарт и часть портированного софта. Ядро FreeBSD не имеет с Linux общего кода.

«BSD мёртв / это музей». FreeBSD отдаёт значительную часть мирового трафика Netflix; WhatsApp на пике держал ~2 млн TCP-соединений на один FreeBSD-сервер; Junos от Juniper — это FreeBSD; PlayStation — FreeBSD. OpenBSD выпускает релиз каждые полгода без единого срыва с 1996 года.

«BSD-лицензия — просто более слабая GPL». Это разные цели, а не разные уровни строгости. BSD максимизирует распространение кода, GPL — гарантирует возврат улучшений.

«В BSD нет софта». В ports FreeBSD ~35 тысяч пакетов, в pkgsrc — около 27 тысяч. Есть почти всё, кроме проприетарных бинарников (Docker Desktop, некоторые драйверы GPU, ряд коммерческих агентов мониторинга) — и вот их отсутствие как раз бывает блокером.

«OpenBSD безопасен, значит на нём мой сервис безопасен». Механизмы защиты OpenBSD спасают от эксплуатации ошибок памяти. Ваша SQL-инъекция, ваш IDOR и ваш забытый /admin без аутентификации остаются ровно там же. pledge/unveil ограничивают ущерб — но только если вы их вызовете.

«Jail = Docker». Jail — это изоляция, а не упаковка. Docker — это ещё и формат образа, реестр, слои и воспроизводимая сборка. Ближайший аналог этой части в FreeBSD — не jail, а связка poudriere + ZFS-снапшоты, и она про другое.

«kqueue быстрее epoll» (или наоборот). На реальных нагрузках разница между ними на порядки меньше, чем разница между «правильной архитектурой event loop» и неправильной. Ценность kqueue — в единообразии API, а не в микросекундах.

Как попробовать за двадцать минут

# Проще всего — виртуалка. Образы qcow2/vmdk есть на сайтах проектов.
$ curl -O https://download.freebsd.org/releases/VM-IMAGES/14.2-RELEASE/amd64/Latest/FreeBSD-14.2-RELEASE-amd64.qcow2.xz
$ unxz FreeBSD-14.2-RELEASE-amd64.qcow2.xz
$ qemu-system-x86_64 -m 2048 -smp 2 -drive file=FreeBSD-14.2-RELEASE-amd64.qcow2,if=virtio -nic user,hostfwd=tcp::2222-:22

# OpenBSD ставится с одного install-образа минут за пять — установщик текстовый,
# но самый понятный из всех, что я видел: он задаёт правильные вопросы и имеет
# разумные ответы по умолчанию.
$ curl -O https://cdn.openbsd.org/pub/OpenBSD/7.7/amd64/install77.img

Первые команды, которые стоит выполнить, чтобы почувствовать разницу:

$ man hier          # ГДЕ ЧТО ЛЕЖИТ в файловой системе — с объяснениями
$ man tuning        # FreeBSD: осмысленный текст про тюнинг системы
$ man afterboot     # OpenBSD: что сделать сразу после установки
$ man pf.conf       # синтаксис файрвола, который читается как английский
$ apropos socket    # поиск по всем man-страницам

Мини-итог

  • BSD — четыре независимые ОС с общим предком 4.4BSD-Lite и одним общим принципом: система разрабатывается целиком, одной командой, в одном репозитории. Отсюда base system, качество документации, физическая граница /usr против /usr/local и отсутствие стабильного syscall ABI.
  • FreeBSD — производительность и полнота: ZFS, jails, bhyve, DTrace, kTLS. Выбор для серверов, storage и сетевых нагрузок.
  • OpenBSD — корректность любой ценой: pledge, unveil, W^X, KARL, privsep, аудит. Выбор для всего, что смотрит в интернет. Плюс бесплатный «фаззер» для вашего C-кода.
  • NetBSD — портируемость и rump kernels: ядро, которое запускается как процесс.
  • DragonFly — альтернативная модель SMP и HAMMER2.
  • Для прикладного программиста три главных практических отличия: kqueue вместо зоопарка Linux-механизмов ожидания, отсутствие /proc (всё через sysctl), BSD-версии утилит вместо GNU coreutils.

Источники и что читать дальше

  • Marshall Kirk McKusick, George Neville-Neil, Robert Watson, «The Design and Implementation of the FreeBSD Operating System», 2nd ed. — каноническая книга по внутренностям FreeBSD.
  • McKusick, Bostic, Karels, Quarterman, «The Design and Implementation of the 4.4BSD Operating System» — исторический первоисточник.
  • FreeBSD Handbook и FreeBSD Architecture Handbook — docs.freebsd.org
  • OpenBSD FAQ и man-страницы — openbsd.org/faq, man.openbsd.org
  • Theo de Raadt, «pledge(2) and unveil(2)» — доклады и слайды: openbsd.org/papers
  • Jonathan Lemon, «Kqueue: A Generic and Scalable Event Notification Facility», USENIX 2001 — people.freebsd.org/~jlemon/papers/kqueue.pdf
  • Poul-Henning Kamp, Robert Watson, «Jails: Confining the omnipotent root», SANE 2000 — phk.freebsd.dk/pubs/sane2000-jail.pdf
  • Robert Watson et al., «Capsicum: practical capabilities for UNIX», USENIX Security 2010 — cl.cam.ac.uk/research/security/capsicum
  • Antti Kantee, «Flexible Operating System Internals: The Design and Implementation of the Anykernel and Rump Kernels» — book.rumpkernel.org
  • Otto Moerbeek о malloc в OpenBSD — openbsd.org/papers (доклады про omalloc и его диагностические возможности)
  • NetBSD Guide — netbsd.org/docs/guide
  • DragonFly BSD handbook и статьи по HAMMER2 — dragonflybsd.org
  • Michael W. Lucas, «Absolute FreeBSD», 3rd ed. и «Absolute OpenBSD», 2nd ed. — лучшие практические книги для входа.

Что дальше

Мы разобрали ветвь Unix, которая осталась Unix’ом. Но мир ОС этим не исчерпывается: рядом живут macOS с гибридным XNU (где BSD-userland надет на mach-микроядро), Windows NT с совершенно иной родословной и объектной моделью ядра, Solaris/illumos с DTrace и зонами, радикальный Plan 9 и системы реального времени, где важна не пропускная способность, а предсказуемость задержки.

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

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

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

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

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