Министерство образования и науки Республики Казахстан Таразский государственный университет им М.Х. Дулати Г.С.Алтыбаев, А.Н.Иманбекова СИСТЕМНОЕ ПРОГРАММИРОВАНИЕ В ОС WINDOWS Учебно-методическое пособие Издательство «Тараз университеті» Тараз, 2016 Алтыбаев Г.С., Иманбекова А.Н. Системное программирование в ОС Windows: Учебно-методическое пособие. – Тараз: Тараз университетi, 2016. – 143 стр. Рецензенты: Аязбаев Т.Л. – Мамаев Ш.М. – Сембина Г.К. – к.ф.-м.н., доцент кафедры «Математика и вычислительная техника» Таразского инновационногуманитарного университета; д.ф.-м.н., заведующий кафедрой «Математика и вычислительная техника» Таразского инновационногуманитарного университета; к.т.н., доцент, заведующая кафедрой «Информационные системы» Таразского государственного университета им. М.Х. Дулати. В пособии рассмотрены вопросы использования интерфейса программирования приложений (API) Win32 для разработки системных программ, предназначенных для управления и взаимодействия процессов и потоков, управления файловой системой, организации обмена между процессами и потоками асинхронным вводом-выводом. В пособии приводятся примеры программирования процесса включения функции в динамическую библиотеку и вызова этой функции из динамической библиотеки. © Алтыбаев Г.С., Иманбекова А.Н. © ТарГУ им. М.Х. Дулати, 2016 Содержание ВВЕДЕНИЕ Лабораторная работа №1. Создание окон с помощью Win32 API функций Лабораторная работа №2. Процессы и их создание в Win32 API Лабораторная работа №3. Создание потоков в Win32 API Лабораторная работа №4. Синхронизация потоков при помощи семафоров и критических секций Лабораторная работа №5. Синхронизация процессов при помощи событий и мьютексов Лабораторная работа №6. Обмен данными по анонимному каналу с сервером Лабораторная работа №7. Обмен данными по именованному каналу с сервером Лабораторная работа №8. Работа с файлами с помощью Win32 API функций Лабораторная работа №9. Работа с атрибутами файлов с помощью Win32 API функций Лабораторная работа №10. Работа с каталогами с помощью Win32 API функций Лабораторная работа №11. Использование динамических библиотек для создания приложений Список рекомендованной литературы 4 6 19 26 32 52 73 94 112 120 127 133 143 3 ВВЕДЕНИЕ Учебно-методическое пособие представляет собой лабораторный практикум программирования в среде Microsoft Visual Studio 2010 с использованием библиотеки Win32 API (далее Win32) – набора базовых функций интерфейсов программирования приложений (API – Application Programming Interface), работающих под управлением операционных систем (ОС) семейства Windows. Лабораторный практикум состоит из 11 лабораторных работ, охватывающих основные разделы программирования в среде Win32. В нем представлены примеры системного программирования приложений, предназначенных для управления файловой системой, управления и взаимодействия процессов и потоков, асинхронным вводом-выводом и т.д. В каждой лабораторной работе приводится теоретический раздел, в котором дается подробное описание используемых функций Win32, параметров функций и их возможные значения, результат выполнения данной функции и правила ее использования в программе. Поскольку программной средой выполнения лабораторной работы является MS Visual Studio 2010, то в лабораторных работах, которые посвящены созданию отдельных видов приложений, описывается процесс создания проекта, приведены исходные коды основных файлов проекта и технология компиляции файла программы. Использование функций Win32 для выполнения лабораторной работы приводится в программе, написанной на Visual Studio 2010. Лабораторная работа состоит из следующих этапов: 1) домашняя подготовка; 2) выполнение работы на компьютере в соответствии с заданием; 3) сдача выполненной работы преподавателю на персональном компьютере; 4) распечатка результатов работы на принтере; 5) оформление отчета; 5) защита лабораторной работы. В процессе домашней подготовки студент изучает лекционный материал, материалы по темам данного пособия и дополнительной литературы, знакомится с заданием на выполнение лабораторной работы и готовит отчет по выполнению лабораторной работы. Выполнение лабораторной работы сводится к созданию исполняемого модуля и запуску программы с соответствующими исходными данными во время занятий в компьютерном классе кафедры «Информационные системы» в присутствии преподавателя. В процессе выполнения лабораторной работы студент последовательно выполняет задание, а по завершению работы – демонстрирует преподавателю результаты. После приема преподавателем лабораторной работы на ПК студент сохраняет результаты лабораторной работы внешнем носителе, готовит отчет по работе и распечатывает результаты на подготовленных листах формата А4. Отчет по каждой лабораторной работе должен содержать: − название работы; 4 − цель лабораторной работы; − задание на выполнение лабораторной работы; − алгоритмы программ; После выполнения лабораторной работы производится защита, на которой студенты должны пояснить технологию создания проекта задания, выбор основных параметров проекта, использование функций Win32, процесс создания Windows-приложения и ответить на контрольные вопросы, приведенные в задании. Использование данного пособия поможет студентам освоить процесс использования функций Win32, программирование интерфейса и Windows приложений под управлением ОС семейства Windows NT. 5 Лабораторная работа №1 Создание окон с помощью Win32 API функций Цель работы: 1. Изучение принципов организации и функционирования стандартных приложений Windows. 2. Изучение структуры приложений, использующих функции Win32API. 3. Создание каркаса оконного приложения Win32 и исследование его структуры. Краткое теоретическое введение Стиль программирования Windows-приложений принципиально отличается от того, который сложился в операционных системах раннего поколения. В MS-DOS программа монопольно владеет всеми ресурсами системы и является инициатором взаимодействия с операционной системой. Совсем иначе дело обстоит в операционной системе Windows, которая строилась как многозадачная, и именно операционная система является инициатором обращения к программе. Все ресурсы Windows являются разделяемыми, и программа, в дальнейшем будем называть ее приложением, не может владеть ими монопольно. В связи с такой идеологией построения операционной системы приложение должно ждать посылки сообщения операционной системы и лишь после его получения выполнить определенные действия, затем вновь перейти в режим ожидания очередного сообщения. На рисунке 1 схематично изображена диаграмма типичного Windows-приложения. Windows генерирует множество различных сообщений, которые направляются приложению, например, щелчок кнопки мыши или нажатие клавиши на клавиатуре. Если приложение не обрабатывает какие-то сообщения, реакция на них осуществляется операционной системой стандартным способом, так что задачей программиста является обработка лишь тех сообщений, которые необходимы приложению. Головная функция WinMain() { ... RegisterClass(&wc); ... CreateWindow(); ... ShowWindow(); ... Цикл обработки сообщений while(); { OS Windows } return 0; } Рисунок 1. Структура приложения Windows 6 Оконная функция WndProc() { ... } Разработчиками операционной системы Windows была создана библиотека функций, при помощи которых и происходит взаимодействие приложения с операционной системой, так называемые функции Программного интерфейса приложений (Application Program Interface, API). Подмножество этих функций, предназначенных для графического вывода на дисплей, графопостроитель и принтер, представляет собой Интерфейс графических устройств (Graphics Device Interface, GDI). Библиотека API-функций разрабатывалась в расчете на то, что ее можно использовать для любого языка программирования, а поскольку разные языки имеют различные типы данных, то были созданы собственные Windows-типы, которые приводятся к типам данных языков программирования. Отметим только, что в Windows нет логического типа bool, но есть Windows-тип BOOL, который эквивалентен целому типу int. Будем рассматривать типы данных Windows по мере необходимости. Еще одной особенностью API-функций является использование обратного, по отношению к принятому в языке С, порядка передачи параметров, как это реализовано в языке Pascal. В С для идентификации таких функций использовалось служебное слово pascal, в Windows введены его синонимы CALLBACK, APIENTRY или WINAPI. По умолчанию С-функции передают параметры, начиная с конца списка так, что первый параметр всегда находится на вершине стека. Именно это позволяет использовать в языке С функции с переменным числом параметров, что в API-функциях невозможно. Каркас Windows-приложения В отличие от программы, выполняемой в операционной системе MS-DOS, даже для создания простейшего приложения под Windows придется проделать намного больше работы. Чтобы иметь возможность работать с оконным интерфейсом, заготовка или каркас Windows-приложения должна выполнить некоторые стандартные действия: 1. Определить класс окна. 2. Зарегистрировать окно. 3. Создать окно данного класса. 4. Отобразить окно. 5. Запустить цикл обработки сообщений. Термин интерфейс здесь следует понимать как способ взаимодействия пользователя и приложения. Класс окна – структура, определяющая его свойства. С помощью листинга 1 рассмотрим "каркас" Windows-приложения. Листинг 1. Минимальный код каркаса Windows-приложения #include <windows.h> #include <tchar.h> LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); TCHAR WinName[] = _T("MainFrame"); int APIENTRY _tWinMain(HINSTANCE This, // Дескриптор текущего приложения HINSTANCE Prev, // В современных системах всегда 0 7 LPTSTR cmd, // Командная строка int mode) // Режим отображения окна { HWND hWnd; // Дескриптор главного окна программы MSG msg; // Структура для хранения сообщения WNDCLASS wc; // Класс окна // Определение класса окна wc.hInstance = This; wc.lpszClassName = WinName; // Имя класса окна wc.lpfnWndProc = WndProc; // Функция окна wc.style = CS_HREDRAW | CS_VREDRAW; // Стиль окна wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); // Стандартная иконка wc.hCursor = LoadCursor(NULL,IDC_ARROW); // Стандартный курсор wc.lpszMenuName = NULL; // Нет меню wc.cbClsExtra = 0; // Нет дополнительных данных класса wc.cbWndExtra = 0; // Нет дополнительных данных окна // Заполнение окна белым цветом wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); if(!RegisterClass(&wc)) return 0; // Регистрация класса окна // Создание окна hWnd = CreateWindow(WinName, // Имя класса окна _T("Каркас Windows-приложения"), // Заголовок окна WS_OVERLAPPEDWINDOW, // Стиль окна CW_USEDEFAULT, // x CW_USEDEFAULT, // y Размеры окна CW_USEDEFAULT, // Width CW_USEDEFAULT, // Height HWND_DESKTOP, // Дескриптор родительского окна NULL, // Нет меню This, // Дескриптор приложения NULL); // Дополнительной информации нет ShowWindow(hWnd, mode); //Показать окно // Цикл обработки сообщений while(GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg);// Функция трансляции кодов нажатой клавиши DispatchMessage(&msg); // Посылает сообщение функции WndProc() } return 0; } // Оконная функция вызывается операционной системой // и получает сообщения из очереди для данного приложения LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { // Обработчик сообщений switch(message) { case WM_DESTROY : PostQuitMessage(0); break; // Завершение программы // Обработка сообщения по умолчанию default : return DefWindowProc(hWnd, message, wParam, lParam); 8 } return 0; } Исследование каркаса Windows-приложения Рассмотрим подробно текст программы. Первая строка содержит файл включений, который обязательно присутствует во всех Windows-программах. #include <windows.h> Если в ранних версиях Visual Studio этот файл содержал основные определения, то сейчас он служит для вызова других файлов включений, основные из которых: windef.h, winbase.h, wingdi.h, winuser.h; а также несколько дополнительных файлов, в которых помещены определения API-функций, констант и макросов. Дополнительно подключим: #include <tchar.h> В этом файле содержатся определения некоторых полезных макросов, например, макрос _Т() служит для создания строки Unicode на этапе компиляции и определен примерно так: #define #ifdef #define #else #define #endif _T(x) T(x) _UNICODE __T(x) L ## x __T(x) x Рисунок 2. Страница общих свойств проекта Макрос преобразуется в оператор "L", который является инструкцией компилятору для образования строки UNICODE, если определена константа UNICODE; и в "пустой оператор", если константа не определена. Константа UNICODE устанавливается в зависимости от установок свойства проекта Character Set (рисунок 2). Диалоговое окно свойств Property Pages доступно сейчас на подложке Property Manager панели управления Solution Explorer. Таким образом, этот макрос позволяет компилировать проект как в кодировке Unicode, так и в Windows-кодировке. Мы подробно рассмотрели 9 данный макрос потому, что многие определения Windows описаны подобным образом. Далее следует прототип оконной функции: LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); Оконная функция также является функцией обратного вызова, что связано с некоторыми особенностями организации вызовов операционной системы. Эта функция регистрируется в системе, а ее вызов осуществляет операционная система, когда требуется обработать сообщение. Тип возвращаемого значения функции LRESULT эквивалентен long для Win32-проекта. На глобальном уровне описывается имя класса окна приложения в виде текстовой строки: TCHAR WinName [] = _T("MainFrame"); Тип TCHAR также преобразуется в wchar_t, если определена константа _UNICODE, и в char, если константа не определена. Тип wchar_t эквивалентен типу short и служит для хранения строк в кодировке Unicode, где для одного символа выделяется 16 бит. Имя класса окна используется операционной системой для его идентификации. Имя может быть произвольным, в частности содержать кириллический текст. Рассмотрим заголовок головной функции: int APIENTRY _tWinMain(HINSTANCE This, // Дескриптор приложения HINSTANCE Prev, // В современных системах всегда 0 LPTSTR cmd, // Командная строка int mode) // Режим отображения окна текущего Для Windows-приложений с Unicode она носит имя wWinMain(), а в 8битной кодировке -WinMain(), выбор варианта определяется префиксом _t, что также является стандартным приемом в библиотеке API-функций. Функция имеет четыре параметра, устанавливаемых при загрузке приложения: − This – дескриптор, присваиваемый операционной системой при загрузке приложения; − Prev – параметр предназначен для хранения дескриптора предыдущего экземпляра приложения, уже загруженного системой. Сейчас он потерял свою актуальность и сохранен лишь для совместимости со старыми приложениями (начиная с Windows 95, параметр устанавливается в нулевое значение); − cmd – указатель командной строки, но без имени запускаемой программы. Тип LPTSTR эквивалентен TCHAR*; − mode – режим отображения окна. Дескриптор (описатель) – тип данных Windows, который используется для описания объектов операционной системы. Дескриптор напоминает индекс хеш-таблицы и позволяет отслеживать состояние объекта в памяти при его перемещении по инициативе операционной системы. Предусмотрено много типов дескрипторов: HINSTANCE, HWND и др., но все они являются 32разрядными целыми числами. Внутри головной функции описаны три переменные: 10 − hWnd — предназначена для хранения дескриптора главного окна программы; − msg — это структура, в которой хранится информация о сообщении, передаваемом операционной системой окну приложения: struct MSG { HWND hWnd; // Дескриптор окна UINT message; // Номер сообщения WPARAM wParam; // 32-разрядные целые содержат LPARAM lParam; // дополнительные параметры сообщения DWORD time; // Время посылки сообщения в миллисекундах POINT pt; // Координаты курсора (x,y) }; struct POINT { LONG x,y; }; Тип WPARAM – "короткий параметр" был предназначен для передачи 16разрядного значения в 16-разрядной операционной системе, в Win32 это такое же 32-разрядное значение, что и LPARAM. – wc – структура, содержащая информацию по настройке окна. Требуется заполнить следующие поля: wc.hlnstance = This; Дескриптор текущего приложения. wc.lpszClassName = WinName; Имя класса окна. • wc.lpfnWndProc = WndProc; Имя оконной функции для обработки сообщений. • wc.style = CS_HREDRAW | CS_VREDRAW; Такой стиль определяет автоматическую перерисовку окна при изменении его ширины или высоты. • wc.hIcon = LoadIcon(NULL,IDI_APPLICATION); Дескриптор пиктограммы (иконки) приложения. Функция LoadIcon() обеспечивает ее загрузку. Если первый параметр NULL, используется системная пиктограмма, которая выбирается по второму параметру из следующего набора: – IDI_APPLICATION — стандартная иконка; – IDI_ASTERISK — звездочка; – IDI_EXCLAMATION — восклицательный знак; – IDI_HAND — ладонь; – IDI_QUESTION — вопросительный знак; – IDI_WINLOGO — логотип Windows; • wc.hCursor = LoadCursor(NULL,IDC_ARROW); Аналогичная функция LoadCursor () обеспечивает загрузку графического образа курсора, где нулевой первый параметр также означает использование системного курсора, вид которого можно выбрать из списка: – IDC_ARROW — стандартный курсор; – IDC_APPSTARTING — стандартный курсор и маленькие песочные часы; – IDC_CROSS — перекрестие; – IDC_IBEAM — текстовый курсор; 11 – IDC_NO — перечеркнутый круг; – IDC_SIZEALL — четырехлепестковая стрелка; – IDC_SIZENESW — двухлепестковая стрелка, северо-восток и юго-запад; – IDC_SIZENWSE — двухлепестковая стрелка, северо-запад и юго-восток; – IDC_SIZENS — двухлепестковая стрелка, север и юг; – IDC_SIZEWE — двухлепестковая стрелка, запад и восток; – IDC_UPARROW — стрелка вверх; – IDC_WAIT — песочные часы; • wc.lpszMenuName = NULL; Ссылка на строку главного меню, при его отсутствии NULL. • wc.cbClsExtra = 0; Дополнительные параметры класса окна. • wc.cbWndExtra = 0; Дополнительные параметры окна. • wc.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); Дескриптор кисти, которая используется для заполнения окна. Стандартная конструкция, создает системную кисть белого цвета WHITE_BRUSH. Требуется явное преобразование типа — HBRUSH. После того как определены основные характеристики окна, можно это окно создать при помощи API-функции CreateWindow(), где также нужно задать параметры: 1. WinName — имя, которое присвоено классу окна. 2. _T("Каркас Windows-приложения") — заголовок окна в виде строки Unicode либо С-строки. 3. WS_OVERLAPPEDWINDOW — макрос, определяющий стиль отображения стандартного окна, имеющего системное меню, заголовок, рамку для изменения размеров, а также кнопки минимизации, развертывания и закрытия. Это наиболее общий стиль окна, он определен так: #define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED|WS_CAPTION|WS_SYSMENU| WS_THICKFRAME|WS_MINIMIZEBOX|WS_MAXIMIZEBOX) Можно создать другой стиль, используя комбинацию стилевых макросов при помощи операции логического сложения, вот некоторые из них: WS_OVERLAPPED — стандартное окно с рамкой; WS_CAPTION — окно с заголовком; WS_THICKFRAME — окно с рамкой; WS_MAXIMIZEBOX — кнопка распахивания окна; WS_MINIMIZEBOX — кнопка минимизации; WS_SYSMENU — системное меню; WS_HSCROLL — горизонтальная панель прокрутки; WS_VSCROLL — вертикальная панель прокрутки; WS_VISIBLE — окно отображается; WS_CHILD — дочернее окно; WS_POPUP — всплывающее окно; 4. Следующие два параметра определяют координаты левого верхнего угла окна (x,y) , еще два параметра: Width — ширину и Height — высоту окна в 12 пикселах. Задание параметра CW_USEDEFAULT означает, что система сама выберет для отображения окна наиболее (с ее точки зрения) удобное место и размер. 5. Следующий параметр — указатель на структуру меню, или NULL, при его отсутствии. 6. Далее требуется указать дескриптор приложения, владельца окна — This. 7. И, наконец, указатель на дополнительную информацию, в нашем случае – NULL. Окно создано, и с ним можно работать, но пока оно не отображается. Для того чтобы окно увидеть, необходимо его отобразить с помощью функции ShowWindow(hWnd, mode), которая принимает два параметра: hWnd – дескриптор окна и mode – режим отображения. В нашем случае используется значение, полученное при открытии приложения через параметр головной функции. Далее, заключительная часть головной функции – цикл обработки сообщений. Он задается оператором while, аргументом которого является функция GetMessage(&msg, NULL, 0, 0). Такой цикл является обязательным для всех Windows-приложений, его цель — получение и обработка сообщений, передаваемых операционной системой. Операционная система ставит сообщения в очередь, откуда они извлекаются функцией GetMessage() по мере готовности приложения: □ первым параметром функции является &msg — указатель на структуру MSG, где и хранятся сообщения; □ второй параметр hWnd — определяет окно, для которого предназначено сообщение, если же необходимо перехватить сообщения всех окон данного приложения, он должен быть NULL; □ остальные два параметра определяют [min, max] диапазон получаемых сообщений. Чаще всего необходимо обработать все сообщения, тогда эти параметры должны быть равны 0. Сообщения определяются их номерами, символические имена для них определены в файле включений winuser.h. Префикс всех системных сообщений WM_. Внутри цикла расположены две функции: TranslateMessage(&msg); DispatchMessage(&msg); Первая из них транслирует код нажатой клавиши в клавиатурные сообщения WM_CHAR. При этом в переменную wParam структуры msg помещается код нажатой клавиши в Windows-кодировке CP-1251, в младшее слово lParam — количество повторений этого сообщения в результате удержания клавиши в нажатом состоянии, а в старшее слово — битовая карта со значениями, приведенными в таблица 1. Использование этой функции не обязательно и нужно только для обработки сообщений от клавиатуры. Вторая функция, DispatchMessage(&msg), обеспечивает возврат преобразованного сообщения обратно операционной системе и инициирует вызов оконной функции данного приложения для его обработки. 13 Данным циклом и заканчивается головная функция. Далее следует описать оконную функцию WndProc(), и построение каркаса Windows-приложения будет закончено. Таблица 1. Битовая карта клавиатуры, HIWORD(lParam) Бит Значение 15 1, если клавиша отпущена, 0 — если нажата 14 1, если клавиша была нажата перед посылкой сообщения 13 1, если нажата клавиша <Alt> 12–9 Резерв 8 1, если нажата функциональная клавиша 7–0 Scan-код клавиши Основной компонент этой функции – переключатель switch, обеспечивающий выбор соответствующего обработчика сообщений по его номеру message. В нашем случае предусмотрена обработка лишь одного сообщения WM_DESTROY. Это сообщение посылается, когда пользователь завершает программу. Получив его, оконная функция вызывает функцию PostQuitMessage(0), которая завершает приложение и передает операционной системе код возврата – 0. Точнее генерируется сообщение WM_QUIT, получив которое функция GetMessage () возвращает нулевое значение. В результате цикл обработки сообщений прекращается и происходит завершение работы приложения. Все остальные сообщения обрабатываются по умолчанию функцией DefWindowProc(), имеющей такой же список параметров и аналогичное возвращаемое значение, поэтому ее вызов помещается после оператора return. Листинг 2. Простое окно сообщений #include <windows.h> // заголовочный файл, содержащий функции API // Основная функция - аналог int main() в консольном приложении: int WINAPI WinMain(HINSTANCE hInstance, // дескриптор экземпляра приложения HINSTANCE hPrevInstance, // в Win32 не используется LPSTR lpCmdLine, // нужен для запуска окна в режиме командной строки int nCmdShow) // режим отображения окна { // Функция вывода окна с кнопкой "ОК" на экран (о параметрах позже) MessageBox(NULL, L"Привет, мир!!!", L"Оконная процедура", MB_OK); return NULL; // возвращаем значение функции } Листинг 3. Сложное окно сообщений #include <windows.h> // содержит API // Основная функция: 14 int WINAPI WinMain (HINSTANCE hInst, // дескриптор экземпляра приложения HINSTANCE hPreviousInst, // в Win32 не используется, но объявление нужно LPSTR lpCommandLine, // нужен для запуска окошка в режиме командной строки int nCommandShow) // режим отображения окна { int result = MessageBox(NULL, L"Вам нравится WINAPI?!", L"Задача", MB_ICONQUESTION | MB_YESNO); switch (result) { case IDYES: MessageBox (NULL, L"Продолжайте в том же духе!!!", L"Ответ", MB_OK| MB_ICONASTERISK); break; case IDNO: MessageBox (NULL, L"Очень жаль!!!", L"Ответ", MB_OK| MB_ICONSTOP); break; } return NULL; } Третьим параметром могут быть записаны следующие идентификаторы: параметры кнопок: MB_ABORTRETRYIGNORE — три кнопки: ABORT, RETRY, IGNORE MB_CANCELTRYCONTINUE — три кнопки: CANCEL, TRY, CONTINUE MB_HELP MB_OK MB_OKCANCEL — 2 кнопки: OK, CANCEL MB_RETRYCANCEL — 2 кнопки: RETRY, CANCEL MB_YESNO — 2 кнопки: YES, NO MB_YESNOCANCEL — три кнопки: YES, NO, CANCEL параметры пиктограммы: MB_ICONSTOP — выводит (крестик) MB_ICONQUESTION — выводит (знак вопроса) MB_ICONEXCLAMATION — выводит восклицательный знак в треугольнике MB_ICONASTERISK — выводит (восклицательный знак) А возвращать значения при нажатии вышеуказанных кнопок функция MessageBox будет такие: IDABORT — при нажатии на ABORT IDCANCEL – …….на кнопку CANSEL IDCONTINUE – ……..на кнопку CONTINUE IDIGNORE – …….на кнопку IGNORE IDNO – …….на кнопку NO IDOK – …….на кнопку OK IDRETRY – …….на кнопку RETRY IDTRYAGAIN – …….на кнопку TRY AGAIN IDYES – …….на кнопку YES 15 Порядок выполнения работы Рассмотрим сначала, как можно "вручную" создать минимальное приложение Win32. Загрузив Visual Studio 2010, выполним команду File | New | Project... и выберем тип проекта — Win32 Project. В раскрывающемся списке Location выберем путь к рабочей папке, а в поле Name имя проекта (рисунок 3). В следующем диалоговом окне, приведенном на рисунке 4, нажимаем кнопку Next, а в окне опций проекта (рисунок 5) выберем флажок Empty project (Пустой проект) и нажмем кнопку Finish — получим пустой проект, в котором нет ни одного файла. Рисунок 3. Выбор типа проекта Рисунок 4. Стартовое окно построителя приложения 16 Рисунок 5. Окно опций проекта С помощью контекстного меню (рисунок 6) добавим файл для кода приложения, имя файла введем в ходе диалога выбора шаблона объекта на рисунке 7. (Тот же самый диалог можно получить по команде меню Project | Add New Item) Рисунок 6. Добавление к проекту нового объекта с помощью контекстного меню Рисунок 7. Выбор шаблона объекта 17 Программа не делает ничего полезного, поэтому, запустив ее на выполнение кнопкой ► (Start Debugging), мы получим изображенное на рисунке 8 пустое окно, имеющее заголовок и набор стандартных кнопок. Рисунок 8. Окно первой Windows-программы Варианты заданий на выполнение 1. После нажатия на левую (правую) клавишу мыши над рабочей областью окна в левом верхнем (правом нижнем) углу области отобразить временное окно размером в четверть области. Временное окно скрыть после отжатия клавиши в любом месте экрана. 2. В рабочей области окна приложения рядом друг с другом расположить 4 временных окна, в заголовках которых указан номер окна. После нажатия левой клавиши мыши временное окно выдает сообщение, содержащее номер окна. 3. Окно размером в четверть площади экрана расположено в центре экрана. После нажатия левой клавиши мыши окно несколько раз меняет подсветку и перемещается в угол экрана так, что курсор мыши оказывается за пределами окна. 4. Дочернее окно размером 100*100 пикселей при перемещении курсора мыши над ним "убегает" от курсора мыши в произвольном направлении, оставаясь в пределах рабочей области родительского окна. 5. В центре рабочей области окна расположено окно без заголовка с вертикальной и горизонтальной полосами просмотра размером в четверть рабочей области. При нажатии разных клавиш мыши временное окно выдает разный звуковой сигнал Контрольные вопросы 1. Что такое Win32 API? 2. Какие операционные системы обслуживает API Win32? 3.Какие особенности имеет Win32 API? 4. Какие преимущества программирования дает Win32 API? 5. Какой основной тип переменных используется в Win32? 6. Для управления каких систем могут быть написаны программы с использованием Win32? 18 Лабораторная работа №2 Тема: Процессы и их создание в Win32 API Цель работы: 1. Изучение основных функций Win32API, используемых для управления процессами 2. Разработка простейшей программы, демонстрирующей создание и завершение процесса. 3. Разработка приложения Win32 API, реализующего функции указанные в варианте. Краткое теоретическое введение 1. Процессы в Windows 1.1. Создание процесса В Windows под процессом понимается объект ядра, которому принадлежат системные ресурсы, используемые приложением. Поэтому можно сказать, что в Windows процессом является приложение. Выполнение каждого процесса начинается с первичного потока. В процессе своего исполнения процесс может создавать другие потоки. Исполнение процесса заканчивается при завершении работы всех его потоков. Процесс может быть также завершен вызовом функций ExitProcess и TerminateProcess, которые будут рассмотрены в следующем параграфе. Новый процесс в Windows создается вызовом функции CreateProcess, которая имеет следующий прототип: BOOL CreateProcess( LPCTSTR lpApplicationName,// имя исполняемого модуля LPTSTR lpCommandLine, // командная строка LPSECURITY_ATTRIBUTES lpProcessAttributes,//атрибуты защиты процесса LPSECURITY_ATTRIBUTES lpThreadAttributes,//атрибуты защиты потока BOOL bInheritHandle, // наследуемый ли дескриптор DWORD dwCreationFlags, // флаги создания процесса LPVOID lpEnvironment, // блок новой среды окружения LPCTSTR lpCurrentDirectory, // текущий каталог LPSTARTUPINFO lpStartUpInfo, // вид главного окна LPPROCES S_INFORMATION lpProcessInformation // информация о процессе ); Функция CreateProcess возвращает значение TRUE, если процесс был создан успешно. В противном случае эта функция возвращает значение FALSE. Процесс, который создает новый процесс, называется родительским процессом (parent process) по отношению к создаваемому процессу. Новый же процесс, который создается другим процессом, называется дочерним процессом (child process) по отношению к процессу родителю. Сейчас мы опишем только назначение некоторых параметров функции CreateProcess. Остальные параметры этой функции будут описываться по мере 19 их использования. Первый параметр lpApplicationName определяет строку с именем exe-файла, который будет запускаться при создании нового процесса. Эта строка должна заканчиваться нулем и содержать полный путь к запускаемому файлу. Для примера рассмотрим следующую программу, которая выводит на консоль свое имя и параметры. Листинг 1. Консольный процесс, который выводит на консоль свое имя и параметры #include <conio.h> int main(int argc, char *argv[]) { int i; _cputs("I am created."); _cputs("\nMy name is: "); _cputs(argv[0]); for (i = 1; i < argc; i++) _cprintf ("\n My %d parameter = %s", i, argv[i]); _cputs("\nPress any key to finish.\n"); _getch(); return 0; } Создадим из этой программы exe-файл, который расположим на диске C и назовем ConsoleProcess.exe. Тогда этот exe-файл может быть запущен из другого приложения следующим образом. Листинг 2. Пример консольного процесса, который создает другое консольное приложение с новой консолью и ждет завершения работы этого приложения. #include <windows.h> #include <conio.h> int main() { char lpszAppName[] = "C:\\ConsoleProcess.exe"; STARTUPINFO si; PROCESS_INFORMATION piApp; ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); // создаем новый консольный процесс if (!CreateProcess(lpszAppName, NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &piApp)) { _cputs("The new process is not created.\n"); _cputs("Check a name of the process.\n"); _cputs("Press any key to finish. \n"); _getch(); return 0; } _cputs("The new process is created.\n"); //ждем завершения созданного процесса WaitForSingleObject(piApp.hProcess, INFINITE); // закрываем дескрипторы этого процесса в текущем процессе CloseHandle(piApp.hThread); CloseHandle(piApp.hProcess); 20 return 0; } Отметим в последней программе два момента. Во-первых, перед запуском консольного процесса ConsoleProcess.exe все поля структуры si типа STARTUPINFO должны заполняться нулями. Это делается при помощи вызова функции ZeroMemory, которая предназначена для этой цели и имеет следующий прототип: VOID ZeroMemory( PVOID Destination, SIZE_T Length ); // адрес блока памяти // длина блока памяти В этом случае вид главного окна запускаемого приложения определяется по умолчанию самой операционной системой Windows. Во-вторых, в параметре dwCreationFlags устанавливается флаг CREATE_NEW_CONSOLE. Это говорит системе о том, что для нового создаваемого процесса должна быть создана новая консоль. Если этот параметр будет равен NULL, то новая консоль для запускаемого процесса не создается и весь консольный вывод нового процесса будет направляться в консоль родительского процесса. Структура piApp типа PROCESS_INFORMATION содержит идентификаторы и дескрипторы нового создаваемого процесса и его главного потока. Мы не используем эти дескрипторы в нашей программе и поэтому закрываем их. Значение FALSE параметра bInheritHandle говорит о том, что эти дескрипторы не являются наследуемыми. О наследовании дескрипторов мы поговорим подробнее в одном из следующих параграфов этой главы. Теперь запустим наш новый консольный процесс другим способом, используя второй параметр функции CreateProcess. Это можно сделать при помощи следующей программы. Листинг 3. Пример процесса, который создает новое консольное приложение с новой консолью #include <windows.h> #include <conio.h> int main() { char lpszCommandLine[] = "C:\\01-1-ConsoleProcess.exe p1 p2 p3"; STARTUPINFO si; PROCESS_INFORMATION piCom; ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); // создаем новый консольный процесс CreateProcess(NULL, lpszCommandLine, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &piCom); // закрываем дескрипторы этого процесса CloseHandle(piCom. hThread); CloseHandle(piCom.hProcess); _cputs("The new process is created.\n"); _cputs("Press any key to finish.\n"); _getch(); return 0; 21 } Отличие этой программы от предыдущей состоит в том, что мы передаем системе имя нового процесса и его параметры через командную строку. В этом случае имя нового процесса может и не содержать полный путь к exe-файлу, а только имя самого exe-файла. При использовании параметра lpCommandLine система для запуска нового процесса осуществляет поиск требуемого exe-файла в следующей последовательности каталогов: – каталог из которого запущено приложение; – текущий каталог родительского процесса; – системный каталог Windows; – каталог Windows; – каталоги, которые перечислены в переменной PATH среды окружения. Для иллюстрации сказанного запустим приложение Notepad.exe, используя командную строку. Программа, запускающая блокнот из командной строки, выглядит следующим образом. Листинг 4. Пример запуска процесса Notepad #include <windows.h> #include <iostream> using namespace std; int main() { STARTUPINFO si; PROCESS_INFORMATION pi; // заполняем значения структуры STARTUPINFO по умолчанию ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); // запускаем процесс Notepad if (!CreateProcess( NULL, // имя не задаем "Notepad.exe", // командная строка, первая лексема указывает имя программы NULL, // атрибуты защиты процесса устанавливаем по умолчанию NULL, // атрибуты защиты первичного потока по умолчанию FALSE, // дескрипторы текущего процесса не наследуются новым процессом 0, // по умолчанию NORMAL_PRIORITY_CLASS NULL, // используем среду окружения вызывающего процесса NULL, // текущий диск и каталог, как и в вызывающем процессе &si, // вид главного окна - по умолчанию &pi // здесь будут дескрипторы и идентификаторы // нового процесса и его первичного потока ) ) { cout << "The mew process is not created." << endl << "Check a name of the process." << endl; return 0; } 4 Sleep(1000); // немного подождем и закончим свою работу // закроем дескрипторы запущенного процесса в текущем процессе CloseHandle(pi.hThread); CloseHandle(pi.hProcess); 22 return 0; } 1.2. Завершение процессов Процесс может завершить свою работу вызовом функции ExitProcess, которая имеет следующий прототип: VOID ExitProcess( UINT uExitCode ); // код возврата для всех потоков При вызове функции ExitProcess завершаются все потоки процесса с кодом возврата, который является параметром этой функции. Приведем пример программы, которая завершает свою работу вызовом функции ExitProcess. Листинг 5. Пример завершения процесса функцией ExitProcess #include <windows.h> #include <iostream> using namespace std; volatile UINT count; volatile char c; void thread() { for ( ; ; ) { count++; Sleep(100); } } int main() { HANDLE hThread; DWORD IDThread; hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)thread, NULL, 0, &IDThread); if (hThread == NULL) return GetLastError(); for ( ; ; ) { cout << "Input 'y' to display the count or 'e' to exit: "; cin >> (char)c; if (c == 'y') cout << "count = " << count << endl; if (c == 'e') ExitProcess(l); } } Один процесс может завершить другой процесс при помощи вызова функции TerminateProcess, которая имеет следующий прототитп: BOOL TerminateProcess( HANDLE hProcess, UINT uExitCode ); Если функция TerminateProcess выполнилась успешно, то она возвращает значение равно TRUE. В противном случае возвращаемое значение равно FALSE. Функция TerminateProcess завершает работу процесса, но не освобождает все ресурсы, принадлежащие этому процессу. Поэтому эта функция должна вызываться только в аварийных ситуациях при зависании 23 процесса. Приведем программу, которая демонстрируют работу функции TerminateProcess. Для этого сначала создадим бесконечный процесс-счетчик, который назовем ConsoleProcess.exe и расположим на диске C. Листинг 6. Пример бесконечного процесса #include <windows.h> #include <iostream> using namespace std; int count; void main() { for ( ; ; ) { count++; Sleep(1000); cout << "count = " << count << endl; } } Ниже приведена программа, которая создает этот процесс, а потом завершает его по требованию пользователя. Листинг 7. Пример процесса, который создает другое консольное приложение с новой консолью, а потом завершает его при помощи функции TerminateProcess #include <windows.h> #include <conio.h> int main() { char lpszAppName[] = "C:\\ConsoleProcess.exe"; STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb=sizeof(STARTUPINFO); // создаем новый консольный процесс if (!CreateProcess(lpszAppName, NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)) { _cputs("The new process is not created.\n"); _cputs("Check a name of the process.\n"); _cputs("Press any key to finish. \n"); _getch(); return 0; } _cputs("The new process is created.\n"); while (true) { char c; _cputs("Input 't' to terminate the new console process: "); c = _getch(); if (c == 't') { _cputs("t\n"); // завершаем новый процесс TerminateProcess(pi.hProcess, 1) ; break; 24 } } // закрываем дескрипторы нового процесса в текущем процессе CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0; } Заданиe на выполнение Написать программы двух консольных процессов Parent и Child, которые выполняют следующие действия. Процесс Parent: 1. Создает бинарный файл, записи которого имеют следующую структуру: struct emp { int num; char name[10]; double grade; }; // номер зачетки // имя студента // средний бал Имя файла и данные о студентах вводятся с консоли. 2. Выводит созданный файл на консоль. 3. Запрашивает с консоли номер зачетки, имя студента и новый средний бал этого студента. 4. Формирует командную строку, которая содержит имя созданного файла и информацию, полученную в пункте 3. 5. Запускает дочерний процесс Child, которому как параметр передается командная строка, сформированная в пункте 4. 6. Ждет завершения работы процесса Child. 7. Выводит откорректированный файл на консоль. 8. Завершает свою работу. Процесс Child: 1. Выводит на консоль информацию, полученную через командную строку. 2. Корректирует в файле, созданном процессом Parent, нужную запись, т.е. устанавливает новый средний бал студента. 3. Завершает свою работу. Для ожидания завершения работы процесса Child использовать функцию: DWORD WaitForSingleObject( HANDLE hHandle, // дескриптор объекта DWORD dwMilliseconds // интервал ожидания в миллисекундах ); где второй параметр установить равным INFINITE, например WaitForSingleObject(hProcess, INFINITE); // ждать завершения процесса Здесь hProcess – дескриптор процесса Child. Контрольные вопросы 1. С помощью каких функций можно создать процесс? 2. С помощью каких функций можно удалит процесс? 3. Какую фунцию выполняет CreateProcess? 25 4. Какую фунцию выполняет OpenProcess? 5. Какую фунцию выполняет ExitProcess ? 6. Какую фунцию выполняет WaitForSingleObject? Лабораторная работа №3 Тема: Создание потоков в Win32 API Цель работы: 1. Изучение основных функций Win32 API, используемых для управления потоками. 2. Разработка простейшей программы, демонстрирующей создание и завершение процесса. 3. Разработка приложения Win32 API, реализующую указанные в варианте функции. Краткое теоретическое введение 1. Обзор потоков Потоки позволяют в рамках одной программы решать несколько задач одновременно. С недавних пор операционные системы для персональных компьютеров сделали это возможным. Пользователи действительно могут запускать одновременно более одной задачи. Планируя время центрального процессора ОС распределяют его между потоками, а не между приложениями. Чтобы использовать все преимущества, обеспечиваемые несколькими процессорами в современных операционных системах, программист должен знать, как создавать потоки. В данной лабораторной работе рассматриваются следующие вопросы: − что такое потоки; − разница между потоком и процессом; − преимущества потоков; − функции Win32 для работы с потоками; − реализация многопоточного приложения; Определение потока довольно простое: потоки – это объекты, получающие время процессора. Время процессора выделяется квантами (quantum, time slice). Квант времени — это интервал, имеющийся в распоряжении потока до тех пор. пока время не будет передано в распоряжение другого потока. Обратите внимание, что кванты выделяются не программам или процессам, а порожденным ими потокам. Как минимум, каждый процесс имеет хотя бы один (главный) поток, но современные операционные системы позволяют запустить в рамках процесса несколько потоков. Если задачи приложения можно разделить на различные подмножества: обработка событий, ввод/вывод, связь и др., то потоки могут быть органично встроены в программное решение. Если разработчик может разделить большую задачу на несколько мелких, это только повысит переносимость кода и возможности его многократного использования. Сделав приложение многопоточным, программист получает дополнительные возможности управления им. Например, через управление 26 приоритетами потоков. Если один из них "притормаживает" приложение, занимая слишком много процессорного времени, его приоритет может быть понижен. Другое важное преимущество внедрения потоков — при возрастании "нагрузки" на приложение можно увеличить количество потоков и тем самым снять проблему. Потоки упрощают жизнь тем программистам, которые разрабатывают приложения в архитектуре клиент/сервер. Когда требуется обслуживание нового клиента, сервер может запустить специально для этого отдельный поток. Такие потоки принято называть симметричными потоками (symmetric threads) – они имеют одинаковое предназначение, исполняют один и тот же код и могут разделять одни и те же ресурсы. Более того, приложения, рассчитанные на серьезную нагрузку, могут поддерживать пул (pool) однотипных потоков. Поскольку создание потока требует определенного времени, для ускорения работы желательно заранее иметь нужное число готовых потоков и активизировать их по мере подключения очередного клиента. Асимметричные потоки (asymmetric threads) – это потоки, решающие различные задачи и, как правило, не разделяющие совместные ресурсы. Необходимость в асимметричных потоках возникает: − когда в программе необходимы длительные вычисления, при этом необходимо сохранить нормальную реакцию на ввод; − когда нужно обрабатывать асинхронный ввод/вывод с использованием различных устройств (СОМ-порта, звуковой карты, принтера и т. п.); − когда вы хотите создать несколько окон и одновременно обрабатывать ввод в них. 2. Потоки и процессы Когда мы говорим "программа" (application), то обычно имеем в виду понятие, в терминологии операционной системы обозначаемое как "процесс". Процесс состоит из виртуальной памяти, исполняемого кода, потоков и данных. Процесс может содержать много потоков, но обязательно содержит, по крайней мере, один. Поток, как правило, имеет "в собственности" минимум ресурсов; он зависит от процесса, который и распоряжается виртуальной памятью, кодом, данными, файлами и другими ресурсами ОС. Почему мы используем потоки вместо процессов, хотя, при необходимости, приложение может состоять и из нескольких процессов? Дело в том, что переключение между процессами — значительно более трудоемкая операция, чем переключение между потоками. Другой довод в пользу использования потоков — то, что они специально задуманы для разделения ресурсов; разделить ресурсы между процессами (имеющими раздельное адресное пространство) не так-то просто. 3. Приоритеты потоков Интерфейс Win32 API позволяет программисту управлять распределением времени между потоками; это распространяется и на приложения, написанные на Delphi. Операционная система планирует время процессора в соответствии с 27 приоритетами потоков. Приоритет потока – величина, складывающаяся из двух составных частей: приоритета породившего поток процесса и собственно приоритета потока. Когда поток создается, ему назначается приоритет, соответствующий приоритету породившего его процесса. В свою очередь, процессы могут иметь следующие классы приоритетов. − Real time; − Normal; − High; − Below normal; − Above normal; − Idle. Класс реального времени задает приоритет даже больший, чем у многих процессов операционной системы. Такой приоритет нужен для процессов, обрабатывающих высокоскоростные потоки данных. Если такой процесс не завершится за короткое время, пользователь почувствует, что система перестала откликаться, т. к. даже обработка событий мыши не получит времени процессора. Использование класса High ограничено процессами, которые должны завершаться за короткое время, чтобы не вызвать сбойной ситуации. Пример – процесс, который посылает сигналы внешнему устройству; причем устройство отключается, если не получит своевременный сигнал. Если у вас возникли проблемы с производительностью вашего приложения, было бы неправильно решать их просто за счет повышения его приоритета до high – такой процесс также влияет на всю ОС. Возможно, в этом случае следует модернизировать компьютер. Большинство процессов запускается в рамках класса с нормальным приоритетом. Нормальный приоритет означает, что процесс не требует какоголибо специального внимания со стороны операционной системы. И, наконец, процессы с фоновым приоритетом запускаются лишь в том случае, если в очереди Диспетчера задач нет других процессов. Обычные виды приложений, использующие такой приоритет, – это программы сохранения экрана и системные агенты (system agents). Программисты могут использовать фоновые процессы для организации завершающих операций и реорганизации данных. Примерами могут служить сохранение документа или резервное копирование базы данных. Приоритеты имеют значения от 0 до 31. Процесс, породивший поток, может впоследствии изменить его приоритет; в этой ситуации программист имеет возможность управлять скоростью отклика каждого потока. Нормальная практика для асимметричных потоков – это назначение потоку, обрабатывающему ввод, более высокого приоритета, а всем остальным – более низкого или даже приоритета idle, если этот поток должен выполняться только во время простоя системы. 28 4. Функции работы с потоками Создается поток функцией CreateThread, которая имеет следующий прототип: HANDLE CreateThread ( LPSECURITY ATTRIBUTES lpThreadAttributes,// атрибуты защиты DWORD dwStackSize,// размер стека потока в байтах LPTHREAD_START_ROUTINE lpStartAddress,// адрес исполняемой функции LPVOID lpParameter,// адрес параметра DWORD dwCreationFlags,// флаги создания потока LPDWORD lpThreadId// идентификатор потока ) ; При успешном завершении функция CreateThread возвращает дескриптор созданного потока и его идентификатор, который является уникальным для всей системы. В противном случае эта функция возвращает значение NULL. Кратко опишем назначение параметров функции CreateThread. Параметр lpThreadAttributes устанавливает атрибуты защиты создаваемого потока. До тех пор пока мы не изучим структуру системы безопасности в Windows, то есть раздел Windows NT Access Control из интерфейса программирования приложений Win32 API, мы будем устанавливать значения этого параметра в NULL при вызове почти всех функций ядра Windows. Это означает, что атрибуты защиты потока совпадают с атрибутами защиты создавшего его процесса. О процессах будет подробно рассказано в следующем разделе. Параметр dwStackSize определяет размер стека, который выделяется потоку при запуске. Если этот параметр равен нулю, то потоку выделяется стек, размер которого равен по умолчанию 1 Мб. Это наименьший размер стека, который может быть выделен потоку. Если величина параметра dwStackSize меньше, значения, заданного по умолчанию, то все равно потоку выделяется стек размеров в 1Мб. Операционная система Windows округляет размер стека до одной страницы памяти, который обычно равен 4 Кб. Параметр lpStartAddress указывает на исполняемую потоком функцию. Эта функция должна иметь следующий прототип: DWORD WINAPI ThreadProc (LPVOID lpParameters); Параметр lpParameter является единственным параметром, который будет передан функции потока. Параметр dwCreationFlags определяет, в каком состоянии будет создан поток. Если значение этого параметра равно 0, то функция потока начинает выполняться сразу после создания потока. Если же значение этого параметра равно CREATE_SUSPENDED, то поток создается в подвешенном состоянии. В дальнейшем этот поток можно запустить вызовом функции ResumeThread. Параметр lpThreadId является выходным, то есть его значение устанавливает Windows. Этот параметр должен указывать на переменную, в которую Windows поместит идентификатор потока, который уникален для всей системы и может в дальнейшем использоваться для ссылок на поток. Приведем пример программы, которая использует функцию CreateThread для создания потока, и продемонстрируем способ передачи параметров исполняемой потоком функции. 29 Листинг 1. Пример создания потока функцией CreateThread #include <windows.h> #include <iostream.h> volatile int n; DWORD WINAPI Add(LPVOID iNum) { cout << "Thread is started." << endl; n += (int)iNum; cout << "Thread is finished." << endl; return 0; } int main() { int inc = 10; HANDLE hThread; DWORD IDThread; cout << "n = " << n << endl; hThread = CreateThread(NULL, 0, Add, (void*)inc, 0, &IDThread); if (hThread == NULL) return GetLastError(); // ждем пока поток Add закончит работу WaitForSingleObject(hThread, INFINITE); // закрываем дескриптор потока Add CloseHandle(hThread); cout << "n = " << n << endl; return 0; } Отметим, что в этой программе используется функция WaitForSingleObject, которая ждет завершения потока Add. Задание на выполнение А. Изучить программу для консольного процесса, который состоит из двух потоков: main и worker. Поток main должен выполнить следующие действия: 1. Создать массив целых чисел, размерность и элементы которого вводятся с консоли. 2. Создать поток worker. 3. Найти минимальный и максимальный элементы массива и вывести их на консоль. После каждого сравнения элементов «спать» 7 миллисекунд. 4. Дождаться завершения потока worker. 5. Подсчитать количество элементов в массиве, значение которых больше среднего значения элементов массива, и вывести его на консоль. 6. Завершить работу. Поток worker должен выполнить следующие действия: 1. Найти среднее значение элементов массива. После каждого суммирования элементов «спать» 12 миллисекунд. 2. Завершить свою работу. Для ожидания завершения работы потока worker использовать функцию: DWORD WaitForSingleObject( HANDLE hHandle,// дескриптор объекта DWORD dwMilliseconds // интервал ожидания в миллисекундах 30 ); где второй параметр установить равным INFINITE. Например WaitForSingleObject(hThread, INFINITE); // ждать завершения потока Здесь hThread – дескриптор потока worker. Для засыпания использовать функцию: VOID Sleep( DWORD dwMilliseconds // миллисекунды ); Например, Sleep (12); // спать 12 миллисекунд 3. Модифицировать и отладить программу в соответствии со своим вариантом. Варианты заданий 1) Поток worker должен найти значение факториала элементов массива. 2) Поток worker должен найти значение суммы четных элементов массива. 3) Поток worker должен найти значение количество четных элементов массива. 4) Поток worker должен найти значение количество нечетных элементов массива. 5) Поток worker должен найти значение суммы нечетных элементов массива. 6) Поток worker должен найти значение среднее значение четных элементов массива. 7) Поток worker должен найти значение среднее значение нечетных элементов массива. 8) Поток worker должен найти значение факториала четных элементов массива. 9) Поток worker должен найти значение факториала нечетных элементов массива. 10) Поток worker должен найти значение среднее значение элементов массива, исключая максимальный элемент. 11) Поток worker должен найти значение среднее значение элементов массива, исключая минимальный элемент. 12) Поток worker должен найти значение факториала элементов массива, исключая максимальный элемент. 13) Поток worker должен найти значение факториала элементов массива, исключая минимальный элемент. 14) Поток worker должен найти значение суммы нечетных элементов массива и минимального элемента. 15) Поток worker должен найти значение суммы четных элементов массива и минимального элемента. 16) Поток worker должен найти значение факториала элементов массива. 17) Поток worker должен найти значение суммы четных элементов массива. 18) Поток worker должен найти значение количество четных элементов 31 массива. 19) Поток worker должен найти значение количество нечетных элементов массива. 20) Поток worker должен найти значение суммы нечетных элементов массива. 21) Поток worker должен найти значение среднее значение четных элементов массива. 22) Поток worker должен найти значение среднее значение нечетных элементов массива. 23) Поток worker должен найти значение факториала четных элементов массива. 24) Поток worker должен найти значение факториала нечетных элементов массива. Контрольные вопросы 1. Дайте определение понятию поток. 2. Что такое «симметричные» и «асимметричные» потоки. В каких ситуациях возникает необходимость в асимметричных потоках? 2. Каково различие процессов от потоков? 3. Что такое приоритет потока? 4. Перечислите классы приоритетов для процессов. 5. Каким образом можно добавить новый поток в текущий процесс? Лабораторная работа №4 Тема: Синхронизация потоков при помощи семафоров и критических секций Цель работы: 1. Изучить объекты синхронизации потоков семафор и критические секции 2. В соответствии с заданным вариантом разработать приложение, реализующее синхронизацию потоков с помощью семафоров. 3. В соответствии с заданным вариантом разработать приложение, реализующее синхронизацию потоков с помощью критических секций. Краткое теоретическое введение 1. Критические секции в Windows В операционных системах Windows проблема взаимного исключения для параллельных потоков, выполняемых в контексте одного процесса, решается при помощи объекта типа CRITICAL_SECTION, который не является объектом ядра операционной системы. Для работы с объектами этого типа используются следующие функции: VOID InitializeCriticalSection (LPCRITICAL_SECTION lpCriticalSection); VOID EnterCriticalSection (LPCRITICAL_SECTION lpCriticalSection); BOOL TryEnterCriticalSection (LPCRITICAL_SECTION lpCriticalSection); 32 VOID LeaveCriticalSection (LPCRITICAL_SECTION lpCriticalSection); VOID DeleteCriticalSection (LPCRITICAL_SECTION lpCriticalSection); каждая из которых имеет единственный параметр, указатель на объект типа CRITICAL_SECTION. Все эти функции, за исключением TryEnterCriticalSection, не возвращают значения. Отметим, что функция TryEnterCriticalSection поддерживается только операционной системой Windows 2000. Кратко рассмотрим порядок работы с этими функциями. Для этого предположим, что при проектировании программы мы выделили некоторый разделяемый ресурс и критические секции в параллельных потоках, которые имеют доступ к этому разделяемому ресурсу. Тогда для обеспечения корректной работы с этим ресурсом нужно выполнить следующую последовательность действий: − определить в нашей программе объект типа CRITICAL_SECTION, имя которого логически связано с выделенным разделяемым ресурсом; − проинициализировать объектом типа CRITICAL_SECTION при помощи функции InitializeCriticalSection; − в каждом из параллельных потоков пред входом в критическую секцию вызвать функцию EnterCriticalSection, которая исключает одновременный вход в критические секции, связанные с нашим разделяемым ресурсом, для параллельно выполняющихся потоков; − после завершения работы с разделяемым ресурсом, поток должен покинуть свою критическую секцию, что выполняется посредством вызова функции LeaveCriticalSection; − после окончания работы с объектом типа CRITICAL_SECTION, необходимо освободить все системные ресурсы, которые использовались этим объектом. Для этой цели служит функция DeleteCriticalSection. Теперь покажем работу этих функций на примере. Для этого сначала рассмотрим пример, в котором выполняются не синхронизированные параллельные потоки, а затем синхронизируем их работу, используя критические секции. Листинг 1. Пример работы не синхронизированных потоков #include <windows.h> #include <iostream> using namespace std; DWORD WINAPI thread(LPVOID) { int i,j; for (j = 0; j < 10; j++) { for (i = 0; i < 10; i++) { cout << j << ' '; cout << flush; Sleep(22); } cout << endl; 33 } return 0; } int main() { int i,j; HANDLE hThread; DWORD IDThread; hThread=CreateThread(NULL, 0, thread, NULL, 0, &IDThread); if (hThread == NULL) return GetLastError(); // так как потоки не синхронизированы, // то выводимые строки непредсказуемы for (j = 10; j < 20; j++) { for (i = 0; i < 10; i++) { cout << j << ' '; cout << flush; Sleep(22); } cout << endl; } // ждем, пока поток thread закончит свою работу WaitForSingleObject(hThread, INFINITE); return 0; } В этой программе каждый из потоков main и thread выводит строки одинаковых чисел. Но из-за параллельной работы потоков, каждая выведенная строка может содержать не равные между собой элементы. Наша задача будет заключаться в следующем: нужно так синхронизировать потоки main и thread, чтобы в каждой строке выводились только равные между собой элементы. Следующая программа показывает решение этой задачи с помощью объекта типа CRITICAL_SECTION. Листинг 2. Пример работы синхронизированных потоков #include <windows.h> #include <iostream> using namespace std; CRITICAL_SECTION cs; DWORD WINAPI thread(LPVOID) { int ij; for (j = 0; j < 10; j++) { // входим в критическую секцию EnterCriticalSection (&cs); for (i = 0; i < 10; i++) { cout << j << ' '; cout.flush(); } cout << endl; // выходим из критической секции LeaveCriticalSection(&cs); } return 0; 34 } int main() { int i,j; HANDLE hThread; DWORD IDThread; // инициализируем критическую секцию InitializeCriticalSection(&cs); hThread=CreateThread(NULL, 0, thread, NULL, 0, &IDThread); if (hThread == NULL) return GetLastError(); // потоки синхронизированы, поэтому каждая // строка содержит только одинаковые числа for (j = 10; j < 20; j++) { // входим в критическую секцию EnterCriticalSection(&cs); for (i = 0; i < 10; i++) { cout << j << ' '; cout.flush(); } cout << endl; // выходим из критической секции LeaveCriticalSection(&cs); } // закрываем критическую секцию DeleteCriticalSection(&cs); // ждем, пока поток thread закончит свою работу WaitForSingleObject(hThread, INFINITE); return 0; } Теперь рассмотрим использование функции TryEnterCriticalSection. Для этого просто заменим в приведенной программе вызовы функции EnterCriticalSection на вызовы функции TryEnterCriticalSection и будем отмечать успешные входы потоков в свои критические секции. Еще раз подчеркнем, что функция TryEnterCriticalSection работает только на платформе операционной системы Windows 2000. Листинг 3. Пример работы синхронизированных потоков. // Работает только в Windows 2000. #include <windows.h> #include <iostream> using namespace std; CRITICAL_SECTION cs; DWORD WINAPI thread(LPVOID) { int ij; for (j = 0; j < 10; j++) { // попытка войти в критическую секцию TryEnterCriticalSection (&cs); for (i = 0; i < 10; i++) 35 { cout << j << " "; cout.flush(); } cout << endl; // выход из критической секции LeaveCriticalSection(&cs); } return 0; } int main() { int i, j; HANDLE hThread; DWORD IDThread; // инициализируем критическую секцию InitializeCriticalSection(&cs); hThread=CreateThread(NULL, 0, thread, NULL, 0, &IDThread); if (hThread == NULL) return GetLastError(); // потоки синхронизированы, поэтому каждая // строка содержит только одинаковые числа for (j = 10; j < 20; j++) { // попытка войти в критическую секцию TryEnterCriticalSection(&cs); for (i = 0; i < 10; i++) { cout << j << " "; cout.flush(); } cout << endl; // выход из критической секции LeaveCriticalSection(&cs); } // удаляем критическую секцию DeleteCriticalSection(&cs); // ждем завершения работы потока thread WaitForSingleObject(hThread, INFINITE); return 0; } Отметим, что, так как объекты типа CRITICAL_SECTION не являются объектами ядра операционной системы, то работа с ними происходит несколько быстрее, чем с объектами ядра операционной системы, так как в этом случае программа меньше обращается к ядру операционной системы. 2. Семафоры Дийкстры Семафор – это неотрицательная целая переменная, значение которой может изменяться только при помощи неделимых операций. Под понятием неделимая операция мы понимаем такую операцию, выполнение которой не может быть прервано. Семафор считается свободным, если его значение больше нуля, в противном случае семафор считается занятым. Пусть s семафор, тогда над ним можно определить следующие неделимые операции: P(s) { 36 если s >0 то s = s - 1; // поток продолжает работу иначе ждать освобождения s; // поток переходит в состояние ожидания } V(s) { если потоки ждут освобождения s, то освободить один поток; иначе s = s + 1; } Семафор с операциями P и V называется семафором Дийкстры, который первым использовал семафоры для решения задач синхронизации. Из определения операций над семафором видно, что если поток выдает операцию P и значение семафора больше нуля, то значение семафора уменьшается на 1 и этот поток продолжает свою работу, в противном случае поток переходит в состояние ожидания до освобождения семафора другим потоком. Вывести из состояния ожидания поток, который ждет освобождения семафора, может только другой поток, который выдает операцию V над этим же семафором. Потоки, ждущие освобождения семафора, выстраиваются в очередь к этому семафору. Дисциплина обслуживания очереди зависит от конкретной реализации. Очередь может обслуживаться как по правилу FIFO, так и при помощи более сложных алгоритмов, учитывая приоритеты потоков. Семафор, который может принимать только значения 0 или 1, называется двоичным или бинарным семафором. Чтобы подчеркнуть отличие бинарного семафора от не бинарного семафора, то есть такого семафора, значение которого может быть больше 1, последний обычно называют считающими семафором. Покажем, как бинарный семафор может использоваться для моделирования критических секций и событий. Для этого сначала рассмотрим следующие потоки. semaphor s = 1; // семафор свободен void thread_1( ) void thread_2( ) { { P(s); P(s); if (n%2 == 0) n++; n = a; V(s); else . n = b; . V(s); . . } } Как следует из определения операций над семафором, данный подход решает проблему взаимного исключения одновременного доступа к переменной n для потоков thread_1 и thread_2. Таким образом, бинарный семафор позволяет решить проблему взаимного исключения. Теперь предположим, что поток thread_1 должен производить проверку значения переменной n только после того, как поток thread_2 увеличит значение этой переменной. Для решения этой задачи модифицируем наши программы следующим образом: semaphor s = 0; // семафор занят void thread_1( ) void thread_2( ) { { 37 P(s); n++; if (n%2 == 0) n = a; . else . n = b; . . } } V(s); Как видно из этих программ, бинарный семафор позволяет также решить задачу условной синхронизации. 3. Семафоры в Windows Семафоры в операционных системах Windows описываются объектами ядра Semaphores, Семафор находится в сигнальном состоянии, если его значение больше нуля. В противном случае семафор находится в не сигнальном состоянии. Создаются семафоры посредством вызова функции CreateSemaphore, которая имеет следующий прототип: HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttribute, // атрибуты защиты LONG lInitialCount, // начальное значение семафора LONG lMaximumCount, // максимальное значение семафора LPCTSTR lpName // имя семафора ); Как и обычно, пока значение параметра lpSemaphoreAttributes будем устанавливать в NULL. Основную смысловую нагрузку в этой функции несут второй и третий параметры. Значение параметра lInitialCount устанавливает начальное значение семафора, которое должно быть не меньше 0 и не больше его максимального значения, которое устанавливается параметром lMaximumCount. В случае успешного завершения функция CreateSemaphore возвращает дескриптор семафора, в случае неудачи - значение NULL. Если семафор с заданным именем уже существует, то функция CreateSemaphore возвращает дескриптор этого семафора, а функция GetLastError, вызванная после функции CreateSemaphore вернет значение ERROR_ALREADY_EXISTS. Значение семафора уменьшается на 1 при его использовании в функции ожидания. Увеличить значение семафора можно посредством вызова функции ReleaseSemaphore, которая имеет следующий прототип: BOOL Release Semaphore( HANDLE hSemaphore, // дескриптор семафора LONG lRelease Count, // положительное число, // на которое увеличивается значение семафора lpPreviousCount // предыдущее значение семафора ); LPLONG В случае успешного завершения функция ReleaseSemaphore возвращает значение TRUE, в случае неудачи - FALSE. Если значение семафора плюс значение параметра lReleaseCount больше максимального значения семафора, то функция ReleaseSemaphore возвращает значение FALSE и значение семафора не изменяется. Значение параметра lpPreviousCount этой функции может быть равно NULL. В этом случае предыдущее значение семафора не возвращается. 38 Приведем пример программы, в которой считающий семафор используется для синхронизации работы потоков. Для этого сначала рассмотрим не синхронизированный вариант этой программы. Листинг 4. Несинхронизированные потоки #include <windows.h> #include <iostream> using namespace std; volatile int a[10]; DWORD WINAPI thread(LPVOID) { int i; for (i = 0; i < 10; i++) { a[i] = i + 1; Sleep(17); } return 0; } int main() { int i; HANDLE hThread; DWORD IDThread; cout << "An initial state of the array: "; for (i = 0; i < 10; i++) cout << a[i] <<' '; cout << endl; // создаем поток, который готовит элементы массива hThread = CreateThread(NULL, 0, thread, NULL, 0, &IDThread); if (hThread == NULL) return GetLastError(); // поток main выводит элементы массива cout << "A modified state of the array: "; for (i = 0; i < 10; i++) { cout << a[i] << ' '; cout.flush(); Sleep(17); } cout << endl; CloseHandle(hThread); return 0; } Теперь кратко опишем работу этой программы. Поток thread последовательно присваивает элементам массива «a» значения, которые на единицу больше чем их индекс. Поток main последовательно выводит элементы массива «а» на консоль. Так как потоки thread и main не синхронизированы, то неизвестно, какое состояние массива на консоль поток main. Наша задача состоит в том, чтобы поток main выводил на консоль элементы массива «a» сразу после их подготовки потоком thread. Для этого мы используем считающий семафор. Следующая программа показывает, как этот считающий семафор используется 39 для синхронизации работы потоков. Листинг 5. Пример синхронизации потоков с использованием семафора #include <windows.h> #include <iostream> using namespace std; volatile int a[10]; HANDLE hSemaphore; DWORD WINAPI thread(LPVOID) { int i; for (i = 0; i < 10; i++) { a[i] = i + 1; // отмечаем, что один ReleaseSemaphore(hSemaphore, 1 ,NULL); Sleep(500); } return 0; элемент готов } int main() { int i; HANDLE hThread; DWORD IDThread; cout << "An initial state of the array: "; for (i = 0; i < 10; i++) cout << a[i] <<' '; cout << endl; // создаем семафор hSemaphore=CreateSemaphore(NULL, 0, 10, NULL); if (hSemaphore == NULL) return GetLastErrorO; // создаем поток, который готовит элементы массива hThread = CreateThread(NULL, 0, thread, NULL, 0, &IDThread); if (hThread == NULL) return GetLastError(); // поток main выводит элементы массива // только после их подготовки потоком thread cout << "A final state of the array: "; for (i = 0; i < 10; i++) { WaitForSingleObject(hSemaphore, INFINITE); cout << a[i] << ' '; cout.flush(); } cout << endl; CloseHandle(hSemaphore); CloseHandle(hThread); return 0; } Может возникнуть следующий вопрос: почему для решения этой задачи используется именно считающий семафор и почему его максимальное значение равно 10. Конечно, поставленную задачу можно было бы решить и другими способами. Но дело в том, что считающие семафоры предназначены именно 40 для решения подобных задач. Подробнее, считающие семафоры используются для синхронизации доступа к однотипным ресурсам, которые производятся некоторым потоком или несколькими потоками, а потребляются другим потоком или несколькими потоками. В этом случае значение считающего семафора равно количеству произведенных ресурсов, а его максимальное значение устанавливается равным максимально возможному количеству таких ресурсов. При производстве единицы ресурса значение семафора увеличивается на единицу, а при потреблении единицы ресурса значение семафора уменьшается на единицу. В нашем примере ресурсами являются элементы массива, заполненные потоком thread, который является производителем этих ресурсов. В свою очередь поток main является потребителем этих ресурсов, которые он выводит на консоль. Так как в общем случае мы не можем сделать предположений о скоростях работы параллельных потоков, то максимальное значение считающего семафора должно быть установлено в максимальное количество производимых ресурсов. Если поток потребитель ресурсов работает быстрее чем поток производитель ресурсов, то, вызвав функцию ожидания считающего семафора, он вынужден будет ждать, пока поток- производитель не произведет очередной ресурс. Если же наоборот, поток-производитель работает быстрее чем поток-потребитель, то первый поток произведет все ресурсы и закончит свою работу, не ожидая, пока второй поток потребит их. Такая синхронизация потоков производителей и потребителей обеспечивает их максимально быструю работу. Доступ к существующему семафору можно открыть с помощью одной из функций CreateSemaphore или OpenSemaphore. Если для этой цели используется функция CreateSemaphore, то значения параметров lInitialCount и lMaximalCount этой функции игнорируются, так как они уже установлены другим потоком, а поток, вызвавший эту функцию, получает полный доступ к семафору с именем, заданным параметром lpName. Теперь рассмотрим функцию OpenSemaphore, которая используется в случае, если известно, что семафор с заданным именем уже существует. Эта функция имеет следующий прототип: HANDLE OpenSemaphore( DWORD dwDesiredAccess, // флаги доступа BOOL bInheritHandle, // режим наследования LPCTSTR lpName // имя события ); Параметр dwDesiredAccess определяет доступ к семафору, и может быть равен любой логической комбинации следующих флагов: SEMAPHORE_ALL_ACCESS SEMAPHORE_MODIFY_STATE SYNCHRONIZE Флаг SEMAPHORE_ALL_ACCESS устанавливает для потока полный доступ к семафору. Это означает, что поток может выполнять над семафором любые действия. Флаг SEMAPHORE_MODIFY_STATE означает, что поток может использовать только функцию ReleaseSemaphore для изменения значения семафора. Флаг SYNCHRONIZE означает, что поток может использовать семафор только в функциях ожидания. Отметим, что последний режим 41 поддерживается только на платформе Windows NT/2000. Задание на выполнение Написать программу для консольного процесса, который состоит из трёх потоков: main , work, и третьего (см. варианты). Глобальные переменные не использовать! Индивидуальные варианты: 1. Поток main должен выполнить следующие действия: − создать массив, размерность и элементы которого вводятся пользователем с консоли; − вывести размерность и элементы исходного массива на консоль; − ввести число k; − запустить поток work; − запустить поток SumElement; − освобождение выходной поток stdout после вывода на консоль каждого нового элемента массива. − выводить на экран поэлементно элементы массива (итогового) параллельно с работой потока work; − известить поток SumElement о начале суммирования (момент запуска произойдёт после того, будут выведены на консоль k элементов массива). Поток work должен выполнить следующие действия (Для синхронизации с потоком main - использовать семафор. Проверить работу программы используя критическую секцию для синхронизации с потоком main, объяснить отличия, если есть!): − запросить у пользователя временной интервал, требуемый для отдыха после подготовки одного элемента в массиве; − найти в массиве неповторяющиеся элементы (разместить их в массиве слева, остальные соответственно справа). Элементы - символы. − извещать поток main о новом элементе; − после каждого готового элемента отдыхать в течение заданного интервала времени; Поток SumElement должен выполнить следующие действия (Для синхронизации с потоком main, использовать бинарный семафор!): − ждёт от потока main сигнал о начале суммирования; − выполнить суммирование элементов итогового массива до заданной позиции k; − вывести итоговую сумму. 2. Поток main должен выполнить следующие действия: − создать массив, размерность и элементы которого вводятся пользователем с консоли; − вывести размерность и элементы исходного массива на консоль; − запустить поток work; − запустить поток SumElement; − освободить выходной поток stdout после вывода на консоль каждого 42 нового элемента массива. − выводить на экран поэлементно элементы массива (итогового) параллельно с работой потока work; Поток work должен выполнить следующие действия (Для синхронизации с потоком main - использовать семафор. Проверить работу используя бинарный семафор для синхронизации с потоком main, объяснить отличия, если есть!): − запросить у пользователя временной интервал, требуемый для отдыха после подготовки одного элемента в массиве; − найти в массиве повторяющиеся элементы (разместить их группы в массиве слева, остальные соответственно справа). Элементы – вещественные числа. − извещать поток main о новом элементе; − после каждого готового элемента отдыхать в течение заданного интервала времени; − известить поток SumElement о начале суммирования (момент запуска произойдёт после того, будет сформирован итоговый массив. Поток SumElement должен выполнить следующие действия (Для синхронизации с потоком work, использовать − ждёт от потока work сигнал о начале суммирования; − выполнить суммирование элементов итогового массива; − вывести итоговую сумму. 3. Поток main должен выполнить следующие действия: − создать массив, размерность и элементы которого вводятся пользователем с консоли; − вывести размерность и элементы исходного массива на консоль; − ввести число k; − запустить поток work; − запустить поток SumElement; − освобождение выходной поток stdout после вывода на консоль каждого нового элемента массива. − выводить на экран поэлементно элементы массива (итогового) параллельно с работой потока work; − -известить поток SumElement о начале суммирования (момент запуска произойдёт после того, будут выведены на консоль k элементов). Поток work должен выполнить следующие действия (Для синхронизации с потоком main - использовать семафор. Проверить работу программы используя критическую секцию для синхронизации с потоком main, объяснить отличия, если есть!): − запросить у пользователя временной интервал, требуемый для отдыха после подготовки одного элемента в массиве; − cортировка методом “пузырька”. Элементы - вещественные числа двойной точности; − извещать поток main о новом элементе; − после каждого готового элемента отдыхать в течение заданного интервала времени. Поток SumElement должен выполнить следующие действия (Для 43 синхронизации с потоком main, использовать бинарный семафор!): − ждёт от потока main сигнал о начале суммирования; − выполнить суммирование элементов итогового массива до заданной позиции k; − вывести итоговую сумму. 4. Поток main должен выполнить следующие действия: − создать массив, размерность и элементы которого вводятся пользователем с консоли; − вывести размерность и элементы исходного массива на консоль; − ввести число k; − запустить поток work; − запустить поток MultElement; − освобождение выходной поток stdout после вывода на консоль каждого нового элемента массива; − выводить на экран поэлементно элементы массива (итогового) параллельно с работой потока work. Поток work должен выполнить следующие действия (Для синхронизации с потоком main – использовать семафор. Проверить работу используя бинарный семафор для синхронизации с потоком main, объяснить отличия, если есть!): − запросить у пользователя временной интервал, требуемый для отдыха после подготовки одного элемента в массиве; − поиск в массиве элементов из диапазона [A,B] (разместить их в массиве слева, остальные элементы массива - заполнить нулями). Элементы целые числа без знака. Числа A,B ввести в потоке main. − извещать поток main о новом элементе; − после каждого готового элемента отдыхать в течение заданного интервала времени; − известить поток MultElement о начале работы (момент запуска произойдёт после того, будет сформирована часть итогового массива (когда будут найдены все элементы из диапазона [A, B]). Поток MultElement должен выполнить следующие действия (Для синхронизации с потоком work, использовать критическую секцию!): − ждёт от потока work сигнал о начале работы; − выполнить произведение элементов итогового массива (когда будут найдены все элементы из диапазона [A, B]); − вывести произведение. 5. Поток main должен выполнить следующие действия: − создать массив, размерность и элементы которого вводятся пользователем с консоли; − вывести размерность и элементы исходного массива на консоль; − ввести число k; − запустить поток work; − запустить поток SumElement; − освобождение выходной поток stdout после вывода на консоль каждого нового элемента массива; − выводить на экран поэлементно элементы массива (итогового) 44 параллельно с работой потока work; − известить поток SumElement о начале суммирования (момент запуска произойдёт после того, будут выведены на консоль k элементов массива). Поток work должен выполнить следующие действия (Для синхронизации с потоком main - использовать семафор. Проверить работу программы используя критическую секцию для синхронизации с потоком main, объяснить отличия, если есть!)'. − запросить у пользователя временной интервал, требуемый для отдыха после подготовки одного элемента в массиве; − сортировка выбором. Элементы - символы; − извещать поток main о новом элементе; − после каждого готового элемента отдыхать в течение заданного интервала времени. Поток SumElement должен выполнить следующие действия (Для синхронизации с потоком main, использовать бинарный семафор!)' − ждёт от потока main сигнал о начале суммирования; − выполнить суммирование элементов (кодов символов) итогового массива до заданной позиции k; − вывести итоговую сумму. 6. Поток main должен выполнить следующие действия: − создать массив, размерность и элементы которого вводятся пользователем с консоли; − вывести размерность и элементы исходного массива на консоль; − ввести число k; − запустить поток work; − запустить поток SumElement; − освобождение выходной поток stdout после вывода на консоль каждого нового элемента массива; − запросить число А; − выводить на экран поэлементно элементы массива (итогового) параллельно с работой потока work. Поток work должен выполнить следующие действия (Для синхронизации с потоком main - использовать семафор. Проверить работу используя бинарный семафор для синхронизации с потоком main, объяснить отличия, если есть!): − запросить у пользователя временной интервал, требуемый для отдыха после подготовки одного элемента в массиве; − поиск в массиве элементов >А (разместить их в массиве слева, остальные элементы массива -заполнить нулями). Элементы - целые числа без знака. Число А ввести в потоке main; − извещать поток main о новом элементе; − после каждого готового элемента отдыхать в течение заданного интервала времени; − известить поток SumElement о начале суммирования (момент запуска произойдёт после того, будет сформирован итоговый массив. Поток SumElement должен выполнить следующие действия (Для 45 синхронизации с потоком work, использовать критическую секцию!): − от потока work сигнал о начале суммирования; − выполнить суммирование элементов итогового массива; − вывести итоговую сумму. 7. Поток main должен выполнить следующие действия: − создать массив, размерность и элементы которого вводятся пользователем с консоли; − вывести размерность и элементы исходного массива на консоль; − ввести число k; − запустить поток work; − запустить поток SumElement; − освобождение выходной поток stdout после вывода на консоль каждого нового элемента массива; − выводить на экран поэлементно элементы массива (итогового) параллельно с работой потока work; − известить поток SumElement о начале суммирования (момент запуска произойдёт после того, будут готовы к печати k - элементов массива). Поток work должен выполнить следующие действия Для синхронизации с потоком main – использовать семафор. Проверить работу программы используя критическую секцию для синхронизации с потоком main, объяснить отличия, если есть!. − запросить у пользователя временной интервал, требуемый для отдыха после подготовки одного элемента в массиве; − Поиск в массиве простых чисел (разместить их в массиве слева, остальные элементы массива - справа). Элементы - целые числа без знака. − извещать поток main о новом элементе, после каждого готового элемента отдыхать в течение заданного интервала времени; − поток SumElement должен выполнить следующие действия Для синхронизации с потоком main, использовать бинарный семафор! − ждёт от потока main сигнал о начале суммирования; − выполнить суммирование элементов итогового массива до заданной позиции k, вывести итоговую сумму. 8. Поток main должен выполнить следующие действия: − создать массив, размерность и элементы которого вводятся пользователем с консоли; − вывести размерность и элементы исходного массива на консоль; − запустить поток work; − запустить поток CountElement; − освобождение выходной поток stdout после вывода на консоль каждого нового элемента массива; − запросить символ X; − выводить на экран поэлементно элементы массива (итогового) параллельно с работой потока work; − поток work должен выполнить следующие действия (Для синхронизации с потоком main - использовать семафор. Проверить 46 работу используя бинарный семафор для синхронизации с потоком main, объяснить отличия, если есть!) − запросить у пользователя временной интервал, требуемый для отдыха после подготовки одного элемента в массиве; − поиск в массиве элементов =Х (разместить их в массиве слева, остальные элементы массива -справа). Элементы - символы. X ввести в потоке main; − извещать поток main о новом элементе; − после каждого готового элемента отдыхать в течение заданного интервала времени; − известить поток CountElement о начале работы (момент запуска произойдёт после того, будет сформирован итоговый массив. Поток CountElement должен выполнить следующие действия (Для синхронизации с потоком work, использовать критическую секцию!) − ждёт от потока work сигнал о начале суммирования; − подсчитать количество элементов равных X; − вывести итоговую сумму. 9. Поток main должен выполнить следующие действия: − создать массив, размерность и элементы которого вводятся пользователем с консоли; − вывести размерность и элементы исходного массива на консоль; − ввести число k; − запустить поток work; − запустить поток MultElement; − освобождение выходной поток stdout после вывода на консоль каждого нового элемента массива; − выводить на экран поэлементно элементы массива (итогового) параллельно с работой потока work; − известить поток MultElement о начале работы (момент запуска произойдёт после того, будут выведены на консоль k элементов массива). − поток work должен выполнить следующие действия (Для синхронизации с потоком main - использовать семафор. Проверить работу программы используя критическую секцию для синхронизации с потоком main, объяснить отличия, если есть!) − запросить у пользователя временной интервал, требуемый для отдыха после подготовки одного элемента в массиве; − поиск в массиве элементов <А (разместить их в массиве слева, остальные элементы массива - справа). Элементы - вещественные числа. Число А ввести в потоке main; − извещать поток main о новом элементе; − после каждого готового элемента отдыхать в течение заданного интервала времени; − поток MultElement должен выполнить следующие действия (Для синхронизации с потоком main, использовать бинарный семафор!) − ждёт от потока main сигнал о начале суммирования; 47 − выполнить произведение элементов итогового массива до заданной позиции k; − вывести итоговое произведений. 10. Поток main должен выполнить следующие действия: − создать массив, размерность и элементы которого вводятся пользователем с консоли; − вывести размерность и элементы исходного массива на консоль; − запустить поток work; − запустить поток SumElement; − освобождение выходной поток stdout после вывода на консоль каждого нового элемента массива. − выводить на экран поэлементно элементы массива (итогового) параллельно с работой потока work; − поток work должен выполнить следующие действия (Для синхронизации с потоком main - использовать семафор. Проверить работу используя бинарный семафор для синхронизации с потоком main, объяснить отличия, если есть!) − запросить у пользователя временной интервал, требуемый для отдыха после подготовки одного элемента в массиве; − поиск в массиве лексем, (разделители - цифры). Полученные лексемы поместить в массиве слева, разделитель - пробел, остальные элементы заполнить символом ‘0’. Элементы массива - символы; − извещать поток main о новом элементе; − после каждого готового элемента отдыхать в течение заданного интервала времени; − известить поток SumElement о начале суммирования (момент запуска произойдёт после того, будет сформирован итоговый массив; − поток SumElement должен выполнить следующие действия (Для синхронизации с потоком work, использовать критическую секцию!) − ждёт от потока work сигнал о начале суммирования; − выполнить суммирование элементов (кодов) итогового массива; − вывести итоговую сумму. 11 . Поток main должен выполнить следующие действия: − создать массив, размерность и элементы которого вводятся пользователем с консоли; − вывести размерность и элементы исходного массива на консоль; − ввести число k; − запустить поток work; − запустить поток SumElement; − освобождение выходной поток stdout после вывода на консоль каждого нового элемента массива; − выводить на экран поэлементно элементы массива (итогового) параллельно с работой потока work; − известить поток SumElement о начале суммирования (момент запуска произойдёт после того, будут выведены на консоль k элементов массива); 48 − поток work должен выполнить следующие действия (Для синхронизации с потоком main - использовать семафор. Проверить работу используя бинарный семафор для синхронизации с потоком main, объяснить отличия, если есть!): − запросить у пользователя временной интервал, требуемый для отдыха после подготовки одного элемента в массиве; − приведение массива к палиндрому (получившейся палиндром поместить в массиве слева, а лишние элементы соответственно - справа ) Элементы – символы извещать поток main о новом элементе; − после каждого готового элемента отдыхать в течение заданного интервала времени; Поток SumElement должен выполнить следующие действия (Для синхронизации с потоком main, использовать критическую секцию!): − ждёт от потока main сигнал о начале суммирования; − выполнить суммирование элементов (кодов) итогового массива до заданной позиции k; − вывести итоговую сумму. 12. Поток main должен выполнить следующие действия: − создать массив, размерность и элементы которого вводятся пользователем с консоли; − вывести размерность и элементы исходного массива на консоль; − запустить поток work; − запустить поток MultElement; − освобождение выходной поток stdout после вывода на консоль каждого нового элемента массива; − выводить на экран поэлементно элементы массива (итогового) параллельно с работой потока work; Поток work должен выполнить следующие действия (Для синхронизации с потоком main - использовать семафор. Проверить работу программы используя критическую секцию для синхронизации с потоком main, объяснить отличия, если есть!): − запросить у пользователя временной интервал, требуемый для отдыха после подготовки одного элемента в массиве; сортировка выбором. Элементы - целые числа, извещать поток main о новом элементе; − после каждого готового элемента отдыхать в течение заданного интервала времени; − известить поток MultElement о начале работы (момент запуска произойдёт после того, будет сформирован итоговый массив. Поток MultElement должен выполнить следующие действия (Для синхронизации с потоком work, использовать бинарный семафор!): − ждёт от потока work сообщения о начале суммирования; − выполнить произведение элементов итогового массива; − вывести произведение. 13. Поток main должен выполнить следующие действия: − создать массив, размерность и элементы которого вводятся пользователем с консоли; 49 − − − − − вывести размерность и элементы исходного массива на консоль; ввести число k; запустить поток work; запустить поток SumElement; освобождение выходной поток stdout после вывода на консоль каждого нового элемента массива; − выводить на экран поэлементно элементы массива (итогового) параллельно с работой потока work; известить поток SumElement о начале суммирования (момент запуска произойдёт после того, будут выведены на консоль k элементов массива). Поток work должен выполнить следующие действия (Для синхронизации с потоком main - использовать семафор. Проверить работу программы используя критическую секцию для синхронизации с потоком main, объяснить отличия, если есть!): − запросить у пользователя временной интервал, требуемый для отдыха после подготовки одного элемента в массиве; − поиск в массиве элементов, соответствующих цифрам (слева поместить в массив цифры, а остальные элементы массива - заполнить пробелами). Элементы - символы. − извещать поток main о новом элементе; − после каждого готового элемента отдыхать в течение заданного интервала времени; Поток SumElement должен выполнить следующие действия (Для синхронизации с потоком main, использовать бинарный семафор!): − ждёт от потока main сообщения о начале суммирования; − выполнить суммирование элементов (кодов) итогового массива до заданной позиции k; − вывести итоговую сумму. 14. Поток main должен выполнить следующие действия: − создать массив, размерность и элементы которого вводятся пользователем с консоли; − вывести размерность и элементы исходного массива на консоль; − запустить поток work; − запустить поток SumElement; − освобождение выходной поток stdout после вывода на консоль каждого нового элемента массива; − выводить на экран поэлементно элементы массива (итогового) параллельно с работой потока work; Поток work должен выполнить следующие действия (Для синхронизации с потоком main – использовать семафор. Проверить работу используя бинарный семафор для синхронизации с потоком main, объяснить отличия, если есть!): − запросить у пользователя временной интервал, требуемый для отдыха после подготовки одного элемента в массиве; − поиск в массиве лексем, начинающихся с цифры (разделители - пробел и тире). Полученные лексемы поместить в массиве слева, а лишние 50 элементы заполнить символом подчеркивания: «_» ). Элементы – символы; − извещать поток main о новом элементе; − после каждого готового элемента отдыхать в течение заданного интервала времени; − известить поток SumElement о начале суммирования (момент запуска произойдёт после того, будет сформирован итоговый массив. Поток SumElement должен выполнить следующие действия (Для синхронизации с потоком work, использовать критическую секцию!): − ждёт от потока work сообщения о начале суммирования; − выполнить суммирование элементов (кодов) итогового массива; − вывести итоговую сумму. 15. Поток main должен выполнить следующие действия: − создать массив, размерность и элементы которого вводятся пользователем с консоли; − вывести размерность и элементы исходного массива на консоль; − запустить поток work; − запустить поток Sum/CountElement; − освобождение выходной поток stdout после вывода на консоль каждого нового элемента массива; − выводить на экран поэлементно элементы массива (итогового) параллельно с работой потока work; Поток work должен выполнить следующие действия (Для синхронизации с потоком main - использовать семафор. Проверить работу используя бинарный семафор для синхронизации с потоком main, объяснить отличия, если есть!): − запросить у пользователя временной интервал, требуемый для отдыха после подготовки одного элемента в массиве; − поиск в массиве лексем, начинающихся с цифры (разделители – пробел и тире). − полученные лексемы поместить в массиве слева, а лишние элементы заполнить символом подчеркивания: «_» ). Элементы – символы; − извещать поток main о новом элементе; − после каждого готового элемента отдыхать в течение заданного интервала времени; − известить поток Sum/CountElement о начале суммирования (момент запуска произойдёт после того, будет сформирован итоговый массив; − поток Sum/CountElement должен выполнить следующие действия (Для синхронизации с потоком work, использовать критическую секцию!): − ждёт от потока work сообщения о начале суммирования; − выполнить суммирование и подсчёт элементов (до символов подчеркивания: «_») итогового массива; − вывести результаты. 51 Лабораторная работа №5 Тема: Синхронизация процессов при помощи событий и мьютексов Цель работы: 1. Изучить объекты синхронизации потоков мьютексы и события. 2. В соответствии с заданным вариантом разработать приложение, реализующее синхронизацию потоков с помощью мьютексов и критических секций. 3. В соответствии с заданным вариантом разработать приложение, реализующее синхронизацию потоков с помощью Краткое теоретическое введение 1. Объекты синхронизации и функции ожидания в Windows В операционных системах Windows объектами синхронизации называются объекты ядра, которые могут находиться в одном из двух состояний: сигнальном (signaled) и несигнальном (nonsignaled). Объекты синхронизации могут быть разбиты на три класса. К первому классу относятся объекты синхронизации, которые служат только для решения проблемы синхронизации параллельных потоков. К таким объектам синхронизации в Windows относятся: − мьютекс (mutex); − событие (event); − семафор (semaphore). Ко второму классу объектов синхронизации относится ожидающий таймер (waitable timer). К третьему классу объектов синхронизации относятся объекты, которые переходят в сигнальное состояние по завершении своей работы или при получении некоторого сообщения. Примерами таких объектов синхронизации являются потоки и процессы. Пока эти объекты выполняются, они находятся в несигнальном состоянии. Если выполнение этих объектов заканчивается, то они переходят в сигнальное состояние. Теперь перейдем к функциям ожидания. Функции ожидания в Windows это такие функции, параметрами которых являются объекты синхронизации. Эти функции обычно используются для блокировки потоков, которая выполняется следующим образом. Если дескриптор объекта синхронизации является параметром функции ожидания, а сам объект синхронизации находится в несигнальном состоянии, то поток, вызвавший эту функцию ожидания, блокируется до перехода этого объекта синхронизации в сигнальное состояние. Сейчас мы будем использовать только две функции ожидания WaitForSingleObject и WaitForMultipleObject. Для ожидания перехода в сигнальное состояние одного объекта синхронизации используется функция WaitForSingleObject, которая имеет следующий прототип: DWORD WaitForSingleObject( HANDLE hHandle, // дескриптор объекта DWORD dwMilliseconds // интервал ожидания в миллисекундах ); 52 Функция WaitForSingleObject в течение интервала времени, равного значению параметра dwMilliseconds, ждет пока объект синхронизации с дескриптором hHandle перейдет в сигнальное состояние. Если значение параметра dwMilliseconds равно нулю, то функция только проверяет состояние объекта. Если же значение параметра dwMilliseconds равно INFINITE, то функция ждет перехода объекта синхронизации в сигнальное состояние бесконечно долго. В случае удачного завершения функция WaitForSingleObject возвращает одно из следующих значений: WAIT_OBJECT_0 WAIT_ABANDONED WAIT_TIMEOUT Значение WAIT_OBJECT_0 означает, что объект синхронизации находился или перешел в сигнальное состояние. Значение WAIT_ABANDONED означает, что объектом синхронизации является мьютекс, который не был освобожден потоком, завершившим свое исполнение. После завершения потока этот мьютекс освободился системой и перешел в сигнальное состояние. Такой мьютекс иногда называется забытым мьютексом (abandoned mutex). Значение WAIT_TIMEOUT означает, что время ожидания истекло, а объект синхронизации не перешел в сигнальное состояние. В случае неудачи функция WaitForSingleObject возвращает значение WAIT_FAILED. Приведем пример простой программы, которая использует функцию WaitForSingleObject для ожидания завершения потока. Отметим также, что эта функция уже использовалась нами в Программе 2.1 для ожидания завершения работы потока Add. Листинг 1. Пример использования функциеи WaitForSingleObject #include <windows.h> #include <iostream> using namespace std; void thread() { int i; for (i = 0; i < 10 ; i++) { cout << i << ' '; cout << flush << '\a'; Sleep(500); } cout << endl; } int main() { HANDLE hThread; DWORD dwThread; hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)thread, NULL, 0, &dwThread); if (hThread == NULL) return GetLastError(); // ждем завершения потока thread if(WaitForSingleObject(hThread, INFINITE) != WAIT_OBJECT_0) { cout << "Wait for single object failed." << endl; cout << 53 "Press any key to exit." << endl; } // закрываем дескриптор потока thread CloseHandle(hThread); return 0; } Для ожидания перехода в сигнальное состояние нескольких объектов синхронизации или одного из нескольких объектов синхронизации используется функция WaitForMultipleObject, которая имеет следующий прототип: DWORD WaitForMultipleObjects( DWORD nCount, // количество объектов CONST HANDLE *lpHandles, // массив дескрипторов объектов BOOL bWaitAll, // режим ожидания DWORD dwMilliseconds // интервал ожидания в миллисекундах ); Функция WaitForMultipleObjects работает следующим образом. Если значение параметра bWaitAll равно TRUE, то эта функция в течение интервала времени, равного значению параметра dwMilliseconds, ждет пока все объекты синхронизации, дескрипторы которых заданы в массиве lpHandles, перейдут в сигнальное состояние. Если же значение параметра bWaitAll равно FALSE, то эта функция в течение заданного интервала времени ждет пока любой из заданных объектов синхронизации перейдет в сигнальное состояние. Если значение параметра dwMilliseconds равно нулю, то функция только проверяет состояние объектов синхронизации. Если же значение параметра dwMilliseconds равно INFINITE, то функция ждет перехода объектов синхронизации в сигнальное состояние бесконечно долго. Количество объектов синхронизации, ожидаемых функцией WaitForMultipleObjects, не должно превышать значения MAXIMUM_WAIT_OBJECTS. Также отметим, что объекты синхронизации не должны повторяться. В случае успешного завершения функция WaitForMultipleObjects возвращает их следующих значений: от WAIT_OBJECT_0 до (WAIT_OBJECT_0 + nCount WAIT_ABANDONED_0 до (WAIT_ABANDONED_0 + nCount - 1); WAIT_TIMEOUT. - 1); от Интерпретация значений, возвращаемых функцией WaitForMultipleObjects, зависит от значения входного параметра bWaitAll. Сначала рассмотрим случай, когда значение этого параметра равно TRUE. Тогда возвращаемые значения интерпретируются следующим образом: - любое из возвращаемых значений, находящихся в диапазоне от WAIT_OBJECT_0 до (WAIT_OBJECT_0 + nCount 1), означает, что все объекты синхронизации находились или перешли в сигнальное состояние; - любое из возвращаемых значений, находящихся в диапазоне от WAIT_ABANDONED_0 до (WAIT_ABANDONED_0 + nCount - 1) означает, что все объекты синхронизации находились или перешли в сигнальное состояние и, по крайней мере, один их них был забытым мьютексом; - возвращаемое значение WAIT_TIMEOUT означает, что время ожидания истекло и не все объекты синхронизации перешли в сигнальное состояние. 54 Теперь рассмотрим случай, когда значение входного параметра bWaitAll равно FALSE. В этом случае значения, возвращаемые функцией WaitForMultipleObjects, интерпретируются следующим образом: - любое из возвращаемых значений, находящихся в диапазоне от WAIT_OBJECT_0 до (WAIT_OBJECT_0 + nCount - 1), означает, что, по крайней мере, один из объектов синхронизации находился или перешёл в сигнальное состояние. Индекс дескриптора этого объекта в массиве определяется как разница между возвращаемым значением и величиной WAIT_OBJECT_0; - любое из возвращаемых значений, находящихся в диапазоне от WAIT_ABANDONED_0 до (WAIT_ABANDONED_0 + nCount - 1) означает, что одним из объектов синхронизации, перешедшим в сигнальное состояние, является забытый мьютекс. Индекс дескриптора этого мьютекса в массиве определяется как разница между возвращаемым значением и величиной WAIT_OBJECT_0; - возвращаемое значение WAIT_TIMEOUT означает, что время ожидания истекло, и ни один из объектов синхронизации не перешел в сигнальное состояние. В случае неудачи функция WaitForMultipleObjects возвращает значение WAIT_FAILED. Приведем пример программы, которая использует функцию WaitForSingleObject для ожидания завершения двух потоков. Листинг 2. Пример использования функциеи WaitForMultipleObjects #include <windows.h> #include <iostream> using namespace std; void thread_0() { int i; for (i = 0; i < 5 ; i++) { cout << i << ' '; cout << flush << '\a'; Sleep(500); } cout << endl; } void thread_1() { int i; for (i = 5; i < 10 ; i++) { cout << i << ' '; cout << flush << '\a'; Sleep(500); } cout << endl; } int main() { 55 HANDLE hThread[2]; DWORD dwThread[2]; // запускаем первый поток hThread[0] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)thread_0, NULL, 0, &dwThread[0]); if (hThread[0] == NULL) return GetLastError(); // запускаем второй поток hThread[1] = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)thread_1, NULL, 0, &dwThread[1]); if (hThread[1] == NULL) return GetLastError(); // ждем завершения потоков thread_1 и thread_2 if (WaitForMultipleObjects(2, hThread, TRUE, INFINITE) == WAIT_F AILED) { cout << "Wait for multiple objects failed." << endl; cout << "Press any key to exit." << endl; } // закрываем дескрипторы потоков thread_0 и thread_1 CloseHandle(hThread[0]); CloseHandle(hThread[ 1 ]); return 0; } Любой ресурс, на доступ к которому претендуют не менее двух параллельных потоков, называется критическим или разделяемым ресурсом. Участок программы, на протяжении которого поток ведет работу с критическим ресурсом, называется критической секцией по отношению к этому ресурсу. Например, рассмотрим два параллельных потока: Поток 1. Поток 2. void thread_1( ) { { if (n%2 == 0) n = a; . else . n = b; . . } } void thread_2( ) ++n; Возможно, что после проверки условия (n%2 == 0) работа первого потока прервется, и процессорное время будет передано второму потоку. Второй поток увеличит значение переменной n на единицу и после этого процессор опять будет передан первому потоку. В этом случае первый поток присвоит переменной n неправильное значение. Для исключения такой ситуации, необходимо блокировать одновременный доступ потоков к переменной n. Следовательно, в этом примере переменная n или, более точно, область памяти, занимаемая этой переменной, является критическим ресурсом, а рассматриваемые участки программного кода являются критическими секциями по отношению к этому ресурсу. Для правильной работы потоков thread_1 и thread_2 необходимо обеспечить, чтобы приведенные участки 56 программного кода не могли работать одновременно. Другими словами нам необходимо решить задачу исключения взаимного доступа потоков thread_1 и thread_2 к критическому ресурсу, которым является переменная n. В общем случае проблема взаимного исключения формулируется следующим образом. Необходимо обеспечить такую работу параллельных потоков с критическим ресурсом, при которой гарантируется, что критические секции этих потоков по отношению к этому ресурсу не работают одновременно. 2. Мьютексы в Windows Для решения проблемы взаимного исключения между параллельными потоками, выполняющимися в контексте разных процессов, в операционных системах Windows используется объект ядра мьютекс. Слово мьютекс является переводом английского слова mutex, которое в свою очередь является сокращением от выражения mutual exclusion, что на русском языке значит взаимное исключение. Мьютекс находится в сигнальном состоянии, если он не принадлежит ни одному потоку. В противном случае мьютекс находится в несигнальном состоянии. Одновременно мьютекс может принадлежать только одному потоку. Создается мьютекс вызовом функции CreateMutex, которая имеет следующий прототип: HANDLE CreateMutex( LPSECURITY_ATTRIBUTES защиты BOOL bInitialOwner, // LPCTSTR lpName // ); lpMutexAttributes, // атрибуты начальный владелец мьютекса имя мьютекса Пока значение параметра LPSECURITY_ATTRIBUTES будем устанавливать в NULL. Это означает, что атрибуты защиты заданы по умолчанию, то есть дескриптор мьютекса не наследуется и доступ к мьютексу имеют все пользователи. Теперь перейдем к другим параметрам. Если значение параметра bInitialOwner равно TRUE, то мьютекс сразу переходит во владение потоку, которым он был создан. В противном случае вновь созданный мьютекс свободен. Поток, создавший мьютекс, имеет все права доступа к этому мьютексу. Значение параметра lpName определяет уникальное имя мьютекса для всех процессов, выполняющихся под управлением операционной системы. Это имя позволяет обращаться к мьютексу из других процессов, запущенных под управлением этой же операционной системы. Длина имени не должна превышать значение MAX_PATH. Значением параметра lpName может быть пустой указатель NULL. В этом случае система создает безымянный мьютекс. Отметим также, что имена мьютексов являются чувствительными к нижнему и верхнему регистрам. В случае удачного завершения функция CreateMutex возвращает дескриптор созданного мьютекса. В случае неудачи эта функция возвращает значение NULL. Если мьютекс с заданным именем уже существует, то функция CreateMutex возвращает дескриптор этого мьютекса, а функция GetLastError, 57 вызванная после функции CreateMutex вернет значение ERROR_ALREADY_EXISTS. Мьютекс захватывается потоком посредством любой функции ожидания, а освобождается функцией ReleaseMutex, которая имеет следующий прототип: BOOL ReleaseMutex( HANDLE hMutex ); // дескриптор мьютекса В случае успешного завершения функция ReleaseMutex возвращает значение TRUE, в случае неудачи - FALSE. Если поток освобождает мьютекс, которым он не владеет, то функция ReleaseMutex возвращает значение FALSE. Для доступа к существующему мьютексу поток может использовать одну из функций CreateMutex или OpenMutex. Функция CreateMutex используется в тех случаях, когда поток не знает, создан или нет мьютекс с указанным именем другим потоком. В этом случае значение параметра bInitialOwner нужно установить в FALSE, так как невозможно определить какой из потоков создает мьютекс. Если поток использует для доступа к уже созданному мьютексу функцию CreateMutex, то он получает полный доступ к этому мьютексу. Для того чтобы получить доступ к уже созданному мьютексу, поток может также использовать функцию OpenMutex, которая имеет следующий прототип: HANDLE OpenMutex( DWORD dwDesiredAccess, // доступ к мьютексу BOOL bInheritHandle // свойство наследования LPCTSTR lpName // имя мьютекса Параметр dwDesiredAccess этой функции может принимать одно из двух значений: MUTEX_ALL_ACCES S SYNCHRONIZE В первом случае поток получает полный доступ к мьютексу. Во втором случае поток может использовать мьютекс только в функциях ожидания, чтобы захватить мьютекс, или в функции ReleaseMutex, для его освобождения. Параметр bInheritHandle определяет свойство наследования мьютекса. Если значение этого параметра равно TRUE, то дескриптор открываемого мьютекса является наследуемым. В противном случае - дескриптор не наследуется. В случае успешного завершения функция OpenMutex возвращает дескриптор открытого мьютекса, в случае неудачи эта функция возвращает значение NULL. Покажем пример использования мьютекса для синхронизации потоков из разных процессов. Для этого сначала рассмотрим пример не синхронизированных потоков. Листинг 3. Не синхронизированные потоки, выполняющиеся в разных процессах #include <windows.h> #include <iostream> using namespace std; int main() { int ij; for (j = 10; j < 20; j++) { for (i = 0; i < 10; i++) 58 { cout << j << ' '; cout.flush(); Sleep(5); } cout << endl; } return 0; } Листинг 4. Не синхронизированные потоки, выполняющиеся в разных процессах #include <windows.h> #include <iostream> using namespace std; int main() { char lpszAppName[] = "D:\\os.exe"; STARTUPINFO si; PROCE S S_INF ORM ATION pi; ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); // создаем новый консольный процесс if (!CreateProcess(lpszAppName, NULL, NULL, NULL, FALSE, NULL, NULL, NULL, &si, &pi)) { cout << "The new process is not created." << endl; cout << "Press any key to exit." << endl; cin.get(); return GetLastError(); } // выводим на экран строки for (int j = 0; j < 10; j++) { for (int i = 0; i < 10; i++) { cout << j << ' '; cout.flush(); Sleep(10); } cout << endl; } // ждем пока дочерний процесс закончит работу WaitForSingleObject(pi.hProcess, INFINITE); // закрываем дескрипторы дочернего процесса в текущем процессе CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0; } Кратко опишем работу этих программ. Вторая из них запускает первую программу, после чего потоки из разных процессов начинают выводить числа в одну консоль. Из-за отсутствия синхронизации, числа в одной строке могут быть из разных потоков. Для того чтобы избежать перемешивания чисел, синхронизируем их вывод с помощью мьютекса. Ниже приведены модификации этих программ с использованием мьютекса для синхронизации работы этих потоков. 59 Листинг 5. Синхронизация потоков, выполняющихся в // разных процессах, с использованинм мьютекса #include <windows.h> #include <iostream> using namespace std; int main() { HANDLE hMutex; int ij; // открываем мьютекс hMutex = OpenMutex(SYNCHRONIZE, FALSE, "DemoMutex"); if (hMutex == NULL) { cout << "Open mutex failed." << endl; cout << "Press any key to exit." << endl; cin.get(); return GetLastError(); } for (j = 10; j < 20; j++) { // захватываем мьютекс WaitForSingleObject(hMutex, INFINITE); for (i = 0; i < 10; i++) { cout << j << ' '; cout.flush(); Sleep(5); } cout << endl; // освобождаем мьютекс ReleaseMutex(hMutex); } // закрываем дескриптор объекта CloseHandle(hMutex); return 0; } Листинг 6. Пример синхронизации потоков, выполняющихся в разных процессах, с использованием мьютекса #include <windows.h> #include <iostream> using namespace std; int main() { HANDLE hMutex; char lpszAppName[] = "D:\\os.exe"; STARTUPINFO si; PROCESS_INFORMATION pi; // создаем мьютекс hMutex = CreateMutex(NULL, FALSE, "DemoMutex"); if (hMutex == NULL) { cout << "Create mutex failed." << endl; cout << "Press any key to exit." << endl; cin.get(); return GetLastError(); } ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); // создаем новый консольный процесс 60 if (!CreateProcess(lpszAppName, NULL, NULL, NULL, FALSE, NULL, NULL, NULL, &si, &pi)) { cout << "The new process is not created." << endl; cout << "Press any key to exit." << endl; cin.get(); return GetLastError(); } // выводим на экран строки for (int j = 0; j < 10; j++) { // захватываем мьютекс W aitForSingleObj ect(hMutex, INFINITE); for (int i = 0; i < 10; i++) { cout << j << ' '; cout.flush(); Sleep(10); } cout << endl; // освобождаем мьютекс ReleaseMutex(hMutex); } // закрываем дескриптор мьютекса CloseHandle(hMutex); // ждем пока дочерний процесс закончит работу W aitForSingleObj ect(pi.hProcess, INFINITE); // закрываем дескрипторы дочернего процесса в текущем процессе CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0; } 3. События в Windows Событием называется оповещение о некотором выполненном действии. В программировании события используются для оповещения одного потока о том, что другой поток выполнил некоторое действие. Сама же задача оповещения одного потока о некотором действии, которое совершил другой поток называется задачей условной синхронизации или иногда задачей оповещения. В операционных системах Windows события описываются объектами ядра Events. При этом различают два типа событий: события с ручным сбросом; события с автоматическим сбросом. Различие между этими типами событий заключается в том, что событие с ручным сбросом можно перевести в несигнальное состояние только посредством вызова функции ResetEvent, а событие с автоматическим сбросом переходит в несигнальное состояние как при помощи функции ResetEvent, так и при помощи функции ожидания. При этом отметим, что если события с автоматическим сбросом ждут несколько потоков, используя функцию WaitForSingleObject, то из состояния ожидания освобождается только один из этих потоков. Создаются события вызовом функции CreateEvent, которая имеет следующий прототип: HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpSecurity Attributes, // защиты BOOL bManualReset, // тип события BOOL bInitialState, // начальное состояние события атрибуты 61 LPCTSTR ); lpName // имя события Как и обычно, пока значение параметра lpSecurity Attributes будем устанавливать в NULL. Основную смысловую нагрузку в этой функции несут второй и третий параметры. Если значение параметра bManualReset равно TRUE, то создается событие с ручным сбросом, в противном случае - с автоматическим сбросом. Если значение параметра bInitialState равно TRUE, то начальное состояние события является сигнальным, в противном случае несигнальным. Параметр lpName задает имя события, которое позволяет обращаться к нему из потоков, выполняющихся в разных процессах. Этот параметр может быть равен NULL, тогда создается безымянное событие. В случае удачного завершения функция CreateEvent возвращает дескриптор события, а в случае неудачи - значение NULL. Если событие с заданным именем уже существует, то функция CreateEvent возвращает дескриптор этого события, а функция GetLastError, вызванная после функции CreateEvent вернет значение ERROR_ALREADY_EXISTS. Ниже приведена программа, в которой безымянные события с автоматическим сбросом используются для синхронизации работы потоков, выполняющихся в одном процессе. Листинг 7. Пример синхронизации потоков при помощи событий с автоматическим сбросом #include <windows.h> #include <iostream> using namespace std; volatile int n; HANDLE hOutEvent, hAddEvent; DWORD WINAPI thread(LPVOID) { int i; for (i = 0; i < 10; i++) { ++n; if (i == 4) { SetEvent(hOutEvent); WaitForSingleObject(hAddEvent, INFINITE); } } return 0; } int main() { HANDLE hThread; DWORD IDThread; cout << "An initial value of n = " << n << endl; // создаем события с автоматическим сбросом hOutEvent = CreateEvent(NULL, FALSE, FALSE, NULL); if (hOutEvent == NULL) return GetLastError(); hAddEvent = CreateEvent(NULL, FALSE, FALSE, NULL); if (hAddEvent == NULL) return GetLastError(); 62 // создаем поток счетчик thread hThread = CreateThread(NULL, 0, thread, NULL, 0, &IDThread); if (hThread == NULL) return GetLastError(); // ждем пока поток thread выполнит половину работы WaitForSingleObject(hOutEvent, INFINITE); // выводим значение переменной cout << "An intermediate value of n = " << n << endl; // разрешаем дальше работать потоку thread SetEvent(hAddEvent); WaitForSingleObject(hThread, INFINITE); cout << "A final value of n = " << n << endl; CloseHandle(hThread); CloseHandle(hOutEvent); CloseHandle(hAddEvent); return 0; } Для перевода любого события в сигнальное состояние используется функция SetEvent, которая имеет следующий прототип: BOOL SetEvent( HANDLE hEvent // дескриптор события ); При успешном завершении эта функция возвращает значение TRUE, в случае неудачи - FALSE. Для перевода любого события в несигнальное состояние используется функция ResetEvent, которая имеет следующий прототип: BOOL ResetEvent( HANDLE hEvent // дескриптор события ); При успешном завершении эта функция возвращает значение TRUE, в случае неудачи - FALSE. Для освобождения нескольких потоков, ждущих сигнального состояния события с ручным сбросом, используется функция PulseEvent, которая имеет следующий прототип: BOOL PulseEvent( HANDLE hEvent // дескриптор события ); При вызове этой функции все потоки, ждущие события с дескриптором hEvent, выводятся из состояния ожидания, а само событие сразу переходит в несигнальное состояние. Если функция PulseEvent вызывается для события с автоматическим сбросом, то из состояния ожидания выводится только один из ожидающих потоков. Если нет потоков, ожидающих сигнального состояния события из функции PulseEvent, то состояние этого события остается несигнальным. Однако заметим, что на платформе Windows NT/2000 для выполнения этой функции требуется, чтобы в дескрипторе события был установлен режим доступа EVENT_MODIFY_STATE. Ниже приведен пример программы, использующей для синхронизации события как с ручным, так и автоматическим сбросом. Листинг 8. Пример синхронизации потоков при помощи событий с ручным сбросом #include <windows.h> 63 #include <iostream> using namespace std; volatile int n,m; HANDLE hOutEvent[2], hAddEvent; DWORD WINAPI thread_1 (LPVOID) { int i; for (i = 0; i < 10; i++) { ++n; if (i == 4) { SetEvent(hOutEvent[0]); WaitForSingleObject(hAddEvent, INFINITE); } } return 0; } DWORD CALLBACK thread_2(LPVOID) { int i; for (i = 0; i < 10; i++) { ++m; if (i == 4) { S etEvent(hOutEvent [ 1 ]); WaitForSingleObject(hAddEvent, INFINITE); } } return 0; } int main() { HANDLE hThread_1, hThread_2; DWORD IDThread_1, IDThread_2; cout << "An initial values of n = " << n << ", m = " << m endl; // создаем события с автоматическим сбросом hOutEvent[0] CreateEvent(NULL, FALSE, FALSE, NULL); if (hOutEvent[0] == NULL) return GetLastError(); hOutEvent[1] = CreateEvent(NULL, FALSE, FALSE, NULL); (hOutEvent[1] == NULL) return GetLastError(); // создаем событие с ручным сбросом hAddEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if (hAddEvent == NULL) return GetLastError(); // создаем потоки счетчики hThread_1 = CreateThread(NULL, 0, thread_1, NULL, &IDThread_1); if (hThread_1 == NULL) return GetLastError(); hThread_2 = CreateThread(NULL, 0, thread_2, NULL, &IDThread_2); if (hThread_2 == NULL) 64 << = if 0, 0, return GetLastError(); // ждем пока потоки счетчики выполнят половину работы WaitForMultipleObjects(2, hOutEvent, TRUE, INFINITE); cout << "An intermediate values of n = " << n << ", m = " << m << endl; // разрешаем потокам счетчикам продолжать работу SetEvent(hAddEvent); // ждем завершения потоков W aitForSingleObj ect(hThread_ 1, INFINITE); W aitForSingleObj ect(hThread_2, INFINITE); cout << "A final values of n = " << n << ", m = " << m << endl; CloseHandle(hThread_ 1); CloseHandle(hThread_2); CloseHandle(hOutEvent[0]); CloseHandle(hOutEvent[ 1 ]); CloseHandle(hAddEvent); return 0; } Доступ к существующему событию можно открыть с помощью одной из функций CreateEvent или OpenEvent. Если для этой цели используется функция CreateEvent, то значения параметров bManualReset и bInitialState этой функции игнорируются, так как они уже установлены другим потоком, а поток, вызвавший эту функцию, получает полный доступ к событию с именем, заданным параметром lpName. Теперь рассмотрим функцию OpenEvent, которая используется в случае, если известно, что событие с заданным именем уже существует. Эта функция имеет следующий прототип: HANDLE OpenEvent( DWORD dwDesiredAccess, // флаги доступа BOOL bInheritHandle, // режим наследования LPCTSTR lpName // имя события ); Параметр dwDesiredAccess определяет доступ к событию, и может быть равен любой логической комбинации следующих флагов: E VENT_ALL_ACCES S EVENT_MODIFY_STATE SYNCHRONIZE Флаг EVENT_ALL_ACCESS означает, что поток может выполнять над событием любые действия. Флаг EVENT_MODIFY_STATE означает, что поток может использовать функции SetEvent и ResetEvent для изменения состояния события. Флаг SYNCHRONIZE означает, что поток может использовать событие в функциях ожидания. В завершение параграфа приведем пример синхронизации потоков, выполняющихся в разных процессах, при помощи события с автоматическим сбросом. В этом примере также используется функция OpenEvent для доступа к уже существующему событию. Листинг 9. Пример синхронизации потоков в разных процессах с использованием именованного события #include <windows.h> #include <iostream> using namespace std; 65 HANDLE hInEvent; CHAR lpEventName[]="InEventName"; int main() { char c; hInEvent = OpenEvent(EVENT_MODIFY_STATE, FALSE, lpEventName); if (hInEvent == NULL) { cout << "Open event failed." << endl; cout << "Input any char to exit." << endl; cin >> c; return GetLastError(); } cout << "Input any char: "; cin >> c; // устанавливаем событие о вводе символа SetEvent(hInEvent); // закрываем дескриптор события в текущем процессе CloseHandle(hInEvent); cout << "Now input any char to exit from the process: "; cin >> c; return 0; } Листинг 10. Пример синхронизации потоков в разных процессах // с использованием именованного события #include <windows.h> #include <iostream> using namespace std; HANDLE hInEvent; CHAR lpEventName[] = "InEventName"; int main() { DWORD dwWaitResult; char szAppName[] = "C:\\ConsoleProcess.exe"; STARTUPINFO si; PROCESS_INFORMATION pi; // создаем событие, отмечающее ввод символа hInEvent = CreateEvent(NULL, FALSE, FALSE, lpEventName); if (hInEvent == NULL) return GetLastError(); // запускаем процесс, который ждет ввод символа ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); if (!CreateProcess(szAppName, NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)) return 0; // закрываем дескрипторы этого процесса CloseHandle(pi.hProcess); CloseHandle(pi.hThread); // ждем оповещение о наступлении события от этого процесса dwWaitResult = WaitForSingleObject(hInEvent, INFINITE); if (dwWaitResult != WAIT_OBJECT_0) return dwWaitResult; cout << "A symbol has got." << endl; CloseHandle(hInEvent); cout << "Press any key to exit: "; cin.get(); return 0; } Кратко опишем работу этих программ. Вторая из них запускает первую 66 программу, после чего ждет, пока первая программа не введет какой-нибудь символ. После ввода символа обе программы заканчивают свою работу. Для оповещения второй программы о вводе символа используется именованное событие. Задание на выполнение 1. Используя представленные в работе примеры программ, реализовать приложения, демонстрирующие использование объектов синхронизации мьютексы и события. 2. Разработать приложение для консольных процессов, характеристики которых указаны в соответствующем варианте. 3. Для синхронизации консольных процессов использовать мьютексы и события. 4. Результаты работы представить преподавателю в виде отчета и продемонстрировать функционирующее приложение. Варианты заданий 1. Написать программы для консольного процесса Boss (Резидент) и консольных процессов Scout (Шпион). При реализации синхронизации процессов использовать функции ожидания сигнального состояния объекта только с равным нулю или бесконечности интервалом ожидания. Каждый отдельный процесс открывать в отдельном консольном окне. Для моделирования передачи сообщений ввести специальные события, которые обозначают «точку» и «тире», конец сеанса. Процесс Boss: − запрашивает у пользователя количество процессов Scout, которые он должен запустить; − запускает заданное количество процессов Scout; − принимает от каждого процесса Scout сообщение и выводит его на консоль в одной строке. Принимать сообщение может только от одного процесса, передача остальных сообщений от других процессов должна блокироваться с помощью мьютекса; − завершает свою работу. Процесс Scout: − запрашивает с консоли символы: «-», «.» (событие «тире», событие «точка» ) и передает соответствующие события процессу Boss; − завершает свою работу, когда будет введён символ, обозначающий конец ввода сообщений. 2. Написать программы для консольного процесса Boss (Резидент) и консольных процессов Scout (Шпион). Для моделирования передачи сообщений ввести специальные события, которые обозначают «1» , «2» и конец сеанса для процессов Scout Процесс Boss: − запрашивает у пользователя количество процессов Scout, которые он должен запустить; − запускает заданное количество процессов Scout; 67 − принимает от каждого процесса Scout сообщение и выводит его на консоль в одной строке. Принимать сообщение может только от двух процессов, передача остальных сообщений от других процессов должна блокироваться с помощью мьютексов; − завершает свою работу. Процесс Scout: − запрашивает с консоли сообщения, состоящее из «1» , «2», и передает их (по одному) процессу Boss; − завершает свою работу. 3. Написать программы для консольного процесса Boss и консольных процессов Parent, Child. Для моделирования передачи сообщений ввести специальные события, которые обозначают любые 4-е цифры и конец сеанса для процессов Parent и Child Процесс Boss: − запрашивает у пользователя количество процессов Parent и количество процессов Child, которые он должен запустить; − запрашивает кол-во сообщений, отправленных Parent и Child − запускает заданное количество процессов Parent, Child; − отправляет сообщения для процессов Parent, Child Отправить сообщение может только трём процессам из всех процессов Child и Parent, передача остальных сообщений от других процессов должна блокироваться с помощью мьютексов; − завершает свою работу. Процесс Parent: − получает сообщение, от процесса Boss и выводит его на консоль; − завершает свою работу. Процесс Child: − получает сообщение, от процесса Boss и выводит его на консоль; завершает свою работу. 4. Написать программы для консольного процесса Boss (Резидент) и консольных процессов Scout (Шпион). Для моделирования передачи сообщений ввести специальные события, которые обозначают любые 4-е цифры. Процесс Boss: − запрашивает у пользователя количество процессов Scout, которые он должен запустить; − запрашивает у пользователя пароль (3 цифры); − запускает заданное количество процессов Scout; − принимает от каждого процесса Scout сообщение и выводит его на консоль в одной строке. Принимать сообщение может только от трёх процессов, передача остальных сообщений от других процессов должна блокироваться; − если приходит сообщение, с цифрой не из пароля, то выводит на консоль текст "ошибка"; − завершает свою работу. Процесс Scout: 68 − запрашивает с консоли сообщение, состоящее из цифр, и передает их (по одному) процессу Boss; − завершает свою работу. 5. Написать программы для консольного процесса Boss и консольных процессов Parent, Child. Для моделирования передачи сообщений ввести специальные события, которые обозначают «А» , «В» и конец сеанса для процессов Parent и Child. Процесс Boss: − запрашивает у пользователя количество процессов Parent и количество процессов Child, которые он должен запустить; − запускает заданное количество процессов Parent, Child; − запрашивает кол-во сообщений, полученных от Parent или Child − принимает от каждого процесса Parent, Child сообщение и выводит сообщение и кто его отправил на консоль в одной строке. Принимать сообщение может только от одного процесса Child и одного процесса Parent, передача остальных сообщений от других процессов должна блокироваться с помощью мьютексов; − завершает свою работу. Процесс Parent: − запрашивает с консоли сообщения, состоящее из «А» и передает их (по одному) процессу Boss; − завершает свою работу. Процесс Child: − запрашивает с консоли сообщения, состоящее из «В» » и передает их (по одному) процессу Boss; − завершает свою работу. 6. Написать программы для консольного процесса Administrator и консольных процессов Reader и Writer. Для моделирования передачи сообщений ввести специальные события, которые обозначают сообщение “A”, сообщение “В”, и конец сеанса для процессов Reader и Writer. Одновременно принимать и отправлять сообщения могут только два процесса Writer и два процесса Reader, передача остальных сообщений от других процессов должна блокироваться с помощью мьютексов; Процесс Administrator: − запрашивает у пользователя количество процессов Writer( Reader); − запрашивает у пользователя количество отправленных (полученных) сообщений для процессов Writer (Reader); − запускает заданное количество процессов Reader и Writer; − принимает от каждого процесса Writer сообщение и выводит на консоль, затем отправляет его процессу Reader; − принимает от каждого процесса Reader и Writer сообщение о завершении сеанса и выводит его на консоль в одной строке; − завершает свою работу. Процесс Writer: − запрашивает с консоли сообщения, состоящее из “A" , “В", и передает их 69 (по одному) процессу Administrator; − передает сообщение о завершении сеанса процессу Administrator; − завершает свою работу. Процесс Reader: − принимает сообщение от процесса Administrator; − выводит на консоль сообщение; − передает сообщение о завершении сеанса процессу Administrator; − завершает свою работу. 7. Написать программы для консольного процесса Boss и консольных процессов Parent, Child. Для моделирования передачи сообщений ввести специальные события, которые обозначают «А» , «В», «С» , «D» и конец сеанса для процессов Parent и Child. Процесс Boss: − запрашивает у пользователя количество процессов Parent и количество процессов Child, которые он должен запустить; − запускает заданное количество процессов Parent, Child; − запрашивает количество сообщений, принятых от Parent или Child − принимает от каждого процесса Parent, Child сообщение и выводит сообщение и кто его отправил на консоль в одной строке. Принимать сообщение может только от двух процессов Child и одного процесса Parent, передача остальных сообщений от других процессов должна блокироваться с помощью мьютексов; − завершает свою работу. Процесс Parent: − запрашивает с консоли сообщения, состоящее из «А» , «В» и передает их (по одному) процессу Boss; − завершает свою работу. Процесс Child: − запрашивает с консоли сообщения, состоящее из «С», «D» и передает их (по одному) процессу Boss; − завершает свою работу. 8. Написать программы для консольного процесса Administrator и консольных процессов Reader и Writer. Для моделирования передачи сообщений ввести специальные события, которые обозначают сообщение “A", сообщение “В", и конец сеанса для процессов Reader и Writer. Одновременно принимать и отправлять сообщения могут только один процесс Writer и один процесс Reader, передача остальных сообщений от других процессов должна блокироваться с помощью мьютексов. Процесс Administrator: − запрашивает у пользователя количество процессов Reader и Writer, которые он должен запустить; − запрашивает у пользователя количество отправленных сообщений для процесса Writer и количество принятых сообщений для процесса Reader(соответствие сообщений проверить и подкорректировать по формуле); 70 − запускает заданное количество процессов Reader и Writer; − принимает от каждого процесса Reader и Writer сообщение о завершении сеанса и выводит его на консоль в одной строке. − завершает свою работу. Процесс Writer: − запрашивает с консоли сообщения, и передает их (по одному) процессу Reader; − передает сообщение о завершении сеанса процессу Administrator; − завершает свою работу. Процесс Reader: − принимает сообщение от процесса Writer; − выводит на консоль сообщение; − передает сообщение о завершении сеанса процессу Administrator; − завершает свою работу. 9. Написать программы для консольного процесса Boss и консольных процессов Employee. Для моделирования передачи сообщений ввести специальные события, которые «0» , «1», «2», «3» и конец сеанса для процессов Employee . Процесс Boss: − запрашивает у пользователя количество процессов Employee, которые он должен запустить; − запускает заданное количество процессов Employee; − принимает от каждого процесса Employee сообщение и выводит его на консоль в одной строке. Принимать сообщение может только от трёх процессов, передача остальных сообщений от других процессов должна блокироваться с помощью мьютексов; − завершает свою работу. Процесс Employee: − запрашивает с консоли сообщения, состоящее из «0» , «1», «2», «3», конец сеанса работы и передает (по одному) его процессу Boss; − завершает свою работу. 10. Написать программы для консольного процесса Administrator и консольных процессов Reader и Writer. Для моделирования передачи сообщений ввести специальные события, которые обозначают сообщение “Л”, сообщение “Б”, и конец сеанса для процессов Reader и Writer. Одновременно принимать и отправлять сообщения могут только два процесса Writer и два процесса Reader, передача остальных сообщений от других процессов должна блокироваться с помощью мьютексов; Процесс Administrator: − запрашивает у пользователя количество процессов Reader и Writer, которые он должен запустить; − запрашивает у пользователя кол-во отправленных сообщений для процесса Writer. Кол-во принятых сообщений для процесса Reader вычислить. (соответствие сообщений проверить и подкорректировать по формуле); 71 − запускает заданное количество процессов Reader и Writer; − принимает от каждого процесса Reader и Writer сообщение о завершении сеанса и выводит его на консоль в одной строке. − завершает свою работу. Процесс Writer: − запрашивает с консоли сообщения, и передает их (по одному) процессу Reader; − передает сообщение о завершении сеанса процессу Administrator; − завершает свою работу. Процесс Reader: − принимает сообщение от процесса Writer; − выводит на консоль сообщение; − передает сообщение о завершении сеанса процессу Administrator; − завершает свою работу. 11. Написать программы для консольного процесса Administrator и консольных процессов Reader и Writer. Для моделирования передачи сообщений ввести специальные события, которые обозначают сообщение “Л” , сообщение “Б”, и конец сеанса для процессов Reader и Writer. Одновременно принимать и отправлять сообщения могут только один процесс Writer и два процесса Reader, передача остальных сообщений от других процессов должна блокироваться с помощью мьютексов; Процесс Administrator: − запрашивает у пользователя количество процессов Reader и Writer, которые он должен запустить; − запрашивает у пользователя кол-во отправленных сообщений для процесса Writer. Кол-во принятых сообщений для процесса Reader вычислить. (соответствие сообщений проверить и подкорректировать по формуле); − запускает заданное количество процессов Reader и Writer; − принимает от каждого процесса Reader и Writer сообщение о завершении сеанса и выводит его на консоль в одной строке. − завершает свою работу. Процесс Writer: − запрашивает с консоли сообщения, и передает их (по одному) процессу Reader; − передает сообщение о завершении сеанса процессу Administrator; − завершает свою работу. Процесс Reader: − принимает сообщение от процесса Writer; − выводит на консоль сообщение; − передает сообщение о завершении сеанса процессу Administrator; − завершает свою работу. Контрольные вопросы 1. Поясните цели синхронизации процессов и потоков многозадачных ОС. 72 2. Что такое мьютексы (mutex) и чем они отличаются от критических областей? 3. Какие функции и типы Windows API используются для получения доступа к мьютексу, его захвата и освобождения? 4.Что такое события (event) в Windows API и для чего они могут использоваться? 5. Какие существуют типы событий и чем они отличаются друг от друга? 6. Какими функциями Windows API осуществляется работа с событиями? 7. В каком случае событие, сбрасываемое вручную, всё-таки сбрасывается автоматически? 8. Поясните общие свойства и различия при использовании событий и мьютексов. Лабораторная работа №6 Тема: Обмен данными по анонимному каналу с сервером Цель работы: 1. Изучение механизмов межпроцессного обмена в ОС семейства Windows. 2. Изучить функции для работы с анонимными каналами. 3. В соответствии с заданным вариантом разработать приложение, реализующее обмен данными между процессами с помощью анонимных каналов. Краткое теоретическое введение 1. Функции для работы с анонимными каналами 1.1. Создание анонимных каналов Анонимные каналы создаются процессом сервером при помощи функции CreatePipe, которая имеет следующий прототип: BOOL CreatePipe ( PHANDLE hReadHandle, // дескриптор для чтения из канала PHANDLE hWriteHandle, // дескриптор для записи в канал LPSECURITY_ATTRIBUTES lpPipeAttributes, // атрибуты защиты DWORD dwSize // размер буфера в байтах ); При удачном завершении функция CreatePipe возвращает значение TRUE, а в случае неудачи – FALSE. Отметим, что операционные системы Windows автоматически определяют размер буфера и поэтому значение параметра dwSize можно установить равным 0, тогда операционная система выберет размер буфера по умолчанию. 1.2 Соединение клиентов с анонимным каналом Так как анонимные каналы не имеют имени, то для соединения процессаклиента с таким каналом необходимо передать клиенту один из дескрипторов 73 анонимного канала. При этом передаваемый дескриптор должен быть наследуемым. Наследование дескрипторов анонимного канала определяется значением поля bInheritHandle в структуре типа SECURITY_ATTRIBUTES, на которую указывает параметр lpPipeAttributes функции CreatePipe. В ранних версиях Windows эта задача решалась путем создания соответственно ненаследуемого или наследуемого дубликата исходного дескриптора, используя функцию DuplicateHandle. После этого исходный дескриптор закрывается. В операционной системе Windows 2000 эта задача может быть также решена, используя функцию SetHandleInformation, которая изменяет свойство наследования дескриптора. Передача наследуемого дескриптора клиенту может выполняться одним из следующих способов: − через командную строку; − через поля hStdInput, hStdOutput и hStdError структуры STARTUPINFO; − посредством сообщения WM_COPYDATA; − через файл. В данной работе будут использованы только первых два способа передачи дескрипторов процессу-клиенту. Третьим способом можно пользоваться только процессам с графическим интерфейсом (GUI). 1.3 Обмен данными по анонимному каналу Для обмена данными по анонимному каналу в операционных системах Windows используются те же функции, что для записи и чтения данных в файл. Для записи данных в анонимный канал используется функция WriteFile, которая имеет следующий прототип: BOOL WriteFile( HANDLE анонимного канала LPCVOID DWORD записи LPDWORD байт LPOVERLAPPED ); hAnonymousPipe, // дескриптор lpBuffer, // буфер данных dwNumberOffiytesToWrite, // число lpNumberOfBytesWritten, lpOverlapped // число байт для записанных // асинхронный ввод Функция WriteFile записывает в анонимный канал количество байт, заданных параметром dwNumberOfBytesToWrite, из буфера данных, на который указывает параметр lpBuffer. Дескриптор вывода этого анонимного канала должен быть задан первым параметром функции WriteFile. Для чтения данных из анонимного канала используется функция ReadFile, которая имеет следующий прототип BOOL ReadFile ( HANDLE hAnonymousPipe, // анонимного канала LPCVOID lpBuffer, // буфер данных DWORD dwNumberOffiytesToRead, // число записи 74 дескриптор байт для LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped ); // число записанных байт // асинхронный ввод Функция ReadFile читает из анонимного канала количество байт, заданных параметром dwNumberOfBytesToRead, в буфер данных, на который указывает параметр lpBuffer. Дескриптор ввода этого ананимного канала должен быть задан первым параметром функции ReadFile. Также как и в случае записи в анонимный канал параметр lpOverlapped должен быть равен NULL. Следует помнить, что обмен данными по анонимному каналу осуществляется только в соответствии с назначением дескриптора этого канала. Дескриптор для записи в анонимный канал должен быть параметром функции WriteFile, а дескриптор для чтения из анонимного канала должен быть параметром функции ReadFile. В этом и состоит смысл передачи данных по анонимному каналу только в одном направлении. Один и тот же процесс может, как писать данные в анонимный канал, так и читать данные из него, должным образом используя дескрипторы этого анонимного канала. После завершения обмена данными по анонимному каналу, потоки должны закрыть дескрипторы записи и чтения анонимного канала, используя функцию CloseHandle. 2. Примеры работы с анонимными каналами Вначале рассмотрим простой пример, в котором процесс-сервер выполняет следующие действия: − создание анонимного канала; − создание дочернего процесса; − передача созданному дочернему процессу одного из дескрипторов созданного анонимного канала, используя для этого командную строку. В этом случае дочерний процесс будет клиентом анонимного канала. Для определенности передадим клиенту дескриптор для записи в анонимный канал и оставим серверу дескриптор для чтения. Сначала приведем программу процесса-клиента анонимного канала. Листинг 1. Пример процесса клиента анонимного канала. // Клиент пишет в анонимный канал. // Дескриптор анонимного канала передается клиенту через командную строку. #include <windows.h> #include <conio.h> int main(int argc, char *argv[]) { HANDLE hWritePipe; // преобразуем символьное представление дескриптора в число hWritePipe = (HANDLE)atoi(argv[1]); // ждем команды о начале записи в анонимный канал _cputs("Press any key to start communication.\n"); _getch(); // пишем в анонимный канал for (int i = 0; i < 10; i++) { 75 DWORD dwBytesWritten; if (!WriteFile( hWritePipe, &i, sizeof(i), &dwBytesWritten, NULL)) { _cputs("Write to file failed.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } _cprintf("The number %d is written to the pipe.\n", i); Sleep(500); } // закрываем дескриптор канала CloseHandle(hWritePipe); _cputs("The process finished writing to the pipe.\n"); _cputs("Press any key to exit.\n"); _getch(); return 0; } Теперь приведем программу процесса-сервера анонимного канала, который запускает клиента и передает ему через командную строку дескриптор записи в анонимный канал. Листинг 2. Пример процесса сервера анонимного канала. // Сервер читает из анонимного канала. // Дескриптор анонимного канала передается // клинету через командную строку. #include <windows.h> #include <conio.h> int main() { char lpszComLine[80]; // для командной строки STARTUPINFO si; PROCESS_INFORMATION pi; HANDLE hWritePipe, hReadPipe, hInheritWritePipe; // создаем анонимный канал if(!CreatePipe( &hReadPipe, // дескриптор для чтения &hWritePipe, // дескриптор для записи NULL, // атрибуты защиты по умолчанию, // в этом случае дескрипторы // hReadPipe и hWritePipe ненаследуемые 0)) // размер буфера по умолчанию { _cputs("Create pipe failed.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } // делаем наследуемый дубликат дескриптора hWritePipe if(!DuplicateHandle( GetCurrentProcess(), // дескриптор текущего процесса hWritePipe, // исходный дескриптор канала 76 GetCurrentProcess(), // дескриптор текущего процесса &hInheritWritePipe, // новый дескриптор канала 0, // этот параметр игнорируется TRUE, // новый декскриптор наследуемый DUPLICATE_SAME_ACCESS ))// доступ не изменяем { _cputs("Duplicate handle failed.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } // закрываем ненужный дескриптор CloseHandle(hWritePipe); // устанавливаем атрибуты нового процесса ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); // формируем командную строку wsprintf(lpszComLine, "C:\\Client.exe %d", (int)hInheritWritePipe); // запускаем новый консольный процесс if (!CreateProcess( NULL, // имя процесса lpszComLine, // командная строка NULL, // атрибуты защиты процесса по умолчанию NULL, // атрибуты защиты первичного потока по умолчанию TRUE, // наследуемые дескрипторы текущего процесса // наследуются новым процессом CREATE_NEW_CONSOLE, // новая консоль NULL, // используем среду окружения процесса предка NULL, // текущий диск и каталог, как и в процессе предке &si, // вид главного окна - по умолчанию &pi // здесь будут дескрипторы и идентификаторы // нового процесса и его первичного потока )) { _cputs("Create process failed.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } // закрываем дескрипторы нового процесса CloseHandle(pi.hProcess); CloseHandle(pi.hThread); // закрываем ненужный дескриптор канала CloseHandle(hInheritWritePipe); // читаем из анонимного канала for (int i = 0; i < 10; i++) { int nData; DWORD dwBytesRead; if (!ReadFile( hReadPipe, &nData, sizeof(nData), &dwBytesRead, NULL)) { _cputs("Read from the pipe failed.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } _cprintf("The number %d is read from the pipe.\n", nData); } // закрываем дескриптор канала CloseHandle(hReadPipe); _cputs("The process finished reading from the pipe.\n"); _cputs("Press any key to exit.\n"); _getch(); return 0; } В следующих программах показывается, как организовать двусторонний обмен данными по анонимному каналу между клиентом и сервером. Для этого дескрипторы чтения и записи в анонимный канал используются как сервером, так и клиентом этого анонимного канала. В этом примере также сначала 77 приведена программа процесса-клиента анонимного канала. Листинг 3. Пример процесса клиента анонимного канала. // Клиент сначала пишет в анонимный канал, а потом читает из него. // Дескриптор анонимного канала передается клиенту через командную строку. #include <windows.h> #include <conio.h> int main(int argc, char *argv[]) { HANDLE hWritePipe, hReadPipe; HANDLE hEnableRead; // для синхронизации обмена данными char lpszEnableRead[] = "EnableRead"; // открываем событие, разрешающее чтение hEnableRead = OpenEvent(EVENT_ALL_ACCESS, FALSE, lpszEnableRead); // преобразуем символьное представление дескрипторов в число hWritePipe = (HANDLE)atoi(argv[1]); hReadPipe = (HANDLE)atoi(argv[2]); // ждем команды о начале записи в анонимный канал _cputs("Press any key to start communication.\n"); _getch(); // пишем в анонимный канал for (int i = 0; i < 10; i++) { DWORD dwBytesWritten; if (!WriteFile( hWritePipe, &i, sizeof(i), &dwBytesWritten, NULL)) { _cputs("Write to file failed.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } _cprintf("The number %d is written to the pipe.\n", i); } _cputs("The process finished writing to the pipe.\n"); // ждем разрешения на чтение WaitForSingleObject(hEnableRead, INFINITE); // читаем ответ из анонимного канала for (int j = 0; j < 10; j++) { int nData; DWORD dwBytesRead; if (!ReadFile( hReadPipe, &nData, sizeof(nData), &dwBytesRead, NULL)) { _cputs("Read from the pipe failed.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } _cprintf("The number %d is read from the pipe.\n", nData); } _cputs("The process finished reading from the pipe.\n"); _cputs("Press any key to exit.\n"); _getch(); // закрываем дескрипторы канала CloseHandle(hWritePipe); CloseHandle(hReadPipe); CloseHandle(hEnableRead); return 0; } Теперь приведем текст программы процесса-сервера анонимного канала, который запускает клиента и передает ему дескрипторы анонимного канала через командную строку. Листинг 4. Пример процесса сервера анонимного канала. // Сервер сначала читает из анонимного канала, а затем пишет в него. 78 // Дескриптор анонимного канала передается клиенту через командную строку. #include <windows.h> #include <conio.h> int main() { char lpszComLine[80]; // для командной строки HANDLE hEnableRead; // для синхронизации обмена данными char lpszEnableRead[] = "EnableRead"; STARTUPINFO si; PROCESS_INFORMATION pi; HANDLE hWritePipe, hReadPipe; SECURITY_ATTRIBUTES sa; // создаем событие для синхронизации обмена данными hEnableRead = CreateEvent(NULL, FALSE, FALSE, lpszEnableRead); // устанавливает атрибуты защиты канала sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.lpSecurityDescriptor = NULL; // защита по умолчанию sa.bInheritHandle = TRUE; // дескрипторы наследуемые // создаем анонимный канал if(!CreatePipe( &hReadPipe, // дескриптор для чтения &hWritePipe, // дескриптор для записи &sa, // атрибуты защиты по умолчанию, // дескрипторы наследуемые 0)) // размер буфера по умолчанию { _cputs("Create pipe failed.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } // устанавливаем атрибуты нового процесса ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); // формируем командеую строку wsprintf(lpszComLine, "C:\\Client.exe %d %d", (int)hWritePipe, (int)hReadPipe); // запускаем новый консольный процесс if (!CreateProcess( NULL, // имя процесса lpszComLine, // командная строка NULL, // атрибуты защиты процесса по умолчанию NULL, // атрибуты защиты первичного потока по умолчанию TRUE, // наследуемые дескрипторы текущего процесса // наследуются новым процессом CREATE_NEW_CONSOLE, // новая консоль NULL, // используем среду окружения процесса предка NULL, // текущий диск и каталог, как и в процессе предке &si, // вид главного окна - по умолчанию &pi // здесь будут дескрипторы и идентификаторы // нового процесса и его первичного потока )) { _cputs("Create process failed.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } // закрываем дескрипторы нового процесса CloseHandle(pi.hProcess); CloseHandle(pi.hThread); // читаем из анонимного канала for (int i = 0; i < 10; i++) { int nData; DWORD dwBytesRead; if (!ReadFile( hReadPipe, &nData, sizeof(nData), &dwBytesRead, NULL)) { _cputs("Read from the pipe failed.\n"); _cputs("Press any key 79 to finish.\n"); _getch(); return GetLastError(); } _cprintf("The number %d is read from the pipe.\n", nData); } _cputs("The process finished reading from the pipe.\n"); // даем сигнал на разрешение чтения клиентом SetEvent(hEnableRead); // пишем ответ в анонимный канал for (int j = 10; j < 20; j++) { DWORD dwBytesWritten; if (!WriteFile( hWritePipe, &j, sizeof(j), &dwBytesWritten, NULL)) { _cputs("Write to file failed.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } _cprintf("The number %d is written to the pipe.\n", j); } // закрываем дескрипторы канала CloseHandle(hReadPipe); CloseHandle(hWritePipe); CloseHandle(hEnableRead); _cputs("The process finished writing to the pipe.\n"); _cputs("Press any key to exit.\n"); _getch(); return 0; } Отметим в последнем примере следующий момент. Для организации двустороннего обмена данными по анонимному каналу, сервер и клиенты анонимного канала должны синхронизировать доступ к этому каналу. То есть для организации передачи данных необходимо разработать протокол передачи данных или использовать объекты синхронизации, которые исключают одновременный неконтролируемый доступ параллельных потоков к анонимному каналу. В приведенном примере событие hEnableRead сигнализирует клиенту, что сервер закончил чтение данных и теперь данные из канала может читать клиент. При отсутствии такой синхронизации возможно одновременное чтение данных сервером и клиентом, так как они работают параллельно, что вызовет неправильную работу программы и её зависание. 3. Перенаправление стандартного ввода-вывода Анонимные каналы часто используются для перенаправления стандартного ввода-вывода. Чтобы подробнее разобраться с этим вопросом, сначала кратко рассмотрим стандартные средства ввода-вывода, используемые в языке С++. Компилятор языка C++ фирмы Microsoft содержит стандартную библиотеку, которая поддерживает три варианта функций стандартного вводавывода. Описания этих функций находятся в следующих заголовочных файлах: <stdio.h>, <iostream.h> и <conio.h>. Функции ввода-вывода, которые описаны в заголовке <stdio.h>, обеспечивают ввод-вывод в следующие стандартные потоки: stdin – стандартный файл ввода; stdout – стандартный файл вывода; stderr – файл вывода сообщений об ошибках. Эти функции составляют стандартную библиотеку ввода-вывода языка С. Функции и операторы ввода- 80 вывода, которые описаны в заголовке <iostream.h>, обеспечивают ввод-вывод в стандартные потоки ввода-вывода cin, cout, cerr. Эти функции составляют стандартную библиотеку ввода-вывода языка С++. При создании консольного процесса или при распределении консоли приложением с графическим интерфейсом, стандартные потоки ввода-вывода связываются с дескрипторами, которые заданы в полях hStdInput, hStdOutput и hStdError структуры типа STARTUPINFO. Поэтому, если в эти поля будут записаны соответствующие дескрипторы анонимного канала, то для передачи данных по анонимному каналу можно использовать функции стандартного ввода-вывода. Такая процедура называется перенаправлением стандартного ввода-вывода. Функции ввода-вывода, которые поддерживаются заголовком <conio.h>, отличаются от функций стандартной библиотеки ввода-вывода языка С только тем, что они всегда связываются с консолью. Поэтому эти функции можно использовать для ввода-вывода на консоль даже в случае перенаправления стандартного ввода-вывода. Ниже приведены программы, в которых стандартный ввод-вывод перенаправляется в анонимный канал, а для обмена данными по анонимному каналу используются перегруженные операторы ввода-вывода. Пример включает программы следующих процессов: два процесса клиента, которые обмениваются данными по анонимному каналу, и процесс сервер, который создает клиентов и передает им дескрипторы анонимного канала через поля структуры STARTUPINFO. Сначала приведем программы, которые описывают процессы клиенты. // Пример обмена данными по анонимному каналу, // используя перенаправленные стандартные потоки ввода-вывода. // Дескрипторы анонимного канала передаются через поля структуры STARTUPINFO. #include <windows.h> #include <conio.h> #include <iostream.h> int main() { // события для синхронизации обмена данными HANDLE hReadFloat, hReadText; char lpszReadFloat[] = "ReadFloat"; char lpszReadText[] = "ReadText"; // открываем события hReadFloat = CreateEvent(NULL, FALSE, FALSE, lpszReadFloat); hReadText = CreateEvent(NULL, FALSE, FALSE, lpszReadText); // ждем команды о начале записи в анонимный канал _cputs("Press any key to start communication.\n"); _getch(); // пишем целые числа в анонимный канал for (int i = 0; i < 5; ++i) { Sleep(500); cout << i << endl; } // ждем разрешение на чтение дробных чисел из канала WaitForSingleObject(hReadFloat, INFINITE); // читаем дробные числа из анонимного канала for (int j = 0; j < 5; ++j) { float nData; cin >> nData; _cprintf("The number %2.1f is read from the pipe.\n", nData); } 81 } // отмечаем, что можно читать текст из анонимного канала SetEvent(hReadText); // теперь передаем текст cout << "This is a demo sentence." << endl; // отмечаем конец передачи cout << '\0' << endl; _cputs("The process finished transmission of data.\n"); _cputs("Press any key to exit.\n"); _getch(); CloseHandle(hReadFloat); CloseHandle(hReadText); return 0; Листинг 5. Пример обмена данными по анонимному каналу. // используя перенаправленные стандартные потоки ввода-вывода. // Дескрипторы анонимного канала передаются через поля структуры STARTUPINFO. #include <windows.h> #include <conio.h> #include <iostream.h> int main() { // события для синхронизации обмена данными HANDLE hReadFloat, hReadText; char lpszReadFloat[] = "ReadFloat"; char lpszReadText[] = "ReadText"; // открываем события hReadFloat = CreateEvent(NULL, FALSE, FALSE, lpszReadFloat); hReadText = CreateEvent(NULL, FALSE, FALSE, lpszReadText); // читаем целые числа из анонимного канала for (int i = 0; i < 5; ++i) { int nData; cin >> nData; _cprintf("The number %d is read from the pipe.\n", nData); } // разрешаем читать дробные числа из анонимного канала SetEvent(hReadFloat); // пишем дробные числа в анонимный канал for (int j = 0; j < 5; ++j) } { Sleep(500); cout << (j*0.1) << endl; } // ждем разрешения на чтение текста WaitForSingleObject(hReadText, INFINITE); _cputs("The process read the text: "); // теперь читаем текст char lpszInput[80]; do { Sleep(500); cin >> lpszInput; _cputs(lpszInput); _cputs(" "); } while (*lpszInput != '\0'); _cputs("\nThe process finished transmission of data.\n"); _cputs("Press any key to exit.\n"); _getch(); CloseHandle(hReadFloat); CloseHandle(hReadText); return 0; Теперь приведем программу, которая описывает сервер анонимного 82 канала. Эта программа просто создает двух клиентов анонимного канала и прекращает свою работу. Листинг 6. Пример процесса сервера анонимного канала. // Сервер создает анонимный канал, а затем два процесса клиента // анонимного канала, которые обмениваются между собой данными по этому каналу. // Дескрипторы анонимного канала передаются клиентам через поля структуры STARTUPINFO. #include <windows.h> #include <conio.h> int main() { char lpszComLine1[80] = "C:\\Client1.exe"; // имя первого клиента char lpszComLine2[80] = "C:\\Client2.exe"; // имя второго клиента STARTUPINFO si; PROCESS_INFORMATION pi; HANDLE hWritePipe, hReadPipe; SECURITY_ATTRIBUTES sa; // устанавливает атрибуты защиты канала sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.lpSecurityDescriptor = NULL; // защита по умолчанию sa.bInheritHandle = TRUE; // дескрипторы наследуемые // создаем анонимный канал if(!CreatePipe( &hReadPipe, // дескриптор для чтения &hWritePipe, // дескриптор для записи &sa, // атрибуты защиты по умолчанию, // дескрипторы наследуемые 0)) // размер буфера по умолчанию { _cputs("Create pipe failed.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } // устанавливаем атрибуты нового процесса ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); // использовать стандартные дескрипторы si.dwFlags = STARTF_USESTDHANDLES; // устанавливаем стандартные дескрипторы si.hStdInput = hReadPipe; si.hStdOutput = hWritePipe; si.hStdError = hWritePipe; // запускаем первого клиента if (!CreateProcess( NULL, // имя процесса lpszComLine1, // командная строка NULL, // атрибуты защиты процесса по умолчанию NULL, // атрибуты защиты первичного потока по умолчанию TRUE, // наследуемые дескрипторы текущего процесса // наследуются новым процессом CREATE_NEW_CONSOLE, // создаем новую консоль NULL, // используем среду окружения процесса предка NULL, // текущий диск и каталог как и в процессе предке &si, // вид главного окна - по умолчанию &pi // здесь будут дескрипторы и идентификаторы // нового процесса и его первичного потока ) ) { _cputs("Create process failed.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } // закрываем дескрипторы первого клиента 83 CloseHandle(pi.hProcess); CloseHandle(pi.hThread); // запускаем второго клиента if (!CreateProcess( NULL, // имя процесса lpszComLine2, // командная строка NULL, // атрибуты защиты процесса по умолчанию NULL, // атрибуты защиты первичного потока по умолчанию TRUE, // наследуемые дескрипторы текущего процесса // наследуются новым процессом CREATE_NEW_CONSOLE, // создаем новую консоль NULL, // используем среду окружения процесса предка NULL, // текущий диск и каталог как и в процессе предке &si, // вид главного окна - по умолчанию &pi // здесь будут дескрипторы и идентификаторы // нового процесса и его первичного потока ) ) { _cputs("Create process failed.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } // закрываем дескрипторы второго клиента CloseHandle(pi.hProcess); CloseHandle(pi.hThread); // закрываем дескрипторы канала CloseHandle(hReadPipe); CloseHandle(hWritePipe); _cputs("The clients are created.\n"); _cputs("Press any key to exit.\n"); _getch(); return 0; } Задание на выполнение 1. Изучить теоретический материал, посвященный обмену данных между процессами посредством анонимных каналов. 2. Используя представленные в работе примеры программ, реализовать приложения, демонстрирующие использование анонимных каналов при межпроцессном обмене. 3. В соответствии с заданным вариантом разработать приложение для консольных процессов, которые обмениваются сообщениями по анонимному каналу. 4. Результаты работы представить преподавателю в виде отчета и продемонстрировать функционирующее приложение. Варинты заданий 1. Написать программы трёх консольных процессов Server и Client, которые обмениваются сообщениями по анонимному каналу. Сообщения бывают двух типов: 1. сообщения первого типа содержат размер буфера экрана и размер курсора для консольного приложения; 2. сообщения второго типа содержат массив символов. 84 Одновременно сообщение может передаваться только одним из процессов. Процесс- Server, который выполняет следующие действия. − Запрашивает размер буфера экрана и размер курсора. − Размер массива вводится с консоли. − Запускает процесс Client. − Получает и передает от процесса-Client по анонимным каналам массив символов. Выводит полученные результаты и переданные массив на консоль. − Передача сообщения первого типа инициируется нажатием правой кнопки мыши. Передача сообщения второго типа инициируется посредством нажатия клавиши “G” на клавиатуре. − Закончить работу , после нажатия левой клавиши мыши. Процесс- Client , который выполняет следующие действия. − Генерирует символы и передает их по анонимному каналу процессусерверу. − Получает по анонимному каналу размер буфера экрана и размер курсора от Server и устанавливает их; − Передача сообщения второго типа инициируется нажатием правой кнопки. − Выводит сгенерированный массив на консоль. − Заканчивает работу. 2. Написать программы трёх консольных процессов Server и Client, Part которые обмениваются сообщениями по анонимному каналу. Одновременно сообщение может передаваться только одним из процессов. Процесс- Server, который выполняет следующие действия. − Создает массив, для хранения вещественных чисел. − Размер массива вводится с консоли. − Запускает процесс Client. − Запускает процесс Part. − Передёт размер массива процессам Client, Part. Получает и передает по анонимным каналам массив чисел. Выводит полученные результаты и переданные массив на консоль. Элементы массива передаются посимвольно. − Повторяет запрос массива от процесса-Client посредством нажатия любой клавиши на клавиатуре. − Закончить работу после нажатия клавиши - “Enter”. Процесс- Client , который выполняет следующие действия. − Запрашивает у пользователя размер буфера экрана и размер курсора консоли и устанавливает их. − Генерирует вещественные числа и передает их по анонимному каналу процессу-серверу. − Выводит сгенерированный массив на консоль. Процесс-Part, который выполняет следующие действия. − Получает массив чисел по анонимному каналу от процесса-сервера − Запрашивает число вещественные числа N и M (N < M ). − Определяет какие из чисел попали в отрезок [N,M], передаёт их по 85 анонимному каналу процессу-серверу. − Передача массива инициируется нажатием левой кнопки мыши. − Выводит полученные числа на консоль. 3. Написать программы трёх консольных процессов Server и Client, Sum которые обмениваются сообщениями по анонимному каналу. Одновременно сообщение может передаваться только одним из процессов. Процесс-Server, который выполняет следующие действия. − Размер массива вводится с консоли. − Запускает процесс Client. − Запускает процесс Sum. − Передача размера массива инициируется нажатием правой кнопки мыши. − Получает и передаёт по анонимным каналам массив чисел. Выводит полученные результаты и переданные массив на консоль. − Передача массив процессу Sum инициируется нажатием левой кнопки мыши. Элементы массива передаются посимвольно. − Повторяет запрос массива от процесса-Client посредством нажатия любой клавиши на клавиатуре. − Закончить работу после нажатия клавиши - “Q”. Процесс-Client , который выполняет следующие действия. − Запрашивает у пользователя размер буфера экрана и размер курсора и устанавливает их. − Получает от сервера размер массива. − Генерирует целые числа и передает их по анонимному каналу процессусерверу. − Выводит сгенерированный массив на консоль. Процесс-Sum, который выполняет следующие действия. − Получает массив символов по анонимному каналу от процесса-сервера − Запрашивает с консоли число N. − Вычисляет сумму квадратов чисел масcива, больших N − Передаёт cумму по анонимному каналу процессу-серверу. − Передача сообщения инициируется двойным нажатием левой кнопки. − Выводит полученное число на консоль. 4. Написать программы двух консольных процессов Server, Mult, Sum которые обмениваются сообщениями по анонимному каналу. Сообщения бывают двух типов: 1. сообщения первого типа содержат размер курсора и цвет фона консоли; 2. сообщения второго типа содержат массив символов. Одновременно сообщение может передаваться только одним из процессов. Процесс- Server, который выполняет следующие действия. − Запрашивает размер массива. − Запрашивает у пользователя размер курсора и цвет фона консоли; − Генерирует целые числа и передает их по анонимному каналу процессу-серверу. − Запускает процессы Mult, Sum. − Получает и передает по анонимным каналам массив символов. 86 Выводит полученные результаты и переданные массив на консоль. − Передача первого сообщения инициируется нажатием левой кнопки мыши. В этом случае данный процесс передаёт размер курсора и цвет фона консоли процессу Sum. − Передача второго сообщения инициируется нажатием клавиши «G» на клавиатуре. В этом случае данный процесс передаёт массив чисел другим процессам. − Запрашивает результат от процессов- Mult, Sum посредством нажатия клавиши «S» на клавиатуре. − Закончить работу после нажатия клавиши - “Enter” Процесс-Sum, который выполняет следующие действия. − Получает массив чисел от сервера. − Получает размер курсора и цвет фона консоли, устанавливает их; − Вычисляет сумму чисел массива. − Передаёт число серверу. − Выводит сумму на консоль. Процесс-Mult, который выполняет следующие действия. − Получает массив чисел от сервера. − Вычисляет произведение чисел массива − Передаёт число серверу. − Выводит сумму на консоль 5. Написать программы трёх консольных процессов Server и Client, Sort которые обмениваются сообщениями по анонимному каналу. Одновременно сообщение может передаваться только одним из процессов. Процесс- Server, который выполняет следующие действия. − Размер массива вводится с консоли. − Запускает процесс Client. − Запускает процесс Sort. − Передача размер массива другому процессу сообщения инициируется нажатием правой кнопки мыши. − Получает по анонимным каналам массив чисел. Выводит полученные результаты и переданные массивы на консоль. − Запрашивает массив от процесса Client посредством нажатия любой клавиши на клавиатуре. − Получает массив от процесса Sort; − Закончить работу после нажатия клавиши - “Q”. Процесс-Client , который выполняет следующие действия. − Получает размер массива .Генерирует целые числа и передает их по анонимному каналу процессу-серверу. − Выводит сгенерированный массив на консоль. − Передаёт массив процессу Server. Элементы массива передаются посимвольно. Процесс-Sort, который выполняет следующие действия. − Получает массив символов по анонимному каналу от процесса Server; − Сортирует массив; − Запрашивает у пользователя размер окна консоли и размер курсора 87 консоли и устанавливает их. − Передаёт отсортированный массив по анонимному каналу процессу. − Передача сообщения Server инициируется двойным нажатием левой кнопки мыши. − Элементы массива передаются посимвольно. − Выводит полученный массив на консоль. 6. Написать программы трёх консольных процессов Server и Client, Hight которые обмениваются сообщениями по анонимному каналу. Одновременно сообщение может передаваться только одним из процессов. Процесс-Server, который выполняет следующие действия. − Создает массив, для хранения вещественных чисел. − Размер массива вводится с консоли. − Запрашивает у пользователя размер курсора экрана и размер буфера окна; − Запускает процесс Client и процесс Hignt. − Получает и передает по анонимным каналам массив чисел. Выводит полученные результаты и переданные массив на консоль. Элементы массива передаются посимвольно. − Передача массива процессу Client, размер курсора экрана и размер буфера окна инициируется нажатием левой кнопки мыши. − Закончить работу после нажатия клавиши – “E”. Процесс- Client , который выполняет следующие действия. − Генерирует вещественные числа. − Получает размер курсора экрана и размер буфера окна и устанавливает их. − Передаёт массив процессу Server по анонимному каналу, посредством нажатия любой клавиши на клавиатуре. − Выводит сгенерированный массив на консоль. Процесс-Hignt, который выполняет следующие действия. − Получает массив чисел по анонимному каналу от процесса- Server, посредством нажатия правой клавиши мыши. − Запрашивает вещественное число N. − Определяет какие из чисел >N передаёт их по анонимному каналу процессу-серверу. − Выводит полученные числа на консоль. 7. Написать программы трёх консольных процессов Server и Client, Simple которые обмениваются сообщениями по анонимному каналу. Сообщения бывают двух типов: 1. сообщения первого типа содержат цвет фона и размер курсора для консольного приложения; 2. сообщения второго типа содержат массив чисел. Одновременно сообщение может передаваться только одним из процессов. Процесс- Server, который выполняет следующие действия. − Размер массива вводится с консоли. − Запрашивает в произвольной форме (лучше, используя палитру цветов) у пользователя цвета фона и размер курсора консоли; 88 − Запускает процесс Client. − Запускает процесс Simple. − Получает и передает по анонимным каналам массив чисел. Выводит полученные и переданные числа на консоль. − Передача сообщения первого типа инициируется нажатием правой кнопки мыши. В этом случае данный процесс устанавливает параметры их на своей консоли и передать их другому процессу. − Передача сообщения второго типа инициируется нажатием левой кнопки мыши. Элементы массива передаются посимвольно. − Закончить работу после нажатия клавиши - “Ctrl”. Процесс- Client , который выполняет следующие действия. − Генерирует целые числа и передает их по анонимному каналу процессу-серверу. − Получает цвет фона и размер курсора от Server и устанавливает их; − Размер массива запрашивается с консоли. − Передача сообщения второго типа инициируется нажатием кнопки на клавиатуре. − Выводит сгенерированные числа на консоль. Процесс-Simple, который выполняет следующие действия. − Получает массив числа по анонимному каналу от процесса-сервера − Находит и передает простые числа по анонимному каналу процессусерверу. − Передача сообщения второго типа инициируется двойным нажатием левой кнопки мыши. Элементы массива передаются посимвольно. − Выводит полученные числа на консоль. 8. Написать программы трёх консольных процессов Server и Client, Small которые обмениваются сообщениями по анонимному каналу. Сообщения бывают двух типов: 1. сообщения первого типа содержат цвет фона и текста консольного приложения; 2. сообщения второго типа содержат массив чисел. Одновременно сообщение может передаваться только одним из процессов. Процесс- Server, который выполняет следующие действия. − Размер массива вводится с консоли. − Запрашивает (использовать палитру цветов) у пользователя цвета фона и цвет текста консоли; − Запускает процесс Client. − Запускает процесс Small. − Передает по анонимным каналам размер массива; − Получает и передает по анонимным каналам массив чисел. Выводит полученные и переданные числа на консоль. − Передача сообщения первого типа инициируется нажатием правой кнопки. − Передача сообщения второго типа инициируется нажатием кнопки. Alt − Элементы массива передаются посимвольно. − Закончить работу 89 Процесс- Client , который выполняет следующие действия. − Генерирует целые числа и передает их по анонимному каналу процессу-серверу. − Получает цвет фона и размер курсора от Server и устанавливает их; − Передача сообщения второго типа инициируется нажатием левой кнопки. − Выводит сгенерированные числа на консоль. Процесс- Small, который выполняет следующие действия. - Получает цвет фона и размер курсора от Server и устанавливает их; − Получает размер массива и массив чисел по анонимному каналу от процесса-сервера − Запрашивает вещественное число N. − Определяет какие из чисел >0 и <N передаёт их по анонимному каналу процессу-серверу. − Передача сообщения второго типа инициируется двойным нажатием левой кнопки. − Элементы массива передаются посимвольно. - Выводит полученные числа на консоль. 9. Написать программы трёх консольных процессов Server и Client, Alfavit которые обмениваются сообщениями по анонимному каналу. Сообщения бывают двух типов: 1. сообщения первого типа содержат цвет фона и координаты позиции курсора экрана для вывода с этой позиции символов для консольного приложения; 2. сообщения второго типа содержат массив символов. Одновременно сообщение может передаваться только одним из процессов. Процесс- Server, который выполняет следующие действия. − Размер массива вводится с консоли. − Запрашивает в произвольной форме (лучше, используя палитру цветов) у пользователя цвета фона, координаты позиции курсора экрана; − Запускает процесс Client. − Запускает процесс Alfavit . − Получает и передает по анонимным каналам массив символов. Выводит полученные и переданные символы на консоль. − Передача сообщения первого типа процессу Alfavit инициируется нажатием кнопки на клавиатуре. − Передача сообщения второго типа инициируется двойным нажатием правой кнопки мыши. Элементы массива передаются посимвольно. − Закончить работу после нажатия клавиши - “Esc”. Процесс- Client , который выполняет следующие действия. − Получает размер массива от процесса Server − Генерирует символы и передает их по анонимному каналу процессусерверу. − Передача сообщения второго типа инициируется нажатием левой кнопки мыши. 90 − Символы передаются посимвольно. − Выводит произведенные символы консоль. Процесс-Alfavit, который выполняет следующие действия. − Получает цвет фона и координаты позиции курсора от Server и устанавливает их; − Получает массив символов по анонимному каналу от процесса-сервера. − Определяет символы, принадлежащие латинскому алфавиту и передает их по анонимному каналу процессу-серверу. − Выводит полученные символы на консоль, начина с позиции курсора. − Передача сообщения второго типа инициируется нажатием правой кнопки мыши. Символы передаются посимвольно. 10. Написать программы трёх консольных процессов Server и Client, Figure которые обмениваются сообщениями по анонимному каналу. Сообщения бывают двух типов: 1. сообщения первого типа содержат заголовок и координаты позиции курсора экрана для вывода с этой позиции символов для консольного приложения; 2. сообщения второго типа содержат массив символов, которые должны выводиться Одновременно сообщение может передаваться только одним из процессов. Процесс- Server, который выполняет следующие действия. - Размер массива вводится с консоли. − Запрашивает заголовок и координаты позиции курсора экрана у пользователя; − Запускает процесс Client. − Запускает процесс Figure. − Получает и передает по анонимным каналам массив символов. Выводит полученные и переданные числа на консоль. − Передача сообщения первого типа инициируется нажатием кнопки на клавиатуре. В этом случае данный процесс установливает их на своей консоли и передать их другим процессам − Передача сообщения второго типа инициируется нажатием правой кнопки мыши. − Элементы массива предаются по одному. − Закончить работу , после нажатия клавиши - “Esc”. Процесс- Client , который выполняет следующие действия. − Генерирует символы и передает их по анонимному каналу процессусерверу. − Получает координаты позиции курсора от Server и устанавливает их; − Передача сообщения второго типа инициируется нажатием левой кнопки мыши. − Символы передаются посимвольно. Выводит произведенные символы консоль, начина с позиции курсора. Процесс-Figure, который выполняет следующие действия. − Получает заголовок от Server и устанавливает его; − Получает массив символов по анонимному каналу от процесса-сервера. 91 − Определяет цифры и передает их по анонимному каналу процессусерверу. − Передача сообщения второго типа инициируется двойным нажатием левой кнопки мыши; − Элементы массива предаются по одному; − Начиная с позиции курсора выводит полученные символы на консоль. 11. Написать программы двух консольных процессов Server, Mult, Sum которые обмениваются сообщениями по анонимному каналу. Сообщения бывают двух типов: 1. сообщения первого типа содержат размер буфера экрана и размер курсора; 2. сообщения второго типа содержат массив символов. Одновременно сообщение может передаваться только одним из процессов. Процесс- Server, который выполняет следующие действия. − Запрашивает размер массив. − Запрашивает у пользователя размер буфера экрана и размер курсора; − Генерирует целые числа для массива и передает их по анонимному каналу другим процессам − Запускает процессы Mult, Sum. − Получает и передает по анонимным каналам массив символов. Выводит полученные результаты на консоль. − Передача первого сообщения инициируется нажатием левой кнопки мыши. В этом случае данный процесс передаёт буфера экрана и позицию курсора процессу Sum. − Передача второго сообщения инициируется нажатием клавиши «G» на клавиатуре. В этом случае данный процесс передаёт массив чисел другим процессам. − Запрашивает результат от процессов- Mult, Sum посредством двойного нажатия кнопки мыши. − Закончить работу , после нажатия клавиши - “Enter” Процесс- Sum, который выполняет следующие действия. − Получает массив чисел от сервера. − Получает размер буфера экрана и размер курсора, устанавливает их; − Вычисляет сумму чисел массива. − Передаёт число серверу. − Выводит сумму на консоль. Процесс- Mult, который выполняет следующие действия. − Получает массив чисел от сервера. − Вычисляет произведение чисел массива − Передаёт число серверу. − Выводит сумму на консоль 12. Написать программы трёх консольных процессов Server и Client, Palindrom которые обмениваются сообщениями по анонимному каналу. Сообщения бывают двух типов: 1. сообщения первого типа содержат цвет символов экрана и размер курсора для консольного приложения; 92 2. сообщения второго типа содержат массив символов. Одновременно сообщение может передаваться только одним из процессов. Процесс- Server, который выполняет следующие действия. − Создает массив, для хранения целых чисел. − Размер массива вводится с консоли. − Запрашивает у пользователя цвет символов экрана и размер курсора консоли; − Запускает процесс Client. − Запускает процесс Palindrom. − Получает и передает по анонимным каналам массив символов. Выводит полученные результаты и переданные массив на консоль. − Передача сообщения первого типа инициируется нажатием правой кнопки. В этом случае данный процесс передаёт их (параметры) другому процессу, который должен установить их (параметры) на своей консоли. − Передача сообщения второго типа инициируется нажатием левой кнопки. Элементы массива передаются посимвольно клавиши мыши − Закончить работу после нажатия клавиши. Процесс- Client , который выполняет следующие действия. − Генерирует целые числа и передает их по анонимному каналу процессу-серверу. − Получает цвет текста от Server и устанавливает их; − Размер массива запрашивается с консоли. − Передача сообщения второго типа инициируется нажатием правой кнопки. − Выводит сгенерированный массив на консоль. − Закончить работу после нажатия клавиши мыши Процесс-Palindrom, который выполняет следующие действия. − Получает размер курсора от Server и устанавливает их; − Получает массив символов по анонимному каналу от процесса-сервера − Находит палиндром в строке и передает полином по анонимному каналу процессу-серверу. − Передача сообщения второго типа инициируется двойным нажатием левой кнопки мыши. − Выводит полученные палиндромы на консоль. 13. Написать программы консольных процессов Server, Produce и Consume для управления параллельным доступом процессов к массиву. Одновременно сообщение может передаваться только одним из процессов. Процесс- Server, который выполняет следующие действия. − Запускает процесс Producer, которые производят элементы для массива. − Запускает процесс Consumer, которые потребляют элементы из массива. − Дает процессам Produce и Consumer команду на начало работы. − Получает и передает по анонимным каналам целые числа от производителей и потребителей соответственно. Выводит полученные 93 и переданные числа на консоль. − Передача элементов массива инициируется двойным нажатием левой кнопки мыши. − Закончить работу, после нажатия клавиши мыши Процесс- Produce , который выполняет следующие действия: − Запрашивает с консоли количество чисел для производства. − Генерирует массив чисел и передает их по анонимному каналу процессу-серверу. − Передача целых чисел начинается по команде сервера. − Выводит произведенные целые числа на консоль. − Процесс- Consume, который выполняет следующие действия. − Получает числа по анонимному каналу от процесса-сервера. − Выводит полученные целые числа на консоль. − Количество чисел, которые должны быть потреблены, запрашивается с консоли. − Отправка целых чисел начинается после нажатия клавиши мыши. Контрольные вопросы 1. Перечислите основные способы взаимодействия процессов. 2. В чем отличие каналов от отображения файлов? 3. Перечислите основные параметры создания анонимных каналов. 4. В чем состоят недостатки анонимных каналов? Лабораторная работа №7 Тема: Обмен данными по именованному каналу с сервером Цель работы: 1. Изучение механизмов межпроцессного обмена в ОС семейства Windows. 2. Изучение функций для работы с именованными каналами. 3. В соответствии с заданным вариантом разработать приложение, реализующее обмен данными между процессами с помощью именованных каналов. Краткое теоретическое введение 1. Создание именованных каналов Работа с именованными каналами также как и работа с анонимными каналами требует совместного использования целого ряда функций. Поэтому сначала рассмотрены все функции, которые предназначены для работы с именованными каналами, а затем приведены несколько примеров, которые иллюстрируют использование этих функций. Именованные каналы создаются процессом-сервером при помощи функции CreateNamedPipe, которая имеет следующий прототип: HANDLE CreateNamedPipe ( LPCTSTR lpName, // имя канала DWORD dwOpenMode, // атрибуты канала DWORD dwPipeMode, // режим передачи данных DWORD nMaxInstances, // максимальное количество 94 экземпляров канала DWORD nOutBufferSize, // размер выходного буфера DWORD nInBufferSize, // размер входного буфера DWORD nDefaultTimeOut, // время ожидания связи с клиентом LPSECURITY_ATTRIBUTES lpPipeAttributes // атрибуты защиты ); где параметры имеют следующие значения. Параметр lpName указывает на строку, которая должна иметь вид: \\.\pipe\<pipe_name> Здесь точка (.) обозначает локальную машину, так как новый именованный канал всегда создается на локальной машине, слово pipe - фиксировано, а <pipe_name> обозначает имя канала, которое задается пользователем и нечувствительно к регистру. Параметр dwOpenMode задает флаги, которые определяют направление передачи данных, буферизацию, синхронизацию обмена данными и права доступа к именованному каналу. Для определения направления передачи данных используются флаги: PIPE_ACCESS_DUPLEX чтение и запись в канал, PIPE_ACCESS_INBOUND клиент пишет, а сервер читает данные, PIPE_ACCESS_OUTBOUND сервер пишет, а клиент читает данные. Флаг, определяющий направление передачи данных по именованному каналу, должен совпадать для всех экземпляров одного и того же именованного канала. Для определения способа буферизации и синхронизации используются флаги: FILE_FLAG_WRITE_THROUGH запрещает буферизацию при передаче данных по сети. FILE_FLAG_OVERLAPPED разрешает асинхронную передачу данных по каналу. Эти флаги могут быть разными для каждого экземпляра одного и того же именованного канала. Флаги для определения атрибутов защиты будут рассмотрены позднее. Параметр dwPipeMode задает флаги, способ передачи данных по именованному каналу. Для определения способов чтения и записи данных в именованный канал используются флаги: PIPE_TYPE_BYTE запись данных потоком, PIPE_TYPE_MESSAGE запись данных сообщениями. PYPE_READMODE_BYTE чтение данных потоком, PYPE_READMODE_MESSAGE чтение данных сообщениями. По умолчанию данные по именованному каналу предаются потоком. Флаги способов чтения и записи данных в именованный канал должны совпадать для всех экземпляров одного и того же именованного канала. Для определения синхронизации доступа к именованному каналу используются флаги: PIPE_WAIT синхронная связь с каналом и обмен данными по каналу, PIPE_NOWAIT асинхронная связь с каналом и обмен данными по каналу. Эти флаги могут быть разными для каждого экземпляра именованного канала. Параметр nMaxInstances определяет максимальное число экземпляров именованного канала, которое может находиться в пределах от 1 до 95 PIPE_UNLIMITED_INSTANCES. Параметры nOutBufferSize и nInBufferSize определяют соответственно размеры выходного и входного буферов для обмена данными по именованному каналу. Однако, эти значения рассматриваются операционными системами Windows только как пожелания пользователя, а сам выбор размеров буферов остается за операционной системой. Параметр nDefaultTimeOut устанавливает время ожидания клиентом связи с сервером, если клиент вызывает функцию WaitNamedPipe, в которой интервал ожидания интервал ожидания задается по умолчанию. При удачном завершение функция CreateNamedPipe возвращает значение дескриптор именованного канала, в случае неудачи - одно из двух значений: INVALID_HANDLE_VALUE неудачное завершение, ERROR_INVALID_PARAMETR значение параметра больше, чем величина PIPE_UNLIMITED_INSTANCES. nMaxInstances Для связи сервера с несколькими клиентами по одному именованному каналу сервер должен создать несколько экземпляров этого канала. Каждый экземпляр именованного канала создается вызовом функции CreateNamedPipe, в которой некоторые флаги должны быть установлены одинаково для всех экземпляров одного и того же именованного канала. Каждый новый вызов этой функции возвращает новый дескриптор на создаваемый экземпляр именованного канала. 2. Соединение сервера с клиентом После того, как сервер создал именованный канал, он должен дождаться соединения клиента с этим каналом. Для этого сервер вызывает функцию BOOL ConnectNamedPipe ( HANDLE hNamedPipe, // дескриптор канала LPOVERLAPPED lpOverlapped // асинхронная связь ); которая возвращает значение TRUE в случае успеха или значение FALSE в случае неудачи. Сервер может использовать эту функцию для связи с клиентом по каждому новому экземпляру именованного канала. После окончания обмена данными с клиентом, сервер может вызвать функцию BOOL DisconnectNamedPipe ( HANDLE hNamedPipe // дескриптор канала ); которая возвращает значение TRUE в случае успеха или значение FALSE в случае неудачи. Эта функция разрывает связь сервера с клиентом. После этого клиент не может обмениваться данными с сервером по данному именованному каналу и поэтому любая операция доступа к именованному каналу со стороны клиента вызовет ошибку. После разрыва связи с одним клиентом, сервер снова может вызвать функцию ConnectNamedPipe, чтобы установить связь по этому же именованному каналу с другим клиентом. 3. Соединение клиентов с именованным каналом Прежде чем соединяться с именованным каналом, клиент должен 96 определить доступен ли какой-либо экземпляр этого канала для соединения. С этой целью клиент должен вызвать функцию: BOOL WaitNamedPipe ( LPCTSTR lpNamedPipeName, // указатель на имя канала DWORD nTimeOut // интервал ожидания ); которая в случае успешного завершения возвращает значение TRUE, а в случае неудачи - FALSE. Параметры этой функции имеют следующие значения. Параметр lpNamedPipeName указывает на строку, которая должна иметь вид \\<server_name>\pipe\<pipe_name> Здесь <server_name> обозначает имя компьютера, на котором выполняется сервер именованного канала. Параметр nTimeOut задает временной интервал в течение которого клиент ждет связь с сервером. Этот временной интервал определяется в миллисекундах или может быть равен одному из следующих значений: NMPWAIT_USE_DEFAULT_WAIT интервал времени ожидания определяется значением параметра nDefaultTimeOut, который задается в функции CreateNamedPipe, NMPWAIT_WAIT_FOREVER бесконечное время ожидания связи с именованным каналом. Сделаем два важных замечания относительно работы функции WaitNamedPipe. Во-первых, если не существует экземпляров именованного канала с именем lpNamedPipe, то эта функция немедленно завершается неудачей, независимо от времени ожидания, заданного параметром nTimeOut. Во-вторых, если клиент соединяется с каналом до вызова сервером функции ConnectNamedPipe, то функция WaitNamedPipe возвращает значение FALSE и функция GetLastError вернет код ERROR_PIPE_CONNECTED. Поэтому функцию WaitNamedPipe нужно вызывать только после соединения сервера с каналом посредством функции ConnectNamedPipe. После того как обнаружен свободный экземпляр канала, для того чтобы установить связь с этим каналом клиент должен вызвать функцию HANDLE CreateFile ( LPCTSTR lpFileName, // указатель на имя канала DWORD dwDesiredAccess, // чтение или запись в канал DWORD dwShareMode, // режим совместного использования LPSECURITY_ATTRIBUTES lpSecurity Attributes, // атрибуты защиты DWORD dwCreationDisposition, // флаг открытия канала DWORD dwFlagsAndAttributes, // флаги и атрибуты HANDLE hTemplateFile // дополнительные атрибуты ); которая в случае успешного завершения возвращает дескриптор именованного канала, а в случае неудачи - значение INVALID_HANDLE_VALUE. Параметры функции CreateFile могут принимать следующие значения, если эта функция используется для открытия именованного канала. Параметр lpFileName должен указывать на имя канала, которое должно быть задано в том же формате, что и в функции WaitNamedPipe. Параметр dwDesiredAccess может принимать одно из следующих значений: 0 разрешает получить атрибуты канала, 97 GENERIC_READ GENERIC_WRITE разрешает чтение из канала, разрешает запись в канал. Следует отметить, что функция CreateFile завершается неудачей, если доступ к именованному каналу, заданный этими значениями, не соответствует значениям параметра dwOpenMode в функции CreateNamedPipe. Кроме того, в этом параметре программист может определить стандартные права доступа к именованному каналу. За более подробной информацией по этому вопросу нужно обратиться к MSDN. Параметр dwShareMode определяет режим совместного использования именованного канала и может принимать значение 0, которое запрещает совместное использование именованного канала или любую комбинацию следующих значений: FILE_SHARE_READ разрешает совместное чтение из канала, FILE_SHARE_WRITE разрешает совместную запись в канал. Параметр lpSecurityAttributes задает атрибуты защиты именованного канала. Для именованного канала параметр dwCreationDisposition должен быть равен значению OPEN_EXISTING, так как клиент всегда открывает существующий именованный канал. Для именованного канала параметр dwFlagsAndAttributes можно задается равным 0, что определяет флаги и атрибуты по умолчанию. Подробную информацию о значениях этого параметра смотри в MSDN. Значение параметра hTemplateFile задается равным NULL. Сделаем следующие замечания относительно работы с функцией CreateFile в случае её использования для открытия доступа к именованному каналу. Вопервых, несмотря на то, что функция WaitNamedPipe может успешно завершиться, последующий вызов функции CreateFile может завершиться неудачей по следующим причинам: между вызовами этих функций сервер закрыл канал, между вызовами функций другой клиент связался с экземпляром этого канала. Для предотвращения последней ситуации сервер должен создавать новый экземпляр именованного канала после каждого успешного завершения функции ConnectNamedPipe или создать сразу несколько экземпляров именованного канала. Во-вторых, если заранее известно, что сервер вызвал функцию ConnectNamedPipe, то функция CreateFile может вызываться без предварительного вызова функции WaitNamedPipe. Кроме того следует отметить, что если клиент работает на той же машине, что и сервер и использует для открытия именованного канала в функции CreateFile имя сервера в виде: \\.\pipe\<pipe_name> то файловая система именованных каналов (NPFS) открывает этот именованный канал в режиме передачи данных потоком. Чтобы открыть именованный канал в режиме передачи данных сообщениями, нужно задавать имя сервера в виде: \\<server_name>\pipe\<pipe_name> Отметим один момент, который касается связи сервера с клиентом 98 именованного канала. Может возникнуть такая ситуация, что сервер вызвал функцию ConnectNamedPipe, а клиента, который хочет связаться с именованным каналом, не существует. В этом случае серверное приложение будет заблокировано. Чтобы иметь возможность обработать такую ситуацию, функцию ConnectNamedPipe следует вызывать в отдельном потоке серверного приложения. Тогда для разблокировки серверного приложения можно вызвать функцию для связи клиента с именованным каналом из другого потока. 4. Получение информации об именованном канале Для получения информации о режимах работы и состоянии именованного канала используются функции: GetNamedPipeHandleState; GetNamedPipeInfo; 5. Изменение состояния именованного канала Изменить состояние именованного канала можно посредством функции SetNamedPipeHandleState; 6. Обмен данными по именованному каналу Как и в случае с анонимным каналом, для обмена данными по именованному каналу используются функции ReadFile и WriteFile, но с одним отличием, которое заключается в следующем. Так как в случае именованного канала разрешен асинхронный обмен данными, то в функциях ReadFile и WriteFile может использоваться параметр lpOverlapped при условии, что в вызове функции CreateNamedPipe в параметре dwOpenMode был установлен флаг FILE_FLAG_OVERLAPPED. Для асинхронного ввода-вывода по именованному каналу могут также использоваться функции ReadFileEx и WriteFileEx, которые будут рассмотрены далее в одной из глав. Для копирования данных из именованного канала используется функция PeekNamedPipe, которая копирует данные в буфер, не удаляя их из канала. Эта функция имеет следующий прототип: PeekNamedPipe Для обмена сообщениями по сети может также использоваться функция TransactNamedPipe, которая объединяет операции записи и чтения в одну операцию ( транзакцию) и имеет следующий прототип: T ransactNamedPipe Параметры этой функции аналогичны параметрам функций ReadFile и WriteFile. Отметим, что функция TransactNamedPipe может использоваться только в том случае, если сервер при создании именованного канала установил флаги PIPE_TYPE_MESSAGE и PIPE_READMODE_MESSAGE. Для передачи единственной транзакции по именованному каналу используется функция CallNamedPipe. После завершения обмена данными по именованному каналу, потоки должны закрыть дескрипторы экземпляров именованного канала, используя 99 функцию CloseHandle. 7. Примеры работы с именованными каналами Вначале рассмотрим простой пример, в котором процесс-сервер создает именованный канал, а затем ждет, пока клиент не соединится с именованным каналом. После этого сервер читает из именованного канала десять чисел и выводит их на консоль. Сначала приведем программу процесса-сервера именованного канала. Листинг 1. Пример процесса сервера именованного канала. #include <windows.h> #include <iostream.h> int main() { char c; // служебный символ HANDLE hNamedPipe; // создаем именованный канал для чтения hNamedPipe=CreateNamedPipe( "\\\\.\\pipe\\demo_pipe", // имя канала PIPE_ACCESS_INBOUND, // читаем из канала PIPE_TYPE_MESSAGE | PIPE_WAIT, // синхронная передача сообщений 1, // максимальное количество экземпляров канала 0, // размер выходного буфера по умолчанию 0, // размер входного буфера по умолчанию INFINITE, // клиент ждет связь бесконечно долго (LPSECURITY_ATTRIBUTES)NULL // защита по умолчанию ); // проверяем на успешное создание if (hNamedPipe==INVALID_HANDLE_VALUE) { cerr << "Creation of the named pipe failed." << endl << "The last error code: " << GetLastError() << endl; cout << "Press any char to finish server: "; cin >> c; return 0; } // ждем пока клиент свяжется с каналом cout << "The server is waiting for connection with a client." << endl; if(!ConnectNamedPipe( hNamedPipe, // дескриптор канала (LPOVERLAPPED)NULL // связь синхронная )) { cerr << "The connection failed." << endl << "The last error code: " << GetLastError() << endl; CloseHandle(hNamedPipe); cout << "Press any char to finish the server: "; cin >> c; return 0; } // читаем данные из канала for (int i=0; i<10; i++) { int nData; DWORD dwBytesRead; if (!ReadFile( hNamedPipe, // дескриптор канала 100 &nData, // адрес буфера для ввода данных sizeof(nData), // количество читаемых байтов &dwBytesRead, // количество прочитанных байтов (LPOVERLAPPED)NULL // передача данных синхронная )) { cerr << "Data reading from the named pipe failed." << endl << "The last error code: " << GetLastError() << endl; CloseHandle(hNamedPipe); cout << "Press any char to finish the server: "; cin >> c; return 0; } // выводим прочитанные данные на консоль cout << "The number " << nData << " was read by the server" << endl; } // закрываем дескриптор канала CloseHandle(hNamedPipe); // завершаем процесс cout << "The data are read by the server."<<endl; cout << "Press any char to finish the server: "; cin >> c; return 0; } Теперь приведем пример клиента именованного канала, который сначала связывается с именованным каналом, а затем записывает в него десять чисел. Листинг 2. Пример процесса клиента именованного канала. #include <windows.h> #include <iostream.h> int main() { char c; // служебный символ HANDLE hNamedPipe; char pipeName[] = "\\\\.\\pipe\\demo_pipe"; // связываемся с именованным каналом hNamedPipe = CreateFile( pipeName, // имя канала GENERIC_WRITE, // записываем в канал FILE_SHARE_READ, // разрешаем только запись в канал (LPSECURITY_ATTRIBUTES) NULL, // защита по умолчанию OPEN_EXISTING, // открываем существующий канал 0, // атрибуты по умолчанию (HANDLE)NULL // дополнительных атрибутов нет ); // проверяем связь с каналом if (hNamedPipe == INVALID_HANDLE_VALUE) { cerr << "Connection with the named pipe failed." << endl << "The last error code: " << GetLastError() << endl; cout << "Press any char to finish the client: "; cin >> c; return 0; } // пишем в именованный канал for (int i=0; i<10; i++) { DWORD dwBytesWritten; if (!WriteFile( hNamedPipe, // дескриптор канала 101 &i, // данные sizeof(i), // размер данных &dwBytes Written, // количество записанных байтов (LPOVERLAPPED)NULL // синхронная запись )) { // ошибка записи cerr << "Writing to the named pipe failed: " << endl << "The last error code: " << GetLastError() << endl; cout << "Press any char to finish the client: "; cin >> c; CloseHandle(hNamedPipe); return 0; } // выводим число на консоль cout << "The number " << i << " is written to the named pipe." << endl; Sleep(1000); } // закрываем дескриптор канала CloseHandle(hNamedPipe); // завершаем процесс cout << "The data are written by the client." << endl << "Press any char to finish the client: "; cin >> c; return 0; } Теперь рассмотрим пример сервера именованного канала, который сначала создает именованный канал, затем ждет подключения к нему клиента. После этого сервер принимает от клиента одно сообщение, выводит это сообщение на консоль и посылает клиенту сообщение в ответ. Листинг 3. Пример сервера именованного канала. // Сервер принимает сообщение от клиента и посылает ему сообщение в ответ. // Внимание: в этом случае для работы в локальной сети вход на клиентскую машину должен быть выполнен // с тем же именем и паролем, что и на сервер. #include <windows.h> #include <iostream.h> int main() { char c; // служебный символ HANDLE hNamedPipe; char lpszInMessage[80]; // для сообщения от клиента DWORD dwBytesRead; // для количества прочитанных байтов char lpszOutMessage[] = "The server has received a message."; // обратное сообщение DWORD dwBytesWrite; // для количества записанных байтов // создаем именованный канал для чтения hNamedPipe = CreateNamedPipe( "V\WV\pipeV\demo_pipe", // имя канала PIPE_ACCESS_DUPLEX, // читаем из канала и пишем в канал PIPE_TYPE_MESSAGE | PIPE_WAIT, // синхронная передача сообщений 1, // максимальное количество экземпляров канала 0, // размер выходного буфера по умолчанию 0, // размер входного буфера по умолчанию 102 INFINITE, // клиент ждет связь 500 мс (LPSECURITY_ATTRIBUTES)NULL // защита по умолчанию ); // проверяем на успешное создание if (hNamedPipe == INVALID_HANDLE_VALUE) { cerr << "Creation of the named pipe failed." << endl << "The last error code: " << GetLastError() << endl; cout << "Press any char to finish server: "; cin >> c; return 0; } // ждем, пока клиент свяжется с каналом cout << "The server is waiting for connection with a client." << endl; if(!ConnectNamedPipe( hNamedPipe, // дескриптор канала (LPOVERLAPPED)NULL // связь синхронная )) { cerr << "The connection failed." << endl << "The last error code: "<<GetLastError() << endl; CloseHandle(hNamedPipe); cout << "Press any char to finish the server: "; cin >> c; return 0; } // читаем сообщение от клиента if (!ReadFile( hNamedPipe, // дескриптор канала lpszInMessage, // адрес буфера для ввода данных sizeof(lpszInMessage), // число читаемых байтов &dwBytesRead, // число прочитанных байтов (LPOVERLAPPED)NULL // передача данных синхронная )) { cerr << "Data reading from the named pipe failed." << endl << "The last error code: "<< GetLastErrorO << endl; CloseHandle(hNamedPipe); cout << "Press any char to finish the server: "; cin >> c; return 0; } // выводим полученное от клиента сообщение на консоль cout << "The server has received the following message from a client: " << endl << "\t" << lpszInMessage << endl; // отвечаем клиенту if (!WriteFile( hNamedPipe, // дескриптор канала lpszOutMessage, // адрес буфера для вывода данных sizeof(lpszOutMessage), // число записываемых байтов &dwBytesWrite, // число записанных байтов (LPOVERLAPPED)NULL // передача данных синхронная )) { cerr << "Data writing to the named pipe failed." << endl << "The last error code: " << GetLastError() << endl; CloseHandle(hNamedPipe); cout << "Press any char to finish the server: "; cin >> c; return 0; } // выводим посланное клиенту сообщение на консоль cout << "The 103 server send the following message to a client: " << endl << "\t" << lpszOutMessage << endl; // закрываем дескриптор канала CloseHandle(hNamedPipe); // завершаем процесс cout << "Press any char to finish the server: "; cin >> c; return 0; } Обратим в этой программе внимание на следующий момент. Если клиент и сервер работают на разных компьютерах локальной сети, то вход как на компьютер сервера, так и на компьютер клиента, должен осуществляться с одинаковыми именами и паролями. Так как по умолчанию атрибуты защиты именованного канала устанавливаются таким образом, что он принадлежит только пользователю, создавшему этот именованный канал. В следующей программе мы установим атрибуты защиты таким образом, чтобы они разрешали доступ к именованному каналу любому пользователю. // Пример процесса сервера именованного канала. // Сервер принимает сообщение от клиента и посылает ему сообщение в ответ. // В этом случае для работы в локальной сети вход на клиентскую машину может быть // выполнен с любым именем и паролем. #include <windows.h> #include <iostream.h> int main() { char c; // служебный символ SECURITY_ATTRIBUTES sa; // атрибуты защиты SECURITY_DESCRIPTOR sd; // дескриптор защиты HANDLE hNamedPipe; char lpszInMessage[80]; // для сообщения от клиента DWORD dwBytesRead; // для числа прочитанных байтов char lpszOutMessage[] = "The server has received a message."; // обратное сообщение DWORD dwBytesWrite; // для числа записанных байтов // инициализация атрибутов защиты sa.nLength = sizeof(sa); sa.bInheritHandle = FALSE; // дескриптор канала ненаследуемый // инициализируем дескриптор защиты InitializeSecurityDescriptor(&sd,SECURITY_DESCRIPTOR_REVISION); // устанавливаем атрибуты защиты, разрешая доступ всем пользователям SetSecurityDescriptorDacl(&sd, TRUE, NULL, FALSE); sa.lpSecurityDescriptor = &sd; // создаем именованный канал для чтения hNamedPipe = CreateNamedPipe( "\\\\.\\pipe\\demo_pipe", // имя канала PIPE_ACCESS_DUPLEX, // читаем из канала и пишем в канал PIPE_TYPE_MESSAGE | PIPE_WAIT, // синхронная передача сообщений 1, // максимальное количество экземпляров канала 0, // размер выходного буфера по умолчанию 0, // размер входного буфера по умолчанию INFINITE, // клиент ждет связь 500 мс &sa // доступ для всех пользователей ); // проверяем на успешное создание if (hNamedPipe == INVALID_HANDLE_VALUE) 104 { cerr << "Creation of the named pipe failed." << endl << "The last error code: " << GetLastErrorO << endl; cout << "Press any char to finish server: "; cin >> c; return 0; } // ждем, пока клиент свяжется с каналом cout << "The server is waiting for connection with a client." << endl; if(!ConnectNamedPipe( hNamedPipe, // дескриптор канала (LPOVERLAPPED)NULL // связь синхронная )) { cerr << "The connection failed." << endl << "The last error code: " << GetLastError() << endl; CloseHandle(hNamedPipe); cout << "Press any char to finish the server: "; cin >> c; return 0; } // читаем сообщение от клиента if (!ReadFile( hNamedPipe, // дескриптор канала lpszInMessage, // адрес буфера для ввода данных sizeof(lpszInMessage), // число читаемых байтов &dwBytesRead, // число прочитанных байтов (LPOVERLAPPED)NULL // передача данных синхронная )) { cerr << "Data reading from the named pipe failed." << endl << "The last error code: " << GetLastError() << endl; CloseHandle(hNamedPipe); cout << "Press any char to finish the server: "; cin >> c; return 0; } // выводим полученное от клиента сообщение на консоль cout << "The server has receivrd the following message from a client: " << endl << "\t" << lpszInMessage << endl; // отвечаем клиенту if (!WriteFile( hNamedPipe, // дескриптор канала lpszOutMessage, // адрес буфера для вывода данных sizeof(lpszOutMessage), // число записываемых байтов &dwBytesWrite, // число записанных байтов (LPOVERLAPPED)NULL // передача данных синхронная )) { cerr << "Data writing to the named pipe failed." << endl << "The last error code: " << GetLastError() << endl; CloseHandle(hNamedPipe); cout << "Press any char to finish the server: "; cin >> c; return 0; } // выводим посланное клиенту сообщение на консоль cout << "The server send the following message to a client: " << endl << "\t" << lpszOutMessage << endl; // закрываем дескриптор канала CloseHandle(hNamedPipe); // завершаем процесс cout << "Press any char to finish the server: "; cin >> c; return 0; 105 } Листинг 4. Пример сервера именованного канала. Теперь приведем пример клиента именованного канала, который вводит сначала с консоли имя компьютера в локальной сети, на котором запущен сервер именованного канала. Затем связывается с этим именованным каналом. После этого клиент передает серверу одно сообщение и получает от него сообщение в ответ, которое выводит на консоль. // Пример процесса клиента именованного канала. #include <windows.h> #include <iostream.h> int main() { char c; // служебный символ HANDLE hNamedPipe; char machineName[80]; char pipeName[80]; char lpszOutMessage[]="How do you do server?"; // сообщение серверу DWORD dwBytesWritten; // для числа записанных байтов char lpszInMessage[80]; // для сообщения от сервера DWORD dwBytesRead; // для числа прочитанных байтов // вводим имя машины в сети, на которой работает сервер cout << "Enter a name of the server machine: "; cin >> machineName; // подставляем имя машины в имя канала wsprintf(pipeName, "\\\\%s\\pipe\\demo_pipe", machineName); // связываемся с именованным каналом hNamedPipe = CreateFile( pipeName, // имя канала GENERIC_READ | GENERIC_WRITE, // читаем и записываем в канал FILE_SHARE_READ | FILE_SHARE_WRITE, // разрешаем чтение и запись в канал (LPSECURITY_ATTRIBUTES) NULL, // защита по умолчанию OPEN_EXISTING, // открываем существующий канал FILE_ATTRIBUTE_NORMAL, // атрибуты по умолчанию (HANDLE)NULL // дополнительных атрибутов нет ); // проверяем связь с каналом if (hNamedPipe==INVALID_HANDLE_VALUE) { cerr << "Connection with the named pipe failed." << endl << "The last error code: " << GetLastError() << endl; cout << "Press any char to finish the client: "; cin >> c; return 0; } // пишем в именованный канал if (!WriteFile( hNamedPipe, // дескриптор канала lpszOutMessage, // данные sizeof(lpszOutMessage), // размер данных &dwBytes Written, // количество записанных байтов (LPOVERLAPPED)NULL // синхронная запись )) { // ошибка записи cerr << "Writing to the named pipe failed: " << endl << "The last error code: " << GetLastError() << endl; cout << "Press any char to finish the client: "; cin >> c; 106 CloseHandle(hNamedPipe); return 0; } // выводим посланное сообщение на консоль cout << "The client has send the following message to a server: " << endl << "\t" << lpszOutMessage << endl; // читаем из именованного канала if (!ReadFile( hNamedPipe, // дескриптор канала lpszInMessage, // данные sizeof(lpszInMessage), // размер данных &dwBytesRead, // количество записанных байт (LPOVERLAPPED)NULL // синхронная запись )) { // ошибка записи cerr << "Reading to the named pipe failed: " << endl << "The last error code: " << GetLastError() << endl; cout << "Press any char to finish the client: "; cin >> c; CloseHandle(hNamedPipe); return 0; } // выводим полученное сообщение на консоль cout << "The client has received the following message from a server: " << endl << "\t" << lpszInMessage << endl; // закрываем дескриптор канала CloseHandle(hNamedPipe); // завершаем процесс cout << "Press any char to finish the client: "; cin >> c; return 0; } Задание на выполнение 1. Изучить теоретический материал, посвященный обмену данных между процессами посредством именованных каналов. 2. Используя представленные в работе примеры программ, реализовать приложения, демонстрирующие использование именованных каналов при межпроцессном обмене. 3. В соответствии с заданным вариантом разработать приложение для консольных процессов, которые обмениваются сообщениями по именованному каналу. 4. Результаты работы представить преподавателю в виде отчета и продемонстрировать функционирующее приложение. Варианты заданий Примечание: Процессы должны работать на разных компьютерах! 1. Написать программы двух консольных процессов Server, Client, работающих на разных компьютерах в локальной сети. Процесс-Server, который выполняет следующие действия. − Запрашивает у пользователя имя консоли процесса Client, цвет всего фона (15 цветов) консоли Client. − Передаёт данные процессу-клиенту. − При нажатии клавиши мыши передаёт Client , следующий цвет в палитре. 107 − Закончить работу, после нажатия клавиши. Процесс-Client, который выполняет следующие действия. − Получает от процесса сервера данные о цвете. − Устанавливает фон. 2. Написать программы двух консольных процессов Server, Client, работающих на разных компьютерах в локальной сети. Процесс-Server, который выполняет следующие действия. − Запрашивает у пользователя координаты и размер курсора консоли. − Запрашивает строку символов. При нажатии клавиши мыши печатает текст с позиции курсора мыши и передаёт новые координаты процессу-клиенту. Процесс-Client, который выполняет следующие действия: − Получает от процесса сервера данные об координатах и размере курсора консоли, строку. − -Печатает строку с переданной позиции. 3. Написать программы двух консольных процессов Server, Client, работающих на разных компьютерах в локальной сети. Процесс-Server, который выполняет следующие действия. − Запрашивает у пользователя размер окна консоли и цвет фона. − Запрашивает строку символов. − Заполняет буфер экрана введённым символом и закрашивает фон цветом. − При нажатии клавиши мыши передаёт данные процессу-клиенту. Процесс-Client, который выполняет следующие действия: − Получает от процесса сервера данные о размере и цвете окна, строку символов. − Заполняет буфер экрана строкой символов. 4. Написать программы двух консольных процессов Server, Client, работающих на разных компьютерах в локальной сети. Процесс-Server, который выполняет следующие действия: − Запрашивает у пользователя цвет выводимых символов, координаты вывода символов; − Запрашивает строку; − Передает эти параметры процессу-клиенту, который запущен на другом компьютере в локальной сети; − При нажатии клавиши мыши передаёт сообщение Client, что нужно всё стереть с экрана. Процесс-Client, который выполняет следующие действия: − Получает от процесса сервера начальные данные о цвете символов, положении курсора в окне консоли; − Устанавливает курсор и выводит строку с заданной позиции; − Стирает всё с экрана, если получено сообщение от сервера. 5. Написать программы двух консольных процессов Server, Client, работающих на разных компьютерах в локальной сети. Процесс-Server, который выполняет следующие действия. − Запрашивает у пользователя цвет фона консоли, цвет выводимых 108 символов, координаты вывода символов. − Запрашивает строку. − Передает эти параметры процессу-клиенту, который запущен на другом компьютере в локальной сети. − При нажатии левой клавиши мыши передаёт сообщение Client, что нужно изменить цвет символов экрана Процесс-Client, который выполняет следующие действия. − Получает от процесса сервера начальные данные о цвете символов, положении курсора в окне консоли. − Выводит строку с заданной позиции. − Меняет цвет выведенных символов на экране, если получено сообщение от сервера. 6. Написать программы двух консольных процессов Server, Client, работающих на разных компьютерах в локальной сети. Процесс-Server, который выполняет следующие действия: − Запрашивает у пользователя цвет фона консоли, цвет выводимых символов, координаты вывода символов. − Запрашивает строку. − Передает эти параметры процессу-клиенту, который запущен на другом компьютере в локальной сети. − При двойном нажатии левой клавиши мыши передаёт сообщение Client, что нужно изменить цвет экрана − При нажатии правой клавиши мыши передаёт сообщение Client, что нужно вывести символы с новой позиции (позиция курсора мыши) Процесс-Client, который выполняет следующие действия: − Получает от процесса сервера начальные данные о цвете символов, положении курсора в окне консоли. − Выводит строку с заданной позиции. − Меняет цвет экрана либо выводит символы с новой позиции, если получено соответствующее сообщение от сервера. 7. Написать программы двух консольных процессов Server, Client, работающих на разных компьютерах в локальной сети. Процесс-Server, который выполняет следующие действия. − Запрашивает у пользователя размер, цвет и начальное положение прямоугольника в окне консоли, число N. − Запрашивает символы для заполнения прямоугольника. − Отображает прямоугольник. − При двойном нажатии правой клавиши мыши рисует прямоугольник другим цветом (выбрать случайным образом), передает данные о прямоугольнике процессу-серверу. − При нажатии левой клавиши мыши - завершение работы процессов. Процесс-Client, который выполняет следующие действия: − Получает от процесса сервера данные о размере, цвете, символе заполнителе и положении прямоугольника в окне консоли. − Отображает прямоугольник. 109 8. Написать программы двух консольных процессов Server, Client, работающих на разных компьютерах в локальной сети. Процесс-Server, который выполняет следующие действия. − Запрашивает у пользователя размер, цвет и начальное положение прямоугольника в окне консоли, число N. − Запрашивает символы для заполнения прямоугольника. − Отображает прямоугольник. − При нажатии правой клавиши мыши увеличивает размер прямоугольника, передает данные о прямоугольниках процессусерверу. Процесс-Client, который выполняет следующие действия. − Получает от процесса сервера данные о размере, цвете, символе заполнителе и положении прямоугольника в окне консоли. − Увеличивает либо уменьшает прямоугольник в N раз. 9. Написать программы двух консольных процессов Server, Client, работающих на разных компьютерах в локальной сети. Процесс-Server, который выполняет следующие действия. − Запрашивает у пользователя строку символов, цвет выводимых символов, координаты вывода символов, размер буфера для заполнения строчками символов. − Передает эти параметры процессу-клиенту, который запущен на другом компьютере в локальной сети. Процесс-Client, который выполняет следующие действия. − Получает от процесса сервера начальные данные о строке, размере, цвете символов, положении курсора в окне консоли и размер буфера для заполнения символами. − Устанавливает курсор и заполняет буфер введённой строкой с заданной позиции. 10. Написать программы двух консольных процессов Server, Client, работающих на разных компьютерах в локальной сети. Процесс-Server, который выполняет следующие действия. − Запрашивает у пользователя, размер, цвет прямоугольника в окне консоли, числа N и M. − Запрашивает, размер и массив символов для заполнения прямоугольников. − Отображает прямоугольник. − Через интервалы времени М, увеличивает размер прямоугольника на число N, и отображает прямоугольник правее (или с другой свободной стороны) от предыдущего − При нажатии клавиши мыши последовательно передает данные о прямоугольниках процессу- серверу. Процесс-Client, который выполняет следующие действия: − Получает от процесса сервера данные о размере, цвете, символе заполнителе и положении прямоугольника в окне консоли. − Отображает прямоугольник. 110 11. Написать программы двух консольных процессов Server, Client, работающих на разных компьютерах в локальной сети. Процесс-Server, который выполняет следующие действия. − Запрашивает у пользователя количество, размер, цвет и начальное положение прямоугольников в окне консоли. − Рисует прямоугольники. − При установке курсора мыши и нажатии клавиши мыши на одном из прямоугольников окне процесса-сервера или в окне процесса-клиента. − Передает данные о размере, цвете и положении прямоугольника процессу-клиенту. Процесс-Client, который выполняет следующие действия: − Получает от процесса сервера начальные данные о размере, цвете и положении прямоугольника в окне консоли. − Рисует прямоугольник. 12. Написать программы двух консольных процессов Server, Client, работающих на разных компьютерах в локальной сети. Процесс-Server, который выполняет следующие действия: − Запрашивает у пользователя количество, размер, цвет символовзаполнителей и начальное положение прямоугольников в окне консоли. − Запрашивает символы для заполнения прямоугольников. − Отображает прямоугольники. − При двойном нажатии правой клавиши мыши последовательно передает данные о прямоугольниках процессу-серверу. − При нажатии левой клавиши мыши последовательно стирает прямоугольники, и Процесс-клиент тоже стирает прямоугольники. Процесс-Client, который выполняет следующие действия. − Получает от процесса сервера данные о размере, цвете, символе заполнителе и положении прямоугольника в окне консоли. − Отображает либо стирает прямоугольник. 13. Написать программы двух консольных процессов Server, Client, работающих на разных компьютерах в локальной сети. Процесс-Server, который выполняет следующие действия. − Запрашивает у пользователя размер окна консоли и цвет фона. − Запрашивает строку символов. − Заполняет буфер экрана введённым символом и закрашивает фон цветом. − При нажатии клавиши мыши передаёт данные процессу-Client. Процесс-Client, который выполняет следующие действия. − Получает от процесса сервера данные о размере и цвете окна, строку символов. − Заполняет буфер экрана строкой символов. Контрольные вопросы 111 1. Что такое именованные каналы (named pipes) в Windows и почему они могут использоваться для межпроцессного взаимодействия? 2. Какие возможны в Windows варианты именованных каналов, каковы ограничения и преимущества каждого из них? 3. Какими функциями Windows API осуществляется использование именованных каналов в программе-сервере? 4. Какими функциями Windows API осуществляется использование именованных каналов в программе-клиенте? 5. В чем заключается в Windows API наследование описателей (handle inheritance) и как это может использоваться для межпроцессного взаимодействия? Лабораторная работа №8 Тема: Работа с файлами с помощью Win32 API функций Цель работы: 1. Изучение основных функций Win32 API, используемых для работы с файлами. 2. Разработка приложения, демонстрирующего создание и открытие файла. Краткое теоретическое введение. 1. Создание и открытие файлов Для создания новых или открытия уже существующих используется функция СreateFile: файлов HANDLE CreateFile( LPCTSTR lpFileName, // имя файла DWORD dwDesiredAccess, // способ доступа DWORD dwShareMode, // режимы совместного использова LPSECUTITY_ATTRIBUTES IpSecurutyAttributes, // атрибуты защиты DWORD dwGreationDisposition, // создание или открытие файла DWORD dwFlagsAndAttributes, // флаги и атрибуты HANDLE hTemplateFile // файл атрибутов ); lpFileName задается указатель на символьную строку, которая содержит полное имя создаваемого или открываемого файла. Если полное имя файла не указано, то файл с заданным именем создается или ищется в текущем каталоге. dwDesiredAccess: 0 — приложение может только определять атрибуты устройства; GENERIC_READ — допускается только чтение данных из файла; GENERIC_WRITE — допускается только запись данных в файл. dwShareMode: FILE_SHARE_READ — файл может использоваться только для совместного чтения несколькими программами; FILE _SHARE_WRITE — файл может использоваться только для совместной записи несколькими программами; FILE _SHARE_DELETE — файл может использоваться несколькими 112 программами при условии, что каждая из них имеет разрешение на удаление этого файла. lpSecurutyAttributes должен задавать атрибуты защиты файла. Пока этот параметр будем устанавливать в null. Это означает, что атрибуты файла устанавливаются по умолчанию, т. е. дескриптор файла не является наследуемым и файл открыт для доступа всем пользователям. dwСreationDisposition: СREATE_NEW — создать новый файл, если файл с заданным именем уже существует, то функция заканчивается неудачей; СREATE _ALWAYS — создать новый файл, если файл с заданным именем уже существует, то он уничтожается и создается новый файл; OPEN_EXISTING — открыть существующий файл, если файл с заданным именем не существует, то функция заканчивается неудачей; OPEN_ALWAYS — открыть файл, если файл с заданным именем не существует, то создается новый файл; TRUNCATE_EXISTING — открыть файл и уничтожить его содержимое, если файл с заданным именем не существует, то функция заканчивается неудачей. Отметим, что в последнем случае вызывающий процесс должен иметь права записи в файл, т. е. в параметре dwDesiredAccess должен быть установлен GENERIC_WRITE. dwFiagsAndAttributes: FILE _ATTRIBUTE_ARCHIVE — архивный файл, который содержит служебную информацию; FILE _ATTRIBUTE_ENCRYPTED — зашифрованный файл; FILE _ATTRIBUTE_HIDDEN — скрытый файл; FILE _ATTRIBUTE_NORMAL — обычный файл, который не имеет, других атрибутов; FILE _ATTRIBUTE_NOT_CONTENT_INDEXED — содержимое файла не индексировано; FILE _ATTRIBUTE_OFFLINE – файл находится во вспомогательной памяти; FILE _ATTRIBUTE_READONLY – файл можно только читать; FILE _ATTRIBUTE_SYSTEM – файл используется операционной системой; FILE _ATTRIBUTE_TEMPORARY – файл используется для временного хранения данных. Замечания: атрибут FILE _ATTRIBUTE_NORMAL должен использоваться только один, зашифрованные файлы не могут иметь атрибут FILE _ATTRIBUTE_SYSTEM. dwFlagsAndAttributes: FILE_ FLAG_ WRITE_THROUGH – запись данных непосредственно на диск, не используя кэширования; FILE_ FLAG_OVERLAPPED – обеспечивает асинхронное выполнение операций чтения и записи; FILE_ FLAG_NO_BUFFERING – не использовать буферизацию при 113 доступе к файлу; FILE_ FLAG_RANDOM_ACCESS – программа предполагает выбирать записи из файла случайным образом; FILE _FLAG_SEQUENTІAL_SCAN – программа будет сканировать файл i последовательно; FILE_FLAG_DELETE_ON_CLOSE – файл будет удален после того, как все дескрипторы этого файла будут закрыты; FILE_FLAG_BACKUP_SEMANTICS – резервный файл; hTemplateFile используется при создании файла, атрибуты которого должны соответствовать атрибутам ранее созданного файла. В этом случае параметр hTemplateFile должен содержать дескриптор файла, атрибуты которого копируются в атрибуты создаваемого файла. 2. Закрытие и удаление файлов BOOL DeleteFile( LPCTSTR lpFileName ); // имя файла При успешном завершении функция возвращает ненулевое значение, а при неудаче – false. 3. Запись данных в файл BOOL WriteFiie( HANDLE hFile, // дескриптор файла LPCVOID lpBuffer, // указатель на буфер данных DWORD nNuiriberOfBytesToWrite, // количество записываемых байтов LPDWORD lpNumberOfBytesWritten, // количество записанных байтов LPOVERLAPPED lpOverlapped //используется при асинхронной записи ); 4. Чтение данных из файла BOOL ReadFile( HANDLE hFile, // дескриптор файла LPVOID lpBuffer, // указатель на буфер данных DWORD nNumberOfBytesToRead, // количество читаемых байтов LPDWORD lpNumberOfBytesRead, // количество прочитанных байтов LPOVERLAPPED lpOverlapped //используется при асинхронной записи ); 5. Копирование и перемещение файлов BOOL CopyFile( LPCTSTR lpExistingFileName, //имя существующего файла LPCTSTR lpNewFileName, // имя нового файла BOOL bFailIfExists // действия в случае существования файла ); BOOL MoveFile( LPCTSTR lpExistingFileName, //имя существующего файла LPCTSTR lpNewFileName, // имя нового файла ); 114 Листинг 1. Пример приложения, создающего файл для записи в него данных. #include <windows.h> #include <iostream.h> int main() { HANDLE hFile; // создаем файл для записи данных hFile = CreateFile( "C:\\demo_file.dat", // имя файла GENERIC_WRITE, // запись в файл 0, // монопольный доступ к файлу NULL, // защиты нет CREATE_NEW, // создаем новый файл FILE_ATTRIBUTE_NORMAL, // обычный файл NULL // шаблона нет ); // проверяем на успешное создание if (hFile == INVALID_HANDLE_VALUE) { cerr << "Create file failed." << endl << "The last error code: " << GetLastError() << endl; cout << "Press any key to finish."; cin.get(); return 0; } // пишем данные в файл for (int i = 0; i < 10; ++i) { DWORD dwBytesWrite; if (!WriteFile( hFile, &i, // дескриптор файла // адрес буфера, откуда идет запись sizeof(i), &dwBytesWrite, (LPOVERLAPPED)NULL )) // количество записываемых байтов // количество записанных байтов // запись синхронная { cerr << "Write file failed." << endl << "The last error code: " << GetLastError() << endl; CloseHandle(hFile); cout << "Press any key to finish."; cin.get(); return 0; } } // закрываем дескриптор файла CloseHandle(hFile); cout << "The file is created and written." << endl; return 0; } 115 Листинг 2. Исходный код приложения, позволяющее выбрать действие над файлом: создание, открытие, удаление, запись и чтение данных, копирование и перемещение файлов. #include <windows.h> #include <iostream> #include <conio.h> using namespace std; int main() { int iMenu;DWORD dwBytesWrite; char cMenu[200]="Выбрать:\n1 - СОЗДАТЬ ФАЙЛ\n2 - ОТКРЫТЬ ФАЙЛ\n3 - УДАЛИТЬ ФАЙЛ\n4 - ЗАПИСАТЬ ДАННЫЕ В ФАЙЛ\n5 - ПРОЧИТАТЬ ДАННЫЕ ИЗ ФАЙЛА\n6 - КОПИРОВАТЬ ФАЙЛ\n7 - ПЕРЕМЕСТИТЬ ФАЙЛ\n8 ВЫЙТИ\n"; CharToOem(cMenu, cMenu); while(1) { system("cls"); cout << cMenu; cin >> iMenu; switch(iMenu) { HANDLE hFile; char buf[50],buf1[50]; case 1: CharToOem("СОЗДАТЬ ФАЙЛ:\nИмя файла?\n", buf); cout << buf; cin >> buf; hFile = CreateFile(buf,GENERIC_WRITE, 0,NULL,CREATE_NEW, FILE_ATTRIBUTE_NORMAL,NULL); if (hFile == INVALID_HANDLE_VALUE) { cerr << "Create file failed." << endl << "The last error code: " << GetLastError(); cout << "\nPress any key to finish."; getch(); break; } cout << "The file is created." << endl; CloseHandle(hFile); getch(); break; case 2: CharToOem("ОТКРЫТЬ ФАЙЛ:\nИмя файла?\n", buf); cout << buf; cin >> buf; hFile = CreateFile(buf,GENERIC_WRITE,0,NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL); if (hFile == INVALID_HANDLE_VALUE) { cerr << "Open file failed." << endl << "The last error code: " << 116 GetLastError(); cout << "\nPress any key to finish."; getch(); break; } cout << "The file is opened." << endl; CloseHandle(hFile); getch(); break; case 3: CharToOem("УДАЛИТЬ ФАЙЛ:\nИмя файла?\n", buf); cout << buf; cin >> buf; if (!DeleteFile(buf)) { cerr << "Delete file failed." << endl << "The last error code: " << GetLastError(); cout << "\nPress any key to finish."; getch(); break; } cout << "The file is deleted." << endl; getch(); break; case 4: CharToOem("ЗАПИСАТЬ ДАННЫЕ В ФАЙЛ:\nИмя файла?\n", buf); cout << buf; cin >> buf; hFile = CreateFile(buf,GENERIC_WRITE,0,NULL,OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL,NULL); if (hFile == INVALID_HANDLE_VALUE) { cerr << "Creat file failed." << endl << "The last error code: " << GetLastError(); cout << "\nPress any key to finish."; getch(); break; } cout << "The file is opened." << endl; CharToOem("Введите данные!\n", buf); cout << buf; cin >> buf; if (!WriteFile(hFile,&buf,sizeof(buf),&dwBytesWrite, (LPOVERLAPPED)NULL)) { cerr << "Write file failed." << endl << "The last error code: " << GetLastError(); CloseHandle(hFile); cout << "\nPress any key to 117 continue."; getch(); break; } cout << "The Information is written." << endl; CloseHandle(hFile); getch(); break; case 5: CharToOem("ЧТЕНИЕ ДАННЫХ ИЗ ФАЙЛА:\nИмя файла?\n", buf); cout << buf; cin >> buf; hFile = CreateFile(buf,GENERIC_READ,0,NULL,OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL); if (hFile == INVALID_HANDLE_VALUE) { cerr << "Open file failed." << endl << "The last error code: " << GetLastError(); cout << "\nPress any key to finish."; getch(); break; } cout << "The file is opened." << endl; if (!ReadFile(hFile,&buf,sizeof(buf),&dwBytesWrite, (LPOVERLAPPED)NULL)) { cerr << "Read file failed." << endl << "The last error code: " << GetLastError(); CloseHandle(hFile); cout << "\nPress any key to continue."; getch(); break; } if(!dwBytesWrite) cout << "The file is empty" << endl; else cout << "Read information:"<< buf << endl; CloseHandle(hFile); getch(); break; case 6: CharToOem("КОПИРОВАНИЕ ФАЙЛОВ:\nИмя файла источника?\n", buf); cout << buf;cin >> buf; CharToOem("Имя нового файла?\n", buf1); cout << buf1;cin >> buf1; if(!CopyFile(buf,buf1,true)) { cerr << "Move file failed. The last error code: " << GetLastError(); cout << "\nPress any key to continue."; 118 getch(); break;; }; cout << "The file is copied."<< endl; getch(); break; case 7: CharToOem("ПЕРЕМЕЩЕНИЕ ФАЙЛОВ:\nИмя файла источника?\n", buf); cout << buf;cin >> buf; CharToOem("Имя нового файла?\n", buf1); cout << buf1;cin >> buf1; if(!MoveFile(buf,buf1)) { cerr << "Move file failed. The last error code: " << GetLastError(); cout << "\nPress any key to continue."; getch(); break;; }; cout << "The file is moved."<< endl; getch(); break; case 8: return 0; } } } Задание на выполнение 1. Набрать программу которая приведена в листинге 1. Ознакомиться с работой функций CreateFile и WriteFile, протестировать работу программы при разных значениях параметров для данных функций. 2. Написать программу в которой были бы реализованы следующие функции работы с файлами: − создание файла; − открытие файла; − удаление файла; − запись данных в файл, данные вводятся с клавиатуры; − чтение данных из файла; − копирование фала; − перемещение файла. Выбор функции организовать в виде меню, пути до файлов вводить с клавиатуры. Ознакомится с основными параметрами данных функций, исследовать их работу при изменении основных параметров. Контрольные вопросы 1. Какая функция используется при создании или открытии файла? Поясните значения ее параметров. 2. Какие функции можно использовать для записи данных файл и чтения из файла? 3. Какую функцию выполняет данный оператор: hIn=CreateFile(argv [1], 119 GENERIC_READ, 0, NULL, OPEN_EXISTING, 0,NULL)? 4. Поясните, какую функцию выполняет данный оператор: hOut=CreateFile(argv[2], GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL). 5. Поясните, какую функцию выполняет данный оператор: while (ReadFile(hIn, Buffer, BUF_SIZE, &nIn, NULL) && nIn > 0). 6.Какую функцию выполняет данный оператор: WriteFile (hOut,Buffer, nIn, &nOut, NULL)? 7. Поясните, какую функцию выполняет данный оператор: if(!СopyFile(argv[1],argv[2],FALSE)). Лабораторная работа №9 Тема: Работа атрибутами файлов с помощью Win32 API функций Цель работы: 1. Изучение основных функций Win32API, используемых для определения и изменения атрибутов файлов. 2. Разработка приложения, демонстрирующего просмотр и изменение атрибутов файла. Краткое теоретическое введение 1. Определение и изменение атрибутов файла Узнать атрибуты файла можно при помощи функции GetFileAttributes: DWORD GetFileAttributes( LPCTSTR lpFileName // имя файла ); В случае успешного завершения эта функция возвращает атрибуты файла, а в случае неудачи — значение -1. Эти атрибуты можно проверить, используя следующие флаги: − FILE_ATTRIBUTE_ARCHIVE – архивный файл; − FILE_ATTRIBUTE_COMPRESSED – сжатый файл; − FILE_ATTRIBUTE_DIRECTORY – файл является каталогом; − FILE_ATTRIBUTE_ENCRYPTED – шифрованный файл; − FILE_ATTRIBUTE_HIDDEN – скрытый файл; − FILE_ATTRIBUTE_NORMAL – нормальный файл; − FILE_ATTRIBUTE_NOT_CONTENT_INDEXED – файл не индексируется; − FILE_ATTRIBUTE_OFFLINE – файл во внешней памяти; − FILE_ATTRIBUTE_READONLY – файл только для чтения; − FILE _ATTRIBUTE_REPARSE_POINT – файл требует интерпретации; − FILE _ATTRIBUTE_SPARSE_FILE – разреженный файл; − FILE _ATTRIBUTE_SYSTEM – системный файл; − FILE _ATTRIBUTE_TEMPORARY – временный файл. Изменить атрибуты файла можно при помощи функции: 120 BOOL SetFileAttributes( STSTR lpFileName, // имя файла dwFileAttributes // атрибуты файла ); 2. Определение и изменение размеров файла Определить размер файла можно при помощи функции: DWORD GetFileSize( HANDLE hFile, // дескриптор файла LPDWORD lpFileSizeHigh // указатель на старшую часть // размера файла ); Определить размер файла можно также при помощи функции GetFileSizeEx (только в Windows 2000 и Windows XP): BOOL GetFileSizeEx ( HANDLE hFile, // дескриптор файла PLARGE_INTEGER lpFileSize // размер файла ); 3. Блокирование файла BOOL LockFile ( HANDLE hFile, // дескриптор файла DWORD dwFileOffsetLow // младшая часть смещения DWORD dwFileOffsetHigh // старшая часть смещения DWORD nNumberOfBytesToLockLow // младшая часть количества байтов DWORD nNumberOfBytesToLockHigh //старшая часть количества байтов ); Для обмены блокировки используется функция BOOL UnlockFile ( HANDLE hFile, // дескриптор файла DWORD dwFileOffsetLow // младшая часть смещения DWORD dwFileOffsetHigh // старшая часть смещения DWORD nNumberOfBytesToLockLow // младшая часть количества байтов DWORD nNumberOfBytesToLockHigh //старшая часть количества байтов ); 4. Получение информации о файле Чтобы получить информацию о файле, можно функцию: использовать GetFilelnformationByHandle ( HANDLE hFile, // дескриптор файла // указатель на информацию LPBY_HANDLE_FILE_INFORMATION lpFilelnformation ); Параметр lpFilelnformation должен указывать на структуру типа LPBY_HANDLE_FILE_INFORMATION, в которую функция запишет информацию о файле. typedef struct _BY_HANDLE_FILE_INFORMATION { DWORD dwFileAttributes; // атрибуты файла 121 FILETIME ftCreationTime; FILETIME ftLastAccessTime; файлу FILETIME ftLastWriteTime; DWORD dwVolumeSerialNumber; DWORD nFileSizeHigh; DWORD nFileSizeLow; DWORD nNumberOfLinks; DWORD nFileIndexHigh; DWORD nFileIndexLow; } BY_HANDLE_FILE_INFORMATION, // время создания файла // время последнего доступа к // // // // // // // время последней записи в файл серийный номер тома старшая часть размера файла младшая часть размера файла количество ссылок на файл старшая часть индекса файла младшая часть индекса файла *LPBY_HANDLE_FILE_INFORMATION; Листинг 1. Пример приложения, демонстрирующего просмотр и изменение атрибутов файла. #include <windows.h> #include <iostream> #include <conio.h> #include <stdio.h> #include <math.h> using namespace std; HANDLE creatfile(char* buf) { HANDLE hFile= CreateFile(buf, GENERIC_WRITE, FILE_SHARE_READ |FILE_SHARE_WRITE,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL, NULL); if ( hFile == INVALID_HANDLE_VALUE) { cerr << "Open file failed." << endl << "The last error code: " << GetLastError(); cout << "\nPress any key to finish."; getch(); return hFile; } cout << "The file is opened." << endl; //CloseHandle(hFile); return hFile; } char * getfileattributes(char* lpFileName) { char cBuffer[0x1000]=""; DWORD dwFileAttributes=GetFileAttributes(lpFileName); cout << endl<< lpFileName << " - 0x" << hex << dwFileAttributes << " : "; dec( cout ); for( int i = 31; i >= 0; i--) if( dwFileAttributes & (1 << i) ) cout << "1"; else cout << "0"; if(dwFileAttributes & FILE_ATTRIBUTE_READONLY) strcat(cBuffer, "\n\tFILE_ATTRIBUTE_READONLY"); if(dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) strcat(cBuffer,"\n\t FILE_ATTRIBUTE_HIDDEN"); if(dwFileAttributes & FILE_ATTRIBUTE_SYSTEM) strcat(cBuffer, "\n\tFILE_ATTRIBUTE_SYSTEM"); if(dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) strcat(cBuffer,"\n\t FILE_ATTRIBUTE_DIRECTORY"); if(dwFileAttributes & FILE_ATTRIBUTE_ARCHIVE) 122 strcat(cBuffer,"\n\t FILE_ATTRIBUTE_ARCHIVE"); if(dwFileAttributes & FILE_ATTRIBUTE_NORMAL) strcat(cBuffer,"\n\t FILE_ATTRIBUTE_NORMAL"); if(dwFileAttributes & FILE_ATTRIBUTE_TEMPORARY) strcat(cBuffer,"\n\t FILE_ATTRIBUTE_TEMPORARY"); if(dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED) strcat(cBuffer,"\n\t FILE_ATTRIBUTE_COMPRESSED"); if(dwFileAttributes & FILE_FLAG_POSIX_SEMANTICS) strcat(cBuffer,"\n\t FILE_FLAG_POSIX_SEMANTICS"); if(dwFileAttributes & FILE_FLAG_BACKUP_SEMANTICS) strcat(cBuffer,"\n\t FILE_FLAG_BACKUP_SEMANTICS"); if(dwFileAttributes & FILE_FLAG_DELETE_ON_CLOSE) strcat(cBuffer, "\n\t FILE_FLAG_DELETE_ON_CLOSE"); if(dwFileAttributes & FILE_FLAG_SEQUENTIAL_SCAN) strcat(cBuffer,"\n\t FILE_FLAG_SEQUENTAL_SCAN"); if(dwFileAttributes & FILE_FLAG_RANDOM_ACCESS) strcat(cBuffer,"\n\t FILE_FLAG_RANDOM_ACCESS"); if(dwFileAttributes & FILE_FLAG_NO_BUFFERING) strcat(cBuffer,"\n\t FILE_FLAG_NO_BUFFERING"); if(dwFileAttributes & FILE_FLAG_OVERLAPPED) strcat(cBuffer,"\n\t FILE_FLAG_OVERLAPPED"); if(dwFileAttributes & FILE_FLAG_WRITE_THROUGH) strcat(cBuffer,"\n\t FILE_FLAG_WRITE_THROUGH"); return cBuffer; } bool setfileattributes(char* lpFileName) { DWORD adwFileAttributes[16]={ FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_HIDDEN, FILE_ATTRIBUTE_SYSTEM, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_ARCHIVE, FILE_ATTRIBUTE_NORMAL, FILE_ATTRIBUTE_TEMPORARY, FILE_ATTRIBUTE_COMPRESSED, FILE_FLAG_POSIX_SEMANTICS, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_DELETE_ON_CLOSE, FILE_FLAG_SEQUENTIAL_SCAN, FILE_FLAG_RANDOM_ACCESS, FILE_FLAG_NO_BUFFERING, FILE_FLAG_OVERLAPPED, FILE_FLAG_WRITE_THROUGH } ; int i,set; DWORD dwFileAttributes=GetFileAttributes(lpFileName); while (1) { system("cls"); cout<<getfileattributes(lpFileName)<<endl; cout << "\nATTRIBUTES:\n"; cout << "\tFILE_ATTRIBUTE_READONLY - 1" << endl; cout << "\tFILE_ATTRIBUTE_HIDDEN - 2"<< endl; 123 cout << "\tFILE_ATTRIBUTE_SYSTEM - 3"<< endl; cout << "\tFILE_ATTRIBUTE_DIRECTORY - 4"<< endl; cout << "\tFILE_ATTRIBUTE_ARCHIVE - 5"<< endl; cout << "\tFILE_ATTRIBUTE_NORMAL - 6"<< endl; cout << "\tFILE_ATTRIBUTE_TEMPORARY - 7"<< endl; cout << "\tFILE_ATTRIBUTE_COMPRESSED - 8"<< endl; cout << "\tFILE_FLAG_POSIX_SEMANTICS - 9"<< endl; cout << "\tFILE_FLAG_BACKUP_SEMANTICS- 10"<< endl; cout << "\tFILE_FLAG_DELETE_ON_CLOSE - 11"<< endl; cout << "\tFILE_FLAG_SEQUENTIAL_SCAN - 12"<< endl; cout << "\tFILE_FLAG_RANDOM_ACCESS - 13"<< endl; cout << "\tFILE_FLAG_NO_BUFFERING - 14"<< endl; cout << "\tFILE_FLAG_OVERLAPPED - 15"<< endl; cout << "\tFILE_FLAG_WRITE_THROUGH - 16"<< endl; cout << "\nType a number!(1..16 - change attribute/0 return for main menu) "; cin >> i; if (!i) return 0; else { cout << "\nType '1' to set attrebut and '0' to unset !(1/0) "; cin >> set; if(set==0) {dwFileAttributes=dwFileAttributes ^ adwFileAttributes[i-1]; } else if(set==1) {dwFileAttributes=dwFileAttributes | adwFileAttributes[i-1];} cout << endl<< "0x" << hex << dwFileAttributes << " : "; dec( cout ); for( int i = 31; i >= 0; i--) if( dwFileAttributes & (1 << i) ) cout << "1"; else cout << "0"; getch(); if(!SetFileAttributes(lpFileName,dwFileAttributes)) cerr << "Set File Attributes failed." << endl << "The last error code: " << GetLastError(); } } } int main() { char buf[256],FileName[256]=""; HANDLE hFile; DWORD lpFileSizeHigh; BY_HANDLE_FILE_INFORMATION fi; FILETIME ft;SYSTEMTIME st; int iMenu;char cMenu[200]="ВЫБЕРИТЕ ДЕЙСТВИЕ:\n1 ПРОСМОТРЕТЬ/ИЗМЕНИТЬ АТРИБУТЫ ФАЙЛА\n2 ПОСМОТРЕТЬ/ИЗМЕНИТЬ РАЗМЕР ФАЙЛА\n3 - БЛОКИРОВАТЬ/РАЗБЛОКИРОВАТЬ ФАЙЛ\n4 - ПОСМОТРЕТЬ ИНФОРМАЦИЮ О ФАЙЛЕ\n5 - ВЫЙТИ\n; CharToOem(cMenu, cMenu); CharToOem("Iм'я файлу?\n", buf); cout << buf; cin >> FileName; if ((hFile=creatfile(FileName))!=INVALID_HANDLE_VALUE) while(1) { 124 system("cls"); cout << cMenu; cin >> iMenu; switch(iMenu) { case 1: CharToOem("АТРИБУТЫ ФАЙЛА:\n", buf); cout << buf; cout<<getfileattributes(FileName)<<endl; CharToOem("\nИзменить атрибуты?(y/n) ",buf); cout << buf; cin >> buf; if(*buf=='y') setfileattributes(FileName); break; case 2: CharToOem("РАЗМЕР ФАЙЛА:\n", buf); cout << buf; cout<<FileName<<" "<<(floor(((double)GetFileSize(hFile,&lpFileSizeHigh)/1024)*100)/1 00)<< " KB" <<endl; CharToOem("\nИзменить размер?(y/n) ",buf); cout << buf; cin >> buf; if(*buf=='y') { CharToOem("Новый размер? ", buf); cout << buf;cin >>lpFileSizeHigh; SetFilePointer(hFile,lpFileSizeHigh,NULL,FILE_BEGIN); if(SetEndOfFile(hFile)) cout<<"size changed"; else cerr<<"size chang failed"<< endl <<"The last error code: "<< GetLastError(); } cout<<"\nPress any key to continue!"; getch(); break; case 3: CharToOem("ДОСТУП К ФАЙЛУ:\n\tблокировать 1\n\tразблокировать - 2\n", buf); cout << buf;cin>>buf; if (*buf=='1') { if(LockFile(hFile,0,0,GetFileSize(hFile,&lpFileSizeHigh),0)) cout << "file locked"; else cerr<<"file lock filed"<< endl <<"The last error code: "<< GetLastError(); }else if (*buf=='2') { if(UnlockFile(hFile,0,0,GetFileSize(hFile,&lpFileSizeHigh),0)) cout << "file unlocked"; else cerr<<"file unlock filed"<< endl <<"The last error code: "<< GetLastError(); } cout<<"\nPress any key to continue!"; 125 getch(); break; case 4: CharToOem("ИНФОРМАЦИЯ О ФАЙЛЕ:\n", buf); cout << buf; if(GetFileInformationByHandle(hFile,&fi)) { cout << endl << "ATTRIBUTES:" <<endl; if(fi.dwFileAttributes & FILE_ATTRIBUTE_ARCHIVE) cout<<"\tFILE_ATTRIBUTE_ARCHIVE\n"; if(fi.dwFileAttributes & FILE_ATTRIBUTE_COMPRESSED) cout<<"\tFILE_ATTRIBUTE_COMPRESSED\n"; if(fi.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) cout<<"\tFILE_ATTRIBUTE_DIRECTORY\n"; if(fi.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) cout<<"\tFILE_ATTRIBUTE_HIDDEN\n"; if(fi.dwFileAttributes & FILE_ATTRIBUTE_NORMAL) cout<<"\tFILE_ATTRIBUTE_NORMAL\n"; if(fi.dwFileAttributes & FILE_ATTRIBUTE_READONLY) cout<<"\tFILE_ATTRIBUTE_READONLY\n"; if(fi.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM) cout<<"\tFILE_ATTRIBUTE_SYSTEM\n"; if(fi.dwFileAttributes & FILE_ATTRIBUTE_TEMPORARY) cout<<"\tFILE_ATTRIBUTE_TEMPORARY\n"; FileTimeToSystemTime(&fi.ftCreationTime, &st); cout<<endl<<"FILE TIME:"<<endl <<"\tCreation Time "<<st.wYear<<""<<st.wMonth<<"-"<<st.wDay<<" "<<st.wHour<<":"<<st.wMinute<<":"<<st.wSecond<<endl; FileTimeToSystemTime(&fi.ftLastAccessTime, &st); cout<<"\tLast Access "<<st.wYear<<""<<st.wMonth<<"-"<<st.wDay<<" "<<st.wHour<<":"<<st.wMinute<<":"<<st.wSecond<<endl; FileTimeToSystemTime(&fi.ftLastWriteTime, &st); cout<<"\tLast Write "<<st.wYear<<""<<st.wMonth<<"-"<<st.wDay<<" "<<st.wHour<<":"<<st.wMinute<<":"<<st.wSecond<<endl; cout<<endl <<"Volume Serial Number "<<fi.dwVolumeSerialNumber<<endl; cout<<"File Size "<<(floor(((double)fi.nFileSizeLow/1024)*100)/100)<< " KB" <<endl; cout<<"Number Of Links "<<fi.nNumberOfLinks<<endl; cout<<"File Index (Low High) "<<fi.nFileIndexLow<<" "<<fi.nFileIndexHigh<<endl; } else cerr<<"Get file information filed"<< endl <<"The last error code: "<< GetLastError(); cout<<"\nPress any key to continue!"; 126 getch(); break; case 5: CloseHandle(hFile);return 0; } } return 0; } Задание на выполнение 1. Написать программу в которой были бы реализованы следующие функции работы с файлами: − определение и изменение атрибутов файла; − определение и изменение размеров файла; − блокирование файла; − получение информации о файле. 2. Выбор функции организовать в виде меню, пути до файлов вводить с клавиатуры. 3. Ознакомится с основными параметрами данных функций, исследовать их работу при изменении основных параметров. Контрольные вопросы 1. Какие существуют функции для работы со временем? 2. Какие значения атрибутов имеют файлы и какой функцией они устанавливаются? 3. Какой функцией можно изменить атрибуты файлов? 4. Какая функция создает имена для временных файлов? Лабораторная работа №10 Тема: Работа с каталогами с помощью Win32 API функций Цель работы: 1. Изучение основных функций Win32API, используемых для работы с каталогами. 2. Разработка приложения, демонстрирующего создание, удаление и перемещение каталогов, поиск файлов и наблюдение за каталогом. Краткое теоретическое введение 1. Создание каталога Для создания каталога используется функция: BOOL CreateDirectory( LPCTSTR lpPathName, // имя каталога LPSECUTITY_ATTRIBUTES lpSecurutyAttributes // атрибуты защиты ); Для создания подкаталогов можно использовать функцию CreateDirectoryEx. BOOL CreateDirectoryEx( LPCSTR lpTemplateDirectory, // имя шаблонного каталога 127 LPCTSTR lpNewDirectory, // имя нового каталога LPSECUTITY_ATTRIBUTES lpSecurutyAttributes // атрибуты защиты ); Для функции: 2. Поиск файлов в каталоге поиска файлов, находящихся в каталоге, используются HANDLE FindFirstFile( LPCTSTR lpFileName, // образец имени для поиска LPWIN32_FIND_DATA lpFindFileData // адрес данных о файле ); В случае успешного завершения функция FindFirstFile возвращает дескриптор для поиска файлов, который используется в дальнейшем функцией FindNextFile, а в случае неудачи — значение INVALID_HANDLE_VALUE. Параметр lpFindFileData должен указывать на структуру типа WIN32_FIND_DATA: typedef struct _WIN32_FIND_DATA { DWORD dwFileAttributes; // атрибуты файла FILETIME ftCreationTime; // время создания файла FILETIME ftLastAccessTime; //время последнего доступа к файлу FILETIME ftLastWriteTime; // время последней записи в файл DWORD nFileSizeHigh; // старшая часть размера файла DWORD nFileSizeLow; // младшая часть размера файла DWORD dwReserved0; // тег для преобразования файла DWORD dwReservedl; // не используется CHAR cFileName[ MAX_PATH ]; // длинное имя файла CHAR cAlternateFileName[ 14 ]; // короткое имя файла } WIN32_FIND_DATA, *PWIN32_FIND_DATA, *LPWIN32_FIND_DATA; FindNextFile: BOOL FindNextFile ( HANDLE hFindFile, // дескриптор для поиска файлов LPWIN32_FIND_DATA lpFindFileData // адрес данных о файле ); Кроме того, отметим, что после завершения поиска файлов нужно вызвать функцию FindClose, которая закрывает дескриптор поиска файлов: BOOL FindClose( HANDLE hFindFile // дескриптор поиска файла ); 3. Удаление каталога Для удаления пустого каталога предназначена функция: BOOL RemoveDirectory( LPCTSTR lpPathName ); // имя каталога 4. Перемещение каталога BOOL MoveFile( LPCTSTR lpExistingFileName, LPCTSTR lpNewFileName 128 // имя существующего файла // имя нового файла ); 5. Определение и установка текущего каталога DWORD GetCurrentDirectory( DWORD nBufferLength, // длина буфера для имени каталога LPTSTR lpBuffer // адрес буфера для имени каталога ); Приложение может изменить имя текущего каталога, используя функцию: BOOL SetСurrentDirectory( LPCTSTR lpPathName // имя нового текущего каталога ); 6. Наблюдение за изменениями в каталоге HANDLE FindFirstChangeNotification ( LPCTSTR lpPathName, // имя каталога BOOL bWatchSubtree, // опция наблюдения DWORD dwNotifyFilter // условия фильтра ); В случае неудачи функция возвращает значение INVALID_HANDLE_VALUE. dwNotifyFilter задает события, которые отслеживаются: FILE_NOTIFY_CHANGE_FILE_NAME – изменение имени файла; FILE_NOTIFY_CHANGE_DIR_NAME – изменение имени каталога; FILE_NOTIFY_CHANGE_ATTRIBUTES – изменение атрибутов; FILE_NOTIFY_CHANGE_SIZE – изменение размеров; Функция для наблюдения за последующими изменениями, происходящими в каталоге. BOOL FindNextChangeNotification( HANDLE hChangeHandle // дескриптор для наблюдения за изменениями ); После завершения наблюдения за изменениями в каталоге нужно вызвать функцию FindCloseChangeNotification, которая закрывает дескриптор наблюдения за изменениями в каталоге. BOOL FindCloseChangeNotification( HANDLE hChangeHandle // дескриптор для наблюдения за изменениями ); Листинг 1. Пример приложения, демонстрирующего работу с каталогами. #include <windows.h> #include <iostream> #include <conio.h> using namespace std; int main() { char buf[256],buf1[256]; int iMenu; char cMenu[200]="ВЫБЕРИТЕ ДЕЙСТВИЕ:\n1 - СОЗДАТЬ КАТАЛОГ\n2 НАЙТИ ФАЙЛЫ В КАТАЛОГЕ\n3 - УДАЛИТЬ КАТАЛОГ\n4 - ПЕРЕМЕСТИТЬ КАТАЛОГ\n5 - ОПРЕДЕЛИТЬ/ИЗМЕНИТЬ ТЕКУЩИЙ КАТАЛОГ\n6 - НАБЛЮДАТЬ ЗА КАТАЛОГОМ\n7 ВЫЙТИ\n"; CharToOem(cMenu, cMenu); while(1) { WIN32_FIND_DATA FindFileData; HANDLE hFind; LONG cFiles=1; BOOL bFindMore = TRUE; system("cls"); cout << cMenu; cin >> iMenu; switch(iMenu) 129 { system("cls"); case 1: CharToOem("СОЗДАТЬ КАТАЛОГ\n Имя каталога? ", buf); cout << buf; cin >> buf; if(!CreateDirectory(buf,NULL)) { cerr << "Create directory failed!" <<endl << "The last error code: " << GetLastError() <<endl; cout << "Press any key to continue"; getch(); break; } else cout << "directory created"; getch(); break; case 2: CharToOem("НАЙТИ ФАЙЛ В КАТАЛОГЕ\n Спецификация файла? ", buf); cout << buf; cin >> buf; if ((hFind=FindFirstFile(buf,&FindFileData)) == INVALID_HANDLE_VALUE) { cerr << endl << "Find First File failed!" << endl << "The last error code: " << GetLastError() <<endl; cout << "Press any key to continue"; getch(); break; } cout << endl << FindFileData.cFileName << endl; while (bFindMore) { if(bFindMore = FindNextFile(hFind, &FindFileData)) { cout << FindFileData.cFileName << endl; cFiles++; } } cout << endl << "The search is completed." << endl << cFiles << " files were found" << endl; cout<<"\nPress any key to continue!"; getch(); break; case 3: CharToOem("УДАЛИТЬ КАТАЛОГ\n Имя каталога?", buf); cout << buf;cin>>buf; if(!RemoveDirectory(buf)) { cerr << endl << "Remove Directory failed!" << endl << "The last error code: " << GetLastError() <<endl; cout << "Press any key to continue"; getch(); 130 break; } cout << "directory removed"; cout<<"\nPress any key to continue!"; getch(); break; case 4: CharToOem("ПЕРЕМЕСТИТЬ КАТАЛОГ\n Имя каталога источника?", buf); cout << buf; cin >> buf; CharToOem("Имя каталога получателя?", buf1); cout << buf1;cin >> buf1; if(!MoveFile(buf,buf1)) { cerr << endl << "Move Directory failed!" << endl << "The last error code: " << GetLastError() <<endl; cout << "Press any key to continue"; getch(); break; } cout << "directory moved"; cout<<"\nPress any key to continue!"; getch(); break; case 5: CharToOem("ТЕКУЩИЙ КАТАЛОГ:\n", buf); cout << buf; GetCurrentDirectory(sizeof(buf),buf); cout << buf <<endl; CharToOem("Изменить текущий каталог?(y/n) ", buf); cout << buf; cin >> buf; if(*buf=='y') { CharToOem("Задайте новый каталог! ", buf);cout << buf; cin >> buf; if(!SetCurrentDirectory(buf)) { cerr << endl << "Set Current failed!" << endl << "The last error code: " << GetLastError() <<endl; } else cout << "The current directory is set" << endl; } cout<<"\nPress any key to continue!"; getch(); break; case 6: CharToOem("НБЛЮДАТЬ ЗА КАТАЛОГОМ\n Имя каталога?", buf); cout << buf; cin>>buf; if ((hFind=FindFirstChangeNotification(buf,false, FILE_NOTIFY_CHANGE_FILE_NAME|FILE_NOTIFY_CHANGE_DIR_NAME|FILE_NOTIFY_CH ANGE_SIZE)) == INVALID_HANDLE_VALUE) { cerr << endl << "Find First Notification failed!" << endl << "The last error code: " << GetLastError() 131 <<endl; cout << "Press any key to continue"; getch(); break; } else cout << "Watch.." << endl; while (WaitForSingleObject(hFind,10000)!=WAIT_OBJECT_0) { } cout << endl << "The watched directory has been changed." << endl; bFindMore=false; cout << "Find next change notification?(y/n)"; cin >>buf; if(*buf='y') bFindMore=true; while (bFindMore) { if(bFindMore = FindNextChangeNotification(hFind)) { cout << "Watch.." << endl; while (WaitForSingleObject(hFind, 10000) != WAIT_OBJECT_0) {} cout << endl << "The watched directory has been changed again." << endl; cout << "Find next change notyfication?(y/n)"; cin >>buf; if(*buf=='y') bFindMore=true; else bFindMore=false;; } else { cerr << endl << "Find Notification failed!" << endl << "The last error code: " << GetLastError() <<endl; bFindMore=false; } } FindCloseChangeNotification(hFind); cout<<"\nPress any key to continue!"; getch(); break; case 7: return 0; } } return 0; } Задание на выполнение 1. Написать программу в которой были бы реализованы следующие функции работы с каталогами: − создание каталога; − поиск файлов в каталоге; − удаление каталога; − перемещение каталога; 132 − определение и установка текущего каталога; − наблюдение за изменениями в каталоге. 2. Выбор функции организовать в виде меню, пути до файлов и каталогов вводить с клавиатуры. 3. Ознакомится с основными параметрами данных функций, исследовать их работу при изменении основных параметров. Контрольные вопросы 1. Какие функции используются для создания каталогов? 2. Какие функции используются для удаления, перемещения и копирования каталогов? 3. С помощью какой функции можно организовать поиск файла в каталоге? 4. Поясните задачу функции: FindFirstFile и ее параметры. 5. Поясните задачу функци: FindNextFile и ее параметры. 6. Поясните задачу функции: FindClose и ее параметры. 7. Какую задачу выполняет функция GetFileAttributes и ее параметры? 8. Какова структура параметра lhffd? 9. Какой параметр функции FindFirstFile указывает на каталог? Лабораторная работа №11 Тема: Использование динамических библиотек для создания приложений Цель работы: 1. Изучение основных функций Win32 API, используемых для создания динамических библиотек DLL. 2. Разработка приложения, демонстрирующего создание, подключение и использование динамических библиотек. Краткое теоретическое введение Наиболее прямой путь создания программы – собрать исходный код всех функций, скомпилировать его и скомпоновать все в единый исполняемый образ. Эта монолитная модель с одним исполняемым образом проста, но обладает рядом недостатков. Динамические библиотеки могут быть использованы для создания разделяемых библиотек. Одной динамической библиотекой пользуются несколько программ, а в память загружается только одна ее копия. Все программы отображают адресные пространства своих процессов в код библиотеки, при этом каждый поток будет иметь собственную копию неразделяемой памяти в стеке. DLL (англ. Dynamic Link Library – динамически загружаемая библиотека) – реализованные компанией Microsoft общие библиотеки в ОС Windows и OS / 2. Как правило библиотеки имеют расширение файла *. dll, *. ocx (для библиотек, содержащих элементы управления ActiveX) или *. drv (драйверы старых версий ОС). Структура DLL такая же, как и в PE-файлов (Portable Executable) для 32 -, 64разрядных Windows, и New-Executable (NE) для 16-битных Windows. DLL может содержать код, данные и ресурсы любой комбинации. 133 Рисунок 1. Иллюстрация механизма связывания В DLL главная функция называется DllMain и вызывается операционной системой при загрузке DLL в адресное пространство процесса и при создании этим процессом нового потока. Главное назначение функции DllMain заключается в инициализации DLL при ее загрузке, а также захвате и освобождении необходимых ресурсов при создании и завершении нового потока в процессе. Эта функция имеет следующий прототип: BOOL WINAPI DllMain( HINSTANCE hinstDLL, // дескриптор DLL DWORD fdwReason, // флаг причины вызова функции DllMain LPVOID lpvReserved // зарезервировано Windows ); 1. Неявное связывание Неявное связывание, или связывание во время загрузки – более простой из двух способов. При использовании языка Microsoft Visual C+ + для этого необходим ряд шагов: 1. Функции для новой библиотеки собираются и компонуются как библиотека DLL, а не как, например, консольное приложение. 2. В процессе компоновки создается библиотечный файл .LIB, который является суррогатом для основного кода. Этот файл необходимо поместить в каталог вызывающей программы. 3. Процесс компоновки создает и файл .DLL, содержащий исполняемый образ. Обычно этот файл помещается в тот же каталог, что и приложение, которое будет его использовать, а приложение загружает .DLL во время инициализации. 2. Экспорт и импорт функций Наиболее существенное изменение, необходимое для переноса функции в динамическую библиотеку - это ее определение как экспортируемой. Это достигается путем использования модификатора для функции, помещаемой в динамическую библиотеку как показано ниже: _declspec (dllexport) DWORD MyFunction ( . . . ) ; _declspec – модификатор функции, MyFunction – функция, помещаемая в библиотеку. В этом случае процесс компоновки создаст файл библиотеки .DLL и файл .LIB. Файл .LIB – это суррогат библиотеки, который должен быть связан с вызывающей ее 134 программой для разрешения внешних ссылок и создания действительных связей с файлом .DLL во время загрузки и подключаемый на стадии компоновки. Вызывающая функцию программа должна определить импортируемую функцию путем использования модификатора _declspec (dllimport) DWORD MyFunction (...); При компоновке вызывающей библиотеку программы, т.е. перед созданием .exe файла, необходимо в меню Project -->Setting на вкладке Link в окне Project_Options набрать путь и имя файла библиотеки MyFunction.lib. После этого необходимо убедиться в том, что файл библиотеки .DLL доступен ей. Обычно это обеспечивается помещением файла .DLL в тот же каталог, в котором находится исполняемый файл. 3. Явное связывание Явное связывание, или связывание во время выполнения, несколько сложнее и требует от программы специального запроса для загрузки библиотеки (функция LoadLibrary) или ее выгрузки (функция FreeLibrary). Затем программа получает адрес нужной точки входа и использует его как указатель в вызове функции. В вызывающей программе функция не объявляется; вместо этого необходимо объявить переменную как указатель на функцию. Поэтому при компоновке программы нет потребности в файле библиотеки. Необходимы три функции: LoadLibrary, GetProcAddress и FreeLibrary. HINSTANCE LoadLibrary (LPCTSTR lpLibFileName); Возвращаемый дескриптор (типа HINSTANCE, а не HANDLE) не примет значения NULL в случае неудачи. Расширение .DLL в имени файла не требуется. С помощью функции LoadLibrary можно загрузить и файл типа .ЕХЕ. Так как динамические библиотеки разделяемые, система ведет учет ссылок на каждую DLL (их число увеличивается при вызове функции LoadLibrary), поэтому не требуется отображения фактически существующего файла. Даже если файл DLL найден, функция LoadLibrary выполнится неудачно в случае, если библиотека неявно связана с другой библиотекой, которая не может быть найдена. Аналогичная функция LoadLibraryEx имеет несколько флагов, используемых для указания альтернативных путей поиска и загрузки библиотеки как файла данных. После работы с данным экземпляром или для загрузки его другой версии необходимо освободить дескриптор библиотеки, освобождая тем самым ресурсы, включая и виртуальное адресное пространство, занятое библиотекой. Если счетчик ссылок указывает на то, что библиотека используется другим процессом, она остается загруженной в память: BOOL FreeLibrary (HINSTANCE hLibModule); После загрузки динамической библиотеки и до ее выгрузки можно получить адреса точек входа с использованием функции GetProcAddress. FARPROC GetProcAddress ( HMODULE module, LPCSTR lpProcName); Параметр hModule, несмотря на другой тип (тип HINSTANCE определен как HMODULE), является экземпляром, получаемым от функций LoadLibrary или GetModuleHandle, которые здесь не описаны. Параметр lpProcName, который не может быть строкой стандарта Unicode, является именем точки входа. В случае неудачи возвращаемое функцией значение будет NULL. Используя функцию GetModuleFileName, можно получить имя файла, 135 связанного с дескриптором hModule. И наоборот, по заданному имени файла (файл типа .ехе или .dll) функция GetModuleHandle возвращает дескриптор, связанный с файлом, если тот был загружен текущим процессом. Функция входа в библиотеку также в выполняется при создании или завершении процессом новых потоков. Листинг 1. Пример приложения, в котором одна функция выводит сообщение, другая функция возвращает принятое значение используя DLL. #include "stdafx.h" #include <iostream> using namespace std; extern "C++" __declspec(dllexport) void LetterList() { cout << "This function was called from LetterList() " << endl; } extern "C++" __declspec(dllexport) int PutInt(int param) { return param; } Листинг 2. Пример динамической библиотеки. // myFirstDll.dll #include <windows.h> int WINAPI DllMain(HINSTANCE hInstance, DWORD fdReason, PVOID pvReserved) { return TRUE; } extern "C" __declspec(dllexport) int MyFunction(char *str) { MessageBox(NULL, str,"Function from DLL",MB_OK); return 1; } Листинг 3. Пример приложения, вызывающего DLL. // myExe.cpp #include <windows.h> extern "C" __declspec(dllimport) int MyFunction(char *str); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { int iCode=MyFunction("Hello!"); return 0; } Порядок выполнения работы 1. Задание динамической библиотеки в MS Visual Studio 2010. 1.1 Создание DLL. Для того чтобы создать DLL в Visual Studio 2010, необходимо, как показано на рисунке 1, выполнить команду: Файл→Создать→Проект→Приложение Win32 (Можно консольное либо проект Win32). 136 Рисунок 1. Окно выбора типа проекта. Далее следует указать параметры приложения, как показано на рисунке 2. Рисунок 2. Окно выбора параметров приложения. После выбора, вводите имя и нажимаете ОК, затем далее и выбираете Тип приложения "библиотека DLL". После этого вы компилируете dll и получаете 2 файла: dll.dll и dll.lib (в нашем примере), затем необходимо создать файл dll.h (в нашем примере), где указываете функции, которые находятся в этом dll файле. Пример написания в хидер файл: void LetterList(); int PutInt(int param); В нем мы описываем прототипы наших функций. Этот файл вы будете вставлять в вашу программу, где будете подключать dll-файл. 1.2. DLL вызов функции. После того как полчим откомпилированный dll-файл, lib-файл и файл заголовков, все это нужно скопировать в нашу программу, где мы будем подключать dll файл. Для этого необходимо создать новый проект: Файл Создать - Проект - Консольное Приложение Win32 - Готово. В этот проект вы указываете, где будет exe файл dll.dll и dll.lib, а хидер файл (dll.h) вы указываете, где ваши хидер файлы вашего проекта. В полученный файл мы вписываем код программы, где вызываем наши две функции (PutInt иLetterList) #include "stdafx.h" #include "dll.h" #include<iostream> 137 #include<conio.h> using namespace std; int _tmain(int argc, _TCHAR* argv[]) { int x = PutInt(5); LetterList(); cout<<x; _getch(); return 0; } Обратите внимание, что подключается файл dll.h ( #include "dll.h" ), который мы создали. 1.3. Подключение dll файла в Visual Studio. Далее необходимо подключить файл. В Visual Studio это делается следущим образом: Проект - Свойства Компоновщик - Ввод - Дополнительные зависимости. Как показано на рисунке 3, здесь нужно добавить путь к файлу.lib. Рисунок 3. Окно «Свойства проекта». В нашем случае – это: D:\studio\Primer\Debug\dll.lib. Теперь компилируем проект и получаем результат, представленный на рисунке 4. Рисунок 4. Окно приложения Задание 2. Создание проекта библиотеки динамической компоновки (DLL) 1. В строке меню выберите Файл, Создать, Проект. 2. В левой области диалогового окна Новый проект разверните Установленные, Шаблоны, Visual C++ и затем выберите Win32. 138 3. В центральной области выберите Консольное приложение Win32. 4. Укажите имя для проекта, например MathFuncsDll, в поле Имя. Укажите имя для решения, например DynamicLibrary, в поле Имя решения. Нажмите кнопку ОК. 5. На странице Обзор диалогового окна Мастер приложений Win32 нажмите кнопку Далее. 6. На странице Параметры приложения выберите в поле Тип приложения пункт DLL. 7. Нажмите кнопку Готово, чтобы создать проект. 2.2. Добавление класса в библиотеку динамической компоновки 1. Чтобы создать файл заголовка для нового класса, в меню Проект выберите пункт Добавить новый элемент. В диалоговом окне Добавить новый элемент в левой области в разделе Visual C++ выберите Код. В центральной области выберите Заголовочный файл (.h). Укажите имя для файла заголовка, например MathFuncsDll.h, а затем нажмите кнопку Добавить. Показан пустой заголовочный файл. 2. Добавьте следующий код в начало файла заголовка: // MathFuncsDll.h #ifdef MATHFUNCSDLL_EXPORTS #define MATHFUNCSDLL_API __declspec(dllexport) #else #define MATHFUNCSDLL_API __declspec(dllimport) #endif 3. Добавьте базовый класс с именем MyMathFuncs для выполнения общих математических операций, таких как сложение, вычитание, умножение и деление. Код должен выглядеть примерно следующим образом: namespace MathFuncs { // This class is exported from the MathFuncsDll.dll class MyMathFuncs { public: // Returns a + b static MATHFUNCSDLL_API double Add(double a, double b); // Returns a - b static MATHFUNCSDLL_API double Subtract(double a, double b); // Returns a * b static MATHFUNCSDLL_API double Multiply(double a, double b); // Returns a / b // Throws const std::invalid_argument& if b is 0 static MATHFUNCSDLL_API double Divide(double a, double b); }; } Когда символ MATHFUNCSDLL_EXPORTS определен, символ MATHFUNCSDLL_API установит модификатор __declspec(dllexport) в объявлениях функций-членов в этом коде. Этот модификатор разрешает экспорт функции библиотекой DLL для использования ее другими приложениями. Если символ MATHFUNCSDLL_EXPORTS не определен, например, когда файл заголовка 139 включен приложением, символ MATHFUNCSDLL_API определяет модификатор __declspec(dllimport) в объявлениях функций-членов. Этот модификатор оптимизирует импорт функции в приложении. По умолчанию шаблон нового проекта для библиотеки DLL добавляет символ PROJECTNAME_EXPORTS в список определенных символов для проекта DLL. В этом примере символ MATHFUNCSDLL_EXPORTS определяется при сборке проекта MathFuncsDll. Если проект DLL собирается в командной строке, воспользуйтесь параметром компилятора /D, чтобы определить символ MATHFUNCSDLL_EXPORTS. 4. В проекте MathFuncsDll в обозревателе решений в папке Исходные файлы откройте файл MathFuncsDll.cpp. 5. Реализуйте функциональность класса MyMathFuncs в исходном файле. Код должен выглядеть примерно следующим образом: //MathFuncsDll.cpp: Defines the exported functions // for the DLL application. #include "stdafx.h" #include "MathFuncsDll.h" #include <stdexcept> using namespace std; namespace MathFuncs { double MyMathFuncs::Add(double a, double b) { return a + b; } double MyMathFuncs::Subtract(double a, double b) { return a - b; } double MyMathFuncs::Multiply(double a, double b) { return a * b; } double MyMathFuncs::Divide(double a, double b) { if (b == 0) { throw invalid_argument("b cannot be zero!"); } return a / b; } } 6. Скомпилируйте библиотеку динамической компоновки, выбрав Собрать решение в меню Сборка. Примечание: При использовании выпуска Express, в котором не отображается меню Сборка, в строке меню выберите Сервис, Параметры, Расширенные параметры, чтобы включить это меню, а затем выберите Сборка, Собрать решение. Примечание: При сборке проекта из командной строки используйте параметр компилятора /LD, указывающий на то, что выходной файл должен являться DLLфайлом..Используйте параметр компилятора /EHsc для включения обработки исключений С++. 140 2.3. Создание приложения, ссылающегося на DLL 1. Чтобы создать приложение С++, которое будет ссылаться и использовать созданную ранее библиотеку DLL, в меню Файл выберите пункт Создать и затем пункт Проект. 2. В левой области в категории Visual C++ выберите Win32. 3. В центральной области выберите Консольное приложение Win32. 4. Укажите имя проекта, например MyExecRefsDll, в поле Имя. В раскрывающемся списке рядом с полем Решение выберите Добавить в решение. В результате новый проект будет добавлен в решение, содержащее библиотеку DLL. Нажмите кнопку ОК. 5. На странице Обзор диалогового окна Мастер приложений Win32 нажмите кнопку Далее. 6. На странице Параметры приложения выберите в поле Тип приложения пункт Консольное приложение. 7. На странице Параметры приложения в разделе Дополнительные параметры снимите флажок Предкомпилированный заголовок. 8. Нажмите кнопку Готово, чтобы создать проект. Использование функциональности из библиотеки классов в приложении 1. После создания консольного приложения будет создана пустая программа. Имя исходного файла будет совпадать с ранее выбранным именем. В этом примере он имеет имя MyExecRefsDll.cpp. 2. Для использования в приложении математических процедур, созданных в библиотеке DLL, необходимо сослаться на эту библиотеку. Для этого в Обозревателе решений выберите проект MyExecRefsDll, а затем в меню Проект выберите пункт Ссылки. В диалоговом окне Страницы свойств разверните узел Общие свойства, выберите .NET Framework и ссылки и нажмите кнопку Добавить новую ссылку. 3. В диалоговом окне Добавление ссылки перечислены библиотеки, на которые можно создать ссылку.На вкладке Проект перечислены все проекты текущего решения и включенные в них библиотеки, если они есть. Установите флажок рядом с MathFuncsDll на вкладке Проекты, а затем нажмите кнопку ОК. 4. Для создания ссылки на файлы заголовка DLL необходимо изменить путь к каталогам включения. Для этого в диалоговом окне Страницы свойств последовательно разверните узлы Свойства конфигурации и C/C++, а затем выберите Общие. В поле Дополнительные каталоги включаемых файлов укажите путь к месту размещения файла заголовка MathFuncsDll.h. Можно использовать относительный путь, например ..\MathFuncsDll\. Затем нажмите кнопку ОК. 5. Теперь класс MyMathFuncs можно использовать в приложении. Замените содержимое файла MyExecRefsDll.cpp следующим кодом. // MyExecRefsDll.cpp // compile with: /EHsc /link MathFuncsDll.lib #include <iostream> #include "MathFuncsDll.h" using namespace std; int main() { double a = 7.4; 141 int b = 99; cout << "a + b = " << MathFuncs::MyMathFuncs::Add(a, b) << endl; cout << "a - b = " << MathFuncs::MyMathFuncs::Subtract(a, b) << endl; cout << "a * b = " << MathFuncs::MyMathFuncs::Multiply(a, b) << endl; cout << "a / b = " << MathFuncs::MyMathFuncs::Divide(a, b) << endl; try { cout << "a / 0 = " << MathFuncs::MyMathFuncs::Divide(a, 0) << endl; } catch (const invalid_argument &e) { cout << "Caught exception: " << e.what() << endl; } return 0; } 6. Соберите исполняемый файл, выбрав команду Собрать решение в меню Сборка. 2.4. Запуск приложения 1. Убедитесь в том, что проект MyExecRefsDll выбран в качестве проекта по умолчанию. В Обозревателе решений выберите MyExecRefsDll и затем в меню Проект выберите Назначить запускаемым проектом. 2. Чтобы запустить проект, в строке меню выберите Отладка, Запуск без отладки. Результат выполнения должен выглядеть примерно следующим образом: 1. a + b = 106,4 2. a - b = -91,6 3. a * b = 732,6 4. a / b = 0,0747475 Перехвачено исключение: b не может быть равно нулю! Контрольные вопросы 1.Какие способы создания динамических библиотек существуют? 2.Опишите процесс неявного связывания функций в динамической библиотеке. 3.Как осуществляется процесс экспорта, импорта функций в динамическую библиотеку при неявном связывании? 4.Опишите функции, которые используются при явном связывании функций в библиотеке (LoadLibrary, FreeLibrary, GetProcAddress). 5.Как создается .DLL файл для функции Asc2Un? 6.Как в вызывающей программе импортируется функция из динамической библиотеки? 7.Поясните работу программы преобразования файла из кодировки ASCII в кодировку Unicode. 142 Список рекомендованной литературы 1. А. П. Побегайло Системное программирование в Windows. СПб.: БХВПетербург, 2006. – 1056 с.: ил. 2. П.В. Румянцев Азбука программирования в Win32 API. М.: Горячая ЛинияТелеком, 2004. – 312 с. 3. В.Г. Гальченко Лабораторный практикум. Системное программирование в среде Win32. Создание Windows приложений. Томск::Изд. ТПУ, 2009. – 68 с. 4. Дж. Рихтер Windows для профессионалов: создание эффективных Win32приложений с учетом специфики 64-разрядной версии Windows / Пер. с англ. – 4-е изд. СПб.: Питер; М.: Издательство «Русская Редакция»; 2008. – 720 стр., ил. 5. Харт М. Джонсон Системное программирование в среде Windows/Пер. с англ. – 3-изд. М.: Издательский дом «Вильямс», 2005. – 592 с., ил. 6. К. Г. Финогенов Win32. Основы программирования. 2-е изд. М.: ДиалогМИФИ, 2006. – 416 с. 7. Р. Лав Linux. Системное программирование. СПб.: Питер, 2008. – 416 с., ил. 8. В.Ю. Пирогов Ассемблер для Windows. СПб.: БХВ-Петербург, 2012. – 896 с., ил. 9. У. Они Использование Microsoft Windows Driver Model. 2-е изд. CПб.: Питер, 2007. – 768 с. 10. М. Руссинович, Д. Соломон Внутреннее устройство Microsoft Windows. 6-е изд. CПб.: Питер, 2013. – 800 с. 143