![]() |
![]() |
Память, как ресурс операционной системыВсё, сказанное ранее относительно виртуальной памяти касалось только аппаратной возможности. Для того, чтобы программа могла действительно воспользоваться виртуальной памятью (иллюзией очень большого количества памяти реальной) требуется, как мы видели, ещё и поддержка со стороны какого-то слоя промежуточного программного обеспечения – кто-то ведь должен обеспечить и обработчик страничной ошибки, и управление доступом к файлу подкачки и переключение процессов... Словом, все описанные ранее аппаратные возможности предназначены только для одного-единственного пользователя, который в состоянии ими воспользоваться – для операционной системы. А операционная система – и сама является источником абстракций. Поэтому программа пользователя "видит" не совсем то, что обеспечивает аппаратура процессора, а лишь то, что обеспечивает ей операционная система. При этом, в идеале, лучше бы, чтобы программа пользователя вообще ничего этого не видела, а пребывала в счастливом обмане, что она и в самом деле исполняется на машине с огромным количеством реальной оперативной памяти – проблем переносимости уже существующих программ в новое операционное окружение было бы меньше. Но, при всей желательности такого исхода, так не получается. Не получается по ряду причин (которые мы рассмотрим ниже), поэтому скрыть операционной системе сам факт того, что она поддерживает виртуальную память – невозможно. И соответствующий интерфейс управления самым нижним слоем системной абстракции, называемой "виртуальное адресное пространство процесса" система вынуждена программе пользователя обеспечивать, а программа пользователя совершенно законно может им воспользоваться. Над этим слоем абстракций можно надстроить и другие – например, "кучу динамической памяти", поэтому всякая программа и не обязана непременно управлять "своей виртуальной памятью", но может положиться на другие "абстракции памяти", тоже реализованные самой операционной системой. Тем не менее, все они начинаются с абстракции данной – виртуального адресного пространства процесса, ниже которой доступных программе пользователя абстракций нет. Рассмотрим её. Традиционно "адресное пространство процесса" определяется как "диапазон доступных процессу адресов памяти". Не утверждается, что все эти адреса доступны безусловно. Возможно, что для корректного доступа к каким-то из них требуется некая специальная процедура (выделение памяти). Не утверждается также и того, что обращение по любому из них обязательно должно быть успешно – возможно, попытка обратиться по адресу Х приведёт к немедленному прерыванию программы (примером такого "доступного и некорректного" адреса является 0x00000000 ), но важно, что программа потенциально может использовать этот диапазон, т.е. породить адрес. Операционная же система предоставляет программе пользователя некий набор элементов в виде которых это "пространство" процессу видимо и им используемо. На рис. 5 приведена "карта" (map) адресного пространства, как оно обеспечивается операционной системой. Видно, что существует несколько моделей. Одна модель поддерживается ядром 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 , сказанным их функциональность отнюдь не исчерпывается. Успешно исполнившаяся функция возвращает начальный адрес региона, который системой "зарезервирован", а вообще в отношении любой страницы виртуальной памяти могут быть применены три значения её состояния (статуса):
Когда память больше не нужна её возврат системе осуществляет функция 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 бит может оказаться если не "разрядностью на века", то уж точно – на многие десятилетия.
Авторские права © 2001 - 2004, Михаил Безверхов Публикация требует разрешения автора |
|