Камчатский государственный технический университет Кафедра систем управления
Ю.В. Марапулец
ОПЕРАЦИОННЫЕ СИСТЕМЫ Метод...
101 downloads
303 Views
775KB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
Камчатский государственный технический университет Кафедра систем управления
Ю.В. Марапулец
ОПЕРАЦИОННЫЕ СИСТЕМЫ Методические указания по выполнению лабораторных работ № 1–8 по курсу "Операционные системы" для студентов специальности 220400 "Программное обеспечение вычислительной техники и автоматизированных систем"
Петропавловск-Камчатский 2004
УДК 681.306 ББК 32.973-018.2 М25
Рецензент: Г.А. Пюкке, кандидат технических наук, заведующий кафедрой систем управления КамчатГТУ
Марапулец Ю.В. М25
Операционные системы. Методические указания по выполнению лабораторных работ №1–8 для студентов специальности 220400 "Программное обеспечение вычислительной техники и автоматизированных систем". – Петропавловск-Камчатский: КамчатГТУ, 2004. – 86 с.
Методические указания предназначены для выполнения лабораторных работ по курсу "Операционные системы". В тексте приводятся задания для каждой лабораторной работы, краткие теоретические сведения, требования по оформлению отчета. Рекомендовано к изданию решением учебно-методического совета КамчатГТУ (протокол № 6 от 20 февраля 2004 г.).
УДК 681.306 ББК 32.973-018.2
© КамчатГТУ, 2004 © Марапулец Ю.В., 2004 2
ВВЕДЕНИЕ Лабораторные работы по курсу "Операционные системы" выполняются в среде разработчика Visual C++ 6 на языке программирования С++. Программы должны устойчиво работать в операционных системах Windows 95–98, ME, NT4, 2000, XP (лабораторная работа № 5 выполняется только в операционных системах Windows NT, 2000, XP).
ПОРЯДОК ВЫПОЛНЕНИЯ ЛАБОРАТОРНЫХ РАБОТ 1. Подготовка и допуск к работе. 1.1. К выполнению лабораторной работы допускаются студенты, которые подготовились к работе и имеют не более двух незащищенных работ. 1.2. Перед работой каждый студент должен: – предъявить преподавателю полностью предыдущей работе; – ответить на вопросы преподавателя.
оформленный
отчет
о
1.3. К работе не допускаются студенты, которые не выполнили одно из вышеперечисленных требований. 1.4.Лабораторные работы, которые студент пропустил, выполняются в конце семестра; допуск к работе производится в порядке, указанном в п.1.1. 2. Требования по содержанию отчета по лабораторным работам приведены в конце каждой лабораторной работы.
3
ЛАБОРАТОРНАЯ РАБОТА №1 "Многопоточное приложение" Цель работы: Изучение принципов разработки программы, позволяющей использовать несколько потоков (На примере программы Threads). Задание к лабораторной работе: 1. Запустить программу Threads. Результат работы программы представлен на рис.1.1. В результате исполнения создаются четыре вторичных потока, каждый из которых рисует в дочернем окне прямоугольники, задавая их размеры и цвет случайным образом. В верхней части окна находится список, хранящий информацию обо всех четырех потоках. Выделив какой-нибудь элемент списка и выбрав определенную команду меню Thread, можно приостановить любой из потоков, возобновить его выполнение или изменить приоритет. С помощью меню Options можно также активизировать исключающий семафор, который позволит в каждый момент времени выполняться только одному потоку.
Рис.1.1. Окно программы Threads'
4
2. Рассмотреть исходный код программы. Первоначально рассмотреть функции инициализации. Данный класс функций регистрируют два класса окон, один из которых предназначен для главного окна, а другой - для дочерних окон, в которых потоки выполняют свои графические операции. Кроме того, создается таймер, позволяющий с пятисекундным интервалом обновлять в списке информацию о каждом из потоков. Функция CreateWindows создает и позиционирует все окна, в том числе и список, в котором содержится информация о каждом потоке. Четыре потока создаются в обработчике сообщения WM_CREATE. /* WIN MAIN - вызов функции инициализации и запуск цикла обработки сообщений */ int WINAPI WinMain ( HINSTANCE hinstThis, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int iCmdShow ) { MSG msg; hInst = hinstThis; // запись в глобальную переменную if (! InitializeApp ( )) { //выход из программы, если приложение не было инициализировано return( 0 ) ; } ShowWindow ( hwndParent, iCmdShow ); UpdateWindow( hwndParent ); // получение сообщений из очереди while ( GetMessage( &msg, NULL, 0, 0 ) ) { TranslateMessage( &msg ) ; DispatchMessage ( &msg ) ; } return( msg.wParam ); } Обратить внимание на отсутствие функции PeekMessage. Это - наглядное свидетельство того, что в приложении реализована приоритетная многозадачностью. (Многозадачность данного типа подразумевает, что система прерывает выполнение потока, предоставляя другим потокам возможность получить доступ к ресурсам центрального процессора. При кооперативной многозадачности система ожидает, пока поток не вернет ей управление над процессором.) Потоки имеют возможность непрерывно производить экранные операции, не монополизируя процессор. В это же время могут выполняться и другие программы. Помимо регистрации класса приложения и выполнения стандартных действий по инициализации, функция InitializeApp задает приоритеты потоков и запускает все потоки в режиме ожидания. 5
/*INITIALIZE APP - регистрация двух классов и создание окон. */ BOOL InitializeApp ( void ) { … // Пометить исходное состояние каждого потока как SUSPENDED // сразу при их создании. for( iCount = 0; iCount < 4; iCount++ ) { iState[iCount] = SUSPENDED; } // Задать первичному потоку более высокий приоритет, // что позволит ускорить ввод/вывод команд пользователем. SetThreadPriority( GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL ); // Создать все окна. return( CreateWindows( ) ); } Вызов функции SetThreadPriority приводит к увеличению приоритета первичного потока. Если все вторичные потоки будут иметь такой же приоритет, как и первичный, то реакция на выбор команд меню будет очень медленной. Убедиться в этом, запустив программу и повысив приоритет вторичных потоков. Функция CreateWindow создает не только основное окно, но и список, а также набор дочерних окон для потоков. /* CREATE WINDOWS - создать главное окно, окно списка и четыре дочерних окна */ BOOL CreateWindows ( void ) { char szAppName[MAX_BUFFER] ; char szTitle[MAX_BUFFER] ; char szThread[MAX_BUFFER]; HMENU hMenu; int iCount; // загрузка соответствующих строк LoadString( hInst, IDS_APPNAME, szAppName, sizeof(szAppName)); LoadString( hInst, IDS_TITLE, szTitle, sizeof(szTitle)); LoadString( hInst, IDS_THREAD, szThread, sizeof(szThread)); // создать родительское окно hMenu = LoadMenu( hInst, MAKEINTRESOURCE(MENU_MAIN) ); hwndParent = CreateWindow( szAppName, szTitle, WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, 6
CW_USEDEFAULT, CW_USEDEFAULT, NULL, hMenu, hinst, NULL ) ; if( ! hwndParent) { return( FALSE ) ; } // создать окно списка hwndList = CreateWindow( "LISTBOX", NULL, WS_BORDER | WS_CHILD | WS_VISIBLE | LBS_STANDARD | LBS_NOINTEGRALHEIGHT, 0, 0, 0, 0, hwndParent, (HMENU)1, hinst, NULL ) ; if( ! hwndList ) { return( FALSE ) ; } // создать четыре дочерних окна for( iCount = 0; iCount < 4; iCount++ ) { hwndChild[iCount] = CreateWindow( "ThreadClass", NULL, WS_BORDER | WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN, 0, 0, 0, 0, hwndParent, NULL, hInst, NULL ); if(! hwndChild ) return( FALSE ); } return( TRUE ); } 3. Рассмотреть функции обработки сообщений. Большинство функций обработки сообщений очень просты. Функция Main_OnTimer вызывает функцию, которая очищает список, генерирует четыре новые информационные строки и выводит их в окне списка. Функция Main_OnSize приостанавливает все вторичные потоки, пока программа изменяет положение дочерних окон в соответствии с новыми размерами родительского окна. В противном случае работающие потоки будут замедлять выполнение операции отображения. Функция Main_OnCreate создает потоки, а также исключающий семафор. /* MAIN_WNDPROC - обработка всех сообщений главного окна */ LRESULT WINAPI Main_WndProc( HWND hWnd, // адрес сообщения UINT uMessage, // тип сообщения WPARAM wParam, // содержимое сообщения LPARAM lParam ) // дополнительное содержимое { switch( uMessage ) 7
{ HANDLE_MSG( hWnd, WM_CREATE, Main_OnCreate ); // Создание окна и потоков HANDLE_MSG( hWnd, WM_SIZE, Main_OnSize ); // Согласование положения дочерних окон при изменении размеров // главного окна HANDLE_MSG( hWnd, WM_TIMER, Main_OnTimer ) ; // Обновление списка через каждые пять секунд HANDLE_MSG( hWnd, WM_INITMENU, Main_OnInitMenu ) ; // Если переменная bUseMutex равна TRUE, пометить пункт меню // Use Mutex. HANDLE_MSG( hWnd, WM_COMMAND, Main_OnCommand ); // Обработка команд меню HANDLE_MSG( hWnd, WM_DESTROY, Main_OnDestroy ); // Очистка экрана и выход из программы default: return( DefWindowProc(hWnd, uMessage, wParam, lParam) ); } return( 0L ) ; } В программе Threads используется макрокоманда обработки сообщений HANDLE_MSG, поэтому компилятор может выдать ряд предупреждений типа "Unreferenced formal parameter" (Неопознанный формальный параметр). Во избежание этого в программу следует включить директиву argsused: #ifdef _BORLANDC_ #pragma argsused #endif Функция Main_OnCreate завершает процесс инициализации потоков и создает исключающий семафор: /* MAIN_ONCREATE - создать четыре потока и установить таймер */ BOOL Main_OnCreate( HWND hWnd, LPCREATESTRUCT lpCreateStruct ) { UINT uRet; int iCount; // создание четырех потоков, приостановленных в исходном состоянии for( iCount = 0; iCount < 4; iCount++ ) { iRectCount[iCount] = 0; dwThreadData[iCount] = iCount; hThread[iCount] = CreateThread( NULL, 0, 8
(LPTHREAD_START_ROUTINE) StartThread, (LPVOID) ( & ( dwThreadData[iCount] ) ), CREATE_SUSPENDED, (LPDWORD) ( & ( dwThreadID(iCount] ) ) ); if( ! hThread[iCount] ) // Был ли поток создан? { return( FALSE ) ; } } // создание таймера с пятисекундным периодом срабатывания; // использование таймера для обновления списка uRet = SetTimer( hWnd, TIMER, 5000, NULL ); if( ! uRet ) { // создать таймер не удалось return( FALSE ) ; } // создание исключающего семафора hDrawMutex = CreateMutex( NULL, FALSE, NULL ); if( ! hDrawMutex ) { //не удалось создать исключающий семафор KillTimer( hWnd, TIMER ); // остановка таймера return( FALSE ) ; } // запуск потоков с приоритетом ниже стандартного for( iCount = 0; iCount < 4; iCount++ ) { SetThreadPriority( hThread[iCount], THREAD_PRIORITY_BELOW_NORMAL ); iState[iCount] = ACTIVE; ResumeThread( hThread[iCount] ); } // Теперь запущены все четыре потока! return( TRUE ) ; } Функция Main_OnSize не только изменяет размер окна приложения, но и корректирует размеры и положение всех дочерних окон. /* MAIN_ONSIZE - Позиционирует окно списка и четыре дочерних окна. */ void Main_OnSize( HWND hWnd, UINT uState, int cxClient, int cyClient ) { char* szText =-"No Thread Data"; int iCount; // Приостанавливает активные потоки, пока не будет завершено // изменение размеров и обновление соответствующих окон. 9
// Эта пауза значительно ускоряет процесс обновления содержимого экрана. for( iCount = 0; iCount < 4; iCount++ ) { if ( iState[iCount] == ACTIVE ) SuspendThread ( hThread[iCount] ); } // Размещает список в верхней четверти главного окна. MoveWindow( hwndList, 0, 0, cxClient, cyClient / 4, TRUE ); // Размещает четыре дочерних окна в трех нижних четвертях // главного окна. // Левая граница первого окна имеет координату 'х', равную 0. MoveWindow ( hwndChild[0], 0, cyClient /4-1, cxClient /4 + 1, cyClient, TRUE ); for( iCount = 1; iCount < 4; iCount++ ) { MoveWindow( hwndChild[iCount], (iCount * cxClient) / 4, cyClient /4-1, cxClient / 4 +1, cyClient, TRUE ); } // Вводит в список строковые значения, заданные по // умолчанию, и выполняет инициализацию. // Начальное число прямоугольников равно 0. for( iCount = 0; iCount < 4; iCount++ ) { iRectCount[iCount] = 0; ListBox_AddString( hwndList, szText ); } ListBox_SetCurSel(hwndList, 0); // Возобновляет выполнение потоков, которые были приостановлены // на время обновления окна. for( iCount = 0; iCount < 4; iCount++ ) { if( iState[iCount] == ACTIVE) { ResumeThread( hThread[iCount] ); } } return; } Обновление окна списка по сообщениям таймера - решение далеко не идеальное. Гораздо целесообразнее, чтобы этот процесс инициировался теми операциями, которые приводят к изменению исходных данных. Однако применение таймера можно считать простейшим способом выполнения поставленной задачи. 10
/*MAIN_ONTIMER - Использует сообщение таймера для обновления списка. */ void Main_OnTimer( HWND hWnd, UINT uTimerID ) { // Обновляет данные, представленные в списке UpdateListBox() ; return; } Функция Main_OnlnitMenu просто выполняет установку или снятие отметки команды меню Use Mutex. /* MAIN_ONINITMENU - Устанавливает или снимает отметку команды меню Use Mutex на основании значения переменной bUseMutex. */ void Main_OnInitMenu( HWND hWnd, HMENU hMenu ) { CheckMenuItem ( hMenu, IDM_USEMUTEX, MF_BYCOMMAND | (UINT)( bUseMutex ? MF_CHECKED : MF_UNCHECKED ) ) ; return; } Функция Main_OnCommand объединяет все сообщения, для которых нет специальных обработчиков. (Обработчики сообщений группируются в функции Main_WndProc, которая с помощью макрокоманды HANDLE_MSG перенаправляет их на обработку соответствующим функциям.) /* MAIN_ONCOMMAND - Реагирует на команды пользователя. */ void Main_OnCommand ( HWND hWnd, int iCmd, HWND hwndCtl, UINT uCode ) { switch ( iCmd ) { case IDM_ABOUT: // вывод окна About MakeAbout ( hWnd ) ; break; case IDM_EXIT: // выход из программы DestroyWindow( hWnd ) ; break; case IDM_SUSPEND: // изменение приоритета или состояния потока case IDM_RESUME: case IDM_INCREASE: case IDM_DECREASE: DoThread( iCmd ); // модификация параметров потока; case IDM_USEMUTEX: // включение или отключение // исключающего семафора ClearChildWindows( ); // установление белого цвета 11
// для окон всех потоков bUseMutex = !bUseMutex; // переключение параметров // исключающего семафора default: break; } return; } 4. Рассмотреть функции изменения параметров. Функция DoThread в ответ на соответствующие команды меню изменяет параметры потока, выбранного в списке. Эта функция может повысить или понизить приоритет потока, а также приостановить или возобновить его выполнение. Текущее состояние каждого потока записывается в массив iState. В массиве hThreads сохраняются дескрипторы каждого из четырех вторичных потоков. /*DO THREAD - Изменяет приоритет потока или его состояние в ответ на команды меню. */ void DoThread( int iCmd ) { int iThread; int iPriority; // Определяет, какой из потоков выбран. iThread = ListBox_GetCurSel ( hwndList ) ; switch ( iCmd ) { case IDM_SUSPEND: // Если поток не остановлен, останавливает его. if( iStatefiThread] != SUSPENDED ) { Suspend/Thread ( hThread[iThread] ); iState[iThread] = SUSPENDED; } break; case IDM_RESUME: // Если поток не активен, активизирует его. if( iState[iThread] != ACTIVE ) { ResumeThread( hThread(iThread] ); iState[iThread] = ACTIVE; } break; case IDM_INCREASE: // Повышает приоритет потока, если только он 12
// уже не является самым высоким iPriority = GetThreadPriority( hThread[iThread] ); switch( iPriority ) { case THREAD_PRIORITY_LOWEST: SetThreadPriority( hThread[iThread], THREAD_PRIORITY_BELOW_NORMAL ) ; break; case THREAD_PRIORITY_BELOW_NORMAL: SetThreadPriority( hThread[iThread], THREAD_PRIORITY_NORMAL ) ; break; case THREAD_PRIORITY_NORMAL: SetThreadPriority( hThread[iThread], THREAD_PRIORITY_ABOVE_NORMAL ) ; break; case THREAD_PRIORITY_ABOVE_NORMAL: SetThreadPriority( hThread[iThread], THREAD_PRIORITY_HIGHEST ) ; break; default: break; } break; case IDM_DECREASE: // Понижает приоритет потока, если только он //не является самым низким iPriority = GetThreadPriority( hThread[iThread] ); switch( iPriority ) { case THREAD_PRIORITY_BELOW_NORMAL: SetThreadPriorityt hThread[iThread], THREAD_PRIORITY_LOWEST ) ; break; case THREAD_PRIORITY_NORMAL: SetThreadPriority (hThread[iThread], THREAD_PRIORITY_BELOW_NORMAL ) ; break; case THREAD_PRIORITY_ABOVE_NORMAL: SetThreadPriority( hThread[iThread], THREAD_PRIORITY_NORMAL ) ; break; case THREAD_PRIORITY_HIGHEST: SetThreadPriority( hThread[iThread], 13
THREAD_PRIORITY_ABOVE_NORMAL ) ; break; default: break; } break; default: break; } return; } 5. Рассмотреть функции для выполнения операций с потоками. Создав вторичные потоки, функция Main_OnCreate при каждом вызове функции CreateThread передает ей указатель на функцию StartThread. Последняя становится стартовой функцией для всех потоков. Она начинает и завершает их выполнение. Если флаг bUseMutex имеет значение TRUE, потоки ожидают разрешения исключающего семафора, который обеспечивает одновременное выполнение графических операций только одним потоком. /*START THREAD - Эта процедура вызывается на начальном этапе выполнения каждого потока. */ LONG StartThread ( LPVOID lpThreadData ) { DWORD *pdwThreadID; // указатель на переменную типа DWORD // для записи идентификатора потока DWORD dwWait; // результирующее значение // функции WaitForSingleObject // получение идентификатора потока pdwThreadID = lpThreadData; // Выполнение графических операций до тех пор, пока // значение флага bTerminate не станет равным TRUE. while( ! bTerminate ) { if (bUseMutex) // Используется ли исключающий семафор? { // Выполнение действий при получении разрешения // от исключающего семафора. dwWait = WaitForSingleObject( hDrawMutex, INFINITE ); if( dwWait == 0 ) { DrawProc( *pdwThreadID ); // рисование // прямоугольников 14
ReleaseMutex( hDrawMutex ); // разрешить выполнение // других потоков } } else { // Исключающий семафор не используется, // разрешить выполнение потока. DrawProc( *pdwThreadID ) ; } } // Этот оператор неявно вызывает команду ExitThread. return ( 0L ) ; } Функция DrawProc предназначена для рисования прямоугольников. Однако во избежание перегрузки системы из-за передачи большого количества мелких сообщений между программой и подсистемой Win32 обращение к GDI не всегда происходит сразу же. Поэтому графические команды становятся в очередь и периодически выполняются все одновременно. Такие задержки несколько преуменьшают влияние приоритетов потоков в программе Threads. /*DRAW PROC - Рисует хаотически расположенные прямоугольники. */ void DrawProc ( DWORD dwID ) { if (bUseMutex) { iTotal = 50; // Если выполняется только один поток, } // разрешить ему рисовать большее число фигур. else { iTotal = 1; } // сброс генератора случайных чисел srand( iRandSeed++ ); // получение размеров окна bError = GetClientRect( hwndChild[dwID], &rcClient ); if( ! bError ) return; cxClient = rcClient.right - rcClient.left; cyClierit = rcClient. bottom - rcClient.top; //не рисовать, если не заданы размеры окна if( ( ! cxClient ) || ( ! cyClient ) ) { 15
return; } // получить контекст устройства для рисования hDC = GetDC( hwndCbild[dwID] ); if( hDC ) { // нарисовать группу случайно расположенных фигур for( iCount = 0; iCount < iTotal; iCount++ ) { iRectCount[dwID]++; // задать координаты xStart = (int)( rand() % cxClient ); xStop = (int)( rand() % cxClient ); yStart = (int) ( rand() % cyClient ); yStop = (int)( rand() % cyClient ); // задать цвет iRed = rand() & 255; iGreen = rand() &, 255; iBlue = rand() & 255; // создать сплошную кисть hBrush = CreateSolidBrush( // предотвратить появление // полутоновых цветов GetNearestColor( hDC, RGB( iRed, iGreen, iBIue ) ) ); hbrOld = SelectBrush( hDC, hBrush); // нарисовать прямоугольник Rectangle( hDC, min( xStart, xStop ), max ( xStart, xStop ), min( yStart, yStop ), max( yStart, yStop ) ); // удалить кисть DeleteBrush( SelectBrush(hDC, hbrOld) ); } // Если выполняется только один поток, // очистить дочернее окно до начала выполнения // другого потока. if( bUseMutex ) { SelectBrush( hDC, GetStockBrush(WHITE_BRUSH) ); PatBlt( hDC, (int)rcClient.left, (int)rcClient.top, (int)rcClient.right, (int)rcClient.bottom, PATCOPY ) ; } // освободить контекст устройства ReleaseDC( hwndChild[dwID], hDC ); } return; } 16
1. 2. 3. 4.
Содержание отчета: Цель работы; Исходный текст программы; Результаты работы программы (главное окно в ОС Windows); Выводы по проделанной работе с указанием достоинств и недостатков предложенного исходного кода.
ЛАБОРАТОРНАЯ РАБОТА №2 "Выделение памяти" Цель работы: Изучение принципов разработки программы, позволяющей резервировать и закреплять память (На примере программы List). Задание к лабораторной работе: 1. Запустить программу List. Результат работы программы представлен на рис.2.1. В результате исполнения создается список, резервируется и закрепляется виртуальная память, когда пользователь вводит новые элементы списка. Список, заполняющий рабочую область окна, содержит перечень всех введенных элементов. Меню List позволяет добавлять и удалять элементы списка. 2. Рассмотреть исходный текст программы. В файле заголовков определены две константы, которые управляют размером и структурой списка. Для каждого элемента списка выделено 1024 символа. Поскольку размер системной страницы (4 Кб) кратен размеру элемента списка (1 Кб), ни один элемент не пересечет границу между страницами. Следовательно, для чтения любого элемента не потребуется загрузка с диска более одной страницы. В программе List максимальный размер массива составляет 500 элементов. Среди глобальных переменных, определенных в программе, имеется два массива: iListLookup и bInUse. int iListLookup[MAX_ITEMS]; // согласует индекс элемента списка // с его положением в массиве BOOL bInUse[MAX_ITEMS]; // отмечает используемые элементы списка Первый массив связывает строки списка с элементами динамического массива. Если элемент массива iListLookup[4] равен 7, строка с номером 5 из списка будет записана в элементе динамического массива с индексом 7. (Как в списке, так и в динамическом массиве нумерация элементов начинается с 0.) 17
Второй массив содержит значения типа Boolean для каждого элемента динамического массива. Когда программа добавляет или удаляет строки, она устанавливает для соответствующего элемента массива bInUse значение TRUE, если позиция занята, и FALSE, если она пуста. При добавлении строки программа ищет пустой элемент в массиве bInUse, а при удалении определяет позицию элемента массива, обращаясь к массиву iListLookup.
Рис 2.1 Окно, позволяющее добавлять новые элементы в список При запуске программы осуществляется вызов функции CreateList, которая резервирует память и инициализирует вспомогательные структуры данных. Команда VirtualAlloc резервирует блок адресов размером 1 Мб. Подобно остальным зарезервированным страницам, адреса должны быть помечены флагом PAGE_NOACCESS, пока они не будут закреплены. BOOL CreateList ( void ) { int i; // зарезервировать 1 Мб адресного пространства pBase = VirtualAlloc( NULL, // начальный адрес (произвольный) MAX_ITEMS * ITEM_SIZE, // один мегабайт MEM_RESERVE, // зарезервировать; //не закреплять PAGE_NOACCESS ); // нет доступа if( pBase == NULL ) { ShowErrorMsg( __LINE__ ) ; return( FALSE ) ; 18
} // инициализация служебных массивов for ( i = 0; i < MAX_ITEMS; i++ ) { bInUse[i] = FALSE; // ни один из элементов // не используется iListLookup[i] = 0; } bListEmpty = TRUE; // обновить глобальные флаги bListFull = FALSE; return ( TRUE ) ; } 3. Добавить элемент списка. При выборе пункта Add Item в меню программы List функция AddItem выполняет следующие действия. 1. Находит первый свободный элемент в массиве (iIndex). 2. Предлагает пользователю ввести новую строку в диалоговом окне. 3. Копирует новую строку в блок памяти, выделенный при выполнении команды CreateList. 4. Обновляет список и несколько глобальных переменных. Первый свободный элемент массива может занимать незакрепленную страницу памяти. В этом случае команда lstrcpy, которая пытается записать новую строку, генерирует исключение. Обработчик исключений ожидает, когда поступит сигнал EXCEPTION_ACCESS_VIOLATIUN, и реагирует на него вызовом функции VirtualAlloc, которая закрепляет страницу из предварительно зарезервированного интервала. void AddItem ( void ) { char szText[ITEM_SIZE]; int iLen; int iIndex; int iPos; int iCount; BOOL bDone;
// текст для одного элемента // длина строки // положение в массиве // положение в списке //счетчик элементов списка // TRUE, если найден свободный // элемент массива // определение положения первого свободного элемента bDone = FALSE; iIndex = 0; while( ( ! bDone ) && ( iIndex < MAX_ITEMS ) ) { if( ! bInUse[iIndex] ) bDone = TRUE;
// используется ли данный элемент? // обнаружен свободный элемент 19
else iIndex++;
// переход к следующему элементу
} // предложить пользователю ввести новую строку iLen = GetItemText(szText); if( ! iLen ) return; В блоке try новый текст копируется в свободную часть массива. Если соответствующая страница памяти не закреплена, функция lstrcpy порождает исключение. Фильтр исключений закрепляет страницу, и выполнение функции продолжается. try {
// записать текст в элемент массива lstrcpy( &( pBase[iIndex * ITEM_SIZE) ), szText );
} except( CommitMemFilter( GetExceptionCode(), iIndex ) ) { // вся работа выполняется фильтром исключений } // пометить данный элемент как занятый bInUse[iIndex] = TRUE; bListEmpty = FALSE; Далее программа добавляет новый текст в список. Строка вставляется в позицию, которая задана переменной iPos. При этом обновляется элемент iListLookup [ipos], который указывает, где в массиве записана новая строка (iIndex). iCount = ListBox_GetCount( hwndList ) ; iPos = ListBox_InsertString( hwndList, iCount, szText ); iCount++; ListBox_SetCurSel( hwndList, iPos ); iListLookup[iPos] = iIndex; if (iCount == MAX_ITEMS) // заполнена ли последняя позиция? { bListFull= TRUE; } return; } Функция CommitMemFilter представляет собой фильтр обработки исключений для функции AddItem. При наличии страничной ошибки функция 20
CommitMemFilter пытается закрепить страницу и, если ее старания приводят к успеху, продолжает выполнение команды lstrcopy. Если функция CommitMemFilter не справляется с задачей, поиск соответствующего обработчика исключения продолжается. LONG CommitMemFilter ( DWORD dwExceptCode, // код, идентифицирующий исключение int iIndex ) // элемент массива, в котором произошла ошибка { LPVOID lpvResult; // Если исключение не является страничной ошибкой, // отказаться обрабатывать его и поручить системе // поиск соответствующего обработчика исключений. if( dwExceptCode != EXCEPTION_ACCESS_VIOLATION ) { return( EXCEPTION_CONTINUE_SEARCH ) ; } // Попытка закрепить страницу. lpvResult = VirtualAlloc( &( pBase(iIndex * ITEM_SIZE] ), // нижняя граница закрепляемой области ITEM_SIZE, // размер закрепляемой области MEM_COMMIT, // новый флаг состояния PAGE_READWRITE ); // уровень защиты if( ! lpvResult ) // произошла ли ошибка выделения памяти? { // Если мы не можем закрепить страницу, то не сможем обработать исключение return( EXCEPTION_CONTINUE_SEARCH ); } // Недостающая страница теперь находится на своем месте. // Система должна вернуться назад и повторить попытку. return( EXCEPTION_CONTINUE_EXECUTION ) ; } 4. Удалить элемент списка. Функция DeleteItem удаляет элемент из виртуального массива, она проверяет, не осталось ли других элементов в этой странице памяти. Если все четыре элемента, которые находятся на странице, пусты, функция отменяет закрепление страницы, передавая дополнительные 4 Кб памяти в распоряжение системы. Виртуальные адреса страницы остаются зарезервированными. Независимо от того, освобождает команда DeleteItem страницу или нет, она удаляет строку из списка и обновляет глобальные переменные состояния. 21
Выполнению этих операций способствуют две дополнительные функции. Функция GetPageBaseEntry получает в качестве аргумента индекс массива и возвращает индекс первого элемента соответствующего страничного блока. Иными словами, эта функция производит округление до ближайшего меньшего значения, кратного четырем. Функция AdjustLookupTable удаляет запись, выделенную в списке, и смещает все последующие элементы для заполнения образовавшейся пустоты. void DeleteItem ( void ) { int iCurSel; // позиция текущей выбранной строки в списке int iPlace; // позиция текущей выбранной строки в массиве элементов int iStart; // позиция первого элемента в той странице памяти, // где находится выбранный элемент int i; // переменная цикла BOOL bFree; // TRUE, если все 4 элемента, содержащиеся в данной // странице памяти, не используются BOOL bTest; // для проверки результатов // определяет смещение в памяти текущей выбранной записи iCurSel = ListBox_GetCurSel( hwndList ) ; iPLace = iListLookup [iCurSel] ; // обнуляет удаленный элемент FillMemory( & (pBase [iPlace * ITEM_SIZE] ) , ITEM_SIZE, 0 ); // помечает этот элемент как свободный bInUse[iPlace] = FALSE; На данном этапе определяется номер первого элемента в текущей странице памяти. Если все четыре элемента, которые находятся на данной странице, свободны, закрепление страницы отменяется. iStart = GetPageBaseEntry( iPlace ) ; bFree = TRUE; for( i = 0; i < 4; i++ ) // проверка четырех записей { if( bInUse[i + iStart] ) // используется? { bFree = FALSE; // страница занята } } Если не используется вся страница памяти, она освобождается. if( bFree ) {
// свободна ли страница? // ДА; освободить ее 22
bTest = VirtualFree( &( pBase[iStart * ITEM_SIZE] ), ITEM_SIZE, MEM_DECOMMIT ) ; if( ! bTest ) { ShowErrorMsg( __LINE__ ); ExitProcess( (UINT)GetLastError() ); } }
Далее обновляеся список и массив связей и проверяется, остались ли еще элементы в списке. ListBox_DeleteString( hwndList, iCurSel ); AdjustLookupTable( iCurSel ); bListEmpty =TRUE; i = 0; while( ( i < MAX_ITEMS ) && ( bListEmpty ) ) { // если элемент используется, значит, список не пустой bListEmpty = !bInUse[i++] ; } // изменить положение маркера выделения в списке if(! bListEmpty ) { if( iCurSel ) // удален ли первый элемент списка? { // нет; выбрать элемент над удаленной // записью ListBox_SetCurSel( hwndList, iCurSel-1 ); } else // удаленная запись была самой верхней // в списке; { // выбрать новую верхнюю запись ListBox_SetCurSel ( hwndList, iCurSel ); } } return; }
Когда программа удаляет все элементы списка, вызывается функция DeleteList, которая освобождает память, прежде занятую записями. 23
void DeleteList() { // отменить закрепление памяти и освободить адресное пространство // Для операции MEM_DECOMMIT необходимо указать размер области if( ! VirtualFree( (void*) pBase, MAX_ITEMS*ITEM_SIZE, МЕМ_DECOMMIT)) ShowErrorMsg ( __LINE__ ) ; // освободить память, начиная с базового адреса; // указывать размер не требуется if( ! VirtualFree( (void*) pBase, 0, MEM_RELEASE ) ) ShowErrorMsg( __LINE__ ); return; } По индексу в списке функция GetPageBaseEntry определяет первый элемент соответствующей страницы памяти, округляя индекс до ближайшего значения, кратного четырем и меньшего или равного iPlace. int GetPageBaseEntry ( int iPlace ) { while( iPlace % 4 ) { iPlace--; } return( iPlace ) ; } После удаления записи из списка необходимо обновить массив, который связывает элементы списка со значениями смещения в памяти. Параметр iStart задает позицию строки, удаленной из списка. void AdjustLookupTable ( int iStart ) { int i; // Этот цикл начинается с позиции, из которой только что // была удалена запись. Все последующие элементы смещаются // таким образом, чтобы заполнить образовавшееся свободное место. for( i = iStart; i < MAX_ITEMS - 1; i++ ) { iListLookup[i] = iListLookup[i + 1]; } iListLookup[MAX_ITEMS - 1] = 0; 24
return; } Остальные функции в программе List служат для вызова диалогового окна; в котором пользователь вводит текст, отображения окна About и вывода сообщений об ошибках. 1. 2. 3. 4.
Содержание отчета: Цель работы; Исходный текст программы; Результаты работы программы (главное окно в ОС Windows); Выводы по проделанной работе с указанием достоинств и недостатков предложенного исходного кода.
ЛАБОРАТОРНАЯ РАБОТА №3 "Реестр ОС Windows" Цель работы: Изучение принципов разработки программы, позволяющей производить запись и считывание данных с реестра ОС Windows (на примере программы Reg_Ops). Задание к лабораторной работе: 1. Перед запуском программы Reg_Ops открыть с помощью проводника Windows, менеджера файлов или любой другой программы просмотра файл Special.REG. Вызванная при этом утилита RegEdit создаст несколько дополнительных записей в разделе HKEY_LOCAL_MACHINE\SOFTWARE\ системного реестра. Содержимое текстового файла Special.REG представлено ниже. REGEDIT4 [HKEY_LOCAL_MACHINE\SOFTWARE\Registry Demo] [HKEY_LOCAL_MACHINE\SOFTWARE\Registry Demo\Charlie Brown] ''Password"="@ABCDGCGJ" "Status"=dword:000003e9 [HKEY_LOCAL_MACHINE\SOFTWARE\Registry Demo\Henry Ford] "Password"="B@@EC" 25
"Status"=dword:000003ea [HKEY_LOCAL_MACHINE\SOFTWARE\Registry Demo\Billy Sol Estes] "Password"="ABDABBCGE" "Status"=dword:000003eb [HKEY_LOCAL_MACHINE\SOFTWARE\Registry Demo\Thomas Alva Edison] "Password"="AA@@F" "Status"=dword:000003ec [HKEY_LOCAL_MACHINE\SOFTWARE\Registry Demo\Blaise Pascal] "Password"="ACBBDEGE" "Status"=dword:000003ed [HKEY_LOCAL_MACHINE\SOFTWARE\Not Very Secret] "Secret Key"="Rumplestiltskin" Программа Reg_Ops использует приведенные записи реестра в качестве исходных данных для демонстрируемых операций. 2. Рассмотреть исходный текст программы Reg_Ops. Программа начинается с вызова метода OnInitDialog, который использует функцию RegCreateKey для открытия дескриптора личного раздела приложения в корневом разделе HKEY_LOCAL_MACHINE. BOOL CReg_OpsDlg::OnInitDialog () { CDialog::OnInitDialog() ; ... // начинается с открытия разделов для записи данных в реестр RegCreateKey ( HKEY_LOCAL_MACHINE, "SOFTWARE \ Registry demo", &m_hRegKey ); ResetButtons() ; // инициализация поля со списком InitializeListBox() ; return TRUE; // возвращается значение TRUE, если фокус не // устанавливается на элементе управления } Поскольку в списке содержатся имена пользователей, флаги состояния и ключи для проверки паролей, программа записывает эту информацию в раздел HKEY_LOCAL_MACHINE, а не в раздел HKEY_USERS. Если нужно зарегистрировать информацию о привилегиях конкретного пользователя, ее следует записать в раздел HKEY_CURRENT_USER. Такая информация будет 26
доступной сразу после входа данного пользователя в систему. Функция RegCreateKey открывает раздел, если он уже существует, или создает его, если раздела еще нет, и возвращает дескриптор в переменную m_hRegKey соответствующего раздела. Поскольку эта переменная является переменнойчленом (тип HKEY), она доступна и для других функций приложения. Если предполагается, что доступ к разделу будет происходить часто и этот раздел будет одновременно использоваться различными приложениями, удобно один раз открыть дескриптор раздела и использовать его в дальнейшем для доступа к подразделам. В других случаях, когда доступ к разделу будет происходить лишь изредка, можно не торопиться и открывать этот раздел только при необходимости. 3. Рассмотреть режим чтения данных из разделов реестра. С помощью переменной m_hRegKey программа инициализирует содержимое списка, читая записи из подразделов реестра (см. файл Reg_OpsDlg.CPP). BOOL CReg_OpsDlg::InitializeListBox() { DWORD dwName, dwSubkeys, dwIndex = 0; long lResult; TCHAR szBuff[MAX_PATH+l] ; CString csBuff; EnableButtons ( FALSE ); lResult = RegQueryInfoKey( m_hRegKey, NULL, NULL, NULL, &dwSubkeys, NULL, NULL, NULL, NULL, NULL, NULL, NULL ) ; if( lResult == ERROR_SUCCESS ) dwIndex = dwSubkeys; else ReportError( lResult ); Прежде чем приступить к чтению содержимого подразделов используется API-функция RegQueryInfoKey, которая определяет, сколько подразделов содержит раздел HKEY_LOCAL_MACHINE\SOFTWARE\Registry Demo. Функция RegQueryInfoKey имеет несколько аргументов, но всем ненужным аргументам можно просто присвоить значение NULL, как это сделано в приведенном выше фрагменте. Такой подход дает возможность запросить только одно значение, не заботясь о параметрах, которые не интересуют. Если для инициализации реестра будут использованы данные, которые содержатся в файле Special.REG, функция RegQueryInfoKey сообщит о наличии пяти подразделов. Теперь, зная, сколько подразделов содержится в выбранном разделе, для переменной dwName следует задать размер, равный размеру массива szBuff. Поскольку величина szBuff не должна изменяться, эта операция будет выполнена только один раз, и значение dwName сохранится неизменным. 27
dwName = sizeof( szBuff ); do { lResult = RegEnumKey( m_hRegKey, --dwIndex, (LPTSTR)szBuff, dwName ); Так как нумерация подразделов начинается с нуля, значение переменной dwIndex перед его использованием следует декрементировать, т.е. необходимо запросить пять подразделов с индексами 4, 3, 2, 1 и 0. Посредством функции RegEnumKey можно осуществить циклический запрос имен подразделов. Каждый раз при вызове функции RegEnumKey со следующим значением номера dwIndex в переменной lpName возвращается имя другой подстроки. Когда список подстрок заканчивается, функция RegEnumKey выдает код ошибки ERROR_NO_MORE_ITEMS, свидетельствующей о том, что подразделов больше нет. Порядок, в котором эти элементы возвращаются, не имеет значения. До тех пор пока не задан индекс, для которого подраздела с данным номером нет, функция будет возвращать значение ERROR_SUCCESS, а в массиве lpName будет храниться имя соответствующего подраздела. При получении любого результирующего значения, кроме ERROR_SUCCESS и ERROR_NO_MORE_ITEMS, должно выводиться сообщение об ошибке. if(lResult != ERROR_SUCCESS && lResult != ERROR_NO_MORE_ITEMS ) ReportError ( lResult ); else if( lResult == ERROR_SUCCESS ) { HKEY hKeyItem; ULONG ulSize, ulType; int nIndex; m_csNameEntry = szBuff; Если был получен результат ERROR_SUCCESS, строку из переменной szBuff необходимо передать в переменную m_csNameEntry, являющуюся членом класса CString. Переменная szBuff сразу же будет использоваться повторно, но содержащееся в ней значение не должно потеряться. Далее снова вызвать функцию RegCreateKey. На этот раз для идентификации открываемого подраздела используется дескриптор корневого раздела m_hRegKey и строковое значение, которое содержится в переменной szBuff. В результате получен дескриптор hKeyItem, идентифицирующий открытый подраздел. 28
RegCreateKey( m_hRegKey, szBuff, ShKeyItem ); Так как заранее известно, что этот раздел существует (об этом свидетельствует результат выполнения функции RegEnumKey), функция RegCreateKey просто открывает раздел и возвращает его дескриптор, который может использоваться в запросах для определения значений параметров, содержащихся в открытом разделе. ulSize = sizeof( m_dwNameStatus ); lResult = RegQueryValueEx ( hKeyltem, "Status", NULL, &ulType, (LPBYTE) &m_dwNameStatus, &ulSize ); Указанная выше функция RegQueryValueEx запрашивает значение, содержащееся в именованном параметре Status. Вместе с этим значением переменная-член ulType возвращает также идентификатор соответствующего типа данных в виде длинного целого без знака. Информация о размере данных будет получена посредством переменной-члена ulSize. Независимо от того, что собой представляют запрашиваемые данные - значение типа DWORD, строковое значение, двоичный массив или данные любого другого типа, адрес получающей их переменной всегда преобразуется в формат указателя на байт. Затем, чтобы убедиться в корректности выполнения функции, произвести проверку значения lResult. В случае некорректного ее завершения на экране появляется сообщение об ошибке. if( lResult != ERROR_SUCCESS ) ReportError( lResult ); К этому моменту уже получен из подраздела код состояния, а также имя самого подраздела. Прежде чем цикл будет продолжен для следующего подраздела, эти значения необходимо задействовать путем размещения их соответственно в качестве строкового элемента списка ComboListbox и индекса данного элемента. nIndex = m_cbNamesList.AddString( m_csNameEntry ); m_cbNamesList.SetItemData( nIndex, m dwNameStatus ); } Затем цикл будет выполнен для всех остальных подразделов. } while( lResult != ERROR_NO_MORE_ITEMS ); return TRUE; }
29
Подпрограмма ConfirmPassword демонстрирует процедуру чтения параметра реестра в виде строкового значения. В этой подпрограмме имя, прочитанное из списка, используется для открытия дескриптора раздела, а затем для чтения пароля. BOOL CReg_OpsDlg::ConfirmPassword() { HKEY hKeyItem; ULONG ulSize, ulType; long lResult; TCHAR szBuff[MAX_PATH+1]; CString csBuff, csPassword; RegCreateKey ( m_hRegKey, m_csNameEntry, &hKeyItem ); ulSize = sizeof( szBuff ); lResult = RegQueryValueEx( hKeyItem, "Password", NULL, &ulType, (LPBYTE) szBuff, &ulSize ); if(lResult != ERROR_SUCCESS ) { ReportError( lResult ) ; return FALSE; } CEnterPassword *pDlg == new CEnterPassword(); if( pDlg->DoModal() == IDOK ) csPassword = theApp.Encrypt( pDlg->m_csPassword ); else return FALSE; return( csPassword == szBuff ); } В эту функцию также включена операция проверки ошибочных результатов. Затем пароль, полученный из диалогового окна класса CEnterPassword, перед проверкой правильности введенных данных передается подпрограмме Encrypt класса CReg_OpsApp. 4. Рассмотреть режим записи данных в разделы реестра. Открыть раздел с помощью функции RegCreateKey. Для этого, сначала вызвать функцию RegOpenKey. Если указанный раздел уже существует, то подготовить его для записи новой информации, в противном случае - создать его. После открытия или создания соответствующего раздела производится вызов функции RegSetValueEx, которая записывает информацию в реестр. Подпрограмма AddToRegistry содержит два примера использования функции RegSetValueEx для записи данных в реестр. Первый раз функция RegSetValueEx применяется для записи строкового значения с указанием типа REG_SZ: 30
BOOL CReg_OpsDlg::AddToRegistry() { HKEY hKeyNew; RegCreateKey( m_hRegKey, m_csNameEntry, ShKeyNew ) ; RegSetValueEx( hKeyNew, "Password", NULL, REG_SZ, (LPBYTE)(LPCTSTR)m_csPassword, m_csPassword.GetLength()+1 ); Поскольку указывается тип REG_SZ, к аргументу, задающему размер значения, необходимо добавить единицу - в таком случае будет включен завершающий нулевой символ, который не учитывается в значении, возвращаемом функцией GetLength. Ранее указывалось, что функции для работы с реестром не распознают классов библиотеки MFC, в том числе и класс CString, однако в представленном примере в качестве аргументов использованы две ссылки на этот класс. Причем в первом случае значение приведено к типу LPCSTR, определяющему его как массив символов, во втором же случае передается целочисленное значение, возвращаемое функцией-членом. Во второй операции с функцией RegSetValueEx необходимо записать значение типа DWORD: RegSetValueEx( hKeyNew, "Status", NULL, REG_DWORD, (LPBYTE)&m_dwNameStatus, sizeof( m_dwNameStatus) ); return TRUE; } Записываемое значение передается с помощью адреса, после которого указывается размер данных (в байтах). В обеих функциях RegSetValueEx использован аргумент типа LPBYTE (указатель на байт) и что данные всегда должны передаваться по адресу. 5. Рассмотреть подпрограмму системных сообщений об ошибках. В данном случае для интерпретации сообщений об ошибках, возвращаемых функциями RegQueryInfoKey, RegEnumKey и RegQueryValueEx, применяется функция ReportError. При вызове функции ReportError ей в качестве аргумента передается код ошибки, возвращаемый любой другой функцией, которая осуществляет обращение к реестру. В функции ReportError вызывается функция FormatMessage с флагом FORMAT_MESSAGE_FROM_SYSTEM, которая возвращает строку с разъяснением соответствующей системной ошибки. Флаг FORMAT_MESSAGE_ALLOCATE_BUFFER позволяет использовать допустимый указатель буфера, реально не выделяя его. Функция FormatMessage сама управляет выделением памяти, руководствуясь размером сообщения. При использовании флага FORMAT_MESSAGE_ALLOCATE_BUFFER после завершения работы необходимо вызвать функцию LocalFree и освободить память, выделенную для размещения буфера. 31
void CReg_OpsDlg::ReportError( long lError ) { #ifdef _DEBUG LPVOID lpMsgBuf; FormatMessage ( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, lError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // язык, заданный по умолчанию (LPTSTR) SipMsgBuf, 0, NULL ) ; MessageBox( (char *)lpMsgBuf ); LocalFree( lpMsgBuf ) ; #endif } Если вместо аргумента lError указать функцию GetLastError, то представленную выше подпрограмму можно будет использовать для интерпретации других ошибок, для которых код ошибки не был явно возвращен. Функцию ReportError не рекомендуется включать в окончательную версию приложения. Возвращаемые ею сообщения об ошибках не несут полезной для пользователей информации, так как предназначены для программистов - помогают им при отладке программы. Поэтому в подпрограмме содержится конструкция #ifdef _DEBUG / #endif. 6. Запустить программу Reg_Ops. Диалоговое окно программы, содержащее поле со списком и набор кнопок состояния, представлено на рисунке 3.1. Программа позволяет выбрать одно из имен, содержащихся в списке (при этом будет запрошен пароль), либо ввести новое имя в поле, назначить статус и задать пароль для данной записи. Введенное имя будет присвоено новому подразделу реестра, а код состояния и пароль станут именованными параметрами этого подраздела.
Рис.3.1
Окно программы Reg_Ops, демонстрирующей выполнения операций с реестром
принципы
Разобраться, какие действия выполняются каждой из кнопок состояния (за исключением кнопки Computer Dweeb). Использование кнопки System Guru (системный гуру) подразумевает, что пользователь обладает 32
определенными знаниями о реестре (не слишком глубокими) - только при таком условии будет присвоен соответствующий статус. Подпрограмма шифрования пароля принимает строку и вырабатывает код проверки, который может использоваться впоследствии для сравнения с новым введенным значением без сохранения пароля. Такой механизм довольно прост и не обеспечивает стопроцентной надежности, однако для целей программы вполне пригоден. Рассмотреть реализацию этого механизма. Ниже представлен список имен, содержащихся в файле Special.REG, и для каждого из них указан пароль. Charlie Brown Henry Ford Billy Sol Estes Thomas Alva Edison Blaise Pascal 1. 2. 3. 4.
Countess Menlo fefifofum edsel anhydrous
Содержание отчета: Цель работы; Исходный текст программы; Результаты работы программы (диалоговое окно в ОС Windows); Выводы по проделанной работе с указанием достоинств и недостатков предложенного исходного кода.
ЛАБОРАТОРНАЯ РАБОТА №4 "Буфер обмена ОС Windows" Цель работы: Изучение принципов работы с буфером обмена. (На примере программы ClipBoard). Задание к лабораторной работе: 1. Запустить программу ClipBoard. Данная программа демонстрирует запись в буфер обмена и чтение из него данных трех различных типов: текста, растрового изображения и метафайла. Меню программы содержит два основных подменю, Data To Clipboard и Data from Clipboard, каждое из которых предлагает весьма "прозаический" набор команд, а именно Write и Retrieve. Команда Write Bitmap осуществляет простейшую операцию захвата экрана (по крайней мере, фрагмента экрана размером 640х480) в виде 33
растрового изображения и записи этого изображения в буфер обмена. Команда Write Text копирует в буфер обмена простую строку текста. 2. Рассмотреть механизм чтения и записи текста. Текстовые операции являются простейшими операциями, которые выполняются с буфером обмена. Запись текста. Поскольку программа ClipBoard является одновременно и источником, и приемником данных, первый шаг при работе с ней заключается в передаче текстовой информации в буфер обмена. В качестве записываемого текста выбрана фиксированная строка "The quick brown fox jumps over the lazy red dog". Механизм обработки текста реализован в виде функции, которая вызывается с двумя параметрами: дескриптором приложения hwnd и указателем строки текста lpText. BOOL TextToClipboard( HWND hwnd, LPSTR lpText ) { int i, wLen; GLOBALHANDLE hGMem; LPSTR lpGMem; В функции TextToClipboard используются четыре локальных переменных, но пояснений требуют только две последние из них. Переменная hGMem содержит глобальный дескриптор блока памяти, который пока не был выделен. Вторая переменная, lpGMem, служит указателем блока памяти. После инициализации переменной wLen и присвоения ей значения длины текстовой строки дескриптор hGMem становится идентификатором глобального блока памяти, выделенного для хранения копии текста. Однако обратите внимание, что значение переменной wLen на единицу больше длины строки, поскольку в строку входит еще и завершающий пустой символ. В буфер обмена текст всегда записывается в формате ASCIIZ (или ANSIZ): wLen = strlen( lpText ); hGMem = GlobalAlloc( GHND, (DWORD) wLen + 1 ); lpGMem = GlobalLock( hGMem ); Наконец, переменной lpGMem присваивается указатель блока памяти, полученный в результате выполнения функции GlobalLock. Но не забывайте, что спецификация GHND объявляет этот блок как перемещаемый. Функция GlobalLock временно предотвращает перемещение блока памяти операционной системой. Вторая особенность спецификации GHND заключается в том, что выделенный блок памяти освобождается от своего текущего содержимого и заполняется нулями. Таким образом, следующая операция - это копирование в выделенный блок памяти локальной строки, заданной указателем lpText:
34
for( i=0; i<wLen; i++ ) *lpGMem = *lpText++; GlobalUnlock( hGMem ); return( TransferToClipboard( hwnd, hGMem, CF_TEXT ) ); } После копирования текста вызывается функция GlobalUnlock, которая отменяет блокировку дескриптора hGMem, разрешая перемещение и повторное выделение блока памяти. Если бы блок памяти был перемещен до копирования текста, указатель lpGMem стал бы недействительным. В завершение процесса передачи данных вызывается рассмотренная выше функция TransferToClipboard с указанием дескриптора блока hGMem, флага CF_TEXT и дескриптора окна приложения. Чтение текста. Чтение текста из буфера обмена осуществляется столь же просто, как и запись, однако отдельная процедура при этом не вызывается, а действия выполняются в ответ на сообщение WM_PAINT. Это позволяет приложению при необходимости обновлять окно. Операции чтения начинаются с открытия буфера обмена и вызова APIфункции GetClipboardData, которая возвращает дескриптор блока памяти, принадлежащего буферу: OpenClipboard( hwnd); hTextMem = GetClipboardData( CF_TEXT ); lpText = GlobalLock( hTextMem ); Как и при записи данных в буфер обмена, функция GlobalLock блокирует область памяти и ее адрес сохраняется в переменной lpText. Но на этот раз содержимое строки копируется из области памяти (переменная lpText) в локальную переменную TextStr не в цикле, а напрямую, с помощью функции lstrcpy: lstrcpy( TextStr, lpText ); GlobalUnlock( hTextMem ); CloseClipboard(); Наконец, функция GlobalUnlock снимает блокировку области памяти, а функция CloseClipboard завершает сеанс работы с буфером обмена. Важно помнить, что область памяти никогда не должна оставаться заблокированной и вызов функции GlobalLock всегда должен сопровождаться вызовом GlobalUnlock.
35
3. Рассмотреть механизм чтения и записи растровых изображений. В программе ClipBoard содержится пример передачи посредством буфера обмена растрового изображения. Эта часть программы начинается с захвата текущего содержимого экрана, которое передается в буфер обмена в виде растрового изображения. На рис. 4.1 показаны окно программы ClipBoard после выполнения операции захвата экрана.
Рис. 4.1. Захват растрового изображения Запись растрового изображения. Процесс передачи изображения в буфер обмена начинается с создания в памяти совместимого контекста устройства hdcMem. Затем происходит создание и выбор (также в памяти) совместимого растрового изображения. hdc = GetDC( hwnd ) ; hdcMem = CreateCompatibleDC( hdc ); hBitmap = CreateCompatibleBitmap( hdc, 640, 480 ); SelectObject( hdcMem, hBitmap ) ; Далее происходит копирование изображения из источника (в нашем случае - с экрана) в контекст памяти. Затем вызывается функция TransferToClipboard, которая, собственно, и осуществляет запись в буфер. StretchBlt( hdcMem, 0, 0, 639, 479, 36
hdc, 0, 0, 639, 479, SRCCOPY ); TransferToClipboard( hwnd, hBitmap, CE_BITMAP ); DeleteDC( hdcMem ); Наконец, контекст устройства удаляется из памяти, а право на владение растровым изображением передается буферу обмена. Чтение растрового изображения. Аналогичным образом происходит и чтение растрового изображения из буфера обмена. Процесс начинается с открытия буфера и чтения из него дескриптора растрового изображения: OpenClipboard( hwnd ); hBitmap = GetClipboardData ( CF_BITMAP ) ; hdcMem = CreateCompatibleDC ( hdc ); SelectObject( hdcMem, hBitmap ); Опять-таки, необходим совместимый контекст устройства. Затем функция SelectObject загружает в него изображение. Параллельно с описанными выполняются еще несколько задач. Вопервых, в контексте памяти необходимо задать режим отображения, совместимый с контекстом дисплея. Во-вторых, перед тем как изображение будет скопировано, нужно определить его размер. Размер изображения можно получить путем копирования его заголовка в локальную переменную bm: SetMapMode(hdcMem,.GetMapMode( hdc ) ); GetObject( hBitmap, sizeof(BITMAP), (LPSTR) &bm ); Далее изображение с помощью функции BitBlt копируется из контекста памяти в контекст устройства: BitBlt( hdc, 0, 0, bm.bmWtdth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY ); ReleaseDC( hwnd, hdc ); DeleteDC( hdcMem ); CloseClipboard(); Осталось только очистить буфер обмена перед его закрытием, и задача будет выполнена. 4. Рассмотреть механизм чтения и записи метафайлов. Запись метафайла. В текстовом и декартовых режимах отображения масштаб является фиксированным. Изотропный и анизотропный режимы требуют наличия специальной информации, сопровождающей команды метафайла. 37
Для передачи метафайла посредством буфера обмена применяется структура METAFILEPICT, в которой записываются режим отображения, информация о размерах, а также сам сценарий метафайла. Структура METAFILEPICT определена в файле WinGDI.H: typedef struct tagMETAFILEPICT { LONG mm; LONG xExt; LONG yExt; HMETAFILE hMF; } METAFILEPICT, FAR *LPMETAFILEPICT; Поле mm содержит идентификатор режима отображения. Поле hMF представляет собой дескриптор набора команд метафайла. Остальные два поля, xExt и yExt, могут содержать два разных типа информации в зависимости от режима отображения. Для текстового и декартовых режимов в полях xExt и yExt задаются размеры содержащегося в метафайле изображения по горизонтали и по вертикали, в единицах, которые соответствуют режиму отображения. При использовании изотропного и анизотропного режимов отображения поля xExt и yExt содержат необязательные, рекомендуемые размеры, выраженные в единицах MM_HIMETRIC. Если рекомендуемые размеры не указываются, эти поля могут содержать нулевые значения. Если же поля xExt и yExt имеют отрицательные значения, значит, они представляют соотношения размеров, а не абсолютные размеры. Функция DrawMetafile создает метафайл в памяти и после создания метафайла следующий шаг заключается в формировании структуры METAFILEPICT в памяти и получении с помощью функции GlobalLock ее указателя lpMFP: hGMem = GlobalAlloc( GHND, (DWORD) sizeof( METAFILEPICT ) ) ; lpMFP = (LPMETAFILEPICT) GlobalLock( hGMem ) ; Теперь можно задать соответствующий режим отображения, размеры изображения и назначить дескриптор метафайла. lpMFP->mm = MM_ISOTROPIC; lpMFP->xExt = 200; // рекомендуемые размеры // lpMFP->yExt = 200; // в единицах MM_HIMETRIC // lpMFP->hMF = hMetaFile;
38
Далее остается только разблокировать память и передать дескриптор метафайла в буфер обмена. GlobalUnlock( hGMem ) ; TransferToClipboard( hwnd, hGMem, CF_METAFILEPICT ); Чтение метафайла. Чтение метафайла начинается с открытия буфера обмена, запроса дескриптора блока памяти, в котором содержится метафайл, и последующей блокировки этой области. OpenClipboard( hwnd ); hGMem = GetClipboardData( CF_METAFILEPICT ); lpMFP = (LPMETAFILEPICT) GlobalLock( hGMem ); Теперь переменная lpMFP содержит указатель блока памяти, в котором находится метафайл, или, точнее, структуры METAFILEPICT, которая, в свою очередь, хранит указатель собственно метафайла. Но прежде чем метафайл будет воспроизведен, необходимо решить ряд дополнительных задач, начав с сохранения текущего контекста устройства. SaveDC( hdc ); CreateMapMode( hdc, lpMFP, cxWnd, cyWnd ) ; Далее информация, которая содержится в структуре METAFILEPICT, передается на обработку функции CreateMapMode. Это делается потому, что расшифровка информации о режиме отображения и о размерах - задача довольно сложная. Функция CreateMapMode вызывается с четырьмя параметрами, которые задают дескриптор контекста устройства для приложения, указатель структуры METAFILEPICT и размеры окна приложения. BOOL CreateMapMode( HDC hdc, LPMETAFILEPICT lpMFP, int cxWnd, int cyWnd ) { long lMapScale; int nHRes, nVRes, nHSize, nVSize; SetMapModet hdc, lpMFP->mm ); if( lpMFP->mm!=MM_ISOTROPIC&&lpMFP->mm!= MM_ANISOTROPIC ) return( TRUE ) ; Сначала функция CreateMapMode устанавливает режим отображения, заданный для метафайла. Если задано какое-либо другое значение, не 39
MM_ISOTROPIC или MM_ANISOTROPIC, функция просто завершает свою работу, не выполняя никаких действий. Если режим отображения является изотропным или анизотропным, функция CreateMapMode должна осуществить дополнительные операции. Вопервых, вызывается функция GetDeviceCaps, которая возвращает горизонтальный и вертикальный размеры экрана, а также разрешение. nHRes = GetDeviceCaps( hdc, HORZRES ) ; nVRes = GetDeviceCaps( hdc, VERTRES ) ; nHSize = GetDeviceCaps( hdc, HORZSIZE ); nVSize = GetDeviceCaps( hdc, VERTSIZE ) ; Последующие действия зависят от значений полей xExt и yExt, которые могут быть положительными, отрицательными или нулевыми. Если размеры полей заданы посредством положительных значений, они воспринимаются как рекомендуемые размеры изображения в единицах MM_HIMETRIC. В этом случае вызывается функция SetViewportExtEx, которая соответствующим образом задает размеры области просмотра. if( lpMFP->xExt > 0 ) SetViewportExtEx( hdc,(int)((long) lpMFP->xExt * nHRes / nHSize / 100 ), (int)((long) lpMFP->yExt * nHRes / nHSize / 100 ), NULL ); Если указаны отрицательные значения, содержимое полей интерпретируется как масштабный коэффициент. Поэтому сначала производится расчет масштаба по отношению к контексту устройства. else if( lpMFP->xExt < 0 ) { lMapScale = min( ( 100L * (long) cxWnd * nHSize / nHRes / -lpMFP->xExt ), (100L * (long) cyWnd * nVSize / nVRes /-lpMFP->yExt ) ); Вычисляются два масштаба: по оси Х и по оси Y. Однако в качестве значения lMapScale берется меньшая из двух величин, чтобы результирующее изображение точно помещалось на экране. Теперь функция SetViewportExtEx вызывается еще раз - для согласования размеров изображения и области просмотра. SetViewportExtEx( hdc,(int)((long) -lpMFP->xExt * lMapScale * nHRes / nHSize / 100 ),(int)((long) -lpMFP->yExt * lMapScale * nVRes / nVSize / 100 ), NULL ); } 40
Кроме того, размеры (или масштабные коэффициенты) могут быть не заданы. В этом случае размеры области просмотра будут просто совпадать с размерами окна. else SetViewportExtEx( hdc, cxWnd, cyWnd, NULL ) ; Когда результат функции CreateMapMode известен, оставшаяся часть задачи выполняется очень просто. Нужно только вызвать функцию PlayMetaFile, точно таким же образом, как было показано ранее. PlayMetaFile( hdc, lpMFP->hMF ); RestoreDC( hdc, -1 ) ; GlobalUnlock( hGMem ) ; CloseClipboard() ; После воспроизведения метафайла вызывается функция RestoreDC с аргументом -1 (чтобы восстановить исходный контекст устройства), разблокируется память метафайла и закрывается буфер обмена. 1. 2. 3. 4.
Содержание отчета: Цель работы; Исходный текст программы; Результаты работы программы (диалоговое окно в ОС Windows); Выводы по проделанной работе с указанием достоинств и недостатков предложенного исходного кода.
ЛАБОРАТОРНАЯ РАБОТА №5 "Безопасность Windows " Цель работы: Изучение принципов обеспечения безопасности приложений в ОС Windows NT (На примере программы File_User). Задание к лабораторной работе: 1. Запустить программу File_User. Данная программа, работающая только в среде Windows NT, демонстрирует, каким образом можно получить текущий дескриптор безопасности файла и обновить его путем добавления записи (АСЕ) AccessAllowed (доступ разрешен) или AccessDenied (доступ запрещен) для заданного пользователя. Для упрощения примера создано консольное 41
приложение Win32, работающее в режиме командной строки. Программа ожидает, когда в командной строке будет задано имя файла, имя пользователя и параметр + (доступ разрешен) или - (доступ запрещен). 2. Рассмотреть исходный код программы. Предложенный код позволяет понять, как осуществляется установка или изменение атрибутов безопасности на уровне объектов в операционной системе Windows NT. Поэкспериментировать с программой, изменив ее таким образом чтобы запретить доступ к данному объекту со стороны всех пользователей или, наоборот, чтобы разрешить всеобщий доступ к нему. Изменить владельца файла. Настроить программу для работы с другими объектами, для этого модифицировать DACL. Содержание отчета: 1. Цель работы; 2. Исходный текст модифицированной программы (согласно задания); 3. Выводы по проделанной работе с указанием достоинств и недостатков предложенного исходного кода.
ЛАБОРАТОРНАЯ РАБОТА №6 "Обработка исключений" Цель работы: Изучение принципов обработки исключений в программе. (На примере программы Exceptions). Задание к лабораторной работе: 1. Запустить программу Exceptions. Данная программа содержит ряд примеров обработки исключений различного типа. Пример, который необходимо запустить, можно выбрать из меню программы. Simple Handler - Демонстрирует простейшую структуру блоков try/catch. Nested Handlers - Демонстрирует вложенную структуру блоков try/catch. Failed Catch - Отключает возможность перехвата ошибки доступа к памяти; может потребовать последующей перезагрузки компьютера. Resource Exception - Ошибка, обусловленная загрузкой несуществующего устройства. User Exception - Генерирует пользовательское исключение.
42
Для удобства в каждом примере в окне приложения отображается последовательность текстовых сообщений, сопровождающих процесс обработки исключения. 2. Рассмотреть механизм простейшего обработчика исключений. Блок try/catch перехватывает исключение при вызове процедуры ForceException, которая порождает исключение доступа к памяти. На рис. 6.1 показано окно программы, сообщения в котором позволяют отслеживать ход выполнения процедуры.
Рис. 6.1. Окно, предназначенное для наблюдения за работой простейшего обработчика исключений В примере исключения перехватываются внутри блока catch (...) и не приводят к системной ошибке. void CExceptionsView::OnExceptionsSimplehandler() { CDC *pDC = GetDC(); CSize cSize = pDC->GetTextExtent( "A", 1 ); // получить вертикальный размер текста int vSize = cSize.cy, iLine = 0; CString csText; Каждый из обработчиков начинается с команды получения контекста устройства, а затем вызывает функцию GetTextExtent, предназначенную для определения размера строки текста по вертикали. Это значение используется для разделения строк при отображении сообщений. Кроме того, вызываются 43
функции Invalidate и OnPaint, посредством которых удаляется текст, оставшийся от предыдущих примеров. Invalidated; OnPaint (); // удаляют весь предшествующий текст csText = "Starting process"; pDC->TextOut( 10, vSize *iLine++, csText ); try { csText = "In try block"; pDC->TextOut( 50, v3ize*iLine++, csText ); csText = "About to cause exception"; pDC->TextOut( 50, vSize*iLine++, csText ); ForceException (); // показана ранее Несмотря на то, что исключение происходит в отдельной процедуре, выполнение блока try приостанавливается и управление передается блоку catch (...). Поэтому следующая строка текста не будет отображаться в окне программы (см. рис. 3.1). Выполнение программы возобновится в блоке catch, где имеется другое сообщение. csText = "This line will not display because execution has passed " "to the catch block"; pDC->TextOut( 50, vSize*iLine++, csText ); } catch (...) // перехватывать все исключения!!! { csText = "In catch all block"; pDC->TextOut( 50, vSize*iLine++, csText ); } csText = "End of process"; pDC->TextOut( 10, vSize*iLine++, csText ); } 3. Рассмотреть механизм обработчика вложенного исключения. Второй пример демонстрирует использование вложенных исключений путем размещения одного блока try/catch внутри другого, внешнего блока. Кроме того, внутренний блок содержит набор команд catch, которые оперируют различными классами, производными от CException. Наконец, оператор throw во внутреннем блоке catch передает перехваченное исключение внешнему блоку catch. void CExceptionsView::OnExceptibnsNestedhandlers() { 44
CDC *pDC = GetDC() ; CSize cSize = pDC->GetTextExtent( "A", 1 ); int vSize = cSize.cy, iLine = 0; CString csText; Invalidate(); OnPaint(); // удаление всего предшествующего текста csText = "Starting process"; pDC->TextOut( 50, vSize*iLine++, csText ); try { csText = "In outer try block"; pDC->TextOut ( 50, vSize*iLine++, csText ); try { csText = "In inner try block"; pDC->TextOut( 90, vSize*iLine+.+, csText ); csText = "About to cause exception ..."; pDC->TextOut( 90, vSize*iLine++, csText ); ForceException (); В этот момент команда ForceException готова породить исключение и передать его внутреннему блоку catch, предотвращая выполнение следующих операторов. csText = "This line will not display because execution " "has passed to the catch block"; pDC->TextOut( 50, vSize*iLine++, csText ); } Далее следует набор блоков catch, каждый из которых предназначен для перехвата исключения, тип которого определяется классом, производным от CException. Но тип исключения, задействованного в данном примере (ошибка доступа к памяти), не относится ни к одному из этих классов. Поэтому реальный перехват не произойдет до тех пор, пока не будет найден подходящий обработчик. Все операторы catch имеют практически одно и то же тело (различаются лишь выводимой на экран строкой), поэтому достаточно рассмотреть одну структуру: catch( CMemoryException *e ) { TCHAR szCause[255]; 45
// нехватка памяти
csText = "CMemoryException cause: "; e->GetErrorMessage( szCause, 255 ); csText += szCause; pDC->TextOut( 90, vSize*iLine++, csText ); } catch ( CFileException *e ) { ... } catch( CArchiveException *e )
// ошибка при работе с. файлом
// ошибка, допущенная при // операции архивации/сериализации
{ ... } catch( CNotSupportedException *e ) // запрос сервиса, { // который не поддерживается ... } catch( CResourceException *e ) // ошибка, допущенная при // выделении ресурса { ... } Поскольку в рассматриваемой программе поддержка баз данных не реализована, классы CDaoException и CDBException не распознаются и закомментированы в исходном тексте. /*
// поддержка баз данных не реализована, // поэтому исключения CDaoException и // CDBEkception в этом примере не распознаются catch ( CDaoException *e ) //исключения, связанные //с базами данных (DAO-классы) { ... } // исключения, связанные catch( CDBException *e ) // с базами данных (ODBC-классы) { ... } 46
*/ catch ( COleException *e ) // OLE-исключения { ... } catch(COleDispatchException *e ) // исключения диспетчеризации // (OLE-автоматизации) { ... } catch( CUserException *e ) // выводит окно сообщения, { //а затем генерирует исключение CException ... } Вслед за этим следует блок catch (CException *e). В противном случае компилятор воспринял бы предыдущие блоки как ошибочные. catch ( CException *e ) // должен следовать после предыдущих блоков, // иначе компилятор выдаст код ошибки { TCHAR szCause[255] ; csText = "CException cause: "; e->GetErrorMessage( szCause, 255 ); csText += szCause; pDC->TextOut( 90, vSize*iLine++, csText ); } И поскольку нам известно, что ни один из классов CException на самом деле не может обработать данное исключение, в программу включен блок catch (...), который перехватывает все исключения, но выдает не очень полную информацию о том, что произошло. // перехват всех исключений!!! catch(...) { csText = "In inner catch all block: "; pDC->TextOut( 90, vSize*iLine++, csText ); csText = "Throwing exception to outer catch block: "; pDC->TextOut( 90, vSize*iLine++, csText ); throw; // передает исключение внешнему блоку catch, // приведенному ниже } Исключение, произошедшее во внутреннем блоке try, перехватывается внутренним блоком catch и передается внешнему блоку catch. Оператор throw предотвращает выполнение фрагмента программы, который следует после 47
блоков try/catch. Аналогичная обработка происходит автоматически во внутреннем блоке try при генерации исходного исключения. Единственное различие между ними заключается в том, что при входе программы во внутренний блок catch внешний блок try исполняться не будет. Это дает дополнительную возможность проконтролировать ход выполнения приложения и позволяет при необходимости предотвратить выполнение последующих операторов программы. csText = "This line will not display because execution has been " "thrown to the outer catch block"; pDC->TextOut( 50, vSize*iLine++, csText ); } // перехват всех сообщений!!! catch(...) { csText = "In outer catch all block, catching thrown exception"; pDC->TextOut( 50, vSize*iLine++, csText ); } csText = "End of process"; pDC->TextOut (10, vSize*iLine++, csText ); } На рис. 6.2 представлен отчет, который генерируется во время выполнения модуля Nested Handlers.
Рис. 6.2. Отчет, генерируемый в процессе выполнения примера Nested Handlers 4. Рассмотреть пример неудачной обработки исключения. Модуль Failed Catch демонстрирует, что происходит в том случае, когда блоки try/catch пытаются защитить выполнение определенного фрагмента программы, однако не могут правильно перехватить исключение (программа наталкивается на 48
исключение, которое не перехватывается этими блоками). Код такого примера практически идентичен приведенному выше. void CExceptionsView::OnExceptionsFailedcatch() { CDC *pDC = GetDC() ; CSize cSize = pDC->GetTextExtent( "A", 1 ); int vSize = cSize.cy, iLine =0, *p = 0x00000000; CString csText; Invalidate () ; OnPaint(); // удаление всего предшествующего текста csText = "Starting process"; pDC->TextOut( 10, vSize*iLine++, csText ); try { csText = "In try block"; pDC->TextOut( 50, vSize*iLine++, csText ); csText = "About to cause exception ..."; pDC->TextOut( 90, vSize*iLine++, csText ); Поскольку эта процедура точно будет выполнена некорректно, то кроме вывода текста она также отображает модальное диалоговое окно сообщения, содержащее соответствующее предупреждение (рис. 6.3).
Рис. 6.3. Окно-предупреждение, генерируемое модулем Failed Catch Ниже представлен исходный код, позволяющий выводить данное предупреждение. MessageBox( "WARNING!\r\nThis exception may require a reboot\r\n" "for full recovery!" ); *p = 999; } // класс CException не может перехватить catch( CException *e ) //код ошибки доступа к памяти { TCHAR szCause[255]; 49
csText = "In catch block"; pDC->TextOut( 50, vSize*iLine++, csText ); csText = _T("CException cause: "); e->GetErrorMessage( szCause, 255 ); csText += szCause; } Следует отметить, что выполнение модуля Failed Catch может привести к возникновению ошибки в системной памяти, что потребует перезагрузки системы. Но в любом случае будет получено предупреждающее сообщение. Фрагмент, содержащий обработчик catch (...), закомментирован, поскольку это единственная подпрограмма обработки, способная перехватывать ошибки доступа к памяти, а целью данного примера является демонстрация событий, которые произойдут в том случае, если соответствующее исключение перехвачено не будет. Если необходимо предотвратить ошибку следует убрать знаки комментария в блоке catch(...). /*
// если сделать блок catch(...) доступным, // ошибка будет перехвачена и не попадет в систему catch (...) // перехватывает все // исключения, в том числе // ошибки доступа к памяти! { csText = "In catch all block"; pDC->TextOut( 50, vSize*iLine++, csText ); }
*/ csText = "End of process"; pDC->TextOut( 10, vSize*iLine++, csText ); } 5. Рассмотреть механизм исключения "Загрузка несуществующего ресурса". Не все исключения генерируются системой автоматически. Во многих случаях исключение пользователю необходимо породить самостоятельно. Такая потребность возникает, когда обнаруживается неправильный результат выполнения функции или нужно перенаправить поток выполнения в обход последующих операторов. Эту задачу проще всего решить путем генерации исключения. Блок catch перехватит его, выдаст сообщение или осуществит иного рода обработку. Так, если в процессе перевода большой строковой таблицы, предназначенной для применения в многоязыковой версии приложения, потеряется одна или несколько строк (а такое вполне возможно), исходный код программы все равно будет скомпилирован, поскольку идентификатор ресурса сохранится. Однако в программе будут содержаться операторы, которые 50
выводят несуществующие записи строковой таблицы. Эта ошибка сама по себе не порождает исключения. Просто в случае отсутствия искомой записи оператор LoadString возвращает строку нулевой длины. Программа Resource Exception генерирует исключение при наличии этой ошибки. Следующий пример начинается с внутреннего и внешнего блоков try и демонстрирует применение оператора throw. Во внутреннем блоке try делается попытка загрузить строковый ресурс. void CExceptionsView::OnExceptionsResourceexception() { CDC *pDC = GetDC() ; CSize cSize = pDC->GetTextExtent( "A", 1 ); int vSize = cSize.cy, iLine = 0; CString csText, csMsg; Invalidate(); OnPaint(); // удаляют весь предшествующий текст csText = "Starting process"; pDC->TextOut( 10, vSize*iLine++, csText ); try { csText = "In outer try block"; pDC->TextOut( 50, vSize*iLine++, csText ); try { csText = "In inner try block"; pDC->TextOut( 90; vSize*iLine++, csText ); csText = "About to cause exception ..."; pDC->TextOut( 90, vSize*iLine++, csText ); if( ! csText.LoadString ( IDS_NON_STRING ) ) AfxThrowResourceException() ; pDC->TextOut( 90, vSize*iLine++, csText ); } При успешном выполнении функции LoadString программа отобразила бы загруженную строку. Но в строковой таблице нет соответствующей записи, хотя файл Resource.H содержит определение идентификатора этого ресурса. Поэтому вызывается функция AfxThrowResourceException, порождающая исключение, которое приводит к выполнению следующих блоков catch. catch( CResourceException *e ) // ошибка выделения ресурса { TCHAR szCause[255]; csText = "CResourceException cause: "; e->GetErrorMessage( szCause, 255 ); 51
csText += szCause; pDC->TextOut( 90, vSize*iLine++, csText ); } В этом блоке исключение перехватывается классом CResourceException, который передает сообщение о недоступности запрашиваемого ресурса "A required resource was unavailable" (рис. 6.4). catch( CException *e )// данный блок должен следовать после // предыдущих блоков, // иначе компилятор выдаст код ошибки { TCHAR szCause[255]; csText = "CException cause: "; e->GetErrorMessage( szCause, 255 ); csText += szCause; pDC->TextOut( 90, vSize*iLine++, csText ); } // перехват всех исключений!!!. catch(...) { csText = "In inner catch all block: "; pDC->TextOut( 90, vSize*iLine++, csText ); csText = "Throwing exception to outer catch block: "; pDC->TextOut( 90, vSize*iLine++, csText ); throw; // передает исключение внешнему блоку catch }
Рис. 6.4. Окно, содержащее сообщения, которые возникают в процессе порождения исключения, связанного с ресурсом Следует обратить внимание на тот факт, что блок catch(. ..) содержит оператор throw, а обработчик CResourceException - нет. Поскольку исключение 52
перехватывается (и обрабатывается) обработчиком CResourceException, оператор throw не выполняется. Поэтому внешний блок try продолжает выполнение и отображает следующее сообщение. csText = "Still in the outer try block. This line displays because"; pDC->TextOut( 50, vSize*iLine++, csText ); csText = "execution was not thrown to the outer catch block"; pDC->TextOut( 50, vSize*iLine++, csText ); } catch (...) // перехватывает все исключения!!! { csText = "In outer catch all block, catching thrown exception"; pDC->TextOut( 50, vSize*iLine++, csText ); } csText = "End of process"; pDC->TextOut( 10, vSize*iLine++, csText ); } Приложение может по-разному реагировать на исключения путем использования различных обработчиков. Кроме того, каждый обработчик может выполнять различные операции в зависимости от условия возникновения исключения. 6. Рассмотреть пример обработки пользовательского исключения. Пользовательские исключения всегда должны генерироваться явным образом. Система не имеет возможности распознавать пользовательские ошибки, поскольку они обычно происходят в контексте приложения. Таким образом, пользовательские исключения всегда следует определять самостоятельно. Исключения должны происходить не обязательно в той же процедуре, где расположены блоки try/catch. Рассмотреть следующий пример: BOOL CExceptionsView::UserException() // предположим, что произошла ошибка { AfxMessageBox( "Drat! The XDR Veng operation failed!" ); AfxThrowUSerException() ; return TRUE; } В данном фрагменте кода функция UserException отображает окно сообщения (рис. 6.5), в котором приведена информация об ошибке, а затем функция AfxThrowUserException генерирует исключение. Функция UserException вызывается из следующей подпрограммы, содержащей блоки try/catch: void CExceptionsView::OnExceptionsUserexception() { CDC *pDC = GetDC(); 53
CSize cSize = pDC->GetTextExtent( "A", 1 ); int vSize = cSize.cy, iLine = 0; CString csText, csMsg; Invalidate(); OnPaint(); // удаление всего предшествующего текста csText = "Starting process"; pDC->TextOut( 10, vSize*iLine++, csText ); try { csText = "In try block, about to call UserException()"; pDC->TextOut( 50, vSize*iLine++, csText ); if ( UserException() ) { csText = "In try block, the XDR Veng operation succeeded (impossible) "; pDC->TextOut( 50, vSize*iLine++, csText ); } csText = "Continuing try block"; pDC->TextOut( 50, vSize*iLine++, csText ); }
Рис. 6.5. Окно, содержащее сообщения, которые возникают в процессе генерации пользовательского исключения Поскольку предполагается, что функция UserException должна породить исключение, сообщение об успешном выполнении операции никогда не появится. Если за этим процессом проследить пошагово, то станет ясно, что функция UserException не возвращает результирующего значения, поскольку выполнение передается непосредственно обработчикам catch. catch( CUserException *e ) 54
{ TCHAR szCause[255]; csText = "In CUserException catch block"; pDC->TextOut( 50, vSize*iLine++, csText ); csText = "CUserException cause: "; e->GetErrorMessage( szCause, 255 ); csText += szCause; pDC->TextOut( 90, vSize*iLine++, csText ); } В обработчике CUserException причина исключения идентифицируется как '"unknown" (неизвестная). В конце концов, класс CUserException не может же знать, почему было сгенерировано исключение. Но с помощью пользовательских производных классов можно обеспечить механизм, выявляющий причину возникновения исключений различных типов. catch( CException *e ) { TCHAR szCause[255] ; csText = "In CException catch block (unexpected)"; pDC->TextOut ( 50, vSize*iLine++, csText ) ; csText = "CException cause: "; e->GetErrorMessage( szCause, 255 ); csText += szCause; pDC->TextOut ( 90, vSize*iLine++, csText ); } csText = "End of process"; pDC->TextOut( 10, vSize*iLine++, csText ); return; } 1. 2. 3. 4.
Содержание отчета: Цель работы; Исходный текст подпрограмм; Результаты работы подпрограмм (главные окна в ОС Windows); Выводы по проделанной работе с указанием достоинств и недостатков предложенного исходного кода. ЛАБОРАТОРНАЯ РАБОТА №7 "Динамический обмен данными"
55
Цель работы: Изучение принципов разработки программы, позволяющей использовать динамический обмен данными (На примере программы DDE_Demo). Задание к лабораторной работе: 1. Запустить программу DDE_Demo. Результат работы программы представлен на рис.7.1. В результате исполнения создаются пять экземпляров приложения, взаимодействующих друг с другом. Каждый из экземпляров можно свернуть или даже убрать с экрана (сделать невидимым), но все они останутся активными вне зависимости от состояния и размеров их окон. Каждый экземпляр программы DDE_Demo взаимодействует со всеми остальными экземплярами и поддерживает локальный элемент данных, который обозначается как Local Stock и выделяется красным цветом. В то же время каждый экземпляр сообщает о значении данного элемента во всех остальных экземплярах, выделяя эту информацию черным цветом.
Рис. 7.1. Пять экземпляров приложения, взаимодействующих посредством DDE 2. Рассмотреть исходный код программы. В функции WinMain происходит инициализация приложения путем вызова функции InitApplication из файла Template.I. Далее вызывается DDEML-функция DdeInitialize, которая устанавливает функцию обратного вызова для управления графиком DDEсообщений: int WINAPI WinMain( HINSTANCE hInstance, HINSTAKCE hPrevInstance, 56
LPSTR lpCmdLine, INT nCmdShow ) { … if( DdeInitialize( &idInst, (PFNCALLBACK) DdeCallback, APPCMD_FILTERINITS | CBF_SKIP_CONNECT_CONFIRMS | CBF_FAIL_SELFCONNECTIONS | CBF_FAIL_POKES, 0 ) ) return(FALSE); // не продолжать при наличии ошибки Одновременно с заданием функции обратного вызова происходит и установка нескольких флагов, которые играют роль фильтров, ограничивающих типы обрабатываемых сообщений. APPCMD_FILTERINITS - Фильтруются все приложения за исключением тех, имена которых совпадают с нашим собственным именем сервиса. CBF SKIP_CONNECT_CONFIRMS - При установлении подключения подтверждающие сообщения не посылаются. CBF_FAIL_SELFCONNECTIONS - Не допускается подключение приложения к самому себе. CBF FAIL_POKES - Не допускаются транзакции XTYP_POKE. Если по каким-либо причинам не удается установить функцию обратного вызова, приложение возвращает значение FALSE и прекращает свою работу (но это событие маловероятно). Далее производится вызов функции CreateWindow. В случае возникновения ошибки перед выходом из приложения принимаются меры по вызову функции DdeUninitialize: hinst = hInstance; hwnd = CreateWindow( szAppName, szAppTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL ); if( ! hwnd ) { DdeUninitialize ( idInst ); return ( FALSE ) ; } На следующем шаге происходит поиск аргумента командной строки, который будет использоваться для идентификации отдельных экземпляров приложения. Поскольку исходному экземпляру не должен передаваться аргумент командной строки, по умолчанию ему назначается номер 1. 57
if( strlen( lpCmdLine ) ) iInst = atoi( lpCmdLine ) ; else iInst = 1; Данные номера не имеют ничего общего с DDEML и служат лишь в качестве меток. DDEML имеет иное средство идентификации экземпляров приложений - дескрипторы, передаваемые операционной системой. Для получения локальных данных предназначен генератор псевдослучайных чисел. Обычно подобные генераторы должны инициализироваться различными начальными значениями, иначе они будут всегда генерировать одну и ту же последовательность чисел. Для этой цели, как правило, применяется Borlandфункция randomize, которая использует в качестве инициализирующего числа текущее значение системного таймера. Но поскольку в компиляторе Microsoft эта функция отсутствует, воспользоваться ее аналогом. #ifdef __BORLANDC__ randomize (); #еlsе srand( (unsigned)time( NULL ) ); #endif Далее первый экземпляр должен запустить остальные четыре экземпляра приложения с помощью функции WinExec, предоставив ей соответствующие аргументы командной строки для каждого экземпляра. switch( iInst ) { // первый экземпляр запускает остальные case 1: xLoc = 195; yLoc = 125; WinExec( "DDE_DEMO 2", SW_SHOW ); WinExec ( "DDE_DEMO 3", SW_SHOW ); WinExec( "DDE_DEMO 4", SW_SHOW ); WinExec( "DDE_DEMO 5", SW_SHOW ); break; case 2: xLoc = 0; yLoc =0; break; case 3: xLoc = 390; yLoc = 0; break; case 4: xLoc = 390; yLoc =250; 58
break; case 5: xLoc = 0; yLoc = 250; break; } Кроме того, каждому экземпляру передаются координаты xLoc и yLoc, что позволяет разместить окна на экране удобным для просмотра образом. 3. Подготовить приложения к подключению. Вызвать функции ShowWindow и UpdateWindow. Перед началом традиционного цикла передачи сообщений необходимо выполнить несколько действий, обязательных при работе с DDEML. hszAppName = DdeCreateStringHandle( idInst, szAppTitle, 0 ); Сначала необходимо создать дескриптор строки с именем приложения. Аргумент idInst, который был получен ранее с помощью функции DdeInitialize, идентифицирует экземпляры приложения. Последний аргумент указывает кодовую страницу (по умолчанию - CP_WINANSI). В случае использования Unicode-версии DDEML следует указать кодовую страницу CP_WINUNICODE. Далее функция RegisterClipboardFormat возвращает дескриптор формата данных, после чего с помощью функции DdeConnectList делается попытка установить соединение с остальными DDE-приложениями. hFormat = RegisterClipboardFormat( szAppTitle ); hConvList = DdeConnectList( idInst, hszAppName, hszAppName, hConvList, NULL ) ; Если ни одно из DDE-приложений не имеет имени и темы, совпадающих с заданными значениями, список диалогов hConvList остается пустым до тех пор, пока не появится приложение, с которым можно установить соединение. После этого происходит вызов функции DdeNameService для регистрации текущего экземпляра приложения. Эта функция посылает всем остальным DDE-при-ложениям широковещательное сообщение о том, что текущий экземпляр приложения доступен, но пока не установил подключения. DdeNameService( idInst, hszAppMame, 0, DMS_REGISTER ); После установки DDE-соединений начинается обычный цикл сообщений, который будет продолжаться вплоть до прекращения работы данного экземпляра приложения. while( GetMessage( &msg, NULL, 0, 0 ) ) { 59
TranslateMessage( smsg ); DispatchMessage( smsg ) ; } Когда приложение будет готово завершить свою работу, желательно выполнить набор обычных в таких случаях операций очистки, в том числе вызвать функцию DestroyWindow,а затем - функцию UnregisterClass. DestroyWindow( hwnd ) ; UnregisterClass( szAppTitle, hInstance ); 4. Рассмотреть реакцию приложений на сообщения. Первая выполняемая транзакция - это XTYP_CONNECT. Поскольку уже были заданы параметры фильтрации (посредством функции DdeInitialize), любые поступающие запросы на подключение предназначены текущему DDE-приложению и могут быть приняты путем возвращения значения TRUE. case XTYP_CONNECT: return( TRUE ) ; Кроме того, поскольку транзакции XTYP_WILDCONNECT не обрабатываются, попытки установить "обобщенные" подключения автоматически завершатся неудачно. При получении запроса XTYP_ADVSTART выполняется операция проверки, позволяющая обеим программам (клиенту и серверу) убедиться в том, что соединение установлено именно с тем приложением и именно по той теме, которые предполагались. case XTYP_ADVSTART: return( (UINT) wFmt == hFormat && hszItem == hszAppName ); Если какой-либо из аргументов не соответствует требованиям, возвращается значение FALSE и диалог прекращается. Транзакция XTYP_REGISTER уведомляет о появлении нового приложения; в ответ обновляется список подключений hConvList и передается сообщение XTYP_ADVSTART. case XTYP_REGISTER: hConvList = DdeConnectList( idInst, hszItem, hszAppName, hConvList, NULL ); PostTransaction( NULL, 0, XTYP_ADVSTART ); UpdateWindow( hwnd ) ; return ( TRUE ) ;
60
При появлении нового DDE-приложения на обновление окна текущего экземпляра затрачивается дополнительное время. Эта операция может быть отложена до поступления дополнительной информации (например, при передаче данных). Кроме того, одно из приложений может прервать диалог. Если это произойдет, достаточно простой команды обновления окна InvalidateRect. case XTYP_DISCONNECT: InvalidateRect( hwnd, NULL, TRUE ); break; Следующие два типа транзакций, XTYP_ADVREQ и XTYP_REQUEST, ожидают ответа в виде информационного сообщения. Во многих DDEприложениях эти транзакции подразумевают передачу данных в текстовом формате (с разделителями), совместимом с форматом CF_TEXT. case XTYP_ADVREQ: case XTYP_REQUEST: return ( DdeCreateDataHandle ( idInst, (PBYTE) &DataOut, sizeoft DataOut ), 0, hszAppName, hFormat, 0 ) ) ; Функция DdeCreateDataHandle используется для получения дескриптора блока данных. Передаваемые данные имеют тип UINT (32-разрядное целое беззнаковое число), причем номер экземпляра исходного приложения, iInst, находится в старшем байте старшего слова, а остальные данные записаны в младшем слове. Формирование этих данных осуществляется процедурой WndProc и будет описано немного позже. Когда приложение выступает в роли клиента, а не сервера, в ответ на транзакцию XTYP_ADVDATA происходит обратный процесс. С помощью функций DdeGetData и DdeSetUserHandle выполняется преобразование полученных данных в приемлемый формат. case XTYP_ADVDATA: if( DdeGetData( hData, (PBYTE) &DataIn, sizeof(DataIn), 0 ) ) DdeSetUserHandle( hConv, QID_SYNC, DataIn ); InvalidateRect( hwnd, NULL, TRUE ); return ( DDE_FACK ) ; Функция DdeGetData копирует данные в локальный буфер DataIn, после чего вызывается функция DdeSetUserHandle для связывания локального буфера с дескриптором диалога hConv. Этот процесс упрощает выполнение асинхронных транзакций (они будут рассмотрены позже) и запускается в подпрограмме WndProc при вызове функции DdeQueryConvInfo в ответ на 61
сообщение WM_PAINT, когда в цикле опрашиваются все участники текущего диалога. Такая транзакция может инициироваться по разным причинам и при различных обстоятельствах - например, по периодическим сигналам таймера или в ответ на изменение определенных значений. Используемый тип обработки связан с особенностями демонстрационной программы. Остается последний тип транзакций, XTYP_EXECUTE (другое приложение или другой экземпляр просит текущий экземпляр приложения выполнить определенные действия). Но прежде чем определить, какие действия запрашиваются, необходимо получить доступ к данным с помощью функции DdeAccessData, которая возвращает локальный указатель строковых данных (локальный по отношению к данному экземпляру приложения и к данной процедуре). case XTYP_EXECUTE: pszExec = DdeAccessData( hData, sdwSize ); if( pszExec ) { Необязательный параметр dwSize содержит информацию о длине возвращаемой строки. Если нет необходимости знать это значение, аргументу dwSize можно присвоить значение NULL. Если параметр pszExec не равен NULL (т.е. указывает на строку), следующий этап заключается в определении запрашиваемой команды. if( ! stricmp( "PAUSE", pszExec ) ) PauseAutomatic( hwnd ); else if( ! stricmpt "RESUME", pszExec ) ) ResumeAutomatic( hwnd ); Здесь представлены только две возможные команды, которые описываются ключевыми словами PAUSE (пауза) и RESUME (продолжить) и вызывают соответствующие подпрограммы. Поскольку операторы switch/case могут принимать только целочисленные аргументы, приходится выполнять серию операторов if/else для проверки строк. При большом количестве операций проверки это может привести к усложнению структуры программы. В качестве альтернативного варианта предлагается воспользоваться циклической проверкой совпадения строковых аргументов с фиксированными записями в таблице строк с последующим использованием найденного номера строки в операторе switch/case. 5. Рассмотреть технологию DDE в процедуре WndProc, которая управляет работой программы DDE_Demo. При создании каждого экземпляра приложения 62
в ответ на сообщение WM_CREATE происходит инициализация таймера, после чего генерируется сообщение WM_SIZE, позволяющее установить размеры и положение окна данного экземпляра. Подпрограмма обработки сообщения WM_TIMER - это первое место, где происходит вызов DDE-функций. Она начинается с обновления переменной LocalStock: case WM_TIMER: if( random( 2 ) ) LocalStock += random( 100 ); else LocalStock -= min( (UINT) random) 100 ), LocalStock ); DataOut = ( iInst