Некоммерческое акционерное общество

АЛМАТИНСКИЙ УНИВЕРСИТЕТ ЭНЕРГЕТИКИ И СВЯЗИ

Кафедра компьютерных технологий

 

 

 

СИСТЕМНОЕ ПРОГРАММИРОВАНИЕ 

Конспект лекций для студентов всех форм обучения специальности

5В070400 – Вычислительная техника и программное обеспечение

 

 

Алматы 2011

СОСТАВИТЕЛИ: Мусатаева Г.Т., Конуспаева А.Т., Байжанова Д.О. Интерфейсы компьютерных систем. Конспект лекций для студентов всех форм обучения специальности 5В070400 – Вычислительная техника и программное обеспечение.- Алматы: АУЭС, 2011. - 52 с.

 

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

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

Библиография – 6 названий. 

 

Рецензент: канд. физ.-мат. наук, доцент Б. М. Шайхин.

 

Печатается по плану издания некоммерческого акционерного общества «Алматинский университет энергетики и связи» на 2011 г. 

 

© НАО «Алматинский университет энергетики и связи», 2011 г.


1 Лекция. Введение. Вопросы системного программирования

 

1.1 Основы программирования в операционной системе Windows

 

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

– классическая структура — имеет одно главное окно;

– диалоговая структура — главным окном является диалоговое окно;

– консольный тип — главным окном является консольное окно (создаваемое или наследуемое);

– безоконная структура — это Windows - приложение, не имеющее главного окна;

– сервисы — программы, имеющие специальную структуру и играющие особую роль в операционной системе;

– драйверы — имеющие особую структуру программы для   управления внешними устройствами.

Рассмотрим первую, классическую структуру

Итак, начнем с нескольких общих положений о программировании в Windows.

1. Программирование в Windows основывается на использовании функций API (Application Program Interface, Программный интерфейс приложе­ния). Взаимодействие с внешними устройствами и ресурсами операционной системы будет про­исходить посредством таких функций.

2. Список функций АРI и их описание лучше всего брать из файла WIN32.HLP, который поставляется, например, с пакетом Borland C++. Подробнейшее описание по функциям API и по программированию в среде Windows  в целом содержится в документации к  Visual Studio.NET.

3.  Главным элементом программы в среде Windows является окно. Для каж­дого окна определяется своя процедура обработки сообщений.

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

5. Операционная система Windows использует линейную адресацию памяти. Другими словами, всю память можно рассматривать как один сегмент.

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

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

1.1.1 Вызов функций API

Начнем с того, как можно вызвать функции API. Выберем любую функцию API, например, MessageBox:

int MessageBox (HWND hwnd, LPCTSTR 1pText, LPCTSTR 1pCaption, UINT uType);

         Данная функция выводит на экран окно с сообщением и кнопкой (или кнопками) выхода. Смысл параметров:

hWnd – дескриптор окна, в котором будет появляться окно-сообщение;

lpText – текст, который будет появляться в окне;

lpCaption – текст в заголовке окна;

uType – тип окна, в частности, можно определить количество кнопок выхода.

Теперь о типах параметров. Все они в действительности 32-битные целые числа:

HWND – 32-битное целое;

LPCTSTR – 32-битный указатель на строку;

UINT – 32-битное целое.

1.1.2 Структура программы

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

- регистрация класса окон;

- создание главного окна;

- цикл обработки очереди сообщений;

- процедура главного окна.

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

Регистрация класса окон

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

Создание окна

На основе зарегистрированного класса с помощью функции Create WindowExA (или Create WindowA) можно создать экземпляр окна. Как можно заметить, это весьма напоминает объектную модель программирования.

 

Цикл обработки очереди сообщений

Вот как выглядит этот цикл на языке Си:

while (GetMessage (&msq, NULL, 0, 0))

{

/ / разрешить использование клавиатуры,

/ / путем трансляции сообщений о виртуальных клавишах

/ / в сообщения о алфавитно-цифровых клавишах

TranslateMessage (&msq);

/ /  вернуть управление Windows и передать сообщение дальше

/ / процедуре окна

DispatchMessage (&msg);

}

         Функция GetMessage() «отлавливает» очередное сообщение из ряда сообщений данного приложения и помещает его в структуру MSG.

         Что касается функции TranslateMessage, то ее компетенция касается сообщений WM_KEYDOWN и WM_KEYUP, которые транслируются в WM_CHAR и WM_DEDCHAR, а также WM_SYSKEYDOWN и WM_SYSKEYUP, преобразующиеся в WM_SYSCHAR и WM_SYSDEADCHAR. Смысл трансляции заключается не в замене, а в отправке дополнительных сообщений. Так, например, при нажатии и отпускании алфавитно-цифровой клавиши в окно сначала придет сообщение WM_KEYDOWN, затем WM_KEYUP, а затем уже WM_CHAR.

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

Процедура главного окна

Вот прототип функции окна на языке С:

LRESULT CALLBACK WindowFunc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

         Смысл параметров:

hwnd – идентификатор окна;

message – идентификатор сообщения;

wParam и lParam – параметры, уточняющие смысл сообщения (для каждого сообщения могут играть разные роли или не играть никаких).

Все четыре параметра имеют тип DWORD.

 

Основная литература: [4] – 31-54, 551 - 559 c.

Контрольные вопросы

1.     Структура программы под Windows.

2.     В каком случае происходит выход из цикла обработки сообщений?

3.     Виды дескрипторных таблиц.

4.     Какую информацию включает адресное пространство процесса?

 


2 Лекция. Управление процессами

 

2.1 Управление процессами

 

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

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

 

2.2 Процессы и потоки в Windows

 

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

С точки зрения программиста, каждый процесс в Win32 включает компоненты, перечисленные ниже:

- Один или несколько потоков.

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

- Один или более сегментов кода, включая код DLL.

- Один или более сегментов данных, содержащих глобальные переменные.

- Строки окружения с информацией о переменных окружения таких, как текущий путь поиска.

- Память кучи процесса.

- Такие ресурсы, как открытые дескрипторы и другие кучи.

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

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

- Локальная память потока (Thread Local Storage — TLS) — массивы указателей, которые дают процессу возможность выделять память для создания собственного уникального окружения данных.

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

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

 

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

 

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

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

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

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

BOOL CreateProcess (LPCTSTR lpszImageName, LPTSTR lpszCommandLine,  LPSECURITY_ATTRIBUTES lpsaProcess,LPSECURITY_ATTRIBUTES lpsaThread, BOOL fInheritHandles, DWORD fdwCreate, LPVOID lpvEnvironment, LPCTSTR lpszCurDir, LPSTARTUPINFO lpsiStartInfo, LPPROCESS_INFORMATION lppiProcInfo);

Возвращаемое значение: TRUE только в случае, если процесс и поток были успешно созданы.

Выход из процесса и его завершение

После того как процесс закончен, он вызывает функцию ExitProcess с кодом завершения.

Эта функция ничего не возвращает, вызвавший ее процесс и все его потоки за­вершаются.

Для определения кода завершения другой процесс может использовать функцию GetExitCodeProcess.

BOOL GetExitCodeProcess ( HANDLE hProcess, LPDWORD lpdwExitCode);

Процесс, определенный переменной hProcess, должен иметь права доступа PROCESS_QUERY_INFORMATION (см. функцию OpenProcess). Параметр lpdwExitCode указывает на переменную типа dword, в которую будет помещено значение. Одно из возможных значений — STILL_ACTIVE; оно указывает, что процесс не был завершен.

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

BOOL TerminateProcess ( HANDLE hProcess, UINT uExitCode)

Ожидание завершения процесса

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

- Эти функции могут быть использованы для большого числа различных типов объектов; дескрипторы процессов — это лишь первое их применение.

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

- Для них может быть использована рекомендательная блокировка по времени.

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

Параметр lphObjects указывает на дескриптор одного процесса (типа hObject) или на массив различных объектов. Значение параметра cObjects (размер массива) не должно превышать константу maximum_wait_objects (в настоящее время ее значение 64 определено в заголовочном файле winnt.h).

 

Основная литература: [1] – 167 - 179 c.

Контрольные вопросы

1.     На основе каких факто­ров проводится планирование потоков?

2.     Какие компоненты включает каждый процесс в Win32?

3.     Какие элементы включает поток?

4.     Параметры API-функции OpenProcess().

 

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

 

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

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

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

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

 

3.1 Создание потока. Функция CreateThread

 

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

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

Параметры

psa - является указателем на структуру SECURITY_ATTRIBUTES.

cbStack - этот параметр определяет, какую часть адресного пространства поток сможет использовать под свой стек.

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

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

pdwThreadID - это адрес переменной типа DWORD, в которой функция возвращает идентификатор, приписанный системой новому потоку.

 

3.2 Завершение потока

 

Поток можно завершить четырьмя способами:

- функция потока возвращает управление (рекомендуемый способ);

- поток самоуничтожается вызовом функции ExitThread  (нежелательный способ);

- один из потоков данного или стороннего процесса вызывает функцию TerminateThread (нежелательный способ);

- завершается процесс, содержащий данный поток (тоже нежелательно).

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

Поток можно завершить принудительно, вызвав:

VOID ExitThread ( DWORD dwExitCode );

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

Вызов следующей функции также завершает поток:

BOOL TeminateThread(HANDLE hThread, DWORD dwExitCode);

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

 

Основная литература: [1]199 - 220 c.

Контрольные вопросы

1.     Назначение счетчика пользователей объектов ядра.

2.     Для чего используется флаг CREATE_SUSPENDED?

3.     Какие данные хранятся в структуре CONTEXT?

4.     В каком диапазоне находятся уровни приоритетов потоков?

 

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

 

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

Необходимость синхронизации потоков

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

Объекты синхронизации потоков

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

Четвертый объект - CRITICAL_SECTION. Благодаря своей простоте и преимуществам в быстродействии, объекты CRITICAL_SECTION являются наиболее пред­почтительным механизмом, если только они соответствуют требованиям программы.

 

4.1 Объект CRITICAL_SECTION

 

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

В качестве простого механизма для реализации идеи критической секции Win32 предоставляет объект CRITICAL_SECTION.

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

Для инициализации и удаления переменной типа CRITICAL_SECTION используйте следующие функции:

VOID InitializeCriticalSection (LPCRITICAL_SECTION lpcsCritical Section)

VOID DeleteCriticalSection (LPCRITICAL_SECTION lpcsCriticalSection)

Функция EnterCriticalSection блокирует поток, если в данной секции находится другой поток. С ожидающего потока снимается блокировка, когда другой поток выполняет функцию LeaveCritcicalSection. Мы говорим, что поток "владеет" КС с того момента, когда он получает управление от функции EnterCriticalSection, по тот момент, как функция LeaveCriticalSection отдает владение КС. Всегда отдавайте владение КС; если этого не сделать, другие потоки вынуждены будут ожидать вечно, даже если владеющий поток будет завершен.

Мы часто будем говорить, что КС "блокирована" или "разблокирована", а вхождение в КС — то же самое, что и ее блокировка.

VOID EnterCriticalSection (LPCRITICAL_SECTION lpcsCriticalSecticn)

VOID LeaveCriticalSection (LPCRITICAL _ SECTION lpcsCriticalSecticn)

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

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

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

BOOL TryEnterCriticalSection (LPCRITICAL_SECTION lpcsCriticalSection)

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

Объекты CRITICAL_SECTION имеют преимущество в том, что не являются объектами ядра и располагаются в пользовательском пространстве.

Управление счетчиком попыток

Если при выполнении функции EnterCriticalSection поток определяет, что КС уже занята, он обычно входит в ядро и блокируется до освобождения объекта CRITICAL_SECTION, что требует временных затрат. Но вы можете заставить поток сделать еще одну попытку перед блокировкой. Для управления счетчиком попыток существуют две функции: SetCriticalSectionSpinCount, которая позволяет динамически изменять счетчик, и InitializeCriticalSectionAndSpinCount, которая служит заменой функции InitializeCriticalSection.

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

 

4.2 Мьютексы

 

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

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

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

Для работы с мьютексами используются функции Win32 CreateMutex, Release­Mutex и OpenMutex.

HANDLE CreateMutex (LPSECURITY_ATTRIBUTES lpsa,BOOL fInitialOwner, LPCTSTR lpszMutexName)

Значение TRUE флага fInitialOwner позволяет вызывающему функцию потоку немедленно вступить во владение новым мьютексом. Эта элементарная операция за­щищает другие потоки от захвата владения мьютексом до того, как это сделает соз­дающий его поток. Как видно из имени, этот флаг игнорируется, если мьютекс уже существует.

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

Возвращенное значение NULL сообщает об ошибке

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

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

BOOL  ReleaseMutex (HANDLE  hMutex)

Покинутые мьютексы

Если поток завершается, не освободив мьютекс, которым он владеет, мьютекс ста­новится покинутым, а его дескриптор переходит в сигнальное состояние. Функция WaitForSingleObject возвратит значение WAIT_ABANDONED_0, а функция WaitForMultipleObjects использует значение WAIT_ABANDONED_0 как базовое для того, чтобы сообщить, что дескрипторы в сигнальном состоянии представляют покинутые мьютексы.

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

 

 

Основная литература: [1] – 228-246 c.

Контрольные вопросы

1. Назовите объекты синхронизации потоков.

2. Какие объекты могут быть использованы для межпроцессной синхронизации потоков в отдельных процессах?

 

5 Лекция. Дополнительные методы синхронизации потоков

 

5.1 События

 

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

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

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

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

Событиями используются функции CreateEvent, OpenEvent, SetEvent, ResetEvent и PulseEvent.

HANDLE CreateEvent (LPSECURITY_ATTRIBUTES lpsa, BOOL fManualReset, BOOL fInitialState, LPCTSTR lpszEventName)

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

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

BOOL SetEvent (HANDLE hEvent)

BOOL ResetEvent (HANDLE hEvent;

BOOL PulseEvent (HANDLE hEvent)

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

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

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

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

Четыре модели использования событий

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

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

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

 

Основная литература: [1] – 246- 258 c.

Контрольные вопросы

1.     Виды событий.

2.     Четыре модели использования событий.

3.     Назначение сблокированных функций.

 

6 Лекция. Управление памятью. Использование виртуальной памяти

 

Архитектура управления памятью

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

Обзор управления памятью

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

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

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

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

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

На рисунке 6.1 показан механизм управления памятью в API Win32, основанный на диспетчере виртуальной память (Virtual Memory Manager). API  виртуальной памяти Win32 (функции VirtualAlloc, VirtualFree, VirtualLock, VirtualUnlock и т.д. работает с целыми страницами. API “кучи” Win32 может работать с единицами память, определяемыми пользователем.

Функция VitualAlloc распределяет ряда страниц в виртуальном адресном пространстве, а функция VitualAllocEx распределяет ряд страниц в виртуальном адресном пространстве указанного процесса.

LPVOID VitualAlloc  LPVOID lpvAddress, DWORD dwSize, DWORD dwAllocationType, DWORD dwProtect).

LPVOID VitualAllocEx (HANDLE hProcess, LPVOID lpvAddress, DWORD dwSize, DWORD dwAllocationType, DWORD  dwProtect).

Функция VirtualFree освобождает ряд страниц в виртуальном адресном пространстве. А функция VirtualFreeEx освобождает ряд страниц в виртуальном адресном пространстве указанного процесса.

BOOL VirtualFree(LPVOID lpvAddress, DWORD dwSize, DWORD dwFreeType)

BOOL VirtualFreeEx(HANDLE hProcess, LPVOID lpvAddress, DWORD dwSize, DWORD dwFreeType)

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

BOOL VirtualLock(LPVOID lpvAddress, DWORD dwSize),

BOOL VirtualUnlock(LPVOID lpvAddress, DWORD dwSize)

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

BOOL VirtualProtect(LPVOID lpvAddress, DWORD dwSize, DWORD dwNewProtect, DWORD pdwOldProtect)

BOOL VirtualProtectEx(HANDLE hProcess, LPVOID lpvAddress, DWORD dwSize, DWORD dwNewProtect, DWORD pdwOldProtect)

 

Основная литература: [1] – 132 –134 c., [9] – 340 –372 c.

Контрольные вопросы

1.     Механизм управления памятью в API Win32.

2.     Основные API функции работы с виртуальной памятью.

3.     Что понимается под страничными ошибками?

 

7 Лекция. Динамически распределяемая память

 

7.1 Кучи

 

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

Часто достаточно одной кучи, но по приведенным ниже причинам используется и много куч. Если достаточно одной кучи, просто используйте функции библиотеки С для управления памятью (malloc, free, calloc, realloc).

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

HANDLE GetProcessHeap (VOID)

Возвращаемое значение: дескриптор кучи процесса; NULL в случае неудачи.

Заметим, что в данном случае для указания на ошибку возвращается значение NULL, а не INVALID_HANDLE_VALUE, как в функции CreateFile.

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

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

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

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

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

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

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

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

HANDLE HeapCreate (DWORD flOptions, SIZE_T dwInitialSize, SIZE_T dwMaximumSize);

Возвращаемое  значение: дескриптор кучи или NULL в  случае ошибки.

Два поля размера имеют тип size_t, а не dword. Тип size_t определен таким образом, что может быть 32-разрядным или 64-разрядным беззнаковым целым, в зависимости от флагов компилятора (_WIN32 или _WIN64). Тип size_t был введен для обеспечения возможности перехода к WIN64.

Переменная flOptions является комбинацией двух флагов:

- HEAP_GENERATE_EXCEPTIONS— при этом значении неудачные попытки выде­ления памяти вызывают исключения, которые будут обработаны структурным обработчиком исключений (Structured Exception HandlerSEH,). Функция HeapCreate сама по себе не вызывает исключений; если этот флаг установлен, исключение вызывают при неудаче такие функции, как HeapAlloc.

- HEAP_NO_SERIALIZE — установка этого флага в некоторых случаях позволяет по­лучить небольшое повышение быстродействия. Следует сказать также несколько слов о dwMaximumSize.

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

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

Отметим, что кучи не имеют атрибутов безопасности, так как они недоступны из­вне процессу.

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

BOOL HeapDestroy (HANDLE hHeap);

Переменная hHeap должна указывать на кучу, созданную функцией HeapCreate. Будьте осторожны, не уничтожьте кучу процесса (полученную функцией GetProcessHeap). Уничтожение кучи освобождает пространство виртуальной памяти и физическую память в файле подкачки. Разумеется, грамотно спроектированные про­граммы должны освобождать кучи, которые больше не используются.

Уничтожение кучи — это также быстрый способ освободить структуру данных без необходимости уничтожать каждый элемент отдельно, хотя экземпляры объектов С++ таким образом не будут уничтожены, поскольку их деструкторы не вызываются. Уничтожение кучи имеет ряд преимуществ:

1) Нет необходимости писать код для поэлементного обхода структуры.

2) Нет необходимости освобождать каждый отдельный элемент.

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

 

7.2 Управление памятью кучи

 

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

LPVOID HeapAlloc (HANDLE hHeap, DWORD dwFlags, SIZE_T dwBytes)

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

Параметры функции HeapAlloc

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

dwFlags представляет собой комбинацию трех флагов.

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

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

dwBytes — размер блока памяти, который должен быть выделен. Для невозрастаю­щих куч его предел составляет 0x7FFF8 (примерно 0,5 Мбайт).

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

BOOL HeapFree (HANDLE hHeap, DWORD dwFlags, LPVOID lpMem)

dwFlags должен быть равен нулю или константе HEAP_NO_SERIALIZE.

lpMem должен иметь значение, возвращенное функцией HeapAlloc или HeapReAlloc,

hHeap — это, конечно, дескриптор кучи, из которой был выделен блок lpMem.

Для изменения размера блоки памяти могут быть выделены заново.

LPVOID HeapReAlloc (HANDLE hHeap, DWORD dwFlags, LPVOID lpMem, SIZE_T dwBytes)

Возвращаемое значение: указатель на выделенный заново блок. При неудаче возвращает NULL или вызывает исключение.

 

Основная литература: [3] – 193-206 c.

Контрольные вопросы

1.     Преимущества использования отдельных куч.

2.     Последовательность действий при работе с кучей.

3.     API функции для работы с кучей.

 

 

8 Лекция. Отображаемые в память файлы

 

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

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

- Нет необходимости выполнять прямой ввод-вывод из файла (запись и чтение).

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

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

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

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

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

- Нет необходимости расходовать пространство файла подкачки.

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

Объекты отображения файлов

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

HANDLE CreateFileMapping (HANDLE hFile, LPSECURITY_ATTRIBUTES lpsa, DWORD fdwProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpszMapName)

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

hFile — дескриптор открытого файла с флагами защиты, совместимыми с пара­метром fdwProtect. Значение (типа HANDLE) 0xFFFFFFFF (эквивалентно константе INVALID_HANDLE_VALUE) указывает на файл подкачки, и вы можете использовать это значение для межпроцессного разделения памяти без создания отдельного файла.

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

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

- Флаг PAGE_READWRITE предоставляет полный доступ  к  объекту, если файл hFile открыт с правами доступа GENERIC_READ и GENERIC_WRITE.

- Флаг PAGE_WRITECOPY определяет, что при изменении содержимого отображен­ной памяти собственная (для данного процесса) копия записывается в файл подкачки, а не в исходный файл. Отладчик может использовать этот флаг при определении точек останова в разделяемом коде. Результат будет различным в Windows.

Параметры dwMaximumSizeHigh и dwMaximumSizeLow определяют размер объекта отображения. Если указан нуль, используется текущий размер; обязательно опреде­ляйте размер при использовании файла подкачки. Если ожидается, что размер файла увеличится, используйте ожидаемый размер, и при необходимости будет немедленно установлен нужный размер файла. Не отображайте область файла за указанной гра­ницей — объект отображения не может расти.

lpszMapName указывает имя объекта отображения, что позволяет другим процес­сам совместно использовать объект. Регистр символов в имени не учитывается. Если разделение памяти не используется, указывайте значение null.

Об ошибке сообщает возвращаемое значение null (a не INVALID_HANDLE_VALUE).

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

HANDLE OpenFileMapping (DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName);

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

dwDesiredAccess использует тот же набор флагов, что и параметр функции CreateFileMapping. Значение lpName — имя, полученное вызовом функции CreateFileMapping. Параметр blnheritHandle - наследование дескриптора.

Функция CloseHandle, как и ожидалось, уничтожает дескрипторы отображения.

Отображение адресного пространства процесса в объекты отображения

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

lpvoidMapViewOfFile (HANDLE hMapObject, DWORD fdwAccess, DWORD dwOffsetHigh,  DWORD dwOffsetLow, SIZE_T cbMap);

Возвращаемое значение: начальный адрес блока (образ файла) или NULL. при  неудаче.

Параметры

hMapObject указывает объект отображения файла, полученный от функций CreateFileMapping или  OpenFileMapping. Значение параметра fdwAccess должно соответствовать правам доступа объекта отображения. возможны три значения флагов: FILE_MAP_WRITE, FILE_MAP_READ и FILE_MAP_ALL_ACCESS (поразрядное "или" двух предыдущих флагов).

Параметры dwOffsetHigh и dwOffsetLow определяют начальное положение об­ласти отображения. Начальный адрес должен быть кратным 64К. Для отображения от начала файла используйте нулевое значение смещения.

cbMap указывает размер отображаемой области в байтах. Его нулевое значение при вы­зове функции MapViewOfFile указывает на то, что весь файл должен быть отображен.

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

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

BOOL UnmapViewOfFile (LPVOID lpBaseAddress)

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

 

Основная литература: [1] – 145- 158 c, [2] – 409-461 c.

Контрольные вопросы

1.     Преимущества применения отображаемых в память файлов.

2.     Стандартная последоватальность действий для отображения файлов.

3.     Ограничения отображения файлов.

 

9 Лекция. Динамические библиотеки

 

В DLL содержатся все функции Windows API. Три самые важные DLL: Kernel32.dll (управление памятью, процессами и потоками), User32.dll (поддержка пользовательского интерфейса, в том числе, функции, связанные с созданием окон и передачей сообщений) и GDI32.dll (графика и вывод текста).

В Windows есть  другие DLL:

AdvAPI32.dll – содержит функции для защиты объектов, работы с реестром и регистрации событий.

ComDlg32.dll – стандартные диалоговые окна (вроде FileOpen и FileSave).

ComCtl32.dll поддерживает стандартные элементы управления. DLL нужно применять для реализации следующих возможностей:

1)     Расширение функциональности приложения.

2)     Возможность использования разных языков программирования.

3)     Более простое управление проектом.

4)     Экономия памяти.

5)     Разделение ресурсов.

6)     Упрощение локализации.

7)     Решение проблем, связанных с особенностями различных платформ.

8)     Реализация специфических возможностей.

DLL и адресное пространство процесса

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

Файлы с исходным кодом компилируются и компонуются так же, как и при создании EXE-файла, но при компоновке нужно указать ключ /DLL.

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

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

Неявное связывание EXE – и DLL – модулей

Неявное связывание (implicit linking) - самый распространенный метод.

Исполняемый модуль (EXE) импортирует функции и переменные из DLL, а DLL– модули экспортирует их в исполняемый модуль. DLL – также может импортировать функции и переменные их других DLL.

Создание DLL-модуля

DLL может экспортировать переменные, функции или С++ классы в другие модули.

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

При компиляции исходного файла DLL MYLIBAPI определяется как __declspec (dllexport) до включения заголовочного файла MyLib.h. Такой модификатор означает, что данная переменная, функция или C++ класс экспортируется  из DLL.

Также следует обратить внимание, что в файле MyLibFile1.cpp перед экспортируемой переменной или функцией не ставится идентификатор MYLIBAPI. Он здесь не нужен: проанализировав заголовочный файл, компилятор запоминает, какие переменные и функции являются экспортируемыми.

Идентификатор MYLIBAPI включает extern. Модификатор extern не даёт компилятору искажать имена переменных или функции, и они становятся доступными исполняемым модулям, написанным на С, С++ или любом другом языке программирования. Этим модификатором можно пользоваться только в коде С++, но ни в коем случае ни в коде на стандартном С.

Мы рассмотрели, как используется заголовочный файл в исходных файлах DLL, а в исходных файлах ЕХЕ-модуля MYLIBAPI определять не надо: включая заголовочный файл, вы определяете этот идентификатор как DLLSPEC(DLLIMPORT), и при помещении исходного кода ЕХЕ-модуля компилятор поймёт, что переменные и функции импортируются из DLL.

Что такое экспорт?

Если перед переменной, прототипом функции или С++ классом указан модификатор – _dlspec(dllexport), компилятор Microsoft C/C++ встраивает в конечный obj-файл дополнительную информацию. Она понадобится при сборке DLL из OBJ-файлов.

Обнаружив такую информацию, компоновщик создает LIB-файл со списком идентификаторов, экспортируемых из DLL. Этот LIB-файл нужен при сборке любого EXE-модуля, ссылающегося на такие идентификаторы. Компоновщик также вставляет в конечный DLL-файл таблицу экспортируемых идентификаторов – раздел экспорта, в котором содержится список (в алфавитном порядке) идентификаторов экспортируемых функций, переменных и классов. Туда же помещается относительный виртуальный адрес (relative virtual address, RVA) каждого идентификатора внутри DLL-модуля.

Создание EXE-модуля

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

//Модуль: MyExeFilel.cpp

//Сюда включаются стандартные заголовочные файлы Windows и библиотеки С

#include <windows.h>

//включаем экспортируемые структуры данных, идентификаторы, функции и переменные

#include “MyLib\MyLib.h”

….

int WINAPI WinMain(HINSTANCE hinstExe, HINSTANCE, LPTSTR pszCmdLine, int){int nLeft=10, nRight=25; TCHAR sz[100]; wsprintf(sz, TEXT(“%d + %d = %d”), nLeft, nRight, Add(nLeft, nRight)); MessageBox(NULL, sz, TEXT(“Calculation”), MB_OK); wsprintf(sz, TEXT(“The result from the last Add is: %d”), g_nResult); MessageBox(NULL, sz, TEXT(“Last Result”), MB_OK); return (0);}

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

MYLIBAPI  в исходных файлах EXE-модуля до заголовочного файла DLL не определяется. Поэтому при компиляции приведенного выше кода MYLIBAPI за счет заголовочного файла MyLib.h будет определен как __declspec(dllimport). Встречая такой модификатор перед именем переменной, функции или С++ класса, компилятор понимает, что данный идентификатор импортируется из какого-то DLL –модуля.

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

Что такое импорт?

Импортируя идентификатор, необязательно прибегать к __declspec(dllimport) – можно использовать стандартное ключевое слово extern языка С. Но компилятор создаст чуть более эффективный код, если ему будет заранее известно, что идентификатор, на который мы ссылаемся, импортируется из LIB-файла DLL –модуля.

Разрешая ссылки на импортируемые идентификаторы, компоновщик создаст в конечном EXE-модуле раздел импорта (import section).

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

Выполнение EXE-модуля

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

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

1)     Каталог, содержащий EXE-файл.

2)     Текущий каталог процесса.

3)     Системный каталог Windows.

4)     Основной каталог Windows.

5)     Каталоги, указанные в переменной окружения PATH.

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

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

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

Загрузчик всех этих DLL и настройка ссылок занимает какое-то время. Чтобы сократить время загрузки приложения нужно модифицировать базовые адреса EXE- и DLL-модулей и провести их связывание.

Явная загрузка DLL и связывание идентификаторов

Чтобы поток мог вызвать функцию из DLL-модуля, DLL надо спроецировать на адресное пространство процесса, которому принадлежит этот поток. Делается это двумя способами:

1)     Код приложения просто ссылается на идентификаторы, содержащиеся в DLL, и тем самым заставляет загрузчик неявно загружать (и связывать) нужную DLL при запуске приложения.

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

 

Основная литература: [1] – 158 - 166 c, [2] – 475-526 c.

Контрольные вопросы

1.     Как создается DLL модуль?

2.     Для чего используются разделы экспорта и импорта?

3.     Как создается EXE модуль, импортирующий идентификаторы из DLL?

 

10 Лекция. Использование файловой системы

 

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

 

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

 

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

Для создания и открытия файлов всех типов служит единственная функция API Win32 – CreateFile. В приложении можно указать, будет ли выполняться чтение из файла, запись в файл или и то и другое. Можно также указать, предусматривается ли совместное использование этого файла для чтения, записи либо для того и другого.

HANDLE CreateFile (LPCTSTR lpszName, DWORD fdwAccess, DWORD fdwShareMode, LPSECURITY_ATTRIBUTES lpsa, DWORD fdwCreate, DWORD fdwAttrsAndFlags, HANDLE hTemplateFile)

Возвращаемое значение: дескриптор объекта открытого файла или INVALID_HANDLE_FAILURE в случае неудачи.

Параметры

lpszName – указатель на строку, содержащую имя файла, канала или другого именованного объекта, который требуется открыть или создать.

fdwAccess определяет режим доступа для чтения или записи значениями GENERIC_READ  и GENERIC_WRITE соответственно. Могут использоваться другие имена констант. Эти значения можно объединить с помощью поразрядного “или”.

fdwShareMode – это объединение поразрядным “или” сочетание приведенных ниже значений:

0 – совместный доступ к файлу не допускается.

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

FILE_SHARE_WRITE – разрешается параллельная запись в файл.

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

lpsa указывает на структуру SECURITY_ATTRIBUTES.

fdwCreate определяет, надо ли создать новый файл, заменить существующий файл и т.д. Отдельные значения можно объединить оператором поразрядного “или”.

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

CREATE_ALWAYS – существующий файл заменяется новым.

OPEN_EXISTING – неудача, если файл не существует.

OPEN_ALWAYS – файл открывается и создается, если он не существует.

TRUNCATE_ EXISTING – устанавливается нулевая длина файла. В fdwAccess должен быть задан по крайней мере доступ GENERIC_WRITE.

fdwAttrsAndFlags определяет атрибуты и флаги. Существует 16 флагов и атрибутов. Атрибуты – это характеристики файла, а не открытого дескриптора HANDLE; они игнорируются, когда открывается существующий флаг. Ниже перечислены некоторые важные атрибуты и флаги.

FILE_ATTRIBUTE_NORMAL – может использоваться, только если не установлены никакие другие атрибуты (однако флаги могут быть установлены).

FILE_ATTRIBUTE_ READONLY – приложения не могут не писать в файл, ни удалять его.

FILE_FLAG_DELITE_ON_CLOSE – полезен для временных файлов. Файл удаляется, когда закрывается его последний открытый дескриптор.

FILE_FLAG_OVERLAPPED – важен для асинхронного ввода-вывода. В Windows он имеет значение NULL всегда, кроме устройств последовательного ввода-вывода.

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

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

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

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

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

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

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

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

BOOL CloseHandle (HANDLE hObject)

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

Чтение файлов

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

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

Параметры

hfile дескриптор файла с доступом GENERIC_READ

lpBuffer указывает на буфер памяти для получения входных данных.

nNumberOfBytesToRead количество байтов, которые нужно прочитать из файла.

lpNumberOfBytesReadуказывает на фактическое число байтов, прочитанное функцией  ReadFile.

lpOverlapped указывает на структуру OVERLAPPED.

Запись в файл

BOOL WriteFile (HANDLE hFile, CONST VOID *lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped)

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

Win32 содержит множество функций управления файлами.

Для удаления файла

BOOL DeleteFile (LPCTSTR  lpszFileName)

Копирование файла

BOOL CopyFile (LPCTSTR lpszExistingFile, LPCTSTR lpszNewFile, BOOL fFaillfExists)

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

Еще две функции служат для переименования, или "перемещения", файла. Эти функции также могут работать с каталогами

BOOL MoveFile ( LPCTSTR lpszExisting, LPCTSTR lpszNew);

BOOL MoveFileEx (LPCTSTR lpszExisting, LPCTSTR lpSzNew, DWORD fdwFlags)

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

Параметры

lpszExisting определяет имя существующего файла или каталога.

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

fdwFlags определяет следующие опции:

- MOVEFILE_REPLACE_EXISTING — используется для замены существующего файла;

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

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

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

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

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

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

 

Основная литература: [1] – 36 - 74 c.

Контрольные вопросы

1.     Функции управления файлами.

2.     Функции управления каталогами.

3.     Параметры функции CreateFile().

 

11 Лекция. Дополнительные методы работы с файлами и каталогами. Реестр

 

11.1 Блокировка файлов

 

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

Win32 может блокировать файлы полностью или частично, т.е. так, чтобы никакой другой процесс (выполняемая программа) не мог обращаться к блокированной области файла. Заблокированный файл может быть открыт только для чтения (совместный доступ) или для чтения и записи (монопольный доступ). Следует подчеркнуть, что блокировка связана с процессом. Любая попытка обратиться к части файла (с помо­щью ReadFile или WriteFile) в нарушение существующей блокировки потерпит неудачу, так как блокировка обязательна на уровне процесса. Любая попытка получить конфликтную блокировку также будет неудачной, даже если процесс уже имеет блокировку. Блокировка файлов — это ограниченная форма синхронизации парал­лельных процессов и потоков.

Наиболее общая функция блокировки LockFileEx реализована только в Windows. Менее общая функция LockFile может применяться в Windows.

LockFileEx принадлежит к классу функций расширенного ввода-вывода; для указания 64-разрядных позиций в файле и размера блокируемой области требуется структура перекрытия, которая уже применялась ранее для указания позиции в функциях ReadFile и WriteFile.

BOOL LockFileEx (HANDLE hFile,  DWORD dwFlags, DWORD dwReserved, DWORD nNumberOfBytesToLockLow, DWORD nNumberOfBytesToLockHigh, LPOVERLAPPED lpOverlapped)

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

Параметры

hFile — дескриптор открытого файла. Дескриптор должен иметь права доступа либо GENERIC_READ, либо GENERIC_READ и GENERIC_WRITE.

dwFlags определяет режим блокировки и устанавливает, следует ли ожидать, пока блокировка не станет возможна.

LOCKFILE_EXCLUSIVE_LOCK обозначает требование на монопольную блокировку с возможностью чтения/записи. Если этот флаг не установлен, запрашивается совместная блокировка (с возможностью чтения).

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

dwReserved должен быть равен нулю. Следующие два параметра задают длину об­ласти байтов и поэтому здесь не поясняются.

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

DWORD offset (именно так, а не OffsetLow).

DWORD OffsetHigh.

HANDLE hEvent должен быть равен нулю.

Блокировка файла снимается с помощью функции UnlockFileEx; параметры для нее указываются те же самые, кроме dwFlags.

BOOL UnlockFileEx (HANDLE hFile, DWORD dwReserved, DWORD nNumberOfBytesToLockLow, DWORD nNumberOfBytesToLockHigh, LPOVERLAPPED lpOverlapped)

При блокировке файлов следует учитывать несколько факторов.

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

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

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

Блокировки не наследуются вновь создаваемым процессом.

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

 

Таблица 11.1 - Логика требования блокировки

Требуемый тип блокировки

Существующая блокировка

Совместная блокировка

Монопольная блокировка

Нет

Предоставляется

Предоставляется

Совместная (одна или несколько)

Предоставляется

Предоставляется

Исключительная

Не предоставляется

Не предоставляется

 

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

 

Таблица 11.2 - Блокировки и операции ввода-вывода

Существующая блокировка

Операция ввода-вывода

Чтение

Запись

Нет

Проходит успешно

Проходит успешно

Совместная (одна или несколько)

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

Неудача

Исключительная

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

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

 

Операции чтения и записи обычно имеют форму функций ReadFile и WriteFile или их расширенных версий ReadFileEx и WriteFileEx. Для диагностики неудачи чтения или записи вызывается GetLastError.

Еще одна форма ввода-вывода — обращение к па­мяти, в которую отображается файл. Конфликты блокировки не обнаруживаются при обращении к памяти; они проявляются, когда вызывается функция MapViewOfFile. Эта функция делает часть файла доступной для процесса, а потому в этот момент сле­дует проверить блокировку.

LockFile — это ограниченная специальная версия функции блокировки. Она обеспечивает рекомендательную блокировку (advisory locking). В этом случае предоставляется только монопольный доступ и функция возвращает управление немедленно. Иначе говоря, LockFile не блокируется. Установлена ли блокировка, можно прове­рить по возвращаемому значению.

Снятие блокировки файла

За каждым успешным вызовом LockFileEx должно следовать одно соответствую­щее ему обращение к UnlockFileEx (это правило распространяется также на LockFile и UnlockFile). Если программа не может снять блокировку или задержи­вает ее дольше, чем необходимо, другие программы не смогут продолжать работу или же это негативно повлияет на их быстродействие. Следовательно, программы должны быть аккуратно спроектированы и реализованы так, чтобы блокировки снимались как можно скорее; логики, которая заставляла бы программу обходить операцию разбло­кировки, следует избегать.

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

 

Основная литература: [1] – 62 - 89 c.

Контрольные вопросы

1.     Назначение блокировки файлов.

2.     Назначение реестра.

3.     API функции для работы с реестром.

 

12 Лекция. Использование ввода-вывода

 

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

 

Win32 имеет три стандартных устройства для ввода, вывода и сообщения об ошибках. Имеется функция для получения дескрипторов стандартных устройств.

HANDLE GetStdHandle (DWORD nStdHandle)

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

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

- STD_INPUT_HANDLE;

- STD_OUTPUT_HANDLE;

- STD_ERROR_HANDLE.

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

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

BOOL SetStdHandle (DWORD nStdHandle,  HANDLE hHandle)

Возвращаемое значение: TRUE или FALSE, в зависимости от успеха или неудачи.

Параметры SetStdHandle

Возможное значение nStdHandle – те же, что и в GetStdHandle. Параметр hHandle определяет открытый файл, который должен быть стандартным устройством.

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

Два имени файлов зарезервированы для консольного ввода (с клавиатуры) и консольного вывода: CONINS и CONOUTS. Консоль можно использовать независимо от любого перенаправления этих стандартных устройств; для этого нужно просто открыть дескрипторы CONINS или CONOUTS с помощью CreateFile.

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

BOOL SetConsoleMode (HANDLE hConsole,  DWORD fdevMode)

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

Параметры SetConsoleMode

nConsole идентифицирует буфер ввода или экрана, который должен иметь атрибут доступа GENERIC_WRITE, даже если соответствующее устройство допускает только ввод.

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

- ENABLE_LINE_INPUT функция чтения (ReadConsole) завершается, когда встречается символ возврата каретки.

- ENABLE_ECHO_INPUT читаемые символы дублируются на экране.

- ENABLE_PROCESSED_INPUT система обрабатывает символы возврата (backspace), возврата каретки и перевода строки.

- ENABLE_ PROCESSED _OUTPUT система обрабатывает символы возврата (backspace), табуляции, звукового сигнала,возврата каретки и перевода строки.

- ENABLE_WRAP_AT_EOL_OUTPUT разрешается перенос строк как при обычном, так и при дублированном выводе.

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

Функции ReadConsole и WriteConsole подобны ReadFile и WriteFile.

BOOL ReadConsole (HANDLE hConsoleInput, LPVOID lpvBuffer, DWORD cchToRead, LPDWORD lpcchRead, LPVOID lpvReserved)

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

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

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

BOOL FreeConsole (VOID)

BOOL AllocConsole (VOID)

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

 

12.2 Асинхронный ввод-вывод и порты завершения

 

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

В Win32 существует три метода выполнения асинхронного ввода-вывода:

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

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

- Процедуры завершения (или расширенный ввод-вывод).  Когда операция ввода-вывода завершается, система вызывает в потоке указанную процедуру завершения.

Ввод-вывод с перекрытием

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

Структуры перекрытия в функции LockFileEx можно использовать для ввода-вывода с перекрытием. Эти структуры — необязательные параметры четы­рех функций ввода-вывода (ReadFile, WriteFile, TransactNamedPipe, ConnectNamedPipe), которые потенциально могут блокировать их при завер­шении операции.

Если указать FILE_FLAG_OVERLAPPED в структуре fdwAttrsAndFlags (для CreateFile) или в структуре fdwOpenMode (для CreateNamedPipe), канал или файл должен использоваться только в асинхронном режиме. Ввод-вывод с перекрытием не действует для анонимных каналов.

Структуры перекрытия

Структура перекрытия (указываемая, например, в параметре lpOverlapped функ­ции ReadFile) содержит следующие данные:

- позицию в файле (64-разрядную), на которой должно начаться чтение или запись;

- событие (с ручным сбросом), которое будет сгенерировано при завершении операции.

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

typedef struct_OVERLAPPED  {DWORD Internal; DWORD InternalHigh;

DWORD Offset; DWORD OffsetHigh; HANDLE hEvent;} OVERLAPPED

Позиция в файле (указатель) занимает два элемента — offset и OffsetHigh, хотя старшая часть обычно равна нулю. Элементы Internal и InternalHigh зарезервированы для системы и не должны использоваться.

hEvent — дескриптор события (созданный функцией CreateEvent). Событие мо­жет быть именованным или неименованным, но обязательно должно иметь ручной сброс. hEvent может быть равен NULL; в этом случае программа может ожидать сигнала дескриптора файла, который также является объектом синхрониза­ции. Когда hEvent равен NULL, система сообща­ет о завершении работы по дескриптору файла, т.е. дескриптор файла становится объ­ектом синхронизации.

Это событие немедленно сбрасывается (переводится в пассивное состояние) системой, когда программа делает вызов операции ввода-вывода. Когда операция ввода-вывода завершается, событие становится активным.

Отмена операций ввода-вывода с перекрытием

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

 

Основная литература: [1] – 46 - 52 c., 359 – 386 с.

Контрольные вопросы

1.     Каким устройствам обычно назначаются стандартные устройства?

2.     Специальные функции консольного ввода-вывода.

3.     Метод выполнения асинхронного ввода-вывода.

 

13 Лекция. Безопасность объектов Win32

 

Цели системы безопасности

У системы безопасности две основные цели: проверка уровня доступа и контроль за действиями клиента. Чтобы выполнить первую цель, необходимо убедиться в том, что пользователь обладает правом доступа к защищаемому объекту. Если это не так, попытка доступа должна окончиться неудачей, а пользователь должен получить сообщение «доступ запрещен» (access denied). Вторая функция системы безопасности — аудит, то есть слежение за действиями клиента — выполняется далеко не всегда. Аудит подразумевает документирование в файле журнала действий клиента, связанных с доступом к тому или иному защищаемому объекту. Администраторы могут включать или выключать систему аудита, они также могут настроить ее таким образом, чтобы документировались не все попытки доступа, а только те, которые были связаны с нарушением нрав доступа и в результате оказались безуспешными.

Права и привилегии

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

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

Атрибуты безопасности

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

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

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

Атрибуты безопасности имеют следующее определение:

Typedef struct _ SECURITY_ATTRIBUTES {DWORD nLength;

// Размер структуры

LPVOID lpSecurityDescriptor;

// Дескриптор безопасности,

// контролирующий доступ к объекту

BOOL bInheritHandle;

// Разрешает наследование дескриптора

// (handle)

// дочерним процессом

} SECURITY_ATTRIBUTES;

Элемент nLength должен иметь значение sizeof (SECURITY_ATTRIBUTES).

Обзор элементов безопасности: дескриптор безопасности

Дескриптор безопасности инициализируется функцией InitializeSecurityDescriptor.

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

Дескриптор безопасности объекта хранит в себе следующую информацию;

- SID (security identifier) владельца объекта;

- SID основной группы владельца объекта;

- список разграничительного контроля доступа (Discretionary Access Control List, DACL-дискреционный список управления доступом);

- системный список управления доступом (System Access Control List, SACL);

- управляющая информация (например, сведения о том, как списки ACL передают информацию дочерним дескрипторам безопасности);

Список DACL определяет, кто обладает (и кто не обладает) правом доступа к объекту. Список SACL определяет, информация о каких действиях вносится в файл журнала.

Функция InitializeSecurityDescriptor инициализирует указанный вами дескриптор таким образом, что в нем отсутствует DASL, SACL, владелец и основная группа владельца, а все управляющие флаги установлены в значение FALSE, При этом дескриптор имеет абсолютный формат. Что это значит? Дескриптор в абсолютном (absolute) формате содержит лишь указатели на информацию, связанную с защитой объекта. В отличие от этого дескриптор в относительном (self-relative) формате включает в себя всю необходимую информацию, которая располагается в памяти последовательно поле за полем. Таким образом, абсолютный дескриптор нельзя записать на диск (так как при последующем чтении его с диска все указатели потеряют смысл), а относительный дескриптор — можно.

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

Преобразование осуществляется при помощи вызовов MakeSelfRelativeSD и MakeAbsoluteSD. Преобразовать абсолютную форму в относительную несложно. Однако обратное преобразование (из относительной в абсолютную) обычно выполняется в несколько этапов.

Функции SetSecurityDescriptorOwner и SetSecurityDescriptorGroup связывают SID с дескрипторами безопасности. Списки ACL инициализируются с помощью функции InitializeAcl и затем связываются с дескриптором безопасности функциями SetSecurityDescriptorDacl или SetSecurityDescriptorSacl.

Списки контроля доступа

Каждый ACL представляет собой набор (список) элементов управления доступом (access control entryACE). Существует два типа элементов управления доступом: для разрешения и для запрещения доступа.

Сначала ACL инициализируется функцией InitializeAcl, а затем в него добавляются элементы управления доступом. Каждый АСЕ содержит идентификатор SID и маску доступа, которая определяет предоставляемые или запрещаемые права. Типичные права доступа — GENERIC_READ и DELETE.

Для добавления элементов управления доступом в разграничительные ACL служат две функции: AddAccessAllowedAce и AddAccessDeniedAce. Первая из этих функций предназначена для добавления к SACL, что вызывает аудит доступа к указанному SID.

И наконец, для удаления элементов управления доступом служит функция DeleteAce, а для обращения к ним — GetAce.

Использование безопасности объектов Win32

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

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

Если идентификаторы пользователя и группы не разрешают доступа, ядро системы ищет права доступа в ACL. Первый элемент, который определенно предоставляет или запрещает требуемую службу, имеет решающее значение. Поэтому важен порядок, в котором элементы управления доступом вводятся в ACL. Часто АСЕ, запрещающие доступ, помещаются в начало списка, чтобы пользователь, определенно лишенный доступа, не получил его на основании членства в группе, имеющей такой доступ. Но можно смешивать элементы разрешения и запрещения, чтобы получить желательную семантику. АСЕ, запрещающий все права, может быть последним в списке; это гарантирует, что всем пользователям, явно не упомянутым в АСЕ, доступ будет запрещен.

Права объекта и доступ к объекту

Объект, например, файл, получает свои права при создании, хотя они могут быть изменены позже. Процесс запрашивает доступ к объекту в тех случаях, когда он хочет использовать дескриптор, например, при обращении к CreateFile. Запрос дескриптора содержит в одном из своих параметров необходимый доступ, например GENERIC_READ. Если процесс имеет права на получение требуемого доступа, запрос проходит успешно. Разные дескрипторы одного и того же объекта могут иметь различный доступ. Для флагов доступа применяются те же значения, которые используются для предоставления или запрещения прав при создании ACL.

Инициализация описателя безопасности

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

Параметру dwRevision присваивается значение константы SECURITY_DES-CRIPTOR_REVISION.

BOOL InitializeSecurityDescriptor (PSECURITY_DESCRIPTOR psd, DWORD dwRevision)

Идентификаторы безопасности

Идентификаторы SID применяются в Win32 для опознания пользователей и групп. Программа может искать SID по имени учетной записи, которая соответствует пользо­вателю, группе, домену и т.д. Учетная запись может находиться в удаленной системе.

BOOL LookupAccountName (LPCTSTR lpszSystem, LPCTSTR lpszAccount, PSID psid, LPDWORD lpcbSid, LPTSTR lpszReferencedDomain, LPDWORD lpcchReferencedDomain, PSID_NAME_USE psnu)

 

Основная литература: [1] – 112 - 131 c.

Контрольные вопросы

1.     Какую информацию включают атрибуты безопасности?

2.     Какие элементы включает дескриптор безопасности?

3.     Назначение списков контроля доступа.

 

14 Лекция. Структурная обработка исключений

 

Структурная обработка исключительных ситуаций (Structured Exception HandlingSEH) в Win32 представляет собой надежный механизм, позволяющий приложениям отвечать на неожиданные события, такие как исключительные ситуации при адресации, сбои при выполнении арифметических операций и системные ошибки. Кроме того, SEH делает возможным завершение программы из любой точки в блоке кода, а также автоматически выполняет указанные программистом действия и восстановле­ние при ошибках.

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

Исключения и их обработчики

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

Блоки Try и Except

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

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

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

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

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

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

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

В примерах как этой главы, так и всей книги для установления контроля за блоком кода создаются следующие блоки try и except:

try   {  /*  Блок контролируемого  кода  */ }

except   {выражение_фильтра) { /*  Блок обработки исключения  */  }

Заметьте, что try и except — ключевые слова, распознаваемые компилятором.

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

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

Выражения фильтра и их значения

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

1) EXCEPTION_EXECUTE_HANDLER система выполняет блок except. Это обычная ситуация.

2) EXCEPTION_CONTINUE_SEARCH — система игнорирует обработчик исключения последовательно ищет его во вложенных блоках, пока не находит.

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

Коды исключений

Блок except или выражение фильтра может определить конкретное исключение с помощью функции

DWORD GetExceptionCode (VOID)

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

except (MyFilter (GetExceptionCode ())) { }

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

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

Программные нарушения, например:

- EXCEPTION_ACCESS_VIOLATION — попытка читать или записывать по виртуальному адресу, к которому процесс не имеет доступа;

- EXCEPTION_DATATYPE_MISALIGNMENT — многие типы процессоров требуют, например, чтобы данные типа DWORD были выровнены по 4-байтовым границам;

- EXCEPTION_NONCONTINUABLE — значение выражения фильтра было exception_continue_execution, но после данного исключения продолжение невозможно.

- Исключения, вызываемые функциями  распределения  памяти, HeapAlloc  и HeapCreate, если в них используется флаг HEAP_GENERATE_EXCEPTIONS. Значение  кода будет STATUS_NO_MEMORY либо EXCEPTION_ACCESS_VIOLATION.

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

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

EXCEPTION_INT_DIVIDE_BY_ZERO и EXCEPTION_FLT_OVERFLOW

- Исключения, используемые отладчиками, например EXCEPTION_BREAKPOINT и EXCEPTION_SINGLE_STEP.

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

LPEXCEPTION_POINTERS GetExceptionInformation (VOID)

Структура EXCEPTION_POINTERS содержит как зависимую от процессора, так и независимую информацию, сгруппированную в двух других структурах:

typedef struct _EXCEPTION_POINTERS {PEXCEPTION_RECORD ExceptionRecord; PCONTEXT ContextRecord;} EXCEPTION_POINTERS;

EXCEPTION_RECORD содержит элемент для ExceptionCode, с тем же набором значений, которые возвращаются функцией GetExceptionCode. Еще один элемент EXCEPTION_RECORDExceptionFlags — имеет значение 0 или EXCEPTION_NONCONTINUABLE, что позволяет функции фильтра определить, следует ли пытаться продолжать выполнение. Другие элементы включают адрес виртуальной памяти ExceptionAddress и массив параметров ExceptionInformation. В случае EXCEPTION_ACCESS_VIOLATION первый элемент указывает, было ли это нарушение при записи (1) или при чтении (0), а второй элемент — адрес виртуальной памяти.

Второй элемент EXCEPTION_POINTERSContextRecord — содержит зависимую от процессора информацию. Есть разные структуры для каждого типа процессора; информацию об этом можно найти в <winnt.h>.

 

Основная литература: [1] – 90 - 111 c.

Контрольные вопросы:

1.     Что представляет собой структурная обработка исключений?

2.     Структура блоков Try и Except.

3.     Назначение обработчиков завершения.

 

15 Лекция. Межпроцессное взаимодействие

 

Двумя основными механизмами Win32 для обеспечения межпроцессного взаимодействия (interprocess communication, IPC) являются анонимный канал и именованный канал.

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

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

Анонимные каналы Win32 позволяют проводить одностороннее (полудуплексное), символьно-ориентированное межпроцессное взаимодействие. Каждый канал имеет два дескриптора: дескриптор чтения и дескриптор записи. Для создания канала служит следующая функция CreatePipe:

BOOL CreatePipe (PHANDLE phRead, PHANDLE phWrite, LPSECURITY_ATTRIBUTES lpsa, DWORD cbPipe)

Чтобы использовать канал для IPC, нужен еще один процесс, с которым будет свя­зан один из дескрипторов канала.

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

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

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

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

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

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

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

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

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

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

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

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

Создание именованных каналов

Ниже приведена функция CreateNamedPipe:

HANDLE CreateNamedPipe (LPCTSTR lpszPipeName, DWQRD fdwOpenMode, DWORD fdwPipeMode, DWORD nMaxInstances, DWORD cbOutBuf, DWORD cblnBuf, DWORD dwTimeOut, LPSECURITY_ATTRIBUTES lpsa)

Параметры

lpszPipeName обозначает имя канала в форме: \\.\pipe\[путь]имя-канала.

Точка обозначает локальную машину; иначе говоря, создать канал на удаленной машине нельзя.

fdwOpenMode имеет одно из следующих значений:

- PIPE_ACCESS_DUPLEX — эквивалентно комбинации GENERIC_READ и GENERIC_WRITE;

- PIPE_ACCESS_INBOUND — направление данных только от клиента к серверу, эквивалентно GENERIC_READ;

- PIPE_ACCESS_OUTBOUND — эквивалентно GENERIC_WRITE.

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

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

- PIPE_READMODE_BYTE и PIPE_READMODE_MESSAGE указывают, читаются ли данные как поток байтов или как поток сообщений. Для PIPE_READMODE_MESSAGE необходимо PIPE_TYPE_MESSAGE.

- PIPE_WAIT и PIPE_NOWAIT определяют, будет ли операция ReadFile блокироваться. Указывайте значение PIPE_WAIT, поскольку для асинхронного ввода-вывода есть способы получше.

nMaxInstances определяет количество экземпляров канала и, следовательно, ко­личество одновременно обслуживаемых клиентов. Если указать значение PIPE_UNLIMITED_ INSTANCES, количество каналов будет определять ОС в зависимости от доступных системных ресурсов.

cbOutBuf и cbInBuf задают размеры в байтах буферов ввода и вывода, используе­мых для именованных каналов. Если указать нуль, будут использоваться значения по умолчанию.

dwTimeOut — принятый по умолчанию тайм-аут (в миллисекундах) для функции WaitNamedPipe. Эта ситуация, в которой функция создания определяет тайм-аут для другой функции, является уникальной.

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

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

Подключение клиента к именованному каналу

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

\\.\pipe\[путь]имя_канала

Если бы сервер находился на другой машине, имя выглядело бы так:

\\имя_сервера\pipe\[путь]имя_канала

Если сервер локальный, то применение для имени символа "." вместо указания имени самой машины значительно повышает быстродействие.

Функции состояния именованного канала

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

SetNamedPipeHandleState позволяет программе задавать эти атрибуты состояния.

GetNamedPipelnfo определяет, связан ли дескриптор с клиентом или сервером, каков размер буфера и т.д.

Функции подключения именованного канала

После создания экземпляра именованного канала сервер может отслеживать подключение клиента (с помощью функций CreateFile или CallNamedPipe), используя функцию ConnectNamedPipe. BOOL ConnectNamedPipe (HANDLE hNamedPipe, LPOVERLAPPED lpo).

Если задать в lpo значение NULL, выполнение ConnectNamedPipe завершится сразу же, как только появится подключение клиента. Обычно функция возвращает TRUE. Значение FALSE может быть, если клиент подключается между вызовами сервером функций CreateNamedPipe и ConnectNamedPipe. В этом случае GetLastError воз­вращает значение ERROR_PIPE_CONNECTED.

После возвращения из ConnectNamedPipe сервер может читать запросы с помощью ReadFile и записывать ответы, используя WriteFile. В заключение сервер должен вызвать функцию DisconnectNamedPipe, чтобы освободить дескриптор (экземпляр канала) для соединения с другим клиентом.

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

Соединение клиента и сервера через именованный канал

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

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

Обратите внимание, что между клиентом и сервером происходит состязание. Во-первых, вызов клиентом WaitNamedPipe завершится неудачно, если сервер еще не создал именованный канал. Во-вторых, возможны обстоятельства, в которых клиент может завершить свой вызов CreateFile прежде, чем сервер вызовет ConnectNamedPipe. В этом случае ConnectNamedPipe возвратит FALSE, но связь через именован­ный канал будет работать, как положено.

Транзакционные функции именованного канала

Клиент выполняет следующее:

- открывает экземпляр канала, создавая долговременное подключение к серверу и потребляя этот экземпляр канала;

- попеременно посылает запросы и ожидает ответов;

- закрывает подключение.

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

BOOL TransactNamedPipe (HANDLE hNamedPipe, LPVOID lpvWriteBuf, DWORD cbWriteBuf, LPVOID  lpvReadBuf, DWORD cbReadBuf, LPDWORD lpcbRead, LPOVERLAPPED lpa)

Назначение параметров не нуждается в объяснении, так как эта функция представляет собой сочетание WriteFile и ReadFile для дескриптора именованного канала. Указываются как выходной, так и входной буферы, а * lpcbRead задает длину сообщения.

Функция TransactNamedPipe удобна, но требует посто­янного подключения, что ограничивает число клиентов.

CallNamedPipe, вторая функция-полуфабрикат, лишена этого недостатка, так как объединяет в себе всю последовательность: CreateFile, WriteFile, ReadFile, CloseHandle.

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

BOOL CallNamedPipe (LPCPSTR lpszPipeName, LPVOID lpvWriteBuf, DWORD сbWriteBuf, LPVOID lpvReadBuf, DWORD сbReadBuf, LPDWORD lpcbRead, DWORD dwTimeOut)

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

Здесь также указывается тайм-аут в миллисекундах — для подключения, но не для транзакции. Для dwTimeOut существует также три специальных значения:

- NMPWAIT_NOWAIT;

- NMPWAIT_WAIT_FOREVER;

- NMPWAIT_USE_DEFAULT_WAIT.

В этом случае используется тайм-аут по умолчанию, заданный при вызове CreateNamedPipe.

 

Основная литература: [1] – 286-306 c.

Контрольные вопросы

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

2.     Как клиенты и серверы используют именованные каналы?

3.     Назначение почтовых ячеек?

Список литературы 

1.     Джеффри Рихтер. Windows. Создание эффективных Win32- приложений с учетом специфики 64-разрядной версии Windows.-СПб., М., Харьков, Минск: “Русская редакция”, “Питер”, 2008 (Серия: для профессионалов).

2.     Ал Вильямс. Системное программирование в Windows. – СПб.: Питер, 2007.

3.     Джонсон М. Харт. Системное программирование в среде Win32. – М.: Издательский дом “Вильямс”, 2007.

4.     Румянцев П.В. Азбука программирования в Win32 API. – М.: Горячая линия – Телеком, 2006.

5.     Румянцев П.В. Работа с файлами в Win32. – М.: Горячая линия – телеком, 2005.

6.     Ганеев Р.М. Проектирование интерфейса пользователя средствами Win32 API. – М.: Горячая линия – Телеком, 2004.

 

Содержание

 

1 Лекция. Введение. Вопросы системного программирования

3

2 Лекция. Управление процессами

6

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

8

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

10

5 Лекция. Дополнительные методы синхронизации потоков

14

6 Лекция. Управление памятью. Использование виртуальной памяти

16

7 Лекция. Динамически распределяемая память

17

8 Лекция. Отображаемые в память файлы

21

9 Лекция. Динамические библиотеки

24

10 Лекция. Использование файловой системы

28

11 Лекция. Дополнительные методы работы с файлами и каталогами. Реестр

32

12 Лекция. Использование ввода-вывода

35

13 Лекция. Безопасность объектов Win32       

39

14 Лекция. Структурная обработка исключений

43

15 Лекция. Межпроцессное взаимодействие

46

Список литературы

51

 

 Сводный план 2011 г. поз. 383