Learning Platform
Глоссарий Troubleshooting
Урок 03.02 · 18 мин
Начальный
KernelUserspaceRingProtectionCPU

Kernel space vs user space — ring 0 и ring 3

Когда ваш Python-скрипт запущен, в нём «уживаются» два мира: ваш код в user space (привилегии низкие) и ядро Linux в kernel space (привилегии полные). Эти два мира физически разделены на уровне процессора и памяти. Если бы их не было разделено — любая программа с багом могла бы испортить ядро, обнулить таблицу процессов, отформатировать диск.

В этом уроке разберём: что именно разделяет user и kernel, как процессор enforce’ит это разделение (через ring levels), почему memory virtualization без этого не работала бы, и как программа всё-таки попадает в ядро, когда нужно.


Зачем разделение

Представьте операционную систему без kernel/user разделения: любой процесс имеет полный доступ к памяти, к железу, к таблице процессов ОС, ко всем файлам. Что произойдёт?

  1. Любой баг — катастрофа. Сегфолт в браузере — система зависла. Один процесс читает чужие пароли из RAM. Программа случайно затёрла таблицу страниц — crash kernel.
  2. Любое вредоносное ПО — root. Открыли PDF с вирусом — он сразу имеет полный контроль над системой. Никаких защитных слоёв.
  3. Невозможно гарантировать ресурсы. Один процесс может занять весь CPU или всю память, и ОС не сможет это остановить.

ОС должна физически препятствовать программам делать опасные вещи. Не на уровне «давайте все договоримся», а на уровне «процессор откажется выполнять». Для этого нужна аппаратная поддержка.


Ring levels: модель защиты x86

Процессор x86 (и его наследник x86_64) имеет четыре уровня привилегий, называемых rings: ring 0, ring 1, ring 2, ring 3. Используются практически только два:

Ring levels x86 -- кто где живёт
Ring 0Самый привилегированный уровень. Может ВСЁ: писать в любой регистр, обращаться к любой физической памяти, переключать страницы, отвечать на interrupts. Linux kernel живёт тут
Ring 1Промежуточный. Изначально планировался для драйверов устройств. На практике почти не используется в современных ОС (Linux, Windows)
Ring 2Промежуточный. Тоже не используется в современных ОС. OS/2 пользовалась в 90-х, дальше отказались
Ring 3Самый низкий уровень привилегий. Не может писать в произвольную память, не может управлять прерываниями, не может переключать процессы. Тут живут ВСЕ ваши процессы

В реальности в Linux используются только ring 0 (kernel) и ring 3 (user). Между ними — пропасть. С точки зрения процессора, есть «kernel mode» и «user mode», и переход между ними строго контролируется.

Что может ring 3 (user mode)

Программа в user mode (ваш Python, браузер, что угодно) может:

  • Читать/писать свою память (только страницы, которые ядро ей выделило).
  • Выполнять обычные инструкции CPU (арифметика, jump’ы, вызовы функций).
  • Делать syscall — специальная инструкция для перехода в kernel mode и обратно.

Что НЕ может ring 3

Запрещены привилегированные инструкции. На x86_64 это:

  • hlt — halt CPU. Если программа сможет выполнить — ядро встанет.
  • cli / sti — disable/enable interrupts. Если запретить — системе крышка.
  • in / out — прямое обращение к I/O портам железа.
  • mov cr0/cr3/cr4 — запись в control registers (управление виртуальной памятью, FPU и т.п.).
  • lgdt / lidt — загрузка GDT/IDT (таблицы дескрипторов).
  • wrmsr / rdmsr — запись/чтение Model-Specific Registers.

Если процесс в ring 3 попробует выполнить такую инструкцию — процессор сгенерирует исключение #GP (General Protection Fault). Ядро поймает это исключение и убьёт процесс. Это происходит физически, аппаратно. Программист не может «обойти» защиту — железо проверяет.


Memory protection: tag на каждой странице

Защита не ограничивается инструкциями. Виртуальная память тоже несёт информацию о привилегиях. На каждую страницу памяти процессор знает:

  • Какие физические байты ей соответствуют (это виртуальная память — разбираем в M05).
  • Какой минимальный privilege level требуется для доступа.
  • Можно ли читать, писать, исполнять.
Memory access permissions для пользовательского процесса
User pagesСтраницы памяти, помеченные как user-accessible. Процесс в ring 3 может читать/писать (если есть права). Тут лежит код вашей программы, стек, heap
Kernel pagesСтраницы, помеченные как только для ring 0. Процесс в user mode НЕ МОЖЕТ их прочитать или записать. Тут код kernel, kernel data structures, страничные таблицы
Read-only pagesСтраницы, помеченные read-only. Можно читать, но не писать. Сюда mmap'ятся .text сегменты (код программы) и shared libraries. Попытка записи -> SIGSEGV
No-exec pagesСтраницы, помеченные no-exec (NX bit). Можно читать и писать, но нельзя исполнять. Защита от exploit'ов, которые внедряют код в стек или heap

Если процесс в ring 3 пытается прочитать страницу, помеченную как kernel-only — процессор немедленно генерирует page fault. Ядро ловит, убивает процесс (SIGSEGV). Это означает: даже если ваш Python-процесс знает физический адрес какого-то ядерного буфера — он не сможет туда дотянуться. Аппаратная защита.

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


Address space: где живёт что

На 64-bit x86, виртуальное адресное пространство процесса — 256 TB. Половина — user space, половина — kernel space, но второе помечено как «доступно только в ring 0».

Виртуальное адресное пространство процесса Linux x86_64
0xFFFFFFFF FFFFFFFFСамый высокий адрес. Сюда mapping для kernel (text, data, structures). Доступ только из ring 0
0xFFFF8000 00000000Начало kernel space. От этого адреса до самого верха -- ядро. 128 TB виртуального пространства, физически конечно меньше
Non-canonicalЗарезервированная зона между user и kernel. Адреса здесь невалидны -- любое обращение -> page fault. Так аппаратура отделяет два мира
0x00007FFF FFFFFFFFКонец user space. Под ним -- стек, выше mmap-зона, ниже heap, ниже text. У каждого процесса своё содержимое
0x00000000 00400000Типичный адрес начала программы. Тут .text (код), .data (инициализированные данные), .bss (нули)
0x00000000 00000000Самый низкий адрес. Обычно несколько страниц около 0 не mapping -- так NULL pointer-dereference даёт SIGSEGV вместо тихой записи

Когда процесс работает, его CPU видит обе половины (user и kernel) — но обращаться может только к user. Когда происходит syscall и процесс переходит в ring 0 — ядро может обращаться к kernel half. Это позволяет ядру быстро работать с памятью процесса (даже user pages видны kernel через ту же page table).

Можете увидеть распределение для вашего процесса:

cat /proc/self/maps

Вывод (сокращённо):

55c1a3d00000-55c1a3d04000 r--p 00000000 fd:00 1234567 /usr/bin/cat
55c1a3d04000-55c1a3d08000 r-xp 00004000 fd:00 1234567 /usr/bin/cat
55c1a3d08000-55c1a3d0b000 r--p 00008000 fd:00 1234567 /usr/bin/cat
55c1a3d0b000-55c1a3d0c000 r--p 0000a000 fd:00 1234567 /usr/bin/cat
55c1a3d0c000-55c1a3d0d000 rw-p 0000b000 fd:00 1234567 /usr/bin/cat
55c1a3e58000-55c1a3e79000 rw-p 00000000 00:00 0       [heap]
7fa1b8000000-7fa1b8024000 r--p 00000000 fd:00 7654321 /usr/lib/x86_64-linux-gnu/libc.so.6
7fa1b8024000-7fa1b81b9000 r-xp 00024000 fd:00 7654321 /usr/lib/x86_64-linux-gnu/libc.so.6
...
7ffd8f3e2000-7ffd8f404000 rw-p 00000000 00:00 0       [stack]

Все адреса — из user-half. Kernel-half (0xFFFF8000…) в выводе нет, потому что /proc/[pid]/maps показывает только user mappings.


Переход между мирами: syscalls

Если процесс в user mode не может делать привилегированных вещей — как он попадает в ядро, когда нужно прочитать файл или открыть сокет? Через системные вызовы (syscalls).

Что такое Linux: ядро, GNU и system calls

Syscall — это контролируемая «дверь» из user space в kernel space. Программа выполняет специальную инструкцию (syscall на x86_64, или int 0x80 исторически), которая:

  1. Переключает CPU в ring 0.
  2. Передаёт управление в строго определённую точку входа в ядре.
  3. Ядро смотрит, какой syscall был запрошен, и выполняет его.
  4. Возвращает результат в регистр RAX.
  5. Переключает CPU обратно в ring 3, возвращает управление в user code.
Жизненный цикл системного вызова

В детали этого процесса погружаемся в M11 (Syscalls Deep). Здесь важно понять: переход управления строго контролируется.

Berkeley sockets API -- основа всех сетевых программ Программа не может «перейти в kernel mode» в произвольной точке — только через syscall с конкретным номером. Это значит, что ядро не может быть скомпрометировано подменой адреса перехода: процессор всегда зайдёт в определённую точку.


Сколько занимает context switch

Переход user -> kernel -> user стоит времени. Несколько микросекунд минимум.

  • Сохранение регистров user процесса — ~20 регистров общего назначения + специальные.
  • Переключение виртуальной памяти — если syscall вызывает schedule (контекстный переключатель), меняются page tables.
  • TLB flush — буфер трансляции виртуальных адресов очищается. Следующие memory access будут медленнее, пока TLB не прогреется.
  • Возврат — восстановление состояния, переключение в ring 3.

На современных CPU один syscall стоит порядка 100-500 нс (если не делает реальной работы). Простой getpid() — около 50 нс. Если syscall блокируется (например, ждёт диск) — это уже миллисекунды или больше.

Поэтому критические по производительности программы (БД, kernel-bypass network stacks типа DPDK) стараются минимизировать syscalls. vDSO (см. M11) — это оптимизация, которая выполняет «лёгкие» syscalls вообще без перехода в kernel.


Mode bit и аппаратные регистры

С точки зрения CPU, информация «мы сейчас в ring 0 или ring 3» хранится в специальных регистрах — сегментных, в первую очередь в CS (Code Segment). Два младших бита CS — это CPL (Current Privilege Level), значения 0 или 3.

При выполнении инструкции CPU смотрит на CPL и на permissions требуемой ресурс (страницы памяти, регистра, инструкции). Если CPL > требуемый уровень — исключение.

Программа в user mode не может изменить CS. Это можно только через специальные инструкции, и они доступны только из ring 0. То есть процесс не может «повысить себе привилегии» — только через syscall, где ядро решает, что делать.

# Если у вас есть С компилятор, попробуйте программу, которая
# пытается выполнить привилегированную инструкцию -- получите SIGSEGV/SIGILL:
cat > /tmp/priv.c << 'EOF'
int main() {
    __asm__("cli");  // disable interrupts -- привилегированная
    return 0;
}
EOF
gcc /tmp/priv.c -o /tmp/priv
/tmp/priv
echo "exit code: $?"
# Получите segmentation fault или illegal instruction

Что если ядру нужны данные из user space?

Допустим, программа делает write(fd, "hello", 5). Ядро должно прочитать строку «hello» из user памяти. Но user pages помечены как user-accessible — может ли ядро их читать в ring 0?

Да. Ring 0 — privileged. Ему доступна вся память: и kernel pages, и user pages. Однако в Linux есть дополнительная защита — даже ядро не должно случайно следовать пользовательскому указателю. Для этого есть специальные функции:

  • copy_from_user(dest, src, n) — читает из user memory с проверкой.
  • copy_to_user(dest, src, n) — пишет в user memory с проверкой.
  • get_user(var, ptr) / put_user(val, ptr) — для одного значения.

Проверка такая: указатель должен быть в user range (ниже kernel-границы) и страница должна быть mapped. Если процесс прислал bogus pointer — syscall вернёт -EFAULT, а не свалит ядро.

Это защита от ошибок и атак: например, в старых ядрах баг в драйвере мог позволить читать kernel memory через специально подобранный pointer от user (Spectre/Meltdown-like атаки — эту тему расширим в M11).


Попробуй сам

Несколько экспериментов:

# 1. Посмотреть, сколько времени занимает syscall:
time strace -c -e getpid bash -c 'for i in {1..10000}; do :; done' 2>&1 | tail
# Каждый getpid() -- порядка 100ns

# 2. Посмотреть на kernel и user CPU usage:
top -bn1 | head -3
# Строка %Cpu(s): X us, Y sy -- "us" это user mode, "sy" это kernel mode (system)

# 3. Сколько процентов вашего CPU за всё время прошло в kernel:
cat /proc/stat | head -1
# user nice system idle iowait irq softirq -- первые две колонки в user, system -- в kernel

# 4. Посмотреть, какие страницы в памяти вашего процесса:
cat /proc/self/maps | head -20
# Все адреса -- в user range (ниже 0x800000000000)

# 5. Узнать через strace, что делает простая программа в kernel:
strace -c true 2>&1 | tail
# В колонках time, calls, syscall -- что вызывалось в kernel и сколько раз

Проверка знанийKnowledge check
Junior спрашивает: 'Если процесс не может выполнять привилегированные инструкции, как тогда работают такие штуки как Docker? Контейнер -- это же не VM, он работает в user mode, как все процессы. Но контейнер может монтировать файловые системы, изменять сетевые интерфейсы, запускать другие процессы под другими UID...'
ОтветAnswer
Отличный вопрос. Контейнеры (Docker, containerd) работают полностью в user mode -- они НЕ имеют ring 0 привилегий. Магия в Linux namespaces и capabilities. Namespaces -- это feature ядра Linux: вместо одной глобальной таблицы (например, PID, mount points, network interfaces), у каждого namespace своя копия. Когда вы создаёте mount namespace, ваш процесс видит свой список mounts. Когда mount/umount в нём -- это не влияет на host. Ядро enforce'ит: процесс может монтировать ТОЛЬКО в своём mount namespace. Это всё происходит через обычные syscalls (clone(2) с CLONE_NEWNS, CLONE_NEWNET и т.д.). Они выполняются в ring 0, как любой syscall. Ядро проверяет: 'есть ли у этого процесса разрешение на mount?'. Раньше mount требовал быть root (UID=0). Теперь Linux capabilities split это право на отдельные: CAP_SYS_ADMIN для mount, CAP_NET_ADMIN для сетевых интерфейсов, CAP_NET_RAW для raw sockets. Docker запускает контейнер с определённым набором capabilities (по умолчанию -- ограниченный). Внутри контейнера root (UID=0) имеет только эти capabilities, а не все. Поэтому контейнер может делать ifconfig в своём network namespace, но не может реально менять host network. Может mount tmpfs внутри своего mount namespace, но не /etc/passwd на host. User namespace идёт дальше: внутри контейнера процесс думает, что он UID=0 (root), а на host он на самом деле UID=1000 (обычный user). Это позволяет 'rootless containers' -- контейнеры без host root, что очень важно для безопасности (Podman это умеет, Docker rootless mode). Итог: контейнеры -- это user-mode абстракция над kernel features (namespaces, cgroups, capabilities). Привилегии всё равно проверяются ядром на каждом syscall. Capabilities заменяют грубое 'root vs не-root' на гранулярные разрешения. Это разбираем подробно в M10 (Permissions).

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 5. На каком уровне привилегий x86 работает ядро Linux и обычные процессы?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 4