Як ядро ​​управляє пам'яттю

    Раніше ми побачили як організована віртуальна пам'ять процесу. Тепер розглянемо механізми, завдяки яким ядро ​​управляє пам'яттю. Звернемося до нашої програми:
 image
 
У Linux процеси реалізовані у вигляді struct-об'єкта task_struct, який по суті є дескриптором процесу. У полі mm об'єкта task_struct міститься покажчик на т.зв. дескриптор пам'яті процесу, struct-об'єкт mm_struct, який містить вичерпну інформацію про те як даний процес використовує пам'ять. У дескрипторі пам'яті процесу зберігається інформація про початковий і кінцевий адресі сегментів процесу як показано на малюнку вгорі, число page-фреймів (фізичних сторінок в оперативній пам'яті), які використовуються процесом (це RSS або т.зв. Резидентний Набір Сторінок), кількість віртуальної пам'яті, виділеної процесу, і іншу дрібницю. Дескриптор пам'яті процесу також вказує на місцезнаходження дескрипторів VMA (virtual memory area або область віртуальної пам'яті) і набір page-таблиць для процесу. Останні дві структури даних це свого роду "робочі конячки", тому що вони задіюються при більшості операцій управління пам'яттю. Області віртуальної пам'яті для нашої програми вказані на малюнку:
 image
 
Область віртуальної пам'яті (VMA) являє собою безперервний діапазон віртуальних адрес; області ніколи не перекривають один одного. Примірник struct-об'єкта vm_area_struct вичерпно описує одну VMA, включаючи початковий і кінцевий віртуальний адреса області, прапори, що визначають права та інші особливості доступу до області, поле vm_file з інформацією про фото, відображеному в дану область (якщо такий файл є). Область віртуальної пам'яті, що не сопоставлена ​​ні з яким файлом, називається анонімною. Кожному з сегментів програми на вищенаведеному малюнку (купа, стек і т.д.) відповідає своя VMA; виняток у цьому відношенні состовляет тільки т.зв. сегмент для меппірованія (memory mapping segment). Дане положення речей не є якоюсь вимогою або чимось визначеним, але у випадку з платформою x86 це в більшості випадків так. Області віртуальної пам'яті все одно якого сегменту відповідати.
 
Набір VMA для даного процесу описаний відразу двома способами. По-перше, в дескрипторі пам'яті процесу (struct-об'єкт mm_struct) є покажчик mmap на зв'язний список дескрипторів VMA (порядок дескрипторів в списку відповідає порядку проходження VMA у віртуальному просторі). По-друге, все в тому ж дескрипторі пам'яті є покажчик mm_rb на структуру, яка представляє собою red-black tree. RB-дерево дозволяє ядру швидко встановлювати факт знаходження деякого віртуального адреси в межах тієї чи іншої віртуальної області. Якщо подивитися вміст файлу / proc / pid_of_process / maps у файловій системі proc, то це буде ніщо інше як інформація, отримана ядром в результаті "проходу" по зв'язков списку дескрипторів VMA.
 
У Windows, блок EPROCESS — це грубо кажучи що то середнє між структурами task_struct і mm_struct. Аналогом дескриптора області віртуальної пам'яті є Virtual Address Descriptor або VAD, інформація про ці дескрипторах зберігатися в AVL-дереві. Знаєте що найсмішніше при порівнянні Windows і Linux? Це те, що відмінностей як раз не так вже й багато.
 
4-гігабайтний віртуальний адресний простір представляється у вигляді послідовності сторінок. 32-бітові процесори x86 підтримують розмір сторінок рівний 4 КБ, 2 МБ і 4 МБ. Linux і Windows використовують 4-кілобайтні сторінки для user space-частини віртуального адресного простору. Байти 0-4095 потрапляють в сторінку # 0, байти 4096-8191 потрапляють в сторінку # 1 і т.д. Розмір області VMA повинен бути кратний розміри сторінки. Ось як виглядає 3-гігабайтний user space простір організоване за допомогою 4-кілобайтних сторінок:
 image
 
Процесор консультується з page-таблицями для того, щоб здійснити перетворення ВА у фізичну. У кожного процесу є свій набір таких page-таблиць; як тільки відбувається перемикання процесу (context switch), змінюються і page-таблиці для user space-частини віртуального адресного простору. У Linux, покажчик на page-таблиці ходу зберігатися в полі pgd дескриптора пам'яті процесу. Кожній віртуальній сторінці відповідає один запис у page-таблиці, і, у випадку з класичним x86-пейджинг, це проста 4-байтовая запис, показана на наступному малюнку:
 
 image
У ядрі Linux є функції, які дозволяють звести або обнулити будь прапор в page table запису. Прапор «P» говорить про те, чи знаходиться сторінка в оперативній пам'яті чи ні. Коли даний прапорець встановлений в 0, доступ до відповідної сторінки викличе page fault. Потрібно врахувати, що якщо даний прапорець встановлений в 0, то ядро ​​може як завгодно використовувати залишилися біти в page table запису. Прапор «R / W» означає запис / читання; якщо прапор не встановлений, то до сторінки можливий доступ тільки на читання. Прапор «U / S» означає користувач / супервайзер; якщо прапор не встановлений, тільки код виконується з рівнем привілеїв 0 (тобто ядро) може звернутися до даної сторінки. Таким чином, дані прапори використовуються для того, щоб реалізувати концепцію адресного простору доступного тільки на запис і простору, яке доступне тільки для ядра.
 
Прапори «D» і «A» означають «dirty» і «accessed». "Dirty"-сторінка — ця та, в яку була недавно проведена запис, а «accessed»-сторінка — це сторінка, до якої було здійснено звернення (читання або запис). Обидва прапора є "липкими", процесор може їх встановити, але не буде обнуляти — робити це повинно ядро. Нарешті, page table запис зберігає початковий фізичну адресу сторінки в пам'яті; адреса завжди буде кратний 4 КБ. Це здавалося б невинне поле є причиною багатьох проблем, тому воно фактично обмежує розмір адресується фізичної пам'яті 4 гігабайтами. Інші поля page table записи розглянемо якось іншим разом, так само як і механізм Physical Address Extension.
 
Захист пам'яті здійснюється на посторінковою основі, оскільки сторінка — це найменший "шматочок" пам'яті, для якого можна виставити прапори «U / S» і «R / W». Варто однак враховувати, що теоретично, дві різні віртуальні сторінки, що мають відрізняється набір прапорів, можуть відповідати одній і тій же фізичній сторінці. Зауважте, у форматі page table записи не передбачені прапори, пов'язані з забороною на виконання коду. Саме тому класичний x86-пейджинг ніяк не перешкоджає виконанню коду в стеці, що полегшує експлуатування вразливостей, в основі яких переповнення буфера в стеку (невиконувані стеки все одно схильні вразливостям, в даному випадку використовується техніка return-to-libc та інші прийоми). Отсутсвие no-execute прапора також свідчить про іншому важливому аспекті: прапори доступу, що містяться в дескрипторі VMA, не завжди мають прямі відповідності в системі захисту, реалізовується процесором, і можуть лише відповідати цій системі в більшій чи меншій мірі. Образно кажучи, ядро ​​робить все, що в його силах, але в кінцевому рахунку архітектура процесора накладає свої обмеження на те, що можливо реалізувати.
 
Звичайно ж віртуальна пам'ять сама по собі нічого не зберігає. Це просто абстракція або механізм, в якому віртуальна пам'ять певним чином поставлена ​​у відповідність фізичної пам'яті. Операції з адресною шиною процесора є досить нетривіальними, але ми зараз можемо від цього абстрагуватися. Будемо вважати, що процесор працює з діапазоном послідовних адрес від нуля до максимально доступного в системі адреси (залежно від кількості оперативної пам'яті) і може при необхідності звернутися до будь-якого байту в цьому діапазоні. Фізичне адресний простір представляється у вигляді послідовності фізичних сторінок (їх ще називають page-фреймами). Процесору мало діла до page-фреймів, а от для ядра вони дуже важливі, тому що page-фрейм — одиниця обліку та управління фізичною пам'яттю, яке і здійснюється ядром. 32-бітові версії Linux і Windows використовують 4-кілобайтні page-фрейми; ось приклад машини з 2 ГБ оперативної пам'яті:
 
 image
 
Ядрі Linux веде облік кожному page-фрейму за допомогою спеціального дескриптора і декількох прапорів. Взяті разом, ці дескриптори описують всю оперативну пам'ять комп'ютера; в кожен момент часу відомо точний стан будь-якого page-фрейма. В основі управління фізичною пам'яттю лежить алгоритм Buddy memory allocation. Таким чином, page-фрейм вважається вільним, якщо він доступний для виділення з погляду Buddy-алгоритму. Виділений під використання page-фрейм може бути "анонімним" (в такому випадку він містить дані програми) або він може перебувати в т.зв. сторінковому кеші (page cache) і зберігати порцію даних з деякого файлу або блокового пристрою. Існують і інші, більш екзотичні варіанти використання page-фреймів, але давайте не будемо їх зараз чіпати. У Windows є аналогічна структура для обліку page-фреймів і називається вона Page Frame Number база даних.
А тепер, давайте збираючи воєдино всі ці концепції — області віртуальної пам'яті (VMA), page table-запису й page-фрейми — і подивимося як це працює. Далі йде приклад купи в user space програми:
 
 image
 
Прямокутники з блакитним фоном позначають віртуальні сторінки, що знаходяться в межах області віртуальної пам'яті. Стрілки позначають page table-записи, за допомогою яких віртуальні сторінки "меппіруются" в page-Фремо (фізичні сторінки). У деяких віртуальних сторінок немає стрілок; це означає, що у відповідних їм page table записах прапор присутності встановлений в 0. Причиною тому може бути те, що дані віртуальні страіци можливо не разу ще не використовувалися або ж тому, що відповідні їм фізичні сторінки були вивантажені в своп. У кожному разі, спроба доступу до цих сторінок призведе до page fault, навіть незважаючи на те, що віртуальні сторінки знаходяться в межах деякої VMA. Може здатися дивним, що існує подібного роду різночитання — сторінки в межах VMA і проте доступ до них є невалідним — але так дійсно часто відбувається.
 
VMA представляє собою свого роду "контракт" між програмою і ядром. Ви просите ядро ​​виконати яку-небудь дію (наприклад, виділити пам'ять або замеппіровать файл), ядро ​​каже "без проблем" і створює нову або оновлює існуючу VMA. Але ядро ​​при цьому не поспішає виконувати саму прохання, замість цього ядро ​​відкладе безпосереднє виконання запитаного дії до того моменту поки не трапиться page fault. Так, виходить, що ядро ​​- це такий собі "ледачий негідник"; це є основоположним принципом управління віртуальною пам'яттю. Даний принцип застосовується в більшості ситуацій — деякі з них можуть бути цілком знайомими, деякі — несподіваними, але загальне правило такого, що VMA лише фіксує, то про що було домовлено, в той час як page table-запису відображають те, що безпосередньо було зроблено ледачим ядром. Ці дві структури разом учавствуют в управлінні пам'яттю програми; обидві структури відіграють певну роль при обробці page fault, вивільненні пам'яті, вивантаженні сторінок в своп і т.д. Розглянемо простий випадок виділення пам'яті:
 
 image
 
Коли програма запитує виділення додаткової пам'яті за допомогою системного виклику brk (), ядро ​​просто напросто оновлює інформацію в дескрипторі VMA і на цьому вважає своє завдання виконаним. У даний момент часу не відбувається ні виділення нових page-фреймів, ні розміщення їх в оперативній пам'яті. Однак, як тільки програма спробує звернутися з сторінці, процесор відловити page fault і буде викликаний оброблювач do_page_fault (). Дана функція здійснить пошук VMA, в межах якої знаходиться адресу, звернення до якого викликало page fault. Якщо така VMA існує, то далі перевіряються прозводится перевірка на відповідність між правами доступу на VMA і те, спроба якого доступу виробляється (доступ на читання або запис). Якщо ж підходящої VMA немає, тоді немає і "контракту", який передбачав би здійснювану спробу звернення до пам'яті. В останньому випадку, процесу посилається сигнал Segmentation Fault і він завершується.
 
Припустимо, VMA таки знайшлася. Подальша обробка page fault така — ядро ​​дивиться на вміст page table запису і тип VMA. У нашому прикладі, page table запис свідчить про те, що сторінки в пам'яті немає. Більше того, наша запис абсолютно порожня (складається з одних нулів), і в Linux це означає, що відповідна віртуальна сторінка взагалі ще жодного разу не була замеппірована. Оскільки ми маємо справу з "анонімної" VMA, то всі подальші дії будуть пов'язані тільки з Опреатівная пам'яттю і для обробки даної ситуації викликати функцію do_anonymous_page (). Дана функція проводить виділення page-фрейма і меппірует в нього віртуальну сторінку шляхом внесення потрібних даних в page table запис.
 
Справа могли обстоять й інакше. Page table запис для вивантажених в своп сторінки, наприклад, має прапор присутності встановлений в нуль, але інша частина запису не порожня. Решта біти зберігають інформацію про знаходження сторінці в своп. Функція do_swap_page () зчитує вміст цієї сторінки з диска і завантажує сторінку в оперативну пам'ять — це тип page fault називають major fault.
 
На цьому завершимо першу частину нашого екскурсу в те, як ядро ​​управляє пам'яттю. У наступній статті ми усложним картину, доповнивши її роботою з файлами, щоб у нас склалося повне уявлення про основні концепції управління пам'яттю, включаючи і деякі аспекти продуктивності.
 
Матеріал підготовлений співробітниками компанії Smart-Soft. Переклад статті How the Kernel Manages Your Memory by Gustavo Duarte
 smart-soft.ru
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.