Ядро і межа user/kernel
Навіщо це
Section titled “Навіщо це”printf("hi") виглядає як звичайний виклик функції. Насправді там
відбувається перехід між двома світами з різними правами, зі збереженням
контексту, перевіркою аргументів і поверненням назад, і коштує це
приблизно у сто разів більше.
Якщо розуміти цю межу, стає ясно чимало речей, які інакше здаються
довільними: чому буферизований ввід швидший за небуферизований, навіщо
взагалі з’явився io_uring, чому мікроядра програли і чому strace
показує зовсім не те, що написано у вихідному коді.
Передумови. Режими виконання і переривання (модуль 2).
Що відбувається при системному виклику
Section titled “Що відбувається при системному виклику”Програма кладе номер виклику й аргументи у визначені регістри та виконує
інструкцію syscall. Процесор перемикає режим і передає керування
за адресою, яку ядро зареєструвало ще при завантаженні. Не за тією,
яку вказала програма: інакше вся ізоляція була б декоративною.
Далі ядро зберігає контекст, за номером знаходить обробник у таблиці й перевіряє аргументи. Це не формальність: вказівник, переданий процесом, цілком може вказувати в пам’ять ядра, і ядро зобов’язане таке помітити.
На поверненні контекст відновлюється, режим користувача повертається, а результат лягає в регістр.
Скільки це коштує
Section titled “Скільки це коштує”Звичайний виклик функції займає одиниці наносекунд, системний — сотні. Після мітигацій Meltdown і Spectre (модуль 16) ще більше, бо перемикання таблиць сторінок стало обов’язковим.
Ця різниця в сто разів пояснює чимало:
printfбуферизує вивід і робить одинwriteзамість сотні, тому вивід у файл і вивід на термінал мають різну швидкість;epollзамістьselectекономить не обчислення, а самі виклики;io_uringузагалі з’явився заради того, щоб прибрати системний виклик із кожної операції (модуль 13);- vDSO — це невеликий шматок коду ядра, відображений у кожен процес,
щоб дешеві запити на кшталт
gettimeofdayобходилися без перетину межі.
Що всередині ядра
Section titled “Що всередині ядра”Хоч би яка була архітектура, ядро містить приблизно одні й ті самі підсистеми:
| Підсистема | Відповідає за | Модуль |
|---|---|---|
| Керування процесами | створення, стани, планування | 6, 7 |
| Керування пам’яттю | таблиці сторінок, виділення, заміщення | 10–12 |
| Віртуальна файлова система | єдиний інтерфейс до різних ФС | 15 |
| Підсистема вводу-виводу | драйвери, черги запитів | 13 |
| Мережевий стек | сокети, протоколи, маршрутизація | — |
| Безпека | права, ізоляція, аудит | 16 |
Питання архітектури — не «що робить ядро», а що з цього виконується в режимі ядра, а що винесено назовні.
Монолітне ядро й мікроядро
Section titled “Монолітне ядро й мікроядро”Монолітне ядро тримає всі підсистеми в одному адресному просторі режиму ядра, тож виклик між ними є звичайним викликом функції. Це швидко, але ізоляції всередині немає, і помилка в будь-якому драйвері здатна зруйнувати всю систему.
Мікроядро лишає в режимі ядра тільки мінімум: планування, базове керування пам’яттю і передавання повідомлень. Драйвери, файлові системи й мережевий стек виносяться в окремі процеси режиму користувача, тому збій драйвера вбиває один процес, який можна перезапустити.
Платити за це доводиться тим, що колишній виклик функції перетворюється на обмін повідомленнями через ядро. Одне читання файлу коштує вже кількох перемикань контексту. Історично ця вартість і вирішила суперечку на користь монолітних ядер.
Гібридні ядра, як-от Windows NT або XNU у macOS, декларують мікроядерну структуру, але з міркувань швидкості тримають критичні підсистеми в режимі ядра.
Протилежна крайність — уніядро, де застосунок і ядро збираються в один образ і працюють в одному адресному просторі без поділу на режими. Ізоляції там немає взагалі, бо єдиний застосунок нема від кого ізолювати, зате немає й вартості переходів. Ніша в них вузька: віртуальні машини, де ізоляцію забезпечує гіпервізор.
Як це насправді в Linux
Section titled “Як це насправді в Linux”strace -c -- cat /etc/hostnameЗведення системних викликів одного запуску. Видно, що більшість часу припадає на кілька викликів, а решта — накладні витрати на запуск.
strace -e trace=write -- sh -c 'printf "a"; printf "b"; printf "c"' > /dev/nullТри printf дають три виклики write на термінал і один — у файл,
бо в файл libc буферизує. Це і є вартість межі, помітна неозброєним оком.
ltrace -e 'printf' -- /bin/echo hi 2>&1 | head -3Виклики бібліотеки — інший рівень. Порівняння виводу ltrace і strace
показує, де саме проходить межа.
grep -c . /usr/include/asm/unistd_64.h 2>/dev/null || \ ausyscall --dump 2>/dev/null | wc -lСкільки всього системних викликів існує. Число росте з кожним ядром і майже ніколи не зменшується — через зобов’язання не ламати ABI.
cat /proc/self/maps | grep -E 'vdso|vsyscall'vDSO, відображений у кожен процес. Саме звідти беруться gettimeofday
і clock_gettime без переходу в режим ядра.
lsmod | head; modinfo $(lsmod | awk 'NR==2{print $1}') 2>/dev/null | head -5Завантажені модулі ядра й опис одного з них.
dmesg --level=err,warn | tail -5Повідомлення ядра. Помилка в модулі проявляється саме тут — і, на відміну від помилки в програмі, здатна вплинути на всю систему.
Типові помилки розуміння
Section titled “Типові помилки розуміння”«Виклик функції з libc — це системний виклик». libc працює
в режимі користувача, і printf може взагалі обійтися без системного
виклику, якщо дані помістилися в буфер.
«Системний виклик — це просто виклик функції ядра». Це перемикання режиму зі збереженням контексту й перевіркою аргументів, і коштує воно приблизно у сто разів більше за звичайний виклик.
«Root виконує код у режимі ядра». Не виконує. Процес від root
працює в режимі користувача так само, як будь-який інший, просто ядро
дозволяє йому більше на своїх системних викликах.
«Модулі роблять Linux мікроядром». Модуль виконується в адресному просторі ядра з повними правами. Мікроядро виносить драйвери в окремі процеси режиму користувача, і це принципово інша модель.
«Мікроядра надійніші, тому їх треба використовувати всюди». Надійніші, і платять за це вартістю обміну повідомленнями. Там, де надійність важливіша за пропускну здатність, вони й використовуються: QNX в автомобілях, seL4 в авіації.
Перевір себе
Лабораторна
Section titled “Лабораторна”A1 — власний shell спирається саме на цей матеріал: кожна дія оболонки
є системним викликом, і strace на власній програмі показує це наочно.
Джерела
Section titled “Джерела”- OSTEP, Limited Direct Execution
- Silberschatz, Operating System Concepts, розділ 2
man 2 syscall,man 2 syscalls,man 7 vdso- The Linux Kernel documentation — розділ про системні виклики
- Tanenbaum–Torvalds debate — суперечка про мікроядра, 1992