Book: Системное программирование в среде Windows



Системное программирование в среде Windows

Джонсон М. Харт

Системное программирование в среде Windows

Третье издание

Введение

В этой книге описывается разработка приложений с использованием интерфейса прикладного программирования (Application Programming Interface, API) операционных систем Windows компании Microsoft, причем основное внимание уделяется базовым системным службам, включая управление файловой системой, процессами и потоками, межпроцессное взаимодействие, сетевое программирование и синхронизацию. Пользовательские интерфейсы, внутренние функции Windows и драйверы ввода/вывода в данной книге не рассматриваются, хотя сами по себе эти темы не менее важны и представляют не меньший интерес. Для примеров преимущественно выбирались реалистичные сценарии, и поэтому многие из них вполне могут служить в качестве основы для построения реальных приложений.

Win32/Win64 API, или обобщенно Windows API, поддерживаются семейством 32– и 64-разрядных операционных систем компании Microsoft, в которое в настоящее время входят Windows XP, Windows 2000 и Windows Server 2003. К числу ранних представителей этого семейства относятся операционные системы Windows NT, Windows Me, Windows 98 и Windows 95; в настоящее время эти системы считаются устаревшими, однако многие из приведенных в книге примеров программ способны выполняться и под их управлением. Вопросы перехода от платформы Win32 к развивающейся платформе Win64 обсуждаются по мере необходимости. Win64, поддерживаемый в качестве 64-разрядного интерфейса в некоторых версиях Windows Server 2003 и Windows XP, почти идентичен Win32.

Не вызывает сомнений, что Windows API является важнейшим фактором, который оказывает влияние на весь процесс разработки приложений, и во многих случаях вытесняет поддерживаемый операционными системами UNIX и Linux POSIX API, поскольку считается более предпочтительным или, по крайней мере, предоставляющим те же возможности для приложений, ориентированных на настольные и серверные системы. Поэтому многие опытные программисты заинтересованы в скорейшем изучении Windows API, и данная книга призвана содействовать этому.

Прежде всего, необходимо рассказать вам о том, что представляет собой Windows API, и показать, как им пользоваться в реальных ситуациях, причем этот рассказ должен быть как можно более кратким и не перегруженным излишними деталями. Поэтому данная книга предназначена не для использования в качестве справочного руководства, а для ознакомления с основными свойствами наиболее важных функций и демонстрации возможностей их применения в ситуациях практического программирования. Вооружившись этими знаниями, читатель сможет воспользоваться обширной справочной документацией, предоставляемой компанией Microsoft, для самостоятельного углубленного изучения отдельных вопросов, расширенных возможностей и менее приметных функций в соответствии с возникшими потребностями или заинтересованностью. Лично мне при таком подходе изучение Windows API далось легко, а разработка Windows-программ доставила огромное удовольствие, хотя и без неприятных минут также не обошлось. Мои порывы энтузиазма легко просматриваются в некоторых местах книги, что, собственно, и неудивительно. Впрочем, это вовсе не свидетельствует о том, что я безоговорочно соглашусь с превосходством Windows API над API других операционных систем (ОС), но относительно того, что у него есть масса положительных качеств, вряд ли кто-либо станет возражать.

Авторы многих книг, посвященных Windows, значительное внимание уделяют объяснению того, что представляют собой процессы, виртуальная память, межпроцессное взаимодействие, вытесняющий планировщик, но при этом не показывают, как все это используется в реальных ситуациях. Программистам, имеющим опыт работы с системами UNIX, Linux, IBM MVS, Open VMS и некоторыми другими ОС эти понятия уже знакомы, и они заинтересованы лишь в том, чтобы как можно быстрее перейти к изучению того, как эти возможности реализованы в Windows. К тому же, в большинстве книг по Windows важное место отводится методам программирования на основе пользовательского интерфейса. С целью концентрации внимания лишь на самых главных базовых возможностях, предоставляемых системой, в данной книге тема пользовательского интерфейса не затрагивается, и мы ограничиваемся обсуждением лишь простого консольного символьного ввода/вывода.

В соответствии с принятой в данной книге точке зрения Windows — это всего лишь API операционной системы, предоставляющий набор вполне понятных средств. Потребность в ускоренном изучении Windows испытывают многие программисты, независимо от уровня их опыта, и без знания Windows немыслимо обсуждение таких, например, тем, как модель компонентного объекта (Component Object Model, СОМ), разработанная компанией Microsoft. В некоторых отношениях системы Windows превосходят остальные системы, в других — отстают от них или находятся примерно на том же уровне. Задача данной книги состоит в том, чтобы продемонстрировать, как эффективнее всего использовать эти возможности в реальных ситуациях для разработки полезных, высококачественных и высокопроизводительных приложений.

Потенциальная аудитория

• Все, кто хочет быстро научиться разрабатывать приложения, независимо от уровня подготовки.

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

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

• Программисты, использующие СОМ и .NET Framework, которые найдут здесь массу полезной информации, облегчающей изучение принципов работы динамически подключаемых библиотек (dynamic link libraries, DLL), моделей потоков и способов их применения, интерфейсов и синхронизации.

• Студенты, изучающие компьютерные дисциплины на старших курсах вузов или занятые подготовкой дипломных работ, связанных с системным программированием или разработкой приложений. Книга будет полезна также тем, кто изучает многопоточное программирование или сталкивается с необходимостью создания сетевых приложений. Ее также можно использовать в качестве полезного дополнения к таким, например, источникам, как книга У. Ричарда Стивенса (W. Richard Stevens) Advanced Programming in the UNIX Environment (см. библиографию), что позволит студентам сравнить возможности Windows и UNIX. Эта книга будет хорошим подспорьем и для студентов, проходящих курс ОС, поскольку в ней показано, какими именно средствами обеспечивается базовая функциональность ОС, представляющих интерес в коммерческом отношении.

Единственным допущением, которое неявно присутствует во всем вышесказанном, является предположение о том, что читатели имеют опыт программирования на языке С.

Изменения в третьем издании

Наряду со значительным обновлением и реорганизацией по сравнению с первыми двумя изданиями, в третьем издании добавлен обширный объем нового материала. Это издание призвано решать следующие задачи:

• Охватить новые возможности, появившиеся в Windows XP, Windows 2000 и Windows Server 2003, а также рассмотреть вопросы перехода к платформе Win64.

• Исключить материал, учитывающий специфику ОС Windows 95, Windows 98 и Windows ME (семейство "Windows 9x"), как устаревший, поскольку на поставляемых в настоящее время персональных системах устанавливается Windows XP, и ограничения, свойственные Windows 9х, уже потеряли свою актуальность.[1] В примерах программ без каких бы то ни было оговорок используются средства, которые входят лишь в текущие версии Windows, хотя в результате этого в Windows 9x некоторые программы работать не будут.

• Предоставить более полное освещение темы потоков и синхронизации, включая связанные с этим аспекты производительности, масштабируемости и надежности. Глава 9, равно как и некоторые из примеров в главе 10, являются новыми.

• Подчеркнуть все возрастающее влияние Windows 2000 и Windows Server 2003 и входящих в их состав новых средств на возможности высокопроизводительных, масштабируемых, многопоточных серверных приложений.

• Исследовать зависимость производительности программ от принципов их построения, обратив особое внимание на многопоточные программы с синхронизацией и на особенности эксплуатации этих программ в условиях симметричных многопроцессорных (Symmetrical Multiprocessor, SMP) систем.

• Учесть замечания читателей и студентов, касающиеся исправления недочетов и улучшения стиля изложения, а также все их советы и пожелания, как важные, так и самые незначительные.

Как организована эта книга

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

В пределах каждой главы, после краткого обсуждения отдельной функциональности, например, управления процессами или отображения файлов, подробно рассматриваются наиболее важные из соответствующих функций Windows и их взаимосвязь между собой. Изложение сопровождается иллюстративными примерами. В основной текст включены лишь наиболее существенные части листингов программ; полные тексты программ, а также необходимые включаемые файлы, вспомогательные функции и прочий код приведены в приложении А или доступны на Web-сайте книги (http://www.awprofessional.com/titles/0321256190). Во всех случаях, когда какие-либо возможности поддерживаются только текущими версиями Windows (XP, 2000 и Server 2003) и не поддерживаются предыдущими, например, Windows 9x и Windows NT, в которых не реализованы многие из усовершенствованных возможностей, делаются отдельные оговорки. В каждой главе приводится список дополнительной рекомендованной литературы и предлагается несколько упражнений. Многие упражнения акцентируют внимание на вопросах, которые имеют важное значение и представляют определенный интерес, но не были отражены в основном тексте, тогда как другие упражнения заставляют читателя глубже разобраться в темах более сложного или специального характера.

В главе 1 предлагается высокоуровневое введение в семейство ОС Windows и Windows API. Используемая в качестве примера простая программа демонстрирует основные элементы стиля программирования Windows и создает фундамент для реализации усовершенствованных возможностей Windows. Win64 и проблемы межплатформенной миграции программ предварительно рассматриваются в главе 1, более полно изучаются в главе 16, и обсуждаются по мере изложения в остальной части книги там, где это оказывается необходимым.

В главах 2 и 3 рассматриваются файловые системы, операции консольного ввода/вывода, блокирование файлов и управление каталогами. В главе 2 также рассказывается о кодировке Unicode — расширенном символьном наборе, который используется в Windows. Соответствующие иллюстративные примеры охватывают последовательный и прямой доступ к содержащимся в файле данным, обход дерева каталогов и архивирование файлов. Глава 3 заканчивается обсуждением программного управления реестром, имеющего много общего с управлением файлами и каталогами.

Глава 4 знакомит читателя с обработкой исключений в Windows, включая структурную обработку исключений (Structured Exception Handling, SEH), которая будет широко использоваться на протяжении всей книги. Во многих книгах изучение SEH откладывается до последних глав, однако мы, ознакомившись с этим средством уже на начальной стадии, получим возможность сразу же пользоваться им, что значительно упростит для нас некоторые задачи программирования и позволит повысить качество программ. Кроме того, здесь описано также одно из новейших средств — векторная обработка исключений.

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

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

Главы 8, 9 и 10 предлагают углубленный анализ одного из наиболее мощных средств Windows — синхронизации потоков. Синхронизация — сложная тема, и поэтому в данных главах содержатся многочисленные примеры и описания вполне понятных моделей, которые должны помочь читателю в полной мере воспользоваться преимуществами потоков для повышения эффективности программирования и производительности программ и вместе с тем обойти множество подводных камней. В эти главы включен новый материал, охватывающий вопросы повышения производительности и масштабируемости, что приобретает особое значение при создании серверных приложений, включая те из них, которые предположительно должны выполняться на SMP-системах.

Главы 11 и 12 посвящены межпроцессным и межпоточным взаимодействиям, а также сетевому программированию. В главе 11 главное внимание уделено средствам, являющимся частью Windows, а именно, анонимным каналам, именованным каналам и почтовым ящикам. В главе 12 обсуждаются сокеты Windows (Windows Sockets), обеспечивающие возможность взаимодействия с системами, не входящими в семейство Windows, посредством стандартных протоколов, главным образом, TCP/IP. И хотя интерфейс Windows Sockets, строго говоря, не является частью Windows API, он способен обеспечивать связь и взаимодействие через сети и Internet, так что предмет рассмотрения данной главы согласуется с остальной частью книги. На примере многопоточной клиент-серверной системы проиллюстрировано, каким образом можно обеспечить межпроцессное взаимодействие наряду с потоками.

В главе 13 показано, каким образом Windows позволяет превращать серверные приложения, аналогичные созданным в главах 11 и 12, в службы Windows (Windows Services), которыми можно управлять как фоновыми серверами. Преобразование сервера в службу требует внесения лишь незначительных изменений в программу.

В главе 14 показано, как осуществлять операции асинхронного ввода/вывода с использованием перекрывающегося ввода/вывода, а также событий и процедур завершения. Тех же результатов можно добиться и с помощью потоков, поэтому приводятся примеры, позволяющие сравнить различные решения с точки зрения простоты и производительности соответствующих программ. В то же время, создание масштабируемых серверов с многопоточной поддержкой требует привлечения портов завершения, использование которых иллюстрируется на примере серверов, созданных в предыдущих главах. Описаны также таймеры ожидания, рассмотрение которых требует привлечения понятий, введенных ранее в этой главе.

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

Глава 16 завершает изложение основного материала рассмотрением вопросов программирования для Win64, а также обеспечения программной совместимости с этой платформой. После этого один из рассмотренных ранее примеров преобразуется к виду, допускающему перенос программы на платформу Win64.



Основной материал книги дополнен тремя приложениями. В приложении А содержатся описания программ, доступных на Web-сайте книги, и рекомендации по их использованию. Приложение Б содержит несколько таблиц, в которых функции Windows сравниваются с аналогичными им функциями, предоставляемыми системой UNIX и стандартной библиотекой С. В приложении В сравнивается производительность альтернативных вариантов реализации некоторых из примеров, приведенных в основном тексте, что позволяет читателю составить собственное представление о сравнительных достоинствах и недостатках средств Windows, как базовых, так и усовершенствованных, и средств, предоставляемых библиотекой С.

Сопоставление с UNIX и библиотекой С

По ходу изложения материала мы сопоставляем стилевые и функциональные особенности средств Windows и аналогичных им средств, входящих в UNIX (Linux) и стандартную библиотеку ANSI С. Как уже отмечалось, в приложении Б приведены таблицы, содержащие полный перечень сопоставимых функций. Включение этой информации мы сочли целесообразным, поскольку многие читатели знакомы с UNIX, и результаты сравнения обеих систем между собой, несомненно, будут представлять для них интерес. Те же, кто не знаком с системой UNIX, могут смело пропустить соответствующие разделы, которые, чтобы их было легче отличить, набраны мелким шрифтом и выделены отступами.

Примеры

При подготовке примеров автор руководствовался следующими соображениями:

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

• Они должны соответствовать реальным ситуациям из сферы программирования, с которыми приходится сталкиваться в процессе разработки программного обеспечения, оказания консультаций и обучения. Некоторые из моих клиентов и слушателей использовали коды примеров при построении собственных систем. При оказании консультаций мне часто попадаются коды программ, аналогичные тем, которые включены в эту книгу, а в нескольких случаях ко мне приходили даже с кодами, непосредственно взятыми из первого или второго изданий. (Кстати, вы также можете использовать примеры из книги в своей работе, а если включите в документацию еще и благодарность в мой адрес, то я буду только рад.) Нередко эти коды встречались мне и в виде отдельных частей объектов СОМ или C++. Примеры, с учетом ограничений на время их подготовки и допустимый объем кода, приближены к "реальной жизни" и решают "реальные" задачи.

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

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

• Многие из примеров в нескольких первых главах реализуют такие команды UNIX, как ls, touch, chmod и sort, и тем самым представляют функции Windows в знакомом для части читателей контексте, одновременно создавая полезный набор вспомогательных функций.[2] Кроме того, наличие разных вариантов реализации одной и той же команды упрощает оценку преимуществ в отношении производительности, достигаемых за счет использования усовершенствованных средств Windows. Соответствующие результаты тестирования приведены в приложении В.

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

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

Все примеры отлажены и протестированы в средах операционных систем Windows XP, Windows 2000 и Windows Server 2003.– В необходимых случаях тестирование проводилось под управлением операционных систем Windows 9x и Windows NT. И хотя для разработки программ в основном использовались однопроцессорные системы на базе процессоров Intel, большинство программ тестировались также на многопроцессорных системах. При тестировании приложений с клиент-серверной архитектурой использовались одновременно несколько клиентов, взаимодействующих с сервером. Тем не менее, никогда нельзя с полной уверенностью заявлять о корректности или завершенности программ и их пригодности для тех или иных целей. Несомненно, даже простейшие примеры могут иметь недостатки и при определенных обстоятельствах вообще не работать — такова участь почти любого программного обеспечения. Поэтому автор будет искренне благодарен всем, кто пришлет сообщения о любых дефектах, обнаруженных в программах, а еще лучше — об ошибках.

Web-сайты, посвященные этой книге

На Web-сайте книги (http://www.awprofessional.com/titles/0321256190) находится загружаемый файл, содержащий весь программный код и проекты для всех примеров, которые приведены в книге, решения ряда упражнений, альтернативные варианты реализации некоторых примеров, указания, а также результаты тестовых оценок производительности. Эта информация по мере надобности периодически обновляется с целью включения в нее нового материала и внесения необходимых исправлений.

На моем персональном Web-сайте (http://www.world.std.com/~jmhart/windows.htm) вы найдете список опечаток, обнаруженных в книге, а также дополнительные примеры, письма читателей и дополнительные объяснения, не считая многого другого. Сюда же включены слайды PowerPoint, которые могут быть использованы в некоммерческих учебных целях. Этими слайдами уже воспользовались слушатели многих профессиональных курсов, но они вполне пригодны также для использования в колледжах.

По мере выявления недостатков и ошибок и получения откликов читателей этот материал будет периодически обновляться. В случае возникновения каких-либо затруднений при работе с программами или любым другим материалом, содержащимся в книге, посетите сначала указанные сайты, где вам, возможно, удастся найти необходимые объяснения или получить информацию об обнаруженных к этому времени ошибках. Если же подобная попытка получения ответа на интересующий вас вопрос окажется безрезультатной, обращайтесь непосредственно ко мне по следующему адресу электронной почты: jmhart@world.std.com.

Благодарности

Во время подготовки третьего издания множество людей оказывали мне действенную помощь, делились советами или просто поддерживали добрым словом, а читатели подсказали целый ряд ценных идей и замечаний. На Web-сайте автора выражена горячая признательность всем тем, чьи советы и замечания нашли свое отражение в третьем издании книги, тогда как в первых двух изданиях содержатся благодарности в адрес тех, кто еще раньше дал нам ценные советы. Кроме того, прекрасный подробный анализ содержания книги был дан в недавних рецензиях Вагифа Абилова (Vagif Abilov), Билла Дрейпера (Bill Draper), Хорста Д. Клаузена (Horst D. Clausen), Майкла Девидсона (Michael Davidson), Дэниела Джанга (Daniel Jiang), Эрика Ландеса (Eric Landes), Клауса X. Пробста (Klaus H. Probst) и Дугласа Рейли (Douglas Reilly), которые отнеслись к этой работе с гораздо большим вниманием, чем того требовали бы одни формальные обязанности; их советы и рекомендации заслуживают самой глубокой благодарности, и мне лишь остается надеяться, что с не меньшим вниманием и я отнесся к результатам их труда. Отдельной благодарности заслуживают мои друзья из ArrAy Inc.; у них я многому научился.

Анни X. Смит (Anne H. Smith), выполнявшая верстку, приложила все свое мастерство, настойчивость и терпение, готовя книгу к публикации; без ее вклада выход книги в свет был бы просто невозможен. Элисса Армер (Elissa Armour), готовившая макеты для первых двух изданий, тем самым заложила фундамент и для настоящего Издания, сделав этот переход как нельзя более гладким.

Криста Мидоубрук (Chrysta Meadowbrooke), редактор рукописи, значительно улучшила точность, ясность и связность изложения материала. Ее внимательное отношение к содержанию книги, острые вопросы и проницательность помогли глубже осветить целый ряд вопросов.

Сотрудники издательства Addison-Wesley Professional проявили такой профессионализм и знание дела, что работать с ними было сплошным удовольствием. Стефани Накиб (Stephane Nakib), редактор, и Карен Гетман (Karen Gettman), главный редактор, работали над проектом с самого начала, когда надо, торопили меня, устраняли все помехи в работе и следили за тем, чтобы я ни на йоту не отклонялся от рабочего графика. Эбони Хейт (Ebony Haight), помощник редактора, осуществлял общее руководство всеми этапами работы, а производственная группа Джона Фуллера (John Fuller) и Патрика Кэш-Петерсона (Patrick Cash-Peterson), координатора производства, заставили считать, что с производственным процессом не могут быть связаны никакие сложности.

Эта книга посвящается горячо любимым Бобу (Bob) и Элизабет (Elizabeth).

Джонсон (Джон) М. Харт (Johnson (John) M. Hart) jmhart@world.std.com  Август 2004 года.

От издательства

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

Мы ждем ваших комментариев и надеемся на них. Вы можете прислать нам бумажное или электронное письмо, либо просто посетить наш Web-сервер и оставить свои замечания там. Одним словом, любым удобным для вас способом дайте нам знать, нравится или нет вам эта книга, а также выскажите свое мнение о том, как сделать наши книги более интересными для вас.

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

Наши координаты:

E-mail: info@williamspublishing.com

WWW: http://www.williamspublishing.com

Информация для писем из: России: 115419, Москва, а/я 783 Украины: 03150, Киев, а/я 152

ГЛАВА 1

Знакомство с Win32 и Win64

В этой главе вы познакомитесь с семейством операционных систем (ОС) Microsoft Windows и интерфейсом прикладного программирования (Application Programming Interface, API), который используется всеми членами этого семейства. Здесь также кратко описывается новейший 64-разрядный API Win64 и достаточно подробно обсуждается проблема переносимости программного обеспечения между Win32 и Win64. Для удобства изложения мы будем ссылаться, главным образом, просто на Windows и Windows API. Как правило, раздельные ссылки на Win32 и Win64 будут делаться лишь в тех случаях, когда различия между этими интерфейсами будут иметь существенное значение. Сориентироваться в том, что именно автор имеет в виду, когда говорит о Windows, — операционную систему или интерфейсе для разработки программ, — читателю поможет контекст.

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

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

Основные возможности операционных систем

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

• Память. ОС управляет сплошным, или плоским (flat), виртуальным адресным пространством большого объема, перемещая данные между физической памятью и диском или иным накопительным устройством прозрачным для пользователя образом.

• Файловые системы. ОС управляет пространством именованных файлов, предоставляя возможности прямого и последовательного доступа к файлам, а также средства управления файлами и каталогами. Используемые в большинстве систем пространства имен являются иерархическими.

• Именование и расположение ресурсов. Файлы могут иметь длинные, описательные имена, причем принятая схема именования распространяется также на такие объекты, как устройства, а также объекты синхронизации или межпроцессного взаимодействия. Размещение именованных объектов и управление доступом к ним также являются прерогативой ОС.

• Многозадачность. ОС должна располагать средствами управления процессами, потоками и другими единицами, способными независимо выполняться в асинхронном режиме. Задачи могут планироваться и вытесняться в соответствии с динамически определяемыми приоритетами.

• Взаимодействие и синхронизация. ОС управляет обменом информацией между задачами и их синхронизацией в изолированных системах, а также взаимодействием сетевых систем между собой и сетью Internet.

• Безопасность и защита. ОС должна предоставлять гибкие механизмы защиты ресурсов от несанкционированного или непреднамеренного доступа и нанесения ущерба системе.

Microsoft Windows Win 32/Win64 API обеспечивает поддержку не только этих, но и множества других средств ОС, и делает их доступными в ряде версий Windows, некоторые из которых постепенно выходит из употребления, а некоторые поддерживает лишь то или иное подмножество полного API.

Эволюция Windows

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

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

• Масштабируемость. Новые версии способны выполняться на более широком спектре систем, включая серверы масштаба предприятия, использующие оперативную память большого объема и запоминающие устройства повышенной емкости.

• Интеграция. Каждый новый выпуск системы интегрирует в себя элементы дополнительных технологий, такие, например, как использование мультимедийных данных или подключение к беспроводным сетям, Web-службы или самонастраивающиеся (plug-and-play) устройства. В целом, рассмотрение этих технологий выходит за рамки данной книги.

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

• Совершенствование API. За время своего существования API был дополнен новыми замечательными возможностями. Именно API является центральной темой данной книги.

Версии Windows

ОС Windows, в виде развивающейся последовательности версий, используется, начиная с 1993 года. Во время написания данной книги на Web-сайте компании Microsoft в качестве основных фигурировали следующие версии:

• Windows XP, включая выпуски Home, Professional и ряд других, которая ориентирована на индивидуальных пользователей. Большинство коммерческих PC, поступающих на рынок на сегодняшний день, включая лэптопы и ноутбуки, поставляются с уже установленной Windows XP соответствующего типа. В рамках данной книги различия между версиями, как правило, существенного значения не имеют.



• Windows Server 2003, которая выпускается в виде продуктов Small Business Server, Storage Server 2003, а также некоторых других, и ориентирована на управление приложениями для предприятий и серверными приложениями. В системах, работающих под управлением Windows Server 2003, часто применяется симметричная многопроцессорная обработка (Symmetric Multiprocessing, SMP), характеризующаяся использованием одновременно нескольких независимых процессоров. Новые 64-разрядные приложения, требующие Win64, появляются преимущественно на системах Windows Server 2003.

• Windows 2000, которая доступна в виде выпусков Professional и нескольких разновидностей выпусков Server и по-прежнему широко используется как в персональных, так и в серверных системах. Со временем Windows XP и будущие версии Windows вытеснят Windows 2000, продажа которой уже прекращена.

• Windows Embedded, Windows СЕ и Windows Mobile, которые представляют собой специализированные версии Windows, ориентированы на использование в малых системах, таких, например, как ручные (palmtop) и встроенные (embedded) устройства обработки данных, и предоставляют широкие подмножества возможностей Windows.

Устаревшие предыдущие версии Windows

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

• Windows NT 3.5, 3.5.1 и 4.0, предшествовавшие современным версиям NT и восходящие своими корнями к 1993 году, из которых наибольшей популярностью пользовалась версия Windows NT 4.0, модернизированная с помощью пакета обновлений Service Pack 3 (SP3). Первоначально системы NT предназначались для профессиональных пользователей и серверов, в то время как версиями, распространяемыми для персональных и офисных нужд, служили версии Windows 9x (см. ниже). Первоначально Windows 2000 называлась Windows NT Version 5.0, в связи с чем многие пользователи, имея в виду системы Windows 2000, Windows Server 2003 или Windows XP, все еще употребляют названия Windows NT или Windows NT5. Несмотря на важные исключения, что особенно касается последних глав, под управлением NT Version 4.0 будут корректно, хотя и не всегда оптимально, работать почти все программы из числа тех, которые представлены в данной книге.

Windows 95, 98 и Windows ME (или просто — Windows 9x,поскольку различия между указанными системами вряд ли могут считаться существенными), которые предназначались главным образом для настольных (desktop) и переносных (laptop) компьютеров и не включали в себя, помимо прочего, средств безопасности Windows NT. В настоящее время эти системы вытесняются системой Windows XP, которая объединяет предоставляемые ими средства со средствами Windows NT. Многие примеры программ, хотя и не все, особенно из числа тех, которые приводятся в начальных главах книги, будут корректно выполняться и под управлением Windows 9x.

Возвращаясь назад в прошлое, можно отметить, что до появления Windows 95 доминировала 16-разрядная ОС Windows 3.1, графический пользовательский интерфейс (Graphical User Interface, GUI) которой можно считать предшественником современных Windows GUI. Вместе с тем, многие важнейшие возможности ОС, такие как истинная многозадачность, управление памятью и средства защиты, она не поддерживала.

Если обратиться к еще более отдаленному прошлому, к концу восьмидесятых годов прошлого столетия, то в качестве исходной "IBM PC-системы можно указать DOS. В DOS имелся всего-навсего простейший интерфейс командной строки, но DOS-команды используются и по сей день. В действительности, большинство из программ, рассматриваемых в данной книге в качестве примеров, написаны в виде консольных приложений и поэтому могут запускаться из командной строки, а для тестирования производительности даже специально предусмотрены пакетные файлы DOS.

Windows NT 5.x

По отношению к системам Windows 2000, Windows XP и Windows Server 2003 используют собирательное название Windows NT Version 5.x или просто NT5. В каждой из трех указанных систем эксплуатируется пятая версия ядра Windows NT, хотя младший номер версии ("х" в "5.x") может быть различным. Так, в Windows XP используется ядро версии NT 5.1.

Несмотря на то что многие программы будут выполняться и под управлением предыдущих версий Windows, мы, как правило, предполагаем применение версии NT5, некоторые возможности которой отсутствуют в предыдущих версиях. Поскольку любая современная Windows-система обладает возможностями NT5, такое предположение не чревато никакими осложнениями, но зато снимает любые ограничения на использование усовершенствованных возможностей ОС. Вместе с тем, на многих устаревших системах все еще могут оставаться установленными ранние версии Windows NT или Windows 9x, и поэтому программы примеров сразу же после запуска проверяют номер версии Windows и в необходимых случаях прекращают свою работу с выводом сообщения об ошибке.

В документации по Microsoft API приводятся требования к номеру версии, сформулированные в терминах NT, Windows (что в данном контексте относится к версиям 9х) и СЕ, и ряд других требований. В случае любого рода сомнений относительно возможности использования тех или иных функций API в конкретной версии Windows следует обратиться к документации.

Другие интерфейсы программирования для Windows

Windows (под этим термином, если отдельно не оговаривается иное, мы подразумеваем API Win32 и Win64, а также NT5) способна обеспечивать также поддержку среды других "подсистем", хотя этой возможностью пользуются редко и прямого отношения к тематике данной книги она не имеет. Ядро ОС NT имеет действительно надежную защиту от воздействия со стороны приложений. Windows является для него лишь одной из нескольких возможных сред. Так, предоставляемый компанией Microsoft набор инструментальных средств Windows Resource Kit включает подсистему POSIX, но кроме того существуют подсистемы POSIX, предлагаемые в рамках проекта разработки программного обеспечения с открытым исходным кодом.

Поддержка процессоров

Хотя это не и не входит в сферу непосредственных интересов разработчика приложений, вам следует знать, что Windows поддерживает самые различные базовые процессоры и архитектуры систем, для чего предусмотрен уровень аппаратных абстракций (Hardware Abstraction Layer, HAL), который обеспечивает переносимость программ на системы с другой архитектурой процессора.

Windows выполняется преимущественно на процессорах семейства Intel x86, новейшими представителями которого являются процессоры Pentium и Xeon, а одним из предыдущих — Intel 486. Широкое распространение получили совместимые с ними процессоры компании Advanced Micro Devices (AMD). Кроме того, Windows изначально проектировалась таким образом, чтобы обеспечивалась независимость от типа процессора. Что немаловажно, Windows Server 2003 поддерживается процессором Intel Itanium, новая 64-разрядная архитектура которого самым радикальным образом отличается от классической архитектуры процессоров семейства х86.

Другими примерами независимости Windows от архитектуры процессоров, относящимися как к прошлому, так и к настоящему, могут служить следующие:

• Windows СЕ способна выполняться на целом ряде процессоров, не принадлежащих семейству х86.

• Windows NT первоначально поддерживалась Alpha-процессорами компании Digital Equipment Corporation (которая была приобретена сначала компанией Compaq, а затем компанией Hewlett Packard).

• 64-разрядные процессоры компании AMD Athlon 64 и Opteron (AMD64) обеспечивают 64-разрядное расширение архитектуры х86, отражающее иной подход по сравнению с тем, который используется в архитектуре Itanium.

• Недавно объявленные компанией Intel 32/64-разрядные процессоры будут представлять собой 64-разрядные расширения процессоров х86.

Воздействие Windows на ситуацию на рынке

Тот факт, что система Windows способна обеспечивать важнейшие функциональные возможности на нескольких платформах, вряд ли можно считать уникальным. В конце концов, этим могут похвастаться многие коммерческие ОС, не говоря уже об ОС с открытым исходным кодом, a UNIX[3] и Linux уже в течение длительного времени эксплуатируются на самых разнообразных системах. Между тем, использование Windows и разработка приложений для Windows сулят значительные преимущества как в коммерческом, так и в техническом отношении.

• Windows занимает на рынке, особенно на рынке настольных систем, господствующее положение, которое прочно удерживается ею в течение многих лет.[4] Поэтому перед приложениями Windows открыт огромный целевой рынок, емкость которого исчисляется десятками миллионов систем, вследствие чего остальные настольные системы, включая UNIX, Linux и Macintosh, играют на рынке значительно меньшую роль.

• Приложения Windows могут использовать GUI, знакомый десяткам миллионов пользователей, а для многих приложений предусмотрена возможность их адаптации, или "локализации", в соответствии с региональными требованиями, предъявляемыми к языку и внешнему виду интерфейса.

• Windows поддерживает SMP-системы. Сфера применения Windows не ограничивается настольными системами и охватывает как серверы масштаба подразделения и предприятия, так и высокопроизводительные рабочие станции.[5]

• Windows (правда, это не относится к Windows 9x и Windows СЕ) сертифицирована Управлением национальной безопасности (National Security Agency, NSA) как система, обеспечивающая уровень безопасности С2.

• Применение большинства других ОС, отличных от UNIX, Linux и Windows, ограничивается только системами, предоставляемыми единственным поставщиком.

• Операционные системы семейства Windows предлагают ряд возможностей, которые в стандартной системе UNIX отсутствуют, хотя и могут быть доступными в некоторых реализациях. В качестве примера можно привести систему безопасности уровня С2, а также службы NT Services.

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

Windows, стандарты и открытые системы

Эта книга посвящена разработке приложений с использованием Windows API. Вполне естественно, что у программистов, воспитанных на UNIX и открытых системах, могут возникнуть следующие вопросы: "Является ли Windows открытой системой?", "Представляет ли собой Windows промышленный стандарт?", "Не является ли Windows всего лишь очередным патентованным API?" Ответы на эти вопросы во многом зависят от того, что именно понимается под определениями открытая (open), промышленный стандарт (industry standard) или патентованный (proprietary), а также от того, какие преимущества ожидаются от использования открытых систем.

Windows API полностью отличается от API стандарта POSIX, поддерживаемого системами Linux и UNIX. Windows не подчиняется стандарту Х/Open, как не подчиняется и никакому другому открытому промышленному стандарту из тех, которые были предложены соответствующими органами стандартизации или промышленными консорциумами.

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

• Унифицированные реализации быстрее достигают рынка.

• Отсутствуют какие-либо неожиданные фирменные "улучшения" или "расширения", с которыми потом приходится бороться программисту, хотя небольшие различия, существующие между различными платформами Windows, все же приходится учитывать.

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

• Базовая аппаратная платформа является открытой. Разработчики могут выбирать любого из многочисленных поставщиков платформ по своему усмотрению.

Жаркие споры относительно того, к добру ли такая ситуация для пользователей и компьютерной индустрии в целом, или она только вредит общему делу, еще не закончились. Мы не будем пытаться участвовать в этом споре; задача данной книги состоит лишь в том, чтобы помочь разработчикам приложений как можно скорее приступить к работе в Windows.

В действительности системы Windows поддерживают многие важные стандарты. Так, Windows поддерживает стандартные библиотеки С и С+ и целый ряд открытых стандартов межплатформенного взаимодействия. В качестве примера можно привести сокеты Windows (Windows Sockets), предоставляющие стандартный интерфейс сетевого программирования, который обеспечивает возможность использования TCP/IP и других сетевых протоколов и тем самым открывает возможности доступа в Internet и взаимодействия с системами, не принадлежащими семейству Windows. To же самое остается справедливым и по отношению к протоколу удаленного вызова процедур (Remote Procedure Calls, RPC).[6] Системы самой различной природы могут связываться с высокоуровневыми системами управления базами данных (СУБД) при помощи языка структурированных запросов (SQL). Наконец, в общий круг предложений Windows входит поддержка Internet, обеспечиваемая Web-серверами и серверам иного рода. Windows поддерживает такие ключевые стандарты, как TCP/IP, а на активно действующем рынке поставщиков решений Windows вам предлагают приобрести за разумную плату множество других ценных дополнительных продуктов, в том числе клиенты и серверы X Window.

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

Библиотеки совместимости

Несмотря на наличие библиотек совместимости (compatibility libraries), ими пользуются очень редко. Существуют две возможности.

• В системах на основе UNIX, Linux, Macintosh и некоторых других может быть развернута одна из библиотек совместимости Windows, например, эмулятор Windows с открытым исходным кодом Wine, что обеспечивает переносимость исходного кода из Windows.

• За счет использования программного обеспечения с открытым исходным кодом и набора инструментальных средств Windows Resource Kit компании Microsoft поверх подсистемы Windows может быть развернута библиотека совместимости POSIX. Весьма ограниченная по своим возможностям библиотека совместимости входит в состав среды визуальной разработки при ложений Microsoft Visual C++.

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

Принципы, лежащие в основе Windows

Полезно никогда не забывать о некоторых базовых принципах Windows. В Windows API имеется множество как самых незаметных, так и значительных отличий от других API, таких как POSIX API, с которым знакомы программисты, работающие в UNIX и Linux. И хотя с применением Windows не связаны какие-либо специфические трудности в работе, она потребует от вас внесения некоторых изменений в привычные стиль и методику программирования.

Ниже описаны некоторые из важнейших характеристик Windows, с которыми вы ближе познакомитесь по мере дальнейшего изложения материала.

Многие системные ресурсы Windows представляются в виде объектов ядра (kernel objects), для идентификации и обращения к которым используются дескрипторы (handles). По смыслу эти дескрипторы аналогичны дескрипторам (descriptors) файлов и идентификаторам (ID) процессов в UNIX.[7]

• Любые манипуляции с объектами ядра осуществляются только с использованием Windows API. "Лазеек" для обхода этого правила нет. Подобная организация работы согласуется с принципами абстрагирования данных, используемыми в объектно-ориентированном программировании, хотя сама система Windows объектно-ориентированной не является.

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

• Windows — богатый возможностями и гибкий интерфейс. Во-первых, одни и те же или аналогичные задачи могут решаться с помощью сразу нескольких функций; так, имеются вспомогательные функции (convenience functions), полученные объединением часто встречающихся последовательностей функциональных вызовов в одну функцию (к числу подобных функций принадлежит и функция CopyFile, используемая в одном из примеров далее в этой главе). Во-вторых, функции часто имеют многочисленные параметры и флаги, многие из которых обычно игнорируются. Данная книга не претендует на роль энциклопедического справочника, и основное внимание в ней концентрируется лишь на наиболее важных функциях и параметрах.

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

• Базовой единицей выполнения в Windows является поток (thread). В одном процессе (process) могут выполняться один или несколько потоков.

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

WaitForSingleObject

WaitForSingleObjectEx

WaitForMultipleObjects

WaitNamedPipe

Существует также несколько соглашений, регулирующих порядок использования имен типов:

• Имена предопределенных типов данных, необходимых API, также являются описательными, и в них должны использоваться прописные буквы.

К числу наиболее распространенных относятся следующие типы данных:

BOOL (определен как 32-битовый объект, предназначенный для хранения одного логического значения)

HANDLE

DWORD (вездесущее 32-битовое целое без знака)

LPTSTR (указатель на строку, состоящую из 8– или 16-битовых символов)

LPSECURITY_ATTRIBUTES

С другими многочисленными типами данных вы будете знакомиться по мере изложения материала.

• В именах предопределенных типов указателей операция * не используется, и они отражают дополнительные отличия между указателями различного типа, как, например, в случае типов LPTSTR (определен как TCHAR *) и LPCTSTR (определен как const TCHAR *). Примечание. Тип TCHAR может обозначать как обычный символьный тип char, так и двухбайтовый тип wchar_t.

• В отношении использования имен переменных, — по крайней мере, в прототипах функций, — также имеются определенные соглашения. Так, имя lpszFileName соответствует "длинному указателю на строку, завершающуюся нулевым символом", которая содержит имя файла. Этот пример иллюстрирует применение так называемой "венгерской нотации", которой мы в данной книге, как правило, не стремимся придерживаться. Точно так же, dwAccess — двойное слово (32 бита), содержащее флаги прав доступа к файлу, где "dw" означает "double word" — "двойное слово".

Примечание

Будет очень полезно, если вы просмотрите системные заголовочные (включаемые) файлы, в которых содержатся определения функций, констант, флагов, кодов ошибок и тому подобное. Многие из представляющих для нас интерес файлов, аналогичных тем, которые предложены ниже в качестве примера, являются частью среды Microsoft Visual C++ и обычно устанавливаются в каталоге Program Files\Microsoft Visual Studio.NET\Vc7\PlatformSDK\Include (или Program Files\Microsoft Visual Studio\VC98\Include в случае VC++ 6.0):

WINDOWS.H (файл, обеспечивающий включение всех остальных заголовочных файлов)

WINNT.Н

WINBASE.H 

Наконец, несмотря на то что оригинальный API Win32 с самого начала разрабатывался как совершенно независимый интерфейс, он проектировался с учетом обеспечения обратной совместимости с API Winl6, входившим в состав Windows 3.1. Это привело к некоторым досадным с точки зрения программиста последствиям:

• В названиях типов встречаются элементы анахронизма, как, например, в случае типов LPTSTR и LPDWORD, ссылающихся на "длинный указатель", который является простым 32– или 64-битовым указателем. Необходимость в указателях какого-либо иного типа отсутствует. Иногда составляющая "длинный" опускается, и тогда, например, типы LPVOID и PVOID являются эквивалентными.[8]

• В имена некоторых символических констант, например WIN32_FIND_DATA, входит компонент "WIN32", хотя те же константы используются и в Win64.

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

Подготовка к работе с Win64

Интерфейс Win64, который во время написания данной книги поддерживался Windows XP и Windows Server 2003 на процессорах семейства AMD64 (Opteron и Athlon 64) компании AMD и процессорах семейства Itanium (ранее известных под кодовыми названиями Merced, McKinley, Madison и IA-64) компании Intel, будет играть все более важную роль при создании крупных приложений. Существенные отличия между Win32 и Win64 обусловлены различиями в размере указателей (64 бита в Win64) и объеме доступного виртуального адресного пространства.

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

С точки зрения программиста основные отличия при переходе к Win64 обусловлены размерами указателей и необходимостью помнить о том, что длины указателя и целочисленной переменной (LONG, DWORD и так далее) не обязательно должны совпадать. С этой целью определены, например, типы DWORD32 и DWORD64, позволяющие явно управлять размером переменных. Два других типа, POINTER_32 и POINTER_64, позволяют управлять размером указателей.

Как вы сами убедитесь, приложив лишь самые незначительные усилия, можно добиться того, чтобы программы работали как в Win32, так и в Win64, и поэтому мы будем часто ссылаться на API просто как на Windows или, иногда, Win32. Дополнительная информация относительно Win64 содержится в главе 16, где, в частности, обсуждаются вопросы совместимости исходных и двоичных кодов. 

Программисты, работающие с UNIX и Linux, столкнутся в Windows с рядом интересных особенностей. Так, в Windows дескрипторы HANDLE являются "непрозрачными". Они не представляют собой ряд последовательно возрастающих целых чисел. В то же время, например, в UNIX дескрипторы файлов 0, 1 и 2 имеют специальное назначение, что должно обязательно учитываться при написании программ. Ничего подобного в Windows вы не обнаружите.

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

Программистам, которые, работая в UNIX, привыкли к коротким именам функций и параметров и использовали преимущественно строчные буквы, придется приспосабливаться к более пространному стилю Windows. Стиль Windows близок к стилю интерфейса компании Hewlett Packard (ранее — DEC и Compaq); программистам, работающим с OpenVMS, многое покажется знакомым. Указанное сходство между OpenVMS и Windows частично объясняется тем, что Дэвид Катлер (David Cutler), создатель первоначальной архитектуры VMS, предполагал, что она должна играть ту же роль, что и NT или Windows.

Радикальные отличия касаются такого хорошо знакомого нам всем понятия, как процессы. В Windows процессы не обладают свойствами наследования, хотя и могут быть организованы в виде объектов заданий.

В завершение следует отметить, что в текстовых файлах Windows конец строки отмечается последовательностью управляющих символов CR-LF, а не LF, как в это принято в UNIX. 

О целесообразности привлечения функций стандартной библиотеки C для обработки файлов

Несмотря на всю уникальность возможностей Windows, старый добрый язык С и его стандартная библиотека ANSI С по-прежнему могут с успехом использоваться при решении большинства задач, связанных с обработкой файлов. Кроме того, библиотека С (указание на ее соответствие стандарту ANSI С мы будем часто опускать) содержит большое число очень нужных функций, аналогов которых среди системных вызовов нет. К их числу относятся, например, функции, описанные в заголовочных файлах <string.h>, <stdlib.h> и <signal.h>, а также функции форматированного и символьного ввода/вывода. В то же время, имеются и такие функции, как fopen и fread, описанные в заголовочном файле <stdio.h>, для которых находятся близко соответствующие им системные вызовы.

В каких же случаях при обработке файлов можно обойтись библиотекой С, а в каких необходимо использовать системные вызовы Windows? Тот же вопрос можно задать и в отношении использования потоков (streams) ввода/вывода C++ или системы ввода/вывода, которая предоставляется платформой .NET. Простых ответов на эти вопросы не существует, но если во главу угла поставить переносимость программ на платформы, отличные от Windows, то в тех случаях, когда приложению требуется только обработка файлов, а не, например, управление процессами или другие специфические возможности Windows, предпочтение следует отдавать библиотеке С и потокам ввода/вывода C++. Вместе с тем, многими программистами ранее уже делались попытки выработать рекомендации относительно адекватности использования библиотеки С в тех или иных случаях, и эти же рекомендации должны быть применимы и в отношении Windows. Кроме того, с учетом возможностей расширения функциональности, а также повышения производительности и гибкости программ, обеспечиваемые Windows, нередко оказывается более удобным или даже необходимым не ограничиваться библиотекой С, в чем вы постепенно станете убеждаться уже начиная с главы 3. К числу возможностей Windows, не поддерживаемых библиотекой С, относятся блокирование и отображение файлов (необходимое для разделения общих областей памяти), асинхронный ввод/вывод, произвольный доступ к файлам чрезвычайно крупных размеров (4 Гбайт и выше) и взаимодействие между процессами.

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

Что требуется для работы с данной книгой

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

Разумеется, прежде всего, вам потребуется весь ваш опыт в области разработки приложений; предполагается также, что язык С вам знаком. Однако прежде, чем браться за решение упражнений и разбор примеров, вы должны убедиться в том, что располагаете всем необходимым аппаратным и программным обеспечением, перечень которого приводится ниже.

• Система с установленной ОС Windows.

• Компилятор С и любая подходящая среда разработки приложений, например, Microsoft Visual Studio .NET или Microsoft Visual C++ версии 6.0. Имеются также системы разработки приложений от других поставщиков, и хотя примеры из книги нами на них не тестировались, из поступивших от нескольких читателей писем нам стало известно, что примеры, пусть даже после внесения в них незначительных изменений, в некоторых случаях успешно выполнялись даже при использовании других систем. Кроме того, в приложении А содержится информация, касающаяся использования инструментальных средств с открытым исходным кодом. Примечание. Наше внимание будет сосредоточено на разработке консольных приложений Windows, и поэтому возможности Microsoft Visual Studio .NET будут задействованы далеко не в полной мере.

• Достаточный для разработки программ объем ОЗУ и наличие свободного места на жестком диске. Практически любая коммерчески доступная система предоставит вам достаточный объем памяти, место на диске и процессорную мощность, которых хватит для запуска примеров и среды разработки приложений, однако предварительно необходимо проверить, какие именно требования к ресурсам предъявляет эта среда.[9]

• Привод компакт-диска, системного или сетевого, для установки среды разработки приложений.

• Оперативная документация наподобие той, которая поставляется вместе с Microsoft Visual C++. Желательно, чтобы вы установили эту документацию на своем жестком диске, поскольку к ней будет требоваться частый доступ. Дополнительную информацию вы всегда сможете получить на Web-сайте компании Microsoft.

Пример: простое последовательное копирование файла

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

1. С использованием библиотеки С.

2. С использованием Windows.

3. С использованием вспомогательной функции Windows — CopyFile.

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

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

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

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

Само по себе копирование файлов не представляет особого интереса, однако сравнение программ не только позволит вам быстро оценить, чем отличаются друг от друга различные системы, но и послужит хорошим предлогом для знакомства с Windows. В последующих примерах реализуется ограниченный вариант одной из команд UNIX — cp, осуществляющей копирование одного файла в другой и требующей задания имен файлов в командной строке. В приведенных программах организована лишь простейшая проверка ошибок, которые могут возникать на стадии выполнения, а существующие файлы просто перезаписываются. Эти и другие недостатки будут учтены в последующих Windows-реализациях этой и других программ. Примечание. Реализация программы для UNIX находится на Web-сайте книги.

Копирование файлов с использованием стандартной библиотеки С

Как видно из текста программы 1.1, стандартная библиотека С поддерживает объекты потоков ввода/вывода FILE, которые напоминают, несмотря на меньшую общность, объекты Windows HANDLE, представленные в программе 1.2.

Программа 1.1. срC: копирование файлов с использованием библиотеки С

/* Глава 1. Базовая программа копирования файлов cp. Реализация, использующая библиотеку С. */

/* cp файл1 файл2: Копировать файл1 в файл2. */

#include <stdio.h>

#include <errno.h>

#define BUF_SIZE 256


int main(int argc, char *argv[]) {

 FILE *in_file, *out_file;

 char rec [BUF_SIZE];

 size_t bytes_in, bytes_out;

 if (argc != 3) {

  printf("Использование: срС файл1 файл2\n");

  return 1;

 }

 in_file = fopen(argv [1], "rb");

 if (in_file == NULL) {

  perror(argv[1]);

  return 2;

 }

 out_file = fopen(argv [2], "wb");

 if (out_file == NULL) {

  perror(argv [2]);

  return 3;

 }

 /* Обработать входной файл по одной записи за один раз. */

 while ((bytes_in = fread(rec, 1, BUF_SIZE, in_file)) > 0) {

  bytes_out = fwrite(rec, 1, bytes_in, out_file);

  if (bytes_out != bytes_in) {

   perror("Неустранимая ошибка записи.");

   return 4;

  }

 }

 fclose (in_file);

 fclose (out_file);

 return 0;

}

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

1. Объекты открытых файлов идентифицируются указателями на структуры FILE (в UNIX используются целочисленные дескрипторы файлов). Указателю NULL соответствует несуществующий объект. По сути, указатели являются разновидностью дескрипторов объектов открытых файлов.

2. В вызове функции fopen указывается, каким образом должен обрабатываться файл — как текстовый или как двоичный. В текстовых файлах содержатся специфические для каждой системы последовательности символов, используемых, например, для обозначения конца строки. Во многих системах, включая Windows, в процессе выполнения операций ввода/вывода каждая из таких последовательностей автоматически преобразуется в нулевой символ, который интерпретируется в языке С как метка конца строки, и наоборот. В нашем примере оба файла открываются как двоичные.

3. Диагностика ошибок реализуется с помощью функции perror, которая, в свою очередь, получает информацию относительно природы сбоя, возникающего при вызове функции fopen, из глобальной переменной errno. Вместо этого можно было бы воспользоваться функцией ferror, возвращающей код ошибки, ассоциированный не с системой, а с объектом FILE.

4. Функции fread и fwrite возвращают количество обработанных байтов непосредственно, а не через аргумент, что оказывает существенное влияние на логику организации программы. Неотрицательное возвращаемое значение говорит об успешном выполнении операции чтения, тогда как нулевое — о попытке чтения метки конца файла.

5. Функция fclose может применяться лишь к объектам типа FILE (аналогичное утверждение справедливо и в отношении дескрипторов файлов UNIX).

6. Операции ввода/вывода осуществляются в синхронном режиме, то есть прежде чем программа сможет выполняться дальше, она должна дождаться завершения операции ввода/вывода.

7. Для вывода сообщений об ошибках удобно использовать входящую в библиотеку С функцию ввода/вывода printf, которая даже будет использована в первом примере Windows-программы.

Преимуществом реализации, использующей библиотеку С, является ее переносимость на платформы UNIX, Windows, а также другие системы, которые поддерживают стандарт ANSI С. Кроме того, как показано в приложении В, в том, что касается производительности, вариант, использующий функции ввода/вывода библиотеки С, ничуть не уступает другим вариантам реализации. Тем не менее, в этом случае программы вынуждены ограничиваться синхронными операциями ввода/вывода, хотя влияние этого ограничения будет несколько ослаблено использованием потоков Windows (начиная с главы 7).

Как и их эквиваленты в UNIX, программы, основанные на функциях для работы с файлами, входящих в библиотеку С, способны выполнять операции произвольного доступа к файлам (с использованием функции fseek или, в случае текстовых файлов, функций fsetpos и fgetpos), но это является уже потолком сложности для функций ввода/вывода стандартной библиотеки С, выше которого они подняться не могут. Вместе с тем, Visual C++ предоставляет нестандартные расширения, способные, например, поддерживать блокирование файлов. Наконец, библиотека С не позволяет управлять средствами защиты файлов.

Резюмируя, можно сделать вывод, что если простой синхронный файловый или консольный ввод/вывод — это все, что вам надо, то для написания переносимых программ, которые будут выполняться под управлением Windows, следует использовать библиотеку С.

Копирование файлов с использованием Windows

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

Программа 1.2. cpW: копирование файлов с использованием Windows, первая реализация 

/* Глава 1. Базовая программа копирования файлов cp. Реализация, использующая Windows. */

/* cpW файл1 файл2: Копировать файл1 в файл2. */

#include <windows.h>

#include <stdio.h>

#define BUF_SIZE 256


int main (int argc, LPTSTR argv []) {

 HANDLE hIn, hOut;

 DWORD nIn, nOut;

 CHAR Buffer [BUF_SIZE];

 if (argc != 3) {

  printf ("Использование: cpW файл1 файл2\n");

  return 1;

 }

 hIn = CreateFile(argv [1], GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);

 if (hIn == INVALID_HANDLE_VALUE) {

  printf("Невозможно открыть входной файл. Ошибка: %х\n", GetLastError());

  return 2;

 }

 hOut = CreateFile(argv[2], GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

 if (hOut == INVALID_HANDLE_VALUE) {

  printf("Невозможно открыть выходной файл. Ошибка: %x\n", GetLastError()); 

  return 3;

 }

 while (ReadFile(hIn, Buffer, BUF_SIZE, &nIn, NULL) && nIn > 0) {

  WriteFile(hOut, Buffer, nIn, &nOut, NULL);

  if (nIn != nOut) {

   printf ("Неустранимая ошибка записи: %x\n", GetLastError());

   return 4;

  }

 }

 CloseHandle(hIn);

 CloseHandle(hOut);

 return 0;

}

Этот простой пример иллюстрирует некоторые особенности программирования в среде Windows, к подробному рассмотрению которых мы приступим в главе 2.

1. В программу всегда включается файл <windows.h>, в котором содержатся все необходимые определения функций и типов данных Windows.[10]

2. Все объекты Windows идентифицируются переменными типа Handle, причем для большинства объектов можно использовать одну и ту же общую функцию CloseHandle.

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

4. Windows определяет многочисленные символические константы и флаги. Обычно они имеют длинные имена, нередко поясняющие назначение данного объекта. В качестве типичного примера можно привести имена INVALID_HANDLE_VALUE и GENERIC_READ.

5. Функции ReadFile и WriteFile возвращают булевские значения, а не количества обработанных байтов, для передачи которых используются аргументы функций. Это определенным образом изменяет логику организации работы циклов.[11] Нулевое значение счетчика байтов указывает на попытку чтения метки конца файла и не считается ошибкой.

6. Функция GetLastError позволяет получать в любой точке программы коды системных ошибок, представляемые значениями типа DWORD. В программе 1.2 показано, как организовать вывод генерируемых Windows текстовых сообщений об ошибках.

7. Windows NT обладает более мощной системой защиты файлов, описанной в главе 15. В данном примере защита выходного файла не обеспечивается.

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

Копирование файлов с использованием вспомогательной функции Windows

Для повышения удобства работы в Windows предусмотрено множество вспомогательных функций (convenience functions), которые, объединяя в себе несколько других функций, обеспечивают выполнение часто встречающихся задач программирования. В некоторых случаях использование этих функций может приводить к повышению производительности (см. приложение В). Например, благодаря применению функции CopyFile значительно упрощается программа копирования файлов (программа 1.3). Помимо всего прочего, это избавляет нас от необходимости заботиться о буфере, размер которого в двух предыдущих программах произвольно устанавливался равным 256.

Программа1.3.cpCF: копирование файлов с использованием вспомогательной функции Windows

/* Глава 1. Базовая программа копирования файлов cp. Реализация, в которой для повышения удобства использования и производительности программы используется функция Windows CopyFile. */

/* cpCF файл1 файл2: Копировать файл1 в файл2. */

#include <windows.h>

#include <stdio.h>


int main (int argc, LPTSTR argv []) {

 if (argc != 3) {

  printf ("Использование: cpCF файл1 файл2\n");

  return 1;

 }

 if (!CopyFile(argv[1], argv[2], FALSE)) {

  printf("Ошибка при выполнении функции CopyFile: %x\n", GetLastError());

  return 2;

 }

 return 0;

} 

Резюме

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

Целевыми платформами для данной книги и содержащихся в ней примеров являются системы NT5 (Windows XP, 2000 и Server 2003). Тем не менее, большая часть материала книги применима также к ранним версиям NT и системам Windows 9x (95, 98 и Me).

В следующих главах

Главы 2 и 3 посвящены гораздо более пристальному рассмотрению функций ввода/вывода и файловой системы. Они включают в себя такие темы, как консольный ввод/вывод, обработка символов ASCII и Unicode, работа с файлами и каталогами, а также программирование реестра. В указанных главах разрабатываются базовые методики и закладывается фундамент для остальной части книги.

Дополнительная литература

Полная информация о рекомендуемых ниже книгах приведена в библиографическом списке в конце книги.

Win32

Двумя доступными в настоящее время книгами, в которых вопросы программирования для Windows рассматриваются с всех возможных точек зрения, являются [5] и [31]. В то же время, существует множество других книг, которые не обновлялись и не отражают прогресс, достигнутый с момента выхода Windows 95 или Windows NT.

По каждой функции Microsoft Visual C++ имеется оперативная гипертекстовая справочная документация, но ту же информацию можно получить, посетив домашнюю страницу компании Microsoft — http://www.microsoft.com, где вы найдете целый ряд ссылок на технические статьи, посвященные различным аспектам Windows. Начните с раздела MSDN (Microsoft Developer's Network) и произведите поиск по любой интересующей вас теме. Вы обнаружите огромное разнообразие официальной документации, описаний продуктов, примеров программного кода, а также другую полезную информацию.

Win64

Win64 обсуждается в нескольких книгах, но обширный материал по этой теме можно найти на домашней странице компании Microsoft. 

Архитектура Windows NT и история ее развития

Читателям, которые хотят больше узнать о целях проектирования Windows NT или понять основные принципы, лежащие в основе ее архитектуры, будет полезна книга [38]. В этой книге рассматриваются объекты, процессы, потоки, виртуальная память, ядро и подсистемы ввода/вывода. Вместе с тем, собственно функции API, а также Windows 9x и СЕ в ней не обсуждаются. Рекомендуем время от времени заглядывать в упомянутую книгу для получения дополнительной информации. Кроме того, обратитесь к ранее вышедшим книгам [9] и [37], в которых содержится важный ретроспективный анализ эволюции NT.

UNIX

В книге [40], написанной ныне покойным Уильямом Ричардом Стивенсом (W. Richard Stevens), UNIX обсуждается во многом в тех же терминах, которые в настоящей книге используются для обсуждения Windows. Книга Стивенса по-прежнему остается стандартным справочником по средствам UNIX, но в ней не рассматриваются потоки. Стандартизация UNIX претерпела изменения, однако в книге Стивенса содержатся удобные рабочие определения всего того, что предлагается в UNIX, а также в Linux. В этой книге сопоставлены возможности функций файлового ввода/вывода библиотеки С и функций ввода/вывода системы UNIX, что имеет отношение и к Windows.

Если вас интересуют сравнительные характеристики ОС и более глубокое обсуждение UNIX, обратитесь к книге [29] и ее русскоязычному изданию [49], которая помимо того, что является весьма полезной, еще и увлекательно написана, хотя некоторым читателям позиция автора может показаться несколько предвзятой.

Программирование с использованием Windows GUI

Пользовательский интерфейс в настоящей книге не рассматривается. В случае необходимости можете обратиться к [30] или [25].

Теория операционных систем

Существует масса хороших учебников по общей теории ОС. Одной из наиболее популярных является книга [35].

Стандартная библиотека ANSI С

Исчерпывающим руководством по этой теме служит книга [27]. Для получения беглого обзора можно обратиться к книге [20] или к ее русскоязычному изданию [48], которая содержит полное описание библиотеки и остается классическим учебником по языку программирования С. Эти книги помогут вам принять решение относительно того, достаточно ли возможностей библиотеки С для решения стоящих перед вами задач обработки файлов.

Windows СЕ

Тем, кто хочет применить материал настоящей книги к Windows СЕ, можно порекомендовать книгу [23].

Эмуляция Windows в UNIX

Для получения необходимой информации по этому вопросу и загрузки пакета с открытым исходным кодом Wine, позволяющего эмулировать Windows API поверх UNIX и X, посетите сайт http://www.winehq.com.

Упражнения

1.1. Скомпилируйте, скомпонуйте и выполните каждую из трех программ, предназначенных для копирования файлов. К числу других возможных вариантов реализации относится использование библиотек совместимости с UNIX, включая библиотеку Microsoft Visual C++ (программа, использующая эту библиотеку, доступна на Web-сайте книги). Примечание. На Web-сайте книги на ходятся исходные коды всех программ. Краткие рекомендации относительно порядка использования этих кодов в средах Microsoft Visual Studio .NET и Microsoft Visual C++ 6.0 вы найдете в приложении А.

1.2. Ознакомьтесь с одной из сред разработки приложений, например, Microsoft Visual Studio .NET или Microsoft Visual C++. В частности, научитесь создавать в выбранной среде консольные приложения. Для проведения самостоятельных экспериментов с использованием рассмотренных в данной главе программ пользуйтесь отладчиком. Инструкции относительно того, как следует приступать к работе, содержатся в приложении А, а обширную дополнительную информацию вы найдете на Web-сайте компании Microsoft и в документации к используемой вами среде разработки приложений.

1.3. В Windows в качестве метки конца строки используется последовательность символов "возврат каретки-перевод строки" (CR-LF). Определите, как изменится поведение программы 1.1, если входной файл открывать в двоичном режиме, а выходной — в текстовом, или наоборот. Как это будет проявляться в системе UNIX и в других системах?

1.4. Выполните для каждой из программ хронометраж при копировании файлов большого размера. Получите соответствующие данные для как можно большего числа различных вариантов и сравните полученные результаты между собой. Вряд ли следует подчеркивать, что быстродействие программ зависит от множества факторов, однако, в предположении, что все остальные параметры системы остаются неизменными, сопоставление результатов, полученных с использованием различных вариантов реализации программы, может представлять определенную ценность. Совет. Для облегчения анализа результатов расположите их в виде таблицы. Программа, обеспечивающая количественный контроль длительности временных промежутков, приведе на в главе 6, а некоторые экспериментальные результаты представлены в приложении В.

ГЛАВА 2

Использование файловой системы и функций символьного ввода/вывода Windows

Нередко самыми первыми средствами операционной системы (ОС), с которыми разработчик сталкивается в любой системе, являются файловая система и простой терминальный ввод/вывод. Ранние ОС для PC, такие как MS-DOS, не могли дать ничего больше, кроме возможностей работы с файлами и терминального (или консольного) ввода/вывода, но эти же ресурсы и сейчас занимают центральное место почти в любой ОС.

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

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

CreateFile

ReadFile

WriteFile

CloseHandle

В данной главе не только подробно описываются эти и родственные им функции, но и обсуждаются функции, предназначенные для обработки символов и обеспечения консольного ввода/вывода. Сначала будут кратко охарактеризованы существующие типы файловых систем и их основные свойства. Далее будет показано, каким образом введение расширенной формы символов в кодировке Unicode помогает приложениям справиться с проблемой поддержки национальных языков. Главу завершает введение в управление файлами и каталогами в Windows.

Файловые системы Windows

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

1. Файловая система NT (NTFS) — современная файловая система, которая поддерживает длинные имена файлов, а также безопасность, устойчивость к сбоям, шифрование, сжатие, расширенные атрибуты, и позволяет работать с очень большими файлами и объемами данных. Заметьте, что на гибких дисках (флоппи-дисках, или дискетах) система NTFS использоваться не может; не поддерживается она и системами Windows 9x.

2. Файловые системы FAT и FAT32 (от File Allocation Table — таблица размещения файлов) происходят от 16-разрядной файловой системы (FAT16), первоначально использовавшейся в MS-DOS и Windows 3.1. FAT32 впервые была введена в Windows 98 для поддержки жестких дисков большого объема и других усовершенствованных возможностей; далее под термином FAT мы будем подразумевать любую из вышеуказанных версий. FAT является единственно доступной файловой системой для дисков (но не компакт-дисков), работающих под управлением Windows 9x, а также гибких дисков. Разновидностью FAT является TFAT — ориентированная на поддержку механизма транзакций версия, используемая в Windows СЕ. Постепенно FAT выходит из употребления и в большинстве случаев ее можно встретить лишь на устаревших системах, особенно тех, обновление которых после первоначальной установки на них Windows 9x выполнялось без преобразования типа существующей файловой системы.

3. Файловая система компакт-дисков (CDFS), как говорит само ее название, предназначена для доступа к информации, записанной на компакт-дисках. CDFS удовлетворяет требованиям стандарта ISO 9660.

4. Универсальный дисковый формат (Universal Disk Format, UDF) поддерживает диски DVD и, в конечном итоге, должен полностью вытеснить систему CDFS. Поддержка UDF в Windows XP поддерживает как чтение, так и запись файлов, тогда как в Windows 2000 для UDF обеспечивается только запись.

Windows поддерживает, причем как на стороне клиента, так и на стороне сервера, такие распределенные файловые системы, как Networked File System (Сетевая файловая система), или NFS, и Common Internet File System (Общая межсетевая файловая система), или CIFS; на серверах обычно используют NTFS. В Windows 2000 и Windows Server 2003 обеспечивается широкая поддержка сетевых хранилищ данных (Storage Area Networks, SAN) и таких развивающихся технологий хранения данных, как IP-хранилища. Кроме того, Windows дает возможность разрабатывать пользовательские файловые системы, которые поддерживают тот же API доступа к файлам, что и API, рассматриваемый в этой и следующей главах.

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

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

Правила именования файлов

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

• Полное имя файла на диске, содержащее путь доступа к нему, начинается с указания буквенного имени диска, например, А: или С:. Обычно буквы А: и В: относятся к флоппи-дисководам, а С:, D: и так далее — к жестким дискам и приводам компакт-дисков. Последующие буквы алфавита, например, Н: или K:, обычно соответствуют сетевым дискам. Примечание. Буквенные обозначения дисков не поддерживаются в Windows СЕ.

• Существует и другой возможный вариант задания полного пути доступа — использование универсальной кодировки имен (Universal Naming Code, UNC), в соответствии с которой указание пути начинается с глобального корневого каталога, обозначаемого двумя символами обратной косой черты (\\), с последующим указанием имени сервера и имени разделяемого ресурса (share name) для определения местоположения ресурса на файловом сервере сети. Таким образом, первая часть полного пути доступа в данном случае будет иметь вид: \\servername\sharename.

• При указании полного пути доступа в качестве разделителя обычно используется символ обратной косой черты (\), но в параметрах API для этой цели можно воспользоваться также символом прямой косой черты (/), как это принято в С.

• В именах каталогов и файлов не должны встречаться символы ASCII, численные значения которых попадают в интервал 1-31, а также любой из перечисленных ниже символов:

< > : " | ? * \ /

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

• Строчные и прописные буквы в именах каталогов и файлов не различаются, то есть имена не чувствительны к регистру (case-insensitive), но в то же время они запоминают регистр (case-retaining); другими словами, если файл был создан с именем MyFile, то это же имя будет использоваться и при его отображении, хотя, например, для доступа к файлу может быть использовано также имя myFILE.

• Длина имени каталога и файла не должна превышать 255 символов, а длина полного пути доступа ограничивается значением параметра МАХ_РАТН (текущим значением которого является 256).

• Для отделения имени файла от расширения используется символ точки (.), причем расширения имен (как правило, два или три символа, находящиеся справа от самой последней точки, входящей в имя файла) обозначают предположительные типы файлов в соответствии с определенными соглашениями. Так, можно ожидать, что файл atou.EXE — это исполняемый файл, а файл atou.С — файл с исходным текстом программы на языке С. Допускается использование в именах файлов нескольких символов точки.

• Одиночный символ точки (.) и два символа точки (..), используемые в качестве имен каталогов, обозначают, соответственно, текущий каталог и его родительский каталог.

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

Операции открытия, чтения, записи и закрытияфайлов

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

Создание и открытие файла

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

Простейшее использование функции CreateFile иллюстрирует приведенный в главе 1 пример ознакомительной Windows-программы (программа 1.2), содержащей два вызова функций, в которых для параметров dwShareMode, lpSecurityAttributes и hTemplateFile были использованы значения по умолчанию. Параметр dwAccess может принимать значения GENERIC_READ и GENERIC_WRITE.

HANDLE CreateFile(LPCTSTR lpName, DWORD dwAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreate, DWORD dwAttrsAndFlags, HANDLE hTemplateFile)

Возвращаемое значение: в случае успешного выполнения — дескриптор открытого файла (типа HANDLE), иначе — INVALID_HANDLE_VALUE.

Параметры

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

lpName — указатель на строку с завершающим нулевым символом, содержащую имя файла, канала или любого другого именованного объекта, который необходимо открыть или создать. Допустимое количество символов при указании путей доступа обычно ограничивается значением МАХ_РАТН (260), однако в Windows NT это ограничение можно обойти, поместив перед именем префикс \\?\, что обеспечивает возможность использования очень длинных имен (с числом символов вплоть до 32 К). Сам префикс в имя не входит. О типе данных LPCTSTR говорится в одном из последующих разделов, а пока вам будет достаточно знать, что он относится к строковым данным.

dwAccess — определяет тип доступа к файлу — чтение или запись, что соответственно указывается флагами GENERIC_READ и GENERIC_WRITE. Ввиду отсутствия флаговых значений READ и WRITE использование префикса GENERIC_ может показаться излишним, однако он необходим для совместимости с именами макросов, определенных в заголовочном файле Windows WINNT.H. Вы еще неоднократно столкнетесь с именами, которые кажутся длиннее, чем необходимо. 

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

GENERIC_READ | GENERIC_WRITE

dwShareMode — может объединять с помощью операции поразрядного "или" следующие значения:

• 0 — запрещает разделение (совместное использование) файла. Более того, открытие второго дескриптора для данного файла запрещено даже в рамках одного и того же вызывающего процесса.

• FILE_SHARE_READ — другим процессам, включая и тот, который осуществил данный вызов функции, разрешается открывать этот файл для параллельного доступа по чтению.

• FILE_SHARE_WRITE — разрешает параллельную запись в файл.

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

lpSecurityAttributes — указывает на структуру SECURITY_ATTRIBUTES. На первых порах при вызовах функции CreateFile и всех остальных функций вам будет достаточно использовать значение NULL; вопросы безопасности файловой системы рассматриваются в главе 15.

dwCreate — конкретизирует запрашиваемую операцию: создать новый файл, перезаписать существующий файл и тому подобное. Может принимать одно из приведенных ниже значений, которые могут объединяться при помощи операции поразрядного "или" языка С.

• CREATE_NEW — создать новый файл; если указанный файл уже существует, выполнение функции завершается неудачей.

• CREATE_ALWAYS — создать новый файл; если указанный файл уже существует, функция перезапишет его.

• OPEN_EXISTING — открыть файл; если указанный файл не существует, выполнение функции завершается неудачей.

• OPEN_ALWAYS — открыть файл; если указанный файл не существует, функция создаст его.

• TRUNCATE_EXISTING — открыть файл; размер файла будет установлен равным нулю. Уровень доступа к файлу, установленный параметром dwAccess, должен быть не ниже GENERIC_WRITE. Если указанный файл существует, его содержимое будет уничтожено. В отличие от случая CREATENEW выполнение функции будет успешным даже в тех случаях, когда указанный файл не существует. 

dwAttrsAndFlags — позволяет указать атрибуты файла и флаги. Всего имеется 16 флагов и атрибутов. Атрибуты являются характеристиками файла, а не открытого дескриптора, и игнорируются, если открывается существующий файл. Некоторые из наиболее важных флаговых значений приводятся ниже.

• FILE_ATTRIBUTE_NORMAL — этот атрибут можно использовать лишь при условии, что одновременно с ним не устанавливаются никакие другие атрибуты (тогда как для всех остальных флагов одновременная установка допускается).

• FILE_ATTRIBUTE_READONLY — этот атрибут запрещает приложениям осуществлять запись в данный файл или удалять его.

• FILE_FLAG_DELETE_ON_CLOSE — этот флаг полезно применять в случае временных файлов. Файл будет удален сразу же после закрытия последнего из его открытых дескрипторов.

• FILE_FLAG_OVERLAPPED — этот флаг играет важную роль при выполнении операций асинхронного ввода/вывода, описанных в главе 14.

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

• FILE_FLAG_WRITE_THROUGH — устанавливает режим сквозной записи промежуточных данных непосредственно в файл на диске, минуя кэш.

• FILE_FLAG_NO_BUFFERING — устанавливает режим отсутствия промежуточi ной буферизации или кэширования, при котором обмен данными происходит непосредственно с буферами данных программы, указанными при вызове функций ReadFile или WriteFile (описаны далее). Соответственно требуется, чтобы начала программных буферов совпадали с границами секторов, а их размеры были кратными размеру сектора тома. Чтобы определить размер сектора при указании этого флага, вы можете воспользоваться функцией GetDiskFreeSpace.

• FILE_FLAG_RANDOM_ACCESS — предполагается открытие файла для произвольного доступа; Windows будет пытаться оптимизировать кэширование файла применительно к этому виду доступа.

• FILE_FLAG_SEQUENTIAL_SCAN — предполагается открытие файла для последовательного доступа; Windows будет пытаться оптимизировать кэширование файла применительно к этому виду доступа. Оба последних режима реализуются системой лишь по мере возможностей.

hTemplateFile — дескриптор с правами доступа GENERIC_READ к шаблону файла, предоставляющему расширенные атрибуты, которые будут применены к создаваемому файлу вместо атрибутов, указанных в параметре dwAttrsAndFlags. Обычно значение этого параметра устанавливается равным NULL. При открытии существующего файла параметр hTemplateFile игнорируется. Этот параметр используется в тех случаях, когда требуется, чтобы атрибуты вновь создаваемого файла совпадали с атрибутами уже существующего файла.

Оба вызова функции CreateFile в программе 1.2 максимально упрощены за счет использования для параметров значений по умолчанию, и, тем не менее, они вполне справляются со своими задачами. В обоих случаях было бы целесообразно использовать флаг FILE_FLAG_SEQUENTIAL_SCAN. (Эта возможность исследуется в упражнении 2.3, а соответствующие результаты тестирования производительности приведены в приложении В.)

Заметьте, что для данного файла могут быть одновременно открыты несколько дескрипторов, если только это разрешается атрибутами совместного доступа и защиты файла. Открытые дескрипторы могут принадлежать одному и тому же или различным процессам. (Управление процессами описано в главе 6).

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

Закрытие файла

Для закрытия объектов любого типа, объявления недействительными их дескрипторов и освобождения системных ресурсов почти во всех случаях используется одна и та же универсальная функция. Исключения из этого правила будут оговариваться отдельно. Закрытие дескриптора сопровождается уменьшением на единицу счетчика ссылок на объект, что делает возможным удаление таких не хранимых постоянно (nonpersistent) объектов, как временные файлы или события. При выходе из программы система автоматически закрывает все открытые дескрипторы, однако лучше все же, чтобы программа самостоятельно закрывала свои дескрипторы перед тем, как завершить работу.

Попытки закрытия недействительных дескрипторов или повторного закрытия одного и того же дескриптора приводят к исключениям (исключения и обработка исключений обсуждаются в главе 4). Не только излишне, но и не следует закрывать дескрипторы стандартных устройств, которые обсуждаются в разделе "Стандартные устройства и консольный ввод/вывод" далее в этой главе. 

BOOL CloseHandle(HANDLE hObject)

Возвращаемое значение: в случае успешного выполнения функции — TRUE, иначе — FALSE. 

Функции UNIX, сопоставимые с рассмотренными выше, отличаются от них в нескольких отношениях. Функция (системный вызов) UNIX open возвращает целочисленный дескриптор (descriptor) файла, а не дескриптор типа HANDLE, причем для указания всех параметров доступа, разделения и создания файлов, а также атрибутов и флагов используется единственный целочисленный параметр oflag. Возможные варианты выбора, доступные в обеих системах, перекрываются, однако набор опций, предлагаемый Windows, отличается большим разнообразием.

В UNIX отсутствует параметр, эквивалентный параметру dwShareMode. Файлы UNIX всегда являются разделяемыми.

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

Функция close, хотя ее и можно сопоставить с функцией CloseHandle, отличается от последней меньшей универсальностью.

Функции библиотеки С, описанные в заголовочном файле <stdio.h>, используют объекты FILE, которые можно поставить в соответствие дескрипторам (дисковые файлы, терминалы, ленточные устройства и тому подобные), связанным с потоками. Параметр mode функции fopen позволяет указать, должны ли содержащиеся в файле данные обрабатываться как двоичные или как текстовые. Имеются также опции открытия файла в режиме "только чтение", обновления файла, присоединения к другому файлу и так далее. Функция freopen обеспечивает возможность повторного использования объектов FILE без их предварительного закрытия. Средства для задания параметров защиты стандартной библиотекой С не предоставляются.

Для закрытия объектов типа FILE предназначена функция fclose. Имена большинства функций стандартной библиотеки С, предназначенных для работы с объектами FILE, снабжены префиксом "f".

Чтение файла 

BOOL ReadFile(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped)

Возвращаемое значение: в случае успешного выполнения (которое считается таковым, даже если не был считан ни один байт из-за попытки чтения с выходом за пределы файла) — TRUE, иначе — FALSE. 

Вплоть до главы 14 мы будем предполагать, что дескрипторы файлов создаются без указания флага перекрывающегося ввода/вывода FILE_FLAG_OVERLAPPED в параметре dwAttrsAndFlags. В этом случае функция ReadFile начинает чтение с текущей позиции указателя файла, и указатель файла сдвигается на число считанных байтов.

Если значения дескриптора файла или иных параметров, используемых при вызове функции, оказались недействительными, возникает ошибка, и функция возвращает значение FALSE. Попытка выполнения чтения в ситуациях, когда указатель файла позиционирован в конце файла, не приводит к ошибке; вместо этого количество считанных байтов (*lpNumberOfBytesRead) устанавливается равным 0.

Параметры

Описательные имена переменных и естественный порядок расположения параметров во многом говорят сами за себя. Тем не менее, ниже приводятся некоторые краткие пояснения.

hFile — дескриптор считываемого файла, который должен быть создан с правами доступа GENERIC_READ. lpBuffer является указателем на буфер в памяти, куда помещаются считываемые данные. nNumberOfBytesToRead — количество байт, которые должны быть считаны из файла.

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

lpOverlapped — указатель на структуру OVERLAPPED (главы 3 и 14). На данном этапе просто устанавливайте значение этого параметра равным NULL.

Запись в файл 

BOOL WriteFile(HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped)

Возвращаемое значение: в случае успешного выполнения — TRUE, иначе — FALSE.

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

Функции ReadFileGather и WriteFileGather позволяют выполнять операции чтения и записи с использованием набора буферов различного размера. 

Сопоставимыми функциями UNIX являются функции read и write, которым программист в качестве параметров должен предоставлять дескриптор файла, буфер и счетчик байтов. Возвращаемые значения этих функций указывают на количество фактически переданных байтов. Возврат функцией read значения 0 означает чтение конца файла, а значения –1 — возникновение ошибки. В противоположность этому в Windows для подсчета количества переданных байтов используется отдельный счетчик, а на успех или неудачу выполнения функции указывает возвращаемое ею булевское значение.

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

Входящие в состав стандартной библиотеки С функции read и fwrite, выполняющие операции ввода/вывода в двоичном режиме, вместо счетчика одиночных байтов, как в UNIX и Windows, используют размер объекта и счетчик объектов. Преждевременное прекращение передачи данных может быть вызвано как достижением конца файла, так и возникновением ошибки; точная причина устанавливается с использованием функций feof или ferror. Библиотека предоставляет полный набор функций, ориентированных на работу с текстовыми файлами, таких как fgets или fputs, для которых в каждой из рассматриваемых ОС аналоги вне библиотеки С отсутствуют.

Вступление: стандартные символы и символы Unicode

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

Windows поддерживает стандартные 8-битовые символы (типы char или CHAR) и (исключая Windows 9x) 16-битовые символы расширенной формы (тип WCHAR, определенный в библиотеке С как wchar_t). В документации Microsoft 8-битовый набор фигурирует как символьный набор ASCII, хотя фактически он является символьным набором Latin-1, однако в целях удобства изложения название "ASCII" будет использоваться и в нашем обсуждении. Обеспечиваемая Windows с использованием кодировки Unicode UTF-16 поддержка обобщенных символов расширенной формы позволяет представлять в стандарте Unicode символы и буквы, встречающиеся во всех основных языках, включая английский, французский, испанский, немецкий, русский, японский и китайский. 

Ниже описаны шаги, которые обычно предпринимаются при написании обобщенных (generic) Windows-приложений, то есть приложений, предусматривающих использование как символов Unicode (UTF-16, а не, например, UCS-4), так и 8-битовых ASCII-символов.

1. Определите все символы и строки с использованием обобщенных типов TCHAR, LPTSTR и LPCTSTR.

2. Чтобы иметь возможность работать с символами в расширенной форме Unicode (wchar_t в ANSI С), включите во все модули исходного кода определения #define UNICODE и #define _UNICODE; если этого не сделать, то тип TCHAR будет эквивалентен типу CHAR (char в ANSI С). Это определение должно помещаться перед директивой #include <windows.h>, и его часто задают в командной строке компилятора. Первая из указанных переменных препроцессора управляет определениями функций Windows, вторая — библиотекой С.

3. Размеры буферов для хранения символов, указываемые, например, при вызове функций ReadFile, могут определяться с использованием функции sizeof(TCHAR).

4. Используйте входящие в состав библиотеки С функции ввода/вывода обобщенных символов и строк, описанные в файле <tchar.h>. В качестве наиболее характерных из доступных функций можно назвать такие функции, как _fgettc, _itot (вместо itoa), _stprintf (вместо sprintf), _tstcpy (вместо strcpy), _ttoi, _totupper, _totlower и _tprintf.[12] Полный и исчерпывающий список таких функций можно найти в оперативной справочной системе. Все перечисленные определения зависят от определения символьной константы _UNICODE. Описанная коллекция функций не является полной. Примером функции, для которой еще не реализован аналог, позволяющий работать с символами расширенной формы, может служить функция memchr. Новые версии предоставляются по мере возникновения необходимости в них.

5. Строковые константы могут принимать одну из трех допустимых форм. Эти же соглашения следует применять и к одиночным символам. Первые две формы предоставляются стандартом ANSI С, третья — макрос _Т (эквиваленты — TEXT и _ТЕХТ) — поставляется вместе с компилятором Microsoft С.

"В этой строке используются 8-битовые символы"

L"B этой строке используются 16-битовые символы"

_Т("В этой строке используются обобщенные символы")

6. Чтобы получить доступ к необходимым определениям текстовых макросов и обобщенным функциям библиотеки С, в модуль следует включить заголовочный файл <tchar.h>, объявление которого должно предшествовать объявлению файла <windows.h>.

16-битовые символы Unicode (кодировка UTF-16) используются в Windows повсеместно; для внутреннего представления имен файлов и путей доступа в файловой системе NTFS также используется Unicode. Если определена символьная константа _UNICODE, то все вызовы функций Windows требуют использования строк, состоящих из расширенных символов; в противном случае строки 8-битовых символов преобразуются в расширенные строки. В случае программ, которые должны выполняться под управлением систем Windows 9x, не являющихся Unicode-системами, определять символические константы UNICODE и _UNICODE не следует. В средах NT или СЕ решение об использовании указанных определений вы принимаете по своему усмотрению, если только для программы не должна быть одновременно сохранена возможность выполнения под управлением Windows 9x.

Во всех последующих примерах вместо обычного типа char для символов и символьных строк будет использоваться тип TCHAR, если только по каким-то вполне обоснованным причинам не возникнет необходимости в обработке отдельных 8-битовых символов. Точно так же, тип LPTSTR соответствует указателю на обобщенную строковую переменную, а тип LPCTSTR — указателю на обобщенную строковую константу. В результате принятия этих мер программа может стать более громоздкой, однако лишь своевременный учет различных возможных вариантов обеспечивает гибкость, необходимую для разработки и тестирования приложений, допускающих как кодировку Unicode, так и 8-битовую кодировку символов, что позволит легко преобразовать программу к использованию символов Unicode, если впоследствии в этом возникнет необходимость. Более того, предоставление возможности выбора между обеими разновидностями кодировок соответствует общепринятой, если не универсальной, практике, которая сложилась к настоящему времени.

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

#ifdef UNICODE

#define TCHAR WCHAR

#else

#define TCHAR CHAR

#endif

Альтернативные функции для работы с обобщенными строками 

В тех случаях, когда при сравнении строк необходим учет специфики языковых и региональных, или локальных, особенностей на стадии выполнения, или же когда требуется сравнивать не строки, а слова,[13] то вместо функций _tcscmp и _tcscmpi вам могут понадобиться функции lstrcmp и lstrcmpi. Сравнение строк осуществляется путем простого сравнения числовых значений символов, тогда как при сравнении слов принимаются во внимание специфические для конкретного языка особенности словообразования. Если применить указанные два метода сравнения к таким парам строк, как coop/co-op и were/we're, то они приведут к противоположным результатам.

В Windows также существует группа функций, предназначенных для работы с символами и строками в представлении Unicode. Эти функции обеспечивают прозрачную обработку региональных особенностей. Типичными функциями этой группы являются функция CharUpper, которую можно применять как к строкам, так и к отдельным символам, и функция IsCharAlphaNumeric. К числу других функций для работы со строками принадлежат функция CompareString (учитывающая особенности локализации) и функция MultiByteToWideChar. Многобайтовые символы Windows 3.1 и 9х расширяют наборы 8-битовых символов, позволяя применять для представления наборов символов, используемых в языках дальневосточных стран, сдвоенные байты. Чтобы продемонстрировать использование функций обоих типов, будут рассмотрены примеры программ, в которых используются как обобщенные функции библиотеки С (_tprintf и подобные ей), так и функции Windows (CharUpper и подобные ей). Примеры в последующих главах в основном опираются на обобщенную библиотеку С.

Обобщенная функция Main

Обозначение С-функции main с ее списком аргументов (argv[]) следует заменить макросом _tmain. В зависимости от определения символической константы _UNICODE макрос разворачивается либо до main, либо до wmain. _tmain определяется в заголовочном файле <tchar.h>, который следует включать после файла <windows.h>. Тогда типичный заголовок основной программы будет иметь следующий вид:

#include <windows.h>

#include <tchar.h>

int _tmain(int argc, LPTSTR argv[]) {

 …

}

В Microsoft С функция _tmain поддерживает дополнительный третий параметр, используемый для строк окружения. Такое нестандартное расширение является обычным в UNIX.

Определения функций

В качестве примера рассмотрим функцию CreateFile. Если символьная переменная UNICODE определена, то эта функция определяется как CreateFileA, а если не определена — то как CreateFileW. Строковые параметры в объявлениях также описываются как строки 8-битовых символов или символов в расширенной форме. Следовательно, если в исходном коде присутствуют такие, например, ошибки, как использование неподходящих параметров в функции CreateFile, то в сообщениях компилятора об этих ошибках будут указываться функции CreateFileA или CreateFileW.

Стратегии использования символов Unicode

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

1. Только 8-битовые символы. Игнорируйте Unicode и продолжайте использовать для таких функций, как printf, atoi и strcmp, типы данных char (или CHAR) и стандартную библиотеку С.

2. 8-битовые символы, но с возможностью использования символов Unicode. Следуйте ранее данным рекомендациям в отношении обобщенных приложений, но не определяйте константы UNICODE и _UNICODE директивами препроцессора. В приведенных в данной книге примерах программ используется именно эта стратегия.

3. Только символы Unicode. Следуйте рекомендациям в отношении обобщенных приложений, но при этом определите директивами препроцессора обе константы UNICODE и _UNICODE. Другой возможный вариант состоит в том, чтобы использовать исключительно расширенную форму символов и функций для работы с символами. Результирующие программы не смогут правильно выполняться под управлением Windows 9x.

4. Символы Unicode и 8-битовые символы. Программа ориентируется на работу как с символами Unicode, так и с ASCII-символами, причем решение относительно того, какие участки программного кода должны работать, принимается программой на стадии выполнения с использованием переключателей времени выполнения или других возможных средств.

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

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

Стандарт локализации приложений POSIX XPG4, предоставляемый многими поставщиками UNIX, существенно отличается от стандарта Unicode. Помимо всего прочего, символы в этом стандарте могут представляться 4, 3 или 1 байтами в зависимости от контекста, особенностей локализации и так далее.

Microsoft С реализует функции стандартной библиотеки С, среди которых имеются также версии, рассчитанные на работу с символами в расширенной форме. Так, заголовочный файл <wchar.h> содержит описание функции _tsetlocale. В Windows NT используются символы Unicode, тогда как в Windows 9x используются те же многобайтовые символы (смесь 8– и 16-битовых символов), что и в Windows 3.1.

Стандартные устройства и консольный ввод/вывод

Как и в UNIX, в Windows предусмотрены три стандартных устройства, предназначенные, соответственно, для ввода данных (input), вывода данных (output) и вывода сообщений об ошибках (error). В UNIX для этих устройств используются известные системе значения дескрипторов файлов (0, 1 и 2), однако в Windows доступ к стандартным устройствам осуществляется с помощью дескрипторов типа HANDLE, для получения которых предоставляется специальная функция.

HANDLE GetStdHandle(DWORD nStdHandle)

Возвращаемое значение: в случае успешного выполнения — действительный дескриптор, иначе — значение INVALID_HANDLE_VALUE.

Параметры

Параметр nStdHandle должен принимать одно из следующих значений:

• STD_INPUT_HANDLE

• STD_OUTPUT_HANDLE

• STD_ERROR_HANDLE

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

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

BOOL SetStdHandle(DWORD nStdHandle, HANDLE hHandle)

Возвращаемое значение: в случае успешного выполнения — TRUE, иначе — FALSE. 

Параметры

Допустимые значения параметра nStdHandle функции SetStdHandle являются теми же, что и в случае функции GetStdHandle. Параметр hHandle указывает открытый файл, который назначается в качестве стандартного устройства.

Одним из методов перенаправления стандартного ввода/вывода является последовательный вызов функций SetStdHandle и GetStdHandle. Полученный в результате этого дескриптор используется в последующих операциях ввода/ вывода.

Для указания путей доступа к консольному вводу (клавиатуре) и консольному выводу предусмотрены два зарезервированных имени: "CONIN$" и "CONOUT$". Роль стандартных устройств ввода, вывода и вывода ошибок первоначально отводится консоли. Однако консоль можно использовать даже после того, как операции ввода/вывода, требующие применения стандартных устройств, будут перенаправлены; для этого требуется лишь открыть дескрипторы для файлов "CONIN$" и "CONOUT$", вызвав функцию CreateFile. 

В UNIX стандартный ввод/вывод может быть перенаправлен одним из трех способов (см. [40], стр. 61—64).

Первый метод является косвенным и основывается на том, что функция dup возвращает дескриптор файла с наименьшим доступным номером. Предположим, вы хотите переназначить стандартный ввод (файловый дескриптор 0) открытому файлу, описанному как fd_redirect. Тогда можно записать следующий код:

close (STDIN_FILENO);

dup (fd_redirect);

Во втором методе используется функция dup2, а третий метод предполагает вызов замысловатой перегруженной функции fcntl с использованием в качестве параметра значения F_DUPFD.

Операции консольного ввода/вывода могут выполняться с помощью функций ReadFile и WriteFile, но проще использовать предназначенные специально для этого функции консоли ReadConsole и WriteConsole. Основное преимущество этих функций заключается в том, что они манипулируют не байтами, а обобщенными символами (TCHAR), и, кроме того, обрабатывают символы в соответствии с текущими режимами консоли, которые устанавливаются функцией SetConsoleMode. 

BOOL SetConsoleMode(HANDLE hConsoleHandle, DWORD fdevMode)

Возвращаемое значение: тогда, и только тогда, когда функция завершается успешно — TRUE, иначе — FALSE. 

Параметры

hConsoleHandle — дескриптор буфера ввода консоли или буфера дисплея, который должен быть создан с правами доступа GENERIC_WRITE, даже если устройство предназначено только для ввода информации.

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

• ENABLE_LINE_INPUT — возврат из функции чтения (ReadConsole) происходит только после считывания символа возврата каретки.

• ENABLE_ECHO_INPUT — эхо-отображение вводимых символов на экране.

• ENABLE_PROCESSED_INPUT — установка этого флага приводит к обработке системой управляющих символов возврата на одну позицию, возврата каретки и перехода на новую строку.

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

• ENABLE_WRAP_AT_EOL_OUTPUT — переход на следующую строку экрана как при обычном выводе символов, так и при их эхо-отображении в процессе ввода.

В случае неудачного завершения функции SetConsoleMode текущий режим остается неизменным, и функция возвращает значение FALSE. Как обычно, для получения номера ошибки следует воспользоваться функцией GetLastError.

Функции ReadConsole и WriteConsole аналогичны функциям ReadFile и WriteFile. 

BOOL ReadConsole(HANDLE hConsoleInput, LPVOID lpBuffer, DWORD cchToRead, LPDWORD lpcchRead, LPVOID lpReserved)

Возвращаемое значение: тогда, и только тогда, когда функция завершается успешно — TRUE, иначе — FALSE.

Параметры у этой функции почти те же, что и у функции ReadFile. Значения обоих параметров, связанных с количеством подлежащих считыванию (cchToRead) и фактически считанных (lpcchRead) символов, выражаются в терминах обобщенных символов, а не байтов, а значение параметра lpReserved должно быть равным NULL. Как и во всех остальных подобных случаях, никогда не используйте для собственных нужд зарезервированные поля, аналогичные lpReserved, которые встречаются в некоторых функциях. Параметры функции WriteConsole имеют тот же смысл и не нуждаются в дополнительных пояснениях. В очередном примере будет проиллюстрировано применение функций Read-Console и WriteConsole, и, кроме того, будет показано, как использовать возможности управления режимом консоли.

Любому процессу в каждый момент времени может быть назначена только одна консоль. Приложениям того типа, с которым мы имели дело до сих пор, консоль передается обычно на стадии инициализации. Однако в целом ряде других случаев, например, в случае серверных или GUI-приложений, у вас может возникнуть необходимость в получении отдельной консоли, на которую можно было бы выводить информацию о состоянии программы или отладочную информацию. Для этих целей можно воспользоваться двумя простыми функциями, не имеющими параметров. 

BOOL FreeConsole(VOID)

BOOL AllocConsole(VOID) 

Функция FreeConsole отключает процесс от его консоли, тогда как функция AllocConsole создает новую консоль, ассоциированную с дескрипторами стандартного ввода информации, стандартного вывода информации и стандартного вывода сообщений об ошибках, принадлежащими данному процессу. Если консоль у процесса уже имеется, функция AllocConsole завершится с ошибкой; чтобы избежать этого, следует предварительно вызывать функцию FreeConsole.

Примечание

GUI-приложения Windows не имеют консоли по умолчанию и должны получить ее, прежде чем смогут воспользоваться функциями WriteConsole или printf для вывода на консоль. Процессы на стороне сервера также могут не иметь консоли. О том, как создать процесс без консоли, рассказано в главе 6. 

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

Исторически сложилось так, что ОС Windows ориентирована на использование терминалов или консолей в меньшей степени, чем UNIX, и не полностью воспроизводит функциональные средства UNIX, поддерживающие работу с терминалами. В книге [40] одна из глав посвящена рассмотрению обеспечиваемых UNIX возможностей терминального ввода/вывода (глава 11), а другая — псевдотерминалам (глава 19).

Разумеется, работа в Windows почти всегда ведется с использованием мощных графических интерфейсов, поддерживающих мышь и ввод с клавиатуры. Несмотря на то что рассмотрение GUI выходит за рамки данной книги, все, что мы здесь обсуждаем, будет работать и в GUI-приложениях.

Пример: вывод на консоль сообщений и подсказок для пользователя

Функция ConsolePrompt, входящая в программу 2.1, является полезной утилитой, которая выводит на консоль заданное сообщение и возвращает ответ пользователя на него. Данная утилита предусматривает возможность подавления эхо-отображения ответной информации, полученной от пользователя. В указанной функции используются функции консольного ввода/вывода и обобщенные символы. Двумя другими точками входа в этом модуле являются функции Print-Strings и PrintMsg; эти функции допускают использование любого дескриптора, однако обычно они применяются совместно с дескрипторами устройств стандартного вывода информации и стандартного вывода сообщений об ошибках. В первой функции разрешается использовать список аргументов переменной длины, тогда как во второй в качестве аргумента можно задавать только одну строку, что в некоторых случаях может оказаться удобнее. Для обработки списка аргументов переменной длины функция PrintStrings использует функции va_start, va_arg и va_end стандартной библиотеки С.

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

Примечание

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

Следует также отметить, что в примере вводится заголовочный файл Envirmnt.h (его код приведен в приложении А и предоставлен на Web-сайте книги), который должен использоваться совместно со всеми приводимыми в книге программами. Этот файл содержит определения символических констант UNICODE и _UNICODE (сами определения "закомментированы"; при компоновке приложений, предназначенных для работы с символами стандарта Unicode, символы комментариев следует удалить), а также необходимых макропеременных, учитывающих особенности окружения. В заголовочных файлах, находящихся на Web-сайте, определены также дополнительные модификаторы, которые обеспечивают импортирование и экспортирование имен функций, а также гарантируют соблюдение соответствующих соглашений о вызове функций.

Программа 2.1. PrintMsg: вспомогательные функции вывода на консоль сообщений и ожидания ответа от пользователя 

/* PrintMsg.с: ConsolePrompt, PrintStrings, PrintMsg */

#include "Envirmnt.h" /* В этом файле устанавливаются директивы #define и #undef для UNICODE. */

#include <windows.h>

#include <stdarg.h>


BOOL PrintStrings (HANDLE hOut, ...)

/* Запись сообщений в буфер экрана консоли. */

{

 DWORD MsgLen, Count;

 LPCTSTR pMsg;

 va_list pMsgList; /* Строка текущего сообщения. */

 va_start (pMsgList, hOut); /* Начать обработку сообщений. */

 while ((pMsg = va_arg(pMsgList, LPCTSTR)) != NULL) {

  MsgLen = _tcslen(pMsg);

  /* Функция WriteConsole может применяться только с дескриптором буфера экрана консоли. */

  if (!WriteConsole(hOut, pMsg, MsgLen, &Count, NULL)

      /* Функция WriteFile вызывается только в случае неудачного завершения функции WriteConsole. */

      && !WriteFile(hOut, pMsg, MsgLen * sizeof (TCHAR), &Count, NULL)) return FALSE;

 }

 va_end(pMsgList);

 return TRUE;

}


BOOL PrintMsg(HANDLE hOut, LPCTSTR pMsg)

/* Версия PrintStrings для вывода одиночного сообщения. */

{

 return PrintStrings(hOut, pMsg, NULL);

}


BOOL ConsolePrompt(LPCTSTR pPromptMsg, LPTSTR pResponse, DWORD MaxTchar, BOOL Echo)

/* Вывести на консоль подсказку для пользователя и получить от него ответ. */

{

 HANDLE hStdIn, hStdOut;

 DWORD TcharIn, EchoFlag;

 BOOL Success;

 hStdIn = CreateFile(_T("CONIN$"), GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

 hStdOut = CreateFile(_T("CONOUT$"), GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

 EchoFlag = Echo ? ENABLE_ECHO_INPUT : 0;

 Success = SetConsoleMode(hStdIn, ENABLE_LINE_INPUT | EchoFlag | ENABLE_PROCESSED_INPUT) &&

           SetConsoleMode (hStdOut, ENABLE_WRAP_AT_EOL_OUTPUT | ENABLE_PROCESSED_OUTPUT) &&

           PrintStrings (hStdOut, pPromptMsg, NULL) &&

           ReadConsole (hStdIn, pResponse, MaxTchar, &TcharIn, NULL);

 if (Success) pResponse [TcharIn – 2] = '\0';

 CloseHandle (hStdIn);

 CloseHandle (hStdOut);

 return Success;

}

Обратите внимание, что при вычислении возвращаемого функцией значения булевской переменной Success, которое служит индикатором успешности выполнения, в программе, с выгодой для логики ее работы, используется тот факт, что стандартом ANSI С гарантируется так называемое "сокращенное" вычисление логических выражений в направлении слева направо; поэтому, как только при вычислении части выражения, расположенной слева от любой из операций логического "и" (&&), в качестве результата будет получено значение FALSE, остальная часть выражения, расположенная справа от данной операции, вычисляться не будет, поскольку результат вычисления всего выражения в целом оказывается предопределенным. Данный стиль написания программ может показаться чересчур компактным, однако он обладает тем преимуществом, что позволяет организовать логически стройную и понятную последовательность системных вызовов, не загромождая программу многочисленными операторами условных переходов. Для получения более подробной информации о возможных ошибках можно воспользоваться функцией GetLastError. Распространенный в Windows возврат функциями логических значений поощряет подобную практику.

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

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

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

Пример: обработка ошибок

В программе 1.2 было продемонстрировано использование лишь самых примитивных средств обработки ошибок, а именно, получение номера ошибки в переменной типа DWORD с помощью функции GetLastError. Вызов функции, а не просто получение глобального номера ошибки, как это делается при помощи функции UNIX errno, гарантирует уникальную идентификацию системных ошибок для каждого из потоков (глава 7), использующих разделяемую область хранения данных.

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

В программе 2.2 представлена полезная универсальная функция ReportError, предназначенная для обработки ошибок и по своим возможностям аналогичная входящей в состав библиотеки С функции perror, а также описанным в [40] функциям err_sys и err_ret. Функция ReportError передает в выходной буфер сообщение в виде, определяемом первым аргументом, и либо прекращает выполнение с кодом выхода по ошибке, либо осуществляет обычный возврат управления, в зависимости от значения второго аргумента. Третий аргумент определяет, должны ли отображаться системные сообщения об ошибках.

Обратите внимание на аргументы функции FormatMessage. В качестве одного из параметров используется значение, возвращаемое функцией GetLastError, a на необходимость генерации сообщения системой указывает флаг. Сгенерированное сообщение сохраняется в буфере, выделяемом функцией, а соответствующий адрес возвращается в параметре. Имеются также другие параметры, для которых указаны значения по умолчанию. Язык сообщений может быть задан как во время компиляции, так и во время выполнения. В этой книге функция Format-Message далее нигде не используется, поэтому никаких дополнительных пояснений относительно нее в тексте не дается. 

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

В программе 2.2 вводится заголовочный файл EvryThng.h. Как следует из самого его названия, этот файл включает в себя файлы <windows.h>, Envirmnt.h и все остальные заголовочные файлы, которые были явно указаны в программе 2.1. Кроме того, в нем описаны такие обычно используемые функции, как PrintMsg, PrintStrings и ReportError. Во всех остальных примерах будет использоваться только этот заголовочный файл, листинг которого приведен в приложении А.

Обратите внимание на вызов функции HeapFree, находящийся почти в конце программы. Об этой функции будет рассказано в главе 5.

Программа 2.2. Функция Report Error, предназначенная для вывода сообщений об ошибках при выполнении системных вызовов 

#include "EvryThng.h"


VOID ReportError(LPCTSTR UserMessage, DWORD ExitCode, BOOL PrintErrorMsg)

/* Универсальная функция для вывода сообщений о системных ошибках. */

{

 DWORD eMsgLen, LastErr = GetLastError();

 LPTSTR lpvSysMsg;

 HANDLE hStdErr = GetStdHandle(STD_ERROR_HANDLE);

 PrintMsg(hStdErr, UserMessage);

 if (PrintErrorMsg) {

  eMsgLen = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, LastErr, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpvSysMsg, 0, NULL);

  PrintStrings (hStdErr, _T("\n"), lpvSysMsg, _T("\n"), NULL);

  /* Освободить блок памяти, содержащий сообщение об ошибке. */

  HeapFree(GetProcessHeap(), 0, lpvSysMsg); /* См. гл. 5. */

 }

 if (ExitCode > 0) ExitProcess (ExitCode);

 else return;

} 

Пример: копирование нескольких файлов на стандартное устройство вывода

В программе 2.3 иллюстрируется использование стандартных устройств ввода/вывода, а также демонстрируется, как улучшить контроль ошибок и усовершенствовать взаимодействие с пользователем. Эта программа представляет собой вариант ограниченной реализации команды UNIX cat, которая копирует один или несколько заданных файлов (или содержимое буфера стандартного устройства ввода, если файлы не указаны) на стандартное устройства вывода.

Программа 2.3 включает полную обработку ошибок. В большинстве других примеров проверка ошибок опущена или сведена к минимуму, но полностью включена в завершенные документированные варианты программ, находящиеся на Web-сайте. Обратите внимание на функцию Options (ее листинг приведен в приложении А), вызываемую в начале программы. Эта функция, которая включена в состав программ, находящихся на Web-сайте, и используется на протяжении всей книги, просматривает параметры в командной строке и возвращает индекс массива argv, соответствующий имени первого файла. Функция Options аналогична функции getopt, которая используется во многих программах в UNIX.

Программа 2.3. cat: вывод нескольких файлов на стандартное устройство вывода 

/* Глава 2. cat. */

/* cat [параметры] [файлы] Допускается только параметр –s, предназначенный для подавления вывода сообщений об ошибках в случае, если один из указанных файлов не существует. */

#include "EvryThng.h"

#define BUF_SIZE 0x200


static VOID CatFile(HANDLE, HANDLE);

int _tmain(int argc, LPTSTR argv[]) {

 HANDLE hInFile, hStdIn = GetStdHandle(STD_INPUT_HANDLE);

 HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);

 BOOL DashS;

 int iArg, iFirstFile;

 /* Переменная DashS будет установлена только в случае задания параметра "-s" в командной строке. */

 /* iFirstFile — индекс первого входного файла в списке argv[]. */

 iFirstFile = Options(argc, argv, _T("s"), &DashS, NULL);

 if (iFirstFile == argc) { /*Отсутствие входных файлов в списке аргументов.*/

  /* Использовать стандартное устройство ввода. */

  CatFile(hStdIn, hStdOut);

  return 0;

 }

 /* Обработать каждый входной файл. */

 for (iArg = iFirstFile; iArg < argc; iArg++) {

  hInFile = CreateFile(argv [iArg], GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

  if (hInFile == INVALID_HANDLE_VALUE && !DashS) ReportError (_T("Cat: ошибка при открытии файла"), 1, TRUE);

  CatFile (hInFile, hStdOut);

  CloseHandle (hInFile);

 }

 return 0;

}


/* Функция, выполняющая всю работу:

/* читает входные данные и копирует их на стандартное устройства вывода. */

static VOID CatFile(HANDLE hInFile, HANDLE hOutFile) {

 DWORD nIn, nOut;

 BYTE Buffer [BUF_SIZE];

 while (ReadFile(hInFile, Buffer, BUF_SIZE, &nIn, NULL) && (nIn != 0) && WriteFile(hOutFile, Buffer, nIn, &nOut, NULL));

 return;

}

Пример: преобразование символов из ASCII в Unicode

Программа 2.4 достраивает программу 1.3, в которой использовалась вспомогательная функция CopyFile. С копированием файлов вы уже знакомы, поэтому в данном примере эта операция дополняется преобразованием файла к кодировке Unicode в предположении, что первоначальной кодировкой символов является ASCII, хотя проверка этого предположения не производится. В программе предусмотрены некоторые возможности вывода сообщений об ошибках и параметр, позволяющий подавить замену существующего файла; завершающий вызов функции CopyFile заменен в программе вызовом новой функции, которая Выполняет преобразование символьных строк файла из кодировки ASCII в кодировку Unicode.

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

Обратите внимание на вызов функции _taccess, проверяющей существование файла. Эта функция является обобщенной версией функции access, которая имеется в библиотеке UNIX, но не входит в состав стандартной библиотеки С. Ее определение содержится в файле <io.h>. Если говорить точнее, функция _taccess осуществляет проверку прав доступа к файлу в соответствии с режимом, установленным значением второго параметра. Значение 0 задает проверку существования файла, 2 — проверку наличия разрешения на запись в файл, 4 — проверку наличия разрешения на чтение из файла, 6 — проверку наличия разрешения как на чтение из файла, так и на запись в файл (эти значения не связаны напрямую с такими параметрами доступа, используемыми в Windows, как GENERIC_READ). Альтернативой проверке существования файла могло бы быть открытие дескриптора при помощи функции CreateFile и его последующее закрытие после проверки действительности дескриптора. 

Программа 2.4. atou: преобразование файла с выводом сообщений об ошибках 

/* Глава 2. atou – копирование файлов с преобразованием из ASCII в Unicode. */

#include "EvryThng.h"


BOOL Asc2Un(LPCTSTR, LPCTSTR, BOOL);

int _tmain(int argc, LPTSTR argv[]) {

 DWORD LocFileIn, LocFileOut;

 BOOL DashI = FALSE;

 TCHAR YNResp[3] = _T("y");

 /* Получить параметры командной строки и индекс входного файла. */

 LocFileIn = Options(argc, argv, _T("i"), &DashI, NULL);

 LocFileOut = LocFileIn + 1;

 if (DashI) { /* Существует ли выходной файл? */

  /* Обобщенная версия функции access, осуществляющая проверку существования файла. */

  if (_taccess(argv[LocFileOut], 0) == 0) {

   _tprintf(_T("Перезаписать существующий файл? [y/n]"));

   _tscanf(_T ("%s"), &YNResp);

   if (lstrcmp(CharLower(YNResp), YES) != 0) ReportError(_T("Отказ от перезаписи"), 4, FALSE);

  }

 }

 /* Эта функция построена на основе функции CopyFile. */

 Asc2Un(argv[LocFileIn], argv [LocFileOut], FALSE);

 return 0;

}

Программа 2.5 — это вызываемая в программе 2.4 функция Asc2Un, осуществляющая преобразование кодировки символов.

Программа 2.5. Функция Asc2Un

#include "EvryThng.h"

#define BUF_SIZE 256


BOOL Asc2Un(LPCTSTR fIn, LPCTSTR fOut, BOOL bFailIfExists)

/* Функция копирования файлов с преобразованием из ASCII в Unicode. Функция построена на основе функции CopyFile. */

{

 HANDLE hIn, hOut;

 DWORD dwOut, nIn, nOut, iCopy;

 CHAR aBuffer[BUF_SIZE];

 WCHAR uBuffer [BUF_SIZE];

 BOOL WriteOK = TRUE;

 hIn = CreateFile(fin, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); 

 /* Определить поведение функции CreateFile, если выходной файл уже существует. */

 dwOut = bFailIfExists ? CREATE_NEW : CREATE_ALWAYS;

 hOut = CreateFile(fOut, GENERIC_WRITE, 0, NULL, dwOut, FILE_ATTRIBUTE_NORMAL, NULL);

 while (ReadFile(hIn, aBuffer, BUF_SIZE, &nIn, NULL)  && nIn > 0 && WriteOK) {

  for (iCopy = 0; iCopy < nIn; iCopy++)

   /* Преобразовать каждый символ. */

   uBuffer[iCopy] = (WCHAR)aBuffer [iCopy];

  WriteOK = WriteFile(hOut, uBuffer, 2 * nIn, &nOut, NULL);

 }

 CloseHandle(hIn);

 CloseHandle(hOut);

 return WriteOK;

}

Производительность программы

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

Управление файлами и каталогами

В этом разделе вводятся основные функции, предназначенные для управления файлами и каталогами.

Управление файлами

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

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

BOOL DeleteFile(LPCTSTR lpFileName) 

Чтобы скопировать файл целиком, достаточно использовать одну функцию. 

BOOL CopyFile(LPCTSTR lpExistingFileName, LPCTSTR lpNewFileName, BOOL fFailIfExists) 

Функция CopyFile копирует существующий файл с заданным именем и присваивает копии указанное новое имя. В случае существования файла с таким же именем он будет заменен новым файлом только в том случае, если значением параметра fFailIfExists является FALSE.

Под управлением NT5 можно создать жесткую ссылку (hard link) для двух файлов, аналогичную жестким ссылкам в UNIX, используя для этого функцию CreateHardLink. Жесткие ссылки делают возможным существование файла под двумя различными именами. Заметьте, что в подобных случаях файл как таковой существует в единственном числе, и поэтому можно произвольно использовать любое из его имен, независимо от того, какое из них было использовано для открытия файла.

BOOL CreateHardLink(LPCTSTR lpFileName, LPCTSTR lpExistingFileName, BOOL lpSecurityAttributes) 

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

Если заглянуть в документацию Microsoft, то можно увидеть, что в структуре BY_HANDLE_FILE_INFO имеется поле "количество ссылок", и именно этот счетчик используется для определения того, может или не может быть удален данный файл. Функция DeleteFile удаляет имя из каталога файловой системы, но сам файл не может быть удален до тех пор, пока значение счетчика "количество ссылок" не станет равным 0.

Гибких ссылок (soft link) в Windows не существует, хотя оболочки Windows (но не сама Windows), руководствующиеся при определении местоположения файла его содержимым, поддерживают ярлыки (shortcuts). Ярлыки предоставляют средства, подобные гибким ссылкам, но воспользоваться ими могут только пользователи оболочки.

Доступны две функции, позволяющие переименовывать, или "перемещать", файл. Эти же функции применимы и к каталогам. (Функции DeleteFile и CopyFile могут применяться только к файлам.) 

BOOL MoveFile(LPCTSTR lpExistingFileName, LPCTSTR lpNewFileName)

BOOL MoveFileEx(LPCTSTR lpExistingFileName, LPCTSTR lpNewFileName, DWORD dwFlags) 

Если новый файл уже существует, функция MoveFile завершается с ошибкой; в этом случае следует использовать функцию MoveFileEx. Заметьте, что суффикс "Ех" обычно применяется для обозначения усовершенствованных версий функций, обладающих расширенными (extended) функциональными возможностями.

Параметры

lpExistingFileName — указатель на строку, содержащую имя существующего файла или каталога.

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

dwFlags — позволяет задавать следующие опции:

• MOVEFILE_REPLACE_EXISTING — разрешает замену существующего файла.

• MOVEFILE_WRITETHROUGH — используйте этот флаг, если необходимо, чтобы функция выполнила возврат лишь после того, как файл будет фактически перемещен на диске.

• MOVEFILE_COPY_ALLOWED — если новый файл находится на другом томе, перемещение осуществляется путем последовательного выполнения функций CopyFile и DeleteFile.

• MOVEFILE_DELAY_UNTIL_REBOOT — установка этого флага, использование которого является прерогативой администратора системы и который не может применяться совместно с флагом MOVEFILE_COPY_ALLOWED, приводит к тому, что фактическое перемещение файла будет осуществлено только после перезагрузки системы.

С перемещением (переименованием) файлов связаны некоторые ограничения.

• В Windows 9x функция MoveFileEx не реализована; вместо нее вы должны использовать последовательные вызовы функций CopyFile и DeleteFile. Это делает возможным одновременное существование двух экземпляров файла, что порождает определенные проблемы в случае дисков, близких к заполнению, или файлов большого размера. При этом временные атрибуты файлов изменяются иначе, нежели при истинном перемещении.

• Использование групповых символов в именах файлов или каталогов запрещено. Необходимо указывать фактические имена. 

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

В UNIX отсутствует функция непосредственного копирования файлов. Вместо этого, чтобы выполнить команду cp, вы должны написать небольшую программу или использовать системный вызов system ().

В UNIX эквивалентом функции DeleteFile служит функция unlink, которая, к тому же, может удалять и каталоги.

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

Управление каталогами

Создание и удаление каталогов осуществляется при помощи двух простых функций. 

BOOL CreateDirectory(LPCTSTR lpPathName, LPSECURITY_ATTRIBUTES lpSecurityAttributes)

BOOL RemoveDirectory(LPCTSTR lpPathName)

lpPathName является указателем на завершающуюся нулевым символом строку, которая содержит путь к создаваемому или удаляемому каталогу. Как и в случае других функций, на данном этапе атрибуты защиты файла должны полагаться равными NULL; вопросы безопасности файлов и объектов рассматриваются в главе 15. Удалить можно только пустой каталог.

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

BOOL SetCurrentDirectory(LPCTSTR lpPathName) 

lpPathName определяет путь к новому текущему каталогу. Это может быть относительный путь или абсолютный полный путь, в начале которого указаны либо буква диска и двоеточие (например, D:), либо имя UNC (например, \\ACCTG_SERVER\PUBLIC).

Если в качестве пути к каталогу указывается только имя диска (например, А: или С:), то рабочим каталогом становится рабочий каталог данного диска. Например, если рабочие каталоги устанавливались в последовательности:

C:\MSDEV

INCLUDE

A:\MEMOS\TODO

С:

то результирующим рабочим каталогом будет:

C:\MSDEV\INCLUDE

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

DWORD GetCurrentDirectory(DWORD cchCurDir, LPTSTR lpCurDir)

Возвращаемое значение: длина строки, содержащей путь доступа к текущему каталогу, или требуемый размер буфера, если буфер не достаточно велик; в случае ошибки — нуль.

cchCurDir — размер буфера, содержащего имя каталога, который определяется количеством символов (а не байт). Размер буфера должен рассчитываться с учетом завершающего нулевого символа строки. lpCurDir является указателем на буфер, предназначенный для получения строки, содержащей путь.

Заметьте, что в случае если размер буфера оказался недостаточным для того, чтобы в нем уместилась вся строка пути, функция возвратит значение, указывающее на требуемый размер буфера. Поэтому при тестировании успешности выполнения функции следует проверять два условия: равно ли возвращаемое значение нулю и не превышает ли оно значение, заданное аргументом cchCurDir.

Подобный метод возврата строк и их длины широко распространен в Windows и требует внимательной обработки результатов. Программа 2.6 иллюстрирует типичный фрагмент кода, реализующего эту логику. Аналогичная логика реализуется и в других примерах. Вместе с тем, указанный метод применяется не всегда. Некоторые функции возвращают булевские значения, а параметр размера в них используется дважды: перед вызовом функции его значение устанавливается равным размеру буфера, а затем изменяется функцией. В качестве одного из многих возможных примеров можно привести функцию LookupAccountName, с которой вы встретитесь в главе 15. 

Альтернативный подход, демонстрируемый в программе 15.4 функцией GetFileSecurity, заключается в выделении буферной памяти в промежутке между двумя вызовами функций. Первый вызов обеспечивает получение длины строки, на основании чего и выделяется память, тогда как второй — получение самой строки. Самым простым подходом в данном случае является выделение памяти для строки, насчитывающей МАХ_РАТН символов.

Пример: печать текущего каталога

Программа 2.6 реализует очередную версию команды UNIX pwd. Размер буфера определяется значением параметра МАХ_РАТН, однако проверка ошибок все равно предусмотрена, чтобы проиллюстрировать работу функции GetCurrent-Directory.

Программа 2.6. pwd: печать текущего каталога 

/* Глава 2. pwd – вывод на печать содержимого рабочего каталога. */

#include "EvryThng.h"

#define DIRNAME_LEN MAX_PATH + 2


int _tmain(int argc, LPTSTR argv[]) {

 TCHAR pwdBuffer [DIRNAME_LEN];

 DWORD LenCurDir;

 LenCurDir = GetCurrentDirectory(DIRNAME_LEN, pwdBuffer);

 if (LenCurDir == 0) ReportError(_T("He удается получить путь."), 1, TRUE);

 if (LenCurDir > DIRNAME_LEN) ReportError(_T("Слишком длинный путь."), 2, FALSE);

 PrintMsg(GetStdHandle(STD_OUTPUT_HANDLE), pwdBuffer);

 return 0;

} 

Резюме

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

Функции Windows во многом напоминают их аналоги в UNIX и библиотеке С, хотя различия между ними также очевидны. В приложении Б представлена таблица, в которой сведены функции Windows, UNIX и библиотеки С и показано, в чем они соответствуют друг другу, а в чем заметно отличаются. 

В следующих главах

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

Дополнительная литература

Организация хранения данных в Windows и NTFS

В книге [22] содержится исчерпывающее обсуждение всего спектра возможных вариантов организации хранения данных в Windows как на непосредственно подключенных, так и на сетевых устройствах. Наряду с внутренними деталями реализации описаны все последние достижения и успехи в данной области, а также прогресс в отношении повышения быстродействия устройств хранения данных.

Книга [10] — это небольшая монография, в которой описаны цели и особенности реализации NTFS. Содержащаяся в ней информация пригодится вам как для этой, так и для следующей глав.

Unicode

В книге [19] показано, как использовать Unicode на практике. Изложение сопровождается различными рекомендациями, а также рассмотрением международных стандартов и вопросов программирования, связанных с учетом региональных особенностей.

На домашней странице компании Microsoft вы найдете несколько полезных статей о стандарте Unicode. Основной является статья "Unicode Support in Win32" ("Поддержка Unicode в Win32"), отталкиваясь от которой вы, используя средства поиска, сможете отыскать все остальные.

UNIX

В главах 3 и 4 книги [40] рассматриваются файлы и каталоги UNIX, а в главе 11 — терминальный ввод/вывод.

Для быстрого ознакомления с командами UNIX можете обратиться к книге [15].

Упражнения

2.1. Напишите небольшую программу для тестирования обобщенных версий функций printf и scanf.

2.2. Модифицируйте функцию CatFile в программе 2.3 таким образом, чтобы при связывании дескриптора стандартного вывода с консолью в ней использовалась не функция WriteFile, а функция WriteConsole.

2.3. Параметры вызова функции CreateFile позволяют задавать различные характеристики способа доступа к файлу, что может быть использовано для повышения производительности программ. В качестве примера можно привести параметр FILE_FLAG_SEQUENTIAL_SCAN. Используйте этот флаг в программе 2.5 и выясните, приведет ли это к улучшению показателей производительности при работе с файлами большого размера. Результаты для нескольких систем приведены в приложении В. Исследуйте также влияние флага FILE_FLAG_NO_BUFFERING.

2.4. Исследуйте, насколько ощутимы различия в производительности для файловых систем FAT и NTFS при использовании функции atou в случае преобразования файлов большого размера.

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

2.6. Сопоставьте информацию, предоставляемую функциями perror (библиотека С) и ReportError в случае таких распространенных ошибок, как попытка открытия несуществующего файла.

2.7. Протестируйте подавление функцией ConsolePrompt (программа 2.1) эхо-отображения клавиатурного ввода, используя ее для вывода запроса на ввод и подтверждение пароля пользователем.

2.8. Выясните, что происходит, когда для вывода на консоль используются смешанные вызовы функций обобщенной библиотеки С и функций Windows WriteFile и WriteConsole. Дайте происходящему свое объяснение.

2.9. Напишите программу сортировки массива строк Unicode. Изучите различия между случаями сортировки слов и строк с помощью функций lstrcmp и _tcscmp. Приводит ли использование функции lstrlen к получению иных результатов по сравнению с функцией _tcslen? Вам могут пригодиться содержащиеся в оперативной справочной системе Microsoft замечания в описании функции CompareString. 

2.10. Расширьте реализацию функции Options таким образом, чтобы она выводила сообщение об ошибке, если в командной строке указаны опции, которые отсутствуют в списке разрешенных опций, заданных в параметре OptionString данной функции.

2.11. В приложении В приводятся данные о показателях производительности для копирования файлов и их преобразования при помощи функции atou с использованием различных вариантов реализации программы и файловых систем. Исследуйте с помощью тестовых программ показатели производительности на доступных вам системах. Кроме того, если возможно, исследуйте показатели производительности для сетевых файловых систем, SAN и так далее, чтобы выяснить, каким образом проявляются различия в организации хранения данных при осуществлении последовательного доступа к файлам.

ГЛАВА 3

Усовершенствованные средства для работы с файлами и каталогами и знакомство с реестром

Файловые системы обеспечивают не только простую последовательную обработку файлов; кроме этого, они должны предоставлять возможности прямого доступа к файлам и блокирования файлов, а также предлагать средства для управления каталогами и атрибутами файлов. В данной главе, которая начинается с обсуждения прямого доступа к файлам, требуемого для обслуживания баз данных, обработки файлов и решения целого ряда других задач, демонстрируются методы непосредственного доступа к данным, находящимся в произвольном месте файла, которые обеспечиваются файловыми указателями. Для этого, в частности, нам надо будет обсудить использование 64-битовых указателей Windows, поскольку файловая система NTFS способна поддерживать файлы гигантских размеров.

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

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

64-битовая файловая система

Win32 и Win64, работающие с NTFS, поддерживают 64-битовую адресацию в файлах, и поэтому допустимыми являются файлы размером до 264 байт.

В 32-разрядных файловых системах, характеризующихся наличием 232 –байтового предела, допустимый размер файлов ограничивается величиной 4 Гбайт (4х109 байт). Для некоторых приложений, включая крупные базы данных и мультимедийные системы, это ограничение носит серьезный характер, что вынуждает современные ОС обеспечивать поддержку файлов гораздо больших размеров. Файлы, размеры которых превышают 4 Гбайт, иногда называют гигантскими (huge).

Вполне очевидно, что многим приложения гигантские файлы никогда не понадобятся, так что большинству программистов на протяжении ближайших нескольких лет возможностей 32-битовой файловой адресации будет вполне достаточно. Однако, с учетом темпов технической модернизации и увеличения емкости дисков[14], улучшения их стоимостных показателей и повышения уровня требований со стороны приложений, целесообразно уже с самого начала работы над новым проектом предусмотреть возможность использования 64-битовых адресов.

Несмотря на возможность использования 64-битовой адресации файлов и поддержку гигантских файлов, интерфейс Win32, в силу его привязки к 32-битовой адресации памяти, о чем говорится в главе 5, остается API 32-битовой ОС, так что для работы с 64-битовыми адресами памяти нам потребуется интерфейс Win64.

Указатели файлов

В Windows аналогично тому, как это предусмотрено в UNIX, библиотеке С и почти любой другой ОС, для каждого дескриптора открытого файла поддерживается указатель файла (file pointer), отмечающий позицию текущего байта в данном файле. Именно эта позиция служит отправной точкой для последующей передачи данных при выполнении очередной операции WriteFile или ReadFile, что сопровождается увеличением значения указателя файла на соответствующее количество переданных байт. При открытии файла путем вызова функции CreateFile указатель файла принимает нулевое значение, отмечающее начало файла, которое изменяется по мере чтения или записи каждого очередного байта. Ключевую роль в обеспечении возможности прямого доступа к данным, хранящимся в файле, играет функция SetFilePointer, позволяющая устанавливать значения указателя файла.

Функция SetFilePointer является первой из функций, на примере которых мы познакомимся с обработкой 64-битовых указателей файлов NTFS. Методы, основанные на этой функции, не всегда удобны в применении, и поэтому функцию SetFilePointer проще всего использовать в случае небольших файлов.

DWORD SetFilePointer(HANDLE hFile, LONG lDistanceToMove, PLONG lpDistanceToMoveHigh, DWORD dwMoveMethod)

Возвращаемое значение: младшее двойное слово (DWORD, беззнаковое) нового значения указателя файла. Старшая часть значения этого указателя помещается в двойное слово, на которое указывает указатель lpDistanceToMoveHigh (если он отличен от NULL). В случае неудачного завершения функция возвращает значение 0xFFFFFFFF.

Параметры

hFile — дескриптор файла, который должен быть создан с правами доступа по чтению или по записи (или с правами доступа одновременно обоих типов).

IDistanceToMove — 32-битовое число типа LONG со знаком, указывающее величину смещения, на которое должен быть перемещен указатель файла, или число типа LONG без знака, указывающее позицию, в которую должен быть перемещен указатель файла, в зависимости от значения параметра dwMoveMethod.

lpDistanceToMoveHigh — указатель на старшую часть 64-битового смещения, на которое должен быть перемещен указатель файла. Если значение этого параметра задано равным NULL, то функция может применяться только к файлам, размер которых не превышает 2³²–2 (в байтах). Этот же параметр используется для получения старшей части возвращаемого функцией значения указателя файла.[15] Младшую часть указателя файла возвращает сама функция.

dwMoveMethod — этот параметр устанавливает один из трех возможных режимов перемещения указателя файла.

• FILE_BEGIN — указатель файла позиционируется относительно начала файла, причем параметр DistanceToMove интерпретируется как беззнаковое число.

• FILE_CURRENT — указатель файла перемещается в сторону больших или меньших значений относительно текущей позиции, причем параметр DistanceToMove интерпретируется как число со знаком. Положительным значениям соответствует перемещение указателя файла в сторону больших значений. 

• FILE_END — указатель файла перемещается в сторону больших или меньших значений относительно позиции конца файла.

Эту функцию можно использовать для получения размера файла, задав нулевое смещение указателя от позиции конца файла.

Описанный метод представления 64-битовых указателей файлов становится причиной некоторых затруднений, поскольку возвращенное функцией значение может представлять как действительную позицию указателя файла, так и код ошибки. Рассмотрите, например, случай, когда фактической позиции указателя соответствует значение 2³²–1 (то есть, 0xFFFFFFFF), а при вызове функции указывается ненулевое значение старшей части перемещения указателя файла. Чтобы определить, представляет ли значение, возвращенное функцией SetFile-Pointer, действительную позицию указателя файла или же код ошибки, следует вызвать функцию GetLastError, возвращаемым значением которой в случае неудачного завершения не может быть NO_ERROR. Из этих рассуждений становится ясно, почему размеры файлов не могут превышать значения 2³²–2, если при вызове функции SetFilePointer старшая часть указателя файла опускается.

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

К счастью, 32-битовой адресации вам будет вполне достаточно для большинства задач программирования. Тем не менее, приведенные в книге примеры программ рассчитаны на далекую перспективу и используют, "как и положено", 64-битовую арифметику.

64-битовая арифметика

Арифметика 64-битовых указателей файлов не так уж сложна, и для ее реализации в примерах программ используется принятый в Microsoft С 64-битовый тип данных LARGE_INTEGER, объединяющий в одном типе данных union величину типа LONGLONG (носящую название QuadPart) и две 32-битовые величины (LowPart типа DWORD и, HighPart типа LONG). Тип данных LONGLONG поддерживает все арифметические операции. Существует также соответствующий тип данных без знака ULONGLONG. 

Аналогами функции SetFilePointer являются функции lseek (UNIX) и fseek (библиотека С). В обеих упомянутых системах выполнение операций чтения или записи также сопровождается перемещением указателя файла. 

Указание позиции файла с помощью структуры OVERLAPPED

Для указания позиции в файле Windows предоставляет еще один способ, не требующий использования функции SetFilePointer. Вспомните, что последним параметром в обеих функциях ReadFile и WriteFile является адрес структуры перекрытия OVERLAPPED, который в предыдущих примерах всегда полагался равным NULL. В структуру перекрытия входят элементы Offset и OffsetHigh. Устанавливая соответствующие значения элементов структуры OVERLAPPED, вы можете добиться того, чтобы выполнение операций ввода/вывода начиналось с указанной позиции. В отличие от указателя файла, значение которого изменяется, соответствуя позиции, следующей за последним переданным байтом, значения элементов структуры OVERLAPPED остаются неизменными. Элементом этой структуры является также дескриптор hEvent, значение которого должно устанавливаться равным NULL.

Примечание

Под управлением Windows 9x описанный метод работать не будет, поскольку в этом случае указатель структуры OVERLAPPED при обработке файлов должен устанавливаться равным NULL.

Предостережение

Хотя в рассмотренном примере и используется структура OVERLAPPED, здесь не идет речь о перекрывающемся вводе/выводе, который обсуждается в главе 14.

Использование структуры OVERLAPPED оказывается особенно удобным в тех случаях, когда требуется обновить запись в файле, что иллюстрирует приведенный ниже фрагмент программного кода; в противном случае вы должны были бы перед каждым вызовом функций ReadFile и WriteFile отдельно вызывать функцию SetFilePointer. Последним из пяти полей структуры OVERLAPPED является поле hEvent, как это видно из оператора инициализации. Для хранения вычисленного значения позиции в файле используется переменная FilePos типа LARGE_INTEGER.

OVERLAPPED ov = { 0, 0, 0, 0, NULL };

RECORD r; /* Хотя определение этой структуры не приведено, в ней имеется поле RefCount. */

LONGLONG n;

LARGE_INTEGER FilePos;

DWORD nRead, nWrite;

/* Обновить счетчик, чтобы он соответствовал n-й записи. */

FilePos.QuadPart = n * sizeof(RECORD);

ov.Offset = FilePos.LowPart;

ov.OffsetHigh = FilePos.HighPart;

ReadFile(hFile, r, sizeof(RECORD), &nRead, &ov);

r.RefCount++; /* Обновить запись. */

WriteFile(hFile, r, sizeof(RECORD), &nWrite, &ov); 

Если дескриптор файла был создан за счет вызова функции CreateFile с установленным флагом FILE_FLAG_NO_BUFFERING, то как смещение позиции в файле, так и размер записи (количество байт) должны быть кратными размеру сектора диска. Соответствующую информацию относительно физического диска, включая информацию о размере сектора, возвращает функция GetDiskFreeSpace.

Структуры OVERLAPPED будут вновь использованы далее в этой главе для указания областей блокирования файлов и в главе 14 для выполнения операций асинхронного ввода/вывода и прямого доступа к файлам.

Определение размера файла

Размер файла можно получить, используя значение указателя файла, возвращаемое функцией SetFilePointer, если при вызове этой функции задать количество байтов, на которое должен быть перемещен указатель файла, равным 0. Для этой же цели можно воспользоваться также функцией GetFileSize. 

DWORD GetFileSize(HANDLE hFile, LPDWORD lpFileSizeHigh)

Возвращаемое значение: младшая компонента размера файла. Значение 0xFFFFFFFF указывает на возможную ошибку; для проверки наличия ошибок следует использовать функцию GetLastError.

Обратите внимание, что для возвращения размера файла используется, по сути, тот же способ, что и для возвращения фактического указателя файла функцией SetFilePointer.

Функции GetFileSize и GetFileSizeEx (возвращающая 64-битовое значение размера файла в одном элементе данных) требуют указания дескриптора, открытого для файла. Для определения размера файла можно применять также имя файла. Функция GetCompressedFileSize возвращает размер сжатого файла, тогда как функция FindFirstFile, которая обсуждается в разделе "Атрибуты файлов и управление каталогами" далее в этой главе, предоставляет точный размер именованного файла.

Установка размера файла, инициализация файла и разреженные файлы

Функция SetEndOfFile позволяет переустановить размер файла, используя текущее значение указателя файла для определения его размера. Возможно как расширение, так и усечение файла. В случае расширения файла содержимое области расширения не определено. Файл будет фактически потреблять выделенные квоты дискового и пользовательского пространств, если только не является разреженным. Файлы можно сжимать с целью уменьшения объема занимаемого ими пространства. Этот вопрос исследуется в упражнении 3.1.

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

В случае разреженных файлов (sparse files), появившихся в Windows 2000, дисковое пространство расходуется лишь по мере записи данных. Администратор может назначать, какие файлы, каталоги или тома должны быть разреженными. Кроме того, можно назначить существующий файл в качестве разреженного с помощью функции DeviceIoControl, если установить при ее вызове флаг FSCTL_SET_SPARSE. Ситуацию, в которой удобно использовать разреженные файлы, иллюстрирует программа 3.1. К разреженным файлам функция SetValidFileData неприменима.

Файлы FAT нулями автоматически не инициализируются. Согласно документации Microsoft содержимое вновь созданных файлов не определено, что подтверждается экспериментами. Поэтому, если для корректной работы требуется инициализация файлов, приложения должны это делать самостоятельно путем вызова функции WriteFile. Файлы NTFS будут инициализированы, поскольку уровень безопасности С2, обеспечиваемый Windows, требует, чтобы чтение содержимого удаленных файлов было невозможным.

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

Пример: обновление записей, находящихся в произвольном месте файла

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

Программа демонстрирует позиционирование указателя файла перед заданной записью, а также выполнение 64-битовых арифметических операций с использованием данных типа LARGE_INTEGER Microsoft С. Чтобы проиллюстрировать логику работы указателей файла, в программу включен код, проверяющий наличие ошибок. Программа в целом иллюстрирует применение файловых указателей и множественных структур OVERLAPPED, а также обновление файлов с использованием 64-битовых файловых указателей.

Общее количество записей в файле указывается в командной строке; при большом количестве записей размеры создаваемых файлов могут быть очень большими и даже гигантскими, поскольку длина одной записи составляет примерно 300 байт. После выполнения нескольких экспериментов вы убедитесь, что большие файлы должны быть разреженным; в противном случае необходимо размещать и инициализировать на диске весь файл целиком, в результате чего может существенно увеличиться время обработки файла и занимаемое им место на диске. Хотя в листинге программы 3.1 это и не отражено, в программе предусмотрен участок кода, обеспечивающий создание разреженных файлов, если в этом возникает необходимость; в некоторых системах, например Windows XP Home, этот код правильно работать не сможет.

На Web-сайте книги предоставляются три дополнительные программы, родственные этой: tail.с — другой пример реализации произвольного доступа к файлу, getn.c — упрощенная версия программы RecordAccess, обеспечивающая лишь чтение записей, и atouMT (включена в программы для главы 14, находящиеся на Web-сайте, однако не включена в программы, приведенные в книге), также иллюстрирующая прямой доступ к файлам.

Программа 3.1. RecordAccess 

/* Глава 3. RecordAccess. */

/* Использование: RecordAccess имя файла [количество записей]

Количество записей (nrec) можно не указывать, если файл с указанным именем уже существует. Если количество записей (nrec) задано, создается файл с указанным именем (если файл с таким именем существует, он уничтожается). При большом количестве записей (nrec) файлы рекомендуется создавать как разреженные. */

/* Программа иллюстрирует:

1. Произвольный доступ к файлам.

2. Арифметику данных типа LARGE_INTEGER и использование 64-битовых указателей файла.

3. Обновление записей на месте.

4. Запись в файл нулей во время инициализации (требует использования файловой системы NTFS).

*/


#include "EvryThng.h"

#define STRING_SIZE 256

typedef struct _RECORD { /* Структура записи в файле */

 DWORD ReferenceCount; /* 0 означает пустую запись. */

 SYSTEMTIME RecordCreationTime;

 SYSTEMTIME RecordLastReferenceTime;

 SYSTEMTIME RecordUpdateTime;

 TCHAR DataString[STRING_SIZE];

} RECORD;

typedef struct _HEADER { /* Дескриптор заголовка файла */

 DWORD NumRecords;

 DWORD NumNonEmptyRecords;

} HEADER;


int _tmain(int argc, LPTSTR argv[]) {

 HANDLE hFile;

 LARGE_INTEGER CurPtr;

 DWORD FPos, OpenOption, nXfer, RecNo;

 RECORD Record;

 TCHAR String[STRING_SIZE], Command, Extra;

 OVERLAPPED ov = {0, 0, 0, 0, NULL}, ovZero = {0, 0, 0, 0, NULL};

 HEADER Header = {0, 0};

 SYSTEMTIME CurrentTime;

 BOOLEAN HeaderChange, RecordChange;

 OpenOption = (argc == 2) ? OPEN_EXISTING : CREATE_ALWAYS;

 hFile = CreateFile(argv[1], GENERIC_READ | GENERIC_WRITE, 0, NULL, OpenOption, FILE_ATTRIBUTE_NORMAL, NULL);

 if (argc >= 3) { /* Записать заголовок и заранее установить размер нового файла */

  Header.NumRecords = atoi(argv[2]);

  WriteFile(hFile, &Header, sizeof(Header), &nXfer, &ovZero);

  CurPtr.QuadPart = sizeof(RECORD)*atoi(argv[2])+sizeof(HEADER);

  FPos = SetFilePointer(hFile, CurPtr.LowPart, &CurPtr.HighPart, FILE_BEGIN);

  if (FPos == 0xFFFFFFFF && GetLastError() != NO_ERROR) ReportError(_T("Ошибка указателя."), 4, TRUE);

  SetEndOfFile(hFile);

 }

 /* Считать заголовок файла: определить количество записей и количество непустых записей. */

 ReadFile(hFile, &Header, sizeof(HEADER), &nXfer, &ovZero);

 /* Предложить пользователю считать или записать запись с определенным номером. */

 while(TRUE) {

  HeaderChange = FALSE;

  RecordChange = FALSE;

  _tprintf(_Т("Введите r(ead)/w(rite)/d(elete)/q Запись#\n"));

  _tscanf(_T("%c" "%d" "%c"), &Command, &RecNo, &Extra );

  if (Command == 'q') break;

  CurPtr.QuadPart = RecNo * sizeof(RECORD) + sizeof(HEADER);

  ov.Offset = CurPtr.LowPart;

  ov.OffsetHigh = CurPtr.HighPart;

  ReadFile(hFile, &Record, sizeof(RECORD), &nXfer, &ov);

  GetSystemTime(&CurrentTime); /* Обновить поля даты и времени в записи. */

  Record.RecordLastRefernceTime = CurrentTime;

  if (Command == 'r' || Command == 'd') { /*Вывести содержимое записи.*/

   if (Record.ReferenceCount == 0) {

    _tprintf(_T("Запись номер %d – пустая.\n"), RecNo);

    continue;

   } else {

    _tprintf(_Т("Запись номер %d. Значение счетчика: %d \n"), RecNo, Record.ReferenceCount);

    _tprintf(_Т("Данные: %s\n"), Record.DataString);

    /* Упражнение: вывести метки времени. См. следующий пример. */

    RecordChange = TRUE;

   }

   if (Command == 'd') { /* Удалить запись. */

    Record.ReferenceCount = 0;

    Header.NumNonEmptyRecords--;

    HeaderChange = TRUE;

    RecordChange = TRUE;

   }

  } else if (Command == 'w') { /* Записать данные. Впервые? */

   _tprintf(_Т("Введите новую строку для записи.\n"));

   _getts(String);

   if (Record.ReferenceCount == 0) {

    Record.RecordCreationTime = CurrentTime;

    Header.NumNonEmptyRecords++;

    HeaderChange = TRUE;

   }

   Record.RecordUpdateTime = CurrentTime;

   Record.ReferenceCount++;

   _tcsncpy(Record.DataString, String, STRING_SIZE-1);

   RecordChange = TRUE;

  } else {

   _tprintf(_T("Допустимые команды: r, w и d. Повторите ввод.\n"));

  }

  /* Обновить запись на месте, если ее содержимое изменилось. */

  if (RecordChange) WriteFile(hFile, &Record, sizeof(RECORD), &nXfer, &ov);

  /* При необходимости обновить количество непустых записей. */

  if (HeaderChange) WriteFile(hFile, &Header, sizeof(Header), &nXfer, &ovZero);

 }

 _tprintf(_T("Вычисленное количество непустых записей: %d\n"), Header.NumNonEmptyRecords);

 CloseHandle(hFile);

 return 0;

} 

Атрибуты файлов и управление каталогами

Существует возможность просмотра указанного каталога с целью поиска файлов и других каталогов, имена которых соответствуют заданному шаблону, одновременно с получением атрибутов файлов. Для выполнения поиска требуется дескриптор поиска (search handle), получаемый с помощью функции FindFirstFile. Для нахождения файлов, имена которых удовлетворяют заданным условиям, используется функция FindNextFile, а для прекращения поиска — функция FindClose. 

HANDLE FindFirstFile(LPCTSTR lpFileName, LPWIN32_FIND_DATA lpffd)

Возвращаемое значение: дескриптор поиска. Значение INVALID_HANDLE_VALUE указывает на неудачное завершение функции.

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

Параметры

lpFileName — указатель на строку, содержащую имя каталога или полное имя файла, при указании которых можно использовать метасимволы (? и *). Если необходимо осуществить поиск конкретного файла, метасимволы опускаются.

lpffd — указатель на структуру WIN32_FIND_DATA, которая принимает информацию о первом найденном файле или каталоге, который удовлетворяет критерию поиска, если таковой был найден.

Структура WIN32_FIND_DATA определяется следующим образом: 

typedef struct_WIN32_FIND_DATA {

 DWORD dwFileAttributes;

 FILETIME ftCreationTime;

 FILETIME ftLastAccessTime;

 FILETIME ftLastWriteTime;

 DWORD nFileSizeHigh;

 DWORD nFileSizeLow;

 DWORD dwReserved0;

 DWORD dwReserved1;

 TCHAR cFileName[MAX_PATH];

 TCHAR cAlternateFileName[14];

 } WIN32_FIND_DATA; 

Параметр dwFileAttributes можно тестировать на присутствие значений, описанных при рассмотрении функции CreateFile, а также некоторых других значений, например, FILE_ATTRIBUTE_SPARSE_FILE или FILE_ATTRIBUTE_ENCRYTPED, которые не устанавливаются функцией CreateFile. Описание меток времени трех типов (время создания, время последнего обращения и время последнего изменения) приведено в одном из следующих разделов. Названия полей размера файла (nFileSizeHigh и nFileSizeLow) говорят сами за себя. cFileName — это не полное имя файла, содержащее путь доступа, а само имя файла. cAlternateFileName — имя файла в формате DOS 8.3 (включая точку); эта информация редко используется и может понадобиться лишь для того, чтобы определить, каким будет имя файла в файловой системе FAT16.

Во многих случаях требуется просматривать каталог с целью поиска файлов, имена которых соответствуют некоторому шаблону, содержащему метасимволы ? и *. Для этого следует использовать дескриптор поиска, полученный из функции FindFirstFile, в котором содержится информация об искомом имени, и вызвать функцию FindNextFile. 

BOOL FindNextFile(HANDLE hFindFile, LPWIN32_FIND_DATA lpffd) 

Функция FindNextFile возвращает значение FALSE, если аргументы недействительны или если не удается найти файл, удовлетворяющий критерию поиска, причем последнему случаю соответствует возвращаемое значение функции GetLastError, равное ERROR_NO_MORE_FILES.

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

BOOL FindClose(HANDLE hFindFile) 

Функция GetFileInformationByHandle позволяет получить информацию о конкретном файле, на который указывает открытый дескриптор файла. Она также возвращает поле nNumberOfLinks, в котором содержится количество жестких ссылок на файл, установленных функцией CreateHardLink.

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

Полные имена файлов

Полное имя файла можно получить, используя функцию GetFullPathName. Функция GetShortPathName возвращает имя файла в формате DOS 8.3, в предположении, что данный том поддерживает короткие имена файлов.

В NT 5.1 была введена функция SetFileShortName, позволяющая изменить существующее сокращенное имя файла или каталога. Иногда это оказывается удобным, поскольку интерпретация сокращенных имен файлов часто вызывает затруднения.

Другие методы определения атрибутов файлов и каталогов

Функции FindFirstFile и FindNextFile позволяют получить следующую информацию, связанную с атрибутами файла: флаги атрибутов, метки времени трех типов и размер файла. Существуют также другие аналогичные функции, одна из которых предназначена для задания атрибутов, причем эти функции могут работать непосредственно с открытыми дескрипторами файлов, не требуя просмотра каталогов или указания имен файлов. Три из этих функций, а именно, GetFileSize, GetFileSizeEx и SetEndOfFile, были описаны ранее в этой главе.

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

BOOL GetFileTime(HANDLE hFile, LPFILETIME lpftCreation, LPFILETIME lpftLastAccess, LPFILETIME lpftLastWrite) 

Указанные здесь и в структуре WIN32_FIND_DATA метки времени представляют собой 64-битовые целые числа без знака, которые выражают величину временного интервала, вычисленную относительно условного начала отсчета (1 января 1601 года) и преобразованную во время UTC (Universal Coordinated Time — всеобщее скоординированное время)[16], в 100-наносекундных единицах времени (107 единиц в 1 секунде). Для работы с этими временными параметрами предусмотрено несколько удобных функций.

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

• Функция SystemTimeToFileTime обращает этот процесс, преобразуя время, выраженное в естественных единицах, в метки времени файла.

• Функция CompareFileTime сравнивает метки времени двух файлов и в случае успешного завершения возвращает значение, зависящее от того, меньше (-1), равно (0) или больше (+1) значение метки времени первого файла по сравнению со значением метки времени второго файла.

• Для изменения меток времени служит функция SetFileTime; метки времени, не подлежащие изменению, при вызове функции указываются равными 0. NTFS поддерживает все три типа меток времени файлов, но FAT дает точные результаты только для меток времени последнего обращения.

• Функции FileTimeToLocalFileTime и LocalFileTimeToFileTime преобразуют значения меток времени, соответственно, от всеобщего скоординированного времени UTC к местному времени и наоборот.

Функция GetFileType, которая здесь подробно не описывается, позволяет различать файлы трех типов: дисковые, символьные (к ним, по сути, относятся такие устройства, как принтеры и консоли) и каналы (см. главу 11). Как и в большинстве других случаев, файл, характеристику которого необходимо определить, задается дескриптором.

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

DWORD GetFileAttributes(LPCTSTR lpFileName)

Возвращаемое значение: в случае успешного завершения — атрибуты файла, иначе — 0xFFFFFFFF.

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

• FILE_ATTRIBUTE_DIRECTORY

• FILE_ATTRIBUTE_NORMAL

• FILE_ATTRIBUTE_READONLY

• FILE_ATTRIBUTE_TEMPORARY

Для изменения атрибутов именованных файлов служит функция SetFileAttributes. 

В UNIX трем вышеописанным функциям Find соответствуют функции opendir, readdir и closedir. Функция stat предоставляет размер файла и значения меток времени, а также информацию о его индивидуальном или групповом владельце, необходимую для защиты файлов в UNIX. Разновидностями этой функции являются функции fstat и lstat. Эти функции позволяют также получать информацию о типе файла. Метки времени файла в UNIX устанавливаются с помощью функции utime. Эквивалента атрибута временного файла в UNIX не существует.

Именование временных файлов

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

Функция GetTempFileName предоставляет уникальное имя файла с расширением .tmp, используя указанный путь доступа, и при необходимости создает файл. Эта функция широко используется в ряде следующих примеров (программа 6.1, программа 7.1 и другие). 

UINT GetTempFileName(LPCTSTR lpPathName, LPCTSTR lpPrefixString, UINT uUnique, LPTSTR lpTempFileName)

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

Параметры

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

lpPrefixString — префикс, используемый в имени временного файла. Допускаются лишь 8-битовые символы ASCII. Значение параметра uUnique обычно устанавливается равным нулю, чтобы функция самостоятельно сгенерировала уникальный четырехразрядный префикс и использовала его в имени создаваемого файла. При ненулевом значении этого параметра файл не создается, так что это необходимо сделать отдельно при помощи функции CreateFile, возможно — с использованием флага FILE_FLAG_DELETE_ON_CLOSE. 

lpTempFileName — указатель на буфер, предназначенный для хранения имени временного файла. Размер буфера, выраженный в байтах, должен быть не менее МАХРАТН. Результирующее полное имя файла получается объединением строк, соответствующих пути доступа к файлу, префикса, четырехразрядного шестнадцатеричного числа и суффикса .tmp.

Точки монтирования

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

Функция SetVolumeMountPoint монтирует диск (второй аргумент) в точке монтирования, указанной первым аргументом. Например, вызов

SetVolumeMountPoint("С:\\mycd\\, "D:\\");

монтирует диск D: (которому в персональных системах часто соответствует привод компакт-диска) в каталоге mycd (точка монтирования), находящемуся на диске С:. Обратите внимание на то, что обозначения всех путей доступа заканчиваются символами обратной косой черты. Тогда после применения этой функции пути доступа C:\mycd\memos\book.doc будет соответствовать путь доступа D:\memos\book.doc.

Одну и ту же точку монтирования можно использовать для подключения нескольких файловых систем. Для размонтирования файловых систем служит функция DeleteMountPoint.

Функция GetVolumePathName возвращает корневую точку монтирования абсолютного или относительного пути доступа или имени файла. В свою очередь, функция GetVolumeNameForVolumeMountPoint предоставляет имя тома, например, C:\, соответствующего точке монтирования.

Пример: вывод списка атрибутов файла

Настало время увидеть функции управления файлами и каталогами в действии. Программа 3.2 представляет собой ограниченную версию команды UNIX ls, предназначенной для вывода содержимого каталогов, которая позволяет вывести дату и время последнего изменения файла и размер файла, хотя данная версия отображает лишь младшую часть размера файла.

Программа просматривает каталог для поиска файлов, соответствующих шаблону поиска. Для каждого найденного файла программа отображает имя файла и, если был задан параметр –1, то и его атрибуты. Данная программа иллюстрирует принцип построения многих, хотя и далеко не всех, функций Windows, предназначенных для работы с каталогами. 

Значительная часть кода программы 3.2 отвечает за обход дерева каталогов. Заметьте, что каждый каталог проходится дважды: при первом проходе обрабатываются файлы, а при втором — подкаталоги, чем обеспечивается поддержка параметра рекурсивного обхода каталогов (-R).

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

lsW –R include\*.h

Вместе с тем, в результате указания абсолютного полного имени файла, например:

lsW –R C:\Projects\ls\Debug\*.obj

правильная работа программы будет нарушена, поскольку в ней самым существенным образом используется привязка каталогов к текущему каталогу. Завершенное решение (доступное на Web-сайте) анализирует абсолютные полные пути доступа к файлам и поэтому обеспечивает правильное выполнение программы и для второй команды.

Программа 3.2. lsw: вывод списка файлов и обход дерева каталогов 

/* Глава 3. lsW — команда вывода списка файлов */

/* lsW [параметры] [файлы] */

#include "EvryThng.h"

BOOL TraverseDirectory(LPCTSTR, DWORD, LPBOOL);

DWORD FileType(LPWIN32_FIND_DATA);

BOOL ProcessItem(LPWIN32_FIND_DATA, DWORD, LPBOOL);


int _tmain(int argc, LPTSTR argv[]) {

 BOOL Flags [MAX_OPTIONS], ok = TRUE;

 TCHAR PathName [MAX_PATH +1], CurrPath [MAX_PATH + 1];

 LPTSTR pSlash, pFileName;

 int i, FileIndex;

 FileIndex = Options(argc, argv, _T("R1"), &Flags[0], &Flags[1], NULL);

 I* "Разобрать" шаблон поиска на "родительскую часть" и имя файла. */

 GetCurrentDirectory(MAX_PATH, CurrPath); /* Сохранить текущий путь доступа. */

 if (argc < FileIndex +1) /* Путь доступа не указан. Использовать текущий каталог. */

  ok = TraverseDirectory(_T("*"), MAX_OPTIONS, Flags);

 else for (i = FileIndex; i < argc; i++) {

  /* Обработать все пути, указанные в командной строке. */

  ok = TraverseDirectory(pFileName, MAX_OPTIONS, Flags) && ok;

  SetCurrentDirectory(CurrPath);

  /* Восстановить каталог. */

 }

 return ok ? 0 : 1;

}


static BOOL TraverseDirectory(LPCTSTR PathName, DWORD NumFlags, LPBOOL Flags)

/* Обход дерева каталогов; выполнить функцию ProcessItem для каждого случая совпадения. */

/* PathName: относительное или абсолютное имя просматриваемого каталога.*/

{

 HANDLE SearchHandle;

 WIN32_FIND_DATA FindData;

 BOOL Recursive = Flags[0];

 DWORD FType, iPass;

 TCHAR CurrPath[MAX_PATH + 1];

 GetCurrentDirectory(MAX_PATH, CurrPath);

 for (iPass = 1; iPass <= 2; iPass++) {

  /* Проход 1: вывод списка файлов. */

  /* Проход 2: обход дерева каталогов (если задана опция –R). */

  SearchHandle = FindFirstFile(PathName, &FindData);

  do {

   FType = FileType(&FindData);

   /* Файл или каталог? */

   if (iPass == 1) /* Вывести имя и атрибуты файла. */

    ProcessItem(&FindData, MAX_OPTIONS, Flags);

   if (FType == TYPE_DIR && iPass == 2 && Recursive) {

    /* Обработать подкаталог. */

    _tprintf(_T ("\n%s\\%s:"), CurrPath, FindData.cFileName);

    /* Подготовка к обходу каталога. */

    SetCurrentDirectory(FindData.cFileName);

    TraverseDirectory(_T("*"), NumFlags, Flags);

    /* Рекурсивный вызов. */

    SetCurrentDirectory(_T(".."));

   }

  } while (FindNextFile(SearchHandle, &FindData));

  FindClose (SearchHandle);

 }

 return TRUE;

}


static BOOL ProcessItem(LPWIN32_FIND_DATA pFileData, DWORD NumFlags, LPBOOL Flags)

/* Выводит список атрибутов файла или каталога. */

{

 const TCHAR FileTypeChar[] = {' ', 'd'};

 DWORD FType = FileType(pFileData);

 BOOL Long = Flags[1];

 SYSTEMTIME LastWrite;

 if (FType != TYPE_FILE && FType != TYPE_DIR) return FALSE;

 _tprintf(_T ("\n"));

 if (Long) { /* Указан ли в командной строке параметр "-1"? */

  _tprintf(_T("%c"), FileTypeChar[FType – 1]);

  _tprintf(_T("%10d"), pFileData->nFileSizeLow); 

  FileTimeToSystemTime(&(pFileData->ftLastWriteTime), &LastWrite);

  _tprintf(_T(" %02d/%02d/%04d %02d:%02d:%02d"), LastWrite.wMonth, LastWrite.wDay, LastWrite.wYear, LastWrite.wHour, LastWrite.wMinute, LastWrite.wSecond);

 }

 _tprintf(_T(" %s"), pFileData->cFileName);

 return TRUE;

}


static DWORD FileType(LPWIN32_FIND_DATA pFileData)

/* Поддерживаемые типы файлов – TYPE_FILE: файл; TYPE_DIR: каталог; TYPE_DOT: каталоги . или .. */

{

 BOOL IsDir;

 DWORD FType;

 FType = TYPE_FILE;

 IsDir = (pFileData->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;

 if (IsDir) if (lstrcmp(pFileData->cFileName, _T(".")) == 0 || lstrcmp(pFileData->cFileName, _T("..")) == 0) FType = TYPE_DOT;

 else FType = TYPE_DIR;

 return FType;

}

Пример: установка меток времени файла

Программа 3.3 реализует UNIX-команду touch, предназначенную для изменения кода защиты файлов и обновления меток времени до текущих значений системного времени. В упражнении 3.11 от вас требуется расширить возможности функции touch таким образом, чтобы новые значения меток времени можно было указывать в параметрах командной строки.

Программа 3.3. touch: установка меток даты и времени файла

/* Глава 3. команда touch. */

/* touch [параметры] [файлы] */

#include "EvryThng.h"


int _tmain(int argc, LPTSTR argv[]) {

 SYSTEMTIME SysTime;

 FILETIME NewFileTime;

 LPFILETIME pAccessTime = NULL, pModifyTime = NULL;

 HANDLE hFile;

 BOOL Flags[MAX_OPTIONS], SetAccessTime, SetModTime, CreateNew;

 DWORD CreateFlag;

 int i, FileIndex;

 FileIndex = Options(argc, argv, _T("amc"), &Flags[0], &Flags[1], &Flags[2], NULL);

 SetAccessTime = !Flags[0];

 SetModTime = !Flags[1];

 CreateNew = !Flags[2];

 CreateFlag = CreateNew ? OPEN_ALWAYS : OPEN_EXISTING;

 for (i = FileIndex; i < argc; i++) {

  hFile = CreateFile(argv[i], GENERIC_READ | GENERIC_WRITE, 0, NULL, CreateFlag, FILE_ATTRIBUTE_NORMAL, NULL);

  GetSystemTime(&SysTime);

  SystemTimeToFileTime(&SysTime, &NewFileTime);

  if (SetAccessTime) pAccessTime = &NewFileTime;

  if (SetModTime) pModifyTime = &NewFileTime;

  SetFileTime(hFile, NULL, pAccessTime, pModifyTime);

  CloseHandle(hFile);

 }

 return 0;

}

Стратегии обработки файлов

Уже на ранних стадиях любого проекта разработки приложения или подготовки его к переносу на другую платформу приходится принимать решение относительно того, должна ли осуществляться обработка файлов с использованием функций библиотеки С или функций Windows. Характер этого решения не относится к категории "или-или", поскольку при соблюдении определенных мер предосторожности смешанное применение функций возможно даже по отношению к одному и тому же файлу.

Библиотека С обладает рядом явных преимуществ, среди которых можно выделить следующие:

• Полученный программный код легко переносится на другие системы.

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

• Функции библиотеки С обычно проще в использовании по сравнению с функциями Windows.

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

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

Тем не менее, использование библиотеки С налагает некоторые ограничения. В пользу этого утверждения можно привести перечисленные ниже соображения: 

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

• В функции fseek, входящей в библиотеку С, используются 32-битовые указатели файла, и поэтому, несмотря на возможность последовательного считывания гигантских файлов, установка произвольной позиции в таком файле, как это требуется, например, в программе 3.1, оказывается невозможной.

• Библиотека С не предоставляет такие развитые возможности, как защита файлов, отображение файлов, блокирование файлов, асинхронный ввод/вывод и взаимодействие между процессами. Вместе с тем, как показано в приложении В, использование некоторых из этих возможностей в ряде случаев может обеспечивать существенное улучшение показателей производительности программ.

Альтернативным вариантом является перенос существующего UNIX-кода с привлечением библиотеки совместимости (compatibility library). Microsoft С предоставляет ограниченную библиотеку совместимости, включающую многие, хотя и далеко не все, функции UNIX. К числу функций UNIX, входящих в состав библиотеки Microsoft, относятся функции ввода/вывода, однако большинство функций управления процессами, не говоря о многих других функциях, в ней отсутствуют. В именах функций-аналогов присутствует префикс в виде символа подчеркивания, например, _read, _write, _stat и так далее.

Решения относительно смешанного использования функций библиотеки С, библиотеки совместимости и Win32/64 API должны приниматься на основании требований проекта. Многие из преимуществ функций Windows будут продемонстрированы в следующих главах, а для ознакомления с количественными данными, характеризующими производительность, которые пригодятся вам в тех случаях, когда этот фактор становится решающим, вы можете обратиться к приложению В.

Блокирование файлов

В системах, допускающих одновременное выполнение нескольких процессов, особую актуальность приобретает проблема координации и синхронизации доступа к разделяемым (совместно используемым) объектам, например файлам.

В Windows имеется возможность блокировать файлы (целиком или частично) таким образом, что никакой другой процесс (выполняющаяся программа) не сможет получить доступ к заблокированному участку файла. Блокирование файла может оставлять другим приложениям возможность доступа только для чтения (разделяемый доступ) или же закрывать им доступ к файлу как для записи, так и для чтения (монопольный доступ). Что немаловажно, владельцем блокировки является блокирующий процесс. Любая попытка получения доступа к части файла (с помощью функций ReadFile или WriteFile) в нарушение существующей блокировки закончится неудачей, поскольку блокировки носят обязательный характер на уровне процесса. Любая попытка получения несовместимой блокировки также завершится неудачей, даже если процесс уже владеет данной блокировкой. Блокирование файлов является ограниченной разновидностью синхронизации параллельно выполняющихся процессов и потоков; обсуждение синхронизации с использованием гораздо более общей терминологии начнется в главе 8.

Для блокирования файлов предусмотрены две функции. Более общей из них является функция LockFileEx, менее общей — LockFile, которую можно использовать и в Windows 9x.

Функция LockFileEx относится к классу функций расширенного (extended) ввода/вывода, поэтому для указания 64-битовой позиции в файле и границ области файла, подлежащей блокированию, необходимо использовать структуру OVERLAPPED, которая ранее уже применялась при указании позиции в файле для функций ReadFile и WriteFile. 

BOOL LockFileEx(HANDLE hFile, DWORD dwFlags, DWORD dwReserved, DWORD nNumberOfBytesToLockLow, DWORD nNumberOfBytesToLockHigh, LPOVERLAPPED lpOverlapped) 

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

Параметры

hFile — дескриптор открытого файла. Дескриптор должен быть создан либо с правами доступа GENERIC_READ, либо с правами доступа GENERIC_READ и GENERIC_WRITE.

dwFlags — определяет вид блокировки файла, а также режим ожидания доступности затребованной блокировки. Этот параметр определяется комбинацией следующих значений:

LOCKFILE_EXCLUSIVE_LOCK — запрос монопольной блокировки в режиме чтения/записи. Если это значение не задано, запрашивается разделяемая блокировка (только чтение).

LOCKFILE_FAIL_IMMEDIATELY — задает режим немедленного возврата функции с возвращаемым значением равным FALSE, если приобрести блокировку не удается. Если это значение не задано, функция переходит в режим ожидания. 

dwReserved — значение этого параметра должно устанавливаться равным 0. Следующие два параметра определяют соответственно младшие и старшие 32-битовые значения размера блокируемого участка файла (в байтах).

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

• DWORD Offset (используется именно такое имя параметра, а не OffsetLow).

• DWORD OffsetHigh.

• HANDLE hEvent должен задаваться равным 0.

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

BOOL UnlockFileEx(HANDLE hFile, DWORD dwReserved, DWORD nNumberOfBytesToLockLow, DWORD nNumberOfBytesToLockHigh, LPOVERLAPPED lpOverlapped) 

Используя блокирование файлов, вы должны принимать во внимание следующие обстоятельства:

• Границы области разблокирования должны в точности совпадать с границами ранее заблокированной области. Не допускается, например, объединение двух ранее заблокированных областей или разблокирование части заблокированной области. Любая попытка разблокирования области, не совпадающей в точности с одной из существующих заблокированных областей, будет неудачной. В этом случае функция вернет значение FALSE, а в выведенном системой сообщении об ошибке будет указано, что данная область блокирования не существует.

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

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

• Блокировки не наследуются вновь создаваемыми процессами.

Логику процедуры блокирования, когда вся область или только некоторая ее часть уже содержат заблокированные участки, иллюстрирует табл. 3.1.


Таблица 3.1. Логика предоставления блокировки 

Тип запрашиваемой блокировки 
Существующая блокировка Разделяемая блокировка Монопольная блокировка
Отсутствует Предоставляется Предоставляется
Разделяемая блокировка (одна или несколько) Предоставляется Отказ
Монопольная блокировка Отказ Отказ

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


Таблица 3.2. Блокировки и выполнение операций ввода/вывода

Операция ввода/вывода
Существующая блокировка Чтение Запись
Отсутствует Успешно выполняется Успешно выполняется
Разделяемая блокировка (одна или несколько) Выполняется. Вызывающий процесс не обязан быть владельцем блокировки данной области файла. Не выполняется
Монопольная блокировка Выполняется, если вызывающий процесс является владельцем блокировки, в противном случае — неудачное завершение. Выполняется, если вызывающий процесс является владельцем блокировки, в противном случае — неудачное завершение.

Обычно операции чтения и записи выполняются путем вызова функций Read-File и WriteFile или их расширенных версий ReadFileEx и WriteFileEx. Для диагностики ошибок, возникающих в процессе выполнения операций ввода/вывода, следует вызывать функцию GetLastError.

Одна из разновидностей операций ввода/вывода с участием файлов предполагает использование отображения файлов, которое обсуждается в главе 5. Обнаружение конфликтов блокировки на этапе обращения к памяти не производится; такая проверка осуществляется во время вызова функции MapViewOfFile. Указанная функция делает часть файла доступной для процесса, вследствие чего проверка наличия блокировок на этом этапе является необходимой.

Разновидностью функции LockFileEx с ограниченной сферой применимости является функция LockFile, вызов которой, скорее, лишь уведомляет о намерении осуществить блокировку. Эту функцию можно использовать в системах Windows 9x, которые не поддерживают функцию LockFileEx. Функция LockFile предоставляет блокирующему процессу только монопольный доступ, а возврат из функции происходит сразу же. Таким образом, функция LockFile не блокируется. Проверить, предоставлена блокировка или нет, можно путем тестирования возвращаемого функцией значения.

Снятие блокировок

Каждый успешный вызов функции LockFileEx должен сопровождаться последующим вызовом функции UnlockFileEx (то же самое касается и пары функций LockFile и UnlockFile). Если программа не позаботится о снятии блокировки или будет удерживать ее в течение большего, чем это необходимо, времени, другие программы либо вовсе не смогут работать, либо будут вынуждены простаивать. Поэтому уже на стадии проектирования и реализации программ необходимо очень тщательно следить за тем, чтобы снятие блокировки осуществлялось сразу же после того, как необходимость в ней отпала, а логика работы программ не позволяла оставлять невыполненными необходимые операции разблокирования файлов.

Одним из способов, гарантирующих своевременное разблокирования файлов, является использование дескрипторов завершения (termination handlers), которые описаны в главе 4.

Следствия принятой логики блокирования файлов

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

• Предположим, что процессы А и В периодически приобретают разделяемые блокировки файла, а процесс С блокируется при попытке получения монопольной блокировки того же файла после того, как процесс А стал владельцем собственной разделяемой блокировки. В этих условиях процесс В может получить свою разделяемую блокировку, но процесс С будет оставаться блокированным даже после того, как процесс А снимет свою блокировку файла. Процесс С будет оставаться блокированным до тех пор, пока все процессы не снимут свои блокировки, даже если они были получены уже тогда, когда процесс С пребывал в блокированном состоянии. Согласно этому сценарию процесс С может оставаться блокированным сколь угодно долго, тогда как другие процессы сохраняют возможность управления своими разделяемыми блокировками.

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

• Все, о чем говорилось выше, относится не только к блокировке файла в целом, но и к блокировке отдельного его участка.

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

Использование блокирования файлов

Рассмотрение примеров блокирования файлов мы отложим до главы 6, в которой обсуждается управление процессами. В программах 4.2, 6.4, 6.5 и 6.6 блокирование файлов используется для обеспечения того, чтобы в каждый момент времени изменять файл мог только один процесс.

В UNIX блокирование файлов является уведомляющим (advisory); выполнение процесса ввода/вывода может продолжаться даже в том случае, если попытка получения блокировки оказалась неудачной (логика, отраженная в табл. 3.1, действует и в этом случае). Это обеспечивает в UNIX возможность блокирования файлов взаимодействующими процессами, но любой другой процесс может нарушить описанный протокол.

Для получения уведомляющей блокировки используются параметры, указываемые при вызове функции fcntl. Допустимыми командами (второй параметр) являются F_SETLK, F_SETLKW и F_GETLK. Информация о типе блокировки (F_RDLCK, F_WRLCK или F_UNLCK) и блокируемой области содержится в дополнительной структуре данных.

Помимо этого, в некоторых UNIX-системах доступна обязательная (mandatory) блокировка, обеспечиваемая путем определения групповых полномочий для файла с помощью команды chmode.

Блокирование файлов в UNIX имеет много особенностей. Например, блокировки наследуются при выполнении вызова функции exec.

Блокирование файлов библиотекой С не поддерживается, но в Visual C++ обеспечивается поддержка нестандартных расширений механизма блокирования.

Реестр

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

Пользователь или системный администратор может просматривать и изменять содержимое реестра, пользуясь редактором реестра, для запуска которого необходимо выполнить команду REGEDIT. Реестром можно управлять также из программ, используя функции API реестра, описанные в данном разделе.

Примечание

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

В парах "имя-значение" реестра хранится следующая информация:

• Номер версии операционной системы, номер сборки и информация о зарегистрированном пользователе.

• Аналогичная информация обо всех приложения, которые были надлежащим образом установлены в системе.

• Информация о типе процессоров в системе и их количестве, системной памяти и тому подобное.

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

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

• Информация об установленных службах (глава 13).

• Список соответствий между расширениями имен файлов и ассоциированными с ними исполняемыми программами. Именно эти соответствия используются системой после того, как пользователь щелкнет на пиктограмме какого-либо файла. Например, щелчок на файле с расширением .doc может приводить к запуску текстового редактора Microsoft Word.

• Отображения сетевых адресов на имена, используемые локальным компьютером.

В операционной системе UNIX аналогичная информация хранится в каталоге /etc и файлах, находящихся в основном каталоге пользователя. В Windows 3.1 для этих целей использовались .INI-файлы. Реестр обеспечивает единообразное централизованное хранение всей информации подобного рода. Кроме того, используя средства защиты, описанные в главе 15, можно обеспечить безопасность реестра. 

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

Системное программирование в среде Windows

Рис. 3.1. Окно редактора реестра


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

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

Ключи реестра

На рис. 3.1 показана аналогия между разделами реестра и каталогами файловой системы. Каждый раздел может содержать другие разделы или последовательности пар "имя-значение". В то время как доступ к файловой системе реализуется посредством указания путей доступа, доступ к реестру осуществляется через его разделы. Существует несколько предопределенных разделов, которые играют роль точек входа в реестр.

• HKEY_LOCAL_MACHINE. В этом разделе хранится информация об оборудовании локального компьютера и установленном на нем программном обеспечении. Информация об установленном программном обеспечении обычно создается в подразделах (subkeys) в виде: SOFTWARE\НазваниеКомпании\НазваниеПродукта\Версия.

• HKEY_USERS. В этом разделе хранится информация о настройке пользовательских конфигураций.

• HKEY_CURRENT_CONFIG. В этом разделе хранятся текущие настройки таких параметров, как разрешение дисплея или гарнитура шрифта.

• HKEY_CLASSES_ROOT. В этом разделе содержатся подчиненные записи, устанавливающие соответствие между именами файлов и классами, а также приложениями, используемыми оболочкой для доступа к объектам, имена которых имеют определенные расширения. В этот раздел также входят все подразделы, необходимые для функционирования модели компонентных объектов (Component Object Model — СОМ), разработанной компанией Microsoft.

• HKEY_CURRENT_USER. В этом разделе хранится информация, определяемая пользователем, в том числе информация о переменных среды, принтерах и предпочтительных для вошедшего в систему пользователя конфигурационных параметрах приложений.

Управление системным реестром

Функции управления реестром позволяют запрашивать и изменять данные, относящиеся к парам "имя-значение", а также создавать новые подразделы и новые пары "имя-значение". Как для указания существующих разделов, так и для создания новых используются дескрипторы типа HKEY.[17] Нужные значения необходимо вводить; тип значения можно выбрать из нескольких готовых вариантов, соответствующих, например, строкам, двойным словам или расширяемым (expandable) строкам, параметры которых могут быть заменены переменными окружения.

Управление подразделами реестра

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

LONG RegOpenKeyEx(HKEY hKey, LPCTSTR lpSubKey, DWORD ulOptions, REGSAM samDesired, PHKEY phkResult) 

Параметры

hKey — указатель на текущий открытый раздел реестра или значение дескриптора одного из предопределенных зарезервированных разделов. pHkResult — указатель на переменную типа HKEY, получающую значение дескриптора вновь открываемого раздела.

lpSubKey — указатель на строку с именем подраздела. Именем подраздела может быть путь, например: Microsoft\WindowsNT\CurrentVersion. Значению NULL соответствует открытие новой копии раздела для hKey. Значение параметра ulOptions должно быть равным 0.

samDesired — маска доступа, описывающая уровень защиты нового раздела. К числу возможных значений относятся значения KEY_ALL_ACCESS, KEY_WRITE, KEY_QUERY_VALUE и KEY_ENUMARATE_SUBKEYS.

В случае успешного завершения функции возвращается значение ERROR_SUCCESS. Возврат любого другого значения указывает на ошибку. Для закрытия дескриптора открытого раздела используется функция RegCloseKey, которая в качестве своего единственного параметра принимает дескриптор.

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

Для получения пар "имя-значение" используются две взаимно дополняющих функции: RegEnumValue и RegQueryValueEx.[18] Функция RegSetValueEx сохраняет данные различного типа в поле значения открытого раздела реестра. Описания перечисленных функций, применение которых будет проиллюстрировано примером, содержатся в этом и следующем разделах книги.

Функция RegEnumKeyEx перечисляет подразделы открытого раздела системного реестра во многом аналогично тому, как функции FindFirstFile и FindNextFile перечисляют содержимое каталогов. Эта функция извлекает имя подраздела, строку с именем класса подраздела, а также дату и время последнего изменения.

LONG RegEnumValue(HKEY hKey, DWORD dwIndex, LPTSTR lpValueName, LPDWORD lpcbValueName, LPDWORD lpReserved, LPDWORD lpType, LPBYTE lpData, LPDWORD lpcbData) 

Значение параметра dwIndex должно устанавливаться равным 0 при первом вызове функции и увеличиваться на единицу при каждом последующем вызове. Название раздела и его размер, а также строка с именем класса и ее размер, возвращаются обычным способом. В случае успешного завершения функция возвращает значение ERROR_SUCCESS, иначе — код ошибки.

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

LONG RegCreateKeyEx(HKEY hKey, LPCTSTR lpSubKey, DWORD Reserved, LPTSTR lpClass, DWORD dwOptions, REGSAM samDesired, LPSECURITY_ATTRIBUTES lpSecurityAttributes, PHKEY phkResult, LPDWORD lpdwDisposition) 

Параметры

lpSubKey — указатель на строку, содержащую имя нового подраздела, создаваемого в разделе, на который указывает дескриптор hKey.

lpClass — указатель на строку, содержащую имя класса, или объектный тип, раздела, описывающее данные, представляемые разделом. Одними из многочисленных возможных значений являются REG_SZ (строка, завершающаяся нулевым символом) и REG_DWORD (двойное слово).

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

Параметр samDesired имеет тот же смысл, что и в случае функции RegOpenKeyEx.

Параметр lpSecurityAttributes может принимать значение NULL или указывать атрибуты защиты. Опции прав доступа к разделу могут выбираться из того же набора значений, что и в случае параметра samDesired.

lpdwDisposition — указатель на переменную типа DWORD, значение которой говорит о том, существовал ли раздел ранее (REG_OPENED_EXISTING_KEY) или он только создается (REG_CREATED_NEW_KEY).

Для удаления раздела используется функция RegDeleteKey. Двумя ее параметрами являются дескриптор открытого раздела и имя подраздела.

Управление значениями

Для перечисления значений параметров открытого раздела реестра используется функция RegEnumValue. Значение параметра dwIndex должно устанавливаться равным 0 при первом вызове функции и увеличиваться на единицу при каждом последующем вызове. После возврата из функции вы получаете строку, содержащую имя перечисляемого параметра, а также размер данных. Кроме того, вы получаете значение перечисляемого параметра и его тип. 

LONG RegEnumValue(HKEY hKey, DWORD dwIndex, LPTSTR lpValueName, LPDWORD lpcbValueName, LPDWORD lpReserved, LPDWORD lpType, LPBYTE lpData, LPDWORD lpcbData) 

Фактическое значение параметра возвращается в буфере, на который указывает указатель lpData. Размер результата содержится в переменной, на которую указывает указатель lpcbData. Тип данных, содержащийся в переменной, на которую указывает указатель lpType, может быть самым различным, включая REG_BINARY, REG_DWORD, REG_SZ (строка) и REG_EXPAND_SZ (расширяемая строка с параметрами, которые заменяются переменными окружения). Полный список типов данных системного реестра можно найти в оперативной справочной системе.

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

Функция RegQueryValueEx ведет себя аналогичным образом, за исключением того, что требует указания имени перечисляемого параметра, а не его индекса. Эту функцию можно использовать в тех случаях, когда известны имена параметров. Если же имена параметров неизвестны, следует использовать функцию RegEnumValueEx.

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

LONG RegSetValueEx(HKEY hKey, LPCTSTR lpValueName, DWORD Reserved, DWORD dwType, CONST BYTE * lpData, CONST cbData) 

Наконец, для удаления именованных значений используется функция RegDeleteValue.

Пример: вывод списка разделов и содержимого реестра

Программа lsReq (программа 3.4), является видоизменением lsW (программа 3.2, предназначенная для вывода списка файлов и каталогов) и обрабатывает не каталоги и файлы, а разделы и пары "имя-значение" системного реестра.

Программа 3.4. lsReq: вывод списка разделов и содержимого системного реестра

/* Глава 3. lsReg: Команда вывода содержимого реестра. Адаптированная версия программы 3.2. */

/* lsReg [параметры] подраздел */

#include "EvryThng.h"

BOOL TraverseRegistry(HKEY, LPTSTR, LPTSTR, LPBOOL);

BOOL DisplayPair(LPTSTR, DWORD, LPBYTE, DWORD, LPBOOL);

BOOL DisplaySubKey (LPTSTR, LPTSTR, PFILETIME, LPBOOL);


int _tmain(int argc, LPTSTR argv[]) {

 BOOL Flags[2], ok = TRUE;

 TCHAR KeyName[MAX_PATH + 1];

 LPTSTR pScan;

 DWORD i, KeyIndex;

 HKEY hKey, hNextKey;

 /* Таблица предопределенных имен и дескрипторов разделов. */

 LPTSTR PreDefKeyNames[] = {

  _Т("HKEY_LOCAL_MACHINE"), _T("HKEY_CLASSES_ROOT"),

  _Т("HKEY CURRENT USER"), _T ("HKEY CURRENT CONFIG"), NULL

 };

 HKEY PreDefKeys[] = {

  HKEY_LOCAL_MACHINE, HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_CURRENT_CONFIG

 };

 KeyIndex = Options(argc, argv, _T("Rl"), &Flags[0], &Flags[1], NULL);

 /* "Разобрать" шаблон поиска на "раздел" и "подраздел". */

 /* Воссоздать раздел. */

 pScan = argv[KeyIndex];

 for (i = 0; *pScan != _T('\\') && *pScan != _T('\0'); pScan++, i++) KeyName [i] = *pScan;

 KeyName[i] = _T('\0');

 if (*pScan == _T('\\')) pScan++;

 /* Преобразовать предопределенное имя раздела в соответствующий HKEY.*/

 for (i = 0; PreDefKeyNames [i] != NULL && _tcscmp(PreDefKeyNames[i], KeyName) != 0; i++);

 hKey = PreDefKeys[i];

 RegOpenKeyEx(hKey, pScan, 0, KEY_READ, &hNextKey);

 hKey = hNextKey;

 ok = TraverseRegistry(hKey, argv[KeyIndex], NULL, Flags);

 return ok ? 0 : 1;

}


BOOL TraverseRegistry(HKEY hKey, LPTSTR FullKeyName, LPTSTR SubKey, LPBOOL Flags)

/*Совершить обход разделов и подразделов реестра, если задан параметр –R.*/

{

 HKEY hSubK;

 BOOL Recursive = Flags[0];

 LONG Result;

 DWORD ValType, Index, NumSubKs, SubKNameLen, ValNameLen, ValLen;

 DWORD MaxSubKLen, NumVals, MaxValNameLen, MaxValLen;

 FILETIME LastWriteTime;

 LPTSTR SubKName, ValName;

 LPBYTE Val;

 TCHAR FullSubKName[MAX_PATH + 1];

 /* Открыть дескриптор раздела. */

 RegOpenKeyEx(hKey, SubKey, 0, KEY_READ, &hSubK);

 /* Определить максимальный размер информации относительно раздела и распределить память. */

 RegQueryInfoKey(hSubK, NULL, NULL, NULL, &NumSubKs, &MaxSubKLen, NULL, &NumVals, &MaxValNameLen, &MaxValLen, NULL, &LastWriteTime);

 SubKName = malloc (MaxSubKLen+1); /* Размер без учета завершающего нулевого символа. */

 ValName = malloc(MaxValNameLen+1); /* Учесть нулевой символ. */

 Val = malloc(MaxValLen); /* Размер в байтах. */

 /* Первый проход: пары "имя-значение". */

 for (Index = 0; Index < NumVals; Index++) {

  ValNameLen = MaxValNameLen + 1; /* Устанавливается каждый раз! */

  ValLen = MaxValLen + 1;

  RegEnumValue(hSubK, Index, ValName, &ValNameLen, NULL, &ValType, Val, &ValLen);

  DisplayPair(ValName, ValType, Val, ValLen, Flags);

 }

 /* Второй проход: подразделы. */

 for (Index = 0; Index < NumSubKs; Index++) {

  SubKNameLen = MaxSubKLen + 1;

  RegEnumKeyEx(hSubK, Index, SubKName, &SubKNameLen, NULL, NULL, NULL, &LastWriteTime);

  DisplaySubKey(FullKName, SubKName, &LastWriteTime, Flags);

  if (Recursive) {

   _stprintf(FullSubKName, _T("%s\\%s"), FullKName, SubKName);

   TraverseRegistry(hSubK, FullSubKName, SubKName, Flags);

  }

 }

 _tprintf(_T("\n"));

 free(SubKName);

 free(ValName);

 free(Val);

 RegCloseKey(hSubK);

 return TRUE;

}


BOOL DisplayPair(LPTSTR ValueName, DWORD ValueType, LPBYTE Value, DWORD ValueLen, LPBOOL Flags)

/* Функция, отображающая пары "имя-значение". */

{

 LPBYTE pV = Value;

 DWORD i;

 _tprintf(_T("\nValue: %s = "), ValueName);

 switch (ValueType) {

 case REG_FULL_RESOURCE_DESCRIPTOR: /* 9: описание оборудования. */

 case REG_BINARY: /* 3: Любые двоичные данные. */

  for (i = 0; i < ValueLen; i++, pV++) _tprintf (_T (" %x"), *pV);

  break;

 case REG_DWORD: /* 4: 32-битовое число. */

  _tprintf(_T ("%x"), (DWORD)*Value);

  break;

 case REG_MULTI SZ: /*7: массив строк, завершающихся нулевым символом.*/

 case REG_SZ: /* 1: строка, завершающаяся нулевым символом. */

  _tprintf(_T("%s"), (LPTSTR)Value);

  break;

 /* … Несколько других типов … */

 }

 return TRUE;

}


BOOL DisplaySubKey(LPTSTR KeyName, LPTSTR SubKeyName, PFILETIME pLastWrite, LPBOOL Flags) {

 BOOL Long = Flags[1];

 SYSTEMTIME SysLastWrite;

 _tprintf(_T("\nSubkey: %s"), KeyName);

 if (_tcslen(SubKeyName) > 0) _tprintf (_T ("\\%s "), SubKeyName);

 if (Long) {

  FileTimeToSystemTime(pLastWrite, &SysLastWrite);

  _tprintf(_T("%02d/%02d/%04d %02d:%02d:%02d"), SysLastWrite.wMonth, SysLastWrite.wDay, SysLastWrite.wYear, SysLastWrite.wHour, SysLastWrite.wMinute, SysLastWrite.wSecond);

 }

 return TRUE;

}

Резюме

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

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

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

В следующих главах

Глава 4 рассказывает о том, как упростить обработку ошибок и исключений, и распространяет применение функции ReportError на случаи обработки любых исключительных ситуаций.

Дополнительная литература

Для получения более подробной информации относительно программирования и использования системного реестра Windows, обратитесь к книге [17]. 

Упражнения

3.1. Используя функции GetDiskFreeSpace и GetDiskFreeSpaceEx, определите, насколько разреженным оказывается файловое пространство, распределяемое различными версиями операционной системы Windows. Например, создайте новый файл, установите для указателя файла большое значение, задайте размер файла и исследуйте наличие свободного пространства на жестком диске при помощи функции GetDiskFreeSpace. Эту же функцию Windows можно использовать для определения того, чтобы определить, каким образом сконфигурирован диск в терминах секторов и кластеров. Определите, инициализируется ли выделенное для вновь созданного файла дисковое пространство. Решение в виде исходного текста функции FreeSpace.с доступно на Web-сайте книги. Сравните результаты, полученные для столь различных систем, как Windows NT и Windows 9x. Представляет интерес также исследование вопроса о том, как сделать файл разреженным.

3.2. Что произойдет, если длину файла задать такой, чтобы его размер превышал объем диска? Обеспечивает ли Windows изящный выход из функции в случае ее неудачного завершения?

3.3. Измените предоставляемую на Web-сайте программу tail.с таким образом, чтобы в ней можно было обойтись без применения функции SetFilePointer; воспользуйтесь для этого структурой OVERLAPPED.

3.4. Исследуйте значение поля "количество ссылок" (nNumberOfLinks), полученное с использованием функции GetFileInformationByHandle. Всегда ли оно равно 1? Различаются ли ответы на этот вопрос для файловых систем NTFS и FAT? He включают ли значения счетчиков ссылок жесткие ссылки и ссылки из родительских каталогов и подкаталогов, как это имеет место в UNIX? Открывает ли Windows каталог как файл для получения дескриптора, прежде чем использовать эту функцию? Что можно сказать о ярлыках, поддерживаемых пользовательским интерфейсом?

3.5. В программе 3.2 поиск текущего и родительских каталогов осуществляется с использованием имен "." и "..". Что произойдет в случае, если файлы с такими именами действительно существуют? Могут ли файлы иметь такие имена?

3.6. Значения какого времени выводятся в программе 3.2 — местного или UCT? При необходимости измените программу таким образом, чтобы выводимые значения соответствовали местному времени.

3.7. Усовершенствуйте программу 3.2 таким образом, чтобы в выводимый список включались также текущий (".") и родительский ("..") каталоги (завершенная программа находится на Web-сайте). Кроме того, добавьте опции, позволяющие наряду с датой и временем последнего изменения отображать дату и время создания файла, а также дату и время последнего доступа к нему. 

3.8. Напишите программу, которая реализует команду rm, позволяющую удалять файлы, изменив для этого функцию ProcessItem в программе 3.2. Решение доступно на Web-сайте.

3.9. Усовершенствуйте команду cp из главы 2, предназначенную для копирования файлов, таким образом, чтобы она позволяла копировать файлы в указанный каталог. Дополнительно предусмотрите опцию рекурсивного копирования файлов (параметр –r) и опцию сохранения вместе с копией также времени последнего изменения файла (параметр –р). Для реализации опции рекурсивного копирования файлов вам потребуется создать новые каталоги. 

3.10. Напишите программу mv, которая реализует одноименную команду UNIX, позволяющую переместить целиком любой каталог. При этом имеет существенное значение, осуществляется ли перемещение файла или каталога на другой диск или они остаются на прежнем диске. В случае смены диска используйте операцию копирования файлов, в противном случае используйте команды MoveFile или MoveFileEx.

3.11. Усовершенствуйте программу 3.3 (touch) таким образом, чтобы новое время создания файла можно было указывать в командной строке. Команда UNIX допускает (по выбору) указание метки времени после обычных параметров, но перед именами файлов. Метки даты и времени имеют формат MMddhhmm [уу], где ММ — месяцы, dd — дни, hh — часы, mm — минуты, уу — года. Двух цифр для обозначения года нам будет недостаточно, поэтому предусмотрите для указания года четыре разряда.

3.12. Программа 3.1 рассчитана на работу с большими файловыми системами NTFS. Если на вашем жестком диске имеется достаточно много свободного места, протестируйте работу этой программы на файлах гигантских размеров (свыше 4 Гбайт). Проверьте, насколько корректно работает 64-битовая арифметика. Выполнять это упражнение на сетевом диске без предварительного разрешения администратора сети не рекомендуется. Завершив работу над этим упражнением, не забудьте удалить тестовый файл.

3.13. Напишите программу, которая блокирует заданный файл и удерживает его в блокированном состоянии в течение длительного времени (вероятно, вызахотите воспользоваться функцией Sleep). Воспользовавшись любым текстовым редактором, попытайтесь получить доступ к файлу (используйте текстовый файл) в период действия блокировки. Что при этом происходит? Заблокирован ли файл должным образом? Вы также можете написать программу, предлагающую пользователю задать блокировку для тестового файла. Чтобы проверить, срабатывает ли блокировка описанным образом, запустите на выполнение два экземпляра программы в разных окнах. Решение этого упражнения содержится в файле TestLock.c, находящемся на Web-сайте. 

3.14. Исследуйте представление временных характеристик файла Windows в формате данных FILETIME. В этом формате используются 64-битовые счетчики, выражающие в 100-наносекундных единицах длительность истекшего периода времени, отсчитываемого от 1 января 1601 года. Когда исчерпаются показания этого счетчика? Какова максимально допустимая дата для временных характеристик файлов UNIX?

3.15. Напишите интерактивную утилиту, в которой пользователю предлагается ввести имя раздела реестра и имя значения реестра. Отобразите текущее значение и предложите пользователю указать новое.

3.16. В этой главе, как и в большинстве других глав книги, описываются наиболее важные функции. Однако во многих случаях вам могут оказаться полезными и другие функции. На страницах оперативного справочного руководства для каждой функции приведены ссылки на родственные функции. Ознакомьтесь с некоторыми из них, такими, например, как FindFirstFileEx, ReplaceFile, SearchPath или WriteFileGather. Некоторые функции доступны не во всех версиях NT5. 

ГЛАВА 4

Обработка исключений

Основное внимание в данной главе сфокусировано на структурной обработке исключений (Structured Exception Handling, SEH), но наряду с этим обсуждены также обработчики управляющих сигналов консоли и векторная обработка исключений (Vectored Exception Handling, VEH).

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

Мы используем SEH в приведенных ниже примерах программ и расширим посредством этого механизма возможности функции обработки ошибок ReportError, которая была введена в главе 2. Обычно сфера применимости SEH ограничивается программами, написанными на языке на С. Вместе с тем, представленные ниже возможности SEH воспроизводятся в C++, С# и других языках программирования с использованием весьма похожих механизмов.

В настоящей главе описаны также обработчики управляющих сигналов консоли, благодаря которым программы могут воспринимать внешние сигналы, вырабатываемые, например, при нажатии сочетания клавиш <Ctrl+C>, выходе пользователя из системы или завершении работы системы. Кроме того, использование подобных сигналов обеспечивает реализацию ограниченных форм межпроцессного взаимодействия.

Глава завершается рассмотрением векторной обработки исключений, которая потребует от вас использования операционных систем Windows XP или Windows Server 2003. Благодаря VEH пользователь получает возможность определить функции, которые должны вызываться сразу же после возникновения исключения, не дожидаясь активизации SEH.

Исключения и обработчики исключений

В отсутствие обработки исключений возникновение любой нестандартной ситуации, например, попытки разыменования нулевого указателя или деления на ноль, приведет к немедленному прекращению выполнения программы. В качестве примера, иллюстрирующего проблемы, которые могут при этом возникать, можно назвать создаваемые программой временные файлы, подлежащие удалению до того, как программа завершит свою работу. SEH предоставляет возможность определить блок программного кода, или обработчик исключений (exception handler), который в случае возникновения исключения удалит временные файлы.

Поддержка SEH обеспечивается за счет совместного использования функций Windows, средств поддержки языков программирования, предоставляемых компилятором, и средств поддержки времени выполнения. Какой именно язык программирования поддерживается, зависит от конкретной системы; наши примеры ориентированы на Microsoft С.

Блоки try и except

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

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

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

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

• Интенсивное использование данных в виде массивов, что может сопровождаться выходом значений индексов элементов массива за границы допустимого диапазона.

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

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

Если при изучении примеров, приведенных в этой главе или книге в целом, вы решите отслеживать исключения, которые могут возникать на том или ином участке программы, создайте для него блоки try и except, как показано ниже:

__try {

 /* Блок контролируемого кода */

} __ except(выражение_фильтра) {

 /* Блок обработки исключений */

}

Имейте в виду, что __try и __except — это ключевые слова, распознаваемые компилятором.

Блоки try являются частью обычного кода приложения. Если на данном участке кода возникает исключение, ОС передает управление обработчику исключений, который представляет собой блок программного кода, следующий за ключевым словом_ except. Характер последующих действий определяется значением параметра выражение_фильтра.

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

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

Выражения фильтров и их значения

Параметр выражение_фильтра в операторе except вычисляется сразу же после того, как возникает исключение. В качестве выражения может выступать литеральная константа, вызов функции фильтра (filter function) или условное выражение. В любом случае выражение должно возвращать одно из следующих трех значений:

1. EXCEPTION_EXECUTE_HANDLER — система выполняет операторы блока обработки исключений, как показано на рис. 4.1 (см. программу 4.1). Это соответствует обычному случаю.

2. EXCEPTION_CONTINUE_SEARCH — система игнорирует данный обработчик исключений и пытается найти обработчик исключений в охватывающем блоке, продолжая этот процесс аналогичным образом до тех пор, пока не будет найден обработчик исключений.

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

Системное программирование в среде Windows

Рис. 4.1. SEH, блоки и функции

Ниже приведен простой пример, в котором обработчик исключений используется для удаления временного файла в тех случаях, когда исключение возникает в теле цикла. Заметьте, что ключевое слово __try может быть применено к любому блоку, включая блоки, связанные с операторами while, if или любым другим оператором ветвления. В данном примере возникновение любого исключения приводит к удалению временного файла и закрытию дескриптора, после чего выполнение цикла возобновляется.

GetTempFileName(TempFile, …);

while (…) __try {

 hFile = CreateFile(TempFile, …, OPEN_ALWAYS, …);

 SetFilePointer(hFile, 0, NULL, FILE_END);

 WriteFile(hFile, …);

 i = *p; /* В этом месте программы возможно возникновение исключения адресации. */

 CloseHandle (hFile);

 …

} __except (EXCEPTION_EXECUTE_HANDLER) {

 CloseHandle(hFile);

 DeleteFile(TempFile);

 /* Переход к выполнению очередной итерации цикла. */

}

/* Сюда передается управление после нормального завершения цикла.

 Каждый раз при возникновении исключения дескриптор временного файла  закрывается, а сам файл удаляется. */

Ниже описана логика приведенного выше фрагмента кода.

• На каждой итерации цикла в конце файла добавляются новые данные.

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

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

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

• Чтобы гарантировать закрытие дескриптора файла, это делается как при выходе из цикла, так и перед началом очередной итерации цикла.

Коды исключений

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

DWORD GetExceptionCode(VOID)

Код исключения должен быть получен сразу же после возникновения исключения. Поэтому функция фильтра не может просто вызвать функцию GetExceptionCode (это ограничение налагается компилятором). Обычный способ решения этой проблемы состоит в том, чтобы осуществить этот вызов в выражении фильтра, как показано в следующем примере, в котором код исключения является аргументом функции фильтра, предоставляемой пользователем:

__except(MyFilter(GetExceptionCode())) {

} 

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

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

• Выполнение программой некорректных действий, например:

EXCEPTION_ACCESS_VIOLATION — попытка чтения или записи по адресу виртуальной памяти, к которой процесс не имеет доступа.

EXCEPTION_DATATYPE_MISALIGNMENT — многие процессоры, например, требуют чтобы данные типа DWORD выравнивались по четырехбайтовым границам.

EXCEPTION_NONCONTINUABLE_EXECUTION — значением выражения фильтра было EXCEPTION_CONTINUE_EXECUTION, но выполнения программы после возникновения исключения не может быть продолжено.

• Исключения, сгенерированные функциями распределения памяти НеарAlloc и HeapCreate, если они используют флаг HEAP_GENERATE_EXCEPTIONS (см. главу 5). Соответствующими значениями кода исключения являются STATUS_NO_MEMORY или EXCEPTION_ACCESS_VIOLATION.

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

• Коды различных арифметических исключений (особенно FP-исключений), например, EXCEPTION_INT_DIVIDE_BY_ZERO или EXCEPTION_FLT_OVERFLOW.

• Исключения, используемые отладчиками, например, EXCEPTION_BREAKPOINT или EXCEPTION_SINGLE_STEP.

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

LPEXCEPTION_POINTERS GetExceptionINFORMATION(VOID) 

Вся информация, как относящаяся, так и не относящаяся к процессору, содержится в структуре EXCEPTION_POINTERS, состоящей из двух других структур.  

typedef struct _EXCEPTION_POINTERS {

 PEXCEPTION_RECORD ExceptionRecord;

 PCONTEXT ContextRecord;

} EXCEPTION POINTERS; 

В структуру EXCEPTION_RECORD входит элемент ExceptionCode, набор возможных значений которого совпадает с набором значений, возвращаемых функцией GetExceptionCode. Элемент ExceptionFlags структуры EXCEPTION_RECORD может принимать значения 0 или EXCEPTION_NONCONTINUABLE, причем последнее значение указывает функции фильтра на то, что она не должна предпринимать попыток продолжения выполнения. К числу других элементов данных этой структуры относятся адрес виртуальной памяти ExceptionAddress и массив параметров ExceptionInformation. В случае исключения EXCEPTION_ACCESS_VIOLATION значение первого элемента этого массива указывает на то, какая именно из операций пыталась получить доступ по недоступному адресу — записи (1) или чтения (0). Второй элемент содержит адрес виртуальный памяти.

Во втором элементе структуры EXCEPTION_POINTERS, а именно, элементе ContextRecord, содержится информация, относящаяся к процессору. Для каждого типа процессоров предусмотрены свои структуры, определения которых содержатся в файле <winnt.h>.

Резюме: последовательность обработки исключений

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

1. Возникло исключение; в данном случае это деление на ноль.

2. Управление передается обработчику исключений, в котором вычисляется выражение фильтра. Сначала вызывается функция GetExceptionCode, а затем ее возвращаемое значение используется в качестве аргумента функции Filter.

3. Функция фильтра выполняет действия, определяемые значением кода исключения.

4. В данном случае значением кода исключения является EXCEPTION_INT_DIVIDE_BY_ZERO.

5. Функция фильтра устанавливает, что должен быть выполнен код обработчика исключений, и поэтому возвращает значение EXCEPTION_EXECUTE_HANDLER.

6. Выполняется код обработчика исключений, связанного с оператором _except.

7. Управление передается за пределы блоков try и except. 

Системное программирование в среде Windows

Рис. 4.2. Последовательность операций при обработке исключений

Исключения, возникающие при выполнении операций над числами с плавающей точкой

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

DWORD _controlfp(DWORD new, DWORD mask) 

Фактическое значение маски определяется ее текущим значением (current_mask) и двумя аргументами следующим образом:

(current_mask & ~mask) | (new & mask)

Данная функция устанавливает лишь те из битов, указанных в аргументе new, которые разрешены аргументом mask. Биты, не активизированные аргументом mask, не изменяются. Маска FP-исключений управляет также точностью, округлением и обработкой значений, соответствующих бесконечности, поэтому при активизации перечисленных исключений необходимо тщательно следить за тем, чтобы случайно не изменить эти установки.

Возвращаемым значением является фактическое значение маски. Так, при нулевых значениях обоих аргументов возвращаемым значением будет текущее значение маски (current_mask), что может быть использовано для восстановления маски, если впоследствии в этом возникнет необходимость. С другой стороны, если задать аргумент mask равным 0xFFFFFFFF, то регистр установится в new, что, например, может быть использовано для восстановления прежнего значения маски.

Обычно для того, чтобы разрешить исключения, связанные с выполнением операций над числами с плавающей точкой, в качестве аргумента mask используют константу MCW_EM, как продемонстрировано в следующем примере. Также заметьте, что при обработке FP-исключения оно должно быть сброшено путем использования функции _clearfp.

#include <float.h>

DWORD FPOld, FPNew; /* Старое и новое значения маски. */

FPOld = _controlfp(0, 0); /* Сохранить старую маску. */

/* Указать в качестве разрешенных шесть типов исключений. */

FPNew = FPOld & ~(EM_OVERFLOW | EM_UNDERFLOW | EM_INEXACT | EM_ZERODIVIDE | EM_DENORMAL | EM_INVALID);

/* Установить новую управляющую маску. Параметр MCW_EM объединяет шесть исключений, указанных в предыдущем операторе. */

_controlfp(FPNew, MCW_EM);

while(…) __try { /* Выполнить вычисления над числами с плавающей точкой. */

 … /* На этом участке кода может возникнуть FP-исключение. */

} __except(EXCEPTION_EXECUTE_HANDLER) {

 … /* Обработать FP-исключение. */

 _clearfp(); /* Сбросить исключение. */

 _controlfp(FPOld, 0xFFFFFFFF); /* Восстановить маску. */

}

В этом примере разрешены все возможные FP-исключения, кроме одного — EXCEPTION_FLT_STACK_CHECK, которое соответствует переполнению стека при выполнении операций над числами с плавающей точкой. Можно поступить и по-другому, разрешая отдельные исключения путем использования только выбранных масок исключений, например EM_OVERFLOW. Аналогичный код используется в программе 4.3 в контексте примера программного кода большего объема.

Ошибки и исключения

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

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

Вместе с тем, указанные различия между ошибками и исключениями являются довольно условными. Windows позволяет управлять генерацией исключений, возникающих в случае нехватки памяти при ее распределении с использованием функций НеарАllос и HeapCreate. Этот процесс описан в главе 5. Помимо этого, программы могут генерировать собственные исключения с кодами, определяемыми программистом, используя для этого функцию RaiseException, о чем далее будет говориться.

Обработчики исключений обеспечивают удобный механизм выхода из внутренних блоков или функций под управлением программы без использования операторов перехода goto или longjmp. Такая возможность оказывается особенно полезной, если блок получил доступ к таким, например, ресурсам, как открытые файлы, память или объекты синхронизации, поскольку обработчик может взять на себя задачу освобождения этих ресурсов. Возможно также продолжение работы программы после выполнения кода обработчика исключений, а не ее обязательное завершение. Кроме того, после выхода из блока программа может восстанавливать прежнее состояние системы, например маску FP-исключений. Именно в этом ключе обработчики используются во многих наших примерах.

Исключения, генерируемые приложением

Существует возможность формирования исключений в любой точке программы в процессе ее выполнения с помощью функции RaiseException. Это позволяет программе обнаруживать и обрабатывать возникающие ошибки как исключения.

VOID RaiseException(DWORD dwExceptionCode, DWORD dwExceptionFlags, DWORD cArguments, CONST DWORD *lpArguments)

Параметры

dwExceptionCode — код исключения, определяемый пользователем. Бит 28 использовать нельзя, так как он зарезервирован системой. Для кода ошибки отводятся биты 27—0 (то есть все слово, кроме самого старшего шестнадцатеричного разряда). Бит 29 должен быть установлен, чтобы показать, что данное исключение имеет "пользовательскую" природу (а не относится к числу тех, которые предусмотрела Microsoft). В битах 31—30 содержится код серьезности ошибки, принимающий приведенные ниже значения, в которых результирующая старшая шестнадцатеричная цифра кода исключения представлена с установленным битом 29.

• 0 — успешное выполнение (старшая шестнадцатеричная цифра кода исключения равна 2).

• 1 — информационный код (старшая шестнадцатеричная цифра кода исключения равна 6).

• 2 — предупреждение (старшая шестнадцатеричная цифра кода исключения равна А).

• 3 — ошибка (старшая шестнадцатеричная цифра кода исключения равна Е).

dwExceptionFlags — обычно устанавливается равным 0, тогда как установка значения EXCEPTION_NONCONTINUABLE будет указывать на то, что выражение фильтра не должно возвращать значение EXCEPTION_CONTINUE_EXECUTION; при попытке это сделать будет немедленно сгенерировано исключение ЕХСЕРTION_NONCONTINUABLE_EXCEPTION.

lpArguments — этот указатель, если он не равен NULL, указывает на массив размера cArguments (третий параметр), содержащий 32-битовые значения, которые должны быть переданы выражению фильтра. Максимально возможное число этих значений ограничивается значением EXCEPTION_MAXIMUM_PARAMETERS, которое в настоящее время установлено равным 15. Для доступа к этой структуре следует использовать функцию GetExceptionInformation.

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

Пример: обработка ошибок как исключений

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

• Даже в тех случаях, когда было бы достаточно прекратить выполнения только одного потока, критическая ошибка приводит к остановке всего процесса (главу 7).

• Вместо завершения процесса может оказаться желательным продолжение выполнения программы.

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

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

Решение заключается в написании новой функции — ReportException. Если ошибка не является критической, эта функция вызывает функцию ReportError (разработанную в главе 2), которая выводит сообщение об ошибке. В случае же возникновения критической ошибки будет сгенерировано исключение. Система будет использовать обработчик исключений из вызывающего try-блока, и поэтому в действительности характер исключения может быть некритическим, если обработчик предоставляет программе возможность восстановиться после сбоя. По существу, функция ReportException дополняет обычные программные методы защиты от ошибок, ранее ограниченные функцией ReportError. В случае обнаружения ошибки обработчик позволяет программе продолжить свою работу после выполнения необходимых восстановительных действий. Эти возможности иллюстрирует программа 4.2.

Функция ReportException представлена в программе 4.1. Необходимые определения и заголовочные файлы не указаны, поскольку эта функция находится в том же модуле исходного кода, что и функция ReportError. 

Программа 4.1. ReportException: функция вывода сообщений об исключениях 

/* Расширение функции ReportError для генерации формируемого приложением кода исключения вместо прекращения выполнения процесса. */

VOID ReportException(LPCTSTR UserMessage, DWORD ExceptionCode)

/* Вывести сообщение о некритической ошибке. */

{

 ReportError(UserMessage, 0, TRUE);

 /* Если ошибка критическая, сгенерировать исключение. */

 if (ExceptionCode != 0) RaiseException((0x0FFFFFFF & ExceptionCode) | 0xE0000000, 0, 0, NULL);

 return;

}

Функция ReportException используется в нескольких последующих примерах. 

Модель сигналов, используемая в UNIX, значительно отличается от SEH. Сигналы могут быть пропущены или игнорированы, и логика их работы иная. Тем не менее, у этих моделей имеются и общие черты.

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

Некоторые сигналы соответствуют исключениямWindows.

Перечень в некоторой мере ограниченных соответствий "сигнал-исключение" представлен ниже:

• SIGILL — EXCEPTION_PRIV_INSTRUCTION

• SIGSEGV — EXCEPTION_ACCESS_VIOLATION

• SIGFPE — семь различных исключений, связанных с выполнением операций над числами с плавающей точкой, например EXCEPTION_FLT_DIVIDE_BY_ZERO

• SIGUSR1 и SIGUSR2 — исключения, определяемые приложением

Функции RaiseException соответствует функция библиотеки С raise.

В Windows сигналы SIGILL, SIGSEGV и SIGFPE не генерируются, хотя функция raise может генерировать один из них. Сигнал SIGINT в Windows не поддерживается.

Функция UNIX kill (kill не входит в состав стандартной библиотеки С), которая посылает сигнал другому процессу, может быть сопоставлена функции Windows GenerateConsoleCtrlEvent (глава 6). Для ограниченного варианта SIGKILL в Windows имеются аналоги в виде функций TerminateProcess и TerminateThread, с помощью которых один процесс (или поток) может уничтожить другой, хотя при использовании этих функций необходимо соблюдать осторожность (см. главы 6 и 7).

Обработчики завершения

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

Обработчик завершения строится с использования ключевого слова __finally в операторе try…finally. Структура этого оператора аналогична структуре оператора try…finally, но в ней отсутствует выражение фильтра. Как и обработчики исключений, обработчики завершения предоставляют удобные возможности для закрытия дескрипторов, освобождения ресурсов, восстановления масок и выполнения иных действий, направленных на восстановление известного состояния системы после выхода из блока. Например, программа может выполнять операторы return внутри блока, оставляя всю работу по "уборке мусора" обработчику завершения. Благодаря этому отпадает необходимость во включении кода очистки в код самого блока или переходе к коду очистки при помощи оператора goto.

__try {

 /* Блок кода. */

} _finally {

 /* Обработчик завершения (блок finally). */

}

Выход из try-блока

Обработчик завершения выполняется всякий раз, когда в соответствии с логикой программы осуществляется выход из try-блока по одной из следующих причин:

• Достижение конца try-блока и "проваливание" в обработчик завершения.

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

return

break

goto[19]

longjmp 

continue

__leave[20]

• Исключение.

Аварийное завершение

Любое завершение выполнения программы по причинам, отличным от достижения конца try-блока и "проваливания вниз" или выполнения оператора __leave, считается аварийным завершением. Результатом выполнения оператора __leave является переход в конец блока __try и передача управления вниз по тексту программы, что намного эффективнее простого использования оператора goto, поскольку не требует разворачивания стека. Для определения того, каким образом завершилось выполнение try-блока, в обработчике завершения используется следующая функция: 

BOOL AbnormalTermination(VOID) 

При аварийном завершении выполнения блока эта функция возвращает значение TRUE, при нормальном — FALSE.

Примечание

Завершение будет считаться аварийным, даже если, например, последним оператором try-блока был оператор return.

Выполнение обработчика завершения и выход из него

Обработчик завершения, или блок __finally, выполняется в контексте блока или функции, работу которых он отслеживает. Управление может переходить от оператора завершения к следующему оператору. Существует и другая возможность, когда обработчик завершения выполняет оператор передачи управления (return, break, continue, goto, longjmp или __leave). Еще одной возможностью является выход из обработчика по причине возникновения исключения.

Сочетание блоков finally и except

Один try-блок может иметь только один блок finally или только один блок except, но не может иметь оба указанных блока одновременно. Поэтому нижеприведенный код вызовет появление ошибок на стадии компиляции. 

__try {

 /* Блок контролируемого кода. */

} __except (filter_expression) {

 /* Блок обработчика исключений. */

} __finally {

 /* Так делать нельзя! Это приведет к ошибке на стадии компиляции. */

}

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

 __try { /* Внешний блок try-except. */

  while (…) __try { /* Внутренний блок try-finally. */

   hFile = CreateFile(TempFile, …);

   if(…) __try { /* Внутренний блок try-except. */

    /* Разрешить FP-исключения. Выполнить вычисления. */

    …

   } __except(EXCEPTION_EXECUTE_HANDLER) {

    … /* Обработать FP-исключение. */

    _clearfp();

   }

   … /* Обработка исключений, не являющихся FP-исключениями. /*

  } __finally { /* Конец цикла while. */

   /* Выполняется на КАЖДОЙ итерации цикла. */

   CloseHandle(hFile);

   DeleteFile(TempFile);

  }

 } __except (filter-expression) {

 /* Обработчик исключений. */

}

Глобальное и локальное разворачивание стека

Исключения и аварийные завершения вызывают глобальное разворачивание стека (global stack unwind) в поиске обработчика, как было показано на рис. 4.1. Предположим, например, что в отслеживаемом блоке примера, приведенного в конце предыдущего раздела, исключение возникает прежде, чем активизируются FP-исключения. Тогда перед обработчиком исключения в стеке могут находиться многочисленные обработчики завершения.

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

• Статической структуры программных блоков.

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

Обработчики завершения: завершение процессов и потоков

Обработчики завершения не выполняются, если выполнение процесса или потока было прекращено независимо от того, было ли это инициировано самим процессом путем использования функций ExitProcess или ExitThread, или вызвано извне, например, инициировано вызовом функций TerminateProcess или TerminateThread из другого места в программе. Поэтому ни одна из этих функций не должна вызываться процессом или потоком внутри блоков try…except или try…finally.

Обратите также внимание, что выполнение функции библиотеки С exit или возврат из функции main приводят к выходу из процесса.

SEH и обработка исключений в C++

При обработке исключений в C++ используются ключевые слова catch и throw, а сам механизм исключений реализован с использованием SEH. Тем не менее, обработка исключений в C++ и SEH — это разные вещи. Их совместное применение требует внимательного обращения, поскольку обработчики исключений, написанные пользователем и сгенерированные C++, могут взаимодействовать между собой и приводить к нежелательным последствиям. Например, находящийся в стеке обработчик __except может перехватить исключение C++, в результате чего данное исключение так и не дойдет до обработчика C++.

Возможно и обратное, когда, например, обработчик C++ перехватит SEH-исключение, сгенерированное функцией RaiseException. Документация Microsoft рекомендует полностью отказаться от использования обработчиков Windows в программах на C++ и ограничиться применением в них только обработчиков исключений C++.

Кроме того, обработчики исключений или завершения Windows не осуществляют вызов деструкторов, что в ряде случаев необходимо для уничтожения экземпляров объектов C++.

Пример: использование обработчиков завершения для повышения качества программ

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

В программе toupper (программа 4.2) эти моменты иллюстрируются с привлечением идей, почерпнутых в программном коде предшествующих примеров. toupper обрабатывает несколько файлов, имена которых указываются в командной строке, переписывая их с преобразованием всех букв в верхний регистр. Имена преобразованных файлов получаются путем добавления префикса UC_ к исходным именам, и согласно "спецификации" программы запись поверх существующих файлов не производится. Преобразование файлов осуществляется в памяти машины, поэтому для каждого файла выделяется большая буферная область (достаточная для размещения всего файла). Кроме того, чтобы исключить любую возможность изменения файлов другими процессами, а также для того, чтобы вновь создаваемые выходные файлы строго соответствовали преобразованным входным файлам, оба вида файлов блокируются во время обработки. Понятно, что на каждой стадии обработки существует вероятность возникновения самых различных сбойных ситуаций, но в программе должна быть предусмотрена защита от подобных ошибок, и она должна располагать средствами, позволяющими ей восстановить свое нормальное состояние и попытаться обработать все остальные файлы, имена которых были указаны в командной строке. Программа 4.2 решает все эти задачи, обеспечивая разблокирование файлов во всех необходимых случаях без применения громоздкой логики операторов ветвления, к которым пришлось бы прибегнуть, если бы не были использованы средства SEH. Более подробные комментарии к программе содержатся в программном коде, находящемся на Web-сайте книги.

Программа 4.2. toupper: обработка файлов с восстановлением нормального состояния программы после сбоев

/* Глава 4. Команда toupper. */

/* Преобразование содержимое одного и более файлов с заменой всех букв на прописные. Имя выходного файла получается из имени входного файла добавлением к нему префикса UC_. */

#include "EvryThng.h"


int _tmain(DWORD argc, LPTSTR argv[]) {

 HANDLE hIn = INVALID_HANDLE_VALUE, hOut = INVALID_HANDLE_VALUE;

 DWORD FileSize, nXfer, iFile, j;

 CHAR OutFileName [256] = "", *pBuffer = NULL;

 OVERLAPPED ov = {0, 0, 0, 0, NULL}; /* Используется для блокирования файлов. */

 if (argc <= 1) ReportError(_T("Использование: toupper файлы"), 1, FALSE);

 /* Обработать все файлы, указанные в командной строке. */

 for (iFile = 1; iFile < argc; iFile++) __try { /* Блок исключений. */

  /* Все дескрипторы файлов недействительны, pBuffer == NULL, а файл OutFileName пуст. Выполнение этих условий обеспечивается обработчиками. */

  _stprintf(OutFileName, "UC_%s", argv[iFile]);

  __try { /* Внутренний блок try-finally. */

   /* Ошибка на любом шаге сгенерирует исключение, и следующий */

   /* файл будет обрабатываться только после "уборки мусора". */

   /* Объем работы по очистке зависит от того, в каком месте */

   /* программы возникла ошибка. */

   /* Создать выходной файл (завершается с ошибкой, если файл уже существует). */

   hIn = CreateFile(argv[iFile], GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);

   if (hIn == INVALID_HANDLE_VALUE) ReportException(argv[iFile], 1);

   FileSize = GetFileSize(hIn, NULL);

   hOut = CreateFile(OutFileName, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_NEW, 0, NULL);

   if (hOut == INVALID_HANDLE_VALUE) ReportException(OutFileName, 1);

   /* Распределить память под содержимое файла. */

   pBuffer = malloc(FileSize);

   if (pBuffer == NULL) ReportException(_T("Ошибка при распределении памяти"), 1);

   /* Блокировать оба файла для обеспечения целостности копии. */

   if (!LockFileEx(hIn, LOCKFILE_FAIL_IMMEDIATELY, 0, FileSize, 0, &ov) ReportException(_T("Ошибка при блокировании входного файла"), 1);

   if (!LockFileEx(hOut, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0, FileSize, 0, &ov) ReportException(_T("Ошибка при блокировании выходного файла "), 1);

   /* Считать данные, преобразовать их и записать в выходной файл. */

   /* Освободить ресурсы при завершении обработки или возникновении */

   /* ошибки; обработать следующий файл. */

   if (!ReadFile(hIn, pBuffer, FileSize, &nXfer, NULL)) ReportException(_T("Ошибка при чтении файла"), 1);

   for (j = 0; j < FileSize; j++) /* Преобразовать данные. */

    if (isalpha(pBuffer [j])) pBuffer[j] = toupper(pBuffer [j]);

   if(WriteFile(hOut, pBuffer, FileSize, &nXfer, NULL)) ReportException(T("Ошибка при записи в файл"), 1);

  } __finally {

   /*Освобождение блокировок, закрытие дескрипторов файлов,*/

   /*освобождение памяти и повторная инициализация */

   /*дескрипторов и указателя. */

   if (pBuffer != NULL) free (pBuffer);

   pBuffer = NULL;

   if (hIn != INVALID_HANDLE_VALUE) {

    UnlockFileEx(hIn, 0, FileSize, 0, &ov);

    CloseHandle(hIn);

    hIn = INVALID_HANDLE_VALUE;

   }

   if (hOut != INVALID_HANDLE_VALUE) {

    UnlockFileEx(hOut, 0, FileSize, 0, &ov);

    CloseHandle(hOut);

    hOut = INVALID_HANDLE_VALUE;

   }

   _tcscpy(OutFileName, _T(""));

  }

 }

 /* Конец основного цикла обработки файлов и блока try. */

 /* Обработчик исключений для тела цикла. */

 __except(EXCEPTION_EXECUTE_HANDLER) {

  _tprintf(_T("Ошибка при обработке файла %s\n"), argv[iFile]);

  DeleteFile (OutFileName);

 }

 _tprintf(_T("Обработаны все файлы, кроме указанных выше \n"));

 return 0;

}

Пример: использование функции фильтра

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

Блок __finally восстанавливает состояние маски FP-исключений. Совершенно очевидно, что восстановление состояния маски в данном случае, когда процесс уже должен завершаться, особого значения не имеет, но эта методика пригодится нам впоследствии, когда мы будем использовать ее на стадии завершения выполнения потока. Вообще говоря, процесс должен восстанавливать и системные ресурсы, например, удалять временные файлы, освобождать ресурсы синхронизации (глава 8) и отменять блокирование файлов (главы 3 и 6). Функция фильтра представлена в программе 4.4.

Данный пример не иллюстрирует обработку исключений, которые могут возникать при распределении памяти; эти исключения мы начнем интенсивно использовать в главе 5.

Программа 4.3. Exception: обработка исключений и завершения выполнения

#include "EvryThng.h"

#include <float.h>


DWORD Filter(LPEXCEPTION_POINTERS, LPDWORD);

double x = 1.0, у = 0.0;


int _tmain(int argc, LPTSTR argv[]) {

 DWORD ECatgry, i = 0, ix, iy = 0;

 LPDWORD pNull = NULL;

 BOOL Done = FALSE;

 DWORD FPOld, FPNew;

 FPOld = _controlfp(0, 0); /* Сохранить старую управляющую маску. */

 /* Разрешить FP-исключения. */

 FPNew = FPOld & ~(EM_OVERFLOW | EM_UNDERFLOW | EM_INEXACT | EM_ZERODIVIDE | EM_DENORMAL | EM_INVALID);

 _controlfp(FPNew, MCW_EM);

 while (!Done) _try { /* Блок try-finally. */

  _tprintf(_T("Введите тип исключения: "));

  _tprintf(_T(" 1: Mem, 2: Int, 3: Flt 4: User 5: __leave "));

  _tscanf(_T("%d"), &i);

  __try { /* Блок try-except. */

   switch (i) {

   case 1: /* Исключение при обращении к памяти. */

    ix = *pNull;

    *pNull = 5;

    break;

   case 2: /* Исключение при выполнении арифметических операций над целыми числами. */

    ix = ix / iy;

    break;

   case 3: /* FP-исключение. */

    x = x / у;

    _tprintf(_T("x = %20e\n"), x);

    break;

   case 4: /* Пользовательское исключение. */

    ReportException(_T("Пользовательское исключение"), 1);

    break;

   case 5: /* Использовать оператор _leave для завершения выполнения.*/

    __leave;

   default:

    Done = TRUE;

   }

  } /* Конец внутреннего блока __try. */

  __except(Filter(GetExceptionInformation(), &ECatgry)) {

   switch(ECatgry) {

   case 0:

    _tprintf(_T("Неизвестное исключение\n"));

    break;

   case 1:

    _tprintf(_T("Исключение при обращении к памяти\n"));

    continue;

   case 2:

    _tprintf(_T("Исключение при выполнении арифметических операций над целыми числами \n"));

    break;

   case 3:

    _tprintf(_Т("FР-исключение\n"));

    _clearfp();

    break;

   case 10:

    _tprintf(_T("Пользовательское исключение\n"));

    break;

   default:

    _tprintf(_T("Неизвестное исключение\n"));

    break;

   } /* Конец оператора switch. */

   _tprintf(_Т("Конец обработчика\n"));

  }

  /* Конец блока try-except. */

 } /* Конец цикла while – ниже находится обработчик завершения. */

 __finally { /* Это часть цикла while. */

  _tprintf(_T("Аварийное завершение?: %d\n"),

  AbnormalTermination());

 }

 _controlfp(FPOld, 0xFFFFFFFF); /* Восстановить старую FP-маску.*/

 return 0;

}

Программа 4.4 представляет функцию фильтра, используемую в программе 4.3. Эта функция просто проверяет и классифицирует различные возможные значения кодов исключений. В программном коде, размещенном на Web-сайте книги, проверяется каждое из возможных значений, в то время как приведенная ниже функция осуществляет проверку лишь тех из них, которые нужны для тестовой программы.

Программа 4.4. Функция Filter

static DWORD Filter(LPEXCEPTION_POINTERS pExP, LPDWORD ECatgry)

/* Классификация исключений и выбор соответствующего действия. */

{

 DWORD ExCode, ReadWrite, VirtAddr;

 ExCode = pExP->ExceptionRecord->ExceptionCode;

 _tprintf(_T("Filter. ExCode:. %x\n"), ExCode);

 if ((0x20000000 & ExCode) != 0) { /* Пользовательское исключение. */

  *ECatgry =10;

  return EXCEPTION_EXECUTE_HANDLER;

 }

 switch (ExCode) {

 case EXCEPTION_ACCESS_VIOLATION:

  ReadWrite = /* Операция чтения или записи? */

   pExP->ExceptionRecord->ExceptionInformation[0];

  VirtAddr = /* Адрес сбоя в виртуальный памяти. */

   pExP->ExceptionRecord->ExceptionInformation [1];

  _tprintf(_T("Нарушение доступа. Чтение/запись: %d. Адрес: %х\n"), ReadWrite, VirtAddr);

  *ECatgry = 1;

  return EXCEPTION_EXECUTE_HANDLER;

 case EXCEPTION_INT_DIVIDE_BY_ZERO:

 case EXCEPTION_INT_OVERFLOW:

  *ECatgry = 2;

  return EXCEPTION_EXECUTE_HANDLER;

 case EXCEPTION_FLT_DIVIDE_BY_ZERO:

 case EXCEPTION_FLT_OVERFLOW:

  _tprintf(_T("FP-исключение — слишком большое значение.\n"));

  *ECatgry = 3;

  _clearfp();

  return (DWORD)EXCEPTION_EXECUTE_HANDLER;

 default:

  *ECatgry = 0;

  return EXCEPTION_CONTINUE_SEARCH;

 }

}

Обработчики управляющих сигналов консоли

Обработчики исключений могут реагировать на самые разнообразные события, но они не в состоянии обнаруживать такие ситуации, как выход пользователя из системы или нажатие комбинации клавиш <Ctrl+C> на клавиатуре с целью прекращения выполнения программы. Для обработки таких событий требуются обработчики управляющих сигналов консоли.

Функция SetConsoleCtrlHandler позволяет одной или нескольким указанным функциям выполняться в ответ на получение сигналов Ctrl-c, Ctrl-break или одного из трех других сигналов, связанных с консолью. Функция GenerateConsoleCtrlEvent, описанная в главе 6, также генерирует эти сигналы, а, кроме того, все эти сигналы могут посылаться другим процессам, совместно использующим ту же консоль. Обработчиками сигналов являются указанные пользователем функции, которые возвращают булевские значения и принимают единственный аргумент типа DWORD, идентифицирующий фактический сигнал.

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

BOOL SetConsoleCtrlHandler(PHANDLER_ROUTINE HandlerRoutine, BOOL Add) 

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

Процедура обработчика возвращает булевское значение и принимает единственный параметр типа DWORD, идентифицирующий фактический сигнал. Использованное в объявлении имя обработчика (HandlerRoutine) является заменителем, и программист может выбирать его по своему усмотрению.

Ниже приводятся дополнительные полезные сведения, касающиеся использования обработчиков управляющих сигналов консоли.

• Если значение параметра HandlerRoutine равно NULL, а параметра Add — TRUE, то сигналы Ctrl-c будут игнорироваться.

• Если при вызове функции SetConsoleMode был задан параметр ENABLE_PROCESSED_INPUT (глава 2), то комбинация <Ctrl+C> будет обрабатываться не как сигнал, а как клавиатурный ввод.

• Процедура обработчика фактически выполняется как независимый поток (см. главу 7) внутри процесса. При этом выполнение основной программы, как показано в следующем примере, не приостанавливается.

• Формирование исключения в обработчике не вызовет исключения в потоки, выполнение которого было прервано, поскольку исключения применяются только к потокам, а не к процессу в целом. Если вы хотите организовать связь с прерванным потоком, используйте переменную, как в следующем примере, или метод синхронизации (глава 8).

Между исключениями и сигналами существует важное отличие. Сигналы применяются к процессу в целом, тогда как исключения — только к потоку, выполняющему код, в котором возникло исключение. 

BOOL HandlerRoutine(DWORD dwCtrlType)

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

1. CTRL_C_EVENT указывает на то, что комбинация <Ctrl+C> должна восприниматься как клавиатурный ввод.

2. CTRL_CLOSE_EVENT указывает на закрытие окна консоли.

3. CTRL_BREAK_EVENT указывает на сигнал Ctrl-break.

4. CTRL_LOGOFF_EVENT указывает на выход пользователя из системы.

5. CTRL_SHUTDOWN_EVENT указывает на завершение работы системы. 

Обработчик сигналов может выполнять операции по "уборке мусора" точно так же, как это делают обработчики исключений и завершения. В случае успешной обработки сигнала обработчик должен вернуть значение TRUE. Если обработчик возвращает значение FALSE, выполняется следующая функция обработчика из числа тех, что указаны в списке. Обработчики сигналов выполняются в порядке, обратном порядку их установки, так что первым будет выполняться самый последний из установленных обработчиков, а системный обработчик будет выполняться самым последним.

Пример: обработчик управляющих сигналов консоли

В программе 4.5 организован бесконечный цикл, в котором каждые 5 секунд вызывается функция Веер, подающая звуковой сигнал. Пользователь может завершить выполнение программы, нажав комбинацию клавиш <Ctrl+C> или закрыв консоль. Процедура обработчика выводит на экран сообщение, выжидает 10 секунд, после чего, казалось бы, выполнение программы должно завершиться с возвратом значения TRUE. Однако в действительности основная программа обнаруживает флаг Exit и останавливает процесс. Это демонстрирует параллельную природу выполнения процедуры обработчика; заметьте, что объем выходной информации обработчика сигналов зависит от временных характеристик сигнала. Обработчики управляющих сигналов консоли будут использоваться также в примерах, приводимых в следующих главах.

Обратите внимание на использование макроса WINAPI; он применяется к пользовательским функциям, передаваемым в качестве аргументов функциям Windows, чтобы гарантировать выполнение соответствующих соглашений о вызовах. Этот макрос определен в заголовочном файле Microsoft С WTYPES.Н.

Программа 4.5. Ctrlc: программа обработки сигналов 

/* Глава 4. Ctrlc.с */

/* Перехватчик событий консоли. */

#include "EvryThng.h"


static BOOL WINAPI Handler(DWORD CtrlEvent); /* См. WTYPES.H. */

volatile static BOOL Exit = FALSE;


int _tmain(int argc, LPTSTR argv[])

 /* Периодическая подача звукового сигнала до поступления сигнала о прекращении выполнения. */

{

 /* Добавить обработчик событий. */

 if (!SetConsoleCtrlHandler(Handler, TRUE)) ReportError(_T("Ошибка при установке обработчика событий."), 1, TRUE);

 while (!Exit) {

  Sleep (5000); /* Подача звукового сигнала каждые 5 секунд. */

  Веер(1000 /* Частота. */, 250 /* Длительность. */);

 }

 _tprintf(_T("Прекращение выполнения программы по требованию.\n"));

 return 0;

}


BOOL WINAPI Handler (DWORD CtrlEvent) {

 Exit = TRUE;

 switch (CntrlEvent) {

 /* Увидите ли вы второе сообщения обработчика, зависит от соотношения временных параметров. */

 case CTRL_C_EVENT:

  _tprintf(_T("Получен сигнал Ctrl-c. Выход из обработчика через 10 секунд.\n"));

  Sleep(4000); /* Уменьшите это значение, чтобы получить другой эффект. */

  _tprintf(_T("Выход из обработчика через 6 секунд.\n"));

  Sleep(6000); /* Попробуйте уменьшить и это значение. */

  return TRUE; /* TRUE указывает на успешную обработку сигнала. */

 case CTRL_CLOSE_EVENT:

  _tprintf(_T("Выход из обработчика через 10 секунд.\n"));

  Sleep(4000);

  _tprintf(_Т ("Выход из обработчика через 6 секунд.\n"));

  Sleep (6000); /* Попробуйте уменьшить и это значение. */

  return TRUE; /* Попробуйте возвратить FALSE. Приводит ли это к изменению поведения программы? */

 default:

  _tprintf(_T("Событие: %d. Выход из обработчика через 10 секунд.\n"), CntrlEvent);

  Sleep(4000);

  _tprintf(_T("Выход из обработчика через 6 секунд.\n"));

  Sleep(6000);

  return TRUE;

 }

} 

Векторная обработка исключений

Функции обработки исключений можно непосредственно связывать с исключениями, точно так же, как обработчики управляющих сигналов консоли можно связывать с управляющими событиями консоли. В этом случае, если возникает исключение, то первыми, еще до того, как система начнет разворачивать стек в поиске структурных обработчиков исключений, будут вызываться векторные обработчики исключений (vectored exception handlers). При этом никакие ключевые слова, аналогичные __try или __catch, не требуются. Такая возможность предоставляется только в Windows XP и Windows Server 2003. 

Работа с векторными обработчиками исключений (Vectored Exception Handling, VEH) напоминает работу с обработчиками управляющих сигналов консоли, хотя детали и отличаются. Для добавления, или регистрации, обработчика служит функция AddVectoredExceptionHandler. 

PVOID AddVectoredExceptionHandler(ULONG FirstHandler, PVECTORED_EXCEPTION_HANDLER VectoredHandler) 

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

Функция RemoveVectoredExceptionHandler, прекращающая регистрацию векторного обработчика исключений, требует задания единственного параметра, адреса обработчика, и в случае успешного выполнения возвращает ненулевое значение.

Функция AddVectoredExceptionHandler в случае успешного выполнения возвращает адрес обработчика исключений, т.е. VectoredHandler. Возвращаемое значение NULL указывает на неудачное завершение выполнения функции.

VectorHandler — это указатель на функцию обработчика, которая имеет следующий прототип: 

LONG WINAPI VectoredHandler(PEXCEPTION_POINTERS ExceptionInfo)

PEXCEPTION_POINTERS — адрес структуры EXCEPTION_POINTERS, которая содержит как информацию, зависящую от типа процессора, так и информацию общего характера. Это та же структура, которую возвращает функция GetExceptionInformation и которая уже использовалась нами в программе 4.4.

От функции VEH-обработчика требуется, чтобы она выполнялась быстро и никогда не получала доступа к объектам синхронизации, таким как мьютекс (см. главу 8). В большинстве случаев VEH-обработчики просто обращаются к структуре исключения, выполняют некоторую минимальную обработку (например, устанавливают флаг) и осуществляют возврат. Возможны два возвращаемых значения, с которыми мы уже встречались при обсуждении SEH-обработчиков.

1. EXCEPTION_CONTINUE_EXECUTION — обработчики далее не выполняются, обработка средствами SEH не производится, и управление передается в ту точку программы, в которой возникло исключение. Как и в случае SEH, это оказывается возможным не всегда.

2. EXCEPTION_CONTINUE_SEARCH — выполняется следующий VEH-обработчик, если таковой имеется. Если обработчиков больше нет, разворачивается стек для поиска SEH-обработчиков.

В упражнении 4.9 вам предлагается добавить VEH в программы 4.3 и 4.4.

Резюме

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

Обработчики управляющих сигналов консоли позволяют реагировать на внешние события, наступление которых не сопровождается генерацией исключений. Векторная обработка исключений является новейшим средством, обеспечивающим выполнение соответствующих функций еще до того, как начнется выполнение SEH-процедур. Механизм VEH аналогичен обычному механизму векторных прерываний.

В следующих главах

Функция ReportException, a также обработчики исключений и завершения будут неоднократно использоваться в последующих примерах, когда в этом возникнет необходимость. Глава 5 посвящена вопросам управления памятью, а в приведенных в ней в качестве примера программах для обнаружения ошибок, которые могут возникать в процессе распределения памяти, используется механизм SEH.

Упражнения

4.1. Расширьте возможности программы 4.2 путем предоставления при каждом вызове функции ReportException достаточно большого объема информации, чтобы обработчик исключений в своих сообщениях указывал точную природу возникающих ошибок и удалял выходные файлы, если их содержимое оказывается незначащим. 

4.2.  Расширьте возможности программы 4.3 за счет генерации таких исключений, связанных с нарушениями доступа к памяти, как выход индекса за пределы допустимого диапазона, а также исключений, обусловленных сбоями при выполнении арифметических операций, и других FP-исключений, не предусмотренных в программе 4.3.

4.3.  Дополните программу 4.3 таким образом, чтобы она выводила на печать фактическое значение FP-маски после разрешения исключений. Все ли исключения оказались действительно разрешенными? Объясните результаты.

4.4.  Какие значения вы в действительности получаете после возникновения таких FP-исключений, как деление на ноль? Можете ли вы установить результат в функции фильтра, как это пытается делать программа 4.3?

4.5. Что произойдет при выполнении программы 4.3, если не сбросить FP-исключение? Объясните результат. Подсказка. Запросите дополнительное исключение после возникновения FP-исключения.

4.6. Расширьте возможности программы 4.5 таким образом, чтобы процедура обработчика формировала исключение, а не возврат из функции. Объясните полученные результаты.

4.7. Расширьте возможности программы 4.5 таким образом, чтобы она могла обрабатывать сигналы, указывающие на выход пользователя из системы и завершение работы системы.

4.8. Экспериментальным путем убедитесь в том, что процедура обработчика в программе 4.5 выполняется параллельно с основной программой.

4.9. Усовершенствуйте программы 4.3 и 4.4. В частности, организуйте обработку арифметических и FP-исключений до активизации SEH.

ГЛАВА 5

Управление памятью, отображение файлов и библиотеки DLL

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

В Windows предусмотрены гибкие механизмы управления динамической памятью программы. Кроме того, Windows предоставляет средства отображения файлов, которые позволяют ассоциировать файл непосредственно с виртуальным адресным пространством процесса, благодаря чему ОС может управлять любыми перемещениями данных между файлом и памятью, так что программисту вообще не приходится иметь дело с функциями ReadFile, WriteFile, SetFilePointer и другими функциями ввода/вывода.

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

Важным специальным случаем отображения файлов и разделения памяти являются динамически компонуемые библиотеки (dynamic linked libraries, DLL), обеспечивающие возможность отображения файлов (обычно, когда они используются только для чтения) на адресное пространство процесса для их выполнения.

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

Архитектура системы управления памятью в Win32 и Win64

Win32 (в данном случае различия между Win32 и Win64 становятся существенными) — это API 32-разрядных ОС семейства Windows. "32-разрядность" проявляет себя при адресации памяти тем, что указатели (LPSTR, LPDWORD и так далее) являются 4-байтовыми (32-битовыми) объектами. Win64 API предоставляет виртуальное адресное пространство гораздо большего объема, и 64-битовые указатели являются естественным результатом эволюции Win32. Тем не менее, о переносимости приложений на платформу Win64 необходимо заботиться отдельно. Настоящее обсуждение будет относиться только к Win32; вопросы миграции приложений на платформу Win64 обсуждаются в главе 16, где также приводятся ссылки на соответствующие источники информации.

Далее, в рамках Win32 у каждого процесса имеется собственное виртуальное адресное пространство объемом 4 Гбайт (232 байт). Разумеется, объем виртуального адресного пространства в Win64 гораздо больше. По крайней мере, половину указанного пространства (2-3 Гбайт; расширение до 3 Гбайт должно производиться во время загрузки) Win32 делает доступной для процесса. Оставшаяся часть виртуального адресного пространства выделяется для совместно используемых данных и кода, системного кода, драйверов и так далее.

Хотя детали описанного распределения памяти и заслуживают интереса, здесь они обсуждаться не будут; прикладные программы используют абстрактные модели памяти, предоставляемые API. С точки зрения программиста ОС просто предоставляет адресное пространство большого объема для размещения кода, данных и других ресурсов. В этой главе мы сосредоточим свое внимание на использовании средств управления памятью в Windows, не заботясь о том, как все это реализуется в ОС. Тем не менее, ниже приводится соответствующий краткий обзор.

Обзор методов управления памятью

Обо всех деталях отображения виртуальных адресов на физические адреса (virtual to physical memory mapping), механизмах страничной подкачки (page swapping) и замещения страниц по запросу (demand paging) и прочих моментах заботится ОС. Эти вопросы подробно обсуждаются в документации по ОС, а также в книге Соломона (Solomon) и Руссиновича (Russinovich) Inside Windows2000. Краткое изложение наиболее существенных сведений приводится ниже:

• Система может располагать сравнительно небольшим объемом физической памяти; на практике для всех систем, кроме Windows XP, необходимый минимум составляет 128 Мбайт, однако в типичных случаях доступные объемы физической памяти оказываются намного большими.[21]

• Каждый отдельный процесс — а таких процессов, как пользовательских, так и системных, может выполняться одновременно несколько — имеет собственное виртуальное адресное пространство, объем которого может значительно превосходить объем доступного физического адресного пространства. Например, емкость виртуального адресного пространства объемом 1 Гбайт, относящегося к одному процессу, в восемь раз превышает емкость физической памяти объемом 128 Мбайт, и таких процессов может быть множество.

• ОС преобразует виртуальные адреса в физические адреса.

• Для большинства виртуальных страниц в физической памяти места не хватит, поэтому ОС имеет возможность реагировать на страничные ошибки (page faults), возникающие при попытках обращения к страницам, которые отсутствуют в памяти, и загружать данные с жесткого диска — из системного файла подкачки (swap file) или из обычного файла. Будучи прозрачными для программиста, страничные ошибки отрицательно влияют на производительность, поэтому программы должны проектироваться таким образом, чтобы вероятность возникновения подобных ошибок была сведена к минимуму. Более подробное освещение этой темы, рассмотрение которой выходит за рамки данной книги, вы найдете в справочной документации по ОС.

На рис. 5.1 проиллюстрировано расположение уровней API управления памятью Windows поверх диспетчера виртуальной памяти (Virtual Memory Manager, VMM). API виртуальной памяти Windows (VirtualAlloc, VirtualFree, Virtual-Lock, VirtualUnlock и так далее) работает с целыми страницами. API кучи Windows управляет блоками памяти, размер которых определяется пользователем.

Мы не будем останавливаться на топологии адресного пространства виртуальной памяти, поскольку она не имеет непосредственного отношения к API, различна в Windows 9x и Windows NT и в будущем может измениться. Соответствующая информация содержится в документации Microsoft.

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

VOID GetSystemInfo(LPSYSTEM_INFO lpSystemInfo) 

Параметром этой функции служит адрес структуры PSYSTEM_INFO, в которой содержится информация относительно размера системной страницы, а также адресах физической памяти, доступных для приложений.

Куча

Windows поддерживает пулы памяти, называемые кучами (heaps). Процесс может иметь несколько куч, которые используются для распределения памяти. 

Системное программирование в среде Windows

Рис. 5.1. Архитектура системы управления памятью Windows


Во многих случаях одной кучи вполне достаточно, но в силу ряда причин, о которых будет сказано ниже, иногда целесообразно иметь в своем распоряжении несколько куч. Если одной кучи вам хватает, можно обойтись использованием функций управления памятью, предоставляемых библиотекой С (malloc, free, calloc, realloc).

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

HANDLE GetProcessHeap(VOID)

Возвращаемое значение: дескриптор кучи процесса; в случае неуспешного завершения — NULL. 

Заметьте, что для индикации неудачного завершения функции используется возвращаемое значение NULL, а не INVALID_HANDLE_VALUE, как в случае функции CreateFile. 

Программа также может создать несколько различных куч. Иногда для размещения в памяти отдельных структур данных оказывается удобным, чтобы для каждой из них была предусмотрена своя куча. Использование независимых куч обеспечивает ряд преимуществ.

• Отсутствие взаимной дискриминации между потоками. Ни один из потоков не сможет получить больше памяти, чем распределено для ее кучи. В частности, так называемая утечка памяти (memory leak), возникающая в тех случаях, когда программа "забывает" своевременно освободить память, занятую элементами данных, необходимости в которых больше нет, будет влиять лишь на один поток процесса.[22]

• Повышение производительности. Предоставление собственной кучи каждого потока уменьшает состязательность между ними, в результате чего общая производительность программы может значительно повыситься. См. главу 9.

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

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

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

Ценность указанных преимуществ может варьироваться в зависимости от приложения, и многие программисты ограничиваются использованием только кучи процесса, для управления которой используют функции библиотеки С. Однако такой выбор лишает программу возможности воспользоваться способностью функций управления памятью Windows генерировать исключения (обсуждается при рассмотрении функций). В любом случае для создания и уничтожения куч применяются две функции, описания которых приводятся ниже.[23] 

Начальный размер кучи, устанавливаемый параметром dwInitialSize (который может быть нулевым), всегда округляется до величины, кратной размеру страницы, и определяет объем физической памяти (в файле подкачки), который передается (commit) в распоряжение кучи (для последующего распределения памяти по запросам) первоначально, а не в ответ на запросы распределения (allocation) памяти из кучи. Когда программа исчерпывает первоначальный размер кучи, куче автоматически передаются дополнительные страницы памяти вплоть до пор, пока она не достигнет установленного для нее максимального размера. Поскольку файл подкачки является ограниченным ресурсом, рекомендуется откладывать передачу памяти куче на более поздний срок, если только заранее не известно, какой размер кучи потребуется. Максимально допустимый размер кучи при ее увеличении в результате динамического расширения определяется значением параметра dwMaximumSize (если оно ненулевое). Рост куч процессов, заданных по умолчанию, также осуществляется динамическим путем. 

HANDLE HeapCreate(DWORD flOptions, SIZE_T dwInitialSize, SIZE_T dwMaximumSize) 

Возвращаемое значение: дескриптор кучи; в случае неудачного завершения — NULL.

Типом данных обоих упомянутых полей, связанных с размерами кучи, является не DWORD, a SIZE_T. Тип данных SIZE_T определяется как 32– или 64-битовое целое число без знака, в зависимости от флагов компилятора (_WIN32 или _WIN64). Этот тип данных был введен специально для того, чтобы обеспечить возможность миграции приложений Win64 (см. главу 16), и охватывает весь диапазон 32– и 64-битовых указателей. Вариантом этого типа данных для чисел со знаком является тип SSIZE_T).

flOptions — этот параметр может объединять следующие два флага:

• HEAP_GENERATE_EXCEPTIONS: в случае ошибки при распределении памяти вместо возврата значения NULL генерируется исключение, которое должно быть обработано средствами SEH (см. главу 4). Если установлен этот флаг, то такие исключения при сбоях будет возбуждаться не самой функцией HeapCreate, а такими функциями, как HeapAlloc, к рассмотрению которых мы вскоре перейдем.

• HEAP_NO SERIALIZE: при определенных обстоятельствах, о которых сказано ниже, установка этого флага может привести к незначительному повышению производительности.

Существуют другие важные моменты, связанные с параметром dwMaximumSize.

• Если параметр dwMaximumSize имеет ненулевое значение, то виртуальное адресное пространство резервируется в соответствии с этим значением, даже если первоначально не все оно передается в распоряжение кучи. Это значение определяет максимальный размер кучи, о котором в этом случае говорят как о нерастущем (nongrowable). Данный параметр ограничивает размер кучи, чтобы, например, обеспечить отсутствие дискриминации между потоками, о чем говорилось выше.

• Если же значение dwMaximumSize равно 0, то куча может расти (grow), превышая предел, установленный начальным размером, и в этом случае максимальный размер кучи ограничивается лишь объемом доступного виртуального адресного пространства, не распределенного в данный момент для других куч и файла подкачки.

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

Для уничтожения объекта кучи используется функция HeapDestroy. Она также может служить примером исключения из общих правил, в данном случае — правила, согласно которому для удаления ненужных дескрипторов любого типа используется функция CloseHandle. 

BOOL HeapDestroy(HANDLE hHeap) 

Параметр hHeap должен указывать на кучу, созданную посредством вызова функции HeapCreate. Будьте внимательны и следите за тем, чтобы случайно не уничтожить кучу процесса, заданную по умолчанию (дескриптор которой получают с помощью функции GetProcessHeap). В результате уничтожения кучи освобождается область виртуального адресного пространства и физическая область сохранения файла подкачки. Разумеется, правильно спроектированная программа должна уничтожать кучи, необходимости в которых больше нет.

Помимо всего прочего, уничтожение кучи позволяет быстро освободить память, занимаемую структурами данных, избавляя вас от необходимости отдельного уничтожения каждой из структур, однако экземпляры объектов C++ уничтожены не будут, поскольку их деструкторы при этом не вызываются. Применение операции уничтожения кучи имеет следующие положительные стороны:

1. Отпадает необходимость в написании программного кода, обеспечивающего обход структур данных.

2. Отпадает необходимость в освобождении памяти, занимаемой каждым из элементов, по отдельности.

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

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

В UNIX адресное пространство процесса может быть увеличено с помощью функции sbrk, однако эта функция не является диспетчером памяти общего назначения.

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

Управление памятью кучи

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

LPVOID НеарАllос(HANDLE hHeap, DWORD dwFlags, SIZE_T dwBytes)

Возвращаемое значение: в случае успешного выполнения — указатель на распределенный блок памяти, иначе — NULL (если только не была указана генерация исключения). 

Параметры

hHeap — дескриптор кучи, из которой должен быть распределен блок памяти. Этот дескриптор должен быть предоставлен либо функцией GetProcessHeap, либо функцией HeapCreate.

dwFlags — может объединять следующие флаги:

• HEAP_GENERATE_EXCEPTIONS и HEAP_NO_SERIALIZE: эти флаги имеют тот же смысл, что и в случае функции HeapCreate. Первый флаг игнорируется, если он был установлен функцией кучи HeapCreate, но активизирует исключения для каждого отдельного вызова функции НеарАllос, даже если функцией HeapCreate флаг HEAP_GENERATE_EXCEPTIONS и не был задан. При распределении памяти из кучи процесса второй флаг использовать не следует.

• HEAP_ZERO_MEMORY: этот флаг указывает, что распределенная память будет инициализирована значениями 0; если этот флаг не установлен, содержимое памяти является неопределенным.

dwBytes — размер блока памяти, который должен быть распределен. Для нерастущих куч значение этого параметра не должно превышать 0x7FFF8 (приблизительно 0,5 Мбайт). 

Примечание

Как только функция HeapAlloc вернула указатель, вы можете использовать его самым обычным способом; ссылаться после этого на его кучу нет никакой необходимости. Заметьте, что тип данных LPVOID может представлять либо 32-битовый, либо 64-битовый указатель.

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

BOOL HeapFree(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem) 

dwFlags — значениями этого параметра должны быть 0 или HEAP_NO_SERIALIZE. Значением параметра lpMem должно быть значение, возвращенное функциями HeapAlloc или HeapReAlloc (описана ниже), а дескриптор hHeap должен быть дескриптором кучи, которой принадлежит освобождаемый блок памяти, указываемый lpMem.

Для повторного распределения блоков памяти с целью изменения их размера используется следующая функция: 

LPVOID HeapReAlloc(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem, SIZE_T dwBytes)

Возвращаемое значение: в случае успешного выполнения — указатель на перераспределенный блок памяти; в противном случае функция возвращает NULL или вызывает исключение. 

Параметры

• HEAP_GENERATE_EXCEPTIONS и HEAP_NO_SERIALIZE: это те же флаги, которые были описаны при рассмотрении функции HeapAlloc.

• HEAP_ZERO_MEMORY: нулями инициализируется лишь вновь распределенная память (когда значение параметра dwBytes превышает первоначальный размер блока). Содержимое исходного блока не изменяется.

• HEAP_REALLOC_IN_PLACE_ONLY: установка этого флага запрещает перемещение блока при перераспределении памяти. Если вы увеличиваете размер блока, адреса добавляемой памяти будут располагаться непосредственно вслед за адресами памяти, занимаемой существующим блоком.

lpMem — указывает на блок памяти, перераспределяемый из кучи hHeap. 

dwBytes — размер нового блока памяти, который может быть как меньше, так и больше размера существующего блока.

Обычно возвращенный указатель имеет то же значение, что и указатель lpMem. В то же время, если блок перемещается (чтобы такое перемещение было разрешено, следует при вызове функции опустить флаг HEAP_REALLOC_IN_PLACE_ONLY), то возвращенное значение будет другим. Следите за своевременным изменением любых ссылок на блок. Независимо от того, перемещается блок или не перемещается, содержащиеся в нем данные остаются неизменными; в то же время, при уменьшении блока часть данных может теряться.

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

DWORD HeapSize(HANDLE hHeap, DWORD dwFlags, LPCVOID lpMem)

Возвращаемое значение: в случае успешного выполнения — размер блока; иначе — ноль.

Флаг HEAP_NO_SERIALIZE

При вызове функций HeapCreate, HeapAlloc и HeapReAlloc можно указывать флаг HEAP_NO_SERIALIZE. Использование этого флага иногда обеспечивает незначительный выигрыш в производительности, поскольку во время обращения функции к куче взаимоисключающая блокировка к потокам в этом случае применяться не будет. Результаты простых тестов, в которых не делалось ничего, кроме распределения блоков памяти, показали повышение производительности примерно на 16 процентов. Этот флаг без какого бы то ни было риска можно использовать в следующих ситуациях:

• Программа не использует потоки (глава 7), или, точнее, процесс (глава 6) имеет только один поток. В данной главе этот флаг используется во всех примерах.

• Каждый поток имеет собственную кучу или набор куч, и никакой другой поток не имеет доступа к этой куче.

• Программа располагает собственным механизмом взаимоисключающей блокировки, который предотвращает одновременный доступ к куче сразу нескольких потоков, использующих функции HeapAlloc и HeapAlloc. Для этой цели также могут применяться функции HeapLock и HeapUnlock. 

Флаг HEAP_GENERATE_EXCEPTIONS

Разрешение исключений вместо возврата значений NULL в случае сбоев при распределении памяти позволяет избавиться от утомительной необходимости тестирования результатов каждой попытки такого распределения. К тому же, обработчики исключений или завершения могут производить очистку памяти, которая к этому моменту была частично распределена. Эта методика применена в нескольких примерах.

Возможны два кода исключения:

1. STATUS_NO_MEMORY: это значение указывает на то, что системе не удалось создать блок запрошенного объема. Причинами этого могут быть фрагментация памяти, достижение нерастущей кучей максимально допустимого размера или исчерпание всей доступной памяти растущими кучами.

2. STATUS_ACCESS_VIOLATION: это значение указывает на повреждение кучи.

Одной из возможных причин этого может быть выполнение программой записи в память с выходом за границы распределенного блока.

Другие функции кучи

Функция HeapCompact пытается уплотнить, или дефрагментировать, смежные блоки в куче. Функция HeapValidate пытается обнаруживать повреждения кучи. Функция HeapWalk перечисляет блоки в куче, а функция GetProcessHeaps получает все действительные дескрипторы куч.

Функции HeapLock и HeapUnlock позволяют потоки сериализовать доступ к куче, о чем говорится в главе 8.

Имейте в виду, что эти функции не работают под управлением Windows 9x или Windows СЕ. Кроме того, имеются некоторые вышедшие из употребления функции, которые использовались ранее для совместимости с 16-битовыми системами. Мы упомянули об этих функциях лишь для того, чтобы лишний раз подчеркнуть тот факт, что многие функции продолжают поддерживаться, хотя никакой необходимости в них больше нет.

Резюме: управление кучами

Обычная процедура использования куч не представляет никаких сложностей:

1. Получите дескриптор кучи, воспользовавшись одной из функций CreateНеар или GetProcessHeap.

2. Распределите блоки из кучи, используя функцию HeapAlloc.

3. В случае необходимости освободите все или только некоторые блоки при помощи функции HeapFree.

4. Уничтожьте кучу и закройте ее дескриптор при помощи функции HeapDestroy.

Этот процесс иллюстрируют рис. 5.2 и программа 5.2. 

В отсутствие необходимости создания отдельных куч или генерации исключений программисты, которые привыкли использовать функции управления памятью из библиотеки С, могут использовать их и далее. При этом, если речь идет о куче процесса, функция malloc эквивалентна функции HeapAlloc, функция realloc — функции HeapReAlloc, а функция free — функции HeapFree. Функция calloc распределяет память и инициализирует объекты, и ее поведение легко эмулируется функцией HeapAlloc. Эквивалент функции HeapSize в библиотеке С отсутствует.

Пример: сортировка файлов с использованием бинарного дерева поиска

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

Программа sortBT (программа 5.1) реализует ограниченную версию UNIX-команды sort за счет создания бинарного дерева поиска с использованием двух куч. Ключи размещаются в куче узлов (node heap), представляющей дерево поиска. Каждый узел содержит левый и правый указатели, ключ и указатель на запись в куче данных (data heap). Заметьте, что куча узлов состоит из блоков фиксированного размера, тогда как куча данных содержит строки переменной длины. Наконец, отсортированный файл выводится путем обхода дерева.

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

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

Примечание

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

Системное программирование в среде Windows
Системное программирование в среде Windows

Рис. 5.2. Управление памятью при наличии нескольких куч


Программа 5.1 иллюстрирует некоторые методики, которые упрощают программу, но были бы невозможны при использовании одной только библиотеки С или же только кучи процесса.

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

• Готовясь к сортировке очередного файла, программа уничтожает две кучи, а не освобождает память, занимаемую отдельными элементами.

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

Если используется Windows, то сфера применимости таких программ, как программа 5.1, ограничивается файлами небольшого размера, поскольку в виртуальной памяти должны находиться целиком весь файл и копии ключей. Абсолютный верхний предел размера файла определяется объемом доступного виртуального адресного пространства (максимум 3 Гбайт); фактически достижимый предел оказывается еще меньшим. В случае Win64 ограничения подобного рода практически отсутствуют.

В программе 5.1 вызываются некоторые функции управления деревом: FillTree, InsertTree, Scan и TreeCompare. Все они представлены в программе 5.2.

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

Программа 5.1. sortBT: сортировка с использованием бинарного дерева поиска 

/* Глава 5. Команда sortBT. Версия, использующая бинарное дерево поиска.*/

#include "EvryThng.h"

#define KEY_SIZE 8


typedef struct _TreeNode {/* Описание структуры узла. */

 struct _TreeNode *Left, *Right;

 TCHAR Key[KEY_SIZE];

 LPTSTR pData;

} TREENODE, *LPTNODE, **LPPTNODE;

#define NODE_SIZE sizeof(TREENODE)

#define NODE_HEAP_ISIZE 0x8000

#define DATA_HEAP_ISIZE 0x8000

#define MAX_DATA_LEN 0x1000

#define TKEY_SIZE KEY_SIZE * sizeof(TCHAR)


LPTNODE FillTree(HANDLE, HANDLE, HANDLE);

BOOL Scan(LPTNODE);

int KeyCompare (LPCTSTR, LPCTSTR); iFile;


BOOL InsertTree (LPPTNODE, LPTNODE);


int _tmain(int argc, LPTSTR argv[]) {

 HANDLE hIn, hNode = NULL, hData = NULL;

 LPTNODE pRoot;

 CHAR ErrorMessage[256];

 int iFirstFile = Options(argc, argv, _T("n"), &NoPrint, NULL);

 /* Обработать все файлы, указанные в командной строке. */

 for (iFile = iFirstFile; iFile < argc; iFile++) __try {

  /* Открыть входной файл. */

  hIn = CreateFile(argv[iFile], GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);

  if (hIn == INVALID_HANDLE_VALUE) RaiseException(0, 0, 0, NULL);

  __try { /* Распределить две кучи. */

   hNode = HeapCreate(HEAP_GENERATE_EXCEPTIONS | HEAP_NO_SERIALIZE, NODE_HEAP_ISIZE, 0);

   hData = HeapCreate(HEAP_GENERATE_EXCEPTIONS | HEAP_NO_SERIALIZE, DATA_HEAP_ISIZE, 0); 

   /* Обработать входной файл, создавая дерево. */

   pRoot = FillTree(hIn, hNode, hData);

   /* Отобразить дерево в порядке следования ключей. */

   _tprintf(_T("Сортируемый файл: %s\n"), argv [iFile]);

   Scan(pRoot);

  } _ finally { /* Кучи и дескрипторы файлов всегда закрываются.

   /* Уничтожить обе кучи и структуры данных. */

   if (hNode !=NULL) HeapDestroy (hNode);

   if (hNode != NULL) HeapDestroy (hData);

   hNode = NULL;

   hData = NULL;

   if (hIn != INVALID_HANDLE_VALUE) CloseHandle (hIn);

  }

 } /* Конец основного цикла обработки файлов и try-блока. */

 __except(EXCEPTION_EXECUTE_HANDLER) {

  _stprintf(ErrorMessage, _T("\n%s %s"), _T("sortBT, ошибка при обработке файла:"), argv [iFile]);

  ReportError(ErrorMessage, 0, TRUE);

 }

 return 0;

}

В программе 5.2 представлены функции, которые фактически реализуют алгоритмы поиска с использованием бинарного дерева. Первая из этих функций, FillTree, распределяет память в обеих кучах. Вторая функция, KeyCompare, используется также в нескольких других программах в данной главе. Заметьте, что функции FillTree и KeyCompare используют обработчики завершения и исключений программы 5.1, которая вызывает эти функции. Таким образом, ошибки распределения памяти будут обрабатываться основной программой, которая после этого продолжит свое выполнение, переходя к обработке следующего файла.

Программа 5.2. FillTree и другие функции управления деревом поиска 

LPTNODE FillTree(HANDLE hIn, HANDLE hNode, HANDLE hData)

/* Заполнение дерева записями из входного файла. Используется обработчик исключений вызывающей программы. */

{

 LPTNODE pRoot = NULL, pNode;

 DWORD nRead, i;

 BOOL AtCR;

 TCHAR DataHold [MAX_DATA_LEN] ;

 LPTSTR pString;

 while (TRUE) {

  /* Разместить и инициализировать новый узел дерева. */

  pNode = HeapAlloc(hNode, HEAP_ZERO_MEMORY, NODE_SIZE);

  /* Считать ключ из следующей записи файла. */

  if (!ReadFile(hIn, pNode->Key, TKEY_SIZE, &nRead, NULL) || nRead != TKEY_SIZE) return pRoot; 

  AtCR = FALSE; /* Считать данные до конца строки. */

  for (i = 0; i < MAX_DATA_LEN; i++) {

   ReadFile(hIn, &DataHold [i], TSIZE, &nRead, NULL);

   if (AtCR && DataHold [i] == LF) break;

   AtCR = (DataHold [i] == CR);

  }

  DataHold[i – 1] = '\0';

  /* Объединить ключ и данные — вставить в дерево. */

  pString = HeapAlloc(hData, HEAP_ZERO_MEMORY, (SIZE_T)(KEY_SIZE + _tcslen (DataHold) + 1) * TSIZE);

  memcpy(pString, pNode->Key, TKEY_SIZE);

  pString [KEY_SIZE] = '\0';

  _tcscat (pString, DataHold);

  pNode->pData = pString;

  InsertTree(&pRoot, pNode);

 } /* Конец цикла while (TRUE). */

 return NULL; /* Ошибка */

}


BOOL InsertTree(LPPTNODE ppRoot, LPTNODE pNode)

/* Добавить в дерево одиночный узел, содержащий данные. */

{

 if (*ppRoot == NULL) {

  *ppRoot = pNode;

  return TRUE;

 }

 /* Обратите внимание на рекурсивные вызовы InsertTree. */

 if (KeyCompare(pNode->Key, (*ppRoot)->Key) < 0) InsertTree(&((*ppRoot)->Left), pNode);

 else InsertTree(&((*ppRoot)->Right), pNode);

}


static int KeyCompare(LPCTSTR pKey1, LPCTSTR pKey2)

/* Сравнить две записи, состоящие из обобщенных символов. */

{

 return _tcsncmp(pKey1, pKey2, KEY_SIZE);

}


static BOOL Scan(LPTNODE pNode)

/* Рекурсивный просмотр и отображение содержимого бинарного дерева. */

{

 if (pNode == NULL) return TRUE;

 Scan(pNode->Left);

 _tprintf(_T ("%s\n"), pNode->pData);

 Scan(pNode->Right);

 return TRUE;

} 

Примечание

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

Отображение файлов

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

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

• Отпадает необходимость в выполнении операций непосредственного файлового ввода/вывода (чтения и записи).

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

• Становится возможным применение удобных и эффективных алгоритмов, ориентированных на работу с файлами "в памяти" (in-memory files) (сортировка, деревья поиска, обработка строк и тому подобное), которые позволяют обрабатывать хранящиеся в файлах данные даже в тех случаях, когда размеры файлов значительно превышают доступный объем физической памяти. При больших размерах файлов особенности организации страничного обмена могут оказывать заметное влияние на производительность.

• В некоторых случаях значительно повышается эффективность обработки файлов.

• Исчезает необходимость в управлении буферами и манипулировании содержащимися в них данными файлов. Всю эту тяжелую работу выполняет ОС, причем делает она это в высшей степени эффективно и надежно.

• Обеспечивается возможность разделения памяти несколькими параллельно выполняющимися процессами (глава 6) за счет отображения на их виртуальные адресные пространства одного и того же обычного файла или файла подкачки (разделение памяти несколькими процессами является одной из основных причин использования объекта отображения файла подкачки).

• Отпадает необходимость в расходовании излишнего пространства файла подкачки. 

ОС сама использует методы отображения файлов для реализации DLL, а также для загрузки и выполнения исполняемых (.EXE) файлов. Библиотеки DLL описаны в конце настоящей главы.

Объекты отображения файлов

Сначала необходимо создать для открытого файла объект отображения файла (file mapping object), у которого имеется дескриптор, а затем отобразить этот файл или только некоторую его часть на виртуальное адресное пространство процесса. Объектам отображения можно присваивать имена, по которым к ним смогут обращаться другие процессы, разделяющие память совместно с данным процессом. Кроме того, объекты отображения файлов имеют параметры размера и атрибуты защиты. 

HANDLE CreateFileMapping(HANDLE hFile, LPSECURITY_ATTRIBUTES lpsa, DWORD dwProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpMapName)

Возвращаемое значение: в случае успешного выполнения — дескриптор объекта отображения файла, иначе — NULL. 

Параметры

hFile — дескриптор открытого файла, атрибуты защиты которого совместимы с флагами защиты, указанными параметром dwProtect. Значение этого дескриптора (тип данных HANDLE), равное 0xFFFFFFFF (его эквивалент — символическая константа INVALID_HANDLE_VALUE), соответствует системному файлу подкачки, и его можно использовать для организации разделения памяти несколькими процессами без создания отдельного файла.

LPSECURITY_ATTRIBUTES — позволяет указать атрибуты защиты объекта отображения.

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

• PAGE_READONLY: страницы в указанной области отображения доступны программе только для чтения; программа не может осуществлять в них запись или запускать на выполнение. Файл с дескриптором hFile должен быть открыт с правами доступа GENERIC_READ.

• PAGE_READWRITE: предоставляет полный доступ к объекту, если файл с дескриптором hFile был открыт с правами доступа GENERIC_READ и GENERIC_WRITE.

• PAGE_WRITECOPY: при изменении отображения файла его приватная (для данного процесса) копия записывается в файл подкачки, а не в исходный файл. Отладчики могут использовать этот флаг для установки точек прерывания в разделяемом коде.

dwMaximumSizeHigh и dwMaximumSizeLow — соответственно, старшая и младшая 32-битовые части значения максимального размера объекта отображения файла. Если оба эти параметры равны 0, используется текущий размер файла; в случае работы с файлом подкачки указание размера является обязательным. Если предполагается, что впоследствии файл может увеличиться, укажите его предполагаемый конечный размер, и, если это необходимо, этот размер будет сразу же установлен для файла. Не пытайтесь отображать область файла, лежащую за пределами указанного размера, поскольку размер объекта отображения расти не может.

lpMapName — указатель на строку, содержащую имя объекта отображения, которое другие процессы могут использовать для разделения объекта; имя объекта чувствительно к регистру. Если не предполагается разделение памяти, используйте для этого параметра значение NULL.

На возникновение ошибок указывает возвращение функцией значения NULL (а не INVALID_HANDLE_VALUE).

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

HANDLE OpenFileMapping(DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpMapName)

Возвращаемое значение: в случае успешного выполнения — дескриптор объекта отображения файла, иначе — NULL. 

Параметр dwDesiredAccess использует тот же набор флагов, что и параметр dwProtect в функции CreateFileMapping. Указатель lpMapName должен указывать на строку с именем, совпадающим с тем, которое было задано при вызове функции CreateFileMapping. Дескриптор наследования (bInheritTable) рассматривается в главе 6.

Как несложно догадаться, для закрытия дескрипторов объектов отображения используется функция CloseHandle.

Отображение файла на адресное пространство процесса

Следующим шагом является распределение виртуального адресного пространства и отображение на него файла с использованием объекта отображения. С точки зрения программиста этот процесс распределения памяти аналогичен тому, который обсуждался при рассмотрении функции HeapAlloc, хотя и делает это намного грубее, оперируя более крупными блоками. В результате этого распределения возвращается указатель на распределенный блок, или представление файла (file view); различие состоит в том, что этот распределенный блок является отображением пользовательского файла, а не файла подкачки. Объект отображения файла играет ту же роль, что и куча в случае использования функции HeapAlloc.

LPVOID MapViewOfFile(HANDLE hMapObject, DWORD dwAccess, DWORD dwOffsetHigh, DWORD dwOffsetLow, SIZE_T cbMap)

Возвращаемое значение: В случае успешного выполнения — начальный адрес блока (представления файла), иначе — NULL. 

Параметры

hMapObject — дескриптор объекта отображения файла, возвращенный функцией CreateFileMapping или OpenFileMapping.

dwAccess — этот параметр должен быть совместимым с разрешенными типами доступа к объекту отображения. Тремя возможными флаговыми значениями являются FILE_MAP_WRITE, FILE_MAP_READ и FILE_MAP_ALL_ACCESS. (Последний флаг является результатом применения поразрядной операции "или" к двум предыдущим флагам).

dwOffsetHigh и dwOffsetLow — соответственно, старшая и младшая 32-битовые части смещения начала отображаемого участка в файле. Значение этого начального адреса должно быть кратным 64 Кбайт. Чтобы начало отображаемого участка совпадало с началом файла, оба параметра следует задать равными 0. 

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

Функция MapViewOfFileEx аналогична функции MapViewOfFile, но дополнительно позволяет указать при вызове начальный адрес памяти для отображенного представления. Например, в качестве такого адреса может быть указан адрес массива в пространстве данных программы. В Windows, если затребованная область памяти уже используется для отображения, выполнение этой функции завершится с ошибкой.

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

BOOL UnmapViewOfFile(LPVOID lpBaseAddress) 

Взаимосвязь между адресным пространством процесса и отображаемым файлом проиллюстрирована на рис. 5.3. 

Системное программирование в среде Windows

Рис. 5.З. Отображение представления файла на адресное пространство процесса


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

В силу этого идея получения доступа к отображаемому файлу с помощью функций ReadFile и WriteFile не сулит ничего хорошего, поскольку согласованность данных при этом не гарантируется. С другой стороны, представления файла для процессов, получающих совместный доступ к нему через разделяемую память, будут согласованными. Если один процесс изменяет какой-либо участок памяти в области отображения файла, то другой процесс при получении доступа к соответствующему участку в своей области отображения файла получит измененные данные. Этот механизм проиллюстрирован на рис. 5.4, из которого следует, что согласованность отображенных представлений файла в двух процессах (РА и РВ) действительно обеспечивается, поскольку виртуальным адресам данных в обоих процессах, несмотря на то, что эти адреса различны, соответствуют одни и те же участки физической памяти; Естественным образом связанная с этим тема синхронизации процессов обсуждается в главах 8—10.[24]


Системное программирование в среде Windows

Рис. 5.4. Разделяемая память


В UNIX (выпуски SVR4 и 4.3+BSD) поддерживается функция mmap, аналогичная функции MapViewOfFile. В ее параметрах указывается та же информация, за исключением того, что объект отображения отсутствует.

Эквивалентом функции UnMapViewOfFile является функция munmap.

Для функций CreateFileMapping и OpenFileMapping эквиваленты отсутствуют. Любой обычный файл может непосредственно отображаться. В UNIX отображение файлов для разделения памяти не используется, и для этих целей предусмотрены специальные функции API, а именно, shmctl, shmat и shmdt.

Ограничения метода отображения файлов

Как уже отмечалось ранее, отображение файлов является весьма мощным и полезным средством. Существующая в Windows диспропорция между 64-битовой файловой системой и 32-битовой адресацией снижает ценность обеспечиваемых этим средством преимуществ; Win64 свободен от этих ограничений.

Основная проблема заключается в том, что при больших размерах файлов (в данном случае, свыше 2—3 Гбайт) отображение всего файла на пространство виртуальной памяти становится невозможным. Более того, не будут доступны даже все 3 Гбайт, поскольку часть виртуального адресного пространства распределяется для других целей, а суммарный объем доступных смежных блоков будет гораздо меньше теоретического максимума. Win64 в значительной степени снимает это ограничение.

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

Двумя другими существенными недостатками метода отображения файлов являются следующие:

• Размер объекта отображения не может увеличиваться. Создавая объект отображения, вы должны заранее определить, какой максимальный размер вам может понадобиться, но во многих случаях сделать это трудно или вообще невозможно.

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

Резюме: отображение файлов

Ниже приведена стандартная последовательность действий, которые необходимо выполнять, если используется отображение файлов:

1. Откройте файл. Убедитесь в том, что он имеет права доступа GENERIC_READ.

2. В случае создания нового файла укажите его размер, используя для этого либо функцию CreateFileMapping (шаг 3), либо функцию SetFilePointer с последующим вызовом функции SetEndOfFile.

3. Отобразите файл при помощи функций CreateFileMapping или OpenFileMapping.

4. Создайте одно или несколько представлений объекта отображения файла с помощью функции MapViewOfFile.

5. Осуществляйте доступ к файлу через обращения к памяти. Для перехода к другим участкам отображаемого файла используйте функции UnmapViewOfFile и MapViewOfFile.

6. Завершив работу, вызовите последовательно функции UnmapViewOfFile, CloseHandle для закрытия дескриптора объекта отображения и CloseHandle для закрытия дескриптора файла.

Пример: последовательная обработка файлов с использованием метода отображения

Программа atou (программа 2.4) иллюстрирует последовательную обработку файлов на примере преобразования ASCII-файлов к кодировке Unicode, приводящего к удвоению размера файла. Этот случай является идеальным для применения отображения файлов, поскольку наиболее естественным способом указанного преобразования является посимвольная обработка данных без обращения к операциям файлового ввода/вывода. Программа 5.3 сначала просто отображает входной и выходной файлы в память, предварительно вычисляя размер выходного файла путем удвоения размера входного файла, а затем осуществляет требуемое посимвольное преобразование.

Этот пример отчетливо демонстрирует, как сложность процесса отображения файлов, выполнение которого необходимо для инициализации программы, компенсируется простотой результирующей обработки. Принимая во внимание, насколько просто выполняются обычные операции файлового ввода/вывода, применение более сложного метода могло бы показаться излишним, однако это с лихвой окупается выигрышем в производительности. В приложении В показано, что версия, использующая отображение файлов, в файловых системах NTFS работает значительно быстрее по сравнению с версиями, использующими обычные способы доступа к файлам, так что некоторое усложнение программы себя полностью оправдывает. Дополнительные результаты анализа производительности приведены на Web-сайте книги, поэтому ниже мы ограничимся лишь краткими выводами.

• Повышение производительности программ за счет использования отображения файлов наблюдается только в случае Windows NT и файловых систем NTFS.

• По сравнению с наилучшими из методик последовательной обработки файлов обеспечивается, по крайней мере, трехкратное повышение производительности .

• При работе с файлами большого размера преимущества в отношении производительности теряются. В нашем примере обычный последовательный просмотр файлов оказывается более предпочтительным, так как размер входного файла составляет около одной трети объема физической памяти. Снижение производительности метода отображения файлов в данном случае объясняется тем, что для входного файла требуется одна треть памяти, а для выходного файла, размер которого в два раза больше, — оставшиеся две трети, что заставляет нас сбрасывать отдельные части выходного файла на диск. Таким образом, в системе с объемом оперативной памяти 192 Мбайт ухудшение производительности метода отображения файлов будет наступать после достижения входными файлами размера 60 Мбайт. В большинстве случаев приходится иметь дело с файлами меньшего размера, в результате чего применение метода отображения файлов становится целесообразным.

В программе 5.3 представлена лишь функция Asc2UnMM. Основная программа совпадает с той, которая приведена в программе 2.4.

Программа 5.3. Asc2UnMM: преобразование файла с использованием метода отображения файлов 

/* Глава 5. Asc2UnMM.c: Реализация, использующая отображение файлов. */

#include "EvryThng.h"


BOOL Asc2Un(LPCTSTR fin, LPCTSTR fOut, BOOL bFaillfExists) {

 HANDLE hIn, hOut, hInMap, hOutMap;

 LPSTR pIn, pInFile;

 LPWSTR pOut, pOutFile;

 DWORD FsLow, dwOut;

 /* Открыть и отобразить входной и выходной файлы. */

 hIn = CreateFile(fIn, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

 hInMap = CreateFileMapping(hIn, NULL, PAGE_READONLY, 0, 0, NULL);

 pInFile = MapViewOfFile(hInMap, FILE_MAP_READ, 0, 0, 0);

 dwOut = bFailIfExists ? CREATE NEW : CREATE ALWAYS; 

 hOut = CreateFile(fOut, GENERIC_READ | GENERIC_WRITE, 0, NULL, dwOut, FILE_ATTRIBUTE_NORMAL, NULL);

 FsLow = GetFileSize (hIn, NULL); /* Установить размер отображения. */

 hOutMap = CreateFileMapping(hOut, NULL, PAGE_READWRITE, 0, 2* FsLow, NULL);

 pOutFile = MapViewOfFile(hOutMap, FILE_MAP_WRITE, 0, 0, (SIZE_T)(2 * FsLow));

 /* Преобразовать данные отображенного файла из ASCII в Unicode. */

 pIn = pInFile;

 pOut = pOutFile;

 while (pIn < pInFile + FsLow) {

  *pOut = (WCHAR) *pIn;

  pIn++;

  pOut++;

 }

 UnmapViewOfFile(pOutFile);

 UnmapViewOfFile(pInFile);

 CloseHandle(hOutMap);

 CloseHandle(hInMap);

 CloseHandle(hIn);

 CloseHandle(hOut);

 return TRUE;

} 

Пример: сортировка отображенных файлов

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

Программа 5.4 предназначена для сортировки файлов с записями фиксированной длины. Данная программа, sortFL, аналогична программе 5.1 в том отношении, что предполагает наличие 8-байтового ключа сортировки в начале записи, но ограничивается записями фиксированной длины. В программе 5.5 этот недостаток будет устранен за счет некоторого усложнения программы.

Сортировку выполняет описанная в файле <stdlib.h> функция qsort, входящая в состав библиотеки С. Заметьте, что эта функции требует от программиста предоставления функции, осуществляющей сравнение записей, в качестве которой нами будет использована функция KeyCompare из программы 5.2.

Структура программы достаточно проста. Сначала на основе временной копии входного файла создается объект отображения файла, затем создается единое представление объекта отображения файла в памяти, и, наконец, вызывается функция qsort. При этом какие-либо операции файлового ввода/вывода отсутствуют. Отсортированный файл направляется далее на стандартный вывод, причем в конце отображения файла добавляется нулевой символ. 

Программа 5.4. sortFL: сортировка файла с использованием его отображения в памяти 

/* Глава 5. sortFL. Сортировка файлов. Записи имеют фиксированную длину.*/

/* Использование: sortFL файл */

#include "EvryThng.h"


typedef struct _RECORD {

 TCHAR Key[KEY_SIZE];

 TCHAR Data[DATALEN];

} RECORD;


#define RECSIZE sizeof(RECORD)


int _tmain(int argc, LPTSTR argv[]) {

 HANDLE hFile = INVALID_HANDLE_VALUE, hMap = NULL;

 LPVOID pFile = NULL;

 DWORD FsLow, Result = 2;

 TCHAR TempFile[MAX_PATH];

 LPTSTR pTFile;

 /* Создать имя временного файла, предназначенного для хранения копии сортируемого файла, которая и подвергается сортировке. */

 /* Можно действовать и по-другому, оставив файл в качестве постоянно хранимой сортируемой версии. */

 _stprintf(TempFile, _T("%s%s"), argv[1], _Т(".tmp"));

 CopyFile(argv[1], TempFile, TRUE);

 Result = 1; /* Временный файл является вновь созданным и должен быть удален. */

 /* Отобразить временный файл и выполнить его сортировку в памяти. */

 hFile = CreateFile(TempFile, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

 FsLow = GetFileSize(hFile, NULL);

 hMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, FsLow + TSIZE, NULL);

 pFile = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0 /* FsLow + TSIZE */, 0);

 qsort(pFile, FsLow / RECSIZE, RECSIZE, KeyCompare);

 /* KeyCompare – как в программе 5.2. */

 /* Отобразить отсортированный файл. */

 pTFile = (LPTSTR)pFile;

 pTFile[FsLow/TSIZE] = '\0';

 _tprintf(_T("%s"), pFile);

 UnmapViewOfFile(pFile);

 CloseHandle(hMap);

 CloseHandle(hFile);

 DeleteFile(TempFile);

 return 0;

} 

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

Базовые указатели

Как показали предыдущие примеры, во многих случаях метод отображения файлов является весьма удобным. Однако предположим, что в программе создается структура данных с указателями, ссылающимися на область отображения файла, и ожидается, что впоследствии к этому файлу будет производиться обращение. В этом случае указатели оказываются установленными относительно виртуального адреса, возвращенного функцией MapViewOfFile, и не будут иметь смысла при использовании представления объекта отображения в следующий раз. Решение состоит в том, чтобы использовать базовые указатели (based pointers), являющиеся фактически смещениями относительно другого указателя. Соответствующий синтаксис Microsoft С, доступный в Visual C++ и некоторых других системах, выглядит следующим образом:

тип _based (база) объявление

Ниже показаны два примера таких указателей.

LPTSTR pInFile = NULL;

DWORD _based (pInFile) *pSize;

TCHAR _based (pInFile) *pIn;

Обратите внимание на тот факт, что синтаксис требует использования символа *, хотя такая практика противоречит соглашениям Windows.

Пример: использование базовых указателей

Рассмотренные выше примеры относились к сортировке файлов в различных ситуациях. Вместе с тем, должно быть очевидным, что наша цель состояла не в обсуждении методик сортировки, а в демонстрации применения различных методов управления памятью. В программе 5.1 используется бинарное дерево поиска, которое уничтожается при переходе к сортировке очередного файла, тогда как в программе 5.4 сортируется массив фиксированных записей, отображенный в памяти компьютера. В приложении В представлены показатели производительности для различных вариантов реализации, включая и тот, который реализует программа 5.5. 

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

Программа 5.5, которая должна применяться совместно с программой 5.6, решает эту проблему, которая проявляется всякий раз, когда отображаются структуры данных, использующие указатели. В предлагаемом решении используется ключевое слово _based, предоставляемое Microsoft С. Альтернативным вариантом было бы отображение файла в массив и обеспечение доступа к записям в представлении объекта отображения файла с помощью индекса.

Программа написана в виде еще одной версии команды sort, которой в данном случае присвоено имя sortMM. Данная версия программы отличается следующими особенностями, заслуживающими внимания:

• Записи могут иметь переменную длину.

• Программа использует первое поле в качестве ключа, но определяет его длину.

• Строятся два представления файла. Одно из них представляет исходный файл, а второе — файл, содержащий отсортированные ключи. Второй файл является индексным файлом (index file), каждая из записей которого содержит ключ и указатель (базовый адрес), относящийся к исходному файлу. Для сортировки индексного файла, во многом по аналогии с программой 5.4, применяется функция qsort.

• Индексный файл сохраняется и впоследствии может быть использован, причем предусмотрена возможность (параметр командной строки –I) отказаться от сортировки и использовать существующий индексный файл. Кроме того, индексный файл может быть использован для быстрого поиска ключей путем проведения бинарного поиска (возможно, с использованием входящей в библиотеку C функции bsearch) в индексном файле.

Взаимосвязь между индексным файлом и сортируемым файлом иллюстрирует рис. 5.5. Главной программой является программа 5.5, которая обеспечивает создание представлений файлов в памяти компьютера, осуществляет сортировку индексного файла и отображает результаты. Эта программа вызывает функцию CreateIndexFile, представленную программой 5.6. 

Системное программирование в среде Windows

Рис. 5.5. Сортировка с использованием отображения индексного файла


Программа 5.5. sortMM: использование базовых указателей в индексном файле 

/* Глава 5. Команда sortMM.

 Сортировка отображенного в памяти файла – только один файл. Опции:

 -r Сортировать в обратном порядке.

 -I Использовать индексный файл для получения отсортированного файла. */

#include "EvryThng.h"


int KeyCompare(LPCTSTR , LPCTSTR);

DWORD CreateIndexFile (DWORD, LPCTSTR, LPTSTR);

DWORD KStart, KSize; /* Начальная позиция и размер ключа (TCHAR) . */

BOOL Revrs;


int _tmain(int argc, LPTSTR argv []) {

 HANDLE hInFile, hInMap; /* Дескрипторы входного файла. */

 HANDLE hXFile, hXMap; /* Дескрипторы индексного файла. */

 HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);

 BOOL IdxExists;

 DWORD FsIn, FsX, RSize, iKey, nWrite, *pSizes;

 LPTSTR pInFile = NULL;

 LPBYTE pXFile = NULL, pX;

 TCHAR _based(pInFile) *pIn; 

 TCHAR IdxFlNam [MAX_PATH], ChNewLine = TNEWLINE;

 int FlIdx = Options(argc, argv, _T("rI"), &Revrs, &IdxExists, NULL);

 /* Шаг 1: открыть и отобразить входной файл. */

 hInFile = CreateFile(argv [FlIdx], GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

 hInMap = CreateFileMapping(hInFile, NULL, PAGE_READWRITE, 0, 0, NULL);

 pInFile = MapViewOfFile(hInMap, FILE_MAP_ALL_ACCESS, 0, 0, 0);

 FsIn = GetFileSize(hInFile, NULL);

 /* Шаги 2 и З: создать имя индексного файла. */

 _stprintf(IdxFlNam, _T("%s%s"), argv[FlIdx], _T(".idx"));

 if (!IdxExists) RSize = CreateIndexFile(FsIn, IdxFlNam, pInFile);

 /* Шаг 4: отобразить индексный файл. */

 hXFile = CreateFile(IdxFlNam, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

 hXMap = CreateFileMapping(hXFile, NULL, PAGE_READWRITE, 0, 0, NULL);

 pXFile = MapViewOfFile(hXMap, FILE_MAP_ALL_ACCESS, 0, 0, 0);

 FsX = GetFileSize(hXFile, NULL);

 pSizes = (LPDWORD)pXFile; /* Поля размера в .idx-файле. */

 KSize = *pSizes; /* Размер ключа */

 KStart = *(pSizes + 1); /* Начальная позиция ключа в записи. */

 FsX –= 2 * sizeof(DWORD);

 /* Шаг 5: сортировать индексный файл при помощи qsort. */

 if (!IdxExists) qsort(pXFile + 2 * sizeof(DWORD), FsX / RSize, RSize, KeyCompare);

 /* Шаг 6: отобразить входной файл в отсортированном виде. */

 рХ = pXFile + 2 * sizeof(DWORD) + RSize – sizeof(LPTSTR);

 for (iKey = 0; iKey < FsX / RSize; iKey++) {

  WriteFile(hStdOut, &ChNewLine, TSIZE, &nWrite, NULL);

  /* Приведение типа рХ, если это необходимо! */

  pIn = (TCHAR _based (pInFile)*) *(LPDWORD)pX;

  while ((*pIn != CR || * (pIn + 1) != LF) && (DWORD) pIn < FsIn) {

   WriteFile(hStdOut, pIn, TSIZE, &nWrite, NULL); pIn++;

  }

  рХ += RSize;

 }

 UnmapViewOfFile(pInFile);

 CloseHandle(hInMap);

 CloseHandle(hInFile);

 UnmapViewOfFile(pXFile);

 CloseHandle(hXMap);

 CloseHandle(hXFile);

 return 0;

}

Программа 5.6 представляет собой функцию CreateIndexFile,  с помощью которой создается индексный файл. Сначала она просматривает входной файл для определения размера ключа по первой записи. После этого она должна просматривать входной файл для нахождения границ каждой из записей переменной длины для организации структуры, представленной на рис. 5.5.

Программа 5.6. sortMM: создание индексного файла 

DWORD CreateIndexFile(DWORD FsIn, LPCTSTR IdxFlNam, LPTSTR pInFile) {

 HANDLE hXFile;

 TCHAR _based (pInFile) *pInScan = 0;

 DWORD nWrite;

 /* Шаг 2а: создать индексный файл. Не отображать его на данной стадии. */

 hXFile = CreateFile(IdxFlNam, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, 0, NULL);

 /* Шаг 2b: получить первый ключ и определить его размер и начальную позицию. Пропустить пробел и получить длину ключа. */

 KStart = (DWORD) pInScan;

 while (*pInScan != TSPACE && *pInScan != TAB) pInScan++; /* Найти поле первого ключа. */

 KSize = ((DWORD)pInScan – KStart) / TSIZE;

 /* Шаг 3: просмотреть весь файл, записывая ключи и указатели записей в индексный файл. */

 WriteFile(hXFile, &KSize, sizeof(DWORD) , &nWrite, NULL);

 WriteFile(hXFile, &KStart, sizeof(DWORD), &nWrite, NULL);

 pInScan = 0;

 while ((DWORD)pInScan < FsIn) {

  WriteFile(hXFile, pInScan + KStart, KSize * TSIZE, &nWrite, NULL);

  WriteFile(hXFile, &pInScan, sizeof(LPTSTR), &nWrite, NULL);

  while ((DWORD)pInScan < FsIn && ((*pInScan != CR) || (*(pInScan + 1) != LF))) {

   pInScan++; /* Пропустить до конца строки. */

  }

  pInScan += 2; /* Пропустить CR, LF. */

 }

 CloseHandle(hXFile);

 /* Размер отдельной записи. */

 return KSize * TSIZE + sizeof(LPTSTR);

} 

Динамически компонуемые библиотеки

Как вы имели возможность убедиться, средства управления памятью и отображения файлов оказываются важными и полезными для широкого класса программ. Системы управления памятью используются также самими ОС, и наиболее важной и заслуживающей внимания сферой применения отображения файлов являются библиотеки DLL. DLL широко используются приложениями Windows, являясь существенным элементом таких высокоуровневых технологий, как СОМ, а многие компоненты программного обеспечения поставляются в виде DLL.

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

Статические и динамические библиотеки

Самый непосредственный способ построения любой программы — это объединение исходных кодов всех функций, их компиляция и компоновка всех необходимых элементов в один исполняемый модуль. Чтобы упростить процесс сборки, такие функции общего назначения, как ReportError, можно поместить в библиотеку. Этот подход применялся во всех представленных до сих пор примерах программ, хотя и касался всего лишь нескольких функций, большинство из которых предназначались для вывода сообщений об ошибках.

Эта монолитная, одномодульная модель отличается простотой, однако обладает и рядом недостатков.

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

• При каждом обновлении потребуется повторная сборка всей программы, даже если необходимые изменения незначительны или носят локальный характер.

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

• Для достижения наилучшей производительности в различных средах может потребоваться использование различных версий программы, в которых применяются различные методики. Так, функция Asc2Un в программе 2.4 (atou) и программе 5.3 (Asc2UnMM) реализована по-разному. Единственный способ выполнения программ, имеющих несколько различных реализаций, — это заранее принять решение относительно того, какую из двух версий запускать, исходя из свойств окружения.

Библиотеки DLL обеспечивают возможность элегантного решения этих и других проблем. 

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

• DLL могут использоваться для создания общих библиотек (shared libraries). Одну и ту же библиотеку DLL могут совместно использовать несколько одновременно выполняющихся программ, но в память будет загружена только одна ее копия. Все программы отображают код DLL на адресные пространства своих процессов, хотя каждый поток будет иметь собственный экземпляр неразделяемого хранилища в стеке. Например, функция ReportError использовалась почти в каждом из приведенных ранее примеров программ, тогда как для всех программ было бы вполне достаточно ее единственной DLL-реализации.

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

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

Библиотеки DLL, иногда в ограниченном виде, используются практически в любой ОС. Так, в UNIX аналогичные библиотеки фигурируют под названием "разделяемых библиотек" (shared libraries). В Windows библиотеки DLL используются, помимо прочего, для создания интерфейсов ОС. Весь Windows API поддерживается одной DLL, которая для предоставления дополнительных услуг вызывает ядро Windows.

Один код DLL может совместно использоваться несколькими процессами Windows, но после его вызова он выполняется как часть вызывающего процесса или потока, Поэтому библиотека может использовать ресурсы вызывающего процесса, например дескрипторы файлов, и стек потока. Следовательно, DLL должны создаваться таким образом, чтобы обеспечивалась безопасная многопоточная поддержка (thread safety). (Более подробная информация относительно DLL и безопасной многопоточной поддержки содержится в главах 8, 9 и 10. Методы создания DLL, предоставляющих многопоточную поддержку, иллюстрируются программами 12.4 и 12.5.) Кроме того, DLL могут экспортировать переменные, а также точки входа функций. 

Неявное связывание

Неявное связывание, или связывание во время загрузки (load-time linking) является простейшей из двух методик связывания. Порядок действий в случае использования Microsoft C++ следующий:

1. После того как собраны все необходимые для новой DLL функции, осуществляется сборка DLL, а не, например, консольного приложения.

2. В процессе сборки создается библиотечный .LIB-файл, играющий роль заглушки (stub) для фактического кода. Этот файл должен помещаться в каталог библиотек общего пользования, указанный в проекте.

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

4. В исходном коде DLL следует предусмотреть экспортирование интерфейсов функций, о чем рассказано ниже.

Экспортирование и импортирование интерфейсов

Самое значительное изменение, которое требуется внести в функцию, прежде чем ее можно будет поместить в DLL, — это объявить ее экспортируемой (UNIX и некоторые другие системы не требуют явного выполнения этого шага). Это достигается либо за счет использования .DEF-файла, либо, что проще и возможно в Microsoft С, за счет использования в объявлениях модификатора _declspec (dllexport) следующим образом:

_declspec(dllexport) DWORD MyFunction (…);

Далее в процессе сборки создаются .DLL-файл и .LIB-файл. .LIB-файл — это библиотека-заглушка, которая должна быть скомпонована с вызывающей программой для разрешения внешних ссылок и создания актуальных связей с . DLL-файлом во время загрузки.

Вызывающая, или клиентская, программа должна объявить о том, что функцию следует импортировать, используя для этого модификатор _declspec (dllexport). Стандартный метод заключается в создании включаемого файла, использующего переменную препроцессора, имя которой формируется на основе имени проекта Microsoft Visual C++, набранного в верхнем регистре и дополненного суффиксом _EXPORTS.

Вам также может потребоваться еще одно объявление. Если вызывающая (клиентская) программа написана на C++, то для нее будет определена переменная препроцессора __cplusplus, и вы должны будете указать на необходимость использования системы соглашений о вызовах, принятой в С, с помощью следующего выражения:

extern "С"

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

#if defined(MYLIBRARY_EXPORTS)

#define LIBSPEC _declspec(dllexport)

#elif defined(__cplusplus)

#define LIBSPEC extern "C" _declspec(dllimport)

#else

#define LIBSPEC _declspec(dllimport)

#endif

LIBSPEC DWORD MyFunction (…);

Visual C++ автоматически определяет MYLIBRARY_EXPORTS при вызове компилятора, если проект предназначен для создания DLL MyLibrary. Клиентский проект, который использует DLL, переменную MYLIBRARYEXPORTS не определяет, и поэтому имя функции импортируется из библиотеки.

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

• Каталог, в котором находится загружаемое приложение.

• Текущий каталог, если он отличен от каталога, содержащего исполняемый модуль.

• Системный каталог Windows. Вы можете определить этот путь, вызвав функцию GetSystemDirectory; таковым обычно является каталог с:\WINDOWS\SYSTEM32.

• Системный каталог 16-разрядной Windows, который отсутствует в системах Windows 9x. Функция, позволяющая получить путь доступа к этому каталогу, отсутствует, и для наших целей он оказывается ненужным.

• Каталог Windows (используйте функцию GetWindowsDirectory).

• Каталоги, перечисленные в переменной окружения PATH, которые будут просматриваться в той последовательности, в какой они указаны. 

Заметьте, что этот стандартный порядок просмотра каталогов при поиске можно изменить, о чем говорится в разделе "Явное связывание". Для получения более подробной информации относительно стратегии поиска посетите Web-сайт по адресу http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dllproc/base/loadlibrary.asp, а также ознакомьтесь с описанием функции SetDllDirectory, введенной в Windows NT 5.1 (то есть Windows XP). Изменить стратегию поиска позволяет также функция LoadLibraryEx, описанная в следующем разделе.

Применение стандартной стратегии поиска иллюстрируется в проекте Utilities, доступном на Web-сайте книги, а такие вспомогательные функции, как ReportError, используются почти в каждом примере проектов.

Возможно также экспортирование и импортирование переменных, а также точек входа функций, хотя эти возможности в примерах не иллюстрируются.

Явное связывание

Явное связывание, или связывание во время выполнения (run-time linking), требует, чтобы в программе содержались конкретные указания относительно того, когда именно необходимо загрузить или освободить библиотеку DLL. Далее программа получает адрес запрошенной точки входа и использует этот адрес в качестве указателя при вызове функции. В вызывающей программе функция не объявляется; вместо этого в качестве указателя на функцию объявляется переменная. Поэтому во время компоновки программы присутствие библиотеки не является обязательным. Для выполнения необходимых операций требуются три функции: LoadLibrary (или LoadLibraryEx), GetProcAddress и FreeLibrary. На 16-битовое происхождение определений функций указывает присутствие в них дальних (far) указателей и дескрипторов различных типов.

Для загрузки библиотеки служат две функции: LoadLibrary и LoadLibraryEx. 

HINSTANCE LoadLibrary(LPCTSTR lpLibFileName) 

HINSTANCE LoadLibraryEx(LPCTSTR lpLibFileName, HANDLE hFile, DWORD dwFlags) 

В обоих случаях значением возвращаемого дескриптора (типа HINSTANCE, а не HANDLE) в случае ошибки будет NULL. Суффикс .DLL в имени файла указывать не обязательно. С помощью функций LoadLibrary можно загружать также .ЕХЕ-файлы. При указании путей доступа должны использоваться символы обратной косой черты (\); символы прямой косой черты (/) в данном случае работать не будут. 

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

Функция LoadLibraryEx аналогична функции LoadLibrary, однако имеет несколько флагов, которые оказываются полезными для указания альтернативных путей поиска и загрузки библиотек в виде файла данных. Параметр hFile зарезервирован для использования в будущем. Параметр dwFlags позволяет определять различные варианты поведения системы путем указания одного из трех значений:

1. LOAD_WITH_ALTERED_SEARCH_PATH: отменяет ранее описанный стандартный порядок просмотра каталогов при поиске, изменяя лишь первый из шагов стратегии поиска. Вместо каталога, из которого загружалось приложение, используется путь поиска, указанный в имени lpLibFileName.

2. LOAD_LIBRARY_AS_DATAFILE: файл воспринимается как файл данных и не требует выполнения каких-либо действий по его подготовке к запуску, на пример вызова функции DllMain (см. раздел "Точки входа библиотеки DLL" далее в этой главе).

3. DONT_RESOLVE_DLL_REFERENCE: функция DllMain для инициализаций процессов и потоков не вызывается; загрузка дополнительных модулей, на которые имеются ссылки в указанной DLL, также не производится.

Закончив работать с экземпляром DLL — возможно, с намерением загрузить другую ее версию — вы должны освободить дескриптор библиотеки, тем самым освобождая ресурсы, в том числе распределенное для библиотеки виртуальное адресное пространство. Однако DLL продолжает оставаться загруженной, если счетчик ссылок указывает на то, что она все еще используется другими процессами. 

BOOL FreeLibrary(HINSTANCE hLibModule) 

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

FARPROC GetProcAddress(HMODULE hModule, LPCSTR lpProcName) 

Параметр hModule, несмотря на другой тип имени (HINSTANCE определен как HMODULE), является экземпляром (instance) библиотеки, получаемым посредством вызова функции LoadLibrary или GetModuleHandle (см. следующий абзац). lpProcName — указатель на строку, содержащую имя точки входа; это имя не может задаваться в кодировке Unicode. В случае неуспешного выполнения функция возвращает значение NULL. Слово FARPROC, означающее "длинный указатель на процедуру", является анахронизмом.

Имя файла, связанного с дескриптором hHandle, можно получить с помощью функции GetModuleFileName. Возможно и обратное: для заданного имени файла (.DLL или .EXE) функция GetModuleHandle в случае успешного выполнения возвратит дескриптор, связанный с этим файлом, если текущий процесс загрузил его.

В следующем примере показано, как использовать адрес точки входа для вызова функции.

Пример: явное связывание функци и преобразования файлов

Программа 2.4, предназначенная для преобразования кодировки текстовых файлов из ASCII в Unicode, вызывает функцию Asc2Un (программа 2.5), выполняющую обработку файла с использованием операций файлового ввода/вывода. Программа 5.3 (Asc2UnMM) представляет альтернативную функцию, которая для выполнения той же операции использует отображение файлов. Обстоятельства, при которых функция Asc2UnMM обеспечивает выигрыш в скорости выполнения преобразования, ранее уже обсуждались; в основном они сводятся к тому, что файловой системой должна быть NTFS, а размер файла не должен быть слишком большим.

Программа 5.7 является модифицированным вариантом вызывающей программы, обеспечивающим возможность принятия решения относительно того, какой вариант реализации функции преобразования должен быть загружен, во время выполнения. Программа загружает DLL, получает адрес точки входа Asc2Un и вызывает функцию. В данном случае существует только одна точка входа, но реализовать вариант с несколькими точками входа не составляет особого труда. Основная программа является, по существу, той же, что и прежде, за исключением того, что библиотека DLL, которую необходимо использовать, указывается в виде параметра командной строки. В упражнении 5.9 вам предлагается написать вариант программы, в котором нужная DLL определяется на основе свойств системы и файла. Обратите внимание на то, каким образом осуществляется приведение типа адреса FARPROC к типу соответствующей функции с использованием необходимого в этом случае, но довольно сложного, синтаксиса С.

Программа 5.7. atouEL: преобразование файлов с использованием явного связывания

/* Глава 5. Версия atou, использующая явное связывание. */

#include "EvryThng.h"


int _tmain(int argc, LPTSTR argv[]) {

 /* Объявить переменную Asc2Un как функцию. */

 BOOL (*Asc2Un)(LPCTSTR, LPCTSTR, BOOL);

 DWORD LocFileIn, LocFileOut, LocDLL, DashI;

 HINSTANCE hDLL;

 FARPROC pA2U;

 LocFileIn = Options(argc, argv, _T("i"), &DashI, NULL);

 LocFileOut = LocFileIn + 1;

 LocDLL = LocFileOut + 1;

 /* Проверить существование файла, а также опущен ли параметр DashI. */

 /* Загрузить функцию преобразования из ASCII в Unicode. */

 hDLL = LoadLibrary(argv[LocDLL]);

 if (hDLL == NULL) ReportError(_T("He удается загрузить DLL."), 1, TRUE);

 /* Получить адрес точки входа. */

 pA2U = GetProcAddress(hDLL, "Asc2Un");

 if (pA2U == NULL) ReportError(_T("He найдена точка входа."), 2, TRUE);

 /* Привести тип указателя. Здесь можно использовать typedef. */

 Asc2Un = (BOOL(*)(LPCTSTR, LPCTSTR, BOOL))pA2U;

 /* Вызвать функцию. */

 Asc2Un(argv[LocFileIn], argv[LocFileOut], FALSE);

 FreeLibrary(hDLL);

 return 0;

} 

Создание библиотек DLL на основе функции Asc2Un

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

Точки входа библиотеки DLL

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

Кроме того, точка входа вызывается всякий раз, когда процесс создает новый поток (глава 7) или прекращает его выполнение.

Точкой входа с именем DllMain, прототип которой приводится ниже, мы воспользуемся в полной мере только в главе 12 (программа 12.4), где она предоставит потокам удобный способ управления ресурсами и так называемыми локальными областями хранения потоков (Thread Local Storage, SLT) в DLL с многопоточной поддержкой. 

BOOL DllMain(HINSTANCE hDll, DWORD Reason, LPVOID Reserved) 

Параметр hDll является дескриптором экземпляра DLL, возвращенным функцией LoadLibrary. Значение NULL параметра Reserved указывает на то, что подключение процесса к библиотеке произошло в результате вызова функции Load-Library; иные значения этого параметра свидетельствуют о подключении к библиотеке в результате неявного связывания во время загрузки. Подобным образом, к значению NULL параметра Reserved приводит и отключение процесса от библиотеки в результате вызова функции FreeLibrary.

Параметр Reason может иметь одно из четырех значений: DLL_PROCESS_ATTACH, DLL_THREAD_ATTACH, DLL_THREAD_DETACH и DLL_PROCESS_DETACH. Функции точки входа DLL обычно используют операторы switch и в качестве индикатора успешного выполнения возвращают значение TRUE.

Система сериализует вызовы DllMain таким образом, что в каждый момент времени выполнять ее может только один поток (к подробному обсуждению потоков мы приступим в главе 7). Эта сериализация весьма существенна, поскольку операции инициализации, которые должна выполнять DllMain, не должны прерываться до их завершения. По этой же причине внутри точки входа не рекомендуется использовать блокирующие вызовы функций, например, функций ввода/вывода или функций ожидания (см. главу 8), поскольку они будут препятствовать запуску точки входа другими потоками. В частности, не следует вызывать внутри точки входа DLL функции LoadLibrary и LoadLibraryEx, поскольку это будет порождать дополнительные вызовы точек входа DLL.

Функция DisableThreadLibraryCalls отменяет отправку указанному экземпляру DLL уведомлений о подключении и отключении потоков. Запрет отправки уведомлений может пригодиться в тех случаях, когда потоки не нуждаются в каких-либо уникальных ресурсах во время инициализации.

Управление версиями DLL

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

• В результате добавления новых функций в случае неявного связывания могут стать недействительными смещения, определенные для приложений во время компоновки с .lib-файлами. От этой проблемы можно избавиться, применив явное связывание.

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

• Для приложений, использующих обновленную функциональность DLL, возможны случая связывания с прежними версиями DLL.

Проблемы совместимости различных версий DLL, носящие жаргонное название "кошмара DLL", не являются столь острыми, если в одном каталоге поддерживать только одну версию DLL. Однако предоставить отдельный каталог для каждой из различных версий вовсе не так просто, как может показаться. Существует несколько других вариантов решения этой проблемы.

• Можно использовать номер версии DLL в именах .DLL– и .LIB-файлов, обычно в виде суффикса. Так, чтобы соответствовать номеру версии, используемой в данной книге, в примерах, приведенных на Web-сайте книги, и во всех проектах используются файлы Utility_3_0.LIB и Utility_3_0.DLL. Применяя явное или неявное связывание, приложения могут формулировать свои требования к версиям и получать доступ к файлам с различными именами. Такое решение характерно для UNIX-приложений.

• Компания Microsoft ввела понятие параллельных DLL (side-by-side DLL), или сборок (assemblies) и компонентов (components). При таком подходе в приложение необходимо включать объявление на языке XML, в котором определяются требования к DLL. Рассмотрение этой темы выходит за рамки данной книги, однако дополнительную информацию вы можете получить на Web-сайте компании Microsoft, в разделе, посвященном вопросам разработки приложений.

• Платформа .NET Framework предоставляет дополнительные средства поддержки выполнения приложений в условиях сосуществования различных версий DLL.

В примерах проектов, используемых в данной книге, используется первый из отмеченных подходов, предусматривающий включение номеров версий в имена файлов. С целью предоставления дополнительной поддержки, обеспечивающей возможность получения приложениями информации о DLL, во всех DLL реализована функция DllGetVersion. Кроме того, Microsoft предоставляет эту косвенно вызываемую функцию в качестве стандартного средства получения информации о версии в динамическом режиме. Указанная функция имеет следующий прототип: 

HRESULT CALLBACK DllGetVersion(DLLVERSIONINFO *pdvi ) 

Информация о DLL возвращается в структуре DLLVERSIONINFO, в которой имеются поля типа DWORD для параметров cbSize (размер структуры), dwMajorVersion, dwMinorVersion, dwBuildNumber и dwPlatformID. В последнем поле, dwPlatformID, может быть установлено значение DLLVER_PLATFORM_NT, если библиотека не выполняется под управлением Windows 9x, или DLLVER_PLATFORM_WINDOWS, если это ограничение отсутствует. В поле cbSize должно находиться значение sizeof (DLLVERSIONINFO). В случае успешного выполнения функция возвращает значение NOERROR. Функция DllGetVersion реализована в проекте Utility_3_0.

Резюме

Система управления памятью Windows предоставляет следующие возможности:

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

• Использование нескольких независимых куч обладает рядом преимуществ по сравнению с распределением памяти из одной кучи.

• Методы отображения файлов, доступные в UNIX, но не предоставляемые библиотекой С, обеспечивают обработку файлов в памяти, что было проиллюстрировано несколькими примерами. Отображение файлов в памяти осуществляется независимо от управления кучей и упрощает решение многих задач программирования. Преимущества использования отображения файлов подтверждаются данными о достигаемом за счет этого повышении производительности, приведенными в приложении В.

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

В следующих главах

Мы завершили обзор задач, решаемых в рамках одного процесса. Далее мы переходим к изучению методов параллельной обработки, сначала на уровне процессов (глава 6), а затем — потоков (глава 7). В последующих главах показано, как организовать синхронизацию и взаимодействие параллельно выполняющихся операций по обработке данных. 

Дополнительная литература

Отображение файлов, виртуальная память и ошибки страниц

Описание этих важных понятий содержится в книге [38], а их углубленное обсуждение вы можете найти в документации, поставляемой вместе с большинством ОС.

Структуры данных и алгоритмы

Деревьям поиска и алгоритмам сортировки посвящено множество работ, включая [39] и [34].

Использование явного связывания

DLL и явное связывание имеют фундаментальное значение для использования модели СОМ, которая широко применяется при разработке программного обеспечения Windows. Важность функций LoadLibrary и GetProcAddress продемонстрирована в главе 1 книги [3].

Упражнения

5.1. Спроектируйте и проведите эксперименты для оценки выигрыша в производительности, достигаемого за счет использования флага HEAP_NO_SERIALIZE при вызове функций HeapCreate и HeapAlloc. Как зависит этот показатель от размера кучи и размера блока? Заметна ли разница в результатах для различных версий Windows? На Web-сайте книги находится программа HeapNoSr.c, которая поможет вам приступить к выполнению этого и следующего упражнений.

5.2. Измените тестовую программу из предыдущего упражнения таким образом, чтобы она позволяла определить, генерирует ли функция malloc исключения или возвращает нулевой указатель в случае нехватки памяти. Является ли обнаруженное поведение функции корректным? Сравните также производительность, обеспечиваемую функцией malloc, с результатами предыдущего упражнения.

5.3. Доля накладных издержек при распределении памяти из кучи колеблется в зависимости от используемой версии Windows, что особенно заметно в случае выходящих из употребления версий Windows 9x. Спроектируйте и проведите эксперимент для определения количества блоков памяти фиксированного размера, которые каждая из систем предоставляет в одной куче. Используя SEH для определения того момента, когда распределенными оказываются все блоки, вы значительно упростите программу. Подобным образом ведет себя программа clear.с, находящаяся на Web-сайте книги, если игнорировать часть ее кода, ответственную за явное тестирование ОС. Между прочим, эта программа используется в некоторых тестах по измерению временных характеристик для гарантии того, что данные, полученные в процессе выполнении предыдущего теста, не остались в памяти.

5.4. Путем изменения программы sortFL (программа 5.4) создайте программу sortHP, распределяющую память для буфера, размер которого достаточно велик, чтобы в нем уместился весь файл, и выполните считывание файла в этот буфер. Отображение файла использовать не следует. Сравните производительность обеих программ.

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

5.6. Напишите программу поиска записей по указанному ключу в файле, проиндексированном с применением программы 5.5. Для этой цели удобно воспользоваться функцией bsearch, входящей в состав библиотеки С.

5.7. Реализуйте программу tail из главы 3, используя отображение файлов.

5.8. Поместите вспомогательные функции ReportError, PrintStrings, PrintMsg и ConsolePrompt в DLL и перекомпонуйте некоторые из программ, с которыми мы работали раньше. Проделайте то же самое с функциями Options и GetArgs, предназначенными, соответственно, для обработки параметров командной строки и аргументов. Важно, чтобы как вспомогательная DLL, так и вызывающая программа использовали также и библиотеку С в виде DLL. Например, в Visual C++ и Visual Studio 6.0 выберите, начав со строки главного меню, следующие команды: Project (Проект), Settings (Параметры), вкладку C/C++, Category (Code Generation) (Категория (Генерация кода)), Use Run-Time Library (Multithreaded DLL) (Использовать библиотеку времени выполнения (многопоточная DLL)). Заметьте, что библиотеки DLL, вообще говоря, должны обеспечивать многопоточную поддержку, поскольку они будут использоваться потоками нескольких процессов. Пример возможного решения содержится в проекте Utilities_3_0, доступном на Web-сайте книги.

5.9. Измените программу 5.7 таким образом, чтобы решение относительно того, какую DLL следует использовать, базировалось на размере файла и конфигурации системы. .LIB-файл здесь не требуется, поэтому продумайте, как отменить его генерацию. Для определения типа файловой системы используйте функцию GetVolumeInformation.

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

ГЛАВА 6

Управление процессами

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

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

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

Процессы и потоки Windows

Внутри каждого процесса могут выполняться одна или несколько потоков, и именно поток является базовой единицей выполнения в Windows. Выполнение потоков планируется системой на основе обычных факторов: наличие таких ресурсов, как CPU и физическая память, приоритеты, равнодоступность ресурсов и так далее. Начиная с версии NT4, в Windows поддерживается симметричная многопроцессорная обработка (Symmetric Multiprocessing, SMP), позволяющая распределять выполнение потоков между отдельными процессорами, установленными в системе.

С точки зрения программиста каждому процессу принадлежат ресурсы, представленные следующими компонентами:

• Одна или несколько потоков.

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

• Один или несколько сегментов кода, включая код DLL.

• Один или несколько сегментов данных, содержащих глобальные переменные.

• Строки, содержащие информацию об окружении, например, информацию о текущем пути доступа к файлам.

• Куча процесса.

• Различного рода ресурсы, например, дескрипторы открытых файлов и другие кучи.

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

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

• Локальные области хранения потока (Thread Local Storage, SLT) — массивы указателей, используя которые каждый поток может создавать собственную уникальную информационную среду.

• Аргумент в стеке, получаемый от создающего потока, который обычно является уникальным для каждого потока.

• Структура контекста, поддерживаемая ядром системы и содержащая значения машинных регистров.

На рис. 6.1 показан процесс с несколькими потоками. Рисунок является схематическим, поэтому на нем не указаны фактические адреса памяти и не соблюдены масштабы.

В данной главе показано, как работать с процессами, состоящими из единственного потока. О том, как использовать несколько потоков, рассказывается в главе 7.

Примечание

Рисунок 6.1 является высокоуровневым с точки зрения программиста представлением процесса. В действительности эта картина должна быть дополнена множеством технических деталей и особенностями реализации. Более подробную информацию заинтересованные читатели могут найти в книге Соломона (Solomon) и Руссиновича (Russinovich) Inside Windows 2000. 

Процессы UNIX сопоставимы с процессами Windows, имеющими единственный поток.

Реализации UNIX недавно пополнились потоками POSIX Pthreads, которые в настоящее время используются почти повсеместно. В [40] потоки не обсуждаются; все рассмотрение основано на процессах.

Наверное, можно было бы даже не напоминать о том, что понятие потоков не является новым, и их различные реализации предлагаются поставщиками уже на протяжении целого ряда лет. Однако потоки Pthreads являются самым распространенным стандартом, в то время как коммерческие реализации потоков являются устаревшими.

Системное программирование в среде Windows

Рис. 6.1. Процесс и его потоки

Создание процесса

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

Обычно принято говорить о процессах-предках, или родительских процессах (parent processes), и процессах-потомках, или дочерних процессах (child processes), однако между процессами Windows эти отношения фактически не поддерживаются. Использование данной терминология является просто удобным способом выражения того факта, что один процесс порождается другим.

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

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

BOOL CreateProcess(lpApplicationName, LPTSTR lpCommandLine, LPSECURITY_ATTRIBUTES lpsaProcess, LPSECURITY_ATTRIBUTES lpsaThread, BOOL bInheritHandles, DWORD dwCreationFlags, LPVOID lpEnvironment, LPCTSTR lpCurDir, LPSTARTUPINFO lpStartupInfo, LPPROCESS_INFORMATION lpProcInfo)

Возвращаемое значение: в случае успешного создания процесса и потока — TRUE, иначе — FALSE. 

Параметры

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

lpApplicationName и lpCommandLine (последний указатель имеет тип LPTSTR, а не LPCTSTR) — используются вместе для указания исполняемой программы и аргументов командной строки, о чем говорится в следующем разделе.

lpsaProcess и lpsaThread — указатели на структуры атрибутов защиты процесса и потока. Значениям NULL соответствует использование атрибутов защиты, заданных по умолчанию, и именно эти значения будут использоваться нами вплоть до главы 15, посвященной рассмотрению средств безопасности Windows.

bInheritHandles — показывает, наследует ли новый процесс наследуемые открытые дескрипторы (файлов, отображений файлов и так далее) из вызывающего процесса. Наследуемые дескрипторы имеют те же атрибуты, что и исходные, и их обсуждение будет продолжено в одном из следующих разделов.

dwCreationFlags — может объединять в себе несколько флаговых значений, включая следующие:

• CREATE_SUSPENDED — указывает на то, что основной поток будет создан в приостановленном состоянии и начнет выполняться лишь после вызова функция ResumeThread. 

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

• Create_New_Process_Group — указывает на то, что создаваемый процесс является корневым для новой группы процессов. Если все процессы, принадлежащие данной группе, разделяют общую консоль, то все они будут получать управляющие сигналы консоли (Ctrl-C или Ctrl-break). Обработчики управляющих сигналов консоли описывались в главе 4, а их применение было продемонстрировано в программе 4.5. Упомянутые группы процессов в некотором отношении аналогичны группам процессов UNIX и рассматриваются далее в этой главе.

Некоторые из флагов управляют приоритетами потоков нового процесса. О возможных значениях этих флагов более подробно говорится в главе 7. Пока же нам будет достаточно использовать приоритет родительского процесса (этот режим устанавливается по умолчанию) или указывать значение NORMAL_PRIORITY_CLASS.

lpEnvironment — указывает на блок параметров настройки окружения нового процесса. Если задано значение NULL, то новый процесс будет использовать значения параметров окружения родительского процесса. Блок параметров содержит строки, в которых заданы пары "имя-значение", определяющие, например, пути доступа к файлам.

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

lpStartupInfo — указатель на структуру, которая описывает внешний вид основного окна и содержит дескрипторы стандартных устройств нового процесса. Используйте соответствующую информацию из родительского процесса, которую можно получить при помощи функции GetStartupInfo. Можно поступить и по-другому, обнулив структуру STARTUPINFO перед вызовом функции CreateProcess. Для указания стандартных устройств ввода, вывода информации и вывода сообщений об ошибках следует определить значения полей дескрипторов стандартных устройств (hStdInput, hStdOutput и hStdError) в структуре STARTUPINFO. Чтобы эти значения не игнорировались, следует задать для другого элемента этой же структуры, а именно, элемента dwFlags, значение STARTF_USESTDHANDLES и определить все дескрипторы, которые потребуются дочернему процессу. Убедитесь в том, что эти дескрипторы являются наследуемыми и что при вызове функции CreateProcess значение параметра bInheritHandles установлено равным TRUE. Более подробная информация по этому вопросу, сопровождаемая соответствующим примером, приводится в разделе "Наследуемые дескрипторы". 

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

typedef struct PROCESS_INFORMATION {

 HANDLE hProcess;

 HANDLE hThread;

 DWORD dwProcessId;

 DWORD dwThreadId;

} PROCESS_INFORMATION;

Зачем процессам и потокам нужны еще и дескрипторы, если они снабжаются глобальными идентификаторами (ID)? Глобальные идентификаторы остаются уникальными для данного объекта на протяжении всего времени его существования и во всех процессах, тогда дескрипторов процесса может быть несколько и каждый из которых может характеризоваться собственным набором атрибутов, например определенными разрешениями доступа. В силу указанных причин одним функциям управления процессами требуется предоставлять идентификаторы процессов, а другим — дескрипторы. Кроме того, необходимость в дескрипторах процессов возникает при использовании универсальных функций, которые требуют указания дескрипторов. В качестве примера можно привести функции ожидания, обсуждаемые далее в этой главе, которые обеспечивают отслеживание переходов объектов различного типа, в том числе и процессов, указываемых с помощью дескрипторов, в определенные состояния. Точно так же, как и дескрипторы файлов, дескрипторы процессов и потоков должны закрываться сразу же после того, как необходимость в них отпала.

Примечание

Новый процесс получает информацию об окружении, рабочем каталоге и иную информацию в результате вызова функции CreateProcess. По завершении этого вызова любые изменения характеристик родительского процесса никак не отразятся на дочернем процессе. Так, после вызова функции CreateProcess рабочий каталог родительского процесса может измениться, но на дочерний процесс это не окажет никакого влияния, если только он сам не сменит рабочий каталог. Оба процесса полностью независимы друг от друга. 

Модели процесса в UNIX и Windows значительно отличаются друг от друга. Прежде всего, в Windows отсутствует эквивалент UNIX-функции fork, создающей копию родительского процесса, включая его пространство данных, кучу и стек. В Windows трудно добиться точной эмуляции fork, но как ни расценивать последствия этого ограничения, остается фактом, что проблемы с использованием функции fork существуют и в многопоточных системах UNIX, поскольку любые попытки создания точной реплики многопоточной системы с копиями всех потоков и объектов синхронизации, особенно в случае SMP-систем, приводят к возникновению множества трудностей. Поэтому в действительности функция fork вообще плохо подходит для многопоточных систем.

В то же время, функция CreateProcess аналогична обычной для UNIX цепочке последовательных вызовов функций fork и execl (или одной из пяти остальных функций exec). В отличие от Windows пути доступа в UNIX определяются исключительно переменной среды PATH.

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

Процессы Windows идентифицируются как дескрипторами, так и идентификаторами процессов, тогда как в UNIX дескрипторы процессов отсутствуют.

Указание исполняемого модуля и командной строки

Для указания имени файла исполняемого модуля используются как параметр lpApplicationName, так и параметр lpCommandLine. При этом действуют следующие правила:

• Указатель lpApplicationName, если его значение не равно NULL, указывает на строку, содержащую имя файла исполняемого модуля. Если имя модуля содержит пробелы, его следует заключить в кавычки. Более детальное описание приводится ниже.

• Если же значение указателя lpApplicationName равно NULL, то имя модуля определяется первой из лексем, переданных параметром lpCommandLine.

Обычно задается только параметр lpCommandLine, в то время как параметр lpApplicationName полагается равным NULL. Тем не менее, ниже приведены более подробные правила, которые определяют порядок использования этих двух параметров.

• Параметр lpApplicationName, если он не равен NULL, определяет исполняемый модуль. В строке, на которую указывает этот указатель, задайте полный путь доступа и имя файла или же ограничьтесь только именем файла, и тогда будут использоваться текущие диск и каталог; дополнительный поиск при этом производиться не будет. В имя файла включите расширение, например, .EXE или .ВАТ.

• Если значение параметра lpApplicationName равно NULL, то именем исполняемого модуля является первая из разделенных пробельными символами лексем, переданных параметром lpCommandLine. Если имя полный путь доступа не указан, то поиск файла осуществляется в следующем порядке: 

 1. Каталог модуля текущего процесса.

 2. Текущий каталог.

 3. Системный каталог Windows, информацию о котором можно получить с помощью функции GetSystemDirectory.

 4. Каталог Windows, возвращаемый функцией GetWindowsDirectory.

 5. Каталоги, перечисленные в переменной окружения PATH.

Новый процесс может получить командную строку посредством обычного argv-механизма или путем вызова функции GetCommandLine для получения командной строки в виде одиночной строки символов.

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

Вовсе не обязательно, чтобы новый процесс создавался с тем же определением UNICODE, что и родительский процесс. Возможны любые комбинации. Использование _tmain, как обсуждалось в главе 2, облегчает разработку программного кода, который сможет работать как с символами Unicode, так и с символами ASCII.

Наследуемые дескрипторы

Часто бывает так, что дочернему процессу требуется доступ к объекту, к которому можно обратиться через дескриптор, определенный в родительском процессе, и если этот дескриптор — наследуемый, то дочерний процесс может получить копию открытого дескриптора родительского процесса. Часто именно так обеспечивается возможность использования дескрипторов стандартного ввода и вывода дочерним процессом. Преобразование дескриптора в наследуемый, чтобы дочерний процесс мог получить и использовать его копию, требует выполнения нескольких шагов.

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

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

В структуре SECURITY_ATTRIBUTES присутствует флаг bInheritHandle, значение которого должно быть установлено равным TRUE. He забывайте также о том, что элемент nLength должен инициализироваться следующим значением:

sizeof(SECURITY_ATTRIBUTES)

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

HANDLE h1, h2, h3;

SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };

h1 = CreateFile(…, &sa, …); /* Наследуемый. */

h2 = CreateFile(…, NULL, …); /* Ненаследуемый. */

h3 = CreateFile(…, &sa, …); /* Наследуемый. Возможно повторное использование структуры sa. */

Однако дочернему процессу значение наследуемого дескриптора пока еще не известно, и поэтому родительский процесс должен передать это значение дочернему процессу либо через механизм межпроцессного взаимодействия (Interprocess Communication, IPC), либо путем назначения дескриптора стандартному устройству ввода/вывода в структуре STARTUPINFO, как это делается в первом из примеров, приведенных в данной главе (программа 6.1), а также в ряде примеров в остальной части книги. Обычно последний метод является более предпочтительным, так как он позволяет перенаправить ввод/вывод стандартным способом без внесения каких-либо изменений в дочернюю программу.

В случае дескрипторов, которые не являются дескрипторами файлов или не используются для перенаправления ввода/вывода, применим другой способ, в соответствии с которым дескриптор преобразуется в текстовый формат и помещается в командную строку или переменную окружения. Такой подход можно использовать лишь в том случае, если дескриптор является наследуемым, поскольку и родительский, и дочерний процессы используют для идентификации дескриптора одно и то же значение. Один из способов реализации этого подхода предлагается в упражнении 6.2, а соответствующее решение приводится на Web-сайте книги.

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

На рис. 6.2 показан пример двух процессов с двумя различными таблицами дескрипторов, в которых с одним и тем же файлом или иным объектом связаны два различных дескриптора. Процесс 1 является родительским, процесс 2 — дочерним. Если принадлежащий дочернему процессу дескриптор был унаследован им, как это имеет место в случае дескрипторов 1 и 3, то значения дескрипторов в обоих процессах будут одинаковыми.

Однако подобные дескрипторы могут иметь и различные значения. Так, на файл D указывают два дескриптора, причем процесс 2 получил дескриптор за счет вызова функции CreateFile, а не путем наследования. Наконец, возможны ситуации, когда один из процессов имеет дескриптор объекта, а второй — не имеет, что наблюдается для файлов В и Е. Так происходит в тех случаях, когда дескриптор создается дочерним процессом или дублируется из одного процесса в другой, о чем говорится в разделе "Дублирование дескрипторов". 

Системное программирование в среде Windows

Рис. 6.2. Таблицы дескрипторов объектов для двух процессов

Счетчики дескрипторов процессов

Распространенной ошибкой программистов является пренебрежение закрытием дескрипторов после того, как необходимость в них отпала; это может стать причиной утечки ресурсов, что, в свою очередь, может приводить к снижению производительности или сбоям в программе и даже влиять на другие процессы. В версии NT 5.1 добавлена новая функция, позволяющая определить количество открытых дескрипторов, принадлежащих указанному процессу. Таким способом вы можете контролировать как собственный, так и другие процессы.

Приведенное ниже определение упомянутой функции не нуждается в отдельных пояснениях:

BOOL GetProcessHandleCount( HANDLE hProcess, PDWORD pdwHandleCount) 

Идентификаторы процессов

Процесс может получить идентификатор и дескриптор нового дочернего процесса из структуры PROCESS_INFORMATION. Разумеется, закрытие дескриптора дочернего процесса не приводит к уничтожению самого процесса; становится невозможным лишь доступ к нему со стороны родительского процесса. Для получения идентификационной информации о текущем процессе служат две функции. 

HANDLE GetCurrentProcess(VOID)

DWORD GetCurrentProcessId(VOID) 

В действительности функция GetCurrentProcess возвращает псевдодескриптор (pseudohandle), который не является наследуемым. Это значение может использоваться вызывающим процессом всякий раз, когда ему требуется его собственный дескриптор. Реальный дескриптор процесса создается на основе идентификатора (ID) процесса, включая и тот, который возвращается функцией GetCurrentProcessID, путем использования функции OpenProcess. Как и в случае любого разделяемого объекта, при отсутствии надлежащих разрешений доступа попытка открытия объекта процесса окажется неуспешной. 

HANDLE OpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId)

Возвращаемое значение: в случае успешного завершения — дескриптор процесса, иначе — NULL. 

Параметры

dwDesiredAccess — определяет права доступа к процессу. Некоторые из возможных значений этого параметра перечислены ниже.

• SYNCHRONIZE — разрешается использование дескриптора процесса в функциях ожидания завершения процесса, которые описываются далее в этой главе.

• PROCESS_ALL_ACCESS — устанавливаются все флаги доступа к процессу.

• PROCESS_TERMINATE — делает возможным завершение процесса с использованием функции TerminateProcess.

• PROCESS_QUERY_INFORMATION — разрешает использование дескриптора процесса в функциях GetExitCodeProcess и GetPriorityClass для получения информации о процессе.

bInheritHandle — позволяет указать, является ли новый дескриптор наследуемым. Параметр dwProcessID является идентификатором процесса, запрашивающего дескриптор.

Наконец, выполняющийся процесс может определить полный путь доступа к файлу исполняемого модуля, который использовался для его запуска, с помощью функций GetModuleFileName или GetModuleFileNameEx, при вызове которых значение параметра hModule должно устанавливаться равным NULL. При вызове этой функции из DLL будет возвращено имя файла DLL, а не .ЕХЕ-файла, который использует эту библиотеку DLL. 

Дублирование дескрипторов

Родительскому и дочернему процессам может требоваться различный доступ к объекту, идентифицируемому дескриптором, который наследует дочерний процесс. Кроме того, процессу вместо псевдодескриптора, получаемого с помощью функции GetModuleFileName или GetModuleFileNameEx, может потребоваться реальный, наследуемый дескриптор, который мог бы использоваться дочерним процессом. Родительский процесс может обеспечить это, создав копию дескриптора с желаемыми разрешениями доступа и свойствами наследования. Функция, позволяющая создавать копии дескрипторов, имеет следующий вид: 

BOOL DuplicateHandle(HANDLE hSourceProcessHandle, HANDLE hSourceHandle, HANDLE hTargetProcessHandle, LPHANDLE lphTargetHandle, DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwOptions) 

По завершении выполнения функции указатель lphTargetHandle будет указывать на копию исходного дескриптора, hSourceHandle. hSourceHandle является дескриптором дублируемого объекта в процессе, указанном дескриптором hSourceProcessHandle, и должен иметь права доступа PROCESS_DUP_HANDLE; если указанного дескриптора в исходном процессе не существует, функция DuplicateHandle завершается ошибкой. Новый дескриптор, на который указывает указатель lphTargetHandle, является действительным в целевом процессе, hTargetProcessHandle. Обратите внимание на то, что в нашем рассмотрении фигурировали три процесса, включая вызывающий. Часто в роли вызывающего процесса выступает целевой или исходный процесс, и тогда соответствующий дескриптор получают с помощью функции GetCurrentProcess. Заметьте также, что процесс может создать дескриптор в другом процессе; если вы это делаете, то вам потребуется механизм, с помощью которого можно было бы передать в другой процесс идентификационные данные нового дескриптора.

Функция DuplicateHandle может применяться к дескрипторам любого типа.

Если действие параметра dwDesiredAccess не отменяется флагом DUPLICATE_SAME_ACCESS параметра dwOptions, то у него может быть много возможных значений (для получения более подробных сведений обратитесь к библиотеке MSDN оперативного справочного руководства).

Параметр dwOptions может содержать любую комбинацию указанных ниже двух флагов.

• DUPLICATE_CLOSE_SOURCE — вызывает закрытие исходного дескриптора.

• DUPLICATE_SAME_ACCESS — вынуждает игнорировать параметр dwDesiredAccess. 

Напоминание

Ядро Windows поддерживает счетчики ссылок для всех объектов; этот счетчик представляет количество различных дескрипторов, ссылающихся на данный объект. В то же время, приложения не имеют доступа к этому счетчику. Любой объект не может быть уничтожен до тех пор, пока не будет закрыт его последний дескриптор, а счетчик ссылок не примет нулевое значение. Унаследованные и продублированные дескрипторы считаются отличными от исходных и также учитываются в счетчике ссылок. Наследуемые дескрипторы используются в программе 6.1 далее в этой главе. В то же время, дескрипторы, переданные из одного процесса в другой посредством той или иной формы механизма IPC, не считаются независимыми, и поэтому если один процесс закрывает такой дескриптор, то другие процессы использовать его не могут. Подобной методикой пользуются редко, однако в упражнении 6.2 вам предлагается передать значение унаследованного дескриптора из одного процесса в другой, используя механизм IPC.

Далее вы узнаете о том, как определить, завершено ли выполнение процесса.

Завершение и прекращение выполнения процесса

После того как процесс завершил свою работу, он, или, точнее, выполняющийся в этом процессе поток, может вызвать функцию ExitProcess, указав в качестве параметра кодом завершения (exit code):

VOID ExitProcess(UINT uExitCode) 

Эта функция не осуществляет возврата. Вместо этого она завершает вызывающий процесс и все его потоки. Обработчики завершения игнорируются, но делаются все необходимые вызовы точек входа DllMain (см. главу 5) с кодом отключения от библиотеки. Код завершения связывается с процессом. Выполнение оператора return в основной программе с использованием кода возврата равносильно вызову функции ExitProcess, в котором этот код возврата указан в качестве кода завершения.

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

BOOL GetExitCodeProcess(HANDLE hProcess, LPDWORD lpExitCode) 

Процесс, идентифицируемый дескриптором hProcess, должен обладать правами доступа PROCESS_QUERY_INFORMATION (см. описание функции OpenProcess, которая нами уже обсуждалась). lpExitCode указывает на переменную типа DWORD, которая принимает значение кода завершения. Одним из ее возможных значений является STILL_ACTIVE, означающее, что данный процесс еще не завершился. 

Наконец, один процесс может прекратить выполнение другого процесса, если у дескриптора завершаемого процесса имеются права доступа PROCESS_TERMINATE. При вызове функции завершения процесса указывается код завершения. 

BOOL TerminateProcess(HANDLE hProcess, UINT uExitCode) 

Предостережение

Прежде чем завершить выполнение процесса, убедитесь в том, что все ресурсы, которые он мог разделять с другими процессами, освобождены. В частности, должны быть корректно обработаны ресурсы синхронизации, о которых говорится в главе 8 (мьютексы, семафоры и события). В этом отношении могут оказаться полезными SEH (глава 4), а вызов функции ExitProcess может быть осуществлен из обработчика. В то же время, при вызове функции ExitProcess обработчики __finally и __except не выполняются, поэтому в идее завершения выполнения изнутри программы нет ничего хорошего. Особенно рискованно применять функцию TerminateProcess, поскольку у завершаемого процесса в этом случае отсутствует возможность выполнить свои SEH или вызвать функции DllMain связанных с ним библиотек DLL. Ограниченной альтернативой являются обработчики управляющих сигналов консоли, обеспечивающие возможность передачи сигнала одним процессом другому, который после этого может корректно организовать свое завершение.

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

Процессы UNIX имеют свои идентификаторы, pid, которые сопоставимы с идентификаторами процессов Windows. Функция getpid аналогична функции GetCurrentProcessID, но эквивалентов функциям getppid и getgpid в Windows не находится ввиду отсутствия предков процессов и групп процессов.

И, наоборот, в UNIX отсутствуют дескрипторы процессов, и поэтому в ней нет функций, которые можно было бы сравнить с функциями GetCurrentProcess или OpenProcess.

В UNIX допускается использование дескрипторов (descriptors) открытых файлов после вызова функции exec, если для дескриптора файла не был установлен флаг close-on-exec. Это правило применимо только к дескрипторам файлов, которые, в силу вышесказанного, можно сравнить с наследуемыми дескрипторами (handles) файлов Windows.

Функция UNIX exit, которая фактически является функцией библиотеки С, аналогична функции ExitProcess; чтобы прекратить выполнение другого процесса ему следует послать сигнал SIGKILL. 

Ожидание завершения процесса

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

• Функции ожидания могут работать с самыми различными типами объектов; дескрипторы процессов являются лишь самым первым из рассматриваемых нами примеров применения этих функций.

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

• Существует возможность устанавливать конечный интервал ожидания (time-out).

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

DWORD WaitForSingleObject(HANDLE hObject, DWORD dwMilliseconds) 

DWORD WaitForMultipleObjects(DWORD nCount, CONST HANDLE *lpHandles, BOOL fWaitAll, DWORD dwMilliseconds) 

Возвращаемое значение: указывает причину завершения ожидания или, в случае ошибки, равно 0xFFFFFFFF (для получения более подробной информации используйте функцию GetLastError).

В аргументах этих функций указывается либо дескриптор одиночного процесса (hObject), либо дескрипторы ряда отдельных объектов, хранящиеся в массиве, на который указывает указатель lpHandles. Значение параметра nCount, определяющего размер массива, не должно превышать значение MAXIMUM_WAIT_OBJECTS (определено равным 64 в файле WINNT.Н).

dwMilliseconds — число миллисекунд интервала ожидания. Если значение этого параметра равно 0, то возврат из функции осуществляется сразу же после проверки состояния указанного объекта, что позволяет программе опрашивать процессы для определения их состояния завершения. Если же значение этого параметра равно INFINITE, то ожидание длится до тех пор, пока ожидаемый процесс не завершится.

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

Возможными возвращаемыми значениями этой функции в случае ее успешного завершения являются следующие:

• WAIT_OBJECT_0 — означает, что указанный объект перешел в сигнальное состояние (в случае функции WaitForSingleObject) или что одновременно все nCount объектов перешли в сигнальное состояние (в специальном случае функции WaitForMultipleObject, когда значение параметра fWaitAll равно TRUE).

• WAIT_OBJECT_0+n, где 0 ≤ n < nCount — вычтите значение WAIT_OBJECT_0 из возвращенного значения, чтобы определить, выполнение какого именно процесса завершилось, когда ожидается завершение выполнения любого из группы процессов. Если в сигнальное состояние перешли несколько объектов, то возвращается наименьшее из возможных значений. WAIT_ABANDONED является возможным базовым значением в случае использования дескрипторов мьютексов; см. главу 8.

• WAIT_TIMEOUT — указывает на то, что в течение отведенного периода ожидания сигнализируемый объект (объекты) не смогли удовлетворить условию ожидания.

• WAIT_FAILED — означает неудачное завершение функции, вызванное, например, тем, что у дескриптора отсутствовали права доступа SYNCHRONIZE.

• WAIT_ABANDONED_0 — это значение невозможно в случае процессов и рассматривается в главе 8 при рассмотрении мьютексов.

Для определения кода завершения процесса используется функция GetExitCodeProcess, описанная в предыдущем разделе.

Блоки и строки окружения

Схема, представленная на рис. 6.1, включает блок окружения процесса. Блок окружения (environment block) процесса содержит последовательность строк вида:

Имя = Значение

Каждая строка окружения (environment string), будучи символьной строкой, заканчивается нулевым символом, а весь блок строк в целом завершается дополнительным нулевым символом. Одним из примеров широко используемых переменных среды является переменная PATH. 

Чтобы передать информацию об окружении из родительского процесса в дочерний, параметр lpEnvironment при вызове функции CreateProcess следует установить равным NULL. В свою очередь, любой процесс может запросить или изменить свои переменные окружения или добавить новые в блок окружения.

Для получения, а также создания новых или изменения существующих переменных окружения используются следующие функции: 

DWORD GetEnvironmentVariable(LPCTSTR lpName, LPTSTR lpValue, DWORD cchValue)

BOOL SetEnvironmentVariable(LPCTSTR lpName, LPCTSTR lpValue) 

lpName — указатель на строку, содержащую имя переменной окружения. После определения переменной окружения она добавляется в блок окружения при условии, что такая переменная ранее не существовала, а определяемое значение не равно NULL. Если же определяемое значение равно NULL, то переменная удаляется из блока. Строка значения не может содержать символы "=".

В случае успешного завершения функция GetEnvironmentVariable возвращает длину строки значения переменной окружения, иначе — 0. Если размер буфера lpValue, указанный значением параметра cchValue, оказался недостаточно большим, то возвращаемое значение равно количеству символов, которое фактически требуется для сохранения значения переменной. Вспомните, что аналогичный механизм используется и в функции GetCurrentDirectory (глава 2).

Защита процесса

Обычно функция CreateProcess предоставляет права доступа к процессу на уровне PROCESS_ALL_ACCESS. Однако имеется возможность определения детализированных прав доступа, из которых в качестве примера можно назвать права доступа PROCESS_QUERY_INFORMATION, CREATE_PROCESS, PROCESS_TERMINATE, PROCESS_SET_INFORMATION, DUPLICATE_HANDLE и CREATETHREAD. В частности, с учетом возможных рисков, которые могут подстерегать вас в случае принудительного завершения выполняющихся процессов, на что мы уже неоднократно обращали ваше внимание, может оказаться полезным ограничить предоставление прав доступа к процессам на уровне PROCESS_TERMINATE для родительского процесса. Подробнее об атрибутах защиты процессов и других объектов говорится в главе 15. 

В UNIX для ожидания завершения процессов используются функции wait и waitpid, однако отсутствует понятие интервала ожидания, хотя функция waitpid может опрашивать процессы (существует возможность ее вызова без блокировки). Эти функции способны ожидать лишь завершения дочерних процессов, и эквивалентных им функций, применимых к ряду процессов, не существует, хотя и возможно ожидание завершения всех процессов, относящихся к одной группе. Кроме того, имеется одно незначительное отличие, заключающееся в том, что функции wait и waitpid возвращают код завершения сами, в результате чего отпадает необходимость в вызове отдельной функции, эквивалентной функции GetExitCodeProcess.

Строки окружения, аналогичные строкам окружения Windows, поддерживаются и в UNIX. Функция getenv (входящая в библиотеку С) имеет те же самые функциональные возможности, что и функция GetEnvironmentVariable, но программист сам должен заботиться о необходимом размере буфера. Функции putenv, setenv и unsetenv обеспечивают различные способы добавления, изменения и удаления переменных окружения и их значений, предлагая функциональность, аналогичную функциональности SetEnvironmentVariable.

Пример: параллельный поиск указанного текстового шаблона

Настало время посмотреть на процессы Windows в действии. Приведенная ниже в качестве примера программа grepMP создает процессы для поиска указанного текстового шаблона в файлах, по одному процессу на каждый файл. Эта программа моделирует UNIX-утилиту grep, хотя используемая нами методика применима к любой программе, которая полагается на стандартный вывод. Рассматривайте программу поиска как "черный ящик" и считайте, что она является просто исполняемой программой, выполнение которой должно контролироваться родительским процессом.

Командная строка программы имеет следующий вид:

grepMP шаблон F1 F2 … FN

Программа 6.1 выполняет следующие виды обработки:

• Для поиска указанного шаблона в каждом из входных файлов, от F1 до FN, используется отдельный процесс, запускающий один и тот же исполняе мый модуль. Для каждого процесса программа создает командную строку такого вида: grep шаблон FK.

• Полю hStdOut структуры STARTUPINFO нового процесса присваивается значение дескриптора временного файла, который определяется как наследуемый.

• Программа организует ожидание завершения всех процессов поиска, используя для этого функцию WaitForMultipleObjects.

• По завершении всех процессов поиска осуществляется поочередный вывод результатов (временных файлов). Вывод временного файла осуществляет процесс, выполняющий утилиту cat (программа 2.3).

• Возможности функции WaitForMultipleObjects ограничиваются лишь максимально допустимым количеством дескрипторов, которое устанавливается значением MAXIMUM_WAIT_OBJECTS (64), поэтому она вызывается многократно.

• Для определения успешности попытки нахождения данным процессом заданного шаблона программа использует код завершения процесса grep.

Порядок обработки файлов программой 6.1 иллюстрируется на рис. 6.3. 

Системное программирование в среде Windows

Рис. 6.З. Поиск текстового шаблона в файлах с использованием нескольких процессов


Программа 6.1. grepMP: выполнение параллельного поиска текстового шаблона 

/* Глава 6. grepMP. */

/* Версия команды grep, использующая несколько процессов. */

#include "EvryThng.h"


int _tmain(DWORD argc, LPTSTR argv[])

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

{

 HANDLE hTempFile;

 SECURITY_ATTRIBUTES StdOutSA = /* Атрибуты защиты для наследуемого дескриптора. */

  {sizeof(SECURITY_ATTRIBUTES), NULL, TRUE};

 TCHAR CommandLine[MAX_PATH + 100];

 STARTUPINFO StartUpSearch, Startup;

 PROCESS_INFORMATION ProcessInfo;

 DWORD iProc, ExCode;

 HANDLE *hProc; /* Указатель на массив дескрипторов процессов. */

 typedef struct {TCHAR TempFile[MAX_PATH];} PROCFILE;

 PROCFILE *ProcFile; /* Указатель на массив имен временных файлов. */

 GetStartupInfo(&StartUpSearch);

 GetStartupInfo(&StartUp);

 ProcFile = malloc((argc – 2) * sizeof(PROCFILE));

 hProc = malloc((argc – 2) * sizeof(HANDLE));

 /* Создать для каждого файла отдельный процесс "grep". */

 for (iProc = 0; iProc < argc – 2; iProc++) {

  _stprintf(CommandLine, _T("%s%s %s"), _T("grep "), argv[1], argv[iProc + 2]);

  GetTempFileName(_T("."), _T("gtm"), 0, ProcFile[iProc].TempFile); /* Для хранения результатов поиска.*/

  hTempFile = /* Этот дескриптор является наследуемым */

   CreateFile(ProcFile[iProc].TempFile, GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, &StdOutSA, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

  StartUpSearch.dwFlags = STARTF_USESTDHANDLES;

  StartUpSearch.hStdOutput = hTempFile;

  StartUpSearch.hStdError = hTempFile;

  StartUpSearch.hStdInput = GetStdHandle(STD_INPUT_HANDLE);

  /* Создать процесс для выполнения командной строки. */

  CreateProcess(NULL, CommandLine, NULL, NULL, TRUE, 0, NULL, NULL, &StartUpSearch, &ProcessInfo);

  /* Закрыть ненужные дескрипторы. */

  CloseHandle(hTempFile);

  CloseHandle(ProcessInfo.hThread);

  hProc[iProc] = ProcessInfo.hProcess;

 }

 /* Выполнить все процессы и дождаться завершения каждого из них. */

 for (iProc = 0; iProc < argc – 2; iProc += MAXIMUM_WAIT_OBJECTS) WaitForMultipleObjects( /* Разрешить использование достаточно большого количества процессов */

  min(MAXIMUM_WAIT_OBJECTS, argc – 2 – iProc), &hProc [iProc], TRUE, INFINITE);

 /* Переслать результирующие файлы на стандартный вывод с использованием утилиты cat */ 

 for (iProc = 0; iProc < argc – 2; iProc++) {

  if (GetExitCodeProcess(hProc[iProc], &ExCode) && ExCode==0) {

   /* Обнаружен шаблон — Вывести результаты. */

   if (argc > 3) _tprintf(_T("%s:\n"), argv [iProc + 2]);

   fflush(stdout); /* Использование стандартного вывода несколькими процессами. */

   _stprintf(CommandLine, _T("%s%s"), _Т("cat "), ProcFile[iProc].TempFile);

   CreateProcess(NULL, CommandLine, NULL, NULL, TRUE, 0, NULL, NULL, &StartUp, &ProcessInfo);

   WaitForSingleObject(ProcessInfo.hProcess, INFINITE);

   CloseHandle(ProcessInfo.hProcess);

   CloseHandle(ProcessInfo.hThread);

  }

  CloseHandle(hProc [iProc]);

  DeleteFile(ProcFile[iProc].TempFile);

 }

 free(ProcFile);

 free(hProc);

 return 0;

} 

Процессы в многопроцессорной среде

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

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

Вместе с тем, существует возможность привязки процессов к определенным процессорам, что позволяет всегда быть уверенным в том, что другие процессоры остаются свободными и их можно использовать для решения каких-либо иных, критических задач. Это достигается за счет применения маски родства процессора (processor affinity mask) (см. главу 9) в объекте задачи. Объекты задач (job objects) описываются в одном из следующих разделов настоящей главы. 

Наконец, внутри процесса можно создавать независимые потоки, и для этих потоков также будет спланировано выполнение с использованием отдельных процессоров SMP для каждого из них. Связь между использованием потоков и показателями производительности обсуждается в главе 7.

Временные характеристики процесса

Воспользовавшись функцией GetProcessTimes, которая в Windows 9x отсутствует, можно получить различные временные характеристики процесса, а именно: истекшее время (elapsed time), время, затраченное ядром (kernel time), и пользовательское время (user time). 

BOOL GetProcessTimes(HANDLE hProcess, LPFILETIME lpCreationTime, LPFILETIME lpExitTime, LPFILETIME lpKernelTime, LPFILETIME lpUserTime) 

Дескриптор процесса может ссылаться как на процесс, который продолжает выполняться, так и на процесс, выполнение которого прекратилось. Вычитая время создания процесса (creation time) из времени завершения процесса (exit time), мы получаем истекшее время, как показано в следующем примере. Тип данных FILETIME является 64-битовым; для вычисления указанной разности объедините переменную этого типа с переменной тип LARGE_INTEGER в структуру типа union. Ранее преобразование и отображение отметок времени файлов было продемонстрировано в главе 3 на примере программы lsw.

Функция GetThreadTimes аналогична только что описанной, но требует указания дескриптора потока в качестве параметра. Управлению потоками посвящена глава 7.

Пример: временные характеристики процессов

Наш следующий пример (программа 6.2) представляет собой команду timep (от time print — вывод временных параметров), аналогичную UNIX-команде time (поскольку команда time поддерживается процессором командной строки, мы должны использовать для нашей команды другое имя). Программа позволяет вывести все три временные характеристики, однако в Windows 9x будет доступно лишь истекшее время процесса.

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

В данной программе используется функция Windows GetCommandLine, которая возвращает целую командную строку, а не отдельные строки из массива argv.

Кроме того, программа использует вспомогательную функцию SkipArg, которая просматривает командную строку и устанавливает в ней указатель в позицию, непосредственно следующую за именем исполняемого файла. Листинг функции SkipArg приведен в приложении А.

Для определения версии ОС в программе 6.2 используется функция GetVer-sionEx. В операционных системах Windows 9x и Windows СЕ доступным будет лишь истекшее время процесса. Программный код для этих систем представлен с той целью, чтобы показать, что в некоторых случаях работоспособность программ, по крайней мере — с частичным сохранением их функциональности, удается обеспечивать для целого диапазона различных версий Windows.

Программа 6.2. timep: временные характеристики процессов 

/* Глава 6. timep. */

#include "EvryThng.h"


int _tmain(int argc, LPTSTR argv[]) {

 STARTUPINFO Startup;

 PROCESS_INFORMATION ProcInfo;

 union { /* Эта структура используется для выполнения арифметических операций с участием временных параметров. */

  LONGLONG li;

  FILETIME ft;

 } CreateTime, ExitTime, ElapsedTime;

 FILETIME KernelTime, UserTime;

 SYSTEMTIME ElTiSys, KeTiSys, UsTiSys, StartTimeSys, ExitTimeSys;

 LPTSTR targv = SkipArg(GetCommandLine());

 OSVERSIONINFO OSVer;

 BOOL IsNT;

 HANDLE hProc;

 OSVer.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);

 GetVersionEx(&OSVer);

 IsNT = (OSVer.dwPlatformId == VER_PLATFORM_WIN32_NT);

 /* NT (все версии) возвращает VER_PLATFORM_WIN32_NT. */

 GetStartupInfo(&StartUp);

 GetSystemTime(&StartTimeSys);

 /* Выполнить командную строку; дождаться завершения процесса. */

 CreateProcess (NULL, targv, NULL, NULL, TRUE, NORMAL_PRIORITY_CLASS, NULL, NULL, &StartUp, &ProcInfo);

 /* Убедиться в наличии ВСЕХ НЕОБХОДИМЫХ прав доступа к процессу. */

 DuplicateHandle(GetCurrentProcess(), ProcInfo.hProcess, GetCurrentProcess(), &hProc, PROCESS_QUERY_INFORMATION | SYNCHRONIZE, FALSE, 0); 

 WaitForSingleObject(hProc, INFINITE);

 GetSystemTime (&ExitTimeSys);

 if (IsNT) { /* Windows NT. Для процесса вычисляется истекшее время, время выполнения в режиме ядра и время выполнения в пользовательском режиме. */

  GetProcessTimes(hProc, &CreateTime.ft, &ExitTime.ft, &KernelTime, &UserTime);

  ElapsedTime.li = ExitTime.li – CreateTime.li;

  FileTimeToSystemTime(&ElapsedTime.ft, &ElTiSys);

  FileTimeToSystemTime(&KernelTime, &KeTiSys);

  FileTimeToSystemTime(&UserTime, &UsTiSys);

  _tprintf(_T("Истекшее время: %02d:%02d:%02d:%03d\n"), ElTiSys.wHour, ElTiSys.wMinute, ElTiSys.wSecond, ElTiSys.wMilliseconds);

  _tprintf(_T("Пользовательское время: %02d:%02d:%02d:%03d\n"), UsTiSys.wHour, UsTiSys.wMinute, UsTiSys.wSecond, UsTiSys.wMilliseconds);

  _tprintf(_T("Системное время: %02d:%02d:%02d:%03d\n"), KeTiSys.wHour, KeTiSys.wMinute, KeTiSys.wSecond, KeTiSys.wMilliseconds);

 } else {

  /* Windows 9x и СЕ. Вычисляется лишь истекшее время. */

  …

 }

 CloseHandle(ProcInfo.hThread);

 CloseHandle(ProcInfo.hProcess);

 CloseHandle(hProc);

 return 0;

}

Использование команды timep

Теперь мы можем воспользоваться командой timep для анализа производительности различных вариантов программ копирования файлов и их преобразования из ASCII в Unicode, таких, например, как утилиты atou (программа 2.4) и sortMP (программа 5.5). Некоторые из полученных результатов и краткий их анализ представлены в приложении В.

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

Генерация управляющих событий консоли

Прерывание выполнения процесса извне может порождать проблемы, поскольку это лишает процесс возможности произвести необходимую завершающую обработку данных и очистку ресурсов. Воспользоваться SEH в данном случае нельзя ввиду того, что не существует общего метода, который позволял бы одному процессу возбуждать исключения в другом[25]. В то же время, с учетом некоторых ограничений, механизм управляющих событий консоли делает возможной передачу одним процессом другому управляющих сигналов, или событий, консоли. В программе 4.5 было продемонстрировано, как установить обработчик для перехвата сигналов и организовать генерацию исключений этим обработчиком. В указанном примере сигнал генерировался по приказу пользователя средствами пользовательского интерфейса.

Таким образом, вполне можно добиться того, чтобы один процесс генерировал сигнал, соответствующий определенному событию, в другом указанном процессе или группе процессов. Вспомните флаг CREATE_NEW_PROCESS_GROUP функции CreateProcess. Если этот флаг установлен, то идентификатор нового процесса идентифицирует группу процессов и является корневым (root) процессом данной группы. Все новые процессы, создаваемые данным родительским процессом, будут автоматически попадать в эту группу до тех пор, пока при вызове функции CreateProcess не будет использован флаг CREATE_NEW_PROCESS_GROUP. Сгруппированные процессы аналогичны группам процессов в UNIX.

Процесс может генерировать события CTRL_C_EVENT или CTRL_BREAK_EVENT в указанной группе процессов, идентифицируя ее с помощью идентификатора корневого процесса. Консоль целевых процессов должна совпадать с консолью процесса, генерирующего событие. В частности, вызывающий процесс не может быть создан с использованием собственной консоли (посредством флагов CREATE_NEW_CONSOLE или DETACHED_PROCESS). 

BOOL GenerateConsoleCtrlEvent(DWORD dwCtrlEvent, DWORD dwProcessGroup) 

Тогда значением первого параметра должно быть либо CTRL_C_EVENT, либо CTRL_BREAK_EVENT. Второй параметр идентифицирует группу процессов. 

Пример: простое управление задачами

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

• jobbg — использует остальную часть командной строки в качестве командной строки для нового процесса, или задачи (job), однако возврат из команды осуществляется немедленно, без ожидания завершения нового процесса. По желанию пользователя новый процесс может либо получить собственную консоль, либо выполняться как отсоединенный (detached) процесс, то есть как процесс, связь с которым не поддерживается. Этот подход аналогичен запуску команд UNIX с указанием опции & в конце команды.

• jobs — выводит список текущих активных задач, снабжая каждую из задач порядковым номером и идентификатором процесса. Эта команда аналогична одноименной команде UNIX.

• kill — прекращает выполнение задачи. В данной реализации используется функция TerminateProcess, которая, как ранее уже отмечалось, не обеспечивает корректного завершения задачи, сопровождающегося "уборкой мусора". Доступна также опция, позволяющая передавать управляющие сигналы консоли.

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

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

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

В полном варианте программы, находящемся на Web-сайте книги, содержится ряд дополнительных возможностей, не представленных в приводимых листингах, например, возможность получения входных данных для командной строки из файла. Программа JobDhell послужит основой для создания более общего "процессора служб" ("service processor") в главе 13 (программа 13.3). Службы NT являются фоновыми процессами, обычно — серверами, управление которыми осуществляется командами запуска, остановки, приостановки, а также другими командами. 

Создание фоновых задач

Программа 6.3 реализует процессор задач, в котором пользователю предлагается ввести одну из трех возможных команд для их дальнейшего выполнения программой. В этой программе используется набор функций управления задачами, представленный программами 6.4, 6.5 и 6.6.

Программа 6.3. JobShell: создание, вывод списка и прекращение выполнения фоновых задач 

/* Глава 6. */

/* JobShell.с – команды управления задачами:

   jobbg — Выполнить задачу в фоновом режиме.

   jobs — Вывести список всех фоновых задач.

   kill — Прекратить выполнение указанной задачи из семейства задач.

   Существует опция, позволяющая генерировать управляющие сигналы консоли. */

#include "EvryThng.h"

#include "JobMgt.h"


int _tmain(int argc, LPTSTR argv[]) {

 BOOL Exit = FALSE;

 TCHAR Command[MAX_COMMAND_LINE + 10], *pc;

 DWORD i, LocArgc; /* Локальный параметр argc. */

 TCHAR argstr[MAX_ARG][MAX_COMMAND_LINE];

 LPTSTR pArgs[MAX_ARG];

 for (i = 0; i < MAX_ARG; i++) pArgs[i] = argstr[i];

 /* Вывести подсказку пользователю, считать команду и выполнить ее. */

 _tprintf(_Т("Управление задачами Windows\n"));

 while (!Exit) {

  _tprintf(_T("%s"), _T("JM$"));

  _fgetts(Command, MAX_COMMAND_LINE, stdin);

  pc = strchr(Command, '\n');

  *pc = '\0';

  /* Выполнить синтаксический разбор входных данных с целью получения командной строки для новой задачи. */

  GetArgs(Command, &LocArgc, pArgs); /* См. Приложение А. */

  CharLower(argstr[0]);

  if(_tcscmp(argstr[0], _T("jobbg")) == 0) {

   Jobbg(LocArgc, pArgs, Command);

  } else if(_tcscmp(argstr[0], _T("jobs")) == 0) {

   Jobs(LocArgc, pArgs, Command);

  } else if(_tcscmp(argstr[0], _T("kill")) == 0) {

   Kill(LocArgc, pArgs, Command);

  } else if(_tcscmp(argstr[0], _T("quit")) == 0) {

   Exit = TRUE;

  } else _tprintf(_T("Такой команды не существует. Повторите ввод\n"));

 }

 return 0;

}


/* jobbg [параметры] командная строка [Параметры являются взаимоисключающими]

 –с: Предоставить консоль новому процессу.

 -d: Отсоединить новый процесс без предоставления ему консоли.

 Если параметры не заданы, процесс разделяет консоль с jobbg. */

int Jobbg(int argc, LPTSTR argv[], LPTSTR Command) {

 DWORD fCreate;

 LONG JobNo;

 BOOL Flags[2];

 STARTUPINFO Startup;

 PROCESS_INFORMATION ProcessInfo;

 LPTSTR targv = SkipArg(Command);

 GetStartupInfo(&StartUp);

 Options(argc, argv, _T("cd"), &Flags[0], &Flags[1], NULL);

 /* Пропустить также поле параметра, если он присутствует. */

 if (argv[1][0] == '-') targv = SkipArg(targv);

 fCreate = Flags[0] ? CREATE_NEW_CONSOLE : Flags [1] ? DETACHED_PROCESS : 0;

 /* Создать приостановленную задачу/поток. Возобновить выполнение после ввода номера задачи. */

 CreateProcess(NULL, targv, NULL, NULL, TRUE, fCreate | CREATE_SUSPENDED | CREATE_NEW_PROCESS_GROUP, NULL, NULL, &StartUp, &ProcessInfo);

 /* Создать номер задачи и ввести ID и дескриптор процесса в "базу данных" задачи. */

 JobNo = GetJobNumber(&ProcessInfo, targv); /* См. "JobMgt.h" */

 if (JobNo >= 0) ResumeThread(ProcessInfo.hThread);

 else {

  TerminateProcess(ProcessInfo.hProcess, 3);

  CloseHandle(ProcessInfo.hProcess);

  ReportError(_T("Ошибка: Не хватает места в списке задач."), 0, FALSE);

  return 5;

 }

 CloseHandle(ProcessInfo.hThread);

 CloseHandle(ProcessInfo.hProcess);

 _tprintf(_T(" [%d] %d\n"), JobNo, ProcessInfo.dwProcessId);

 return 0;

}


/* jobs: вывод списка всех выполняющихся и остановленных задач. */

int Jobs(int argc, LPTSTR argv[], LPTSTR Command) {

 if (!DisplayJobs ()) return 1; /*См. описание функций управления задачами*/

 return 0;

}


/* kill [параметры] Номер задачи (JobNumber)

 –b: Генерировать Ctrl-Break.

 –с: Генерировать Ctrl-C.

 В противном случае прекратить выполнение процесса. */

int Kill(int argc, LPTSTR argv[], LPTSTR Command) {

 DWORD ProcessId, JobNumber, iJobNo;

 HANDLE hProcess;

 BOOL CntrlC, CntrlB, Killed;

 iJobNo = Options(argc, argv, _T("bc"), &CntrlB, &CntrlC, NULL);

 /* Найти ID процесса, связанного с данной задачей. */

 JobNumber = _ttoi(argv [iJobNo]);

 ProcessId = FindProcessId(JobNumber); /* См. описание функций управления задачами. */

 hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);

 if (hProcess == NULL) { /* ID процесса может не использоваться. */

  ReportError(_T("Выполнение процесса уже прекращено.\n"), 0, FALSE);

  return 2;

 }

 if (CntrlB) GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, ProcessId);

 else if (CntrlC) GenerateConsoleCtrlEvent(CTRL_C_EVENT, ProcessId);

 else TerminateProcess(hProcess, JM_EXIT_CODE);

 WaitForSingleObject(hProcess, 5000);

 CloseHandle(hProcess);

 _tprintf(T("Задача [%d] прекращена или приостановлена \n"), JobNumber);

 return 0;

} 

Обратите внимание на то, как команда jobbg создает процесс в приостановленном состоянии, а затем вызывает функцию управления задачами Get JobNumber (программа 6.4) для получения номера задачи, а также регистрации задачи и процесса, который с ней связан. Если в силу каких-либо причин задача не может быть зарегистрирована, выполнение данного процесса немедленно прекращается. Обычно' генерируется корректный номер задачи, после чего выполнение основного потока возобновляется, и он может продолжать выполнение.

Получение номера задачи

Следующие три программы представляют три отдельные функции управления задачами. Все эти функции включены в единый файл JobMgt.c, содержащий все исходные тексты.

Первая из них, программа 6.4, представляет функцию Get JobNumber. Обратите внимание на использование блокирования файлов, а также обработчиков завершения, осуществляющих разблокирование файлов. Эта методика обеспечивает защиту от исключений и непреднамеренного обхода вызова функции разблокирования файлов. Переходы такого рода могут быть случайно вставлены в процессе сопровождения кода, даже если исходная программа корректна. Обратите также внимание на блокирование попыток записи за пределами конца файла в тех случаях, когда файл должен быть расширен за счет добавления новой записи.

Программа 6.4. JobMgt: создание информации о новой задаче 

/* Вспомогательная функция управления задачами. */

#include "EvryThng.h"

#include "JobMgt.h" /* Листинг приведен в приложении А. */

void GetJobMgtFileName (LPTSTR);


LONG GetJobNumber(PROCESS_INFORMATION *pProcessInfo, LPCTSTR Command)

/* Создать номер задачи для нового процесса и ввести информацию о новом процессе в базу данных задачи. */

{

 HANDLE hJobData, hProcess;

 JM_JOB JobRecord;

 DWORD JobNumber = 0, nXfer, ExitCode, FsLow, FsHigh;

 TCHAR JobMgtFileName[MAX_PATH];

 OVERLAPPED RegionStart;

 if (!GetJobMgtFileName(JobMgtFileName)) return –1;

 /* Предоставление результата в виде строки "\tmp\UserName.JobMgt" */

 hJobData = CreateFile(JobMgtFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

 if (hJobData == INVALID_HANDLE_VALUE) return –1;

 /* Блокировать весь файл плюс одну возможную запись для получения исключительного доступа. */

 RegionStart.Offset = 0;

 RegionStart.OffsetHigh = 0;

 RegionStart.hEvent = (HANDLE)0;

 FsLow = GetFileSize(hJobData, &FsHigh);

 LockFileEx(hJobData, LOCKFILE_EXCLUSIVE_LOCK, 0, FsLow + SJM_JOB, 0, &RegionStart);

 __try {

  /* Чтение записи для нахождения пустого сегмента. */

  while(ReadFile(hJobData, &JobRecord, SJM_JOB, &nXfer, NULL) && (nXfer > 0)) {

   if (JobRecord.ProcessId == 0) break;

   hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, JobRecord.ProcessId);

   if (hProcess == NULL) break;

   if (GetExitCodeProcess(hProcess, &ExitCode) && (ExitCode != STILL_ACTIVE)) break;

   JobNumber++;

  } 

  /* Либо найден пустой сегмент, либо мы находимся в конце файла и должны создать новый сегмент. */

  if (nXfer != 0) /* Не конец файла. Резервировать. */

   SetFilePointer(hJobData, –(LONG)SJM_JOB, NULL, FILE_CURRENT);

  JobRecord.ProcessId = pProcessInfo->dwProcessId;

  _tcsnccpy(JobRecord.CommandLine, Command, MAX_PATH);

  WriteFile(hJobData, &JobRecord, SJM_JOB, &nXfer, NULL);

 } /* Конец try-блока. */

 __finally {

  UnlockFileEx(hJobData, 0, FsLow + SJM_JOB, 0, &RegionStart);

  CloseHandle(hJobData);

 }

 return JobNumber + 1;

}

Вывод списка фоновых задач

Программа 6.5 реализует функцию управления задачами DisplayJobs.

Программа 6.5. JobMgt: отображение списка активных задач 

BOOL DisplayJobs(void)

/* Просмотреть файл базы данных, сообщить статус задачи. */

{

 HANDLE hJobData, hProcess;

 JM_JOB JobRecord;

 DWORD JobNumber = 0, nXfer, ExitCode, FsLow, FsHigh;

 TCHAR JobMgtFileName[MAX_PATH];

 OVERLAPPED RegionStart;

 GetJobMgtFileName(JobMgtFileName);

 hJobData = CreateFile(JobMgtFileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

 RegionStart.Offset = 0;

 RegionStart.OffsetHigh = 0;

 RegionStart.hEvent = (HANDLE)0;

 FsLow = GetFileSize(hJobData, &FsHigh);

 LockFileEx(hJobData, LOCKFILE_EXCLUSIVE_LOCK, 0, FsLow, FsHigh, &RegionStart);

 __try {

  while(ReadFile(hJobData, &JobRecord, SJM_JOB, &nXfer, NULL) && (nXfer > 0)) {

   JobNumber++;

   if (JobRecord.ProcessId == 0) continue; 

   hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, JobRecord.ProcessId);

   if (hProcess != NULL) GetExitCodeProcess(hProcess, &ExitCode);

   _tprintf(_T(" [%d] "), JobNumber);

   if (hProcess == NULL) _tprintf(_T(" Готово"));

   else if (ExitCode != STILL_ACTIVE) _tprintf(_T("+ Готово"));

   else _tprintf(_T(" "));

   _tprintf(_T(" %s\n"), JobRecord.CommandLine);

   /* Удалить процессы, которые в системе уже не присутствуют. */

   if (hProcess == NULL) {

    /* Зарезервировать одну запись. */

    SetFilePointer(hJobData, –(LONG)nXfer, NULL, FILE_CURRENT);

    JobRecord.ProcessId = 0;

    WriteFile(hJobData, &JobRecord, SJM_JOB, &nXfer, NULL);

   }

  } /* Конец цикла while. */

 } /* Конец __try-блока. */

 __finally {

  UnlockFileEx(hJobData, 0, FsLow, FsHigh, &RegionStart);

  CloseHandle(hJobData);

 }

 return TRUE;

}

Поиск задачи в файле списка задач

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

Программа 6.6. JobMgt: получение идентификатора процесса по номеру задачи 

DWORD FindProcessId(DWORD JobNumber)

/* Получить ID процесса для задачи с указанным номером. */

{

 HANDLE hJobData;

 JM_JOB JobRecord;

 DWORD nXfer;

 TCHAR JobMgtFileName[MAX_PATH];

 OVERLAPPED RegionStart;

 /* Открыть файл управления задачами. */

 GetJobMgtFileName(JobMgtFileName);

 hJobData = CreateFile(JobMgtFileName, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

 if (hJobData == INVALID_HANDLE_VALUE) return 0;

 /* Перейти к позиции записи, соответствующей указанному номеру задачи.

  * В полной версии программы обеспечивается принадлежность номера задачи (JobNumber) допустимому диапазону значений. */

 SetFilePointer(hJobData, SJM_JOB * (JobNumber – 1), NULL, FILE_BEGIN);

 /* Блокировка и чтение записи. */

 RegionStart.Offset = SJM_JOB * (JobNumber – 1);

 RegionStart.OffsetHigh =0; /* Предполагаем, что файл "короткий". */

 RegionStart.hEvent = (HANDLE)0;

 LockFileEx(hJobData, 0, 0, SJM_JOB,0, &RegionStart);

 ReadFile(hJobData, &JobRecord, SJM_JOB, &nXfer, NULL);

 UnlockFileEx(hJobData, 0, SJM_JOB, 0, &RegionStart);

 CloseHandle(hJobData);

 return JobRecord.ProcessId;

} 

Объекты задач

Процессы можно объединять в объекты задач (job objects), что позволяет управлять процессами как группой, устанавливать лимиты ресурсов для всех процессов, входящих в объект задачи, и вести учетную информацию. Объекты задач были впервые введены в Windows 2000 и теперь поддерживаются во всех системах NT5.

Первым шагом является создание пустого объекта задачи с помощью функции CreateObject, которая принимает два аргумента, имя и атрибуты защиты, и возвращает дескриптор объекта задачи. Существует также функция OpenJobObject, которую можно применять к именованным объектам задач. Для уничтожения объектов используется функция CloseHandle.

Функция AssignProcessToJobObject просто добавляет процесс с указанным дескриптором в объект задачи; она принимает только два параметра. Процесс может принадлежать только одной задаче, поэтому в тех случаях, когда процесс, связанный с указанным дескриптором, уже является элементом какого-либо задания, функция AssignProcessToJobObject завершается с ошибкой. Добавляемый в задачу процесс наследует значения всех ограничений, связанных с задачей, и добавляет в задачу свою учетную информацию, например использованное процессорное время.

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

Наконец, для установления управляющих лимитов процессов, входящих в задачу, используется функция SetInformationJobObject. 

BOOL SetInformationJobObject(HANDLE hJob, JOBOBJECTINFOCLASS JobObjectInformationClass, LPVOID lpJobObjectInformation, DWORD cbJobObjectInformationLength)

• hJob — дескриптор существующего объекта задачи.

• JobObjectInformationClass — указывает информационный класс устанавливаемых ограничений. Всего существует пять возможных значений; одним из них является значение JobObjectBasicLimitInformation, используемое для указания такой информации, как ограничения общего времени и времени, приходящегося на один процесс, ограничения размеров рабочего набора (working set)[26], ограничения на количество активных процессов, приоритет и родство процессоров (в SMP-системах родственными называются процессоры, которые могут использоваться потоками в процессах задач).

• lpJobObjectInformation — указывает на фактическую информацию, необходимую для предыдущего параметра. Для каждого информационного класса существует своя структура.

• JOBOBJECT_BASIC_ACCOUNTING_INFORMATION — позволяет получить суммарные временные характеристики (пользовательское, системное и истекшее время) процессов, входящих в задачу.

• Значением последнего параметра является размер предыдущей структуры.

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

Резюме

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

В следующих главах

В следующей главе описываются потоки, являющиеся независимыми единицами выполнения внутри процесса. В некоторых отношениях управление потоками аналогично управлению процессами; все, что связано с кодами завершения, прекращением выполнения и ожиданием завершения, применимо и к потокам. Чтобы продемонстрировать эту аналогию, самый первый из рассматриваемых в главе 7 примеров является переделанным вариантом программы grepMP (программа 6.1), который приспособлен для работы с потоками.

Глава 8 ознакомит вас с методами синхронизации, которые могут быть использованы для координации выполнения потоков, принадлежащих одному и тому же или различным процессам.

Упражнения

6.1. Расширьте возможности программы 6.1 (grepMP) таким образом, чтобы она принимала также параметры командной строки, а не только текстовый шаблон.

6.2. Вместо того чтобы передавать дочернему процессу имя временного файла, как это делается в программе 6.1, преобразуйте наследуемый дескриптор файла к типу DWORD (для типа HANDLE требуется 4 байта), а затем в строку символов. Передайте эту строку дочернему процессу в командной строке. В свою очередь, дочерний процесс должен осуществить обратное преобразование строки символов в значение дескриптора файла, который будет использован для вывода. Эту методику иллюстрируют программы catHA.с и grepHA.с, доступные на Web-сайте книги.

6.3. Программа 6.1 ожидает завершения всех процессов и лишь после этого выводит результаты. При этом возможность определения того, в каком именно порядке завершились процессы внутри программы, отсутствует. Модифицируйте программу таким образом, чтобы она определяла очередность завершения процессов. Подсказка. Измените вызов функции WaitForMultipleObjects таким образом, чтобы возврат из нее осуществлялся после завершения каждого отдельного процесса. Другой возможностью является сортировка времени завершения процессов.

6.4. В программе 6.1 временные файлы должны удаляться явным образом. Возможно ли использование флага FILE_FLAG_DELETE_ON_CLOSE при создании временных файлов таким образом, чтобы избавиться от необходимости удаления указанных файлов?

6.5. Определите, заметны ли какие-либо преимущества программы grepMP в отношении производительности (по сравнению с простой последовательной обработкой) в случае SMP-систем, если такая возможность у вас имеется, или при размещении файлов на отдельных или сетевых дисках. Частичные результаты соответствующих тестов приведены в приложении В. 

6.6. Можете ли вы предложить способ, возможно, связанный с использованием объектов задач, для определения времени, затраченного на выполнение операций в пользовательском режиме и в режиме ядра? Использование объектов задач может потребовать внесения изменений в программу grepMP.

6.7. Улучшите функцию grepMP (программа 6.5) таким образом, чтобы она сообщала код завершения для каждой завершенной задачи. Кроме того, организуйте вывод временных характеристик (истекшего времени, времени работы в режиме ядра и времени работы в пользовательском режиме) суммарно для всех процессов.

6.8. У функций управления задачами есть один трудно устранимый недостаток. Предположим, что задача уничтожена и что главная программа повторно использует идентификатор процесса данного задания еще до того, как этот идентификатор будет удален из файла управления задачами. Вместе с тем, ранее этот идентификатор мог быть использован функцией OpenProcess для создания дескриптора какого-либо процесса, хотя теперь этот же идентификатор ссылается на совершенно другой процесс. Чтобы устранить возможность появления проблем подобного рода, требуется создать вспомогательный процесс, в котором будут храниться копии дескрипторов каждого созданного процесса, что позволит избежать повторного использования идентификаторов. Другая возможная методика заключается в сохранении времени запуска процесса в файле управления задачами. Это время должно совпадать со временем запуска процесса, полученного с использованием идентификатора. Примечание. Идентификаторы процессов быстро исчерпываются, и поэтому вероятность их повторного использования очень велика. В UNIX для получения идентификаторов новых процессов применяются последовательно увеличиваемые значения 32-битового счетчика, так что идентификаторы могут повторяться только после исчерпания этих значений, что происходит очень редко. В отличие от этого, в программах Windows никогда нельзя полагаться на то, что идентификатор процесса не будет повторно использован.

6.9. Измените программу JobShell таким образом, чтобы информация сохранялась в реестре, а не во временном файле. 

6.10. Измените программу JobShell таким образом, чтобы процессы связывались с объектом задачи. Наложите временные и другого рода ограничения на объекты задач, предоставив пользователю возможность ввода числовых значений некоторых из этих ограничений.

6.11. Улучшите программу JobShell таким образом, чтобы команда jobs обеспечивала подсчет числа дескрипторов, используемых каждой из задач. Подсказка. Воспользуйтесь функцией GetProcessHandleCount, для которой требуется NT 5.1. 

6.12. Создайте проект Version (находится на Web-сайте), использующий программу verison.c. Попытайтесь произвести пробные запуски этой программы под управлением как можно большего числа различных версий Windows, к которым у вас имеется доступ, включая Windows 9x и NT 4.0, если это возможно. Каковы старшие и младшие номера версий для этих систем, полученные вами, и какую дополнительную информацию о версиях вам удалось получить?

ГЛАВА 7

Потоки и планирование выполнения

Основной единицей выполнения в Windows является поток, и одновременно несколько потоков могут выполняться в рамках одного процесса, разделяя его адресное пространство и другие ресурсы. В главе 6 процессы ограничивались только одним потоком, однако существует много ситуаций, в которых возможность использования более одного потока была бы весьма желательной. Настоящая глава посвящена описанию потоков и иллюстрации областей их применения. Глава 8 продолжает эту тему описанием объектов синхронизации и анализом как положительных, так и отрицательных аспектов использования потоков, в то время как в главе 9 исследуется проблема повышения производительности и компромиссные способы ее решения. В главе 10 описываются методы и модели программирования объектов синхронизации, позволяющие значительно упростить проектирование и разработку надежных многопоточных программ. Эти методы применяются далее во всей оставшейся части книги.

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

Обзор потоков

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

При традиционном подходе программы выполняются в виде единственного потока. Несмотря на возможность организации параллельного выполнения нескольких процессов, что было продемонстрировано на ряде примеров в главе 6, и даже их взаимодействия между собой посредством таких механизмов, как разделение памяти или каналы (глава 11), однопоточные процессы имеют ряд недостатков.

• Переключение между выполняющимися процессами потребляет заметную долю временных и других ресурсов ОС, а в случаях, аналогичных многопроцессному поиску (grepMP, программа 6.1), все процессы заняты выполнением одной и той же программы. Организация параллельной обработки файла с помощью потоков в контексте единственного процесса позволяет снизить общие накладные расходы системы.

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

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

• Тесно связанные с выполнением операций ввода/вывода программы, подобные рассмотренной в главе 2 программе преобразования файлов из ASCII в Unicode (atou, программа 2.4), вынуждены ограничиваться простой моделью "чтение-изменение-запись". При обработке последовательностей файлов гораздо эффективнее инициализировать выполнение как можно большего числа операций чтения. Windows NT предлагает дополнительные возможности перекрывающегося асинхронного ввода/вывода (глава 14), однако потоки позволяют добиться того же эффекта с меньшими усилиями.

• В SMP-системах планировщик Windows распределяет выполнение отдельных потоков между различными процессорами, что во многих случаях приводит к повышению производительности.

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

Перспективы и проблемы

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

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

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

• При определенных обстоятельствах вместо улучшения производительности может наблюдаться ее резкое ухудшение.

• Разделение потоками общей памяти и других ресурсов в контексте одного процесса может стать причиной нарушения условий состязаний между процессами и вызывать блокирование некоторых из них.

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

Основные сведения о потоках

В предыдущей главе на рис. 6.1 было показано, каким образом обеспечивается существование потоков в среде процесса. Использование потоков на примере многопоточного сервера, способного обрабатывать запросы одновременно нескольких клиентов, иллюстрирует рис. 7.1; каждому клиенту отведен поток. Эта модель будет реализована в главе 11.

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

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

• При создании потока вызывающий процесс может передать ему аргумент (Arg на рис. 7.1), который обычно является указателем. На практике этот аргумент помещается в стек потока.

• Каждый поток может распределять индексы собственных локальных областей хранения (Thread Local Storage, TLS), а также считывать и устанавливать значения TLS. TLS, описанные далее, предоставляют в распоряжение потоков небольшие массивы данных, и каждый из потоков может обращаться к собственной TLS. Одним из преимуществ TLS является то, что они обеспечивают защиту данных, принадлежащих одному потоку, от воздействия со стороны других потоков.

Системное программирование в среде Windows

Рис. 7.1. Потоки в среде сервера


Аргумент потока и TLS могут использоваться для указания произвольной структуры данных. Применительно к представленному на рис. 7.1 примеру сервера эта структура может содержать текущий запрос и отклик потока на этот запрос, а также предоставлять рабочую память для других целей.

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

Управление потоками

Вероятно, вы не будете удивлены, узнав о том, что у потоков, как и у любого другого объекта Windows, имеются дескрипторы и что для создания потоков, выполняющихся в адресном пространстве вызывающего процесса, предусмотрен системный вызов CreateThread. Как и в случае процессов, мы будем говорить иногда о "родительских" и "дочерних" потоках, хотя ОС не делает в этом отношении никаких различий. Системный вызов CreateThread предъявляет ряд специфических требований:

• Укажите начальный адрес потока в коде процесса.

• Укажите размер стека, и необходимое пространство стека будет выделено из виртуального адресного пространства процесса. Размер стека по умолчанию равен размеру стека основного потока (обычно 1 Мбайт). Первоначально для стека отводится одна страница (см. главу 5). Новые страницы стека выделяются по мере надобности до тех пор, пока стек не достигнет своего максимального размера, поэтому не сможет больше расти.

• Задайте указатель на аргумент, передаваемый потоку. Этот аргумент может быть чем угодно и должен интерпретироваться самим потоком.

• Функция возвращает значение идентификатора (ID) и дескриптор потока.

В случае ошибки возвращаемое значение равно NULL.

HANDLE CreateThread(LPSECURITY_ATTRIBUTES lpsa, DWORD dwStackSize, LPTHREAD_START_ROUTINE lpStartAddr, LPVOID lpThreadParm, DWORD dwCreationFlags, LPDWORD lpThreadId) 

Параметры

lpsa — указатель на уже хорошо знакомую структуру атрибутов защиты.

dwStackSize — размер стека нового потока в байтах. Значению 0 этого параметра соответствует размер стека по умолчанию, равный размеру стека основного потока.

lpStartAddr — указатель на функцию (принадлежащую контексту процесса), которая должна выполняться. Эта функция принимает единственный аргумент в виде указателя и возвращает 32-битовый код завершения. Этот аргумент может интерпретироваться потоком либо как переменная типа DWORD, либо как указатель. Функция потока (ThreadFunc) имеет следующую сигнатуру: 

DWORD WINAPI ThreadFunc(LPVOID) 

lpThreadParm — указатель, передаваемый потоку в качестве аргумента, который обычно интерпретируется потоком как указатель на структуру аргумента.

dwCreationFlags — если значение этого параметра установлено равным 0, то поток запускается сразу же после вызова функции CreateThread. Установка значения CREATE_SUSPENDED приведет к запуску потока в приостановленном состоянии, из которого поток может быть переведен в состояние готовности путем вызова функции ResumeThread.

lpThreadId — указатель на переменную типа DWORD, которая получает идентификатор нового потока; в Windows 9x и Windows NT 3.51 значение NULL для этого параметра устанавливать нельзя.

Любой поток процесса может сама завершить свое выполнение, вызвав функцию ExitThread, однако более обычным способом самостоятельного завершения потока является возврата из функции потока с использованием кода завершения в качестве возвращаемого значения. По завершении выполнения потока память, занимаемая ее стеком, освобождается. В случае если поток был создан в библиотеке DLL, будет вызвана соответствующая точка входа DllMain (глава 4) с указанием флага DLL_THREAD_DETACH в качестве "причины" этого вызова. 

VOID ExitThread(DWORD dwExitCode) 

Когда завершается выполнение последнего потока, завершается и выполнение самого процесса.

Выполнение потока также может быть завершено другим потоком с помощью функции TerminateThread, однако освобождения ресурсов потока при этом не происходит, обработчики завершения не выполняются и уведомления библиотекам DLL не посылаются. Лучше всего, когда поток сам завершает свое выполнение; применять для этого функцию TerminateThread крайне нежелательно. Функции TerminateThread присущи те же недостатки, что и функции TerminateProcess.

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

BOOL GetExitCodeThread(HANDLE hThread, LPDWORD lpExitCode) 

lpExitCode — будет содержать код завершения потока, указывающий на его состояние. Если поток еще не завершен, значение этой переменной будет равно STILL_ACTIVE.

Идентификация потоков

Функции, используемые для получения идентификаторов (ID) и дескрипторов потоков, напоминают те, которые используются для аналогичных целей в случае процессов. 

• GetCurrentThread — возвращает ненаследуемый псевдодескриптор вызывающего потока.

• GetCurrentThreadId — позволяет получить идентификатор потока, а не его дескриптор.

• GetThreadId — позволяет получить идентификатор потока, если известен его дескриптор; эта функция требует использования Windows Server 2003.

• OpenThread — создает дескриптор потока по известному идентификатору.

В программе JobShell (программа 6.3) нам очень пригодилась функция OpenProcess, и функция OpenThread может применяться для аналогичных целей.

Дополнительные функции управления потоками

Несмотря на то что функций управления потоками, которые мы выше обсуждали, вполне достаточно для большинства случаев, в том числе и для примеров, приведенных в этой книге, в Windows XP и Windows Server 2003 были введены две дополнительные функции. Их краткие описания представлены ниже.

1. Функция GetProcessIdOfThread, требующая использования Windows Server 2003, позволяет получать идентификатор процесса, которому принадлежит поток, по известному дескриптору потока. Вы могли бы задействовать эту функцию в программах, предназначенных для управления потоками, принадлежащими другим процессам, или взаимодействия с такими потоками. Если необходимо получить дескриптор процесса, применяйте для этого функцию OpenProcess.

2. Функция GetThreadIOPendingFlag позволяет определить, имеются ли у потока, на который указывает дескриптор, необслуженные запросы ввода/вывода. Например, поток мог быть заблокирован во время выполнения операции ReadFile. В качестве результата возвращается состояние потока во время выполнения данной функции; фактическое состояние может в любой момент измениться, если целевой поток завершает или начинает выполнение операции. Эта функция требует использования NT 5.1 и поэтому доступна лишь в Windows XP или Windows Server 2003.

Приостановка и возобновление выполнения потока

Для каждого потока поддерживается счетчик приостановок (suspend count), и выполнение потока может быть продолжено лишь в том случае, если значение этого счетчика равно 0. Поток может увеличивать или уменьшать значение счетчика приостановок другого потока с помощью функций SuspendThread и Resume-Thread. Вспомните, что поток можно создать в приостановленном состоянии со счетчиком приостановок равным 1. 

DWORD ResumeThread(HANDLE hThread)

DWORD SuspendThread(HANDLE hThread) 

В случае успешного выполнения обе функции возвращают предыдущее значение счетчика приостановок, иначе — 0xFFFFFFFF.

Ожидание завершения потока

Поток может дожидаться завершения выполнения другого потока точно так же, как потоки могут дожидаться завершения процесса, что обсуждалось в главе 6. В этом случае при вызове функций ожидания (WaitForSingleObject и WaitForMultipleObjects) вместо дескрипторов процессов следует использовать дескрипторы потоков. Заметьте, что не все дескрипторы в массиве, передаваемом функции WaitForMultipleObjects, должны быть обязательно одного и того же типа; например, в одном вызове могут быть одновременно указаны дескрипторы потоков, процессов и других объектов.

Допустимое количество объектов, одновременно ожидаемых функцией WaitForMultipleObjects, ограничено значением MAXIMUM_WAIT_OBJECTS (64), но при большом количестве потоков можно воспользоваться серией вызовов функций ожидания. Эта техника уже была продемонстрирована в программе 6.1; программы, приведенные в книге, ожидают завершения выполнения одиночных объектов, но на Web-сайте приведены полные решения.

Функция ожидания дожидается, пока объект, указанный дескриптором, не перейдет в сигнальное состояние. В случае потоков объект потока переводится в сигнальное состояние при помощи функций ExitThread и TerminateThread, что приводит к освобождению всех других потоков, дожидающихся перехода данного объекта в сигнальное состояние, включая и те потоки, которые могли оставаться в состоянии ожидания и впоследствии, после того, как поток завершится. Дескриптор потока, перешедший в сигнальное состояние, не выходит из этого состояния. То же самое остается справедливым и по отношению к дескрипторам процессов, но не относится к дескрипторам некоторых других объектов, например, мьютексов и событий (описываются в следующей главе).

Заметьте, что дожидаться перехода в сигнальное состояние одного и того же объекта могут одновременно несколько потоков. Аналогично, функция ExitProcess переводит в сигнальное состояние как сам процесс, так и все его потоки.

Удаленные потоки

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

У функции CreateRemoteThread есть одно очень интересное применение. Вместо того чтобы вызывать функцию TerminateProcess, управляющий процесс может создать поток, выполняющийся в другом процессе, который и организует корректное завершение этого процесса. Однако в главе 10 демонстрируется более безопасный метод, позволяющий одному потоку завершить другой с использованием асинхронного вызова процедур. 

Понятие о потоках твердо упрочилось во многих ОС, и исторически так сложилось, что многие поставщики и пользователи UNIX предоставляли собственные частные варианты их реализации. Были разработаны некоторые библиотеки, обеспечивающие многопоточную поддержку вне ядра. В настоящее время стандартом в этой области являются потоки POSIX Pthreads. Потоки Pthreads включены в частные варианты реализации UNIX и Linux и иногда считаются частью UNIX. Соответствующие системные вызовы отличаются от обычных системных вызовов UNIX наличием в именах префикса pthread. Потоки Pthreads поддерживаются также некоторыми другими системами, отличными от UNIX, такими, например, как Open VMS.

Системный вызов pthread_create эквивалентен вызову CreateThread, a системный вызов pthread_exit — вызову ExitThread. Для организации ожидания одним потоком завершения другого применяется системный вызов pthread_join. Потоки Pthreads предоставляют очень полезную функцию pthread_cancel, гарантирующую, в отличие от функции TerminateThread, выполнение обработчиков завершения и уничтожение ненужных дескрипторов. Возможность уничтожения потоков была бы в Windows крайне желательной, но в главе 10 представлен метод, обеспечивающий получение такого же эффекта.

Использование библиотеки С впотоках

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

Пример функции strtok показывает, почему при написании некоторых функций библиотеки С не учитывалась многопоточная поддержка. Функция strtok, просматривающая строку в поиске очередного вхождения определенной лексемы, поддерживает сохранение состояния (persistent state) между последовательными вызовами функции, и это состояние хранится в области статической памяти, совместный доступ к которой имеют все потоки, вызывающие эту функцию. 

Microsoft С решает эту проблему, предлагая реализацию библиотеки С под названием LIBCMT.LIB, которая обеспечивает многопоточную поддержку. Однако, это еще не все. Вы не должны использовать функцию CreateThread; для запуска потока и создания специфической для него области рабочей памяти библиотеки LIBCMT.LIB необходимо пользоваться специальной функцией С, а именно, функцией _beginthreadex. Для завершения потока вместо функции ExitThread применяется функция _endthreadex.

Примечание

В качестве упрощенного варианта функции _beginthreadex предусмотрена функция _beginthread, однако использовать ее не рекомендуется. Прежде всего, функция _beginthread не имеет ни атрибутов, ни флагов защиты и не возвращает идентификатор потока. Более того, в действительности она закрывает дескриптор потока, который создает, в результате чего возвращенное значение дескриптора может оказаться недействительным на момент его сохранения родительским потоком. Не следует вызывать и функцию _endthread; она не позволяет пользоваться возвращаемым значением.

Аргументы функции _beginthreadex в точности совпадают с аргументами функций Windows, однако типы данных Windows для этой функции не определены, и поэтому тип возвращаемого значения функции _beginthread необходимо привести к типу HANDLE, что позволит избежать появления предупреждающих сообщений. Убедитесь в том, что определение символической константы _МТ предшествует любому из включаемых файлов; в примерах программ это определение содержится в файле Envirmnt.h. Больше от вас ничего не требуется. Резюмируя, перечислим действия, которые вы должны выполнить, если имеете дело со средой разработки Visual C++.

• Подключите библиотеку LIBCMT.LIB и откажитесь от использования библиотеки, заданной по умолчанию.

• Включите директиву #define _MT во все исходные файлы, в которых используется библиотека С.

• Добавьте включаемый файл <process.h>, содержащий определения функций _beginthreadex и _endthreadex.

• Создайте потоки с помощью функции _beginthreadex; не применяйте для этой цели функцию CreateThread.

• Завершите потоки посредством функции _endthreadex или просто воспользуйтесь оператором return в конце функции потока.

В приложении А вы найдете указания относительно того, как создавать многопоточные приложения. В частности, можно, и даже рекомендуется, указывать библиотеку и определять константу _МТ непосредственно в среде разработки.

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

Библиотеки с многопоточной поддержкой

При проектировании пользовательских библиотек следует уделять самое пристальное внимание тому, чтобы избежать возникновения проблем, связанных с параллельным выполнением нескольких потоков, особенно в тех случаях, когда речь идет о сохранении информации о состоянии процессов. Одна из возможных стратегий демонстрируется в примере в главе 12 (программа 12.4), где библиотека DLL для сохранения информации о состоянии использует отдельный параметр.

Еще один пример в главе 12 (программа 12.5) иллюстрирует альтернативный подход, в котором применяется функция DllMain и TLS, описанные далее в настоящей главе.

Пример: многопоточный поиск контекста

В программе 6.1 (grepMP) для выполнения одновременного поиска текстового шаблона в нескольких файлах использовались процессы. Программа 7.1 (grepMT), которая включает исходный код функции поиска текстового шаблона grep, обеспечивает выполнение поиска несколькими потоками в рамках одного процесса. Код функции поиска основан на вызовах функций файлового ввода/вывода библиотеки С. Основная программа аналогична той, которая предлагалась в варианте реализации, основанном на использовании процессов.

Этот пример также показывает, что применение потоков позволяет выполнять асинхронные операции ввода/вывода даже без привлечения специально для этого предназначенных методов, описанных в главе 14. В данном примере параллельным вводом/выводом с участием нескольких файлов управляет программа, в то время как основной или любого другого потока предоставляется возможность в ожидании завершения ввода/вывода выполнять дополнительную обработку. По мнению автора, способ реализации асинхронного ввода/вывода, обеспечиваемый потоками, является более простым, а сравнительный анализ эффективности различных методов, представленный в главе 14, поможет вам выработать собственное мнение на этот счет.

Мы увидим, однако, что в сочетании с портами завершения ввода/вывода операции асинхронного ввода/вывода становятся очень полезным, а часто и необходимым средством в тех случаях, когда количество потоков очень велико.

В иллюстративных целях в программу grepMT введено дополнительное отличие по сравнению с программой grepMP. В данном случае функция WaiForMultipleObjects ожидает завершения не всех потоков, а только одного. Соответствующая информация выводится без ожидания завершения других потоков. В большинстве случае порядок завершения потоков будет меняться от одного запуска программы к другому. Программу легко видоизменить таким образом, чтобы результаты отображались в порядке указания аргументов в командной строке; для этого будет достаточно сымитировать программу grepMP. 

Наконец, обратите внимание на ограничение в 64 потока, обусловленное значением константы MAXIMUM_WAIT_OBJECTS, которая ограничивает количество дескрипторов при вызове функции WaitForMultipleObjects. Если у вас возникнет необходимость в большем количестве потоков, организуйте для функций WaitForSingleObjects или WaitForMultipleObjects соответствующий цикл.

Предостережение

Программа grepMP осуществляет асинхронный ввод/вывод в том смысле, что отдельные потоки выполняют параллельное синхронное чтение различных файлов, которые блокируются до момента завершения операции чтения. Можно также организовать параллельное чтение одного и того же файла, если у него имеются различные дескрипторы (обычно, по одному дескриптору для каждого потока). Эти дескрипторы должны быть сгенерированы функцией CreateFile, а не функцией DuplicateHandle. В главе 14 описывается асинхронный ввод/вывод, осуществляемый как с использованием, так и без использования пользовательских потоков, а в примере, доступном на Web-сайте (программа atouMT, описанная в главе 14), операции ввода/вывода выполняются с использованием нескольких потоков по отношению к одному и тому же файлу.

Программа 7.1. grepMT: многопоточный поиск текстового шаблона 

/* Глава 7. grepMT. */

/* Параллельный поиск текстового шаблона — версия, использующая несколько потоков. */

#include "EvryThng.h"


typedef struct { /* Структура данных потока поиска. */

 int argc;

 TCHAR targv[4][МАХ_РАТН];

} GREP_THREAD_ARG;

typedef GREP_THREAD_ARG *PGR_ARGS;

static DWORD WINAPI ThGrep(PGR_ARGS pArgs);


int _tmain(int argc, LPTSTR argv[]) {

 GREP_THREAD_ARG * gArg;

 HANDLE * tHandle;

 DWORD ThdIdxP, ThId, ExitCode;

 TCHAR CmdLine[MAX_COMMAND_LINE];

 int iThrd, ThdCnt;

 STARTUPINFO Startup;

 PROCESS_INFORMATION ProcessInfo;

 GetStartupInfo(&StartUp);

 /* Основной поток: создает отдельные потоки поиска на основе функции "grep" для каждого файла. */

 tHandle = malloc((argc – 2) * sizeof(HANDLE));

 gArg = malloc((argc – 2) * sizeof(GREP_THREAD_ARG));

 for (iThrd = 0; iThrd < argc – 2; iThrd++) {

  _tcscpy(gArg[iThrd].targv[1], argv[1]); /* Pattern. */

  _tcscpy(gArg[iThrd].targv[2], argv[iThrd + 2]);

  GetTempFileName /* Имя временного файла. */

   (".", "Gre", 0, gArg[iThrd].targv[3]); 

  gArg[iThrd].argc = 4;

  /* Создать рабочий поток для выполнения командной строки. */

  tHandle[iThrd] = (HANDLE)_beginthreadex(NULL, 0, ThGrep, &gArg[iThrd], 0, &ThId);

 }

 /* Перенаправить стандартный вывод для вывода списка файлов. */

 Startup.dwFlags = STARTF_USESTDHANDLES;

 Startup.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);

 /* Выполняются все рабочие потоки. Ожидать их завершения. */

 ThdCnt = argc – 2;

 while (ThdCnt > 0) {

  ThdIdxP = WaitForMultipleObjects(ThdCnt, tHandle, FALSE, INFINITE);

  iThrd = (int)ThdIdxP – (int)WAIT_OBJECT_0;

  GetExitCodeThread(tHandle [iThrd], &ExitCode);

  CloseHandle(tHandle [iThrd]);

  if (ExitCode ==0) { /* Шаблон найден. */

   if (argc > 3) {

    /* Вывести имя файла, если имеется несколько файлов. */

    _tprintf(_T("\n**Результаты поиска – файл: %s\n"), gArg[iThrd].targv [2]);

    fflush(stdout);

   }

   /* Использовать программу "cat" для перечисления результирующих файлов. */

   _stprintf(CmdLine, _T("%s%s"), _Т("cat "), gArg [iThrd].targv[3]);

   CreateProcess(NULL, CmdLine, NULL, NULL, TRUE, 0, NULL, NULL, &StartUp, &ProcessInfo);

   WaitForSingleObject(ProcessInfo.hProcess, INFINITE);

   CloseHandle(ProcessInfo.hProcess);

   CloseHandle(ProcessInfo.hThread);

  }

  DeleteFile(gArg[iThrd].targv[3]);

  /* Скорректировать массивы потоков и имен файлов. */

  tHandle[iThrd] = tHandle[ThdCnt – 1];

  _tcscpy(gArg[iThrd].targv[3], gArg[ThdCnt – 1].targv[3]);

  _tcscpy(gArg[iThrd].targv[2], gArg[ThdCnt – 1].targv[2]);

  ThdCnt--;

 }

}


/* Прототип функции контекстного поиска:

static DWORD WINAPI ThGrep(PGR_ARGS pArgs){ } */

Потоки и производительность

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

В то же время, существует весьма распространенное заблуждение, суть которого состоит в том, что отмеченный параллелизм, независимо от того, касается ли он использования нескольких процессов, как в случае grepMP, или же применения нескольких потоков, как в случае grepMT, способен приводить к повышению производительности лишь в случае SMP-систем. Выигрыш в производительности можно получить и при использовании нескольких дисков, а также при любом другом распараллеливании в системе хранения. Во всех подобных случаях операции ввода/вывода с участием нескольких файлов будут осуществляться в параллельном режиме.

Модель "хозяин/рабочий" и другие модели многопоточных приложений

Программа grepMT демонстрирует модель многопоточных приложений, носящую название модели "хозяин/рабочий" ("boss/worker"), а рис. 6.3, после замены в нем термина "процесс" на термин "поток", может служить графической иллюстрацией соответствующих отношений. Главный поток (основной поток в данном случае) поручает выполнение отдельных задач рабочим потокам. Каждый рабочий, поток получает файл, в котором она должна выполнить поиск, а полученные рабочим потоком результаты передаются главному потоку во временном файле.

Существуют многочисленные вариации этой модели, одной из которых является модель рабочей группы (work crew model), в которой рабочие потоки объединяют свои усилия для решения одной задачи, причем каждый отдельный поток выполняет свою небольшую часть работы. Модель рабочей группы используется в нашем следующем примере (рис. 7.2). Рабочие группы даже могут самостоятельно распределять работу между собой без получения каких-либо указаний со стороны главного потока. В многопоточных программах может быть применена практически любая из схем управления, разработанных для коллективов в человеческом обществе. 

Системное программирование в среде Windows

Рис. 7.2. Выполнение сортировки слиянием с использованием нескольких потоков


Двумя другими основными моделями являются модель "клиент/сервер" (client/server) (проиллюстрирована на рис. 7.1, а пример ее практической реализации рассматривается в главе 11) и конвейерная модель (pipeline model), в которой выполнение задания передается от одного потока к другому (пример многоступенчатого конвейера рассматривается в главе 10 и иллюстрируется на рис. 10.1).

При проектировании многопоточных систем эти модели обладают целым рядом преимуществ, к числу которых можно отнести следующие:

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

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

• Эти модели естественным образом соответствуют структуре большинства обычных задач программирования.

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

• Находить неполадки в незнакомой программе гораздо легче, если ее можно анализировать в терминах моделей. Очень часто главную причину неполадок удается установить на основании видимых нарушений базовых принципов одной из моделей.

• Многие распространенные дефекты программ, например, нарушение условий состязаний задач и их блокирование, также можно описать с использованием простых моделей, к числу которых относятся эффективные методы использования объектов синхронизации, описанные в главах 9 и 10.

Эти классические модели потоков реализованы во многих ОС. В модели компонентных объектов (Component Object Model, COM), широко используемой во многих Windows-системах, применяется другая терминология, и хотя рассмотрение модели СОМ выходит за рамки данной книги, об этих моделях говорится в конце главы 11, где они сравниваются с другими примерами программ.

Пример: применение принципа "разделяй и властвуй" для решения задачи сортировки слиянием в SMP-системах

Этот пример демонстрирует возможности значительного повышения производительности за счет использования потоков, особенно в случае SMP-систем. Основная идея заключается в разбиении задачи на более мелкие составляющие, распределении выполнения подзадач между отдельными потоками и последующем объединении результатов для получения окончательного решения. Планировщик Windows автоматически назначит потокам отдельные процессоры, в результате чего задачи будут выполняться параллельно, снижая общее время выполнения приложения.

Эта стратегия, которую часто называют стратегией "разделяй и властвуй" (divide and conquer), или моделью рабочей группы (work crew model), оказалась весьма полезной и в качестве средства повышения производительности, и в качестве метода проектирования алгоритмов. Одним из примеров ее применения служит программа grepMT (программа 7.1), в которой для каждой файловой операции ввода/вывода и для поиска шаблона создается отдельный поток. Как показано в приложении B, в случае SMP-систем производительность повышается, поскольку планировщик может распределять выполнение потоков между различными процессорами.

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

Решение задачи сортировки слиянием (merge-sort), в которой сортируемый массив разбивается на несколько массивов меньшего размера, является классическим примером алгоритма, построенного на принципе "разделяй и властвуй". Каждый из массивов небольшого размера сортируется по отдельности, после чего отсортированные массивы попарно объединяются с образованием отсортированных массивов большего размера. Описанное слияние массивов попарно осуществляется вплоть до завершения всего процесса сортировки. В общем случае, сортировка слиянием начинается с массивов размерности 1, которые сами по себе не нуждаются в сортировке. В данном примере сортировка начинается с массивов большей размерности, чтобы на каждый процессор приходилось по одному массиву. Блок-схема используемого алгоритма показана на рис. 7.2.

Детали реализации представлены в программе 7.2. Число задач задается пользователем в командной строке. Временные показатели сортировки приведены в приложении В. В упражнении 7.9 вам предлагается изменить программу sortMT таким образом, чтобы она сначала определяла количество доступных процессоров, используя для этого функцию GetSystemInfo, а затем создавала по одному потоку для каждого процессора.

Заметьте, что эта программа эффективно выполняется в однопроцессорных системах, в которых имеется достаточно большой запас оперативной памяти, и обеспечивает значительное повышение производительности в SMP-системах. Предостережение. Представленный алгоритм будет корректно работать лишь при условии, что число записей в сортируемом файле нацело делится на число потоков, а число потоков выражается степенью 2. В упражнении 7.8 упомянутые ограничения снимаются.

Примечание

Изучая работу этой программы, постарайтесь отделить логику управления потоками от логики определения части массива, которую должна сортировать тот или иной поток. Обратите также внимание на использование функции qsort из библиотеки С, применение которой избавляет нас от необходимости самостоятельно разрабатывать эффективную функцию сортировки.

Программа 7.2. sortMT: сортировка слиянием с использованием нескольких потоков 

/* Глава 7. SortMT.

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

   sortMT [параметры] число_задач файл */


#include "EvryThng.h"

#define DATALEN 56 /* Данные: 56 байт; ключ: 8 байт. */

#define KEYLEN 8


typedef struct _RECORD {

 CHAR Key[KEYLEN];

 TCHAR Data[DATALEN];

} RECORD;


#define RECSIZE sizeof (RECORD)

typedef RECORD * LPRECORD;


typedef struct _THREADARG { /* Аргумент потока */

 DWORD iTh; /* Номер потока: 0, 1, 2, … */

 LPRECORD LowRec; /* Младшая часть указателя записи */

 LPRECORD HighRec; /* Старшая часть указателя записи */

} THREADARG, *PTHREADARG;


static int KeyCompare(LPCTSTR, LPCTSTR);

static DWORD WINAPI ThSort(PTHREADARG pThArg);

static DWORD nRec; /* Общее число записей, подлежащих сортировке. */

static HANDLE* ThreadHandle;


int _tmain(int argc, LPTSTR argv[]) {

 HANDLE hFile;

 LPRECORD pRecords = NULL;

 DWORD FsLow, nRead, LowRecNo, nRecTh, NPr, ThId, iTh;

 BOOL NoPrint;

 int iFF, iNP;

 PTHREADARG ThArg;

 LPTSTR StringEnd;

 iNP = Options(argc, argv, _T("n"), &NoPrint, NULL);

 iFF = iNP + 1;

 NPr = _ttoi(argv[iNP]); /* Количество потоков. */

 hFile = CreateFile(argv[iFF], GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);

 FsLow = GetFileSize(hFile, NULL);

 nRec = FsLow / RECSIZE; /* Общее число записей. */

 nRecTh = nRec / NPr; /* Количество записей на один поток. */

 /* Распределить память для аргументов потока и массива дескрипторов и выделить в памяти место для файла. Считать весь файл. */

 ThArg = malloc(NPr * sizeof(THREADARG));

 /* Аргументы потоков. */

 ThreadHandle = malloc(NPr * sizeof(HANDLE));

 pRecords = malloc(FsLow + sizeof(TCHAR));

 ReadFile(hFile, pRecords, FsLow, &nRead, NULL);

 CloseHandle(hFile);

 LowRecNo = 0; /* Создать потоки, выполняющие сортировку. */

 for (iTh = 0; iTh < NPr; iTh++) {

  ThArg[iTh].iTh = iTh;

  ThArg[iTh].LowRec = pRecords + LowRecNo;

  ThArg[iTh].HighRec = pRecords + (LowRecNo + nRecTh);

  LowRecNo += nRecTh;

  ThreadHandle[iTh] = (HANDLE)_beginthreadex (NULL, 0, ThSort, &ThArg[iTh], CREATE_SUSPENDED, &ThId);

 }

 for (iTh = 0; iTh < NPr; iTh++) /* Запустить все потоки сортировки. */

  ResumeThread(ThreadHandle [iTh]);

 WaitForSingleObject(ThreadHandle[0], INFINITE);

 for (iTh = 0; iTh < NPr; iTh++) CloseHandle(ThreadHandle [iTh]);

 StringEnd = (LPTSTR)pRecords + FsLow;

 *StringEnd = '\0';

 if (!NoPrint) printf("\n%s", (LPCTSTR)pRecords);

 free(pRecords);

 free(ThArg);

 free(ThreadHandle);

 return 0;

 } /* Конец tmain. */


static VOID MergeArrays(LPRECORD, LPRECORD);


DWORD WINAPI ThSort(PTHREADARG pThArg) {

 DWORD GrpSize = 2, RecsInGrp, MyNumber, TwoToI = 1;

 LPRECORD First;

 MyNumber = pThArg->iTh;

 First = pThArg->LowRec;

 RecsInGrp = pThArg->HighRec – First;

 qsort(First, RecsInGrp, RECSIZE, KeyCompare);

 while ((MyNumber % GrpSize) == 0 && RecsInGrp < nRec) {

  /* Объединить слиянием отсортированные массивы. */

  WaitForSingleObject(ThreadHandle[MyNumber + TwoToI], INFINITE);

  MergeArrays(First, First + RecsInGrp);

  RecsInGrp *= 2;

  GrpSize *= 2;

  TwoToI *= 2;

 }

 _endthreadex(0);

 return 0; /* Подавить вывод предупреждающих сообщений. */

}


static VOID MergeArrays(LPRECORD p1, LPRECORD p2) {

 DWORD iRec = 0, nRecs, i1 = 0, i2 = 0;

 LPRECORD pDest, p1Hold, pDestHold;

 nRecs = p2 – p1;

 pDest = pDestHold = malloc(2 * nRecs * RECSIZE);

 p1Hold = p1;

 while (i1 < nRecs && i2 < nRecs) {

  if (KeyCompare((LPCTSTR)p1, (LPCTSTR)p2) <= 0) {

   memcpy(pDest, p1, RECSIZE);

   i1++;

   p1++;

   pDest++;

  } else {

   memcpy(pDest, p2, RECSIZE);

   i2++;

   p2++;

   pDest++;

  }

 }

 if (i1 >= nRecs) memcpy(pDest, p2, RECSIZE * (nRecs – i2));

 else memcpy(pDest, p1, RECSIZE * (nRecs – i1));

 memcpy(p1Hold, pDestHold, 2 * nRecs * RECSIZE);

 free (pDestHold);

 return;

} 

Производительность

В приложении В представлены результаты сортировки файлов большого размера, содержащих записи длиной 64 байта, для случаев использования одной, двух и четырех потоков. SMP-системы позволяют получать значительно лучшие результаты. Упомянутый принцип "разделяй и властвуй" обеспечивает нечто большее, чем просто стратегию проектирования алгоритмов; он также служит ключом к использованию потоков и SMP. Результаты для однопроцессорных систем могут быть различными в зависимости от остальных характеристик системы. В системах с ограниченным объемом памяти (то есть объема физической памяти не достаточно для того, чтобы наряду с ОС и другими активными процессами в ней уместился весь файл) использование нескольких потоков увеличивает время сортировки, поскольку потоки состязаются между собой в захвате доступной физической памяти. С другой стороны, если памяти имеется достаточно, то многопоточный вариант может привести к повышению производительности и в случае однопроцессорных систем. Кроме того, как следует из приложения В, получаемые результаты существенно зависят от начального распределения данных.

Локальные области хранения потоков

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

Кроме того, Windows предоставляет локальные области хранения потоков (Thread Local Storage, TLS), обеспечивающие каждый из потоков собственным массивом указателей. Организация TLS показана на рис. 7.3.

Индексы (строки) TLS первоначально не распределены, но в любой момент времени можно добавлять новые строки и освобождать существующие, причем минимально возможное число строк для любого процесса определяется значением TLS_MINIMUM_AVAILABLE (равным, по крайней мере, 64). Число столбцов может изменяться по мере создания новых потоков и завершения существующих.

Сначала мы рассмотрим управление индексами TLS. Логическим пространством для этого служит основной поток, но управлять индексами может любой поток.

Функция TlsAlloc возвращает распределенный индекс (> 0) или –1 (0xFFFFFFFF) в случае отсутствия доступных индексов. 

DWORD TlsAlloc(VOID)

BOOL TlsFree(DWORD dwIndex) 

Системное программирование в среде Windows

Рис. 7.3. Локальные области хранения потоков в контексте процесса


Каждый отдельный поток может выбирать и устанавливать значения (указатели типа void), связанные с ее областью памяти, используя индексы TLS.

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

LPVOID TlsGetValue(DWORD dwTlsIndex)

BOOL TlsSetValue(DWORD dwTlsIndex, LPVOID lpTlsValue) 

TLS предоставляют удобный механизм организации памяти, являющейся глобальной в контексте потока, но недоступной другим потокам. Обычные глобальные хранилища разделяются всеми потоками. Несмотря на то что ни один поток не может получить доступа к TLS другого потока, любой поток может уничтожить индекс TLS другого потока, вызвав функцию TlsFree, так что этой функцией следует пользоваться с осторожностью. TLS часто используются DLL в качестве замены глобальной памяти библиотеки; в результате этого каждый поток получает в свое распоряжение собственную глобальную память. Кроме того, TLS обеспечивают вызывающим программам удобный способ взаимодействия с функциями DLL, и именно этот способ применения TLS является наиболее распространенным. В качестве примера в главе 12 (программа 12.4) TLS используются для создания библиотеки DLL с многопоточной поддержкой; другим важным элементом этого решения являются уведомления DLL о присоединении/отсоединении потоков и процессов путем вызова функции DllMain (глава 5). 

Приоритеты процессов и потоков и планирование выполнения

Ядро Windows всегда запускает тот из потоков, готовых к выполнению, который обладает наивысшим приоритетом. Поток не является готовым к выполнению, если он находится в состоянии ожидания, приостановлен или блокирован по той или иной причине.

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

• IDLE_PRIORITY_CLASS, базовый приоритет 4.

• NORMAL_PRIORITY_CLASS, базовый приоритет 9 или 7.

• HIGH_PRIORITY_CLASS, базовый приоритет 13.

• REALTIME_PRIORITY_CLASS, базовый приоритет 24.

Оба предельных класса используются редко, и в большинстве случаев можно обойтись нормальным (normal) классом. Windows NT (все версии) не является ОС реального времени (real-time), чего нельзя сказать, например, о Windows СЕ, и в случаях, аналогичных последнему, классом REALTIME_PRIORITY_CLASS следует пользоваться с осторожностью, чтобы не допустить вытеснения других процессов. Нормальный базовый приоритет равен 9, если фокус ввода с клавиатуры находится в окне; в противном случае этот приоритет равен 7.

Один процесс может изменить или установить свой собственный приоритет или приоритет другого процесса, если это разрешено атрибутами защиты. 

BOOL SetPriorityClass(HANDLE hProcess, DWORD dwPriority)

DWORD GetPriorityClass(HANDLE hProcess)

Приоритеты потоков устанавливаются относительно базового приоритета процесса, и во время создания потока его приоритет устанавливается равным приоритету процесса. Приоритеты потоков могут принимать значения в интервале ±2 относительно базового приоритета процесса. Результирующим пяти значениям приоритета присвоены следующие символические имена:

• THREAD_PRIORITY_LOWEST

• THREAD_PRIORITY_BELOW_NORMAL

• THREAD_PRIORITY_NORMAL

• THREAD_PRIORITY_HIGHEST 

Для установки и выборки относительного приоритета потока следует использовать эти значения. Обратите внимание на использование целых чисел со знаком вместо чисел типа DWORD. 

BOOL SetThreadPriority(HANDLE hThread, int nPriority)

int GetThreadPriority(HANDLE hThread) 

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

• THREAD_PRIORITY_IDLE имеет значение 1 (или 16 — для процессов, выполняющихся в режиме реального времени).

• THREAD_PRIORITY_TIME_CRITICAL имеет значение 15 (или 31 — для процессов, выполняющихся в режиме реального времени).

Приоритеты потоков автоматически изменяются при изменении приоритета процесса. Помимо этого, ОС может регулировать приоритеты потоков динамическим путем на основании поведения потоков. Вы можете активизировать или отключить это средство с помощью функции SetThreadPriorityBoost.

Предостережение относительно использования приоритетов потоков и процессов

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

Кроме того, приводимые в последующих главах высказывания относительно корректности многопоточных программ справедливы лишь при условии соблюдения принципа равноправия (fairness) потоков. Равноправие потоков означает, что все они, в конечном счете, будут выполняться. Если не соблюдать этот принцип, то потоки с более низким приоритетом смогут удерживать ресурсы, необходимые потокам, имеющим более высокий приоритет. При описании недостатков планирования, осуществляемого с нарушением принципа равноправия, используют термины зависание потоков (thread starvation) и инверсия приоритетов (priority inversion). 

Состояния потоков

На рис. 7.4, взятом из [9] (см. также [38], версию, обновленную Соломоном (Solomon) и Руссиновичем (Russinovych)), представлена схема планирования потоков и показаны их возможные состояния. Кроме того, этот рисунок иллюстрирует результаты работы программы. Такие диаграммы состояния являются общими для всех многозадачных ОС и помогают выяснить, каким образом планируется выполнение потоков и как они переходят из одного состояния в другое. 

Системное программирование в среде Windows

 Рис. 7.4. Состояния потоков и переходы между состояниями (Источник: Inside Windows NT, Copyright © 1993, by Helen Custer. Copyright Microsoft Press. Воспроизводится с разрешения Microsoft Press. Все права сохранены.)


Ниже приводится краткая сводка основных положений. Для получения более подробной информации по этому вопросу обратитесь в [38] или к руководству по ОС.

• Поток находится в состоянии выполнения (running state), если она фактически выполняется процессором. В SMP-системах в состоянии выполнения могут находиться одновременно несколько потоков.

• Планировщик переводит поток в состояние ожидания (wait state), если он выполняет функцию ожидания несигнализирующих объектов, например, потоков или процессов, или перехода в сигнальное состояние объектов синхронизации, о чем говорится в главе 8. Операции ввода/вывода также будут ожидать завершения передачи дисковых или иных данных, но ожидание может быть вызвано и другими многочисленными функциями. О потоках, находящихся в состоянии ожидания, нередко говорят как о блокированных (blocked) или спящих (sleeping).

• Поток находится в состоянии готовности (ready state), если она может выполняться. Планировщик в любой момент может перевести такой поток в состояние выполнения. Когда процессор станет доступным, планировщик запустит тот из потоков, находящихся в состоянии готовности, который обладает наивысшим приоритетом, а при наличии нескольких потоков с одинаковым высшим приоритетом запущен будет та, который пребывал в состоянии готовности дольше всех. При этом поток проходит через состояние простоя (standby state), или резервное состояние.

• Обычно, в соответствии с приведенным описанием, планировщик помещает поток, находящийся в состоянии готовности, на любой доступный процессор. Программист может указать маску родства процессоров (processor affinity mask) для потока (см. главу 9), предоставляя потоку процессоры, на которых он может выполняться. Используя этот способ, программист может распределять процессоры между потоками. Соответствующими функциями являются SetProcessorAffinityMask и GetProcessorAffinityMask. Функция SetThreadIdealProcessor позволяет указать предпочтительный процессор, подлежащий использованию планировщиком при первой же возможности.

• После истечения кванта времени, отведенного выполняющемуся потоку, планировщик без ожидания переводит его в состояние готовности. В результате выполнения функции Sleep(0) поток также будет переведен из состояния выполнения в состояние готовности.

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

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

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

• Поток переходит в состояние завершения (terminated state) тогда, когда его выполнение завершается, и остается в этом состоянии до тех пор, пока остается открытым хотя бы один из ее дескрипторов. Это позволяет другим потокам запрашивать состояние данного потока и его код завершения.

Возможные ловушки и распространенные ошибки

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

Существенно то, что потоки выполняются в асинхронном режиме. По отношению к ним не действует никакая система упорядочения, если только вы не создали ее явно. Именно асинхронное поведение потоков делает их столь полезными, однако при отсутствии должного внимания можно столкнуться с серьезными трудностями.

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

• Не делайте никаких предположений относительно очередности выполнения родительских и дочерних потоков. Вполне возможно, что дочерний поток будет выполняться вплоть до своего завершения, прежде чем родительский поток вернется из функции CreateThread, или наоборот, дочерний поток может вообще не выполняться в течение длительного периода времени. В случае же SMP-систем возможно параллельное выполнение родительского и одного или нескольких дочерних потоков.

• Убедитесь в том, что до вызова функции CreateThread были завершены все действия по инициализации данных, необходимые для правильной работы дочернего потока, либо приостановите поток или же воспользуйтесь любой другой подходящей методикой. Несвоевременная инициализация данных, требуемых дочерним потоком, может создать "условия состязаний" ("race conditions"), суть которых заключается в том, что родительский поток "состязается" с дочерним, чтобы инициализировать данные до того, как они начнут использоваться дочерним потоком.

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

• Выполнение любого потока может быть прервано в любой момент, и точно так же выполнение любого потока в любой момент может быть возобновлено.

• Не пользуйтесь приоритетами потоков в качестве замены явной синхронизации.

• Никогда не прибегайте к аргументации наподобие "вряд ли это может произойти" при анализе корректности программы. Если что-то может произойти, оно обязательно произойдет, причем тогда, когда вы меньше всего этого ожидаете.

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

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

• Убедитесь в том, что предусмотрели для потоков достаточно большой объем стека, хотя заданного по умолчанию стека размером 1 Мбайт в большинстве случаев вам должно хватить.

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

• К счастью, чаще всего корректно работают самые простые программы и те, отличительной чертой которых является элегантность. При малейшей возможности избегайте усложнения программ.

Ожидание в течение конечного интервала времени

Наконец, рассмотрим функцию Sleep, позволяющую потоку отказаться от процессора и перейти из состояния выполнения в состояние ожидания, которое будет длиться в течение заданного промежутка времени. Например, выполнение задачи потоком может продолжаться в течение некоторого периода времени, после чего поток приостанавливается. По истечении периода ожидания планировщик вновь переводит поток в состояние готовности. Именно эта техника применена в одной из программ в главе 11 (программа 11.4). 

VOID Sleep(DWORD dwMilliseconds) 

Длительность интервала ожидания указывается в миллисекундах, и одним из ее возможных значений является INFINITE, что соответствует бесконечному периоду ожидания, при котором выполнение приостанавливается на неопределенное время. Значению 0 соответствует отказ потока от оставшейся части отведенного ей временного промежутка; в этом случае ядро переводит поток из состояния выполнения в состояние готовности, как показано на рис. 7.4.

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

UNIX-функция sleep аналогична функции Sleep, но длительность периода ожидания измеряется в секундах. Чтобы получить миллисекундное разрешение, используйте функции select или poll без дескрипторов файлов.

Облегченные потоки

Примечание

Облегченные потоки относятся к специальной тематике. Ознакомьтесь с комментарием, включенным в конце первого абзаца приведенного ниже списка, и решите для себя, стоит ли вам читать данный раздел.

Облегченные потоки (fibers), как говорит само их название, являются элементами потока. Точнее, облегченный поток — это единица выполнения в контексте потока, планируемая приложением, а не ядром. В потоке могут быть запланированы несколько облегченных потоков, и облегченные потоки сами определяют, какой из них должен выполняться следующим. Облегченные потоки имеют независимые стеки, но во всем остальном выполняются исключительно в контексте потока, имея, например, доступ к TLS потока и любому мьютексу[27], владельцем которого является данный поток. Более того, вся работа с облегченными потоками осуществляется вне ядра исключительно в пользовательском пространстве. Между потоками и облегченными потоками существуют многочисленные отличия.

Облегченные потоки могут использоваться в нескольких целях. 

• Следует отметить тот немаловажный факт, что во многих приложениях, особенно в приложениях UNIX, использующих патентованные реализации потоков, которые в настоящее время можно, как правило, считать устаревшими, предусмотрено планирование собственных потоков. Использование облегченных потоков упрощает перенос таких приложений в среду Windows. Поскольку для большинства читателей этот вопрос не является актуальным, они, вероятно, предпочтут пропустить данный раздел.

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

• Облегченные потоки действуют в контексте потока и имеют доступ к ресурсам потока и процесса. В отличии от потоков вытесняющее планирование к облегченным потокам не применяется. В действительности планировщику Windows об облегченных потоках ничего не известно; управление такими потоками осуществляется из DLL облегченных потоков исключительно в пользовательском пространстве.

• Облегченные потоки позволяют реализовать механизм сопрограмм (co-routines), посредством которого приложение может переключаться между несколькими взаимосвязанными задачами. Добиться этого с помощью потоков невозможно, поскольку в распоряжении программиста нет средств, обеспечивающих непосредственное управление очередностью выполнения потоков.

• Основные разработчики программного обеспечения, использующие облегченные потоки, представляют этот элемент как фактор повышения производительности. Так, в приложении Oracle Database 10g предусмотрена возможность переключения в "режим облегченных потоков" ("fiber mode") (см. http://download.oracle.com/owsf_2003/40171_colello.ppt; там же находится описание многопоточной модели).

API облегченных потоков представлен шестью функциями. Порядок их использования описывается ниже и иллюстрируется рис. 7.5.

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

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

3. Отдельный облегченный поток может получить свои данные, назначенные ему функцией CreateFiber, обратившись к функции GetFiberData.

4. Аналогично, облегченный поток может идентифицировать себя при помощи функции GetCurrentFiber.

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

6. Функция DeleteFiber уничтожает существующий облегченный поток и все относящиеся к нему данные.

7. В Windows XP (NT 5.1) наряду с локальными областями хранения облегченных потоков введены новые функции, такие, например, как ConvertFiberToThread (которая освобождает ресурсы, созданные функцией ConvertThreadToFiber). 

Системное программирование в среде Windows

Рис. 7.5. Передача управления между облегченными потоками внутри потока


Схема взаимодействия между облегченными потоками в контексте потока представлена на рис. 7.5. Этот пример иллюстрирует два способа вытеснения одного потока другим.

• Подчиненное планирование (master-slave scheduling). Только один, главный (master) облегченный поток, в данном случае — основной, принимает решения относительно того, какой облегченный поток должен выполняться, и этот облегченный поток всегда уступает управление главному облегченный потоке. На рис. 7.5 главным является облегченный поток 1.

• Равноправное планирование (peer-to-peer scheduling). Облегченный поток сам определяет, какой из других облегченных потоков должен выполняться следующим. Определение очередного облегченного потока может базироваться на таких стратегиях, как круговое планирование (round-robin scheduling), приоритетное планирование на основании схемы приоритетов и тому подобное. По принципу равноправного планирования реализуются сопрограммы. На рис. 7.5 такого типа передача управления осуществляется между облегченными потоками 0 и 2.

Резюме

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

В следующих главах

Рассмотрение темы синхронизации, которое начинается в главе 8 с описания и сравнительного анализа объектов синхронизации Windows, продолжается в главах 9 и 10 обсуждением более сложных вопросов синхронизации с привлечением многочисленных примеров. В главе 11 реализуется сервер с многопоточной поддержкой; он показан на рис. 7.1.

Дополнительная литература

Windows

Книга [1] полностью посвящена потокам Win32. Также заслуживают внимания книги [26] и [7]. В то же время во многих из этих и других книг не учтены новшества, появившиеся в Windows 2000, XP и Server 2003. 

UNIX и Pthreads

В книге [40] применение потоков в UNIX не рассматривается, однако для изучения этой темы можно порекомендовать книгу [6]. В этой книге даются многочисленные рекомендации, касающиеся проектирования и реализации многопоточных программ. Приведенная в ней информация применима в равной степени как к потокам Pthreads, так и к потокам Windows, и многие примеры без труда переносятся в Windows. В ней также хорошо изложены модели "хозяин/рабочий", "клиент/сервер" и конвейерная модель, и представление Бутенхофа (Butenhof) было положено в основу описания указанных моделей в данной главе.

Упражнения

7.1. Реализуйте набор функций, позволяющий приостанавливать и возобновлять выполнение потоков, и, кроме того, получать значение счетчика приостановок потоков.

7.2. Сравните производительность программ параллельного поиска, одна из которых использует потоки (программа 7.1, GrepMT), а другая — процессы (программа 6.1, GrepMP). Сравните полученные результаты с теми, которые приведены в приложении В.

7.3. Проведите дополнительное исследование производительности программы GrepMT для случаев, когда файлы находятся на различных дисках или являются сетевыми файлами. Определите также выигрыш в производительности, если таковой будет наблюдаться, в случае SMP-систем.

7.4. Измените программу 7.1, GrepMT, таким образом, чтобы она выводила результаты в той очередности, в какой файлы указаны в командной строке. Сказываются ли эти изменения каким-либо образом на показателях производительности?

7.5. Дополнительно усовершенствуйте программу 7.1, GrepMT, таким образом, чтобы она выводила время, потребовавшееся для выполнения каждого из потоков. Для этого вам понадобится функция GetThreadTimes, аналогичная функции GetProcessTimes, которая была использована в главе 6. Указанное усовершенствование сможет работать лишь в Windows NT4 и более поздних версиях.

7.6. На Web-сайте книги находится многопоточная программа подсчета слов, wcMT.c, структура которой аналогична структуре программы grepMT.c. Там же находится версия этой программы, wcMTx, в которую были намеренно введены некоторые дефекты. Попытайтесь найти и устранить указанные дефекты, в том числе и синтаксические ошибки, не сверяясь с корректным решением. Кроме того, создайте тестовые примеры, иллюстрирующие эти дефекты, и выполните эксперименты по определению производительно сти, аналогичные тем, которые предлагались для программы grepMT. На Web-сайте находится также однопоточная версия упомянутой программы, wcST.c, которую можно использовать для того, чтобы определить, обеспечивают ли потоки выигрыш в производительности по сравнению с последовательной обработкой.

7.7. На Web-сайте находится программа grepMTx.c, в которой имеются дефекты, связанные с нарушением базовых правил, соблюдение которых необходимо для безопасного выполнения нескольких потоков. Опишите, к чему это приводит, а также найдите и устраните ошибки.

7.8. Для правильного выполнения программы sortMT требуется, чтобы количество записей в массиве нацело делилось на количество потоков, а количество потоков равнялось степени 2. Устраните эти ограничения.

7.9. Усовершенствуйте программу sortMT таким образом, чтобы в случаях, когда количество потоков, заданное в командной строке, равно 0, программа определяла количество процессоров, установленных в локальной системе, с помощью функции GetSystemInfo. Задавая количество потоков равным различным кратным количества процессоров (используя коэффициенты кратности 1, 2, 4 и так далее), определите, изменяется ли при этом производительность. 

7.10. Видоизмените программу sortMT таким образом, чтобы рабочие потоки не приостанавливались при их создании. Сказываются ли, и если да, то каким именно образом, возникающие при этом нежелательные условия состязаний на работе программы?

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

7.12. Видоизмените одну из приведенных в данной главе программ (grepMT или sortMT) таким образом, чтобы специфическая для потоков информация частично или полностью передавалась через TLS, а не через структуры данных.

7.13. Наблюдается ли выигрыш в производительности в случае предоставления некоторым потокам в программе sortMT более высокого приоритета по сравнению со всеми остальными? Например, может оказаться выгодным предоставить таким потокам, как поток 3 на рис. 7.2, которая занята лишь сортировкой без слияния, более высокий приоритет, чем всем остальным. Объясните результаты.

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

7.15. Алгоритм быстрой сортировки (Quicksort), обычно используемый функцией qsort библиотеки С, как правило, характеризуется высоким быстродействием, но в некоторых случаях может замедляться. В большинстве учебников по алгоритмам рассматривается его версия, которая работает быстрее всего в тех случаях, когда массив отсортирован в обратном порядке, и медленнее всего, когда массив является уже отсортированным. Однако реализация этого алгоритма в библиотеке Microsoft С ведет себя иначе. Определите из кода библиотечной программы, какого типа последовательности элементов массива будут приводить к наилучшим и наихудшим результатам, и исследуйте показатели производительности программы sortMT для этих двух крайних случаев. Как влияет на результаты увеличение или уменьшение количества потоков? Примечание. Исходный код библиотеки С мог быть установлен в подкаталоге CRT основного каталога Visual Studio на вашей машине. Ищите функцию qsort.с. Кроме того, вы можете попытаться отыскать эту функцию на установочном компакт-диске.

7.16. На Web-сайте содержится программа sortMTx, в которую были намеренно внесены некоторые дефекты. Продемонстрируйте наличие дефектов на тестовых примерах, а затем объясните и устраните указанные дефекты, не сверяясь с корректными решениями. Предостережение. Версии программ, в которых присутствуют дефекты, могут содержать как синтаксические ошибки, так и ошибки в логике организации выполнения потоков.

7.17. Прочитайте статью Джейсона Кларка (Jason Clark) "Waiting for More than 64 Objects" ("Ожидание более чем 64 объектов"), опубликованную в октябрьском номере журнала Windows Developer's Journal за 1997 год. Примените описанное в ней решение к программе grepMT. Найти старые выпуски журналов иногда бывает очень трудно, поэтому воспользуйтесь каким-нибудь поисковым механизмом для поиска сразу по нескольким ключевым словам. Мною для этого поиска была использована фраза "wait for multiple objects more than 64", хотя другие варианты могут оказаться более эффективными. 

ГЛАВА 8

Синхронизация потоков

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

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

Синхронизация потоков является одной из важнейших и интереснейших тем и играет существенную роль почти в любом многопоточном приложении. Тем не менее, те из читателей, которые заинтересованы главным образом в межпроцессном взаимодействии, сетевом программировании и построении серверов с многопоточной поддержкой, могут перейти непосредственно к главе 11 и вернуться к изучению глав 8-10 в качестве вспомогательного материала, лишь в том случае, если в этом возникнет необходимость.

Необходимость в синхронизации потоков

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

Программа sortMT несколько сложнее, поскольку рабочие потоки должны синхронизировать свое выполнение, ожидая завершения смежных потоков, и не могут быть запущены до тех пор, пока главный поток не создаст все рабочие потоки. Как и в случае программы grepMT, синхронизация достигается за счет ожидания завершения одного или нескольких потоков.

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

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

На рис. 8.1 показано, что может случиться, когда две несинхронизированные потоки разделяют общий ресурс, например ячейку памяти. Оба потока увеличивают значение переменной N на единицу, но в силу специфики очередности, в которой могут выполняться потоки, окончательное значение N равно 5, тогда как правильным значением является 6. Заметьте, что представленный здесь частный результат не обладает ни повторяемостью, ни предсказуемостью; другая очередность выполнения потоков могла бы привести к правильному результату. В SMP-системах эта проблема еще более усугубляется.

Критические участки кода

Инкрементирование N при помощи единственного оператора, например, в виде N++, не улучшает ситуацию, поскольку компилятор сгенерирует последовательность из одной или более машинных инструкций, которые вовсе не обязательно должны выполняться атомарно (atomically), то есть как одна неделимая единица выполнения.

Системное программирование в среде Windows

Рис. 8.1. Разделение общей памяти несинхронизированными потоками


Основная проблема состоит в том, что имеется критический участок кода (critical section) (в данном примере — код, который увеличивает N на 1), характеризующийся тем, что если один из потоков приступил к его выполнению, то никакой другой поток не должен входить в данный код до тех пор, пока его не покинет первый поток. Проблему критических участков кода можно считать разновидностью проблемы состязаний, поскольку первый поток "состязается" со вторым потоком в том, чтобы завершить выполнения критического участка еще до того, как его начнет выполнять любой другой поток. Таким образом, мы должны так синхронизировать выполнение потоков, чтобы можно было гарантировать, что в каждый момент времени код будет выполняться только одним потоком.

Неудачные пути решения проблемы критических участков кода

К аналогичным непредсказуемым результатам будет приводить и код, в котором предпринимается попытка защитить участок инкрементирования переменной путем опроса состояния флага.

while (Flag) Sleep (1000);

Flag = TRUE;

N++;

Flag = FALSE;

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

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

DWORD WINAPI ThFunc(TH_ARGS pArgs) {

 volatile DWORD N;

 … N++; …

}

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

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

Класс памяти volatile

Даже если решить проблему синхронизации, все равно остается еще один скрытый дефект. Оптимизирующие компиляторы могут оставлять значение N в регистре, а не заносить его обратно в ячейку памяти, соответствующую переменной N. Попытка решения этой проблемы путем переустановки переключателей опций компилятора окажет отрицательное воздействие на скорость выполнения остальных участков программы. Правильное решение состоит в том, чтобы использовать определенный в стандарте ANSI С спецификатор памяти volatile, который гарантирует, что после изменения значения переменной оно будет сохраняться в памяти, а при необходимости будет всегда извлекаться из памяти. Ключевое слово volatile сообщает компилятору, что значение переменной может быть в любой момент изменено.