Менеджер пам'яті delphi

Менеджер пам'яті delphi

Навіщо це потрібно? Адже, здавалося б, працює собі і працює, навіщо його чіпати? Це потрібно з кількох причин. По-перше, ніколи не завадить розбір чужого коду, особливо якщо це грамотний код. Це можливість навчитися чогось нового, а також отримати естетичну насолоду. По-друге, ніколи не зайве глибше розібратися в чомусь, переконатися в тих речах, в яких ви раніше не були впевнені або ж, навпаки, знайти слабкі місця, про які ви раніше і не підозрювали, щоб у майбутньому писати більш ефективний код .

Для розуміння роботи менеджера пам'яті Delphi вам знадобляться знання принципів роботи віртуальної пам'яті в Windows, а також розуміння принципів роботи чотирьох функцій: VirtualAlloc, VirtualFree, LocalAlloc і LocalFree. Ця інформація виходить за рамки даної статті, і може бути отримана або з книги Джеффрі Ріхтера "Windows для професіоналів, 4-е видання", або з MSDN.

Перш за все, домовимося про сенс використовуваних термінів. Основна плутанина виникає через те, що менеджер пам'яті Delphi сам використовує послуги менеджера віртуальної пам'яті Windows. Блок пам'яті, вільний з точки зору програми, може вважатися виділеним з точки зору Windows. Отже, фрагмент пам'яті будемо називати вільним, якщо він цілком складається зі сторінок вільної пам'яті в сенсі Windows. Фрагмент пам'яті будемо називати зарезервованим, якщо він цілком складається з сторінок пам'яті, зарезервованих додатком. Фрагмент пам'яті будемо називати виділеним, якщо він складається цілком зі сторінок пам'яті, під які виділено фізична пам'ять. Відповідно, фрагмент пам'яті будемо називати не виділеним, якщо він складається з сторінок, зарезервованих додатком, але під що не виділено фізичної пам'яті. Фрагмент пам'яті будемо називати використовуваним, якщо він був переданий Delphi-програми. І, нарешті, фрагмент пам'яті будемо називати невживаних, якщо під ним розташована фізична пам'ять, але в даний час він не використовується додатком Delphi. Вся ієрархія пам'яті представлена ​​на рис. 1.

Структура менеджера пам'яті Delphi

Менеджер пам'яті Delphi складається з чотирьох адміністраторів, а також налагоджувальних і діагностичних функцій. Схема залежності адміністраторів представлена ​​на малюнок 2. Розповімо коротко про кожну з них.

Перший з них - адміністратор описателей блоків. Його завдання - надати іншим адміністраторам функції для роботи з двонаправленими кільцевими списками. Як ми побачимо пізніше, вся інформація про виділену і зарезервованої пам'яті зберігається в таких списках. При цьому виникає проблема, яка полягає в тому, що елементи подібних списків необхідно створювати динамічно. Адміністратор описателей блоків вирішує цю проблему, звертаючись до головної купі процесу.

Адміністратор зарезервованої пам'яті відповідає за пам'ять, зарезервовану менеджером пам'яті Delphi. В силу особливостей реалізації менеджера віртуальної пам'яті Windows, цей адміністратор обробляє також запити на передачу і на повернення фізичної пам'яті в зазначеному регіоні.

Головною функцією адміністратора виділеної пам'яті є обробка запитів по виділенню і звільнення пам'яті. Він зберігає також інформацію про всю зарезервованої, але ще не виділеної пам'яті.

Адміністратор використовуваної пам'яті є самим низькорівневим адміністратором. Він безпосередньо спілкується з самим додатком Delphi, задовольняючи його запити на виділення і звільнення пам'яті. Він зберігає в собі інформацію про всю виділеної пам'яті.

Однією з особливостей роботи всіх адміністраторів (крім адміністратора описателей блоків) є та обставина, що вони працюють за принципом "якомога більше, не можна менше". При запиті адміністратор може виділити більше запитуваної кількості пам'яті, і вже адміністратор нижчого рівня повинен вирішувати, як йому використовувати залишок. Так, скажімо, при спробі зарезервувати область пам'яті розміром 100 байт (звернення від адміністратора виділеної пам'яті до адміністратора зарезервованої пам'яті), все одно буде зарезервований регіон в 1 Мб, і адміністратор виділеної пам'яті сам повинен буде вирішити, як йому чинити з залишком. Відповідно при спробі звільнення пам'яті адміністратор може звільнити менший фрагмент.

Зробимо також ряд зауважень з приводу схеми обробки помилок менеджером пам'яті Delphi. Якщо функція повертає виділений блок пам'яті, то в разі помилки повертається блок зі значенням addr, рівним nil. У разі помилки при звільненні пам'яті встановлюється глобальна змінна heapErrorCode. Крім того, ряд функцій, які повертають значення типу Boolean, сигналізують про те що помилку, повертаючи значення False.

Адміністратор описателей блоків

Майже вся інформація про зарезервованої, виділеної і невиділеної пам'яті зберігається у вигляді кільцевих двонапрямлених списків. Головною їх перевагою є те, що для видалення елемента з такого списку досить знати лише покажчик на цей елемент (див. Наприклад, функцію DeleteBlock). Адміністратор описателей блоків надає іншим адміністраторам функції для роботи з такими списками, а також для динамічного виділення в пам'яті елементів цих списків.

Відразу відзначимо, що кільцеві двонаправлені списки, які використовуються менеджером пам'яті Delphi, бувають двох типів. Основна відмінність типів - в способі виконання операцій вставки блоку в список і видалення блоку зі списку. При додаванні блоку в список першого типу, решта блоки в списку залишаються без змін. Як наслідок, видалити з такого списку ми можемо лише той блок, який був вставлений в нього раніше. При додаванні блоку в список другого типу перевіряється, чи не межує чи додається блок з іншими блоками в списку. Якщо це так, то відбувається "злиття" блоків, т. Е. Все межують між собою блоки замінюються одним. Відповідно, видалити з такого списку можна будь-який блок пам'яті, аби він містився всередині одного з блоків, присутніх в списку. В цьому випадку може статися "розщеплення" блоку на два. Використання списків другого типу дозволяє уникнути фрагментації пам'яті.

Розглянемо тепер динамічне виділення пам'яті під описатели. Всі дані, що відносяться до адміністратора описателей блоків, зберігаються у вигляді двох односпрямованих списків. У першому з них (blockDescBlockList) зберігаються самі описатели групами по сто. У другому списку (blockDescFreeList) зберігаються вільні зараз описатели, а в якості зв'язку між ними використовується поле next опису. Кожен з описателей знаходиться в одній зі структур типу TBlockDescBlock списку blockDescBlockList, а якщо описатель вільний, то також і в списку blockDescFreeList. Типове стан адміністратора описателей блоків наведено на малюнку 3.

Коротко опишемо функції адміністратора описателей блоків.

Ця функція додає вказаний блок пам'яті в двонаправлений кільцевий список другого типу (див. Вище). Значенням є блок пам'яті, що утворився в результаті злиття додається блоку з уже існуючими. У разі помилки значення addr в повернутому блоці встановлено в nil.

Ця функція видаляє блок пам'яті з кільцевого двонаправленого списку другого типу. У разі успіху функція повертає True.

Адміністратор зарезервованої пам'яті

Таким чином, адміністратор зарезервованої пам'яті містить єдиний кільцевої двонаправлений список spaceRoot, в якому зберігається опис всіх звернень до функції VirtualAlloc з метою резервування пам'яті. Спочатку цей список не містить елементів. При роботі адміністратора використовуються константи:

  • cSpaceAlign, що показує, з якої кордоні будуть вирівнюватися резервуються фрагменти пам'яті;
  • cSpaceMin, що показує мінімальний розмір блоку пам'яті, який буде зарезервований;
  • cPageAlign, що задає розмір сторінки.

Коротко опишемо призначення і особливості кожної з функцій, що входять до складу адміністратора.

Резервує регіон пам'яті розміром, як мінімум, рівним minSize байт. Повертає блок пам'яті, який був фактично зарезервований. У разі помилки повертає в поле addr блоку пам'яті значення nil. Зарезервована пам'ять має атрибут доступу PAGE_NOACCESS.

Ця функція має одну цікаву особливість, а саме, вона може зарезервувати фрагмент пам'яті, який має розмір менше запитуваної. Детальніше ця особливість буде розглянута при описі функції GetCommittedAt.

Адміністратор виділеної пам'яті

Адміністратор виділеної пам'яті зберігає інформацію про зарезервованої, але не виділеної пам'яті. Зверніть увагу на гру слів: адміністратор займається виділеної пам'яттю, але зберігає інформацію про не виділеної. Цей адміністратор обробляє запити на виділення пам'яті від адміністратора використовуваної пам'яті. Тому він зберігає інформацію про невиділеної, але зарезервованої пам'яті, щоб з цих запасів задовольняти потреби адміністратора використовуваної пам'яті. Якщо запасу не виділеної пам'яті не вистачає, адміністратор звертається до адміністратора зарезервованої пам'яті.

Адміністратор виділеної пам'яті містить один двонаправлений список decommittedRoot, в якому міститься інформація про всю зарезервованої, але не виділеної пам'яті. Спочатку список не містить елементів. Пам'ять додається і видаляється зі списку decommittedRoot за допомогою викликів MergeBlockAfter і RemoveBlock, таким чином, в цьому списку не може міститися двох суміжних фрагментів пам'яті (т. Е. Це список другого типу). Крім цього, в роботі адміністратора використовується одна константа - cCommitAlign, яка показує, з якої кордоні будуть вирівнюватися запити на виділення пам'яті.

Розповімо трохи про взаємодію між адміністраторами зарезервованої пам'яті і виділеної пам'яті. Для всіх фрагментів пам'яті, які входять в список decommittedRoot, адміністратор виділеної пам'яті виробляє передачу фізичній пам'яті, користуючись функцією Commit адміністратора зарезервованої пам'яті. Якщо ж для задоволення запиту адміністратору зарезервованої пам'яті не вистачає зарезервованої, але не виділеної пам'яті, він викликає GetSpace. Відповідно адміністратор виділеної пам'яті повертає фізичну пам'ять за допомогою виклику функції Decommit адміністратора зарезервованої пам'яті. Після цього зазначений фрагмент додається до списку decommittedRoot, і проводиться контрольна перевірка на зняття резервування з нового фрагмента пам'яті, утвореного злиттям вивільняється блоку пам'яті з тими, які вже перебували в списку decommittedRoot.

Слід звернути увагу на одну особливість поведінки функції GetCommittedAt адміністратора виділеної пам'яті. Ця функція в циклі викликає функцію GetSpaceAt до тих пір, поки розмір зарезервованого простору не перевищить запитуваний розмір (см. Особливості функції GetSpaceAt). Але при цьому даного фрагменту пам'яті буде відповідати кілька описателей в списку spaceRoot. Для чого це було зроблено? Згадаймо, що виклик функції VirtualAlloc для звільнення сторінки (прапор MEM_RELEASE) повинен в точності відповідати викликом VirtualAlloc, який резервував пам'ять. Таким чином, при черговому зменшенні розміру блоку (або при його звільненні), менеджер пам'яті буде в змозі повернути системі хоча б частину зарезервованих фрагментів, в яких розташовувався цей блок пам'яті.

Коротко опишемо кожну з функцій адміністратора виділеної пам'яті.

Виділяє область пам'яті розміром не менше minSize байт. Повертає блок пам'яті, який був фактично виділено. У разі помилки повертає в поле addr блоку пам'яті значення nil.

Схожі статті