Если вы впервые учитесь программированию под Windows и используете для этого Windows 95, то вы — счастливый человек. Вы даже не представляете себе, как вам повезло. В самом деле, основной урок, который вы извлечете из этой главы, можно сформулировать кратко следующим образом: при работе с памятью или файлами вам редко (а, может быть, и никогда) придется использовать что-либо кроме функций из стандартной библиотеки времени выполнения языка C.

Причина, по которой рекомендуется использовать библиотечные функции C (такие как malloc, free, fopen, fclose и т. д.), состоит в том, что они просты и понятны, и, кроме того, вероятно, вам хорошо знакомы. Но самое главное заключается в том, что у вас не возникнет никаких проблем при использовании этих функций в программах, написанных для Windows 95. Как будет показано ниже, так было далеко не всегда.

Управление памятью и файловый ввод/вывод являются очень старыми услугами, которые предоставляет программам операционная система (например, старая добрая неграфическая MS DOS). Третьей услугой является подсистема выполнения, которая в MS DOS поддерживала простейшую загрузку из файла в память и запуск на выполнение одной задачи. Кроме этих трех услуг, четвертой важной, может считаться отслеживание даты и времени.

Набор системных услуг, поддерживаемых ядром Windows 95, гораздо более широк. Он включает в себя динамическое связывание (оно будет рассмотрено в главе 19), многозадачность, многопоточность и синхронизацию потоков (глава 14), связь между процессами (главы 16, 17 и 20), а также некоторые другие услуги, которые не включены в данную книгу.

Хотя использовать библиотечные функции языка C удобно, возможно, в принципе, написание программы для Windows 95 вообще без использования этих функций. Каждая библиотечная функция, которая требует обращения к операционной системе (такие как функции управления памятью или файлового ввода/вывода) имеет соответствующую, и, как правило, более развитую и гибкую функцию операционной системы. Какой путь выбрать — использование функций библиотеки языка C или функций операционной системы — дело ваше. Можете испробовать оба варианта и сравнить.

Управление памятью: хорошо, плохо
и ужасно

Для того чтобы увидеть, насколько далеко продвинулась вперед Windows за последние десять лет, достаточно интересно и полезно ознакомиться с минимальными требованиями к компьютеру для работы Windows 1.0 выпуска ноября 1985 года: 320 Кбайт памяти, операционная система MS DOS 2.0 и выше, два дисковода, графическая видеокарта. Эти требования отражают тип компьютера, на котором в то время работало большинство пользователей. Оглядываясь назад, можно сказать, что Microsoft добилась совершенно уникального результата, заставив Windows работать в такой ограниченной среде. Управление памятью в Windows 1.0 было очень странным, часто даже ужасным, но оно работало, по крайней мере, большую часть времени.

Сегментированная память

Windows 1.0 была разработана для микропроцессоров Intel 8086 и Intel 8088. Это были 16-разрядные микропроцессоры, способные адресовать 1МБ памяти. В компьютерах, совместимых с IBM PC, верхние 384 КБ этой памяти резервировались для памяти видеоадаптера и системного BIOS. При этом для программ и данных оставалось ничтожно мало — всего 640 КБ памяти.

Для того чтобы адресовать 1 МБ памяти требуется 20-разрядный адрес (220 = 1048576). В процессорах 8086 и 8088 этот 20-разрядный адрес формировался из двух 16-разрядных значений: компоненты сегмента и компоненты смещения внутри сегмента. Микропроцессор имел четыре сегментных регистра: кода, данных, стека и дополнительный. 20-разрядный физический адрес получался сдвигом сегмента влево на 4 разряда и добавлением к полученной величине смещения:

Сегмент:         ssssssssssssssss0000

+          Смещение:      0000oooooooooooooo

=          Адрес:             aaaaaaaaaaaaaaaaaaaa

Таким образом строится 20-разрядный адрес, с помощью которого можно адресовать до 1 МБ памяти (220).

Если сегментные регистры содержат константы, то программа использует только 16-разрядные смещения для доступа к коду и данным. (В соответствии с архитектурой языка C сегмент стека устанавливался равным сегменту данных, используемому для хранения статических данных.) Каждый из этих двух сегментов давал возможность адресации 64 КБ памяти. Для однозадачных операционных систем, где программам требовалось только 64 КБ для кода и 64 КБ для данных, этого было достаточно.

Вместе с тем, по мере того, как прикладные программы становились более сложными, и следовательно, большими по объему, появлялась необходимость во множестве сегментов для кода и данных. Это заставило производителей компиляторов языка C определить близкие (near) указатели, имевшие величину 16 бит, и используемые для доступа к сегментам кода и данных по умолчанию, и дальние (far) указатели, которые имели ширину 32 бита, и состоявшие из смещения и сегмента. Однако, с помощью этого 32-разрядного адреса нельзя было адресовать память непосредственно. Кроме того, нельзя было увеличить адрес на 1 без учета логики, обрабатывающей переполнение смещения и установку сегментного адреса. Производители компиляторов языка C определили различные модели программирования: маленькая (small) (один сегмент кода, один сегмент данных), средняя (medium) (много сегментов кода), компактная (compact) (много сегментов данных), большая (large) (много сегментов кода и данных), огромная (huge) (аналогично большой, но со встроенной логикой обработки увеличения адреса).

Сама MS DOS не имела достаточно большой поддержки управления памятью. Многие программы ранних версий MS DOS просто определяли, какое количество памяти имеется в их распоряжении, и использовали ее всю целиком. В те годы программисты гордились собой тем больше, чем лучше они могли использовать персональный компьютер, обходя ограничения операционной системы.

Поскольку Windows 1.0 была многозадачной средой, появилась необходимость расширить возможности управления памятью по сравнению с возможностями MS DOS. Подумайте, пожалуйста, над утверждением: в то время, как множество программ загружаются в память, позднее освобождают ее, память становится фрагментированной. Операционная система должна перемещать блоки памяти, чтобы объединить свободное пространство. Другими словами, многозадачность без управления памятью существовать не может.

Как это может быть реализовано? Вы не можете просто перемещать блоки кода и данных в памяти безотносительно прикладной программы. В этом случае она будет содержать неправильный адрес. И вот здесь сегментированная память показывает свои возможности. Если программа использует только смещения, то сегменты могут быть изменены операционной системой. Именно это и осуществлялось в ранних версиях Windows.

Одним из следствий этого было то, что программы для Windows были ограничены использованием только маленькой и средней моделями памяти с одним 64-килобайтным сегментом для данных. Программы использовали близкие указатели для ссылок на свой сегмент данных; адрес сегмента данных для конкретного процесса устанавливался операционной системой при передаче управления программе. Это позволяло Windows перемещать сегмент данных программы и переустанавливать адрес сегмента. Все дальние вызовы функций, выполняемые программой (включая вызовы функций операционной системы), выполнялись тогда, когда сегменты кода, используемые программой, были перемещены в память.

Программа, выполнявшаяся в 16-разрядной версии Windows, могла либо выделять память из собственного сегмента данных (называемого локальной памятью, адресуемой с помощью 16-разрядного указателя смещения), либо за пределами своего сегмента данных (эта область памяти называлась глобальной, и адресовалась с помощью 32-разрядных адресов). В обоих случаях функции выделения памяти возвращали описатель блока памяти. Программы должны были фиксировать блок в памяти. Функции фиксации возвращали дальний указатель. После использования памяти следовало снять фиксацию с блока памяти. Это давало Windows возможность перемещать блоки памяти при необходимости. Процесс фиксации и снятия фиксации часто выполнялся в процессе обработки одного сообщения, и часто приводил к многочисленным ошибкам и головной боли для программистов.

Как и в других случаях, описатели памяти, в действительности, были просто указателями на таблицу в ядре Windows. Ранние версии Windows содержали сегмент памяти, названный BURGERMASTER, который содержал главную таблицу описателей памяти. Этот сегмент был так назван в честь излюбленного ресторанчика разработчиков ранних версий Windows, который располагался на противоположной стороне высокоскоростной магистрали от первых офисов фирмы Microsoft в Вашингтоне. (Если посетить этот ресторанчик, то можно заметить, что разработчики Windows должны были быть очень хорошими бегунами, чтобы пересекать столь оживленное шоссе без происшествий.)

Существует все-таки одна причина, по которой важно обсуждение этой темы. Она состоит в том, что в Windows 95 содержатся некоторые части описанной выше схемы адресации Windows 1.0. Архитектура Windows 95 довольно сильно скрывает это, но иногда, это все-таки проявляется в структуре и синтаксисе вызовов функций.

Проблемы с ближними и дальними адресами в 16-разрядных версиях Windows переносились и на файловый ввод/вывод. Во многих случаях программа получала от диалогового окна имя файла, которое являлось дальним указателем. Но в маленькой и средней моделях памяти библиотечные функции файлового ввода/вывода C ожидали ближнего указателя. Аналогично, программа, которая хранила данные в блоках глобальной памяти, должна была адресовать их с помощью дальних указателей и, как следствие, файловый ввод/вывод в эти блоки был затруднен. Возникала необходимость, чтобы Windows дублировала вызовы всех функций файлового ввода/вывода, написанных для MS DOS.

Промежуточные решения

Очевидно, что обсуждение этого вопроса уже возбудило неприятные воспоминания у ветеранов программирования в MS DOS и Windows. Другим неприятным воспоминанием является, вероятно, спецификация отображаемой памяти (Expanded Memory Specification), разработанная фирмами Lotus, Intel и Microsoft (LIM EMS). Для реализации этой спецификации обычно использовалась специальная плата, содержащая память, которая могла быть адресована 16-килобайтными блоками через 64-килобайтное окно, располагавшееся в верхней зоне памяти, не занятой платой видеоадаптера и ROM BIOS. Различные блоки памяти размером 16 КБ могли быть включены/выключены в/из окно. Windows 2.0 поддерживала спецификацию LIM EMS, имеющую несколько функций.

К тому времени, как версия Windows 3.0 получила широкое распространение, у Microsoft появилась возможность поддерживать защищенный режим (protected mode) процессора Intel 286 (вместо реального режима процессоров 8086 и 8088, рассмотренного выше), причем без существенных проблем для уже существовавших программ. В защищенном режиме сегментный адрес называется селектором (selector). Он также имеет ширину 16 бит, но внутри 286 процессора он ссылается на 24-разрядный базовый адрес (base address), который складывается затем с 16-разрядным смещением, и таким образом, формируется 24-разрядный физический адрес, с помощью которого можно адресовать до 16 МБ памяти:

            Селектор                           ssssssssssssssss

 

               Таблица
        дескрипторов

 

            База:                bbbbbbbbbbbbbbbbbbbbbbbb

+          Смещение:      00000000oooooooooooooooo

=          Адрес:             aaaaaaaaaaaaaaaaaaaaaaaa

Чтобы обеспечить переход от более ранних версий Windows, описатель, возвращаемый функцией выделения глобальной памяти, был просто селектором. Такое использование защищенного режима отчасти облегчало управление памятью, но истинная цель еще не была достигнута.

И, наконец, 32 бита

Windows 95 требует наличия микропроцессоров Intel 386, 486 или Pentium. Эти микропроцессоры используют 32-разрядную адресацию памяти и, следовательно, могут реализовать доступ к 232, т. е. 4 294 967 296 байтам (или 4 ГБ) физической памяти. Конечно, большинству пользователей Windows еще очень далеко до этого предела. В соответствии с официальной информацией, операционная система Windows 95 требует всего 4 МБ памяти, но рекомендуется 8 МБ. На сегодняшний день 16 МБ памяти считается достаточным для обеспечения необходимого свободного пространства для большинства приложений.

Несмотря на то, что микропроцессоры Intel 386, 486 или Pentium могут использовать сегментную адресацию памяти, Windows 95 сохраняет неизменными сегментные регистры и использует 32-разрядную плоскую (flat) адресацию памяти. Это означает, что адреса в приложении Windows 95 хранятся как простые 32-разрядные величины, обеспечивая доступ к 4 ГБ памяти.

Используемые прикладными программами Windows 95 32-разрядные адреса для доступа к коду и данным, не являются 32-разрядными физическими адресами, которые микропроцессор использует для адресации физической памяти. Адрес, который используется приложением, называется виртуальным адресом (virtual address). Он преобразуется в физический адрес посредством таблицы страниц (page table). Этот процесс обычно прозрачен для прикладных программ. Программе кажется, что она расположена в 32-разрядном адресном пространстве, и для доступа к ней не требуется никаких особых усилий. Однако, в технической документации по Windows 95 существуют ссылки на виртуальные адреса и таблицы страниц. Поэтому, полезно будет рассмотреть механизм виртуальной памяти.

Физическая память делится на страницы (pages) размером 4096 байт (4 КБ). Следовательно, каждая страница начинается с адреса, в котором младшие 12 бит нулевые. Машина, оснащенная 8 МБ памяти, содержит 2048 страниц. Операционная система Windows 95 хранит набор таблиц страниц (каждая таблица сама представляет собой страницу) для преобразования виртуального адреса в физический.

Каждый процесс, выполняемый в Windows 95, имеет свою собственную страницу каталога (directory page) таблиц страниц, которая содержит до 1024 32-разрядных дескриптора таблиц страниц. Физический адрес страницы каталога таблиц страниц хранится в регистре CR3 микропроцессора. Содержимое этого регистра изменяется при переключении Windows 95 управления между процессами. Старшие 10 бит виртуального адреса определяют один из 1024 возможных дескрипторов в каталоге таблиц страниц. В свою очередь, старшие 20 бит дескриптора таблицы страниц определяют физический адрес таблицы страниц (младшие 12 бит физического адреса равны нулю). Каждая таблица страниц содержит, в свою очередь, до 1024 32-разрядных дескриптора страниц. Выбор одного из этих дескрипторов определяется содержимым средних 10 битов исходного виртуального адреса. Старшие 20 бит дескриптора страницы определяют физический адрес начала страницы, а младшие 12 бит виртуального адреса определяют физическое смещение в пределах этой страницы.

Очевидно, что это сложно понять с первого раза. Проиллюстрируем этот процесс еще раз в символьной форме. Вы можете представить 32-разрядный виртуальный адрес (с которым оперирует программа) в виде 10-разрядного индекса в таблице каталога таблиц страниц (d), 10-разрядного индекса в таблице страниц (p), 12-разрядного смещения (o):

dddd-dddd-ddpp-pppp-pppp-oooo-oooo-oooo

Для каждого процесса микропроцессор хранит в регистре CR3 (r) старшие 20 бит физического адреса таблицы каталога таблиц страниц:

rrrr-rrrr-rrrr-rrrr-rrrr

Начальный физический адрес каталога таблиц страниц определяется как:

rrrr-rrrr-rrrr-rrrr-rrrr-0000-0000-0000

Запомните, что каждая страница имеет размер 4 КБ и начинается с адреса, у которого 12 младших бит нулевые. Сначала микропроцессор получает физический адрес:

rrrr-rrrr-rrrr-rrrr-rrrr-dddd-dddd-dd00

По этому адресу содержится другое 20-разрядное значение (t-table):

tttt-tttt-tttt-tttt-tttt

соответствующее начальному физическому адресу таблицы страниц:

tttt-tttt-tttt-tttt-tttt-0000-0000-0000

Затем, микропроцессор осуществляет доступ по физическому адресу:

tttt-tttt-tttt-tttt-tttt-pppp-pppp-pp00

Здесь хранится 20-битная величина, являющаяся основой для физического адреса начала страницы памяти (f-page frame):

ffff-ffff-ffff-ffff-ffff

Результирующий 32-разрядный физический адрес получается в результате комбинирования основы физического адреса страницы и 12-разрядного смещения виртуального адреса:

ffff-ffff-ffff-ffff-ffff-oooo-oooo-oooo

Это и есть результирующий физический адрес.

Может показаться, что для преобразования виртуального адреса в физический требуется много времени, но на самом деле, это не так. Микропроцессоры Intel 386, 486 и Pentium имеют внутреннюю кэш-память, в которой могут храниться таблицы страниц. Фактически, преобразование адреса осуществляется очень быстро без каких-либо существенных потерь производительности. Такое двухступенчатое разделение памяти на страницы дает каждому приложению теоретическое ограничение по памяти в 1 миллион страниц по четыре килобайта каждая.

Преимущества разделения памяти на страницы огромны. Во-первых, приложения изолированы друг от друга. Никакой процесс не может случайно или преднамеренно использовать адресное пространство другого процесса, т. к. он не имеет возможности его адресовать без указания соответствующего значения регистра CR3 этого процесса, которое устанавливается только внутри ядра Windows 95.

Во-вторых, такой механизм разделения на страницы решает одну из основных проблем в многозадачной среде — объединение свободной памяти. При более простых схемах адресации в то время, как множество программ выполняются и завершаются, память может стать фрагментированной. В случае, если память сильно фрагментирована, программы не могут выполняться из-за недостатка непрерывной памяти, даже если общего количества свободной памяти вполне достаточно. При использовании разделения на страницы нет необходимости объединять свободную физическую память, поскольку страницы необязательно должны быть расположены последовательно. Все управление памятью производится с помощью манипуляций с таблицами страниц. Потери связаны только собственно с самими таблицами страниц и с их 4 КБ размером.

В-третьих, в 32-битных дескрипторах страниц существует еще 12 бит, кроме тех, которые используются для адреса страницы. Один из этих битов показывает возможность доступа к конкретной странице (он называется битом доступа, "accessed bit"); другой показывает, была ли произведена запись в эту страницу (он называется битом мусора, "dirty bit"). Windows 95 может использовать эти биты для того чтобы определить, можно ли сохранить эту страницу в файле подкачки для освобождения памяти. Еще один бит — бит присутствия (present bit) показывает, была ли страница сброшена на диск и должна ли быть подкачена обратно в память.

Другой бит ("чтения/записи") показывает, разрешена ли запись в данную страницу памяти. Этот бит обеспечивает защиту кода от "блуждающих" указателей. Например, если включить следующий оператор в программу для Windows:

* (int*) WinMain = 0 ;

то на экран будет выведено следующее окно сообщения:

"This program has performed an illegal operation and will be shutdown." ("Эта программа выполнила недопустимую операцию и будет завершена"). Этот бит не препятствует компилированной и загруженной в память программе быть запущенной на выполнение.

Приведем несколько замечаний по поводу управления памятью в Windows 95:

Виртуальные адреса имеют разрядность 32 бита. Программа и данные имеют адреса в диапазоне от 0x00000000 до 0x7FFFFFFF. Сама Windows 95 использует адреса от 0x80000000 до 0xFFFFFFFF. В этой области располагаются точки входа в динамически подключаемые библиотеки Windows 95.

Общее количество свободной памяти, доступной программе, определяется как количество свободной физической памяти плюс количество свободного места на жестком диске, доступного для свопинга страниц. Как правило, при управлении виртуальной памятью Windows 95 использует алгоритм LRU (least recently used) для определения того, какие страницы будут сброшены на диск. Бит доступа и бит мусора помогают осуществить эту операцию. Страницы кода не должны сбрасываться на диск: поскольку запись в его страницы запрещена, они могут быть просто загружены из файла с расширением .EXE или из динамически подключаемой библиотеки.

Иногда вы можете заметить, что происходит обращение к диску во время перемещения мыши из рабочей области одной программы в рабочую область другой программы. Почему это происходит? Windows 95 должна посылать сообщения о передвижении мыши второму приложению. Если программа, выполняющая обработку этого сообщения, не находится в данный момент в памяти, то Windows загружает ее с диска. Если запущено несколько больших приложений одновременно, а в компьютере немного памяти, то вы можете стать свидетелем чрезмерного количества обращений к диску в моменты перехода от одной программы к другой, т. к. Windows вновь загружает с диска ранее удаленные страницы. Время от времени отдельные программы будут замедляться (или приостанавливаться на какое-то время), пока Windows осуществляет свопинг.

Страницы кода могут разделяться между приложениями. Это особенно полезно для динамически подключаемых библиотек. Несколько программ, выполняемых одновременно, могут использовать одни и те же функции Windows 95, не требуя, чтобы один и тот же код загружался в память несколько раз. Достаточно одной копии.

При динамическом выделении памяти каждый выделяемый блок необязательно получает свою собственную страницу. Последовательное выделение небольших объемов памяти производится в одной и той же физической странице с ближайшего 4-разрядного стартового адреса (т. е. для выделения одного байта используется 16 бит). Если какой-либо блок расширяется, то он может быть физически перемещен в памяти, если следующий за ним блок памяти занят.

Кроме разделения памяти на страницы по 4 КБ физическая память не может стать безнадежно фрагментированной, поскольку дефрагментация заключается только в манипуляциях с таблицами страниц. Однако виртуальная память конкретного приложения может стать фрагментированной, если приложение осуществляет выделение, освобождение, повторное выделение и освобождение слишком большого числа блоков памяти. Ограничения в 2 ГБ обычно достаточно для приложения и его данных. Но возможно, что программа столкнется с нехваткой физической памяти до того, как будет достигнут предел виртуальной памяти. Если вы считаете, что это может случиться с вашей программой, то вам стоит подумать об использовании перемещаемой (moveable) памяти. Об этом будет рассказано ниже в этой главе.

И в заключение, после всех предварительных замечаний, наш совет остается тем же: используйте библиотечные функции C везде, где это возможно. В вашем распоряжении имеется 32-разрядное адресное пространство.

Выделение памяти

Достаточно ли убедил вас совет использовать библиотечные функции C для выделения памяти? Для того чтобы подкрепить это утверждение, начнем с краткого обзора.

Библиотечные функции C

Вы можете определить в программе указатель (например, на массив целых чисел) следующим образом:

int *p ;

Указатель p — 32-разрядное число, которое неинициализировано. Вы можете выделить блок памяти, на который будет указывать p, следующим образом:

p = (int *) malloc (1024) ;

При этом выделяется блок памяти размером 1024 байта, который может хранить 256 32-разрядных целых. Указатель, равный NULL, показывает, что выделение памяти не было успешным. Можно также выделить такой блок памяти, используя следующий вызов:

p = (int *) calloc (256, sizeof (int)) ;

Два параметра функции calloc перемножаются и в результате получается 1024 байта. Кроме того, функция calloc производит обнуление блока памяти.

Если вам необходимо увеличить размер блока памяти (например, удвоить его), то вы можете вызвать функцию:

p = (int *) realloc (p, 2048) ;

Указатель является параметром функции, и указатель (возможно, отличающийся по значению от первого, особенно, если блок увеличивается) является возвращаемым значением функции. Этот пример показывает, что операционная система (в данном случае Windows 95) может перемещать блок в рамках виртуальной памяти. Например, если вы выделили блок размером 1024 байта, то его виртуальный адрес может быть равен 0x00750100. Вы можете выделить второй блок памяти и получить виртуальный адрес 0x00750504. Расширив первый блок памяти до 2048 байт, использовать тот же виртуальный адрес невозможно. В этом случае Windows 95 должна переместить блок в физической памяти на новую страницу.

При окончании работы с памятью, вызовите функцию:

free (p) ;

Только указанные четыре функции определены в стандарте ANSI языка C. Часто производители компиляторов реализуют несколько большее количество функций, наиболее распространенной из которых является функция _msize, возвращающая размер выделенного блока.

Фундаментальное выделение памяти
в
Windows 95

Как уже говорилось ранее, все, что вы можете делать с помощью библиотечных функций C, вы можете делать самостоятельно, или используя вызовы функций ядра Windows 95. Ниже приведена функция Windows 95 для выделения блока памяти для указателя на целые:

p = (int *) GlobalAlloc (uiFlags, dwSize) ;

Функция имеет два параметра: набор флагов и размер выделяемого блока в байтах. Она возвращает виртуальный адрес, который может использоваться в программе для доступа к выделенной памяти. Значение NULL говорит о том, что имеющейся в распоряжении памяти для выделения недостаточно.

За исключением одной, для каждой функции, начинающейся со слова Global, существует другая, начинающаяся со слова Local. Эти два набора функций в Windows 95 идентичны. Два различных слова сохранены для совместимости с предыдущими версиями Windows, где функции Global возвращали дальние указатели, а функции Local — ближние.

Хотя определения параметров немного отличаются, они оба являются 32-разрядными беззнаковыми целыми. Если первый параметр задать нулевым, то это эквивалентно использованию флага.

GMEM_FIXED (равен нулю).

Такой вызов функции GlobalAlloc эквивалентен вызову функции malloc. В ранних версиях Windows присутствие флага GMEM_FIXED приводило к проблемам управления памятью, поскольку Windows не могла перемещать такие блоки в физической памяти. В Windows 95 флаг GMEM_FIXED вполне допустим, поскольку функция возвращает виртуальный адрес, и операционная система может перемещать блок в физической памяти, внося изменения в таблицу страниц.

Вы можете также использовать флаг:

GMEM_ZEROINIT

для обнуления всех байтов выделяемого блока памяти. Флаг GPTR включает в себя флаги GMEM_FIXED и GMEM_ZEROINIT, как определено в заголовочных файлах Windows:

#define GPTR       (GMEM_FIXED | GMEM_ZEROINIT)

Имеется также функция изменения размера блока памяти:

p = (int *) GlobalReAlloc (p, dwSize, uiFlags) ;

Вы можете использовать флаг GMEM_ZEROINIT для обнуления добавляющихся в блок памяти байтов, если блок расширяется.

Существует функция, возвращающая размер блока памяти:

dwSize = GlobalSize (p) ;

и функция освобождения памяти:

GlobalFree (p) ;

Перемещаемая память

Как уже отмечалось ранее, в предыдущих версиях Windows наличие флага GMEM_FIXED приводило к проблемам при управлении памятью, поскольку Windows должна была сохранять блок памяти в фиксированном месте физической памяти. В Windows 95 блок памяти может быть перемещен в физической памяти при сохранении виртуального адреса неизменным. Функция GlobalAlloc, тем не менее, поддерживает флаг

GMEM_MOVEABLE

и комбинированный флаг для дополнительного обнуления блока памяти (как описано в заголовочных файлах Windows):

#define GHND (GMEM_MOVEABLE | GMEM_ZEROINIT)

Флаг GMEM_MOVEABLE позволяет перемещать блок памяти в виртуальной памяти. Это необязательно означает, что блок памяти будет перемещен в физической памяти, но адрес, которым пользуется программа для чтения и записи, может измениться. Это звучит странно? Возможно. Но вскоре мы увидим, как работает этот механизм.

Вы можете задать вопрос: почему я могу захотеть использовать перемещаемую память, если я не обязан это делать? (И вы можете поставить его более четко после того, как увидите, что для этого требуется.) Ответ состоит в том, чтобы сохранить совместимость с существующим исходным кодом программ для Windows. Более удачный ответ — вы можете беспокоиться о фрагментации виртуальной памяти. Если ваша программа выделяет, расширяет, освобождает память, как сумасшедшая, то виртуальная память может сделаться фрагментированной. Может ли ваша программа дойти до предела в 2 ГБ виртуальной памяти до того, как будет достигнут предел 4, 8 или 16 МБ физической памяти? Это может произойти. Проблема встает острее при непрерывном использовании машин. (Как известно, после окончания рабочего дня запускаются программы-хранители экрана.) Программы, выполнение которых происходит в течение нескольких дней, в конце концов могут столкнуться с большей фрагментацией памяти, чем программы, разработанные для выполнения в течение одного-двух часов.

Поскольку это потенциальная проблема, то вы можете захотеть использовать перемещаемую память. Теперь рассмотрим, как это делается. Первым делом определим указатель и переменную типа GLOBALHANDLE:

int *p ;

GLOBALHANDLE hGlobal ;

Затем, выделим память, например так:

hGlobal = GlobalAlloc (GHND, 1024) ;

Обратите внимание, что не требуется никакого преобразования типа для возвращаемого функцией GlobalAlloc значения. Функция определена, как возвращающая значение типа GLOBALHANDLE, поскольку именно так и было в предыдущих версиях Windows.

Как и при использовании любого другого описателя Windows вам не нужно беспокоиться о его численном значении. Когда вам необходимо обратиться к памяти, для фиксации блока используйте вызов:

p = (int *) GlobalLock (hGlobal) ;

Эта функция преобразует описатель памяти в указатель. Пока блок зафиксирован, Windows не изменяет его виртуальный адрес. Когда вы заканчиваете работу с блоком, для снятия фиксации вызовите функцию:

GlobalUnlock (hGlobal) ;

Этот вызов дает Windows свободу перемещать блок в виртуальной памяти. Для того чтобы правильно осуществлять этот процесс (и чтобы испытать муки программистов более ранних версий Windows), вы должны фиксировать и снимать фиксацию блока памяти в ходе обработки одного сообщения.

Когда вы хотите освободить память, вызовите функцию GlobalFree с параметром-описателем, а не указателем. Если в данный момент вы не имеете доступа к описателю, то вам необходимо использовать функцию:

hGlobal = GlobalHandle (p) ;

Вы можете фиксировать блок памяти несколько раз до того, как снять с него фиксацию. Windows запоминает количество фиксаций, и каждое фиксирование требует снятия для того чтобы дать возможность блоку перемещаться. Перемещение блока в виртуальной памяти не есть перемещение байтов с одного места в другое — производятся только манипуляции с таблицами страниц. Единственной причиной для выделения перемещаемой памяти служит предотвращение фрагментации виртуальной памяти.

Удаляемая память

Если вы уже набрались смелости, чтобы использовать опцию GMEM_MOVEABLE, то, может быть, у вас хватит смелости попробовать использовать опцию:

GMEM_DISCARDABLE

Эта опция может быть использована только совместно с GMEM_MOVEABLE. Блок памяти, выделенный с этим флагом, может быть удален из физической памяти ядром Windows, когда необходима свободная память.

Может быть, это звучит кощунственно, но немного подумайте об этом. Например, блоки памяти, содержащие код, являются удаляемыми. Они являются защищенными от записи. Следовательно, быстрее загрузить код из исходного файла .EXE, чем записывать его на диск, а затем вновь загружать с диска. Если вы выделяете память для неизменяемых данных, которые могут быть легко регенерированы (обычно загрузкой из файла), то можно сделать этот блок удаляемым. О том, что данные были сброшены, вы узнаете, когда вызовите функцию GlobalLock и получите в ответ NULL. Теперь, вы восстанавливаете данные.

Блок памяти не может быть удален до тех пор, пока счетчик фиксаций больше нуля. Для преднамеренного удаления блока памяти, вызовите:

GlobalDiscard (hGlobal) ;

Другие функции и флаги

Другим доступным для использования в функции GlobalAlloc является флаг GMEM_SHARE или GMEM_DDESHARE (они идентичны). Как следует из его имени, этот флаг предназначен для динамического обмена данными, который подробно рассматривается в главе 17.

Функции GlobalAlloc и GlobalReAlloc могут также включать флаги GMEM_NODISCARD и GMEM_NOCOMPACT. Эти флаги дают указание Windows не удалять и не перемещать блоки памяти для удовлетворения запросов памяти. Только излишне альтруистичные программисты используют эти флаги.

Функция GlobalReAlloc может также изменять флаги (например, преобразовывать фиксированный блок памяти в перемещаемый, и наоборот), если новый размер блока задан равным нулю, и указан флаг GMEM_MODIFY в параметре флагов.

Функция GlobalFlags возвращает комбинацию флагов GMEM_DISCARDABLE, GMEM_DISCARDED и GMEM_SHARE.

Наконец, вы можете вызвать функцию GlobalMemoryStatus (для этой функции нет функции-двойника со словом Local ) с указателем на структуру типа MEMORYSTATUS для определения количества физической и виртуальной памяти, доступной приложению.

На этом заканчивается обзор функций, начинающихся со слова Global. Windows 95 также поддерживает некоторые функции, которые вы реализуете сами или дублируете библиотечными функциями C. Это функции FreeMemory (заполнение конкретным байтом), ZeroMemory (обнуление памяти), CopyMemory и MoveMemory — обе копируют данные из одной области памяти в другую. Если эти области перекрываются, то функция CopyMemory может работать некорректно. Вместо нее используйте функцию MoveMemory.

Хорошо ли это?

Перед тем как осуществить доступ к памяти, вам, может быть, захочется проверить, возможен доступ или нет. Если указатель является недействительным, исключается общая защита программы. Предварительная проверка указателя гарантирует, что этого не произойдет. Функции IsBadCodePtr, IsBadReadPtr, IsBadWritePtr и IsBadStringPtr выполняют эту проверку. Первая из этих функций просто принимает указатель в качестве параметра и возвращает ненулевое значение (TRUE), если указатель действителен. Другие три функции получают указатель в качестве первого параметра и длину блока памяти в качестве второго параметра. Четвертая функция, кроме того, осуществляет проверку до тех пор, пока не встретит нулевой ограничитель строки.

Функции управления виртуальной памятью

Windows 95 поддерживает ряд функций, начинающихся со слова Virtual. Эти функции предоставляют значительно больше возможностей управления памятью. Однако, только очень необычные приложения требуют использования этих функций.

Например, вы разрабатываете интегрированную среду разработчика, которая включает в себя компилятор и исполнительную систему. Программа читает исходный код и компилирует его, а результат компиляции заносится в память. Затем, вы хотите пометить этот блок памяти как "только для выполнения", т. е. так, чтобы было невозможно случайно или преднамеренно прочитать (или того хуже, записать) в него что-либо из программы, которая была только что скомпилирована и готова к выполнению. Это одно из действий, которое должна осуществлять развитая среда разработчика для обработки ошибок при работе с указателями в пользовательской программе. Вы можете также пометить часть блоков памяти "только для чтения". Оба указанных действия выполняются только с помощью функций управления виртуальной памятью. Но, повторяем, эта операция достаточно экзотическая.

Может быть, более часто возникает необходимость зарезервировать большой блок виртуальной памяти для данных, который может быть сильно увеличен в процессе выполнения программы. Действуя обычным путем — т. е. часто используя функции realloc или функцию Windows GlobalReAlloc для динамического изменения размера выделенного блока памяти — можно резко снизить производительность программы. Функции управления виртуальной памятью могут помочь избежать этого. Рассмотрим теперь, как это делается.

В Windows 95 любой блок виртуальной памяти может находиться в одном из трех состояний: "committed" (т. е. блок спроецирован в физическую память), "free" (свободен, т. е. доступен для будущего выделения), "reserved" (зарезервирован, это нечто среднее между двумя предыдущими состояниями). Зарезервированный блок виртуальной памяти не отображается в физическую память. Адреса в пределах этого блока будут недоступны до тех пор, пока всему блоку или его какой-либо части не будет передан блок физической памяти. Таким образом, вы можете зарезервировать достаточно большой блок виртуальной памяти, не передавая ему физической памяти. Когда будет необходимо обратиться по какому-либо виртуальному адресу в пределах этого блока, вы передаете по этому адресу ровно столько физической памяти, сколько необходимо, т. е. в зарезервированном блоке виртуальной памяти могут быть участки, как связанные, так и несвязанные с блоками физической памяти. Спроецировав физическую память на нужный участок зарезервированной области виртуальной памяти, программа может обращаться к нему, не вызывая при этом исключения нарушения доступа.

Для того чтобы использовать функции работы с виртуальной памятью, вашей программе необходимо знать размер страницы памяти. В отличие от Windows NT, Windows 95 работает только на микропроцессорах фирмы Intel, и размер страницы всегда равен 4096 байт. Если ваша программа разрабатывается также для запуска под Windows NT, используйте функцию GetSystemInfo для получения размера страницы. Эта функция имеет один параметр, который является указателем на структуру типа SYSTEM_INFO. Поле dwPageSize этой структуры содержит размер страницы. Используются также поля lpMinimumApplicationAddress и lpMaximumApplicationAddress, содержащие минимальный и максимальный адреса, имеющиеся в распоряжении приложения. Для Windows 95 эти значения равны соответственно 0x00400000 и 0x7FFFFFFF.

Функция VirtualAlloc выглядит следующим образом:

p = VirtualAlloc (pAddr, dwSize, iAllocType, iProtect) ;

Первый параметр показывает желаемый стартовый базовый адрес виртуальной памяти, и вы можете установить его значение в NULL при первом вызове этой функции. Второй параметр задает размер. Третий параметр может быть равен MEM_RESERVE или MEM_COMMIT для резервирования блока виртуальной памяти или для резервирования и передачи ему физической памяти. Четвертый параметр может быть константой, начинающейся с префикса PAGE_ (например, PAGE_READONLY или PAGE_EXECUTE) для задания защиты блока памяти. Последовательные вызовы функции VirtualAlloc могут передавать или резервировать секции этого блока. Функция VirtualFree используется для освобождения виртуальной памяти.

Функции работы с "кучей"

Последняя группа функций работы с памятью — это функции, имена которых начинаются со слова Heap (куча). Эти функции создают и поддерживают непрерывный блок виртуальной памяти, из которого вы можете выделять память более мелкими блоками. Вы начинаете с вызова функции HeapCreate. Затем, используете функции HeapAllocate, HeapReAllocate и HeapFree для выделения и освобождения блоков памяти в рамках "кучи". "Куча" может быть уплотнена для объединения свободного пространства.

Существует немного причин для использования функций, работающих с "кучей".

Файловый ввод/вывод

Повторим еще раз: для файлового ввода/вывода используйте библиотечные функции языка C везде, где это возможно. Это уже было сделано в программе POPPAD3 в главе 11. Там использовались функции fopen, fseek, fread, fwrite и fclose.

Старый путь

Работа с файлами под Windows постепенно совершенствовалась год от года. В те времена, когда использовались Windows 1.0 и Windows 2.0, единственной функцией файлового ввода/вывода была функция OpenFile, и официально рекомендовавшимся подходом к чтению и записи файлов была запись маленьких файлов на ассемблере, которые непосредственно осуществляли доступ к функциям MS DOS. Хотя использование стандартных функций библиотеки времени выполнения языка C было возможно, в маленькой и средней моделях памяти эти функции работали только с ближними указателями. Это было неудобно в программах, хранивших файлы данных в блоках глобальной памяти. Даже имена файлов, которые часто получали из диалоговых окон, были доступны с помощью дальних указателей.

К счастью, многие программисты вскоре обнаружили несколько недокументированных функций для работы с файлами с использованием дальних указателей. Они имели имена _lopen, _lread, _lwrite и т. д., и содержали непосредственные вызовы функций MS DOS. Начиная с Windows 3.0, эти функции были документированы и приняты как стандартные функции работы с файлами при программировании под Windows. Но применять их при программировании для Windows 95 не рекомендуется.

Отличия Windows 95

Windows 95 реализует несколько усовершенствований файлового ввода/вывода по сравнению с более ранними версиями Windows.

Первое, Windows 95 так же как и Windows 3.1 поддерживает библиотеку диалоговых окон общего пользования (common dialog box library), которая содержит диалоговые окна FileOpen и FileSave. Использование этих диалоговых окон было показано в главе 11. Рекомендуется при программировании использовать именно эти диалоговые окна. При их использовании исчезает необходимость разбора имени файла, который может быть системно-зависимым.

Второе, Windows 95 является 32-разрядной системой. Это значит, что вы можете читать и записывать файл большими блоками информации за один прием, используя однократный вызов функций fread и fwrite (или их эквивалентами, поддерживаемыми Windows 95). Изменения по отношению к существующему коду состоит в том, что отпадает необходимость в использовании циклов при работе с файлами большого размера.

Третье, Windows 95 поддерживает длинные имена файлов. Самое лучшее, что могут делать ваши программы с длинными именами, это просто ничего с ними не делать. (Хорошо звучит, не правда ли?) В документации по Windows сказано, что вы можете использовать данные, возвращаемые функцией GetVolumeInformation, для динамического выделения буферов для хранения имен файлов. Но, обычно в этом нет необходимости. Вам рекомендуется использовать две константы, определенные в файле STDLIB.H: _MAX_PATH (равно 260) и _MAX_FNAME (256) для статического выделения памяти.

Функции файлового ввода/вывода,
поддерживаемые
Windows 95

Если вы не пользуетесь функциями файлового ввода/вывода стандартной библиотеки времени выполнения языка C, то вы можете использовать функции, поддерживаемые Windows 95. Функция CreateFile является достаточно мощной:

hFile = CreateFile (szName, dwAccess, dwShare, NULL, dwCreate,
                                      dwFlags, 0) ;

Несмотря на имя, эта функция используется также для открытия существующего файла. Кроме того, эта функция используется для открытия каналов, используемых при обмене между процессами, а также коммуникационных устройств.

Для файлов — первый параметр является именем файла. Второй имеет значение либо GENERIC_READ, либо GENERIC_WRITE, либо GENERIC_READ | GENERIC_WRITE. Использование нулевого значения позволяет получить информацию о файле без доступа к его содержимому. Параметр dwShare открывает файл с общими атрибутами, позволяя другим процессам читать из него (FILE_SHARE_READ), или записывать в него (FILE_SHARE_WRITE), или и то и другое вместе.

Флаг dwCreate — это одна из нескольких констант, показывающая, каким образом файл должен быть открыт. Их имена сжаты и прекрасно поясняют суть. Флаг CREATE_NEW вызывает ошибку, если файл уже существует, в то время как флаг CREATE_ALWAYS приводит к удалению содержимого существующего файла. Аналогичным образом, флаг OPEN_EXISTING вызывает ошибку, если файл не существует, а флаг OPEN_ALWAYS создает файл, если он не существует. Флаг TRUNCATE_EXISTING приводит к ошибке, если файл не существует, и удаляет все содержимое, если файл существует.

Параметр dwFlags может быть комбинацией констант, начинающихся со слов FILE_ATTRIBUTE и FILE_FLAG, для установки атрибутов файла и других особенностей.

Функция CreateFile возвращает переменную типа HANDLE. При завершении работы с файлом его необходимо закрыть, используя функцию CloseHandle с описателем файла в качестве параметра. Функции ReadFile и WriteFile похожи:

ReadFile (hFile, pBuffer, dwToRead, &dwHaveRead, NULL) ;
WriteFile (hFile, pBuffer, dwToWrite, &dwHaveWritten, NULL) ;

Второй параметр — это указатель на буфер, содержащий данные; третий параметр содержит количество байтов для чтения или записи; четвертый параметр — указатель на переменную, в которую при возврате из функции будет занесено количество байтов, которые были реально считаны или записаны. (Последний параметр используется только для файла, открываемого с флагом FILE_FLAG_OVERLAPPED, но этот случай не входит в предмет рассмотрения данной книги.)

Ввод/вывод с использованием файлов,
проецируемых в память

При работе в Windows 95 (и это является одним из усовершенствований системы по сравнению с более ранними 16-разрядными версиями Windows) существует возможность читать и записывать данные в файл так, как будто это блок памяти. На первый взгляд это может показаться несколько странным, но со временем становится понятно, что это очень удобный механизм. Это прием рекомендуется использовать также при разделении памяти между двумя и более процессами. Пример такого использования файлов, проецируемых в память, приведен в главе 19 "Динамически подключаемые библиотеки".

Вот простейший подход к вводу/выводу с использованием файлов, проецируемых в память (memory mapped files): Сначала создается обычный файл с использованием функции CreateFile. Затем вызывается функция:

hMap = CreateFileMapping (hFile, NULL, dwProtect, 0, 0, szName) ;

Параметр dwProtect может принимать одно из следующих значений, и должен быть совместим с режимом разделения файла: PAGE_READONLY, PAGE_WRITECOPY, PAGE_READWRITE. Последний параметр функции — необязательное имя, обычно используемое для разделения данных между процессами. В этом случае, функция OpenFileMapping открывает тот же файл с указанным именем. Обе функции возвращают значение типа HANDLE.

Если вам необходимо осуществить доступ к части файла, вызовите функцию MapViewOfFile:

p = MapViewOfFile (hMap, dwAccess, dwHigh, dwLow, dwNumber) ;

Весь файл или его часть могут быть спроецированы в память, начиная с заданного 64-разрядного смещения, которое задается параметрами dwHigh и dwLow. (Очевидно, что dwHigh будет иметь нулевое значение, если файл имеет размер менее 4 ГБ.) Параметр dwNumber задает количество байтов, которое вы хотите спроецировать в память. Параметр dwAccess может быть равен FILE_MAP_WRITE (данные можно записывать и считывать) или FILE_MAP_READ (данные можно только считывать), и должен соответствовать параметру dwProtect функции CreateFileMapping.

После этого вы можете использовать указатель, возвращаемый функцией, для доступа или модификации данных в файле. Функция FlushViewOfFile записывает на диск все измененные страницы файла, спроецированного в память. Функция UnmapViewOfFile делает недействительным указатель, возвращаемый функцией MapViewOfFile. Затем необходимо закрыть файл, используя функцию CloseHandle.