Залізо очима ОС
Навіщо це
Section titled “Навіщо це”Практично кожна абстракція в цьому курсі спирається на щось конкретне в залізі й без цієї опори просто не існувала б.
Витісняльна багатозадачність можлива тому, що є таймер, здатний перервати процес. Ізоляція процесів можлива тому, що є MMU. Віртуальна пам’ять можлива тому, що апаратура вміє повідомити про відсутнє відображення. А захист від запису в чужу пам’ять реалізований не перевіркою в коді ядра, як можна було б подумати, а одним бітом у таблиці сторінок.
Цей модуль про той мінімум апаратури, без якого решта курсу перетворюється на заучування визначень.
Два режими виконання
Section titled “Два режими виконання”У процесора є рівні привілеїв. На x86 їх формально чотири, кільця 0–3, але реально використовуються два: режим ядра (кільце 0) і режим користувача (кільце 3).
У режимі ядра доступно все: перепрограмувати MMU, вимкнути переривання, звернутися до порту пристрою, зупинити процесор. У режимі користувача спроба виконати таку інструкцію не ігнорується, а викликає виняток, і керування переходить до ядра.
Перемкнутися в режим ядра можна трьома способами, і всі три передають керування коду ядра за наперед заданою адресою:
- системний виклик, коли програма свідомо просить обслуговування;
- виняток, коли програма зробила щось, чого апаратура не змогла виконати;
- переривання, тобто зовнішня подія, яка до програми взагалі не стосується.
Інструкції «перейти в кільце 0 і продовжити виконувати мій код» не існує. На цьому й тримається вся ізоляція.
Переривання і винятки
Section titled “Переривання і винятки”І те, і те передає керування ядру, але з різних причин.
Переривання приходить асинхронно, від пристрою. Диск дочитав блок, надійшов пакет, спрацював таймер. До того, що процесор виконує в цю мить, воно не має жодного стосунку.
Виняток, навпаки, синхронний: його спричинила саме та інструкція, яка зараз виконується. Ділення на нуль, звернення за недійсною адресою, привілейована інструкція в режимі користувача, сторінковий виняток.
Обробляються вони однаково. Процесор зберігає мінімальний контекст, за номером події бере адресу з таблиці векторів і переходить туди вже в режимі ядра.
Відрізняється те, що робити потім. Після переривання виконання продовжується з наступної інструкції, а після сторінкового винятку та сама інструкція виконується повторно, бо відображення тепер уже є.
Блок керування пам’яттю перетворює логічні адреси на фізичні при кожному зверненні до пам’яті. Він же перевіряє права: спроба записати в сторінку, позначену тільки для читання, дає виняток, а не тихий запис.
Ядру MMU потрібен одразу для трьох речей. Для ізоляції, бо процес фізично не може сформувати адресу до чужої пам’яті. Для переміщуваності, бо процес не знає, де він лежить насправді. І для віртуальної пам’яті, бо відображення може бути відсутнє, і про це повідомлять.
Деталі будуть у модулях 10 і 11,
а тут важливо тільки одне: це апаратура, і без неї захист пам’яті
неможливий у принципі. Мікроконтролери без MMU виконують увесь код
у спільному просторі, тому там немає ні процесів, ні SIGSEGV.
Ієрархія пам’яті
Section titled “Ієрархія пам’яті”Швидка пам’ять дорога, дешева повільна, і компроміс тут завжди один: кілька рівнів, де кожен наступний більший і повільніший за попередній.
Працює це тільки завдяки локальності. Часовій — до чого зверталися щойно, ймовірно, звернуться знову. І просторовій — якщо звернулися за адресою, скоро знадобиться сусідня. Обидві є емпіричними властивостями реальних програм, а не законом природи, тож програму, яка локальність порушує, кеші не врятують.
Через це кеш оперує не байтами, а рядками по 64 байти. Прочитати один байт фізично неможливо, приїде весь рядок. Наслідків для продуктивності звідси багато: обхід масиву за рядками швидший, ніж за стовпцями, а структура, що вміщається в один рядок, обробляється швидше за розкидану.
Ті самі два порядки, які відділяють оперативну пам’ять від SSD, пояснюють, чому процес переводять у стан «заблокований» замість того, щоб чекати: за 50 мікросекунд процесор виконав би сотні тисяч інструкцій.
Якби процесор переносив кожен байт із диска власноруч, копіювання гігабайта займало б його цілком. Контролер прямого доступу до пам’яті робить це самостійно: процесор задає адресу й розмір, а потім отримує одне переривання про завершення.
Наслідок побачите в модулі 13 — саме тому копіювання великого файлу майже не завантажує процесор.
Багато ядер
Section titled “Багато ядер”Одне ядро процесора виконує один потік. Щойно ядер стає кілька, з’являються задачі, яких на одному не існувало.
Почнімо з кешів. Кожне ядро має власний L1, спільним зазвичай лишається тільки L3. Якщо два ядра пишуть у той самий рядок кешу, апаратурі доводиться узгоджувати копії, і ця когерентність кешів коштує грошей. Звідси береться хибне спільне використання, коли дві цілком незалежні змінні випадково потрапили в один рядок і перетворили паралельний код на послідовний.
Далі — порядок операцій. Процесор виконує інструкції поза чергою і має буфери запису, тому записи стають видимими іншим ядрам не в тій послідовності, у якій виконувались. Через це й існують бар’єри пам’яті (модуль 9).
І нарешті NUMA. На кількох сокетах пам’ять фізично приєднана до конкретного процесора, тож звернення до «чужої» пам’яті йде через міжпроцесорний зв’язок і коштує помітно дорожче. Тому планувальник намагається тримати процес там, де лежить його пам’ять (модуль 7).
Як це насправді в Linux
Section titled “Як це насправді в Linux”lscpu | grep -E 'Model name|^CPU\(s\)|Thread|Core|Socket|NUMA|L1d|L2|L3'Ядра, потоки на ядро, сокети, розміри кешів і вузли NUMA — уся топологія, з якою працює планувальник.
getconf LEVEL1_DCACHE_LINESIZEРозмір рядка кешу. Саме це число визначає, з якою гранульованістю пам’ять їздить між рівнями — і на скільки байтів треба рознести дві змінні, щоб уникнути хибного спільного використання.
head -20 /proc/interruptsСкільки переривань кожного джерела обробило кожне ядро. Стовпці — ядра; перекіс тут означає, що одне ядро стане вузьким місцем.
grep -E 'LOC|RES|CAL|TLB' /proc/interruptsLOC — переривання локального таймера, ті самі, що дають витіснення.
Їхня кількість росте безперервно, поки система працює.
numactl --hardware 2>/dev/null || echo 'один вузол NUMA'Вузли NUMA і відносна вартість доступу між ними. На одному сокеті матриця буде тривіальною, на сервері — ні.
perf stat -e cache-misses,cache-references,instructions,cycles -- \ sh -c 'i=0; while [ $i -lt 300000 ]; do i=$((i+1)); done'Промахи кеша, інструкції й такти. Відношення інструкцій до тактів (IPC) менше за одиницю зазвичай означає, що процесор чекає на пам’ять, а не рахує.
Типові помилки розуміння
Section titled “Типові помилки розуміння”«Ядро постійно контролює виконання програм». Не контролює. Код процесу виконує сам процесор, без жодних посередників. Ядро отримує керування лише на системному виклику, винятку або перериванні, а в решту часу не бере участі взагалі.
«Переривання і виняток — те саме». Переривання приходить ззовні й асинхронно, виняток спричиняє конкретна інструкція. Після переривання виконання триває далі, після сторінкового винятку інструкція повторюється.
«Кеш читає байти». Кеш оперує рядками по 64 байти, і одного байта як одиниці обміну просто не існує. Через це порядок обходу масиву й впливає на швидкість у рази.
«Захист пам’яті реалізований у ядрі». Реалізований він в апаратурі. Ядро тільки заповнює таблиці, а перевіряє їх MMU при кожному зверненні.
«Багато ядер — просто швидше». До швидкості додаються когерентність кешів, переставляння операцій із пам’яттю і NUMA. Наївно розпаралелений код на 32 ядрах цілком може виявитися повільнішим, ніж на одному.
Перевір себе
Лабораторна
Section titled “Лабораторна”B2 — mini-top. Читання /proc/stat і /proc/interrupts: побачити
переривання таймера, розподіл переривань по ядрах і обчислити завантаження
за різницею лічильників.
Джерела
Section titled “Джерела”- Silberschatz, Operating System Concepts, розділ 1.2
- Таненбаум, Modern Operating Systems, розділ 1.3
- What Every Programmer Should Know About Memory — Ульріх Дреппер
- Latency Numbers Every Programmer Should Know
man 1 lscpu,man 8 numactl,man 1 perf-stat