Основы организации операционных систем Microsoft Windows
Коньков Константин Алексеевич

Содержание


Лекция 1. Создание ОС Windows. Структура ОС Windows

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

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

Из курсов по теории ОС (см., например, [2], [8]) известно, что операционная система является базисной системной программой. Обычно аппаратно-программное обеспечение типовой вычислительной системы представляют в виде набора слоев (рис. 1.1), при этом операционной системе соответствует слой между оборудованием компьютера и остальным программным обеспечением. Такое расположение позволяет ОС обеспечивать возможность рационального использования оборудования компьютера удобным для пользователя образом путем создания среды для функционирования и разработки прикладных программ.

Слои программного обеспечения компьютерной системы


Рис. 1.1.  Слои программного обеспечения компьютерной системы

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

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

Краткая история создания ОС Windows

Первая версия описываемого ряда операционных систем - ОС Windows NT появилась в 1993 г. Краткий исторический экскурс позволяет объяснить ряд ее особенностей и отличительных черт.

Наиболее важные моменты эволюции операционных систем

Известно ([2]), что операционные системы приобрели современный облик в период развития третьего поколения вычислительных машин, то есть с середины 60-х до 1980 года. В это время существенное повышение эффективности использования процессора было достигнуто за счет реализации многозадачности, в том числе вытесняющей (preemptive) многозадачности. Для поддержки псевдопараллельной работы нескольких программ и асинхронного режима работы внешних устройств в составе вычислительных систем были реализованы следующие программно-аппаратные новшества и подсистемы:

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

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

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

Онтогенез повторяет филогенез

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

Архитектурные особенности операционных систем.

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

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

Реализация модели клиент-сервер в рамках микроядерной архитектуры


Рис. 1.2.  Реализация модели клиент-сервер в рамках микроядерной архитектуры

Создание ОС Windows

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

Операционные системы корпорации Microsoft можно условно разделить на три группы:

Однозадачная 16-разрядная ОС MS-DOS была выпущена в начале 80-х годов и затем широко применялась на компьютерах с процессором x86. Вначале MS-DOS была довольно примитивна (деградация ОС), ее оболочка занималась, главным образом, обработкой командной строки, но в последующие версии было внесено много улучшений, заимствованных, главным образом, из ОС Unix. Затем под влиянием успехов дружественного графического интерфейса корпорации Apple для компьютеров Macintosh была разработана система Windows. Особенно широкое распространение получили версии Windows 3.0, 3.1 и 3.11. Первоначально это была не самостоятельная ОС, а скорее многозадачная (с невытесняющей многозадачностью) графическая оболочка MS-DOS, которая контролировала компьютер и файловую систему.

В 1995 г. была выпущена 32-разрядная ОС Windows 95, где была реализована вытесняющая многозадачность. ОС Windows 95 включала большой объем 16-разрядного кода, главным образом для обеспечения преемственности с приложениями MS-DOS. 16-разрядный код присутствовал и в последующих версиях этой серии Windows 98 и Windows Me. Другой проблемой данной версии Windows, во многом обусловленной той же причиной, была нереентерабельность существенной части кода ядра. Так, если один из потоков был занят модификацией данных в ядре, другой поток, чтобы не получить эти данные в противоречивом состоянии, вынужден был ждать, то есть не мог воспользоваться системными сервисами. Это, зачастую, сводило на нет преимущества многозадачности.

ОС Windows NT (New Technology) - новая 32-разрядная ОС, совместимая с предшествующими версиями Windows по интерфейсу. Работу над созданием системы возглавил Дэвид Катлер, один из ключевых разработчиков ОС VAX VMS. Ряд идей системы VMS присутствует в NT (см рис. 1.3). Заметна преемственность в системе управления большим адресным пространством и резидентным множеством процесса, в системе приоритетов обычных процессов и процессов реального времени, в средствах синхронизации и т.д. Вместе с тем Windows NT - это совершенно новый амбициозный проект разработки системы с учетом новейших достижений в области архитектуры микроядра. Первая версия, названная Windows NT 3.1 для соответствия популярной Windows 3.1, была выпущена в 1993 г. Коммерческого успеха добилась версия Windows NT 4.0, заимствовавшая графический интерфейс Windows 95. В начале 1999 г. была выпущена Windows NT 5.0, переименованная в Windows 2000. Следующая версия этой ОС данной серии - Windows XP появилась в 2001 г., а Windows Server 2003 - в 2003 г. В настоящее время выпущена Windows Vista, ранее известная под кодовым именем Longhorn, - новая версия Windows, продолжающая линейку Windows NT.

Сравнение архитектур ОС Windows и VAX/VMS


увеличить изображение

Рис. 1.3.  Сравнение архитектур ОС Windows и VAX/VMS

Объем исходных текстов ядра ОС Windows неизвестен. По некоторым оценкам, объем ядра Windows NT 3.5 составляет приблизительно 10Мб, а с каждой новой версией ОС Windows этот объем неуклонно увеличивается в полтора-два раза.

Возможности системы

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

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

Структура ОС Windows

Общее описание структуры системы

Архитектура ОС Windows (в данном разделе она излагается, следуя главным образом [3] и [6]), претерпела ряд изменений в процессе эволюции. Первые версии системы имели микроядерный дизайн, основанный на микроядре Mach, которое было разработано в университете Карнеги-Меллона. Архитектура более поздних версий системы микроядерной уже не является.

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

Высокая модульность и гибкость первых версий Windows NT позволила успешно перенести систему на такие отличные от Intel платформы, как Alpha (корпорация DEC), Power PC (IBM) и MIPS (Silicon Graphic). Более поздние версии ограничиваются поддержкой архитектуры Intel x86.

Упрощенная схема архитектуры, ориентированная на выполнение Win32-приложений, показана на рис. 1.4.

Упрощенная архитектурная схема ОС Windows


Рис. 1.4.  Упрощенная архитектурная схема ОС Windows

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

Задача уровня абстрагирования от оборудования (hardware abstraction layer, HAL) - скрыть аппаратные различия аппаратных архитектур для потенциального переноса системы с одной платформы на другую. HAL предоставляет выше лежащим уровням аппаратные устройства в абстрактном виде, свободном от индивидуальных особенностей. Это позволяет изолировать ядро, драйверы и исполнительную систему ОС Windows от специфики оборудования (например, от различий между материнскими платами).

Ядром обычно называют все компоненты ОС, работающие в привилегированном режиме работы процессора или в режиме ядра. Корпорация Microsoft называет ядром (kernel) компонент, находящийся в невыгружаемой памяти и содержащий низкоуровневые функции операционной системы, такие, как диспетчеризация прерываний и исключений, планирование потоков и др. Оно также предоставляет набор процедур и базовых объектов, применяемых компонентами высших уровней.

Ядро и HAL являются аппаратно-зависимыми и написаны на языках Си и ассемблера. Верхние уровни написаны на языке Си и являются машинно-независимыми.

Исполнительная система (executive) обеспечивает управление памятью, процессами и потоками, защиту, ввод-вывод и взаимодействие между процессами. Драйверы устройств содержат аппаратно-зависимый код и обеспечивают трансляцию пользовательских вызовов в запросы, специфичные для конкретных устройств. Подсистема поддержки окон и графики реализует функции графического пользовательского интерфейса (GUI), более известные как Win-32-функции модулей USER и GDI

В пространстве пользователя работают разнообразные сервисы (аналоги демонов в Unix), управляемые диспетчером сервисов и решающие системные задачи. Некоторые системные процессы (например, обработка входа в систему) диспетчером сервисов не управляются и называются фиксированными процессами поддержки системы. Пользовательские приложения (user applications) бывают пяти типов: Win32, Windows 3.1, MS-DOS, POSIX и OS/2 1.2. Среду для выполнения пользовательских процессов предоставляют три подсистемы окружения: Win32, POSIX и OS/2. Таким образом, пользовательские приложения не могут вызывать системные вызовы ОС Windows напрямую, а вынуждены обращаться к DLL подсистем (краткое определение dll имеется в приложении).

Основные компоненты ОС Windows реализованы в следующих системных файлах, находящихся в каталоге system32:

Подсистема Win32

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

Win32 API (Application Programming Interface) - основной интерфейс программирования в семействе операционных систем Microsoft Windows. Функции Win32 API , например, CreateProcess или CreateFile, - документированные, вызываемые подпрограммы, реализуемые Win32 подсистемой.

В состав Win32 подсистемы (см. рис. 1.4) входят: cерверный процесс подсистемы окружения csrss.exe, драйвер режима ядра Win32k.sys, dll - модули подсистем (kernel32.dll, advapi32.dll, user32.dll и gdi32.dll), экспортирующие Win32-функции и драйверы графических устройств. В процессе эволюции структура подсистемы претерпела изменения. Например, функции окон и рисования с целью повышения производительности были перенесены из серверного процесса, работающего в режиме пользователя, в драйвер режима ядра Win32k.sys. Однако это и подобные изменения никак не отразились на работоспособности приложений, поскольку существующие вызовы Win32 API не изменяются с новыми выпусками системы Windows, хотя их состав постоянно пополняется.

Приложение, ориентированное на использование Win32 API, может работать практически на всех версиях Windows, несмотря на то, что сами системные вызовы в различных системах различны (см. рис. 1.5). Таким путем корпорация Microsoft обеспечивает преемственность своих операционных систем.

Поддержка единого программного интерфейса для различных версий Windows


Рис. 1.5.  Поддержка единого программного интерфейса для различных версий Windows

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

Различные маршруты выполнения вызовов Win32 API.


Рис. 1.6.  Различные маршруты выполнения вызовов Win32 API.

При вызове приложением одной из Win32-функций dll-подсистем может возникнуть одна из трех ситуаций (см. рис. 1.6).

Некоторые функции (например, CreateProcess ) требуют выполнения обоих последних пунктов.

В первых версиях ОС Windows практически все вызовы Win32 API выполнялись, следуя маршруту 2 (2a, 2b, 2c). После того, как существенная часть кода системы для увеличения производительности была перенесена в ядро (начиная с Windows NT 4.0), вызовы Win32 API, как правило, идут напрямую по 3-му (3a, 3b) пути, минуя подсистему окружения Win32. В настоящее время лишь небольшое число вызовов выполняется по длинному 2-му маршруту.

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

Список экспортируемых каждой конкретной dll функций можно посмотреть с помощью утилиты depends, входящей в пакет Platform SDK. Так, на рис. 1.7 приведена информация о структуре библиотеки kernel32.dll ОС Windows XP, экспортирующей 949 функций.

Окно утилиты depends.exe


увеличить изображение

Рис. 1.7.  Окно утилиты depends.exe

Заключение

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

Приложение. Некоторые понятия и термины

DLL (динамически подключаемая библиотека)

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

Процессы и потоки

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

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

Более подробно процессы и потоки описаны в части II.

Лекция 2. Разработка Win32 приложений. Инструментальные средства изучения системы

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

Win32 API

Уже отмечалось, что в ОС Windows между приложением и совокупностью системных вызовов (системных сервисов в терминологии Microsoft) расположен дополнительный абстрактный слой - программный интерфейс Win32 API. За счет этого Win32-приложение может работать практически во всех версиях Windows (см. рис. 1.5), несмотря на то, что сами системные вызовы в различных версиях системы различны и не документированы.

Исчерпывающая информация по программному интерфейсу Win32 API содержится в справочной документации на Win32 API. Эту документацию можно просмотреть на сайте http://msdn.microsoft.com или на компакт-дисках MSDN (Microsoft Developer Network Library). MSDN является программой технической поддержки разработчиков.

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

Если заглянуть в раздел MSDN \Platform SDK\ Win32\ Overview of the Win32 API, то можно увидеть, что Win32 API подразделяются на следующие группы.

В рамках данного курса нам потребуются главным образом функции, относящиеся к первому пункту списка и описанные в MSDN разделе \Platform SDK\Base Services, а также функции, описанные в разделе \Platform SDK\ Security.

Компилятор Visual C++ и среда программирования для Windows

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

Компилятор Windows Visual C++ удобно объединять со справочной системой MSDN Library, которая при этом вызывается через пункт меню "Справка" ("Help") в интегрированной среде Microsoft Visual C++, а также может использоваться автономно. Запуск установленной графической оболочки Microsoft Visual C++ осуществляется стандартными средствами системы.

Чтобы из текста программы на языке высокого уровня (файл с расширением "c" или "cpp") получить исполняемую программу в машинных кодах (файл с расширением "exe"), необходимо в графической оболочке Microsoft Visual Studio C++ создать рабочий проект, который представляет собой совокупность служебных файлов, необходимых для дальнейшей работы.

Программные примеры, иллюстрирующие данный курс, представляют собой Windows-приложения с текстовым интерфейсом (консольные). Разработка приложений с дружественным графическим интерфейсом сама по себе достаточно сложна и должна изучаться в рамках специальных учебных курсов. Поэтому, формируя проект, далее в графической оболочке Visual Studio в диалоговом окне "New" нужно выбрать Win32 Console Application в качестве типа приложения, а также дать проекту имя, указать каталог расположения файлов проекта и нажать кнопку "OK".

Затем при помощи пунктов меню и всплывающих окон графической оболочки нужно включить в проект файлы, содержащие программу, или ввести программу с клавиатуры с последующим ее сохранением в одном из файлов проекта. Через пункт меню "Build" можно выполнить компиляцию программы, создать исполняемый модуль, запустить программу на счет и, при необходимости, выполнить ее отладку. Существует большое количество разнообразных руководств по использованию Microsoft Visual Studio C++.

Прогон программы "Hello, world"

В качестве самостоятельного упражнения рекомендуется реализовать простейшую программу в интегрированной среде компилятора Visual C++, например, хрестоматийную программу "Hello, world", и ознакомиться со средствами разработки, отладки и контекстной помощи.

Типы данных, используемые в Win32-приложениях

Мобильность программ и их независимость от конкретной платформы во многом обеспечивается введением новых стандартных типов данных - определенных на основе простых типов языка программирования Си. Имена стандартных типов данных состоят из символов верхнего регистра, для них не применяется оператор "*".

Полный перечень используемых данных можно увидеть в разделе \Plarform SDK \Win32 API\ Reference \ Data Types. Ниже приведен список наиболее распространенных типов: симовольных, целых, булевских, указателей и описателей (handles). Символьные, целые и булевские типы соответствуют аналогичным типам большинства диалектов языка Си. Имена типов-указателей содержат префикс "P" или "LP". Описатели имеют отношение к ресурсам, загруженным в память.

Наиболее часто используются следующие типы данных:

Остальные типы данных будут изучаться по мере необходимости. Некоторые Win32 приложения могут быть выполнены в среде более ранних версий ОС Windows, в том числе и 16-разрядных. Вследствие этого имена некоторых типов отражают систему адресации ОС MS-DOS, например, LP (long pointer) означает "длинный" указатель, а на самом деле - это обычный указатель.

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

Unicode

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

Стандарт Unicode поддерживается консорциумом, в который входят такие компании, как Apple, Compaq, Hewlett-Packard, IBM, Microsoft и многие другие; подробная информация об этом имеется на сайте http://www.unicode.org. В справочнике MSDN соответствующие сведения хранятся в разделе \Visual Studio documentation\ Visual C++ Programmer's Guide \ Adding Program Functionality \ Overviews \ Unicode Programming.

Для совместимости со стандартами языков программирования и предыдущими версиями Windows в системе наряду с 16-битными (двухбайтовыми) символами активно используются и 8-битные (однобайтовые) ANSI символы. Так, многие Win32-функции , принимающие строковые параметры, существуют в двух версиях: для Unicode и для ANSI. Обычно при вызове ANSI-версии Win32-функции входные строковые параметры перед обработкой системой преобразуются в Unicode. В связи с этим перед разработчиками стоит задача написания приложений, способных работать с обеими кодировками.

Разработка приложений с использованием Unicode

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

Обычно, имена Unicode данных и функций содержат префикс "wc" (от wide character), "w". Например, WCHAR - Unicode символ, wcscmp - функция сравнения Unicode строк. Можно также поставить префикс "L" перед текстовой строкой, например, L"Текстовая строка" - строка в формате Unicode.

Чтобы реализовать возможности компиляции двойного назначения, нужно включить в состав программы заголовочный файл tchar.h. Он состоит из макросов, которые ссылаются на Unicode данные и функции, если в программе определен макрос UNICODE, и на ANSI - в противном случае. Так, для объявления символьного массива универсального назначения применяется тип TCHAR, который транслируется в WCHAR, если UNICODE определен, и в CHAR, если не определен. Аналогичным образом макросы с префиксом "l" переопределяют строковые функции ( lstrlen вместо strlen и т.д.), а для определения символьных и строковых литералов применяется макрос _TEXT (или просто _T ). Более подробно этот материал описан в [4].

Прогон программы вывода строки в формате Unicode

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

#define UNICODE
#ifdef UNICODE
#define _UNICODE
#endif
#include <windows.h>
#include <tchar.h>
#include <stdio.h>

void main() {
PTCHAR TextString = _T("Hello, world");
_tprintf(_T("String -  %s\n"), TextString);
}

Необходимо убедиться, что программа одинаково работает в случае применения и отключения Unicode.

Прогон программы записи в файл в Unicode и обычном формате

Приведенная программа может вывести строку "Hello, world" в файл MyFile.txt в обычном формате и в формате Unicode.

#define UNICODE
#ifdef UNICODE
#define _UNICODE
#endif
#include <windows.h>
#include <tchar.h>
#include <stdio.h>

void main() {

HANDLE hFile;
PTCHAR  FileName = _T("MyFile.txt");
PTCHAR TextString = _T("Hello, world.");
DWORD iWrite, StringLength = lstrlen(TextString);

_tprintf(_T("There are %ld symbols in text string  %s\n"),  StringLength, TextString);


hFile = CreateFile(FileName, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | 0, NULL); 

iWrite = StringLength;
#ifdef UNICODE
 iWrite = 2*StringLength;
#endif

WriteFile(hFile, TextString, iWrite, &iWrite, NULL);
printf("%d bytes are written to file\n", iWrite);
CloseHandle(hFile); 
}

Рекомендуется оба варианта получившегося файла просмотреть с помощью блокнота Nоtepad.

Обработка ошибок

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

Вызываемая Win32-функция может возвратить значение, свидетельствующее об ошибке (например, NULL для функций типа HANDLE или ноль для функций типа BOOL). В таких случаях можно попытаться выявить тип ошибки при помощи функции GetLastError - она возвращает код последней ошибки, который хранится в локальной памяти потока, вызвавшего ошибку. Коды ошибок (а их более 10 тысяч), представляющие собой 32-битные числа, находятся в заголовочном файле WinError.h.

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

Для преобразования кода ошибки в ее содержательное описание предназначена Win32-функция FormatMessage. Получить описание ошибки по ее коду можно также с помощью утилиты errlook.exe, поставляемой в составе Visual Studio. Аналогичная информация содержится в справочной системе MSDN.

В тех случаях, когда об ошибке необходимо оповестить пользователя, можно использовать звуковой сигнал (функция MessageBeep ). Для обработки ошибок также активно применяется структурная обработка исключений (Structured Exception Handling, см., [4], [9]).

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

#include <windows.h>
#include <stdio.h>

void GetError() { 
 DWORD dw = GetLastError(); 
 printf("GetLastError returned %u\n", dw); 
} 
void SetError() {
 DWORD dw = ERROR_FILE_NOT_FOUND;
 SetLastError (dw);
}
     
void main() {
 SetError();
 GetError();
}

Рекомендуется реализовать данную программу и сверить номер выдаваемой ошибки с соответствующим перечнем в MSDN или файле заголовка WinError.h.

Инструментальные средства изучения системы

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

Штатные и встроенные средства

Большое количество полезных инструментов поставляется вместе с системой. Это, во-первых, штатные утилиты, такие, как диспетчер задач, редактор реестра, разнообразные средства настройки и администрирования, информативные панели. Очень много полезной информации можно получить путем интерпретации показаний многочисленных счетчиков производительности, предназначенных для мониторинга системы. Счетчики производительности, а их более сотни для различных объектов, доступны через оснастку "Производительность" административной панели управления, а также через API системы.

Большое количество полезных утилит входит в состав Windows Support Tools, для их установки надо запустить Setup из папки \Support \ Tools в дистрибутиве системы.

Утилиты и программные средства, входящие в состав Platform SDK

В состав Microsoft Platform SDK входит более 100 полезных утилит, находящихся после установки пакета в каталоге Program Files\Microsoft SDK\Bin. Их использование регламентируется встроенными подсказками, а также прилагаемой к Platform SDK гипертекстовой системой контекстной помощи. Кроме того, в состав пакета входит большое количество библиотек, заголовочных файлов, примеров программного кода и полезной документации.

Утилиты, поставляемые в составе Resourse Kit (ресурсы Windows)

В комплект входит большое число утилит. Их состав частично пересекается с утилитами, входящими в комплект Microsoft Platform SDK.

Утилиты с сайта sysinternals.com

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

Помимо перечисленных разработчиками активно используются утилиты, входящие в состав пакетов Visual Studio, DDK (Device Driver Kit), и разнообразные средства отладки. Вместе с тем, обилие инструментальных возможностей никоим образом не исключает необходимости разработки программ для всестороннего изучения ОС. Практическое применение API системы позволяет лучше изучить ее особенности, а также дает возможность создавать гибкие приложения, которые соответствуют сложным сценариям и требованиям, предъявляемым к современному программному обеспечению.

Заключение

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

Лекция 3. Базовые понятия ОС Windows

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

Прерывания, исключения, системные вызовы

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

Из теории ОС известно [2], [11], [8], что современные ОС реализуют поддержку системных вызовов, обработку прерываний и исключительных ситуаций, которые относят к основным механизмам ОС.

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

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

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

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

Прогон программы реализующей структурную обработку исключений

В качестве упражнения рекомендуется выполнить прогон программы, в которой произведена обработка деления на 0. Особенности применения операторов try и except описаны в MSDN.

#include <stdio.h>

void main() {

int i = 1, j = 0, k = 0;
__try {
   k = i / j;
   puts("in try");
   printf("k=%d\n",k);
}
__except (1) {
   puts("in except");
   printf("k=%d\n",k);
}
}

Реализация прерываний, системных вызовов и исключений в ОС Windows

Рассмотрим реализацию основных механизмов операционной системы в ОС Windows. Следует отметить, что терминология корпорации Microsoft несколько отличается от общепринятой. Например, системные вызовы называются системными сервисами, а под программным прерыванием (см. прерывания DPC и APC) понимается выполнение специфичных функций ядра, требующих прерывания работы текущего процесса.

Ловушки

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

В типичном случае сохраняются и впоследствии восстанавливаются:

Эта информация специфицирована в структуре CONTEXT (файл winnt.h), и может быть получена пользователем с помощью функции GetThreadContext.

Адрес части ядра ОС, ответственной за обработку данного конкретного события определяется из вектора прерываний, который номеру события ставит в соответствие адрес процедуры его первичной обработки. Это оказывается возможным, поскольку все события типизированы и их число ограничено. Для асинхронных событий их номер определяется контроллером прерываний, а для синхронных - ядром. В [6] описана процедура просмотра вектора прерываний, который в терминологии корпорации Microsoft называется таблицей диспетчеризации прерываний (interrupt dispatch table, IDT), при помощи отладчика kd. Например, для x86 процессора прерыванию от клавиатуры соответствует номер 0x52, системным сервисам - 0x2e, а исключительной ситуации, связанной со страничной ошибкой, - 0xE (см. рис. 3.1рс. 3.1).

Вектор прерываний (IDT)


Рис. 3.1.  Вектор прерываний (IDT)

После прохождения первичной обработки для каждого события предусмотрена процедура его последующей обработки другими частями ОС. Например, обработка системного сервиса (системного вызова) предполагает передачу управления по адресу 0x2e, где располагается диспетчер системных сервисов, которому через регистры EAX и EBX передаются номер запрошенного сервиса и список параметров, передаваемых этому системному сервису.

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

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

mov еах, Ox17   номер системного сервиса для NtCreateFile
mov ebx, esp    
int Ox2E        обработка системного сервиса
ret Ox2C        возврат управления

Рисунок 3.2 иллюстрирует дальнейшую обработку данного сервиса.

Пример обработки системного вызова (системного сервиса).


Рис. 3.2.  Пример обработки системного вызова (системного сервиса).

Приоритеты. IRQL

В большинстве операционных систем аппаратные прерывания имеют приоритеты, которые определяются контроллерами прерываний. Однако ОС Windows имеет свою аппаратно-независимую шкалу приоритетов, которые называются уровни запросов прерываний (interrupt request levels, IRQL), и охватывает не только прерывания, а все события, требующие системной обработки. В таблице 3.1 приведены значения IRQL уровней для x86 систем.

Таблица 3.1. Уровни запросов прерываний (IRQL) в x86 системах
УровеньЗначениеНомер
HighНаивысший уровень31
Power failОтказ электропитания30
Inter-process interruptМежпроцессорный сигнал29
ClockСистемные часы28
ProfileКонтроль производительности ядра27
Device nПрерывание от устройства26
Прерывания от устройств
Device 1Прерывание от устройства3
DPC/dispatchОтложенные операции и планирование2
APCАсинхронные вызовы процедур1
PassiveНормальное выполнение потоков0

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

Значения IRQL для аппаратных прерываний расставляются диспетчером Plug and Play с помощью уровня абстрагирования от оборудования HAL, а для остальных событий - ядром. Таким образом, уровень IRQL определяется источником события, что имеет иной смысл, нежели приоритеты в стратегии планирования потоков. Разбиение на IRQL уровни является основным механизмом упорядочивания по приоритетам действий операционной системы.

Можно сказать, что в ОС Windows действует двухуровневая схема планирования. Приоритеты высшего уровня (в данном случае IRQLs) определяются аппаратными или программными прерываниями, а приоритеты низшего уровня (в своем диапазоне от 0 до 31) устанавливаются для пользовательских потоков, выполняемых на нулевом уровне IRQL, и контролируются планировщиком.

На нулевом (PASSIVE LEVEL) уровне IRQL работают пользовательские процессы и часть кода операционной системы. Программа, работающая на этом уровне, может быть вытеснена почти любым событием, случившимся в системе. Большинство процедур режима ядра старается удерживать IRQL уровень процессора как можно более низким.

IRQL уровни 1 (APC LEVEL) и 2 (DISPATCH LEVEL) предназначены для так называемых программных (в терминологии Microsoft) прерываний соответственно: асинхронный вызов процедуры - APC (asynchronous procedure call) и отложенный вызов процедуры - DPC (deferred procedure call). Если ядро принимает решение выполнить некоторую системную процедуру, но нет необходимости делать это немедленно, оно ставит ее в очередь DPC и генерирует DPC прерывание. Когда IRQL процессора станет достаточно низким, эта процедура выполняется. Характерный пример - отложенная операция планирования. Из этого следует, что код, выполняемый на IRQL уровне, выше или равном 2, не подвержен операции планирования. Асинхронный вызов процедур - механизм, аналогичный механизму DPC, но более общего назначения, в частности, доступный пользовательским процессам.

IRQL уровни 3-26 относятся к обычным прерываниям от устройств. Более подробное описание IRQL уровней имеется в [6].

Заключение

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

Лекция 4. Объекты. Менеджер объектов. Реестр

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

Введение

Для работы с важными системными ресурсами ОС Windows создает объекты, управление которыми осуществляет менеджер объектов. Когда приложение открывает файл, создает поток или семафор, оно получает описатель ( handle ) соответствующего объекта (см. рис. 4.1). Например, после выполнения программного оператора

hSemaphore = CreateSemaphore(NULL, 0, MaxCount, "MySemaphore");

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

Создание объекта "семафор" приложением


Рис. 4.1.  Создание объекта "семафор" приложением

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

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

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

Объекты ядра

В рамках данного курса нам придется активно использовать объекты, называемые в руководствах по Win32-программированию объектами ядра (kernel objects). Поддержка объектов ядра осуществляется собственно ядром и исполнительной системой. Помимо объектов ядра имеются также объекты, предназначенные для управления окнами (User), и объекты, предназначенные для управления графикой (GDI). Изучение этих категорий объектов, реализуемых подсистемой поддержки окон и графики и ориентированных на разработку графических интерфейсов пользователя, выходит за пределы данного курса.

К сожалению, понятие "объект ядра" имеет разный смысл у разных авторов (ср., например, это понятие в MSDN или в [4], c одной стороны, и в [6] - с другой), поэтому для дальнейшего изложения потребуется уточнение терминологии.

Дело в том, что совокупность объектов образует слоеную структуру. Ядро поддерживает базовые объекты двух видов: объекты диспетчера (события, мьютексы, семафоры, потоки ядра, таймеры и др.) и управляющие (DPC, APC, прерывания, процессы, профили и др.) Более подробно эти внутриядерные объекты описаны в [6].

Над объектами ядра находятся объекты исполнительной системы, каждый из которых инкапсулирует один или более объектов ядра. Объекты исполнительной системы предназначены для управления памятью, процессами и межпроцессным обменом. Они экспортируются в распоряжение пользовательских приложений через Win32 функции. К ним относятся такие объекты, как: процесс, поток, открытый файл, семафор, мьютекс, маркер доступа и ряд других. Полный список можно увидеть в MSDN. Эти объекты и называются объектами ядра в руководствах по программированию.

Внешнее отличие объектов ядра (объектов исполнительной системы) от объектов User и GDI состоит в наличии у первых атрибутов защиты, которые являются одним из параметров, создающих объект ядра функций. Далее эти объекты ядра (объекты исполнительной системы) будут называться просто объектами.

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

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

Структура объекта. Методы объекта

Структура объекта


Рис. 4.2.  Структура объекта

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

Счетчик ссылок на объект увеличивается на 1 при открытии объекта и уменьшается на 1 при его закрытии. Значение счетчика ссылок, равное нулю, означает, что объект больше не используется и выделенное ему адресное пространство ядра может быть освобождено. Наличие счетчика означает, что даже после завершения процесса, создавшего объект, этот объект может не быть разрушен (если его счетчик не обнулен).

Квота устанавливает ограничения на объемы ресурсов. Несмотря на то, что в ОС Windows реализован код для отслеживания квот, в настоящее время квоты не применяются и существуют достаточно мягкие ограничения. Например, по умолчанию лимит на открытые объекты для процесса - 230. Множество объектов делится на типы, а у каждого из объектов есть атрибуты, неизменные для объектов данного типа. Ссылка на тип объекта также входит в состав заголовка. Поля имя объекта и каталог будут описаны в разделе "именование объектов".

Методы объекта

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

Описатели объектов

Создание новых объектов, или открытие по имени уже существующих, приложение может осуществить при помощи Win32-функций, таких, как CreateFile, CreateSemaphore, OpenSemaphore и т.д. Это библиотечные процедуры, за которыми стоят сервисы Windows и методы объектов. В случае успешного выполнения создается 64-битный описатель в таблице описателей процесса в памяти ядра. На эту таблицу есть ссылка из блока управления процессом EPROCESS (см. гл. Реализация процессов).

Из 64-х разрядов описателя 29 разрядов используются для ссылки на блок памяти объекта ядра, 3 - для флагов, а оставшиеся 32 - в качестве маски прав доступа. Маска прав доступа формируется на этапе создания или открытия объекта, когда выполняется проверка разрешений. Таким образом, описатель объекта - принадлежность процесса, создавшего этот объект. По умолчанию он не может быть передан другому процессу. Тем не менее, система предоставляет возможность дублирования описателя и передачи его другому процессу специальным образом (см. ниже раздел "Совместное использование объектов" и часть IV "Безопасность").

Объекты и их описатели


Рис. 4.3.  Объекты и их описатели

Win32-функции, создающие объект, возвращают приложению не сам описатель, а индекс в таблице описателей, то есть малое число: типа 1,2 а не 64-разрядное (см. рис. 4.3). Впоследствии это значение передается одной из функций, которая принимает описатель объекта в качестве аргумента. Одной из таких функций является функция CloseHandle, задача которой - закрыть объект. Во избежание утечки памяти всегда рекомендуется закрывать объект, если в нем отпала надобность. Впрочем, по окончании работы процесса система закрывает все его объекты. Таким образом, структуры объектов ядра доступны только ядру, приложение не может самостоятельно найти эти структуры в памяти и напрямую модифицировать их содержимое.

Именование объектов. Разделяемые ресурсы

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

"Увидеть" пространство имен можно только при помощи специальных инструментальных средств, например, с помощью утилиты winobj, входящей в состав MS Platform SDK. Другую версию этой утилиты можно бесплатно получить на сайте http://www.sysinternals.com.

Окно утилиты winobj


Рис. 4.4.  Окно утилиты winobj

Прогон программы, создающей объект

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

#include <windows.h>
#include <stdio.h>
void main(void)
{

HANDLE hMapFile;
HANDLE hFile; 
 
hFile = CreateFile("MyFile.txt", 
  GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ,
     NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); 
 

hMapFile = CreateFileMapping(hFile, NULL,
  PAGE_READWRITE, 0, 0, "MyFileObject");
getchar();
}

В результате работы этой программы файл "myfile.txt" отображается в память, при этом в каталоге объектов BaseNamedObjects создается объект "секция" с именем "MyFileObject".

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

Именование допускают не все объекты ядра. Обычно функция, создающая именованный объект, имеет в качестве параметра имя объекта. Например, функция CreateMutex создает именованный объект.

HANDLE CreateMutex( PSECURITY_ATTRIBUTES psa, 
  BOOL bInitialOwner, PCTSTR pszName);

Последний параметр pszName и есть искомое имя (можно создать безымянный объект, если указать здесь NULL ). Между прочим, объект "открытый файл" является неименованным, и, следовательно, создающая этот объект функция CreateFile этого параметра не имеет. Собственного имени файла вполне достаточно для его идентификации и разделения, а пространство имен файлов является продолжением пространства имен объектов (см. рис. 4.5)

Связь пространства имен объектов и пространства имен файловой системы


Рис. 4.5.  Связь пространства имен объектов и пространства имен файловой системы

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

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

Наиболее распространенный вариант - когда двум или более процессам известно имя разделяемого объекта. В этом случае один из процессов создает объект (например, с помощью функции CreateSemaphore ), а остальные открывают его для себя (например, с помощью функции OpenSemaphore ).

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


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

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

Система управляет объектами "файл" отлично от других объектов ядра. Объект "файл" содержит указатель текущей позиции в файле. Поэтому каждый вызов функции CreateFile для одного и того же файла приводит к созданию нового объекта "файл" и соответственно - нового описателя объекта. Лишь процедуры дублирования описателей и наследования позволяют получить новый описатель к одному и тому же объекту. Более сложные сценарии создания объектов и дублирования описателей к ним имеются в MSDN

Реестр

Операционная система управляет большим объемом информации, необходимой для ее загрузки и конфигурирования. В ранних версиях Windows эта информация содержалась в различных текстовых файлах с расширением .ini (Win.ini, System.ini и т.д.). Начиная с Windows 95, эта информация хранится в централизованной общесистемной базе данных, называемой реестром (registry). Для просмотра и модификации данных реестра имеются штатные утилиты (regedit или редактор реестра, например), однако рекомендуется это делать с помощью административной консоли управления.

Общий вид редактора реестра


Рис. 4.7.  Общий вид редактора реестра

Данные реестра хранятся в виде иерархической древовидной структуры. Каждый узел или каталог называется разделом или ключом (keys), а названия каталогов верхнего уровня начинаются со строки HKEY. Каждый раздел может содержать подраздел (subkey). Записи нижней части структуры называются параметрами (values), данные которых строго типизированы, см. MSDN.

Реестр содержит шесть корневых разделов: HKEY_CURRENT_USER, HKEY__USERS, HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, HKEY_PERFORMANCE_DATA и HKEY_CURRENT_CONFIG. Наиболее важным, вероятно, является раздел HKEY_LOCAL_MACHINE. В нем содержится вся информация о локальной системе.

Пространство имен реестра интегрировано с общим пространством имен ядра. Оно является третьим пространством имен в системе наряду с пространствами имен объектов и файлов. Для интеграции система поддерживает объект "раздел реестра" (key есть среди типов объектов).

Реестр хранится на диске в виде набора файлов, называемых "кустами" или "ульями" (hives). Большинство из них находится в каталоге \Systemroot\System32\Config. Большое значение уделяется повышению надежности хранения. В частности, система ведет протоколы модификации кустов (при помощи так называемых регистрационных кустов, log hives), которые обеспечивают гарантированную возможность восстановления постоянных кустов реестра. Для еще большей защиты целостности на диске поддерживаются зеркальные копии критически важных кустов. Структура кустов подробно описана в [6], а их описатели можно просмотреть с помощью утилиты Handleex.exe с сайта http://www.sysinternals.com.

Данные реестра полностью доступны через Win32 API. Существуют вызовы для просмотра, создания и удаления разделов и параметров. Чтобы получить доступ к данным, нужно (при наличии необходимых привилегий) открыть соответствующий раздел с помощью функции RegOpenKeyEx. Для записи или удаления можно использовать функции RegSetValue, RegDeleteValue. Более подробно работа с данными реестра описана в MSDN и в [4], [5].

Заключение

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

Лекция 5. Реализация процессов и потоков

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

Понятие процесса и потока

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

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

Для описания столь сложного динамического объекта ОС поддерживает набор структур, главную из которых принято называть блоком управления процессом (PCB, Process control block). В состав PCB обычно включают:

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

Потоки

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

Процесс с несколькими потоками


Рис. 5.1.  Процесс с несколькими потоками

В этом случае процесс можно рассматривать в качестве контейнера ресурсов, а все проблемы, связанные с динамикой исполнения, решаются на уровне потоков. Обычно каждый процесс начинается с одного потока, а остальные (при необходимости) создаются в ходе выполнения. Теперь уже не процесс, а поток характеризуется состоянием, поток является единицей планирования, процессор переключается между потоками, и необходимо сохранять контекст потока (что существенно проще, чем сохранение контекста процесса). Подобно процессам потоки (нити, threads) в системе описываются структурой данных, которую обычно называют блоком управления потоком (thread control block, TCB).

Реализация процессов

Внутреннее устройство процессов в ОС Windows

В 32-разрядной версии системы у каждого процесса есть 4-гигабайтное адресное пространство, в котором пользовательский код занимает нижние 2 гигабайта (в серверах 3 Гбайта). В своем адресном пространстве, которое представляет собой набор регионов и описывается специальными структурами данных (см. часть III "система управления памятью"), процесс содержит потоки, учетную информацию и ссылки на ресурсы, которые обобществляются всеми потоками процесса.

Блок управления процессом (PCB) реализован в виде набора связанных структур, главная из которых называется блоком процесса EPROCESS. Соответственно, каждый поток также представлен набором структур во главе с блоком потока ETHREAD. Эти наборы данных, за исключением блоков переменных окружения процесса и потока (PEB и TEB), существуют в системном адресном пространстве. Упрощенная схема структур данных процесса показана на рис. 5.2.

Управляющие структуры данных процесса


Рис. 5.2.  Управляющие структуры данных процесса

Содержимое блока EPROCESS подробно описано в [6]. Блок KPROCESS (на рис. справа), блок переменных окружения процесса (PEB) и структура данных, поддерживаемая подсистемой Win32 (блок процесса Win32), содержат дополнительные сведения об объекте "процесс".

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

Создание процесса

Обычно процесс создается другим процессом вызовом Win32-функции CreateProcess (а также CreateProcessAsUser и CreateProcessWithLogonW ). Создание процесса осуществляется в несколько этапов.

На первом этапе, выполняемом библиотекой kernel32.dll в режиме пользователя, на диске отыскивается нужный файл-образ, после чего создается объект "раздел" памяти для его проецирования на адресное пространство нового процесса.

На втором этапе выполняется обращение к системному сервису NtCreateProcess для создания объекта "процесс". Формируются блоки EPROCESS, KPROCESS и блок переменных окружения PEB. Менеджер процессов инициализирует в блоке процесса маркер доступа (копируя аналогичный маркер родительского процесса), идентификатор и другие поля.

На третьем этапе в уже полностью проинициализированном объекте "процесс" необходимо создать первичный поток. Это, посредством системного сервиса NtCreateThread, делает библиотека kernel32.dll.

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

Наконец, запускается первичный поток, для чего формируются его начальный контекст и стек, и выполняется запуск стартовой процедуры потока режима ядра KiThreadStartup. После этого стартовый код из библиотеки C/C++ передает управление функции main() запускаемой программы.

В книге [6] этапы создания процесса описаны более подробно.

Функция CreateProcess

Таким образом, если приложение намерено создать новый процесс, один из его потоков должен обратиться к Win32-функции CreateProcess.

BOOL CreateProcess( 
  PCTSTR pszApplicationName, 
  PTSTR pszCommandLine, 
  PSECURITY_ATTRIBUTES psaProcess, 
  PSECURITY_ATTRIBUTES psaThread, 
  BOOL bInheritHandles, 
  DWORD fdwCreate, 
  PVOID pvEnvironment,
  PCTSTR pszCurDir, 
  PSTARTUPINFO psiStartInfo, 
  PPROCESS_INFORMATION ppiProcInfo);

Описание параметров функции можно посмотреть в MSDN.

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

Прогон программы создания процесса

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

#include <windows.h>
#include <stdio.h>
void main( VOID )
{
    STARTUPINFO StartupInfo;
    PROCESS_INFORMATION ProcInfo;
    TCHAR CommandLine[] = TEXT("sleep");

    ZeroMemory( &StartupInfo, sizeof(StartupInfo) );
    StartupInfo.cb = sizeof(StartupInfo);
    ZeroMemory( &ProcInfo, sizeof(ProcInfo) );

    if( !CreateProcess( NULL, // Не используется имя модуля 
        CommandLine,          // Командная строка
        NULL,                 // Дескриптор процесса не наследуется. 
        NULL,                 // Дескриптор потока не наследуется. 
        FALSE,                // Установка описателей наследования
        0,                    // Нет флагов создания процесса
        NULL,                 // Блок переменных окружения родительского процесса
        NULL,                 // Использовать текущий каталог родительского процесса
        &StartupInfo,         // Указатель на структуру  STARTUPINFO.
        &ProcInfo )           // Указатель на структуру информации о процессе.
      )
    
	printf( "CreateProcess failed." );
  
    // Ждать окончания дочернего процесса
    WaitForSingleObject( ProcInfo.hProcess, INFINITE );

    // Закрыть описатели процесса и потока 
    CloseHandle( ProcInfo.hProcess );
    CloseHandle( ProcInfo.hThread );
}

В приведенной программе имя запускаемого модуля передается через второй параметр функции CreateProcess. В примере в качестве дочерней программы используется простейшая программа sleep, задача которой - выдержать паузу длительностью 10 секунд.

#include <windows.h>
#include <stdio.h>
void main( VOID )
{
printf("Данная программа будет спать 
в течение 10000 мс\n");
Sleep(10000);
}

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

Завершение процесса может быть осуществлено различными способами, например, с помощью функций ExitProcess, TerminateProcess. Однако, единственным способом, гарантирующим корректную очистку всех ресурсов, является возврат управления входной функцией первичного потока. Помимо перечисленных в системе имеется много полезных функций, реализующих API для управления процессами. Их полный перечень содержится в MSDN.

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

Реализация потоков

Состояния потоков

Каждый новый процесс содержит, по крайней мере, один поток, остальные потоки создаются динамически. Потоки составляют основу планирования и могут: выполняться на одном из процессоров, ожидать события или находиться в каком-то ином состоянии (см. рис. 5.3 и лекцию "Планирование потоков").

Состояния потоков в ОС Windows (версии Server 2003)


Рис. 5.3.  Состояния потоков в ОС Windows (версии Server 2003)

Обычно в состоянии "Готовности" имеется очередь готовых к выполнению (running) потоков. В данном случае это состояние распадается на три составляющих. Это, собственно, состояние "Готовности (Ready)"; состояние "Готов. Отложен (Deferred Ready)", что означает, что поток выбран для выполнения на конкретном процессоре, но пока не запланирован к выполнению; и, наконец, состояние "Простаивает (Standby)", в котором может находиться только один выбранный к выполнению поток для каждого процессора в системе.

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

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

Прогон программы, иллюстрирующей состояния потоков

Системный монитор (обкладка "Производительность") представляет собой удобное средство наблюдения за состояниями потоков. Предлагается осуществить прогон программы, которая содержит длительные циклы счета и ожидания. Например, программа, вычисляющая 5*107 значений функций sin(x):

#include <windows.h>
#include <stdio.h>
#include <math.h>
 
VOID main( VOID ) { 
  int i,N=50000000;
  double a,b;
  getchar();
  printf("Before circle\n");
  for ( i = 0; i<N; i++) {
	 b=(double)i / (double)N;
	 a=sin(b);	
	}
	printf("After circle\n");
	getchar();
}

Графическое представление предполагает присвоение цифровых значений различным состояниям потока (например, готовность - 1, выполнение - 2, ожидание - 5 и т.п.). Результат работы монитора на однопроцессорной системе для данной программы представлен на рис. 5.4.

Иллюстрация перехода потока из одного состояния в другое


Рис. 5.4.  Иллюстрация перехода потока из одного состояния в другое

Горизонтальные участки со значением 5 соответствуют ожиданию нажатия клавиши ввода, а значению 1 (читателю предлагается самостоятельно ответить на вопрос, почему значению 1, а не значению 2) соответствует счетный цикл.

Отдельные характеристики потоков

Идентификаторы потоков, так же как и идентификаторы процессов, кратны четырем, выбираются из того же пространства, что и идентификаторы процессов, и с ними не пересекаются.

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

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

Волокна и задания

Переключение между потоками занимает довольно много времени, поэтому для облегченного псевдопараллелизма в системе поддерживаются волокна (fibers). Наличие волокон позволяет реализовать собственный механизм планирования, не используя встроенный механизм планирования потоков на основе приоритетов. ОС не знает о смене волокон, для управления волокнами нет и настоящих системных вызовов, однако есть вызовы Win32 API ConvertThreadToFiber, CreateFiber, SwitchToFiber и т. д. Подробнее функции, связанные с волокнами, описаны в документации Platform SDK.

В системе есть также задания (job object), которые обеспечивают управление одним или несколькими процессами как группой.

Внутреннее устройство потоков

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

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

Управляющие структуры данных потока


Рис. 5.5.  Управляющие структуры данных потока

Изображенные на рис. 5.5 структуры, за исключением блоков переменных окружения потока (TEB), существуют в системном адресном пространстве. Помимо этого, параллельная структура для каждого потока, созданного в Win32-процессе, поддерживается процессом Csrss подсистемы Win32. В свою очередь, часть подсистемы Win32, работающая в режиме ядра (Win32k.sys), поддерживает для каждого потока структуру W32THREAD.

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

Создание потоков

Создание потока инициируется Win32-функцией CreateThread, которая находится в библиотеке Kernel32.dll. При этом создается объект ядра "поток", хранящий статистическую информацию о создаваемом потоке. В адресном пространстве процесса выделяется память под пользовательский стек потока. Затем инициализируется аппаратный контекст потока (ниже имеется описание соответствующей структуры CONTEXT).

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

Функция CreateThread

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

HANDLE CreateThread ( 
PSECURITY_ATTRIBUTES psa, 
DWORD cbStack,
PTHREAD_START_ROUTINE  pfnStartAddr, 
PVOID pvParam, 
DWORD fdwCreate, 
PDWORD pdwThreadID);

Прогон программы создания потока

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

#include <windows.h>
#include <stdio.h>

DWORD WINAPI MyThread( LPVOID lpParam ) 
{ 
 printf("Parameter = %d\n", *(DWORD*)lpParam);
 return 0; 
} 
 
VOID main( VOID ) 
{ 
  DWORD ThreadId, ThreadParameter = 10; 
  HANDLE hThread; 
  
  hThread = CreateThread( 
    NULL,            // атрибуты безопасности по умолчанию 
    0,               // размер стека по умолчанию  
    MyThread  ,        // указатель на процедуру создаваемого потока
    &ThreadParameter,      // аргумент, передаваемый функции потока 
    0,               // флаги создания по умолчанию
    &ThreadId);          // возвращаемый идентификатор потока

	 if (hThread == NULL)  printf("CreateThread failed." );    
  
    getchar();
    CloseHandle( hThread );
   }

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

Завершение потока можно организовать разными способами, например, с помощью функций ExitThread или TerminateThread. Рекомендуемый [4] способ - возврат управления функцией потока. Это единственный способ, который гарантирует корректную очистку всех ресурсов, принадлежавших потоку.

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

Контекст потока, переключение контекстов

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

Эта информация сохраняется в текущем стеке ядра потока.

Контекст отражает состояние регистров процессора на момент последнего исполнения потока и хранится в структуре CONTEXT, определенной в заголовочном файле WinNT.h. Элементы этой структуры соответствуют регистрам процессора, например, для процессоров x86 процессоров в ее состав входят Eax, Ebx, Ecx, Edx и т д.. Win32-функция GetThreadContext позволяет получить текущее состояние контекста, а функция SetThreadContext - задать новое содержимое контекста. Перед этой операцией поток рекомендуется приостановить.

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

Заключение

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

Лекция 6. Планирование потоков

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

Введение

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

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

Процедура планирования обычно связана с весьма затратной процедурой диспетчеризации - переключением процессора на новый поток, поэтому планировщик должен заботиться об эффективном использовании процессора. Принадлежность потоков к процессу при планировании не учитывается, то есть единицей планирования в ОС Windows является именно поток. Запуск процедуры планирования удобно проиллюстрировать на упрощенной (по сравнению с диаграммой, изображенной на рис. 5.3) диаграмме состояний потока, см. рис. 6.1.

Упрощенная диаграмма состояний потоков в ОС Windows


Рис. 6.1.  Упрощенная диаграмма состояний потоков в ОС Windows

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

Это, во-первых, события, связанные с освобождением процессора.

(1) Завершение потока

(2) Переход потока в состояние готовности в связи с тем, что его квант времени истек

(3) Переход потока в состояние ожидания

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

(4) Поток вышел из состояния ожидания

(5) Поток только что создан

(6) Деятельность текущего потока может иметь следствием вывод другого потока из состояния ожидания.

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

Наконец, процедура планирования может быть запущена, если изменяется приоритет потока в результате вызова системного сервиса или самой Windows, а также если изменяется привязка (affinity) потока к процессору, из-за чего поток не может больше выполняться на текущем процессоре.

Заметим, что переключение из пользовательского режима в режим ядра (и обратно) не влияет на планирование потока, так как контекст в этом случае не меняется.

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

Перевод потока из состояния ожидания в состояние готовности (вариант 4) может быть следствием прерывания, свидетельствующим об окончании операции ввода-вывода. В этом случае процедура планирования может быть отложена (deffered procedure call) до окончания выполнения высокоприоритетного системного кода.

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

Алгоритмы планирования

Приоритеты

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

В системе предусмотрено 32 уровня приоритетов. Шестнадцать значений приоритетов (16-31) соответствуют группе приоритетов реального времени, пятнадцать значений (1-15) предназначены для обычных потоков, и значение 0 зарезервировано для системного потока обнуления страниц (см. рис. 6.2).

Приоритеты потоков


Рис. 6.2.  Приоритеты потоков

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

Относительный приоритет потока устанавливается аналогичными параметрами функции SetThreadPriority:

Совокупность из шести классов приоритетов процессов и семи классов приоритетов потоков образует 42 возможные комбинации и позволяет сформировать так называемый базовый приоритет потока (см. таб. 6.1).

Таблица 6.1. Формирование базового приоритета потока из класса приоритета процесса и относительного приоритета потока
Приоритеты потоков
Классы приоритетов процессовКритичный ко времениСамый высокийВыше нормыНормальныйНиже нормыСамый низкийНеработающий
Неработающий15654321
Ниже нормы15876541
Нормальный151098761
Выше нормы15121110981
Высокий1515141312111
Реального времени31262524232216

Базовый приоритет процесса и первичного потока по умолчанию равен значению из середины диапазонов приоритетов процессов (24, 13, 10, 8, 6 или 4). Смена приоритета процесса влечет за собой смену приоритетов всех его потоков, при этом их относительные приоритеты остаются без изменений.

Приоритеты с 16 по 31 в действительности приоритетами реального времени не являются, поскольку в рамках поддержки мягкого реального времени, которая реализована в ОС Windows, никаких гарантий относительно сроков выполнения потоков не дается. Это просто более высокие приоритеты, которые зарезервированы для системных потоков и тех потоков, которым такой приоритет дает пользователь с административными правами. Тем не менее, наличие приоритетов реального времени, а также вытесняемость кода ядра, локализация страниц памяти (см. лекцию 10) и ряд дополнительных возможностей - все это позволяет выполнять в среде ОС Windows приложения мягкого реального времени, например, мультимедийные. Системный поток с нулевым приоритетом занимается обнулением страниц памяти. Обычные пользовательские потоки могут иметь приоритеты от 1 до 15.

Динамическое повышение приоритета

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

Например, после завершения операции ввода-вывода увеличивают приоритет потока, чтобы дать ему возможность быстрее начать выполнение и, может быть, вновь инициировать операцию ввода-вывода. Таким способом система поощряет интерактивные потоки и поддерживает занятость устройств ввода-вывода. Величина, на которую повышается приоритет, не документирована и зависит от устройства (рекомендованные значения для диска и CD - это 1, для сети - 2, клавиатуры и мыши - 6 и звуковой карты - 8). В дальнейшем в течение нескольких квантов времени приоритет плавно снижается до базового.

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

Динамическое повышение приоритета решает также проблему голодания потоков, долго не получающих доступ к процессору. Обнаружив такие потоки, простаивающие в течение примерно 4 сек., система временно повышает их приоритет до 15 и дает им два кванта времени. Побочным следствием применения этой технологии может быть решение известной проблемы инверсии приоритетов [11]. Эта проблема возникает, когда низкоприоритетный поток удерживает ресурс, блокируя высокоприоритетные потоки, претендующие на этот ресурс. Решение состоит в искусственном повышении его приоритета на некоторое время.

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

Прогон программы, демонстрация приоритетного планирования

#include <windows.h>
#include <stdio.h>
#include <math.h>


void Calculations()
{
int i,N=50000000;
double a,b;
for ( i = 0; i<N; i++) {
  b=(double)i / (double)N;
  a=sin(b);
 }
}

DWORD WINAPI SecondThread( LPVOID lpParam ) 
{
	
 printf("Begin of Second Thread\n");
 Calculations();
 printf("End of Second Thread\n");
 
return 0; 
} 
 
VOID main( VOID ) 
{ 
 DWORD dwThreadId, dwThrdParam; 
 HANDLE hThread; 

 hThread = CreateThread( 
        NULL,                        
        0,                           
        SecondThread,                
        &dwThrdParam,                
        0,                           
        &dwThreadId);                
        
 if (hThread == NULL) 
  {
   printf("CreateThread failed\n" ); 
   return;
  }

 SetThreadPriority(hThread, THREAD_PRIORITY_ABOVE_NORMAL);
	 
   SuspendThread(hThread);    
   getchar();
   ResumeThread(hThread);

  printf("Begin of First Thread\n");
  Calculations();
  printf("End of First Thread\n");   
}

В приведенной программе два параллельных потока выполняют длительный счетный цикл (подпрограмма Calculations). Второй поток в силу более высокого приоритета выполняется раньше. Пара функций Suspend/ResumeThread (приостановка и возобновление потока) используется для фиксации начала соревнования. Если закомментировать SetThreadPriority, то можно будет увидеть, что оба потока заканчивают работу одновременно.

В качестве самостоятельного упражнения рекомендуется реализовать более гибкие сценарии планирования, например, с добавлением функций SwitchToThread (передача управления потоку), или Sleep (приостановка потока в течение заданного промежутка времени). В MSDN имеется описание множества полезных функций, связанных с планированием потоков.

Величина кванта времени

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

По умолчанию начальная величина кванта в Windows Professional равна двум интервалам таймера, а в Windows Server эта величина увеличена до 12, чтобы свести к минимуму переключение контекста. Длительность интервала таймера определяется HAL и составляет примерно 10 мс для однопроцессорных x86 систем и 15 мс - для многопроцессорных. Величину интервала системного таймера можно определить с помощью свободно распространяемой утилиты Clockres (сайт http://sysinternals.com).

Выбор между короткими и длинными значениями можно сделать с помощью панели "свойства" "Моего компьютера". Величина кванта задается в параметре HKLM\SYSTEM\CurrentControlSet\Control\PriorityControl\Win32PrioritySeparation реестра.

Планирование в условиях многопроцессорности

Реентерабельность кода ядра позволяет ОС Windows поддерживать симметричные мультипроцессорные системы (процессоры идентичны). Необходимость загрузки нескольких процессоров усложняет задачу планирования. Количество процессоров система определяет при загрузке, и эта информация становится доступной приложениям через функцию GetSystemInfo. Число процессоров, используемых системой, может быть ограничено с помощью параметра NumPcs из файла Boot.ini.

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

Привязка к процессорам

У каждого потока имеется маска привязки к процессорам (affinity mask), указывающая, на каких процессорах можно выполнять данный поток. По умолчанию Windows использует нежесткую привязку (soft affmity) потоков к процессорам. Это означает, что некоторое преимущество имеет последний процессор, на котором выполнялся поток, чтобы повторно использовать данные из кэша этого процессора (родственное планирование). Потоки наследуют маску привязки процесса. Изменение привязки процесса и потока может быть осуществлено с помощью Win32-функций SetProcessAffinityMask и SetThreadAfftnityMask или с помощью инструментальных средств Windows (например, это может сделать диспетчер задач). Есть также возможность сформировать априорную маску привязки в файле образе запускаемого процесса.

Помимо номера последнего процессора в блоке ядра потока KTHREAD хранится номер идеального процессора (ideal processor) - предпочтительного для выполнения данного потока. Идеальный процессор выбирается случайным образом при создании потока. Это значение увеличивается на 1 всякий раз, когда создается новый поток, поэтому создаваемые потоки равномерно распределяются по набору доступных процессоров. Поток может поменять это значение с помощью функции SetThreadIdealProcessor.

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

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

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

Жесткая привязка (hard affinity), выполняемая с помощью функций SetProcessAffinityMask и SetThreadAfftnityMask, целесообразна в архитектурах с неунифицируемым (NUMA) доступом, где скорость доступа к памяти зависит от взаимного расположения процессоров и банков памяти на системных платах.

Заключение

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

Лекция 7. Межпроцессный обмен

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

Введение

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

Известно также, что в основе межпроцессного (Inter Process Communications, IPC) обмена обычно находится разделяемый ресурс (например, канал или сегмент разделяемой памяти), и, следовательно, ОС должна предоставить средства для генерации, именования, установки режима доступа и атрибутов защиты таких ресурсов. Обычно такой ресурс может быть доступен всем процессам, которые знают его имя и имеют необходимые привилегии.

Кроме того, организация связи между процессами всегда предполагает установления таких ее характеристик, как:

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

Способы межпроцессного обмена.

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

Основные способы межпроцессного обмена


Рис. 7.1.  Основные способы межпроцессного обмена

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

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

Ограниченный объем курса не позволяет рассмотреть другие механизмы межпроцессного обмена, реализованные в ОС Windows, например, сокеты, Clipboard или удаленный вызов процедуры (RPC). Исчерпывающая справочная информация на эту тему имеется в MSDN.

Понятие о разделяемом ресурсе

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

В качестве примера рассмотрим общение через разделяемую память (рис. 7.2).

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


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

В ОС Windows сегмент разделяемой памяти создается с помощью Win32-функции CreateFileMapping (см. рис. 7.3). В случае успешного выполнения данной функции создается ресурс - фрагмент памяти, доступный по имени (параметр lpname ), который базируется на соответствующем объекте ядра - "объекте-файле, отображаемом в память" с присущими любому объекту атрибутами. Процессу-создателю возвращается описатель (handle) ресурса. Другие процессы, желающие иметь доступ к ресурсу, также должны получить его описатель. В данном случае это можно сделать с помощью функции OpenFileMapping, указав имя ресурса в качестве одного из параметров.

Создание сегмента разделяемой памяти базируется на разделяемом ресурсе, которому соответствует объект ядра


Рис. 7.3.  Создание сегмента разделяемой памяти базируется на разделяемом ресурсе, которому соответствует объект ядра

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

Обмен через каналы связи осуществляется через буфер в адресном пространстве ядра системы


Рис. 7.4.  Обмен через каналы связи осуществляется через буфер в адресном пространстве ядра системы

Каналы связи

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

Механизм генерации канала предполагает получение процессом-создателем (процессом-сервером) двух описателей (handles) для пользования этим каналом. Один из описателей применяется для чтения из канала, другой - для записи в канал.

Один из вариантов использования канала - это его использование процессом для взаимодействия с самим собой. Рассмотрим следующее изображение системы, состоящей из процесса и ядра, после создания канала (рис. 7.5):

Общение процесса с самим собой через канал связи


Рис. 7.5.  Общение процесса с самим собой через канал связи

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

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

Общение процессов через канал связи


Рис. 7.6.  Общение процессов через канал связи

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

Передача информации от клиента серверу через канал связи


Рис. 7.7.  Передача информации от клиента серверу через канал связи

Организация каналов в ОC Windows

Анонимные каналы

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

Анонимные каналы создаются процессом сервером при помощи функции CreatePipe:

BOOL CreatePipe(
  PHANDLE hReadPipe,                       // описатель для чтения
  PHANDLE hWritePipe,                      // описатель для записи
  LPSECURITY_ATTRIBUTES lpPipeAttributes,  // атрибуты безопасности
  DWORD nSize                              // размер канала
);

Функция CreatePipe возвращает два описателя (дескриптора) для чтения и записи в канал. После создания канала необходимо передать клиентскому процессу эти дескрипторы (или один из них), что обычно делается с помощью механизма наследования.

Для наследования описателя нужно, чтобы дочерний процесс создавался функцией CreateProcess с флагом наследования TRUE. Предварительно нужно создать наследуемые описатели. Это можно, например, сделать путем явной спецификации параметра bInheritHandle структуры SECURITY_ATTRIBUTES при создании канала.

Другим способом является создание наследуемого дубликата имеющегося описателя при помощи функции DuplicateHandle и последующая передача его создаваемому процессу через командную строку или каким-либо иным образом.

Получив нужный описатель, клиентский процесс, так же как и сервер, может далее взаимодействовать с каналом при помощи функций ReadFile и WriteFile. По окончании работы с каналом оба процесса должны закрыть описатели при помощи функции CloseHandle.

Прогон программы общения процесса через анонимный канал с самим собой

#include <windows.h>
#include <stdio.h>

int main()
{
	HANDLE hRead, hWrite;
	char BufIn[100], *BufOut = "0123456789";
	int BufSize = 100;
	int BytesOut = 10, BytesIn = 5, i;
	
	if(!CreatePipe(&hRead, &hWrite, NULL, BufSize))  
		printf("Create pipe failed.\n");
	
	WriteFile(hWrite, BufOut, BytesOut, &BytesOut, NULL);
	 printf("Write into pipe %d bytes : ", BytesOut);
        for(i=0; i<BytesOut;i++) printf("%c",BufOut[i]);
	 printf("\n");
	
	ReadFile(hRead, BufIn, BytesIn, &BytesIn, NULL);
	 printf("Read from pipe %d bytes : ", BytesIn);
	
	 for(i=0; i<5;i++) printf("%c",BufIn[i]);
	 
	return 0;
}

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

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

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

Именованные каналы

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

Каналы должны иметь уникальные в рамках сети имена в соответствии с правилами именования ресурсов в сетях Windows (Universal Naming Convention, UNC), например, \\ServerName\pipe\PipeName. Для общения внутри одного компьютера имя записывается в форме \\.\pipe\PipeName, где "." обозначает локальную машину. Слово "pipe" в составе имени фиксировано, а PipeName - имя, задаваемое пользователем. Эти имена, подобно именам открытых файлов, не являются именами объектов. Они относятся к пространству имен под управлением драйверов файловых систем именованных каналов ( \Winnt\System32\Drivers\Npfs.sys ), привязанному к специальному объекту устройству \Device\NamedPipe, на которое есть ссылка в каталоге глобальных имен объектов \??\Pipe (эти последние имена "видит" утилита WinObj).

Имена созданных именованных каналов можно перечислить с помощью свободно распространяемой утилиты pipelist с сайта http://www.sysinternals.com. Поскольку имена каналов интегрированы в общую структуру имен объектов, приложения могут открывать именованные каналы с помощью функции CreateFile и взаимодействовать с ними через функции ReadFile и WriteFile.

Использование именованных каналов

Сервер создает именованный канал при помощи функции CreateNamedPipe (см. MSDN).

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

Повторно вызывая CreateNamedPipe, можно создавать дополнительные экземпляры этого же канала.

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

После того как соединение по именованному каналу установлено, клиент и сервер могут использовать его для чтения и записи данных через Win32-функции ReadFile и WriteFile.

Прогон программы общения двух процессов через именованный канал

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

Сервер

#include <stdio.h>
#include <windows.h>

void main()
{
	PROCESS_INFORMATION piProcInfo;
	STARTUPINFO SI;
	char * ClientName = "client.exe";
	HANDLE hPipe;
	LPTSTR PipeName = TEXT("\\\\.\\pipe\\MyPipe");
	char Buff[255];
	DWORD iNumBytesToRead = 255, i;
	
	ZeroMemory(&SI, sizeof(STARTUPINFO));
	SI.cb = sizeof(STARTUPINFO); 
	ZeroMemory(&piProcInfo, sizeof(piProcInfo));

	hPipe = CreateNamedPipe( 
          PipeName,			   // имя канала
          PIPE_ACCESS_DUPLEX,       // чтение и запись из канала
          PIPE_TYPE_MESSAGE |       // передача сообщений по каналу
          PIPE_READMODE_MESSAGE |   // режим чтения сообщений 
          PIPE_WAIT,                // синхронная передача сообщений 
          PIPE_UNLIMITED_INSTANCES, // число экземпляров канала 
          4096,			   // размер выходного буфера
          4096,		          // размер входного буфера  
          NMPWAIT_USE_DEFAULT_WAIT, // тайм-аут клиента 
          NULL);                    // защита по умолчанию
	
	if (hPipe == INVALID_HANDLE_VALUE) 
    {
	 printf("CreatePipe failed: error code %d\n", (int)GetLastError());
        return;
    }
	
	if((CreateProcess(NULL, ClientName, NULL, NULL, FALSE, 0, NULL, NULL, &SI, &piProcInfo))==0)
	{
	printf("create client process: error code %d\n", (int)GetLastError());
	return;
	}
	
	if((ConnectNamedPipe(hPipe, NULL))==0)
	{
		printf("client could not connect\n");
		return;
	}
	
	ReadFile(hPipe, Buff, iNumBytesToRead, &iNumBytesToRead, NULL);
	for(i=0; i<iNumBytesToRead; i++) printf("%c",Buff[i]);
 }
Пример 7.1. (html, txt)

Клиент

#include <stdio.h>
#include <windows.h>

void main()
{
	HANDLE hPipe;
	LPTSTR PipeName = TEXT("\\\\.\\pipe\\MyPipe");
	DWORD NumBytesToWrite;
	char Buff[] = "Message from Client";

	hPipe = CreateFile( 
         PipeName,	      // имя канала
         GENERIC_READ |  // чтение и запись в канал
         GENERIC_WRITE, 
         0,              // нет разделяемых операций 
         NULL,           // защита по умолчанию
         OPEN_EXISTING,  // открытие существующего канала 
         0,              // атрибуты по умолчанию
         NULL);          // нет дополнительных атрибутов 

	WriteFile(hPipe, Buff, strlen(Buff), &NumBytesToWrite, NULL);
	
}

В данном примере сервер создает канал, затем запускает процесс-клиент и ждет соединения. Далее он читает сообщение, посланное клиентом.

Помимо перечисленных выше система представляет еще ряд полезных функций для работы с именованными каналами. Для копирования данных из именованного канала без удаления их из канала используется функция PeekNamedPipe. Функция TransactNamedPipe применяется для объединения операций чтения и записи в канал в одну операцию, которая называется транзакцией. Имеются информационные функции для определения состояния канала, например, GetNamedPipeHandleState или GetNamedPipeInfo. Полный перечень находится в MSDN.

Заключение

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

Лекция 8. Синхронизация потоков

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

Введение. Проблема взаимоисключения

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

Предположим, что два потока, фиксирующие какие-либо события, пытаются дать приращение общей переменной Count, счетчику этих событий (рис. 8.1).

Два параллельных потока увеличивают значение общей переменной Count


Рис. 8.1.  Два параллельных потока увеличивают значение общей переменной Count

Операция Count++ не является атомарной. Код операции Count++ будет преобразован компилятором в машинный код, который выглядит примерно так:

(1) MOV EAX, [Count]  ;   значение из Count помещается в регистр 
(2) INC EAX      ;        значение регистра увеличивается на 1 
(3) MOV [Count], EAX ;    значение из регистра помещается обратно в Count

В мультипрограммной системе с разделением времени может наступить неблагоприятная ситуация перемешивания (interleaving'а), когда поток T1 выполняет шаг (1), затем вытесняется потоком T2 , который выполняет шаги (1)-(3), а уже после этого поток T1 заканчивает операцию, выполняя шаги (2)-(3). В этом случае результирующее приращение переменной Count будет равно 1 вместо правильного приращения - 2.

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

Для устранения условий состязания необходимо обеспечить каждому потоку эксклюзивный доступ к разделяемым данным. Такой прием называется взаимоисключением (mutual exclusion). Часть кода потока, выполнение которого может привести к race condition, называется критической секцией (critical section). Например, операции (1)-(3) в примере, приведенном выше, являются критическими секциями обоих потоков. Таким образом, взаимоисключение необходимо обеспечить для критических секций потоков.

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

while (some condition) {	    
entry section	   
 critical section	   
 exit section	   
 remainder section
	}

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

Переменная-замок

Одним из возможных не вполне корректных решений проблемы синхронизации является использование переменной-замка. Например, можно сделать условием вхождения в критическую секцию значение 0 некоторой разделяемой переменной lock. Сразу же после проверки это значение меняется на 1 (закрытие замка). При выходе из критической секции замок открывается (значение переменной lock сбрасывается в 0 ).

shared int lock = 0;
 T1                                       T2
        while (some condition) {
         while(lock); 
         lock = 1;
                critical section
          lock = 0;
                remainder section
}

К сожалению, предложенное решение не всегда обеспечивает взаимоисключение. Вследствие того, что действие-пролог, состоящее из двух операций while(lock); lock = 1; не является атомарным, существует отличная от нуля вероятность вытеснения потока между этими операциями. При этом управление может перейти ко второму потоку, который, узнав, что переменная lock все еще равна 0, может войти в свою критическую секцию.

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

TSL команды

Многие вычислительные архитектуры имеют инструкции, которые могут обеспечить атомарность последовательности операций при входе в критическую секцию. Такие команды называются Test and_Set Lock или TSL командами. Если представить себе такую команду как функцию

int TSL (int *target){ 
int tmp = *target; 
*target = 1; 
return tmp; 
}

то, заменив в предыдущем примере последовательность операций while(lock); lock = 1; на TSL(lock), мы получаем решение проблемы взаимоисключения.

Семейство Interlocked-функций

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

LONG InterlockedExchangeAdd( PLONG plAddend, LONG lncrement);

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

InterlockedExchangeAdd (&Count,  1);

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

Реализация Interlocked-функций зависит от аппаратной платформы. На x86-процессорах они выдают по шине аппаратный сигнал, закрывая для других процессоров конкретный адрес памяти.

Существенно то, что Interlocked-функции выполняются в пользовательском режиме работы процессора в течение примерно 50 тактов, то есть чрезвычайно быстро.

Прогон программы синхронизации с помощью переменной замка

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

#include <windows.h>
#include <stdio.h>
#include <math.h>

int Sum = 0, iNumber=5, jNumber=300000;

DWORD WINAPI SecondThread(LPVOID){
  int i,j;
  double a,b=1.;
  
  for (i = 0; i < iNumber; i++)
  {
    for (j = 0; j < jNumber; j++)
    {
      Sum = Sum + 1;  a=sin(b);
    }
  }
  return 0;
}

void main(){
  int i,j;
  double a,b=1.;
  HANDLE  hThread;
  DWORD  IDThread;

  hThread=CreateThread(NULL, 0, SecondThread, NULL, 0, &IDThread);
  if (hThread == NULL) return;    

  for (i = 0; i < iNumber; i++)
  {
    for (j = 0; j < jNumber; j++)
    {
      Sum = Sum - 1;  a=sin(b);
    }
	printf(" %d ",Sum);	
  }	  
	  
 WaitForSingleObject(hThread, INFINITE); // ожидание окончания потока SecondThread
 printf(" %d ",Sum);	
 }

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

Рекомендуется ввести в данную программу синхронизацию с помощью глобальной переменной-замка, включив в нее операции while(lock) ; и lock=1 ; и добиться предсказуемости в работе программы.

Поскольку ситуация, в которой квант времени, выделенный потоку, истекает между while(lock) ; и lock=1 ; маловероятна, можно смоделировать ее искусственно, введя между этими операциями паузу (функция Sleep, например). Наконец, желательно реализовать правильное решение путем опроса и модификации переменной замка с помощью TSL-инструкции (функция InterlockedCompareExchange ).

Спин-блокировка

Рассмотренные решения проблемы синхронизации, безусловно, являются корректными. Они реализуют следующий алгоритм: перед входом в критическую секцию поток проверяет возможность входа и, если такой возможности нет, продолжает опрос значения переменной-замка. Такое поведение потока, связанное с его вращением в пустом цикле, называется активным ожиданием или спин-блокировкой (spin lock).

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

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

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

Critical Sections

В составе API ОС Windows имеются специальные и эффективные функции для организации входа в критическую секцию и выхода из нее потоков одного процесса в режиме пользователя. Они называются EnterCriticalSection и LeaveCriticalSection и имеют в качестве параметра предварительно проинициализированную структуру типа CRITICAL_SECTION.

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

CRITICAL_SECTION cs; 
DWORD WINAPI SecondThread() 
{ 
 InitializeCriticalSection(&cs);

EnterCriticalSection(&cs); 
… критический участок кода
LeaveCriticalSection(&cs); 
} 

main () 
{
InitializeCriticalSection(&cs);
CreateThread(NULL, 0, SecondThread,…);


EnterCriticalSection(&cs); 
… критический участок кода
LeaveCriticalSecLion(&cs); 
DeleteCriticalSection(&cs);
}

Функции EnterCriticalSection и LeaveCriticalSection реализованы на основе Interlocked-функций, выполняются атомарным образом и работают очень быстро. Существенным является то, что в случае невозможности входа в критический участок поток переходит в состояние ожидания. Впоследствии, когда такая возможность появится, поток будет "разбужен" и сможет сделать попытку входа в критическую секцию. Механизм пробуждения потока реализован с помощью объекта ядра "событие" (event), которое создается только в случае возникновения конфликтной ситуации.

Уже говорилось, что иногда, перед блокированием потока, имеет смысл некоторое время удерживать его в состоянии активного ожидания. Чтобы функция EnterCriticalSection выполняла заданное число циклов спин-блокировки, критическую секцию целесообразно проинициализировать с помощью функции InitalizeCriticalSectionAndSpinCount.

Прогон программы

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

Синхронизация потоков с использованием объектов ядра

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

Почти все объекты ядра, рассмотренные ранее, в том числе, процессы, потоки и файлы, пригодны для решения задач синхронизации. В контексте задач синхронизации о каждом из объектов можно сказать, находится ли он в свободном (сигнальном, signaled state) или занятом (nonsignaled state) состоянии. Правила перехода объекта из одного состояния в другое зависят от объекта. Например, если поток выполняется, то он находится в занятом состоянии, а если поток успешно завершил ожидание семафора, то семафор находится в занятом состоянии.

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

DWORD WaitForSingleObject(HANDLE hObject, DWORD dwMilliseconds);

где hObject - описатель ожидаемого объекта ядра, а второй параметр - максимальное время ожидания объекта.

Поток создает объект ядра при помощи семейства функций Create ( CreateSemaphore, CreateThread и т.д.), после чего объект посредством описателя становится доступным всем потокам данного процесса. Копия описателя может быть получена при помощи функции DuplicateHandle и передана другому процессу, после чего потоки смогут воспользоваться этим объектом для синхронизации.

Другим, более распространенным способом получения описателя является открытие существующего объекта по имени, поскольку многие объекты имеют имена в пространстве имен объектов. Имя объекта - один из параметров Create -функций. Зная имя объекта, поток, обладающий нужными правами доступа, получает его описатель с помощью Open -функций. Напомним, что в структуре, описывающей объект, имеется счетчик ссылок на него, который увеличивается на 1 при открытии объекта и уменьшается на 1 при его закрытии.

Несколько подробнее рассмотрим те объекты ядра, которые предназначены непосредственно для решения проблем синхронизации.

Семафоры

Известно, что семафоры, предложенные Дейкстрой в 1965 г., представляет собой целую переменную в пространстве ядра, доступ к которой, после ее инициализации, может осуществляться через две атомарные операции: wait и signal (в ОС Windows это функции WaitForSingleObject и ReleaseSemaphore соответственно).

wait(S):  если S <= 0 процесс блокируется 
   (переводится в состояние ожидания);    
              в противном случае S = S - 1;                               
signal(S):  S = S + 1

Семафоры обычно используются для учета ресурсов (текущее число ресурсов задается переменной S ) и создаются при помощи функции CreateSemaphore, в число параметров которой входят начальное и максимальное значение переменной. Текущее значение не может быть больше максимального и отрицательным. Значение S, равное нулю, означает, что семафор занят.

Ниже приведен пример синхронизации программы async с помощью семафоров.

#include <windows.h>
#include <stdio.h>
#include <math.h>

int Sum = 0, iNumber=5, jNumber=300000;
HANDLE hFirstSemaphore, hSecondSemaphore;

DWORD WINAPI SecondThread(LPVOID)
{
  int i,j;
  double a,b=1.;
    for (i = 0; i < iNumber; i++)
  {
    WaitForSingleObject(hSecondSemaphore, INFINITE); 
    for (j = 0; j < jNumber; j++)
    {
      Sum = Sum + 1;  a=sin(b); 
     }
    
     ReleaseSemaphore(hFirstSemaphore, 1, NULL);
    }
  return 0;
}

void main()
{
  int i,j;
  HANDLE  hThread;
  DWORD  IDThread;
  double a,b=1.;

  hFirstSemaphore = CreateSemaphore(NULL, 0, 1, "MyFirstSemaphore");
  hSecondSemaphore = CreateSemaphore(NULL, 1, 1, "MySecondSemaphore1");
  hThread=CreateThread(NULL, 0, SecondThread, NULL, 0, &IDThread);
  if (hThread == NULL) return;    
  
  for (i = 0; i < iNumber; i++)
  {
	WaitForSingleObject(hFirstSemaphore, INFINITE);
        for (j = 0; j < jNumber; j++)
    {
      Sum = Sum - 1;  a=sin(b);
	}
    	printf(" %d ",Sum);	
	ReleaseSemaphore(hSecondSemaphore, 1, NULL);  
  }	  

WaitForSingleObject(hThread, INFINITE); // ожидание окончания потока SecondThread
CloseHandle(hFirstSemaphore);
CloseHandle(hSecondSemaphore);
printf(" %d ",Sum);	
return;
}

В данной программе синхронизация действий двух потоков , обеспечивающая одинаковый результат для всех запусков программы, выполнена с помощью двух семафоров, примерно так, как это делается в задаче producer-consumer, см., например [11]. Потоки поочередно открывают друг другу дорогу к критическому участку. Первым начинает работать поток SecondThread, поскольку значение счетчика удерживающего его семафора проинициализировано единицей при создании этого семафора. Синхронизацию с помощью семафоров потоков разных процессов рекомендуется выполнить в качестве самостоятельного упражнения.

Мьютексы

Мьютексы также представляют собой объекты ядра, используемые для синхронизации, но они проще семафоров, так как регулируют доступ к единственному ресурсу и, следовательно, не содержат счетчиков. По существу они ведут себя как критические секции, но могут синхронизировать доступ потоков разных процессов. Инициализация мьютекса осуществляется функцией CreateMutex, для входа в критическую секцию используется функция WaitForSingleObject, а для выхода - ReleaseMutex.

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

События

Объекты "события" - наиболее примитивные объекты ядра. Они предназначены для информирования одного потока другим об окончании какой-либо операции. События создаются функцией CreateEvent. Простейший вариант синхронизации: переводить событие в занятое состояние функцией WaitForSingleObject и в свободное - функцией SetEvent.

В руководстве по программированию [4], [9], рассматриваются более сложные сценарии, связанные с типом события (сбрасываемые вручную и сбрасываемые автоматически) и с управлением синхронизацией групп потоков, а также ряд дополнительных полезных функций.

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

Суммарные сведения об объектах ядра

В руководствах по программированию, см., например, [4], и в MSDN содержатся сведения и о других объектах ядра применительно к синхронизации потоков.

В частности, существуют следующие свойства объектов:

Синхронизация в ядре

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

Одна из проблем связана с тем, что код ядра зачастую работает на приоритетных IRQL (уровни IRQL рассмотрены в лекции 3 ) уровнях "DPC/dispatch" или "выше", известных как "высокий IRQL". Это означает, что традиционные средства синхронизации, связанные с приостановкой потока, не могут быть использованы, поскольку процедура планирования и запуска другого потока имеет более низкий приоритет. Вместе с тем существует опасность возникновения события, чей IRQL выше, чем IRQL критического участка, который будет в этом случае вытеснен. Поэтому в подобных ситуациях прибегают к приему, который называется "запрет прерываний" [2], [11]. В случае Windows этого добиваются, искусственно повышая IRQL критического участка до самого высокого уровня, используемого любым возможным источником прерываний. В результате критический участок может беспрепятственно выполнить свою работу.

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

Наконец, имея набор глобальных ресурсов, в данном случае - спин-блокировок, необходимо решить проблему возникновения потенциальных тупиков [7]. Например, поток 1 захватывает блокировку 1, а поток 2 захватывает блокировку 2. Затем поток 1 пытается захватить блокировку 2, а поток 2 - блокировку 1. В результате оба потока ядра виснут. Одним из решений данной проблемы является нумерация всех ресурсов и выделение их только в порядке возрастания номеров [2]. В случае Windows имеется иерархия спин-блокировок: все они помещаются в список в порядке убывания частоты использования и должны захватываться в том порядке, в каком они указаны в списке.

В случае низких IRQL синхронизация осуществляется традиционным образом - при помощи объектов ядра.

Заключение

Проблема недетерминизма является одной из ключевых в параллельных вычислительных средах. Традиционное решение - организация взаимоисключения. Для синхронизации с применением переменной-замка используются Interlocked-функции, поддерживающие атомарность некоторой последовательности операций. Взаимоисключение потоков одного процесса легче всего организовать с помощью примитива Crytical Section. Для более сложных сценариев рекомендуется применять объекты ядра, в частности, семафоры, мьютексы и события. Рассмотрена проблема синхронизации в ядре, основным решением которой можно считать установку и освобождение спин-блокировок.

Лекция 9. Введение. Виртуальное адресное пространство процесса

Система управления памятью является одной из наиболее важных в составе ОС. Традиционная схема предполагает связывание виртуального и физического адресов на стадии исполнения программы. Для управления виртуальным адресным пространством в нем принято организовывать сегменты (регионы), для описания которых используются структуры данных VAD (Virtual Address Descriptors). Для создания региона и передачи ему физической памяти можно использовать функцию VirtualAlloc. Описана техника использования таких регионов, как куча процесса, стек потока и регион файла, отображаемого в память

Введение

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

Для описания системы управления памятью активно используются понятия физической и логической (виртуальной) памяти.

Физическая память является аппаратным запоминающим устройством компьютера. Менеджер памяти имеет дело с двумя уровнями физической памяти: оперативной (основной, первичной) и внешней, или вторичной. Оперативная память изготавливается с применением полупроводниковых технологий и теряет свое содержимое при отключении питания. Вторичная память (это, главным образом, диски) характеризуется гораздо более медленным доступом, однако имеет большую емкость и является энергонезависимой. Она используется в качестве расширения основной памяти. Обычно информация, хранимая в оперативной памяти, за исключением самых последних изменений, хранится также во внешней памяти. Если процессор не обнаруживает нужную информацию в оперативной памяти, он начинает искать ее во вторичной. Когда нужная информация найдена во внешней памяти, она переносится в оперативную память. Менеджер памяти старается по возможности снизить частоту обращений к вторичной памяти (свойство локальности или локализации обращений). В результате эффективное время доступа к памяти оказывается близким к времени доступа к оперативной памяти и составляет несколько десятков наносекунд.

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

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

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

Расположение сегментов процессов в памяти компьютера


Рис. 9.1.  Расположение сегментов процессов в памяти компьютера

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

Связывание адресов

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

Формирование логического адреса и связывание логического адреса с физическим


Рис. 9.2.  Формирование логического адреса и связывание логического адреса с физическим

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

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

Общее описание виртуальной сегментно-страничной памяти ОС Windows

Размер пользовательского процесса ограничен объемом логического адресного пространства. Характерный размер логической памяти определяется разрядностью архитектуры и составляет для современных систем 232 (в недалеком будущем 264) байт. Эта величина обычно существенно превышает объем оперативной памяти, поэтому часть пользовательского процесса прозрачным образом может быть размещена во внешней памяти. Поэтому у пользователя создается иллюзия того, что он имеет дело с виртуальной памятью, отличной от реальной, размер которой потенциально больше, чем размер оперативной памяти. В дальнейшем наряду с термином "логическая память" будет употребляться термин "виртуальная память".

Для определения схемы виртуальной памяти, реализованной в ОС Windows, лучше всего подходит термин "сегментно-страничная виртуальная память". Подробное описание сегментно-страничной модели можно найти в [2]. Для нее характерно представление адресного пространства процесса в виде набора сегментов переменного размера, содержащих однородную информацию (данные, текст программы, стек, сегмент разделяемой памяти и др.). Для удобства отображения на физическую память каждый сегмент делится на страницы - блоки фиксированного размера, при этом физическая память делится на блоки того же размера - страничные кадры (фреймы). Функция связывания логического адреса с физическим возлагается на таблицу страниц, которая каждой логической странице сегмента ставит в соответствие страничный кадр. В тех случаях, когда для нужной страницы не находится места в оперативной памяти (page fault), она подкачивается с диска. Заметим, что в каноническом виде данной схемы каждый сегмент процесса находится в отдельном логическом адресном пространстве и использует свою собственную таблицу страниц. Последнее обстоятельство, в силу сложной организации и большого объема таблицы страниц, имеет следствием тот факт, что реальные системы редко придерживаются канонической формы.

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

Одна из задач, которая решается при этом, - избежать появления в системе большого количества таблиц страниц за счет организации неперекрывающихся регионов в одном виртуальном пространстве, для описания которого хватает одной таблицы страниц. Таким образом, одна таблица страниц будет отводиться для всех сегментов памяти процесса. То, как это делается можно увидеть на рис. 9.3. Задействовано всего четыре аппаратных сегмента с номерами селекторов 08, 10, 1b и 23. Первый используется для адресации кода ОС и имеет атрибуты RE, второй с атрибутами RW - для данных и стека ОС, третий с атрибутами RE - для кода пользовательского процесса, а четвертый с атрибутами RW - для данных и стека пользовательского процесса. Первые два сегмента недоступны для непривилегированного режима работы процессора.

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

Образование неперекрывающихся регионов (программных сегментов) в линейном виртуальном адресном  пространстве процесса


Рис. 9.3.  Образование неперекрывающихся регионов (программных сегментов) в линейном виртуальном адресном пространстве процесса

Любопытно, что наличие у аппаратного сегмента атрибута не является препятствием для нецелевого использования хранимой в сегменте информации. Например, код процесса, находящийся в сегменте 1b, может быть доступен через 23-й сегмент с атрибутами RW. Собственно защита регионов организована на уровне их описателей, которые хранятся в таблице описателей VAD (virtual address descriptors) в адресном пространстве процесса. Таким образом, аппаратная поддержка сегментации обеспечивает лишь минимальную защиту - невозможность доступа к данным ОС из непривилегированного режима. Можно сказать, что в ОС Windows осуществляется программная поддержка сегментации (в данном случае регионов). Между прочим, многие другие ОС (например, Linux) ведут себя аналогично. Программная поддержка сегментов более универсальна и способствует большей переносимости кода. В дальнейшем для обозначения непрерывного фрагмента виртуального адресного пространства, содержащего однородную информацию, будет использоваться термин "регион".

Таблица страниц ставит в соответствие виртуальной странице номер страничного кадра в оперативной памяти. Для описания совокупности занятых и свободных страничных кадров ОС Windows использует базу данных PFN (page frame number). В силу несоответствия размеров виртуальной и оперативной памяти достаточно типичной является ситуация отсутствия нужной страницы в оперативной памяти (page fault). К счастью, копии всех задействованных виртуальных страниц хранятся на диске (так называемые теневые страницы). В случае обращения к отсутствующей странице ОС должна разыскать соответствующую теневую страницу и организовать ее подкачку с диска.

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

Таким образом, деятельность системы управления памятью сводится к созданию регионов (программных сегментов) в виртуальном адресном пространстве, выделения для них места в физической памяти (частично в оперативной памяти и частично на диске) и прозрачное перенаправление обращений к виртуальным адресам к их аналогам в физической памяти. Регионы создаются операционной системой. Иногда это происходит по инициативе пользовательской программы (например, в результате вызова функций VirtualAlloc, CreateFileMapping, CreateHeap и др.). Существенная часть деятельности менеджера памяти связана с оптимизацией. В частности, много усилий затрачивается на сокращение количества обращений к внешней памяти

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

Инструментальные средства наблюдения за работой менеджера памяти

Из инструментальных средств Windows, описанных в лекции 2, для лучшего практического ознакомления с деятельностью по управлению памятью в работе будут активно использоваться вкладки "Процессы и "Быстродействие" диспетчера задач, а также разнообразные счетчики производительности, за поведением которых можно следить из оснастки "Производительность" ("Системный монитор") административной консоли панели управления.

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

Полезными оказываются и некоторые общедоступные утилиты, например, утилита наблюдения за ошибками страниц pfmon.

Авторы книги[6] отдельные аспекты функционирования менеджера памяти иллюстрируют с помощью отладчика.

Виртуальное адресное пространство процесса

В 32-битных системах процессор может сгенерировать 32-битный адрес. Это означает, что каждому процессу выделяется диапазон виртуальных адресов от 0x00000000 до 0xFFFFFFFF. Эти 4 Гб адресов система делит примерно пополам, и для кода и данных пользовательского режима отводятся 2 Гб в нижней части памяти. Если быть более точным, то речь идет об виртуальных адресах, начиная с 0x00010000 и кончая 07FFEFFFF (см. [6]). Таким образом, система управления памятью позволяет пользовательской программе с помощью Win32 API записать нужный байт в любую виртуальную ячейку из этого диапазона адресов. Адреса верхней части виртуальной памяти используется для кода и данных режима ядра и других системных нужд.

По умолчанию адресное пространство каждого процесса изолировано. Данные двух разных процессов, записанные по одному и тому же виртуальному адресу, оказываются в разных страницах физической памяти при помощи корректной работы системы трансляции адреса. В ряде случаев изоляция может быть частично снята (файлы, отображаемые в память; разделяемая память). Разумеется, в подобных случаях нужно отдельно обеспечить контроль доступа к области памяти, для чего создается отдельный объект (объект-секция или объект-раздел, section object), включающий атрибуты защиты. Ниже будет также приведен пример контроля процессом памяти другого процесса - прием, которым активно пользуются отладчики.

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

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

Совокупность регионов описывается структурой VAD, которая организована в виде двоичного дерева и хранится в PCB (структура EPROCESS, см. лекцию 5) процесса. Для вновь создаваемого региона можно запросить любой диапазон адресов с учетом уже существующих регионов. Первые регионы для кода, стека, стандартной кучи процесса и ряд других создает операционная система в момент загрузки процесса. Последующие регионы приложение создает самостоятельно (см. рис. 9.4).

Совокупность регионов в пользовательской части (нижние 2 Гб) виртуального адресного пространства процесса


Рис. 9.4.  Совокупность регионов в пользовательской части (нижние 2 Гб) виртуального адресного пространства процесса

Создание (резервирование) региона и передача ему физической памяти

Для создания региона явным образом обычно используется функция VirtualAlloc (вызов ряда Win32 функций, таких, как CreateFileMapping или CreateHeap, также имеет следствием создание региона). В процессе создания региона выделяют два этапа: резервирование региона и передачу ему физической памяти (commit). Оба этапа выполняются в результате вызова VirtualAlloc и могут быть объединены. В итоге каждая виртуальная страница может оказаться в одном из трех состояний: свободная (free), зарезервированная (reserve) и переданная (committed)

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

Резервирование региона предполагает выравнивание начала региона с учетом гранулярности памяти (обычно это 64 Кб). Кроме того, размер региона должен быть кратен объему страницы (4Кб для x86 процессора. Узнать размер страницы можно при помощи функции GetSystemInfo ). В случае успешного резервирования происходит коррекция дерева VAD.

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

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

Прогон программы выделения памяти при помощи функции Virtual Alloc

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

#include <windows.h>
#include <stdio.h>

void main(void)
{
PVOID pMem = NULL;
char * String;
char * pMemCommited;
int nPageSize = 4096;
int Shift = 4080;

pMem =  VirtualAlloc(0, nPageSize*16, MEM_RESERVE, PAGE_READWRITE);
pMemCommited = (char *)pMem + 3 * nPageSize;
VirtualAlloc((PVOID) pMemCommited, nPageSize, MEM_COMMIT, PAGE_READWRITE);

String = pMemCommited + Shift;

sprintf(String,"Hello, world");
printf("%s\n", String);

VirtualFree(String, 0, MEM_RELEASE);
}

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

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

#include <windows.h>
#include <stdio.h>

void main(void)
{

PVOID pMem = NULL;
int nPageSize = 4096;
long SizeCommit = 0;
int nPages = 200;

SizeCommit = nPages * nPageSize;

getchar();

pMem =  VirtualAlloc(0, SizeCommit, 
MEM_RESERVE| MEM_COMMIT, PAGE_READWRITE);
if(pMem == NULL) printf("VirtualAlloc Error\n");
getchar();

pMem =  VirtualAlloc(0, SizeCommit, 
MEM_RESERVE| MEM_COMMIT, PAGE_READWRITE);
if(pMem == NULL) printf("VirtualAlloc Error\n");
getchar();

pMem =  VirtualAlloc(0, SizeCommit, 
MEM_RESERVE| MEM_COMMIT, PAGE_READWRITE);
if(pMem == NULL) printf("VirtualAlloc Error\n");
getchar();

pMem =  VirtualAlloc(0, SizeCommit, 
MEM_RESERVE| MEM_COMMIT, PAGE_READWRITE);
if(pMem == NULL) printf("VirtualAlloc Error\n");
getchar();
}

Из текста программы видно, что каждый раз по нажатию клавиши "Enter" процессу передается 200 страниц (819200 байт) виртуальной памяти. Это легко проверить по соответствующему приращению счетчика "Байт виртуальной памяти" (см. рис. 9.5).

Поведение счетчика "Байт виртуальной памяти",  выделенной процессу DemoVM.


Рис. 9.5.  Поведение счетчика "Байт виртуальной памяти", выделенной процессу DemoVM.

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

Регион куча

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

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

Передачей памяти кучам, а также учетом свободной и занятой памяти в куче занимается специальный диспетчер куч (heap manager). Эта деятельность не документирована.

Стандартная куча процесса размером 1 Мб (эту величину можно изменить) резервируется в момент создания процесса. Обычно куча динамически меняет свой размер (флаг growable ). Стандартную кучу процесса используют не только приложения, но и некоторые Win32-функции. Для использования стандартной кучи необходимо получить ее описатель при помощи функции GetProcessHeap.

При желании процесс может создать дополнительные кучи при помощи функции HeapCreate (обратная операция HeapDestroy ). Прикладная программа выделяет память в куче с помощью функции HeapAlloc, а освобождает при помощи HeapFree.

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

Прогон программы выделения памяти в стандартной куче

#include <windows.h>
#include <stdio.h>

void main(void)
{
 HANDLE hHeap;
 long Size = 1024;
 long Shift = 1017;
 char * pHeap;
 char * String;

 hHeap = GetProcessHeap();

 pHeap  = (char *) HeapAlloc(hHeap, HEAP_ZERO_MEMORY, Size);
 if(pHeap == NULL) { printf("HeapAlloc error\n"); return; }

 String = pHeap + Shift;
 sprintf(String, "Hello, world");
 printf("Heap contents string: %s\n", String);

 HeapFree(hHeap,0,pHeap);
}

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

В качестве самостоятельного упражнения можно рекомендовать наблюдение за наращиванием объема переданной в куче памяти при помощи счетчиков производительности. Объем виртуальной памяти процесса должен начать расти в случае выхода за пределы начального размера кучи (1 Мб) и при образовании дополнительных куч.

Регион стека потока. Сторожевые страницы

Для поддержки функционирования стека потока также резервируется соответствующий регион. Стек - динамическая структура. Принято, чтобы стек увеличивал свой размер в сторону уменьшения адресов. Сколько страниц памяти потребуется стеку потока, заранее не известно. Поэтому в ОС Windows организована поэтапная, по мере необходимости, передача физической памяти стеку при помощи механизма так называемых "сторожевых" страниц (guard page). Обращение к сторожевой странице имеет следствием уведомление системы об этом (исключительная ситуация 0x80000001 ), после чего флаг PAGE GUARD сбрасывается и со страницей можно работать как с обычной страницей преданной памяти. Сторожевая страница служит ловушкой для перехвата ссылок за ее пределы.

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

Прогон программы, моделирующей обращение к сторожевым страницам

#include <windows.h>
#include <stdio.h>

void main(void)
{
PVOID pMem = NULL;
char * String;
char * pMemCommited;
int nPageSize = 4096;
int Shift = 4000;

pMem =  VirtualAlloc(0, nPageSize*16, 
MEM_RESERVE |MEM_COMMIT, PAGE_READWRITE| PAGE_GUARD );
pMemCommited = (char *)pMem + 3 * nPageSize;

String = pMemCommited + Shift;
__try {
sprintf(String,"Hello, world");
printf("Before exception number 0x80000001 string: %s \n", String);
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
sprintf(String,"Hello, world");
printf("After exception number 0x80000001 string: %s \n", String);
}
VirtualFree(String, 0, MEM_RELEASE);
}

В приведенной программе происходит передача памяти региону и установка флага PAGE_GUARD для его страниц. Кроме того, используется структурная обработка исключений. В случае попытки записи текстовой строки на сторожевую страницу (в блоке try ) возникает исключительная ситуация exception 0x80000001. При повторной попытке записи на эту страницу (блок except ) подобная ситуация уже не возникает.

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

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

Регион файла, отображаемого в память

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

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

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

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

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

#include <windows.h>
#include <stdio.h>

void main(void){

HANDLE hMapFile;
LPVOID lpMapAddress;
HANDLE hFile; 
char * String;
 
hFile = CreateFile( "MyFile.txt",                       // имя файла
                    GENERIC_READ | GENERIC_WRITE,       // режим доступа
                    FILE_SHARE_READ| FILE_SHARE_WRITE,  // совместный доступ                                
                    NULL,                               // защита по умолчанию                 
	              CREATE_ALWAYS,                      // способ создания
                    FILE_ATTRIBUTE_NORMAL,              // атрибуты файла
                    NULL);                              // файл атрибутов 
 
if (hFile == INVALID_HANDLE_VALUE)  printf("Could not open file\n");   

hMapFile = CreateFileMapping(hFile,    // описатель отображаемого файла 
    NULL,                              // атрибуты защиты по умолчанию
    PAGE_READWRITE,                    // режим доступа 
    0,                                 // старшее двойное слово размера буфера
    20,                                // младшее двойное слово размера буфера
   "MyFileObject");                    // имя объекта
 
if (hMapFile == NULL) { 
    printf("Could not create file-mapping object.\n"); return;
} 

lpMapAddress = MapViewOfFile(hMapFile,  // описатель отображаемого файла
    FILE_MAP_ALL_ACCESS,                // режимы доступа
    0, 0,                               // отображение файла с начала
    0);                                 // отображение целого файла
 
if (lpMapAddress == NULL) { 
    printf("Could not map view of file.\n"); return;
} 

String = (char *)lpMapAddress;
sprintf(String, "Hello, world");
printf("%s\n", String);

if (!UnmapViewOfFile(lpMapAddress)) printf("Could not unmap view of file.\n"); 
}

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

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

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

Решение задачи совместного доступа к памяти будет приведено ниже.

Здесь мы временно прекратим изучение виртуальной памяти процесса. Основным итогом изложенного можно считать знакомство с возможностями ОС создавать в ней разнообразные регионы. В качестве самостоятельного упражнения можно рекомендовать анализ состояния виртуального адресного пространства при помощи функций VirtualQuery и VirtualQueryEx (см., например, [4]). Теперь перейдем к рассмотрению других аспектов функционирования менеджера памяти: структуре физической памяти и особенностям трансляции адреса.

Заключение

Система управления памятью является одной из наиболее важных в составе ОС. Традиционная схема предполагает связывание виртуального и физического адреса на стадии исполнения программы. Для управления виртуальным адресным пространством в нем принято организовывать сегменты (регионы), для описания которых используются структуры данных VAD (virtual address descriptors). Для создания региона и передачи ему физической памяти можно использовать функцию VirtualAlloc. Описана техника использования таких регионов, как куча процесса, стек потока и регион файла, отображаемого в память.

Лекция 10. Функционирование менеджера памяти

Рассмотрены особенности поддержки виртуальной памяти. Базовой операцией менеджера памяти является трансляция виртуального адреса в физический с помощью таблицы страниц и ассоциативной (TLB) памяти. В ряде случаев, для реализации разделяемой памяти, интеграции с системой ввода/вывода и др., применяется прототипная таблица страниц, которая является промежуточным звеном между обычной таблицей страниц и физической памятью. Для описания страниц физической памяти поддерживается база данных PFN (Page Frame Number). Локализацию страниц памяти, контроль процессом памяти другого процесса и технику копирования при записи можно отнести к интересным особенностям системы управления памятью ОС Windows

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

Трансляция адреса в страничной виртуальной схеме


Рис. 10.1.  Трансляция адреса в страничной виртуальной схеме

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

Каждая строка в таблице страниц называется PTE (page table entry). В 32-разрядных архитектурах IA-32 PTE занимает 4 байта (см. рис. 10.2).

Структура строки таблицы страниц (PTE)


Рис. 10.2.  Структура строки таблицы страниц (PTE)

Назначение отдельных атрибутов ясно из рисунка. Например, информация об обращении к странице или ее модификации (биты 5 и 6) позволяет собирать статистику обращений к памяти, которую используют алгоритмы выталкивания страниц. С точки зрения процесса трансляции наиболее важную роль играет бит присутствия V (Valid). Согласно терминологии, принятой в [6], PTE с установленным битом V называются "действительными", а соответствующая страница находится в оперативной памяти.

При сброшенном V бите возникает страничная ошибка (page fault). Наиболее распространенный вариант page fault'а - нахождение страницы в файле выгрузки (отсутствующая страница). В этом случае первые 20 битов PTE будут указывать на смещение в страничном файле. Обработка таких ситуаций состоит в приостановке процесса, сгенерировавшего page fault, подкачке страницы с диска в свободный кадр физической памяти, модификации PTE и возобновлении неудавшейся операции. Таким образом, недействительный PTE превращается в действительный. Эта ситуация возникает часто, более того, нередки случаи обращения к одной и той же отсутствующей странице двух потоков одного процесса (конфликт ошибок страниц), которые успешно обрабатываются системой (см. [6]). Помимо нужной страницы диспетчер на всякий случай загружает в память несколько, обычно от 1 до 8, соседних страниц, чтобы минимизировать количество обращений к диску (стратегия подкачки по требованию с кластеризацией).

В действительности трансляция происходит более сложно (см., например, [2]). Существенная часть записей PTE кэшируется в ассоциативной памяти (TLB регистрах) процессора. При этом таблицу страниц в силу ее большого объема конструируют из двух уровней: каталог таблиц страниц (page directory) и таблицы страниц (page table), см. рис. 10.3.

Трансляция адреса с использованием ассоциативной памяти и двухуровневой таблицы страниц


увеличить изображение

Рис. 10.3.  Трансляция адреса с использованием ассоциативной памяти и двухуровневой таблицы страниц

Размер таблицы страниц подобран таким образом, что она целиком заполняет одну страницу оперативной памяти - 4 Кб. Для быстрого нахождения таблицы страниц один из регистров процессора (CR3 в Intel) указывает на каталог таблиц страниц (page directory) данного процесса, который хранится по адресу 0хС0300000. Значение этого регистра входит в контекст процесса. Поэтому, а также потому, что при смене исполняемого потока буфер ассоциативной памяти нуждается в обновлении, несколько увеличивается время переключения контекстов. Эти особенности архитектуры хорошо известны и здесь не будут описываться подробно. В дальнейшем для упрощения в схеме трансляции адреса таблица страниц будет изображаться одномерной.

Разделяемая память

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

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

Реализация разделяемого между двумя процессами региона проецируемого в память файла


увеличить изображение

Рис. 10.4.  Реализация разделяемого между двумя процессами региона проецируемого в память файла

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

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

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

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

Рассмотрим текст двух программ first.c и second.c

First.c
#include <windows.h>
#include <stdio.h>

void main(void){
HANDLE hMapFile;
LPVOID lpMapAddress;
HANDLE hFile; 
char * String;
 
hFile = CreateFile("MyFile.txt",                     // имя файла
                   GENERIC_READ | GENERIC_WRITE,     // файл для чтения и записи 
                   FILE_SHARE_READ| FILE_SHARE_WRITE,// режим совместного доступа 
                   NULL,                             // защита по умолчанию
                   OPEN_EXISTING,                    // файл должен существовать 
                   FILE_ATTRIBUTE_NORMAL,            // атрибуты файла
                   NULL);                            // файл атрибутов 
 
if (hFile == INVALID_HANDLE_VALUE)   printf("Could not open file\n"); 

hMapFile = CreateFileMapping(hFile,    // описатель отображаемого файла 
    NULL,                              // атрибуты защиты по умолчанию
    PAGE_READWRITE,                    // режим доступа 
    0,                                 // старшее двойное слово размера буфера
    0,                                 // младшее двойное слово размера буфера
   "MyFileObject");                    // имя объекта

if (hMapFile == NULL) printf("Could not create file-mapping object.\n"); 


lpMapAddress = MapViewOfFile(hMapFile,  // описатель отображаемого файла
    FILE_MAP_ALL_ACCESS,                // режимы доступа
    0, 0,                               // отображение файла с начала
    0);                                 // отображение целого файла

if (lpMapAddress == NULL) printf("Could not map view of file.\n"); 

String = (char *)lpMapAddress;
sprintf(String, "Hello, world");
getchar();
}
Пример 10.1. (html, txt)
second.c
#include <windows.h>
#include <stdio.h>

void main(void){

HANDLE hMapFile;
LPVOID lpMapAddress;
HANDLE hFile; 
char * String;
 
hFile = CreateFile("MyFile.txt",                     // имя файла
                   GENERIC_READ | GENERIC_WRITE,     // файл для чтения и записи 
                   FILE_SHARE_READ| FILE_SHARE_WRITE,// режим совместного доступа 
                   NULL,                             // защита по умолчанию
                   OPEN_EXISTING,                    // файл должен существовать 
                   FILE_ATTRIBUTE_NORMAL,            // атрибуты файла
                   NULL);                            // файл атрибутов 

if (hFile == INVALID_HANDLE_VALUE) 
{ 
        printf("Could not open file\n");   // process error 
} 


hMapFile = OpenFileMapping(FILE_MAP_ALL_ACCESS, // разрешение чтения-записи 
                           FALSE,               // описатель не наследуется
                          "MyFileObject");      // имя объекта проецируемого файла 

if (hMapFile == NULL)  printf("Could not open Filemapping\n"); 

lpMapAddress = MapViewOfFile(hMapFile,  // описатель отображаемого файла
    FILE_MAP_ALL_ACCESS,                // режимы доступа
    0, 0,                               // отображение файла с начала
    0);                                 // отображение целого файла

if (lpMapAddress == NULL) printf("Could not map view of file.\n"); 

String = (char *)lpMapAddress;
printf("%s\n", String);
getchar();
}
Пример 10.2. (html, txt)

Программа first создает в своем адресном пространстве буфер разделяемой памяти, а программа second отображает тот же самый буфер в свое адресное пространство. Затем программа first записывает в этот буфер текстовую строку, а программа second выводит ее содержимое на экран. Обе программы должны быть запущены из одного каталога с уже существующим файлом MyFile.txt. Для наглядности рекомендуется, чтобы длина файла была изначально больше длины строки "Hello, world".

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

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

Физическая память

Физическая (в данном случае оперативная) память и внешняя память также описываются соответствующими структурами данных.

ОС Windows поддерживает до 4 Гб (некоторые версии и более) физической памяти. Память более 32 Мб считается "большой". Объем памяти можно посмотреть на вкладке "Быстродействие" диспетчера задач. Информация о состоянии страниц физической памяти и их принадлежности процессам находится в базе данных PFN (page frame number), а использование внешней памяти осуществляется через страничные файлы или файлы выгрузки.

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

Структура системных страничных файлов недокументирована. Известно, что в системе может быть до 16 страничных файлов. Информация о страничных файлах находится в разделе HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management\PagingFiles реестра, однако управление страничными файлами рекомендуется осуществлять через апплет "система" административной консоли управления. У каждого файла подкачки есть начальный и максимальный размер. С целью уменьшения вероятной фрагментации их создают максимального размера.

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

Рабочие наборы процессов

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

Согласно документации по ОС Windows, рабочим набором процесса называется совокупность физических страниц, выделенных процессу. Размер рабочего набора должен находиться в некоторых пределах, определяемых константами системы в зависимости от суммарного объема физической памяти. Например, если физической памяти достаточно, то рабочий набор процесса должен быть в диапазоне от 50 до 345 страниц. Имея привилегию Increase Scheduling Priority (о привилегиях см. часть V), эти значения можно менять при помощи функции SetProcessWorkingSet.

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

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

Прогон программы, иллюстрирующей увеличение рабочего набора процесса

Рассмотрим легкую модификацию программы DemoVM, добавив туда операцию записи одного байта на каждую страницу переданной памяти (программа DemoPageFaults.c).

#include <windows.h>
#include <stdio.h>

void main(void)
{

PVOID pMem = NULL;
int nPageSize = 4096;
int nPages = 200;
long SizeCommit = 0;
int i;
char * Ptr;

SizeCommit = nPages * nPageSize;
getchar();
pMem =  VirtualAlloc(0, SizeCommit, MEM_RESERVE| MEM_COMMIT, PAGE_READWRITE);
if(pMem == NULL) printf("VirtualAlloc Error\n");
Ptr = (char *)pMem;
for(i=0; i<nPages; i++) Ptr[i*nPageSize] = '0';
getchar();

pMem =  VirtualAlloc(0, SizeCommit, MEM_RESERVE| MEM_COMMIT, PAGE_READWRITE);
if(pMem == NULL) printf("VirtualAlloc Error\n");
Ptr = (char *)pMem;
for(i=0; i<nPages; i++) Ptr[i*nPageSize] = '0';
getchar();

pMem =  VirtualAlloc(0, SizeCommit, MEM_RESERVE| MEM_COMMIT, PAGE_READWRITE);
if(pMem == NULL) printf("VirtualAlloc Error\n");
Ptr = (char *)pMem;
for(i=0; i<nPages; i++) Ptr[i*nPageSize] = '0';
getchar();

pMem =  VirtualAlloc(0, SizeCommit, MEM_RESERVE| MEM_COMMIT, PAGE_READWRITE);
if(pMem == NULL) printf("VirtualAlloc Error\n");
Ptr = (char *)pMem;
for(i=0; i<nPages; i++) Ptr[i*nPageSize] = '0';
getchar();
}

Наращивание объема переданной памяти и размера рабочего набора будет происходить по нажатию клавиши "Enter". Посмотрим на поведение счетчика "Рабочее множество" для процессов DemoVM и DemoPageFaults. Несмотря на одинаковый объем переданной физической памяти, размеры рабочего набора сильно отличаются. У DemoVM он остается близким к нулю, тогда как у процесса DemoPageFaults идет заметное ступенчатое приращение рабочего набора (см. рис. 10.5)

Наблюдение за изменениями рабочих наборов процессов


Рис. 10.5.  Наблюдение за изменениями рабочих наборов процессов

Замещение страниц в рабочем наборе процесса - одна из наиболее ответственных операций. Дело в том, что уменьшение частоты page fault`ов является одной из ключевых задач системы управления памятью (например, известно, что вероятности page fault'а 5*10-7 оказывается достаточно, чтобы снизить производительность страничной схемы управления памятью на 10%.). Решение этой задачи связано с разумным выбором алгоритма замещения страниц. Если стратегия замещения выбрана правильно, то в оперативной памяти остается только самая актуальная информация, которая может понадобиться в недалеком будущем и которая не нуждается в замещении (на эту тему написано много книг, см., например, [2]).

В ОС Windows используются алгоритмы FIFO (first input first output) в многопроцессорном варианте и LRU - в однопроцессорном. На самом деле применяется не LRU, а его программная реализация NFU (not frequently used), согласно которой страница характеризуется не давностью, а частотой использования. Однако, согласно документации по ОС Windows, алгоритм, осуществляющий модификацию размера рабочего набора процесса, называется именно LRU. Что касается алгоритма FIFO, несмотря на известные недостатки, его применение упрощает обработку ссылок на страницу от нескольких процессоров.

База данных PFN. Страничные демоны

В процессе функционирования операционной системы в физической памяти располагаются рабочие наборы процессов, системный рабочий набор, свободные фрагменты и многое другое. Для учета состояния физической памяти поддерживается база данных PFN (page frame number). Это таблица записей фиксированной длины. Количество записей в ней совпадает с количеством страничных кадров.

Известно, что подсистема виртуальной памяти работает производительно при наличии резерва свободных страничных кадров. Тогда в случае страничной ошибки требуется только одна дисковая операция (чтение), и свободная страница может быть найдена немедленно. Алгоритмы, обеспечивающие поддержку системы в оптимальном состоянии, реализованы в составе фоновых процессов (их часто называют демонами или сервисами), которые периодически "просыпаются" и инспектируют состояние памяти. Их задача - обеспечивать достаточное количество свободных страниц, поддерживая систему в состоянии наилучшей производительности.

Формально, каждая страница физической памяти должна находиться в составе рабочего набора или входить в один из поддерживаемых базой связных списков страниц. Перемещение страниц между списками и рабочими наборами осуществляется системными потоками-демонами, входящими в состав менеджера памяти (см. [6]). Параметры настройки демонов хранятся в разделе HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Memory Management реестра

Чаще всего для обслуживания ошибки страницы в соответствии с требованиями защиты уровня C2 (см. часть V) требуется обнуленная страница, которая извлекается из соответствующего списка. Список обнуленных страниц пополняется потоком обнуления страниц (zero page thread) в фоновом режиме за счет списка свободных страниц. Иногда, например, для отображения файла, обнуленные страницы не нужны, и можно обойтись свободными страницами. Если у рабочего набора процесса отбирается страница, она попадает в список модифицированных страниц или в список свободных страниц. Подсистема записи модифицированных страниц (modified page writer) записывает их содержание на диск, когда количество таких страниц превышает установленный лимит. Страницы проецируемого файла можно сбросить на диск явным образом (при помощи функции FlushViewOfFile ). После записи модифицированная страница попадает в список свободных страниц.

Общее руководство и реализацию общих правил управления памятью осуществляет диспетчер рабочих наборов (working set manager), который вызывается системным потоком ядра - диспетчером настройки баланса - раз в секунду или при уменьшении объема свободной памяти ниже порогового значения.

Эксперимент. Наблюдение за ошибками страниц

Количество ошибок страниц, генерируемых процессом, можно наблюдать при помощи счетчика "Ошибок страницы". На рис. приведены графики поведения счетчиков "Ошибок страниц" и "Рабочее множество" для процесса DempPageFaults. (см. программу, описанную выше)

Наблюдение за размером рабочего набора процесса и количеством страничных ошибок


Рис. 10.6.  Наблюдение за размером рабочего набора процесса и количеством страничных ошибок

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

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

…
SOFT: RtlFillMemoryUlong+0x10 : 0x00232000
SOFT: RtlFillMemoryUlong+0x10 : 0x00233000
SOFT: GetConsoleInputWaitHandle+0x11a : GetConsoleInputWaitHandle+0x119
SOFT: FindFirstFileExA+0x285 : FindFirstFileExA+0x285
SOFT: main+0xe4 : 0x00440000
SOFT: main+0xe4 : 0x00441000
SOFT: main+0xe4 : 0x00442000
SOFT: main+0xe4 : 0x00443000
SOFT: main+0xe4 : 0x00444000
SOFT: main+0xe4 : 0x00445000
SOFT: main+0xe4 : 0x00446000
SOFT: main+0xe4 : 0x00447000
SOFT: main+0xe4 : 0x00448000
SOFT: main+0xe4 : 0x00449000
SOFT: main+0xe4 : 0x0044a000
SOFT: main+0xe4 : 0x0044b000
SOFT: main+0xe4 : 0x0044c000
SOFT: main+0xe4 : 0x0044d000
SOFT: main+0xe4 : 0x0044e000
SOFT: main+0xe4 : 0x0044f000
SOFT: main+0xe4 : 0x00450000
SOFT: main+0xe4 : 0x00451000
SOFT: main+0xe4 : 0x00452000
SOFT: main+0xe4 : 0x00453000
SOFT: main+0xe4 : 0x00454000
SOFT: main+0xe4 : 0x00455000
…
Пример 10.3. Часть результатов работы утилиты Pfmon.exe по отношению к процессу DemoPageFaults (html, txt)

Отдельные аспекты функционирования менеджера памяти

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

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

Локализация страниц в памяти

По умолчанию, процессу разрешается блокировать максимум 30 страниц памяти. Если увеличить рабочее множество процесса при помощи функции SetProcessWorkingSetSize, то, согласно документации, максимальное число страниц, которое процесс может блокировать, равно минимальному размеру его рабочего набора за вычетом 8 страниц.

Локализация страниц в памяти осуществляется при помощи Win32 функции VirtualLock, а освобождение страниц - при помощи VirtualUnlock. Учет локализованных страниц ведется в страничной базе PFN.

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

Приведенный листинг является примером такой программы.

#include <windows.h>
#include <stdio.h>

void main(void)
{

PVOID pMem = NULL;
int nPageSize = 4096;
int nPages = 400;
int nPageLock = 100;
long SizeCommit = 0;
int i;
char * Ptr;
int nMinPages = 200, nMaxPages = 500;
long dwMinimumWorkingSetSize = 0, dwMaximumWorkingSetSize = 0;
HANDLE hProcess;

hProcess = GetCurrentProcess();
dwMinimumWorkingSetSize = nMinPages * nPageSize;
dwMaximumWorkingSetSize = nMaxPages * nPageSize;

i = SetProcessWorkingSetSize(hProcess, dwMinimumWorkingSetSize, dwMaximumWorkingSetSize);
if(i==0) printf("SetProcessWorkingSetSize Error\n");

SizeCommit = nPages * nPageSize;

pMem =  VirtualAlloc(0, SizeCommit, MEM_RESERVE| MEM_COMMIT, PAGE_READWRITE);
if(pMem == NULL) printf("VirtualAlloc Error\n");

Ptr = (char *)pMem;
for(i=0; i<nPages; i++) Ptr[i*nPageSize] = '0';

i = VirtualLock(pMem, nPageLock * nPageSize);
if(i==0) printf("VirtualLock Error\n");

for(i=0; i<nPages; i++) Ptr[i*nPageSize] = '0';

VirtualUnlock(pMem, nPageLock * nPageSize);
VirtualFree(pMem, 0, MEM_RELEASE);
}

Копирование при записи

Другой нюанс в работе менеджера памяти, который можно проиллюстрировать на практике, связан с реализацией алгоритма отложенного выделения памяти - копирование при записи (copy-on-write). Это один из примеров алгоритма отложенной оценки (lazy evaluation), которые усложняют систему, но делают её более эффективной.

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

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

Прогон программы, иллюстрирующей отложенное выделение памяти

#include <windows.h>
#include <stdio.h>

void main(void)
{
HANDLE hMapFile;
LPVOID lpMapAddress;
HANDLE hFile; 
char * String;
 
hFile = CreateFile("MyFile.txt",GENERIC_READ | GENERIC_WRITE,
		       FILE_SHARE_READ| FILE_SHARE_WRITE, NULL, OPEN_ALWAYS,
                    FILE_ATTRIBUTE_NORMAL,NULL);
 
if (hFile == INVALID_HANDLE_VALUE) printf("Could not open file\n"); 

hMapFile = CreateFileMapping(hFile, NULL, 
                             PAGE_WRITECOPY,  // копирование при записи                                        
                             0,0,"MyFileObject");
                              
if (hMapFile == NULL)  printf("Could not create file-mapping object.\n"); 

 lpMapAddress = MapViewOfFile(hMapFile, 
                              FILE_MAP_COPY, // копирование при записи							    
                              0,0,0);
                              
if (lpMapAddress == NULL) printf("Could not map view of file.\n"); 

 String = (char *)lpMapAddress;
 getchar();
 sprintf(String, "Hello, world");
 printf("%s\n", String);

if (!UnmapViewOfFile(lpMapAddress))  printf("Could not unmap view of file.\n"); 
}

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

Контроль процессом памяти другого процесса

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

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

Для создания регионов в памяти другого процесса можно использовать функцию VirtualAllocEx, которой нужно передать описатель этого процесса в качестве параметра. Для доступа к памяти другого процесса применяются функции ReadProcessMemory и WriteProcessMemory.

Написание, компиляция и выполнение программы, осуществляющей доступ к памяти дочернего процесса

Рекомендуется самостоятельно написать программу, которая создает регион памяти в адресном пространстве дочернего процесса и записывает в него текстовую строку. Задача дочернего процесса - вывести эту строку на экран.

Заключение

Базовой операцией менеджера памяти является трансляция виртуального адреса в физический с помощью таблицы страниц и ассоциативной (TLB) памяти. В ряде случаев, для реализации разделяемой памяти, интеграции с системой ввода-вывода и др., применяется прототипная таблица страниц, которая является промежуточным звеном между обычной таблицей страниц и физической памятью. Для описания страниц физической памяти поддерживается база данных PFN (page frame number). Локализацию страниц памяти, контроль процессом памяти другого процесса и технику копирования при записи можно отнести к интересным особенностям системы управления памятью ОС Windows.

Лекция 11. Интерфейс файловой системы

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

Введение

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

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

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

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

Основные функции для работы с файлами

Предметное изучение интерфейса файловой системы лучше начать с описания простейшей программы чтения и записи в файл, которая использует основные ( CreateFile, ReadFile и WriteFile ) операции для работы с файлами.

Прогон программы чтения и записи в файл

Следующая программа открывает существующий файл, считывает из него 10 байтов с начала файла и записывает в файл фразу "some bytes to write", начиная с 11-й позиции. Для буфера выделяется память из стандартной кучи процесса (см. лекцию 9).

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

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

Именование файлов

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

Имя файла задается параметром lpFileName - указателем на строку, заканчивающуюся нулем. В соответствии со стандартом POSIX ОС Windows оперирует длинными (до 255 символов) именами. Если быть более точным, максимальная длина полного имени файла при создании файла равна MAX_PATH. Значение MAX_PATH определено как 260, но система позволяет преодолеть это ограничение и использовать имена файлов длиной до 32000 символов в формате Unicode.

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

Типы файлов

ОС Windows поддерживает типизацию файлов. Основные типы файлов: регулярные (обычные) файлы и директории (справочники, каталоги).

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

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

Далее речь пойдет, главным образом, об обычных файлах.

Прикладные программы, работающие с файлами, как правило, распознают тип файла по его имени в соответствии с общепринятыми соглашениями. Например, файлы с расширениями .c, .pas - текстовые файлы, хранящие программы на Си и Паскале, а файлы с расширениями .exe - исполняемые, и т.д. Связь имен с обрабатывающими программами реализована в реестре.

Атрибуты файлов

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

>Echo  содержимое файла > MyFile:Stream1
>more <  MyFile:Stream1

то на экране должны появиться слова "содержимое файла".

Вот далеко не полный перечень атрибутов файла в NTFS:

Имя файла тоже является одним из атрибутов. Атрибуты хранятся в виде пары: <наименование атрибута, значение атрибута> в записи о файле в главной файловой таблице MFT (см. следующую лекцию).

Часть атрибутов файла можно определить при его создании (через параметры функции CreateFile ) или позже при помощи SetFileAttributes, сославшись на файл по имени. Можно также специфицировать атрибуты защиты файла при помощи параметра lpSecurityAttributes. Если же значение lpSecurityAttributes равно NULL, то соответствующие атрибуты файла будут содержать параметры так называемой стандартной защиты (подробнее об этом часть V).

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

Прогон программы получения атрибутов файла

#include <windows.h>
#include <stdio.h>

void main(void) {

DWORD dwFileAttributes;

dwFileAttributes = GetFileAttributes("tmp");
if(dwFileAttributes == -1) printf(" GetFileAttributes Error\n");

if (dwFileAttributes & FILE_ATTRIBUTE_NORMAL) 
 printf("This file is normal\n");
if (dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) 
 printf("This file is directory\n");
if (dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) 
 printf("This file is reparse point\n");
}

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

Рекомендуется самостоятельно написать программу, где применяется функция SetFileAttributes, например, устанавливается флаг "FILE_ATTRIBUTE_READONLY" для атрибутов указанного файла.

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

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

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

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

Пример применения операции асинхронного чтения из файла

Для того чтобы воспользоваться возможностями асинхронного ввода-вывода, нужно вызвать функцию CreateFile с установленным флагом FILE_FLAG_OVERLAPPED, входящим в состав параметра dwFlagsAndAttrs, и указать: с какой позиции осуществлять чтение (запись), сколько байтов считать (записать) и какое событие должно сигнализировать о том, что операция завершена. Для этого необходимо проинициализировать поля структуры OVERLAPPED в параметре pOverlapped функций ReadFile или WriteFile.

Структура OVERLAPPED
typedef struct _OVERLAPPED { 
    ULONG_PTR  Internal; 
    ULONG_PTR  InternalHigh; 
    DWORD  Offset; 
    DWORD  OffsetHigh; 
    HANDLE hEvent; 
} OVERLAPPED;

Параметр Internal используется для хранения кода возможной ошибки, а параметр InternalHigh - для хранения числа переданных байт. Вначале разработчики Windows не планировали делать их общедоступными - отсюда и такие не содержащие мнемоники имена. Offset и OffsetHigh - соответственно младшие и старшие разряды текущей позиции файла. hEvent специфицирует событие, сигнализирующее окончание операции ввода-вывода.

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

#include <windows.h>
#include <stdio.h>

void main(void) {

HANDLE hFile, hHeap;
int iRet = 0;
void *pMem;
long BufSize = 512;
DWORD iRead = 10;
char * String;
OVERLAPPED ov = {0};


hFile = CreateFile("MYFILE.TXT", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING,                                                          
                    FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, NULL); 
if (hFile == INVALID_HANDLE_VALUE) printf("Could not open file.");

hHeap = GetProcessHeap();
pMem = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, BufSize);
String = (char *)pMem;

ov.Offset = 3;

ReadFile(hFile, pMem, iRead, &iRead, &ov);
WaitForSingleObject(hFile, INFINITE);
printf("Read %d bytes: %s\n", iRead, String);
printf("Read %d bytes: %s\n", ov.InternalHigh, String);

HeapFree(hHeap, 0, pMem);
CloseHandle(hFile); 
}

В программе проинициализирована структура OVERLAPPED и передана функции ReadFile в качестве параметра. Чтение начинается с 3-й позиции. Узнать число прочитанных байтов можно из ov.InternalHigh - компонента структуры OVERLAPPED. Обратите внимание, что значение переменной iRead, которая должна содержать количество прочтенных байтов, равно 0, так как функция вернула управление до завершения операции ввода-вывода. Обычно это справедливо для первого запуска программы. При последующих запусках, поскольку данные файла находятся в кэше, запрос может успеть выполниться синхронно и значение iRead уже будет равно числу прочитанных байтов.

В программе выбран простейший вариант синхронизации - сигнализация от объекта, управляющего устройством, в данном случае - открытого файла (функции WaitForSingleObject передан описатель открытого файла в качестве параметра). Существуют и другие, более интересные способы синхронизации, например, использование порта завершения ввода-вывода ( IoCompletitionPort ). Более подробно возможности асинхронного ввода-вывода описаны в [4], [5] и [9].

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

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

Итак, в случае асинхронного доступа позиция, начиная с которой будет осуществляться операция чтения-записи, содержится в запросе на операцию (параметр структуры OVERLAPPED ). Рассмотрим теперь особенности позиционирования при обычном синхронном вводе-выводе. В этом случае реализуется схема с "сохранением состояния", 64-разрядный указатель текущей для чтения-записи позиции хранится в составе атрибутов объекта "открытый файл" (его не нужно путать с атрибутами файла), описатель которого возвращает функция CreateFile.

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

SetFilePointer(hFile,17 , NULL, FILE_BEGIN);

устанавливает указатель текущей позиции на 17-й байт с начала файла.

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

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

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

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

Директории. Логическая структура файлового архива

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

Формат каталога


Рис. 11.1.  Формат каталога

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

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

Иерархическая древовидная структура файловой системы


Рис. 11.2.  Иерархическая древовидная структура файловой системы

Поскольку имена файлов, находящихся в разных каталогах, могут совпадать, уникальность имени файла на диске обеспечивается добавлением к собственному имени файла списка вложенных каталогов, содержащих данный файл. Так образуется хорошо известное абсолютное или полное имя (pathname), например, \Games\Heroes\heroes.exe. Таким образом, использование древовидных каталогов минимизирует сложность назначения уникальных имен.

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

В системе поддерживается большое количество Win32-функций для манипуляции с каталогами, их полный перечень имеется в MSDN. В частности, для создания каталогов можно использовать функцию CreateDirectory. Вновь созданная директория включает записи с именами '.' и '..', однако считается пустой. Для работы с текущим каталогом можно использовать функции GetCurrentDirectory и SetCurrentDirectory. Работа с этими функциями проста и не нуждается в специальных разъяснениях.

Прогон программы, задача которой создать каталог на диске и сделать его текущим

#include <windows.h>
#include <stdio.h>

void main(void) {

int iRet = 0;
char Buf[512];
int bufSize = 512;

iRet = GetCurrentDirectory(bufSize, Buf);
 printf("iRet = %d, current directory %s\n", iRet, Buf);

iRet = CreateDirectory("f:\\tmp1", NULL);
 if(!iRet) printf("CreateDirectory error\n");

iRet = SetCurrentDirectory("f:\\tmp1");
 if(!iRet) printf("SetCurrentDirectory error\n");

iRet = GetCurrentDirectory(bufSize, Buf);
 printf("iRet = %d, current directory %s\n", iRet, Buf);
}

Приведенная программа выводит на экран название текущего каталога, создает каталог "tmp1" на диске "F:" , делает его текущим и выводит на экран его название в качестве текущего каталога.

Самостоятельное упражнение

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

Разделы диска. Операция монтирования

В ОС Windows принято разбивать диски на логические диски (это низкоуровневая операция), иногда называемые разделами (partitions). Бывает, что, наоборот, объединяют несколько физических дисков в один логический диск. На разделе или логическом диске хранится корневой каталог данного и все вложенные в него каталоги, а задание пути к файлу начинается с имени логического диска или "буквы" диска.

Имена логических дисков хранятся в каталоге "\??" пространства имен объектов, которые и осуществляют связь логического диска и реального устройства. Указав букву диска, прикладная программа получает доступ к его файловой системе.

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

Эксперимент. Монтирование логического диска с помощью штатной утилиты mountvol

Чтобы смонтировать логический диск, нужно выполнить команду

>mountvol  [<диск>:]<путь> <имя тома>

Здесь параметр <путь> задает имя пустого каталога, а имя тома задается в виде \\?\Volume{код_GUID}\, где GUID - глобальный уникальный идентификатор.

Например

>mountvol  f:\tmp1   \\?\Volume\{2eca078d-5cbc-43d3-aff8-7e8511f60d0e}\}

Имена глобальных уникальных идентификаторов и их связь с буквами диска можно узнать, дав команду

>mountvol /?

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

Наконец, смонтировать диск можно программным образом с помощью Win32-функции SetVolumeMountPoint.

Защита файлов

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

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

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

В новых версиях NTFS дескрипторы защиты всех файлов хранятся в отдельном файле метаданных \$Secure, который описывается 9-й записью главной файловой таблицы тома MFT (консолидированная защита).

Более подробно защита от несанкционированного доступа описана в части V данного курса.

Заключение

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

Лекция 12. Реализация файловой системы. Файловая система NTFS

Настоящая лекция описывает отдельные аспекты реализации файловой системы NTFS. Главная функция файловой системы — связь символьного имени с блоками диска — реализована за счет поддержки списка блоков в записи о файле в главной файловой таблице MFT. Для быстрого поиска файла по имени каталог может быть организован в виде B+ дерева. Проблемы монтирования дисков и связывания файлов решаются с помощью точек повторного анализа. Производительность файловой системы обеспечивается менеджером кэша, а также путем оптимального размещения информации на диске. Для восстановления системы после отказа питания ведется журнал файловых операций с метаданными. Поддержка нескольких файловых систем в ОС Windows обеспечивается оригинальной структурой подсистемы ввода/вывода, в рамках которой для каждой файловой системы имеется соответствующий драйвер

Введение

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

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

В традиционной многоуровневой системе построения операционных систем с устройствами (дисками) непосредственно взаимодействует часть ОС, называемая системой ввода-вывода, основу которой составляют драйверы устройств. Задача системы ввода-вывода: скрыть особенности работы с дисками и предоставить в распоряжение более высокоуровневого компонента ОС - файловой системы - используемое дисковое пространство в виде непрерывной последовательности блоков фиксированного размера. Файловая система располагается соответственно между системой ввода-вывода и прикладной программой см., например, [2].



Рис. 12.1. 

В ОС Windows файловая система интегрирована в систему ввода-вывода (см. рис. 12.1), построенную в виде набора разнообразных драйверов, и также реализована в виде драйвера, например, драйвера NTFS или драйвера FAT. Общение драйверов организовано путем посылки так называемых IRP (I/O request packet) пакетов. Функционирование системы ввода-вывода подробно описано в [6], [7]. В данной лекции речь пойдет о ее файловой подсистеме.

Кластеры

Обычно диски разбиты на блоки (секторы) размером - 512 б. Однако удобнее оперировать блоками более крупного размера - кластерами (cluster). Размер кластера равен размеру сектора, умноженному на кластерный множитель (claster factor), и может быть установлен во время операции форматирования диска. По умолчанию это значение равно 4 Кб и может быть изменено. Альтернативные значения размера кластера можно, например, извлечь из справочной информации команды format.

Эксперимент. Получение информации о потенциальном размере кластера

Ниже приведена часть результатов вывода команды "format /?"

ключ:  /A:размер

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

NTFS поддерживает размеры 512, 1024, 2048, 4096, 8192, 16КБ, 32КБ, 64K.

FAT32 поддерживает размеры 512, 1024, 2048, 4096, 8192, 16КБ, 32КБ, 64КБ, (128КБ, 256КБ для размера сектора > 512 Байт).

Файловые системы FAT и FAT32 налагают следующие ограничения на число кластеров тома:

FAT: число кластеров <= 65526

FAT32: 65526 < число кластеров < 4177918

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

Сжатие томов NTFS не поддерживается для размеров кластеров более 4096 Байт.

Размер кластера, которые в дальнейшем также будут называться блоками диска, играет немаловажную роль. Небольшой размер блока будет приводить к тому, что каждый файл будет содержать много блоков и читаться медленно. Большие блоки обеспечивают более высокую скорость обмена с диском, но из-за внутренней фрагментации (каждый файл занимает целое число блоков, и в среднем половина последнего блока пропадает) снижается процент полезного дискового пространства. Специально проведенные исследования показали, что оптимальным является компромиссный размер блока, лежащий в диапазоне от 1-го до 8 Кб.

Система различает кластеры диска (volume claster) и кластеры диска, принадлежащие файлу (logical claster). Для них поддерживается разная нумерация, соответственно VCN и LCN.

Структуры данных, необходимые для описания файловой системы на диске

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

Например, в файловой системе FAT, одной из файловых систем, поддерживаемых ОС Windows, имеется таблица отображения файлов (file allocation table), которая поддерживает связный список блоков для каждого файла. Таблица индексирована по номерам блоков. Запись в каталоге указывает на строку в таблице, содержащую первый блок файла, а далее по таблице можно найти все остальные блоки данного файла. Принцип работы файловой системы FAT более подробно описан в [2].

Рассмотрим устройство базовой файловой системы ОС Windows - NTFS.

Главная файловая таблица MFT

В файловой системе NTFS запись о файле в каталоге сопоставляется с записью о файле в главной файловой таблице диска - MFT (master file table), которая содержит информацию о расположении данных файла.

Записи MFT


Рис. 12.2.  Записи MFT

MFT - главная структура данных на диске, представляет собой обычный файл, содержащий до 248 записей размером 1 Кб каждая (см. рис. 12.2). Каждому файлу или каталогу соответствует одна запись. Записи 12-15 зарезервированы для служебных файлов, а записи, начиная с 16-й, предназначены для файлов пользователей. Для больших файлов требуется несколько записей, первая из которых называется базовой. Таблица MFT может располагаться в любом месте диска.

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

Чаще данные файла все же не помещаются в записи MFT. Рассмотрим данный вариант несколько подробнее. В этом случае вслед за заголовком в записи размещается список дисковых блоков файла (рис. 12.3).

Запись MFT для 10-блочного файла, состоящего из четырех фрагментов (серий)


Рис. 12.3.  Запись MFT для 10-блочного файла, состоящего из четырех фрагментов (серий)

Напомним, что решается задача приведения в соответствие номера блока в файле (LCN) номеру блока на диске (VCN). Для этого блоки диска представляются в виде совокупности серий, каждая из которых является непрерывной последовательностью блоков. Например, на рис. показано отображение 10-блочного файла, блоки которого размещаются в 9, 10, 25, 26, 27, 63, 85, 86, 87 и 88-м блоках диска. Схема весьма эффективна, особенно для не слишком фрагментированных файлов, например, непрерывный файл независимо от размера описывается всего одной серией.

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

Номера дисковых кластеров файлов можно узнать при помощи утилиты nfi.exe (NTFS File Sectors Information Util), входящей в состав ресурсов Windows.

Эксперимент. Просмотр кластеров, принадлежащих файлу с помощью утилиты nfi.exe
\TMP\Nfi\exp.h
    $STANDARD_INFORMATION (resident)
    $FILE_NAME (resident)
    $DATA (nonresident)
        logical sectors 471790-471794 (0x732ee-0x732f2)

File 33
\TMP\Nfi\h.h
    $STANDARD_INFORMATION (resident)
    $FILE_NAME (resident)
    $DATA (nonresident)
        logical sectors 471798-471809 (0x732f6-0x73301)

Здесь приведена информация о файлах \tmp\nfi\exp.h и \tmp\nfi\h.h, которую выдает утилита nfi . Наиболее интересно расположение на диске нерезидентного атрибута потока данных, дисковые номера кластеров которого в данном случае обозначаются как logical sectors.

Управление свободным и занятым дисковым пространством

В системе Windows учет свободных и занятых дисковых блоков ведется при помощи битового вектора (bit map или bit vector), например, 00111100111100011000001, где каждый блок представлен одним битом, принимающим значение 0 или 1, в зависимости от того, занят он или свободен. В файловой системе NTFS битовый массив сам является файлом. Его атрибуты и дисковые адреса хранятся в 6-й записи таблицы MFT.

Реализация директорий

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

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

MFT запись для небольшого каталога


Рис. 12.4.  MFT запись для небольшого каталога

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

Поиск файла по имени

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

При поиске файла вначале при помощи механизма символьных ссылок в пространстве имен объектов решается задача трансляции имени диска "в стиле DOS" или буквы диска во внутренние имена устройств Windows. Для этого библиотечный вызов, содержащий имя файла в качестве параметра, передается библиотеке kernel32.dll и перед именем помещается название каталога именованных ресурсов "\??\" в пространстве имен менеджера объектов. В результате "F:\tmp\MyFile.txt" преобразуется в "\??\F:\tmp\MyFile.txt". Далее в каталоге \??\ ищется символьное имя "F:", которое является ссылкой на объект-раздел жесткого диска, например, "\Device\Harddisk\Volume5". Далее находится таблица MFT этого раздела, затем осуществляется навигация по каталогам и отыскивается искомый файл, см. рис. 12.5.

Процесс поиска файла по имени


Рис. 12.5.  Процесс поиска файла по имени

Для поиска файлов в каталоге применяются функции FindFirstFile и FindNextFile (см. MSDN).

Прогон программы, осуществляющей поиск файла в каталоге

Приведенная программа выводит список файлов в каталоге с заданным шаблоном поиска. Информация о найденных файлах содержится в структуре WIN32_FIND_DATA, которая является одним из параметров функций FindFirstFile и FindNextFile. Входным параметром функции FindFirstFile служит шаблон поиска, а выходным - дескриптор поиска, который является входным параметром функции FindNextFile и хранит информацию о текущем состоянии поиска (аналог указателя текущей позиции). Для закрытия описателя в данном случае применяется функция FindClose (а не CloseHandle как обычно).

Точки повторного анализа. Монтирование дисков. Образование ссылок

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

Монтирование файловых систем

Операция монтирования файловой системы, хранящейся на разделе диска, обеспечивает ей связь с уже существующей иерархией файловых систем и делает ее файлы доступными для процессов. Техника монтирования описана в предыдущей лекции. Монтирование базовых дисков осуществляется автоматически при первом обращении к диску. Это делает диспетчер монтирования (Mountmgr.sys). Сведения о монтированных дисках имеются в реестре в разделе HKLM\SYSTEM\MountedDevices.

Создание точек монтирования (mount points) - связывание каталога NTFS реализовано с помощью точек повторного анализа. Программы, осуществляющие навигацию по каталогам, должны, обнаружив точку повторного анализа (в данном случае точку монтирования), прибегнуть к помощи кода, направляющего процесс дальнейшей навигацию на новый том. Программы, предоставляющие информацию о суммарном количестве файлов на диске или в каталоге, также должны учитывать наличие точек монтирования.

Точки монтирования в системе можно найти при помощи Win32-функций FindFirstVolumeMountPoint и FindNextVolumeMountPoint.

Создание связей

Связывание файлов - техника, заимствованная из Unix, - образование для файла или каталога нескольких родительских каталогов, см. рис. 12.6.

Образование связей в файловой системе


Рис. 12.6.  Образование связей в файловой системе

Соединение между директорией и разделяемым файлом называется "связью" или "ссылкой" (link). Дерево файловой системы превращается в циклический граф.

ОС Windows (как и Unix) поддерживает два вида связей - жесткие (hard link) и символические (symbolic link). В случае жесткой связи запись о файле появляется в новом каталоге, а MFT-запись этого файла включает счетчик количества ссылок на данный файл. Удаление файла приводит к уменьшению счетчика на 1, и реальное удаление и освобождение его блоков происходит, когда значение счетчика равно 0.

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

Жесткие связи создаются вызовом Win32-функции CreateHardLink. Штатной утилиты, поддерживающей жесткие связи, нет, хотя в состав ресурсов Windows для этих целей включена POSIX утилита ln.

Символическую связь (иногда говорят точку соединения, junction) можно образовать при помощи входящей в состав ресурсов Windows утилиты linkd.exe или при помощи свободно распространяемой утилиты junction.exe с сайта http://www.sysinternals.com.

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

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

Образование циклического графа можно проиллюстрировать на примере [6] образования рекурсивной точки монтирования.

Эксперимент

Создайте на диске X: каталог Recurse и смонтируйте его с корневым каталогом диска X: Затем для этого каталога выполните команду dir /s.

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

Создайте пустой каталог и сделайте его точкой монтирования или символической связью. При помощи функции GetFileAttributes установите у данного каталога наличие флага FILE_ATTRIBUTE_REPARSE_POINT.

Совместный доступ к файлу

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

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

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


Рис. 12.7.  Организация совместного доступа к файлу

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

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

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

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

Эффективность работы - одна из важнейших задач подсистемы управления файлами.

Кэширование

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

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

В ОС Windows кэш работает на более высоком уровне, нежели файловая система (см. рис. 12.8).

Место менеджера кэша в системе ввода-вывода


Рис. 12.8.  Место менеджера кэша в системе ввода-вывода

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

Файлы различного размера, спроецированные в системный кэш


Рис. 12.9.  Файлы различного размера, спроецированные в системный кэш

В результате запрос на чтение с текущей позиции может быть непосредственно удовлетворен из кэша. Если же нужных байтов файла в кэше нет, то файловая система вычисляет логический номер блока в файле (LCN), затем логический номер блока на диске (VCN). После этого делается запрос к системе ввода-вывода на чтение этого блока, точнее, проецирование в буфер кэша части файла, содержащей данный блок. Подобная организация позволяет системе поддерживать единый централизованный кэш для всех используемых файловых систем (NTFS, FAT, CDFS, удаленная FS и др.), а файловые системы не обязаны управлять своими кэшами.

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

Аккуратная реализация кэширования требует решения нескольких проблем.

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

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

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

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

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

Прогон программы, иллюстрирующей функционирование кэша

#include <windows.h>
#include <stdio.h>

void main(void) {

HANDLE hFile, hHeap;
int iRet = 0;
void *pMem;
long FileSize = 0, FilePos = 0;
DWORD iRead = 0, iWrite = 0;
char * String;

hFile = CreateFile("MYFILE.TXT", GENERIC_READ| GENERIC_WRITE,0,
                    NULL, OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL |0, NULL);
if (hFile == INVALID_HANDLE_VALUE) printf("Could not open file \n");


FileSize = GetFileSize(hFile, NULL);
printf("FileSize = %d\n",FileSize);

hHeap = GetProcessHeap();
pMem = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, FileSize + 2);
String = (char *)pMem;

ReadFile(hFile, pMem, FileSize, &iRead, NULL);
printf("Read %d bytes \n", iRead);

for(FilePos = 0; FilePos < FileSize; FilePos++)
String[FilePos] = '1';

SetFilePointer(hFile, 0, NULL, FILE_BEGIN);
getchar();

WriteFile(hFile, pMem, FileSize, &iWrite, NULL);
printf("Write %d bytes \n", iWrite);

iRet = FlushFileBuffers(hFile);
 if(iRet == 0) printf("FlushFileBuffer Error\n");
 
HeapFree(hHeap, 0, pMem);
CloseHandle(hFile); 
}

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

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

Поведение счетчика "сбросов данных" кэша


Рис. 12.10.  Поведение счетчика "сбросов данных" кэша

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

Оптимальное размещение информации на диске

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

Hадежность файловой системы

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

Восстанавливаемая файловая система NTFS

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

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

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

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

Для файла-журнала отводится вторая запись таблицы MFT тома NTFS. Для минимизации возможных пагубных последствий сервис файла журнала (log file service, LFS) осуществляет записи в следующей последовательности [6].

  1. Вначале в кэшируемый файл журнала заносится запись о предполагаемой транзакции.
  2. Затем делается собственно транзакция, то есть модифицируется файловая система (также в кэше).
  3. После этого запись в файле журнала сбрасывается на диск.
  4. Наконец, сбросив на диск файл журнала, диспетчер кэша записывает на диск изменения в файловой системе (результаты операции над метаданными).

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

Проверка целостности файловой системы при помощи утилит

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

Решение проблемы плохих блоков

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

Фиксация изменений в файловой системе

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

Для решения подобных задач ОС Windows располагает самыми широкими возможностями. Для знакомства с данной техникой рекомендуется изучить работу программных примеров из MSDN (см. раздел MSDN Using File I/O), или особенности применения функции FindFirstChangeNotification.

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

Подобно многим современным операционным системам ОС Windows поддерживает несколько файловых систем (CDFS, UDF, FAT, NTFS, удаленные FS). Эта возможность заложена в архитектуре системы ввода-вывода. Список зарегистрированных файловых систем можно "увидеть" с помощью утилиты WinObj.

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

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

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

Заключение

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

Лекция 13. Система управления доступом

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

Введение

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

Для построения формальных моделей безопасности принято представлять ИС в виде совокупности взаимодействующих сущностей - субъектов (s) и объектов (o).

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

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

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

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

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

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

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

Инструментальные средства управления безопасностью

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

Для управления системой безопасности в ОС Windows имеются разнообразные и удобные инструментальные средства. В частности, в рамках данной темы потребуется умение управлять учетными записями пользователей при помощи панели "Пользователи и пароли". Кроме того понадобится контролировать привилегии пользователей при помощи панели "Назначение прав пользователям". Рекомендуется также освоить работу с утилитой просмотра данных маркера доступа процесса WhoAmI.exe, утилитами просмотра и редактирования списков контроля доступа (cacls.exe, ShowACLs.exe, SubInACL,exe, SvcACL.exe), утилитой просмотра маркера доступа процесса PuList.exe и рядом других.

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

Пользователи и группы пользователей

Каждый пользователь (и каждая группа пользователей) системы должен иметь учетную запись (account) в базе данных системы безопасности. Учетные записи идентифицируются именем пользователя и хранятся в базе данных SAM (Security Account Manager) в разделе HKLM/SAM реестра.

Учетная запись пользователя содержат набор сведений о пользователе, такие, как имя, пароль (или реквизиты), комментарии и адрес. Наиболее важными элементами учетной записи пользователя являются: список привилегий пользователя в отношении данной системы, список групп, в которых состоит пользователь, и идентификатор безопасности SID (Security IDentifier). Идентификаторы безопасности генерируются при создании учетной записи. Они (а не имена пользователей, которые могут не быть уникальными) служат основой для идентификации субъектов внутренними процессами ОС Windows.

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

Создание учетной записи пользователя

Основным средством создания учетной записи пользователя служит Win32-функция NetUserAdd, принадлежащая семейству сетевых ( Net ) функций ОС Windows, подробное описание которой имеется в MSDN. При помощи Net -функций можно управлять учетными записями пользователей, как на локальной, так и на удаленной системе (более подробно об использовании Net -функций можно прочитать в [4], [5]).

Для успешного применения Net -функций достаточно знать следующее. Во-первых, Net -функции входят в состав библиотеки NetApi32.Lib, которую нужно явным образом добавить в проект, а прототипы функций объявлены в заголовочном файле Lm.h. Во-вторых, Net -функции поддерживают строки только в формате Unicode (см. лекцию 2). Наконец, информацию об учетной записи Net -функции нужно передавать с помощью специализированных структур, наименее сложная из которых структура USER_INFO_1.

Прогон программы создания новой учетной записи

Для иллюстрации рассмотрим несложную программу, задача которой - создать новую учетную запись для пользователя "ExpUser".

#ifndef UNICODE
#define UNICODE
#endif

#include <stdio.h>
#include <windows.h> 
#include <lm.h>


BOOL CreateUser(PWSTR pszName, PWSTR pszPassword) {
  USER_INFO_1 ui = {0};
  NET_API_STATUS nStatus;
  ui.usri1_name = pszName;              // имя пользователя
  ui.usri1_password = pszPassword;      // пароль пользователя
  ui.usri1_priv = USER_PRIV_USER;       // обычный пользователь

  nStatus = NetUserAdd(NULL, 1, (LPBYTE)&ui, NULL);
  return (nStatus == NERR_Success);
}


void main() {
   if(!CreateUser(L"ExpUser", L"123"))
      printf("A system error has occurred");
   }

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

Для удаления учетной записи пользователя используется функция NetUserDel.

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

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

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

Идентификатор безопасности SID

Структура идентификатора безопасности

SID пользователя (и группы) является уникальным внутренним идентификатором и представляют собой структуру переменной длины с коротким заголовком, за которым следует длинное случайное число. Это числовое значение формируется из ряда параметров, причем утверждается [6], что вероятность появления двух одинаковых SID практически равна нулю. В частности, если удалить пользователя в системе, а затем создать его под тем же именем, то SID вновь созданного пользователя будет уже другим.

Узнать свой идентификатор безопасности пользователь легко может при помощи утилит whoami или getsid из ресурсов Windows. Например, так:

> whoami /user /sid

При помощи команды whoami /all можно получить всю информацию из маркера доступа процесса, см. следующие разделы.

Задание

Выполните следующую последовательность действий.

  1. Создайте учетную запись пользователя при помощи инструментальных средств ОС Windows
  2. Выясните значение его SID'а.
  3. Затем удалите эту учетную запись и вновь создайте под тем же именем.
  4. Сравните SID нового пользователя с предыдущим значением SID'а.

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

В текстовой форме каждый идентификатор безопасности имеет определенный формат. Вначале находится префикс S, за которым следует группа чисел, разделенных дефисами. Например, SID администратора системы имеет вид: S-1-5-<домен>-500, а SID группы everyone, в которую входят все пользователи, включая анонимных и гостей - S-1-1-0.

Поскольку структура SID имеет переменную длину, для копирования SID и преобразования его в текстовую форму и обратно рекомендуется прибегать к специальным функциям CopySID, ConvertSidToStringSid и ConvertStringSidToSid.

Чтобы получить бинарное значение SID по имени пользователя, нужно использовать функцию LookupAccountName. Обратная задача может быть решена при помощи функции LookupAccountSid.

Прогон программы получения идентификатора безопасности

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

#define _WIN32_WINNT 0x0500

#define UNICODE
#ifdef UNICODE
#define _UNICODE
#endif


#include <windows.h>
#include <stdio.h>
#include <sddl.h>


void main(void){

 wchar_t UserName[256];
 int MaxUserNameLength = 256; 
 
 SID Sid[1024];
 PSID pSid;
 LPTSTR StringSid;
 DWORD SidSize=1024;  
 SID_NAME_USE SidType;
 
 LPTSTR DomainName=NULL;
 DWORD DomainNameSize=16; // длина имени домена
 

 HANDLE hHeap;
 hHeap = GetProcessHeap();
 pSid = &Sid[0];


 GetUserName(UserName, &MaxUserNameLength); // получаем имя пользователя

 DomainName = (LPTSTR)HeapAlloc(hHeap,0,DomainNameSize * sizeof(TCHAR));

 LookupAccountName(
                   NULL,           // локальный компьютер
                   UserName, pSid, &SidSize, DomainName,&DomainNameSize,&SidType);

 if (!ConvertSidToStringSid(pSid, &StringSid))   /* память для строки выделяет сама  функция */
	 printf("Convert SID to string SID failed.");
 wprintf(L"StringSid %s\n", StringSid);

 LocalFree(StringSid);
 HeapFree(hHeap,0,DomainName);
}

В приведенной программе вначале формируются параметры вызова функции LookupAccountName. В частности, имя текущей учетной записи возвращает функция GetUserName(). Кроме того, необходимо выделить память для имени домена. Результирующее значение SID можно сравнить с тем, которое выдает утилита Whoami.exe.

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

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

Объекты. Дескриптор защиты

В ОС Windows все типы объектов защищены одинаковым образом. С каждым объектом связан дескриптор защиты (security descriptor). Дескриптор защиты описывается структурой типа SECURITY_DESCRIPTOR и инициализируется функцией InitializeSecurityDescriptor.

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

Дескриптор защиты (см. рис. 13.1) содержит SID владельца объекта, SID групп для данного объекта и два указателя на списки DACL (Discretionary ACL) и SACL (System ACL) контроля доступа. DACL и SACL содержат разрешающие и запрещающие доступ списки пользователей и групп, а также списки пользователей, чьи попытки доступа к данному объекту подлежат аудиту.

Структура каждого ACL списка проста. Это набор записей ACE (Access Control Entry), каждая запись содержит SID и перечень прав, предоставленных субъекту с этим SID.

Структура дескриптора защиты для файла


Рис. 13.1.  Структура дескриптора защиты для файла

В списке ACL есть записи ACE двух типов - разрешающие и запрещающие доступ. Разрешающая запись содержит SID пользователя или группы и битовый массив (access mask), определяющий набор операций, которые процессы, запускаемые этим пользователем, могут выполнять с данным объектом. Запрещающая запись действует аналогично, но в этом случае процесс не может выполнять перечисленные операции. Битовый массив или маска доступа состоит из 32 битов и обычно формируется программным образом из предопределенных констант, которые описаны в файлах-заголовках компилятора (главным образом в файле WinNT.h). Формат маски доступа можно посмотреть, например, в [4], [5].

На примере, изображенном на рис. 13.1, владелец файла Александр имеет право на все операции с данным файлом, всем остальным обычно дается только право на чтение, а Павлу запрещены все операции. Таким образом, список DACL описывает все права доступа к объекту. Если этого списка нет, то все пользователи имеют все права; если этот список существует, но он пустой, права имеет только его владелец.

Кроме списка DACL дескриптор защиты включает также список SACL, который имеет такую же структуру, что и DACL, то есть состоит из таких же ACE записей, только вместо операций, регламентирующих доступ к объекту, в нем перечислены операции, подлежащие аудиту. В примере на рис. 13.1 операции с файлом процессов, запускаемых Сергеем, описанные в соответствующем битовом массиве будут регистрироваться в системном журнале.

Задание. Сформировать список прав доступа для файла при помощи инструментальных средств ОС Windows.

Для установки прав доступа к файлу, находящемуся на NTFS разделе диска, нужно выбрать вкладку "Безопасность" апплета "Свойства", который возникает в Windows Explorer при нажатии на ярлык файла правой кнопкой мыши.

Итак, дескриптор защиты имеет достаточно сложную структуру и его формирование выглядит непростой задачей. К счастью, в Windows есть стандартный механизм, назначающий доступ к объектам "по умолчанию", если приложение не позаботилось создать его явно. В таких случаях говорят, что объекту назначена стандартная защита. Примером может служить создание файла при помощи функции CreateFile, где параметру-указателю на структуру SECURITY_ATTRIBUTES присвоено значение NULL. Некоторые объекты используют только стандартную защиту (мьютексы, события, семафоры).

Субъекты хранят информацию о стандартной защите, которая будет назначена создаваемым объектам, в своем маркере доступа (см. следующий раздел). Разумеется, в ОС Windows есть все необходимые средства для настройки стандартной защиты, в частности, списка DACL "по умолчанию", в маркере доступа субъекта.

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

Прогон программы получения информации из дескриптора защиты файла

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

#define _WIN32_WINNT 0x0500
#ifndef UNICODE
#define UNICODE
#endif

#include <windows.h>
#include <stdio.h>
#include <sddl.h>
#include <Aclapi.h>

void main(void){

 PSID pSid;
 PSECURITY_DESCRIPTOR pSD;
 PACL pDACL;
 LPTSTR StringSid = NULL;
 ULONG Error;
 HANDLE hFile;
 
 hFile = CreateFile(TEXT("MyFile.txt"), READ_CONTROL, 0, NULL, OPEN_EXISTING,
                                  FILE_ATTRIBUTE_NORMAL, NULL);
 
 Error = GetSecurityInfo(hFile, 
	                  SE_FILE_OBJECT,        // тип объекта
	                  OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, 
                         &pSid,                 // Sid владельца
			     NULL,                  // Sid группы
			     &pDACL, NULL,   // списки прав доступа
			     &pSD         // дескриптор защиты);
   if(Error != ERROR_SUCCESS) printf("GetSecurityInfo Error\n");

 
 if (!ConvertSidToStringSid(pSid, &StringSid)) /* память для строки выделяет сама  функция */
  printf("Convert SID to string SID failed.");
 
wprintf(L"StringSid %s\n", StringSid);
 
 LocalFree(pSD);
 LocalFree(StringSid);
}

Задача данной программы - открыть существующий файл MyFile.txt и применить функцию GetSecurityInfo для извлечения идентификатора безопасности владельца из дескриптора защиты файла. Затем идентификатор преобразуется в текстовую форму и выводится на экран.

Написание, компиляция и прогон программы получения списка контроля доступа к файлу из его дескриптора защиты

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

Субъекты безопасности. Процессы, потоки. Маркер доступа

Так же как и объекты, субъекты должны иметь отличительные признаки - контекст пользователя, для того, чтобы система могла контролировать их действия. Сведения о контексте пользователя хранятся в маркере (употребляются также термины "токен", "жетон") доступа. При интерактивном входе в систему пользователь обычно вводит свое имя и пароль. Система (процедура Winlogon) по имени находит соответствующую учетную запись, извлекает из нее необходимую информацию о пользователе, формирует список привилегий, ассоциированных с пользователем и его группами, и все это объединяет в структуру данных, которая называется маркером доступа. Маркер также хранит некоторые параметры сессии, например, время окончания действия маркера. Таким образом, именно маркер является той визитной карточкой, которую субъект должен предъявить, чтобы осуществить доступ к какому-либо объекту.

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

Основные компоненты маркера доступа показаны на рис. 13.2.

Основные компоненты маркера доступа


Рис. 13.2.  Основные компоненты маркера доступа

Включая в маркер информацию о защите, в частности, DACL, Windows упрощает создание объектов со стандартными атрибутами защиты. Как уже говорилось, если процесс не позаботится о том, чтобы явным образом указать атрибуты безопасности объекта, на основании списка DACL, присутствующего в маркере, будут сформированы права доступа к объекту по умолчанию. Настройку стандартной защиты можно осуществить при помощи функции SetTokenInformation. При этом, поскольку объекты в Windows отличаются большим разнообразием, в списке DACL "по умолчанию" можно указать только так называемые базовые права доступа, из которых система будет формировать стандартные права доступа в зависимости от вида создаваемого объекта. То, как это делается и как сформировать список DACL, по умолчанию присутствующий в маркере, подробно описано в [4], [5].

Задание

Осуществить просмотр данных маркера доступа с помощью утилиты WhoAmi

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

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

#define _WIN32_WINNT 0x0500
#ifndef UNICODE
#define UNICODE
#endif

#include <windows.h>
#include <stdio.h>
#include <sddl.h>

void main(void){
 
 DWORD TokenUserBufSize=256;
 LPTSTR StringSid;
 TOKEN_USER *ptUser;
 HANDLE hHeap;
 HANDLE hToken   = NULL;

 hHeap = GetProcessHeap();
 ptUser = (TOKEN_USER *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, TokenUserBufSize); 

 if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken)) 
           printf("OpenProcessToken Error\n");

 if (!GetTokenInformation(hToken,  // описатель маркера доступа
	                   TokenUser, // нужна информация о пользователе
				ptUser,  // буфер для информации
				TokenUserBufSize, &TokenUserBufSize))  
           printf("GetTokenInformation Error\n");;
      
 if (!ConvertSidToStringSid(ptUser->User.Sid, &StringSid))  
	 printf("Convert SID to string SID failed.");
  wprintf(L"StringSid %s\n", StringSid);

 CloseHandle(hToken);
 LocalFree(StringSid);
 HeapFree(hHeap,0,ptUser);
}

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

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

Написание, компиляция и прогон программы получения стандартного списка контроля доступа к объектам из маркера доступа процесса

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

Проверка прав доступа

После формализации атрибутов защиты субъектов и объектов можно перечислить основные этапы проверки прав доступа [6], [4], [5], см. рис. 13.3.

Пример проверки прав доступа к защищенному объекту


Рис. 13.3.  Пример проверки прав доступа к защищенному объекту

Этапов проверки довольно много. Наиболее важные этапы из них:

Очевидно, что для процедуры проверки важен порядок расположения ACE в DACL. Поэтому Microsoft предлагает так называемый предпочтительный порядок размещения ACE. Например, для ускорения рекомендуется размещать запрещающие элементы перед разрешающими.

Заключение

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

Приложение. Формальные модели защищенности в ОС Windows

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

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

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

Имеется совокупность объектов {Oi}, субъектов {Si} и пользователей {Ui}. Вводится операция доступа {Si} -> {Oj}, под которой подразумевается использование i-м субъектом информации из j-го объекта. Основные варианты доступа: чтение, запись и активация процесса, записанного в объекте. В результате последней операции появляется новый субъект {Si} -> {Oj} -> {Sk}.

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

Пример графа доступа


Рис. 13.4.  Пример графа доступа

Множество графов доступа можно рассматривать как фазовое пространство, а функционирование конкретной системы - как траекторию в фазовом пространстве. Защита информации может состоять в том, чтобы избегать "неблагоприятных" траекторий. Практически такое управление возможно только ограничением на доступ в каждый момент времени, или, как утверждается в известной "оранжевой" книге [1], все вопросы безопасности информации определяются описанием доступов субъектов к объектам.

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

Дискреционная модель контроля и управления доступом

Модели управления доступом регламентируют доступ субъектов к объектам. Наиболее распространена так называемая дискреционная (произвольная) модель, в которой обычные пользователи могут принимать участие в определении функций политики и присвоении атрибутов безопасности. Среди дискреционных моделей классической считается модель Харрисона-Руззо-Ульмана (см. [11]) - в ней система защиты представлена в виде набора множеств, элементами которых являются составные части системы защиты: субъекты, объекты, уровни доступа, операции и т.п.

С концептуальной точки зрения текущее состояние прав доступа при дискреционном управлении описывается матрицей (см. рис. 13.5), в строках которой перечислены субъекты, в столбцах - объекты, а в ячейках - операции, которые субъект может выполнить над объектом. Операции зависят от объектов. Например, для файлов это операции чтения, записи, выполнения, изменения атрибутов, а для принтера - операции печати и управления. Поведение системы моделируется с помощью понятия состояния Q = (S,O,M), где S,O,M - соответственно текущие множества субъектов, объектов и текущее состояние матрицы доступа.

Пример матрицы доступа


Рис. 13.5.  Пример матрицы доступа

С учетом дискреционной модели можно следующим образом сформулировать задачу системы защиты информации. С точки зрения безопасности, поведение системы может моделироваться как последовательность состояний, описываемых совокупностью субъектов, объектов и матрицей доступа. Тогда можно утверждать, что для безопасности системы в состоянии Q0 не должно существовать последовательности команд, в результате которой право R будет занесено в ячейку памяти матрицы доступа M, где оно отсутствовало в состоянии Q0. (критерий Харрисона-Руззо-Ульмана). По существу необходимо ответить на вопрос: сможет ли некоторый субъект S когда-либо получить какие-либо права доступа к некоторому объекту O?

Очевидно, что для обеспечения безопасности необходимо наложить запрет на некоторые отношения доступа. Харрисон, Руззо и Ульман доказали, что в общем случае не существует алгоритма, который может для произвольной системы, ее начального состояния Q0 и общего права r решить, является ли данная конфигурация безопасной. Чтобы удовлетворить критерию безопасности в общем случае, в ИС должны отсутствовать некоторые операции создания и удаления сущностей, в результате чего эксплуатация подобной системы теряет практический смысл.

Каналы утечки информации в системах с дискреционным доступом

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

Очевидно, что для каждого субъекта S, активизированного в момент времени t, существует единственный активизированный субъект S', который активизировал субъект S. Поэтому можно, анализируя граф, подобный изображенному на рис. 13.5, выявить единственного пользователя, от имени которого активизирован субъект S.

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

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

Ui -> (Write)-> O, в момент времени t1 и Uj -> (Read)-> O в момент времени t2, где i≠j и t1 < t2,

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

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

Ролевая политика безопасности

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

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

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

Лекция 14. Структура системы защиты. Привилегии

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

Основные компоненты системы безопасности ОС Windows

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

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

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

Структура системы безопасности ОС Windows


Рис. 14.1.  Структура системы безопасности ОС Windows

Система защиты ОС Windows состоит из следующих компонентов (см. рис. 14.1).

Все компоненты активно используют базу данных Lsass, содержащую параметры политики безопасности локальной системы, которая хранится в разделе HKLM\SECURITY реестра.

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

Политика безопасности

При оценке степени защищенности операционных систем действует нормативный подход, в соответствии с которым совокупность задач, решаемых системой безопасности, должна удовлетворять определенным требованиям - их перечень определяется общепринятыми стандартами. Система безопасности ОС Windows отвечает требованиям класса C2 "оранжевой" книги [1] и требованиям стандарта Common Criteria, которые составляют основу политики безопасности системы. Политика безопасности подразумевает ответы на следующие вопросы: какую информацию защищать, какого рода атаки на безопасность системы могут быть предприняты, какие средства использовать для защиты каждого вида информации.

Требования, предъявляемые к системе защиты, таковы

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

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

Ролевой доступ. Привилегии

Понятие привилегии

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

В соответствии со своей ролью каждый пользователь обладает определенными привилегиями и правами на выполнение различных операций в отношении системы в целом, например, право на изменение системного времени или право на создание страничного файла. Аналогичные права в отношении конкретных объектов называются разрешениями. И права, и привилегии назначаются администраторами отдельным пользователям или группам как часть настроек безопасности. Многие системные функции (например, LogonUser и InitiateSystemShutdown ) требуют, чтобы вызывающее приложение обладало соответствующими привилегиями.

Каждая привилегия имеет два текстовых представления: дружественное имя, отображаемое в пользовательском интерфейсе Windows, и программное имя, используемое приложениями, а также Luid - внутренний номер привилегии в конкретной системе. Помимо привилегий в Windows имеются близкие к ним права учетных записей. Привилегии перечислены в файле WinNT.h, а права - в файле NTSecAPI.h из MS Platform SDK. Чаще всего работа с назначением привилегий и прав происходит одинаково, хотя и не всегда. Например, функция LookupPrivelegeDisplayName, преобразующая программное имя в дружественное, работает только с привилегиями.

Ниже приведен перечень программных и отображаемых имен привилегий (права в отношении системы в данном списке отсутствуют) учетной записи группы с административными правами в ОС Windows 2000.

  1. SeBackupPrivilege (Архивирование файлов и каталогов)
  2. SeChangeNotifyPrivilege (Обход перекрестной проверки)
  3. SeCreatePagefilePrivilege (Создание страничного файла)
  4. SeDebugPrivilege (Отладка программ)
  5. SeIncreaseBasePriorityPrivilege (Увеличение приоритета диспетчирования)
  6. SeIncreaseQuotaPrivilege (Увеличение квот)
  7. SeLoadDriverPrivilege (Загрузка и выгрузка драйверов устройств)
  8. SeProfileSingleProcessPrivilege (Профилирование одного процесса)
  9. SeRemoteShutdownPrivilege (Принудительное удаленное завершение)
  10. SeRestorePrivilege (Восстановление файлов и каталогов)
  11. SeSecurityPrivilege (Управление аудитом и журналом безопасности)
  12. SeShutdownPrivilege (Завершение работы системы)
  13. SeSystemEnvironmentPrivilege (Изменение параметров среды оборудования)
  14. SeSystemProfilePrivilege (Профилирование загруженности системы)
  15. SeSystemtimePrivilege (Изменение системного времени)
  16. SeTakeOwnershipPrivilege (Овладение файлами или иными объектами)
  17. SeUndockPrivilege (Извлечение компьютера из стыковочного узла)

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

Внутренний номер привилегии используется для специфицирования привилегий, назначаемых субъекту, и однозначно связан с именами привилегии. Например, в файле WinNT.h это выглядит так:

#define SE_SHUTDOWN_NAME            TEXT("SeShutdownPrivilege")

Управление привилегиями

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

Использование функций LSA начинается с получения описателя объекта политики ( PolicyHandle ) при помощи функции LsaOpenPolicy, которая открывает описатель объекта на локальной или удаленной машине. Имя целевого компьютера передается функции в качестве одного из параметров. После завершения работы с объектом политики необходимо его закрыть, вызвав функцию LsaClose(PolicyHandle).

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

Задача перечисления привилегий учетной записи

Первая задача - перечисление привилегий данной учетной записи - решается при помощи функции LsaEnumerateAccountRights. Эта функция перечисляет привилегии пользователя с заданным идентификатором безопасности Sid в отношении системы с объектом политики PolicyHandle, передаваемыми ей в качестве параметров.

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

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

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

#ifndef UNICODE
#define UNICODE
#endif

#include <windows.h>
#include <stdio.h>
#include "ntsecapi.h"

BOOL GetUserSID(TOKEN_USER *, PDWORD pdwSize); 

void main(void){
 PSID pSid;
 DWORD TokenUserBufSize=256;
 TOKEN_USER *pUser;
 HANDLE hHeap;
 LSA_HANDLE hPolicy=NULL;
 LSA_OBJECT_ATTRIBUTES ObjAttributes = {0};
 ULONG Count = 0, i = 0, PrivDispBufLen = 512, Language = 0;
 PLSA_UNICODE_STRING Privileges = NULL; 
 WCHAR PrivBuf[512], PrivDispBuf[512];
 CHAR TempPrivDispBuf[512], TempPrivBuf[512];
 BOOL Return;

 hHeap = GetProcessHeap();
 pUser = (TOKEN_USER *)HeapAlloc(hHeap, HEAP_ZERO_MEMORY, TokenUserBufSize);
 if(!GetUserSID(pUser, &TokenUserBufSize)) printf("GetUserSid Error\n");
 pSid = pUser->User.Sid;  
 if(!IsValidSid(pSid)) { printf("Sid Error\n"); return; }
 
 ObjAttributes.Length = sizeof(ObjAttributes);
LsaOpenPolicy(NULL, &ObjAttributes, POLICY_VIEW_LOCAL_INFORMATION| POLICY_LOOKUP_NAMES| POLICY_CREATE_ACCOUNT, &hPolicy); 

 LsaEnumerateAccountRights(hPolicy, pSid, &Privileges, &Count);
     printf("Current user has %d privileges:\n",Count);  
 for(i = 0; i < Count; i ++) {
  lstrcpyn(PrivBuf, Privileges[i].Buffer, Privileges[i].Length);
                  PrivBuf[Privileges[i].Length] = 0;

  PrivDispBufLen = 512;
Return = LookupPrivilegeDisplayName(NULL, PrivBuf, PrivDispBuf, &PrivDispBufLen, &Language);
   if(!Return) lstrcpy(PrivDispBuf, TEXT("Дружественное имя привилегии не найдено"));
  
  CharToOem(PrivDispBuf, TempPrivDispBuf);
  CharToOem(PrivBuf, TempPrivBuf);
  printf("%s   (%s)\n", TempPrivBuf, TempPrivDispBuf);
 }
 
 if(Privileges) LsaFreeMemory(Privileges);
 LsaClose(hPolicy);
 HeapFree(hHeap,0,pUser);
}

BOOL GetUserSID(TOKEN_USER * pUser, PDWORD pdwSize) {
   BOOL   fSuccess = FALSE;
   HANDLE hToken   = NULL;
   DWORD dwSize;
    if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken))  return FALSE;
    if (!GetTokenInformation(hToken, TokenUser, pUser, *pdwSize, &dwSize))
	return FALSE;
    fSuccess = TRUE;
   if (hToken != NULL) CloseHandle(hToken);
   return(fSuccess);
}

Данная программа получает маркер доступа текущего процесса, извлекает из него Sid пользователя, при помощи функции LsaOpenPolicy открывает объект политики безопасности и вызывает функцию LsaEnumerateAccountRights для получения списка привилегий. Функция LookupPrivilegeDisplayName преобразует программное имя привилегии в дружественное имя. Для вывода имени привилегии на экран на русском языке используется функция CharToOem. Если число привилегий оказывается равным нулю, то с учетом замечания, сделанного выше, рекомендуется добавить пользователю, от имени которого запускается программа, одну или несколько привилегий с помощью административной консоли управления.

Добавление привилегий пользователю

Для назначения привилегий имеется функция LsaAddAccountsRights, а для отзыва привилегий - функция LsaRemoveAccountRights. Функция LsaAddAccountRights назначает одну или несколько привилегий учетной записи. Параметры повторяют параметры функции LsaEnumerateAccountRight, однако на этот раз структуру LSA_UNICODE_STRING, задающую привилегию, нужно сформировать явно. Например, если нужна привилегия завершения работы системы, это можно сделать следующим образом:

PLSA_UNICODE_STRING pUserRights;
PUserRights[0].Buffer = SE_SHUTDOWN_NAME;
PUserRights[0].Length = lstrlen(PUserRights[0].Buffer)+sizeof(WCHAR);
PUserRights[0].MaximumLength = PUserRights[0].Length + sizeof(WCHAR);

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

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

Заключение

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

Лекция 15. Отдельные аспекты безопасности Windows

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

Аутентификация пользователя. Вход в систему

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

Согласно п.1 политики безопасности для доступа к компьютеру пользователь должен пройти процедуру аутентификации. Эта процедура инициируется комбинацией клавиш "ctrl+alt+del". Данная комбинация клавиш, известная как SAS (secure attention sequence), всегда перехватывается драйвером клавиатуры, который вызывает при этом настоящую (а не троянского коня) программу аутентификации. Пользовательский процесс не может сам перехватить эту комбинацию клавиш или отменить ее обработку драйвером. Говоря языком стандартов, можно сказать, что в системе реализована функциональность пути доверительных отношений (trusted path functionality). Данная особенность также отвечает требованиям защиты уровня B Оранжевой книги.

Процедурой аутентификации пользователя в системе управляет программа, WinLogon, представляющая собой начальную интерактивную процедуру, которая отображает начальный диалог с пользователем на экране. Процесс WinLogon активно взаимодействует с библиотекой GINA (Graphic Identification aNd Authentication - графической библиотекой идентификации и аутентификации). Библиотека GINA является заменяемым компонентом, интерфейс с ней хорошо документирован, поэтому иногда в приложениях, реализующих защиту, присутствует версия GINA, отличная от оригинальной. Получив имя и пароль пользователя от GINA, WinLogon вызывает модуль Lsass для аутентификации этого пользователя. В случае успешного входа в систему Winlogon извлекает из реестра профиль пользователя и определяет тип запускаемой оболочки.

Комбинация SAS может быть получена системой не только на этапе регистрации пользователя. Если пользователь уже вошел в систему, то после нажатия клавиш "ctrl-alt-del" он получает возможность: посмотреть список активных процессов, инициировать перезагрузку или выключение компьютера, изменить свой пароль и заблокировать рабочую станцию. В свою очередь, если рабочая станция заблокирована, то после ввода SAS пользователь имеет возможность ее разблокировки. Иногда может быть осуществлен принудительный вывод пользователя из системы с последующим входом в нее администратора.

В процессе аутентификации вызывается системная функция LogonUser (см. MSDN), которая исходя из имени пользователя, его пароля и имени рабочей станции или домена возвращает указатель на маркер доступа пользователя. Маркер впоследствии передается всем дочерним процессам. При формировании маркера используются ключи SECURITY и SAM реестра. Первый ключ определяет общую политику безопасности, а второй ключ содержит информацию о защите для индивидуальных пользователей.

Задание

Рекомендуется осуществить ввод комбинации клавиш "ctrl-alt-del" в разных ситуациях и инициировать одно из перечисленных выше действий.

Написание, компиляция и прогон программы создания нового пользователя с правами входа в систему

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

Написание, компиляция и прогон программы получения маркера доступа по имени пользователя и его паролю

В качестве сложного задания рекомендуется написать программу, которая имитирует процедуру входа в систему, то есть получает новый маркер доступа при помощи функции LogonUser. Для справки можно использовать книги [4], [5].

Выявление вторжений. Аудит системы защиты

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

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

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

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

Задание. Организовать аудит доступа к файлу с помощью инструментальных средств ОС Windows

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

  1. Открыть диалоговое окно "Локальные параметры политики безопасности\ Политика аудита" (см. рис. 15.1) административной консоли панели управления.

    Диалоговое окно "Локальные параметры политики безопасности\ Политика аудита" административной консоли панели  управления


    Рис. 15.1.  Диалоговое окно "Локальные параметры политики безопасности\ Политика аудита" административной консоли панели управления

  2. Включить в список проверки нужную совокупность событий, например, аудит доступа к объектам.
  3. С помощью программы Windows Explorer выбрать файл для аудита. Вызвать панель фиксации событий, которые связаны с файлом, подлежащим аудиту. Для этого при помощи мыши в контекстном меню открыть окно "Свойства", выбрать вкладку "Безопасность", затем, нажав кнопку "Дополнительно", выбрать "Аудит". Затем с помощью кнопки "Добавить" выбрать пользователя, действия которого подлежат аудиту, и перечень событий, подлежащих аудиту (см. рис. 15.2).

    Диалоговое окно установки событий, подлежащих аудиту для конкретного файла


    Рис. 15.2.  Диалоговое окно установки событий, подлежащих аудиту для конкретного файла

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

Как уже говорилось, список SACL, входящий в состав дескриптора защиты объекта, можно формировать и модифицировать программными средствами. Это можно сделать с помощью функции SetSecurityInfo, которая является обратной по отношению к уже знакомой нам функции GetSecurutyInfo и содержит тот же набор параметров. Поскольку речь идет об аудите, то параметр SecurityInfo, который специфицирует компонент дескриптора защиты, подлежащий изменению, должен включать значение SACL_SECURITY_INFORMATION.

Недопустимость повторного использования объектов

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

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

В ОС Windows гарантируется безопасность повторного использования областей физической памяти. Согласно [6], если пользовательскому процессу понадобилась свободная страница памяти, она может быть выделена только из списка обнуленных страниц базы данных PFN (см. раздел, связанный с работой менеджера памяти). Если этот список пуст, страница берется из списка свободных страниц и заполняется нулями. Если и этот список пуст, диспетчер памяти извлекает страницу из списка простаивающих (standby) страниц и обнуляет ее. Необнуленная страница передается только для отображения проецируемого файла. В этом случае фрейм необнуленной страницы инициализируется данными с диска.

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

Защита от внешнего навязывания

В соответствии с политикой безопасности операционная система должна защищать себя от внешнего влияния или навязывания, такого, как модификация загруженной системы или системных файлов, хранимых на диске. Чтобы удовлетворить требованиям политики безопасности, в ОС Windows встроены средства защиты файлов (Windows File Protection, WFP), которые защищают системные файлы даже от изменений со стороны пользователя с административными правами. В основе защиты лежат средства фиксации изменений в системных файлах (см. лекцию 12 ). Данная мера, безусловно, повышает стабильность системы.

Согласно документации, мониторингу и защите подлежат все поставляемые в составе ОС файлы с расширениями sys, dll, exe и ocx, а также некоторые шрифты TrueType (Micros.ttf, Tahoma.ttf и Tahomabd.ttf). Если выясняется, что файл подменен, он заменяется копией из каталога %systemroot%\system32\dllcache, на который указывает одна из записей в реестре. Если размер места, выделенного для этого каталога, не достаточен, то в нем сохраняются копии не всех системных файлов. В тех случаях, когда делается попытка удаления системного файла и при этом его не оказывается в каталоге dllcache, система пытается восстановить его с установочного компакт-диска ОС Windows или из сетевых ресурсов.

Кроме того, администратор системы с помощью утилиты Sfc.exe (system file checker) может осуществить проверку корректности версии всех системных файлов. Сведения о файлах с некорректным номером версии заносятся в протокол. Корректность системных файлов проверяется с помощью механизма электронной подписи и к неподписанным файлам следует относиться с осторожностью. Для проверки подписи и выявления неподписанных файлов служит штатная утилита SigVerif.exe.

Задание. Попробуйте удалить какой-либо системный файл из каталога Windows\system32, например, sol.exe (игра "косынка").

Маркер доступа. Контекст пользователя

Как уже говорилось, наиболее важной характеристикой субъекта является маркер доступа. Обычно маркер создается при интерактивном входе пользователя в систему и хранит сведения о контексте пользователя. Вначале маркер связывается с процессом-оболочкой Windows Explorer, а затем все процессы, порожденные пользователем во время сеанса работы, получают дубликат данного маркера. Важно понимать, что самостоятельно создать маркер пользовательское приложение не может. Это может сделать только служба Lsass.

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

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

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

В предыдущих лекциях описаны привилегии пользователя (раздел "ролевой доступ") и основные функции для считывания информации из маркера ( OpenProcessToken, GetTokenInformation ) в разделе "субъекты". Для получения списка привилегий из маркера при помощи функции GetTokenInformation нужно для параметра TokenInformationClass, выбрать значение TokenPrivileges. Ранее мы уже получали перечень привилегий учетной записи. Перечень привилегий, входящих в состав маркера, существенно шире, поскольку включает в себя привилегии пользователя и привилегии групп, в состав которых входит пользователь.

Прогон программы получения списка привилегий из маркера доступа процесса

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

#include <windows.h>
#include <stdio.h>
#pragma hdrstop

void main()
{
 HANDLE hToken;
 LUID setcbnameValue;
 TOKEN_PRIVILEGES tkp;
 DWORD errcod;
 LPVOID lpMsgBuf;
 LPCTSTR msgptr;

 UCHAR InfoBuffer[1000];
 PTOKEN_PRIVILEGES ptgPrivileges = (PTOKEN_PRIVILEGES) InfoBuffer;
 DWORD dwInfoBufferSize;
 DWORD dwPrivilegeNameSize;
 DWORD dwDisplayNameSize;
 UCHAR ucPrivilegeName[500];
 UCHAR ucDisplayName[500];
 DWORD dwLangId;
 UINT i;

 if ( ! OpenProcessToken( GetCurrentProcess(),
  TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken ) )
 {
  puts( "OpenProcessToken" );
  return;
 }

 GetTokenInformation( hToken, TokenPrivileges, InfoBuffer,
  sizeof InfoBuffer, &dwInfoBufferSize);

 printf( "Account privileges: \n\n" );
 for( i = 0; i < ptgPrivileges->PrivilegeCount; i ++ )
 {
  dwPrivilegeNameSize = sizeof ucPrivilegeName;
  dwDisplayNameSize = sizeof ucDisplayName;
  LookupPrivilegeName( NULL, &ptgPrivileges->Privileges[i].Luid,
   (char *)ucPrivilegeName, &dwPrivilegeNameSize );
  LookupPrivilegeDisplayName( NULL, (char *)ucPrivilegeName,
   (char *)ucDisplayName, &dwDisplayNameSize, &dwLangId );
  printf( "%s   (%s)\n", ucPrivilegeName, ucDisplayName);
 }

}

Задача приведенной программы - получить описатель текущего процесса при помощи функции OpenProcessToken и применить функцию GetTokenInformation для извлечения из него списка привилегий. Функция LookupPrivilegeName позволяет получить программное имя привилегии по ее локальному идентификатору (Luid), а функция LookupPrivilegeDisplayName преобразует программное имя привилегии в дружественное имя.

Включение и отключение привилегий в маркере

Большую часть информации в маркере менять нельзя. Например, нельзя ни добавить, ни удалить привилегию. Согласно [4], [5], возможность добавления привилегий в маркер представляла бы собой "глубокий подкоп под систему защиты Windows". Последнее означает, что изменение привилегий субъекта - прерогатива подсистемы локальной авторизации (LSA) и должно осуществляться централизовано только при помощи функций семейства LSA (см. лекцию 14).

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

Перевоплощение

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

Другой способ выполнить код с заимствованным маркером - осуществить перевоплощение (impersonation). Перевоплощение означает, что один из потоков процесса функционирует с маркером, отличным от маркера текущего процесса. В частности, клиентский поток может передать свой маркер доступа серверному потоку, чтобы сервер мог получить доступ к защищенным файлам и другим объектам от имени клиента. Впоследствии поток может вернуться в нормальное состояние, то есть использовать маркер процесса.

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

Маркер перевоплощения обычно создают путем дублирования существующего маркера и придания ему нужных прав доступа и уровня имперсонации. Это можно сделать при помощи функции DuplicateTokenEx. Собственно перевоплощение осуществляется при помощи функции ImpersonateLoggedOnUser. Чтобы вернуться в исходное состояние, поток должен вызвать функцию RevertToSelf.

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

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

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

Другие возможности настройки контекста пользователя

В ОС Windows имеются и другие возможности настройки контекста пользователя в соответствии со сложными требованиями защиты с учетом принципа минимума привилегий. В качестве примера можно упомянуть схему контроля доступа при помощи маркеров, называемых ограниченными. Ограниченные маркеры создаются функцией CreateRestrictedToken. В этот маркер в момент его создания можно внести следующие изменения: удалить привилегии, отключить некоторые SID-идентификаторы и добавить "ограниченные" SID'ы учетных записей. Последние просто добавляют ряд новых проверок к уже существующим при организации доступа к объекту. Ограниченные маркеры удобны, когда приложение подменяет клиента при выполнении кода, способного нанести ущерб системе.

Заключение

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

Дополнения


Литература

  1. , Department of Defense, Trusted Computer System Evaluation Criteria. — DoD 5200.28, STD. 1993
  2. Карпов В.Е., Коньков К.А, Основы операционных систем, Издательство "Интуит.ру". 2005 г.– 2-е издание
  3. Кастер Хелен, Основы Windows NT и NTFS, М.: Русская редакция. 1996
  4. Рихтер, Windows для профессионалов: создание эффективных Win32-приложений с учетом специфики 64разрядныой версии Windows, СПб: Питер; M.: Издательско-торговый дом "Русская редакция", 2003
  5. Рихтер, Кларк, Программирование серверных приложний для Microsof Windows 2000, СПб: Питер; M.: Издательско-торговый дом "Русская редакция", 2001
  6. Руссинович М., Соломон Д, Внутреннее устройство Microsoft Windows: Windows Server 2003, Windows XP и Windows 2000, M.: Издательско-торговый дом "Русская редакция"; СПб.: Питер, 2005
  7. Сорокина С.И., Тихонов А.Ю., Щербаков А.Ю, Программирование драйверов и систем безопасности, СПб.: БХВ-Петербург, М.:Издатель Молгачева С.В., 2003
  8. Столлингс В, Операционные системы, М.: Вильямс, 2001
  9. Харт Д, Системное программирование в среде Win32, М.: Издательский дом "Вильямс", 2001
  10. Ховард, Разработка защищенных Web-приложений на платформе Microsoft Windows 2000, СПб: Питер; M.: Издательско-торговый дом "Русская редакция", 2001
  11. Таненбаум Э, Современные операционные системы, СПб.: Издательский дом Питер, 2002