Ядро и пользовательское окружение
Ядро работает в ring 0 и владеет каждым CPU, страницей памяти и устройством. Твои программы работают в ring 3 и могут взаимодействовать с оборудованием только через syscall — контролируемое пересечение границы привилегий.
Ты вводишь ls в терминале. Оболочка вызывает openat(), чтобы прочитать директорию, затем write(), чтобы отправить имена на экран. Обе операции касаются железа — диска и дисплея — но код оболочки никогда не общается с железом напрямую. Между ними есть посредник: ядро.
Разделение между «кодом, владеющим железом» (ядро) и «кодом, который не владеет» (всё остальное — оболочка, интерпретатор Python, nginx) — это важнейшая структурная граница в операционной системе. Каждый сбой, каждая ошибка прав доступа, каждый segfault, каждый побег из контейнера, которые ты когда-либо будешь отлаживать, затрагивают эту границу. Понимание её — предпосылка для понимания всего остального в этом треке.
После этого урока ты сможешь объяснить, чем владеет ядро, что такое userland и зачем он существует, что такое syscall и почему он является единственной точкой пересечения, а также что означают кольца привилегий CPU на практике.
Ядро владеет железом — единолично. При загрузке Linux ядро загружается первым и немедленно берёт под контроль каждое ядро процессора, каждую страницу памяти, каждую сетевую карту, каждый дисковый контроллер. Никакое другое программное обеспечение не может напрямую прочитать байт с диска или отправить пакет без участия ядра. Это не соглашение и не рекомендация — это аппаратное ограничение CPU.
Процессоры реализуют уровни привилегий, называемые кольцами. Ring 0 (режим ядра) — наиболее привилегированный: код здесь может выполнять любую инструкцию, отображать любую память, работать с любым устройством. Ring 3 (пользовательский режим) — наименее привилегированный: код здесь не может напрямую обращаться к железу. Современные процессоры x86-64 и ARM на практике используют только кольца 0 и 3.
Ядро работает в ring 0. Каждый процесс, который ты запускаешь — твоя оболочка, база данных, редактор — работает в ring 3.
Userland — это всё, что выше ядра. Как только ядро заканчивает загрузку, оно запускает первый процесс userland (PID 1, которым становится systemd или другой init). Дальше каждая программа — bash, nginx, Python, весь экосистемный стек пакетов — это процесс userland. Их объединяет одно: они не могут работать с железом напрямую.
Это разделение защищает систему. Ошибка в коде твоего приложения не может повредить память ядра — процессор сначала поднимет исключение. Вредоносная программа не может открыть сырой сокет и перехватывать сетевой трафик без запроса к ядру, которое проверит права доступа.
Практическое следствие: если твоя программа падает, она падает. Ядро продолжает работать. Если падает ядро — вся машина идёт вниз. Это называется kernel panic.
Syscall — это то, как userland просит ядро выполнить привилегированную работу. Когда твоей программе нужно прочитать файл, записать в stdout, открыть сокет или форкнуть дочерний процесс, она выполняет syscall. Процессор переходит из ring 3 в ring 0 контролируемым образом — программа не может выбрать, что происходит в ring 0, только какой syscall вызвать и с какими аргументами.
# strace показывает каждый syscall процесса
strace ls /tmp 2>&1 | head -20Ты увидишь вызовы вроде openat(AT_FDCWD, "/tmp", O_RDONLY|O_DIRECTORY) и getdents64(...). Это ls просит ядро открыть директорию и прочитать её содержимое. Ядро проверяет, есть ли у ls права доступа, выполняет аппаратную работу, затем возвращает результат в ring 3.
В Linux около 400 системных вызовов. Ты используешь десятки из них при каждой команде, но они скрыты за обёртками библиотек. fopen() в C вызывает openat() под капотом. Python-вский open() тоже в итоге вызывает openat().
Граница syscall поддаётся аудиту и принуждению. Поскольку каждое взаимодействие с железом должно пересекать эту границу, ядро может логировать, фильтровать и блокировать syscall. seccomp (secure computing mode) позволяет ограничить, какие syscall вправе вызывать процесс. Docker использует это, чтобы запретить контейнерам вызывать опасные syscall, такие как reboot() или kexec_load(). strace перехватывает каждое пересечение для отладки.
# Посмотреть, какие syscall использует процесс (сводка)
strace -c ls /tmpВывод показывает счётчики и время на syscall. Процесс, который ни разу не вызвал socket(), почти наверняка не имеет сетевой активности — это значимое утверждение безопасности.
Стоимость syscall реальна, но невелика. Пересечение границы кольца требует от CPU сохранения состояния процесса, переключения на стек ядра, выполнения обработчика syscall, затем восстановления состояния и возврата. На современном железе это стоит примерно 100–300 нс на пересечение. Для большинства программ это пренебрежимо мало. Для высокопроизводительного I/O-кода (база данных, пишущая миллионы строк, прокси, обрабатывающий миллионы пакетов) суммарная стоимость имеет значение — поэтому разработчики ядра вкладываются в пакетные API вроде io_uring, позволяющие userland ставить в очередь много операций и пересекать границу один раз.
Понимание этой стоимости — вот почему ты позже узнаешь о sendfile(), zero-copy networking и почему отображение файлов в память может обгонять циклы read().
Проследи, что происходит при запуске cat /etc/hostname.
strace cat /etc/hostname 2>&1 | grep -E 'open|read|write'Ты увидишь:
openat(AT_FDCWD, "/etc/hostname", O_RDONLY)— ядро открывает файл, возвращает файловый дескриптор (целое число).read(3, "myserver\n", 131072)— ядро читает байты с диска в буфер ядра, копирует их в userland.3— файловый дескриптор.write(1, "myserver\n", 9)— ядро записывает байты из памяти userland в stdout (fd 1), который отображает терминальный драйвер.
Три syscall, три пересечения кольца. Файл не «перемещается» с диска в терминал — ядро посредничает на каждом шаге. cat сам по себе ничего не делает, кроме как вызывает нужные syscall в нужном порядке.
▸Почему это работает
macOS использует ядро BSD-происхождения (XNU) с той же моделью колец, но разными номерами и соглашениями syscall. strace на macOS не существует — используй dtruss (требует root) или dtrace. Концептуальное разделение (ядро/userland/граница syscall) идентично; детали реализации различаются. Везде в этом треке используются имена и номера syscall Linux.
▸Частая ошибка
Распространённое заблуждение: «ядро — это просто программа». Это программа в том смысле, что это скомпилированный код на C — но работает она на уровне привилегий, который делает её категорически иной. Ядро нельзя убить с помощью kill. Его нельзя отлаживать с помощью gdb из userland. У него нет родительского процесса. Когда ты слышишь «ОС зависла», это ядро в панике — единственный способ восстановить работу — перезагрузка.
Процесс в ring 3 должен записать байт на диск. Что должно произойти сначала?
Ядро работает в ring 0 и владеет каждым аппаратным ресурсом машины. Процессы userland работают в ring 3 и физически лишены CPU возможности касаться железа. Единственная точка пересечения — граница syscall: контролируемая ловушка из ring 3 в ring 0, которую ядро использует для проверки прав и выполнения привилегированной работы. Каждое чтение файла, сетевой пакет и форк процесса пересекают эту границу. Инструменты вроде strace делают её видимой. Стоимость — ~100–300 нс на пересечение: пренебрежимо мало для большинства кода, существенно для высокопроизводительного I/O.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.