Пам'ять, як ресурс операційної системи

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

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

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

На рис. 5 приведена "карта" (map) адресного простору, як воно забезпечується операційною системою.

рис 5. Карта розподілу адресного простору процесу в операційних системах Windows

Видно, що існує кілька моделей. Одна модель підтримується ядром Windows NT (і наступних модифікацій), а друга - ядром Windows 95 (і наступних модифікацій). Різниця між моделями і взагалі їх наявність в кількості більше однієї пояснюється тим, що це різні операційні системи. Різні саме з точки зору їх внутрішнього устрою. Операційна система Windows NT в повній мірі використовує апаратний механізм захисту привілеїв що виконується коду, Windows 95 не робить цього. В силу цього виклик драйвера в Windows 95 проходить легше і простіше, але ціною наявності принципово доступної програмі "дірки" в системний код. У Windows NT дозволяти собі такого роду вольності - гарантовано нарватися на переривання і аварійне завершення програми по порушенню захисту пам'яті, зате і виклик системних функцій проходить з перемиканням привілеїв, тобто виклик виконується "важче". Оскільки виклик функції це завжди "перехід за адресою", будь то "своя" або системна функція, природно, що цей пристрій хоч якось, але відбивається і на моделі адресного простору. Втім, відома і третя модель - в Microsoft Windows NT Advanced Server і Microsoft Windows NT Server Enterprise Edition системний розділ просто скорочений на 1 Гб, а розділ "приватна власність процесу" збільшений до 3 Гб - все-таки сервер!

Дамо пояснення до малюнка. В обох моделях існує кілька діапазонів адрес, які загальне доступне простір логічно ділять на кілька якісно відрізняються областей. І кордони областей в обох моделях - не збігаються. Єдино "корисною" програмі користувача областю є "приватна власність процесу". Інші області - допоміжні. Вони, за винятком області "розділ для операційної системи", вийшли внаслідок якихось додаткових вимог і обмежень. Наприклад, при організації циклу вихід індексу за оголошену кордон на одиницю - нерідке подія. Часте подія і звернення по порожньому вказівником. Мабуть тому в моделі Windows NT область користувача знизу і зверху обмежена "буферними зонами" в яких ніколи не може розташовуватися "правильної пам'яті", а, значить ці помилки ще під час налагодження проявлять себе. При розробці Windows 95 потрібно було забезпечити сумісність з MS DOS, тому в складі моделі з'явилися спеціалізовані області, явно зайві з точки зору саме моделі ізольованого адресного простору.

Також звертає на себе особливу увагу - операційна система "з'їдає" для себе половину (2 Гб) адресного простору. Оскільки ми маємо справу з мультизадачной операційною системою, яка може підтримувати відразу кілька "ізольованих адресних просторів", оскільки операційна система і сама - процес, то це може викликати деяке здивування - чому так багато і як тоді потрібно було б розуміти "операційну систему" в процесі користувача?

Це, в даній операційній системі, - питання складне. За часів "старої доброї DOS" всі програми виконувалися в одному адресному просторі, а операційна система була просто додатковим кодом, який резидентно завантажувався в пам'ять і служив чимось на зразок "додаткової бібліотеки коду", яку використовувала исполняющаяся програма користувача. Ця "бібліотека коду" реалізовувала функції, знати реалізацію яких програмі було не обов'язково, а то і зовсім шкідливо - в силу залежності від апаратури і т.п. обставин. Оскільки робила це DOS часто дуже погано, то програма користувача "на свій страх і ризик" могла DOS проігнорувати і всі необхідні дії (включаючи управління апаратурою) виконати самостійно - додатковий код він і є просто додатковий код. DOS не володіла "самостійної волею" (тобто не утворювала ніяких потоків для свого виконання) і взагалі кожна програма, що виконувалася під DOS не тільки могла думати, що вона - єдина исполняющаяся програма в системі (і, відповідно, - монопольно розпоряджатися ресурсами машини), а так воно насправді і було. Багато погані звички програмістів відбуваються з тих часів.

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

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

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

Сказане може бути не всім зрозуміло і тут ми підходимо до дуже цікавого обставині. "Діапазон адрес" і "пам'ять" - різні речі! Хоча, якщо вдуматися, нічого незвичайного в цьому немає - не дивує же нас, що значення адреси - може бути, а пам'яті за цією адресою - бути не може (0x00000000)? Реальна пам'ять - то, що дійсно може запам'ятовувати і відтворювати дані, - в системі з віртуальною пам'яттю існує абсолютно окремо від способу, яким до неї звертаються. Це ми дізналися з попереднього розділу. А "адресний простір" - ніяка не "пам'ять", але тільки спосіб доступу! І для того, щоб "способу доступу" відповідав "об'єкт доступу" в системі з віртуальною пам'яттю потрібно зробити додаткове і явне дію - вказати відповідність одного іншому. Іншими словами, потрібно явно вказати операційній системі, що діапазону адрес від M до N вона повинна зіставити реальні байти ОЗУ. А наведені вище "карти" - не більше, ніж схема в якому діапазоні адрес у операційної системи можна вимагати надання реальної пам'яті, а в якому - вимагати цього не можна.

Чому зроблено так? Давайте уявимо собі, а чи можна було зробити інакше ... Уявімо собі, що при старті процесу система це відображення робить сама і прозоро для програми користувача. Оскільки заздалегідь система нічого не знає про те скільки і де програмі знадобиться пам'яті вона буде змушена "отмаппіровать" все. Програма може звернутися до двох гігабайтам? Може. Ось два гігабайти система і буде попередньо розміщувати. Як ми бачили з попереднього розділу - розміщувати ці два гігабайти вона буде в файлі підкачки, тобто його мінімальний розмір для запуску одного процесу повинен бути не менше 2 Гб. Для запуску одночасно двох процесів - 4 Гб, адже простору-то - ізольовані. На машині, на якій пишуться ці рядки, зараз одночасно запущено 35 процесів (велика частина яких - спить і на роботу не впливає), але це не означає, що ця машина укомплектована більш, ніж 70 Гб вінчестером. Навпаки, ця машина цілком обходиться файлом підкачки об'ємом в 400 Мб, тобто не те, що одному процесу, а й усім процесам разом більше 400 Мб ніколи не потрібно.

Саме це - необхідність в іншому випадку жахливо розбазарювати ресурси і не дозволяє приховати від програми користувача "віртуальність" пам'яті. Зобов'язувати програму ставити явні запити на виділення необхідної реальної пам'яті - виявляється не просто "економічніше", а - це єдино економічно прийнятне рішення.

Як це робиться? Досить просто. Адресний простір "згідно наведеній карті" - священна приватна власність процесу. І як його процес всередині самого себе розподіляє - справа процесу. Процес викликає з себе спеціальну функцію API VirtualAlloc і повідомляє цим викликом операційній системі, що він хотів би зарезервувати регіон - діапазон адрес від M до N, якому він може бути захоче зіставити деяку фізичну пам'ять. Регіон не може бути переривчастим і суміжні регіони не можуть перетинатися. За цим стежить система і вона не дасть зарезервувати пересічні регіони. Резервування регіону призводить всередині операційної системи до створення якихось структур, пов'язаних з управлінням пам'яттю і за своєю суттю аналогічно запиту до функції malloc - якщо комусь виданий адресу даного регіону, то нікому іншому його вже не видадуть.

Резервування регіону означає резервування пам'яті. Але саме по собі не означає того, що ця пам'ять надається "в натурі". Для того, щоб в цьому регіоні з'явилися сторінки пам'яті реальної необхідно викликати функцію VirtualAlloc і вказати, що регіону потрібно надати сторінки реальної пам'яті. Система виділить запитане кількість сторінок - вона фізично заведе їх в сторінковому файлі підкачки і зв'яже з даним регіоном. Так що звернення за цими адресами буде викликати сторінкову помилку і далі - як описано в попередньому розділі. Відзначте - можна завести дуже великий регіон і виділити йому тільки кілька сторінок реальної пам'яті, причому - не обов'язково підряд, а саме "в тих місцях", де це потрібно.

Чому процес зроблений в дві ступені? Головним чином тому, що фізична пам'ять, в самому буквальному сенсі, - дорогоцінний ресурс. Програмі, в принципі, може знадобитися дуже багато пам'яті (що вона резервуванням регіону і відзначає), але в даний конкретний момент їй може вимагатися зовсім малесеньке цієї пам'яті кількість (що вона відзначає вимогою надати регіону сторінки). А, оскільки вони і ті ж фізичні сторінки ОЗУ обслуговують собою всі процеси виконуються в системі в даний момент, то поділ "вимог взагалі" і "вимог в даний момент" призводить до кращого використання фізичної пам'яті в сукупності.

Сказане ілюструється, вже став хрестоматійним, прикладом. Припустимо, нам потрібно сформулювати електронні таблиці - Excel, в просторіччі. Є лист, що складається з N стовпців і M рядків. У кожному осередку може розташовуватися значення довжиною не більше 1К (у реального Excel - 255 символів). Якщо у нас N = 20 і M = 100 (досить скромно), то для того, щоб цю сторінку уявити рядком байтів потрібно 20 * 100 * 1K = 20000К ~ 20Мб пам'яті (порівняйте з 128Мб фізичної пам'яті взагалі і з розміром виконуваного файлу всього реального Excel в 5-6 Мб). При цьому - аж ніяк не в кожній клітинці таблиці розташовується 1K інформації. Деякі осередки - взагалі порожні. Якщо цю таблицю просто описувати масивом, що дуже зручно з точки зору поводження з нею з програми, то у нас буде дуже неефективно використовується пул пам'яті в 20Мб розміром. Якщо ж ми зарезервуємо регіон в 20 Мб, але фізичну пам'ять виділимо лише тим осередкам в яких є якесь значення, то ми - дозволимо це протиріччя. У нас, з точки зору програми, як і раніше буде масив в 20 Мб, але реально пам'яті (системних ресурсів) цього масиву буде виділено значно менше. А решта ресурси в даний конкретний момент часу - будуть використані там, де вони дійсно потрібні, можливо - в іншому процесі. При спробі звернення до тих осередків масиву, яким не виділено сторінок реальної пам'яті виникне сторінкова помилка. Оскільки система цю сторінкову помилку в даному випадку виправити не в змозі вона "підніме" її до рівня виключення в програмі, програма виняток перехопить і ... запросить (VirtualAlloc) у операційної системи виділення сторінок фізичної пам'яті тим осередкам таблиці, які цього вимагають. Система - виділить пам'ять, програма - повернеться з обробника виключення і знову встановиться оптимальне для всіх рівновагу.

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

Тут відразу ж прокоментуємо це ще раз. Сказане вичерпно пояснює, що в файлі підкачки не зберігається "адресний простір". Тобто файл на диску не є простим двійковим чином ОЗУ, як це часом прийнято думати. У файлі підкачки зберігаються тільки образи тих сторінок, яким в розмічених адресних просторах всіх одночасно виконуються процесів відповідають розподілені сторінки фізичної пам'яті. Саме тому файл підкачки відповідає сумарній потреби у фізичній пам'яті (400 Мб max), а не кратний 2-м Гб на процес - зберігаються образи сторінок, а не "адресний простір"!

Відповідно, нужно хоч немного Сказати и про роботу з функцією VirtualAlloc. Буває, що саме безпосереднє використання абстракції "віртуальна пам'ять" є найкращим з усіх можливих рішень. І - нічого "дуже складного" в цьому немає. Використання VirtualAlloc - не складніше використання malloc, хоча, звичайно, їх функціонування забезпечується різними верствами проміжного програмного забезпечення.

Прототип VirtualAlloc описується в MSDN так:

LPVOID VirtualAlloc (LPVOID lpAddress, // регіон, який бере участь в операції SIZE_T dwSize, // розмір регіону DWORD flAllocationType, // тип розміщення DWORD flProtect // тип захисту доступу);

а параметри функції визначаються так:

lpAddress - вказує стартовий адресу регіону, який приймає участь в операції. Адреса регіону повинен бути вирівняний на кордон 64K (не завжди, але подробиці - в MSDN). Якщо все одно в якому місці розташовувати регіон, а цікавить тільки резервування регіону зазначеної довжини, то параметр може бути NULL.

dwSize - вказує в байтах розмір регіону. Розмір регіону повинен бути кратний розміру сторінки даного процесора - на різних процесорах він може бути різний. З'ясувати поточний розмір сторінки даного процесора можна за допомогою функції API GetSystemInfo. Якщо розмір не кратний сторінці, то система сама його округлити до потрібного в більшу сторону (не завжди саме так, але подробиці - в MSDN).

flAllocationType - прапорці, які визначають дію, яке має здійснити VirtualAlloc. Точні подробиці (прапорців там багато) - в MSDN, але, наприклад, прапорець MEM_RESERVE наказує зарезервувати регіон, а прапорець MEM_COMMIT - виділити сторінки фізичної пам'яті регіону. Прапорці можна і скомбінувати, якщо це потрібно, і виконати і резервування і виділення пам'яті за один виклик функції.

flProtect - вказує прапорці доступу до сторінки, якими повинні володіти сторінки регіону. Сучасний процесор вміє відрізняти "доступ на запис" від "доступу на читання", а всередині "читання" розрізняє ще "читання даних" і "вибірку команд". Вказуючи ці обмеження можна запрограмувати виняток "порушення доступу" (memory access violation), коли, наприклад, до сторінок коду буде застосована спроба прочитати їх як дані. Подробиці вживання прапорців - в MSDN, сказаним їх функціональність аж ніяк не вичерпується.

Успішно виконав функція повертає початкову адресу регіону, який системою "зарезервований", а взагалі щодо будь-якої сторінки віртуальної пам'яті можуть бути застосовані три значення її стану (статусу):

  • free - діапазон адрес цієї сторінки жодним чином не пов'язаний жодними обмеженнями
  • reserved - сторінка належить якомусь зарезервувати регіону, але з нею не пов'язане ніяких сторінок фізичної пам'яті
  • commited - зі сторінкою пов'язана фізична пам'ять

Коли пам'ять більше не потрібна її повернення системі здійснює функція API VirtualFree:

BOOL VirtualFree (LPVOID lpAddress, // адреса регіону SIZE_T dwSize, // розмір регіону DWORD dwFreeType // тип операції);

Параметри у неї ті ж самі, що і параметри функції VirtualAlloc - адреса регіону, розмір і прапорці, що потрібно зробити з вказаною пам'яттю. Хоча, звичайно, значення прапорців не збігаються з значеннями прапорців функції VirtualAlloc. Однак, оскільки в нашій статті описуються тільки концептуальні можливості, всі подробиці - в MSDN.

Природно, що в системі є і функція за допомогою якої можна просто з'ясувати стан сторінки пам'яті з даними адресою, її ім'я - VirtualQuery. Є й третя функція, яка дозволяє легко змінювати прапорці доступу до сторінок - VirtualProtect.

Cказав стосувалося тільки "свого адресного простору". Тим часом, в системі є принаймні одна програма, яку дуже цікавить що робиться в адресному просторі чужому. Ім'я цієї програми - відладчик, його призначення - розглядати чужі ресурси, як свої власні. А, оскільки "стандартна схема" ізолює адресні простори, то в системі є спеціальні функції API, які підтримують "здорове цікавість» - не діяти же отладчику в обхід системних правил? Ці функції - ReadProcessMemory і WriteProcessMemory. Вони дозволяють читати і записувати байти в адресному просторі іншого процесу, як в своєму власному. Правда не завжди, а лише тоді, коли піддослідний процес це дозволяє, але це вже відноситься, скоріше, до теми управління процесами.

І останній штрих до даного рівня абстракції. Життя не стоїть на місці. Колись і 4 Гб пам'яті здавалося астрономічної величиною. Нині ж можна знайти такі області додатків в яких це - аж ніяк не гранична величина. Великі бази даних, наприклад, потужні сервери ... Тому Microsoft завбачливо розвиває свою модель доступу до пам'яті. З'явилося такий засіб, як Address Windowing Extensions (AWE), яке дозволяє адресувати більше, ніж 4 Гб фізичної пам'яті в рамках 32-хбітового адресного простору. Зводиться вона до того, що організовується свого роду "вікно" з прямо відображаються в регіон "позасистемних сторінок" фізичної пам'яті. Читачі зі стажем можуть пригадати, що свого часу існували подібні технології LIM EMS і XMS, які користувалися тим же самим прийомом, але відносно сторінок пам'яті за межею в 1 Мб - тодішнім межею процесора 8086. У точності подібно до того рішенню і AWE є рішення "паліативне" - доступ до цих сторінок "не зовсім такий", як до сторінок "штатної пам'яті". Але навряд чи цю проблему можна вирішити "системно" - справжня її причина полягає в тому, що бракує не сторінок фізичної пам'яті, а самого адресного простору. Стало бути і пролноценное її рішення знайдеться тільки у сімейства процесорів більшої розрядності. 64 біта адреси дозволяють адресувати 2 64 = 2 34 Гігабайт (чотири гігабайти гігабайт!) Пам'яті в рамках єдиного адресного простору, що, очевидно, виснажиться ще не скоро. У всякому разі думається, що розрядність в 128 біт може виявитися якщо не "розрядністю на століття", то вже точно - на багато десятиліть.

Авторські права & copy 2001 - 2004, Михайло Безверхов
Публікація вимагає дозволу автора


Але два гігабайти це - багато?
Чи багато в даний історичний час відомо програм, яким дійсно життєво необхідно хоча б два гігабайти оперативної пам'яті?
Чому зроблено так?
Програма може звернутися до двох гігабайтам?
Як це робиться?
Чому процес зроблений в дві ступені?

Дополнительная информация

rss
Карта