Александр Побегайло СИСТЕМНОЕ ПРОГРАММИРОВАНИЕ ■ WINDOWS Санкт-Петербург «БХВ-Петербург» 2006
681.3.06 ББК 32.973.26-018.1 П41 Побегайло А. П. П41 Системное программирование в Windows. — СПб.: БХВ-Петербург, 2006. - 1056 с: ил. ISBN 5-94157-792-3 Подробно рассматриваются вопросы системного программирования с использованием интерфейса Win32 API. Описываются управление потоками и процессами, включая их диспетчеризацию; синхронизация потоков; передача данных между процессами, с использованием анонимных и именованных каналов, а также почтовых ящиков; структурная обработка исключений; управление виртуальной памятью; управление файлами и каталогами; асинхронная обработка данных; создание динамически подключаемых библиотек; разработка сервисов. Отдельная часть книги посвящена управлению безопасностью объектов в Windows. Каждая тема снабжена практическими примерами использования функций Win32 API, которые представлены работающими листингами. Это позволяет использовать книгу в качестве пособия по системному программированию или справочника для системного программиста. Прилагаемый компакт-диск содержит листинги и проекты всех программ, рассмотренных в книге. Для программистов Группа подготовки издания: УДК 681.3.06 ББК 32.973.26-018.1 Главный редактор Екатерина Кондукова Зам. главного редактора Игорь Шишигин Зав. редакцией Григорий Добин Редактор Андрей Смышляев Компьютерная верстка Натальи Караваевой Корректор Наталия Першакова Оформление обложки Елены Беляевой Зав. производством Николай Тверских Лицензия ИД № 02429 от 24.07.00. Подписано в печать 15.01.06. Формат 70х1001/16. Печать офсетная. Усл. печ. л. 85,14. Тираж 3000 экз. Заказ № 514 "БХВ-Петербург", 194354, Санкт-Петербург, ул. Есенина, 5Б. Санитарно-эпидемиологическое заключение на продукцию № 77.99.02.953.Д.006421.11.04 от 11.11.2004 г. выдано Федеральной службой по надзору в сфере защиты прав потребителей и благополучия человека. Отпечатано с готовых диапозитивов в ОАО "Техническая книга" 190005, Санкт-Петербург, Измайловский пр., 29. ISBN 5-94157-792-3 © Побегайло А. П., 2006 © Оформление, издательство "БХВ-Петербург", 2006
Оглавление Предисловие 15 Глава 1. Операционные системы и их интерфейсы 19 1.1. Назначение операционной системы 19 1.2. Типы операционных систем 19 1.3. Интерфейс программирования приложений Win32 API 21 1.4. Типы данных в Win32 API 22 1.5. Объекты и их дескрипторы в Windows 24 ЧАСТЬ I. УПРАВЛЕНИЕ ПОТОКАМИ И ПРОЦЕССАМИ 27 Глава 2. Потоки и процессы 29 2.1. Определение потока 29 2.2. Контекст потока 31 2.3. Состояния потока 33 2.4. Диспетчеризация и планирование потоков 37 2.5. Определение процесса 40 Глава 3. Потоки в Windows 41 3.1. Определение потока 41 3.2. Создание потоков 42 3.3. Завершение потоков 47 3.4. Приостановка и возобновление потоков 49 3.5. Псевдодескрипторы потоков 52 3.6. Обработка ошибок в Windows 53 Глава 4. Процессы в Windows 58 4.1. Определение процесса 58 4.2. Создание процессов 58
4 Оглавление 4.3. Завершение процессов 64 4.4. Наследование дескрипторов 67 4.5. Дублирование дескрипторов 75 4.6. Псевдодескрипторы процессов 81 4.7. Обслуживание потоков 82 4.8. Динамическое изменение приоритетов потоков 88 ЧАСТЬ II. СИНХРОНИЗАЦИЯ ПОТОКОВ И ПРОЦЕССОВ 93 Глава 5. Синхронизация 95 5.1. Непрерывные действия и команды 95 5.2. Определение синхронизации 96 5.3. Программная реализация синхронизации 97 5.4. Аппаратная реализация синхронизации 101 5.5. Примитивы синхронизации 104 Глава 6. Синхронизация потоков в Windows 109 6.1. Критические секции 109 6.2. Объекты синхронизации и функции ожидания 115 6.3. Мьютексы 121 6.4. События 128 6.5. Семафоры 137 Глава 7. Взаимоисключающий доступ к переменным 143 7.1. Атомарные операции 143 7.2. Замена значения переменной 144 7.3. Условная замена значения переменной 146 7.4. Инкремент и декремент переменной 148 7.5. Изменение значения переменной 150 Глава 8. Тупики 153 8.1. Определение тупиков 153 8.2. Классификация системных ресурсов 154 8.3. Обнаружение тупиков 156 8.4. Восстановление заблокированного процесса 158 8.5. Предотвращение тупиков 160 8.6. Безопасное завершение потоков в Windows 161
Оглавление 5 ЧАСТЬ III. ПРОГРАММИРОВАНИЕ КОНСОЛЬНЫХ ПРИЛОЖЕНИЙ 165 Глава 9. Структура консольного приложения 167 9.1. Структура консоли 167 9.2. Входной буфер консоли 167 9.3. Буфер экрана 171 Глава 10. Работа с консолью 172 10.1. Создание консоли 172 10.2. Освобождение консоли 177 10.3. Стандартные дескрипторы ввода-вывода 178 Глава 11. Работа с окном консоли 180 11.1. Получение дескриптора окна консоли 180 11.2. Получение и изменение заголовка консоли 181 11.3. Определение максимального размера окна 183 11.4. Установка координат окна 184 Глава 12. Работа с буфером экрана 188 12.1. Создание и активация буфера экрана 188 12.2. Определение и установка параметров буфера экрана 191 12.3. Функции для работы с курсором 194 12.4. Чтение и установка атрибутов консоли 197 Глава 13. Ввод-вывод на консоль 203 13.1. Ввод-вывод высокого уровня 203 13.2. Ввод низкого уровня 207 13.3. Вывод низкого уровня 215 13.4. Режимы ввода-вывода консоли 225 13.5. Прокрутка буфера экрана 229 ЧАСТЬ IV. ОБМЕН ДАННЫМИ МЕЖДУ ПАРАЛЛЕЛЬНЫМИ ПРОЦЕССАМИ 235 Глава 14. Передача данных 237 14.1. Способы передачи данных между процессами 237 14.2. Связи между процессами 239 14.3. Передача сообщений 240
6 Оглавление 14.4. Синхронный и асинхронный обмен данными 241 14.5. Буферизация 242 Глава 15. Работа с анонимными каналами в Windows 243 15.1. Анонимные каналы 243 15.2. Создание анонимных каналов 244 15.3. Соединение клиентов с анонимным каналом 245 15.4. Обмен данными по анонимному каналу 246 15.5. Примеры работы с анонимными каналами 247 15.6. Перенаправление стандартного ввода-вывода 257 Глава 16. Работа с именованными каналами в Windows 265 16.1. Именованные каналы 265 16.2. Создание именованных каналов 266 16.3. Соединение сервера с клиентом 268 16.4. Соединение клиентов с именованным каналом 269 16.5. Обмен данными по именованному каналу 272 16.6. Копирование данных из именованного канала 285 16.7. Передача транзакций по именованному каналу 289 16.8. Определение и изменение состояния именованного канала 295 16.9. Получение информации об именованном канале 303 Глава 17. Работа с почтовыми ящиками в Windows 307 17.1. Концепция почтовых ящиков 307 17.2. Создание почтовых ящиков 308 17.3. Соединение клиентов с почтовым ящиком 309 17.4. Обмен данными через почтовый ящик 311 17.5. Получение информации о почтовом ящике 315 17.6. Изменение времени ожидания сообщения 321 ЧАСТЬ V. СТРУКТУРНАЯ ОБРАБОТКА ИСКЛЮЧЕНИЙ 325 Глава 18. Фреймовая обработка исключений 327 18.1. Исключения и их обработчики 327 18.2. Получение кода исключения 330 18.3. Функции фильтра 332 18.4. Получение информации об исключении 334 18.5. Генерация программных исключений 337 18.6. Необработанные исключения 340 18.7. Обработка исключений с плавающей точкой 342
Оглавление 7 18.8. Обработка вложенных исключений 344 18.9. Передача управления и выход из фрейма 346 18.10. Встраивание SEH в механизм исключений C++ : 348 Глава 19. Финальная обработка исключений 351 19.1. Финальные блоки фрейма 351 19.2. Проверка завершения фрейма 353 19.3. Обработка вложенных финальных блоков 354 ЧАСТЬ VI. РАБОТА С ВИРТУАЛЬНОЙ ПАМЯТЬЮ 357 Глава 20. Виртуальная память 359 20.1. Концепция виртуальной памяти 359 20.2. Организация виртуальной памяти 360 20.3. Алгоритмы замещения страниц 362 20.4. Рабочее множество процесса 363 20.5. Организация виртуальной памяти в Windows 363 Глава 21. Работа с виртуальной памятью в Windows 367 21.1. Состояния виртуальной памяти процесса 367 21.2. Резервирование, распределение и освобождение виртуальной памяти 368 21.3. Блокирование виртуальных страниц в реальной памяти 376 21.4. Изменение атрибутов доступа к виртуальной странице 378 21.5. Управление рабочим множеством страниц процесса 380 21.6. Инициализация и копирование блоков виртуальной памяти 383 21.7. Определение состояния памяти 385 21.8. Работа с виртуальной памятью в другом процессе 388 Глава 22. Работа с кучей в Windows 393 22.1. Создание и удаление кучи 393 22.2. Распределение и освобождение памяти из кучи 395 22.3. Перераспределение памяти из кучи 401 22.4. Блокирование и разблокирование кучи 403 22.5. Проверка состояния кучи 406 22.6. Уплотнение кучи 411
8 Оглавление ЧАСТЬ VII. УПРАВЛЕНИЕ ФАЙЛАМИ 415 Глава 23. Общие концепции 417 23.1. Накопители на жестких магнитных дисках 417 23.2. Секторы и кластеры 418 23.3. Форматирование дисков 419 23.4. Функции файловой системы 420 23.5. Каталоги 420 23.6. Буферизация ввода-вывода 421 23.7. Кэширование ввода-вывода 421 Глава 24. Работа с файлами в Windows 423 24.1. Именование файлов в Windows 423 24.2. Создание и открытие файлов 424 24.3. Закрытие и удаление файлов 427 24.4. Запись данных в файл 428 24.5. Освобождение буферов файла 430 24.6. Чтение данных из файла 433 24.7. Копирование файла 435 24.8. Перемещение файла 437 24.9. Замещение файла 438 24.10. Работа с указателем позиции файла 440 24.11. Определение и изменение атрибутов файла 446 24.12. Определение и изменение размеров файла 449 24.13. Блокирование файла 455 24.14. Получение информации о файле 459 Глава 25. Работа с каталогами (папками) в Windows 468 25.1. Создание каталога 468 25.2. Поиск файлов в каталоге 470 25.3. Удаление каталога 473 25.4. Перемещение каталога 476 25.5. Определение и установка текущего каталога 477 25.6. Наблюдение за изменениями в каталоге 479 ЧАСТЬ VIII. АСИНХРОННАЯ ОБРАБОТКА ДАННЫХ 483 Глава 26. Асинхронный вызов процедур 485 26.1. Механизм асинхронного вызова процедур 485 26.2. Установка асинхронных процедур 486
Оглавление 9 26.3. Приостановка потока 487 26.4. Ожидание события 489 26.5. Оповещение и ожидание события 494 Глава 27. Асинхронный доступ к данным 499 27.1. Концепция асинхронного ввода-вывода 499 27.2. Асинхронная запись данных 500 27.3. Асинхронное чтение данных 506 24.4. Блокирование файлов 511 27.5. Определение состояния асинхронной операции ввода-вывода 518 27.6. Отмена асинхронной операции ввода-вывода 522 27.7. Процедуры завершения ввода-вывода 528 27.8. Асинхронная запись данных с процедурами завершения 529 27.9. Асинхронное чтение данных с процедурами завершения 532 Глава 28. Порты завершения 536 28.1. Концепция порта завершения 536 28.2. Создание порта завершения 537 28.3. Получение пакета из порта завершения 538 28.4. Посылка пакета в порт завершения 539 Глава 29. Работа с ожидающим таймером 544 29.1. Ожидающий таймер 544 29.2. Создание ожидающего таймера 545 29.3. Установка ожидающего таймера 546 29.4. Отмена ожидающего таймера 549 29.5. Открытие существующего ожидающего таймера 552 29.6. Процедуры завершения ожидания 555 ЧАСТЬ IX. ДИНАМИЧЕСКИ ПОДКЛЮЧАЕМЫЕ БИБЛИОТЕКИ 559 Глава 30.Отображение файлов в память 561 30.1. Концепция механизма отображения файлов в память 561 30.2. Создание и открытие объекта, отображающего файл 562 30.3. Отображение файла в память 564 30.4. Обмен данными между процессами через отображаемый в память файл 569 30.5. Сброс вида в файл 573
10 Оглавление Глава 31. Динамически подключаемые библиотеки 578 31.1. Концепция динамически подключаемых библиотек 578 31.2. Создание DLL 579 31.3. Динамическая загрузка и отключение DLL 581 31.4. Использование DLL 584 31.5. Использование файла определений 588 31.6. Статическая загрузка DLL 592 Глава 32. Локальная память потока 594 32.1. Динамическая локальная память потока 594 32.2. Распределение и освобождение локальной памяти потока 595 32.3. Запись и чтение из локальной памяти потока 595 32.4. Статическая локальная память потока 602 ЧАСТЬ X. РАЗРАБОТКА СЕРВИСОВ В WINDOWS 605 Глава 33. Сервисы в Windows 607 33.1. Концепция сервиса 607 33.2. Структура сервиса 608 33.3. Организация функции main 609 33.4. Организация функции ServiceMain 611 33.5. Организация обработчика управляющих команд 617 Глава 34. Работа с сервисами в Windows 620 34.1. Открытие доступа к базе данных сервисов 620 34.2. Установка сервиса 621 34.3. Открытие доступа к сервису 627 34.4. Запуск сервиса : 627 34.5. Определение и изменение состояния сервиса 630 34.6. Определение и изменение конфигурации сервиса 634 34.7. Определение имени сервиса 641 34.8. Управление сервисом 646 34.9. Удаление сервисов 649 34.10. Блокирование базы данных сервисов 653 ЧАСТЬ XI. УПРАВЛЕНИЕ БЕЗОПАСНОСТЬЮ В WINDOWS 659 Глава 35. Система информационной безопасности 661 35.1. Контроль доступа к ресурсам 661 35.2. Политика безопасности 662
Оглавление 11 35.3. Модель безопасности 663 35.4. Дискреционная политика безопасности 664 35.5. Дискреционная модель безопасности 665 35.6. Реализация дискреционной модели безопасности 668 Глава 36. Управление безопасностью в Windows 671 36.1. Модель безопасности в Windows 671 36.2. Учетные записи 672 36.3. Домены 674 36.4. Группы 676 36.5. Идентификаторы безопасности 678 36.6. Дескрипторы безопасности 682 36.7. Списки управления доступом ACL 683 36.8. Маркеры доступа 687 36.9. Создание новых объектов 693 36.10. Контроль доступа к охраняемому объекту 694 36.11. Аудит доступа к охраняемому объекту 696 36.12. Структура системы безопасности 696 Глава 37. Управление пользователями 699 37.1. Создание учетной записи пользователя 699 37.2. Получение информации о пользователе 704 37.3. Перечисление пользователей 706 37.4. Перечисление групп, которым принадлежит пользователь 710 37.5. Изменение учетной записи пользователя 715 37.6. Изменение пароля пользователя 719 37.7. Удаление учетной записи пользователя 721 Глава 38. Управление группами 724 38.1. Создание локальной группы 724 38.2. Получение информации о локальной группе 727 38.3. Перечисление локальных групп 729 38.4. Изменение информации о локальной группе 732 38.5. Добавление членов локальной группы 736 38.6. Установка членов локальной группы 742 38.7. Перечисление членов локальной группы 745 38.8. Удаление членов локальной группы 748 38.9. Удаление локальной группы 754 Глава 39. Работа с идентификаторами безопасности 756 39.1. Структура идентификатора безопасности 756 39.2. Создание идентификатора безопасности 757
12 Оглавление 39.3. Определение учетной записи по идентификатору безопасности 764 39.4. Определение идентификатора безопасности по имени учетной записи 769 39.5. Получение характеристик идентификатора безопасности 773 39.6. Копирование и сравнение идентификаторов безопасности 777 39.7. Строковое представление идентификатора безопасности 782 Глава 40. Работа с дескрипторами безопасности 788 40.1. Форматы дескрипторов безопасности 788 40.2. Создание нового дескриптора безопасности 791 40.3. Определение длины дескриптора безопасности 797 40.4. Получение дескриптора безопасности по имени объекта 802 40.5. Получение дескриптора безопасности по дескриптору объекта 806 40.6. Получение данных из дескриптора безопасности 810 40.7. Получение состояния управляющих флагов дескриптора безопасности 815 40.8. Изменение дескриптора безопасности по имени объекта 818 40.9. Изменение дескриптора безопасности по дескриптору объекта 823 40.10. Изменение состояния управляющих флагов дескриптора безопасности 827 40.11. Строковое представление дескрипторов безопасности 831 Глава 41. Работа со списками управления доступом на высоком уровне 840 41.1. Структура TRUSTEE 840 41.2. Инициализация структуры TRUSTEE 842 41.3. Структура EXPLICIT_ACCESS 846 41.4. Инициализация структуры EXPLICIT_ACCESS 849 41.5. Создание нового списка управления доступом 850 41.6. Модификация списка управления доступом 862 41.7. Получение элементов из списка управления доступом 870 41.8. Получение информации из структуры TRUSTEE 871 41.9. Получение прав доступа из списка управления доступом 874 41.10. Получение из списка управления доступом прав, которые подвергаются аудиту 878 Глава 42. Работа с привилегиями 885 42.1. Локальные идентификаторы привилегий 885 42.2. Инициализация локального идентификатора 887 42.3. Получение локального идентификатора привилегии 888 42.4. Получение имени привилегии 888 42.5. Получение имени привилегии для отображения 891
Оглавление 13 Глава 43. Работа с маркерами доступа 894 43.1. Открытие маркера доступа процесса 894 43.2. Открытие маркера доступа потока 896 43.3. Структуры, используемые для работы с маркером доступа 896 43.4. Получение информации из маркера доступа 900 43.5. Изменение информации в маркере доступа 908 43.6. Настройка привилегий 917 43.7. Настройка групп 918 43.8. Создание маркера ограниченного доступа 920ь 43.9. Дублирование маркеров доступа 927 43.10. Замещение маркеров доступа потока 929 43.11. Проверка идентификатора безопасности на принадлежность маркеру доступа 932 Глава 44. Работа со списками управления доступом на низком уровне 939 44.1. Структура списка управления доступом 939 44.2. Структура элемента списка управления доступом 940 44.3. Инициализация списка управления доступом 943 44.4. Проверка достоверности списка управления доступом 944 44.5. Добавление элементов в список управления доступом 945 44.6. Получение элементов из списка управления доступом 972 44.7. Удаление элементов из списка управления доступом 977 44.8. Получение информации о списке управления доступом 981 44.9. Установка версии списка управления доступом 985 44.10. Определение доступной памяти 986 Глава 45. Управление безопасностью объектов на низком уровне 987 45.1. Доступ к информации о владельце объекта 988 45.2. Доступ к информации о первичной группе владельца объекта 992 45.3. Доступ к списку DACL 997 45.4. Доступ к списку SACL 1004 45.5. Защита файлов и каталогов 1006 45.6. Защита объектов ядра 1016 45.7. Защита сервисов 1024 45.8. Защита ключей реестра 1031 45.9. Защита объектов пользователя 1037 Приложение. Описание компакт-диска 1045 Предметный указатель 1047
Предисловие Эта книга предназначена для начинающих системных программистов. Но поскольку она содержит большой объем справочной информации по интерфейсу программирования приложений Win32 API (Application Programming Interface — интерфейс программирования приложений), то может использоваться и опытными программистами в качестве справочного пособия. Начинающие системные программисты могут и не совсем ясно понимать, чем же отличается системное программирование от обычного (или прикладного) программирования, и, вообще-то, можно поговорить о том, кто такие системные программисты и чем они занимаются. Очевидно, что как системные, так и прикладные программисты пишут программы. Чем же отличаются системные программы от прикладных? Ключом к ответу на этот вопрос является понятие системы, которое в различных прикладных областях знаний имеет разные определения. Если говорить понятным для программистов языком, то программная система это набор функций, при помощи которых можно решить любую задачу из некоторой предметной области. Почему же системное программирование обычно ассоциируется с операционными системами и их интерфейсами для разработки программ? Так сложилось исторически. Первыми серьезными программными системами были именно операционные системы, поэтому и основные концепции системного программирования отрабатывались при разработке и реализации операционных систем. Затем эти технологии использовались при разработке других программных систем, таких как, например, системы управления базами данных (СУБД). Хотя в настоящее время наблюдается и обратное влияние. Классическое системное программирование рассматривает круг вопросов, связанных с синхронизацией и диспетчеризацией потоков и процессов, обменом данными между процессами, управлением устройствами компьютера и файлами. В последнее время большое внимание при проектировании систем также уделяется и обеспечению безопасности данных, что вызвано возросшими угрозами несанкционированного доступа к данным. Средства операционных
16 Предисловие систем Windows, предназначенные для решения этих задач, исключая управление устройствами, и рассмотрены в этой книге. Первоначально, материал, представленный в книге, был подготовлен как пособие для студентов, изучающих курс "Операционные системы". Как правило, продолжительность такого курса — I семестр, за который нужно разобрать основные концепции операционных систем, да еще выполнить лабораторные работы по данному курсу. Времени катастрофически не хватает даже на рассмотрение основных теоретических концепций и технических приемов, используемых при построении операционных систем. А тут еще надо и обучить студентов системному программированию. А студенты-то и прикладные программы еще не очень хорошо пишут, т. к. опыта программирования маловато. Хотя они и владеют языком программирования С (и в некоторой степени C++), но разрабатывать учебные проекты операционных систем при таком уровне знаний нереально, поэтому, как правило, лабораторные работы заключаются в разработке программ, решающих конкретные системные задачи. Для этого нужно использовать функции из интерфейса операционной системы, предназначенные для системного программирования. Объяснить на лекциях или семинарах назначение и работу этих функций невозможно из-за громоздкости материала и недостатка времени. В общем, нужно пособие по элементарным приемам системного программирования под Windows. После создания этого пособия на лекциях излагались только концептуальные теоретические и технические вопросы курса, а техника программирования изучалась студентами самостоятельно. Если же какие-то технические вопросы и возникали при программировании задач, то они, как правило, решались прямо на лабораторных занятиях. Это позволило разгрузить лекции от многих технических подробностей и облегчить концептуальное построение курса. Думаю, эти замечания, как и само пособие, окажутся полезными как преподавателям, так и студентам, изучающим курс "Операционные системы", используя для практической работы платформы Microsoft Windows. После этого вступления становится понятной структура книги. Каждая глава посвящена отдельной теме, связанной с системным программированием под Windows. Чтобы иметь представление о задачах, которые решаются при помощи рассматриваемых функций, первый раздел или параграф каждой главы содержит основные теоретические моменты, относящиеся к данной тематике. После этого рассматриваются функции из интерфейса Windows, предназначенные для решения системных задач из данной области, и приведены примеры использования этих функций. Все примеры настолько элементарны, насколько это возможно. Поэтому можно надеяться, что они будут понятны начинающим программистам. Все представленные материалы готовились довольно продолжительное время. Поэтому изложение может показаться неровным. Но переработка такого объема информации также займет продолжительное время. Поэтому я ре-
Предисловие 17 шил оставить все как есть. Все программы были протестированы на платформе операционной системы Windows 2000, используя среду разработки Microsoft Visual Studio 6.0. Думаю, что в настоящее время именно эта среда и используется в вузах при обучении. За исключением нескольких незначительных моментов, связанных с изменением типов параметров в прототипах функций, проблем с переходом на среду разработки Microsoft Visual Studio 7.0 (.NET) быть не должно. Хотя все программы и были протестированы, но не исключены ошибки, которые могли возникнуть при форматировании текста. Но поскольку программы очень простые, то устранение этих ошибок не должно вызвать затруднений. Теперь о двух вопросах, которые могут возникнуть при рассмотрении программ. Первый вопрос касается стиля программирования, а именно — проверки значения переменной, которая чаще всего содержит дескриптор объекта, на равенство величине null. С одной стороны символическая константа null по стандартам языков программирования С и C++ — это ноль. Поэтому значение переменной в этом случае можно рассматривать просто как логическое значение. С другой стороны, в примерах из MSDN дескриптор объекта проверяется на равенство null посредством оператора сравнения ==. По-видимому, это обусловлено тем, что символическая константа null также определена и в интерфейсе прикладного программирования Win32 API, который не стандартизирован. Естественно, что в Win32 API эта константа также определена как ноль. В программах, приведенных в книге, принят такой же подход, как и в примерах из MSDN (справочная система). Второй вопрос связан с вводом/выводом на консоль. В программах для этих целей используются функции из заголовочных файлов stdio.h, iostream.h и conio.h. Я считаю, что программист должен одинаково хорошо знать стандартные функции языков программирования С и C++, поэтому использование функций из заголовочных файлов stdio.h и iostream.h не обсуждается. Что касается функций из заголовочного файла conio.h, то они полезны в том случае, если стандартные потоки ввода/вывода перенаправляются в файлы или анонимные каналы. В этом случае для тестирования программ приходится использовать функции из заголовочного файла conio.h, т. к. они всегда работают с консолью. Кроме того, по-видимому, эти функции имеют более простую реализацию, поэтому при их использовании не возникает проблем, связанных с синхронизацией ввода/вывода на консоль. Учитывая вышесказанное, думаю программистам полезно также познакомиться и с этими функциями. Автор
Глава 1 Операционные системы и их интерфейсы 1.1. Назначение операционной системы Физическими или аппаратными ресурсами компьютера называются физические устройства, из которых состоит компьютер. К таким устройствам относятся центральный процессор, оперативная память, внешняя память, шины передачи данных и различные устройства ввода-вывода информации. Логическими или информационными ресурсами компьютера называются данные и программы, которые хранятся в памяти компьютера. Когда говорят обо всех ресурсах компьютера, включая как физические, так и логические ресурсы, то обычно используют термины ресурсы компьютера или системные ресурсы. Для выполнения на компьютере какой-либо программы необходимо, чтобы она имела доступ к ресурсам компьютера. Этот доступ обеспечивает операционная система. Можно сказать, что операционная система — это комплекс программ, который обеспечивает доступ к ресурсам компьютера и управляет ими. Другими словами, операционная система — это администратор или менеджер ресурсов компьютера. Назначение операционной системы состоит в обеспечении пользователя программными средствами для использования ресурсов компьютера и эффективном разделении этих ресурсов между пользователями. Отсюда следует, что главными функциями операционной системы являются управление ресурсами компьютера и диспетчеризация или планирование этих ресурсов. 1.2. Типы операционных систем Все программы, которые работают на компьютере под управлением операционной системы, называются пользовательскими программами. Совокупность пользовательских программ, которая предназначена для решения определенной
20 Глава 1 задачи, называется приложением. Если операционная система одновременно может исполнять только одну пользовательскую программу, то она называется однопрогроммной или однопользовательской. Если же под управлением операционной системы могут одновременно выполняться несколько пользовательских программ, то такая операционная система называется мультипрограммной или многопользовательской. В зависимости от назначения операционной системы и аппаратуры компьютера, на котором она работает, можно определить несколько типов операционных систем. Если операционная система может работать только на компьютере с одним процессором, то такая операционная система называется однопроцессорной. Если же операционная система может работать также и на компьютере, который содержит несколько процессоров, то такая операционная система называется мультипроцессорной. Следует делать различие между операционными системами, которые предназначены для обработки информации под управлением пользователя, и операционными системами, которые предназначены для управления объектами при помощи компьютера в реальном времени без участия пользователя. Такими объектами могут быть, например, робот или самолет. Операционная система, предназначенная для работы в режиме реального времени, называется операционной системой реального времени. Главное отличие операционных систем реального времени заключается в их быстром реагировании на внешние события и надежности функционирования. Если пользователь, сидя у компьютера, будет только раздражен медленной или ненадежной работой операционной системы, то медленная или ненадежная работа операционной системы реального времени может вызвать поломку оборудования и аварию. В дальнейшем будут рассматриваться только операционные системы фирмы Microsoft, а именно Windows 98 и Windows 2000, которые предназначены для использования на персональных компьютерах. Эти операционные системы отличаются своей внутренней организацией, но используют один и тот же интерфейс для программирования приложений — Win32 API. Мы не будем рассматривать операционную систему Windows СЕ, которая предназначена для использования в таких различных устройствах, как, например, устройства бытовой электроники, контроллеры для управления технологическими процессами и устройства управления коммуникационным оборудованием. Но, разобравшись в изложенном материале, вы получите опыт, который поможет вам как в изучении Windows СЕ, так и других операционных систем. Относительно операционной системы Windows XP можно сказать следующее. Те приемы системного программирования, которые рассмотрены в этой книге для операционной системы Windows 2000, также работают и в операционной системе Windows XP.
Операционные системы и их интерфейсы 21 1.3. Интерфейс программирования приложений Win32 API Интерфейс программирования приложений Win32 API представляет собой набор функций и классов, которые используются для программирования приложений, работающих под управлением операционных систем фирмы Microsoft. Следует отметить, что в работе многих функций Win32 API существуют различия, которые зависят от типа операционной системы. Кроме того, некоторые функции работают только в операционной системе Windows 2000 и не поддерживаются операционной системой Windows 98. Все эти случаи будут отмечаться отдельно. Но все же в работе функций Win32 API в разных версиях операционных систем гораздо больше общего, чем различий. Поэтому чаще всего мы будем говорить, что функции Win32 API предназначены для разработки приложений на платформах операционных систем Windows, не делая различия между операционными системами Windows 98 и Windows 2000. Это соглашение значительно облегчит изложение материала, не загромождая его ненужными подробностями, которые отвлекают от сути рассматриваемых вопросов. Функционально Win32 API подразделяется на следующие категории: □ Base Services (базовые сервисы); □ Common Control Library (библиотека общих элементов управления); □ Graphics Device Interface (интерфейс графических устройств); □ Network Services (сетевые сервисы); □ User Interface (интерфейс пользователя); □ Windows NT Access Control (управление доступом для Windows NT); □ Windows Shell (оболочка Windows); □ Windows System Information (информация о системе Windows). Кратко опишем функции, которые выполняются в рамках этих категорий. Функции базовых сервисов обеспечивают приложениям доступ к ресурсам компьютера. Категория Common Control Library содержит классы окон, которые часто используются в приложениях. Интерфейс графических устройств обеспечивает функции для вывода графики на дисплей, принтер и другие графические устройства. Сетевые сервисы используются при работе компьютеров в компьютерных сетях. Интерфейс пользователя обеспечивает функции для взаимодействия пользователя с приложением, используя окна для ввода-вывода информации. Категория Windows NT Access Control содержит функции, которые используются для защиты информации путем контроля и ограничения доступа к защищаемым объектам. Категории Windows Shell и Windows System Information содержат соответственно функции для работы с оболочкой и конфигурацией операционной системы Windows.
22 Глава 1 В курсе системного программирования главным образом изучается назначение и использование функций из категорий Base Services и Windows NT Access Control. Функции из категорий Common Control Libraiy, Graphics Device Interface и User Interface используются для разработки интерфейса приложений, а курс, который изучает назначение и использование этих функций, как правило, называется "Программирование пользовательских интерфейсов в Windows". Изучив два этих курса и добавив сюда свои знания по программированию на языке C++, вы получите довольно содержательное представление о разработке приложений на платформе Win32 API. В связи с тем, что программирование графических пользовательских интерфейсов в Windows само по себе является довольно трудоемким занятием, мы будем изучать функции ядра Windows, работая только с консольными приложениями. Это упростит изложение предмета и избавит нас от большого количества кода, не относящегося к существу рассматриваемых вопросов. 1.4. Типы данных в Win32 API Прежде всего заметим, что интерфейс программирования приложений Win32 API ориентирован на язык программирования С или, в более широком смысле, на процедурные языки программирования. Поэтому в этом интерфейсе, не используются такие возможности языка программирования C++, как классы, ссылки и механизм обработки исключений. Чтобы сделать интерфейс Win32 API более независимым от конкретного языка программирования или, может быть, более соответствующим аппаратному обеспечению компьютера, разработчики этого интерфейса определили новые простые типы данных. Эти типы данных используются в прототипах функций интерфейса Win32 API. Новые простые типы данных определены как синонимы простых типов данных языка программирования С. Чтобы отличать эти типы от других типов, их имена определены прописными буквами. Общее количество простых типов данных, определенных в интерфейсе Win32 API, довольно велико. Поэтому ниже приведены определения только тех простых типов данных из этого интерфейса, которые очевидным образом переименовывают простые типы данных языка программирования С. typedef char CHAR; typedef unsigned char UCHAR; typedef UCHAR *PUCHAR; typedef unsigned char BYTE; typedef BYTE *PBYTE; typedef BYTE *LPBYTE;
Операционные системы и их интерфейсы 23 typedef short SHORT; typedef unsigned short USHORT; typedef USHORT *PUSHORT; typedef unsigned short WORD; typedef WORD *PWORD; typedef WORD *LPWORD; typedef typedef typedef typedef typedef typedef int int int int BOOL BOOL INT; *PINT; *LPINT; BOOL; *PBOOL; *LPBOOL typedef unsigned int UINT; typedef unsigned int *PUINT; typedef long LONG; typedef long *LPLONG; typedef unsigned long ULONG; typedef ULONG *PULONG; typedef unsigned long DWORD; typedef DWORD *PDWORD; typedef DWORD *LPDWORD; typedef float FLOAT; typedef FLOAT *PFLOAT; typedef void *LPVOID; typedef CONST void *LPCVOID; Остальные простые типы данных, определенные в интерфейсе Win32 API, имеют, как правило, специфическое назначение и поэтому они будут описаны при их использовании. Кроме того, в интерфейсе Win32 API определены символические константы false и true для обозначения соответственно ложного и истинного логических значений. Определения этих констант приведены ниже. #ifndef FALSE #define FALSE 0
24 Глава 1 #endif #ifndef TRUE #define TRUE 1 #endif В интерфейсе Win32 API также определено множество сложных типов данных, таких как структуры и перечисления. Как правило, эти типы данных имеют специфическое назначение и поэтому будут описаны при их непосредственном использовании. 1.5. Объекты и их дескрипторы в Windows Объектом в Windows называется структура данных, которая представляет системный ресурс. Таким ресурсом может быть, например, файл, канал, графический рисунок. Операционные системы Windows предоставляют приложению объекты трех категорий: □ User (объекты интерфейса пользователя); □ Graphics Device Interface (объекты интерфейса графических устройств); □ Kernel (объекты ядра). Категория User включает объекты, которые используются приложением для интерфейса с пользователем. К таким объектам относятся, например, окна и курсоры. Категория Graphics Device Interface включает объекты, которые используются для вывода информации на графические устройства. К таким объектам относятся, например, кисти и перья. Категория Kernel включает объекты ядра операционной системы Windows. К таким объектам относятся, например, файлы и каналы. При изучении системного программирования подробно рассматриваются только объекты категории Kernel. Объекты двух оставшихся категорий рассматриваются при изучении программирования графических интерфейсов. Под доступом к объектам понимается возможность приложения выполнять над объектом некоторые функции. Приложение не имеет прямого доступа к объектам, а обращается к ним косвенно. Для этого в операционных системах Windows каждому объекту ставится в соответствие дескриптор (handle). В Win32 API дескриптор имеет тип handle. Дескриптор объекта представляет собой запись в таблице, которая поддерживается системой и содержит адрес объекта и средства для идентификации типа объекта. Дескрипторы объектов создаются операционной системой и возвращаются функциями Win32 API, которые создают объекты. За редким исключением, эти функции имеют вид createobject, где слово object заменяется именем конкретного объекта. Например, процесс создается при помощи вызова функции createProcess. Как правило, такие функции возвращают дескриптор соз-
Операционные системы и их интерфейсы 25 данного объекта. Если это значение не равно null (или отрицательному значению), то объект создан успешно. После завершения работы с объектом его дескриптор нужно закрыть, используя функцию closeHandle, которая имеет следующий прототип: BOOL CloseHandle( HANDLE hObject // дескриптор объекта ); При успешном завершении функция closeHandle возвращает ненулевое значение, в противном случае — false. Функция closeHandle удаляет дескриптор объекта, но сам объект удаляется не всегда. Дело в том, что в Windows на один и тот же объект могут ссылаться несколько дескрипторов, которые создаются другими функциями для доступа к уже созданному ранее объекту. Функция closeHandle уничтожает объект только в том случае, если на него больше не ссылается ни один дескриптор.
Часть I Управление потоками и процессами Глава 2. Потоки и процессы Глава 3. Потоки в Windows Глава 4. Процессы в Windows
Глава 2 Потоки и процессы 2.1. Определение потока Определение потока тесно связано с последовательностью действий процессора во время исполнения программы. Исполняя программу, процессор последовательно выполняет инструкции программы, иногда осуществляя переходы в зависимости от некоторых условий. Такая последовательность выполнения инструкций программы называется потоком управления внутри программы. Отметим, что поток управления зависит от начального состояния переменных, которые используются в программе. В общем случае различные исходные данные порождают различные потоки управления. Поток управления можно представить как нить в программе, на которую нанизаны инструкции, выполняемые микропроцессором. Поэтому часто поток управления также называется нитью (thread). В русскоязычной литературе за потоком управления закрепилось название поток. Для пояснения понятия потока рассмотрим следующую программу, которая выводит минимальное число из двух целых чисел или сообщение о том, что числа равны. #include <iostream.h> int main() { int a, ' b; cout « "Input two integers: "; cin » a » b; if (a == b) { cout « "There is no min." « endl; return 0;
30 Часть I. Управление потоками и процессами if (а < Ь) cout « "min = " « а « endl; else cout « "min = " « b « endl; return 0; } Предположим, что перегруженные операторы ввода-вывода не образуют новых потоков. Тогда в зависимости от входных данных эта программа образует один из трех возможных потоков управления. А именно, если выполняется условие (а == Ь), то образуется поток: cout « "Input two integers: "; cin » a » b; if (a == b) { cout « "There is no min." « endl; return 0; } Если выполняется условие (а < b), то образуется поток: cout « "Input two integers: "; cin ». a » b; if (a == b) if (a < b) cout « "min = " « a « endl; return 0; Если же выполняется условие (а > b), то образуется поток cout « "Input two integers: "; cin » a » b; if (a == b) if (a < b) cout « "min = " « b « endl; return 0; Теперь перейдем к классификации программ в зависимости от количества определяемых ими параллельных потоков управления. Будем говорить, что программа является многопоточной, если в ней может одновременно существовать несколько потоков. Сами потоки в этом случае называются параллельными. Если в программе одновременно может существовать только один поток, то такая программа называется однопотонной. Например, еле-
Глава 2. Потоки и процессы 31 дующая программа, которая просто вычисляет сумму двух чисел, является однопоточной: #include <iostream.h> int sum(int a, int b) { return a + b; } int main() { int a, b; int с = 0; cout « "Input two integers: "; cin » 'a » b; с = sum (a, b) ; cout « "Sum = " « с « endl; return 0; } Теперь предположим, что после вызова функции sum функция main не ждет возвращения значения из функции sum, а продолжает выполняться. В этом случае получим программу, состоящую из двух потоков, один из которых определяется функцией main, а второй — функцией sum. Причем эти потоки независимы, т. к. они не имеют доступа к общим или, другими словами, разделяемым переменным. Правда в этом случае не гарантируется, что поток main выведет сумму чисел а и ь, т. к. инструкция вывода значения суммы может отработать раньше, чем поток sum вычислит эту сумму. Из этих рассуждений видно, что для того чтобы отметить функцию, которая порождает новый поток в программе, должна использоваться специальная нотация. В операционных системах Windows для обозначения того, что функция образует поток, используются специальные спецификаторы функции. Такая функция обычно также называется потоком. 2.2. Контекст потока В общем случае содержимое памяти, к которой поток имеет доступ во время своего исполнения, называется контекстом потока. Определим, каким ограничениям на доступ к памяти должны удовлетворять функции, чтобы их можно было безопасно вызывать в параллельных потоках. Для этого рассмотрим следующую функцию: int f(int n)
32 Часть I. Управление потоками и процессами if (n > 0) —п; if (n < 0) ++п; return n; } Сколько бы раз эта функция не вызывалась параллельно работающими потоками, она будет корректно изменять значение переменной п, т. к. эта переменная является локальной в функции f. To есть для каждого нового вызова функции f будет создан новый локальный экземпляр переменной п. Такая функция f называется безопасной для потоков. Теперь введем глобальную переменную п и изменим нашу функцию следующим образом: int n ; void g() { if (n > 0) —П; if (n < 0) ++П; } В этом случае параллельный вызов функции g несколькими потоками может дать некорректное изменение значения переменной п, т. к. значение этой переменной будет изменяться одновременно несколькими функциями д. В этом случае функция д не является безопасной для потоков. Та же проблема встречается и в случае, когда функция использует статические переменные. Для разбора этого случая рассмотрим функцию int count() { static int n = 0; ++n; return n; } которая возвращает количество своих вызовов. Если эта функция будет вызвана несколькими параллельно исполняемыми потоками, то цельзя точно определить значение переменной п, которое вернет эта функция, т. к. это значение изменяется всеми потоками параллельно. В общем случае функция называется повторно входилюй или реентерабельной (reentrant или reenterable), если она удовлетворяет следующим требованиям: □ не использует глобальные переменные, значения которых изменяются параллельно исполняемыми потоками;
Глава 2. Потоки и процессы 33 □ не использует статические переменные, определенные внутри функции; □ не возвращает указатель на статические данные, определенные внутри функции. В системном программировании часто также рассматриваются программы в кодах микропроцессора, выполнение которых может прерываться и возобновляться в любой момент времени. Причем одна и та же программа может запускаться прежде, чем завершилось исполнение предыдущего экземпляра этой программы. В этом случае также необходимо, чтобы программный код допускал корректное параллельное выполнение нескольких экземпляров программы. Это условие обеспечивается в том случае, если программа не изменяет свой код во время исполнения. Здесь под кодом подразумеваются как команды, так и данные, принадлежащие программе. Программа в кодах микропроцессора, которая не изменяет свой код, также называется реентерабельной. В дополнение к реентерабельным функциям определяют также функции, безопасные для вызова параллельно исполняемыми потоками. Функция называется безопасной для потоков, если она обеспечивает блокировку доступа к ресурсам, которые она использует. Как обеспечить блокирование доступа к ресурсам, рассматривается в гл. 6, 7, посвященных синхронизации потоков. Сейчас же только скажем, что в этом случае решается задача взаимного исключения доступа к разделяемым ресурсам, используя примитивы синхронизации. Очевидно, что если функция не является реентерабельной, то она также не является и безопасной для потоков, т. к. в этом случае несколько потоков разделяют общую память, не блокируя доступ к ней. А память, как уже говорилось, также является системным ресурсом. 2.3. Состояния потока Как видно из определения, поток описывает динамическое поведение всей программы или какой-либо функции в программе. Для удобства обозначений предположим, что программа является однопоточной. Тогда поток можно рассматривать как пару: поток = (процессор, программа). Программа может исполняться процессором только в том случае, если она готова к исполнению. То есть все системные ресурсы, которые необходимы для исполнения этой программы, свободны для использования. Кроме того, для исполнения программы необходимо, чтобы и сам процессор был свободен и готов к исполнению этой программы. Для более формального описания этих ситуаций вводятся понятия "состояние процессора" и "состояние 2 Зак. 514
34 Часть I. Управление потоками и процессами программы". При этом предполагают, что процессор и программа могут находиться в следующих состояниях. □ Состояния процессора: • процессор не выделен для исполнения программы; • процессор выделен для исполнения программы. □ Состояния программы: • программа не готова к исполнению процессором; • программа готова к исполнению процессором. Для краткости записи введем для этих состояний следующие названия: □ Состояния процессора: • "не выделен"; • "выделен". □ Состояния программы: • "не готова"; • "готова". Тогда мы можем определить состояние потока как пару состояний: состояние потока = (состояние процессора, состояние программы). Перечислив различные комбинации состояний процессора и программы, можно описать все возможные состояния потока. Введем для состояний потока следующие названия: □ поток блокирован = ("не выделен", "не готова"); □ поток готов к выполнению = ("не выделен", "готова"); □ поток выполняется = ("выделен", "готова"); Будем считать, что состояние ("выделен", "не готова") является недостижимым для потока. То есть программе, не готовой к исполнению, процессор не выделяется. Более кратко эти состояния потока будем просто обозначать словами: "блокирован", "готов" и "выполняется". Для полноты картины нужно ввести для потоков еще два состояния: "новый" и "завершен", которые описывают соответственно поток, еще не начавший свою работу, и поток, завершивший свою работу. Тогда диаграмма возможных переходов потока из состояния в состояние может быть изображена, как это показано на рис. 2.1. В результате мы получили простейшую диаграмму переходов потока из состояния в состояние. Сами переходы потока из состояния в состояние, которые на диаграмме обозначаются дугами, описывают некоторые операции
Глава 2. Потоки и процессы 35 над потоком. Названия этих операций указаны рядом со стрелками. Кратко опишем эти операции. □ Операция create выполняется потоком, который создает новый поток из функции. Эта операция переводит поток из состояния "новый" в состояние "готов". □ Операция Exit выполняется самим исполняемым потоком в случае его завершения. Эта операция переводит поток из состояния "выполняется" в состояние "завершен". / \ Create /'"" новый j< ^ готов ) ^выполняется^) ^*" >■( завершен ---.. - --С interrupt UnblockV ^У ^ ( блокирован^) Рис. 2.1. Модель пяти состояний потока. Оставшиеся четыре операции выполняются операционной системой. □ Операция Run запускает готовый поток на выполнение, т. е. выделяет ему процессорное время. Эта операция переводит поток из состояния "готов" в состояние "выполняется". Поток получает процессорное время в том случае, если подошла его очередь к процессору на обслуживание. □ Операция interrupt задерживает исполнение потока и переводит его из состояния "выполняется" в состояние "готов". Эта операция выполняется над потоком в том случае, если истекло процессорное время, выделенное потоку на исполнение, или исполнение потока прервано по каким-либо другим причинам. П Операция Block блокирует исполнение потока, т. е. переводит его из состояния "выполняется" в состояние "блокирован". Эта операция выполняется над потоком в том случае, если он ждет наступления некоторого события, например, завершения операции ввода-вывода или освобождения ресурса. □ Операция unblock разблокирует поток, т. е. переводит его из состояния "блокирован" в состояние "готов". Эта операция выполняется над потоком в том случае, если событие, ожидаемое потоком, наступило. Разрешим потокам также выполнять операции друг над другом. Для этого введем операции Suspend И Resume. □ Операция suspend приостанавливает исполнение потока. □ Операция Resume возобновляет исполнение потока.
36 Часть I. Управление потоками и процессами Используя эти операции, один поток может соответственно приостановить или возобновить исполнение другого потока независимо от того, в каком состоянии этот последний поток находится. Впрочем, заметим, что поток может приостановить и свое исполнение. Если над потоком выполнена операция suspend, то будем говорить, что поток находится в приостановленном или подвешенном состоянии. Кратко будем говорить, что в этом случае поток "подвешен". Дополним диаграмму состояний потока, изображенную на рис. 2.1, этими новыми операциями и состояниями. Получим более полную диаграмму состояний потока, которая показана на рис. 2.2. Create Run ( НОВЫЙ V \ готов ~jr ^ Interrupt / Resumes *^""" ""\ Exit w / \ выполняется j— >ч завершен ) Unblock Suspend /' подвешен ^ ^ и готов у Block ( блокирован) Рис. 2.2. Модель семи состояний потока Теперь разрешим потоку выполнять операции над самим собой. Для этого введем операцию sleep. □ Операция sleep позволяет потоку приостановить свое исполнение на некоторый интервал времени или, другими словами, заснуть. Разбудить поток должна операционная система по истечении заданного интервала времени, используя операцию wakeup. Если поток выполнил операцию sleep, то будем говорить, что он перешел в сонное состояние или "спит". П Операция wakeup позволяет операционной системе разбудить поток. В результате можно построить полную диаграмму состояний потока, которая и приведена на рис. 2.3. В заключение этого параграфа скажем, что в конкретных операционных системах для работы с потоками могут быть определены и другие состояния,
Глава 2. Потоки и процессы 37 а также операции, которые переводят потоки в эти состояния. В гл. 3 будет рассмотрена модель состояний потока в операционной системе Windows 2000. новый N Exit | выполняется ь Resumej Unblock Resume jSuspend ^i_I \ /""подвешен*^ J \ ч и спит J i \ "** * " / \ T \ Wakeup | 4 завершен ) Block ( блокирован ) Рис. 2.3. Модель девяти состояний потока 2.4. Диспетчеризация и планирование потоков В однопрограммной операционной системе одновременно может выполняться только один поток, которому доступны все ресурсы компьютера. Поэтому блокировка потока может происходить только в случаях ожидания этим потоком события, отмечающего завершение операций ввода-вывода. Недостатком однопрограммных операционных систем является их низкая производительность, т. к. процессор простаивает, если поток блокирован. В мультипрограммных операционных системах одновременно могут существовать несколько потоков, что повышает производительность компьютера. Однако в этом случае требуется некоторая дисциплина обслуживания этих
38 Часть I. Управление потоками и процессами потоков, смысл которой заключается в порядке выделения конкурирующим потокам ресурсов компьютера. Для простоты дальнейшего изложения будем считать, что компьютер имеет только один процессор. Тогда общий подход к обслуживанию потоков в мультипрограммных операционных системах состоит в следующем. Время работы процессора делится на кванты (интервалы), которые выделяются потокам для работы. По истечении кванта времени исполнение потока прерывается и процессор назначается другому потоку. Распределением квантов времени между потоками занимается специальная программа, которая называется менеджер потоков. Когда менеджер потоков переключает процессор на исполнение другого потока, он должен выполнить следующие действия: П сохранить контекст прерываемого потока; П восстановить контекст запускаемого потока на момент его прерывания; □ передать управление запускаемому потоку. Контекст потока это содержимое памяти, с которой работает поток. Поэтому в каждый момент времени работы потока, его контекст полностью определяется содержимым регистров микропроцессора в этот момент времени. Отсюда следует, что для сохранения контекста потока необходимо сохранить содержимое регистров микропроцессора на момент прерывания потока, а при восстановлении контекста потока необходимо восстановить содержимое этих регистров. Теперь кратко расскажем о сути алгоритмов управления потоками. Сначала предположим, что все потоки имеют одинаковый приоритет. Тогда они выстраиваются в одну очередь на обслуживание к процессору. Процессор обслуживает потоки в порядке FIFO (first in — first out), т. е. первым пришел — первым вышел, и прерванные потоки становятся в конец очереди. Такая дисциплина обслуживания называется циклическим обслуживанием. Так как незавершившиеся потоки блокируются до следующего обслуживания, а не уходят не обслуженными, то циклическое обслуживание также называется FCFS (first come — first served), т. е. первым пришел — первым обслужен. Схематически циклическое обслуживание потоков показано на рис. 2.4. очередь потоков центральный процессор новые потоки t { W W W обслуженные потоки прерванные потоки Рис. 2.4. Циклическое обслуживание потоков
Глава 2. Потоки и процессы 39 Если потоки имеют разные приоритеты, то для управления ими используются более сложные дисциплины обслуживания с несколькими очередями. В этом случае каждая очередь включает потоки, которые имеют одинаковый приоритет. Схематически дисциплины обслуживания с несколькими очередями показаны на рис. 2.5. очередь потоков центральный процессор новые потоки очередь 1 очередь п обслуженные потоки прерванные потоки Рис. 2.5. Дисциплины обслуживания с несколькими очередями Простейший алгоритм обслуживания нескольких очередей заключается в следующем: первыми обслуживаются потоки, которые имеют наивысший приоритет. В общем случае управление потоками разделяется на планирование и диспетчеризацию. Под планированием потоков понимается алгоритм, используемый для постановки прерванных потоков в очереди. Менеджер потоков (диспетчер) может изменять приоритет прерванного потока, что изменяет очередь, в которую этот поток будет поставлен. Алгоритмы планирования изучаются математической дисциплиной, которая называется теория расписаний. Под диспетчеризацией потоков понимается алгоритм, устанавливающий порядок, в котором процессор обслуживает очереди. Алгоритмы диспетчеризации изучаются математической дисциплиной, которая называется теория массового обслуживания. Алгоритмы управления потоками разрабатывают таким образом, чтобы оптимизировать следующие параметры системы: □ время загрузки микропроцессора работой должно быть максимальным; □ пропускная способность системы должна быть максимальной; □ время нахождения потока в системе должно быть минимальным; □ время ожидания потока в очереди должно быть минимальным; □ время реакции системы на обслуживание заявки должно быть минимальным.
40 Часть I. Управление потоками и процессами При этом для каждой системы должен быть выбран оптимальный интервал обслуживания потоков, который снижает затраты на переключение контекстов потоков. В общем случае разделение времени работы процессора между потоками позволяет быстрее выполнять потоки, которые требуют немного времени на свое исполнение, но замедляет исполнение трудоемких потоков. 2.5. Определение процесса Процессом или задачей называется исполняемое на компьютере приложение вместе со всеми ресурсами, которые требуются для его исполнения. Все ресурсы, необходимые для исполнения процесса, также называются контекстом процесса. Процессу обязательно принадлежат следующие ресурсы: □ адресное пространство процесса; П потоки, исполняемые в контексте процесса. Адресное пространство — это виртуальная память, выделенная процессу для запуска программ. Об устройстве виртуальной памяти будет рассказано в гл. 20. Адресные пространства разных процессов не пересекаются. Более того, процесс не имеет непосредственного доступа в адресное пространство другого процесса. Это позволяет избежать влияния ошибок, произошедших в каком-либо процессе, на исполнение других процессов, что повышает надежность системы в целом. Потоки, исполняемые в контексте процесса, запускаются в одном адресном пространстве, которое принадлежит этому процессу. В принципе, основной причиной, вызвавшей введение в системное программирование понятия потока, и было разделение адресных пространств процессов. Дело в том, что в этом случае взаимодействие между параллельными процессами требует больших затрат на пересылку данных, что заметно замедляет работу приложений. Потоки же выполняются в адресном пространстве одного процесса и, следовательно, могут обращаться к общим адресам памяти, что упрощает их взаимодействие.
Глава 3 Потоки в Windows 3.1. Определение потока Потоком в Windows называется объект ядра, которому операционная система выделяет процессорное время для выполнения приложения. Каждому потоку принадлежат следующие ресурсы: □ код исполняемой функции; □ набор регистров процессора; □ стек для работы приложения; □ стек для работы операционной системы; □ маркер доступа, который содержит информацию для системы безопасности. Все эти ресурсы образуют контекст потока в Windows. Кроме дескриптора каждый поток в Windows также имеет свой идентификатор, который уникален для потоков выполняющихся в системе. Идентификаторы потоков используются служебными программами, которые позволяют пользователям системы отслеживать работу потоков. В операционных системах Windows различаются потоки двух типов: □ системные потоки; □ пользовательские потоки. Системные потоки выполняют различные сервисы операционной системы и запускаются ядром операционной системы. Пользовательские потоки служат для решения задач пользователя и запускаются приложением. На рис. 3.1 показана диаграмма состояний потока, работающего в среде операционной системе Windows 2000. В работающем приложении различаются потоки двух типов: □ рабочие потоки (working threads); □ потоки интерфейса пользователя (user interface threads).
42 Часть /. Управление потоками и процессами Pick to run Switch выполняется) Ресурс доступен Unblock/Resume ресурс доступен Terminate Unblock ресурс недоступен ждет Block/Suspend А. завершен Рис. 3.1. Модель состояний потока в Windows 2000 Рабочие потоки выполняют различные фоновые задачи в приложении. Потоки интерфейса пользователя связаны с окнами и выполняют обработку сообщений, поступающих этим окнам. Каждое приложение имеет, по крайней мере, один поток, который называется первичным (primary) или главным (main) потоком. В консольных приложениях это поток, который исполняет функцию main. В приложениях с графическим интерфейсом это поток, который ИСПОЛНЯеТ фуНКЦИЮ WinMain. 3.2. Создание потоков Создается поток функцией CreateThread, которая имеет следующий прототип: HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes, // атрибуты защиты DWORD dwStackSize, // размер стека потока в байтах LPTHREAD_START_ROUTINE IpStartAddress, // адрес функции LPVOID lpParameter, // адрес параметра DWORD dwCreationFlags, // флаги создания потока LPDWORD lpThreadld // идентификатор потока При успешном завершении функция CreateThread возвращает дескриптор созданного потока и его идентификатор, который является уникальным для всей системы. В противном случае эта функция возвращает значение null. Кратко ОПИШем назначение параметров функции CreateThread.
Глава 3. Потоки в Windows 43 Параметр lpThreadAttributes устанавливает атрибуты защиты создаваемого потока. До тех пор пока мы не изучим систему безопасности в Windows, мы будем устанавливать значения этого параметра в null при вызове почти всех функций ядра Windows. В данном случае это означает, что операционная система сама установит атрибуты защиты потока, используя настройки по умолчанию. О процессах будет подробно рассказано в следующей главе. Параметр dwstacksize определяет размер стека, который выделяется потоку при запуске. Если этот параметр равен нулю, то потоку выделяется стек, размер которого по умолчанию равен 1 Мбайт. Это наименьший размер стека, который может быть выделен потоку. Если величина параметра dwstacksize меньше значения, заданного по умолчанию, то все равно потоку выделяется стек размером в 1 Мбайт. Операционная система Windows округляет размер стека до одной страницы памяти, который обычно равен 4 Кбайт. Параметр lpstartAddress указывает на исполняемую потоком функцию. Эта функция должна иметь следующий прототип: DWORD WINAPI имя_функции_потока(LPVOID lpParameters); Видно, что функции потока может быть передан единственный параметр lpParameter, который является указателем на пустой тип. Это ограничение следует из того, что функция потока вызывается операционной системой, а не прикладной программой. Программы операционной системы являются исполняемыми модулями и поэтому они должны вызывать только функции, сигнатура которых заранее определена. Поэтому для потоков определили самый простой список параметров, который содержит только указатель. Так как функции потоков вызываются операционной системой, то они также получили название функции обратного вызова. Параметр dwCreationFiags определяет, в каком состоянии будет создан поток. Если значение этого параметра равно 0, то функция потока начинает выполняться сразу после создания потока. Если же значение этого параметра равно create_suspended, то поток создается в подвешенном состоянии. В дальнейшем ЭТОТ ПОТОК МОЖНО запусТИТЬ ВЫЗОВОМ фуНКЦИИ ResumeThread. Параметр lpThreadid является выходным, т. е. его значение устанавливает Windows. Этот параметр должен указывать на переменную, в которую Windows поместит идентификатор потока. Этот идентификатор уникален для всей системы и может в дальнейшем использоваться для ссылок на поток. Идентификатор потока главным образом используется системными функциями и редко функциями приложения. Действителен идентификатор потока только на время существования потока. После завершения потока тот же идентификатор может быть присвоен другому потоку. В операционной системе Windows 98 этот параметр не может быть равен null. В Windows NT и 2000 допускается установить его значение в null — тогда операционная система не возвратит идентификатор потока.
44 Часть I. Управление потоками и процессами В листинге 3.1 приведен пример программы, которая использует функцию CreateThread для создания потока и демонстрирует способ передачи параметров исполняемой потоком функции. f Щи^йШ^^М^ш^ потока функцией 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; // запускаем поток Add hThread = CreateThread (NULL, 0, Add, (void*) inc, 0, & IDThread) ; if (hThread == NULL) return GetLastError(); // ждем, пока поток Add закончит работу- Wait For Sing leObj ее t( hThread, INFINITE); // закрываем дескриптор потока Add CloseHandle(hThread); cout « "n = " « n « endl; return 0;
Глава 3. Потоки в Windows 45 Отметим, ЧТО В ЭТОЙ Программе ИСПОЛЬЗуеТСЯ фуНКЦИЯ WaitForSingleObject, которая ждет завершения потока Add. Подробно эта функция будет рассмотрена далее, в разд. 6.2, посвященном объектам синхронизации и функциям ожидания. ( Замечание ^ Отметим, что перед компиляцией этой программы в консольном проекте необходимо установить режим отладки многопоточных приложений. В среде разработки Visual C++ 6.0 это делается следующим образом: в пункте меню Project выбирается команда Settings. Далее, в появившемся диалоговом окне ProjectSettings выбирается вкладка C/C++. Теперь в списке Category выбираем строку Code Generation, а в списке Use run-time library выбираем строку Debug Multithreaded, если программа будет отлаживаться, или Multithreaded, если программа уже готова к использованию. После этого нажимаем ОК и программа готова к компиляции, редактированию связей и выполнению. Эти же действия необходимо выполнить перед отладкой любого многопоточного приложения. Для создания потоков можно также использовать макрокоманду _beginthreadex, которая описана в заголовочном файле process.h и имеет те же параметры, что и функция createThread. Как утверждает Джеффри Рихтер в своей книге "Программирование приложений для Windows", использование этой макрокоманды более надежно, чем непосредственный вызов функции CreateThread. За более подробной информацией по этому вопросу нужно обратиться к первоисточнику, а именно к вышеупомянутой книге Джеффри Рихтера. В листинге 3.2 приведен пример программы, которая использует макрокоманду _beginthreadex для создания потока и демонстрирует способ передачи параметров исполняемой потоком функции. I Листинг 3.2. Создание потока макрокомандой „beginthreadex #include <windows.h> #include <iostream.h> #include <string.h> #include <process.h> UINT WINAPI thread(void *pString) { int i = 1; char *pLexema; pLexema = strtok((char*) pString," ");
46 Часть I. Управление потоками и процессами while (pLexema != NULL) { cout « "Thread find the lexema " « i « " : "« pLexema « endl; pLexema = strtok(NULL/ " ") ; i++; } return 0; } int main() { char sentence[80]; int i, j , к = 0; HANDLE hThread; UINT IDThread; cout « "Input string: "; cin.getline(sentence, 80); j = strlen(sentence); // создаем поток для подсчета лексем hThread = (HANDLE) _beginthreadex(NULL, 0, thread, sentence, 0, &IDThread); if (hThread == NULL) return GetLastError(); // сами подсчитываем количество букв "а" в строке for (i=0; i<j; i++) if (sentence[i] == 'a') k++; cout « "Number of symbols 'a* in the string = " « k « endl; // ждем окончания разбора на лексемы WaitForSingleObject(hThread, INFINITE); // закрываем дескриптор потока thread CloseHandle(hThread); return 0;
Глава 3. Потоки в Windows 47 3.3. Завершение потоков Поток завершается вызовом функции ExitThread, которая имеет следующий прототип: VOID ExitThread( DWORD dwExitCode // код завершения потока ); Эта функция может вызываться как явно, так и неявно при возврате значения из функции потока. При выполнении этой функции система посылает динамическим библиотекам, которые загружены процессом, сообщение dll_thread_detach, которое говорит о том, что поток завершает свою работу. ЕСЛИ ПОТОК СОЗДаеТСЯ При ПОМОЩИ Макрокоманды _beginthreadex, ТО ДЛЯ завершения потока нужно использовать макрокоманду _endthreadex, единственным параметром которой является код возврата из потока. Эта макрокоманда описана в заголовочном файле process.h. Причина использования в этом случае макрокоманды _endthreadex заключается в том, что она не только выполняет выход из потока, но и освобождает память, которая была распределена макрокомандой _beginthreadex. Если поток создан функцией _beginthreadex, TO ДЛЯ ВЫХОДа ИЗ ПОТОКа функция _endthreadex МОЖет ВЫ- зываться как явно, так и неявно при возврате значения из функции потока. Один поток может завершить другой поток, вызвав функцию TerminateThread, которая имеет следующий прототип: BOOL TerminateThread( HANDLE hThread, // дескриптор потока DWORD dwExitThread // код завершения потока ); В случае успешного завершения функция TerminateThread возвращает ненулевое значение, В ПРОТИВНОМ случае — FALSE. Функция TerminateThread завершает поток, но не освобождает все ресурсы, принадлежащие этому потоку. Это происходит потому, что при выполнении этой функции система не посылает динамическим библиотекам, загруженным процессом, сообщение о том, что поток завершает свою работу. В результате динамическая библиотека не освобождает ресурсы, которые были захвачены для работы с этим потоком. Поэтому эта функция должна вызываться только в аварийных ситуациях при зависании потока. В листинге 3.3 приведена программа, которая демонстрирует работу функции TerminateThread. В этой программе следует обратить внимание на ква- лификатор типа volatile, который указывает компилятору, **то значение переменной count должно храниться в памяти, т. к. к этой переменной имеют доступ параллельные потоки. Дело в том, что сам компилятор языка
48 Часть I. Управление потоками и процессами программирования С или C++ не знает, что такое поток. Для него это просто функция. А в языках программирования С и C++ любая функция вызывается только синхронно, т. е. функция, вызвавшая другую функцию, ждет завершения этой функции. Если не использовать квалификатор volatile, то компилятор может оптимизировать код и в одном потоке хранить значение переменной в регистре, а в другом потоке — в оперативной памяти. В результате параллельно работающие потоки будут обращаться к разным переменным. | Листинг 3,3. Завершение потока функцией TerminateThread # include <windows.h> #include <iostream.h> volatile UINT count; void thread() { for (;;) { ++count; Sleep(lOO); // немного отдохнем } } int main() { HANDLE hThread; DWORD IDThread; char c; 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 any char to finish: ";
Глава 3. Потоки в Windows 49 if (с == 'у') cout « "count = " « count « endl; else break; > // прерываем выполнение потока thread Termina teThread(hThread, 0); // закрываем дескриптор потока CloseHandle(hThread); return 0; > 3.4. Приостановка и возобновление потоков Каждый созданный поток имеет счетчик приостановок, максимальное значение которого равно maximum_suspend_count. Счетчик приостановок показывает, сколько раз исполнение потока было приостановлено. Поток может исполняться только при условии, что значение счетчика приостановок равно нулю. В противном случае поток не исполняется или, как говорят, находится в подвешенном состоянии. Исполнение каждого потока может быть приостановлено вызовом функции suspendThread, которая имеет следующий прототип: DWORD SuspendThread( HANDLE hThread // дескриптор потока ); Эта функция увеличивает значение счетчика приостановок на 1 и, при успешном завершении, возвращает текущее значение этого счетчика. В случае неудачи функция SuspendThread возвращает значение, равное —1. Отметим, что поток может приостановить также и сам себя. Для этого он ДОЛЖен Передать фунКЦИИ SuspendThread СВОЙ ПСеВДОДескрИПТОр, КОТОрЫЙ можно получить при помощи функции GetcurrentThread. Подробнее псевдодескрипторы потоков будут рассмотрены в разд. 3.5. Для возобновления исполнения потока используется функция ResumeThread, которая имеет следующий прототип: DWORD ResumeThread( HANDLE hThread // дескриптор потока
50 Часть I. Управление потоками и процессами Функция ResumeThread уменьшает значение счетчика приостановок на 1 при условии, что это значение было больше нуля. Если полученное значение счетчика приостановок равно 0, то исполнение потока возобновляется, в противном случае поток остается в подвешенном состоянии. Если при вызове функции ResumeThread значение счетчика приостановок было равным О, то это значит, что поток не находится в подвешенном состоянии. В этом случае функция не выполняет никаких действий. При успешном завершении функция ResumeThread возвращает текущее значение счетчика приостановок, в противном случае — значение —1. Поток может задержать свое исполнение вызовом функции sleep, которая имеет следующий прототип: VOID Sleep( DWORD dwMiHiseconds // миллисекунды ); Единственный параметр функции sleep определяет количество миллисекунд, на которые поток, вызвавший эту функцию, приостанавливает свое исполнение. Если значение этого параметра равно 0, то выполнение потока просто прерывается, а затем возобновляется при условии, что нет других потоков, ждущих выделения процессорного времени. Если же значение этого параметра равно infinite, to поток приостанавливает свое исполнение навсегда, что приводит к блокированию работы приложения. В листинге 3.4 приведена программа, которая демонстрирует работу функций SuspendThread, ResumeThread И Sleep. | ЛИСТИНГ ЗА Пример работы ФУНКЦИЙ Suej^dtoread, ResumeThread и Sleep [ # include <windows.h> #include <iostream.h> volatile UINT nCount; volatile DWORD dwCount; void thread() { for (;;) { nCount++; // приостанавливаем поток на 100 миллисекунд SleepA00);
Глава 3. Потоки в Windows 51 } int main () { HANDLE hThread; DWORD IDThread; char c; hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) thread, NULL, 0, &IDThread); if (hThread == NULL) return GetLastError(); for (;;) { cout « "Input cout « "\t'n' cout « "\t'y' cout « "\t's' cout « "\t'r' cin » c; if (c == 'n') break; switch (c) { case 'y': cout « "count = " « nCount « endl; break; case 's': // приостанавливаем поток thread dwCount = SuspendThread(hThread); cout « "Thread suspend count = " « dwCount « endl; break; case 'r': // возобновляем поток thread dwCount = ResumeThread(hThread); cout « "Thread suspend count = " « dwCount « endl; break; :" « endl; to exit" « endl; to display the count" « endl; to suspend thread" « endl; to resume thread" « endl;
52 Часть I. Управление потоками и процессами } } // прерываем выполнение потока thread TeirminateThread (hThread, 0); // закрываем дескриптор потока thread CloseHandle(hThread); return 0; } 3.5. Псевдодескрипторы потоков Иногда потоку требуется знать свой дескриптор, чтобы изменить какие-то свои характеристики. Например, поток может изменить свой приоритет. Для этих целей в Win32 API существует функция GetcurrentThread, которая имеет следующий прототип: HANLDE GetcurrentThread(VOID); и возвращает псевдодескриптор текущего потока. Псевдодескриптор текущего потока отличается от настоящего дескриптора потока тем, что он может использоваться только самим текущим потоком и, следовательно, может наследоваться другими процессами. Псевдодескриптор потока не нужно закрывать после его использования. Из псевдодескриптора потока можно получить настоящий дескриптор потока, для этого псевдодескриптор нужно продублировать, вызвав функцию DupiicateHandie. Подробно дублирование дескрипторов рассматривается в гл. 4. В листинге 3.5 приведен пример программы, которая вызывает функцию GetcurrentThread, а затем выводит на консоль полученный псевдодескриптор. | Листинг 3.5» Пример работы функции GetcurrentThread # include <windows.h> #include <iostream.h> int main () { HANDLE hThread; // получаем псевдодескриптор текущего потока hThread = GetcurrentThread();
Глава 3. Потоки в Windows 53 // выводим псевдодескриптор на консоль cout « hThread « endl; cin.get(); return 0; 3.6. Обработка ошибок в Windows Большинство функций Win32 API возвращают код, по которому можно определить, как завершилась функция: успешно или нет. Если функция завершилась неудачей, то код возврата обычно равен false, null или — 1. В этом случае функция Win32 API также устанавливает внутренний код ошибки, который называется кодом последней ошибки (last-error code) и поддерживается отдельно для каждого потока. Чтобы получить код последней ошибки, нужно вызвать функцию GetLastError, которая имеет следующий прототип: DWORD GetLastError(VOID); Эта функция возвращает код последней ошибки, установленной в потоке. Установить код последней ошибки в потоке можно при помощи функции setLastError, имеющей следующий прототип: VOID SetLastError( DWORD dwErrCode // код ошибки Чтобы получить сообщение, соответствующее коду последней ошибки, необходимо использовать функцию FormatMessage, которая имеет следующий прототип: // режимы форматирования // источник сообщения // идентификатор сообщения // идентификатор языка // буфер для сообщения // максимальный размер буфера для сообщения // список значений для вставки в сообщение DWORD FormatMessage( DWORD LPCVOID DWORD DWORD LPTSTR DWORD va list dwFlags, IpSource, dwMessageld, dwLanguageld, lpBuffer, nSize, *Arguments ); Мы не будем подробно рассматривать эту функцию, которая предназначена для форматирования символьных сообщений, приведем только пример ее использования для вывода сообщения об ошибке (message box). Для этого
54 Часть I. Управление потоками и процессами приведем сначала, в листинге 3.6, текст функции, которая выводит сообщение об ошибке, а затем, в листинге 3.7, программу, которая использует эту функцию. Листинг 3.6, Функция до# вывода сообщений об ошибке в MessageBox # include <windows.h> void ErrorMessageBox() { LPVOID lpMsgBuf; Forma tMes sage ( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT) , // язык по умолчанию (LPTSTR) &lpMsgBuf, 0, NULL ); // Показать ошибку в MessageBox. MessageBox( NULL, (LPCTSTR)lpMsgBuf, "Ошибка Win32 API", MB_OK | MB_ICONINFORMATION ); // Освободить буфер. LocalFree(lpMsgBuf); } Листинг 3,7. Пример вывода сообщения об ошибке в ммвадовох #include <windows.h> // прототип функции вывода сообщения об ошибке в MessageBox
Глава 3. Потоки в Windows 55 void ErrorMessageBox(); // тест для функции вывода сообщения об ошибке на консоль int main() { HANDLE hHandle=NULL; // неправильный вызов функции закрытия дескриптора if (!CloseHandle(hHandle)) ErrorMessageBox(); return 0; } Теперь, раз мы работаем с консольными приложениями, рассмотрим, как выводить сообщение об ошибке на консоль. Для этого нам нужно научиться выводить русский текст на консоль. Это можно сделать при помощи функции charToOem, которая имеет следующий прототип: BOOL CharToOem ( LPCTSTR IpszSrc, // строка для перекодировки LPSTR IpszDst // перекодированная строка '); Эта функция перекодирует символы из кодировки Microsoft в кодировку, определенную производителем оборудования. Аббревиатура OEM расшифровывается как Original Equipment Manufacturer (настоящий производитель аппаратуры). В ЛИСТИНГе 3.8 Приведен пример ИСПОЛЬЗОВаНИЯ фуНКЦИИ CharToOem. Листинг 3*8» Пример перекодировки русских букв для вывода на консоль >ч:",-..j #include <windows.h> #include <iostream.h> int main() { char big[] = "АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЬЫЬЭЮЯ"; char sml[] = "абвгдеёжзийклмнопрстуфхцчшщъыьэюя"; CharToOem (big, big) ;
56 Часть I. Управление потоками и процессами CharToOem (sml, sml) ; cout « big « endl; cout « sml « endl; return 0; } Теперь определим функцию, которая выводит сообщение об ошибке на консоль, на русском языке. Текст этой функции приведен в листинге 3.9. |У1ушй1шЖ Функция для вывода сообщения об ошибке на консоль на русском i #include <windows.h> #include <iostream.h> void CoutErrorMessage() { char prefix[] = "Ошибка Win32 API: "; LPVOID lpMsgBuf; CharToOem(prefix,prefix); // перекодируем заголовок FormatMessage ( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT) , // язык по умолчанию (LPTSTR) SclpMsgBuf, NULL ); // перекодируем русские буквы CharToOem((char*)lpMsgBuf,(char*)lpMsgBuf); // выводим сообщение об ошибке на консоль
Глава 3. Потоки в Windows 57 cout « prefix « (char*) lpMsgBuf « endl; // освобождаем буфер LocalFree(lpMsgBuf); } В ЛИСТИНГе 3.10 приведен Пример Использования функции CoutErrorMessage в консольном приложении. | Листинг 110, Пример вывода сообщения об ошибке на консоль #include <windows.h> #include <iostream.h> // прототип функции для вывода сообщения об ошибке на консоль void CoutErrorMessage(); // тест для функции вывода сообщения об ошибке на консоль int main () { HANDLE nHandle=NULL; // неправильный вызов функции закрытия дескриптора if (!CloseHandle(hHandle)) CoutErrorMessage(); return 0; } ( Замечание ) Если при запуске приложения используется отладчик, то текст сообщения, соответствующий коду последней ошибки, можно посмотреть в окне watch, если набрать в строке "имя переменной" следующий текст: @err,hr.
Глава 4 Процессы в Windows 4.1. Определение процесса В Windows под процессом понимается объект ядра, которому принадлежат системные ресурсы, используемые исполняемым приложением. Поэтому можно сказать, что в Windows процессом является исполняемое приложение. Выполнение каждого процесса начинается с первичного потока. Во время своего исполнения процесс может создавать другие потоки. Исполнение процесса заканчивается при завершении работы всех его потоков. Каждый процесс в операционной системе Windows владеет следующими ресурсами: □ виртуальным адресным пространством; □ рабочим множеством страниц в реальной памяти; □ маркером доступа, содержащим информацию для системы безопасности; □ таблицей для хранения дескрипторов объектов ядра. Кроме дескриптора, каждый процесс в Windows имеет свой идентификатор, который является уникальным для процессов, выполняющихся в системе. Идентификаторы процессов используются, главным образом, служебными программами, которые позволяют пользователям системы отслеживать работу процессов. 4.2. Создание процессов Новый процесс в Windows создается вызовом функции createProcess, которая имеет следующий прототип: BOOL CreateProcess( LPCTSTR lpApplicationName, // имя исполняемого модуля LPTSTR lpCoramandLine, // командная строка
Глава 4. Процессы в Windows 59 LPSECURITY_ATTRIBUTES lpProcessAttributes, // защита процесса LPSECURITY_ATTRIBUTES lpThreadAttributes, // защита потока BOOL blnheritHandles, // признак наследования дескриптора DWORD dwCreationFlags, // флаги создания процесса LPVOID IpEnvironment, // блок новой среды окружения LPCTSTR lpCurrentDirectory, // текущий каталог LPSTARTUPINFO IpStartUpInfo, // вид главного окна LPPROCESS_INFORMATION lpProcessInformation // информация о процессе ); Функция createProcess возвращает ненулевое значение, если процесс был создан успешно. В противном случае эта функция возвращает значение false. Процесс, который создает новый процесс, называется родительским процессом (parent process) по отношению к создаваемому процессу. Новый же процесс, который создается другим процессом, называется дочерним процессом (child process) по отношению к процессу-родителю. Сейчас мы опишем ТОЛЬКО назначение некоторых параметров фуНКЦИИ CreateProcess. Остальные параметры этой функции будут описываться по мере их использования. Первый параметр lpAppiicationName определяет строку с именем исполняемого файла, который имеет тип ехе и будет запускаться при создании нового процесса. Эта строка должна заканчиваться нулем и содержать полный путь к исполняемому файлу. Для примера рассмотрим программу, которая выводит на консоль свое имя и параметры. Эта программа приведена в листинге 4.1. . .->„,-.- ~ . .,..,.. .. _. . ._ . ; Листинг 4,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)
60 Часть I. Управление потоками и процессами _cprintf ("\n My %d parameter = %s", i, argv[i]); _cputs("\nPress any key to finish.\n"); _getch(); return 0; } Скомпилируем эту программу. Полученный ехе-файл сохраним на диске С: и назовем ConsoleProcess.exe. Наша задача состоит в запуске этого файла как нового процесса. Как это сделать показано в листинге 4.2, где приведена профамма, запускающая созданный ехе-файл как консольный процесс с новой консолью. | Листинг 4,2. Программа процесса, который создает процесс с новой консолью #include <windows.h> #include <conio.h> int main() { char IpszAppName[] = "С:\\ConsoleProcess.exe"; STARTUPINFO Si; PROCESS_INFORMATION piApp; ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); // создаем новый консольный процесс if (!CreateProcess(IpszAppName, 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");
Глава 4. Процессы в Windows 61 // ждем завершения созданного процесса WaitForSingleObject(piApp.hProcess, INFINITE); // закрываем дескрипторы этого процесса в текущем процессе CloseHandle(piApp.hThread); CloseHandle(piApp.hProcess); return 0; } Отметим в этой программе два момента. Во-первых, перед запуском консольного процесса ConsoleProcess.exe все поля структуры si типа startupinfo должны заполняться нулями. Это делается при помощи вызова функции zeroMemory, которая предназначена для этой цели и имеет следующий прототип: VOID ZeroMemory ( PVOID Destination, SIZE_T Length ); В этом случае вид главного окна запускаемого приложения определяется по умолчанию самой операционной системой Windows. Во-вторых, в параметре dwCreationFlags устанавливается флаг CREATE_NEW_CONSOLE. Это ГОВОрИТ системе о том, что для запускаемого процесса должна быть создана новая консоль. Если этот параметр будет равен null, to новая консоль для запускаемого процесса не создается и весь консольный вывод нового процесса будет направляться в консоль родительского процесса. Структура piApp типа process_information содержит идентификаторы и дескрипторы нового создаваемого процесса и его главного потока. Мы не используем эти дескрипторы в нашей программе и поэтому закрываем их. Значение false параметра binheritHandie говорит о том, что эти дескрипторы не являются наследуемыми. О наследовании дескрипторов мы поговорим подробнее в разд. 4.4. Теперь запустим наш новый консольный процесс другим способом, используя второй параметр функции createProcess. Это можно сделать при помощи программы, приведенной в листинге 4.3. [ Листинг 4,3. Программа процесса, который создает процесс с новой консолью | # include <windows.h> #include <conio.h> // адрес блока памяти // длина блока памяти int main()
62 Часть /. Управление потоками и процессами { char lpszCoramandLine[] = "С: WConsoleProcess .exe pi p2 p3"; STARTUPINFO si; PROCESS_INFORMATION piCom; ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof (STARTUPINFO) ; // создаем новый консольный процесс CreateProcess(NULL, IpszCommandLine, 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; } Отличие этой программы от программы, приведенной в листинге 4.2, состоит в том, что мы передаем системе имя нового процесса и егч мраметры через командную строку. В этом случае имя нового процесса может и не содержать полный путь к ехе-файлу, а только имя самого ехе-файла. При использовании параметра lpCommandLine система для запуска нового процесса осуществляет поиск требуемого ехе-файла в следующей последовательности каталогов: □ каталог, из которого запущено приложение; □ текущий каталог родительского процесса; □ системный каталог Windows; □ каталог Windows; □ каталоги, которые перечислены в переменной PATH среды окружения. Для иллюстрации сказанного запустим приложение Notepad.exe (Блокнот), используя командную строку. Программа, запускающая Блокнот из командной строки, приведена в листинге 4.4.
Глава 4. Процессы в Windows 63 :...&5ь-.«£..а ..^~.~:Azzsz~$>Ji..£.:£;J!L.JL £.'^$£.:£И..^£.~Д£. ~.~ 1.'—. . ~..г.._:.^.. . .„.J #include <windows.h> #include <iostream.h> 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, // дескрипторы текущего процесса не наследуются О, //по умолчанию NORMAL_PRIORITY_CLASS NULL, // используем среду окружения вызывающего процесса NULL, // текущий диск и каталог, как и в вызывающем процессе &si, // вид главного окна - по умолчанию &pi // информация о новом процессе ) ) { cout « "The mew process is not created." « endl « "Check a name of the process." « endl; return 0; } SleepA000); // немного подождем и закончим свою работу // закроем дескрипторы запущенного процесса в текущем процессе CloseHandle(pi.hThread) ; CloseHandle(pi.hProcess); return 0;
64 Часть I. Управление потоками и процессами 4.3. Завершение процессов Процесс может завершить свою работу вызовом функции ExitProcess, которая имеет следующий прототип: VOID ExitProcess( UINT uExitCode // код возврата из процесса ); При вызове функции ExitProcess завершаются все потоки процесса с кодом возврата, который является параметром этой функции. При выполнении этой функции система посылает динамическим библиотекам, которые загружены процессом, сообщение dll_process_detach, которое говорит о том, что динамическую библиотеку необходимо отсоединить от процесса. В листинге 4.5 приведен пример программы, которая завершает свою работу ВЫЗОВОМ фуНКЦИИ ExitProcess. (Листинг 4 Д. Завершение процесса функцией Bxl tProcess #include <windows.h> #include <iostream.h> volatile UINT count; void thread() { for (;;) { count++; SleepA00); } } int main() { char c; HANDLE hThread; DWORD IDThread; hThread = CreateThread(NULL/ 0, (LPTHREAD_START_ROUTINE)thread, NULL, 0, &IDThread);
Глава 4. Процессы в Windows 65 if (hThread == NULL) return GetLastError(); for (;;) { cout « "Input 'y' to display the count or any char to exit: "; cin » (char)c; if (c == 'у') cout « "count = " « count « endl; else ExitProcessA) ; } } Один процесс может быть завершен другим при помощи вызова функции TerminateProcess, Которая Имеет следующий ПрОТОТИП! BOOL TerminateProcess( HANDLE hProcess, // дескриптор процесса UINT uExitCode // код возврата ); Если функция TerminateProcess выполнилась успешно, то она возвращает ненулевое значение. В противном случае возвращаемое значение равно FALSE. ФунКЦИЯ TerminateProcess Завершает работу Процесса, НО He OCBO- бождает все ресурсы, принадлежащие этому процессу. Это происходит потому, что при выполнении этой функции система не посылает динамическим библиотекам, зафуженным процессом, сообщение о том, что библиотеку необходимо отсоединить от процесса. Поэтому эта функция должна вызываться только в аварийных ситуациях при зависании процесса. Приведем программу, которая демонстрирует работу функции TerminateProcess. Для этого сначала создадим бесконечный процесс- счетчик, который назовем ConsoleProcess.exe, и расположим на диске С: (листинг 4.6). I Листинг 4,6. Программа бесконечного процесса # include <windows . h> #include <iostream.h> int count; void main() 3 Зак. 514
66 Часть I. Управление потоками и процессами for ( ; ; ) { count++; SleepA000); cout « "count = " « count « endl; } } Теперь рассмотрим программу, которая создает этот бесконечный процесс- счетчик, а потом завершает его по требованию пользователя, используя для этого функцию TerminateProcess. Эта программа приведена в листинге 4.7. I Листинг 4J, Завершение процесса функцией TeminateProcess #include <windows.h> #include <conio.h> int main () { char IpszAppName [] = "С:\\ConsoleProcess.exe"; STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof (STARTUPINFO) ) ; si.cb=sizeof(STARTUPINFO); // создаем новый консольный процесс if (!CreateProcess(IpszAppName, 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");
Глава 4. Процессы в Windows 67 while (true) { char с; _cputs("Input 't' to terminate the new console process: "); с = _getch(); if (c == 't') { _cputs("t\n"); // завершаем новый процесс TerminateProcess(pi.hProcess,1); break; } } // закрываем дескрипторы нового процесса в текущем процессе CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0; } 4.4. Наследование дескрипторов Большинство объектов категории Kernel могут быть наследуемыми или ненаследуемыми. Свойство наследования объекта означает, что если наследуемый объект создан или открыт в некотором процессе, то к этому объекту будут также иметь доступ все процессы, которые создаются этим процессом, т. е. являются его потомками. Свойство наследования объекта определяется его дескриптором, который также может быть наследуемым или ненаследуемым. Для того чтобы объект стал наследуемым, необходимо сделать наследуемым его дескриптор и наоборот. Свойство наследования не поддерживается для объектов, использование которых несколькими процессами нарушило бы изолированность памяти процесса от других процессов. Поэтому не могут наследоваться следующие дескрипторы: □ дескриптор виртуальной памяти, который возвращает любая из функций LocalAlloc, GlobalAlloc, HeapCreate ИЛИ HeapAlloc; □ дескриптор динамической библиотеки, который возвращает функция LoadLibrary.
68 Часть I. Управление потоками и процессами Однако для того чтобы дочерний процесс имел доступ к наследуемому объекту в родительском процессе, недостаточно просто сделать дескриптор этого объекта наследуемым. Кроме этого, нужно, во-первых, установить значение параметра MnheritHandles функции CreateProcess В TRUE И, ВО- вторых, передать сам дескриптор дочернему процессу, который создается функцией CreateProcess. Наследуемый дескриптор передается системой дочернему процессу неявно и поэтому он скрыт от программ, которые выполняются в дочернем процессе. То есть программа дочернего процесса должна явно знать этот дескриптор и передать его этой программе должна программа родительского процесса. Одним из способов передачи дескрипторов дочернему процессу является использование командной строки, которая позволяет передавать дескрипторы как параметры. Для пояснения сказанного приведем пример двух процессов, в которых используются наследуемые дескрипторы. В листинге 4.8 приведена программа дочернего процесса. Этот процесс получает дескриптор потока от родительского процесса и, по требованию пользователя, прекращает выполнение этого потока в родительском процессе. I Листинг 4.8. Завершение потока в родительском процессе, используя [ дескриптор потока, который передается через командную строку # inc lude <windows . h> #include <conio.h> int main(int argc, char *argv[]) { HANDLE hThread; char с; // преобразуем символьное представление дескриптора в число hThread = (HANDLE)atoi(argv[l]); // ждем команды о завершении потока while (true) { _cputs("Input 't' to terminate the thread: "); с = _getch () ; if (c == -f) { _Cputs("t\n"); break;
Глава 4. Процессы в Windows 69 } // завершаем поток TerminateThread(hThread, 0); // закрываем дескриптор потока CloseHandle(hThread); _cputs("Press any key to exit.\n"); _getch(); return 0; } В листинге 4.9 приведена программа родительского процесса, который создает дочерний процесс и передает ему через командную строку наследуемый дескриптор потока. Этот поток и должен быть завершен дочерним процессом, программа которого приведена в листинге 4.8. Листинг 4.9. Процесс, который передает наследуемый дескриптор потока [ дочернему процессу черъь командную строку #include <windows.h> #include <conio.h> volatile int count; void thread() { for (;;) { count++; SleepE00); _cprintf ("count = %d\n"/ count); } } int main() { char lpszComLine[80]; // для командной строки STARTUPINFO si;
70 Часть /. Управление потоками и процессами PROCESS_INFORMATION pi; SECURITY_ATTRIBUTES sa; HANDLE hThread; DWORD IDThread; _cputs("Press any key to start the count-thread.\n"); _getch(); // устанавливает атрибуты защиты потока sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.lpSecurityDescriptor = NULL; // защита по умолчанию sa.blnheritHandle = TRUE; // дескриптор потока наследуемый // запускаем поток-счетчик hThread = CreateThread(&sa, 0, (LPTHREAD_START_ROUTINE)thread, NULL, 0, &IDThread); if (hThread == NULL) return GetLastError(); // устанавливаем атрибуты нового процесса ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb=sizeof(STARTUPINFO); // формируем командную строку wsprintf(IpszComLine, "С:\\ConsoleProcess.exe %d", (int)hThread); // запускаем новый консольный процесс if (ICreateProcess( NULL, // имя процесса IpszComLine, // адрес командной строки NULL, // атрибуты защиты процесса по умолчанию NULL, // атрибуты защиты первичного потока по умолчанию TRUE, // наследуемые дескрипторы текущего процесса // наследуются новым процессом CREATE_NEW_CONSOLE, // новая консоль NULL, // используем среду окружения процесса предка NULL, // текущий диск и каталог, как и в процессе-предке &si, // вид главного окна - по умолчанию &pi // здесь будут дескрипторы и идентификаторы // нового процесса и его первичного потока
Глава 4. Процессы в Windows 71 ) ) { _cputs("The new process is not created.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } // закрываем дескрипторы нового процесса CloseHandle(pi.hProcess); CloseHandle(pi.hThread); // ждем закрытия потока-счетчика WaitForSingleObject(hThread, INFINITE); _cputs("Press any key to exit.\n"); _getch(); // закрываем дескриптор потока CloseHandle(hThread); return 0; } В программе из листинга 4.9 особенно нужно обратить внимание на два момента: значение параметра binheritHandie функции CreateProcess И использование структуры sa типа security_attributes, адрес которой является первым параметром функции CreateThread. Если значение параметра binheritHandie равно true, то наследуемые дескрипторы родительского процесса передаются дочернему процессу. Поле binheritHandie структуры sa имеет тип bool. Если значение этого поля установлено в true, to дескриптор создаваемого потока является наследуемым, в противном случае — ненаследуемым. Теперь рассмотрим следующую ситуацию. Предположим, что дескриптор созданного объекта является ненаследуемым, а нам необходимо сделать его наследуемым. Для решения этой проблемы в операционной системе Windows 2000 МОЖНО ИСПОЛЬЗОВатЬ фуНКЦИЮ SetHandlelnformation, КОТОрая используется для изменения свойств дескрипторов и имеет следующий прототип: BOOL SetHandlelnformation( HANDLE hObject, // дескриптор объекта DWORD dwMask, // флаги, которые изменяем
72 Часть I. Управление потоками и процессами DWORD dwFlags // новые значения флагов ); В случае успешного завершения эта функция возвращает ненулевое значение, в противном случае — false. ДЛЯ ИЛЛЮСТрациИ работы фуНКЦИИ SetHandlelnformation изменим программу, приведенную в листинге 4.9. Модифицированная профамма приведена в листинге 4.10. Листинг* 4,10, Изменение свойства наследования дескриптора # include <windows.h> #include <conio.h> volatile int count; void thread() { for (;;) { count++; SleepE00); _cprintf ("count = %d\n"/ count); } } int main() { // имя нового процесса с пробелом char lpszComLine[80]="С:\\ConsoleProcess.exe "; // для символьного представления дескриптора char lpszHandle[20]; STARTUPINFO si; PROCESS_INFORMATION pi; HANDLE hThread; DWORD IDThread; _cputs("Press any key to start the count-thread.\n");
Глава 4. Процессы в Windows 73 _cputs("After terminating the thread press any key to exit.\n"); _getch(); // запускаем поток-счетчик hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)thread, NULL, 0, &IDThread); if (hThread == NULL) return GetLastError(); // делаем дескриптор потока наследуемым if(!SetHandleInformation( hThread, // дескриптор потока HANDLE_FLAG_INHERIT, // изменяем наследование дескриптора HANDLE_FLAG_INHERIT)) // делаем дескриптор наследуемым { _cputs("The inheritance is not changed.\n"); _cputs("Press any char to finish.\n"); _getch(); return GetLastError(); } // устанавливаем атрибуты нового процесса ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb=sizeof(STARTUPINFO); // преобразуем дескриптор в символьную строку _itoa((int)hThread,IpszHandle,10); // создаем командную строку strcat(IpszComLine,IpszHandle); // запускаем новый консольный процесс if (!CreateProcess( NULL, // имя процесса IpszComLine, // адрес командной строки NULL, // атрибуты защиты процесса по умолчанию NULL, // атрибуты защиты первичного потока по умолчанию TRUE, // наследуемые дескрипторы текущего процесса // наследуются новым процессом CREATE_NEW_CONSOLE, // новая консоль NULL, // используем среду окружения процесса-предка NULL, // текущий диск и каталог, как и в процессе-предке
74 Часть I. Управление потоками и процессами &si, // вид главного окна - по умолчанию &pi // здесь будут дескрипторы и идентификаторы // нового процесса и его первичного потока ) ) { _cputs("The new process is not created.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } // закрываем дескрипторы нового процесса CloseHandle(pi.hProcess); CloseHandle(pi.hThread); _getch(); // закрываем дескриптор потока CloseHandle(hThread); return 0; } Для определения свойств дескриптора используется функция GetHandlelnformation, которая имеет следующий проТОТИП! BOOL GetHandlelnformation( HANDLE hObject, // дескриптор объекта LPDWORD lpdwFlags // свойства дескриптора ); Эта функция также в случае успешного завершения возвращает ненулевое значение. В противном случае возвращаемое значение равно false. ( Замечание ^ Отметим важную деталь, функции SetHandlelnformation и GetHandlelnf ormation правильно работают только в операционной системе Windows 2000. В операционной системе Windows 98 эти функции всегда возвращают значение false. Для решения рассматриваемой проблемы в операционной системе Windows 98 нужно использовать дублирование дескрипторов, которое мы рассмотрим в разд. 4.5.
Глава 4. Процессы в Windows 75 В завершение этого раздела разберем несколько подробнее, зачем нужно наследование объектов, тогда как любые процессы, включая дочерние, могут получить доступ к объекту по его имени. Проблема как раз и состоит в именовании объектов. Во-первых, затрачивается время на поиск имени созданного объекта. Но это не столь важно, важнее то, что, во-вторых, объекты должны иметь уникальные имена. Это требуется для того, чтобы не допустить ошибки при создании объекта по причине присутствия другого объекта с таким же именем. То есть нельзя допустить, чтобы совершенно разные приложения непреднамеренно создавали никак не связанные между собой объекты с одинаковыми именами. Кроме того, при неуникальном именовании объектов также возможны проблемы при одновременной работе двух экземпляров одного приложения. Эта проблема уже гораздо сложнее, и для ее решения используются специальные программы, которые могут генерировать уникальные имена. Такие имена обычно называются guid — глобальными универсальными идентификаторами. Поэтому, как видим, проще и быстрее создавать анонимные наследуемые объекты и передавать их дескрипторы дочерним процессам, чем заниматься уникальным именованием объектов. 4.5. Дублирование дескрипторов Дублирование дескрипторов необходимо для решения следующей задачи. Иногда при передаче дескриптора из одного процесса в другой необходимо изменить не только свойство наследования дескриптора, но и другие свойства этого дескриптора, которые управляют доступом к объекту. Для решения этой проблемы предназначена функция DupiicateHandie, которая имеет следующий прототип: BOOL DupiicateHandie( HANDLE hSourceProcessHandle, // дескриптор процесса источника HANDLE hSourceHandle, // исходный дескриптор HANDLE hTargetProcessHandle, // дескриптор процесса приемника LPHANDLE lpTargetHandle, // дубликат исходного дескриптора DWORD dwDesiredAccess, // флаги доступа к объекту BOOL blnheritHandle, // наследование дескриптора DWORD dwOptions // дополнительные необязательные флаги ); Если функция DupiicateHandie завершается успешно, то она возвращает ненулевое значение. В противном случае эта функция возвращает значение FALSE. Отметим назначение трех последних параметров В функции DupiicateHandie.
76 Часть I. Управление потоками и процессами Начнем с последнего параметра — dwOptions, в котором может быть установлена комбинация флагов duplicate_close_source и duplicate_same_access. Если установлен флаг duplicate_close_source, to при любом своем завершении функция DupiicateHandie закрывает исходный дескриптор. Если установлен флаг duplicate_same_access, to режимы доступа к объекту через дублированный дескриптор совпадают с режимами доступа к объекту через исходный дескриптор. Совместное использование этих флагов обеспечивает выполнение двух указанных действий. Теперь перейдем к параметру dwDesiredAccess, который определяет возможные режимы доступа к объекту через дубликат исходного дескриптора, используя определенную комбинацию флагов. Значения этих флагов отличаются для объектов разных типов и будут описаны далее, в процессе работы с объектами. Если доступ к объекту не изменяется, что определяется значением последнего параметра dwOptions, то система игнорирует значение параметра dwDesiredAccess. Параметр blnheritHandle функции DupiicateHandie устанавливает СВОЙСТВО наследования нового дескриптора. Если значение этого параметра равно true, то создаваемый дубликат исходного дескриптора является наследуемым, в случае false — ненаследуемым. В листинге 4.11 приведен пример программы, которая использует функцию DupiicateHandie для разрешения дочернему процессу завершить поток в родительском процессе. Эта программа является другим решением задачи, решаемой программой, приведенной в листинге 4.10. | Листинг 4.11. Создание наследуемого дескриптора функцией DupiicateHandie #include <windows.h> #include <conio.h> volatile int count; void thread() { for (;;) { count++; SleepE00); _cprintf ("count = %d\n"/ count);
Глава 4. Процессы в Windows 77 int main() { // имя нового процесса с пробелом char lpszComLine[80]="C:WConsoleProcess.exe "; // для символьного представления дескриптора char lps zHandle[20]; STARTUPINFO si; PROCESS_INFORMATION pi; HANDLE hThread, hlnheritThread; DWORD IDThread; _cputs("Press any key to start the count-thread.\n"); _cputs("After terminating the thread press any key to exit.\n"); _getch(); // запускаем поток-счетчик hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)thread, NULL, 0, &IDThread); if (hThread == NULL) return GetLastError(); // создаем наследуемый дубликат дескриптора потока if(IDuplicateHandle( GetCurrentProcess(), // дескриптор текущего процесса hThread, // исходный дескриптор потока GetCurrentProcess(), // дескриптор текущего процесса &hInheritThread, // новый дескриптор потока 0, // этот параметр игнорируется TRUE, // новый дескриптор наследуемый DUPLICATE_SAME_ACCESS)) // доступ не изменяем { _cputs("The handle is not duplicated.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError();
78 Часть I. Управление потоками и процессами // устанавливаем атрибуты нового процесса ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb=sizeof(STARTUPINFO); // преобразуем наследуемый дескриптор в символьную строку _itoa((int)hInheritThread/IpszHandle,10); // создаем командную строку strcat(lpszComLine,IpszHandle); // запускаем новый консольный процесс if (ICreateProcess( NULL, // имя процесса IpszComLine, // адрес командной строки NULL, // атрибуты защиты процесса по умолчанию NULL, // атрибуты защиты первичного потока по умолчанию TRUE, // наследуемые дескрипторы текущего процесса // наследуются новым процессом CREATE_NEW_CONSOLE, // новая консоль NULL, // используем среду окружения процесса-предка NULL, // текущий диск и каталог, как и в процессе-предке &si, // вид главного окна - по умолчанию &pi // здесь будут дескрипторы и идентификаторы // нового процесса и его первичного потока ) ) { _cputs("The new process is not created.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } // закрываем дескрипторы нового процесса CloseHandle(pi.hProcess); CloseHandle(pi.hThread); _getch(); // закрываем дескриптор потока CloseHandle(hThread); return 0;
Глава 4. Процессы в Windows 79 Теперь приведем, в листинге 4.12, пример программы, которая дублирует дескриптор, но при этом изменяет доступ к нему. Разрешим дочернему процессу прекращать выполнение потока в родительском процессе. В этом случае остальные функции над потоками, такие как, например, suspendThread и ResumeThread, будут недоступны для выполнения в дочернем процессе. Обратите внимание на изменение значений параметров функции DuplicateHandle. #include <windows.h> #include <conio.h> volatile int count; void thread() { for (;;) { count++; SleepE00); _cprintf ("count = %d\n", count); } } int main() { // имя нового процесса с пробелом char lpszComLine[80]="С:\\ConsoleProcess.exe "; // для символьного представления дескриптора char lps zHandle[20]; STARTUPINFO si; PROCESS_INFORMATION pi; HANDLE hThread, hlnheritThread; DWORD IDThread; _cputs("Press any key to start the count-thread.\n");
80 Часть I. Управление потоками и процессами _cputs ("After terminating the thread press any key to exit.\n"); _getch(); // запускаем поток-счетчик hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) thread, NULL, 0, &IDThread); if (hThread == NULL) return GetLastError(); // создаем наследуемый дубликат дескриптора потока if(!DuplicateHandle( GetCurrentProcess(), // дескриптор текущего процесса hThread, // исходный дескриптор потока GetCurrentProcess(), // дескриптор текущего процесса &hInheritThread, // новый дескриптор потока THREAD_TERMINATE, // только завершение потока TRUE, // новый дескриптор наследуемый 0)) . // не используем { _cputs("The handle is not duplicated.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } // устанавливаем атрибуты нового процесса ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb=sizeof(STARTUPINFO); // преобразуем наследуемый дескриптор в символьную строку _itoa((int)hlnheritThread,IpszHandle,10) ; // создаем командную строку strcat(IpszComLine,IpszHandle); // запускаем новый консольный процесс if (!CreateProcess( NULL, // имя процесса IpszComLine, // адрес командной строки NULL, // атрибуты защиты процесса по умолчанию NULL, // атрибуты защиты первичного потока по умолчанию TRUE, // наследуемые дескрипторы текущего процесса
Глава 4. Процессы в Windows 81 // наследуются новым процессом CREATE_NEW_CONSOLE, // новая консоль NULL, // используем среду окружения процесса-предка NULL, // текущий диск и каталог, как и в процессе-предке &si, // вид главного окна - по умолчанию &pi // здесь будут дескрипторы и идентификаторы // нового процесса и его первичного потока ) ) { _cputs("The new process is not created.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } // закрываем дескрипторы нового процесса CloseHandle(pi.hProcess); CloseHandle(pi.hThread); _getch(); // закрываем дескриптор потока CloseHandle(hThread); return 0; 4.6. Псевдодескрипторы процессов Иногда процессу требуется знать свой дескриптор, чтобы изменить какие-то свои характеристики. Например, процесс может изменить свой приоритет. Для этих целей в Win32 API существует функция GetcurrentProcess, которая имеет следующий прототип: HANLDE GetCurrentProcess(VOID); и возвращает псевдодескриптор текущего процесса. Псевдодескриптор текущего процесса отличается от настоящего дескриптора процесса тем, что он может использоваться только текущим процессом и не может наследоваться другими процессами. Псевдодескриптор процесса не нужно закрывать после его использования. Из псевдодескриптора процесса можно получить на-
82 Часть I. Управление потоками и процессами стоящий дескриптор процесса: для этого псевдодексриптор нужно продублировать, ВЫЗВав ФУНКЦИЮ DuplicateHandle. В листинге 4.13 приведен пример программы, которая поучает псевдодескриптор процесса посредством вызова функции GetcurrentProcess, а затем выводит полученный псевдодескриптор на консоль. | Листинг 4.13. Получение псевдодескриптора процесса | #include <windows.h> #include <iostream.h> int main() { HANDLE hProcess; // получаем псевдодескриптор текущего процесса hProcess = GetcurrentProcess(); // выводим псевдодескриптор на консоль cout « hProcess « endl; cin.get(); return 0; } 4.7. Обслуживание потоков Операционные системы Windows распределяют процессорное время между потоками в соответствии с их приоритетами. По истечении кванта времени исполнение текущего потока прерывается, его контекст запоминается и процессорное время передается потоку с высшим приоритетом. Часто говорят, что поток с высшим приоритетом вытесняет поток с низшим приоритетом. Такое обслуживание потоков в Windows называется вытесняющая многозадачность (preempting multitasking). Величина кванта времени, выделяемого потоку, зависит от типа операционной системы Windows, типа процессора и приблизительно равна 20 мс. Приоритеты потоков в Windows определяются относительно приоритета процесса, в контексте которого они исполняются, и изменяются от 0 (низший приоритет) до 31 (высший приоритет). Приоритет процессов устанавливается При ИХ создании функцией CreateProcess, ИСПОЛЬЗуя параметр
Глава 4. Процессы в Windows 83 dwCreationFiags. Для установки приоритета процесса в этом параметре нужно установить один из следующих флагов. □ idle_priority_class — класс фоновых процессов; □ below_normal_priority_class — класс процессов ниже нормальных; □ normal_priority_class — класс нормальных процессов; □ above_normal_priority_class — класс процессов выше нормальных; □ high_priority_class — класс высокоприоритетных процессов; □ real_time_priority_class — класс процессов реального времени. Отметим, что флаги below_normal_priority_class и above_normal_ priority_class используются только в операционных системах, начиная с версии Windows 2000. Рассмотрим правила, используемые для назначения приоритетов процессам в Windows. Предполагается, что операционная система Windows различает четыре типа процессов в соответствии с их приоритетами: фоновые процессы, процессы с нормальным приоритетом, процессы с высоким приоритетом и процессы реального времени. Рассмотрим подробнее каждый из этих типов процессов. □ Фоновые процессы выполняют свою работу, когда нет активных пользовательских процессов. Обычно эти процессы следят за состоянием системы. Приоритет таких процессов устанавливается флагом idle_priority_class. □ Процессы с нормальным приоритетом — это обычные пользовательские процессы. Приоритет таких процессов устанавливается флагом normal_priority_class. Этот приоритет также назначается пользовательским процессам по умолчанию. В Windows 2000 приоритет обычных пользовательских процессов может также устанавливаться флагами below_normal_priority_class или above_normal_priority_class, которые соответственно немного повышают или понижают приоритет пользовательского процесса. □ Процессы с высоким приоритетом это такие пользовательские процессы, от которых требуется более быстрая реакция на некоторые события, чем от обычных пользовательских процессов. Приоритет таких процессов устанавливается флагом high_priority_class. Эти процессы должны содержать небольшой программный код и выполняться очень быстро, чтобы не замедлять работу системы. Обычно такие приоритеты имеют другие системы, работающие на платформе операционных систем Windows. □ К последнему типу процессов относятся процессы реального времени. Приоритет таких процессов устанавливается флагом real_time_priority_class. Работа таких процессов обычно происходит в масштабе реального времени и связана с реакцией на внешние события. Эти процессы должны работать непосредственно с аппаратурой компьютера.
84 Часть I. Управление потоками и процессами Приоритет процесса можно изменить при помощи функции setPriorityCiass, которая имеет следующий прототип: BOOL SetPriorityCiass( HANDLE hProcess, // дескриптор процесса DWORD dwPriorityClass // приоритет ); При успешном завершении функция SetPriorityCiass возвращает ненулевое значение, в противном случае значение — false. Параметр dwPriorityClass этой функции должен быть равен одному из флагов, которые приведены выше. Узнать приоритет процесса можно посредством вызова функции GetPriorityCiass, которая имеет следующий прототип: DWORD GetPriorityCiass( HANDLE hProcess // дескриптор процесса ); При успешном завершении эта функция возвращает флаг установленного приоритета процесса, в противном случае возвращаемое значение равно нулю. В листинге 4.14 приведена программа, которая демонстрирует работу функций SetPriorityCiass И GetPriorityCiass. | Листинг 4,14. Пример работы функций SetPriorityCiass и GetPriorityCiass \ #include <windows.h> #include <conio.h> int main() { HANDLE hProcess; DWORD dwPr i or i ty; // получаем псевдодескриптор текущего потока hProcess = GetCurrentProcess(); // узнаем приоритет текущего процесса dwPriority = GetPriorityCiass(hProcess); _cprintf("The priority of the process = %d.\n", dwPriority); // устанавливаем фоновый приоритет текущего процесса
Глава 4. Процессы в Windows 85 if (!SetPriorityClass(hProcess, IDLE_PRIORITY_CLASS)) { _cputs("Set priority class failed.\n"); _cputs("Press any key to exit.\n"); _getch(); return GetLastError(); } dwPriority = GetPriorityClass(hProcess); _cprintf("The priority of the process = %d.\n", dwPriority); // устанавливаем высокий приоритет текущего процесса if (!SetPriorityClass(hProcess, HIGH_PRIORITY_CLASS)) { _cputs("Set priority class failed.\n"); _cputs("Press any key to exit.\n"); _getch(); return GetLastError(); } dwPriority = GetPriorityClass(hProcess); _cprintf("The priority of the process = %d.\n", dwPriority); _cputs("Press any key to exit.\n"); _getch(); return 0; } Отметим в связи с этой программой, что числовые значения флагов не соответствуют числовым значениям приоритетов процессов. Так, например, числовое значение флага idle_priority_class больше чем числовое значение флага normal_priority_class. Но система считает, что приоритет нормального процесса выше, чем приоритет фонового процесса. Теперь перейдем к приоритетам потоков, задание которых в Windows довольно запутанное. Приоритет потока, который учитывается системой при выделении потокам процессорного времени, называется базовым (base) или основным приоритетом потока. Всего существует 32 базовых приоритета — от 0 до 31. Для каждого базового приоритета существует очередь потоков. При диспетчеризации потоков квант процессорного времени выделяется потоку, который стоит первым в очереди с наивысшим базовым приоритетом. Базовый приоритет потока определяется как сумма приоритета процесса
86 Часть I. Управление потоками и процессами и уровня приоритета потока, который может принимать одно из следующих значений, которые разобьем на две группы. Первая состоит из: □ THREAD_PRIORITY_LOWEST — НИЗШИЙ Приоритет; □ thread_priority_below_normal — приоритет ниже нормального; □ thread_priority_normal — нормальный приоритет; □ thread_priority_above_normal — приоритет выше нормального; □ THREAD_PRIORITY_HIGHEST — ВЫСШИЙ Приоритет. Вторая: □ thread_priority_idle — приоритет фонового потока; □ thread_priority_time_critical — приоритет потока реального времени. Значения уровня приоритета потока из первой группы в сумме с приоритетом процесса, в контексте которого этот поток выполняется, уменьшают, оставляют неизменным или увеличивают значение базового приоритета потока соответственно на величину —2, —1, 0, 1, 2. Уровень приоритета потока thread_priority_idle устанавливает базовый приоритет потока равным 16, если приоритет процесса, в контексте которого выполняется поток, равен real_time_priority_class, и 1 — в остальных случаях. Уровень приоритета потока thread_priority_time_critical устанавливает базовый приоритет потока равным 31, если приоритет процесса, в контексте которого выполняется поток, равен real_time_priority_class, и 15 — в остальных случаях. В табл. 4.1 приведены базовые приоритеты потоков в зависимости от приоритета процесса и уровня приоритета потока. Таблица 4.1. Базовые приоритеты потоков Real High Above Normal Below Idle time normal normal Time critical Highest Above normal Normal Below normal Lowest Idle 31 26 25 24 23 22 16 15 15 14 13 12 11 1 15 12 11 10 9 8 1 15 10 9 8 7 6 1 15 8 7 6 5 4 1 15 6 5 4 3 2 1 В этой таблице по столбцам указаны приоритеты классов процессов, а по строкам — уровни приоритетов потоков.
Глава 4. Процессы в Windows 87 При создании потока его базовый приоритет устанавливается как сумма приоритета процесса, в контексте которого этот поток выполняется, и уровня приоритета потока thread_priority_normal. Для изменения приоритета потока используется функция setThreadPriority, которая имеет следующий прототип: BOOL SetThreadPriority( HANDLE hThread, // дескриптор потока Int nPriority // уровень приоритета потока ); При удачном завершении функция SetThreadPriority возвращает ненуле- вое значение, в противном случае — false. Параметр nPriority этой функции должен быть равен одному из перечисленных уровней приоритетов. Узнать уровень приоритета потока можно посредством вызова функции GetThreadPriority, которая имеет следующий прототип: DWORD GetThreadPriority( HANDLE hThread // дескриптор потока ); При успешном завершении эта функция возвращает одно из значений УРОВНЯ приоритета, В ПРОТИВНОМ случае фуНКЦИЯ GetThreadPriority ВОЗ- вращает значение thread_priority_error_return. В листинге 4.15 приведена программа, которая демонстрирует работу функций SetThreadPriority И GetThreadPriority. | Листинг 4,15. Получение и изменение приоритетов потоков #include <windows.h> #include <conio.h> int main () { HANDLE hThread; DWORD dwPriority; // получаем псевдодескриптор текущего потока hThread = GetCurrentThread(); // узнаем уровень приоритета текущего процесса dwPriority = GetThreadPriority(hThread); _cprintf("The priority level of the thread = %d.\n", dwPriority);
88 Часть I. Управление потоками и процессами // понижаем приоритет текущего потока if (!SetThreadPriority(hThread/ THREAD_PRIORITY_LOWEST)) { _cputs("Set thread priority failed.\n"); _cputs("Press any key to exit.Xn"); _getch(); return GetLastError(); } // узнаем уровень приоритет текущего потока dwPriority = GetThreadPriority(hThread); _cprintf("The priority level of the thread = %d.\n"/ dwPriority); // повышаем приоритет текущего потока if (!SetThreadPriority(hThread, THREAD_PRIORITY_HIGHEST)) { _cputs("Set thread priority failed.\n"),- _cputs("Press any key to exit.Xn"); _getch(); return GetLastError(); } // узнаем уровень приоритета текущего потока dwPriority = GetThreadPriority(hThread); _cprintf("The priority level of the thread = %d.\n'\ dwPriority); _cputs("Press any key to exit.Xn"); _getch(); return 0; } 4.8. Динамическое изменение приоритетов потоков Базовый приоритет потока может динамически изменяться системой, если этот приоритет находится в пределах между уровнями от 0 до 15. При получении потоком сообщения или при его переходе в состояние готовности система повышает базовый приоритет этого потока на 2. В процессе выполнения базовый приоритет такого потока понижается на 1, с каждым отрабо-
Глава 4. Процессы в Windows 89 танным квантом времени, но никогда не опускается ниже исходного базового приоритета. В операционной системе Windows 2000 возможно программное управление режимом динамического изменения базовых приоритетов потоков. Отмена или возобновление режима динамического изменения базового приоритета всех потоков, исполняемых в контексте процесса, выполняется при помощи функции setProcessPriorityBoost, которая имеет следующий прототип: BOOL SetProcessPriorityBoost( HANDLE hProcess, // дескриптор процесса BOOL DisablePriorityBoost // состояние повышения приоритета ); Если функция SetProcessPriorityBoost завершается успешно, то она возвращает ненулевое значение, в противном случае возвращаемое значение равно false. Значение параметра DisablePriorityBoost устанавливает состояние режима динамического повышения базовых приоритетов потоков. Если это значение равно true, то режим динамического повышения базовых приоритетов потоков, выполняемых в контексте процесса с дескриптором hProcess, запрещается. Если же значение этого параметра равно false, to, наоборот, режим динамического повышения базовых приоритетов этих потоков разрешается. Узнать, разрешен ли режим динамического повышения базовых приоритетов ПОТОКОВ, МОЖНО посредством ВЫЗОВа функции GetProcessPriorityBoost, которая имеет следующий прототип: BOOL GetProcessPriorityBoost( HANDLE hProcess, II дескриптор процесса PBOOL pDisablePriorityBoost // состояние повышения приоритета ); Если функция GetProcessPriorityBoost завершается успешно, то она возвращает ненулевое значение, в противном случае возвращаемое значение раВНО FALSE. Значение булевой переменной, на которую указывает параметр pDisablePriorityBoost, определяет состояние режима динамического повышения базовых приоритетов потоков. Если это значение равно true, to режим динамического повышения базовых приоритетов потоков, выполняемых в контексте процесса с дескриптором hProcess, запрещен. Если же значение этого параметра равно false, to, наоборот, режим динамического повышения базовых приоритетов этих потоков разрешен.
90 Часть I. Управление потоками и процессами Для отмены или возобновления режима динамического изменения базового приоритета только одного потока используется функция SetThreadPriorityBoost, которая имеет следующий прототип: BOOL SetThreadPriorityBoost( HANDLE hThread, // дескриптор потока BOOL DisablePriorityBoost // состояние повышения приоритета ); Эта фуНКЦИЯ работает аналогично функции SetProcessPriorityBoost, НО ТОЛЬКО ДЛЯ ОДНОГО ПОТОКа С ДескрИПТОрОМ hThread. Чтобы определить, разрешен ли режим динамического повышения базового приоритета для какого-то конкретного потока, используется функция GetThreadPriorityBoost, которая имеет следующий прототип: BOOL GetThreadPriorityBoost( HANDLE hThread, // дескриптор потока PBOOL pDisablePriorityBoost // состояние повышения приоритета ); Эта фуНКЦИЯ работает так Же, как И фуНКЦИЯ GetProcessPriorityBoost, НО ТОЛЬКО ДЛЯ ОДНОГО ПОТОКа С ДескрИПТОрОМ hThread. В листинге 4.16 приведена программа, которая демонстрирует работу функций SetProcessPriorityBoost, GetProcessPriorityBoost, SetThreadPriorityBoost и GetThreadPriorityBoost. Еще раз отметим, что эти функции работают корректно только в операционной системе Windows 2000. В операционной системе Windows 98 эти функции всегда возвращают значение false. Шпстшг 4.16, Управление динамическим изменением приоритетов потоков # include <windows.h> #include <conio.h> int main() { HANDLE hProcess, hThread; BOOL bPriorityBoost; // получаем псевдодескриптор текущего процесса hProcess = GetCurrentProcess(); // узнаем режим динамического повышения приоритетов для процесса if (!GetProcessPriorityBoost(hProcess, &bPriorityBoost))
Глава 4. Процессы в Windows 91 _cputs("Get process priority boost failed.\n"); _cputs("Press any key to exit.\n"); _getch(); return GetLastError(); } _cprintf("The process priority boost = %d.\n"/ bPriorityBoost); // выключаем режим динамического повышения приоритетов для процесса if (ISetProcessPriorityBoost(hProcess, TRUE)) { _cputs("Set process priority boost failed.\n"); _cputs("Press any key to exit.\n"); _getch(); return GetLastError(); } // получаем псевдодескриптор текущего потока hThread = GetCurrentThread() ; // узнаем режим динамического повышения приоритетов для потока if (IGetThreadPriorityBoost(hThread, &bPriorityBoost)) { _cputs("Get process priority boost failed.\n"); _cputs("Press any key to exit.Xn"); _getch(); return GetLastError(); } _cprintf("The thread priority boost = %d.\n"/ bPriorityBoost); // включаем режим динамического повышения приоритетов для потока if (ISetThreadPriorityBoost(hThread, FALSE)) { _cputs("Set process priority boost failed.\n"); _cputs("Press any key to exit.\n"); _getch(); return GetLastError(); } _cputs("Press any key to exit.\n"); _getch(); return 0;
Часть II Синхронизация потоков и процессов Глава 5. Синхронизация Глава 6. Синхронизация потоков в Windows Глава 7. Взаимоисключающий доступ к переменным Глава 8. Тупики
Глава 5 Синхронизация 5.1. Непрерывные действия и команды Действием называется изменение контекста потока или, другими словами, действием можно назвать любую последовательность команд, которая изменяет контекст потока. Под контекстом действия понимается только та часть контекста потока, которая используется этим действием. В простейшем случае можно считать, что контекст действия определяется только переменными, которые используются этим действием. Действие называется непрерывным, если оно удовлетворяет следующим двум требованиям: □ не прерывается во время своего исполнения; □ контекст действия изменяется только самим действием. Если непрерывное действие представляется одной командой микропроцессора, то оно называется непрерывной инструкцией или командой. В англоязычной литературе для обозначения непрерывного действия используется термин atomic action. Поэтому в русскоязычной литературе можно встретить также термины атомарное действие и неделимое действие, используемые для обозначения непрерывных действий. Более подробное обсуждение этого вопроса можно найти в книге Грегори Р. Эндрюса "Основы многопоточного, параллельного и распределенного программирования". Теперь поговорим подробнее о тех требованиях, которым должны удовлетворять непрерывные действия. Сначала рассмотрим первое требование, которое касается непрерывности действия. Действие может быть прервано только сигналом прерывания, который устанавливает соответствующий флаг микропроцессора. Этот сигнал может генерироваться как внешним устройством, требующим обслуживания, так и самим микропроцессором для обработки исключительных ситуаций, возникающих при выполнении программы. Будем считать, что обработка исключительных ситуаций неизбежна для нормального продолжения
96 Часть II. Синхронизация потоков и процессов работы программы или ее завершения. Поэтому для того, чтобы обеспечить непрерывность действия, необходимо запретить обработку сигнала прерывания от внешних устройств во время выполнения этого действия. Однако отметим, что такой подход работает только в однопроцессорных системах, т. к. в мультипроцессорных системах действия могут выполняться параллельно разными процессорами. Причем возможна такая ситуация, в которой контексты параллельных действий пересекаются. В этом случае даже запрещение обработки прерываний на каждом микропроцессоре не обеспечивает непрерывности действий, т. к. может быть нарушено второе условие непрерывности действия. То есть, собственно говоря, второе требование к непрерывности действия и обеспечивает непрерывность действия на мультипроцессорных системах или, другими словами, запрещает действию, исполняемому одним процессором, изменять контекст действия, исполняемого другим процессором. 5.2. Определение синхронизации Если рассматривать параллельные процессы абстрактно, то синхронизация процессов — это есть достижение некоторого фиксированного соотношения (порядка) между сигналами, которыми обмениваются эти процессы. В программировании рассматриваются параллельные процессы, которые являются программами, исполняемыми процессором. Поэтому, чтобы избежать технических подробностей, связанных с обменом сигналами между процессами, упростим задачу. То есть будем рассматривать не параллельные процессы, а параллельные потоки. В этом случае обмен сигналами между потоками может происходить только через глобальные переменные. Отсюда следует, что установить некоторый порядок выполнения инструкций потоков можно только посредством проверки этими потоками значений глобальных переменных. Для формализации этого подхода определим условные непрерывные действия путем введения оператора await, который имеет следующий синтаксис: await(логическое условие) действие; Будем считать, что этот оператор работает следующим образом. Он ждет, пока логическое условие не примет значение "истина", и как только это случилось, то выполняется действие. Причем ожидание истинности значения логического условия и выполнение действия происходят непрерывным образом. То есть оператор await является непрерывным. Теперь можно дать определение синхронизации, которое является более естественным для практики программирования. Под синхронизацией потоков понимается исполнение этими потоками условных непрерывных действий. Рассмотрим частные случаи синхронизации, которые наиболее часто встречаются на практике.
Глава 5. Синхронизация 97 Если оператор await имеет следующий вид: await(логическое условие); то он просто ждет оповещения о выполнении некоторого логического условия. Этот случай называется условной синхронизацией, а само логическое условие также называется событием. В этом случае часто говорят, что оператор await ждет наступления некоторого события. Если оператор await имеет вид: await(TRUE) действие; то происходит безусловное выполнение непрерывного действия. Этот случай называется взаимным исключением, а программный код, исполняемый внутри непрерывного действия, называется критической секцией. 5.3. Программная реализация синхронизации Сначала кратко поговорим о том, как же оператор await может быть реализован на практике. Фактически на однопроцессорной системе исполнение оператора await аналогично исполнению программы обработки прерывания. В этом случае сигнал прерывания можно рассматривать как оповещение о том, что логическое условие приняло значение "истина", а действие является программой обработки прерывания, которая своей первой командой запрещает прерывания. В мультипроцессорных системах нужно также обеспечить, чтобы контексты программ обработки прерываний не пересекались. Теперь перейдем к частным случаям синхронизации, которые могут быть реализованы и без использования механизма обработки прерываний. Сначала рассмотрим условную синхронизацию, а затем проблему взаимного исключения. Задача условной синхронизации ДЛЯ Постановки ЭТОЙ задачи рассмотрим Два ПОТОКа thread_l И thread_2, которые работают следующим образом. Поток thread_i выполняет некоторые действия, а затем ждет наступления события event, после которого выполняет другие действия. В свою очередь поток thread_2 также выполняет некоторые действия, а после их завершения оповещает поток thread_i о наступлении события event. Затем поток thread_2 выполняет оставшиеся действия. Такая синхронизация работы потоков и называется задачей условной синхронизации. Схематически программный код потоков thread_i и thread_2, который решает поставленную задачу условной синхронизации, приведен ниже. bool event = false; // событие event void thread_l() // поток thread_l 4 Зак. 514
98 Часть II. Синхронизация потоков и процессов { actions_before_event(); // действия до наступления события while(!event); // ждем, пока событие не произошло actions_after_event(); // действия после наступления события } void thread_2() // поток thread_2 { some_actions(); // действия, о которых оповещает событие event = true; // отмечаем о наступлении события other_actions(); // действия, происходящие после события } Рассматривая этот код, во-первых, заметим следующее — фактически наступление некоторого события равносильно выполнению некоторого действия. Поэтому событие часто и определяют как действие. В потоке thread_i это действие обозначается функцией some_actions (). Во-вторых, как видно из приведенного программного кода, для решения задачи условной синхронизации для двух потоков достаточно определить глобальную булеву переменную event, начальное значение которой установить в false. Затем в потоке thread_2 установить значение этой переменной в true после наступления события event. Тогда поток thread_i ждет наступления события event посредством оператора while(!event), который циклически проверяет значение булевой переменной event до тех пор, пока эта переменная не примет значение true. Очевидно, что подобным образом задача условной синхронизации может быть решена и для произвольного количества потоков, ждущих наступления события event. Задача взаимного исключения Теперь рассмотрим задачу взаимного исключения. Чтобы упростить рассуждения, эта задача будет сформулирована только для двух параллельных потоков. Сначала предположим, что два параллельных потока работают с одним и тем же ресурсом, который в этом случае называется разделяемым или совместно используемым ресурсом. Далее считаем, что в каждом потоке программный код, который осуществляет доступ к этому ресурсу, заключен в свою критическую секцию. Тогда задача взаимного исключения для двух потоков может быть сформулирована следующим образом: обеспечить двум потокам взаимоисключающий доступ к некоторому совместно используемому ресурсу. Причем решение этой задачи должно удовлетворять следующим требованиям: □ требование безопасности — в любой момент времени в своей критической секции может находиться только один поток;
Глава 5. Синхронизация 99 П требование поступательности — потоки не могут блокировать работу друг друга, ожидая разрешения на вход в критическую секцию; □ требование справедливости — каждый поток получает доступ в критическую секцию за ограниченное время. Ниже приведено простейшее из известных решений задачи взаимного исключения для двух потоков, которое было опубликовано Гэри Л. Петерсо- ном в 1981 году. bool xl = false; bool x2 = false; int q; // номер потока, которому предоставляется очередь входа в // критическую секцию void thread_l() // поток thread_l { while(true) { non_critical_section_l(); // код вне критической секции xl = true; // поток thread_l хочет войти в критическую секцию q = 2; // предоставить очередь потоку thread_2 while(х2 && q == 2); // ждем, пока в критической секции находится // поток thread_2 critical_section_l(); // входим в критическую секцию xl = false; // поток thread_l находится вне критической секции } } void thread_2() // поток thread_2 { while(true) { non_critical_section_2(); // код вне критической секции х2 = true; // поток thread_2 хочет войти в критическую секцию q = 1; // предоставить очередь потоку thread_l while(xl && q == 1); // ждем, пока в критической секции находится // поток thread_l critical_section_2(); // входим в критическую секцию х2 = false; // поток thread_2 находится вне критической секции
100 Часть II. Синхронизация потоков и процессов Покажем, что это решение удовлетворяет трем вышеперечисленным требованиям, предъявляемым к решению задачи взаимного исключения. Сначала рассмотрим требование безопасности. Поток thread_i входит в критическую секцию critcai_section_i() только в том случае, если не выполняется условие (х2 ScSc q == 2). Это эквивалентно тому, что выполняется условие ! (х2 && q == 2), которое в свою очередь можно привести к условию (!х2 || q == 1), используя логический закон отрицания конъюнкции. Кроме того, заметим, что если поток thread_i находится в критической секции, то выполняется условие (xi == true). Теперь введем предикат: Ql = xl ScSc ( !х2 || q == 1) КОТОРЫЙ ЯВЛЯеТСЯ инвариантом Критической сеКЦИИ critical_section_l() . То есть если поток thread_i находится внутри своей критической секции, то предикат qi принимает значение "истина". Аналогично определим предикат Q2 = х2 ScSc ( !xl | | q == 2) КОТОРЫЙ ЯВЛЯеТСЯ Инвариантом Критической СеКЦИИ critical_section_2 () . Теперь найдем истинностное значение предиката (qi && Q2). Используя законы логики высказываний, получим: Ql ScSc Q2 = = (xl ScSc ( !х2 | | q == 1) ) ScSc (x2 ScSc ( !xl | | q == 2) ) = = xl ScSc x2 ScSc (!xl | | q == 2) && ( !x2 | | q == 1) = = xl ScSc x2 ScSc ( ( !xl ScSc !x2) | | ( !xl && q == 1) | | ( !x2 ScSc q == 2) | | (q ==1 ScSc q == 2) ) = = xl ScSc x2 ScSc ( ( !xl ScSc !x2) | | ( !xl && q == 1) | | ( !x2 ScSc q == 2) ) = = (xl ScSc !xl ScSc x2 ScSc !x2) | | ( (xl ScSc x2 ScSc !xl && q == 1) | | (xl ScSc x2 ScSc !x2 ScSc q == 2) = = false Символ = обозначает равносильность или, другими словами, логическую эквивалентность выражений. В результате получили, что предикат (qi && Q2) имеет тождественно ложное истинностное значение. Отсюда следует, что потоки thread_i и thread_2 не могут одновременно находиться в своих критических секциях. Таким образом, доказано, что решение Петерсона удовлетворяет требованию безопасности. Теперь рассмотрим требование поступательности. Поток thread_i может быть заблокирован, только если выполняется условие (х2 && q == 2). Аналогично, поток thread_2 может заблокироваться только при выполнении
Глава 5. Синхронизация 101 УСЛОВИЯ (xl && q == 1). РаССМОТрИМ Предикат (х2 && q == 2) && (xl ScSc q == 1) и найдем его истинностное значение. Получим: (х2 ScSc q == 2) && (xl ScSc q == 1) = = xl && x2 && q == 1 && q == 2 = false т. к. переменная q не может принимать одновременно значения 1 и 2. То есть этот предикат имеет тождественно ложное значение. Откуда следует, что потоки thread_i и thread_2 не могут быть заблокированы одновременно. Таким образом, доказано, что решение Петерсона удовлетворяет требованию поступательности. Наконец покажем, что решение Петерсона удовлетворяет требованию справедливости. Для этого предположим противное, т. е. поток thread_i заблокирован. Тогда должно выполняться условие (х2 && q == 2). Из истинности этого условия следует, что выполняются условия (х2 == true) и (q == 2). Но из выполнения условия поступательности следует, что поток thread_2 не может быть заблокирован одновременно с потоком thread_i. Следовательно, должны также выполняться условия (х2 == false) ИЛИ (q == 1).Полу- чили противоречие с нашим предположением. Поэтому наше предположение неверно и поток thread_i не может быть заблокирован. Таким образом, доказано, что алгоритм Петерсона решает задачу взаимного исключения для двух потоков. Решение задачи взаимного исключения для произвольного количества потоков является более сложным. Подводя итог изложенного материала, можно сделать следующий вывод: синхронизация потоков может осуществляться чисто программными средствами, используя глобальные переменные. 5.4. Аппаратная реализация синхронизации Программная реализация синхронизации возможна только на однопроцессорных системах. Как уже было сказано выше, для синхронизации параллельных потоков, каждый из которых выполняется отдельным процессором, необходимо обеспечить взаимоисключающий доступ этих потоков к общим переменным. Для этого в каждом процессоре существуют специальные инструкции, которые изменяют содержимое памяти и не прерываются во время своего исполнения. При исполнении такой инструкции процессор "запирает" шину передачи данных, блокируя доступ к памяти другим процессорам. Мы рассмотрим только две из таких инструкций. Инструкция "проверить и установить" В наборе инструкций процессора Motorola 68000 существует инструкция tas (test and set), которая имеет следующую реализацию: int tas(int& target)
102 Часть II. Синхронизация потоков и процессов int temp ; temp = target; target = 1; return temp; } Выполняется она непрерывным образом, т. е. выполнение этой инструкции не прерывается. Используя эту инструкцию, можно реализовать взаимоисключающий доступ параллельных потоков к критической секции по следующему алгоритму: int lock =0; // переменная, которая запирает вход в критическую // секцию void thread() { while(true) { while(tas(lock)); // ждать, пока замок закрыт critical_section(); // критическая секция lock =0; // открыть замок non_critical_section(); // остальной код } } Покажем, что этот алгоритм удовлетворяет требованию безопасности. Для этого предположим противное, т. е. что критические секции выполняются одновременно в двух разных потоках. Это может произойти только в том случае, если инструкции tas (lock), исполняемые в разных потоках, прервали друг друга. Но это невозможно, т. к. инструкция tas выполняется непрерывным образом. Следовательно, наше предположение неверно и в критической секции может находиться только один из потоков. Теперь покажем, что этот алгоритм удовлетворяет требованию поступательности. Для этого предположим противное, т. е. что все потоки одновременно заблокированы на ВЫПОЛНенИИ СВОИХ ЦИКЛОВ while (tas (lock) ). Но ЭТО возможно только в том случае, если начальное значение переменной lock было равно 1, что противоречит тексту программы. Следовательно, наше предположение неверно и требование поступательности выполняется. Наконец поговорим о справедливости алгоритма. В общем случае выполнение этого требования зависит от арбитра шины данных, который управляет доступом микропроцессоров к шине данных. Для выполнения условия справедливости необходимо, чтобы арбитр шины обеспечивал справедливый доступ процессоров к шине данных.
Глава 5. Синхронизация 103 Инструкция "обменять содержимое переменных" В наборе инструкций процессоров семейства Intel x86 существует инструкция xchg (exchange), которая имеет следующую реализацию: void xchg(register int r, int x) { int temp; temp = r; r = x; x = temp; } и выполняется непрерывным образом, т. е. выполнение этой инструкции не прерывается. Используя эту инструкцию, можно реализовать взаимоисключающий доступ параллельных потоков к критической секции по следующему алгоритму: int lock = 0; // переменная, которая запирает вход в критическую // секцию void thread() { while(true) { register int key; // ключ к замку key = 1; while(key == 1) xchg(key, lock); critical_section(); lock = 0; non_critical_section() // ждать, пока замок закрыт // критическая секция // открыть замок // остальной код } По аналогии с алгоритмом, использующим инструкцию tas, можно показать, что приведенный алгоритм также удовлетворяет двум первым требованиям, выдвигаемым к решению задачи взаимного исключения. Для того чтобы терминологию, относящуюся к аппаратной синхронизации потоков, сделать независимой от типов команд, которые используются в процессорах для этих целей, ввели такое понятие, как спин блокировка (spinlock) или активное ожидание. Спин блокировкой называется цикл while с непре-
104 Часть II. Синхронизация потоков и процессов рываемой командой процессора, который ждет разрешения на вход в критическую секцию. В заключение этого раздела сделаем два важных замечания. Во-первых, аппаратные алгоритмы синхронизации могут использоваться любым количеством параллельных потоков. Во-вторых, как программные, так аппаратные алгоритмы синхронизации имеют один существенный недостаток: впустую тратится процессорное время в циклах while, ждущих разрешения на вход в критическую секцию. Поэтому все эти алгоритмы получили общее название алгоритмы, занимающиеся ожиданием (busy waiting algorithms), или алгоритмы активного ожидания. 5.5. Примитивы синхронизации Примитивом синхронизации называется программное средство высокого уровня для решения задач синхронизации. Обычно примитивы синхронизации реализованы как объекты ядра операционной системы, которые предназначены для решения задач синхронизации потоков и процессов. В пользовательских программах доступ к примитивам синхронизации выполняется или посредством вызова функций, которые работают с объектами синхронизации, или при помощи специальных инструкций, которые встроены в язык программирования. Часто языки программирования, в которые встроены объекты синхронизации, называются языками системного программирования. Мы рассмотрим реализацию примитивов синхронизации только для случая однопроцессорных систем. Поэтому непрерывность действий будем обеспечивать запрещением прерываний. Теперь перейдем непосредственно к реализации примитивов синхронизации. Для этих целей будем использовать символический код в стиле языка программирования C++, избегая при этом деталей реализации функций, которые затруднят изучение сути вопроса. Во-первых, чтобы избежать "занятия активным ожиданием", будем блокировать исполнение потоков. Для этого с каждым примитивом синхронизации свяжем очередь заблокированных потоков. Ниже приведена спецификация такой очереди: class Thread { /* ... */ }; ■// класс потоков class ThreadQueue // класс очередей потоков { Thread* tp; // список потоков void IncludeThreadToList(Threads t); // включить поток в список Threads ExcludeThreadFromList(); // исключить поток из очереди
Глава 5. Синхронизация 105 void SuspendThread(Threads t); // заблокировать поток void ResumeThread(Threads t); // разблокировать поток public: ThreadQueue(): tp(NULL) {} // конструктор -ThreadQueue() { /* очищаем список потоков */ } // деструктор void EnqueueThread(Threads t) // поставить поток в очередь { IncludeThreadToList(t); SuspendThread(t); } bool DequeueThread() // исключить поток из очереди { if(tp == NULL) return false; else { ResumeThread(ExcludeThreadFromList()); return true; } } }; Теперь можно перейти к реализации примитивов синхронизации. Рассмотрим реализацию только двух примитивов синхронизации: condition (условие) и semaphore (семафор), наиболее часто встречающихся на практике. Другие примитивы синхронизации могут быть реализованы подобным им образом. После реализации этих примитивов покажем, как они могут применяться для решения задач синхронизации параллельных потоков. Примитив синхронизации condition (условие) Ниже приведен класс condition, определяющий одноименный примитив: class Condition { bool event; ThreadQueue tq; // очередь потоков public: Condition(bool b): event(b) {} // конструктор
106 Часть II. Синхронизация потоков и процессов ~Condition() {} // деструктор void Signal() // сигнализировать о том, что условие выполнено { disable_interrupt(); // запрещаем прерывания if(!tq.DequeueThread()) event = true; enable_interrupt(); // разрешаем прерывания } void Wait(Thread t) // ждать выполнения условия { disable_interrupt(); // запрещаем прерывания i f(event) event = false; // сбрасываем условие else tq.EnqueueThread(t); // ставим поток в очередь ожидания enable_interrupt(); // разрешаем прерывания } }; Этот примитив может использоваться для решения как задачи условной синхронизации, так и задачи взаимного исключения. Приведем решение задачи условного ожидания, используя класс (примитив синхронизации) Condition. В следующем примере поток thread_2 сигнализирует потоку thread_l О наступлении события event. Condition event(false); // событие event void thread_l() // поток thread_l { actions_before_event(); // действия до наступления события event.Wait(thread_l); // ждем, пока событие не произошло actions_after_event(); // действия после наступления события } void thread_2() // поток thread_2 { some_actions(); // действия, о которых оповещает событие event.Signal(); // отмечаем о наступлении события other_actions(); // действия, происходящие после события
Глава 5. Синхронизация 107 Как видно, в этом случае поток thread_i не тратит процессорное время на ожидание события event, а блокируется до наступления этого события. Теперь приведем решение задачи взаимного исключения, используя примитив синхронизации condition. В этом примере примитив синхронизации event управляет ВХОДОМ В Критическую сеКЦИЮ ПОТОКа thread. Condition event(true); // переменная, которая разрешает вход в // критическую секцию void thread() { while(true) { event.Wait(thread); critical_section(); event.Signal(); non_critical_section(), } } // ждать разрешения на вход // критическая секция // разрешить вход // остальной код Примитив синхронизации semaphore (семафор) Ниже приведен класс semaphore, определяющий одноименный примитив. class Semaphore { // счетчик // очередь потоков unsigned counter; ThreadQueue tq; public: Semaphore(unsigned n): counter(n) {} // конструктор -Semaphore() {} // деструктор void Signal() // сигнализировать о том, что условие выполнено { disable_interrupt(); // запрещаем прерывания if(!tq.DequeueThread()) ++counter; enable_interrupt(); // разрешаем прерывания } void Wait(Thread t) // ждать выполнения условия
108 Часть II. Синхронизация потоков и процессов disable_interrupt(); // запрещаем прерывания if(counter > 0) —counter; // сбрасываем условие else tq.EnqueueThread(t); // ставим поток в очередь ожидания enable_interrupt(); // разрешаем прерывания } }; При помощи примитива semaphore (семафор) также могут быть решены задачи условной синхронизации и взаимного исключения. Эти решения полностью совпадают с решениями, использующими примитив condition (условие) за исключением того, что семафор должен принимать значения 0 и 1 вместо true и false соответственно. Семафор, который может принимать только значения 0 или 1 называется бинарным. Такой семафор собственно ничем не отличается от примитива синхронизации condition (условие). Если же семафор может принимать любые положительные целочисленные значения, то такой семафор называется считающим. Считающие семафоры используются для подсчета количества ресурсов, производимых параллельными потоками. Такие потоки обычно называются производителями. Уменьшают значение считающего семафора потоки, которые потребляют эти ресурсы, такие потоки называются потребителями. Сделаем еще одно замечание относительно семафоров, которое, впрочем, касается и примитива синхронизации condition (условие). Если очередь семафора обслуживается по алгоритму FIFO, то семафор называется сильным, а в противном случае слабым. На этот момент следует обратить внимание при разработке приложений. Если в спецификациях операционной системы ничего не сказано о порядке обслуживания очереди потоков, ждущих некоторого события или положительного значения семафора, то не нужно делать на этот счет никаких предположений. Так как потоки, стоящие в очереди к примитиву синхронизации, могут обслуживаться как по алгоритму FIFO, так и учитывая их приоритеты.
Глава 6 Синхронизация потоков в Windows 6.1. Критические секции В операционных системах Windows проблема взаимного исключения для параллельных потоков, выполняемых в контексте одного процесса, решается при помощи объекта типа critical_section, который не является объектом ядра операционной системы. Для работы с объектами типа critical_section используются следующие функции: // инициализация критической секции VOID InitializeCriticalSection(LPCRITICAL_SECTION lpCriticalSection); // вход в критическую секцию VOID EnterCriticalSection(LPCRITICAL_SECTION lpCriticalSection); // попытка войти в критическую секцию BOOL TryEnterCriticaisection(LPCRITICAL_SECTION lpCriticalSection); // выход из критической секции VOID LeaveCriticalSection(LPCRITICAL_SECTION lpCriticalSection); // разрушение объекта критическая секция VOID DeleteCriticalSection(LPCRITICAL_SECTION lpCriticalSection); Каждая из этих функций имеет единственный параметр, указатель на объект типа CRITICAL_SECTION. Все ЭТИ фуНКЦИИ, за Исключением TryEnterCriticaisection, не возвращают значения. Функция TryEnterCriticaisection возвращает ненулевое значение, если поток вошел в критическую секцию или уже находится в ней, в противном случае функция возвращает значение false. Отметим также, ЧТО фуНКЦИЯ TryEnterCriticaisection поддерживается только операционной системой Windows 2000. Кратко рассмотрим порядок работы с этими функциями. Для этого предположим, что при проектировании программы мы выделили в параллельных потоках критические секции, в которых используется ресурс, разделяемый
110 Часть II. Синхронизация потоков и процессов этими потоками. Тогда мы определяем в нашей программе объект типа critical_section и считаем, что имя этого объекта логически связано с используемым разделяемым ресурсом. Перед тем как начать работу с объектом типа critical_section, его необходимо инициализировать. Для этого и предназначена функция initiaiizecriticaisection. После инициализации нашего объекта типа critical_section мы в каждом из параллельных потоков пред входом в критическую секцию вызываем функцию Entercriticaisection, которая исключает одновременный вход в критические секции, выполняющиеся в параллельных потоках и связанные с нашим разделяемым ресурсом. После завершения работы с разделяемым ресурсом поток должен покинуть свою критическую секцию, что выполняется посредством ВЫЗОВа функции LeaveCriticalSection. После окончания работы с объектом типа critical_section необходимо освободить все системные ресурсы, которые использовались этим объектом. Для этой цели служит ФУНКЦИЯ DeleteCriticalSection. Теперь покажем работу этих функций. Для этого сначала рассмотрим пример, в котором выполняются несинхронизированные параллельные потоки, а затем синхронизируем их работу, используя критические секции. Программа, которая иллюстрирует работу несинхронизированных потоков, приведена в листинге 6.1. | Листинг &1 .Пример работы несинхронизированных потоков #include <windows. h> #include <iostream.h> DWORD WINAPI thread (LPVOID) { int i, j ; for (j = 0; j < 10; ++j) { // выводим строку чисел j for (i = 0; i < 10; ++i) { cout « j « ' ' « flush; SleepA7); } cout « endl;
Глава 6. Синхронизация потоков в Windows 111 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) { // выводим строку чисел j cout « j « ' ' « flush; SleepA7); } cout « endl; } // ждем, пока поток thread закончит свою работу WaitForSingleObject(hThread, INFINITE); return 0; } В ПрОфамме ИЗ ЛИСТИНГа 6.1 КаЖДЫЙ ИЗ ПОТОКОВ main И thread ВЫВОДИТ строки одинаковых чисел. Но из-за параллельной работы потоков каждая выведенная строка может содержать не равные между собой элементы. Наша задача будет заключаться в следующем: нужно так синхронизировать потоки main и thread, чтобы в каждой строке выводились только равные между собой числа. Программа из листинге 6.2 показывает решение этой задачи с помощью объекта типа critical_section.
112 Часть II. Синхронизация потоков и процессов #include <windows.h> #include <iostream.h> CRITICAL_SECTION cs; DWORD WINAPI thread(LPVOID) { int i,j; for (j = 0; j < 10; ++j ) { // входим в критическую секцию EnterCriticalSection (&cs); for (i = 0; i < 10; ++i) { cout « j « ' '« flush; SleepG); } 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);
Глава 6. Синхронизация потоков в Windows 113 if (hThread == NULL) return GetLastError(); for (j = 10; j < 20; ++j ) { // входим в критическую секцию EnterCriticalSection(&cs); for (i = 0; i < 10; ++i) { cout « j « ' ' « flush; SleepG); } cout « endl; // выходим из критической секции LeaveCriticalSection(&cs); } // ждем, пока поток thread закончит свою работу WaitForSingleObj ect(hThread, INFINITE); // закрываем критическую секцию DeleteCriticalSection(&cs); return 0; } Теперь рассмотрим ИСПОЛЬЗОВание фуНКЦИИ TryEnterCriticalSection. Для ЭТОГО просто заменим в программе из листинга 6.2 вызовы функции EnterCriticalSection на ВЫЗОВЫ фуНКЦИИ TryEnterCriticalSection И будем отмечать успешные входы потоков в свои критические секции. Еще раз подчеркнем, ЧТО функция TryEnterCriticalSection работает ТОЛЬКО на платформе операционной системы Windows 2000. Программа, в которой для ВХОДа В критические сеКЦИИ ИСПОЛЬЗУЮТСЯ фуНКЦИИ TryEnterCriticalSection, приведена в листинге 6.3. I Листинг 6.3. Пример попытки входа в критическую секцию , ^ ; , ^ :; | #define _WIN32_WINNT 0x0500 #include <windows.h> #include <iostream.h>
114 Часть II. Синхронизация потоков и процессов CRITICAL_SECTION cs; DWORD WINAPI thread(LPVOID) { int i, j; for (j = 0; j < 10; ++j) // попытка войти в критическую секцию if (TryEnterCriticalSection (&cs)) { for (i = 0; i < 10; i++) cout « j « flush; сout << 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++) // попытка войти в критическую секцию if (TryEnterCriticalSection(&cs)) { for (i = 0; i < 10; i++) cout « j « ' ' « flush; cout « endl;
Глава 6. Синхронизация потоков в Windows 115 // выход из критической секции LeaveCriticalSection(&cs); } // ждем завершения работы потока thread WaitForSingleObject(hThread, INFINITE); // удаляем критическую секцию DeleteCriticalSection(&cs); return 0; } В заключение этого раздела отметим следующее. Так как объекты типа critical_section не являются объектами ядра операционной системы, то работа с ними происходит несколько быстрее, чем с объектами синхронизации, которые являются объектами ядра операционной системы. Это происходит потому, что обращение к объектам ядра операционной системы требует дополнительной работы на переключение контекстов потоков из режима пользователя в защищенный режим ядра операционной системы. Поэтому при разработке многопоточных приложений для решения задач взаимного исключения, как правило, используют объекты типа critical_section. 6.2. Объекты синхронизации и функции ожидания В операционных системах Windows объектами синхронизации называются объекты ядра, которые могут находиться в одном из двух состояний: сигнальном (signaled) и несигнальном (nonsignaled). Объекты синхронизации могут быть разбиты на четыре класса. К первому классу относятся собственно объекты синхронизации, т. е. те, которые служат только для решения задач синхронизации параллельных потоков. К таким объектам синхронизации в Windows относятся: □ мьютекс (mutex); □ событие (event); □ семафор (semaphore). Ко второму классу объектов синхронизации относится ожидающий таймер (waitable timer), который переходит в сигнальное состояние по истечении заданного интервала времени. К третьему классу синхронизации относятся объекты, которые переходят в сигнальное состояние по завершении своей работы: □ работа Oob);
116 Часть II. Синхронизация потоков и процессов П процесс (process); □ поток (thread). К четвертому классу относятся объекты синхронизации, которые переходят в сигнальное состояние после получения сообщения об изменении содержимого объекта. К ним относятся: □ изменение состояния каталога (change notification); □ консольный ввод (console input). В этой части мы будем рассматривать только следующие объекты синхронизации: мьютексы, события, семафоры, а также потоки и процессы. Остальные объекты, которые могут использоваться в функциях ожидания, будут рассмотрены по мере их изучения в следующих главах. Теперь перейдем к функциям ожидания. Функции ожидания в Windows это такие функции, параметрами которых являются объекты синхронизации. Эти функции обычно используются для блокировки потоков. Сама блокировка потока выполняется следующим образом. Если дескриптор объекта синхронизации является параметром функции ожидания, а сам объект синхронизации находится в несигнальном состоянии, то поток, вызвавший эту функцию ожидания, блокируется до перехода этого объекта синхронизации в сигнальное состояние. Сейчас мы будем использовать только две функции ожидания WaitForsingieObject И WaitForMultipleObject. Остальные функции ожидания будут описаны в части, посвященной асинхронному вызову процедур в Windows. Для ожидания перехода в сигнальное состояние одного объекта синхронизации ИСПОЛЬЗуется фунКЦИЯ WaitForsingieObject, которая имеет следующий прототип: DWORD WaitForsingieObject( HANDLE hHandle, // дескриптор объекта DWORD dwMilliseconds // интервал ожидания в миллисекундах ); Функция WaitForsingieObject в течение интервала времени, равного значению параметра dwMilliseconds, ждет перехода объекта синхронизации, дескриптор которого задается параметром hHandle, в сигнальное состояние. Если значение параметра dwMilliseconds равно нулю, то функция только проверяет состояние объекта синхронизации. Если же значение параметра dwMilliseconds равно infinite, то функция ждет перехода объекта синхронизации в сигнальное состояние бесконечно долго. В случае успешного завершения функция WaitForsingieObject возвращает одно из следующих значений: □ wait_object_o — объект перешел в сигнальное состояние;
Глава 6. Синхронизация потоков в Windows 117 □ wait_abandoned — забытый мьютекс; □ wait_timeout — время ожидания истекло. Значение wait_object_o означает, что объект синхронизации находился или перешел в сигнальное состояние. Значение wait_abandoned означает, что объектом синхронизации являлся мьютекс, который не освободился завершившимся потоком. В этом случае мьютекс освобождается операционной системой и поэтому также переходит в сигнальное состояние. Такой мьютекс иногда называется забытый или заброшенный мьютекс (abandoned mutex). Значение wait_timeout означает, что время ожидания истекло, а объект синхронизации так и не перешел в сигнальное состояние. В случае неудачи функция waitForsingieObject возвращает значение WAIT_FAILED. В листинге 6.4 приведен пример программы, которая использует функцию WaitForsingieObject для ожидания завершения потока. Отметим также, что эта функция уже неоднократно использовалась нами ранее для ожидания завершения работы потоков. I Листинг 6,4, Пример использования функции WaitForsingieObject # include <windows.h> #include <iostream.h> void thread() { int i; for (i = 0; i < 10 ; ++i) { cout « i « ' ' « flush; SleepA00); } cout « endl; } int main() { HANDLE hThread; DWORD dwThread;
118 Часть II. Синхронизация потоков и процессов hThread = CreateThread(NULL, О, (LPTHREAD_START_ROUTINE) thread, NULL, О, &dwThread); if (hThread == NULL) return GetLastError(); // ждем завершения потока thread if(WaitForSingleObject(hThread, INFINITE) != WAIT_OBJECT_0) { cout « "Wait for single object failed." « endl; cout « "Press any key to exit." « endl; cin.get(); } // закрываем дескриптор потока thread CloseHandle(hThread); return 0; } Для ожидания перехода в сигнальное состояние нескольких объектов синхронизации или одного из нескольких объектов синхронизации используется функция waitForMuitipieObject, которая имеет следующий прототип: DWORD WaitForMultipleObjects( DWORD nCount, CONST HANDLE *lpHandles, BOOL bWaitAll, DWORD dwMilliseconds ); Функция waitForMuitipieObjects работает следующим образом. Если значение параметра bwaitAii равно true, to эта функция в течение интервала времени, равного значению параметра dwMilliseconds, ждет пока все объекты синхронизации, дескрипторы которых заданы в массиве lpHandies, перейдут в сигнальное состояние. Если же значение параметра bwaitAii равно false, то эта функция в течение заданного интервала времени ждет, пока любой из заданных объектов синхронизации перейдет в сигнальное состояние. Если значение параметра dwMilliseconds равно нулю, то функция только проверяет состояние объектов синхронизации. Если же значение параметра dwMilliseconds равно infinite, то функция ждет перехода объектов синхронизации в сигнальное состояние бесконечно долго. Количество Объектов СИНХроНИЗаЦИИ, ОЖИДаеМЫХ функцией WaitForMuitipieObjects, // количество объектов // массив дескрипторов объектов // режим ожидания // интервал ожидания в миллисекундах
Глава 6. Синхронизация потоков в Windows 119 не должно превышать значения maximum_wait_objects. Также отметим, что объекты синхронизации не должны повторяться. В случае успешного завершения функция waitForMuitipieobjects возвращает следующие значения: □ ОТ WAIT_OBJECT_0 ДО (WAIT_OBJECT_0 + nCount - 1); □ ОТ WAIT_ABANDONED_0 ДО (WAIT_ABANDONED_0 + nCount - 1); □ WAIT_TIMEOUT. Интерпретация значений, возвращаемых функцией WaitForMuitipieobjects, зависит от значения входного параметра bwaitAii. Сначала рассмотрим случай, когда значение этого параметра равно true. Тогда возвращаемые значения интерпретируются следующим образом: □ любое из возвращаемых значений, находящихся в диапазоне от wait_object_o до (wait_object_o + nCount - 1), означает, что все объекты синхронизации находились или перешли в сигнальное состояние; □ любое из возвращаемых значений, находящихся в диапазоне от WAIT_ABANDONED_0 ДО (WAIT_ABANDONED_0 + nCount - 1), Означает, ЧТО все объекты синхронизации находились или перешли в сигнальное состояние и по крайней мере один их них был забытым мьютексом; □ возвращаемое значение wait_timeout означает, что время ожидания истекло и не все объекты синхронизации перешли в сигнальное состояние. Теперь рассмотрим случай, когда значение входного параметра bwaitAii равно false. В этом случае значения, возвращаемые функцией WaitForMuitipieobjects, интерпретируются следующим образом: □ любое из возвращаемых значений, находящихся в диапазоне от wait_object_o до (wait_object_o + nCount - 1), означает, что, по крайней мере, один из объектов синхронизации находился или перешел в сигнальное состояние. Индекс дескриптора этого объекта в массиве определяется как разница между возвращаемым значением и величиной wait_object_o; □ любое из возвращаемых значений, находящихся в диапазоне от wait_abandoned_o до (wait_abandoned_o + ncount - 1), означает, что одним из объектов синхронизации, перешедшим в сигнальное состояние, является забытый мьютекс. Индекс дескриптора этого мьютекса в массиве определяется как разница между возвращаемым значением и величиной wait_object_o; □ возращаемое значение wait_timeout означает, что время ожидания истекло, и ни один из объектов синхронизации не перешел в сигнальное состояние. В случае неудачи функция WaitForMuitipieobjects возвращает значение WAIT_FAILED.
120 Часть II. Синхронизация потоков и процессов В листинге 6.5 приведен пример программы, которая использует функцию WaitForMuitipieObjects для ожидания завершения двух потоков. РЩ&Р использования функции WaitForMultipleObjects завершения двух потоков #include <windows.h> #include <iostream.h> void thread_0() { int i; for (i = 0; i < 5 ; ++i) { cout « i « • • « flush; SleepG); } cout « endl; } void thread_l() { int i; for (i = 5; i < 10 ; ++i) { cout « i « ' ' « flush; SleepG); } cout « endl; } int main() { HANDLE hThread[2]; DWORD dwThread[2]; // запускаем первый поток
Глава 6. Синхронизация потоков в Windows 121 hThread[0] = CreateThread(NULL, O, (LPTHREAD_START_ROUTINE) thread_07 NULL, 0, &dwThread[0]); if (hThread[0] == NULL) return GetLastError(); // запускаем второй поток hThread[l] = CreateThread (NULL, 0, (LPTHREAD_START_ROUTINE) thread_l, NULL, 0, &dwThread[l]); if (hThread[1] == NULL) return GetLastError(); // ждем завершения потоков thread_l и thread_2 if (WaitForMultipleObjectsB, hThread, TRUE, INFINITE) == WAIT_FAILED) { cout « "Wait for multiple objects failed." « endl; cout « "Input any char to exit." « endl; cin.get(); } // закрываем дескрипторы потоков thread_0 и thread_l CloseHandle(hThread[0]); CloseHandle(hThread[1]); return 0; } 6.3. Мьютексы Для решения проблемы взаимного исключения между параллельными потоками, выполняющимися в контекстах разных процессов, в операционных системах Windows используется объект ядра мьютекс. Слово мьютекс происходит от английского слова mutex, которое в свою очередь является сокращением от выражения mutual exclusion, что на русском языке значит "взаимное исключение". Мьютекс находится в сигнальном состоянии, если он не принадлежит ни одному потоку. В противном случае мьютекс находится в несигнальном состоянии. Одновременно мьютекс может принадлежать только одному потоку. Потоки, ждущие сигнального состояния мьютекса, обслуживаются в порядке FIFO, т. е. потоки становятся в очередь к мьютексу с дисциплиной обслуживания "первый пришел — первый вышел". Однако если поток ждет
122 Часть II. Синхронизация потоков и процессов наступления асинхронного события, то функции ядра могут исключить поток из очереди к мьютексу для обслуживания наступления этого события. После этого поток становится в конец очереди мьютекса. Создается мьютекс вызовом функции CreateMutex, которая имеет следующий прототип: HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes, // атрибуты защиты BOOL blnitialOwner, // начальный владелец мьютекса LPCTSTR lpName // имя мьютекса ); Пока значение параметра lpsecurity_attributes будем устанавливать в null. Это означает, что атрибуты защиты заданы по умолчанию, т. е. дескриптор мьютекса не является наследуемым и доступ к мьютексу открыт для всех пользователей. Теперь перейдем к другим параметрам. Если значение параметра blnitialOwner равно true, to мьютекс сразу переходит во владение потоку, которым он был создан. В противном случае вновь созданный мьютекс свободен. Поток, создавший мьютекс, имеет все права доступа к этому мьютексу. Значение параметра lpName определяет уникальное имя мьютекса для всех процессов, выполняющихся под управлением операционной системы. Это имя позволяет обращаться к мьютексу из других процессов, запущенных под управлением этой же операционной системы. Длина имени не должна превышать значение мах_ратн. Значением параметра lpName может быть пустой указатель null. В этом случае система создает безымянный мьютекс. Отметим также, что имена мьютексов являются чувствительными к нижнему и верхнему регистрам. В случае удачного завершения функция CreateMutex возвращает дескриптор созданного мьютекса. В случае неудачи эта функция возвращает значение null. ЕСЛИ МЬЮТекС С заданным Именем уже Существует, ТО фуНКЦИЯ CreateMutex возвращает дескриптор этого мьютекса, а функция GetLastError, вызванная ПОСЛе фуНКЦИИ CreateMutex, Вернет Значение ERROR_ALREADY_EXISTS. Мьютекс захватывается потоком посредством любой функции ожидания, а освобождается функцией ReleaseMutex, которая имеет следующий прототип: BOOL ReleaseMutex ( HANDLE hMutex // дескриптор мьютекса ); В случае успешного завершения функция ReleaseMutex возвращает ненулевое значение, а в случае неудачи — false. Если поток освобождает мьютекс, которым он не владеет, то функция ReleaseMutex возвращает значение false.
Глава 6. Синхронизация потоков в Windows 123 Для доступа к существующему мьютексу поток может использовать одну из ФУНКЦИЙ CreateMutex ИЛИ OpenMutex. ФунКЦИЯ CreateMutex ИСПОЛЬЗуетСЯ В тех случаях, когда поток не знает, создан или нет мьютекс с указанным именем другим потоком. В этом случае значение параметра bmitiaiowner нужно установить в false, т. к. невозможно определить какой из потоков создает мьютекс. Если поток использует для доступа к уже созданному мьютексу функцию CreateMutex, то он получает полный доступ к этому мьютексу. Для того чтобы получить доступ к уже созданному мьютексу поток может также использовать функцию OpenMutex, которая имеет следующий прототип: HANDLE OpenMutex ( DWORD dwDesiredAccess, // доступ к мьютексу BOOL blnheritHandle // свойство наследования LPCTSTR lpName // имя мьютекса ); Параметр dwDesiredAccess ЭТОЙ функции МОЖет принимать ОДНО ИЗ двух значений: □ MUTEX_ALL_ACCESS — ПОЛНЫЙ ДОСТуп; □ SYNCHRONIZE — СИНХрОНИЗаЦИЯ. В первом случае поток получает полный доступ к мьютексу. Во втором случае поток может использовать мьютекс только в функциях ожидания, чтобы захватить мьютекс, или в функции ReieaseMutex для его освобождения. Параметр blnheritHandle определяет свойство наследования мьютекса. Если значение этого параметра равно true, to дескриптор открываемого мьютекса является наследуемым. В противном случае дескриптор не наследуется. В случае успешного завершения функция OpenMutex возвращает дескриптор открытого мьютекса, в случае неудачи эта функция возвращает значение null. Покажем пример использования мьютекса для синхронизации потоков из разных процессов. Для этого сначала рассмотрим пример несинхронизиро- ванных потоков. Программы несинхронизированных процессов приведены в листингах 6.6, 6.7. Листинг 6*8. Несинхронизированньш потоки, выполняющиеся в разных процессах #include <windows.h> #include <iostream.h> int main()
124 Часть II. Синхронизация потоков и процессов { char IpszAppName[] = "С:\\ConsoleProcess.ехе"; STARTUPINFO si; PROCESS_INFORMATION pi ; ZeroMemory(&s i, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); // создаем новый консольный процесс if (!CreateProcess(IpszAppName, 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.getO ; return GetLastError(); } // выводим на экран строки for (int j = 0; j < 10; ++j) { for (int i = 0; i < 10; ++i) { cout « j « ' ' « flush; SleepA0); } cout « endl; } // ждем, пока дочерний процесс закончит работу WaitForSingleObject(pi.hProcess, INFINITE); // закрываем дескрипторы дочернего процесса в текущем процессе CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0;
Глава 6. Синхронизация потоков в Windows 125 | Листинг 6,7. Несинхронизированные потоки, выполняющиеся ш разных | процессах л #include <windows.h> #include <iostream.h> int main() { int i/j; for (j = 10; j < 20; ++j) { for (i = 0; i < 10; ++i) { cout « j « ' ' « flush; SleepE); } cout « endl; } return 0; } Кратко опишем работу программ, приведенных в листингах 6.6, 6.7. Первая из них запускает вторую программу, не создавая при этом новую консоль. После этого потоки из разных процессов начинают выводить числа в одну консоль. Из-за отсутствия синхронизации числа в одной строке могут быть из разных потоков. Для того чтобы избежать перемешивания чисел, синхронизируем вывод с помощью мьютекса. Далее, в листингах 6.8, 6.9, приведены модификации этих программ с использованием мьютекса для синхронизации работы этих потоков. | Листинг 6.8. Синхронизация потоков, выполняющихся в разных процессах, j с использованием мьютекса # include <windows.h> #include <iostream.h> int main()
126 Часть II. Синхронизация потоков и процессов HANDLE hMutex; char IpszAppName [] = "С:\\ConsoleProcess.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.getO ; return GetLastError(); } ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); // создаем новый консольный процесс if (ICreateProcess(IpszAppName, 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) { // захватываем мьютекс WaitForSingleObject(hMutex, INFINITE); for (int i = 0; i < 10; i++) { cout « j « ' ' « flush; SleepA0);
Глава 6. Синхронизация потоков в Windows 127 } cout « endl; // освобождаем мьютекс ReleaseMutex(hMutex); } // закрываем дескриптор мьютекса CloseHandle(hMutex) ; // ждем пока дочерний процесс закончит работу WaitForSingleObject (pi.hProcess, INFINITE) ; // закрываем дескрипторы дочернего процесса в текущем процессе CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0; } 1и|тинг 6,9. Синхронизации потоков, выполняющихся в разных процессах, #include <windows.h> #include <iostream.h> int main() { HANDLE hMutex; int i, j ; // открьшаем мьютекс hMutex = OpenMutex(SYNCHRONIZE, FALSE, "DemoMutex") ; if (hMutex == NULL) { cout « "Open mutex failed." « endl; cout « "Press any key to exit." « endl; cin.get(); return GetLastError();
128 Часть II. Синхронизация потоков и процессов for (j = 10; j < 20; j++) { // захватываем мьютекс WaitForSingleObject(hMutex, INFINITE); for (i = 0; i < 10; i++) { cout « j « ' ' « flush; SleepE); } cout « endl; // освобождаем мьютекс ReleaseMutex(hMutex); } // закрываем дескриптор объекта CloseHandle(hMutex); return 0; } 6.4. События Событием называется оповещение о некотором выполненном действии. В программировании события используются для оповещения одного потока о том, что другой поток выполнил некоторое действие. Сама же задача оповещения одного потока о некотором действии, которое совершил другой поток, называется задачей условной синхронизации или иногда задачей оповещения. В операционных системах Windows события описываются объектами ядра Events. При этом различают два типа событий: □ события с ручным сбросом; □ события с автоматическим сбросом. Различие между этими типами событий заключается в том, что событие с ручным сбросом можно перевести в несигнальное состояние только посредством вызова функции ResetEvent, а событие с автоматическим сбросом переходит в несигнальное состояние как при помощи функции ResetEvent, так и при помощи функции ожидания. При этом отметим, что если события с автоматическим сбросом ждут несколько потоков, используя функцию WaitForSingleObject, то из состояния ожидания освобождается только один из этих потоков.
Глава 6. Синхронизация потоков в Windows 129 Создаются события вызовом функции CreateEvent, которая имеет следую- щий прототип: HANDLE CreateEvent( LPSECURITY_ATTRIBUTES IpSecurityAttributes, // атрибуты защиты BOOL bManuaiReset, // тип события BOOL blnitialState, // начальное состояние события LPCTSTR lpName // имя события ); Как и обычно, пока значение параметра IpSecurityAttributes будем устанавливать в null. Основную смысловую нагрузку в этой функции несут второй и третий параметры. Если значение параметра bManuaiReset равно true, то создается событие с ручным сбросом, в противном случае — с автоматическим сбросом. Если значение параметра binitiaistate равно true, то начальное состояние события является сигнальным, в противном случае — несигнальным. Параметр lpName задает имя события, которое позволяет обращаться к нему из потоков, выполняющихся в разных процессах. Этот параметр может быть равен null, тогда создается безымянное событие. В случае удачного завершения функция CreateEvent возвращает дескриптор события, а в случае неудачи — значение null. Если событие с заданным именем уже существует, то функция CreateEvent возвращает дескриптор этого события, а функция GetLastError, вызванная после функции CreateEvent, вернет значение error_already_exists. Для перевода любого события в сигнальное состояние используется функция setEvent, которая имеет следующий прототип: BOOL SetEvent( HANDLE hEvent // дескриптор события ); При успешном завершении эта функция возвращает ненулевое значение, а в случае неудачи — false. В листинге 6.10 приведен пример программы, в которой безымянные события с автоматическим сбросом используются для синхронизации работы потоков, выполняющихся в одном процессе. \ Листинг 6.10. Синхронизации потоков при помощи событий с автоматическим ; сбросом # include <windows.h> #include <iostream.h> HANDLE hOutEvent, hAddEvent; 5 Зак. 514
130 Часть II. Синхронизация потоков и процессов DWORD WINAPI thread (LPVOID) { for (int i = 0; i < 10; ++i) if (i == 4) { SetEvent(hOutEvent); WaitForSingleObject(hAddEvent, INFINITE); } return 0; } int main() { HANDLE hThread; DWORD IDThread; // создаем события с автоматическим сбросом hOutEvent = CreateEvent(NULL, FALSE, FALSE, NULL); if (hOutEvent == NULL) return GetLastError(); hAddEvent = CreateEvent(NULL, FALSE, FALSE, NULL); if (hAddEvent == NULL) return GetLastError(); // создаем поток thread hThread = CreateThread(NULL, 0, thread, NULL, 0, &IDThread); if (hThread == NULL) return GetLastError(); // ждем, пока поток thread выполнит половину работы WaitForSingleObject(hOutEvent, INFINITE); // выводим значение переменной cout « "A half of the work is done." « endl; cout « "Press any key to continue." « endl; cin.get(); // разрешаем дальше работать потоку thread SetEvent(hAddEvent);
Глава 6. Синхронизация потоков в Windows 131 WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); CloseHandle(hOutEvent); CloseHandle(hAddEvent); cout « "The work is done." « endl; return 0; } Теперь опишем функцию ResetEvent, которая используется для перевода любого события в несигнальное состояние. Эта функция имеет следующий прототип: BOOL ResetEvent( HANDLE hEvent // дескриптор события ); При успешном завершении эта функция возвращает ненулевое значение, а в случае неудачи — false. Для освобождения потоков, ждущих сигнального состояния события с ручным сбросом, используется функция PuiseEvent, которая имеет следующий прототип: BOOL PuiseEvent( HANDLE hEvent // дескриптор события ); При вызове этой функции все потоки, ждущие события с дескриптором hEvent, выводятся из состояния ожидания, а само событие сразу переходит в несигнальное состояние. В этом случае говорят, что все потоки, ждущие сигнального состояния события с ручным сбросом, подошли к барьеру, через который они все могут перешагнуть только при переходе события с ручным сбросом в сигнальное состояние. Если функция PuiseEvent вызывается для события с автоматическим сбросом, то из состояния ожидания выводится только один из ожидающих потоков. Если нет потоков, ожидающих сигнального состояния события с дескриптором hEvent, то состояние этого события остается несигнальным. В ЛИСТИНГе 6.11 Приведена Программа, ИСПОЛЬЗуЮЩая фуНКЦИЮ PuiseEvent для синхронизации потоков, выполняющихся в одном процессе.
132 Часть II. Синхронизация потоков и процессов ; Листинг 6Л1. Синхронизации потоков при помощи событий с ручным сбросом #include <windows.h> #include <iostream.h> HANDLE hOutEvent [ 2 ] , hAddEvent ; DWORD WINAPI thread_l(LPVOID) { for (int i = 0; i < 10; ++i) if (i == 4) { SetEvent(hOutEvent[0]); WaitForSingleObj ect(hAddEvent, INFINITE); } return 0; } DWORD CALLBACK thread_2(LPVOID) { for (int i = 0; i < 10; ++i) if (i == 4) { SetEvent(hOutEvent[1]); WaitForSingleObject(hAddEvent, INFINITE); } return 0; } int main () { HANDLE hThread_l, hThread_2; DWORD IDThread_l, IDThread_2; // создаем события с автоматическим сбросом hOutEvent[0] = CreateEvent(NULL, FALSE, FALSE, NULL);
Глава 6. Синхронизация потоков в Windows 133 if (hOutEvent[0] == NULL) return GetLastError(); hOutEvent[l] = CreateEvent(NULL, FALSE, FALSE, NULL); if (hOutEvent[l] == NULL) return GetLastError(); // создаем событие с ручным сбросом hAddEvent = CreateEvent(NULL, TRUE, FALSE, NULL); if (hAddEvent == NULL) return GetLastError(); // создаем потоки hThread_l = CreateThread(NULL, 0, thread_l, NULL, 0, &IDThread_l); if (hThread_l == NULL) return GetLastError(); hThread_2 = CreateThread(NULL, 0, thread_2, NULL, 0, &IDThread_2); if (hThread_2 == NULL) return GetLastError(); // ждем, пока потоки-счетчики выполнят половину работы WaitForMultipleObjectsB, hOutEvent, TRUE, INFINITE); cout « "A half of the work is done." « endl; cout « "Press any key to continue." « endl; cin.get(); // разрешаем потокам-счетчикам продолжать работу PulseEvent(hAddEvent); // ждем завершения потоков WaitForSingleObject(hThread_l, INFINITE); WaitForSingleObj ect(hThread_2, INFINITE); // закрываем дескрипторы CloseHandle(hThread_l); CloseHandle(hThread_2); CloseHandle(hOutEvent[0]); CloseHandle(hOutEvent[1]);
134 Часть II. Синхронизация потоков и процессов CloseHandle(hAddEvent); cout « "The work is done." « endl; return 0; } Доступ к существующему событию можно открыть с помощью функции CreateEvent ИЛИ OpenEvent. Если для этой цели используется функция CreateEvent, то значения параметров bManualReset И blnitialState ЭТОЙ функции игнорируются, Т. К. они уже установлены другим потоком, а поток, вызвавший эту функцию, получает полный доступ к событию с именем, заданным параметром lpName. Теперь рассмотрим функцию OpenEvent, которая используется в том случае, если известно, что событие с заданным именем уже существует. Эта функция имеет следующий прототип: HANDLE OpenEvent( DWORD dwDesiredAccess, // флаги доступа BOOL blnheritHandle, // режим наследования LPCTSTR lpName // имя события ); Параметр dwDesiredAccess определяет доступ к событию и может быть равен любой логической комбинации следующих флагов: □ EVENT_ALL_ACCESS — ПОЛНЫЙ ДОСТуп; □ event_modify_state — модификация состояния; □ SYNCHRONIZE — СИНХрОНИЗацИЯ. Эти флаги устанавливают следующие режимы доступа к событию: □ флаг event_all_access означает, что поток может выполнять над событием любые действия; □ флаг event_modify_state означает, что поток может использовать функции SetEvent И ResetEvent ДЛЯ Изменения СОСТОЯНИЯ события; □ флаг synchronize означает, что поток может использовать событие в функциях ожидания. В листингах 6.12, 6.13 приведены программы, иллюстрирующие синхронизацию потоков, выполняющихся в разных процессах, при помощи события с автоматическим сбросом. В этом примере также используется функция OpenEvent для доступа к уже существующему событию.
Глава 6. Синхронизация потоков в Windows 135 #include <windows.h> #include <iostream.h> HANDLE hlnEvent; CHAR IpEventName [ ] = "InEventName"; int main () { DWORD dwWaitResul thenar szAppName[] = "D:\\ConsoleProcess.exe"; STARTUPINFO si; PROCESS_INFORMATION pi; // создем событие, отмечающее ввод символа hlnEvent = CreateEvent(NULL, FALSE, FALSE, IpEventName); if (hlnEvent == NULL) return GetLastError(); // запускаем процесс, который ждет ввод символа ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof (STARTUPINFO) ; if (ICreateProcess(szAppName, NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)) return 0; // закрываем дескрипторы этого процесса CloseHandle(pi.hProcess); CloseHandle(pi.hThread); // ждем оповещение о наступлении события о вводе символа dwWaitResult = WaitForSingleObject(hlnEvent, INFINITE); if (dwWaitResult != WAIT_OBJECT_0) return dwWaitResult; cout « "A symbol has got. " « endl;
136 Часть II. Синхронизация потоков и процессов CloseHandle(hlnEvent); cout « "Press any key to exit."; cin.getO ; return 0; Листинг 6Л 3. Дочерний процесс, устанавливающий событие в сигнальное ! состояние^" #include <windows.h> #include <iostream.h> HANDLE hlnEvent; CHAR IpEven tName [ ] =" InEven tName" ; int main() { char c; hlnEvent = OpenEvent(EVENT_MODIFY_STATE/ FALSE, IpEventName); if (hlnEvent == NULL) { cout « "Open event failed." « endl; cout « "Input any char to exit." « endl; cin.get(); return GetLastError(); } cout « "Input any char: "; cin » c; // устанавливаем событие о вводе символа SetEvent (hlnEvent) ; // закрываем дескриптор события в текущем процессе CloseHandle(hlnEvent); cin.get();
Глава 6. Синхронизация потоков в Windows 137 cout « "Press any key to exit." « endl; cin.get(); return 0; } Кратко опишем работу этих программ. Родительский процесс, программа которого приведена в листинге 6.12, создает событие с автоматическим сбросом, а затем запускает дочерний процесс, программа которого приведена в листинге 6.13. После этого родительский процесс ждет до тех пор, пока дочерний процесс не установит это событие в сигнальное состояние. Дождавшись этого, он завершает свою работу. Дочерний процесс открывает событие, созданное в родительском процессе, после чего вводит символ с консоли. Если символ введен, то он устанавливает событие в сигнальное состояние и завершает свою работу. 6.5. Семафоры Семафоры в операционных системах Windows описываются объектами ядра semaphores. Семафор находится в сигнальном состоянии, если его значение больше нуля. В противном случае семафор находится в несигнальном состоянии. Потоки, ждущие сигнального состояния семафора, обслуживаются в порядке FIFO, т. е. потоки становятся в очередь к семафору с дисциплиной обслуживания "первый пришел, первый вышел". Однако если поток ждет наступления асинхронного события, то функции ядра могут исключить поток из очереди к семафору для обслуживания наступления этого события. После этого поток становится в конец очереди семафора. Создаются семафоры посредством вызова функции createsemaphore, которая имеет следующий прототип: HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES IpSemaphoreAttribute, // атрибуты защиты LONG llnitialCount, // начальное значение семафора LONG IMaximumCount, // максимальное значение семафора LPCTSTR lpName // имя семафора ); Пока значение параметра lpSemaphoreAttributes будем устанавливать в null. Основную смысловую нагрузку в этой функции несут второй и третий параметры. Значение параметра linitialcount устанавливает начальное значение семафора, которое должно быть не меньше 0 и не больше его максимального значения, которое устанавливается параметром IMaximumCount.
138 Часть II. Синхронизация потоков и процессов Параметр lpName может указывать на имя семафора или содержать значение null. В последнем случае создается безымянный семафор. В случае успешного завершения функция createsemaphore возвращает дескриптор семафора, в случае неудачи — значение null. Если семафор с заданным именем уже существует, то функция createsemaphor возвращает дескриптор этого семафора, а функция GetLastError, вызванная после ФУНКЦИИ Createsemaphor, вернет Значение ERROR_ALREADY_EXISTS. Значение семафора уменьшается на 1 при его использовании в функции ожидания. Увеличить значение семафора можно посредством вызова функции ReleaseSemaphore, Которая имеет следующий ПроТОТИП! BOOL ReleaseSemaphore( HANDLE hSemaphore, // дескриптор семафора LONG IReleaseCount, // положительное число, на которое // увеличивается значение семафора LPLONG lpPreviousCount // предыдущее значение семафора ); В случае успешного завершения функция ReleaseSemaphore возвращает ненулевое значение, а в случае неудачи — false. Если значение семафора плюс значение параметра IReleaseCount больше максимального значения семафора, то функция ReleaseSemaphore возвращает значение false и значение семафора не изменяется. Значение параметра lpPreviousCount этой функции может быть равно null. В этом случае предыдущее значение семафора не возвращается. Доступ к существующему семафору можно открыть с помощью одной из ФУНКЦИЙ CreateSemaphore ИЛИ OpenSemaphore. ЕСЛИ ДЛЯ ЭТОЙ Цели ИСПОЛЬ- зуетСЯ функция CreateSemaphore, ТО значения параметров UnitialCount И lMaximalcount этой функции игнорируются, т. к. они уже установлены другим потоком, а поток, вызвавший эту функцию, получает полный доступ к семафору с именем, заданным параметром lpName. Теперь рассмотрим функцию OpenSemaphore, которая используется в случае, если известно, что семафор с заданным именем уже существует. Эта функция имеет следующий прототип: HANDLE OpenSemaphore( DWORD dwDesiredAccess, // флаги доступа BOOL blnheritHandle, // режим наследования LPCTSTR lpName // имя события ); Параметр dwDesiredAccess определяет доступ к семафору и может быть равен любой логической комбинации следующих флагов: □ semaphore_all_access — полный доступ к семафору;
Глава 6. Синхронизация потоков в Windows 139 П semaphore_modify_state — изменение состояния семафора; □ SYNCHRONIZE — СИНХрОНИЗаЦИЯ. Опишем назначение этих флагов чуть подробнее. □ флаг semaphore_all_access устанавливает для потока полный доступ к семафору. Это означает, что поток может выполнять над семафором любые действия; □ флаг semaphore_modify_state означает, что поток может использовать только функцию ReieaseSemaphore для изменения значения семафора; □ флаг synchronize означает, что поток может использовать семафор только в функциях ожидания. Отметим, что этот флаг поддерживается только на платформе Windows NT/2000. В листинге 6.15 приведен пример программы, в которой считающий семафор используется для синхронизации работы потоков. Для этого сначала рассмотрим несинхронизированный вариант этой программы, который представлен в листинге 6.14. Ш истинг 6.14. Несинхрон из и рованные потоки # include <windows.h> #include <iostream.h> volatile int a[10]; DWORD WINAPI thread (LPVOID) { for (int i = 0; i < 10; i++) { a[i] = i + 1; SleepG); } return 0; } int main() { int i; HANDLE hThread; DWORD IDThread;
140 Часть II. Синхронизация потоков и процессов 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] « ' ' « flush; SleepA1); } cout « endl; С1oseHandle(hThread); return 0; } Теперь кратко опишем работу программы из листинга 6.14. Поток thread последовательно присваивает элементам массива а значения, которые на единицу больше, чем их индекс. Поток main последовательно выводит элементы массива а на консоль. Так как потоки thread и main не синхронизированы, то измененное состояние массива, которое выведет на консоль поток main, неизвестно. Наша задача состоит в том, чтобы поток main выводил на консоль элементы массива а сразу после их подготовки потоком thread. Для этого мы используем считающий семафор. Программа из листинга 6.15 показывает, как этот считающий семафор используется для синхронизации работы потоков. j Листинг $Л& Синхронизации потоков с использованием семафора # include <windows.h> #include <iostream.h> volatile int a[10];
Глава 6. Синхронизация потоков в Windows 141 HANDLE hSemaphore; DWORD WINAPI thread(LPVOID) { for (int i = 0; i < 10; i++) { a[i] = i + 1; // отмечаем, что один элемент готов ReleaseSemaphore(hSemaphore,1,NULL); SleepE00); } return 0; } int mainO { 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 GetLastError(); // создаем поток, который готовит элементы массива 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++)
142 Часть //. Синхронизация потоков и процессов { WaitForSingleObject(hSemaphore/ INFINITE); cout « a[i] « " \a" « flush; } cout « endl; CloseHandle(hSemaphore); CloseHandle(hThread); return 0; } Может возникнуть следующий вопрос: почему для решения этой задачи используется именно считающий семафор и почему его максимальное значение равно 10? Конечно, поставленную задачу можно было бы решить и другими способами. Но дело в том, что считающие семафоры предназначены именно для решения подобных задач. Считающие семафоры используются для синхронизации доступа к однотипным ресурсам, которые производятся одним или несколькими потоками, а потребляются другими — одним или несколькими потоками. В этом случае значение считающего семафора равно количеству произведенных ресурсов, а его максимальное значение устанавливается равным максимально возможному количеству таких ресурсов. При производстве единицы ресурса значение семафора увеличивается на единицу, а при потреблении единицы ресурса значение семафора уменьшается на единицу. В нашем примере ресурсами являются элементы массива, заполненные потоком thread, который является производителем этих ресурсов. В свою очередь, поток main является потребителем этих ресурсов — он выводит их на консоль. Так как в общем случае мы не можем сделать предположений о скоростях работы параллельных потоков, то максимальное значение считающего семафора должно быть установлено в максимальное количество производимых ресурсов. Если поток-потребитель ресурсов работает быстрее чем поток-производитель ресурсов, то, вызвав функцию ожидания считающего семафора, он вынужден будет ждать, пока поток-производитель не произведет очередной ресурс. Если же наоборот — поток-производитель работает быстрее чем поток-потребитель, то первый поток произведет все ресурсы и закончит свою работу, не дожидаясь, пока второй использует их. Такая синхронизация потоков-производителей и потребителей обеспечивает их максимально быструю работу.
Глава 7 Взаимоисключающий доступ к переменным 7.1. Атомарные операции Иногда параллельным потокам необходимо выполнять некоторые несложные действия над общими переменными, исключая совместный доступ к этим переменным. Если в этом случае для синхронизации доступа к переменным использовать критические секции или мьютексы (см. разд. 6.1, 6.3), то может возникнуть такая ситуация, что затраты на синхронизацию потоков значительно превышают затраты на выполнение самих операций. Для решения этой проблемы используются специальные функции, которые выполняют несложные действия над переменными, блокируя при этом доступ к этим переменным со стороны других потоков. Такие функции называются блокирующими функциями (interlocked functions). Блокирующие функции выполняют одну или несколько элементарных операций, которые объединяются в одну неделимую операцию, которая в этом случае также называется атомарной операцией. Блокирующие функции могут использоваться потоками, выполняющимися в разных процессах, для взаимоисключающего доступа к переменным, расположенным в разделяемой этими процессами памяти. Процессы могут разделять общую память при помощи механизма отображения файлов в память, который описан в гл. 30. Атомарная операция обычно включает операцию, выполняющую некоторое действие, и, может быть, операцию сравнения, которая позволяет выполнять это действие при некотором условии. В качестве операций, которые выполняют действия, могут выступать операции замены значения переменной или арифметические операции. Такие операции характеризуют атомарную операцию и используются для ее названия. В операционных системах Windows блокирующие функции можно разбить на четыре группы, принимая во внимание типы характерных элементарных операций, которые они выполняют. Эти блокирующие функции рассмотрены в разделах этой главы.
144 Часть II. Синхронизация потоков и процессов Заметим, что все блокирующие функции требуют, чтобы адреса переменных были выровнены на границу слова, т. е. были кратны 32. Для такого выравнивания адреса достаточно, чтобы переменная была объявлена в программе СО спецификатором ТИПа long, ИЛИ unsigned long, ИЛИ ОДНИМ ИЗ ИХ СИНОНИМОВ LONG, ULONG ИЛИ DWORD. 7.2. Замена значения переменной Для замены значения переменной в операционных системах Windows используется блокирующая функция mteriockedExchange, которая имеет следующий прототип: LONG InterlockedExchange( LPLONG lpTarget, // адрес переменной, значение которой заменяется LONG lvalue // новое значение переменной ); Эта функция предназначена для замены значения переменной, адрес которой задан параметром lpTarget, на новое значение, которое задано параметром lvalue. Значение, возвращаемое функцией InterlockedExchange, равно старому значению изменяемой переменной. ( Замечание ^ Адрес lpTarget, который указывает на изменяемую переменную, должен быть выровнен на границу слова, т. е. должен быть кратен 32. В ЛИСТИНГе 7.1 приведен Пример использования фуНКЦИИ InterlockedExchange для взаимоисключающей замены значения переменной. В этой программе поток producer производит товары — целые числа, а поток consumer потребляет эти товары. После того как товар произведен, он помещается потоком producer в контейнер, роль которого выполняет переменная п. Чтобы потребить товар, поток consumer должен забрать его из контейнера. Требуется, чтобы операции загрузки нового товара в контейнер и извлечение товара из контейнера не прерывали друг друга. Для выполнения этого требования В программе И ИСПОЛЬЗуетСЯ функция InterlockedExchange. j Листинг 7Л. Пример использования функции int«rlockec№xchange ttinclude <windows.h> #include <iostream.h> volatile long n;
Глава 7. Взаимоисключающий доступ к переменным 145 void producer() { long goods = 0; for (;;) { ++goods; // производим новое число InterlockedExchange((long*) &n, goods); // помещаем число в контейнер SleepA50); } } void consumer() { long goods; for (;;) { SleepD00); InterlockedExchange(&goods, n); // извлекаем число из контейнера cout « "Goods are consumed: " « goods « endl; } } int main() { HANDLE hThread_p/ hThread_c; DWORD IDThread_p/ IDThread_c; cout « "Press any key to terminate threads." « endl; // создаем потоки hThread_p = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)producer, NULL, 0, &IDThread_p); if (hThread_p == NULL) . return GetLastError(); hThread_c = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)consumer, NULL, 0, &IDThread_c); if (hThread_c == NULL) return GetLastError(); cin.getO ;
146 Часть II. Синхронизация потоков и процессов // прерываем выполнение потоков TerminateThread(hThread_p, 0); TerminateThread(hThread_c/ 0); // закрываем дескрипторы потоков С1os eHandle(hThread_c); CloseHandle(hThread_p); return 0; } 7.3. Условная замена значения переменной Для условной замены значения переменной в операционных системах Windows ИСПОЛЬЗуетсЯ блокирующая функция InterlockedCompareExchange, которая имеет следующий прототип: PVOID InterlockedCompareExchange( PVOID *Destination, // адрес переменной, значение которой заменяется PVOID Exchange, // новое значение переменной PVOID Comperand // значение для сравнения ); Эта функция предназначена для замены значения переменной, адрес которой задан параметром Destination, на новое значение, которое задано параметром Exchange, при условии, что старое значение переменной равно значению, заданному параметром comperand. Значение, возвращаемое функцией InterlockedCompareExchange, paBHO старому значению изменяемой переменной. ( Замечание j Адреса всех переменных, используемых функцией InterlockedCompareExchange, должны быть выровнены на границу слова, т. е. должны быть кратны 32. В листинге 7.2 приведен пример использования функции InterlockedCompareExchange ДЛЯ УСЛОВНОЙ замены значения переменной. Эта программа отличается от программы из листинга 7.1 только тем, что поток producer будет помещать новое число в контейнер только в том случае, если поток consumer освободил контейнер от старого числа.
# include <windows.h> #include <iostream.h> volatile long n; void producer() { long goods = 0; for (;;) { ++goods; // производим новое число // помещаем число в контейнер, если он пустой Inter lockedCoirpareExchange ( (PVOID*) &n, (PVOID) goods, 0) ; SleepA50); } void consumer() { long goods; for (;;) { SleepD00) ; goods = n; // извлекаем число из контейнера InterlockedExchange((LONG*)&n, 0); // отмечаем, что контейнер пустой cout « "Goods are consumed: " « goods « endl; } } int mainO { HANDLE hThread_p, hThread_c; DWORD IDThread_p/ IDThread_c; cout « "Press any key to terminate threads." « endl; // создаем потоки
148 Часть II. Синхронизация потоков и процессов hThread_p = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) producer, NULL, 0, &IDThread_p); if (hThread_p == NULL) return GetLastError(); hThread_c = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) consumer, NULL, 0, &IDThread_c); if (hThread_c == NULL) return GetLastError(); cin.get(); // прерываем выполнение потоков TerminateThread(hThread_p, 0) ; TerminateThread(hThread_c, 0); // закрываем дескрипторы потоков CloseHandle(hThread_c); CloseHandle(hThread_p); return 0; } 7.4. Инкремент и декремент переменной Для изменения значения переменной на единицу в операционных системах Windows используются следующие блокирующие функции: Interlockedlncrement И InterlockedDecrement. ФУНКЦИЯ Interlockedlncrement имеет следующий проТОТИП! LONG Interlockedlncrement( LPLONG IpAddend // адрес переменной для инкремента ); Эта функция предназначена для увеличения значения переменной, адрес которой задан параметром IpAddend, на единицу. Значение, возвращаемое функцией interlockedlncrement, равно новому значению изменяемой переменной. ФуНКЦИЯ InterlockedDecrement Имеет следующий ПроТОТИП: LONG InterlockedDecrement( LPLONG IpAddend // адрес переменной для декремента
Глава 7. Взаимоисключающий доступ к переменным 149 Эта функция предназначена для уменьшения значения переменной, адрес которой задан параметром ipAddend, на единицу. Значение, возвращаемое функцией interiockedDecrement, равно новому значению изменяемой переменной. ( Замечание ^ В этих функциях адрес изменяемой переменной, который задается параметром lpTarget, должен быть выровнен на границу слова, т. е. должен быть кратен 32. В ЛИСТИНГе 7.3 Приведен Пример использования функции Interlockedlncrement для взаимоисключающего увеличения значения переменной на 1. Эта программа отличается от программы из листинга 7.1 только тем, что поток producer корректирует значение числа прямо в контейнере, не используя для этого локальную переменную. | Листинг 7,3, Пример использования функции interlockedlncrearant # inc lude <windows . h> #include <iostream.h> volatile long n; void producer() { for (;;) { Interlockedlncrement((long*)&n); /* изменяем число в контейнере */ SleepA50); } } void consumer() { long goods; for (;;) { SleepD00); InterlockedExchange(&goods, n); // извлекаем число из контейнера cout « "Goods are consumed: " « goods « endl;
150 Часть II. Синхронизация потоков и процессов } } int main() { HANDLE hThread_p, hThread_c; DWORD IDThread_p, IDThread_c; cout « "Press any key to terminate threads." « endl; // создаем потоки hThread_p = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) producer, NULL, 0, &IDThread_p); if (hThread_p == NULL) return GetLastError(); hThread_c = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) consumer, NULL, 0, &IDThread_c); if (hThread_c == NULL) return GetLastError(); cin.getO ; // прерываем выполнение потоков TerminateThread(hThread_p, 0); TerminateThread(hThread_c, 0); // закрываем дескрипторы потоков С1oseHandle(hThread_c); CloseHandle(hThread_p); return 0; } 7.5. Изменение значения переменной Для изменения значения переменной в операционных системах Windows ИСПОЛЬЗуется блокирующая функция InterlockedExchangeAdd, Которая ИМе- ет следующий прототип: LONG InterlockedExchangeAdd ( LPLONG lpAddend, // адрес переменной, значение которой изменяется
Глава 7. Взаимоисключающий доступ к переменным 151 LONG Increment // прибавляемое значение ); Эта функция прибавляет значение, заданное параметром increment, к переменной, адрес которой задан параметром IpAddend. Значение, возвращаемое функцией InteriockedExchangeAdd, равно старому значению изменяемой переменной. ( Замечание ^ Адрес изменяемой переменной, который задается параметром IpAddend, должен быть выровнен на границу слова, т. е. должен быть кратен 32. В листинге 7.4 приведен пример использования функции InteriockedExchangeAdd для взаимоисключающего увеличения значения переменной. Эта программа отличается от программы из листинга 7.3 только тем, что значение переменной п увеличивается не на 1, а на 10. : Листинг 7.4- Пример использования функции InteriockedExchangeAdd #include <windows.h> #include <iostream.h> volatile long n; void producer() { for (;;) { /* изменяем число в контейнере */ InteriockedExchangeAdd((long*)&n, 10); SleepA50); } } void consumer() { long goods; for (;;) { SleepD00);
152 Часть II. Синхронизация потоков и процессов InterlockedExchange(&goods, n) ; // извлекаем число из контейнера cout « "Goods are consumed: " « goods « endl; } } int main() { HANDLE hThread_p, hThread_c; DWORD IDThread_p, IDThread_c; cout « " Press any key to terminate threads." « endl ; // создаем потоки hThread_p = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) producer, NULL, 0, &IDThread_p); if (hThread_p == NULL) return GetLastError(); hThread_c = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) consumer, NULL, 0, &IDThread_c); if (hThread_c == NULL) return GetLastError(); cin.getO ; // прерьгоаем выполнение потоков TerminateThread(hThread_p, 0); TerminateThread(hThread_c, 0); // закрываем дескрипторы потоков CloseHandle(hThread_c)/ CloseHandle(hThread_p); return 0;
Глава 8 Тупики 8.1. Определение тупиков Говорят, что поток находится в тупике (deadlock), если он ждет событие, которое никогда не произойдет. Событие может никогда не произойти по следующим двум причинам: П не существует потока, который оповещает о наступлении ожидаемого события; П поток, оповещающий о наступлении ожидаемого события, существует, но сам находится в тупике. Если в тупике находится хотя бы один из потоков процесса, то считается, что этот процесс также находится в тупике. Если мы рассматриваем систему, которая состоит из нескольких процессов, и хотя бы один из этих процессов находится в тупике, то и сама рассматриваемая система также находится в тупике. Например, рассмотрим два потока одного процесса: void thread_l( ) { EnterCriticalSection(&csl)/ cin » a; EnterCriticalSection(&cs2); x = a - b; LeaveCriticalSection(&cs2); LeaveCriticalSection(&csl); } void thread_2( ) { EnterCriticalSection(&cs2);
154 Часть II. Синхронизация потоков и процессов cin » b; EnterCriticalSection(&csl); у = а + b; LeaveCriticalSection(&csl); LeaveCriticalsection(&cs2); } Если после операции ввода (cin » а) в первом потоке выполнение этого потока прервется и управление будет передано второму потоку в функцию EnterCriticalSection(&cs2), ТО процесс ВОЙдет В ТуПИК. Это произойдет потому, что поток thread_i будет ждать освобождения входа в критическую секцию cs2, которая занята потоком thread_2, а поток thread_2 будет ждать освобождения входа в критическую секцию csi, которая занята потоком thread_i. Но эти события никогда не наступят. Таким образом, потоки thread_i и thread_2 блокируют друг друга. Процесс находится в тупике, т. к. каждый поток ждет ресурс, захваченный другим потоком, при этом не освобождая принадлежащие ему ресурсы. Аналогичная ситуация произойдет и в том случае, если после операции ввода (cin » ь) во втором потоке выполнение этого потока прервется и управление будет передано первому ПОТОКУ В фуНКЦИЮ EnterCriticalSection(&csl). Очевидно, что при разработке программ необходимо избегать ситуаций, которые могут привести к тупикам, т. к. в этом случае система становится неработоспособной. В следующих разделах мы рассмотрим способы обнаружения, предотвращения и восстановления после тупиков. 8.2. Классификация системных ресурсов Прежде чем разбираться с тупиками, нам необходимо более точно классифицировать ресурсы, используемые потоками, и рассмотреть специальные ориентированные графы, которые называются графами распределения ресурсов процесса. Этими вопросами мы сейчас и займемся. Системные ресурсы можно классифицировать по нескольким признакам, которые перечислены ниже. Если классифицировать ресурсы по количеству потоков, которые могут одновременно иметь доступ к ним, то получим следующие классы ресурсов: □ совместно используемые ресурсы, т. е. ресурсы, каждый из которых может использоваться одновременно несколькими потоками (например — файл); □ монопольные ресурсы, т. е. ресурсы, каждый из которых не может одновременно использоваться несколькими потоками (например — квант процессорного времени).
Глава 8. Тупики 155 Если классифицировать ресурсы по способу распределения этих ресурсов между потоками, то получим следующие классы ресурсов: П перераспределяемые ресурсы (preemptable) — те, которые могут быть отобраны у потока, владеющего этим ресурсом, и переданы другому потоку (например, страницы реальной памяти в системах с виртуальной памятью); П неперераспределяемые ресурсы (non-preemtable) — те, которые не могут быть отобраны у потока, владеющего ресурсом (например, принтер). Если классифицировать ресурсы по времени их существования, то получим следующие классы ресурсов: П повторно используемые ресурсы — те, которые после их освобождения одним потоком могут использоваться другим потоком (например, страницы виртуальной памяти); П потребляемые ресурсы — те, которые исчезают после их использования потоком (например, сообщения). Последние два класса ресурсов рассмотрим более подробно. Сначала остановимся на повторно используемых ресурсах. Как правило, к ним относятся физические ресурсы компьютера и файлы. Повторно используемые ресурсы имеют следующие свойства: П количество единиц ресурса постоянно; П ресурсы являются монопольными; П ресурсы не перераспределяются. Теперь перейдем к потребляемым ресурсам. Потребляемые ресурсы создаются потоками-производителями этих ресурсов, а используются потоками- потребителями. Причем после использования потребляемого ресурса он перестает существовать. Очевидно, что потребляемыми ресурсами могут быть только логические ресурсы компьютера. Потребляемые ресурсы характеризуются следующими свойствами: П количество единиц ресурса может изменяться; П ресурс может быть как монопольным, так и совместным; П ресурс может быть как перераспределяемым, так и неперераспреде- ляемым. Примером совместно используемого потребляемого ресурса может быть сообщение, которое предназначено для нескольких процессов. Примером перераспределяемого потребляемого ресурса является квант процессорного времени, который выделяется потоку для работы. Так как поток может быть прерван другим потоком с более высоким приоритетом, то оставшееся от кванта время будет передано новому потоку.
156 Часть II. Синхронизация потоков и процессов 8.3. Обнаружение тупиков После классификации ресурсов можно перейти к обнаружению тупиков. Для этого определим граф распределения ресурсов процесса. Это такой ориентированный граф, вершины которого обозначают потоки, а дуга, изображенная на рис. 8.1, интерпретируется следующим образом: □ в случае повторно используемых ресурсов: поток Tj запрашивает ресурс Л, занятый потоком Т\\ П в случае потребляемых ресурсов: поток Т2 использует ресурс Л, произведенный ПОТОКОМ Т\. ©-J-0 Рис. 8.1. Дуга графа распределения ресурсов процесса Сначала рассмотрим процесс, в котором потоки используют только повторно используемые ресурсы. В этом случае поток такого процесса находится в тупике, если он бесконечно долго ждет ресурс, захваченный другим потоком. Критерием для обнаружения тупика в случае повторно используемых ресурсов является следующая теорема. Теорема 1 Процесс, потоки которого используют только повторно используемые ресурсы, находится в тупике тогда и только тогда, когда граф распределения ресурсов этого процесса содержит цикл. Доказательство Докажем необходимость. Для этого рассмотрим процесс, который находится в тупике. Построим граф G распределения ресурсов этого процесса. Предположим противное, т. е., что этот граф не имеет циклов. Возьмем произвольную вершину Г графа Си найдем все пути, началом которых служит эта вершина. По предположению каждый из этих путей не образует цикл. Следовательно, не исключена возможность следующей работы потоков. Потоки, которые являются концами путей, завершают свою работу и освобождают все захваченные ими ресурсы. Тогда будут разблокированы потоки, которые связаны с концевыми вершинами. Повторяя эти рассуждения, в конце концов, получим, что будет разблокирован поток, представленный вершиной Г. Получили противоречие с условием, что рассматриваемый процесс находится в тупике. Следовательно, наше предположение неверно и в графе распределения ресурсов есть цикл.
Глава 8. Тупики 157 Теперь докажем достаточность. Для этого предположим, что граф распределения ресурсов процесса содержит цикл: Tj, Г2, ..., Тп. Следовательно, поток Т^ будет заблокирован до тех пор, пока ресурс, требуемый этим потоком, не будет освобожден потоком Т\. Повторяя эти рассуждения и учитывая транзитивность блокировки потоков, получим, что поток Тп будет заблокирован до тех пор, пока поток Т\ не освободит ресурс, требуемый потоком 72. Но этого не произойдет никогда, т. к. в свою очередь поток Т\ ждет ресурс, захваченный заблокированным потоком Тп. Следовательно, рассматриваемый процесс находится в тупике. Теорема доказана. Например, если потоки, рассмотренные в разд. 8.1, находятся в тупике, то граф распределения ресурсов процесса может быть таким, как это показано на рис. 8.2. Здесь под ресурсами понимаются критические секции, освобождения которых ждут потоки. Рис. 8.2. Граф распределения ресурсов процесса (для тупика) © © а) узел б) цикл, но не узел Рис. 8.3. Примеры узла и цикла в графе потребляемых ресурсов процесса
158 Часть II. Синхронизация потоков и процессов Теперь рассмотрим процесс, в котором потоки используют только потребляемые ресурсы. Пусть G — граф распределения ресурсов такого процесса. Введем понятие узла. Подграф Н графа G называется узлом, если выполняются следующие условия: □ через все вершины подграфа Я проходит цикл; □ для каждой дуги, связывающей вершину из графа G, не входящую в подграф Я, с вершиной из подграфа Я, вершина из подграфа Я является началом этой дуги. На рис. 8.3 приведены примеры узла и цикла в графе потребляемых ресурсов процесса. Теперь можно изложить критерий тупика для процесса, который использует только потребляемые ресурсы. Теорема 2 Процесс, потоки которого используют только потребляемые ресурсы, находится в тупике тогда и только тогда, когда граф распределения ресурсов этого процесса содержит узел. Доказывается эта теорема по аналогии с теоремой 1. Из двух приведенных теорем следует, что для обнаружения тупика нужно построить граф распределения ресурсов процесса и проверить, содержит ли этот граф цикл для случая повторно используемых ресурсов или узел, для случая потребляемых ресурсов. 8.4, Восстановление заблокированного процесса После обнаружения тупика должно быть выполнено восстановление процесса, которое заключается в разблокировании потоков этого процесса. Существуют три подхода к восстановлению заблокированного процесса: □ прекращение работы заблокированных потоков (termination); □ перераспределение ресурсов (preemption); □ откат на контрольную точку (rollback). Рассмотрим каждый из этих подходов подробнее. Самым простым решением проблемы тупика является прекращение работы всех заблокированных потоков процесса и освобождение всех ресурсов, захваченных этими потоками. Этот подход к разблокированию процесса является самым плохим, т. к. в этом случае нарушается целостность обрабатываемой информации. Это происходит потому, что очень сложно обеспечить
Глава 8. Тупики 159 корректность состояния ресурсов в случае аварийного завершения работы одного потока другим потоком. Однако этот подход может применяться, если потоки обрабатывают только локальные данные и аварийное завершение этих потоков не вызовет нарушения целостности и непротиворечивости глобальных данных. Если это возможно, то лучше всего разблокировать процесс путем перераспределения ресурсов между потоками этого процесса. То есть забрать ресурс у одного из заблокированных потоков и передать его другому заблокированному потоку, который ждет этот ресурс. В этом случае все потоки, за исключением одного, продолжат свою работу. Кроме того, поток, у которого забрали ресурс, также продолжит свою работу, после того как этот ресурс станет свободным. Этот подход является самым лучшим с концептуальной точки зрения, но он не всегда может быть реализован на практике по той причине, что не каждый ресурс является перераспределяемым. Третий подход, использующий откат на контрольную точку, используется наиболее часто и является компромиссом между двумя первыми подходами. При использовании отката на контрольную точку должны быть решены две проблемы: □ корректное освобождение ресурсов, захваченных заблокированными потоками; П восстановление контекста потока на момент, предшествующий тупику. Для реализации этого подхода в программе устанавливаются такие точки, в которых запоминается состояние контекста потока. Эти точки называются контрольными точками потока. Изменение контекста потока между двумя контрольными точками называется транзакцией (tranzaction). Транзакция может быть зафиксирована или отменена. Отмена транзакции называется откатом (rollback). При откате контекст потока возвращается на контрольную точку, в которой не было тупика. При этом выполняется освобождение ресурсов, захваченных после прохождения контрольной точки, и восстановление контекста потока на момент прохождения контрольной точки. Если транзакция зафиксирована, то откат невозможен. Очевидно, что после выполнения отката одного из заблокированных потоков остальные заблокированные потоки получат доступ к освобожденным этими потоками ресурсам и, следовательно, будут разблокированы. Следует отметить, что при использовании откатов на контрольную точку нужно также решить, для какого из заблокированных потоков выполнить откат. Подход к решению этого вопроса зависит от работы, которую выполняют потоки. Например, можно выполнить откат заблокированного потока, который меньше всего захватил ресурсов после прохождения последней контрольной точки.
160 Часть И. Синхронизация потоков и процессов 8.5. Предотвращение тупиков Для обнаружения тупиков необходимо исследовать графы распределения ресурсов процесса, а затем в случае обнаружения тупика выполнить восстановление потоков, находящихся в тупике. Эта задача является довольно трудоемкой, поэтому лучше предпринимать некоторые стратегии при захвате ресурсов, которые позволяют избегать появления тупиков. Рассмотрим некоторые из этих стратегий и отметим недостатки каждой из них. Первая стратегия, направленная на предотвращение тупиков, заключается в следующем: поток должен захватывать сразу все необходимые ему для работы ресурсы и только потом начинать свою работу. Очевидно, что в данном случае возникновение тупика невозможно, т. к. поток не будет запрашивать дополнительных ресурсов, а после окончания своей работы освободит все захваченные им ресурсы. Но данная стратегия имеет и свои недостатки: □ неэффективное использование ресурсов компьютера, т. к. они блокируются для использования другими потоками. То есть может возникнуть такая ситуация, что поток захватывает все необходимые ему для работы ресурсы, но не все эти ресурсы он использует одновременно. В этом случае ресурсы простаивают, хотя могли бы использоваться другими потоками; □ не всегда возможно определить полную потребность в ресурсах, необходимых для работы потока. Вторая стратегия, направленная на предотвращение тупиков, заключается в следующем: если в процессе работы потоку требуется дополнительный ресурс, но он получает отказ на захват этого ресурса, то поток должен освободить все принадлежащие ему ресурсы. Очевидно, что в этом случае возникновение тупиков также невозможно, т. к. все ресурсы могут быть освобождены. Эта стратегия более предпочтительна, чем первая, т. к. не мешает эффективному использованию ресурсов. Но и она имеет один недостаток, а именно — системные затраты на откат потока и его повторный рестарт могут быть очень большими. Нет никакой гарантии того, что поток, запросивший некоторый ресурс и не получивший его сразу или по истечении некоторого конечного интервала времени, находится в тупике. Поэтому могут быть фальшивые откаты и рестарты потоков и процессов, что также снижает производительность системы в целом. Третья стратегия, направленная на предотвращение тупиков, заключается в следующем: все ресурсы линейно упорядочиваются по типам, а в процессе своей работы поток может захватывать только те ресурсы, тип которых больше типа уже используемых им ресурсов. Эта стратегия также позволяет избегать тупиков, т. к. однотипные ресурсы захватываются все сразу, что исключает блокировку по каждому из типов ресурсов. Использование этой стратегии обычно не снижает эффективность использования ресурсов и позволяет избежать откатов и рестартов потоков. Но она также имеет свой
Глава 8. Тупики 161 недостаток. Эта стратегия хорошо подходит потокам, для которых потребность в ресурсах может быть определена до запуска этого потока. Но она совершенно не пригодна для потоков, которые определяют потребность в ресурсах динамически в процессе своей работы. К сожалению, в системном программировании на практике чаще всего встречается именно такая ситуация, когда поток заранее не знает, сколько ресурсов определенного типа ему потребуется. В противном случае это, как правило, не системная, а прикладная программа, которая не обладает универсальностью. Как видно из вышесказанного, не существует общей стратегии для предотвращения тупиков. Каждая из стратегий имеет свои достоинства и недостатки. Кроме того, слепое следование этим стратегиям приводит к неэффективной работе системы. При разработке систем, как правило, используют все из этих стратегий, но не в ущерб производительности системы. Поэтому в современных системах также возможны тупики и с этим приходится мириться. 8.6. Безопасное завершение потоков в Windows После обнаружения тупика должно быть выполнено восстановление процесса, которое заключается в разблокировании потоков этого процесса. Как уже было сказано, восстановление невозможно без рестарта, по крайней мере, одного из заблокированных потоков. При этом возникает следующая задача оповещения потока об освобождении захваченных им ресурсов. Рассмотрим, как эту задачу можно решить средствами операционных систем Windows. Для этого в программе нужно определить дополнительные события, которые сообщают потоку о том, что ему нужно сделать: освободить ресурсы и закончить свою работу или продолжить свое исполнение. Для иллюстрации этого подхода приведем пример программы, в которой выполняется разблокирование потоков. Прежде чем привести текст этой программы, кратко изложим суть ее работы. В рамках процесса выполняются два потока main и marker. Поток marker заполняет целыми числами элементы целочисленного массива a [size] при условии, что элемент массива является пустым. Мы предполагаем, что вначале все элементы массива a [size] являются пустыми и инициализированы нулями. Заполнение элементов происходит следующим образом: поток marker генерирует положительное случайное число и находит остаток от деления этого числа на size, а затем заполняет элемент, индекс которого равен этому остатку. В качестве заполнителя используется единица. Ясно, что когда-нибудь поток marker войдет в тупик, т. к. может сгенерировать индекс уже заполненного элемента. Задача потока main — обнаружить этот тупик и разблокировать поток marker. Разблокирование потока marker выполняется следующим образом: или 6 Зак. 514
162 Часть II. Синхронизация потоков и процессов потоку marker разрешается продолжить свою работу, или он должен безопасно завершить свою работу. Под безопасным завершением работы потока мы понимаем корректное освобождение всех захваченных этим потоком ресурсов и завершение этого потока посредством вызова функции ExitThread. В листинге 8.1 приведен текст программы, выполняющей указанные действия. i процесса после обнаружения тупика #include <windows.h> #include <iostream.h> #include <math.h> const int size = 10; // размерность массива int a[size]; // обрабатываемый массив HANDLE hDeadlock; // сигнал о тупике HANDLE hAnswer[2]; // для обработки тупика DWORD WINAPI marker (LPVOID) { int i ; DWORD dwValue; for (;;) { // вычисляем случайный индекс i = abs(randO) % size; // проверяем, занят ли элемент if (!a[i]) // нет, заполняем элемент a[i] = 1; else { // да, сигнализируем о тупике SetEvent(hDeadlock); // ждем ответа dwValue = WaitForMultipleObjects( 2, hAnswer, FALSE, INFINITE); if (dwValue == WAIT_FAILED)
Глава 8. Тупики 163 cerr « "Wait function failed." « endl; cerr « "Press any key to exit." « endl; cin.getO; return GetLastError(); } // вычисляем индекс сигнального объекта dwValue -= WAIT_OBJECT_0; swi tch (dwValue) { case 0: // продолжаем работу continue; case 1: // завершаем работу ExitThread(l); break; default: ExitThreadB); break; } } } } int main() { HANDLE hMarker; DWORD idMarker; // создаем событие, оповещающее о тупике hDeadlock = CreateEvent(NULL, FALSE, FALSE, NULL); // создаем события для обработки тупика hAnswer[0] = CreateEvent(NULL, FALSE, FALSE, NULL); hAnswer[l] = CreateEvent(NULL, FALSE, FALSE, NULL); // запускаем поток marker hMarker = CreateThread(NULL, 0, marker,NULL, 0, &idMarker) ; if (hMarker == NULL) return GetLastError(); for (;;)
164 Часть II. Синхронизация потоков и процессов char с; // ждем сигнал о тупике WaitForSingleObject(hDeadlock, INFINITE) ; // выводим на консоль текущее состояние массива cout « "Current state of the array: "; for (int i = 0; i < size; ++i) cout « a[i] « ' ' ; cout « endl; // завершать или нет поток marker? cout « "Input 'у' to continue: "; cin » c; if (с == 'y') SetEvent(hAnswer[0]) ; // продолжаем работу else { SetEvent(hAnswer[1]); // завершаем работу break; } } WaitForSingleObject(hMarker, INFINITE); CloseHandle(hMarker); return 0; } Обратим внимание на обработку в этой программе ситуации, когда возникает тупик. В этом случае мы используем для обработки тупика два события, которые сигнализируют потоку marker о завершении или продолжении работы. Второй момент касается заполнения элементов массива целыми числами. В нашем случае у нас фактически работает только один поток marker, а поток main просто ждет до тех пор, пока поток marker войдет в тупик. Поэтому для проверки значений элементов массива a [size] поток marker просто выполняет сравнения значений элементов этого массива с нулем. Если бы параллельно работало несколько потоков marker, то для выполнения этих действий ПрИШЛОСЬ бы ИСПОЛЬЗОВать фунКЦИЮ InterlockedCompareExchange.
Часть III Программирование консольных приложений Глава 9. Структура консольного приложения Глава 10. Работа с консолью Глава 11. Работа с окном консоли Глава 12. Работа с буфером экрана Глава 13. Ввод-вывод на консоль
Глава 9 Структура консольного приложения 9.1. Структура консоли Консолью называется интерфейс, который используется приложением для ввода-вывода текстовой информации. В этом случае приложение, которое использует консоль для обмена данными с пользователем, называется консольным приложением. Консольные приложения применяются главным образом в системном программировании для разработки различных сервисов и для обработки неустранимых ошибок, возникающих при работе графического приложения. Консоль состоит из одного входного буфера и одного или нескольких буферов экрана. Входной буфер содержит информацию о событиях ввода. Каждое событие ввода описывается записью. Все записи упорядочены в очередь, которая хранится в буфере ввода. Буфер экрана содержит информацию для вывода в окно приложения и является двумерным массивом, который содержит символы и данные о цвете. Консоль обеспечивает два уровня ввода-вывода текстовой информации: высокий и низкий. Функции высокого уровня обеспечивают ввод-вывод символов с консоли, игнорируя остальные события. Функции низкого уровня обеспечивают обработку всех событий, связанных с консольным приложением. 9.2. Входной буфер консоли Входной буфер консоли содержит очередь записей, которые описывают события ввода. События ввода подразделяются на следующие категории: □ ввод с клавиатуры; □ ввод с мыши; □ изменение размеров окна;
168 Часть III. Программирование консольных приложений П изменение фокуса ввода; □ события, связанные с меню. Два последних события ввода, связанные с фокусом ввода и меню, обрабатываются системой и должны игнорироваться приложением. Каждому событию ввода в очереди сообщений соответствует запись типа input_record, которая имеет следующую структуру: typedef struct _INPUT_RECORD { WORD EventType; union { KEY_EVENT_RECORD KeyEvent; MOUSE_EVENT_RECORD MouseEvent; WINDOW_BUFFER_SIZE_RECORD WindowBuf f erSizeEvent ; MENU_EVENT_RECORD MenuEvent; FOCUS_EVENT_RECORD Focus Even t; } Event; } INPUT_RECORD; где поле EventType определяет тип события ввода и может принимать одно из следующих значений: □ key_event — ввод с клавиатуры; □ mouse_event — ввод с мыши; □ window_buffer_size_event — изменение размеров окна; □ menu_event — событие, связанное с меню; □ focus_event — изменение фокуса ввода. В свою очередь объединение Event содержит запись только одного из перечисленных ТИПОВ В ЗаВИСИМОСТИ ОТ Значения ПОЛЯ EventType. Рассмотрим структуру записи для каждого события ввода. Запись для ввода с клавиатуры имеет следующую структуру: typedef struct _KEY_EVENT_RECORD { BOOL bKeyDown; WORD wRepeatCount; WORD wVirtualKeyCode; WORD wVirtualScanCode; union { WCHAR UnicodeChar; CHAR AsciiChar; } uChar;
Глава 9. Структура консольного приложения 169 DWORD dwControlKeyState; } KEY_EVENT_RECORD; Кратко опишем поля этой структуры: □ значение поля bKeyDown равно true, если клавиша нажата, в противном случае значение этого поля равно false; □ поле wRepeatcount содержит счетчик задержки при нажатии клавиши; □ поле wvirtuaiKeyCode содержит код клавиши, который не зависит от типа клавиатуры; □ поле wvirtuaiscancode содержит код клавиши, который генерируется клавиатурой. Значение этого поля зависит от клавиатуры, т. к. клавиатуры разных производителей могут генерировать различные коды при нажатии одинаковых клавиш; □ объединение uchar содержит символ, который соответствует нажатой клавише. Этот символ может быть представлен как в коде Unicode, так и в коде ASCII; □ поле dwControlKeyState содержит комбинацию флагов, которые описывают состояние управляющих клавиш. Эти флаги могут принимать следующие значения: • capslock_on — включен индикатор <Caps Lock>; • enhanced_key — дополнительные клавиши; • left_alt_pressed — нажата левая клавиша <Alt>; • left_ctrl_pressed — нажата левая клавиша <Ctrl>; • numlock_on — включен индикатор <Num Lock>; • right_alt_pressed — нажата правая клавиша <Alt>; • right_ctrl_pressed — нажата правая клавиша <Ctrl>; • scrolllock_on — включен индикатор <Scoll Lock>; • shift_pressed — нажата клавиша <Shift>. ( Замечание ) К дополнительным клавишам относится самый правый блок клавиш на клавиатуре, который обычно находится под индикаторами состояния клавиатуры. Кроме того, отметим, что автономное нажатие клавиши <Alt> имеет специальный смысл для системы и поэтому не передается приложению для обработки. Теперь перейдем к описанию событий, связанных с мышью. Запись для ввода с мыши имеет следующую структуру: typedef struct _MOUS_EVENT_RECORD { COORD dwMousePosition;
170 Часть III. Программирование консольных приложений DWORD dwButtonstate; DWORD dwControiKeyState; DWORD dwEventFiags; } MOUSE_EVENT_RECORD; поля которой имеют следующее назначение: □ поле dwMousePosition определяет координаты курсора относительно буфера экрана; □ поле dwButtonstate содержит флаги, которые определяют состояние кнопок мыши. Эти флаги описываются следующими символическими константами в порядке их следования по битам, начиная с младшего бита: • from_left_ist_button_pressed — нажата самая левая кнопка мыши; • rightmost_button_pressed — нажата самая правая кнопка мыши; • from_left_2nd_button_pressed — нажата вторая слева кнопка мыши; • from_left_3rd_button_pressed — нажата третья слева кнопка мыши; • from_left_4th_button_pressed — нажата четвертая слева кнопка мыши; □ поле dwControiKeyState определяет состояние управляющих клавиш и может принимать те же значения, что и соответствующее поле в записи key_event_record; □ поле dwEventFiags отмечает тип события и может принимать одно из следующих значений: • о — кнопка мыши нажата или отпущена; • double_click — кнопка мыши нажата второй раз, первое нажатие отмечается о; • mouse_moved — изменение позиции мыши; • MOUSE_WHEELED — КруТИТСЯ КОЛеСО МЫШИ. ( Замечание ^ Значение mouse_wheeled используется, начиная только с версии Windows 2000. В заключение этого раздела рассмотрим события, которые связаны с изменением размеров окна. Записи, описывающие такие события, имеют следующую структуру: typedef struct _WINDOW_BUFFER_SIZE_RECORD { COODR dwSize; } WINDOW_BUFFER_SIZE_RECORD; где поле dwSize определяет новый размер буфера экрана в символах.
Глава 9. Структура консольного приложения 171 9.3. Буфер экрана Буфер экрана является двумерным массивом, элементы которого представляют собой записи типа: typedef struct _CHAR_INFO { union { WCHAR UnicodeChar; CHAR AsciiChar; } Char; WORD Attributes; } CHAR_INFO, *PCHAR_INFO; где объединение char содержит символ, представленный в коде Unicode или ASCII, а поле Attributes определяет цвет фона и цвет текста, которыми выводятся символы на экран дисплея. Это значение может быть равно о, что обозначает фон — черный, а цвет — белый, или любой комбинации из следующих констант: □ BACKGROUND_BLUE — фон СИНИЙ; □ background_green — фон зеленый; □ BACKGROUND_RED — фон КраСНЫЙ; □ BACKGROUND_INTENSITY — фон ЯрКИЙ; □ FOREGROUND_BLUE — TeKCT СИНИЙ; □ FOREGROUND_GREEN — TeKCT ЗелеНЫЙ*, □ foreground_red — текст красный; □ FOREGROUND_INTENSITY — ТеКСТ ЯрКИЙ. Цвет фона и цвет текста будем называть атрибутами текста. Сделаем несколько замечаний относительно использования этих констант. Цвет фона и цвет текста выбираются как комбинация базовых цветов синего, зеленого и красного. То есть в этом случае используется цветовая модель RGB. Можно подсчитать, что всего существует семь возможных комбинаций из трех цветов. Белый цвет определяется комбинацией всех трех цветов. Если сюда добавить черный цвет, который определяется как побитовое отрицание белого цвета, то всего существует восемь возможных вариантов, как для цвета фона, так и для цвета текста. Теперь обсудим использование констант background_intensity и foreground_intensity, задающих яркость. Обычно если окно консольного приложения имеет фокус ввода, то фон и текст выводятся на экран яркими цветами, что и задается этими константами. Если же окно не имеет фокус ввода, то его фон и текст выводятся на экран приглушенными цветами.
Глава 10 Работа с консолью 10.1. Создание консоли Процесс может быть связан только с одной консолью. Новая консоль может создаваться одним из следующих двух способов. Первый способ заключается в том, что при создании консольного процесса КОМанДОЙ CreateProcess Нужно установить флаг CREATE_NEW_CONSOLE. От- метим, что в этом случае, если консольный процесс создается из консольного приложения, а указанный флаг не установлен, то новый процесс присоединяется к консоли родительского процесса. Более подробно функция CreateProcess рассмотрена в разд. 4.2. Второй способ заключается в использовании функции AiiocConsole, которая имеет следующий прототип: BOOL AiiocConsole(VOID); Эта функция возвращает ненулевое значение, если консоль создана успешно, и false — в противном случае. В обоих этих случаях заголовок окна консоли, его параметры, а также цвет фона и цвет текста задаются следующими полями структуры startupinfo: □ lpTitie — заголовок окна консоли; □ dwx, dwY — позиция левого угла окна консоли; □ dwXSize, dwYSize — размеры ОКНа; □ dwXCountChars, dwYCountChars — размеры буфера экрана; □ dwFiiiAttributes — цвет фона и цвет текста; □ wshowwindow — способ отображения окна при запуске приложения. Все эти параметры используются при запуске процесса только в случае, если в поле dwFiags этой же структуры установлены следующие управляющие флаги: □ STARTF_USEPOSITION — ИСПОЛЬЗОВаТЬ ПОЛЯ dwX, dwY;
Глава 10. Работа с консолью 173 □ STARTF_USESIZE — ИСПОЛЬЗОВаТЬ ПОЛЯ dwXSize, dwYSize*, □ STARTF_USECOUNTCHARS — ИСПОЛЬЗОВаТЬ ПОЛЯ dwXCountChars, dwYCountCharsJ □ STARTF_USEFILLATTRIBUTE — ИСПОЛЬЗОВаТЬ ПОЛе dwFillAttributes; □ STARTF_USESHOWWINDOW — ИСПОЛЬЗОВаТЬ ПОЛе wShowWindow. В противном случае значения этих полей игнорируются. Отметим, что в операционной системе Windows 98 при создании новой консоли используется только значение поля ipTitie, а остальные атрибуты консоли используются по умолчанию (например, черный цвет фона и белый цвет текста). В листингах 10.1, 10 2 приведены тексты программ, которые создают новую консоль. | ,nv,„w„, ,m ,v. my, „.».« ,.., »., „.«„«.„ ,,.;»,„«., ,„, ,..« v,„v,„s #include <windows.h> #include <iostream.h> int main() { char IpszAppNamel[] = "С:\\ConsoleProcess.exe"; STARTUPINFO si; PROCESS_INFORMATION pi; // заполняем поля структуры STARTUPINFO ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); si.IpTitie = "This is a new title"; si.dwX =200; // позиция левого угла окна консоли si.dwY = 200; si.dwXSize =300; // размеры окна si.dwYSize = 200; si.dwXCountChars = 100; // размер буфера экрана по горизонтали si.dwYCountChars = 100; // размер буфера экрана по вертикали // красные буквы на синем фоне si.dwFillAttribute = FOREGROUND_RED|FOREGROUND_INTENSITY| BACKGROUND_INTENSITY | BACKGROUND_BLUE ; // используем все параметры, что установили si.dwFlags = STARTF_USECOUNTCHARS | STARTFJJSEFILLATTRIBUTE|
174 Часть Hi Программирование консольных приложений STARTFJJSEPOSITION | STARTFJJSESHOWWINDOW | STARTFJJSESIZE; si.wShowWindow = SW_SHOWNORMAL; // запускаем процесс с новой цветной консолью if ( !CreateProcess(lpszAppNamel, NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi) ) { cout « "The new process is not created." « endl « "Check a name of the process." « endl « "Press any key to finish." « endl; cin.get(); return 0; } // закрываем дескрипторы процесса CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return 0; #include <iostream.h> int mainO { cout « "I am created." « endl; cout « "Press any key to finish."; cin.get(); return 0; } Теперь рассмотрим профамму, которая создает консоль, используя функцию AiiocConsoie. Параметры этой консоли берутся из структуры startupinfo, которую заполняет процесс, запускающий эту профамму.
Глава 10. Работа с консолью 175 В листингах 10.3, 10.4 приведены программы, которые демонстрируют создание НОВОЙ КОНСОЛИ При ПОМОЩИ фуНКЦИИ AllocConsole. #include <windows.h> #include <iostream.h> int main() { char IpszAppName [ ] = " С: \ \ConsoleProcess. exe" ; STARTUPINFO si; PROCESS_INFORMATION pi; // заполняем поля структуры STARTUPINFO ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); si.lpTitle = "This is a new title"; si.dwX = 200; // позиция левого угла окна консоли si.dwY = 200; si.dwXSize = 200; // размеры окна si.dwYSize = 100; si.dwXCountChars = 150; // размер буфера экрана по горизонтали si.dwYCountChars =50; // размер буфера экрана по вертикали // красные буквы на синем фоне si. dwFillAt tribute = FOREGROUND_RED | FOREGROUND_INTENSITY | BACKGROUND_BLUE | BACKGROUND_INTENSITY; // используем все параметры, что установили si.dwFlags = STARTF_USECOUNTCHARS|STARTF_USEFILLATTRIBUTE| STARTFJJSEPOSITION|STARTF_USESIZE; si.wShowWindow = SW_SHOWNORMAL ; // запускаем процесс, который сам распределяет консоль if (!CreateProcess(IpszAppName, NULL, NULL, NULL, FALSE, DETACHED_PROCESS, NULL, NULL, &si, &pi))
176 Часть III. Программирование консольных приложений cout « "The new process is not created." « endl « "Check a name of the process." « endl « "Press any key to finish." « endl; cin.get(); return 0; } // закрываем дескрипторы процесса CloseHandle(pi.hProcess); CloseHandle(pi.hThread); return 0; #include <windows.h> #include <conio.h> int main () { if (!AllocConsole()) { MessageBox(NULL, "Console allocation failed", "Ошибка Win32 API", MB_OK | MB_ICONINFORMATION ); return 0; } _cputs("I am created.") ; _cputs("\nPress any char to exit.\n"); _getch(); return 0;
Глава 10. Работа с консолью 177 10.2. Освобождение консоли Приложение освобождает консоль посредством вызова функции FreeConsole, которая имеет следующий прототип: BOOL FreeConsole(VOID); В случае успешного завершения эта функция возвращает ненулевое значение, а в противном случае — false. В листинге 10.5 приведена программа процесса, который запускается без консоли. В начале своей работы этот процесс распределяет консоль и затем выводит несколько сообщений. После этого процесс освобождает консоль посредством ВЫЗОВа функции FreeConsole. Листинг 10.5. Освобождение консоли функцией FreeConsole #include <windows.h> #include <conio.h> int main () { // распределяем консоль if (!AllocConsole()) { MessageBox(NULL, "Console allocation failed", "Ошибка Win32 API" MB_OK | MB_ICONINFORMATION ); return 0; } _cputs("I am created.\n"); _cputs("Press any char to exit.\n"); _getch(); // освобождаем консоль if (!FreeConsole()) { _cputs("Free console failed.\n"); &*
178 Часть III. Программирование консольных приложений _cputs("Press any key to exit.\n"); _getch(); } return 0; } 10.3. Стандартные дескрипторы ввода-вывода При создании новой консоли система создает три дескриптора, которые обозначаются stdin, stdout, stderr и называются соответственно стандартными дескрипторами ввода, вывода и ошибки. Дескриптор stdin связывается с буфером ввода, а дескрипторы stdout и stderr связываются с буфером экрана. Эти дескрипторы используются в функциях, предназначенных для работы с консолью. Значения стандартных дескрипторов ввода-вывода можно получить, используя функцию GetstdHandie, которая имеет следующий прототип: HANDLE GetstdHandie(DWORD dwStdHandle); где параметр dwstdHandie может принимать одно из следующих значений: □ std_input_handle — дескриптор стандартного ввода; □ std_output_handle — дескриптор стандартного вывода; □ std_error_handle — дескриптор стандартной ошибки. При успешном завершении функция GetstdHandie возвращает требуемый дескриптор, а в случае неудачи — значение invalid_handle_value. Установить значения стандартных дескрипторов ввода-вывода можно при помощи функции setstdHandie, которая имеет следующий прототип: BOOL SetstdHandie( DWORD dwStdHandle, // тип дескриптора HANDLE hHandie // новое значение дескриптора ); где параметр dwstdHandie принимает те же значения, что и в функции GetstdHandie, а параметр hHandie равен новому значению для стандартного дескриптора. При успешном завершении функция setstdHandie возвращает ненулевое значение, а в случае неудачи — false. Обычно функция setstdHandie используется для перенаправления потоков стандартного ввода-вывода. В этом случае дескрипторы стандартных пото-
Глава 10. Работа с консолью 179 ков ввода-вывода могут быть определены вызовом функции createFile, которая в качестве имени файла (параметр IpNameFiie) принимает одно из значений: conin$ или conout$. Остальные параметры устанавливаются следующим образом. Для входного потока conin$: □ dwDesiredAccess — устанавливается в generic_read; □ dwShareMode — устанавливается в file_share_read, если консоль наследуется; □ dwCreationDisposition — устанавливается В OPEN_EXISTING. Для выходного потока conout$: □ dwDesiredAccess — устанавливается в generic_write; □ dwShareMode — устанавливается в file_share_write, если консоль насле- дуется; □ dwCreationDisposition — устанавливается В OPEN_EXISTING. В Обоих случаях параметры dwFlagsAndAttributes И hTemplateFile ИГНОРИРУЮТСЯ, а параметр IpSecurityAttributes ИСПОЛЬЗуеТСЯ ТОЛЬКО ДЛЯ ТОГО, чтобы сделать консоль наследуемой. Более подробно перенаправление стандартного ввода/вывода будет рассмотрено в гл. 13, посвященной передаче данных между процессами при помощи анонимных каналов.
Глава 11 Работа с окном консоли 11.1. Получение дескриптора окна консоли Дескриптор ОКНа КОНСОЛИ МОЖНО ПОЛУЧИТЬ, ВЫЗВав фуНКЦИЮ GetConsoleWindow, которая имеет следующий прототип: HWND GetConsoleWindow (VOID) ; В случае успеха эта функция возвращает дескриптор окна консоли, а в случае неудачи — null. Неудача означает, что консоли у приложения нет. Отметим, что функция GetConsoleWindow работает только на платформе операционных систем Windows 2000/XP. В листинге 11.1 приведена программа, которая получает дескриптор окна КОНСОЛИ, ИСПОЛЬЗУЯ ДЛЯ ЭТОГО фуНКЦИЮ GetConsoleWindow. [^Л«о^нг^¥*11 ^оупучение дескриптора окна консоли #include <windows.h> #include <stdio.h> extern "С" WINBASEAPI HWND WINAPI GetConsoleWindow (); int main() { HWND hWindow = NULL; HDC hDeviceContext; HPEN hPen; HGDIOBJ hObject; // дескриптор окна // контекст устройства // дескриптор пера // дескриптор GDI объекта // получаем дескриптор окна
Глава 11. Работа с окном консоли 181 hWindow = GetConsoleWindow () ; if (hWindow == NULL) { printf("Get console window failed.\n"); return 1; } else printf("Cet console window is done.\n"); // получаем контекст устройства hDeviceContext = GetDC(hWindow); // создаем перо hPen = CreatePen(PS_SOLID, 10, RGB@, 255, 0) ) ; // устанавливает перо hObject = SelectObjееt(hDeviceContext, hPen); // рисуем линию MoveToEx(hDeviceContext, 100, 100, NULL); LineTo(hDeviceContext, 500, 100); // восстанавливает старый объект SelectObj ect(hDeviceContext, hobj ect); // освобождаем объекты DeleteObject(hPen); ReleaseDC(hWindow, hDeviceContext); return 0; } 11.2. Получение и изменение заголовка консоли ДЛЯ Чтения заголовка ОКНа КОНСОЛИ ИСПОЛЬЗуеТСЯ фуНКЦИЯ GetConsoleTitle, которая имеет следующий прототип: DWORD GetConsoleTitle( LPTSTR lpConsoleTitle, // адрес буфера для заголовка
182 Часть III. Программирование консольных приложений DWORD nSize // размер буфера для заголовка в символах ); В случае успеха эта функция возвращает длину строки с заголовком в символах, а в случае неудачи — 0. Для установки заголовка окна консоли используется функция setconsoieTitie, которая имеет следующий прототип: BOOL SetConsoleTitle( LPCTSTR lpConsoleTitle // указатель на строку с заголовком ); В случае успеха эта функция возвращает ненулевое значение, а в случае неудачи — false. В листинге 11.2 приведена программа, которая сначала читает заголовок окна консоли, используя функцию GetConsoleTitle, а затем изменяет его, ИСПОЛЬЗуя функцию SetConsoleTitle. ! Листинг 11.2. Чтение и изменение заголовка окна консоли # include <windows.h> #include <iostream.h> int main () { char ConsoleTitleBuffer[80]; // указатель на буфер с заголовком DWORD dwBufferSize = 80; // размер буфера для заголовка DWORD dwTitleSize; // длина заголовка // читаем заголовок консоли dwTitleSize = GetConsoleTitle(ConsoleTitleBuffer, dwBufferSize); // выводим на консоль результат cout « "Title length = " « dwTitleSize « endl; cout « "The window title = " « ConsoleTitleBuffer « endl; cout « "Input new title: "; cin.getline(ConsoleTitleBuffer, 80); // устанавливаем новый заголовок консоли if (!SetConsoleTitle(ConsoleTitleBuffer))
Глава 11. Работа с окном консоли 183 cout « "Set console title failed." « endl; cout « "The title was changed." « endl; cout « "Press any key to exit."; cin.getO ; return 0; } 11.3. Определение максимального размера окна Максимальный размер окна консоли можно определить, вызвав функцию GetLargestConsoleWindowSize, которая имеет следующий ПрОТОТИП! COORD GetLargestConsoleWindowSize( HANDLE hConsoleOutput // дескриптор буфера экрана ); В случае успеха эта функция возвращает в структуру следующего типа: typedef struct _COORD { SHORT X, SHORT Y, } COORD; Поле x содержит максимальное количество столбцов, а поле y — максимальное количество строк окна консоли в символах. Следует отметить, что х и y зависят от текущего размера шрифта и величины экрана. В случае неудачи функция GetLargestConsoleWindowSize возвращает в полях структуры COORD НУЛИ. В листинге 13.3 приведена программа, которая, используя функцию GetLargestConsoleWindowSize, определяет максимальные размеры окна консоли. Е Листинг 11.3. Определение максимальных размеров окна консоли # include <windows.h> #include <iostream.h> int main() { HANDLE hConsoleOutput; // дескриптор буфера экрана
184 Часть III. Программирование консольных приложений COORD coord; // координаты окна консоли // получаем дескриптор окна консоли hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE); if (hConsoleOutput == INVALID_HANDLE_VALUE) { cout « "Get standard handle failed." « endl; return GetLastError(); } // получаем максимальные размеры окна консоли coord = GetLargestConsoleWindowSize(hConsoleOutput); if (coord.X == 0 ScSc coord.Y == 0) { cout « "Get largest console window size failed." « endl; return GetLastError(); } cout « "Coordinate X = " « coord.X « endl; cout « "Coordinate Y = " « coord.Y « endl; cout « "Press any key to exit."; cin.get(); return 0; 11.4. Установка координат окна Установить окно консоли относительно буфера экрана можно вызовом функции setconsoiewindowinfo, которая имеет следующий прототип: BOOL Setconsoiewindowinfo( HANDLE hConsoleOutput, // дескриптор буфера экрана BOOL bAbsolut, // тип координат CONST SMALL_RECT *lpConsoleWindow // углы окна ); Параметры этой функции могут принимать следующие значения: □ параметр hConsoleOutput должен содержать дескриптор буфера экрана, который должен быть открыт в режиме generic_write (см. гл. 12)\
Глава 11. Работа с окном консоли 185 П в параметре bAbsoiute может быть установлено значение false или true. Если значение этого параметра равно false, to поля структуры small_rect определяют сдвиг буфера экрана относительно левого верхнего и правого нижнего углов текущего окна консоли. Если же значение параметра bAbsoiute равно true, to поля структуры small_rect задают абсолютное положение левого верхнего и правого нижнего углов окна консоли относительно буфера экрана; □ для задания левого верхнего и правого нижнего углов окна используются поля структуры small_rect, которая имеет следующий тип: typedef struct _SMALL_RECT { SHORT Left; // x - координата левого верхнего угла окна консоли SHORT Top; //у - координата левого верхнего угла окна консоли SHORT Right; // x - координата правого нижнего угла окна консоли SHORT Bottom; //у - координата правого нижнего угла окна консоли } SMALL_RECT; Координаты в структуре small_rect определяют ячейки буфера экрана. В случае успеха функция setconsoiewindowinfo возвращает ненулевое значение, а в противном случае — false. Неудачное завершение функции может быть вызвано неправильным заданием координат окна консоли. Например, координаты окна консоли превышают размер буфера экрана, или не выполняется условие Left > о, или не выполняется условие Left < Right и т. д. Отметим, что функция setconsoiewindowinfo используется для прокрутки буфера экрана в окне консоли в операционной системе Windows 98. В листинге 11.4 приведена программа, в которой устанавливаются абсолютные размеры окна консоли относительно буфера экрана. | Листинг 114» Установка абсолютных размеров окна консоли #include <windows.h> #include <iostream.h> int main() { HANDLE hStdOut; // дескриптор стандартного вывода SMALL_RECT sr; // прямоугольник окна // читаем дескриптор стандартного вывода hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); cout « "Set new window rectangle in characters." « endl « endl;
186 Часть III. Программирование консольных приложений // устанавливаем прямоугольник окна в символах cout « "Input left coordinate @-79): "; cin » sr.Left; cout « "Input top coordinate @-24): "; cin » sr.Top; cout « "Input right coordinate @-79): "; cin » sr.Right; cout « "Input bottom coordinate @-24): "; cin » sr.Bottom; // устанавливаем новое окно if (!SetConsoleWindowInfo(hStdOut, TRUE, &sr)) cout « "Set console window info failed." « endl; cin.getO ; cout « endl « "Press any key to exit."; cin.getO ; return 0; } Теперь, в листинге 11.5, приведем программу, в которой устанавливаются относительные размеры окна. Отметим, что в этой программе числовые пределы координат окна установлены для работы в операционной системе Windows 2000. В операционной системе Windows 98 координаты правого нижнего угла могут принимать только отрицательные значения. Листинг 1 i Ж Установка относительных размеров окна консоли \t:^^.^.-M^.~.l~.^;L.~.j£.~:.~.~. L.~ л ^-. !L --!_. — ; #include <windows.h> #include <iostream.h> int main () { HANDLE hStdOut; // дескриптор стандартного вывода SMALL_RECT sr; // прямоугольник окна // читаем дескриптор стандартного вывода hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); cout « "Set new window rectangle in characters. "<<endl«endl;
Глава 11. Работа с окном консоли 187 // устанавливаем прямоугольник окна в символах cout « "Input left coordinate @-79): "; cin » sr.Left; cout « "Input top coordinate @-24): "; cin » sr.Top; cout « "Input right coordinate (" « (sr.Left - 79) « ' ,' « sr.Left « "): cin » sr.Right; cout « "Input bottom coordinate (" « (sr.Top - 24) « ', ' « sr.Top « "): "; cin » sr.Bottom; // устанавливаем новое окно в относительных координатах if (!SetConsoleWindowInfo(hStdOut/ FALSE, &sr)) cout « "Set console window info failed." « endl; cin.getO ; cout « endl « "Press key to exit."; cin.getO ; return 0;
Глава 12 Работа с буфером экрана 12.1. Создание и активация буфера экрана Буфер экрана может быть создан посредством вызова функции CreateConsoleScreenBuffer, Которая имеет следующий ПРОТОТИП! HANDLE CreateConsoleScreenBuffer( DWORD dwDesiredAccess, // режимы доступа DWORD dwShareMode, // режимы разделения доступа CONST SECURITY_ATTRIBUTES *lpSecurityAttributes, // атрибуты зашиты DWORD dwFlags, // тип буфера экрана LPVOID IpScreenBufferData // зарезервировано ); В случае успешного завершения эта функция возвращает дескриптор нового буфера экрана, а в случае неудачи значение invalid_handle_value. Параметры этой функции имеют следующее назначение. В параметре dwDesiredAccess задаются флаги, которые определяет способ доступа к буферу экрана. Этот параметр может принимать любую комбинацию следующих значений: □ generic_read — процессу разрешается чтение данных из буфера экрана; □ generic_write — процессу разрешается запись в буфер экрана. Параметр dwShareMode определяет, может ли буфер экрана использоваться несколькими процессами одновременно. Если этот параметр равен 0, то создаваемый буфер экрана не может использоваться несколькими процессами одновременно. Кроме того, этот параметр может принимать любую комбинацию следующих значений: □ file_share_read — буфер экрана допускает совместное чтение; □ file_share_write — буфер экрана допускает совместную запись.
Глава 12. Работа с буфером экрана 189 Параметр IpSecurityAttributes задает атрибуты защиты. Пока он будет устанавливаться нами в null, что задает атрибуты защиты по умолчанию. Параметр dwFiags задает тип буфера экрана и может принимать только одно значение console_textmode_buffer. Параметр lpscreenBuf ferData зарезервирован для дальнейшего использования и поэтому должен быть установлен в null. После создания буфер экрана содержит пробелы, а курсор установлен в позицию с координатами @, 0). Чтобы сделать буфер экрана активным, т. е. выводить его содержимое на экран, нужно вызвать функцию setconsoieActiveScreenBuf f er, которая имеет следующий прототип: BOOL SetconsoieActiveScreenBuffer(HANDLE hConsoleOutput); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — значение false. Единственным параметром этой функции является дескриптор буфера экрана. В листинге 12.1 приведена программа, которая сначала создает второй буфер экрана, а затем этот буфер экрана делает активным. Отметим, что в этой программе используется функция writeconsoie для вывода в новый буфер экрана. Более подробно работа этой функции будет рассмотрена в разд. 13.1, посвященном выводу на высоком уровне. ГГ7" """ " v""~ w"v'"v """ v'"v --ч— ™ - - — — ^»— ,• • — [-Листинг 12.1, Создание и активация буфера экрана ,| #include <windows.h> #include <conio.h> int mainO { HANDLE hStdOutOld, hStdOutNew; // дескрипторы буфера экрана DWORD dwWritten; // для количества выведенных символов // создаем буфер экрана hStdOutNew = CreateConsoleScreenBuffer( GENERIC_READ | GENERICJWRITE, // чтение и запись 0, //не разделяемый NULL, // защита по умолчанию CONSOLE_TEXTMODE_BUFFER/ / текстовый режим NULL); //не используется if (hStdOutNew == INVALID_HANDLE_VALUE)
190 Часть III. Программирование консольных приложений _cputs("Create console screen buffer failed.\n"); return GetLastError(); } // сохраняем старый буфер экрана hStdOutOld = GetStdHandle(STD_OUTPUT_HANDLE) ; // ждем команду на переход к новому буферу экрана _cputs("Press any key to set new screen buffer active.\n"); _getch(); // делаем активным новый буфер экрана if (!SetConsoleActiveScreenBuffer(hStdOutNew)) { _cputs("Set new console active screen buffer failed.\n"); return GetLastError(); } // выводим текст в новый буфер экрана char text[] = "This is a new screen buffer."; if (!WriteConsole( hStdOutNew, // дескриптор буфера экрана text, // символы, которые выводим sizeof(text), // длина текста SdwWritten, // количество выведенных символов NULL)) //не используется _cputs("Write console output character failed.\n"); // выводим сообщение о вводе символа char str[] = "\nPress any key to set old screen buffer."; if (!WriteConsole( hStdOutNew, // дескриптор буфера экрана str, // символы, которые выводим sizeof(str), // длина текста SdwWritten, // количество выведенных символов NULL)) //не используется _cputs("Write console output character failed.\n") ; _getch(); // восстанавливаем старый буфер экрана
Глава 12. Работа с буфером экрана 191 if (ISetConsoleActiveScreenBuffer(hStdOutOld)) { _cputs("Set old console active screen buffer failed.\n"); return GetLastError(); } // пишем в старый буфер экрана _cputs("This is an old console screen buffer.\n"); // закрываем новый буфер экрана CloseHandle(hStdOutNew); // ждем команду на завершение программы _cputs("Press any key to finish.\n"); _getch(); return 0; } 12.2. Определение и установка параметров буфера экрана Параметры буфера экрана можно определить с помощью функции GetconsoieScreenBufferinfо, которая имеет следующий прототип: BOOL GetConsoleScreenBufferinfо( HANDLE hConsoleOutput, // дескриптор буфера экрана // указатель на строку параметров буфера экрана PCONSOLE_SCREEN_BUFFER_INFO lpConsoleScreenBufferinfо ); В случае успеха эта функция возвращает ненулевое значение, а в случае неудачи — false. При успешном завершении функция GetconsoieScreenBufferinfo возвращает в параметре IpCqnsoieScreenBuf ferinf о указатель на структуру следующего типа: typedef Struct _CONSOLE_SCREEN_BUFFER_INFO { COORD dwSize; // размер буфера экрана в символах (столбцы, строки) COORD dwCursorPosition; // координаты курсора в символах WORD wAttributes; // цвет фона и цвет текста SMALL_RECT; SrWindow; // левый верхний и правый нижний углы // окна относительно буфера экрана COORD dwMaximumWindowSize; // максимальный размер окна } CONSOLE_SCREEN_BUFFER_INFO;
192 Часть III. Программирование консольных приложений которая содержит параметры буфера экрана, заданного параметром hConsoleOutput. В листинге 12.2 приведена программа, которая выводит на экран информацию О буфере экрана, ИСПОЛЬЗуя функцию GetConsoleScreenBufferlnfo. ^^^^р^^^щпт^^т^-""" "—"г- " ищ^нг 12.2* Вывод на экран информации о буфере экрана #include <windows.h> #include <iostream.h> int mainO { HANDLE hStdOut; // дескриптор стандартного вывода CONSOLE_SCREEN_BUFFER_INFO csbi; // для параметров буфера экрана // читаем стандартный дескриптор вывода hStdOut = GetStdHandle(STD_OUTPUT_HAM)LE); // читаем параметры буфера экрана if (!GetConsoleScreenBufferlnfo(hStdOut,&csbi)) cout « "Console screen buffer info failed." « endl; cout « "Console screen buffer info: " « endl « endl; // выводим на консоль параметры выходного буфера cout « "A number of columns = " « csbi.dwSize.X « endl; cout « "A number of rows = " « csbi.dwSize.Y « endl; icout « "X cursor coordinate = " « csbi.dwCursorPosition.X « endl; cout « "Y cursor coordinate = " « csbi.dwCursorPosition.Y « endl; cout « "Attributes = " « hex « csbi.wAttributes « dec « endl; cout « "Window upper corner = " « csbi.srWindow.Left « "," « csbi.srWindow.Top « endl; cout « "Window lower corner = " « csbi.srWindow.Right « "," « csbi.srWindow.Bottom « endl; cout « "Maximum number of columns = " « csbi.dwMaximumWindowSize.X « endl; cout « "Maximum number of rows = " « csbi.dwMaximumWindowSize.Y « endl << endl; return 0;
Глава 12. Работа с буфером экрана 193 Размер буфера экрана можно изменить, вызвав функцию SetConsoleScreenBufferSize, КОТОрая имеет следующий ПРОТОТИП: BOOL SetConsoleScreenBufferSize ( HANDLE hConsoleOutput, // дескриптор буфера экрана COORD dwSize // новый размер буфера экрана ); В случае успеха эта функция возвращает ненулевое значение, а в случае неудачи — false. Размер буфера экрана задается в символах и не может быть меньше размера окна консоли. Минимальные размеры буфера экрана также ограничены системой и зависят от размера используемого шрифта и системных метрик sm_cxmin и sm_cymin. В листинге 12.3 приведена программа, которая изменяет размер буфера экрана. [Листинг 12,3, Установка новых размеров буфера экрана #include <windows.h> #include <iostream.h> int main () { COORD coord; // для размера буфера экрана HANDLE hStdOut; // дескриптор стандартного вывода // читаем дескриптор стандартного вывода hStdOut = GetStdHandle(STD_OUTPUT_HAMDLE); // вводим новый размер буфера экрана cout « "Enter new screen buffer size." << endl; cout « "A number of columns: "; сin » coord.X; cout « "A number of rows: "; сin » coord.Y; // устанавливаем новый размер буфера экрана if (!SetConsoleScreenBufferSize(hStdOut, coord)) { cout « "Set console screen buffer size failed." « endl; return GetLastError(); } return 0; 7 Зак. 514
194 Часть III. Программирование консольных приложений 12.3. Функции для работы с курсором Информацию о положении и видимости курсора можно получить, используя функцию GetconsoieCursorinfо, которая имеет следующий прототип: BOLL GetconsoieCursorinfо( HANDLE hConsoleOutput, // дескриптор буфера экрана PCONSOLE_CURSOR_INFO lpConsoleCursorlnfo // информация о курсоре ); В случае успеха эта функция возвращает ненулевое значение, а в случае Неудачи — FALSE. При успешном завершении фуНКЦИЯ GetconsoieCursorinf о возвращает в структуре типа console_cursor_info, адрес которой должен быть задан в параметре ipconsoiecursorinfo, информацию о курсоре. Эта структура имеет следующий формат: typedef struct _CONSOLE_CURSOR_INFO { DWORD dwSize; BOOL bVisible; } CONSOLE_CURSOR_INFO, * PCONSOLE_CURSOR_INFO ; Поле dwsize изменяется в интервале от 1 до 100 и определяет размер курсора в процентах от размера клетки для символа, а поле bvisibie определяет видимость курсора. Если значение поля bvisibie равно true, to курсор видим, в противном случае — невидим. Размер и видимость курсора устанавливаются функцией SetConsoleCursorlnfo," которая имеет следующий прототип: BOOL SetConsoleCursorlnfo ( HANDLE hConsoleOutput, // дескриптор буфера экрана // информация о курсоре CONST CONSOLE_CURSOR_INFO *lpConsoleCursorlnfo ); В случае успеха эта функция возвращает ненулевое значение, а в случае неудачи — значение false. При этом нужные значения размера и видимости курсора передаются через параметр ipconsoiecursorinfo, который указывает на структуру типа console_cursor_info, содержащую эти значения. В листинге 12.4 приведена программа, которая использует функции GetConsoleCursorlnfo И SetConsoleCursorlnfo ДЛЯ чтения И установки параметров курсора.
Глава 12. Работа с буфером экрана 195 #include <windows.h> #include <iostream.h> int main() { char с; HANDLE hStdOut; // дескриптор стандартного вывода CONSOLE_CURSOR_INFO cci; // информация о курсоре // читаем дескриптор стандартного вывода hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); // читаем информацию о курсоре if (!GetConsoleCursorInfo(hStdOut/ &cci)) cout « "Get console cursor info failed." « endl; // выводим информацию о курсоре cout « "Size of cursor in procents of char= " « cci.dwSize « endl; cout « "Visibility of cursor = " « cci.bVisible « endl; // читаем новый размер курсора cout « "Input a new size of cursor A-100): "; cin » cci.dwSize; // устанавливаем новый размер курсора if (!SetConsoleCursorInfo(hStdOut/ &cci)) cout « "Set console cursor info failed." « endl; cout « "Input any char to make the cursor invisible: "; cin » c; // делаем курсор невидимым cci.bVisible = FALSE; // устанавливаем невидимый курсор if (!SetConsoleCursorlnfо(hStdOut, &cci)) cout « "Set console cursor info failed." « endl; cout « "Input any char to make the cursor visible: "; cin » c; // делаем курсор невидимым
196 Часть III. Программирование консольных приложений cci.bVisible = TRUE; // устанавливаем видимый курсор if (!SetConsoleCursorInfo(hStdOut/ &cci)) cout « "Set console cursor info failed." « endl; return 0; } Позиция курсора в буфере экрана устанавливается функцией SetconsoieCursorPosition, которая имеет следующий прототип: BOOL SetconsoieCursorPosition( HANDLE hConsoleOutput, // дескриптор буфера экрана COORD dwCursorPosition // новая позиция курсора ); В случае успеха эта функция возвращает ненулевое значение, а в случае неудачи — значение false. Функция SetconsoieCursorPosition устанавливает курсор в позицию, заданную параметром dwCursorPosition. В листинге 12.5 приведена программа, которая демонстрирует использование функции SetconsoieCursorPosition. | Листинг 12.5» Установка курсора в новую позицию #include <windows.h> #include <iostream.h> int main() { char с; HANDLE hStdOut; // дескриптор стандартного вывода COORD coord; // для позиции курсора // читаем дескриптор стандартного вывода hStdOut = GetStdHandle(STD_OUTPUT_HAM)LE); cout << "Input new cursor position." << endl; cout « "X = cin » coord.X; cout « "Y = ";
Глава 12. Работа с буфером экрана 197 сin » coord.Y; // установить курсор в новую позицию if (!SetConsoleCursorPosition(hStdOut, coord)) { cout « "Set cursor position failed." « endl; return GetLastErrorO ; } cout « "This is a new position." « endl; cout « "Input any char to exit: "; cin » c; return 0; } 12.4. Чтение и установка атрибутов консоли Для установки атрибутов консоли используется функция FiiiconsoieOutputAttribute, которая имеет следующий прототип: BOOL FiiiconsoieOutputAttribute( HANDLE hConsoleOutput, // дескриптор буфера экрана WORD wAttributes, // цвет фона и цвет текста DWORD nLength, // количество заполняемых клеток COORD dwWriteCoord, // координаты первой клетки LPDWORD lpNumberOfAttrsWritten // количество заполненных клеток ); В случае успешного завершения возвращает ненулевое значение, а в случае неудачи — значение FALSE. Функция FiiiconsoieOutputAttribute заполняет клетки экрана, количество которых задано параметром nLength, атрибутами, которые заданы параметром wAttribute. Параметр dwwritecoord задает координаты первой заполняемой клетки, а параметр lpNumberOfAttrsWritten должен указывать на переменную типа dword, в которую функция FiiiconsoieOutputAttribute помещает количество заполненных клеток. Фактически функция FiiiconsoieOutputAttribute ИСПОЛЬЗуется ДЛЯ заполнения новыми атрибутами прямоугольных областей консоли. Программа из листинга 12.6 показывает, как заполнить всю консоль новыми атрибутами.
198 Часть III. Программирование консольных приложений #include <windows.h> #include <iostream.h> int main() { char c; HANDLE hStdOut; WORD wAttribute; DWORD dwLength; DWORD dwWritten; COORD coord; // дескриптор стандартного вывода // цвет фона и текста // количество заполняемых клеток // для количества заполенных клеток // координаты первой клетки CONSOLE_SCREEN_BUFFER_INFO csbi; // для параметров буфера экрана cout « "In order to fill console attributes, input any char: "; cin » c; // читаем стандартный дескриптор вывода hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); if (hStdOut == INVALID_HANDLE_VALUE) { cout « "Get standard handle failed." « endl; return GetLastErrorO; } // читаем параметры выходного буфера if (!GetConsoleScreenBufferInfo(hStdOut/ &csbi)) { cout « "Console screen buffer info failed." « endl; return GetLastErrorO; } " // вычисляем размер буфера экрана в символах dwLength = csbi.dwSize.X * csbi.dwSize.Y; // начинаем заполнять буфер с первой клетки coord.X = 0; coord.Y = 0; // устанавливаем цвет фона голубым, а цвет символов желтым wAttribute = BACKGROUND_BLUE | BACKGROUND_INTENSITY |
Глава 12. Работа с буфером экрана 199 FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY; // заполняем буфер атрибутами if (!FillConsoleOutputAttribute( hStdOut, // стандартный дескриптор вывода wAttribute, // цвет фона и текста dwLength, // длина буфера в символах coord, // индекс первой клетки SdwWritten)) // количество заполненных клеток { cout « «Fill console output attribute failed.» « endl; return GetLastError(); cout « «The fill attributes was changed.» « endl; return 0; } Для того чтобы установить атрибуты символов, которые пишутся в буфер экрана функциями writeFile и writeconsole, а также отображаются на экране При ЧТеНИИ ФУНКЦИЯМИ ReadFiie И ReadConsole, ИСПОЛЬЗуеТСЯ фуНК- ция setconsoieTextAttributes, которая имеет следующий прототип: BOOL SetconsoieTextAttributes ( HANDLE hConsoleOutput, // дескриптор буфера экрана WORD wAttribute // цвет фона и цвет текста ); В случае успеха эта функция возвращает ненулевое значение, а в случае неудачи — значение false. Параметр wAttribute содержит новые цвета фона и текста, которыми будут выводиться символы. Программа из листинга 12.7 показывает, как устанавливаются атрибуты текста консоли. # inc lude <windows. h> #include <iostream.h> int main() { char c;
200 Часть III. Программирование консольных приложений HANDLE hStdout; // дескриптор стандартного вывода WORD wAttribute; // цвет фона и текста cout « "In order to set text attributes, input any char: "; cin » c; // читаем стандартный дескриптор вывода hStdout = GetStdHandle(STD_OUTPUT_HAKDLE) ; // задаем цвет фона зеленым, а цвет символов красным wAttribute = BACKGROUND_GREEN | BACKGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_INTENSITY; // устанавливаем новые атрибуты if (!SetConsoleTextAttribute(hStdout,wAttribute)) { cout « "Set console text attribute failed." « endl; return GetLastError(); } cout « "The text attributes was changed." « endl; return 0; } Для установки атрибутов в последовательные клетки буфера экрана используется функция WriteConsoleOutputAttribute, которая имеет следующий прототип: BOOL WriteConsoleOutputAttribute ( HANDLE hConsoleOutput, // дескриптор буфера экрана CONST WORD *IpAttribute, // указатель на атрибуты DWORD nLength, // количество заполняемых клеток COORD dwWriteCoord, // координаты первой клетки LPDWORD lpNumberOfAttrsWritten // количество заполненных клеток ); При успешном завершении эта функция возвращает ненулевое значение, а в случае неудачи — значение false. Все параметры этой функции, за исключением параметра IpAttribute, имеют тот же смысл, что и соответствующие параметры функции SetConsoleTextAttributes. Параметр IpAttribute указывает на массив атрибутов, которые будут заноситься в последовательные клетки буфера экрана.
Глава 12. Работа с буфером экрана 201 HANDLE LPWORD DWORD COORD hConsoleOutput, lpAttribute, nLength, dwWriteCoord, В ОТЛИЧИе ОТ фуНКЦИИ SetConsoleTextAttributes функция writeConsoieOutputAttribute позволяет заполнять последовательные клетки буфера экрана не одним, а разными атрибутами. Для чтения атрибутов текста буфера экрана из последовательных ячеек используется фуНКЦИЯ ReadConsoleOutputAttribute, которая имеет следующий прототип: BOOL ReadConsoleOutputAttribute( // дескриптор буфера экрана // указатель на атрибуты // количество читаемых клеток // координаты первой клетки LPDWORD lpNumberOfAttrsRead // количество прочитанных клеток ); При успешном завершении эта функция возвращает ненулевое значение, а в случае неудачи — значение false. Назначение параметров hconsoieoutput и dwwritecoord этой функции совпадает с назначением соответствующих параметров фуНКЦИИ WriteConsoieOutputAttribute. Параметр lpAttribute указывает на область памяти, в которую будут читаться атрибуты. Параметр nLength должен содержать количество клеток, из которых будут читаться атрибуты, а параметр lpNumberOfAttrsRead — указывать на область памяти, в которую функция поместит количество прочитанных атрибутов. В листинге 12.8 приведена программа, в которой используются функции WriteConsoieOutputAttribute И ReadConsoleOutputAttribute ДЛЯ установки и чтения атрибутов текста консоли из последовательных клеток. Листинг 12.8. Установка и чтение атрибутов текста консоли #include <windows.h> #include <iostream.h> int main() { HANDLE hStdOut; WORD lpAttribute[4]; DWORD nLength = 4; COORD dwCoord = {8, 0} DWORD NumberOfAttrs; // дескриптор буфера экрана // массив клеток с атрибутами // количество клеток // координата первой клетки // количество обработанных клеток // читаем стандартный дескриптор вывода hStdOut=GetStdHandle(STD_OUTPUT_HANDLE);
202 Часть III. Программирование консольных приложений // выводим демо-текст cout « "Console text attributes." « endl; // ждем команды на изменение атрибутов слова "текст" cout « "Press any key to change attributes."; cin.get(); // устанавливаем новые атрибуты lpAttribute[0] = BACKGROUND_BLUE | BACKGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_INTENSITY; lpAttribute[l] = BACKGROUND_GREEN | BACKGROUND_INTENSITY | FOREGROUND_BLUE | FOREGROUND_INTENSITY; IpAt tribute [2] = BACKGROUND_RED | BACKGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_INTENSITY; lpAttribute[3] = BACKGROUND_GREEN | BACKGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_INTENSITY; // записываем новые атрибуты в буфер экрана if ( !WriteConsoleOutputAttribute(hStdOut, lpAttribute, nLength, dwCoord, &NumberOfAttrs)) { cout « "Read console output attribute failed." « endl; return GetLastError(); } // читаем атрибуты слова "текст" if (!ReadConsoleOutputAttribute(hStdOut, lpAttribute, nLength, dwCoord, &NumberOfAttrs)) { cout « "Read console output attribute failed." « endl; return GetLastError(); } // распечатываем атрибуты слова "текст" cout « hex; cout « "Attribute[0] = " « lpAttribute[0] « endl; cout « "Attribute[1] = " « lpAttribute[1] « endl; cout « "Attribute[2] = " « lpAttribute[2] « endl; cout « "Attribute[3] = " « lpAttribute[3] « endl; // ждем команду на завершение программы cout « "Press any key to exit."; cin.get(); return 0;
Глава 13 Ввод-вывод на консоль HANDLE LPVOID DWORD hConsolelnput, lpBuffer, nNumberOfCharsToRead, 13.1. Ввод-вывод высокого уровня К функциям ввода-вывода высокого уровня относятся следующие функции: WriteConsole, ReadConsole, WriteFile И ReadFile. Сейчас Подробно рассмотрим работу С ФУНКЦИЯМИ ReadConsole И WriteConsole. Затем рассмотрим фуНКЦИИ WriteFile И ReadFile. Для чтения строки символов из входного буфера консоли экрана используется функция ReadConsole, которая имеет следующий прототип: BOOL ReadConsole( // дескриптор буфера экрана // массив для ввода символов // количество читаемых символов // количество прочитанных символов // зарезервировано ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — значение false. Назначение параметров этой функции очевидно. Отметим лишь, что последний параметр всегда должен быть установлен в null. Функция ReadConsole вводит символы последовательно друг за другом, при этом курсор передвигается в следующую свободную позицию. Если включен режим отображения введенных символов, а он включен по умолчанию, то при вводе символы отображаются на экране. Подробно режимы управления вводом- выводом рассмотрены в разд. 13.3. Кроме того, в процессе ввода символов из входного буфера консоли эта функция игнорирует все события ввода, которые отличаются от ввода символа. LPDWORD lpNumberOfCharsRead, LPVOID lpReserved
204 Часть III. Программирование консольных приложений Для записи строки символов в буфер экрана используется функция writeconsole, которая имеет следующий прототип: BOOL WriteConsole ( HANDLE hConsoleOutput, // дескриптор буфера экрана CONST VOID *lpBuffer, // массив с символами для вывода DWORD nNumberOfCharsToWrite, // количество записываемых символов LPDWORD lpNumberOfCharsWritten, // количество записанных символов LPVOID lpReserved // зарезервировано ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — значение false. Как и в случае с функцией Readconsole параметр lpReserved всегда должен быть установлен в null. В листинге 13.1 приведена программа, которая выводит и вводит на консоль Информацию, ИСПОЛЬЗУЯ фуНКЦИИ WriteConsole И ReadConsole. Листинг 13.1. Чтение и запись на консоль посредством функций Readconsole И WriteConsole #include <windows.h> #include <iostream.h> int main() { HANDLE hStdOut, hStdln; // дескрипторы консоли DWORD dwWritten, dwRead; // для количества символов char buffer[80]; // для ввода символов char str[] = "Input any string:"; char с; // читаем дескрипторы консоли hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); hStdln = GetStdHandle(STD_INPUT_HANDLE); if (hStdOut == INVALID_HANDLE_VALUE | | hStdln == INVALID_HANDLE_VALUE) { cout « "Get standard handle failed." « endl; return GetLastError(); } // выводим сообщения о вводе строки if(!WriteConsole(hStdOut, &str, sizeof(str), &dwWritten, NULL))
Глава 13. В вод-вы вод на консоль 205 { cout « "Write console failed." « endl; return GetLastError(); } // вводим строку if(!ReadConsole(hStdln, &buffer, sizeof(buffer), &dwRead, NULL)) { cout « "Read console failed." « endl; return GetLastError(); } // ждем команду на завершение работы cout « "Input any char to exit: "; cin » c; return 0; } Теперь скажем о том, что ввод-вывод на консоль при помощи функций ReadFile И WriteFiie ВЫПОЛНЯетСЯ аналогично функциям ReadConsole И Writeconsoie. To есть эти функции имеют аналогичные списки параметров. Единственное отличие заключается в том, что последний параметр в функциях ReadFile И WriteFiie указывает на СИНХРОННЫЙ ИЛИ асинхронный ввод-вывод. Более подробно эти функции будут рассмотрены в гл. 24, посвященной вводу-выводу из файлов. В листинге 13.2 приведен текст программы, в которой эти функции используются для ввода-вывода на консоль. | Листинг 13,2* Чтение и запись на консоль посредством функций ReadFile | и WriteFiie #include <windows.h> HANDLE hStdOut, hStdln; int main(void) { LPSTR lpszPromptl = "Input 'q' and press Enter to exit.\n"; LPSTR lpszPrompt2 = "Input string and press Enter:\n"; CHAR chBuffer[80]; DWORD cRead, cWritten; // читаем дескрипторы стандартного ввода и вывода
206 Часть III. Программирование консольных приложений hStdln = GetStdHandle(STD_INPUT_HANDLE) ; hStdOut = GetStdHandle(STD_OUTPUT_HANDLE) ; if (hStdln == INVALID_HANDLE_VALUE | | hStdOut == INVALID_HANDLE_VALUE) { MessageBox(NULL, "Get standard handle failed", "Win32 API error", MB_OK | MB_ICONINFORMATION) ; return GetLastError(); } //по умолчанию установлены режимы ввода: ENABLE_LINE_INPUT, // ENABLE_ECHO_INPUT, ENABLE_PROCESSED_INPUT // выводим сообщение о том, как выйти из цикла чтения if (!WriteFile( hStdOut, // дескриптор стандартного вывода lpszPromptl, // строка, которую выводим lstrlen(lpszPromptl), // длина строки ScWritten, // количество записанных байтов NULL)) // синхронный вывод { MessageBox(NULL, "Write file failed", "Win32 API error", MB_OK | MB_ICONINFORMATION) ; return GetLastError(); } // цикл чтения for (;;) { // выводим сообщение о вводе строки if (!WriteFile( hStdOut, // дескриптор стандартного вывода lpszPrompt2, // строка, которую выводим lstrlen(lpszPrompt2), // длина строки ScWritten, // количество записанных байтов NULL)) // синхронный вывод { MessageBox(NULL, "Write file failed", "Win32 API error", MB_OK | MB_ICONINFORMATION) ; return GetLastError(); } // вводим строку с клавиатуры и дублируем ее на экран
Глава 13. Ввод-вывод на консоль 207 if (!ReadFile( hStdln, // дескриптор стандартного ввода chBuffer, // буфер для чтения 80, // длина буфера &cRead, // количество прочитанных байтов NULL)) // синхронный ввод { MessageBox(NULL, "Write file failed", "Win32 API error" MB_OK | MB_ICONINFORMATION) ; return GetLastError(); } // выход из программы if (chBuffer[0] == 'q') return 1; return 0; } В заключение данного параграфа сделаем следующие замечания. В случае С КОНСОЛЬЮ фуНКЦИИ WriteFile, ReadFile, WriteConsole И ReadConsole ЧИ- тают и записывают символы потоком. Функции ReadFile и WriteFile работают только с символами, заданными в кодировке ASCII. Функции ReadConsole И WriteConsole ОТЛИЧаютСЯ ОТ фаЙЛОВЫХ функций ТОЛЬКО тем, что работают также с символами, заданными в кодировке Unicode. Кроме того, эти функции не обрабатывают управляющие символы при вводе-выводе на консоль. 13.2. Ввод низкого уровня Функции ввода низкого уровня работают непосредственно с записями входного буфера КОНСОЛИ. К НИМ ОТНОСЯТСЯ фуНКЦИИ ReadConsolelnput, PeekConsolelnput, WriteConsolelnput, GetNumberOfConsolelnputEvents И FlushConsoieinputBuf f er, работу с которыми мы сейчас рассмотрим. Для чтения записей из входного буфера используется функция ReadConsolelnput, которая имеет следующий прототип: BOOL ReadConsolelnput( HANDLE hConsolelnput, // дескриптор входного буфера консоли PINPUT_RECORD lpBuffer, // буфер данных DWORD nLength, // количество читаемых записей
208 Часть III. Программирование консольных приложений LPDWORD lpNumberOfEventsRead // количество прочитанных записей ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — значение false. Отметим, что после чтения записи из входного буфера консоли функция Readconsoieinput удаляет ее оттуда. Теперь перейдем к описанию параметров этой функции. Параметр hconsoieinput должен содержать дескриптор входного буфера консоли. Параметр IpBuffer должен указывать на область памяти, в которую будут читаться записи из входного буфера консоли. Параметр nLength должен содержать количество записей, которые пользователь хочет прочитать из входного буфера консоли. А по адресу, заданному параметром lpNumberOfEventsRead, функция запишет количество прочитанных записей из буфера консоли. Для чтения записей из входного буфера консоли, не удаляя их оттуда, используется функция Peekconsoieinput. Параметры этой функции полностью совпадают С параметрами функции Readconsoieinput. В листинге 13.3 приведен текст программы, которая читает по одной записи из входного буфера и просто распечатывает их содержимое на экран. | Листинг 13.3, Чтение записей входного буфера консоли # include <windows. h> #include <iostream.h> HANDLE hStdln, hStdOut; // для дескрипторов стандартного ввода и вывода BOOL bRead = TRUE; // для цикла обработки событий // функция обработки сообщений от клавиатуры VOID KeyEventProc(KEY_EVENT_RECORD kir) { cout « "\tKey event record:" « endl; // просто выводим на консоль содержимое записи cout « "bKeyDown = " « hex « kir.bKeyDown « endl; cout « "wRepeatCount = " « dec « kir.wRepeatCount << endl; cout « "wVirtualKeyCode = " « hex « kir.wVirtualKeyCode « endl; cout « "wVirtualScanCode = " « kir.wVirtualScanCode « endl; cout « "uChar.AsciiChar = " « kir.uChar.AsciiChar « endl;
Глава 13. В вод-вы вод на консоль 209 cout « "dwControlKeyState = " « kir.dwControlKeyState « endl; // если ввели букву 'q', то выходим из цикла обработки событий if (kir.uChar.AsciiChar == 'q') bRead = FALSE; } // функция обработки сообщений от мыши VOID MouseEventProc(MOUSE_EVENT_RECORD mer) { cout « "\tMouse event record:" « endl « dec; // просто выводим на консоль содержимое записи cout « "dwMousePosition.X = " « mer.dwMousePosition.X « endl; cout « "dwMousePosition.Y = " « mer.dwMousePosition.Y « endl; cout « "dwButtonState = " « hex « mer.dwButtonState « endl; cout « "dwControlKeyState = " « mer.dwControlKeyState « endl; cout « "dwEventFlags = " « mer.dwEventFlags « endl; } // функция обработки сообщения об изменении размеров окна VOID ResizeEventProc(WINDOW_BUFFER_SIZE_RECORD wbsr) { // изменяем размеры буфера вывода SetConsoleScreenBufferSize(hStdOut, wbsr.dwSize); } int main() { INPUT_RECORD ir; // входная запись DWORD cNumRead; // для количества прочитанных записей // получить дескрипторы стандартного ввода и вывода hStdln = GetStdHandle(STD_INPUT_HANDLE); if (hStdln == INVALID_HANDLE_VALUE) { cout « "Get standard input handle failed." « endl; return GetLastErrorO;
210 Часть III. Программирование консольных приложений hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); if (hStdOut == INVALID_HANDLE_VALUE) { cout « "Get standard output handle failed." « endl; return GetLastError(); } // начинаем обработку событий ввода cout « "Begin input event queue processing." « endl; cout « "Input 'q1 to quit."« endl « endl; // цикл обработки событий ввода while (bRead) { // ждем событие ввода WaitForSingleObject(hStdIn/ INFINITE); // читаем запись ввода if (IReadConsolelnput( hStdln, // дескриптор ввода &ir, // буфер для записи 1, // читаем одну запись ScNumRead)) // количество прочитанных записей { cout « "Read console input failed." « endl; break; // вызываем соответствующий обработчик swi tch(ir.EventType) { case KEY_EVENT: // событие ввода с клавиатуры KeyEventProc(ir.Event.KeyEvent); break; case MOUSE_EVENT: // событие ввода с мыши MouseEventProc(ir.Event.MouseEvent); break; case WINDOW_BUFFER_SIZE_EVENT: // изменения размеров окна ResizeEventProc(
Глава 13. Ввод-вывод на консоль 211 ir.Event.WindowBufferSizeEvent); break; case FOCUS_EVENT: // события фокуса ввода игнорируем break; case MENU_EVENT: // события меню игнорируем break; default: // неизвестное событие cout « "Unknown event type."; break; } } return 0; } ( Замечание ^ Отметим, что в операционной системе Windows 2000 программа из листинга 13.3 работает только в полноэкранном режиме. Для того чтобы эта программа работала в графическом режиме нужно поступить следующим образом (для русской локализации). Правой кнопкой мыши щелкнуть по пиктограмме консольного приложения. Затем в появившемся диалоговом окне Свойства перейти на вкладку Общие. На этой вкладке сбросить в группе Редактирование флаг Выделение мышью. Теперь рассмотрим функции WriteConsolelnput, GetNuiriberOfConsolelnputEvents И FlushConsolelnputBuffer. Функция WriteConsolelnput предназначена для записи событий ввода во входной буфер консоли. Эта функция имеет следующий прототип: BOOL WriteConsolelnput( HANDLE hConsoleInput, // дескриптор входного буфера консоли CONST INPUT_RECORD *lpBuffer, // указатель на буфер с записями DWORD nLength, // количество записываемых записей LPDWORD lpNumberOfEventsWritten // количество записанных записей ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. Кратко опишем назначение параметров этой функции.
212 Часть III. Программирование консольных приложений Параметр hconsoleinput должен содержать дескриптор входного буфера консоли. Параметр lpBuffer должен указывать на область памяти, из которой будут записываться записи во входной буфер консоли. Параметр nLength должен содержать количество записей, которые пользователь хочет записать во входной буфер консоли. А по адресу, заданному параметром lpNumberof Event sWr it ten, функция запишет количество записей во входном буфере консоли. Для чтения количества записей, находящихся во входном буфере консоли ИСПОЛЬЗуетсЯ функция GetNumberOfConsolelnputEvents, которая имеет следующий прототип: BOOL GetNumberOfConsolelnputEvents( HANDLE hconsoleinput, // дескриптор входного буфера консоли LPDWORD lpNumberOfEvents // указатель на количество записей ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. В случае успешного завершения адресу, заданному параметром lpNumberof Events, эта функция записывает количество записей, находящихся в буфере ввода консоли. Для очистки буфера ввода консоли используется функция FlushConsoieinputBuffer, которая имеет следующий прототип: BOOL FlushConsoieinputBuffer( HANDLE hconsoleinput, // дескриптор входного буфера консоли ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. В листинге 13.4 приведена программа, в которой показаны примеры использования функций WriteConsolelnput, GetNumberOfConsolelnputEvents И FlushConsoieinputBuffer. Листинг 13.4» Пример записи в буфер ввода, подсчет количества записей и очистка буфера ввода #include <windows.h> #include <iostream.h> int main() { HANDLE hStdln; // для дескриптора стандартного ввода
Глава 13. Ввод-вывод на консоль 213 INPUT_RECORD ir; // входная запись DWORD dwNumberWritten; // количество записанных записей DWORD dwNumber; // для количества записей в буфере ввода // получить дескриптор стандартного ввода hStdln = GetStdHandle(STD_INPUT_HANDLE); if (hStdln == INVALID_HANDLE_VALUE) { cout « "Get standard input handle failed." « endl; return GetLastError(); } // подсчитываем записи в буфере ввода if (!GetNumberOfConsolelnputEvents(hStdln, &dwNumber)) { cout « "Get number of console input events failed." « endl; return GetLastError(); } // печатаем количество событий ввода cout « "Number of console input events = " « dwNumber « endl; // инициализируем запись события ввода ir.EventType = KEY_EVENT; ir.Event.KeyEvent.bKeyDown = Oxl; ir.Event.KeyEvent.wRepeatCount = 1; ir.Event.KeyEvent.wVirtualKeyCode = 0x43; ir.Event.KeyEvent.wVirtualScanCode = 0x2e; ir.Event.KeyEvent.uChar.Asc i iChar = ' с' ; ir.Event.KeyEvent.dwControlKeyState =0x2 0; // записываем запись в буфер ввода if (!WriteConsoleInput(hStdln, &ir, 1, &dwNumberWritten)) { cout « "Write console input failed." « endl; return GetLastError(); } cout « "Write one record into the input buffer." « endl; // подсчитываем записи в буфере ввода if (!GetNumberOfConsolelnputEvents(hStdln, &dwNumber)) { cout « "Get number of console input events failed." « endl; return GetLastError();
214 Часть III. Программирование консольных приложений } // печатаем количество событий ввода cout « "Number of console input events = " « dwNumber « endl; // очищаем входной буфер cout « "Flush console input buffer." « endl; if (IFlushConsolelnputBuffer(hStdln)) { cout « "Flush console input buffer failed." « endl; return GetLastError(); } // подсчитываем записи в буфере ввода if (IGetNumberOfConsolelnputEvents(hStdln, &dwNumber) ) { cout « "Get number of console input events failed." « endl; return GetLastError(); } // печатаем количество событий ввода cout « "Number of console input events = " « dwNumber « endl; return 0; } Для определения количества кнопок у мыши используется функция GetNumberofConsoieMouseButton, которая имеет следующий прототип: BOOL GetNumberofConsoieMouseButton( LPDWORD IpNumberOfMouseButtons // количество кнопок у мыши ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — значение false. При успешном завершении эта функция записывает ПО адресу, заданному параметром IpNumberOfMouseButtons, количество кнопок у мыши. В листинге 13.5 приведена программа, которая показывает пример использования функции GetNumberofConsoieMouseButton. | Листинг 13.5. Определение количества кнопок у мыши #include <windows.h> #include <iostream.h> int main ()
Глава 13. Ввод-вывод на консоль 215 { DWORD dwNumber; // для количества кнопок у мыши // подсчитываем количество кнопок у мыши if (!GetNumberOfConsoleMouseButtons(&dwNumber)) { cout « "Get number of console mouse buttons failed." « endl; return GetLastError(); } // выводим количество кнопок у мыши cout « "Number of console mouse buttons = " « dwNumber « endl; return 0; } 13.3. Вывод низкого уровня Функции вывода низкого уровня работают непосредственно с элементами буфера экрана. Эти функции можно разбить на три группы. □ Чтение и запись последовательности символов: • Readconsoieoutputcharacter — чтение последовательности символов из буфера экрана; • WriteConsoleOutputCharacter — запись последовательности СИМВОЛОВ в буфер экрана. □ Заполнение буфера экрана заданным символом: • Filiconsoieoutputcharacter — заполнение буфера экрана. □ Чтение и запись прямоугольных областей символов: • Readconsoieoutput — чтение прямоугольной области символов из буфера экрана; • writeconsoieoutput — запись прямоугольной области символов в буфер экрана. Рассмотрим работу с этими функциями подробнее. Сначала рассмотрим функции из первой группы, которые предназначены для работы с последовательностями символов. Для ввода последовательности символов из буфера экрана используется ФУНКЦИЯ ReadConsoleOutputCharacter, КОТОрая Имеет Следующий ПрОТОТИП: BOOL ReadConsoleOutputCharacter( HANDLE hConsoleOutput, // дескриптор буфера экрана
216 Часть III. Программирование консольных приложений LPTSTR lpCharacter, // указатель на буфер с символами DWORD nLength, // количество читаемых символов COORD dwReadCoord, // координаты первого читаемого символа LPDWORD lpNumberOfCharsRead // количество прочитанных символов ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. Кратко опишем назначение параметров этой функции. Параметр hconsoieoutput этой функции должен содержать дескриптор буфера экрана. Параметр lpCharacter должен указывать на область памяти, в которую будет читаться содержимое буфера экрана. Параметр nLength должен содержать количество читаемых символов из буфера экрана. Параметр dwReadCoord должен содержать координаты первого символа, который будет читаться из буфера экрана. При этом поля х и y структуры coord задают соответственно индексы строки и столбца первого читаемого элемента. По адресу, заданному параметром lpNumberOfCharsRead, функция ReadConsoleOutputCharacter ВОЗВраТИТ КОЛИЧесТВО прочитанных СИМВОЛОВ из буфера экрана. В листинге 13.6 приведена программа, которая демонстрирует использование фуНКЦИИ ReadConsoleOutputCharacter ДЛЯ ЧТеНИЯ СОДержИМОГО ПрЯМО- угольной области буфера экрана. |; Листинг 13.6, Чтение последовательности символов из буфера экрана # include <windows.h> #include <iostream.h> int main () { HANDLE hConsoleOutput; // для дескриптора буфера экрана CHAR lpBuffer[80]; // буфер для ввода COORD dwReadCoord = {0, 0}; // координаты первого элемента в буфере DWORD nNumberOfCharsRead; // количество прочитанных символов // получаем дескриптор буфера экрана hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
Глава 13. Ввод-вывод на консоль 217 if (hConsoleOutput == INVALID_HANDLE_VALUE) { cout « "Get standard handle failed." « endl; return GetLastError(); } // вьгоодим те символы, которые будем читать cout « 'а' « 'b' « endl; // читаем эти символы в буфер if (!ReadConsoleOutputCharacter( hConsoleOutput, // дескриптор буфера экрана lpBuffer, // буфер для ввода символов 2, // количество читаемых символов dwReadCoord, // координата первого символа &nNumberOfCharsRead)) // количество прочитанных символов { cout « "Read consoleoutput character failed." « endl; return GetLastError(); } // выводим количество прочитанных символов и сами символы cout « "Number of chars read: " << nNumberOfCharsRead « endl; cout « "Read chars: " « lpBuffer[0] « lpBuffer[1] « endl; return 0; } Для записи последовательности символов в буфер экрана используется ФУНКЦИЯ WriteConsoleOutputCharacter, Которая имеет следующий ПроТО- тип: BOOL WriteConsoleOutputCharacter( HANDLE hConsoleOutput, // дескриптор буфера экрана LPCTSTR lpCharacter, // указатель на массив символов DWORD nLength, // количество записываемых символов COORD dwWriteCoord, // координаты первого символа в буфере экрана LPDWORD lpNumberOfCharsWritten // количество символов, // записанных в буфер экрана ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — значение false. Эта функция записывает последовательность символов, на которую указывает параметр ipcharacter, в буфер
218 Часть III. Программирование консольных приложений экрана, начиная с позиции, на которую указывает параметр dwwritecoord. Количество записываемых символов задается параметром nLength, а количество фактически записанных СИМВОЛОВ фунКЦИЯ WriteConsoleOutputCharacter Возвращает ПО адресу, заданному параметром lpNumberOfCharsWritten. В листинге 13.7 приведена программа, в которой показано, как можно вывести последовательность символов с заданной позиции в буфер экрана, используя фуНКЦИЮ WriteConsolelnputCharacter. #include <windows.h> #include <iostream.h> int main() { HANDLE hConsoleOutput; // для дескриптора буфера экрана CHAR lpBuffer[] = "abed"; // буфер с символами для вывода COORD dwWriteCoord = {10, 10};// координаты первого элемента в буфере DWORD nNumberOfCharsWritten; // количество записанных символов // получаем дескриптор буфера экрана hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE); if (hConsoleOutput == INVALID_HANDLE_VALUE) { cout « "Get standard handle failed." « endl; return GetLastError(); } // записываем символы в буфер экрана if (!WriteConsoleOutputCharacter( hConsoleOutput, // дескриптор буфера экрана lpBuffer, // буфер для ввода символов sizeof(lpBuffer), // количество записываемых символов dwWriteCoord, // координата первого символа &nNumberOfCharsWritten)) // количество записанных символов { cout « "Read console output character failed." « endl; return GetLastError(); } // выводим количество записанных символов
Глава 13. Ввод-вывод на консоль 219 cout « "Number of chars written: " « nNumberOfCharsWritten « endl; return 0; } Теперь рассмотрим функцию из второй группы нашей классификации. Для заполнения всего или части буфера экрана определенным символом используется фуНКЦИЯ FillConsoleOutputCharacter, КОТОрая имеет следующий прототип: BOOL FillConsoleOutputCharacter( HANDLE hConsoleOutput, // дескриптор буфера экрана TCHAR cCharacter, // символ-заполнитель DWORD nLength, // длина заполняемой области COORD dwWriteCoord, // координаты первой клетки буфера экрана LPDWORD lpNumberOfCharsWritten // количество заполненных клеток ); В случае успеха функция возвращает ненулевое значение, а случае неудачи — значение false. Назначение параметров этой функции очевидно. Назначение параметров этой функции очевидно, принимая во внимание рассмотренные нами выше функции. В листинге 13.8 приведен пример программы, в которой эта функция используется для заполнения буфера экрана заданным символом, а затем для очистки буфера экрана. |^истинг13Д Заполнение буфер экрана ^\ #include <windows.h> #include <iostream.h> int main() { char c; HANDLE hStdOut; // дескриптор стандартного вывода DWORD dwLength; // количество заполняемых клеток DWORD dwWritten; // для количества заполенных клеток COORD coord; // координаты первой клетки CONSOLE_SCREEN_BUFFER_INFO csbi; // для параметров буфера экрана // читаем дескриптор стандартного вывода hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
220 Часть Ш. Программирование консольных приложений if (hStdOut == INVALID_HANDLE_VALUE) { cout « "Get standard handle failed." « endl; return GetLastError(); } // читаем параметры выходного буфера if (!GetConsoleScreenBufferInfo(hStdOut, &csbi)) { cout « "Console screen buffer info failed." « endl; return GetLastError(); } // вычисляем размер буфера экрана в символах dwLength = csbi.dwSize.X * csbi.dwSize.Y; // устанавливаем координаты первой клетки coord.X = 0; coord.Y = 0; // вводим символ-заполнитель cout « "Input any char to fill screen buffer: "; cin » c; // заполняем буфер экрана символом-заполнителем if (IFillConsoleOutputCharacter( hStdOut, // стандартный дескриптор вывода с, // символ заполнения dwLength, // длина буфера в символах coord, // индекс первой клетки &dwWritten)) // количество заполненных клеток { cout « "Fill console output character failed." « endl; return GetLastError(); } // ждем команды на очищение буфера экрана cout « "In order to clear screen buffer, press any char: "; cin » c; // очищаем буфер экрана пробелами if (IFillConsoleOutputCharacter( hStdOut, // стандартный дескриптор вывода 1 ', // символ заполнения dwLength, // длина буфера в символах coord, // индекс первой клетки
Глава 13. Ввод-вывод на консоль 221 &dwWritten)) // количество заполненных клеток { cout « "Fill console output character failed." « endl; return GetLastError(); } return 0; } Теперь перейдем к функциям из третьей группы, которые предназначены для чтения и записи прямоугольных областей символов. Функция ReadconsoieOutput предназначена для чтения прямоугольных областей символов и их атрибутов из буфера экрана. Эта функция имеет следующий прототип: BOOL ReadConsoleOutput( HANDLE hConsoleOutput, // дескриптор буфера экрана PCHAR_INFO lpBuffer, // указатель на буфер с данными COORD dwBufferSize, // размер буфера с данными COORD dwBufferCoord, // координаты для первого элемента в буфере PSMALL_RECT lpReadRegion // область ввода в буфере экрана ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — значение false. Кратко опишем назначение параметров этой функции. Параметр hConsoleOutput этой функции должен содержать дескриптор буфера экрана. Параметр lpBuffer должен указывать на область памяти, в которую будет читаться содержимое буфера экрана. Причем эта область памяти рассматривается функцией ReadconsoieOutput как двумерный массив, элементами которого являются струкутры типа char_info. Размерность этого массива задается параметром dwBuffersize. При этом поля х и y структуры coord задают соответственно количество столбцов и строк этого массива. Параметр lpReadRegion является указателем на структуру типа small_rect, которая содержит координаты левого верхнего и правого нижнего углов прямоугольника в буфере экрана, содержимое которого будет прочитано в область памяти, заданную параметром lpBuffer. В листинге 13.9 приведена программа, которая демонстрирует использование функции ReadconsoieOutput для чтения содержимого прямоугольной области буфера экрана.
222 Часть III. Программирование консольных приложений #include <windows.h> #include <iostream.h> int main() { HANDLE hConsoleOutput; // для дескриптора буфера экрана CHAR_INFO lpBuffer[4]; // буфер для ввода COORD dwBufferSize = {2, 2}; // размеры буфера COORD dwBufferCoord = {0, 0}; // координаты первого элемента в буфере SMALL_RECT ReadRegion = {0, 0, 1, 1}; // прямоугольник, который читаем // выводим символы, которые будем читать cout « 'а' « 'Ъ' « endl « 'с' « 'd' « endl; // получаем дескриптор ввода hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE); if (hConsoleOutput == INVALID_HANDLE_VALUE) { cout « "Get standard handle failed." « endl; return GetLastError(); } // читаем символы if (!ReadConsоleOutput(hConsoleOutput, lpBuffer, dwBufferSize, dwBufferCoord, &ReadRegion)) { cout « "Read console input failed." « endl; return GetLastError(); } // распечатываем прочитанные символы cout « "Read cells." « hex « endl; for (int i = 0; i < 4; ++i) cout « lpBuffer[i] .Attributes « ' ' « lpBuffer[i] .Char.AsciiChar « endl; return 0;
Глава 13. Ввод-вывод на консоль 223 Для записи прямоугольных областей символов и их атрибутов в буфер экрана используется функция writeconsoleoutput, которая имеет следующий прототип: BOOL WriteConsoleOutput( HANDLE hConsoleOutput, // дескриптор буфера экрана CONST CHAR_INFO *lpBuffer, // указатель на буфер с данными COORD dwBufferSize, // размер буфера с данными COORD dwBufferCoord, // координаты первого элемента в буфере PSMALL_RECT lpWriteRegion // область вывода в буфере экрана ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — значение false. Эта функция записывает символы и их атрибуты из буфера, на который указывает параметр IpBuf fer, в область вывода буфера экрана, на которую указывает параметр lpWriteRegion. Остальные параметры этой функции имеют тот же смысл, что и параметры ФУНКЦИИ ReadConsoleOutput. В листинге 13.10 приведена программа, которая показывает, как можно заполнить прямоугольник в буфере экрана пробелами, используя функцию WriteConsolelnput. Шб^-хШик.ьн.^.^л J^.Z^.«^-..i. .:..~.^..Г._ : ^.._:^£^„^.™.i_^._-.^.«i^.^.z.' *_..aaw&..i :^:i.^._±js.^.^'..^. 1.1........« **-. ._..i #include <windows.h> #include <iostream.h> int main() { HANDLE hStdOut; // дескриптор стандартного вывода CHAR_INFO ci[80*25]; // прямоугольник, из которого будем выводить COORD size; // размеры этого прямоугольника // координаты левого угла прямоугольника, из которого выводим COORD coord; // координаты левого угла прямоугольника, в который пишем SMALL_RECT sr; // читаем стандартный дескриптор вывода hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); // заполняем прямоугольник, который будем выводить, пробелами for (int i = 0; i < 80*25; ++i)
224 Часть III. Программирование консольных приложений { ci[i].Char.AsciiChar = ' '; ci[i] .Attributes = BACKGROUND_BLUE | BACKGROUND_INTENSITY; } // устанавливаем левый угол ^многоугольника, из которого пишем coord.X = 0; coord.Y = 0; // устанавливаем размеры прямоугольника, который пишем size.X = 80; size.Y = 25; // вводим координаты левого верхнего угла многоугольника, //в который пишем cout « "Input left coordinate to write: "; cin » sr.Left; cout « "Input top coordinate to write: "; cin » sr.Top; // вводим координаты правого нижнего угла прямоугольника, //в который пишем cout « "Input right coordinate to write: "; cin » sr.Right; cout « "Input down coordinate to write: "; cin » sr.Bottom; // пишем прямоугольник в буфер экрана if (IWriteConsoleOutput( hStdOut, // дескриптор буфера экрана ci, // прямоугольник, из которого пишем size, // размеры этого прямоугольника coord, //и его левый угол &sr)) // прямоугольник, в который пишем { cout « "Write console output failed." « endl; return GetLastError(); } return 0;
Глава 13. В вод-вы вод на консоль 225 13.4. Режимы ввода-вывода консоли Функции ввода-вывода на консоль могут работать в нескольких режимах, которые устанавливаются функцией setconsoleMode, которая имеет следующий прототип: BOOL SetconsoleMode( HANDLE hConsoleHandle, // дескриптор ввода или вывода DWORD dwMode // режим ввода или вывода ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — значение false. Если параметр hConsoleHandle является дескриптором входного буфера, то режим ввода, который описывается параметром dwMode, может быть произвольной комбинацией следующих флагов: □ ENABLE_LINE_INPUT — фуНКЦИЙ ReadFile И ReadConsole ЧИТаЮТ СИМВОЛЫ до тех пор, пока не встретят символ enter. В противном случае читается доступное количество символов во входном буфере; □ ENABLE_ECHO_INPUT — СИМВОЛЫ, Прочитанные ФУНКЦИЯМИ ReadFile И ReadConsole, выводятся на экран. Этот режим может использоваться только совместно с режимом enable_line_input; □ enable_processed_input — комбинация клавиш <Ctrl>+<C> обрабатывается системой. Если этот режим используется совместно с режимом enable_line_input, то управляющие символы \ь, \г и \п также обрабатываются системой; □ enable_window_input — изменение размеров окна обрабатывается приложением; □ enable_mouse_input — сообщения от мыши обрабатываются приложением. Отметим следующий момент: как видно из описания режимов, значения ENABLE_LINE_INPUT, ENABLE_ECHO_INPUT И ENABLE_PROCESSED_INPUT ВЛИЯЮТ на работу функций ввода-вывода высокого уровня, а значения ENABLE_WINDOW_INPUT И ENABLE_MOUSE_INPUT — На работу фуНКЦИЙ НИЗКОГО уровня ввода-вывода. Если параметр hConsoleHandle является дескриптором буфера экрана, то режим ввода, который задается параметром dwMode, может быть комбинацией следующих значений: □ ENABLE_PROCESSED_OUTPUT — фуНКЦИЙ WriteFile И WriteConsole обрабатывают во входном буфере управляющие символы: \n, \t, \b, \v и \а. Эти же действия выполняются при выводе на экран ввода функций ReadFile И ReadConsole; 8 Зак. 514
226 Часть III. Программирование консольных приложений О enable_wrap_at_eol_output — разрешает прокручивать буфер экрана при выводе данных на консоль. Для чтения установленных режимов ввода-вывода используется функция GetconsoieMode, которая имеет следующий прототип: BOOL GetconsoieMode( HANDLE hConsoleHandle, // входной или выходной дескриптор консоли LPDWORD lpMode // указатель на флаги режимов консоли ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — значение false. Если функция GetconsoieMode выполнилась успешно, то в двойном слове, на которое ссылается параметр LpMode, будут установлены флаги режимов консоли. В листинге 13.11 в качестве примера приведена программа, в которой отключается режим эхо-вывода на экран введенных с консоли символов. |Л йстинг 1ШЩ Отключение режима эхо-вывода #include <windows.h> HANDLE hStdOut, hStdln; int main (void) { LPSTR lpszPromptl = "Input ESC and press Enter to exit An"; LPSTR lpszPrompt2 = "Input string and press Enter An"; CHAR chBuffer[80]; DWORD cRead, cWritten; DWORD dwOldMode, dwNewMode; // читаем дескрипторы стандартного ввода и вывода hStdln = GetStdHandle(STD_INPUT_HANDLE) ; hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); if (hStdln == INVALID_HANDLE_VALUE | | hStdOut == INVALID_HANDLE_VALUE) { MessageBox(NULL/ "Get standard handle failed.", "Win32 API error", MB_OK | MB_ICONINFORMATION) ; return GetLastError();
Глава 13. Ввод-вывод на консоль 227 // читаем режимы, установленные по умолчанию if (!GetConsoleMode(hstdin, &dw01dMode)) { MessageBox(NULL/ "Get console mode failed.", "Win32 API error", MB_OK | MB_ICONINFORMATION) ; return GetLastError(); } // отключаем режим ENABLE_ECHO_INPUT dwNewMode = dwOldMode & ~ENABLE_ECHO_INPUT ; // устанавливаем новый режим if (!SetConsoleMode (hstdin, dwNewMode)) { MessageBox(NULL/ "Set console mode failed.", "Win32 API error", MB_OK | MB_ICONINFORMATION) ; return GetLastError(); } // выводим сообщение о том, как выйти из цикла чтения if (!WriteConsole( hStdOut, // дескриптор стандартного вывода lpszPromptl, // строка, которую выводим lstrlen(lpszPromptl), // длина строки ScWritten, // количество записанных байтов NULL)) //не используется { MessageBox(NULL, "Write file failed.", "Win32 API error", MB_OK | MB_ICONINFORMATION) ; return GetLastError(); } // цикл чтения for (;;) { // выводим сообщение о вводе строки if (!WriteConsole( hStdOut, // дескриптор стандартного вывода lpszPrompt2, // строка, которую выводим lstrlen(lpszPrompt2), // длина строки
228 Часть III. Программирование консольных приложений ScWritten, // количество записанных байтов NULL)) //не используется { MessageBox(NULL/ "Write file failed.", "Win32 API error", MB_OK | MB_ICONINFORMATION) ; return GetLastErrorO; } // вводим строку с клавиатуры if (!ReadConsole( hstdln, // дескриптор стандартного ввода chBuffer, // буфер для чтения 80, // длина буфера &cRead, // количество прочитанных байт NULL)) //не используется { MessageBox(NULL/ "Read file failed.", "Win32 API error", MB_OK | MB_ICONINFORMATION) ; return GetLastErrorO; } // выход из программы if (chBuffer[0] == ЛОЗЗ') return 1; // дублируем строку на экране if (!WriteConsole( hStdOut, // дескриптор стандартного вывода chBuffer, // строка, которую выводим cRead, // длина строки ScWritten, // количество записанных байтов NULL)) //не используется { MessageBox(NULL/ "Write file failed.", "Win32 API error", MB_OK | MB_ICONINFORMATION) ; return GetLastError(); } return 0;
Глава 13. Ввод-вывод на консоль 229 13.5. Прокрутка буфера экрана Для перемещения блоков данных внутри буфера экрана используется функция scroiiconsoieScreenBuffer, которая имеет следующий прототип: BOOL ScroiiconsoieScreenBuffer( HANDLE hConsoleOutput, // дескриптор буфера экрана CONST SMALL_RECT *lpScrollRectangle, // исходный прямоугольник CONST SMALL_RECT *lpClipRectangle, // прямоугольник отсечения COORD dwDestinationOrigin, // целевой прямоугольник CONST CHAR_INFO *lpFill // символ-заполнитель ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Кратко опишем назначение параметров этой функции. В параметре hconsoieOutput должен быть установлен дескриптор буфера экрана. Параметр lpScroiiRectangie должен указывать на прямоугольник, который будет перемещаться внутри буфера экрана. Параметр lpciipRectangie должен указывать на прямоугольник, внутри которого выполняется изменение буфера экрана при перемещении исходного прямоугольника. За пределами этого прямоугольника изменения в буфере экрана производиться не будут. Такой процесс называется отсечением изменений вне прямоугольника, а сам прямоугольник называется прямоугольником отсечения. Параметр dwDestinationOrigin ДОЛЖен указывать на ПРЯМОУГОЛЬНИК, В КО- торый будет переписано содержимое исходного прямоугольника. Параметр lpFiii должен указывать на символ-заполнитель. Этим символом будут заполняться те области целевого прямоугольника, которые не перекрываются исходным прямоугольником. Главным образом фуНКЦИЯ ScroiiconsoieScreenBuf fer ИСПОЛЬЗуетСЯ ДЛЯ прокрутки буфера экрана. Как это можно сделать, показано в программе из листинга 13.12. | Листинг 13,12, Прокрутка буфера экрана # include <windows.h> HANDLE hStdOut, hStdln; // функция перехода на новую строку в буфере экрана
230 Часть III. Программирование консольных приложений int GoToNewLine(void) { CONSOLE_SCREEN_BUFFER_INFO csbi; // информация о буфере экрана SMALL_RECT srScroll; // перемещаемый прямоугольник SMALL_RECT srClip; // рассматриваемая область COORD coord; // новое положение CHAR_INFO ci; // символ-заполнитель // читаем информацию о буфере экрана if (!GetConsoleScreenBufferInfo(hStdOut/ &csbi)) { MessageBox(NULL, "Get console screen buffer info failed.", "Win32 API error", MB_OK | MB_ICONINFORMATION) ; return 0; } // переходим на первый столбец csbi.dwCursorPosition.X = 0; // если это не последняя строка, if ((csbi.dwCursorPosition.Y+1) < csbi.dwSize.Y) //то переводим курсор на следующую строку csbi.dwCursorPosition.Y += 1; // иначе прокручиваем буфер экрана else { // координаты прямоугольника, который прокручиваем srScroll.Left = 0; srScroll.Top = 1; srScroll.Right = csbi.dwSize.X; srScroll.Bottom = csbi.dwSize.Y; // координаты прямоугольника буфера экрана srClip.Left = 0; srClip.Top = 0; srClip.Right = csbi.dwSize.X; srClip.Bottom = csbi.dwSize.Y; // устанавливаем новые координаты левого угла прямоугольника srScroll coord.X = 0; coord.Y = 0; // устанавливаем атрибуты и символ-заполнитель для последней строки ci.Attributes = csbi.wAttributes;
Глава 13. Ввод-вывод на консоль 231 ci.Char.AsciiChar = ' '; // прокручиваем прямоугольник srScroll if (!ScrollConsoleScreenBuffer( hStdOut, // дескриптор стандартного вывода &srScroll, // прокручиваемый прямоугольник &srClip, // буфер экрана coord, // начало буфера экрана &ci)) // атрибуты и символ-заполнитель { MessageBox(NULL/ "Set console window info failed.", "Win32 API error", MB_OK | MB_ICONINFORMATION) ; return 0; } } // теперь устанавливаем курсор if (ISetConsoleCursorPosition(hStdOut, csbi.dwCursorPosition)) { MessageBox(NULL/ "Set console cursor position failed."/ "Win32 API error", MB_OK | MB_ICONINFORMATION) ; return 0; } return 0; } int main (void) { LPSTR IpszPrompt = "Press ESC to exit.\n"; CHAR c; DWORD cRead, cWri tten; DWORD dwOldMode, dwNewMode; // читаем дескрипторы стандартного ввода и вывода hStdln = GetStdHandle(STD_INPUT_HANDLE) ; hStdOut = GetStdHandle(STD_OUTPUT_HANDLE); if (hStdln == INVALID_HANDLE_VALUE | | hStdOut == INVALID_HANDLE_VALUE) { MessageBoxdSfULL, "Get standard handle failed.", "Win32 API error", MB_OK | MB_ICONINFORMATION) ;
232 Часть III. Программирование консольных приложений return GetLastError(); } // читаем режимы, установленные по умолчанию if ( !GetConsoleMode(hStdIn, ScdwOldMode) ) { MessageBox(NULL/ "Get console mode failed.", "Win32 API error", MB_OK | MB_ICONINFORMATION) ; return GetLastError(); } // отключаем режиы ENABLE_LINE_INPUT и ENABLE_ECHO_INPUT dwNewMode=dwOldMode & ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT) ; // устанавливаем новый режим if (!SetConsoleMode(hStdln, dwNewMode)) { MessageBox(NULL, "Set console mode failed.", "Win32 API error", MB_OK | MB_ICONINFORMATION) ; return GetLastError(); } // выводим сообщение о том, как выйти из цикла чтения if (!WriteFile( hStdOut, // дескриптор стандартного вывода lpszPrompt, // строка, которую выводим lstrlen(IpszPrompt), // длина строки Rewritten, // количество записанных байт NULL)) // синхронный вывод { MessageBox(NULL, "Write file failed.", "Win32 API error", MB_OK | MB_ICONINFORMATION) ; return GetLastError(); } // цикл чтения for ( ; ; ) { // читаем следующий символ if (!ReadFile(hStdIn, &c, 1, &cRead, NULL)) { MessageBox(NULL, «Write file failed.», «Win32 API error», MB_OK | MB_ICONINFORMATION) ; return GetLastError() ;
Глава 13. Ввод-вывод на консоль 233 } // выбор действия switch (с) { // переход на новую строку- case ' \г' : if (!GoToNewLine()) { MessageBox(NULL/ «Go to a new line failed.», «Win32 API error», MB_OK | MB_ICONINFORMATION) ; return GetLastError(),- } break; // выход из программы case ' \033*: return 1; // распечатываем введенный символ default: if (!WriteFile(hStdOut, &c, cRead, fccWritten, NULL)) { MessageBox(NULL, «Write file failed.», «Win32 API error», MB_OK | MB_ICONINFORMATION) ; return GetLastError(); } } } return 0; }
Часть IV Обмен данными между параллельными процессами Глава 14. Передача данных Глава 15. Работа с анонимными каналами в Windows Глава 16. Работа с именованными каналами в Windows Глава 17. Работа с почтовыми ящиками в Windows
Глава 14 Передача данных 14.1. Способы передачи данных между процессами Под обменом данными между параллельными процессами понимается пересылка данных от одного потока к другому потоку, предполагая, что эти потоки выполняются в контекстах разных процессов. Поток, который посылает данные другому потоку, называется отправителем. Поток, который получает данные от другого потока, называется адресатом или получателем. Если потоки выполняются в одном процессе, то для обмена данными между ними можно использовать глобальные переменные и средства синхронизации потоков. Дело обстоит сложнее в том случае, если потоки выполняются в разных процессах — потоки не могут обращаться к общим переменным и для обмена данными между ними существуют специальные средства операционной системы. Если говорить концептуально, то для обмена данными между процессами создается канал передачи данных, организация которого схематически показана на рис. 14.1. Кратко поясним устройство и работу канала передачи данных. Канал данных включает входной и выходной буферы памяти, потоки ядра операционной системы и общую память, доступ к которой имеют оба потока ядра. Работает канал передачи данных следующим образом: □ первый поток ядра операционной системы читает данные из входного буфера В\ и записывает их в общую память М\ П второй поток ядра читает данные из общей памяти М и записывает их в буфер i?2- Пользовательские потоки Т\ и Ti посредством вызова функций ядра операционной системы имеют доступ к буферам В\ и В^ соответственно. Поэтому пересылка данных из потока Т\ в поток Т^ происходит следующим образом: □ пользовательский поток Т\ записывает данные в буфер В\, используя специальную функцию ядра операционной системы;
238 Часть IV. Обмен данными между параллельными процессами П поток К[ ядра операционной системы читает данные из буфера В\ и записывает их в общую память М\ □ поток Ki ядра операционной системы читает данные из общей памяти М и записывает их в буфер В^\ П пользовательский поток 72 читает данные из буфера Bj. Ti, T2 — пользовательские потоки Bi, B2 — буферы Ki, K2 — потоки ядра операционной системы М — общая память Рис. 14.1. Схема канала передачи данных Отсюда видно, что в любом случае обмен данными может быть организован только через цепочку взаимодействующих потоков, которые обмениваются между собой данными через общую, только для них, память. На схему канала, показанную на рис. 14.1, можно взглянуть и шире. Она также подходит для организации канала передачи данных по сети. Только в этом случае общая память Л/, может рассматриваться как передающая среда, устройство которой аналогично устройству канала передачи данных. Теперь скажем о том, как организовать канал передачи данных между процессами программным образом. Для этого нужно потоки ядра операционной системы и общую память, используемую для обмена данными, заменить файлом. И мы получим простейший канал передачи данных между потоками, выполняющимися в контекстах разных процессов. Таким образом, обмен данными между процессами через общий файл представляет собой организацию простейшего канала передачи данных между процессами. Иногда операционная система может упростить доступ к разделяемому файлу, чтобы ускорить обмен данными. Например, операционные системы Windows для этих целей могут отображать или, другими словами, проецировать файл на адресное пространство процесса. При обмене данными между параллельными процессами различают два способа передачи данных: □ потоком; □ сообщением.
Глава 14. Передача данных 239 Если данные передаются непрерывной последовательностью байтов, то такая пересылка данных называется передача данных потоком. В этом случае общая память М, доступная потокам ядра операционной системы, может и отсутствовать, а пересылка данных выполняется одним потоком ядра непосредственно из буфера В\ в буфер В^ Если данные пересылаются группами байтов, то такая группа байтов называется сообщением, а сама пересылка данных называется передачей данных сообщениями. 14.2. Связи между процессами Прежде чем передавать данные между процессами, нужно установить между этими процессами связь. Связь между процессами может устанавливаться как на физическом (или аппаратном), так и на логическом (или программном) уровнях. С точки зрения направления передачи данных различают следующие виды связей: □ полудуплексная связь, т. е. данные по этой связи могут передаваться только в одном направлении; □ дуплексная связь, т. е. данные по этой связи могут передаваться в обоих направлениях. Теперь, предполагая, что рассматриваются только полудуплексные связи, определим возможные топологии связей. Под топологией связи будем понимать конфигурацию связей между процессами-отправителями и адресатами. С точки зрения топологии различают следующие виды связей: □ 1 —» 1 — между собой связаны только два процесса; □ 1 —> N — один процесс связан с TV процессами; □ N —> 1 — каждый из N процессов связан с одним процессом; □ N —> М — каждый из N процессов связан с каждым из М процессов. Эти виды связей схематически показаны на рис. 14.2, где процессы изображены кругами, связи дугами, а прямоугольники обозначают средства передачи данных. Здесь нужно было бы сказать не средства, а каналы передачи данных, но для названия каналов передачи данных, которые обеспечивают различные виды связей, используются специальные термины. Причем эти термины могут отличаться в различных системах и, кроме того, часто один канал передачи данных может поддерживать различные виды связей между процессами. При разработке систем с обменом данными между процессами, прежде всего, должна быть выбрана топология связей и направления передачи данных по этим связям. После этого в программах реализуются выбранные связи между процессами с использованием специальных функций операционной сие-
240 Часть IV. Обмен данными между параллельными процессами темы, которые предназначены для установки связи между процессами. Эти функции, а также функции для обмена данными между процессами обеспечивает система передачи данных, которая обычно является частью ядра операционной системы. и- а) 1 —1 б) 1 — N /"Л e)N —1 2)N —M Рис. 14.2. Топология связей между процессами 14.3. Передача сообщений Концептуально обмен сообщениями между процессами выполняется при помощи двух функций: □ send — послать сообщение; □ receive — ПОЛУЧИТЬ сообщение. Само сообщение состоит из двух частей: заголовка и тела сообщения. В заголовке сообщения находится такая служебная информация, как: □ тип сообщения; □ имя адресата сообщения; □ имя отправителя сообщения; □ длина сообщения (контрольная информация). Тело сообщения содержит само сообщение.
Глава 14. Передача данных 241 При передаче сообщений может использоваться прямая или косвенная адресация процессов. При прямой адресации процессов в функциях send и receive явно указываются процессы отправитель и адресат. В этом случае функции обмена данными имеют следующий вид: send(Process Р, сообщение); // послать сообщение процессу Р receive(Process Q, сообщение); // получить сообщение от процесса Q При косвенной адресации в функциях send и receive указываются не адреса, а имя связи, по которой передается сообщение. В этом случае функции обмена данными имеют следующий вид: send(Connection S, сообщение); // послать сообщение по связи S receive(Connection R, сообщение); // получить сообщение по связи R Адресация процессов может быть симметричной и асимметричной. Если при обмене сообщениями между процессами используется только прямая или только косвенная адресация, то такая адресация процессов называется симметричной. Если же при обмене сообщениями между процессами используется как прямая, так и косвенная адресация, то такая адресация процессов называется асимметричной. Асимметричная адресация процессов используется в системах "клиент-сервер". В этом случае клиенты знают адрес сервера и посылают ему сообщения, используя функцию: send(Process Сервер, сообщение); Сервер "слушает" канал связи и принимает сообщения от всех клиентов, используя функцию: receive(Connection S, сообщение); Часто эта функция так и называется listen (слушать). В заключение этого раздела дадим важное определение. Набор правил, по которым устанавливаются связи и передаются данные между процессами, называется протоколом. Можно немного пояснить, в чем заключаются правила протокола с точки зрения программирования передачи данных. Эти правила включают набор процедур или функций, которые используются для передачи данных, и порядок использования этих функций. То есть при использовании функций нужно учитывать определенную последовательность их вызовов. Например, нельзя начинать передачу данных, не установив связь между отправителем и адресатом. 14.4. Синхронный и асинхронный обмен данными При передаче данных различают синхронный и асинхронный обмен данными. Если поток-отправитель, отправив сообщение функцией send, блокируется до получения этого сообщения потоком-адресатом, то такое отправление
242 Часть IV. Обмен данными между параллельными процессами сообщения называется синхронным. В противном случае отправление сообщения называется асинхронным. Если поток-адресат, вызвавший функцию receive, блокируется до тех пор, пока не получит сообщение, то такое получение сообщения называется синхронным. В противном случае получение сообщения называется асинхронным. Обмен сообщениями называется синхронным, если поток-отправитель синхронно передает сообщения, а поток-адресат синхронно принимает эти сообщения. В противном случае обмен сообщениями называется асинхронным. Предполагается, что обмен данными потоком всегда происходит синхронным образом, т. к. в этом случае между отправителем и адресатом устанавливается непосредственная связь. Синхронный обмен данными в случае прямой адресации процессов называется рандеву (rendezvous, франц. "встреча"). Такой механизм обмена сообщениями используется в языке программирования Ада. Учитывая возможности синхронного обмена данными, этот механизм может использоваться для решения задач синхронизации процессов. При этом следует отметить, что в операционных системах Windows синхронизация процессов, выполняющихся на разных компьютерах в локальной сети, может осуществляться только при помощи механизма синхронного обмена данными. 14.5. Буферизация Буфером называется вместимость связи между процессами, т. е. количество сообщений, которые могут одновременно пересылаться по этой связи. Различаются три типа буферизации: □ нулевая вместимость связи (нет буфера) — в этом случае возможен только синхронный обмен данными между процессами; □ ограниченная вместимость связи (ограниченный буфер) — в этом случае, если буфер полон, то отправитель сообщения должен ждать очистки буфера хотя бы от одного сообщения; □ неограниченная вместимость связи (неограниченный буфер) — в этом случае отправитель никогда не ждет при отправке сообщения. Как видно из этих определений, типы буферизации тесно связаны с синхронизацией передачи данных и поэтому также должны учитываться при разработке систем, которые содержат обмен данными между процессами.
Глава 15 Работа с анонимными каналами в Windows Так как работа с анонимными каналами требует совместного использования целого ряда функций, то данная глава организована следующим образом. Сначала дано определение анонимных каналов и рассмотрены все функции, которые предназначены для работы с ними. Потом приведены примеры, иллюстрирующие использование этих функций. В последнем разделе показано, как при помощи анонимных каналов можно перенаправить стандартный ввод-вывод. 15.1. Анонимные каналы Анонимным каналом называется объект ядра операционной системы, который обеспечивает передачу данных между процессами, выполняющимися на одном компьютере. Процесс, который создает анонимный канал, называется сервером анонимного канала. Процессы, которые связываются с анонимным каналом, называются клиентами анонимного канала. Другими словами можно сказать, что анонимный — это такой канал передачи данных между процессами, который не имеет имени. Следовательно, доступ к такому каналу имеют только родительский процесс-сервер и дочерние процессы-клиенты этого канала. Перечислим характеристики анонимных каналов, которые необходимо учитывать при их использовании для обмена данными между параллельными процессами: □ не имеют имени; □ полудуплексные; □ передача данных потоком; □ синхронный обмен данными; □ возможность моделирования любой топологии связей.
244 Часть IV. Обмен данными между параллельными процессами Немного поясним характеристику направления передачи данных по анонимному каналу. Так как анонимный канал полудуплексный, то может создаться впечатление, что по этому каналу можно передавать данные от процесса к процессу только в одном направлении. На самом деле это не так. Да, действительно, по анонимному каналу можно передавать данные только в одном направлении. Но каждый конец анонимного канала имеет свой дескриптор, который можно передать любому дочернему процессу. Поэтому каждый процесс может как записывать данные в анонимный канал, так и читать данные оттуда. ( Замечание ^ В операционных системах Windows NT анонимный канал реализован при помощи именованного канала с уникальным именем. Отсюда следует, что в этих операционных системах функции, которые работают с именованными каналами, могут работать также и с анонимными каналами. Теперь приведем порядок работы с анонимными каналами, которого и будем придерживаться в дальнейшем: □ создание анонимного канала сервером; □ соединение клиентов с каналом; □ обмен данными по каналу; □ закрытие канала. Подробно эти пункты работы с анонимными каналами будут рассмотрены в следующих разделах этой главы. 15.2. Создание анонимных каналов Анонимные каналы создаются процессом-сервером при помощи функции createPipe, которая имеет следующий прототип: BOOL CreatePipe( PHANDLE hReadHandle, // дескриптор для чтения из канала PHANDLE hWriteHandle, // дескриптор для записи в канал LPSEOTRITY_ATTRIBUTES lpPipeAttributes, II атрибуты защиты DWORD dwSize // размер буфера в байтах ); При удачном завершении функция CreatePipe возвращает ненулевое значение, а в случае неудачи — false. Рассмотрим кратко назначение параметров этой функции. В случае успешного завершения функция CreatePipe создает два дескриптора анонимного канала — один для чтения данных из канала, а второй для записи данных в канал. Эти дескрипторы возвращаются в переменных,
Глава 15. Работа с анонимными каналами в Windows 245 на которые указывают параметры hReadPipe и hwritePipe. Дескриптор, на который указывает параметр hReadPipe, в дальнейшем используется в функциях чтения данных из канала, а дескриптор hwritePipe — в функциях записи данных в канал. Параметр IpPipeAttributes определяет атрибуты защиты анонимного канала. Установку значения этого параметра IpPipeAttributes мы рассмотрим в разд. 15.3. Параметр dwsize определяет размер буфера ввода-вывода анонимного канала. Отметим, что операционные системы Windows автоматически определяют размер буфера и поэтому значение параметра dwsize является только пожеланием операционной системе при выборе размера буфера. Значение этого параметра можно установить равным 0, тогда операционная система выберет размер буфера по умолчанию. 15.3. Соединение клиентов с анонимным каналом Так как анонимные каналы не имеют имени, то для соединения процесса- клиента с таким каналом необходимо передать ему один из дескрипторов анонимного канала. При этом передаваемый дескриптор должен быть наследуемым, а сам процесс-клиент должен быть дочерним процессом процесса сервера анонимного канала и наследовать наследуемые дескрипторы процесса-сервера. Наследование дескрипторов анонимного канала определяется значением поля binheritHandie в структуре типа security_attributes, на которую указывает параметр IpPipeAttributes функции CreatePipe. ЕСЛИ значение ЭТОГО ПОЛЯ, которое имеет тип bool, равно true, to дескрипторы анонимного канала создаются наследуемыми, в противном случае — дескрипторы создаются ненаследуемыми. Если процессу-клиенту передаются два дескриптора анонимного канала, то естественно, что оба они должны быть сделаны наследуемыми при создании анонимного канала процессом-сервером. Если же процессу-клиенту передается только один из дескрипторов, то возможны два варианта действий. Либо создать наследуемые дескрипторы анонимного канала, а затем тот дескриптор, который не передается клиенту, сделать ненасле- дуемым. Либо, наоборот, создать ненаследуемые дескрипторы анонимного канала, а затем тот дескриптор, который передается клиенту, сделать наследуемым. В операционной системе Windows 98 обе эти задачи решаются путем создания соответственно ненаследуемого или наследуемого дубликата исходного дескриптора, используя функцию DupiicateHandie. После этого исходный дескриптор закрывается. В операционной системе Windows 2000 эта задача также МОЖет быть решена С ПОМОЩЬЮ фунКЦИИ SetHandlelnformation, которая изменяет свойство наследования дескриптора.
246 Часть IV. Обмен данными между параллельными процессами Для того чтобы процесс-клиент наследовал дескрипторы анонимного канала он должен быть создан функцией createProcess в процессе-сервере анонимного канала и параметр binheritHandies этой функции должен быть установлен в true. Явная передача наследуемого дескриптора процессу-клиенту анонимного канала может выполняться одним из следующих способов: □ через командную строку; □ Через ПОЛЯ hStdlnput, hStdOutput И hStdError структуры STARTUPINFO; □ посредством сообщения wm_copydata; □ через файл. В данной главе мы будем использовать только первые два способа передачи дескрипторов процессу-клиенту, что наиболее естественно для анонимных каналов. В завершение этого раздела скажем, что более подробно вопросы наследования и дублирования дескрипторов были рассмотрены в гл. 4. 15.4. Обмен данными по анонимному каналу Для обмена данными по анонимному каналу в операционных системах Windows используются те же функции, что для записи/чтения данных в файл. Для записи данных в анонимный канал используется функция writeFile, которая имеет следующий прототип: BOOL WriteFile( HANDLE hAnonymousPipe, // дескриптор анонимного канала LPCVOID lpBuffer, // буфер данных DWORD dwNumberOfBytesToWrite, // количество байтов для записи LPDWORD lpNumberOfBytesWritten, // количество записанных байтов LPOVERLAPPED lpOverlapped // асинхронный ввод ); Функция WriteFile записывает в анонимный канал количество байтов, заданных параметром dwNumberOfBytesToWrite, из буфера данных, на который указывает параметр lpBuffer. Дескриптор вывода этого анонимного канала должен быть задан первым параметром функции writeFile. При успешном завершении функция writeFile возвращает ненулевое значение, а в случае неудачи — false. Количество байт, записанных функцией writeFile в анонимный канал, возвращается в переменной, на которую указывает параметр lpNumberOfBytesWritten. Параметр lpOverlapped предназначен для выполнения асинхронной операции вывода, т. к. анонимные каналы по/церживают только синхронную передачу данных, то в нашем случае этот параметр всегда будет равен null.
Глава 15. Работа с анонимными каналами в Windows 247 Для чтения данных из анонимного канала используется функция ReadFiie, которая имеет следующий прототип: BOOL ReadFiie( HANDLE hAnonymousPipe, // дескриптор анонимного канала LPCVOID lpBuffer, // буфер данных DWORD dwNumberOfBytesToRead, // количество байт для записи LPDWORD lpNumberOfBytesRead, // количество записанных байтов LPOVERLAPPED lpOverlapped // асинхронный ввод ); Функция ReadFiie читает из анонимного канала количество байт, заданных параметром dwNumberOfBytesToRead, в буфер данных, на который указывает параметр lpBuf f er. Дескриптор ввода этого анонимного канала должен быть задан первым параметром функции ReadFiie. При успешном завершении функция ReadFiie возвращает ненулевое значение, а в случае неудачи — false. Количество байт, прочитанных функцией writeFile из анонимного канала, возвращается в переменной, на которую указывает параметр lpNumberOfBytesRead. Также как и в случае записи в анонимный канал параметр lpOverlapped ДОЛЖен быть равен NULL. Отметим, что обмен данными по анонимному каналу осуществляется только в соответствии с назначением дескриптора этого канала. Дескриптор для записи в анонимный канал должен быть параметром функции WriteFile, а дескриптор для чтения из анонимного канала должен быть параметром функции ReadFiie. В этом и состоит смысл передачи данных по анонимному каналу только в одном направлении. Однако это не означает, что один процесс может использовать анонимный канал только для записи или только для чтения. Один и тот же процесс может как писать в анонимный канал, так и читать данные из него, должным образом используя дескрипторы этого канала. В завершение этого раздела отметим, что после завершения обмена данными по анонимному каналу потоки должны закрыть дескрипторы записи и чтения аноНИМНОГО канала, ИСПОЛЬЗуя фуНКЦИЮ CloseHandle. 15.5. Примеры работы с анонимными каналами Сначала рассмотрим пример, в котором процесс-сервер создает анонимный канал и дочерний процесс, которому передает один из дескрипторов этого анонимного канала. Для передачи дескриптора используется командная строка и, в этом случае, дочерний процесс является клиентом анонимного канала. Для определенности передадим клиенту дескриптор для записи в анонимный канал и оставим серверу дескриптор для чтения.
248 Часть IV. Обмен данными между параллельными процессами В листинге 15.1 приведена программа процесса-клиента анонимного канала. эограмма процесса-клиента анонимного канала # inc lude <windows . h> #include <conio.h> int main(int argc, char *argv[]) { HANDLE hWritePipe; // преобразуем символьное представление дескриптора в число hWritePipe = (HANDLE)atoi(argv[l]); // ждем команды о начале записи в анонимный канал _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.Xn", i); SleepE00); } // закрываем дескриптор канала CloseHandle(hWritePipe); _cputs("The process finished writing to the pipe.\n");
Глава 15. Работа с анонимными каналами в Windows 249 _cputs("Press any key to exit.\n"); _getch(); return 0; } В листинге 15.2 приведена программа процесса-сервера анонимного канала, который запускает клиента и передает ему дескриптор записи в анонимный канал через командную строку. Листинг 15.2. Программа процесса-сервера анонимного канала #include <windows.h> #include <conio.h> int main() { char lpszComLine[80]; // для командной строки STARTUPINFO si; PROCESS_INFORMATION pi; HANDLE hWritePipe, hReadPipe, hlnheritWritePipe; // создаем анонимный канал if(!CreatePipe( SnReadPipe, // дескриптор для чтения SnWritePipe, // дескриптор для записи NULL, // атрибуты зашиты по умолчанию, в этом случае // дескрипторы hReadPipe и hWritePipe ненаследуемые 0)) // размер буфера по умолчанию { _cputs("Create pipe failed.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } // делаем наследуемый дубликат дескриптора hWritePipe if(!DuplicateHandle( GetCurrentProcess(), // дескриптор текущего процесса
250 Часть IV. Обмен данными между параллельными процессами hWritePipe, // исходный дескриптор канала GetCurrentProcess(), // дескриптор текущего процесса ShlnheritWritePipe, // новый дескриптор канала О, // этот параметр игнорируется 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(lpszCombine, "C:\\Client.exe %d", (int)hlnheritWritePipe); // запускаем новый консольный процесс if (!CreateProcess( NULL, // имя процесса IpszComLine, // командная строка 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();
Глава 15. Работа с анонимными каналами в Windows 251 } // закрываем дескрипторы нового процесса CloseHandle(pi.hProcess); CloseHandle(pi.hThread); // закрываем ненужный дескриптор канала CloseHandle(hlnheritWritePipe); // читаем из анонимного канала 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; } В программах, приведенных в листингах 15.3 и 15.4, показывается, как организовать двусторонний обмен данными по анонимному каналу между клиентом и сервером. Для этого дескрипторы чтения и записи анонимного канала используются как сервером, так и клиентом этого анонимного канала.
252 Часть IV. Обмен данными между параллельными процессами В этих листингах сначала приведена программа процесса-клиента анонимного канала, а затем программа процесса-сервера анонимного канала. Как и в листингах 15.1, 15.2, дескрипторы анонимного канала передаются через командную строку. I JMtaxMr 15.3, Процесс-клиент анонимного канала LtfbLJL£J*. l:..~L.:i_ ~ ~ ~ ~ ~ #include <windows.h> #include <conio.h> int main(int argc, char *argv[]) { HANDLE hWritePipe, nReadPipe; HANDLE hEnableRead; // для синхронизации обмена данными char lpszEnableRead[] = "EnableRead"; // открываем событие, разрешающее чтение hEnableRead = OpenEvent(EVENT_ALL_ACCESS/ FALSE, IpszEnableRead); // преобразуем символьное представление дескрипторов в число hWritePipe = (HANDLE)atoi(argv[l]); nReadPipe = (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();
Глава 15. Работа с анонимными каналами в Windows 253 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.Xn", 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;
254 Часть IV. Обмен данными между параллельными процессами В листинге 15.4 приведена программа процесса-сервера анонимного канала, который запускает клиента и передает ему дескрипторы анонимного канала через командную строку. # include <windows.h> #include <conio.h> int main () { char IpszComLine[80]; // для командной строки HANDLE hEnableRead; // для синхронизации обмена данными char lpszEnableRead[] = "EnableRead"; STARTUP INFO si; PROCESS_INFORMATION pi; HANDLE hWritePipe, hReadPipe; SECURITY_ATTRIBUTES sa; // создаем событие для синхронизации обмена данными hEnableRead = CreateEvent(NULL, FALSE, FALSE, IpszEnableRead); // устанавливает атрибуты защиты канала sa.nLength = sizeof(SECURITY_ATTRIBUTES); sa.lpSecurityDescriptor = NULL; // защита по умолчанию sa.blnheritHandle = TRUE; // дескрипторы наследуемые // создаем анонимный канал if(!CreatePipe( ShReadPipe, // дескриптор для чтения ShWritePipe, // дескриптор для записи &sa, // атрибуты защиты по умолчанию, дескрипторы наследуемые 0)) // размер буфера по умолчанию { _cputs("Create pipe failed.\n"); _cputs("Press any key to finish.\n");
Глава 15. Работа с анонимными каналами в Windows 255 _getch(); return GetLastError(); } // устанавливаем атрибуты нового процесса ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); // формируем командную строку wsprintf(IpszComLine, "С:\\Client.exe %d %d", (int)hWritePipe, (int)hReadPipe); // запускаем новый консольный процесс if (ICreateProcess( NULL, // имя процесса IpszComLine, // командная строка 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(
256 Часть IV. Обмен данными между параллельными процессами 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.Xn", nData); } _cputs("The process finished reading from the pipe.\n"); // даем сигнал на разрешение чтения клиентом SetEvent(hEnableRead); // пишем ответ в анонимный канал for (int j = 10; j < 20; j++) { DWORD dwBytesWritten; if (!WriteFile( hWritePipe, sizeof(j), &dwBytesWri tten, 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.Xn", j); }■ // закрываем дескрипторы канала CloseHandle(hReadPipe); CloseHandle(hWritePipe);
Глава 15. Работа с анонимными каналами в Windows 257 CloseHandle (hEnableRead) ; _cputs("The process finished writing to the pipe.\n"); _cputs("Press any key to exit.\n"); _getch(); return 0; } Отметим в листинге 15.4 следующий момент: для организации двустороннего обмена данными по анонимному каналу сервер и клиенты канала должны синхронизировать доступ к этому каналу. То есть для организации передачи данных необходимо разработать протокол передачи данных или использовать объекты синхронизации, которые исключают одновременный неконтролируемый доступ параллельных потоков к анонимному каналу. В приведенном листинге событие hEnableRead сигнализирует клиенту о том, что сервер закончил чтение данных и теперь данные из канала может читать клиент. При отсутствии такой синхронизации возможно одновременное чтение данных сервером и клиентом, т. к. они работают параллельно, и это вызовет неправильную работу программы и ее зависание. 15.6. Перенаправление стандартного ввода-вывода Анонимные каналы часто используются для перенаправления стандартного ввода-вывода. Чтобы подробнее разобраться с этим вопросом, сначала кратко рассмотрим стандартные средства ввода-вывода, используемые в языке программирования C++. Компилятор языка программирования C++ фирмы Microsoft содержит стандартную библиотеку, которая поддерживает три варианта функций стандартного ввода-вывода. Эти функции описываются в заголовочных файлах stdio.h, iostream.h и conio.h. Функции, которые описаны в файле stdio.h, обеспечивают ввод-вывод в следующие стандартные файлы: □ stdin — стандартный файл ввода; □ stdout — стандартный файл вывода; □ stderr — файл вывода сообщений об ошибках. Эти функции составляют стандартную библиотеку ввода-вывода языка программирования С. Функции и операторы, которые описаны в заголовке iostream.h, обеспечивают ввод-вывод в стандартные потоки. Эти функции составляют стандарт- 9 Зак. 514
258 Часть IV. Обмен данными между параллельными процессами ную библиотеку ввода-вывода в языке программирования C++. При создании консольного процесса или при распределении консоли с помощью функции Aiiocconsole стандартные файлы и стандартные потоки ввода- вывода связываются с дескрипторами, которые заданы в полях hstdinput, hstdoutput и hstdError структуры типа startupinfo. Поэтому если в эти поля будут записаны соответствующие дескрипторы анонимного канала, то для передачи данных по анонимному каналу можно использовать функции стандартного ввода-вывода. Такая процедура называется перенаправлением стандартного ввода-вывода. Функции ввода-вывода из заголовочного файла conio.h отличаются от функций ввода-вывода из заголовочного файла stdio.h стандартной библиотеки языка программирования С тем, что они всегда работают с консолью. Поэтому эти функции можно использовать для ввода-вывода на консоль даже в случае перенаправления стандартного ввода-вывода. В листингах 15.5, 15.6, 15.7 приведены программы, в которых стандартный ввод-вывод перенаправляется в анонимный канал, а для обмена данными по анонимному каналу используются перегруженные операторы ввода-вывода языка программирования C++. Пример включает программы следующих процессов: два процесса-клиента, которые обмениваются данными по анонимному каналу, и процесс-сервер, который создает клиентов и передает им дескрипторы анонимного канала через поля структуры startupinfo. В листингах 15.5, 15.6 приведены программы процессов-клиентов. | Листинг 15.5, Процесс-клиент, записывающий данные в анонимный канал # include <windows.h> #include <conio.h> #include <iostream.h> int main() { // события для синхронизации обмена данными HANDLE hReadFloat, hReadText; char lpszReadFloat[] = "ReadFloat"; char IpszReadText[] = "ReadText"; // открываем события hReadFloat = CreateEvent(NULL, FALSE, FALSE, IpszReadFloat); hReadText = CreateEvent(NULL, FALSE, FALSE, IpszReadText); // ждем команды о начале записи в анонимный канал
Глава 15. Работа с анонимными каналами в Windows 259 _cputs("Press any key to start communication.\n"); _getch(); // пишем целые числа в анонимный канал for (int i = 0; i < 5; ++i) { SleepE00); 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); } // отмечаем, что можно читать текст из анонимного канала 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;
260 Часть IV Обмен данными между параллельными процессами #include <windows.h> #include <conio.h> #include <iostream.h> int mainO { // события для синхронизации обмена данными HANDLE hReadFloat, hReadText; char IpszReadFloat[] = "ReadFloat"; char IpszReadText[] = "ReadText"; // открываем события hReadFloat = CreateEvent(NULL, FALSE, FALSE, IpszReadFloat); hReadText = CreateEvent(NULL, FALSE, FALSE, IpszReadText); // читаем целые числа из анонимного канала for (int i = 0; i < 5; ++i) { int nData; cin » nData; _cprintf("The number %d is read from the pipe.Xn", nData); } // разрешаем читать плавающие числа из анонимного канала SetEvent(hReadFloat); // пишем плавающие числа в анонимный канал for (int j = 0; j < 5; ++j) { SleepE00); cout « (j*0.1) « endl; } // ждем разрешения на чтение текста WaitForSingleObject(hReadText, INFINITE); _cputs("The process read the text: "); // теперь читаем текст char lps zInput[80];
Глава 15. Работа с анонимными каналами в Windows 261 do { SleepE00); cin » lpszlnput; _cputs(lps zInput); _cputs(" "); } while (*lpszlnput != ' \0'); _cputs("\nThe process finished transmission of data.Xn"); _cputs("Press any key to exit.\n"); _getch(); CloseHandle(hReadFloat); CloseHandle(hReadText); return 0; } Теперь приведем, в листинге 15.7, программу процесса-сервера анонимного канала. Эта программа просто создает двух клиентов анонимного канала и прекращает свою работу. #include <windows.h> #include <conio.h> int main() { char lpszComLinel[80] char lpszComLine2[80] STARTUPINFO si; PROCESS_INFORMATION pi; HANDLE hWritePipe, hReadPipe; SECURITY_ATTRIBUTES sa; // устанавливает атрибуты защиты канала sa.nLength = sizeof(SECURITY_ATTRIBUTES); = "С:WClientl.exe"; // имя первого клиента = "С:\\Client2.ехе"; // имя второго клиента
262 Часть IV. Обмен данными между параллельными процессами sa.lpSecurityDescriptor = NULL; // защита по умолчанию sa.blnheritHandle = TRUE; // дескрипторы наследуемые // создаем анонимный канал if(!CreatePipe( ShReadPipe, // дескриптор для чтения ShWritePipe, // дескриптор для записи &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.hStdlnput = hReadPipe; si.hStdOutput = hWritePipe; si.hStdError = hWritePipe; // запускаем первого клиента if (ICreateProcess( NULL, // имя процесса IpszCoraLinel, // командная строка NULL, // атрибуты защиты процесса по умолчанию NULL, // атрибуты защиты первичного потока по умолчанию TRUE, // наследуемые дескрипторы текущего процесса // наследуются новым процессом CREATE_NEW_CONSOLE, // создаем новую консоль NULL, // используем среду окружения процесса-предка NULL, // текущий диск и каталог, как и в процессе-предке
Глава 15. Работа с анонимными каналами в Windows 263 &si, // вид главного окна - по умолчанию &pi // здесь будут дескрипторы и идентификаторы // нового процесса и его первичного потока ) ) { _cputs("Create process failed.\n"); _cputs("Press any key to finish.\n"); _getch(); return GetLastError(); } // закрываем дескрипторы первого клиента 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();
264 Часть IV Обмен данными между параллельными процессами } // закрываем дескрипторы второго клиента 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; }
Глава 16 Работа с именованными каналами в Windows Работа с именованными каналами, также как и работа с анонимными каналами, требует совместного использования целого ряда функций. Поэтому сначала будут даны характеристики именованных каналов и рассмотрены все функции, которые предназначены для передачи данных по именованным каналам. Затем будут приведены несколько примеров, которые проиллюстрируют использование этих функций. После этого будут рассмотрены служебные функции для работы с именованными каналами. 16.1. Именованные каналы Именованным каналом называется объект ядра операционной системы, который обеспечивает передачу данных между процессами, выполняющимися на компьютерах в одной локальной сети. Процесс, который создает именованный канал, называется сервером именованного канала. Процессы, которые связываются с именованным каналом, называются клиентами именованного канала. Перечислим характеристики именованных каналов: □ имеют имя, которое используется клиентами для связи с именованным каналом; □ могут быть как полудуплексные, так и дуплексные; □ передача данных может осуществляться как потоком, так и сообщениями; □ обмен данными может быть как синхронным, так и асинхронным; □ возможность моделирования любой топологии связей. Теперь приведем порядок работы с именованными каналами, который и будет использоваться в дальнейшем: □ создание именованного канала сервером: □ соединение сервера с экземпляром именованного канала; □ соединение клиента с экземпляром именованного канала;
266 Часть IV. Обмен данными между параллельными процессами П обмен данными по именованному каналу; □ отсоединение сервера от экземпляра именованного канала; □ закрытие именованного канала клиентом и сервером. Подробно эти пункты будут рассмотрены в следующих разделах. 16.2. Создание именованных каналов Именованные каналы создаются процессом-сервером при помощи функции CreateNamedPipe, КОТОрая имеет Следующий ПрОТОТИП! HANDLE CreateNamedPipe( LPCTSTR lpName, // имя канала DWORD dwOpenMode, // атрибуты канала DWORD dwPipeMode, // режим передачи данных DWORD nMaxInstances, // максимальное количество экземпляров канала DWORD nOutBufferSize, // размер выходного буфера DWORD nlnBufferSize, // размер входного буфера DWORD nDefaultTimeOut, // время ожидания связи с клиентом LPSECURITY_ATTRIBUTES lpPipeAttributes // атрибуты безопасности ); При удачном завершении функция CreateNamedPipe возвращает дескриптор именованного канала, а в случае неудачи — одно из двух значений: □ iiwalid_handle_value — неудачное завершение; □ ERROR_INVALID_PARAMETR — значение Параметра nMaxInstances больше, чем величина pipe_unlimited_instances. Опишем параметры этой функции. Параметр lpName указывает на строку, которая должна иметь вид: \\.\pipe\pipe_name Здесь "." обозначает локальную машину, т. к. новый именованный канал всегда создается на локальной машине, слово pipe — фиксировано, a pipe_name обозначает имя канала, которое задается пользователем и нечувствительно к регистру. Параметр dwOpenMode задает флаги, которые определяют направление передачи данных, буферизацию, синхронизацию обмена данными и права доступа к именованному каналу. Для определения направления передачи данных используются флаги: □ pipe_access_duplex — чтение и запись в канал; □ pipe_access_inbound — клиент пишет, а сервер читает данные; □ pipe_access_outbound — сервер пишет, а клиент читает данные.
Глава 16. Работа с именованными каналами в Windows 267 Кроме того, каждый из этих флагов позволяет использовать дескриптор канала для синхронизации. Флаг, определяющий направление передачи данных по именованному каналу, должен совпадать для всех экземпляров одного и того же именованного канала. Для определения способа буферизации и синхронизации используются флаги: □ file_flag_write_through — запрещает буферизацию при передаче данных по сети; □ file_flag_overlapped — разрешает асинхронную передачу данных по каналу. Эти флаги могут быть разными для каждого экземпляра одного и того же именованного канала. В этом же параметре могут быть заданы другие режимы доступа к именованному каналу. Флаги, используемые для определения атрибутов безопасности, подробно рассмотрены в разд. 36.7, 41.3. Здесь же только скажем, что рассмотренные выше флаги pipe_access_duplex, pipe_access_inbound и pipe_access_outbound определяют специфические права доступа к именованному каналу, которые включают следующие родовые права доступа: □ pipe_access_duplex — включает родовые права доступа generic_read, GENERIC_WRITE И SYNCHRONIZE; □ pipe_access_inbound — включает родовые права доступа generic_read и synchronize; □ pipe_access_outbound — включает родовые права доступа generic_write И SYNCHRONIZE. Подробнее о правах доступа рассказано в разд. 36.7, 41.3. Параметр dwPipeMode задает флаги, определяющие способ передачи данных по именованному каналу. Для определения способов чтения и записи данных в именованный канал используются флаги: □ pipe_type_byte — запись данных потоком; □ pipe_type_message — запись данных сообщениями; □ pype_readmode_byte — чтение данных потоком; □ pype_readmode_message — чтение данных сообщениями. По умолчанию данные по именованному каналу передаются потоком. Флаги, определяющие способ чтения и записи данных в именованный канал, должны совпадать для всех экземпляров одного и того же канала. Для определения синхронизации доступа к именованному каналу используются флаги: □ pipe_wait — синхронная связь с каналом и обмен данными по каналу; □ pipe_nowait — асинхронная связь с каналом и обмен данными по каналу.
268 Часть IV. Обмен данными между параллельными процессами Эти флаги могут быть разными для каждого экземпляра именованного канала. Параметр nMaxinstances определяет максимальное число экземпляров именованного канала, которое может находиться в пределах от 1 до pipe_unlimited_instances. Каждый экземпляр канала предназначен для обмена данными по каналу между сервером и отдельным клиентом. Параметры nOutBufferSize И nlnBufferSize определяют соответственно размеры выходного и входного буферов для обмена данными по именованному каналу. Однако эти значения рассматриваются операционными системами Windows только как пожелания пользователя, а сам выбор размеров буферов остается за операционной системой. Параметр nDefauitTimeOut устанавливает время ожидания клиентом связи с сервером. Это время используется при вызове клиентом функции WaitNamedPipe, В КОТОрОЙ параметр nTimeOut имеет значение NMPWAIT_USE_ DEFAULT_WAIT. Для связи сервера с несколькими клиентами по одному именованному каналу сервер должен создать несколько экземпляров этого канала. Каждый экземпляр именованного канала создается вызовом функции createNamedPipe, которая возвращает дескриптор экземпляра именованного канала. Отметим, что в этом случае поток, создающий экземпляр именованного канала, должен иметь право доступа file_create_pipe_instance к именованному каналу. Этим правом по умолчанию обладает владелец именованного канала, т. е. тот процесс, который создал этот именованный канал. 16.3. Соединение сервера с клиентом После того как сервер создал именованный канал, он должен дождаться соединения клиента с этим каналом. Для этого сервер вызывает функцию connectNamedPipe, которая имеет следующий прототип; BOOL ConnectNamedPipe( HANDLE hNamedPipe, // дескриптор канала LPOVERLAPPED lpOverlapped // асинхронная связь ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — значение false. Сервер использует эту функцию для связи с клиентом по каждому свободному экземпляру именованного канала. После окончания обмена данными с клиентом сервер может вызвать функцию DisconnectNamedPipe, КОТОрая имеет следующий прОТОТИП*. BOOL DisconnectNamedPipe ( HANDLE hNamedPipe // дескриптор канала
Глава 16. Работа с именованными каналами в Windows 269 И возвращает ненулевое значение — в случае успеха — или значение false — в случае неудачи. Эта функция разрывает связь сервера с клиентом. После этого клиент не может обмениваться данными с сервером по данному именованному каналу и поэтому любая операция доступа к именованному каналу со стороны клиента вызовет ошибку. После разрыва связи с одним клиентом сервер снова может вызвать функцию connectNamedPipe, чтобы установить связь по этому же именованному каналу с другим клиентом. 16.4. Соединение клиентов с именованным каналом Прежде чем соединяться с именованным каналом, клиент должен определить, доступен ли какой-либо экземпляр этого канала для соединения. С этой целью клиент должен вызвать функцию waitNamedpipe, которая имеет следующий прототип: BOOL Wa i tNamedP ipe ( LPCTSTR lpNamedPipeName, // указатель на имя канала DWORD nTimeOut // интервал ожидания ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Опишем параметры этой функции. Параметр lpNamedPipeName указывает на строку, которая должна иметь вид: \ \ s erver_name \pipe \pipe_name Здесь server_name обозначает имя компьютера, на котором выполняется сервер именованного канала, слово pipe фиксировано, a pipe_name задает имя именованного канала. Параметр nTimeOut задает временной интервал, в течение которого клиент ждет связь с сервером. Этот временной интервал определяется в миллисекундах или может быть равен одному из следующих значений: □ nmpwait_use_default_wait — интервал времени ожидания определяется значением параметра nDefauitTimeOut, который задается в функции CreateNamedPipe; □ nmpwait_wait_forever — бесконечное время ожидания связи с именованным каналом. Сделаем два важных замечания относительно работы функции waitNamedpipe. ( Замечание ^ Если не существует экземпляров именованного канала с именем lpNamedPipe, то функция WaitNamedpipe немедленно завершается неудачей независимо от времени ожидания, заданного параметром nTimeOut.
270 Часть IV. Обмен данными между параллельными процессами ( Замечание ^ Если клиент соединяется с каналом до вызова сервером функции ConnectNamedPipe, то функция WaitNamedPipe возвращает значение FALSE и ФУНКЦИЯ GetLastError Вернет КОД ERROR_PIPE_CONNECTED. ПОЭТОМУ фуНКЦИЮ WaitNamedPipe нужно вызывать только после соединения сервера с каналом посредством функции ConnectNamedPipe. После того как обнаружен свободный экземпляр канала, чтобы установить связь с этим каналом клиент должен вызвать функцию CreateFiie, которая имеет следующий прототип: HANDLE CreateFiie( LPCTSTR lpFileName, // указатель на имя канала DWORD dwDesiredAccess, // чтение или запись в канал DWORD dwShareMode, // режим совместного использования LPSECURITY_ATTRIBUTES IpSecurityAttributes, // атрибуты безопасности DWORD dwCreationDisposition, // флаг открытия канала DWORD dwFlagsAndAttributes, II флаги и атрибуты HANDLE hTemplateFile // дополнительные атрибуты ); В случае успешного завершения эта функция возвращает дескриптор именованного канала, а в случае неудачи — значение invalid_handle_value. Если функция используется для открытия именованного канала, то ее параметры могут принимать следующие значения. Параметр lpFileName должен указывать на имя канала, которое должно быть задано в том же формате, что и в функции WaitNamedPipe. Отметим, что если клиент работает на той же машине, что и сервер, и использует для открытия именованного канала в функции createFiie имя канала как \\.\pipe\pipe_name, то файловая система именованных каналов (Named Pipe File System, NPFS) открывает этот именованный канал в режиме передачи данных потоком. Чтобы открыть именованный канал в режиме передачи данных сообщениями нужно задавать имя канала в виде: \ \server_name\pipe\pipe_name Параметр dwDesiredAccess может принимать одно из следующих значений: □ о — разрешает получить атрибуты канала; □ generic_read — разрешает чтение из канала; □ generic_write — разрешает запись в канал. Следует отметить, что функция CreateFiie завершается неудачей, если доступ к именованному каналу, заданный этими значениями, не соответствует
Глава 16. Работа с именованными каналами в Windows 271 значениям параметра dwOpenMode В функции CreateNamedPipe. Кроме ТОГО, в этом параметре программист может определить стандартные права доступа к именованному каналу. За более подробной информацией по этому вопросу нужно обратиться к разд. 36.7, 41.3, в которых описываются стандартные режимы доступа. Параметр dwshareMode определяет режим совместного использования именованного канала и может принимать значение 0, которое запрещает совместное использование именованного канала или любую комбинацию следующих значений: □ file_share_read — разрешает совместное чтение из канала; □ file_share_write — разрешает совместную запись в канал. Параметр lpSecurityAttributes задает атрибуты безопасности именованного канала. Пока значение этого параметра будем устанавливать в null. Для именованного канала параметр dwCreationDisposition должен быть равен значению open_existing, т. к. клиент всегда открывает существующий именованный канал. Для именованного канала параметр dwFiagsAndAttributes можно задать равным 0, что определяет флаги и атрибуты по умолчанию. Подробную информацию о значениях этого параметра смотрите в гл. 24, посвященной работе с файлами в Windows. Значение параметра hTempiateFile задается равным null. Сделаем следующие замечания относительно работы с функцией createFile в случае, когда она используется для открытия доступа к именованному каналу. ( Замечание J Несмотря на то, что функция waitNamedPipe может успешно завершиться, последующий вызов функции CreateFile может завершиться неудачей по следующим причинам: между вызовами этих функций сервер закрыл канал или между вызовами функций другой клиент связался с экземпляром этого канала. Для предотвращения последней ситуации сервер должен создавать новый экземпляр именованного канала после каждого успешного завершения функции ConnectNamedPipe или создать сразу несколько экземпляров именованного канала. ( Замечание ^ Если заранее известно, что сервер вызвал функцию ConnectNamedPipe, то функция CreateFile может вызываться без предварительного вызова функции Wai tNamedPipe.
272 Часть IV. Обмен данными между параллельными процессами Отметим также один момент, который касается связи сервера с клиентом именованного канала. Может возникнуть такая ситуация, что сервер вызвал функцию connectNamedPipe, а клиента, который хочет связаться с именованным каналом, не существует. В этом случае серверное приложение будет заблокировано. Чтобы иметь возможность обработать такую ситуацию, функцию ConnectNamedPipe следует вызывать в отдельном потоке серверного приложения. Тогда для разблокировки серверного приложения можно вызвать функцию для связи клиента с именованным каналом из другого потока этого приложения. 16.5. Обмен данными по именованному каналу Как и в случае с анонимным каналом, для обмена данными по именованному каналу ИСПОЛЬЗУЮТСЯ фуНКЦИИ ReadFile И WriteFile, НО С ОДНИМ ОТ- личием, которое заключается в следующем. Так как в случае именованного канала разрешен асинхронный обмен данными, то в функциях ReadFile и WriteFile может использоваться параметр lpoveriapped при том условии, ЧТО В ВЫЗОВе фуНКЦИИ CreateNamedPipe В параметре dwOpenMode был установлен флаг file_flag_overlapped. Максимально в именованный канал может быть записано до 65 535 байт одной операцией WriteFile. Подробно работа с функциями ReadFile и WriteFile рассмотрена в гл. 24, которая посвящена работе с файлами в Windows. Кроме того, отметим, что для асинхронного обмена данными по именованному каналу могут использоваться также функции ReadFiieEx и writeFiieEx, которые также рассмотрены в гл. 24. После завершения обмена данными по именованному каналу потоки должны закрыть дескрипторы экземпляров именованного канала, используя фуНКЦИЮ CloseHandle. Теперь перейдем к примерам программ, в которых показано, как обмениваться данными по именованному каналу. Сначала рассмотрим пример, в котором процесс-сервер создает именованный канал, а затем ждет, пока клиент не соединится с именованным каналом. После этого сервер читает из именованного канала десять чисел и выводит их на консоль. Сначала приведем программу процесса-сервера именованного канала. ^^^^nlfll Процесс-сервер именованного канала ; #include <windows.h> #include <iostream.h> int main()
Глава 16. Работа с именованными каналами в Windows 273 { HANDLE hNamedPipe; // создаем именованный канал для чтения hNamedPipe = CreateNamedPipe ( "\\\\.\\pipe\\demo_pipe", // имя канала PIPE_ACCESS_INBOUND, // читаем из канала PIPE_TYPE_MESSAGE | PIPE_WAIT, // синхронная передача сообщений 1, I/ максимальное количество экземпляров канала О, // размер выходного буфера по умолчанию О, // размер входного буфера по умолчанию INFINITE, // клиент ждет связь бесконечно долго NULL // защита по умолчанию ); // проверяем на успешное создание if (hNamedPipe == INVALID_HANDLE_VALUE) { cerr « "Create named pipe failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to exit."; cin.get(); return 0; } // ждем, пока клиент свяжется с каналом cout « "The server is waiting for connection with a client." « endl; if(!ConnectNamedPipe( hNamedPipe, // дескриптор канала NULL // связь синхронная )) { cerr « "The connection failed." « endl « "The last error code: " « GetLastError() « endl; С1oseHandle(hNamedPipe); cout « "Press any key to exit."; cin.get(); return 0;
274 Часть IV. Обмен данными между параллельными процессами } // читаем данные из канала for (int i = 0; i < 10; i++) { int nData; DWORD dwBytesRead; if (!ReadFile( hNamedPipe, // дескриптор канала &nData, // адрес буфера для ввода данных sizeof(nData), // число читаемых байтов SdwBytesRead, // число прочитанных байтов NULL // передача данных синхронная )) { cerr « "Read file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hNamedPipe) ; cout « "Press any key to exit."; cin.get(); 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 key to exit."; cin.get(); return 0; } Теперь, в листинге 16.2, приведем программу процесса-клиента именованного канала, который сначала связывается с именованным каналом, а затем записывает в него десять чисел.
Глава 16. Работа с именованными каналами в Windows 275 ; Листинг 16.2. Процесс-клиент именованного канала } LL~*LL* , 1—>*** х-- -~Л~, #include <windows.h> #include <iostream.h> int main() { HANDLE hNamedPipe; char pipeName [ ] = " \ \ \ \ . \ \pipe\ \demo_pipe" ; // связываемся с именованным каналом hNamedPipe = CreateFile( pipeName, // имя канала GENERIC_WRITE, // записываем в канал FILE_SHARE_READ, // разрешаем одновременное чтение из канала NULL, // зашита по умолчанию OPEN_EXISTING, // открываем существующий канал О, // атрибуты по умолчанию NULL // дополнительных атрибутов нет ); // проверяем связь с каналом if (hNamedPipe == INVALID_HANDLE_VALUE) { cerr « "Connection with the named pipe failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to exit."; return 0; } // пишем в именованный канал for (int i = 0; i < 10; i++) { DWORD dwBytesWritten; if (!WriteFile( hNamedPipe, // дескриптор канала &i, // данные sizeof(i), // размер данных
276 Часть IV. Обмен данными между параллельными процессами SdwBytesWritten, // количество записанных байтов NULL // синхронная запись )) { // ошибка записи cerr « "Writing to the named pipe failed: " « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hNamedPipe); cout « "Press any key to exit."; cin.get(); return 0; } // выводим число на консоль cout « "The number " « i « " is written to the named pipe." « endl; SleepA000); } // закрываем дескриптор канала CloseHandle(hNamedPipe); // завершаем процесс cout « "The data are written by the client." « endl « "Press any key to exit."; cin.get(); return 0; } В листинге 16.3 рассмотрим программу процесса-сервера именованного канала, который сначала создает именованный канал, затем ждет подключения к нему клиента. После этого сервер принимает от клиента одно сообщение, выводит это сообщение на консоль и посылает клиенту ответное сообщение. Листинг 16.3, Сервер именованного канапе, отвечающий на сообщение ^опшиента L£~...&.LS... ._.. . .^...1.. >. ;*..„.„ „. . ..„ „ „ „.. .. „...„..„. .^ „.._. .. ....... J #include <windows.h> #include <iostream.h> #include <string.h>
Глава 16. Работа с именованными каналами в Windows 277 int main() { HANDLE hNamedPipe; DWORD dwBytesRead; // для количества прочитанных байтов DWORD dwBytesWrite; // для количества записанных байтов char pchMessage[80]; // для сообщения int nMessageLength; // длина сообщения // создаем именованный канал для чтения и записи hNamedPipe = CreateNamedPipe ( " WW. WpipeWdemo_pipe", // имя канала PIPE_ACCESS_DUPLEX, // читаем из канала и пишем в канал PIPE_TYPE_MESSAGE | PIPE_WAIT, // синхронная передача сообщений 1, // максимальное количество экземпляров канала О, // размер выходного буфера по умолчанию О, // размер входного буфера по умолчанию INFINITE, // клиент ждет связь бесконечно долго NULL // безопасность по умолчанию >; // проверяем на успешное создание if (hNamedPipe == INVALID_HANDLE_VALUE) { cerr « "Create named pipe failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to exit."; cin.get(); return 0; } // ждем, пока клиент свяжется с каналом cout « "The server is waiting for connection with a client." « endl; if(IConnectNamedPipe( hNamedPipe, // дескриптор канала NULL // связь синхронная )) { cerr « "Connect named pipe failed." « endl « "The last error code: "«GetLastError() « endl;
278 Часть IV. Обмен данными между параллельными процессами CloseHandle(hNamedPipe) ; cout « "Press any key to exit."; cin.get(); return 0; } // читаем сообщение от клиента if (!ReadFile( hNamedPipe, // дескриптор канала pchMessage, // адрес буфера для ввода данных sizeof(pchMessage), // количество читаемых байтов &dwBytesRead, // количество прочитанных байтов NULL)) // передача данных синхронная { cerr « "Data reading from the named pipe failed." « endl « "The last error code: "« GetLastError() « endl; CloseHandle(hNamedPipe); cout « "Press any key to exit."; cin.get(); return 0; } // выводим полученное от клиента сообщение на консоль cout « "The server received the message from a client: " « endl « '\t' « pchMessage « endl; // вводим строку cout « "Input a string: "; сin.getline(pchMessage/ 80); // определяем длину строки nMessageLength = strlen(pchMessage) + 1; // отвечаем клиенту if (!WriteFile( hNamedPipe, // дескриптор канала pchMessage, // адрес буфера для вывода данных nMessageLength, // количество записываемых байтов
Глава 16. Работа с именованными каналами в Windows 279 &dwBytesWrite, // количество записанных байтов NULL // передача данных синхронная )) { cerr « "Write file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hNamedPipe); cout « "Press any key to exit."; cin.get(); return 0; } // выводим посланное клиенту сообщение на консоль cout « "The server sent the message to a client: " « endl « ' \f « pchMessage « endl; // закрываем дескриптор канала CloseHandle(hNamedPipe); // завершаем процесс cout << "Press any key to exit."; cin.get(); return 0; } В этой программе обратим внимание на следующий момент: если клиент и сервер работают на разных компьютерах в локальной сети, то вход, как на компьютер сервера, так и на компьютер клиента, должен осуществляться с одинаковыми именами (логинами) и паролями. Так как по умолчанию атрибуты безопасности именованного канала устанавливаются таким образом, что он принадлежит только пользователю, создавшему этот именованный канал. В следующем примере процесса-сервера именованного канала атрибуты безопасности будут установлены таким образом, чтобы они разрешали доступ к именованному каналу любому пользователю. Теперь же, в листинге 16.4, приведем программу процесса-клиента именованного канала, который сначала вводит с консоли имя компьютера в локальной сети, на котором запущен сервер именованного канала. Затем связывается с этим именованным каналом. После этого клиент передает серверу одно сообщение и получает от него ответное сообщение и выводит на консоль.
280 Часть IV. Обмен данными между параллельными процессами рЙистинг 16 А КЯйент именованного канала, посыпающий сообщение серверу #include <windows.h> #include <iostream.h> #include <string.h> int main() { char machineName [80] ; char pipeName[80] ; HANDLE hNamedPipe; DWORD dwBytesWritten; // для количества записанных байтов DWORD dwBytesRead; // для количества прочитанных байтов char pchMessage[80]; // для сообщения int nMessageLength; // длина сообщения // вводим имя машины в сети, на которой работает сервер cout « "Enter a name of the server machine: "; с in" » machineName; // подставляем имя машины в имя канала wsprintf (pipeName, "\\\\%s\\pipe\\demo_pipe", machineName); ^~i / связываемся с именованным каналом hNamedPipe = CreateFile( pipeName, // имя канала GENERIC_READ | GENERIC_WRITE, // читаем и записываем в канал FILE_SHARE_READ | FILE_SHARE_WRITE, // разрешаем чтение и запись NULL, // безопасность по умолчанию OPEN_EXISTING, // открываем существующий канал FILE_ATTRIBUTE_NORMAL, // атрибуты по умолчанию NULL); // дополнительных атрибутов нет // проверяем связь с каналом if (hNamedPipe==INVALID_HANDLE_VALUE) { cerr « "Connection with the named pipe failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to exit.";
Глава 16. Работа с именованными каналами в Windows 281 cin.get(); return 0; } // вводим строку cin.get(); cout « "Input a string: "; сin.getline(pchMessage, 80); // определяем длину строки nMessageLength = strlen(pchMessage) + 1; // пишем в именованный канал if (!WriteFile( hNamedPipe, // дескриптор канала pchMessage, // данные nMessageLength, // размер данных SdwBytesWritten, // количество записанных байтов NULL)) // синхронная запись { // ошибка записи cerr « "Write file failed: " « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hNamedPipe); cout « "Press any key to exit."; cin.get(); return 0; } // выводим посланное сообщение на консоль cout « "The client sent the message to a server: " « endl « '\t' « pchMessage « endl; // читаем из именованного канала if (!ReadFile( hNamedPipe, // дескриптор канала pchMessage, // данные sizeof(pchMessage), // размер данных SdwBytesRead, // количество записанных байтов NULL)) // синхронное чтение
282 Часть IV. Обмен данными между параллельными процессами { // ошибка чтения cerr « "Read file failed:. " « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hNamedPipe); cout « "Press any key to exit."; cin.get(); return 0; } // выводим полученное сообщение на консоль cout « "The client received the message from a server: " « endl « '\t' « pchMessage « endl; // закрываем дескриптор канала CloseHandle(hNamedPipe); // завершаем процесс cout « "Press any key to exit."; cin.get(); return 0; } Теперь, в листинге 16.5, приведем программу процесса-сервера именованного канала, который создает именованный канал, доступный всем пользователям. В этом случае связаться с именованным каналом можно любому пользователю. | Листинг 16,5. Сервер, создающий общедоступный именованный канал #include <windows.h> #include <iostreaiti.h> #include <string.h> int main() { HANDLE hNamedP ipe; SECURITY_ATTRIBUTES sa; // атрибуты безопасности SECURITY_DESCRIPTOR sd; // дескриптор безопасности DWORD dwBytesRead; // для количества прочитанных байтов
Глава 16. Работа с именованными каналами в Windows 283 DWORD dwBytesWrite; // для количества записанных байтов char pchMessage[80]; // для сообщения int nMessageLength; // длина сообщения // инициализация атрибутов безопасности sa.nLength = sizeof(sa); sa.blnheritHandle = 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, // максимальное количество экземпляров канала О, // размер выходного буфера по умолчанию О, // размер входного буфера по умолчанию INFINITE, // клиент ждет связь бесконечно долго &sa // доступ для всех пользователей ); // проверяем на успешное создание if (hNamedPipe == INVALID_HANDLE_VALUE) { cerr « "Create named pipe failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to exit."; cin.getO ; return 0; } // ждем, пока клиент свяжется с каналом cout « "The server is waiting for connection with a client." « endl; if(!ConnectNamedPipe( hNamedPipe, // дескриптор канала
284 Часть IV Обмен данными между параллельными процессами NULL // связь синхронная )) { cerr « "Connect named pipe failed." « endl « "The last error code: "«GetLastError () « endl; CloseHandle(hNamedPipe); cout « "Press any key to exit."; cin.get(); return 0; } // читаем сообщение от клиента if (!ReadFile( hNamedPipe, // дескриптор канала pchMessage, // адрес буфера для ввода данных sizeof(pchMessage), // количество читаемых байтов &dwBytesRead, // количество прочитанных байтов NULL)) // передача данных синхронная { cerr « "Data reading from the named pipe failed." « endl « "The last error code: "« GetLastError() « endl; С1 oseHandle (hNamedPipe) ; cout « "Press any key to exit."; cin.getO ;. return 0; } // выводим полученное от клиента сообщение на консоль cout « "The server received the message from a client: " « endl « '\t' « pchMessage « endl; // вводим строку cout « "Input a string: "; сin.getline(pchMessage, 80); // определяем длину строки nMessageLength = strlen(pchMessage) + 1; // отвечаем клиенту if (!WriteFile(
Глава 16. Работа с именованными каналами в Windows 285 hNamedPipe, // дескриптор канала pchMessage, // адрес буфера для вывода данных nMessageLength, // количество записываемых байтов SdwBytesWrite, // количество записанных байтов NULL // передача данных синхронная )) { cerr « "Write file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hNamedPipe); cout << "Press any key to exit."; cin.getO ; return 0; } // выводим посланное клиенту сообщение на консоль cout « "The server sent the message to a client: " « endl « '\t' « pchMessage « endl; // закрываем дескриптор канала CloseHandle(hNamedPipe); // завершаем процесс cout « "Press any key to exit."; cin.getO ; return 0; } В заключение этого раздела отметим, что для создания и связи с именованными каналами пользователь должен иметь соответствующие полномочия. 16.6. Копирование данных из именованного канала Для копирования данных из именованного канала используется функция PeekNamedPipe, которая копирует данные в буфер, не удаляя их из канала. Эта функция имеет следующий прототип: BOOL PeekNamedPipe ( HANDLE hNamedPipe, // дескриптор канала
286 Часть IV. Обмен данными между параллельными процессами LPVOID lpBuffer, // буфер данных DWORD nBufferSize, // размер буфера данных LPDWORD lpBytesRead, // количество прочитанных байтов LPDWORD lpTotalBytesAvail, // количество доступных байтов LPDWORD lpBytesLeftThisMessage // количество непрочитанных байтов ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Следует отметить, что если данных в канале нет, то функция немедленно возвращает управление и устанавливает в ноль значения переменных, на которые указывают параметры lpBytesRead, lpTotalBytesAvail И lpBytesLeftThisMessage. Параметры функции PeekNamedPipe имеют следующее назначение. В параметре hNamedPipe должен быть установлен дескриптор именованного канала. Причем канал должен быть открыт в режиме чтения. Параметр lpBuf fer должен указывать на область памяти, в которую функция читает данные из именованного канала. Если в этом параметре установлено значение null, to данные из именованного канала читаться не будут. В параметре nBuf fersize должен быть установлен размер области памяти, на которую указывает параметр lpBuf fer. Параметр lpBytesRead должен указывать на переменную типа dword, в которую функция записывает количество прочитанных байтов. Если данные из именованного канала не читаются, то значение этого параметра может быть установлено в null. Параметр lpTotalBytesAvail должен указывать на переменную типа dword, в которую функция записывает количество доступных для чтения байтов, находящихся в именованном канале. Если данные из именованного канала не читаются, то значение этого параметра может быть установлено в null. Параметр lpBytesLeftThisMessage должен указывать на переменную, в которую функция помещает количество непрочитанных байтов из сообщения. Если данные из именованного канала не читаются, то значение этого параметра может быть установлено в null. Отметим, что если данные передаются потоком, а не сообщениями, то функция PeekNamedPipe читает ровно столько байтов данных, какова длина буфера данных. Если же данные передаются сообщениями, то функция читает полностью сообщение, которое входит в буфер данных. В противном случае читается часть сообщения, а количество непрочитанных байтов возвращается через переменную, на КОТОРУЮ указывает параметр lpBytesLeftThisMessage. В листинге 16.6 приведена программа процесса-клиента именованного канала, КОТОРЫЙ ИСПОЛЬЗует функцию PeekNamedPipe ДЛЯ копирования данных из именованного канала.
Глава 16. Работа с именованными каналами в Windows 287 ■ Листинг 16,6. Пример копирования сообщения клиентом именованного канала j # include <windows.h> #include <iostream.h> #include <string.h> int main() { char machineName [80] ; char pipeName [80] ; HANDLE hNamedPipe; DWORD dwBytesRead; // для количества прочитанных байтов DWORD dwTotalBytesAvail; // количество байтов в канале DWORD dwBytesLeftThisMessage; // количество непрочитанных байтов char pchMessage[80]; // для сообщения // вводим имя машины в сети, на которой работает сервер cout « "Enter a name of the server machine: "; cin » machineName; cin.get () ; // подставляем имя машины в имя канала wspr int f (pipeName, "\\\\%s\\pipe\\demo_pipe", machineName); // связываемся с именованным каналом hNamedPipe = CreateFile( pipeName, // имя канала GENERIC_READ | GENERIC_WRITE, // читаем и записываем в канал FILE_SHARE_READ | FILE_SHARE_WRITE, // разрешаем чтение и запись NULL, // безопасность по умолчанию OPEN_EXISTING, // открываем существующий канал FILE_ATTRIBUTE_NORMAL, // атрибуты по умолчанию NULL); // дополнительных атрибутов нет // проверяем связь с каналом if (hNamedPipe==INVALID_HANDLE_VALUE) { cerr « "Connection with the named pipe failed." « endl « "The last error code: " « GetLastError() « endl;
288 Часть IV Обмен данными между параллельными процессами cout « "Press any key to exit."; cin.get(); return 0; } // ждем команду на копирование сообщения из канала cout « "Press any key to peek a message." « endl; cin.getO; // копируем информацию из именованного канала if (!PeekNamedPipe( hNamedPipe, // дескриптор канала pchMessage, // данные sizeof(pchMessage), // размер данных SdwBytesRead, // количество записанных байтов &dwTotalBytesAvail, // количество байтов в канале &dwBytesLeftThisMessage // количество непрочитанных байтов )) { // ошибка чтения сообщения cerr « "Peek named pipe failed: " « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hNamedPipe); cout « "Press any key to exit."; cin.get(); return 0; } // выводим полученное сообщение на консоль if (dwTotalBytesAvail) cout « "The peeked message: " « endl « '\t' « pchMessage « endl; else cout « "There is no mesage." « endl; // теперь читаем сообщение из именованного канала if (!ReadFile( hNamedPipe, // дескриптор канала
Глава 16. Работа с именованными каналами в Windows 289 pchMessage, // данные sizeof(pchMessage), // размер данных &dwBytesRead, // количество записанных байтов NULL)) // синхронное чтение { // ошибка чтения cerr « "Read file failed: " « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hNamedPipe); cout « "Press any key to exit."; cin.get(); return 0; } // выводим полученное сообщение на консоль cout « "The client received the message from a server: " « endl « '\t' « pchMessage « endl; // закрываем дескриптор канала CloseHandle(hNamedPipe); // завершаем процесс cout « "Press any key to exit." « endl; cin.get(); return 0; } 16.7. Передача транзакций по именованному каналу Для обмена сообщениями по сети может также использоваться функция TransactNamedPipe, которая объединяет операции записи и чтения в одну операцию, которая называется транзакцией. Отметим, что функция TransactNamedPipe может ис пользоваться только в том случае, если сервер при создании именованного канала установил флаг pipe_type_message. ФуНКЦИЯ TransactNamedPipe имеет следующий проТОТИП: BOOL TransactNamedPipe ( HANDLE hNamedPipe, // дескриптор именованного канала LPVOID lpInBuffer, // буфер для записи в канал DWORD dwInBufferSize, // длина буфера для записи 10 Зак. 514
290 Часть IV. Обмен данными между параллельными процессами LPVOID lpOutBuffer, // буфер для чтения из канала DWORD dwOutBufferSize, // длина буфера для чтения LPDWORD lpBytesRead, // количество прочитанных байтов LPOVERLAPPED lpOverlapped // асинхронный доступ к каналу ); В случае успешного завершения функция TransactNamedPipe возвращает ненулевое значение, а в случае неудачи — false. Параметры функции имеют следующее назначение. В параметре hNamedPipe должен быть установлен дескриптор именованного канала. Параметр lpinBuf fer должен указывать на буфер, из которого записываются данные в именованный канал. Параметр dwinBuf fer size должен содержать длину передаваемого сообщения в байтах. Параметр lpOutBuffer должен указывать на буфер, в который читаются данные из именованного канала. Параметр dwOutBuffersize должен содержать длину буфера, в который читается сообщение. Параметр lpBytesRead должен указывать на переменную типа dword, в которую функция поместит количество прочитанных байтов. Если осуществляется асинхронный доступ к именованному каналу, то значение этого параметра можно установить в null. Параметр lpoveriapped используется в том случае, если осуществляется асинхронный доступ к именованному каналу. В этом случае параметр должен указывать на структуру типа overlapped. Пока будем устанавливать этот параметр в null, что задает синхронную передачу данных. Подробно асинхронная передача данных описана в гл. 27. В листинге 16.7 приведена программа процесса-клиента именованного канала, которая передает транзакцию, используя функцию TransactNamedPipe. Отме- тим, что если клиент и сервер именованного канала работают на одном компьютере, то для связи клиента с именованным каналом нужно вводить полное имя компьютера. Если же вместо имени компьютера будет введена точка, то именованный канал откроется в режиме передачи данных потоком, а не сообщениями. Это, в свою очередь, вызовет ошибку при передаче данных. [ Листинг i 6,7. Пример передачи транзакции по именованному каналу #include <windows.h> #include <iostream.h> #include <string.h>
Глава 16. Работа с именованными каналами в Windows 291 int main() { char machineName [80] ; char pipeName [80] ; HANDLE hNamedPipe; DWORD dwBytesRead; // для количества прочитанных байтов char pchlnBuffer[80]; // для записи сообщения char pchOutBuffer[80]; // для чтения сообщения int nMessageLength; // длина сообщения // вводим имя машины в сети, на которой работает сервер cout « "Enter a name of the server machine: "; cin » machineName; // подставляем имя машины в имя канала wspr int f (pipeName, "\\\\%s\\pipe\\demo_pipe", machineName); // связываемся с именованным каналом hNamedPipe = CreateFile( pipeName, // имя канала GENERIC_READ | GENERIC_WRITE, // читаем и записываем в канал FILE_SHARE_READ | FILE_SHARE_WRITE, // разрешаем чтение и запись NULL, // безопасность по умолчанию OPEN_EXISTING, // открываем существующий канал FILE_ATTRIBUTE_NORMAL, // атрибуты по умолчанию NULL); // дополнительных атрибутов нет // проверяем связь с каналом if (hNamedPipe==INVALID_HANDLE_VALUE) { cerr « "Connection with the named pipe failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to exit."; cin.get(); return 0; } // вводим строку cin.get();
292 Часть IV. Обмен данными между параллельными процессами cout « "Input a string: "; cin.getline(pchInBuffer, 80); // определяем длину строки nMessageLength = strlen(pchInBuffer) + 1; // пишем и читаем из именованного канала одной транзакцией if (!TransaetNamedPipe( hNamedPipe, // дескриптор канала SpchlnBuffer, // адрес входного буфера канала nMessageLength, // длина входного сообщения SpchOutBuffer, // адрес выходного буфера канала sizeof(pchOutBuffer), // длина выходного буфера канала SdwBytesRead, // количество прочитанных байтов NULL)) // передача транзакции синхронная { // ошибка транзакции cerr « "Transact named pipe failed: " « endl « "The last error code: " « GetLastError() « endl; С1oseHandle(hNamedPipe); cout « "Press any key to exit."; cin.getO ; return 0; } // выводим посланное сообщение на консоль cout « "The sent message: " « endl « '\t' « pchlnBuffer « endl; // выводим полученное сообщение на консоль cout « "The received message: " « endl « '\t' « pchOutBuffer « endl; // закрываем дескриптор канала CloseHandle(hNamedPipe); // завершаем процесс cout « "Press any key to exit."; cin.getO ; return 0;
Глава 16. Работа с именованными каналами в Windows 293 Для передачи единственной транзакции по именованному каналу используется функция callNamedPipe, которая работает следующим образом. Сначала она связывается с именованным каналом, используя его имя. При этом заметим, что именованный канал должен быть открыт в режиме передачи данных сообщениями. Потом функция передает по именованному каналу единственное сообщение и получает сообщение в ответ, а после этого разрывает связь с именованным каналом. Функция CallNamedPipe имеет следующий прототип: BOOL CallNamedPipe ( LPCTSTR LPVOID DWORD LPVOID DWORD LPDWORD DWORD lpNamedPipeName, lpInBuffer, dwInBufferSize, lpOutBuffer, dwOutBufferSize, lpBytesRead, dwTimeOut // // // // // // // имя именованного канала буфер для записи данных в канал размер буфера для записи данных буфер для чтения данных из канала размер буфера для чтения данных количество прочитанных байтов интервал ожидания ); В случае успешного завершения функция CallNamedPipe возвращает ненулевое значение, а в случае неудачи — false. Опишем параметры этой функции. Параметр lpNamedPipeName должен указывать на строку, содержащую имя именованного канала. Параметр lpinBuffer должен указывать на буфер, из которого записываются данные в именованный канал. Параметр dwinBuffersize должен содержать длину передаваемого сообщения в байтах. Параметр lpOutBuffer должен указывать на буфер, в который читаются данные из именованного канала. Параметр dwOutBuf fersize должен содержать длину буфера, в который читается сообщение. Параметр lpBytesRead должен указывать на переменную типа dword, в которую функция поместит количество прочитанных байтов. В параметре dwTimeOut должен быть установлен интервал ожидания в миллисекундах, в течение которого функция ждет связи с именованным каналом. Кроме того, можно установить следующие значения этого параметра: П nmpwait_nowait — если нет свободного экземпляра именованного канала, то функция немедленно возвращает управление; П nmpwait_wait_forever — функция ждет бесконечно долго связи с экземпляром именованного канала;
294 Часть IV. Обмен данными между параллельными процессами П nmpwait_use_default_wait — интервал ожидания определяется значением. Заданным При СОЗДанИИ Именованного канала функцией CreateNamedPipe. В листинге 16.8 приведена программа процесса-клиента именованного канала, КОТОРЫЙ Передает Транзакцию, ИСПОЛЬЗУЯ фуНКЦИЮ CallNamedPipe. # inc lude <windows. h> #include <iostream.h> #include <string.h> анзакции по именованному каналу int main() { char char DWORD char char int machineName[80]; pipeName[80] ; dwBytesRead; pchInBuffer[80]; pchOutBuffer[80] , nMessageLength; // для количества прочитанных байтов // для записи сообщения // для чтения сообщения // длина сообщения // вводим имя машины в сети, на которой работает сервер cout « "Enter a name of the server machine: "; cin » machineName; // подставляем имя машины в имя канала wsprintf (pipeName, "\\\\%s\\pipe\\demo_pipe", machineName); // вводим строку cin.get(); cout « "Input a string: "; cin.getline(pchInBuffer, 80); // определяем длину строки nMessageLength = strlen(pch!nBuffer) + 1; // связываемся, пишем и читаем из именованного канала одной транзакцией if (!CallNamedPipe( pipeName, // имя именованного канала &pchInBuffer, // адрес входного буфера канала nMessageLength, // длина входного сообщения SpchOutBuffer, // адрес выходного буфера канала
Глава 16. Работа с именованными каналами в Windows 295 sizeof(pchOutBuffer), // длина выходного буфера канала &dwBytesRead, // количество прочитанных байтов NMPWAIT_WAIT_FOREVER) ) // ждем бесконечно долго { // ошибка транзакции cerr « "Call named pipe failed: " « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to exit."; cin.getO ; return 0; } // выводим посланное сообщение на консоль cout « "The sent message: " « endl « '\t' « pchlnBuffer « endl; // выводим полученное сообщение на консоль cout « "The received message: " « endl « '\t' « pchOutBuffer « endl; // завершаем процесс cout « "Press any key to exit."; cin.getO ; return 0; } 16.8. Определение и изменение состояния именованного канала Для получения информации о состоянии именованного канала используется ФУНКЦИЯ GetNamedPipeHandleState, КОТОрая имеет следующий ПрОТОТИП! BOOL GetNamedPipeHandleState( HANDLE hNamedPipe, // дескриптор именованного канала LPDWORD IpState, // состояние канала LPDWORD lpCurrentInstances, // количество экземпляров канала LPDWORD lpMaxCollectionCount, // максимальное количество байтов LPDWORD lpCollectionDataTimeout, // интервал ожидания LPTSTR lpUserName, // имя клиента именованного канала DWORD dwMaxUserNameSize // длина буфера для имени клиента
296 Часть IV. Обмен данными между параллельными процессами В случае успешного завершения функция GetNamedPipeHandleState ВОЗ- вращает ненулевое значение, а в случае неудачи — false. Параметры функции имеют следующее назначение. В параметре hNamedPipe должен быть установлен дескриптор именованного канала. Причем канал должен быть открыт в режиме чтения. Параметр ipstate должен указывать на переменную типа dword, в которую функция записывает любую комбинацию следующих значений: □ pipe_nowait — канал не блокирован; □ pipe_readmode_message — канал открыт в режиме передачи данных сообщениями. Если значение pipe_nowait не установлено, то канал блокирован. Если не установлено значение pipe_readmode_message, to канал открыт в режиме передачи данных потоком. Если определять состояние именованного канала не нужно, то в параметре ipstate может быть установлено значение null. Параметр lpcurrentinstances должен указывать на переменную типа dword, в которую функция записывает количество созданных экземпляров именованного канала. Если эта информация не нужна, то значение этого параметра может быть установлено в null. Параметр lpMaxCoiiectionCount должен указывать на переменную типа dword, в которую функция запишет максимальное количество байтов, которые клиент именованного канала должен записать в этот канал, прежде чем данные будут переданы серверу канала. Этот параметр должен быть установлен в null, если функция вызывается сервером именованного канала или если клиент и сервер работают на одном компьютере и для связи с сервером клиент использует символ "." вместо имени сервера. Если информация о максимальном количестве байтов не нужна, то в этом параметре может быть установлено значение null. Параметр IpCoiiectionDataTimeout должен указывать на переменную типа dword, в которую функция поместит количество миллисекунд, которые могут пройти, прежде чем данные могут быть переданы по сети. Как и в предыдущем случае, этот параметр должен быть установлен в null, в том случае, если функция вызывается сервером именованного канала или если клиент и сервер работают на одном компьютере, и для связи с сервером клиент использует символ "." вместо имени сервера. Если информация об интервале задержки перед передачей данных не нужна, то в этом параметре может быть установлено значение null. Параметр lpuserName должен указывать на символьный массив, в который функция поместит строку с именем владельца именованного канала. Имя владельца можно получить только в том случае, если доступ к каналу открыт всем пользователям. Если информация об имени владельца канала не нужна, то этот параметр может быть установлен в null.
Глава 16. Работа с именованными каналами в Windows 297 Параметр dwMaxuserNamesize должен содержать размер области памяти, на КОТОРУЮ указывает параметр lpUserName. В листинге 16.9 приведена программа процесса-клиента именованного канала, которая получает информацию о состоянии именованного канала Посредством ВЫЗОВа функции GetNamedPipeHandleState. Листинг 16$, Пример определения состояния именованного канала #include <windows.h> #include <iostream.h> #include <string.h> int main() { char machineName [80] ; char pipeName [80] ; HANDLE hNamedPipe; DWORD DWORD DWORD DWORD TCHAR dwState; dwCurlnstances; dwMaxCollectionCount; dwCollectDataTimeout; chUserName[255]; // // // // // состояние канала количество экземпляров канала размер буфера клиента канала задержка перед передачей данных имя владельца именованного канала // вводим имя машины в сети, на которой работает сервер cout << "Enter a name of the server machine: "; cin » machineName; cin.get(); // подставляем имя машины в имя канала wspr int f (pipeName, "\\\\%s\\pipe\\demo_pipe", machineName); // связываемся с именованным каналом hNamedPipe = CreateFile( pipeName, // имя канала GENERIC_READ | GENERIC_WRITE, // читаем и записываем в канал FILE_SHARE_READ | FILE_SHARE_WRITE, // разрешаем чтение и запись //в канал NULL, // безопасность по умолчанию OPEN_EXISTING, // открываем существующий канал
298 Часть IV. Обмен данными между параллельными процессами FILE_ATTRIBUTE_NORMAL, // атрибуты по умолчанию NULL); // дополнительных атрибутов нет // проверяем связь с каналом if (hNamedPipe==INVALID_HANDLE_VALUE) { cerr « "Connection with the named pipe failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to exit."; cin.getO ; return 0; } // определяем состояние канала if (!GetNamedPipeHandleState( hNamedPipe, // дескриптор именованного канала &dwState, // состояние именованного канала &dwCurInstances, // количество экземпляров канала &dwMaxCollectionCount, // размер буфера клиента канала &dwCollectDataTimeout, // макс, задержка перед передачей данных chUserName, // имя пользователя канала 255)) // максимальная длина имени { cerr « "Get named pipe handle state failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to exit."; cin.getO ; return 0; } // выводим состояние канала на консоль cout « "State: switch (dwState) { case (PIPE_NOWAIT): cout « "PIPE_NOWAIT" « endl; break;
Глава 16. Работа с именованными каналами в Windows 299 case (PIPE_READMODE_MESSAGE): cout « "PIPE_READMODE_MESSAGE" « endl; break; case (PIPE_NOWAIT | PIPE_READMODE_MESSAGE): cout « "PIPE_NOWAIT and PIPE_READMODE_MESSAGE" « endl; break; default: cout « "Unknown state." « endl; break; } cout « "Current instances: " « dwCurInstances « endl « "Max collection count: " « dwMaxCollectionCount « endl « "Collection data timeout: " « dwCollectDataTimeout « endl « "User name: " « chUserName « endl; // закрываем дескриптор канала С1oseHandle(hNamedPipe); // завершаем процесс cout « "Press any key to exit."; cin.getO ; return 0; Изменить некоторые характеристики именованного канала можно посредством функции SetNamedPipeHandiestate, которая имеет следующий прототип: BOOL SetNamedPipeHandiestate( HANDLE hNamedPipe, // дескриптор именованного канала LPDWORD lpMode, // новый режим передачи данных LPDWORD lpMaxCollectionCount, // максимальное количество байтов LPDWORD lpCollectionDataTimeout // интервал ожидания ); В случае успешного завершения функция SetNamedPipeHandiestate возвращает ненулевое значение, а в случае неудачи — false. Параметры функции имеют следующее назначение. В параметре hNamedPipe должен быть установлен дескриптор именованного канала. Причем канал должен быть открыт в режиме записи.
300 Часть IV. Обмен данными между параллельными процессами Параметр lpMode должен указывать на переменную типа dword, которая содержит новые режимы работы именованного канала. Возможно изменить следующие режимы работы именованного канала: режим передачи данных и режим ожидания при выполнении записи или чтении данных в именованный канал, а также ожидания сервером соединения клиента с именованным каналом. Режим передачи данных может принимать следующие значения: □ pipe_readmode_byte — передача данных потоком; □ pipe_readmode_message — передача данных сообщениями. Режим ожидания может принимать следующие значения: □ pipe_wait — блокирование приложения до завершения выполнения функций ConnectNamedPipe, WriteFile И ReadFile; □ PIPE_NOWAIT — выполнение фуНКЦИЙ ConnectNamedPipe, WriteFile И ReadFile не блокирует работу приложения. Отметим, что эти режимы ожидания не влияют на работу асинхронных операций доступа к именованному каналу, а предназначены только для работы менеджера локальной сети. В параметре lpMode может быть установлена любая комбинация флагов режимов передачи данных и ожидания или только один из этих флагов. Если режимы работы именованного канала не изменяются, то параметр lpMode должен содержать значение null. Параметр lpMaxCoiiectionCount должен указывать на переменную типа dword, которая содержит максимальное количество байтов, которые клиент именованного канала может записать в этот канал, прежде чем эти данные будут переданы серверу канала. Однако если клиент открыл именованный канал В режиме FILE_FLAG_WRITE_THROUGH, TO параметр lpMaxCoiiectionCount игнорируется. Этот параметр должен быть установлен в null, если функция не изменяет размер максимального количества записываемых перед передачей байтов. Параметр IpCoiiectionDataTimeout должен указывать на переменную типа dword, которая содержит новое количество миллисекунд, которые могут пройти, прежде чем данные могут быть переданы по сети. Как и в предыдущем случае, если клиент открыл именованный канал в режиме file_flag_write_through, to параметр IpCoiiectionDataTimeout игнорируется. ЭТОТ параметр ДОЛЖен быть установлен в null, если функция не изменяет интервал ожидания до передачи данных по именованному каналу. В листинге 16.10 приведена программа процесса-клиента именованного канала, которая изменяет состояние именованного канала посредством вызова функции SetNamedPipeHandleState.
Глава 16. Работа с именованными каналами в Windows 301 \ Листинг 16.10. Пример изменения состояния именованного канала #include <windows.h> #include <iostream.h> #include <string.h> int main() { char machineName [80] ; char pipeName [80] ; HANDLE hNamedPipe ; DWORD dwState; DWORD dwCurInstances; DWORD dwMaxCollectionCount; DWORD dwCollectDataTimeout; данных TCHAR chUserName[255]; // состояние канала // количество экземпляров канала // размер буфера клиента канала // временная задержка перед передачей // имя владельца именованного канала // вводим имя машины в сети, на которой работает сервер cout « "Enter a name of the server machine: " ; cin » machineName; cin.get(); // подставляем имя машины в имя канала wspr int f (pipeName, " \\\\%s\\pipe\\demo_pipe", machineName); // связываемся с именованным каналом hNamedPipe = CreateFile( pipeName, // имя канала GENERIC_READ | GENERIC_WRITE, // читаем и записываем в канал FILE_SHARE_READ | FILE_SHARE_WRITE, // разрешаем чтение и запись NULL, // безопасность по умолчанию OPEN_EXISTING, // открываем существующий канал FILE_ATTRIBUTE_NORMAL, // атрибуты по умолчанию NULL); // дополнительных атрибутов нет // проверяем связь с каналом if (hNamedPipe==INVALID_HANDLE_VALUE)
302 Часть IV Обмен данными между параллельными процессами cerr « "Connection with the named pipe failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to exit."; cin.getO / return 0; } // определяем состояние канала if (!GetNamedPipeHandleState( hNamedPipe, // дескриптор именованного канала SdwState, // состояние именованного канала SdwCurlnstances, // количество экземпляров канала &dwMaxCollectionCount, // размер буфера клиента канала &dwCollectDataTimeout, // макс, задержка перед передачей данных chUserName, // имя пользователя канала 255)) // максимальная длина имени { cerr « "Get named pipe handle state failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to exit."; cin.getO ; return 0; } // выводим интервал ожидания на консоль cout « "Collection data timeout: " « dwCollectDataTimeout « endl; // изменяем состояние именованного канала dwCollectDataTimeout = 100; if (!SetNamedPipeHandleState( hNamedPipe, // дескриптор именованного канала NULL, // режим передачи данных не изменяем NULL, // размер буфера не изменяем SdwCollectDataTimeout)) // макс, задержка равна 100 миллисекунд { cerr « "Set named pipe handle state failed." « endl « "The last error code: " « GetLastError() « endl;
Глава 16. Работа с именованными каналами в Windows 303 cout « "Press any key to exit."; cin.getO ; return 0; } // определяем состояние канала GetNamedPipeHandleState( hNamedPipe, // дескриптор именованного канала SdwState, // состояние именованного канала &dwCurInstances, // количество экземпляров канала &dwMaxCollectionCount, // размер буфера клиента канала SdwCollectDataTimeout, // макс, задержка перед передачей данных chUserName, // имя пользователя канала 255); // максимальная длина имени // выводим интервал ожидания на консоль cout « "Collection data timeout: " « dwCollectDataTimeout « endl; // закрываем дескриптор канала CloseHandle(hNamedPipe) ; // завершаем процесс cout « "Press any key to exit."; cin.get(); return 0; } 16.9. Получение информации об именованном канале Для получения информации об атрибутах именованного канала, которые не МОГУТ быть Изменены, ИСПОЛЬЗуетСЯ фуНКЦИЯ GetNamedPipeinfo. Эта фуНК- ция имеет следующий прототип: BOOL GetNamedPipeinfo ( HANDLE hNamedPipe, // дескриптор именованного канала LPDWORD lpFlags, // тип канала LPDWORD lpOutBufferSize, // размер выходного буфера
304 Часть IV. Обмен данными между параллельными процессами LPDWORD lpInBufferSize, // размер входного буфера LPDWORD lpMaxInstances // макс, количество экземпляров канала ); В случае успешного завершения функция GetNamedPipeinfo возвращает ненулевое значение, а в случае неудачи — false. Опишем параметры этой функции. В параметре hNamedPipe должен быть установлен дескриптор именованного канала. Причем канал должен быть открыт в режиме чтения. Параметр lpFiags должен указывать на переменную типа dword, в которой установлен тип именованного канала, о котором нужно получить информацию. Тип именованного канала должен содержать информацию о способе передачи данных по каналу и указывать, кому принадлежит дескриптор: клиенту или серверу именованного канала. Для этого должны использоваться следующие константы: □ pipe_client_end — дескриптор клиента именованного канала; □ pipe_server_end — дескриптор сервера именованного канала; □ pipe_type_byte — передача данных потоком; □ pipe_type_message — передача данных сообщениями. Если эта информация не установлена, т. е. параметр lpFiags имеет значение null, то функция рассматривает тот конец именованного канала, который соединен с клиентом, и по которому данные передаются потоком. Параметр lpOutBuffersize должен указывать на переменную типа dword, в которую функция GetNamedPipeinfo поместит размер выходного буфера именованного канала. Если информация о размере выходного буфера не нужна, то значение этого параметра может быть установлено в null. Параметр lpinBuffersize должен указывать на переменную типа dword, в которую функция GetNamedPipeinfo поместит размер входного буфера именованного канала. Если информация о размере входного буфера не нужна, то значение этого параметра может быть установлено в null. Параметр lpMaxInstances должен указывать на переменную типа dword, в которую функция GetNamedPipeinfo поместит число, обозначающее максимальное допустимое количество экземпляров именованного канала. Если это число равно значению pipe_unlimited_instances, to количество экземпляров именованного канала ограничено только наличием системных ресурсов. Если информация о максимальном количестве экземпляров именованного канала не нужна, то значение этого параметра может быть установлено в null. В листинге 16.11 приведена программа процесса-клиента именованного канала, которая получает информацию об неизменяемых атрибутах именованного канала посредством ВЫЗОВа фуНКЦИИ GetNamedPipeinfo.
Глава 16. Работа с именованными каналами в Windows 305 Листинг 16.11. Пример получения информации об именованном канапе #include <windows.h> #include <iostream.h> #include <string.h> int main() { char machineName [ 80 ] ; char pipeName [80] ; HANDLE hNamedPipe; DWORD dwFlags = PIPE_CLIENT_END | PIPE_TYPE_MESSAGE; // клиент канала и передача данных сообщениями DWORD dwOutBufferSize; // состояние канала DWORD dwInBufferSize; // количество экземпляров канала DWORD dwMaxInstances; // размер буфера клиента канала // вводим имя машины в сети, на которой работает сервер cout « "Enter a name of the server machine: "; cin » machineName; cin.getO ; // подставляем имя машины в имя канала wsprintf (pipeName, " \\\\%s\\pipe\\demo_pipe", machineName); // связываемся с именованным каналом hNamedPipe = CreateFile( pipeName, // имя канала GENERIC_READ | GENERIC_WRITE, // читаем и записываем в канал FILE_SHARE_READ | FILE_SHARE_WRITE, // разрешаем чтение и запись NULL, // безопасность по умолчанию OPEN_EXISTING, // открываем существующий канал FILE_ATTRIBUTE_NORMAL, // атрибуты по умолчанию NULL); // дополнительных атрибутов нет // проверяем связь с каналом if (hNamedPipe==INVALID_HANDLE_VALUE)
306 Часть IV. Обмен данными между параллельными процессами cerr « "Connection with the named pipe failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to exit."; cin.get(); return 0; } // получаем информацию о канале if (!GetNamedPipeInfo( hNamedPipe, // дескриптор именованного канала SdwFlags, // тип канала SdwOutBufferSize, // размер выходного буфера SdwInBufferSize, // размер входного буфера SdwMaxInstances)) // максимальное количество экземпляров канала { сегг « "Get named pipe info failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to exit."; cin.get(); return 0; } // выводим информацию на консоль cout « "Out buffer size: " « dwOutBufferSize « endl « "In buffer size: " « dwInBufferSize « endl « "Max instances: " « dwMaxInstances « endl; // закрываем дескриптор канала CloseHandle(hNamedPipe); // завершаем процесс cout « "Press any key to exit. cin.get(); return 0;
Глава 17 Работа с почтовыми ящиками в Windows 17.1. Концепция почтовых ящиков Почтовым ящиком называется объект ядра операционной системы, который обеспечивает передачу сообщений от процессов-клиентов к процессам- серверам, выполняющимся на компьютерах в пределах локальной сети. Процесс, который создает почтовый ящик, называется сервером почтового ящика. Процессы, которые связываются с именованным почтовым ящиком, называются клиентами почтового ящика. Перечислим характеристики почтовых ящиков: □ имеют имя, которое используется клиентами для связи с почтовыми ящиками; □ направление передачи данных от клиента к серверу; □ передача данных осуществляется сообщениями; □ обмен данными может быть как синхронным, так и асинхронным. Хотя передача данных осуществляется только от клиента к серверу, один почтовый ящик может иметь несколько серверов. Это происходит в том случае, если несколько серверов создают почтовые ящики с одинаковыми именами. Тогда все сообщения, которые посылает клиент в такой почтовый ящик, будут получать все серверы этого почтового ящика. Однако это выполняется только при условии, что длина сообщения меньше 425 байт, так как в этом случае сообщения передаются дейтаграммами. Таким образом, можно сказать, что почтовые ящики обеспечивают однонаправленную связь типа "многие-ко-многим". При этом доставка сообщения от клиента к серверам почтового ящика не подтверждается системой. Заметим также, что операционные системы семейства Windows NT не поддерживают передачу сообщений длиной 425 и 426 байт. Немного коснемся вопроса передачи сообщений посредством почтовых ящиков. Если длина сообщения меньше чем 425 байт, то такое сообщение передается как дейтаграмма. Дейтаграмма представляет собой небольшой
308 Часть IV. Обмен данными между параллельными процессами пакет с передаваемым по сети сообщением, который содержит также информацию об отправителе и получателе сообщения. Дейтаграмма рассылается всем серверам данного почтового ящика. Так как размер пакета небольшой, то дейтаграммы рассылаются быстро, но нет гарантии доставки сообщения, так как в дейтаграмме не хранится информация, поддерживающая контроль доставки. Если же длина сообщения больше 426 байт, то такие сообщения могут передаваться только от одного клиента к одному серверу, используя при этом SMB (Server Message Block) протокол передачи данных по сети. При этом отметим, что длина сообщения, передаваемого в почтовый ящик, не может превышать 64 Кбайт. Кроме того, при использовании протокола SMB сообщения передаются только от одного клиента к одному серверу почтового ящика. Почтовые ящики можно рассматривать как псевдофайлы, расположенные в оперативной памяти компьютера. Поэтому для доступа к почтовым ящикам используются те же функции, что и для доступа к обычным файлам. Подробно эти функции рассмотрены в гл. 24, посвященной работе с файлами. Теперь приведем порядок работы с почтовыми ящиками, который и будет использоваться в дальнейшем. 1. Создание почтового ящика сервером. 2. Соединение клиента с почтовым ящиком. 3. Обмен данными через почтовый ящик. 4. Закрытие почтового ящика клиентом и сервером. Подробно эти пункты работы с почтовыми ящиками рассмотрены в следующих разделах. 17.2. Создание почтовых ящиков Создаются почтовые ящики процессом-сервером при помощи функции createMaiisiot, которая имеет следующий прототип: HANDLE CreateMaiisiot( LPCTSTR lpName, // имя почтового ящика DWORD dwMaxMessageSize // максимальная длина сообщения DWORD dwReadTimeout, // интервал ожидания LPSECURITY_ATTRIBUTES IpSecurityAttributes // атрибуты безопасности ); В случае успешного завершения эта функция вернет дескриптор почтового ящика, а в случае неудачи — значение invalid_handle_value. Опишем параметры функции. Параметр lpName указывает на строку, которая должна иметь вид: \ \. \mailslot\mailslot_name
Глава 17. Работа с почтовыми ящиками в Windows 309 Здесь символ "." обозначает локальную машину, так как новый почтовый ящик всегда создается на локальной машине, слово maiisiot — фиксировано, a maiisiot_name обозначает имя почтового ящика, которое задается пользователем и нечувствительно к верхнему и нижнему регистрам. Параметр dwMaxMessagesize задает максимальную длину сообщения в байтах, которое может быть записано в почтовый ящик. Параметр dwReadTimeout задает в миллисекундах временной интервал, в течение которого функция ReadFile ждет поступления сообщения в почтовый ящик. Если в этом параметре установлено значение 0, то в случае отсутствия в почтовом ящике сообщения функция немедленно возвращает управление. Для задания бесконечного времени ожидания в этом параметре нужно установить значение mailslot_wait_forever. Заметим, что несколько процессов могут создать почтовые ящики с одним и тем же именем. В этом случае сообщение, посланное клиентом, может доставляться не только в почтовый ящик одного процесса, а также в почтовые ящики всех таких процессов при условии, что они работают на компьютерах внутри одного домена. Режим доставки сообщений зависит от режима открытия почтового ящика клиентом. Пример создания почтового ящика сервером будет показан в разд. 17.4, посвященном обмену данными через почтовый ящик. 17.3. Соединение клиентов с почтовым ящиком Как уже говорилось, для установления связи с почтовым ящиком клиент использует функцию createFile, которая имеет следующий прототип: HANDLE CreateFile( LPCTSTR lpFileName, // указатель на имя почтового ящика DWORD dwDesiredAccess, // чтение или запись в канал DWORD dwShareMode, // режим совместного использования LPSECURITY_ATTRIBUTES IpSecurityAttributes, // атрибуты безопасности DWORD dwCreationDisposition, // флаги открытия почтового ящика DWORD dwFlagsAndAttributes, // флаги и атрибуты HANDLE hTemplateFile // дополнительные атрибуты ); В случае успешного завершения эта функция возвращает дескриптор почтового ящика, а в случае неудачи — значение invalid_handle_value. Параметры функции CreateFile могут принимать следующие значения (если эта функция используется для открытия почтового ящика).
310 Часть IV. Обмен данными между параллельными процессами Параметр lpFiieName должен указывать на имя почтового ящика, которое может быть задано в одном из следующих форматов: □ почтовый ящик на локальном компьютере: \\ . \таИз1оЪ\имя_почтового_ящика □ почтовый ящик на компьютере с указанным именем: \\имя_компьтера\та11з1о^имя_почтового_ящика □ почтовый ящик в домене с указанным именем: \\имя_домена\та11з1о^имя_почтового_ящика □ почтовый ящик в первичном домене системы: \\*\та11з1о^имя_почтового_ящика В первом случае сообщения будут доставляться только в почтовые ящики с заданным именем, которые расположены на локальной машине. Во втором случае сообщения будут доставляться в почтовые ящики с заданным именем, расположенные на компьютере с указанным именем. В третьем случае сообщения будут доставляться в почтовые ящики с заданным именем, которые созданы внутри домена с указанным именем. В четвертом случае сообщения будут доставляться в почтовые ящики с заданным именем, которые созданы внутри первичного домена системы. Параметр dwDesiredAccess должен иметь значение generic_write, которое разрешает запись в почтовый ящик. Параметр dwShareMode определяет режим совместного использования почтового ящика и может принимать любую комбинацию из следующих значений: □ file_share_read — разрешает совместное чтение из почтового ящика; □ file_share_write — разрешает совместную запись в почтовый ящик. Параметр lpSecurityAttributes задает атрибуты безопасности почтового ящика. Пока этот параметр будем устанавливать в null. ДЛЯ ПОЧТОВОГО ЯЩИКа Параметр dwCreationDisposition ДОЛЖен быть равен значению open_existing, т. к. клиент всегда открывает существующий почтовый ящик. ДЛЯ ПОЧТОВОГО ЯЩИКа параметр dwFiagsAndAttributes МОЖНО задать равным О, что определяет флаги и атрибуты по умолчанию, или установить в этом параметре значение file_attribute_normal. Параметр hTempiateFile при работе с почтовыми ящиками не используется, поэтому в нем устанавливается значение null. Пример соединения клиента с почтовым ящиком приводится в следующем разделе, посвященном обмену данными через почтовый ящик.
Глава 17. Работа с почтовыми ящиками в Windows 311 17.4. Обмен данными через почтовый ящик Для обмена данными через почтовый ящик используются обычные функции доступа к файлу writeFiie и ReadFiie. Процесс-клиент записывает данные в почтовый ящик при помощи функций writeFiie, а процесс-сервер читает данные из почтового ящика, используя функции ReadFiie. Подробно эти функции рассмотрены в гл. 24, посвященной работе с файлами в Windows. В этом разделе приведем только программы, которые обмениваются данными через почтовый ящик. Конечно, процесс-сервер также может записывать сообщения в почтовый ящик, но это имеет смысл лишь в том случае, если в домене расположено несколько почтовых ящиков с одинаковыми именами, которые были созданы разными процессами. Тогда одинаковые сообщения получают все почтовые ящики с одинаковыми именами. Сначала, в листинге 17.1, приведем программу процесса-сервера, который создает почтовый ящик. В этой программе почтовый ящик создается на локальной машине. | Листинг 17.1» Процесс-сервер почтового ящика # include <windows.h> #include <iostream.h> int main() { HANDLE hMailslot; // дескриптор почтового ящика // создаем почтовый ящик hMailslot = CreateMailslot( "WW . \\mailslot\\demo_mailslot", // имя почтового ящика О, // длина сообщения произвольна MAILSLOT_WAIT_FOREVER, // ждем сообщения произвольно долго NULL // безопасность по умолчанию ); // проверяем на успешное создание if (hMailslot == INVALID_HANDLE_VALUE) { cerr « "Create mailslot failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish server.";
312 Часть IV. Обмен данными между параллельными процессами cin.getO ; return 0; } cout « "The mailslot is created." « endl; cout « "The mailslot is waiting a message." « endl; // читаем одно целое число из почтового ящика int nData; DWORD dwBytesRead; if (!ReadFile( hMailslot, // дескриптор почтового ящика SnData, // адрес буфера для ввода данных sizeof(nData), // количество читаемых байтов SdwBytesRead, // количество прочитанных байтов (LPOVERLAPPED)NULL // передача данных синхронная )) { cerr « "Read file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hMailslot); cout « "Press any key to finish server."; cin.getO ; return 0; } // выводим число на консоль cout « "The number " « nData « " was read by the server" « endl; // закрываем дескриптор почтового ящика CloseHandle(hMailslot); // завершаем процесс cout « "Press any key to exit."; cin.getO ; return 0;
Глава 17. Работа с почтовыми ящиками в Windows 313 Теперь, в листинге 17.2, приведем программу процесса-клиента почтового ящика. В этой программе предполагается, что процесс-клиент запускается на той же локальной машине, что и процесс-сервер. Иначе в имени почтового ящика нужно указать не точку, а имя компьютера, на котором создан почтовый ящик, или имя домена, в котором созданы почтовые ящики с одинаковыми именами. Листинг 17,2* Процесс-клиент почтового ящика #include <windows.h> #include <iostream.h> int main() { HANDLE hMailslot; char mails lotName[] = "WW . \\mailslot\\demo_mailslot" ; // связываемся с почтовым ящиком hMailslot = CreateFile( mailslotName, // имя почтового ящика GENERIC_WRITE, // записываем в ящик FILE_SHARE_READ, // разрешаем одновременное чтение из ящика NULL, // защита по умолчанию OPEN_EXISTING, // открываем существующий канал О, // атрибуты по умолчанию NULL // дополнительных атрибутов нет // проверяем связь с почтовым ящиком if (hMailslot == INVALID_HANDLE_VALUE) { cerr « "Create file failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish the client."; cin.getO ; return 0; } // вводим целое число
314 Часть IV. Обмен данными между параллельными процессами int n; cout « "Input an integer: "; cin » n; // пишем число в почтовый ящик DWORD dwBytesWritten; if (!WriteFile( hMailslot, // дескриптор почтового ящика &n, // данные sizeof(n), // размер данных &dwBytesWritten, // количество записанных байтов NULL // синхронная запись )) { // ошибка записи cerr « "Write file failed: " « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish the client.11; cin.get(); CloseHandle(hMailslot); return 0; } // закрываем дескриптор канала CloseHandle(hMailslot); // завершаем процесс cout « "The number is written by the client." « endl « "Press any key to exit." « endl; cin.get(); return 0; } В заключение этого раздела отметим, что, т. к. доступ к почтовым ящикам не отличается от доступа к файлам, для обмена данными через почтовые ЯЩИКИ МОЖНО также ИСПОЛЬЗОВатЬ фуНКЦИИ WriteFileEx И ReadFileEx.
Глава 17. Работа с почтовыми ящиками в Windows 315 17.5. Получение информации о почтовом ящике Для получения информации о характеристиках почтового ящика используется функция GetMaiisiotinfo, которая имеет следующий прототип: BOOL GetMaiisiotinfo( HANDLE hMailslot, // дескриптор почтового ящика LPDWORD lpMaxMessageSize // максимальная длина сообщения LPDWORD lpNextSize, // длина следующего сообщения LPDWORD lpMessageCount // количество сообщений LPDWORD lpReadTimeout // интервал ожидания сообщения ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — null. Параметры функции GetMaiisiotinfo имеют следующее назначение. В параметре hMailslot должен быть установлен дескриптор почтового ящика, КОТОРЫЙ был ВОЗВращен функцией CreateMailslot. Параметр lpMaxMessageSize должен указывать на переменную типа dword, в которую функция GetMaiisiotinfo поместит максимальную длину сообщения, которое может быть записано в почтовый ящик. Если это значение не нужно, то этот параметр может быть установлен в null. Параметр lpNextsize должен указывать на переменную типа dword, в которую функция GetMaiisiotinfo поместит длину следующего сообщения в почтовом ящике. Если в почтовом ящике нет сообщений, то в этот параметр функция запишет значение mailslot_no_message. Если значение длины последнего сообщения не нужно, то параметр lpNextsize может быть установлен в null. Параметр lpMessageCount должен указывать на переменную типа dword, в которую функция GetMaiisiotinfo поместит количество сообщений, находящихся в почтовом ящике. Если это значение не нужно, то этот параметр может быть установлен в null. Параметр lpReadTimeout должен указывать на переменную типа dword, в которую функция GetMaiisiotinfo поместит целое число без знака, обозначающее временной интервал в миллисекундах. Если почтовый ящик пуст, то в течение этого интервала функция ReadFile будет ждать, пока процесс-клиент не запишет сообщение в почтовый ящик. Если это значение не нужно, то в этом параметре может быть установлено значение null. В листингах 17.3, 17.4 приведены программы, в которых функция GetMaiisiotinfo используется для получения информации о сообщениях,
316 Часть IV. Обмен данными между параллельными процессами хранящихся в почтовом ящике. Сначала приведем программу сервера почтового ящика, который читает сообщения по мере их поступления от клиента. ! Листинг 17.3. Процесс-сервер почтового ящика # inc lude <windows. h> #include <iostream.h> int main() { HANDLE hMailslot; DWORD dwNextMessageSize; DWORD dwMessageCount; // создаем почтовый ящик hMailslot = CreateMailslot( "WW . \\mailslot\\demo_mailslot", // имя почтового ящика О, // длина сообщения произвольна MAILSLOT_WAIT_FOREVER, // ждем сообщения произвольно долго NULL // безопасность по умолчанию ); // проверяем на успешное создание if (hMailslot == INVALID_HANDLE_VALUE) { cerr « "Create mailslot failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish server."; cin.getO ; return 0; } cout « "The mailslot is created." « endl; // ждем сообщений cout « "Press any key to read messages." « endl; cin.getO ; // дескриптор почтового ящика // размер следующего сообщения // количество сообщений // получаем информацию о почтовом ящике
Глава 17. Работа с почтовыми ящиками в Windows 317 if (!GetMailslotInfo( hMailslot, // дескриптор почтового ящика NULL, // максимальный размер сообщения не нужен &dwNextMessageSize, // размер следующего сообщения &dwMessageCount, // количество сообщений NULL)) // интервал ожидания не нужен { cerr « "Get mailslot info failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish server."; cin.get(); return 0; } // читаем сообщения while (dwMessageCount != 0) { DWORD dwBytesRead; char* pchMessage; // захватываем память для сообщения pchMessage = (char*) new char[dwNextMessageSize]; // читаем одно сообщение if (!ReadFile( hMailslot, // дескриптор канала pchMessage, // адрес буфера для ввода данных dwNextMessageSize, // количество читаемых байтов &dwBytesRead, // количество прочитанных байтов NULL // передача данных синхронная )) { cerr « "Read file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hMailslot); cout « "Press any key to finish server."; cin.get();
318 Часть IV. Обмен данными между параллельными процессами return 0; } // выводим сообщение на консоль cout « "The message « " « pchMessage « " » was read" « endl; // получаем информацию о следующем сообщении if (!GetMailslotInfo( hMailslot, // дескриптор почтового ящика NULL, // максимальный размер сообщения не нужен &dwNextMessageSize, // размер следующего сообщения &dwMessageCount, // количество сообщений NULL)) // интервал ожидания не нужен { cerr « "Get mailslot info failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish server."; cin.getO ; return 0; } // освобождаем память для сообщения delete[] pchMessage; } // закрываем дескриптор почтового ящика CloseHandle(hMailslot); // завершаем процесс cout « "Press any key to exit."; cin.get(); return 0; } Теперь, в листинге 17.4, приведем программу клиента почтового ящика, которая посылает сообщения процессу-серверу. Отметим, что как процесс- сервер, так и процесс-клиент находятся на локальном компьютере.
Глава 17. Работа с почтовыми ящиками в Windows 319 ^ йстинг 17,4/Процесс-клиент почтового ящика .#include <windows.h> #include <iostream.h> #include <string.h> int main () { HANDLE hMailslot; char mailslotName[] = "WW . \\mailslot\\demo_mailslot" ; // связываемся с почтовым ящиком hMailslot = CreateFile( mailslotName, // имя почтового ящика GENERIC_WRITE, // записываем в ящик FILE_SHARE_READ, // разрешаем одновременное чтение из ящика NULL, // защита по умолчанию OPEN_EXISTING, // открываем существующий канал О, // атрибуты по умолчанию NULL // дополнительных атрибутов нет ); // проверяем связь с почтовым ящиком if (hMailslot == INVALID_HANDLE_VALUE) { cerr « "Create file failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish the client."; cin.get(); return 0; } // вводим количество" передаваемых сообщений int n ; cout « "Input a number of messages: "; cin » n; cin.getO ;
320 Часть IV Обмен данными между параллельными процессами // пишем сообщения в почтовый ящик for (int i = 0; i < n; ++i) { DWORD dwBytesWri t ten; char pchMessage[256]; int nMessageSize; cout « "Input message: "; // читаем сообщение cin.getline(pchMessage, 256); // определем длину сообщения nMessageSize = strlen(pchMessage) + 1; // пишем сообщение if (!WriteFile( hMailslot, // дескриптор почтового ящика pchMessage, // данные nMessageSize, // размер данных SdwBytesWritten, // количество записанных байтов NULL // синхронная запись )) { // ошибка записи cerr « "Write file failed: " « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish the client."; cin.getO; CloseHandle(hMailslot); return 0; } } // закрываем дескриптор канала CloseHandle(hMailslot); // завершаем процесс
Глава 17. Работа с почтовыми ящиками в Windows 321 cout « "The messages are written by the client." « endl « "Press any key to exit." « endl; cin.get(); return 0; } 17.6. Изменение времени ожидания сообщения Для изменения времени ожидания сервером сообщения от клиента используется функция SetMaiisiotinfo, которая имеет следующий прототип: BOOL SetMaiisiotinfo( HANDLE hMailslot, // дескриптор почтового ящика DWORD dwReadTimeout // интервал ожидания сообщения ); В случае успешного завершения эта функция возвратит ненулевое значение, а в случае неудачи — null. В параметре hMailslot должен быть установлен дескриптор почтового ящика, КОТОРЫЙ был ПОЛучен ВЫЗОВОМ фуНКЦИИ CreateMailslot. Параметр dwReadTimeout задает в миллисекундах новый временной интервал, в течение которого функция ReadFiie ждет поступления сообщения в почтовый ящик. Если в этом параметре устанавливается значение 0, то в случае отсутствия в почтовом ящике сообщения функция немедленно возвращает управление. Для задания бесконечного времени ожидания в этом параметре нужно установить значение mailslot_wait_forever. В листинге 17.5 приведена программа, в которой функция SetMaiisiotinfo используется для установки нового временного интервала для ожидания поступления сообщения в почтовый ящик. I Листинг 17,5, Изменение времени ожидания сообщения #include <windows.h> #include <iostream.h> int main() { HANDLE hMailslot; // дескриптор почтового ящика 11 Зак. 514
322 Часть IV. Обмен данными между параллельными процессами DWORD dwReadTimeout; // интервал для ожидания сообщения // создаем почтовый ящик hMailslot = CreateMailslot( "WW. \\mailslot\\demo_mailslot", // имя почтового ящика О, // длина сообщения произвольна О, // интервал ожидания равен нулю NULL // защита по умолчанию ); // проверяем на успешное создание if (hMailslot == INVALID__HANDLE_VALUE) { cerr « "Create mailslot failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish server."; cin.get(); return 0; } cout « "The mailslot is created." « endl; // получаем информацию о почтовом ящике if (!GetMailslotInfo( hMailslot, // дескриптор почтового ящика NULL, // максимальный размер сообщения не нужен NULL, // размер следующего сообщения не нужен NULL, // количество сообщений не нужно &dwReadTimeout)) // интервал ожидания сообщения { cerr « "Get mailslot info failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish server."; cin.get(); return 0; } cout « "Read timeout: " « dwReadTimeout « endl;
Глава 17. Работа с почтовыми ящиками в Windows 323 if (!SetMailslotlnfо( hMailslot, // дескриптор почтового ящика 3000)) // изменяем интервал ожидания { cerr « "Set mailslot info failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish server."; cin.getO ; return 0; } // получаем информацию о почтовом ящике if (!GetMailslotInfo( hMailslot, // дескриптор почтового ящика NULL, // максимальный размер сообщения не нужен NULL, // размер следующего сообщения не нужен NULL, // количество сообщений не нужно SdwReadTimeout)) // интервал ожидания сообщения { cerr « "Get mailslot info failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish server."; cin.getO ; return 0; } cout « "Read timeout: " « dwReadTimeout « endl; // закрываем дескриптор почтового ящика CloseHandle(hMailslot); // завершаем процесс cout « "Press any key to exit."; cin.getO; return 0;
Часть V Структурная обработка исключений Глава 18. Фреймовая обработка исключений Глава 19. Финальная обработка исключений
Глава 18 Фреймовая обработка исключений 18.1. Исключения и их обработчики Исключением называется событие, которое произошло во время выполнения программы, в результате совершения которого дальнейшее нормальное выполнение программы становится невозможным. Как правило, такие события являются ошибками в программе. Поэтому для дальнейшей работы приложения требуется или восстановление программы в рабочее состояние, или ее аварийное завершение с освобождением всех захваченных программой ресурсов. В операционных системах Windows для этой цели предназначен механизм структурной обработки исключений (structured exception handling, SEH). Смысл механизма структурной обработки исключений заключается в следующем. В программе выделяется блок программного кода, в котором может произойти исключение. Такой блок кода называется фреймом, а сам код называется охраняемым кодом. Затем, после фрейма вставляется программный блок, который обрабатывает происшедшее исключение. Этот блок называется обработчиком исключения. После обработки исключения управление передается первой инструкции, следующей за обработчиком исключения. Очевидно, что для того чтобы использовать этот механизм в программе, в язык программирования C++ нужно ввести новые ключевые слова. Такими ключевыми словами являются try и except, которые расширяют список стандартных ключевых слов языка программирования C++ и различаются только компилятором фирмы Microsoft. Ключевое слово try отмечает фрейм, а ключевое слово except отмечает обработчик исключения. В результате фрагмент программы, который использует механизм структурной обработки исключений, выглядит следующим образом: _try { // охраняемый код
328 Часть V. Структурная обработка исключений } except (выражение-филь тр) { // код обработки исключения } Здесь выражение-фильтр является выражением языка программирования C++ и указывает на то, как должна выполняться программа после обработки исключения. Вычисление этого выражения выполняется сразу после возникновения исключения и должно давать в результате одно из следующих значений: □ exception_execute_handler — управление передается обработчику исключений; □ exception_continue_search — система продолжает поиск обработчика исключения; □ exception_continue_execution — система передает управление в точку прерывания программы. Сделаем два важных замечания относительно механизма структурной обработки исключений. Во-первых, не допускается использование оператора goto для передачи управления внутрь фрейма или обработчика исключения. Во-вторых, в выражении фильтра допускается использование функций GetExceptionCode И GetExceptionlnformation, КОТОрые предоставляют Информацию о происшедшем исключении. Эти функции будут рассмотрены в разд. 18.3. Отметим также, что переменные, объявленные внутри фрейма или блока обработки исключения, являются локальными и видны только внутри соответствующего блока, как это и принято в языке программирования C++. Рассмотрим, в листинге 18.1, программу, в которой происходит исключение при попытке обращения к памяти по адресу, который не инициализирован. Так как значение выражения-фильтра равно exception_execute_handler, to управление передается обработчику исключений, который выводит на консоль сообщение об исключении и инициализирует этот указатель. После этого программа выводит на консоль нужное значение числа и заканчивает свое выполнение. Листинг 18,1, Обработка исключения # include <windows.h> #include <iostream.h> int main()
Глава 18. Фреймовая обработка исключений 329 { int a = 10; int *р = NULL; // пустой указатель на целое число —try { cout « "a = " « *р « endl; // ошибка, так как р = NULL } except(EXCEPTION_EXECUTE_HANDLER) { cout « "There was some exception." « endl; P = &a; } cout « "a = " « *p « endl; // нормально return 0; } Остальные варианты обработки исключений в зависимости от значения выражения-фильтра будут в разд. 18.2, 18.3. А в заключение этого параграфа сделаем несколько замечаний относительно механизма структурной обработки исключений и механизма обработки исключений в языке программирования C++. Концептуально механизм структурной обработки исключений в Windows немного отличается от механизма обработки исключений, принятого в языке программирования C++. Дело в том, что механизм структурной обработки исключений был разработан раньше, чем принят стандарт языка C++. Кроме того, в отличие от языка программирования C++ механизм структурной обработки исключений ориентирован не только на обработку программных исключений, но и на обработку аппаратных исключений. В SEH исключение рассматривается как ошибка, происшедшая при выполнении программы. В языке программирования C++ используется более абстрактный подход и исключение рассматривается как объект произвольного типа, который может выбросить программа, используя оператор throw. В свою очередь обработчик исключения catch может рассматриваться как функция с одним параметром, которая выполняется только в том случае, если тип ее параметра соответствует типу выброшенного исключения. Другие отличия касаются раскрутки стека и будут рассмотрены в разд. 18.9.
330 Часть V. Структурная обработка исключений 18.2. Получение кода исключения Получить код происшедшего исключения можно при помощи функции GetExceptionCode, которая имеет следующий прототип: DWORD GetExceptionCode(VOID); Функция GetExceptionCode возвращает одно из следующих значений: □ exception_access_violation — попытка чтения или записи в виртуальную память без соответствующего права доступа; □ exception_breakpoint — встретилась точка останова; □ exception_datatype_misalignment — доступ к данным, адрес которых не выровнен по границе слова или двойного слова; □ exception_single_step — механизм трассировки программы сообщает, что выполнена одна инструкция; □ exception_array_biunds_exceeded — выход за пределы массива, если аппаратное обеспечение поддерживает такую проверку; □ exception_flt_denormal_operand — один из операндов с плавающей точкой является ненормализованным; □ exception_flt_divide_by_zero — попытка деления на ноль в операции с плавающей точкой; □ exception_flt_inexact_result — результат операции с плавающей точкой не может быть точно представлен десятичной дробью; □ exception_flt_invalid_operation — ошибка в операции с плавающей точкой, для которой не предусмотрены другие коды исключения; □ exception_flt_overflow — при выполнении операции с плавающей точкой произошло переполнение; □ exception_flt_stack_check — переполнение или выход за нижнюю границу стека при выполнении операции с плавающей точкой; □ exception_flt_underflow — результат операции с плавающей точкой является числом, которое меньше минимально возможного числа с плавающей точкой; □ exception_int_divide_by_zero — попытка деления на ноль в операции с целыми числами; □ exception_int_overflow — при выполнении операции с целыми числами произошло переполнение; □ exception_priv_instruction — попытка выполнения привилегированной инструкции процессора, которая недопустима в текущем режиме процессора;
Глава 18. Фреймовая обработка исключений 331 П EXCEPTION_NONCONTINUABLE_EXCEPTION — ПОПЫТКа ВОЗОбнОВЛеНИЯ ИСПОЛ- нения программы после исключения, которое запрещает выполнять такое действие. Отметим, ЧТО фуНКЦИЯ GetExceptionCode МОЖет ВЫЗЫВатЬСЯ ТОЛЬКО В ВЫра- жении-фильтре или в блоке обработки исключения. Следовательно, эта функция вызывается всегда только в том случае, если исключение произошло. Отсюда можно определить назначение функции GetExceptionCode. Если эта функция вызывается в выражении фильтра, то она используется для того, чтобы определить выполняет ли текущий обработчик исключения обработку исключений с данным кодом или нужно продолжить поиск подходящего обработчика исключения. Если же функция GetExceptionCode вызывается в блоке обработки исключения, то она также предназначена для проверки кодов исключений, которые обрабатывает текущий обработчик исключения, но в этом случае поиск другого обработчика исключений не выполняется. В ЛИСТИНГе 18.2 Приведена Программа, В КОТОРОЙ фуНКЦИЯ GetExceptionCode вызывается в выражении фильтра и используется для проверки того, что нужно ли передавать управление текущему блоку обработки исключений или продолжить поиск другого обработчика. I Листинг 18.2. Проверка кода исключения в выражении-фильтре #include <windows.h> #include <iostream.h> int main() { int a = 10; int *p = NULL; —try { cout « "a = " « *p « endl; // ошибка, так как р = NULL } except (GetExceptionCode () == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { cout « "Exception access violation." « endl; p = &a; } cout « "a = " « *p « endl; // нормально return 0;
332 Часть V. Структурная обработка исключений Теперь приведем в листинге 18.3 программу, в которой проверка кода исключения используется в обработчике исключений. [ Листинг 18,3, Проверка кода исключения ш обработчике исключений | # include <windows.h> #include <iostream.h> int main() { int a = 10; int *p = NULL; // пустой указатель на целое число _try { cout « "а = " « *р « endl; // ошибка, так как р = NULL } except (EXCEPTION_EXECUTE_HANDLER) { DWORD ее = GetExceptionCode(); // получаем код исключения if (ее == EXCEPTION_ACCESS_VIOLATION) cout « "Exception access violation." « endl; else cout « "Some other exception." « endl; p = &a; } cout « "a = " « *p « endl; // нормально return 0; } 18.3. Функции фильтра Если для принятия решения об обработке исключения требуется более детально обработать информацию об исключении, то в выражении-фильтре используют функцию, которая в этом случае называется функцией фильтра.
Глава 18. Фреймовая обработка исключений 333 В фуНКЦИИ фильтра Не разрешается ВЫЗЫВаТЬ фунКЦИИ GetExceptionCode И GetExceptionlnformation, ОДНаКО ЭТИ фуНКЦИИ МОГуТ ВЫЗЫВатЬСЯ ДЛЯ ИНИ- циализации параметров функции фильтра. В листинге 18.4 приведена программа, в которой используется функция фильтра для принятия решения о дальнейшей обработке исключения. В этой программе функция фильтра возвращает одно из двух значений EXCEPTION_CONTINUE_EXECUTION ИЛИ EXCEPT ION_EXECUTE_HANDLER. Первое значение возвращается в случае исключения, которое генерируется системой при целочисленном делении на ноль, а второе — в остальных случаях. | Листинг 18А Функция фильтра и возобновление исполнения программы #include <windows.h> #include <iostream.h> DWORD filter_function(DWORD ec, int &a) { // проверяем код исключения if (ее == EXCEPTION_INT_DIVIDE_BY_ZERO) { cout « "Integer divide by zero exception." « endl; cout « "a = " « a « endl; // восстанавливаем ошибку a = 10; // возобновляем выполнение программы cout « "Continue execution." « endl; cout « "a = " « a « endl; return EXCEPTION_CONTINUE_EXECUTION; } else // прекращаем выполнение программы return EXCEPTION_EXECUTE_HANDLER; } int main ()
334 Часть V. Структурная обработка исключений { int а = 0; int Ъ = 1000; _try { Ь /= а; cout « "b = " « b « endl; } except(filter_function(GetExceptionCode(), a)) { cout « "There was some exception." « endl; } return 0; } Отметим, что в общем случае возобновление программы выполнить довольно сложно, т .к. процессор повторяет попытку выполнения программы с машинной команды, на которой произошло исключение. Естественно, что эта машинная команда не обязательно соответствует инструкции на языке высокого уровня, которая вызвала исключение. 18.4. Получение информации об исключении Более подробную информацию об исключении можно получить при помощи ВЫЗОВа фуНКЦИИ GetExceptionlnformation, КОТОрая Имеет следующий прототип: LPEXCEPTION_POINTERS GetExceptionlnformation(VOID); Эта функция возвращает указатель на структуру типа: typedef struct EXCEPTION_POINTERS { PEXEPTION_RECORD Except ionRecord; PCONTEXT Context; } EXCEPTION_POINTERS, * PEXCEPTION_POINTERS; которая, в свою очередь, содержит два указателя: ExceptionRecord и context на структуры типа exception_record и context соответственно. В структуру типа context система записывает содержимое всех регистров процессора на момент исключения. Эта структура имеет довольно громоздкое описание, которое можно найти в заголовочном файле WinNt.h.
Глава 18. Фреймовая обработка исключений 335 Структура типа exception_record имеет следующий формат: typedef struct EXCEPTION_RECORD { DWORD Except i onCode; DWORD ExceptionFlags, strict _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress, DWORD Number Parameters, ULONG_PTR Exceptionlnf ormation [ EXCEPTION_MAXIMUM_PARAMETERS ] ; } EXCEPTION_RECORD, *PEXCEPTION_RECORD; В нее система записывает информацию об исключении. Поля этой структуры имеют следующее назначение. Поле Exceptioncode содержит код исключения, который может принимать такие же значения, как и код исключения, возвращаемый функцией GetExceptionCode. Поле ExceptionFlags может принимать одно из двух значений: □ О — которое обозначает, что после обработки исключения возможно возобновление выполнения программы; □ exception_noncontinuable — которое обозначает, что после обработки исключения возобновление выполнения программы невозможно. Если установлено значение exception_noncontinuable и выполнена попытка возобновления выполнения программы, то система выбросит исключение EXCEPTION_NONCONTINUABLE_EXCEPTION. Поле ExceptionRecord содержит указатель на следующую структуру типа exception_record, которая может быть создана в случае вложенных исключений. Поле ExceptionAddress содержит адрес инструкции в программе, на которой произошло исключение. Поле NumberParameters содержит количество параметров, заданных в поле Exceptioninformation, которое является последним в этой структуре. Поле Exceptionlnformation[EXCEPTION_MAXIMUM_PARAMETERS] Определяет массив 32-битных аргументов, которые описывают исключение. Элементы этого массива могут использоваться функцией генерации программных исключений RaiseException. Отметим, что в операционных системах Windows NT/2000 для исключения с кодом exception_access_violation определены значения первых двух элементов этого массива. В этом случае первый элемент массива содержит одно из значений: □ 0 — попытка чтения виртуальной памяти; □ 1 — попытка записи в виртуальную память.
336 Часть V. Структурная обработка исключений А второй элемент массива содержит адрес виртуальной памяти, по которому программа пыталась прочитать или записать данные. Сделаем важное замечание О ТОМ, ЧТО фунКЦИЯ GetExceptionlnformation может вызываться только в выражении фильтра. Поэтому эта функция вызывается всегда только в том случае, если исключение произошло. Кроме того, структуры типа exception_pointers, exception_record и context действительны только на время вычисления выражения-фильтра. Чтобы использовать содержимое структур типа exception_record и context в блоке обработки исключения, его нужно сохранить в объявленных в программе переменных такого же типа. Как видно из описания структуры EXCEPTION_RECORD, фунКЦИЮ GetExceptionlnformation МОЖНО ИСПОЛЬЗОВаТЬ для двух целей: первая цель заключается в получении более подробной информации об исключении, учитывая содержимое структуры типа context; вторая цель состоит в обработке вложенных исключений. Теперь приведем, в листинге 18.5, программу, которая выводит на консоль информацию об исключении, используя для получения этой информации ФУНКЦИЮ GetExceptionlnformation. | Листинг 18,5. Получение информации об исключении #include <windows.h> #include <iostream.h> EXCEPTION_RECORD er; // информация об исключении DWORD filter_function(EXCEPTION_POINTERS *p) { // сохраняем содержимое структуры EXCEPTION_RECORD er = *(p->ExceptionRecord); return EXCEPTION_EXECUTE_HANDLER; } int main() { int *p = NULL; // пустой указатель на целое число _try { *р = 10; // ошибка, так как пустой указатель
Глава 18. Фреймовая обработка исключений 337 } except(filter_function(GetExceptionlnformation())) { // распечатываем информацию об исключении cout « "ExceptionCode = " << er.ExceptionCode « endl; cout « "ExceptionFlags = " « er.ExceptionFlags « endl; cout « "ExceptionRecord = " « er.ExceptionRecord « endl; cout « "ExceptionAddress = " « er.ExceptionAddress « endl; cout « "NumberParameters = " « er.NumberParameters « endl; // распечатываем параметры if (er.ExceptionCode == EXCEPTION_ACCESS_VIOLATION) { cout « "Type of access = " « er.Exceptionlnformation[0] « endl; cout « "Address of access = " « er.Exceptionlnformation[1] « endl; } cout << endl; } return 0; } 18.5. Генерация программных исключений Механизм структурной обработки исключений в Windows позволяет генерировать Профаммные ИСКЛЮЧенИЯ при ПОМОЩИ фуНКЦИИ RaiseException, которая имеет следующий прототип: VOID RaiseException( DWORD dwExcерtionCode, // код исключения DWORD dwExcерtionFlags, // флаг возобновляемого исключения DWORD nNuniber Of Arguments, // количество аргументов CONST ULONG_PTR *lpArgumens // массив аргументов ); Эта функция выбрасывает исключение с кодом, заданным параметром dwExceptionCode. Отметим, что код исключения следует формировать в соответствии с правилами, принятыми в операционных системах Windows. Формат кода ошибки в Windows определен в файле winerror.h и имеет структуру, которая описана в табл. 18.1.
338 Часть V. Структурная обработка исключений Таблица 18.1. Формат кода ошибки Биты Назначение Описание 0 — 15 Код ошибки Устанавливает пользователь, могут принимать любые значения 16 — 27 Код подсистемы, в которой Устанавливает пользователь, могут прини- произошла ошибка мать любые значения 28 Зарезервирован системой Всегда сбрасывается системой в ноль 29 Флаг системной или поль- Может принимать одно из двух значений: зовательской ошибки 0 - системная ошибка; 1 — пользовательская ошибка 30 — 31 Код серьезности Могут принимать следующие значения: 0 — успех; 1 — информация; 2 — предупреждение; 3 — ошибка Остальные параметры функции RaiseException имеют следующее назначение. Параметр dwExceptionFiag может принимать одно из двух значений: о или exception_noncontinuable. Значение 0 означает, что после исключения возможно восстановление программы, вызвавшей это исключение. Если же Параметр dwExceptionFiag Имеет Значение EXCEPTION_NONCONTINUABLE, TO выполнение программы не может быть возобновлено после исключения. В случае попытки возобновления программы система сгенерирует исключение С КОДОМ EXCEPTION_NONCONTINUABLE_EXCEPTION. Параметр nNumberOfArguments указывает количество параметров, передаваемых функции фильтра из блока обработки исключения. Это значение не может превышать величину exception_maximum_parameters. Параметр ipArguments задает параметры, которые передаются в функцию фильтра из блока обработки исключения. Это значение может быть установлено В NULL. В ЭТОМ случае параметр nNumber Of Arguments Игнорируется. В листинге 18.6 приведена программа, которая генерирует программное исключение и передает функции фильтра два параметра. # include <windows.h> #include <iostream.h> EXCEPTION_RECORD er;
Глава 18. Фреймовая обработка исключений 339 DWORD filter_function(EXCEPTION_POINTERS *p) . { // сохраняем содержимое структуры EXCEPTION_RECORD er = *(p->ExceptionRecord); // передаем управление обработчику исключений return EXCEPTION_EXECUTE_HANDLER; } int main() { int a = 10; int b = 20; DWORD Arguments [2] ; —try { if (a < b) { // устанавливаем аргументы исключения Arguments[0] = a; Arguments [1] = b; // генерируем исключение RaiseException(OxFF, 0,2, Arguments); } else { cout « "There is no any exception." « endl; cout « "a - b = " « (a - b) « endl; } } except(filter_function(GetExceptionInformation())) { cout « "There is an exception." « endl; if (er.ExceptionCode == OxFF) { cout « "Exception code = " « hex « er. Except ionCode « endl; cout « "Number parameters = " « dec « er. Number Parameters « endl;
340 Часть V. Структурная обработка исключений cout « "Parameter[0] = " « er.ExceptionInformation[0] « endl; cout « "Parameter[1] = " « er.ExceptionInformation[l] « endl; } } return 0; } 18.6. Необработанные исключения Если в программе произошло исключение, для которого не существует обработчика исключений, то в этом случае вызывается функция-фильтр системного обработчика исключений, которая выводит на экран окно сообщений с предложением пользователю закончить программу аварийно или выполнить Отладку Приложения. Системная фуНКЦИЯ-филЬТр UnhandledExceptionFilter имеет следующий прототип: LONG UnhandledExceptionFilter(PEXCEPTION_POINTERS pExceptionlnfo); Эта функция имеет один параметр, который указывает на структуру типа exception_info и возвращает одно из следующих значений: □ exception_conthstue_search — передать управление отладчику приложения; □ exception_execute_handler — передать управление обработчику исключений. Приложение может заменить системную функцию-фильтр с помощью фуНКЦИИ SetUnhandledExceptionFilter, КОТОрая имеет следующий ПроТО- тип: LPTOP_LEVEL_EXCEPTION_FILTER SetUnhandledExceptionFilter( LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter ); Эта функция возвращает адрес старой функции фильтра или null, если установлен системный обработчик исключений. Единственным параметром этой функции является указатель на новую функцию-фильтр, которая будет установлена вместо системной. Эта функция-фильтр должна иметь прототип, соответствующий СИСТеМНОЙ фуНКЦИИ фильтра UnhandledExceptionFilter, и возвращать одно из следующих значений: □ exception_execute_handler — выполнение программы прекращается; □ exception_continue_execution — возобновить исполнение программы с точки исключения; □ exception_continue_search — выполняется системная функция UnhandledExceptionFilter.
Глава 18. Фреймовая обработка исключений 341 Для того чтобы восстановить системную функцию-фильтр UnhandledExceptionFilter, НуЖНО Вызвать функцию UnhandledExceptionFilter с параметром null. В листинге 18.7 приведена программа, которая устанавливает новую функцию-фильтр для необработанных исключений, а затем восстанавливает системную фуНКЦИЮ UnhandledExceptionFilter. [ Листинг 18 J, Установка новой функции-фильтра wm необработанных ^ ;; | | исключений -.«•..:>**,! !.„ . „.. .. „..„..,... J;.... L. :>.— ....1. .~.Jj.L..~..~^...~..J&..~.„..JX?~..£."~.~.~.—-.'l...^.r..-.r„..J..„.: # include <windows.h> #include <iostream.h> LONG new_filter(PEXCEPTION_POINTERS pExceptionlnfo) { cout « "New filter-function is called." « endl; cout « "Exception code = " « hex « pExceptionInfo->ExceptionRecord->ExceptionCode « endl; return EXCEPTION_EXECUTE_HANDLER; } int main() { int *p = NULL; LPTOP_LEVEL_EXCEPTION_FILTER old_filter; // устанавливаем новую функцию-фильтр необработанных исключений old_filter = SetUnhandledExceptionFilter((LPTOP_LEVEL_EXCEPTION_FILTER)new_filter); // выводим адрес старой функции-фильтра cout « "Old filter-function address = " « hex « old_filter « endl; // создаем необработанное исключение *p = 10; return 0;
342 Часть V. Структурная обработка исключений 18.7. Обработка исключений с плавающей точкой По умолчанию система отключает все исключения с плавающей точкой. Поэтому если при выполнении операции с плавающей точкой было получено число, которое не входит в диапазон представления чисел с плавающей точкой, то в результате система вернет nan или infinity в случае слишком малого или слишком большого числа соответственно. Чтобы включить режим генерации исключений с плавающей точкой нужно изменить состояние слова, управляющего обработкой операций с плавающей точкой. Это можно сделать при помощи функции _controifp, которая имеет следующий прототип: unsigned int _controlfp( unsigned int new, unsigned int mask ); Прототип определен в заголовочном файле float.h. Эта функция возвращает старое слово, управляющее обработкой исключений. Здесь параметр new задает новое управляющее слово, а параметр mask определяет, какая битовая маска устанавливается в этом управляющем слове. Для разрешения генерации исключений с плавающей точкой параметр mask должен принимать значение _mcw_em. Если значение этого параметра равно 0, то функция _controifp просто возвращает старое управляющее слово. В параметре new для управления исключениями можно сбрасывать или устанавливать следующие значения: □ _em_invalid — исключение exception_flt_invalid_operation; □ _em_denormal — исключение exception_flt_denormal_operand; □ _em_zerodivide — исключение exception_flt_divide_by_zero; □ _em_overflow — исключение exception_flt_overflow; □ _em_underflow — исключение exception_flt_underflow; □ _em_inexact — исключение exception_flt_inexact_result. Если бит, соответствующий одному из этих значений, сброшен, то система генерирует соответствующее исключение с плавающей точкой, в противном случае исключение не генерируется. В листинге 18.8 приведена программа, которая обрабатывает исключение с плавающей точкой при делении на ноль.
Глава 18. Фреймовая обработка исключений 343 #include <windows.h> #include <iostream.h> #include <float.h> int main() { double a = 0; // получить управляющее слово, заданное по умолчанию int cw = _controlfp( 0, 0 ); // разрешить обработку исключений с плавающей точкой cw &=~(EM_OVERFLOW | EM_UNDERFLOW | EM_INEXACT | EM_ZERODIVIDE | EM_DENORMAL) ; // установить новое управляющее слово _controlfp( cw, _MCW_EM ); // теперь можно обрабатывать исключения _try { double b; b = 1/a; // ошибка, деление на 0 } except(EXCEPTION_EXECUTE_HANDLER) { DWORD ec = GetExceptionCode () ; // получаем код исключения if (ее == EXCEPTION_FLT_DIVIDE_BY_ZERO) cout « "Exception float divide by zero." « endl; else cout « "Some other exception." « endl; return 0;
344 Часть V. Структурная обработка исключений 18.8. Обработка вложенных исключений При использовании структурной обработки исключений возможно вкладывать блоки try и except в другой блок try. В этом случае если функция-фильтр внутреннего блока except возвращает значение exception. continue_search, то система удаляет все локальные объекты, принадлежащие текущим блокам try и except, и продолжает поиск обработчика исключений во внешних блоках try и except. Так как локальные объекты, определенные внутри любого блока, хранятся в стеке процесса, то фактически система очищает стек процесса. Область стека, которую занимают локальные объекты одного блока, называется фреймом стека. Поэтому можно сказать, что при обработке вложенных исключений выполняется очистка стека процесса от локальных объектов, определенных внутри вложенных блоков try и except. Такая очистка стека от локальных объектов называется глобальной раскруткой стека или просто раскруткой стека. В листинге 18.9 приведена программа, в которой используется обработка вложенных исключений. ; Листинг 18.9. Обработка вложенных исключений #include <windows.h> #include <iostream.h> void f(int *p) { int a = 10; *p = a; // ошибка, так как пустой указатель } void g(int *p) { int a = 0; *р /= а; // ошибка, деление на ноль } int main() { int a = 10;
Глава 18. Фреймовая обработка исключений 345 int *p = NULL; // пустой указатель на целое число _try { _try { f(p); } except(GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { cout « "Exception access violation." « endl; } p = &a; // указатель не пустой _try { g(p) ; } except(GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { cout « "Exception access violation." « endl; } } except(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) { cout « "Exception integer divide by zero." « endl; } return 0; } В работе программы из листинга 18.9 отметим, что, в первом случае вызывается вложенный обработчик исключения, а во втором — вызывается внешний обработчик исключения.
346 Часть V. Структурная обработка исключений 18.9. Передача управления и выход из фрейма Для передачи управления из фрейма можно использовать инструкцию goto языка программирования C++. В этом случае система считает, что блок с охраняемым кодом завершился аварийно и поэтому выполняет глобальную раскрутку стека. Следовательно, использование инструкции goto вызывает исполнение дополнительного программного кода, что замедляет выполнение программы. В листинге 18.10 приведена программа, которая использует инструкцию goto для выхода из блока try. # include <windows. h> #include <iostream.h> int main() { int *p = NULL; // пустой указатель на целое число _try { if (!p) { cout « "Exit with goto from the try block." « endl; goto exit_try; // выходим их блока } else *p = 10; } except (EXCEPTION_EXECUTE_HANDLER) { cout « "There was some exception." « endl; } cout « "*p = " « *p « endl; exit_try: return 0;
Глава 18. Фреймовая обработка исключений 347 Отметим, что в программе из листинга 18.10 выход из фрейма при помощи инструкции goto используется для обхода инструкции, следующей после блока except. Если необходимо просто завершить выполнение блока try без аварийного выхода, т. е. не начиная раскрутку стека, то в этом случае нужно использовать инструкцию leave, которая введена в язык профаммирования C++ фирмой Microsoft. В листинге 18.11 приведена профамма, в которой используется инструкция leave для выхода из блока try. | Листинг 18.11. Выход из блока try о использованием инструкции _leave #include <windows.h> #include <iostream.h> int main() { int *p = NULL; // пустой указатель на целое число _try { if (!p) { cout « "Exit with leave from the try block." « endl; leave; // выходим их блока } else { *p = 10; cout « "*p = " « *p « endl; } } except (EXCEPTION_EXECUTE_HANDLER) { cout « "There was some exception." « endl; } return 0;
348 Часть V. Структурная обработка исключений В заключение этого раздела можно сказать, что если в программе используется инструкция goto для выхода из блока try, то, как правило, эта программа плохо структурирована. 18.10. Встраивание SEH в механизм исключений C++ В реализации языка программирования C++ фирмой Microsoft, т. е. в Visual C++, предусмотрен механизм, который позволяет использовать механизм структурной обработки исключений в механизме обработки исключений, используемом в C++. Для этой цели была разработана функция _set_se_transiator. Эта функция устанавливает в системе функцию, которая называется функцией-транслятором, назначение которой состоит в том, чтобы преобразовывать структурные исключения в исключения языка программирования C++. Если функция-транслятор установлена, то она вызывается всегда при выбросе структурного исключения. В функции-трансляторе можно использовать инструкцию throw языка программирования C++, которая будет выбрасывать исключение C++ нужного типа. Функция-транслятор должна иметь следующий прототип: typedef void (*_se_translator_function) (unsigned int, Struct _EXCEPTION_POINTERS*); который описан в заголовочном файле eh.h. Как видно из этого описания — функция-транслятор не возвращает значения и получает два параметра: код исключения и указатель на структуру типа _exception_pointers. Функция _set_se_transiator, которая используется для установки функции-транслятора, также описана в заголовочном файле eh.h и имеет следующий прототип: _se_translator_function _set_se_translator(_se_translator_function se_trans_func); Единственным параметром этой функции является указатель на новую функцию-транслятор, а возвращает функция _set_se_transiator адрес старой функции-транслятора, которая в дальнейшем может быть восстановлена при помощи вызова _set_se_transiator. Если функция-транслятор устанавливается в первый раз, то возвращаемое значение может быть равно NULL. В листинге 18.12 приведена программа, в которой устанавливается функция- транслятор. Эта функция просто выбрасывает исключение языка программирования C++, которое является кодом структурного исключения.
Глава 18. Фреймовая обработка исключений 349 7&V '" 7Гь"™~7Т ШП^ТШ Листинг 18,12. Выброс кода структурного искШЪчен программирования C++ # inc lude <windows . h> #include <iostream.h> #include <eh.h> void se_trans_func(unsigned code, EXCEPTION_POINTERS *) { throw code; } int main() { int a = 10, b = 0; // устанавливаем функцию-транслятор _set_se_translator (se_trans_func) ; // перехватываем структурное исключение средствами C++ try { а /= b; // ошибка, деление на ноль } catch(unsigned code) { cout « "Exception code = " « hex « code « endl; } return 0; } Теперь приведем в листинге 18.13 программу, которая выбрасывает структуру типа exception_pointers как исключение языка программирования C++. 1истинг 18.13. Выброс структуры типа жшф^^ 1зыка программирования C++ * *Ч * yrv ' *\?*** *<-;^>^ ~:й% ч-^ #include <windows.h> #include <iostream.h> #include <eh.h>
350 Часть V. Структурная обработка исключений void se__trans_func (unsigned code, EXCEPTION_POINTERS *info) { EXCEPTION_RECORD er; CONTEXT c; EXCEPTION_POINTERS ep = {&er, &c}; er = *(info->ExceptionRecord); с = *(info->ContextRecord); throw ep; } int main() { int a = 10, b = 0; // устанавливаем функцию-транслятор _set_se_translator (se_trans_func) ; // перехватьшаем структурное исключение средствами C++ try { а /= b; // ошибка, деление на ноль } catch(EXCEPTION_POINTERS ep) { cout « "Exception code = " « hex « ep.ExceptionRecord->ExceptionCode « endl; } return 0; }
Глава 19 Финальная обработка исключений 19.1. Финальные блоки фрейма В операционных системах Windows существует еще один способ обработки исключений, суть которого заключается в следующем. Код, при исполнении которого возможен выброс исключения, как и в случае с фреймовой обработкой исключений, заключается в блок try. Но только теперь за блоком try следует код, который заключается в блок finally. Система гарантирует, что при любой передаче управления из блока try, независимо от того, произошло или нет исключение внутри этого блока, предварительно управление будет передано блоку finally. Такой способ обработки исключений называется финальная обработка исключений. Структурно финальная обработка исключений выглядит следующим образом: _try { // охраняемый код } finally { // финальный код } Очевидно, что, как и в случае с фреймовой обработкой исключений, финальная обработка исключений требует поддержки как компилятора, так и операционной системы. Кроме того, в язык программирования C++ добавляется новое ключевое слово finally. Финальная обработка исключений используется для того, чтобы при любом исходе исполнения блока try освободить ресурсы, которые были захвачены внутри этого блока. Такими ресурсами могут быть память, файлы, критические секции и т. д.
352 Часть V. Структурная обработка исключений В листинге 19.1 приведена программа, в которой используется финальная обработка исключений. #include <windows.h> #include <iostream.h> int main() { char c; int *p = NULL; // пустой указатель на целое число // введите 'у', чтобы распределить память под целое число cout « "Input 'у' to allocate memory: "; // вводим символ cin » с; // если ввели 'у', то память распределяется, иначе - не распределяется if (с == 'у') р = new int; _try { *р = 10; // может быть исключение, если память не распределена } finally { //в любом случае пробуем освободить память delete р; // выводим сообщение cout « "The finally block finished." « endl; } return 0; } Отметим, что блок finally выполняется при любом исходе выполнения блока try. Недостатком такой работы блока finally является то, что инструкция delete будет выполняться в любом случае, независимо от того, произошло исключение в блоке try или нет. Чтобы избежать такой си-
Глава 19. Финальная обработка исключений 353 туации, нужно проверить, как завершился блок try — нормально или нет. О том, как это делается, будет рассказано в следующем разделе. 19.2. Проверка завершения фрейма Управление из блока try может быть передано одним из следующих способов: □ нормальное завершение блока; □ выход из блока при помощи управляющей инструкции leave; □ выход из блока при помощи одной из управляющих инструкций return, break, continue ИЛИ goto ЯЗЫКа программирования C++; □ передача управления обработчику исключения. В первых двух случаях считается, что блок try завершился нормально, а в последних двух случаях — ненормально. Для того чтобы определить, как завершился блок try, используется функция AbnormaiTermination, которая имеет следующий прототип: BOOL AbnormaiTermination (VOID) ; В случае если блок try завершился ненормально, эта функция возвращает ненулевое значение, а в противном случае — значение false. Используя функцию AbnormaiTermination, ресурсы, захваченные в блоке try, можно освобождать только в том случае, если блок try завершился ненормально. В листинге 19.2 приведена программа, которая использует функцию AbnormaiTermination для проверки нормального завершения блока try. |Листинг 19.2» Проверка нормального завершения блока _try #include <windows.h> #include <iostream.h> int main () { char c; int *p = NULL; // пустой указатель на целое число // введите 'у', чтобы распределить память под целое число cout « "Input 'у' to allocate memory: " ; // вводим символ 12 Зак. 514
354 Часть V. Структурная обработка исключений cin » с; // если ввели 'у', то память распределяется, иначе - не распределяется if (с == 'у') р = new int; —try { *р = 10; // может быть исключение, если память не распределена } finally { // если блок try закончился нормально if (!AbnormalTermination()) { //то освобождаем память delete p; cout « "The memory is free." « endl; } else // иначе нечего освобождать cout « "The memory was not allocated." « endl; } return 0; } 19.3. Обработка вложенных финальных блоков Как и в случае фреймовой обработки исключений, можно вкладывать блоки try и finally в другой блок try. В этом случае, если внутри самого внутреннего блока try произошло исключение, то, как и в случае фреймовой обработки исключений, выполняется раскрутка стека. Если самые внутренние блоки try и finally вложены в другие блоки try с финальной обработкой исключений, то при раскрутке стека управление передается всем вложенным блокам finally в порядке, обратном их вложенности. В листинге 19.3 приведена программа, в которой используется обработка вложенных исключений.
Глава 19. Финальная обработка исключений 355 # include <windows.h> #include <iostream.h> int main() { _try { int *a, *b; _try { a = new intA0); —try { b = new int@); // ошибка, деление на ноль cout « "a/b = " « (*a)/(*b) « endl; } finally { // освобождаем память для fbf delete b; cout « "The memory for 'b' is free." « endl; } } finally { // освобождаем память для 'а' delete a; cout « "The memory for 'a' is free." « endl; } } except(EXCEPTION_EXECUTE_HANDLER) { cout « "There was some exception." « endl; } return 0;
Часть VI Работа с виртуальной памятью Глава 20. Виртуальная память Глава 21. Работа с виртуальной памятью в Windows Глава 22. Работа с кучей в Windows
Глава 20 Виртуальная память 20.1. Концепция виртуальной памяти Интегральные схемы, предназначенные для хранения программ и данных, называются физической памятью. Обычно под физической памятью мы понимаем память, к которой процессор может обращаться, используя адресную шину и шину данных, а внутренняя память самого процессора представляется регистрами. Каждый байт физической памяти имеет свой номер или индекс, который называется физическим адресом. При обращении к физической памяти процессор должен выставить на адресную шину физический адрес памяти, к которой он хочет получить доступ. Под логической памятью процесса понимается массив байтов, к которым может обратиться процесс. Индекс каждого элемента этого массива называется логическим адресом. Так как логическая память процесса представляется линейным массивом байтов, то логический адрес процесса обычно называют линейным адресом. Так как в действительности процесс может работать только с данными в физической памяти, то во время работы процесса необходимо отображать логическую память процесса в физическую память компьютера. Обычно прямое отображение невозможно по той простой причине, что объем логической памяти процесса превышает объем физической памяти компьютера. Для решения этой задачи физическую память компьютера дополняют памятью на дисках. Полученную расширенную память называют виртуальной памятью, а адрес элемента этой памяти называют виртуальным адресом. Тогда при работе процесса выполняется преобразование адресов, которое схематически показано на рис. 20.1. Преобразование линейного адреса процесса в виртуальный адрес выполняется операционной системой посредством настройки регистров процессора. Обычно линейный адрес процесса отличается от виртуального адреса только интерпретацией бит в этом адресе. Преобразование виртуального адреса в физический адрес выполняется аппаратным образом, а именно — процессором.
360 Часть VI. Работа с виртуальной памятью линейный адрес виртуальный адрес физический адрес Рис. 20.1. Преобразование адресов при работе процесса Так как виртуальная память также представляет собой физическую память, то часто для того, чтобы отличать виртуальную память от физической на интегральных схемах, последнюю также называют реальной памятью. 20.2. Организация виртуальной памяти Для реализации преобразования виртуального адреса в физический поступают следующим образом. Виртуальную память разбивают на блоки одинаковой длины, обычно равной 4 Кбайт, которые называют страницами. В этом случае файлы, в которых хранятся страницы виртуальной памяти, называются файлами страниц или файлами подкачки. При обращении процесса по адресу в виртуальной странице, если необходимо, то происходит загрузка этой страницы в реальную память компьютера и настройка адресного пространства процесса на работу с этой страницей. Такая организация виртуальной памяти называется страничной. Учитывая страничную организацию виртуальной памяти — физический и виртуальный адреса имеют форматы, которые показаны на рис. 20.2. г d V d г — номер реальной страниц d — смещение в реальной странице v — номер виртуальной страницы d — смещение в виртуальной странице а) формат реального адреса б) формат вируального адреса Рис. 20.2. Форматы реального и виртуального адресов Фактически в этом случае адрес делится на две части: старшую и младшую. Старшая часть адреса рассматривается как номер страницы в реальной или виртуальной памяти, а его младшая часть — как смещение внутри этой страницы. Например, если адрес имеет длину 32 бита, а длина страницы равна 4 Кбайт, то младшие 12 бит рассматриваются как смещение внутри
Глава 20. Виртуальная память 361 страницы, старшие 20 бит — как номер страницы. Это разбиение обусловлено тем, что 212 = 4096 байт. Единственное различие между виртуальным и реальным адресами состоит в том, что виртуальный адрес имеет большую длину поля, отведенного на номер виртуальной страницы. Для преобразования виртуальных адресов в реальные адреса в системной области физической памяти для каждого процесса хранится таблица страниц, строки которой имеют структуру, которая показана на рис. 20.3. far f— флаг, отмечающий нахождение виртуальной страницы в реальной памяти а — адрес вирутуальной страницы во внешней памяти г— адрес реальной страницы в физической памяти Рис. 20.3. Формат строки таблицы страниц процесса b 1 L J* Базовый адрес * таблицы страниц Таблицы страниц —f— 1 Виртуальный адрес v | d 1 Г 1 f Г 1 <Г^ Реальный адрес Рис. 20.4. Схема преобразования виртуального адреса в реальный адрес Схема преобразования виртуального адреса в физический адрес изображена на рис. 20.4, где регистр b содержит базовый адрес таблицы страниц процесса. Используя эту схему преобразования адресов, алгоритм отображения виртуальной памяти в реальную память может быть описан следующим образом. 1. Находится строка в таблице страниц, соответствующая номеру виртуальной страницы. Индекс этой строки равен b + v. 2. Если f = 1, то виртуальная страница находится в реальной памяти. Если же f = 0, то виртуальная страница находится на диске. В этом случае выполняется загрузка виртуальной страницы в реальную память. После чего значение столбца f устанавливается в 1, а столбец г указывает на адрес виртуальной страницы в реальной памяти.
362 Часть VI. Работа с виртуальной памятью 3. Вычисляется реальный адрес, который формируется из адреса реальной страницы, заданного значением г, и смещения в виртуальной странице, заданного значением d. Повторим, что все эти действия выполняются аппаратным образом, а именно, процессором. В заключение этого раздела отметим, что возможен другой подход к организации виртуальной памяти, при котором виртуальная память разбивается на блоки неравной длины, которые называются сегментами. В этом случае в реальную память загружаются не страницы, а блоки виртуальной памяти. Существует также комбинированный подход, который заключается в том, что виртуальная память разбивается на страницы, которые потом объединяются в сегменты. Эти подходы к организации виртуальной памяти нами рассматриваться не будут, т. к. в операционных системах Windows используется страничная организация виртуальной памяти. Сегментная организация виртуальной памяти использовалась в старой операционной системе Windows 3.1. 20.3. Алгоритмы замещения страниц При подкачке виртуальной страницы в физическую память может оказаться, что все страницы физической памяти уже заняты другими виртуальными страницами. В этом случае одна из физических страниц выталкивается из физической памяти на диск, а на ее место с диска загружается требуемая виртуальная страница. Возникает следующая проблема — какую виртуальную страницу вытолкнуть из физической памяти на диск? Для определения такой страницы чаще всего используются следующие алгоритмы: □ алгоритм FIFO (first in first out), который заключается в том, что первой выталкивается на диск первая из загруженных в реальную память виртуальных страниц; □ алгоритм LRU (least recently used), который заключается в том, что первой из памяти выталкивается на диск виртуальная страница, которая дольше всего не использовалась; □ алгоритм LFU (least frequently used), который заключается в том, что на диск выталкивается виртуальная страница, которая наименее часто используется; □ алгоритм NUR (not used recently), который похож на алгоритм LRU, но при выталкивании на диск выбираются те страницы, на которые дольше всего не было записи. Все эти алгоритмы имеют свои преимущества и недостатки, и для каждого из них можно подобрать такой случай использования виртуальных страниц, при котором система начнет буксовать. То есть при каждом новом обращении
Глава 20. Виртуальная память 363 к памяти будет необходима подкачка новой виртуальной страницы в реальную память. Оптимального алгоритма подкачки виртуальных страниц не существует, т. к. поведение приложений непредсказуемо. Если загрузкой виртуальных страниц управляет сама программа, все шаги выполнения которой заранее известны, то, очевидно, что возможен оптимальный порядок загрузки и выгрузки виртуальных страниц, который минимизирует обращения к диску. Так как запись виртуальной страницы на диск довольно медленная операция, то обычно элемент таблицы страниц содержит также флаг, отмечающий, была ли произведена запись на виртуальную страницу, находящуюся в реальной памяти. Если записи не было, то эта виртуальная страница просто затирается при подкачке на ее место другой виртуальной страницы с диска. 20.4. Рабочее множество процесса Эмпирически было определено, что при работе многих программ наблюдается свойство локальности. То есть выполняемый в какой-то интервал времени код программы и используемая программой память расположены локально, а не разбросаны по всей программе. Обычно программисты пишут свои программы, также следуя этому правилу. Поэтому для эффективной работы программы необходимо, чтобы какое-то множество часто используемых на данном интервале времени виртуальных страниц находилось в реальной памяти. Это множество виртуальных страниц называется рабочим множеством страниц процесса. Естественно, рабочее множество страниц процесса изменяется со временем: какие-то страницы начинают использоваться реже, а какие-то — чаще. Но для каждого процесса можно определить размеры рабочего множества страниц таким образом, что на каждом интервале времени все часто используемые страницы будут находиться в реальной памяти. 20.5. Организация виртуальной памяти в Windows Как уже говорилось, в операционных системах Windows используется страничная организация виртуальной памяти. При этом линейный адрес процесса совпадает с его виртуальным адресом, формат которого показан на рис. 20.5. Исходя из этого формата, реальный адрес вычисляется по схеме, которая показана на рис. 20.6. Единственное отличие этой схемы от изложенной ранее схемы преобразования виртуального адреса в реальный заключается в том, что теперь адрес таблицы страниц хранится не в регистре процессора, а в специальной таблице, которая называется каталогом таблиц страниц. Каждая строка катало-
364 Часть VI. Работа с виртуальной памятью га таблиц страниц содержит адрес уникальной таблицы страниц. В свою очередь поле р виртуального адреса имеет уникальное значение для каждого процесса. Поэтому каждый процесс использует единственную таблицу страниц. Отсюда следует, что адресные пространства разных процессов изолированы друг от друга. 31 22 р 21 V 12 11 d 0 р — смещение в каталоге таблиц страниц v — номер виртуальной страницы d — смещение в виртуальной странице Рис. 20.5. Формат виртуального адреса в Windows с I 1 \. 1 J + ъ, W Базовый адрес каталога таблиц страниц t *< ^^ 1 У Каталог таолиц страниц Виртуальный адрес W ^ Та блица стран АЦ Р | V Y г Ре эальн d i f d ый aji Фес Рис. 20.6. Схема преобразования виртуального адреса в реальный адрес в Windows Строка таблицы страниц имеет структуру, которая показана на рис. 20.7. Как видно из этого рисунка, строка таблицы страниц имеет довольно сложную структуру, основным назначением которой является сокращение обмена виртуальными страницами между реальной и виртуальной памятью. Опишем назначение полей строки таблицы страниц более подробно. Это прояснит организацию виртуальной памяти в Windows.
Глава 20. Виртуальная память 365 131 27 р 26 7 А 6 3 F 2 0 s S — описывает состояние страницы F — задает номер файла подкачки А — содержит физический адрес страницы Р — содержит атрибуты доступа к странице Рис. 20.7. Структура строки таблицы страниц в Windows Поле S описывает состояние виртуальной страницы, которое определяется комбинацией трех бит. Различные комбинации этих бит определяют следующие состояния виртуальной страницы: □ страницы нет в реальной памяти (invalid page); □ страница находится в реальной памяти (valid page); □ страница находится в реальной памяти и была модифицирована (valid dirty page); □ страница загружается в реальную память (invalid page in transition); □ страница сохраняется на диск (invalid dirty page in transition). Поле F задает номер файла подкачки на диске. Учитывая длину этого поля, можно определить до 16 файлов подкачки виртуальных страниц. Поле А содержит физический адрес страницы в реальной памяти при условии, что страница загружена с диска. Учитывая длину этого поля, можно определить, что виртуальная память процесса может содержать 220 виртуальных страниц. Отсюда следует, что, т. к. длина виртуальной страницы равна 4 Кбайт, то вся виртуальная память процесса составляет 220 х 4 Кбайт = 4 Гбайта виртуальной памяти. Поле Р содержит атрибуты доступа к виртуальной странице, которые могут принимать следующие значения: □ page_noaccess — доступ к странице запрещен; □ page_readonly — доступно только чтение страницы; □ page_readwrite — доступны чтение и запись страницы. В заключение этого раздела отметим, что в операционных системах семейства Windows NT управлением виртуальной памяти занимается специальный процесс, который называется менеджером виртуальной памяти (Virtual Memory Manager — VMM). Менеджер виртуальной памяти поддерживает свое внутреннее описание состояния каждой страницы в реальной памяти. В соответствии с этим описанием каждая страница реальной памяти может находиться в одном из следующих состояний: □ страница в рабочем состоянии и используется процессом (valid);
366 Часть VI. Работа с виртуальной памятью П страница записывается на диск (modified); □ страница удаляется из рабочего множества страниц процесса (standby); □ страница освобождена процессом, но не заполнена нулями (free); □ страница заполнена нулями и может использоваться любым процессом (zeroed); □ страница в нерабочем состоянии (bad). Для каждого процесса операционная система Windows определяет рабочее множество страниц этого процесса. При замещении страниц Wndows использует алгоритм LRU, но только с тем отличием, что он применяется не для всех виртуальных страниц, находящихся в реальной памяти, а отдельно для рабочего множества страниц каждого процесса. Во время работы процесса менеджер виртуальной памяти периодически проверяет частоту использования страниц из рабочего множества процесса. Если некоторая виртуальная страница используется редко, то она удаляется из рабочего множества процесса.
Глава 21 Работа с виртуальной памятью в Windows 21.1. Состояния виртуальной памяти процесса Линейный адрес процесса в Windows состоит из 32 бит и изменяется в пределах от Охоооооооо до Oxffffffff. Это теоретически позволяет процессу обращаться к 4 Гбайт логической памяти. В операционных системах семейства Windows NT процессу доступны два младших гигабайта этой памяти с диапазоном адресов от Охоооооооо до 0x7fffffff, а ее старшие два гигабайта с диапазоном адресов от Oxsooooooo до Oxffffffff используются системой. В операционной системе Windows 98 из 2 Гбайт памяти доступной процессу, операционная система использует еще 64 Кбайт памяти с диапазоном адресов от Охоооооооо до 0x0000ffff для проверки присваивания значений через указатели, значения которых инициализированы в null, и для поддержки совместимости со старыми операционными системами MS-DOS и Windows 3.1. В операционных Windows виртуальный адрес процесса отличается от линейного адреса этого же процесса только интерпретацией бит линейного адреса. Поэтому, можно сказать, что каждому процессу в Windows также доступно два гигабайта виртуальной памяти. Это не значит, что процесс может использовать всю эту память одновременно. Количество виртуальной памяти, доступной процессу, зависит от емкости физической памяти и дисков. Чтобы ограничить процесс в использовании виртуальной памяти, некоторые страницы в таблице страниц могут быть помечены как недоступные. После этих замечаний перейдем к описанию состояния виртуальной памяти процесса. С точки зрения процесса, страницы его виртуальной памяти могут находиться в одном из трех состояний: □ свободны для использования (free); □ распределены процессу для использования (committed). □ зарезервированы, но не используются процессом (reserved);
368 Часть VI. Работа с виртуальной памятью Поясним эти состояния более подробно. Первоначально, при запуске процесса, все страницы виртуальной памяти считаются свободными, естественно кроме тех, в которые загружена сама программа. Адреса загрузки всех модулей можно узнать, выбрав, при отладке программы, в среде разработки Visual C++ в пункте меню Debug | Modules. Чтобы распределить для использования свободные или зарезервированные страницы виртуальной памяти, процесс должен вызвать функцию virtuaiAiioc. Только после успешного завершения этой функции процесс может использовать распределенную ему виртуальную память. Третье состояние характеризует виртуальные страницы как зарезервированные. Это значит, что эти виртуальные страницы зарезервированы процессом для дальнейшего использования и не будут выделяться системой для использования процессу без точного указания процессом их адреса. Следует отметить, что при резервировании виртуальных страниц реальная память под эти страницы не выделяется. 21.2. Резервирование, распределение и освобождение виртуальной памяти Для резервирования или распределения области виртуальной памяти процесс должен вызвать функцию virtuaiAiioc, которая имеет следующий прототип: LPVOID VirtuaiAiioc( LPVOID IpAddress, // область для распределения или резервирования SIZE_T dwSize, // размер области DWORD flAllocationType, // тип распределения DWORD flProtect // тип защиты доступа ); В случае успешного завершения эта функция возвращает адрес виртуальной памяти, распределенной или зарезервированной процессом, а в случае неудачи — null. При этом отметим такую деталь, если распределение виртуальной памяти функцией virtuaiAiioc завершается успешно, то выделенная память автоматически инициализируется нулями. Опишем назначение параметров этой функции. Параметр ipAddress устанавливается вызывающей программой и указывает системе начальный адрес виртуальной памяти, которую процесс хочет зарезервировать или распределить. Этот адрес может указывать как на свободную, так и зарезервированную ранее виртуальную память. При установке этого адреса следует различать следующие ситуации: □ в случае резервирования виртуальной памяти этот адрес выравнивается системой до границы в 64 Кбайт, которая предшествует указанному адресу;
Глава 21. Работа с виртуальной памятью в Windows 369 □ в случае распределения виртуальной памяти из зарезервированной ранее области этот адрес округляется операционной системой до границы виртуальной страницы, содержащей этот адрес; □ в случае, если параметр lpAddress равен null, to операционная система сама выбирает начальный адрес области виртуальной памяти. Параметр dwsize устанавливается вызывающей программой и указывает размер распределяемой или резервируемой области виртуальной памяти в байтах. Если параметр lpAddress равен null, to система округляет эту величину в большую сторону до кратности размеру виртуальной страницы. Если же память распределяется по конкретным адресам, то эта память будет включать все страницы, которые содержат байты из диапазона от lpAddress ДО lpAddress+dwSize. Параметр fiAiiocationType устанавливается программой и указывает на тип операции, которую выполняет функция virtuaiAiioc. Значением этого параметра может быть любая комбинация следующих флагов: □ mem_commit — распределить память программе; □ mem_reserve — зарезервировать область физической памяти. В операционных системах Windows NT/2000 в этом параметре могут устанавливаться следующие флаги: □ mem_reset — память временно не используется; □ mem_top_down — распределить память, начиная с наибольшего из свободных адресов. Кроме того, в операционной системе Windows 98 может быть установлен флаг: □ mem_write_watch — запоминать адреса виртуальных страниц, в которые была проведена запись. Впоследствии адреса этих страниц можно узнать, вызвав функцию Getwritewatch. Очистить список таких страниц можно посредством вызова функции Resetwritewatch. Так как использование этих функций возможно только в операционной системе Windows 98, то мы их подробно рассматривать не будем. Параметр f lProtect устанавливает атрибуты доступа к области виртуальной памяти, которые разрешают выполнять над страницами виртуальной памяти только определенные операции. Этот параметр может быть комбинацией следующих флагов: П page_readonly — разрешает только чтение виртуальных страниц; □ page_readwrite — разрешает чтение и запись в виртуальных страницах; □ page_execute — разрешает только исполнение кода в виртуальных страницах;
370 Часть VI. Работа с виртуальной памятью П page_execute_read — разрешает исполнение и чтение кода в виртуальных страницах; □ page_execute_readwrite — разрешает выполнение, чтение и запись виртуальных страниц; □ page_noaccess — к виртуальным страницам нет доступа; □ page_nocache — виртуальные страницы можно не помещать в кэш. Отметим, что попытка чтения или записи в страницу, которая предназначена только для исполнения кода (флаг page_execute), вызовет ошибку доступа (access violation). Эта же ошибка возникнет в случае любого доступа к виртуальной странице, которая отмечена флагом page_noaccess. В операционных системах Windows NT/2000 в параметре fiProtect может быть также установлен флаг: □ page_guard — охраняемая страница. Если этот флаг установлен и к странице произведено обращение, все равно, чтение или запись, система сбрасывает этот флаг и возбуждает исключение типа exception_guard_page, которое имеет код Oxsooooooi. Этот флаг используется для определения момента, когда процессу необходимо выделить дополнительную виртуальную память. Например, этот флаг можно установить для последней виртуальной страницы области, которая используется процессом при работе с некоторыми данными. Тогда при попытке доступа к этой странице генерируется исключение, и процесс будет знать, что виртуальная память для данных заканчивается. Отметим также, что флаг page_guard не может быть использован совместно с флагом page_noaccess. После завершения работы с виртуальной памятью ее необходимо освободить, используя функцию virtualFree, которая имеет следующий прототип: BOOL VirtualFree( LPVOID lpAddress, // адрес области виртуальной памяти SIZE_T dwSize, // размер области DWORD dwFreeType // тип операции ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Описание параметров этой функции начнем с последнего параметра, т. к. его значение влияет на значения, в которые программа должна установить первых два параметра. Параметр dwFreeType может принимать любую комбинацию следующих двух флагов: □ mem_decommit — отменить распределение виртуальной памяти; □ mem_release — освободить виртуальную память.
Глава 21. Работа с виртуальной памятью в Windows 371 Если используется флаг mem_decommit, to память не освобождается, а остается в зарезервированном состоянии. Чтобы освободить зарезервированную виртуальную память, нужно установить флаг mem_release. В общем случае относительно установки значения этого параметра можно сказать, что оно должно соответствовать состоянию области памяти, с которой работает ФУНКЦИЯ VirtualFree. Параметр lpAddress должен указывать на базовый адрес области, для которой нужно отменить распределение или освободить. Если в параметре dwFreeType установлен флаг mem_release, то этот адрес должен совпадать с адресом, который возвратила функция virtualAiioc. Параметр dwsize задает в байтах размер области виртуальной памяти, распределение которой нужно отменить. Если в параметре dwFreeType установлен флаг mem_release, то значение параметра dwsize должно быть равно нулю. Теперь можно перейти к примерам, которые иллюстрируют работу функций virtualAiioc и VirtualFree. Сначала рассмотрим программу в листинге 21.1, которая распределяет область виртуальной памяти под массив целых чисел, а затем освобождает ее. Отметим один момент в этой программе. Чтобы сразу распределить область виртуальной памяти, мы должны установить параметр lpAddress в значение null. В этом случае система сама определяет начальный адрес области виртуальной памяти для распределения процессу. Как распределяется память по конкретному виртуальному адресу, будет показано в листинге 21.2. [ЛИСТИНГ21Л,Р^ :>v" ^ ™| #include <windows.h> #include <iostream.h> int main() { int *a; // указатель на массив целых чисел const int size = 1000; // размерность массива // распределяем виртуальную память а = (int*)VirtualAiioc( NULL, size * sizeof(int), MEM_COMMIT, PAGE_READWRITE);
372 Часть VI. Работа с виртуальной памятью if(!а) { cout « "Virtual allocation failed." « endl; return GetLastError(); } cout « "Virtual memory address: " « a « endl; // освобождаем виртуальную память if (!VirtualFree(a, 0, MEM_RELEASE)) { cout « "Memory release failed." « endl; return GetLastError(); } return 0; } В программе из листинга 21.2 показано, как распределить область виртуальной памяти по конкретному адресу. Отметим, что в этом случае значение параметра fiAiiocationType должно быть установлено как комбинация флагов mem_reserve и mem_commit. То есть при распределении памяти по конкретному адресу эта память должна быть предварительно зарезервирована, ЧТО МОЖеТ быТЬ ВЫПОЛНеНО ОДНИМ ВЫЗОВОМ фуНКЦИИ VirtualAlloc. [Листинг 21.2. Распределение виртуальной памяти по конкретному адресу #include <windows.h> #include <iostream.h> int main () { LPVOID lp; const int size = 1000; // распределяем виртуальную память lp = VirtualAlloc( (LPVOID) 0x00890002, size, MEM_RESERVE | MEM_COMMIT/
Глава 21. Работа с виртуальной памятью в Windows 373 PAGE_READWRITE); if('lp) { cout « "Virtual allocation failed." « endl; return GetLastError(); } cout « "Virtual memory address: " « lp « endl; // освобождаем виртуальную память if (!VirtuaiFree(lp, 0, MEM_RELEASE)) { cout « "Memory release failed." « endl; return GetLastError(); } return 0; } Обратим внимание на следующий момент в программе из листинга 21.2. Хотя мы и установили начальный адрес виртуальной памяти на 0х00890002, система все равно выровняла его до ближайшей нижней границы виртуальной страницы, т. е. до 0x00890000. Теперь распределим память в два этапа. Сначала зарезервируем область виртуальной памяти, а затем распределим некоторую часть этой виртуальной памяти. Как это делается — показано в программе из листинга 21.3. Заметим, что в этой программе функция virtuaiFree используется также для отмены распределения области виртуальной памяти. |Листинг 21,3, Резервирование и распределение |йрту^1*«^ЙЙ1|Щ^ &£$$& ~ Щ\ #include <windows.h> #include <iostream.h> int main() { LPVOID lpr, lpc; const int Kb = 1024; const int size = 100; // резервируем виртуальную память
374 Часть VI. Работа с виртуальной памятью lpr = VirtualAlloc( (LPVOID) 0x00880000, size * Kb, MEM_RESERVE, PAGE_READWRITE); if(!lpr) { cout « "Virtual memory reservation failed." « endl; return GetLastErrorO; } cout « "Virtual memory address: " « lpr « endl; // распределяем виртуальную память lpc = VirtualAlloc( (LPVOID) 0x00888000, Kb, MEM_COMMIT, PAGE_READWRITE); if(!lpc) { cout « "Virtual memory allocation failed." « endl; return GetLastErrorO; } cout « "Virtual memory address: " « lpc « endl; // отменяем распределение if (!VirtualFree(lpc, Kb, MEM_DECOMMIT)) { cout « "Memory decommit failed." « endl; return GetLastError(); } // освобождаем виртуальную память if (!VirtualFree(lpr, 0, MEM_RELEASE)) { cout « "Memory release failed." « endl;
Глава 21. Работа с виртуальной памятью в Windows 375 return GetLastError(); } return 0; } В завершение этого раздела рассмотрим программу из листинга 21.4, которая устанавливает страницу в охраняемое состояние. При первой попытке доступа к этой странице система сбрасывает флаг page_guard и генерирует исключение exception_guard_page. Состояние страницы не изменяется. Повторный доступ к странице происходит обычным образом, т. к. никаких исключений не генерируется. Заметим, что начальное состояние распределенной виртуальной памяти инициализируется нулями. Также отметим, что этот код работает только в операционной системе Windows 2000. # include <windows.h> #include <iostream.h> int main() { int *a; // указатель на массив целых чисел const int size = 1024; // размерность массива // распределяем виртуальную память а = (int*)VirtualAlloc( NULL, size * sizeof(int), MEM_COMMIT, PAGE_READWRITE|PAGE_GUARD); if(!a) { cout « "Virtual allocation failed." « endl; return GetLastError(); } _try { a[10] = 10;
376 Часть VI. Работа с виртуальной памятью } except (EXCEPTION_EXECUTE_HANDLER) { DWORD ecode = GetExceptionCode(); if (ecode == EXCEPTION_GUARD_PAGE) cout « "Access to a guard virtual page." « endl; else cout « "Some exception." « endl; } cout « "a[10] = " « a[10] « endl; a[10] = 10; cout « "a[10] = " « a[10] « endl; // освобождаем виртуальную память if (!VirtualFree(a, 0, MEM_RELEASE)) { cout « "Memory release failed." « endl; return GetLastError(); } return 0; } 21.3. Блокирование виртуальных страниц в реальной памяти Если некоторая область виртуальной памяти будет часто использоваться процессом, то можно запретить системе выгружать эти виртуальные страницы из реальной памяти, иначе говоря, запереть или блокировать эти виртуальные страницы в реальной памяти. Для этого нужно использовать функцию virtuaiLock, которая имеет следующий прототип: BOOL VirtuaiLock( LPVOID IpAddress, // адрес области SIZE_T dwSize // размер области ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Параметры IpAddress и dwsize указывают,
Глава 21. Работа с виртуальной памятью в Windows 377 соответственно, базовый адрес и размеры области, которая запирается в реальной памяти. Подчеркнем, что в реальной памяти запираются все виртуальные страницы, которые содержат диапазон адресов от ipAddress до lpAddress+dwsize. При этом нельзя запирать в реальной памяти виртуальные страницы, к которым доступ запрещен, т. е. для которых установлен флаг page_noaccess. Кроме того, операционная система не позволяет процессу блокировать в реальной памяти более 30 виртуальных страниц сразу. Для отмены блокировки виртуальных страниц в реальной памяти используется функция virtuaiuniock, которая имеет следующий прототип: BOOL VirtualUnlock( LPVOID IpAddress, // адрес области SIZE_T dwSize // размер области ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Параметры этой функции имеют то же назначение, что и параметры функции virtuaiLock. При этом до вызова ФУНКЦИИ VirtualUnlock диапазон адресов ОТ IpAddress ДО lpAddress+dwsize должен быть предварительно блокирован функцией virtuaiLock. Если это условие не выполняется, то операционная система освобождает страницы из рабочего множества страниц процесса. Необходимо ОТМетИТЬ, ЧТО фуНКЦИИ VirtuaiLock И VirtualUnlock работают только в операционных системах Windows NT/2000. Теперь, в листинге 21.5, приведем программу, которая блокирует, а затем разблокирует страницы в виртуальной памяти. j Листинг 213. Блокирование и разблокирование виртуальных страниц % ^ | #include <windows.h> #include <iostream.h> int main() { LPVOID vm; // указатель на виртуальную память SIZE_T size = 4096; // размер памяти // распределяем виртуальную память vm = VirtualAlloc ( NULL, size, MEM_COMMIT,
378 Часть VI. Работа с виртуальной памятью PAGE_READWRITE); if(!vm) { cout « "Virtual allocation failed." « endl; return GetLastError(); } // блокируем виртуальную память if ( !VirtualLock(vm, size)) { cout « "Virtual lock failed." « endl; return GetLastError(); } // разблокируем виртуальную память if (!VirtualUnlock(vm/ size)) { cout « "Virtual unlock failed." « endl; return GetLastError(); } // освобождаем виртуальную память if (!VirtualFree(vm/ 0, MEM_RELEASE)) { cout « "Memory release failed." « endl; return GetLastError(); } return 0; 21.4. Изменение атрибутов доступа к виртуальной странице Изменить атрибуты доступа к области виртуальной памяти можно при помощи вызова функции virtuaiProtect, которая имеет следующий прототип: BOOL VirtuaiProtect( LPVOID lpAddress, // адрес области памяти SIZE_T dwSize, // размер области памяти
Глава 21. Работа с виртуальной памятью в Windows 379 DWORD flNewProtect, // флаги новых атрибутов доступа PDWORD lpflOldProtect // указатель на старые атрибуты доступа ); В случае успешного завершения эта функция возвращает значение, а в случае неудачи — false. Параметры этой функции имеют тот же смысл, что и при распределении виртуальной памяти функцией virtuaiAiioc. Единственное отличие состоит в том, что старые атрибуты доступа возвращаются функцией virtuaiProtect по адресу lpf loidProtect, который должна установить вызывающая программа. В листинге 21.6 приведена программа, которая изменяет атрибуты доступа к виртуальной странице, разрешая не только чтение этой виртуальной страницы, но и запись. #include <windows.h> #include <iostream.h> int main () { DWORD dwOldProtect; // для старых атрибутов защиты int *a; // указатель на массив целых чисел const int size =1000; // размерность массива // распределяем виртуальную память а = (int*)VirtuaiAiioc( NULL, size * sizeof(int), MEM_COMMIT, PAGE_READONLY); if(!a) { cout « "Virtual allocation failed." « endl; return GetLastError(); } // попробуем записать в виртуальную память _try { а[10] = 10;
380 Часть VI. Работа с виртуальной памятью } except (EXCEPTION_EXECUTE_HANDLER) { DWORD ecode = GetExceptionCode(); if (ecode == EXCEPTION_ACCESS_VIOLATION) cout « "Access to write protected page." « endl; else cout « "Some exception." « endl; } // изменим атрибуты доступа if (!VirtualProtect(a/ size, PAGE_READWRITE, ScdwOldProtect)) { cout « "Virtual protect failed." « endl; return GetLastError(); } // теперь можно писать в виртуальную память а[10] = 10; cout « "а[10] = " « а[10] « endl; // освобождаем виртуальную память if (!VirtualFree(a, 0, MEM_RELEASE)) { cout « "Memory release failed." « endl; return GetLastError(); } return 0; } 21.5. Управление рабочим множеством страниц процесса Узнать о количестве страниц, которые входят в рабочее множество процесса, МОЖНО посредством ВЫЗОВа функции GetProcessWorkingSetSize, КОТОрая имеет следующий прототип: BOOL GetProcessWorkingSetSize( HANDLE hProcess, // дескриптор процесса
Глава 21. Работа с виртуальной памятью в Windows 381 PSIZE_T lpMinWorkingSetSize, // мин. размер рабочего множества PSIZE_T lpMaxWorkingSetSize // макс, размер рабочего множества ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. При вызове этой функции в параметре hProcess должен быть установлен дескриптор процесса, для которого мы хотим узнать диапазон рабочего множества страниц. При успешном завершении функция GetProcessworkingsetsize возвращает по адресам, заданным в параметрах lpMinWorkingSetSize И lpMaxWorkingSetSize, соответственно МИНИМальНЫЙ и максимальный размеры рабочего множества процесса в байтах. При работе процесса менеджер виртуальной памяти поддерживает количество виртуальных страниц, распределенных процессу, в этом диапазоне. Минимальный и максимальный размеры рабочего множества страниц процесса можно изменить посредством вызова функции SetProcessWorkingSetSize, КОТОрая Имеет следующий ПрОТОТИП! BOOL SetProcessWorkingSetSize( HANDLE hProcess, // дескриптор процесса SIZE_T dwMinWorkingSetSize, // мин. размер рабочего множества SIZE_T dwMaxWorkingSetSize // макс, размер рабочего множества ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Как и в функции GetProcessworkingsetsize, параметр hProcess содержит дескриптор процесса, для которого изменяется диапазон рабочего множества страниц. Параметры dwMinWorkingSetSize И dwMaxWorkingSetSize устанавливаются в вызывающей программе. Они указывают на новые минимальный и максимальный размеры рабочего множества процесса в байтах. Если значения обоих этих параметров установлены в -1, то из рабочего множества страниц процесса удаляются все страницы. Отметим, ЧТО функция SetProcessWorkingSetSize ИЗМеНИТ Диапазон рабочего множества страниц процесса только в том случае, если в дескрипторе процесса hProcess установлен режим доступа process_set_quota. Этот режим будет установлен автоматически, если процесс создается администратором. В заключение отметим, ЧТО функции GetProcessworkingsetsize И SetProcessWorkingSetSize работают только в операционных системах Windows NT/2000. Теперь приведем в листинге 21.7 программу, которая определяет размеры рабочего множества страниц процесса, а затем устанавливает новые размеры.
382 Часть VI. Работа с виртуальной памятью # inc lude <windows. h> #include <iostream.h> int main() { const int size = 4096; // размер страницы HANDLE hProcess; // дескриптор процесса SIZE_T min, max; // мин. и макс, размеры рабочего множества страниц SIZE_T *pMin = &min; // указатель на минимальный размер SIZE_T *рМах = &тах; // указатель на максимальный размер // получить дескриптор текущего процесса hProcess = GetCurrentProcess(); // прочитать границы рабочего множества if(!GetProcessWorkingSetSize(hProcess, pMin, pMax)) { cout « "Get process working set size failed." « endl; return GetLastError(); } else { cout « "Min = " « (min/size) « endl; cout « "Max = " « (max/size) « endl; } ' // установить новые границы рабочего множества if(ISetProcessWorkingSetSize(hProcess, min-10, max-10)) { cout « "Set process working set size failed." « endl; return GetLastError(); } // прочитать новые границы рабочего множества if(!GetProcessWorkingSetSize(hProcess, pMin, pMax))
Глава 21. Работа с виртуальной памятью в Windows 383 cout « "Get process working set size failed." « endl; return GetLastError(); } else { cout « "Min = " « (min/size) « endl; cout « "Max = " « (max/size) « endl; } return 0; } 21.6. Инициализация и копирование блоков виртуальной памяти Чтобы заполнить блок памяти определенным значением, используется функция FiliMemory, которая имеет следующий прототип: VOID FiliMemory ( PVOID Destination, // адрес блока памяти SIZE_T Length, // длина блока BYTE Fill // символ-заполнитель ); Эта функция заполняет блок памяти, длина в байтах и базовый адрес которого задаются соответственно параметрами Length И Destination, СИМВОЛОМ, заданным в параметре Fill. Если блок памяти необходимо заполнить нулями, то для этого можно использовать функцию zeroMemory, которая имеет следующий прототип: VOID ZeroMemory ( PVOID Destination, // адрес блока памяти SIZE_T Length, // длина блока ); Параметры этой функции имеют то же назначение, что и параметры функции FiliMemory , исключая символ-заполнитель. Для копирования блока виртуальной памяти используется функция соруметогу, которая имеет следующий прототип: VOID CopyMemory ( PVOID Destination, // адрес области назначения CONST VOID *Source, // адрес исходной области SIZE_T Length // длина блока памяти
384 Часть VI. Работа с виртуальной памятью Эта функция копирует блок памяти, длина в байтах и базовый адрес которого задаются соответственно параметрами Length и source в область памяти по адресу Destination. Отметим, что результат выполнения функции соруметогу непредсказуем, если исходный и результирующий блоки памяти перекрываются. Для копирования перекрывающихся блоков памяти используется функция MoveMemory, которая имеет следующий прототип: VOID MoveMemory ( PVOID Destination, // адрес области назначения CONST VOID *Source, // адрес исходной области SIZE_T Length // длина блока памяти ); Параметры этой функции полностью совпадают с параметрами функции CopyMemory. Теперь в листинге 21.8 приведем программу, которая использует перечисленные выше функции. | Листинг 21.8. Инициализаций и копирование блоков виртуальной памяти #include <windows.h> #include <iostream.h> int mainO { BYTE *a, *b; // указатели на блоки памяти const int size = 1000; // размерность массива // распределяем виртуальную память а = (BYTE*)VirtualAlloc (NULL, size, MEM_COMMIT, PAGE_READWRITE) ; if(!a) { cout « "Virtual allocation failed." << endl; return GetLastError(); } b= (BYTE*)VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_READWRITE); if(!b) { cout « "Virtual allocation failed." « endl; return GetLastError();
Глава 21. Работа с виртуальной памятью в Windows 385 } // инициализируем символом X FillMemory(a, size, 'X'); // копируем блок А в блок В CopyMemory(b, a, size); // распечатываем результат cout « **b[10] = " « b[10] « endl; // освобождаем виртуальную память if (!VirtualFree(a, 0, MEM_RELEASE)) { cout « "Memory release failed." « endl; return GetLastError(); } return 0; } 21.7. Определение состояния памяти Определить состояние области виртуальной памяти процесса можно при помощи вызова функции virtuaiQuery, которая имеет следующий прототип: DWORD VirtuaiQuery( LPCVOID lpAddress, // адрес области PMEMORY_BASIC_INFORMATION lpBuffer, // буфер для информации DWORD dwLength // длина буфера ); Параметр lpAddress указывает на область виртуальной памяти, информацию о которой нужно получить. При этом размер области определяется количеством последовательных виртуальных страниц, которые имеют одинаковые атрибуты. Параметр ipBuf fer указывает на структуру типа memory_basic_information, в которую функция virtuaiQuery поместит информацию об указанной параметром lpAddress области виртуальной памяти. Параметр dwLength должен содержать длину структуры, на которую указывает параметр ipBuf fer. 13 Зак. 514
386 Часть VI. Работа с виртуальной памятью После своего завершения функция virtuaiQuery возвращает действительное количество байтов, записанных в структуру по адресу lpBuf f er. Прежде чем приводить пример программы, использующей функцию virtuaiQuery, рассмотрим более подробно структуру типа memory_basic_ information, которая состоит из следующих полей: typedef struct _MEMORY_BASIC_INFORMATION { PVOID BaseAddress; // базовый адрес области виртуальной памяти PVOID AllocationBase; // базовый адрес распределенной памяти DWORD AllocationProtect; // атрибуты доступа к распределенной памяти SIZE_T Regionsize; // размер области с одинаковыми атрибутами DWORD State/ // состояние памяти DWORD Protect; // атрибуты доступа к области виртуальной памяти DWORD Type; // тип страниц в области виртуальной памяти } MEMORY_BAS IC_INFORMATION, * PMEMORY_BAS IC_INFORMATION ; Значения всех этих полей, исключая последнее поле туре, совпадают со значениями параметров, которые задаются в функциях для работы с виртуальной памятью. Существует различие только в областях памяти, которые описывают эти поля. Различие между значениями полей BaseAddress и AllocationBase заключается в том, что первое указывает на область виртуальной памяти, состояние которой мы хотим узнать, а второе — на область виртуальной памяти, которая была первоначально распределена функцией virtuaiAiioc и содержит интересующую нас область. Такое же различие существует и между назначением полей AllocationProtect И Protect. Поле AllocationProtect содержит атрибуты доступа к области памяти, распределенной функцией virtuaiAiioc, а второе поле — атрибуты доступа области виртуальной памяти, о которой мы хотим получить информацию. Эти поля принимают те же значения, что и параметр flProtect при ВЫЗОВе функции VirtuaiAiioc. Поле state принимает те же значения, что и параметр fiAiiocationType При ВЫЗОВе фунКЦИИ VirtuaiAiioc. Поле туре описывает тип страниц в области виртуальной памяти, информация о которой определяется. Это поле может принимать одно из следующих значений: □ mem_image — исполняемый код; □ mem_mapped — файл, проецируемый в память; □ mem_private — память, принадлежащая процессу. Теперь, в листинге 21.9, приведем программу, которая использует функцию virtuaiQuery для определения состояния области виртуальной памяти.
Глава 21. Работа с виртуальной памятью в Windows 387 #include <windows.h> #include <iostream.h> int main() { BYTE *a, *b; // базовый адрес области и подобласти const int size = 10000; // размер области const int shift = 5000; // смещения для подобласти MEMORY_BASIC_INFORMATION mbi; // структура для информации //о виртуальной памяти DWORD mbi_size = sizeof (MEMORY_BASIC_INFORMATION) ; // распределяем виртуальную память а= (BYTE* )VirtualAlloc (NULL, size, MEM_COMMIT, PAGE_READWRITE) ; if(!a) { cout « "Virtual allocation failed." « endl; return GetLastError(); } // устанавливает адрес подобласти b = a + shift; // определяем информацию о виртуальной памяти if(mbi_size != VirtualQuery(b, &mbi, mbi_size) ) { cout « "Virual query failed." « endl; return GetLastError(); } // распечатьгоаем эту информацию cout « "Base address: " « mbi.BaseAddress « endl; cout « "Allocation base: " « mbi.AllocationBase « endl; cout « "Allocation protect: " « mbi.AllocationProtect « endl; cout « "Region size: " « mbi.RegionSize « endl; cout « "State: " « mbi.State « endl;
388 Часть VI. Работа с виртуальной памятью cout « "Protect: " « mbi.Protect « endl; cout « "Type: " « mbi.Type << endl; // освобождаем виртуальную память if (!VirtualFree(a, 0, MEM_RELEASE)) { cout « "Memory release failed." « endl; return GetLastError(); } return 0; } 21.8. Работа с виртуальной памятью в другом процессе В операционных системах Windows существует набор следующих функций: □ virtuaiAiiocEx — распределить виртуальную память (Windows NT/2000); □ virtuaiFreeEx — освободить виртуальную память (Windows NT/2000); □ virtualProtectEx — изменить атрибуты доступа; □ virtuaiQueryEx — определить состояние области. Эти функции предназначены для работы с виртуальной памятью в адресном пространстве другого процесса. Причем первые две работают только в операционных системах Windows NT/2000. Имена этих функций отличаются от имен рассмотренных нами функций только суффиксом ex. Список параметров этих функций содержит такие же параметры, как и рассмотренные выше, но дополнительным первым параметром является дескриптор процесса, с адресным пространством которого работает функция. В листингах 21.10 и 21.11 приведены программы, которые используют некоторые из этих функций. Это два консольных процесса, которые обмениваются сообщениями через виртуальную память одного из них. В этом случае программа, которая осуществляет доступ к виртуальной памяти другого процесса, ДОЛЖНа ИСПОЛЬЗОВать функции ReadProcessMemory И WriteProcessMemory. Функция ReadProcessMemory, предназначенная для чтения данных из виртуальной памяти другого процесса, имеет следующий прототип: BOOL ReadProcessMemory! HANDLE hProcess, // дескриптор процесса LPCVOID lpBaseAddress, // базовый адрес области
Глава 21. Работа с виртуальной памятью в Windows 389 HANDLE LPCVOID LPVOID DWORD LPDWORD ); hProcess, lpBaseAddress, lpBuffer, nSize, lpNumberOfBytesRead LPVOID lpBuffer, // буфер данных DWORD nSize, // количество считываемых байтов LPDWORD lpNumberOfBytesRead // количество прочитанных байтов ); Функция WriteProcessMemory, предназначенная для записи данных в виртуальную память другого процесса, имеет следующий прототип: BOOL WriteProcessMemory! // дескриптор процесса // базовый адрес области // буфер данных // количество записываемых байтов // количество прочитанных байтов Обе эти функции в случае успешного завершения возвращают ненулевое значение, а в случае неудачи — значение false. Опишем назначение параметров этих функций. Назначение и тип первых двух параметров совпадают для обеих функций, а последние три параметра имеют одинаковый тип, но различаются по смыслу. Параметр hProcess должен содержать дескриптор процесса, к виртуальной памяти которого обращается функция. Параметр lpBaseAddress должен указывать на область виртуальной памяти, к которой обращается функция. Параметр lpBuffer указывает на область памяти, в которую функция ReadProcessMemory будет читать данные из виртуальной памяти другого процесса, а функция WriteProcessMemory записывать данные В виртуальную память другого процесса. Параметр nSize должен содержать количество байтов, которые функция ReadProcessMemory читает из виртуальной памяти другого процесса, а функция WriteProcessMemory записывает в виртуальную память другого процесса. Параметр lpNumberofBytes должен указывать на двойное слово, в которое вызываемая функция ReadProcessMemory ИЛИ WriteProcessMemory ПОМесТИТ соответственно количество прочитанных или записанных байтов. Этот параметр может быть установлен в значение null и тогда вызываемая функция игнорирует его. Теперь, в листингах 21.10 и 21.11, приведем две программы, первая из которых создает консольный процесс, а затем пишет и читает данные из виртуальной памяти этого процесса. Адрес виртуальной памяти, по которому дочерний консольный процесс должен записать ответное сообщение, передается этому процессу через командную строку. Кроме того, отметим, что программы
390 Часть VI. Работа с виртуальной памятью синхронизируют свой доступ к виртуальной памяти, чтобы корректно передать и получить сообщения. #include <windows.h> #include <iostream.h> int main() { char с; // служебный символ char lpszCoramandLine[80] ; // командная строка char send[] = "This is a message."; // строка для пересылки char buffer[80]; // буфер для ответа LPVOID v = (LPVOIDHx00880000; // указатель на область памяти HANDLE hWrite, hRead; // события для синхронизации // записи-чтения в виртуальную память char WriteEvent [] = "WriteEvent"; char ReadEvent[] = "ReadEvent"; // создаем события hWrite = CreateEvent(NULL, FALSE, FALSE, WriteEvent); hRead = CreateEvent(NULL, FALSE, FALSE, ReadEvent); STARTUPINFO si; PROCESS_INFORMATION pi; ZeroMemory(&si, sizeof (STARTUPINFO) ) ; si.cb = sizeof(STARTUPINFO); // формируем командную строку wsprintf(IpszCoinmandLine, "С:\\ConsoleProcess.exe %d", (int)v); // создаем новый консольный процесс if (!CreateProcess(NULL, IpszCoinmandLine, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi)) { cout « "Create process failed." « endl; return GetLastError();
Глава 21. Работа с виртуальной памятью в Windows 391 } // распределяем виртуальную память в этом процессе v = VirtualAllocEx( pi.hProcess, v, sizeof(send), MEM_RESERVE | МШ_С0ММ1Т, PAGE_READWRITE); if(!v) { cout « "Virtual allocation failed." « endl; return GetLastError(); } // записываем в виртуальную память сообщение WriteProcessMemory(pi.hProcess, v, (void*) send, sizeof(send), NULL); /У оповещаем о записи SetEvent(hWrite); // ждем сигнала на чтение WaitForSingleObjectfhRead, INFINITE); // читаем ответ ReadProcessMemory(pi.hProcess, v, (void*)buffer, sizeof(buffer), NULL); // выводим ответ cout « buffer « endl; // освобождаем виртуальную память if (!VirtualFreeEx(pi.hProcess, v, 0, MEM_RELEASE)) { cout « "Memory release failed." « endl; return GetLastError(); } cout « "Input any char to exit: "; cin » c; return 0;
392 Часть VI. Работа с виртуальной памятью #include <windows.h> #include <iostream.h> int main(int argc, char *argv[]) { char c; char answer[] = "This is an answer."; HANDLE hWrite, hRead; // события для синхронизации char WriteEvent [ ] = "WriteEvent"; char ReadEvent [] = "ReadEvent"; char *v; // для адреса виртуальной памяти // открываем события hWrite = OpenEvent (EVENT_MODIFY_STATE/ FALSE, WriteEvent); hRead = OpenEvent (EVENT_MODIFY_STATE/ FALSE, ReadEvent); // преобразуем параметр в адрес v = (char*)atoi(argv[l]); // выводим сообщение cout « v « endl; // ждем разрешения на запись WaitForSingleObject(hWrite, INFINITE); // записываем ответ strcpy(v/ "This is an answer."); // разрешаем чтение SetEvent(hRead); // закрываем дескрипторы CloseHandle(hWrite); CloseHandle(hRead); // ждем команды на завершение cout « "Input any char to exit: "; cin » c; return 0;
Глава 22 Работа с кучей в Windows 22.1. Создание и удаление кучи Кучей или пулом памяти называется распределенная процессом область виртуальной памяти, используемая им для захвата и освобождения блоков памяти, размер которых меньше размера виртуальной страницы. Куча называется сериализуемой, если система синхронизирует доступ параллельно работающих потоков к этой куче. В Windows каждая куча имеет свой дескриптор и, следовательно, является объектом ядра. Для каждого процесса Windows по умолчанию резервирует одну кучу размером в 1 Мбайт и сразу распределяет из нее 4 Кбайт виртуальной памяти для использования процессом. Функции mailoc и free из стандартной библиотеки языка программирования С, а также операторы new и delete языка программирования C++ распределяют память из кучи, зарезервированной для процесса по умолчанию. Дескриптор кучи, созданной для процесса по умолчанию, можно получить при помощи функции GetProcessHeap, которая имеет следующий прототип: HANDLE GetProcessHeap(VOID); В случае успешного завершения эта функция возвращает дескриптор кучи, а в случае неудачи — значение null. Кроме того, процесс может создавать кучи динамически во время своей работы. Динамическое создание куч, как правило, направлено на ускорение работы приложения с динамически распределяемой памятью. Эта цель достигается двумя приемами работы с динамически созданной кучей. Во- первых, динамически созданная куча используется для хранения только однотипных объектов. Так как в этом случае исключается фрагментация кучи, то динамическое распределение памяти выполняется быстрее, чем в обычной куче. Да и память в этом случае используется более экономно. Во- вторых, динамически созданная куча может быть несериализуемой, что также ускоряет работу с этой кучей, т. к. не нужно синхронизировать доступ
394 Часть VI. Работа с виртуальной памятью потоков к такой куче. Но в этом случае кучу может использовать только один поток. Для доступа нескольких потоков к такой куче эти потоки должны выполнять синхронизацию самостоятельно. Для динамического создания кучи используется функция Heapcreate, которая имеет следующий прототип: HANDLE HeapCreate( DWORD flOptions, // атрибуты распределения кучи SIZE_T dwInitialState, // начальное состояние SIZE_T dwMaximumSize // максимальный размер кучи ); В случае успешного завершения эта функция возвращает дескриптор кучи, а в случае неудачи — значение null. Параметр f loptions задает дополнительные атрибуты для создаваемой кучи. Эти атрибуты определяются следующими флагами, которые могут быть установлены в любой комбинации: □ heap_generate_exceptions — в случае ошибки функции, которая работает с кучей, система будет генерировать исключение, а не возвращать null, как она делает это по умолчанию; □ heap_no_serialize — определяет, что куча является сериализуемой (по умолчанию куча не является сериализуемой). Параметр dwinitiaisize задает начальный размер физической памяти, которая распределяется куче. Этот размер задается в байтах и округляется системой в большую сторону до кратности размеру виртуальной страницы. Параметр dwMaximumSize задает максимальный размер кучи в байтах, который округляется системой в большую сторону до кратности размеру виртуальной страницы и не может превышать величины в 0x7fff8 байт. Если параметр dwMaximumSize определяет конкретный размер кучи, то размер кучи не может превысить этот размер. Если же параметр dwMaximumSize установлен в о, то размер кучи ограничен только доступной виртуальной памятью. Для уничтожения кучи нужно использовать функцию HeapDestroy, которая имеет следующий прототип: BOOL HeapDes troy( HANDLE hHeap // дескриптор кучи ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — значение false. Здесь параметр ьнеар задает дескриптор уничтожаемой кучи. Функцию HeapDestroy не нужно применять к куче, созданной для процесса по умолчанию, т. к. в этом случае функция возвратит значение true, но сама куча уничтожена не будет.
Глава 22. Работа с кучей в Windows 395 22.2. Распределение и освобождение памяти из кучи Для распределения памяти из кучи используется функция неарАНос, которая имеет следующий прототип: LPVOID HeapAlloc( HANDLE hHeap, // дескриптор кучи DWORD dwFlags, // управляющие флаги SIZE_T dwBytes // размер распределяемой памяти ); В случае успешного завершения эта функция возвращает адрес распределенной памяти, а в случае неудачи возможны два варианта работы функции. ЕСЛИ флаг HEAP_GENERATE_EXCEPTIONS He установлен, ТО фуНКЦИЯ HeapAlloc в случае неудачи возвращает значение null. Если же этот флаг установлен, то в случае неудачи эта функция генерирует одно из следующих исключений: □ status_no_memory — не хватает памяти или повреждение кучи; □ status_access_violation — повреждение кучи или неправильные параметры функции. Теперь кратко опишем назначение параметров этой функции. В параметре hHeap должен быть установлен дескриптор кучи, из которой распределяется память. Параметр dwFlags управляет режимом работы функции и может быть установлен в любую комбинацию из следующих управляющих флагов: □ heap_generate_exceptions — в случае неудачи функция сгенерирует исключение; □ heap_no_serialize — нет взаимного исключения при доступе к куче; □ heap_zero_memory — распределенная память инициализируется нулями. Параметр dwBytes задает в байтах размер памяти, которая будет распределена из кучи. Если память, распределенная из кучи, больше не используется программой, то ее нужно вернуть обратно в кучу, т. е. — освободить. Для освобождения памяти, распределенной из кучи, используется функция HeapFree, которая имеет следующий прототип: BOOL HeapFree( HANDLE hHeap, // дескриптор кучи DWORD dwFlags, // управляющие флаги LPVOID lpMem // адрес памяти
396 Часть VI. Работа с виртуальной памятью В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — значение false. Параметр пнеар этой функции задает дескриптор кучи, из которой освобождается память, а параметр lpMemory — адрес этой памяти. Параметр dwFiags задает флаги, которые управляют режимом работы функции. В настоящее время в этом параметре может быть установлен только один флаг: □ heap_no_serialize — нет взаимного исключения при доступе к куче. Теперь приведем программы, которые иллюстрируют работу с кучей. Сначала рассмотрим программу из листинга 22.1, которая распределяет, а затем освобождает память из кучи процесса. I ~ ~ ""■" ""* """" ""■" I [ Листинг 22,1, Распределение и освобождение памяти из кучи процесса #include <windows.h> #include <iostream.h> int main(). { HANDLE hHeap; int *a = NULL; // указатель на массив int size = 1000; // размерность массива // получаем дескриптор кучи процесса, созданной по умолчанию hHeap = GetProcessHeap(); if (!hHeap) { cout « «Heap create failed.» « endl; return GetLastError(); } // распределяем память под массив а = (int*)HeapAllос(hHeap, HEAP_ZERO_MEMORY, size * sizeof(int)); // распечатываем один элемент массива cout « <<а[10] = « « а[10] « endl; // освобождаем память из кучи if (!HeapFrее(hHeap, NULL, a)) { cout « «Heap free failed.» « endl;
Глава 22. Работа с кучей в Windows 397 return GetLastError(); } return 0; } Теперь приведем в листинге 22.2 программу, которая выполняет такие же операции, но с кучей, созданной динамически. Листинг 22.2, Распределение и освобождение памяти из кучи, созданной динамически #include <windows.h> #include <iostream.h> int main () { HANDLE hHeap; int *a = NULL; // указатель на массив int h_size = 4096; // размер кучи int a_size =2048; // размер массива // создаем кучу динамически hHeap = HeapCreate(HEAP_NO_SERIALIZE/ h_size, h_size); if (!hHeap) { cout « "Heap create failed." « endl; return GetLastError(); } // распределим память под массив а = (int*)HeapAlloc(hHeap, NULL, a_size); // обрабатываем ошибку в случае неудачи if (!a) { cout « "Heap allocation failed." « endl; return GetLastError(); } // распечатываем распределенную память cout « na[10] = " « a[10] « endl;
398 Часть VI. Работа с виртуальной памятью // освобождаем память из кучи if (!HeapFrее(hHeap, NULL, a)) { cout « "Heap free failed." « endl; return GetLastError(); } // разрушаем кучу if (!HeapDestroy(hHeap)) { cout « "Heap destroy failed." « endl; return GetLastError(); } return 0; } Различие между этими двумя программами состоит в следующем. Во- первых, кучу процесса не нужно создавать и, следовательно, затем уничтожать. Но эти операции необходимо выполнить с кучей, создаваемой динамически. Во-вторых, при распределении памяти из кучи процесса был установлен флаг heap_zero_memory, поэтому распределенная память была инициализирована нулями. При работе же с кучей, созданной динамически, этот флаг не был установлен. Поэтому элементы массива содержат непредсказуемые значения. Теперь рассмотрим программы, которые показывают, как обрабатывать ошибки при работе с кучей в зависимости от того, установлен ли флаг heap_generate_exceptions. Приведенная в листинге 22.3 программа не устанавливает этот флаг и поэтому обрабатывает ошибку нехватки памяти, Проверяя КОД Возврата ИЗ фуНКЦИИ НеарАНос. р. , .^^..^...^му,.^. p^j^.w-.^M.vv.vfj^.w w..w „ v...„.w..,^..,.,.v„.v...v v v v...v..„ ~..^, ~ , [ Листинг 22.3» Обработка ошибки при распределении памяти проверкой кода 1 возврата ^; ? #include <windows.h> #include <iostream.h> int main() { HANDLE hHeap; int *a = NULL; // указатель на массив
Глава 22. Работа с кучей в Windows 399 int size =4096; // размер массива, а также кучи // создаем кучу динамически hHeap = HeapCreate(HEAP_NO_SERIALIZE, size, size); if (!hHeap) { cout « "Heap create failed." « endl; return GetLastError(); } // пытаемся распределить память под массив а = (int*)HeapAlloc(hHeap, NULL, size * sizeof(int)); // обрабатываем ошибку в случае неудачи if (!a) { cout « "Heap allocation failed." « endl; return GetLastError(); } // разрушаем кучу if (!HeapDestroy(hHeap)) { cout « "Heap destroy failed." « endl; return GetLastError(); } return 0; } Теперь в листинге 22.4 приведем программу, которая устанавливает флаг heap_generate_exceptions и поэтому ошибка нехватки памяти обрабатывается, используя структурную обработку исключений. #include <windows.h> #include <iostream.h> int main()
400 Часть VI. Работа с виртуальной памятью HANDLE hHeap; int *a = NULL; // указатель на массив int size = 4096; // размер массива, а также кучи // создаем кучу динамически hHeap = HeapCreate(HEAP_NO_SERIALIZE | HEAP_GENERATE_EXCEPTIONS, size, size); if (!hHeap) { cout « "Heap create failed." « endl; return GetLastError(); // пытаемся распределить память из кучи —try а = (int*)НеарА11ос(hHeap, NULL, size * sizeof(int)); except (EXCEPTION_EXECUTE_HANDLER) DWORD ecode = GetExceptionCode(); if (ecode == STATUS_NO_MEMORY) cout « "STATUS_NO_MEMORY exception." « endl; else cout « "Some exception." « endl; } // разрушаем кучу if (!HeapDestroy(hHeap)) { cout « "Heap destroy failed." « endl; return GetLastError(); } return 0;
Глава 22. Работа с кучей в Windows 401 22.3. Перераспределение памяти из кучи Для перераспределения памяти в куче используется функция HeapReAiioc, которая имеет следующий прототип: LPVOID HeapReAiioc( HANDLE hHeap, // дескриптор кучи DWORD dwFlags, // управляющие флаги LPVOID lpMem, // адрес перераспределяемой памяти SIZE_T dwBytes // новый размер блока памяти в байтах ); Она, как и функция неарАНос, в случае успешного завершения возвращает адрес перераспределенной памяти, а в случае неудачи ведет себя в зависимости от того, установлен ли флаг heap_generate_exceptions. Если этот флаг не установлен, то в случае неудачи эта функция возвращает значение null. В противном случае эта функция генерирует одно из таких же исключений, как И фуНКЦИЯ НеарАНос: □ status_no_memory — не хватает памяти или повреждение кучи; □ status_access_violation — повреждение кучи или неправильные параметры функции. Параметр hHeap функции HeapReAiioc должен указывать на дескриптор кучи, из которой распределяется память. Параметр dwFlags управляет режимом работы функции и может быть установлен в любую комбинацию из следующих флагов: □ heap_generate_exceptions — в случае неудачи функция сгенерирует исключение; □ heap_no_serialize — нет взаимного исключения при доступе к куче; □ heap_realloc_in_place_only — перераспределяемый блок памяти передвигать на новое место нельзя, по умолчанию блок может быть переписан на новое место; □ heap_zero_memory — дополнительная распределенная память инициализируется нулями. Параметр lpMem задает адрес памяти, которая перераспределяется вызовом функции HeapReAiioc. Этот адрес должен быть предварительно возвращен ОДНОЙ ИЗ функций HeapAlloc ИЛИ HeapReAiioc. Параметр dwBytes задает размер памяти в байтах, которая будет перераспределена из кучи. Теперь приведем в листинге 22.5 программу, в которой перераспределяется память из кучи. Отметим, что в этой программе при первом распределении
402 Часть VI. Работа с виртуальной памятью память не инициализируется, а при перераспределении добавленная память инициализируется нулями. #include <windows.h> #include <iostream.h> int mainO { HANDLE hHeap; int *a = NULL; // указатель на массив int h_size =4096; // размер кучи int a_size =5; // размер массива // создаем кучу динамически hHeap = HeapCreate(HEAP_NO_SERIALIZE | HEAP_GENERATE_EXCEPTIONS, h_size, 0); if (!hHeap) { cout « "Heap create failed." « endl; return GetLastError(); } // распределяем память из кучи а = (int*)НеарА11ос(hHeap, NULL, a_size * sizeof(int)); // инициализируем и распечатываем массив for (int i = 0; i < a_size; i++) { a[i] = i; cout « "a[" « i «"]="« a[i] « endl; } // распределяем дополнительный блок a = (int*)HeapReAlloc(hHeap, HEAP_ZERO_MEMORY, a, 2 * a_size * sizeof(int)); // распечатываем элементы массива for (i = 0; i < 2 * a_size; i++) cout « "\ta[" « i « "] = " « a[i] « endl; // разрушаем кучу < if (!HeapDestroy(hHeap))
Глава 22. Работа с кучей в Windows 403 { cout « "Heap destroy failed." « endl; return GetLastError(); } return 0; } 22.4. Блокирование и разблокирование кучи Если куча не является сериализуемой, то параллельный доступ нескольких потоков к этой куче может нарушить ее непротиворечивое состояние и вызвать ошибку в работе приложения. Чтобы избежать такой ситуации, в операционных системах Windows предусмотрены функции HeapLock и HeapUnlock, которые блокируют параллельный доступ нескольких потоков к куче. Для того чтобы получить монопольный доступ к куче, поток должен вызвать функцию HeapLock, которая имеет следующий прототип: BOOL HeapLock (HANDLE hHeap); В случае успешного завершения эта функция блокирует доступ остальных потоков к куче и возвращает ненулевое значение, а в случае неудачи возвращает значение false. Единственным параметром этой функции является дескриптор кучи, к которой поток хочет получить монопольный доступ. Если куча заблокирована потоком при помощи функции HeapLock и другой поток вызывает какую-нибудь функцию для доступа к этой куче, то система переведет его в состояние ожидания до тех пор, пока поток, вызвавший функцию HeapLock, не вызовет функцию HeapUnlock, которая имеет следующий прототип: BOOL HeapUnlock(HANDLE hHeap); В случае успешного завершения эта функция разблокирует кучу и возвращает ненулевое значение, а в случае неудачи куча остается заблокированной и функция возвращает значение false. В заключение заметим, что эти функции поддерживаются только операционными системами Windows NT/2000. Теперь приведем, в листинге 22.6, программу, которая вызывает функции HeapLock и HeapUnlock для блокирования и разблокирования созданной динамически кучи. В связи с этой программой отметим, что блокирование и разблокирование кучи можно выполнить только в том случае, если куча является сериализуемой.
404 Часть VI. Работа с виртуальной памятью кучи #include <windows.h> #include <iostream.h> HANDLE hHeap; DWORD WINAPI thread (LPVOID) { int *a; // бесконечный цикл распределения и освобождения памяти из кучи for ( ; ; ) { // распределяем память а = (int*)НеарА11ос(hHeap, NULL, sizeof(int)); cout « "\tHeap allocated." « endl; // освобождаем память HeapFrее(hHeap, NULL, a); cout « "\tHeap freed." « endl; // немного подождем SleepB000); } } int main () { HANDLE hThread; DWORD IDThread; int size = 4096; // размер кучи char с; // служебный символ // создаем кучу динамически hHeap = HeapCreate(NULL, size, 0) ; if (!hHeap) { cout « "Heap create failed." « endl; return GetLastError();
Глава 22. Работа с кучей в Windows 405 // напечатаем как управлять программой cout « "Input " « endl; cout « "\tl - to lock the heap." « endl; cout « "\tu - to unlock the heap." « endl; cout « "\te - to exit from the process." « endl « endl; // ждем ввод символа для продолжения выполнения программы cout « "Now input any char to continue: "; cin » c; // запускаем поток, работающий с кучей hThread = CreateThread(NULL, 0, thread, NULL, NULL, &IDThread); if (!hThread) { cout « "Create thread failed." « endl; return GetLastError(); } // блокируем/разблокируем кучу for ( ; ; ) { cin » с; switch (с) { case '1': if (!HeapLock(hHeap)) { cout « "Heap lock failed." « endl; return GetLastError(); } cout « "Heap locked." « endl; break; case 'u': if (!HeapUnlock(hHeap)) { cout « "Heap unlock failed." « endl; return GetLastError(); } cout « "Heap unlocked." « endl; break;
406 Часть VI. Работа с виртуальной памятью case 'е': // разрушаем кучу if (!HeapDestroy(hHeap)) { cout « "Heap destroy failed." « endl; return GetLastError(); } // прерываем выполнение потока thread TerminateThread(hThread/ 0); // закрываем дескриптор потока CloseHandle(hThread); // выводим сообщение о завершении работы cout « "Exit from the process." « endl; return 0; } } } 22.5. Проверка состояния кучи Чтобы получить информацию о состоянии кучи или отдельного блока виртуальной памяти, распределенного из кучи, можно использовать функцию Heap validate, которая имеет следующий прототип: BOOL HeapValidate( HANDLE hHeap, // дескриптор кучи DWORD dwFlags, // управляющие флаги LPCVOID lpMem // адрес блока памяти ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — значение false. Успешное завершение этой функции означает, что куча или проверяемый блок памяти из кучи находятся в непротиворечивом состоянии. То есть адрес и длина отдельного блока или всех блоков в структурах, управляющих распределением памяти из кучи, соответствуют действительным значениям этих величин. Также отметим, что в случае неудачи функция Heap validate не устанавливает дополнительную информацию об ошибке, которую можно получить, используя функцию GetLastError. Параметр hHeap этой функции указывает на дескриптор кучи, состояние Которой ДОЛЖНа определить фуНКЦИЯ HeapValidate.
Глава 22. Работа с кучей в Windows 407 Параметр dwFiags ,может принимать значение heap_no_serialize, которое говорит о том, что нет взаимного исключения при доступе к куче. Если это значение установлено, то во время работы функции Heapvaiidate доступ других потоков к куче блокируется. В противном случае блокировка одновременного доступа потоков к куче не происходит. Параметр lpMem должен указывать на блок памяти, который был распределен из кучи и состояние которого проверяется. Если значение этого параметра равно null, то проверяется вся куча. Отметим, что функция Heapvaiidate работает только в операционных системах Windows NT/2000. Теперь приведем в листинге 22.7 программу, которая проверяет состояние блока памяти, распределенного из кучи. [ Листинг 22.7. Проварка состояния блока кучи * #include <windows.h> #include <iostream.h> int main() { HANDLE hHeap; int *a = NULL; // указатель на массив int size = 4096; // размер кучи // создаем кучу динамически hHeap = HeapCreate(HEAP_NO_SERIALIZE/ size, 0); if (!hHeap) { cout « "Heap create failed." « endl; return GetLastError(); } // распределяем память под массивы а = (int*)НеарА11ос(hHeap, NULL, 4 * sizeof(int)); // проверяем состояние распределенного блока памяти if (!Heapvaiidate(hHeap, HEAP_NO_SERIALIZE, a)) cout « "The block is bad." « endl; else cout « "The block is good." « endl; // разрушаем кучу
408 Часть VI. Работа с виртуальной памятью HeapDestroy(hHeap) ; return 0; } Для получения более подробной информации о состоянии кучи нужно использовать функцию Heapwaik, которая записывает информацию о блоках виртуальной памяти из кучи в структуру типа: typedef struct _PROCESS_HEAP_ENTRY { PVOID lpData, // адрес элемента данных DWORD cbData, // длина элемента данных в байтах BYTE cbOverhead, // длина данных, описывающих элемент данных BYTE iRegionIndex, // индекс области, содержащей элемент данных WORD wFiags, // управляющие флаги union { struct { HANDLE hMem, // дескриптор распределенного блока памяти DWORD dwReserved[3]; //не используется } Block; struct { DWORD dwCommittedSize; // длина распределенной памяти DWORD dwUnCommittedSize; // длина свободной памяти LPVOID lpFirstBlock; // адрес первого занятого блока LPVOID lpLastBlock; // адрес первого свободного блока } Region; } } PROCESS_HEAP_ENTRY, *LPPROCESS_HEAP_ENTRY; Прежде чем описать назначение полей этой структуры, рассмотрим поле wFiags, от возможных значений которого зависит назначение других полей этой структуры. Поле wFiags может принимать следующие значения: □ process_heap_region — элемент кучи находится в начале непрерывной области виртуальной памяти; □ process_heap_uncommitted_range — элемент кучи находится в области нераспределенной виртуальной памяти, которая зарезервирована для кучи; □ process_heap_entry_busy — элемент кучи является распределенным блоком памяти.
Глава 22. Работа с кучей в Windows 409 Кроме того, совместно с флагом process_heap_entry_busy могут использоваться следующие флаги: □ process_heap_entry_moveable — блок памяти является перемещаемым; □ process_heap_entry_ddeshare — блок памяти разделяется протоколом DDE. Теперь рассмотрим назначение полей IpData, cbData, cboverhead структуры ТИПа PROCESS_HEAP_ENTRY. Если в поле wFiags установлено значение process_heap_region, to содержимое этих полей интерпретируется следующим образом: □ поле ipData указывает на первый виртуальный адрес, используемый в области виртуальной памяти; □ поле cbData содержит общий размер в байтах области виртуальной памяти; □ поле cboverhead содержит размер в байтах управляющей структуры, которая описывает область виртуальной памяти. Если же в поле wFiags установлено значение process_heap_uncommitted_ range, то содержимое этих полей интерпретируется следующим образом: □ поле ipData указывает на начальный виртуальный адрес нераспределенной области виртуальной памяти; □ поле cbData содержит общий размер в байтах нераспределенной области виртуальной памяти; □ поле cboverhead содержит размер в байтах управляющей структуры, которая описывает нераспределенную область виртуальной памяти. Анонимное объединение, которое расположено после поля wFiags, содержит две перекрывающиеся структуры типов Block и Region. Если в поле wFiags установлены значения process_heap_entry_busy и process_heap_ entry_moveable, to анонимное объединение содержит структуру Block. Поля этой структуры описывают следующие значения: □ поле hMem содержит дескриптор распределенного и перемещаемого блока виртуальной памяти; □ поле dwReserved является зарезервированным и не используется. Если же в поле wFiags установлено значение process_heap_region, to объединение содержит структуру Region, поля которой описывают следующие значения: □ поле dwcommittedsize содержит длину блока или длину управляющей структуры, которая описывает блок; □ поле dwuncommittedsize содержит длину блока, который не распределен процессу; □ поле lpFirstBiock указывает на первый занятый блок памяти в куче; □ поле lpLastBiock указывает на первый неиспользуемый блок памяти в куче.
410 Часть VI. Работа с виртуальной памятью Теперь можно перейти к рассмотрению функции Heapwalk, которая позволяет последовательно просматривать информацию о блоках памяти в куче при помощи структур типа process_heap_entry. Эта функция имеет следующий прототип: BOOL HeapWalk( HANDLE hHeap, // дескриптор кучи LPPROCESS_HEAP_ENTRY IpEntry // адрес структуры PROCESS_HEAP_ENTRY ); В случае удачного завершения функция Heapwalk возвращает значение true и записывает состояние элемента кучи в структуру process_heap_entry, адрес которой задается параметром IpEntry. В случае неудачи функция возвращает значение false. Чтобы начать просмотр элементов кучи, нужно установить значение поля lpData в структуре process_heap_entry в значение null. Для продолжения просмотра состояния элементов кучи нужно последовательно вызывать функцию Heapwalk, не изменяя значения параметров hHeap, IpEntry и содержимого полей структуры process_heap_entry. При вызове функции после просмотра последнего элемента кучи функция Heapwalk возвращает значение false и последующий вызов функции GetLastError вернет значение ERROR_NO_MORE_ITEMS. Теперь приведем в листинге 22.8 программу, которая проверяет состояние Элементов КуЧИ, ИСПОЛЬЗУЯ ДЛЯ ЭТОГО фуНКЦИЮ HeapWalk. НЩ?Ш1Ж^^^^^в^^^ 7 ~| #include <windows.h> #include <iostream.h> int main () { HANDLE hHeap; int size =4096; // размер кучи int *a = NULL, *b = NULL; // указатели на массивы PROCESS_HEAP_ENTRY phe; // состояние элемента кучи // создаем кучу динамически hHeap = HeapCreate(HEAP_NO_SERIALIZE/ size, 0); // распределяем память под массивы а = (int*)HeapAlloc(hHeap, NULL, 4 * sizeof(int));
Глава 22. Работа с кучей в Windows 411 Ъ = (int*)HeapAlloc(hHeap, NULL, 16 * sizeof(int)); // инициализируем цикл проверки состояния кучи phe.lpData = NULL; // проверяем состояние элементов кучи while (HeapWalk(hHeap, &phe)); { if (phe.wFlags & PROCESS_HEAP_REGION) cout « "PROCESS_HEAP_REGION flag is set." « endl; if (phe.wFlags & PROCESS_HEAP_UNCOMMITTED_RANGE) cout « "PROCESS_HEAP_UNCOMMITTED_RANGE flag is set" « endl; cout « "lpData = " « phe.lpData « endl; cout « "cbData = " « phe.cbData « endl; cout « endl; } // разрушаем кучу HeapDestroy(hHeap); return 0; } 22.6. Уплотнение кучи Если при работе с кучей распределяются и освобождаются блоки памяти разной длины, то со временем происходит фрагментация кучи, т. е. внутри кучи образуются свободные блоки. Это происходит потому, что длина свободного блока в куче не всегда соответствует длине распределяемого блока памяти. Фрагментация кучи приводит к двум последствиям. Во-первых, к неэффективному использованию памяти, т. к. внутри кучи находятся свободные неиспользуемые блоки виртуальной памяти, которые часто не могут быть использованы для распределения памяти из кучи из-за своей малой длины. Во-вторых, к более медленной работе с кучей, т. к. для распределения блока памяти требуется просмотр списка свободных блоков кучи. Чтобы частично избежать этих последствий, необходимо выполнить уплотнение кучи, которое заключается в соединении последовательных свободных блоков памяти в куче в один блок большей длины и освобождении больших блоков свободной виртуальной памяти. Для уплотнения кучи используется функция HeapCompact, которая имеет следующий прототип: UINT HeapCompact( HANDLE hHeap, // дескриптор кучи
412 Часть VI. Работа с виртуальной памятью DWORD dwFlags // управляющие флаги ); В случае успешного завершения эта функция возвращает длину наибольшего свободного блока памяти в куче (в байтах), а в случае неудачи — ноль. Параметр hHeap указывает на дескриптор кучи, которую нужно уплотнить, а в параметре dwFlags может быть установлено значение: □ heap_no_serialize — нет взаимного исключения при доступе к куче. Если это значение установлено, то во время работы функции Heapcompact доступ других потоков к куче блокируется. В противном случае блокировка одновременного доступа потоков к куче не происходит. В заключение отметим, что эта функция работает только в операционных системах Windows NT/2000. В листинге 22.9 приведена программа, которая уплотняет кучу, используя ДЛЯ ЭТОГО фуНКЦИЮ HeapCompact. | Яшщ^&Я* Уплотнение щнп # include <windows.h> #include <iostream.h> int main () { HANDLE hHeap; int int int *a = NULL *b = NULL *c = NULL // указатель на массив // указатель на массив // указатель на массив int size = 4096; // размер кучи UINT free; // длина свободной памяти после уплотнения // создаем кучу динамически hHeap = HeapCreate(HEAP_NO_SERIALIZE/ size, size); if (!hHeap) { cout « "Heap create failed." « endl; return GetLastError(); } // распределяем память под массивы а = (int*)HeapAlloc(hHeap, NULL, 1024); cout « "a = " « a « endl; b= (int*)HeapAlloc(hHeap, NULL, 1024);
Глава 22. Работа с кучей в Windows 413 cout « "b = " « b « endl; с = (int*)HeapAlloc (hHeap, NULL, 1024); cout « "c = " « с « endl « endl; // проверяем, сколько осталось свободного места в куче free = HeapCompact(hHeap, HEAP_NO_SERIALIZE); if (!free) { cout « "Heap compact failed." « endl; return GetLastError(); } else cout « "Free: " « free « endl; // освобождаем первых два массива HeapFree(hHeap, NULL, b); HeapFrее(hHeap, NULL, a); // уплотняем кучу free = HeapCompact(hHeap, HEAP_NO_SERIALIZE) ; if (!free) { cout « "Heap compact failed." « endl; return GetLastError(); } else cout « "Free: " « free « endl; // разрушаем кучу HeapDestroy(hHeap); return 0;
Часть VII Управление файлами Глава 23. Общие концепции Глава 24. Работа с файлами в Windows Глава 25. Работа с каталогами (папками) в Windows
Глава 23 Общие концепции 23.1. Накопители на жестких магнитных дисках Схематически накопитель на жестких магнитных дисках (НЖМД) с перемещаемыми головками показан на рис. 23.1. Основными элементами НЖМД являются круглые алюминиевые или некристаллические стекловидные пластины. Эти пластины нельзя согнуть и поэтому они называются жесткими дисками. Жесткие диски покрыты слоем ферромагнитного материала, который позволяет хранить информацию, используя направление магнитного поля. Жесткие диски также называются жесткими магнитными дисками. Жесткие магнитные диски закреплены на стержне, который вращается с большой скоростью. Данные записываются на поверхностях жестких магнитных дисков с помощью магнитных головок, которые расположены над каждой дисковой поверхностью. Магнитной головке доступны только те данные, которые находятся на участке дисковой поверхности под или над ней. Все магнитные головки закреплены на одном стержне, который, совершая вращательные движения, перемещает головки по радиусам магнитных дисков в обоих направлениях. Если магнитная головка не перемещается, то она описывает на дисковой поверхности окружность, которая называется дорожкой. Дорожки нумеруются от 0 до п, где дорожка с индексом 0 имеет наибольший радиус. Группа дорожек, находящихся под всеми магнитными головками в каком-то конкретном положении стержня с магнитными головками, называется цилиндром. Теперь кратко поясним, как выполняются операции чтения и записи данных на магнитные диски. Но прежде дадим определение термину доступ к данным — под ним будем понимать операции записи данных на магнитные диски и чтения данных с магнитных дисков. Чтобы получить доступ к данным на магнитном диске, необходимо выполнить следующие операции: □ установить магнитные головки на соответствующий цилиндр; 14 Зак. 514
418 Часть VII. Управление файлами П дождаться, пока под магнитной головкой окажется точка на вращающемся магнитном диске, с которой начинаются данные; □ прочитать или записать данные на магнитный диск во время его вращения. & Ее Гх жесткие диски стержень магнитные головки стержень Рис. 23.1. Схема накопителя на жестких магнитных дисках Так как каждая из этих операций связана с механическим движением, то общее время доступа к данным на магнитном диске достаточно велико по сравнению со скоростью работы интегральных схем. 23.2. Секторы и кластеры Сектором называется наименьшая область (дуга) одной дорожки магнитного диска, которая может быть записана или считана магнитной головкой диска за его один полный поворот. Размер сектора равен 512 байт. Обычно плотность записи данных на диск является одинаковой для всех дорожек. Поэтому дорожки, находящиеся ближе к центру диска, содержат меньше секторов, чем дорожки, находящиеся ближе к краю диска. Одна дорожка жесткого диска может содержать от 380 до 700 секторов. Секторы каждой дорожки перенумерованы, причем эта нумерация начинается с 1. В начале каждого сектора хранится заголовок или префикс, который определяет начало и номер сектора. В конце каждого сектора хранится заключение или суффикс, который содержит контрольную сумму, необходимую для проверки целостности данных. Не так давно нумерация секторов одной дорожки не являлась последовательной. Секторы нумеровались через один сектор. Это было сделано для того, чтобы контроллер дискового устройства при доступе к жесткому магнитному диску не пропускал секторы при вращении. В настоящее время скорость работы контроллеров жестких дисков сравнима со скоростью
Глава 23. Общие концепции 419 вращения диска, поэтому секторы каждой дорожки нумеруются последовательно. Так как дорожки содержат разное количество секторов, то скорость обмена данными с накопителем на жестких магнитных дисках зависит от номера дорожки. Это вызвано тем, что при постоянной угловой скорости вращения стержня с дисками линейная скорость перемещения секторов относительно головки различается для разных дорожек. Эта скорость тем выше, чем дальше дорожка находится от центра диска. Поэтому диск разбивают на зоны. Одна зона содержит несколько цилиндров диска и на каждой из дорожек, входящих в одну зону, находится одинаковое количество секторов. Поэтому доступ к секторам одной зоны выполняется с одинаковой скоростью. Теперь перейдем к кластерам. Кластером называется наименьшая область магнитного диска, которая может быть записана или прочитана операционной системой на диск. Обычно кластер состоит из нескольких секторов, имеющих последовательные номера. 23.3. Форматирование дисков Прежде чем использовать жесткий магнитный диск для хранения информации, он должен быть отформатирован. Существуют три уровня форматирования жесткого магнитного диска: □ физическое форматирование или форматирование низкого уровня; П разбиение диска на разделы, □ логическое форматирование или форматирование высокого уровня. Форматирование низкого уровня (или низкоуровневое) заключается в разбиении дорожек на секторы. При этом формируются префикс, область данных и суффикс каждого сектора, а также интервалы между секторами и дорожками. Область данных сектора заполняется фиктивными данными или специальными тестовыми наборами данных. При разбиении диска на разделы диск разбивается на области, которые также называются разделами или логическими дисками. В каждом разделе может быть установлена своя файловая система. В настоящее время операционные системы Windows используют файловые системы FAT16, FAT32 и NTFS. При форматировании высокого уровня операционная система записывает в раздел структуры данных, необходимые для работы с файлами. В каждый раздел записывается загрузочный сектор (Boot Sector), который используется для загрузки операционной системы, таблицы размещения файлов и корневой каталог (Root Directory).
420 Часть VII. Управление файлами 23.4. Функции файловой системы Теперь перейдем к описанию файлов. Логической записью или структурой называется упорядоченное множество данных разных типов. Порядок следования этих данных называется структурой записи. На уровне прикладной программы файл представляет собой множество логических записей. На физическом уровне файл представляет собой поименованное множество секторов или кластеров, хранящихся на диске. Так как длина логической записи обычно не совпадает с длиной кластера, то кластер может содержать несколько логических записей или, наоборот, логическая запись может располагаться на нескольких кластерах. Часть операционной системы, которая обеспечивает доступ к файлам и выполняет связывание между логическими записями файла и их физическим представлением, называется системой управления файлами или файловой системой. Для того чтобы выполнять операции доступа к логическим записям файла, с каждым файлом связывают указатель файла, который указывает на текущую логическую запись файла. После каждой операции записи или чтения логической записи файловая система передвигает указатель файла на следующую логическую запись. Для обеспечения доступа к файлам система управления файлами должна выполнять, по крайней мере, следующие функции: □ создание файла; □ удаление файла; □ открытие доступа к существующему файлу; □ закрытие доступа к существующему файлу; □ запись данных в файл; □ чтение данных из файла; □ установка указателя файла на нужную запись. 23.5. Каталоги Каталогом называется файл, который содержит имена и местонахождение других файлов. Каталоги имеют древовидную структуру, в которой каждая вершина указывает на каталог, а каждый лист — на файл. Корень этой структуры называется корневым каталогом, который, как уже было сказано, создается системой на этапе форматирования высокого уровня и обычно обозначается символом \ (обратная косая). Файловая система обеспечивает следующие функции для работы с каталогами: □ создание каталога; □ удаление каталога;
Глава 23. Общие концепции 421 П включение подкаталога в каталог; □ исключение подкаталога из каталога; □ включение файла в каталог; □ исключение файла из каталога. 23.6. Буферизация ввода-вывода Буфером ввода-вывода называется область оперативной памяти, предназначенная для временного хранения записей файла. Обычно длина буфера выбирается кратной длине кластера. Буферы ввода-вывода предназначены для решения двух задач: □ устранение несоответствия между размером логической записи файла, определяемым в приложении, и размером кластера, который записывается на диск; □ снижение влияния внешних устройств на скорость работы процессора, которая значительно превышает скорость работы внешних устройств. Для решения этих задач при выводе данных файловая система сначала полностью заполняет буфер логическими записями, а затем дает команду внешнему устройству на запись данных на диск. При вводе данных система управления файлами сначала заполняет буфер кластерами, а затем управляет чтением логических записей из буфера в программу пользователя. Для ускорения ввода-вывода данных обычно используется несколько буферов ввода-вывода, которые организованы в кольцевую очередь. Во время работы пользовательского процесса с одним буфером файловая система параллельно осуществляет ввод или вывод данных в другие буферы. 23.7. Кэширование ввода-вывода Рассмотрим два соседних уровня иерархии памяти, которые показаны на рис. 23.2. При этом предположим, что память верхнего уровня работает быстрее, чем память нижнего уровня. Считаем, что нужные данные хранятся в памяти нижнего уровня, а механизм управления памятью обеспечивает передачу на верхний уровень тех данных, к которым наиболее вероятно обращение программы пользователя. В таких случаях говорят, что верхний уровень памяти работает как кэш по отношению к нижнему уровню памяти. Вначале этот термин применялся только к памяти самого высокого уровня, которая располагается между регистрами процессора и оперативной памятью. В настоящее время этот термин используется в более широком смысле для обозначения упреждающего ввода данных.
422 Часть VII. Управление файлами Быстрая память ~т~ Медленная память Рис. 23.2. Иерархия памяти Кэширование ввода данных подразумевает, что система выполняет упреждающее чтение данных с магнитного диска без ожидания следующей команды на чтение данных из приложения. Это сокращает время на чтение записей файла, если они читаются приложением последовательно. В операционной системе Windows 2000 кэшированием ввода данных с диска занимается специальная программа ядра операционной системы, которая называется менеджером кэширования (Cash Manager).
Глава 24 Работа с файлами в Windows 24.1. Именование файлов в Windows В операционных системах Windows полное имя файла представляется строкой, которая заканчивается пустым символом. Причем длина такой строки не может превышать мах_ратн символов. Файловые системы FAT32 и NTFS поддерживают полные имена файлов длиной до 255 символов. Такие имена называются длинными. Файловая система FAT 16, которая использовалась в операционной системе MS-DOS, поддерживает файловые имена длиной до 8 символов плюс 3 символа на расширение файла. Полное имя файла состоит из компонент (подстрок), каждая из которых разделяется символом \ (обратная косая). Каждая не последняя компонента полного имени файла задает имя каталога, в котором находится файл, а последняя компонента задает имя самого файла. Поэтому полное имя файла также часто называют путем к файлу, т. к. полное имя файла фактически описывает путь по дереву каталогов к его листу, который и представляет сам файл. При формировании пути к файлу нужно придерживаться определенных правил, которые перечислены ниже: □ имена каталогов и файлов не должны содержать символов, ASCII-коды которых находятся в диапазоне от 0 до 31 (это служебные символы); □ имена каталогов и файлов не должны содержать символы <,>,:,",/, \ и |; □ имена каталогов и файлов могут содержать символы из расширенного множества, которое включает символы с кодами от 128 до 255; □ для обозначения текущего каталога в качестве компоненты пути используется символ . (точка); □ для обозначения родительского каталога для текущего каталога в качестве компоненты пути используются символы . . (две точки); □ в качестве компонент пути нельзя использовать имена устройств, как, например, aux, con, lpti и pm.
424 Часть VII. Управление файлами Кроме того, отметим, что имена файлов нечувствительны к регистрам клавиатуры. То есть файловая система не различает имена файлов, которые отличаются только прописным или заглавным написанием букв. Например, имена Demo, demo и demo неразличимы. 24.2. Создание и открытие файлов Для создания новых или открытия уже существующих файлов используется функция createFile, которая имеет следующий прототип: HANDLE CreateFile( LPCTSTR lpFileName, // имя файла DWORD dwDesiredAccess, // способ доступа DWORD dwShareMode, // режимы совместного использования LPSECUTITY_ATTRIBUTES IpSecurutyAttributes, // атрибуты защиты DWORD dwCreationDisposition, // создание или открытие файла DWORD dwFlagsAndAttributes, // флаги и атрибуты HANDLE hTemplateFile // файл атрибутов ); В случае успешного завершения функция возвращает дескриптор созданного или открытого файла, а в случае неудачи — значение invalid_handle_value. В параметре lpFiieName задается указатель на символьную строку, которая содержит полное имя создаваемого или открываемого файла. Если полное имя файла не указано, то файл с заданным именем создается или ищется в текущем каталоге. Параметр dwDesiredAccess задает способ доступа к файлу и может принимать любую комбинацию следующих значений: □ о — приложение может только определять атрибуты устройства; □ generic_read — допускается только чтение данных из файла; □ generic_write — допускается только запись данных в файл. Это общие или родовые режимы доступа к файлу. Существуют также и другие режимы, значения которых зависят от доступа, заданного при определении атрибутов защиты файла. Эти режимы будут рассмотрены в разд. 45.5, посвященном защите файлов. Пока же будем считать, что файл получает атрибуты защиты по умолчанию, что разрешает выполнять над ним все существующие операции. Параметр dwShareMode задает режимы совместного доступа к файлу. Если значение этого параметра равно нулю, то файл не может использоваться для совместного доступа. Иначе параметр dwShareMode может принимать любую комбинацию следующих значений: □ file_share_read — файл может использоваться только для совместного чтения несколькими программами;
Глава 24. Работа с файлами в Windows 425 П file_share_write — файл может использоваться только для совместной записи несколькими программами; П file_share_delete — файл может использоваться несколькими про- фаммами при условии, что каждая из них имеет разрешение на удаление этого файла. Отметим, что последнее значение может использоваться только в операционных системах Windows NT/2000. Параметр ipsecurutyAttributes должен задавать атрибуты защиты файла. Пока этот параметр будем устанавливать в null. Это означает, что атрибуты защиты файла устанавливаются по умолчанию, т. е. дескриптор файла не является наследуемым и файл открыт для доступа всем пользователям. Параметр dwCreationDisposition задает действия, которые нужно выполнить при создании или открытии файла. Этот параметр может принимать одно из следующих значений: □ create_new — создать новый файл, если файл с заданным именем уже существует, то функция заканчивается неудачей; □ create_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 — содержимое файла не индексировано;
426 Часть УН. Управление файлами □ file_attribute_offline — файл находится во вспомогательной памяти; □ FILE_ATTRIBUTE_READONLY — фаЙЛ МОЖНО ТОЛЬКО ЧИТаТЬ; □ file_attribute_system — файл используется операционной системой; □ file_attribute_temporary — файл используется для временного хранения данных. Сделаем несколько замечаний относительно некоторых атрибутов файлов. Сначала отметим, что зашифрованные файлы не могут иметь также атрибут file_attribute_system. Теперь заметим, что атрибут file_attribute_normal должен использоваться только один, а не в комбинации с другими атрибутами. Кроме того, в параметре dwFiagsAndAttributes может быть установлена любая комбинация следующих управляющих флагов: □ file_flag_write_through — запись данных непосредственно на диск, не используя кэширования; □ file_flag_overlapped — обеспечивается асинхронное выполнение операций чтения и записи; □ file_flag_no_buffering — не использовать буферизацию при доступе к файлу; □ file_flag_random_access — программа предполагает выбирать записи из файла случайным образом; □ file_flag_sequential_scan — программа будет сканировать файл последовательно; □ file_flag_delete_on_close — файл будет удален после того, как все дескрипторы этого файла будут закрыты; □ file_flag_backup_semantics — резервный файл; □ file_flag_posix_semantics — доступ к файлу будет осуществляться по стандарту POSIX; □ file_flag_open_reparse_point — при доступе к файлу используется системный фильтр; □ file_flag_open_no_recall — при использовании иерархической систе- мы управления памятью файл не должен читаться в оперативную память, а оставаться на нижнем уровне иерархии. Отметим, что флаг file_flag_backup_semantics может использоваться только в операционных системах Windows NT/2000. Параметр hTempiateFile используется при создании файла, атрибуты которого должны соответствовать атрибутам ранее созданного файла. В этом случае параметр hTempiateFile должен содержать дескриптор файла, атрибуты которого копируются в атрибуты создаваемого файла.
Глава 24. Работа с файлами в Windows 427 В остальных параметрах, кроме параметра dwCreationDisposition, может быть установлено значение 0. На платформах Windows 98/ME в этом параметре должно быть установлено значение null. В заключение этого раздела скажем, что пример использования функции createFile приведен в разд. 24.4. 24.3. Закрытие и удаление файлов Для закрытия доступа к файлу, как и для закрытия доступа к любому другому объекту ядра, используется функция cioseHandie, единственным параметром которой является дескриптор открытого файла. Для физического удаления файла с диска используется функция DeieteFile, которая имеет следующий прототип: BOOL DeieteFile( LPCTSTR IpFiieName // имя файла ); Единственный параметр IpFiieName является указателем на строку, которая указывает полный путь к файлу. При успешном завершении функция возвращает ненулевое значение, а при неудаче — false. В листинге 24.1 приведена программа, которая удаляет файл с именем demo_file.dat, который расположен в корневом каталоге на диске С:. Листинг 24.1. Удаление файла #include <windows.h> #include <iostream.h> int main () { // удаляем файл if(!DeieteFile("C:Wdemo_file.dat")) { cerr « "Delete file failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.get(); return 0; } cout « "The file is deleted." « endl; return 0;
428 Часть VII. Управление файлами 24.4. Запись данных в файл Для записи данных в файл служит функция writeFiie, причем отметим, что эта функция может использоваться как для синхронной, так и для асинхронной записи данных. В этом разделе будет рассмотрена только синхронная запись данных в файл. В этом случае данные записываются в файл последовательно — байт за байтом, и указатель файла передвигается по мере записи данных на новую позицию. Асинхронный ввод-вывод данных будет рассмотрен в гл. 27. Теперь рассмотрим функцию writeFiie более подробно. Эта функция имеет следующий прототип BOOL WriteFiie( HANDLE hFile, // дескриптор файла LPCVOID lpBuffer, // указатель на буфер данных DWORD nNumberOfBytesToWrite, // количество записываемых байтов LPDWORD lpNumberOfBytesWritten, // количество записанных байтов LPOVERLAPPED lpOverlapped // используется при асинхронной записи ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — значение false. Параметр hFile должен содержать дескриптор файла, причем файл должен быть открыт в режиме записи. Параметр lpBuffer должен указывать на область памяти, в которую будут читаться данные. Параметр nNumberOfBytesToWrite должен содержать количество байт, которые предполагается записать в файл посредством вызова функции WriteFiie. Параметр lpNumberOfBytesWritten должен содержать адрес памяти, в которую функция WriteFiie поместит количество фактически записанных байт. При работе на платформе Windows 98 этот параметр должен иметь значение, отличное от null. При выполнении функции writeFiie операционная система записывает по этому адресу ноль, прежде чем выполнить запись данных в файл. Так как в этом разделе не будет рассматриваться асинхронная запись данных в файл, то параметр lpoveriapped будет устанавливаться в null. Подробно асинхронный ввод-вывод данных будет рассмотрен в гл. 27. В листинге 24.2 приведена программа, которая создает файл и записывает в него последовательность целых чисел.
Глава 24. Работа с файлами в Windows 429 Листинг 24.2. Создание файла и запись в него данных #include <windows.h> #include <iostream.h> int main () { HANDLE hFile; // создаем файл для записи данных hFile = CreateFile( "С: Wdemo_file.dat", // имя файла GENERIC_WRITE, // запись в файл О, // монопольный доступ к файлу 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), // количество записываемых байтов SdwBytesWrite, // количество записанных байтов
430 Часть VII. Управление файлами (LPOVERLAPPED)NULL // запись синхронная )) { cerr « "Write file failed.." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.getO ; return 0; } } // закрьгоаем дескриптор файла CloseHandle(hFile); cout « "The file is created and written." << endl; return 0; } 24.5. Освобождение буферов файла Часто несколько приложений имеют совместный доступ к одному и тому же файлу. При этом может потребоваться, чтобы приложение, которое читает данные из файла, имело доступ к последней версии этого файла. Для этого необходимо, чтобы приложение, которое изменяет содержимое файла, фиксировало изменение файла после обработки нужных записей. Так как не исключена возможность того, что последние обработанные записи хранятся в буфере файла, то в этом случае необходимо освободить буфер от записей. ВЫПОЛНИТЬ ЭТу операцию МОЖНО Посредством фуНКЦИИ FlushFileBuffers, которая имеет следующий прототип: BOOL FlushFileBuffers( HANDLE hFile // дескриптор файла ); В случае удачного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. В ЛИСТИНГе 24.3 приведена программа, В КОТОРОЙ фуНКЦИЯ FlushFileBuffers используется для сброса данных из буферов в файл после записи половины файла.
Глава 24. Работа с файлами в Windows 431 #include <windows.h> #include <iostream.h> int main() { . HANDLE hFile; // создаем файл для записи данных hFile = CreateFile( "С: Wdemo_file.dat", GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, 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), SdwBytesWrite, // дескриптор файла // адрес буфера, откуда идет запись // количество записываемых байтов // количество записанных байтов
432 Часть VII. Управление файлами (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; } // если достигли середины файла, то освобождаем буфер if(i == 5) { if(IFlushFileBuffers(hFile)) { cerr « "Flush file buffers failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.get(); return 0; } // теперь можно просмотреть содержимое файла cout « "A half of the file is written." « endl « "Press any key to continue."; cin.get(); } } // закрываем дескриптор файла CloseHandle(hFile); cout « "The file is created and written." « endl; return 0; } В заключение этого раздела отметим, что можно отменить режим буферизации файла, установив флаг file_flag_no_buffering в параметре dwFiagsAn- dAttributes функции CreateFiie. Однако в этом случае длина записывае-
Глава 24. Работа с файлами в Windows 433 мых или считываемых данных из файла должна быть кратна размеру сектора. Например, в операционных системах Windows длина сектора равна 512 байт. 24.6. Чтение данных из файла Для чтения данных из файла служит функция ReadFile, которая может использоваться как для синхронного, так и асинхронного чтения данных. В этом разделе будет рассмотрено только синхронное чтение данных из файла. В этом случае данные читаются из файла последовательно байт за байтом, и указатель файла передвигается по мере чтения данных на новую позицию. Асинхронный ввод-вывод данных будет рассмотрен в гл. 27. Теперь более подробно рассмотрим функцию ReadFile, которая имеет следующий прототип: BOOL ReadFile( HANDLE hFile, // дескриптор файла LPVOID lpBuffer, // указатель на буфер данных DWORD nNumberOfBytesToRead, // количество читаемых байтов LPDWORD lpNumberOfBytesRead, // количество прочитанных байтов LPOVERLAPPED lpOverlapped // используется при асинхронной записи ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — значение false. Параметр hFile должен содержать дескриптор файла, причем файл должен быть открыт в режиме чтения. Параметр lpBuffer должен указывать на область памяти, из которой будут читаться данные. Параметр nNumberOfBytesToRead должен содержать количество байт, которые предполагается читать из файла посредством вызова функции ReadFile. Параметр lpNumberOfBytesRead должен содержать адрес памяти, в которую функция ReadFile поместит количество фактически прочитанных из файла байтов. Как и в случае функции WriteFiie, при работе на платформе Windows 98 этот параметр должен иметь значение, отличное от null. При выполнении функции ReadFile операционная система записывает по этому адресу 0, прежде чем выполнить чтение данных из файла. Так как в этом разделе не будет рассматриваться асинхронное чтение данных из файла, то параметр lpOverlapped будет устанавливаться в null. Подробно асинхронный ввод-вывод данных будет рассмотрен в гл. 27. В листинге 24.4 приведена программа, которая читает данные из файла, созданного программой из листинга 24.2, и выводит эти данные на консоль.
434 Часть VII. Управление файлами # include <windows. h> #include <iostream.h> int main () { HANDLE hFile; // открываем файл для чтения hFile = CreateFile( "С: Wdemo_file.dat", GENERIC_READ, О, NULL, OPEN_EXISTING, 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.getO ; return 0; } // читаем данные из файла for (;;) { DWORD dwBytesRead; int n ; // читаем одну запись if (!ReadFile( hFile, &n, sizeof(n), // дескриптор файла // адрес буфера, куда читаем данные // количество читаемых байтов
Глава 24. Работа с файлами в Windows 435 &dwBytesRead, // количество прочитанных байтов (LPOVERLAPPED)NULL // чтение синхронное )) { cerr « "Read file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.get(); return 0; } // проверяем на конец файла if (dwBytesRead == 0) // если да, то выходим из цикла break; else // иначе выводим запись на консоль cout « п « ' '; } cout « endl; // закрываем дескриптор файла CloseHandle(hFile); cout « "The file is opened and read." « endl; return 0; } В связи с этой программой отметим обработку конца файла. При достижении конца файла и запросе на чтение записи функция ReadFiie возвращает ненулевое значение и при этом устанавливает значение количества прочитанных байтов в 0. 24.7. Копирование файла Для копирования файлов используется функция CopyFiie, которая имеет следующий прототип: BOOL CopyFiie( LPCTSTR IpExistingFileName, // имя существующего файла
436 Часть VII. Управление файлами LPCTSTR lpNewFileName, // имя нового файла BOOL bFaiiifExists // действия в случае существования файла ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — значение false. При этом отметим, что функция copyFile копирует для нового файла также и атрибуты доступа старого файла, но атрибуты безопасности не копируются. Параметры этой функции имеют следующее назначение. Параметр lpExistingFiieName должен указывать на строку, содержащую имя копируемого файла. Параметр lpNewFileName должен указывать на строку с именем файла, в который будет копироваться существующий файл. При этом отметим, что новый файл создается самой функцией CopyFile. Параметр bFaiiifExists определяет действия, которые нужно осуществить в случае, если файл, в который выполняется копирование, уже существует. Если значение этого параметра равно false, to функция перезаписывает существующий файл. Если же значение этого параметра равно true, to выполнение функции в этом случае заканчивается неудачей. В листинге 24.5 приведена программа, которая выполняет копирование файла, ИСПОЛЬЗуя ФУНКЦИЮ CopyFile. [ Листинг 24.5. Копирование файла # include <windows.h> #include <iostream.h> int main() { // копируем файл if(!CopyFile("C:\\demo_file.dat", "C:\\new_file.daf', FALSE)) { cerr « "Copy file failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.getO ; return 0; } cout « "The file is copied." « endl; return 0;
Глава 24. Работа с файлами в Windows 437 В операционных системах семейства Windows NT D.0\2000\XP) для копирования файлов может использоваться функция copyFiieEx, которая предоставляет более широкие возможности по копированию файлов. 24.8. Перемещение файла Теперь разберем функцию MoveFile, которая служит для перемещения файлов. Перемещение файла отличается от копирования файла только тем, что старый файл после его перемещения удаляется. Функция MoveFile имеет следующий прототип: BOOL MoveFile( LPCTSTR IpExistingFiieName, // имя существующего файла LPCTSTR lpNewFileName // имя нового файла ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — значение false. Отметим, что функция MoveFile сохраняет все атрибуты перемещаемого файла. Параметры этой функции имеют следующее назначение. Параметр IpExistingFiieName должен указывать на строку, содержащую имя перемещаемого файла. Параметр lpNewFileName должен указывать на строку с именем файла, в который будет перемещаться существующий файл. При этом отметим, что новый файл Создается самой функцией MoveFile. В листинге 24.6 приведена программа, которая выполняет перемещение файла, ИСПОЛЬЗуя функцию MoveFile. | Листинг 24.6, Перемещение файла # include <windows.h> #include <iostream.h> int main () { // перемещаем файл if(!MoveFile("C:Wdemo_file.dat", "C:\\new_file.dat")) { cerr « "Move file failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish.";
438 Часть VII. Управление файлами cin.get(); return 0; } cout « "The file is moved." « endl; return 0; } В операционных системах семейства Windows NT D.0\2000\XP) для перемещения файлов может также использоваться функция MoveFiieEx, в которой можно задавать режимы перемещения файлов. 24.9. Замещение файла В Windows 2000 и Windows XP определена функция RepiaceFiie, которая предназначена для замещения файлов. Существенное отличие этой функции от функций copyFile и MoveFile состоит в том, что она копирует в заме- щаемый файл не только атрибуты доступа, но также и атрибуты безопасности файла-заместителя. Отметим также, что функция RepiaceFiie работает только с файлами, которые находятся на одном томе (логическом диске). Функция RepiaceFiie имеет следующий прототип: BOOL RepiaceFiie( LPCTSTR lpReplacedFileName, // имя замещаемого файла LPCTSTR lpReplacementFileName, // имя файла-заместителя LPCTSTR lpBackupFileName, // имя резервной копии файла-заместителя DWORD dwReplасеFlags, // опции замещения LPVOID IpExclude, // не используется LPVOID lpReserved //не используется ); Функция замещения файлов в случае успешного завершения возвращает ненулевое значение, а в случае неудачи — false. Кратко опишем назначение параметров этой функции. Параметр lpReplacedFileName должен указывать на строку, которая содержит ИМЯ замещаемого файла. При ЭТОМ ОТМеТИМ, ЧТО фуНКЦИЯ RepiaceFiie может как создавать новый файл, так и использовать уже существующий. Параметр lpReplacementFileName ДОЛЖеН указывать на строку, КОТОраЯ содержит имя файла-заместителя. Этот файл заменит файл, на который указывает параметр lpReplacedFileName. При ЭТОМ ОТМСТИМ, ЧТО сам фаЙЛ- заместитель удаляется с диска.
Глава 24. Работа с файлами в Windows 439 В параметре lpBackupFiieName устанавливается адрес строки, которая содержит имя файла, который содержит резервную копию файла-заместителя. Если этот параметр установлен в null, to резервная копия файла-заместителя не создается. В параметре dwRepiaceFiags устанавливаются флаги, которые указывают режимы замещения файла. Можно установить любую комбинацию следующих флагов: □ replacefile_write_through — освободить буферы перед выходом из функции; □ replacefile_ignore_merge_errors — игнорировать ошибки при копировании данных. Параметры IpExclude И IpReserved He ИСПОЛЬЗуЮТСЯ. ПОЭТОМУ ОНИ ДОЛЖНЫ быть установлены в null. В листинге 24.7 приведена программа, в которой показан пример использования функция RepiaceFiie для замещения файла. | Листинг 24.7. Замещение файла #include <windows.h> #include <iostream.h> int main() { // перемещаем файл if(!RepiaceFiie( "C: \\new_file.dat", // имя замещаемого файла "С:\\demo_file.dat", // имя файла-заместителя "С:\\back_file.dat", // имя резервного файла REPLACEFILE_WRITE_THROUGH, // освободить буферы NULL, NULL //не используются )) { cerr « "Replace file failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.getO ; return 0;
440 Часть VII. Управление файлами cout « "The file is replaced." « endl; return 0; } 24.10. Работа с указателем позиции файла Прежде чем разбираться с функцией для работы с указателем позиции файла, рассмотрим формат самого указателя позиции файла. Указатель позиции файла состоит из двух значений типа dword, которые будут называться старшая и младшая часть указателя позиции файла соответственно. Следовательно, указатель позиции имеет длину в 64 бита. Если длина файла не превышает двух гигабайт без двух байт, т. е. 231 — 2 байта, то в указателе позиции используется только его младшая часть, которая рассматривается как целое число со знаком, но при этом старшая часть указателя позиции должна быть установлена в null. ДЛЯ работы С указателем ПОЗИЦИИ файла СЛУЖИТ фуНКЦИЯ SetFilePointer, которая имеет следующий прототип: DWORD SetFilePointer( HANDLE hFile, // дескриптор файла LONG IDistanceToMove, // младшая часть сдвига указателя в байтах PLONG lpDistanceToMoveHigh, // указатель на старшую часть сдвига // указателя в байтах DWORD dwMoveMethod // начальная точка сдвига ); В случае удачного завершения эта функция возвращает младшую часть новой позиции указателя файла, а по адресу, заданному параметром lpDistanceToMoveHigh, записывает старшую часть новой позиции указателя файла. Если функция устанавливает старшую часть указателя позиции в null, то младшая часть указателя позиции представлена положительным целым числом. В случае неудачного завершения функция SetFilePointer возвращает значение -1 и при этом устанавливает значение параметра lpDistanceToMoveHigh в null. Если же значение этого параметра не установлено в null, to возвращаемое значение -1 может быть и действительной младшей частью указателя позиции. В этом случае нужно проверить код последней ОШИбкИ, КОТОРЫЙ ВОЗВращаеТ фуНКЦИЯ GetLastError. ЕСЛИ ЭТОТ КОД равен no_error, то ошибки нет, а в противном случае выполнение функции SetFilePointer завершилось неудачей. Теперь кратко опишем назначение параметров функции SetFilePointer. Параметр hFile должен содержать дескриптор файла, причем сам файл должен быть открыт в режиме чтения или записи.
Глава 24. Работа с файлами в Windows 441 Параметр lDistanceToMove должен содержать младшую часть сдвига для указателя ПОЗИЦИИ файла. ЕСЛИ значение параметра IpDistanceToMoveHigh установлено в null, to значение этого параметра рассматривается как целое число со знаком. В случае положительного числа функция выполняет сдвиг указателя вперед на заданное количество байт, а в случае отрицательного числа выполняется сдвиг назад. Параметр IpDistanceToMoveHigh должен содержать адрес старшей части сдвига для указателя позиции файла. Старшая и младшая части указателя позиции рассматриваются как целое число со знаком. Если значение этого параметра равно null, то сдвиг задается только младшей частью. Отметим, что в операционной системе Windows 98 значение этого параметра может быть равно только одному из следующих значений: null, о и -1. Параметр dwMoveMethod задает начальную точку, от которой выполняется сдвиг указателя позиции. Этот параметр может принимать только одно из следующих значений: □ file_begin — сдвиг от начала файла; □ file_current — сдвиг от текущей позиции файла; □ file_end — сдвиг от конца файла. В листинге 24.8 приведена программа, которая читает запись файла, предварительно установив на эту запись указатель. Кстати, такое чтение записей файла называется прямым доступом к файлу. В общем случае прямой доступ к файлу подразумевает чтение записи с заданным значением ключа, который определяется содержимым одного или нескольких полей записи. Правда, в этом случае нужно знать зависимость указателя позиции файла от значения ключа записи. Листинг 24,8. Установка указателя позиции файла при помощи функции SetFil©Pointer #include <windows.h> #include <iostream.h> int main() { HANDLE hFile; // дескриптор файла long n; // для номера записи long p; // для указателя позиции DWORD dwBytesRead; // количество прочитанных байт int m; // прочитанное число // открываем файл для чтения
442 Часть VII. Управление файлами hFile = CreateFile( "С: Wdemo_file.dat", // имя файла GENERIC_READ, // чтение из файла О, // монопольный доступ к файлу NULL, // защиты нет OPEN_EXISTING, // открываем существующий файл 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; } // вводим номер нужной записи cout « "Input a number from 0 to 9: "; cin » n; // сдвигаем указатель позиции файла р = SetFilePointer(hFile, n * sizeof(int), NULL, FILE_BEGIN); if(p == -1) { cerr « "Set file pointer failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.get(); return 0; } // выводим на консоль значение указателя позиции файла cout « "File pointer: " « р « endl; // читаем данные из файла if (!ReadFile( hFile, // дескриптор файла &mf // адрес буфера, куда читаем данные sizeof(m)f //количество читаемых байтов
Глава 24. Работа с файлами в Windows 443 &dwBytesRead, // количество прочитанных байтов (LPOVERLAPPED)NULL // чтение синхронное )) { cerr « "Read file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.get(); return 0; } // выводим прочитанное число на консоль cout « "The read number: " « m « endl; // закрываем дескриптор файла CloseHandle(hFile); return 0; } При помощи функции setFiiePointer можно также определить текущее состояние указателя позиции файла. Для этого нужно просто сдвинуть указатель файла от текущей позиции на нулевое количество байт. В результате функция SetFiiePointer вернет текущее состояние указателя позиции файла. Начиная с операционной системы Windows 2000, для работы с указателем позиции файла можно использовать функцию setFiiePointerEx, которая более проста в использовании и имеет следующий прототип: BOOL SetFiiePointerEx( HANDLE hFile, // дескриптор файла LARGE_INTEGER liDistanceToMove, // сдвиг в байтах PLARGE_INTEGER lpNewFilePointer, // новый указатель позиции файла DWORD dwMoveMethod // начальная точка сдвига ); В случае удачного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Первый и последний параметры этой функции имеют такое же назначение, как и в функции SetFiiePointer. Поэтому кратко опишем только оставшиеся параметры. Параметр liDistanceToMove задает сдвиг указателя позиции файла, который рассматривается как целое число со знаком. В случае положительного числа указатель позиции сдвигается вперед, а в случае отрицательного — назад.
444 Часть VII. Управление файлами Параметр IpNewFiiePointer должен указывать на объединение типа LARGE_INTEGER, В Которое фуНКЦИЯ SetFiiePointerEx вернет НОВОе Значение индикатора позиции файла. Объединение типа large_integer имеет следующий формат: . typedef union _LARGE_INTEGER { struct { DWORD LowPart; // младшая часть LONG HighPart; // старшая часть }; LONGLONG QuadPart; // все части } LARGE_INTEGER, *PLARGE_INTEGER; Если значение параметра IpNewFiiePointer равно null, то новое значение индикатора позиции не будет возвращаться. В листинге 24.9 приведена программа, которая для установки индикатора позиции использует функцию SetFiiePointerEx, а затем читает из файла запись, на которую установлен индикатор позиции. ; Листинг 24,3, Установка указателя позиции файла при помощи | ФУНКЦИИ SetFilePointerEK #include <windows.h> #include <iostream.h> int main () { HANDLE hFile; // дескриптор файла int n; // для номера записи LARGE_INTEGER p; // для указателя позиции DWORD dwBytesRead; // количество прочитанных байтов int m; // прочитанное число // открываем файл для чтения hFile = CreateFile( "С: \\demo_file.dat", // имя файла GENERIC_READ, // чтение из файла О, // монопольный доступ к файлу NULL, // защиты нет
Глава 24. Работа с файлами в Windows 445 OPEN_EXISTING, // открываем существующий файл FILE_ATTRIBUTE_NORMAL, // обычный файл NULL // шаблона нет ); // проверяем на успешное открытие if (hFile == INVALID_HAMDLE_VALUE) { cerr « "Create file failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.get(); return 0; } // вводим номер нужной записи cout « "Input a number from 0 to 9: "; cin » n; // сдвигаем указатель позиции файла if(!SetFilePointerEx(hFile, n * sizeof(int)/ &p, FILE_BEGIN)) { cerr « "Set file pointer failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.get(); return 0; } // выводим на консоль значение указателя позиции файла cout « "File pointer: " « р « endl; // читаем данные из файла if (!ReadFile( hFile, // дескриптор файла &m, // адрес буфера, куда читаем данные sizeof(m), // количество читаемых байтов &dwBytesRead, // количество прочитанных байтов (LPOVERLAPPED)NULL // чтение синхронное )) { cerr « "Read file failed." « endl
446 Часть VII. Управление файлами « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.get(); return 0; } // выводим прочитанное число на консоль cout « "The read number: " « m « endl; // закрываем дескриптор файла CloseHandle(hFile); return 0; } 24.11. Определение и изменение атрибутов файла УзнаТЬ атрибуты файла МОЖНО При ПОМОЩИ фуНКЦИИ 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 — фаЙЛ ТОЛЬКО ДЛЯ ЧТеНИЯ;
Глава 24. Работа с файлами в Windows 447 □ file_attribute_reparse_point — файл требует интерпретации; □ file_attribute_sparse_file — разреженный файл; □ file_attribute_system — системный файл; □ FILE_ATTRIBUTE_TEMPORARY — ВремеННЫЙ фаЙЛ. Более подробно атрибуты файла описаны в разд. 24.2. Другие атрибуты файла, которые не вошли в вышеприведенный список, можно узнать, используя ФУНКЦИЮ GetFileAttributesEx. Изменить атрибуты файла МОЖНО при ПОМОЩИ фуНКЦИИ SetFileAttributes, которая имеет следующий прототип: BOOL SetFileAttributes( LPCTSTR lpFileName, // имя файла DWORD dwFileAttributes // атрибуты файла ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Параметр lpFileName должен содержать имя файла, а в параметре dwFileAttributes можно установить следующие атрибуты файла: □ file_attribute_archive — архивный файл; □ 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, to он должен быть один, т. к. все остальные атрибуты, аннулируют этот атрибут. В листинге 24.10 приведена программа, которая читает и изменяет атрибуты файла. Листинг 24.10, Чтение и изменение атрибутов файла #include <windows.h> #include <iostream.h> int main ()
448 Часть VII. Управление файлами DWORD file_attr; // читаем атрибуты файла file_attr = GetFileAttributes("С:Wdemo_file.dat"); if(file_attr == -1) { cerr « "Get file attributes failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.get(); return 0; } // проверяем, является ли файл нормальным if(file_attr == FILE_ATTRIBUTE_NORMAL) cout « "This is a normal file." « endl; else { cout « "This is a not normal file." « endl; return 0; } // устанавливаем атрибут скрытого файла if ( !SetFileAttributes ("С: \\demo_file.dat", FILE_ATTRIBUTE_HIDDEN) ) { cerr « "Set file attributes failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.get(); return 0; } // Теперь можно проверить, что файл скрылся cout « "Now the file is hidden." « endl « "Press any key to continue."; cin.get(); // Обратно делаем файл обычным if ( '.SetFileAttributes ("С: Wdemo_file.dat", FILE_ATTRIBUTE_NORMAL) ) { cerr « "Set file attributes failed." « endl « "The last error code: " « GetLastError() « endl;
Глава 24. Работа с файлами в Windows 449 cout « "Press any key to finish."; cin.get(); return 0; } cout « "Now the file is again normal." « endl; return 0; } 24.12. Определение и изменение размеров файла Прежде чем описывать функции для работы с размером файла, скажем, что размер файла является целым числом и для его хранения требуются два значения типа dword или, другими словами, два двойных слова. Первое из этих двойных слов содержит старшую часть размера файла, а второе — младшую. Если размер файла входит только в младшую часть, то значение старшей устанавливается в null. Определить размер файла можно при помощи функции GetFiiesize, которая имеет следующий прототип: DWORD GetFileSize( HANDLE hFile, // дескриптор файла LPDWORD lpFileSizeHigh // указатель на старшую часть размера файла ); В случае успешного завершения эта функция возвращает младшую часть размера файла, а по адресу, указанному в параметре IpFiiesizeHigh, записывает старшую часть размера файла. В случае неудачи функция GetFiiesize возвращает значение -1, если значение адреса, заданного параметром IpFiiesizeHigh, установлено в null. Если же значение этого адреса не равно null и функция закончилась неудачей, то она возвращает значение -1 и функция кода последней ошибки GetLastError возвратит значение, отличное от no_error. Отметим, что для правильной работы функции GetFiiesize необходимо, чтобы файл был открыт в режиме чтения или записи. В листинге 24.11 приведена программа, которая определяет размер файла, ИСПОЛЬЗуя функцию GetFileSize. 15 Зак. 514
450 Часть VII. Управление файлами # inc lude <windows. h> #include <iostream.h> ЭИ ПОМОЩИ ФУНКЦИИ GetFilQSize j int main() { HANDLE hFile; DWORD dwFileSize; // младшая часть размера файла // открываем файл для чтения hFile = CreateFile( "С: \\demo_file.dat", GENERIC_READ, О, NULL, OPEN EXISTING, // имя файла // чтение из файла // монопольный доступ к файлу // зашиты нет // открываем существующий файл 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.getO; return 0; // определяем размер файла dwFileSize = GetFileSize(hFile, NULL); if (dwFileSize == -1) { cerr « "Get file size failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile);
Глава 24. Работа с файлами в Windows 451 cout « "Press any key to finish."; cin.get(); return 0; } // выводим размер файла cout « "File size: " « dwFileSize « endl; // закрываем дескриптор файла CloseHandle(hFile); return 0; } Определить размер файла можно также при помощи функции GetFiieSizeEx, которая поддерживается только операционными системами Windows 2000 и Windows XP. Эта функция имеет следующий прототип: BOOL GetFiieSizeEx( HANDLE hFile, // дескриптор файла PLARGE_INTEGER lpFileSize // размер файла ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — значение false. Параметр hFile этой функции должен содержать дескриптор файла, а параметр ipFiiesize указывает на объединение типа large_integer, в которое функция GetFiieSizeEx записывает размер файла. Формат объединения типа large_integer приведен в разд. 24.10. В листинге 24.12 приведена программа, которая определяет размер файла, ИСПОЛЬЗуЯ фуНКЦИЮ GetFiieSizeEx. [Листинг 24,12* Определение размера файла при помощи функции ! GetFiieSizeEx у^м , s- : _ л ^ ^;_ ^ ; _, j #include <windows.h> #include <iostream.h> int main() { HANDLE hFile; LARGE_INTEGERD liFileSize; // размер файла // открываем файл для чтения
452 Часть VII. Управление файлами hFile = CreateFile( "С: Wdemo_file.dat", // имя файла GENERIC_READ, // чтение из файла О, // монопольный доступ к файлу NULL, // защиты нет OPEN_EXISTING, // открываем существующий файл 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.getO ; return 0; } // определяем размер файла if (IGetFileSizeEx(hFile, fcliFileSize)) cerr « "Get file size failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.get(); return 0; } // выводим размер файла cout « "File size: " « liFileSize.LowPart « endl; // закрываем дескриптор файла CloseHandle(hFile); return 0;
Глава 24. Работа с файлами в Windows 453 Изменить размер файла можно при помощи функции setEndofFile, которая имеет следующий прототип: BOOL SetEndOfFile( HANDLE hFile // дескриптор файла ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Единственным параметром функции является дескриптор файла, размер которого изменяется. При этом отметим, что файл должен быть открыт в режиме записи. Функция SetEndofFile работает следующим образом. Она передвигает маркер конца файла eof на позицию, которую содержит указатель позиции файла. Если указатель позиции файла указывает на запись, которая не является последней, то все записи, которые находятся за текущей записью, отбрасываются. Таким образом, в этом случае содержимое файла урезается. Если же указатель позиции файла указывает за пределы файла, то объем файла расширяется за счет добавления новых кластеров к нему. При этом содержимое добавленных кластеров не определено. В листинге 24.13 приведен пример программы, которая уменьшает размер файла в два раза. Листинг 24.13. Изменение размера файла # include <windows.h> #include <iostream.h> int main() { HANDLE hFile; // дескриптор файла DWORD dwFileSize; // размер файла long p; // указатель позиции // открываем файл для чтения hFile = CreateFile( "С: \\demo_file.dat", // имя файла GENERIC_WRITE, // запись в файл О, // монопольный доступ к файлу NULL, // защиты нет OPEN_EXISTING, // открываем существующий файл FILE_ATTRIBUTE_NORMAL, // обычный файл NULL // шаблона нет
454 Часть VII. Управление файлами // проверяем на успешное открытие 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; } // определяем размер файла dwFileSize = GetFileSize(hFile, NULL); if (dwFileSize == -1) { cerr « "Get file size failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.get(); .return 0; } // выводим на консоль размер файла cout « "Old file size: " « dwFileSize « endl; // уменьшаем размер файла вдвое dwFileSize /= 2; // сдвигаем указатель позиции файла р = SetFilePointer(hFile, dwFileSize, NULL, FILE_BEGIN); if(p == -1) { cerr « "Set file pointer failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.get(); return 0; } // устанавливаем новый размер файла
Глава 24. Работа с файлами в Windows 455 if ( ! SetEndOfFile (hFile) ) { cerr « "Set end of file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.getO ; return 0; } // определяем новый размер файла dwFileSize = GetFileSize(hFile, NULL); if (dwFileSize == -1) { cerr « "Get file size failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.getO ; return 0; } // выводим на консоль размер файла cout « "New file size: " « dwFileSize « endl; // закрываем дескриптор файла CloseHandle(hFile); return 0; 24.13. Блокирование файла Часто несколько приложений имеют совместный доступ к одному и тому же файлу. При этом может потребоваться, чтобы приложение, которое изменяет данные в файле, имело к этому файлу монопольный доступ. Для этого необходимо блокировать весь файл или только его часть для доступа другим приложениям. Для этих целей используется функция LockFile, которая имеет следующий прототип: BOOL LockFile( HANDLE hFile, // дескриптор файла
456 Часть VII. Управление файлами DWORD dwFileOffsetLow, DWORD dwFileOffsetHigh, DWORD nNumberOfBytesToLockLow, DWORD nNumberOfBytesTiLockHigh // младшая часть смещения // старшая часть смещения // младшая часть количества байтов // старшая часть количества байтов ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. Кратко опишем назначение параметров этой функции. Параметр hFile должен содержать дескриптор файла. Причем этот файл должен быть открыт в режиме записи или чтения. В параметрах dwFileOffsetLow И dwFileOffsetHigh ДОЛЖНЫ быть установлены соответственно младшая и старшая части смещения от начала файла в байтах. Для операционной системы Windows 98 значение параметра dwFileOf fsetHigh ДОЛЖНО быть установлено В 0. В параметрах nNumberOfBytesToLockLow И nNumberOfBytesTiLockHigh ДОЛЖНЫ быть установлены соответственно старшая и младшая части длины области файла, которая блокируется для монопольного доступа приложением. Для отмены блокировки области файла используется функция uniockFile, которая имеет следующий прототип: BOOL UniockFile( HANDLE hFile, DWORD dwFileOffsetLow, DWORD dwFileOffsetHigh, DWORD nNumberOfBytesToLockLow, DWORD nNumberOfBytesTiLockHigh // дескриптор файла // младшая часть смещения // старшая часть смещения // младшая часть количества байтов // старшая часть количества байтов В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — значение false. Все параметры этой функции аналогичны параметрам функции LockFiie. В листинге 24.14 приведена программа, которая сначала блокирует доступ к файлу, а затем разблокирует его. Листинг 24.14. Блокировка и разблокировка файла # include <windows.h> #include <iostream.h> int main()
Глава 24. Работа с файлами в Windows 457 HANDLE hFile; DWORD dwFileSize; // открываем файл для записи hFile = CreateFile( "С:\\demo_file.dat", // имя файла GENERIC_WRITE, // запись в файл О, // монопольный доступ к файлу NULL, // защиты нет OPEN_EXISTING, // открываем существующий файл 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; } // определяем размер файла dwFileSize = GetFileSize(hFile, NULL); if (dwFileSize == -1) { cerr « "Get file size failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.get(); return 0; } // блокируем файл if (ILockFile(hFile, 0, 0, dwFileSize, 0))
458 Часть VII. Управление файлами cerr « "Lock file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.get(); return 0; } cout « "Now the file is locked." « endl « "Press any key to continue." « endl; cin.getO ; // разблокируем файл if (!UnlockFile(hFile, 0, 0, dwFileSize, 0)) { cerr « "Unlock file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.getO ; return 0; } cout « "Now the file is unlocked." « endl « "Press any key to continue." « endl; cin.getO ; // закрываем дескриптор файла CloseHandle(hFile); return 0; } В заключение этого раздела сделаем несколько замечаний относительно использования фуНКЦИЙ LockFiie И UnlockFile. Прежде Всего, ОТМСТИМ, ЧТО если приложение выдает запрос на блокирование области файла, которая уже заблокирована самим приложением, то выполнение функции LockFiie завершится неудачей. Кроме того, блокирование и разблокирование файлов должно совпадать по областям.
Глава 24. Работа с файлами в Windows 459 24.14. Получение информации о файле Чтобы получить информацию о файле, можно использовать функцию GetFilelnformationByHandle, которая имеет следующий прототип: BOOL GetFilelnformationByHandle( HANDLE hFile, // дескриптор файла // указатель на информацию LPBY_HANDLE_FILE_INFORMATION lpFilelnformation ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. В параметре hFiie этой функции должен быть установлен дескриптор файла, информацию о котором требуется получить. Отметим, что этот файл может быть открыт в любом режиме доступа. Параметр lpFilelnformation должен указывать на структуру типа BY_HANDLE_FILE_INFORMATION, В КОТОруЮ фуНКЦИЯ ЗаПИШеТ Информацию О файле. Эта структура имеет следующий формат: typedef struct _BY_HANDLE_FILE_INFORMATION { DWORD dwFileAttributes; // атрибуты файла FILETIME ftCreationTime; // время создания файла FILETIME ftLastAccessTime; // время последнего доступа к файлу // время последней записи в файл // серийный номер тома // старшая часть размера файла // младшая часть размера файла // количество ссылок на файл // старшая часть индекса файла // младшая часть индекса файла *LPBY_HANDLE_FILE_INFORMATION; FILETIME ftLastWriteTime; DWORD dwVolumeSerialNuniber; DWORD nFileSizeHigh; DWORD nFileSizeLow; DWORD nNumberOfLinks; nFilelndexHigh; nFilelndexLow; DWORD DWORD } BY_HANDLE_FILE_INFORMATION, В листинге 24.15 приведена программа, которая получает информацию о файле и распечатывает ее. Листинг 24,15, Получение информации о файле #include <windows.h> #include <iostream.h> int main()
460 Часть VII. Управление файлами HANDLE hFile; BY_HANDLE_FILE_INFORMATION bhfi; // информация о файле // открываем файл для чтения hFile = CreateFile( "С: Wdemo_file.dat", // имя файла О, // получение информации о файле О, // монопольный доступ к файлу NULL, // защиты нет OPEN_EXISTING, // открываем существующий файл 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; } // получаем информацию о файле if (IGetFilelnformationByHandle(hFile, &bhfi)) { cerr « "Get file information by handle failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.get(); return 0; } // распечатываем информацию о файле cout « "File attributes: " « bhfi.dwFileAttributes « endl « "Creation time: high date: " « bhfi.ftCreationTime.dwHighDateTime « endl « "Creation time: low date: " « bhfi.ftCreationTime.dwLowDateTime « endl
Глава 24. Работа с файлами в Windows 461 « "Last access time: high date: " « bhfi. ftLastAccessTime.dwHighDateTime « endl « "Last access time: low date: " « bhfi.ftLastAccessTime.dwLowDateTime « endl « "Last write time: high date: " « bhfi.ftLastWriteTime.dwHighDateTime « endl « "Last write time: low date: " « bhfi.ftLastWriteTime.dwLowDateTime « endl « "Volume serial number: " « bhfi.dwVolumeSerialNumber « endl « "File size high: " « bhfi.nFileSizeHigh « endl « "File size low: " « bhfi.nFileSizeLow « endl « "Number of links: " « bhf i .nNumberOf Links « endl << "File index high: " « bhfi.nFilelndexHigh « endl « "File index low: " « bhfi.nFilelndexLow « endl; // закрываем дескриптор файла CloseHandle(hFile); return 0; } Структура типа by_handle_file_information содержит структуру типа filetime, которая служит для хранения времени. Эта структура имеет следующий формат: typedef struct _FILETIME { DWORD dwLowDateTime; // младшая часть времени DWORD dwHighDateTime; // старшая часть времени } FILETIME, *PFILETIME; Само время задается в интервалах, каждый из которых равен 100 наносекунд. Естественно, что такое время неудобно просматривать пользователю. Поэтому для перевода времени в более удобную форму существует функция FileTimeToSystemTime, Которая имеет следующий Прототип: BOOL FileTimeToSystemTime ( CONST FILETIME *lpFileTime; // указатель на время в формате "файл" LPSYSTEMTIME IpSystemTime // указатель на время в формате "система" ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false.
462 Часть VII. Управление файлами Параметр IpFiieTime этой функции должен указывать на структуру типа filetime, которая содержит время в формате, используемом для хранения в файловой системе. Параметр lpSystemTime должен указывать на структуру типа systemtime, которая имеет следующий формат: typedef struct _SYSTEMTIME { WORD WORD WORD WORD WORD WORD WORD WORD wYear; wMonth; wDayOfWeek; wDay; wHour; wMinute; wSecond; // // // // // // // wMilliseconds; год месяц день недели день час минута секунда // миллисекунда } SYSTEMTIME, *LPSYSTEMTIME; В листинге 24.16 приведена программа, которая использует функцию FiieTimeToSystemTime для перевода времени из формата файловой системы в системный формат. \ Листинг 24.16. Преобразование времени в системный формат \ # include <windows.h> #include <iostream.h> int main () { HANDLE hFile; BY_HANDLE_FILE_INFORMATION bhfi; // информация о файле SYSTEMTIME st; // системное время // открываем файл для чтения hFile = CreateFile( "С: Wdemo_file.dat", О, О, NULL, OPEN EXISTING, // имя файла // получение информации о файле // монопольный доступ к файлу // защиты нет // открываем существующий файл FILE_ATTRIBUTE_NORMAL, // обычный файл NULL // шаблона нет
Глава 24. Работа с файлами в Windows 463 ); // проверяем на успешное открытие 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; } // получаем информацию о файле if (IGetFilelnformationByHandle(hFile, &bhfi)) { cerr « "Get file information by handle failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.get(); return 0; } // переводим время создания файла в системное время if (!FileTimeToSystemTime(&(bhfi.ftCreationTime), &st)) { cerr « "File time to system time failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.get(); return 0; } // распечатываем системное время cout « "File creation time in system format: " « endl « "\tYear: " « st.wYear « endl « "\tMonth: " « st.wMonth « endl « "\tDay of week: " « st.wDayOfWeek « endl « "\tDay: " « st.wDay « endl « "\tHour: " « st.wHour « endl « "\tMinute: " « st.wMinute « endl
464 Часть УН. Управление файлами « "\tSecond: " « st.wSecond « endl « "\tMilliseconds: " « st.wMilliseconds « endl; // закрываем дескриптор файла CloseHandle(hFile); return 0; } Определить тип файла можно при помощи функции GetFileType, которая имеет следующий прототип: DWORD GetFileType( HANDLE hFile // дескриптор файла ); Единственным параметром этой функции является дескриптор файла. Функция GetFileType возвращает одно из следующих значений: □ file_type_unknown — неизвестный тип файла; □ file_type_disk — дисковый файл; □ file_type_char — символьный файл; □ file_type_pipe — именованный или анонимный канал. Отметим, что под символьным файлом обычно понимается принтер или консоль. В листинге 24.17 приведена программа, которая определяет тип файла, используя функцию GetFileType. | Листинг 24,17, Определение типа файла #include <windows.h> #include <iostream.h> int main() { HANDLE hFile; DWORD dwF i1eType; // открываем файл для чтения hFile = CreateFile( "С: \\demo_file.dat", // имя файла
Глава 24. Работа с файлами в Windows 465 О, // получение информации о файле О, // монопольный доступ к файлу NULL, // защиты нет OPEN_EXISTING, // открываем существующий файл 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; } // определяем тип файла dwFileType = GetFileType(hFile); // распечатываем тип файла switch (dwFileType) { case FILE_TYPE_UNKNOWN: cout « "Unknown type file." « endl; break; case FILE_TYPE_DISK: cout « "Disk type file." « endl; break; case FILE_TYPE_CHAR: cout « "Char type file." « endl; break; case FILE_TYPE_PIPE: cout « "Pipe type file." « endl; break; default: break; } return 0;
466 Часть УН. Управление файлами На платформе Windows NT можно определить, является ли файл исполняемым. ДЛЯ ЭТОГО НУЖНО ИСПОЛЬЗОВатЬ фунКЦИЮ GetBinaryType, Которая Имеет следующий прототип: BOOL GetBinaryType( LPCTSTR lpApplicationName, // имя приложения LPDWORD lpBinaryType // тип исполняемого файла ); Если файл является исполняемым, то эта функция возвращает ненулевое значение, в противном случае — false. В параметре lpApplicationName устанавливается указатель на строку, которая содержит имя проверяемого файла. Параметр lpBinaryType должен указывать на переменную типа dword, в которую функция GetBinaryType помещает тип исполняемого файла. Этот тип может принимать одно из следующих значений: □ scs_32bit_binary — приложение Win32; □ scs_dos_binary — приложение MS-DOS; □ scs_os216_binary — 16-битовое приложение OS/2; □ scs_pif_binary — PIF-файл; □ scs_posix_binary — приложение POSIX; □ scs_wow_binary — 16-битовое приложение Windows. В листинге 24.18 приведена программа, которая определяет тип исполняемого файла, ИСПОЛЬЗуя функцию GetBinaryType. ЙййгЙнг 2418, Определение типа исполняемого файла #include <windows.h> #include <iostream.h> int main () { DWORD dwBinaryType; // определяем тип файла if ( ! GetBinaryType (" С: \ \ temp. exe", ScdwBinaryType) ) { cerr « "Get binary type failed." « endl « "The file may not be executable." « endl
Глава 24. Работа с файлами в Windows 467 « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.getO ; return 0; } // распеча«1?ываем тип файла if (dwBinaryType == SCS_32BIT_BINARY) cout « "The file is Win32 based application." « endl; else cout « "The file is not Win32 based application." « endl; return 0;
Глава 25 Работа с каталогами (папками) в Windows 25.1. Создание каталога Для создания каталога используется функция CreateDirectory, которая имеет следующий прототип: BOOL CreateDirectory( LPCTSTR lpPathName, // имя каталога LPSECUTITY_ATTRIBUTES IpSecurutyAttributes // атрибуты защиты ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. В параметре lpPathName задается указатель на символьную строку, которая содержит имя создаваемого каталога, а параметр IpSecurutyAttributes задает атрибуты безопасности этого каталога. В листинге 25.1 приведена программа, которая создает каталог. | Листинг 25.1. Пример создания каталога # include <windows.h> #include <iostream.h> int main() { // создаем каталог if (!CreateDirectory("C:\\demo_dir", NULL)) { cerr « "Create directory failed." << endl « "The last error code: " « GetLastError() << endl; cout « "Press any key to finish.";
Глава 25. Работа с каталогами (папками) в Windows 469 cin.get(); return 0; } cout « "The directory is created." « endl; return 0; } После создания каталога можно получить его дескриптор, используя для этого ФУНКЦИЮ CreateFile С установленным флаГОМ FILE_FLAG_BACKUP_SEMANTICS. ДЛЯ СОЗДаНИЯ подкаталогов МОЖНО ИСПОЛЬЗОВатЬ фуНКЦИЮ CreateDirectoryEx, которая позволяет наследовать атрибуты другого каталога, который в этом случае называется шаблонным каталогом. В качестве шаблонного каталога можно указать родительский каталог, в котором создается текущий каталог. ФуНКЦИЯ CreateDirectoryEx имеет СЛедуЮЩИЙ ПРОТОТИП! BOOL CreateDirectoryEx( LPCTSTR lpTemplateDirectory, // имя шаблонного каталога LPCTSTR lpNewDirectory, // имя нового каталога LPSECUTITY_ATTRIBUTES IpSecurutyAttributes // атрибуты зашиты ); Эта функция отличается от функции createDirectory только наличием параметра lpTemplateDirectory, который содержит указатель на имя шаблонного каталога. Например, программа из листинга 25.2 создает подкаталог в каталоге, созданном программой из листинга 25.1. } Листинг 25.2, Пример создания подкаталога #include <windows.h> #include <iostream.h> int main () { // создаем подкаталог if (! CreateDirectoryEx ("С: \\demo_dir", "С: \\demo_dir\\demo_subdir", NULL)) { cerr « "Create directory failed." « endl
470 Часть VII. Управление файлами « "The last error code: " « GetLastError() « endl; cout << "Press any key to finish."; cin.get(); return 0; } cout « "The subdirectory is created." « endl; return 0; } 25.2. Поиск файлов в каталоге Для поиска файлов, находящихся в каталоге, используются функции FindFirstFile И FindNextFile. Причем фуНКЦИЯ FindFirstFile НахоДИТ первый файл, имя которого удовлетворяет образцу поиска, а функция FindNextFile находит последующие такие файлы. При этом отметим, что в образцах имен файлов для поиска могут использоваться символы- заместители ? и *. Символ ? замещает один неизвестный символ в имени файла, а символ * — любую последовательность символов. Рассмотрим функцию FindFirstFile, которая предназначена для нахождения первого файла в каталоге. Эта функция имеет следующий прототип: HANDLE FindFirstFile( LPCTSTR lpFileName, // образец имени для поиска LPWIN32_FIND_DATA lpFindFileData // адрес данных о файле ); В случае успешного завершения функция FindFirstFile возвращает дескриптор для поиска файлов, который используется в дальнейшем функцией FindNextFile, а в случае неудачи — значение invalid_handle_value. В параметре lpFileName функции должен быть установлен указатель на символьную строку, которая содержит образец имени файла для поиска. Параметр lpFindFileData должен указывать на структуру типа win3 2_find_data, в которую функция запишет о найденном файле. Эта структура имеет следующий формат: typedef struct _WIN32_FIND_DATA { DWORD dwFileAttributes; // атрибуты файла FILETIME ftCreationTime; // время создания файла FILETIME ftLastAccessTime; // время последнего доступа к файлу
Глава 25. Работа с каталогами (папками) в Windows 471 FILETIME ftLastWriteTime; // время последней записи в файл DWORD nFileSizeHigh; // старшая часть размера файла DWORD nFileSizeLow; // младшая часть размера файла DWORD dwReservedO; // тег для преобразования файла DWORD dwReservedl; // не используется CHAR cFileName[ MAX_PATH ]; // длинное имя файла CHAR cAlternateFileName[ 14 ] ; // короткое имя файла } WIN32_FIND_DATA/ *PWIN32_FIND_DATA, *LPWIN32_FIND_DATA; В случае успешного завершения функция FindNextFiie записывает в эту структуру данные о первом найденном файле. Отметим, что в поле dwFileAttributes структуры WIN32_FIND_DATA МОГуТ бЫТЬ установлены СЛе- дующие флаги: П file_attribute_archive — архивный файл; П file_attribute_compressed — сжатый файл; П FILE_ATTRIBUTE_DIRECTORY — КаТЭЛОГ; П file_attribute_encrypted — зашифрованный файл; П file_attribute_hidden — скрытый файл; П file_attribute_normal — нормальный файл; П file_attribute_offline — файл во внешней памяти; П file_attribute_readonly — файл предназначен только для чтения; П file_attribute_reparse_point — файл содержит точку преобразования; П file_attribute_sparse_file — разряженный файл; П file_attribute_system — системный файл; □ file_attribute_temporary — временный файл. Теперь рассмотрим функцию FindNextFiie, которая предназначена для нахождения в каталоге последующих файлов после первого найденного файла. Порядок перечисления файлов зависит от файловой системы. В файловой системе FAT файлы перечисляются в порядке их создания, а в файловых системах NTFS и CDFS — перечисляются в алфавитном порядке. Эта функция имеет следующий прототип: BOOL F indNextFi1е( HANDLE hFindFile, // дескриптор для поиска файлов LPWIN32_FIND_DATA lpFindFileData // адрес данных о файле ); В случае успешного завершения функция FindNextFiie возвращает ненулевое значение, а в случае неудачи — false.
472 Часть VII. Управление файлами В параметре hFiieFind этой функции должен быть установлен дескриптор ДЛЯ ПОИСКа фаЙЛОВ, КОТОРЫЙ был ПОЛучеН ВЫЗОВОМ фуНКЦИИ FindFirstFile. Параметр lpFindFiieData должен содержать адрес структуры типа WIN32_FIND_DATA, В КОТОруЮ фуНКЦИЯ FindNextFile В Случае СВОСГО успеш- ного_завершения поместит информацию о следующем найденном файле. Особенно отметим, что как функция FindFirstFile, так и функция FindNextFile ВОЗВращаЮТ В Структуре ТИПа WIN32_FIND_DATA Информацию как о файлах, так и о подкаталогах. В листинге 25.3 приведена программа, которая выводит на консоль информацию обо всех файлах и подкаталогах заданного каталога. Как видно из результатов, в этот список входят текущий и родительский каталоги. Кроме того, отметим, что после завершения поиска файлов нужно вызвать функцию Findciose, которая закрывает дескриптор поиска файлов и имеет следующий прототип: BOOL FindClose( HANDLE hFindFile // дескриптор поиска файла ); Функция Findciose имеет единственный параметр hFindFile, который должен содержать дескриптор поиска файла. В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — FALSE. | Листинг 25*3* Пример поиска файлов в каталоге #include <windows.h> #include <iostream.h> int main() { HANDLE hFindFile; WIN32_FIND_DATA fd; // находим первый файл hFindFile = FindFirstFile("С:\\demo_dir\\* ", &fd) ; if (hFindFile == INVALID_HANDLE_VALUE) { cerr « "Find first file failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish.";
Глава 25. Работа с каталогами (папками) в Windows 473 cin.get(); return 0; }. // выводим на консоль имя первого файла cout « "The first file name: " « fd.cFileName « endl; // находим следующий файл и выводим на консоль его имя while (FindNextFile(hFindFile, &fd)) cout « "The next file name: " « fd.cFileName « endl; // закрываем дескриптор поиска FindClose(hFindFile); return 0; } Для более продвинутого поиска файлов может использоваться функция FindFirstFiieEx, которая позволяет учитывать при поиске файла не только его имя, но также и его атрибуты. Отметим, что эта функция поддерживается только в операционных системах Windows NT/2000/XP. 25.3. Удаление каталога ДЛЯ удаления ПУСТОГО каталога Предназначена функция RemoveDdirectory, которая имеет следующий прототип: BOOL RemoveDirectory ( LPCTSTR lpPathName // имя каталога ); Единственный параметр этой функции должен указывать на символьную строку, содержащую имя удаляемого каталога. В случае успешного завершения функция RemoveDdirectory возвращает ненулевое значение, а в случае неудачи — false. В листинге 25.4 приведена программа, в которой удаляется каталог, используя функцию RemoveDdirectory. Еще раз отметим, что для успешного удаления каталог должен быть пуст. j Листинг 25А Пример удаления пустого каталога #include <windows.h> #include <iostream.h> int main()
474 Часть VII. Управление файлами { // удалаем каталог if ( ! RemoveDirectory ("С: \\demo_dir") ) { cerr « "Remove directory failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.get(); return 0; } cout « "The directory is removed." « endl; return 0; } На практике каталог редко бывает пустым, поэтому прежде чем удалить сам каталог, нужно удалить все находящиеся в нем подкаталоги и файлы. Опишем, как удалять из каталога файлы. Этот же подход используется и для удаления файлов из подкаталогов. В листинге 25.5 приведена программа, которая удаляет из каталога файлы, а затем удаляет сам каталог. ^Листинг 25.5. Пример удаления каталога о файлами #include <windows.h> #include <iostream.h> #include <stdio.h> int main() { HANDLE hFindFile; WIN32_FIND_DATA fd; char szFullFileName[MAX_PATH] ; // находим первый файл hFindFile = FindFirstFile("С:\\demo_dir\\*", &fd) ; if (hFindFile == INVALID_HANDLE_VALUE) { cerr « "Find first file failed." « endl
Глава 25. Работа с каталогами (папками) в Windows 475 « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.get(); return 0; } // выводим на консоль имя первого файла cout « "The first file name: " « fd.cFileName « endl; // удаляем из каталога файлы while (FindNextFile(hFindFile, &fd)) { // если это не подкаталог, то удаляем его if ({(fd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) { // формируем имя файла sprintf(szFullFileName, "С:\\demo_dir\\%s", fd.cFileName); // удаляем файл if ("DeleteFile(szFullFileName)) { cerr « "Delete file failed." « endl. « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.get(); return 0; } else cout « "The next file: " « fd.cFileName « " is deleted." « endl; } else cout « "The next directory: " « fd.cFileName « " is not deleted." « endl; } // закрываем дескриптор поиска if (!FindClose(hFindFile)) { cout « "Find close failed." « endl; return (); } // удалаем каталог if (IRemoveDirectory("C:\\demo_dir")) { cerr « "Remove directory failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish.";
476 Часть VII. Управление файлами cin.get(); return 0; } cout « "The directory is removed." « endl; // закрываем дескриптор поиска FindClose(hFindFile); return 0; } 25.4. Перемещение каталога Так же как и файлы, каталоги можно перемещать при помощи функции MoveFile. В этом случае на исполнение функции MoveFile накладывается одно ограничение, которое заключается в том, что можно перемещать только каталоги, которые находятся на одном томе. Напомним, что функция MoveFile имеет следующий прототип: BOOL MoveFile( LPCTSTR IpExistingFileName, // имя существующего файла LPCTSTR lpNewFileName // имя нового файла ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — значение false. В случае перемещения каталогов параметры IpExistingFileName И lpNewFileName ДОЛЖНЫ указывать на Строки, которые содержат имена перемещаемого и нового каталогов соответственно. В листинге 25.6 приведена программа, которая выполняет перемещение каталога, ИСПОЛЬЗУЯ фуНКЦИЮ MoveFile. | Листинг 25.$. Перемещение каталога #include <windows.h> #include <iostream.h> int mainO { // перемещаем каталог
Глава 25. Работа с каталогами (папками) в Windows 477 if(!MoveFile("С:\\demo_dir", "С:\\new_dir")) { cerr « "Move file failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.get(); return 0; } cout << "The directory is moved." « endl; return 0; } 25.5. Определение и установка текущего каталога Каталог, из которого стартует приложение, называется текущим каталогом этого приложения. Как правило, в текущем каталоге находятся необходимые для работы приложения, исполняемые файлы и библиотеки. Определить ИМЯ текущего каталога МОЖНО при ПОМОЩИ функции GetCurrentDirectory, которая имеет следующий прототип: DWORD GetCurrentDirectory( DWORD nBufferLength, // длина буфера для имени каталога LPTSTR lpBuffer // адрес буфера для имени каталога ); Если размер буфера достаточен для имени текущего каталога, то функция GetCurrentDirectory в случае успешного завершения возвращает количество символов, записанных в буфер, на который указывает параметр lpBuf f er, а в случае неудачи — о. Если же размер буфера недостаточен для имени каталога, то функция GetCurrentDirectory возвращает необходимую длину буфера для имени текущего каталога, включая завершающий пустой символ. В параметре nBuf ferLength должна быть задана длина буфера, куда функция GetCurrentDirectory записывает имя текущего каталога. Параметр lpBuffer должен указывать на буфер, куда функция GetCurrentDirectory записывает имя текущего каталога.
478 Часть VII. Управление файлами Приложение может изменить имя текущего каталога, используя функцию SetcurrentDirectory, которая имеет следующий прототип: BOOL SetcurrentDirectory( LPCTSTR lpPathName // имя нового текущего каталога ); В случае успешного завершения функция SetcurrentDirectory возвращает ненулевое значение, а в случае неудачи — false. Параметр lpPathName должен указывать на символьную строку, которая содержит имя нового текущего каталога. В листинге 25.7 приведена профамма, которая определяет имя текущего каталога, а затем устанавливает новый каталог в качестве текущего каталога. [р?1| ' "Т" " ''"'" v "' ' " [Листинг 2S.7. Определение и установка текущего каталога # include <windows.h> #include <iostream.h> int raain() { DWORD dwNumberO fChar; char s zDi rName [ MAX_PATH ] ; // определяем имя текущего каталога dwNumberOfChar = GetCurrentDirectory(MAX_PATH/ szDirName) ; if (dwNumberOfChar == 0) { cerr « "Get current directory failed." « endl << "The last error code: " « GetLastError() « endl; cout << "Press any key to finish."; cin.get(); return 0; } // выводим на консоль имя текущего каталога cout « "Current directory name: " « szDirName « endl; // устанавливаем текущий каталог для удаления из него файлов if (ISetCurrentDirectory("С:\\demo_dir"))
Глава 25. Работа с каталогами (папками) в Windows 479 cerr « "Set current directory failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.getO ; return 0; } // определяем имя нового текущего каталога dwNumberOfChar = GetCurrentDirectory(MAX_PATH, szDirName); if (dwNumberOfChar == 0) { cerr « "Get current directory failed." « endl « "The last error code: " << GetLastError() « endl; cout « "Press any key to finish."; cin.get(); return 0; } // выводим на консоль имя нового текущего каталога cout « "Current directory name: " « szDirName « endl; return 0; } 25.6. Наблюдение за изменениями в каталоге В операционных системах Windows предусмотрены функции, позволяющие следить за изменениями, происходящими в каталогах. Для этих целей предназначены ФУНКЦИИ FindFirstChangeNotification И FindFirstChangeNotif i cat ion, Которые отслеживают соответственно первое и последующие изменения, происходящие в каталоге. Сначала рассмотрим функцию FindFirstChangeNotification, которая имеет следующий прототип: HANDLE FindFirstChangeNotification( LPCTSTR lpPathName, // имя каталога BOOL bWatchSubtree, // опция наблюдения DWORD dwNotifyFilter // условия фильтра
480 Часть VII. Управление файлами В случае успешного завершения фунКЦИЯ FindFirstChangeNotification возвращает дескриптор для наблюдения за каталогом. Этот дескриптор может использоваться в функциях ожидания для отслеживания изменений в каталоге. В случае неудачи фунКЦИЯ FindFirstChangeNotification ВОЗВра- щает значение invalid_handle_value. Параметр lpPathName должен указывать на символьную строку, содержащую имя каталога. Параметр bwatchsubtree определяет структуру дерева каталогов, которые необходимо наблюдать. Если этот параметр равен false, to система будет отслеживать только изменения в заданном каталоге. Если же значение этого параметра равно true, to система будет отслеживать изменения как в заданном каталоге, так и в подкаталогах, содержащихся в нем. Параметр dwNotifyFiiter задает события, которые отслеживаются. Эти события соответствуют флагам, которые могут быть установлены в этом параметре. Возможно установить следующие флаги: □ file_notify_change_file_name — изменение имени файла; □ file_notify_change_dir_name — изменение имени каталога; □ file_notify_change_attributes — изменение атрибутов; □ file_notify_change_size — изменение размеров; □ file_notify_change_last_write — изменение времени последней записи; □ file_notify_change_security — изменение атрибутов защиты. Теперь рассмотрим функцию FindNextchangeNotif ication, которая предназначена для наблюдения за последующими изменениями, происходящими в каталоге. Эта функция имеет следующий прототип: BOOL FindNextChangeNotification( HANDLE hChangeHandle // дескриптор для наблюдения за изменениями ); В случае успешного завершения фунКЦИЯ FindNextChangeNotification ВОЗ- вращает ненулевое значение, а в случае неудачи — false. В параметре hChangeHandle должен быть установлен дескриптор для наблюдения за изменениями в каталоге, который был получен вызовом функции FindFirstChangeNotification. После завершения наблюдения за изменениями в каталоге нужно вызвать функцию FindcioseChangeNotif ication, которая закрывает дескриптор наблюдения за изменениями в каталоге. Эта функция имеет следующий прототип: BOOL FindcioseChangeNotification( HANDLE hChangeHandle // дескриптор для наблюдения за изменениями
Глава 25. Работа с каталогами (папками) в Windows 481 ФУНКЦИЯ FindCloseChangeNotification имеет единственный параметр hchangeHandie, который должен содержать дескриптор наблюдения за изменениями в каталоге. В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. В листинге 25.8 приведена программа, которая наблюдает за изменениями в каталоге. Программа отслеживает только два события, каждое из которых может быть вызвано или изменением имени файла, хранящегося в каталоге, или изменением размера каталога. | Листинг 25*8» Пример отслеживания изменений ш каталоге #include <windows.h> #include <iostream.h> int main() { HANDLE hChangeHandle; // находим первое изменение в каталоге hchangeHandie = FindFirstChangeNotification( "С:\\demo_dir", // имя каталога TRUE, // отслеживать также подкаталоги FILE_NOTIFY_CHANGE_FILE_NAME // отслеживать изменение имени каталога I FILE_NOTIFY_CHANGE_SIZE //и изменение его размера ); if (hchangeHandie == INVALID_HANDLE_VALUE) { cerr « "Find first change notification failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.get(); return 0; } // нужно изменить каталог cout « "Wait for changes in the directory." « endl; // ждем первого изменения в каталоге if (WaitForSingleObject(hchangeHandie, INFINITE) == WAIT_OBJECT_0) 16 Зак. 514
482 Часть VII. Управление файлами cout « "First notification: the directory was changed." « endl; else { cerr « "Wait for single object failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.getO ; return 0; } // находим второе изменение в каталоге if (!FindNextChangeNotification(hChangeHandle)) { cerr « "Find next change notification failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.getO ; return 0; } // ждем второго изменения в каталоге if (WaitForSingleObject(hChangeHandle, INFINITE) == WAIT_OBJECT_0) cout « "Next notification: the directory was changed." « endl; else { cerr « "Wait for single object failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.get(); return 0; } // закрываем дескриптор поиска FindCloseChangeNotification(hChangeHandle); return 0;
Часть VIII Асинхронная обработка данных Глава 26. Асинхронный вызов процедур Глава 27. Асинхронный доступ к данным Глава 28. Порты завершения Глава 29. Работа с ожидающим таймером
Глава 26 Асинхронный вызов процедур 26.1. Механизм асинхронного вызова процедур Асинхронной процедурой называется функция, которая исполняется асинхронно в контексте какого-либо потока. Для исполнения асинхронной процедуры необходимо выполнить три условия: □ определить асинхронную процедуру; □ определить поток, в контексте которого эта процедура будет выполняться; □ дать разрешение на выполнение асинхронной процедуры. Рассмотрим подробнее каждое из этих условий. Асинхронная процедура должна определяться функцией, которая имеет следующий прототип: VOID CALLBACK имя_асинхронной_процедуры (DWORD dwParam) ; Отсюда видно, что асинхронная процедура не возвращает значения и должна иметь только один параметр. Теперь рассмотрим, как определятся поток, в контексте которого исполняется асинхронная процедура. Для этого в операционных системах Windows существует функция QueueuserAPC, которая связывает асинхронную процедуру с потоком. Как работает эта функция, будет рассмотрено в разд. 26.2. Сейчас же скажем, что каждый поток имеет очередь асинхронных процедур и, вызвав функцию QueueuserAPC, мы помещаем нашу асинхронную процедуру в эту очередь. Очередь асинхронных процедур работает по алгоритму FIFO (см. разд. 20.3). Фактически каждый поток имеет две очереди асинхронных процедур: одну пользовательскую, а вторую системную. В пользовательскую очередь асинхронная процедура ставится при помощи функции QueueuserAPC, а в системную очередь асинхронная процедура ставится операционной системой.
486 Часть VIII. Асинхронная обработка данных Наконец, рассмотрим, как вызывается асинхронная процедура. Для вызова асинхронной процедуры необходимо, чтобы поток находился в настороженном (alertable) состоянии. Перевести поток в настороженное состояние можно При ПОМОЩИ ВЫЗОВа функций SleepEx, WaitForSingleObjectEx, WaitForMultipleObjectsEx И SignalObjectAndWait. Все ЭТИ функции будут рассмотрены в следующих разделах. Если поток входит в настороженное состояние, то при следующем выделении кванта времени этому потоку сначала выполняются все асинхронные процедуры этого потока, затем настороженное состояние потока сбрасывается и начинает выполняться сам поток. Отметим, что системные асинхронные процедуры вызываются всегда перед передачей управления потоку. 26.2. Установка асинхронных процедур Для установки асинхронной процедуры в очередь потока используется функция QueueUserAPC, которая имеет следующий прототип: DWORD QueueUserAPC( PAPCFUNC pfnAPC, // имя асинхронной процедуры HANDLE hThread, // дескриптор потока DWORD dwData // параметр асинхронной процедуры ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — 0. При этом отметим, что в случае неудачи не устанавливается код последней ошибки, который может быть получен вызовом ФУНКЦИИ GetLastError. Параметр pfnAPC должен содержать имя функции, которая включается в очередь асинхронных процедур. Причем прототип этой функции должен совпадать с прототипом асинхронной процедуры, который был рассмотрен в предыдущем параграфе. В параметре hThread должен быть установлен дескриптор потока, в очередь асинхронных процедур которого включается функция, заданная параметром pfnAPC. В параметр dwData устанавливается значение, которое передается асинхронной процедуре при ее вызове. Примеры установки и вызова асинхронных процедур будут рассмотрены в следующих разделах, посвященных функциям перевода потока в настороженное состояние.
Глава 26. Асинхронный вызов процедур 487 26.3. Приостановка потока Поток может перевести себя в состояние ожидания до истечения некоторого интервала времени или вызова асинхронной процедуры посредством вызова функции sieepEx, которая имеет следующий прототип: DWORD SleepEx( DWORD dwMilliseconds, // интервал времени BOOL bAlertable // режим завершения ); В случае истечения заданного интервала времени эта функция возвращает значение 0. Если же завершение функции вызвано другими причинами, то функция sieepEx возвращает ненулевое значение. При этом, если возврат из функции происходит по причине завершения операции асинхронного ввода- вывода, то функция возвращает значение wait_io_completion. Параметр dwMiiiiseconds задает в миллисекундах интервал времени, на который может быть заблокирован поток. Для задания бесконечного интервала времени нужно использовать значение infinite. Параметр bAlertable может принимать одно из двух значений — false или true. Если установлено значение false, to поток не переходит в настороженное состояние. Следовательно в этом случае асинхронные процедуры не вызываются и поток блокируется на заданный интервал времени. Если же в этом параметре установлено значение true, to поток переходит в настороженное состояние и блокируется до тех пор, пока не выполнится одно из следующих условий: □ закончится заданный интервал времени ожидания; □ асинхронная процедура поставлена в очередь потока; □ процедура завершения асинхронной операции ввода-вывода поставлена в очередь потока. В программе из листинга 26.1 показано, как установить асинхронную процедуру и вызвать ее при помощи функции sieepEx. | Листинг 26.1. Вызов асинхронной процедуры путем приостановки потока '' #define _WIN32_WINNT 0x0400 // версия не ниже, чем Windows NT 4.0 # include <windows.h> #include <iostream.h> HANDLE hThread; // дескриптор потока
488 Часть VIII. Асинхронная обработка данных DWORD IDThread; // идентификатор потока DWORD dwRet; // возвращаемое значение при установке асинхронной // процедуры // процедура, которая вызывается асинхронно void CALLBACK a_prос(DWORD p) { int n; DWORD *ptr = (DWORD*)p; cout « "The asynchronous procedure is called." « endl; // ждем ввода целого числа cout « "Input integer: "; cin » n; // увеличиваем счетчик *ptr += n; cout « "The asynchronous procedure is returned." « endl; }л // поток, в очередь которого включается асинхронная процедура DWORD WINAPI add(LPVOID ptr) { // распечатываем начальное значение счетчика cout « "Initial count = " « *(DWORD*)ptr « endl; // ждем, пока выполнится асинхронная процедура SleepEx(INFINITE, TRUE); // распечатываем конечное значение счетчика cout « "Final count = " « *(DWORD*)ptr « endl; return 0; } // главный поток int raain() { DWORD count = 10; // запускаем поток
Глава 26. Асинхронный вызов процедур 489 hThread = CreateThread(NULL, 0, add, &count, 0, &IDThread); if (hThread == NULL) return GetLastError(); SleepA000); // чтобы успел стартовать поток // устанавливаем асинхронную процедуру для потока dwRet = QueueUserAPC(a_proc, hThread, (DWORD) &count); if (IdwRet) { cout « "Queue user APC failed:" « dwRet « endl; return 0; } // ждем, пока поток add закончит работу WaitForSingleObject(hThread, INFINITE); // закрываем дескриптор потока CloseHandle(hThread); return 0; } В заключение этого раздела сделаем следующее замечание: включить асинхронную процедуру в очередь потока может как сам поток, так и любой другой поток. В нашем случае поток main включает асинхронную процедуру аргос в очередь асинхронных процедур потока add. 26.4. Ожидание события Для ожидания наступления некоторого события или выполнения асинхронной процедуры можно использовать функцию waitForSingieObjectEx, которая имеет следующий прототип: DWORD WaitForSingieObjectEx( HANDLE hHandle, // дескриптор объекта DWORD dwMilliseconds, // временной интервал в миллисекундах BOOL bAlertable // режим завершения ); В случае успешного завершения эта функция возвращает одно из следующих значений: П waitobjecto — объект в сигнальном состоянии; □ wait_abandoned — забытый мьютекс;
490 Часть VIII. Асинхронная обработка данных □ wait_io_completion — асинхронная процедура поставлена в очередь; □ wait_timeout — истек временной интервал. В случае неудачи функция WaitForSingleObjectEx возвращает -1. Кратко опишем назначение параметров этой функции. Параметр hHandle должен содержать дескриптор объекта, сигнальное состояние которого ожидает функция. Параметр dwMiiiiseconds должен содержать временной интервал, заданный в миллисекундах, в течение которого функция ожидает перехода объекта в сигнальное состояние или включение асинхронной процедуры в очередь потока. Параметр bAiertable может принимать одно из двух значений — false или true. Если установлено значение false, to поток не переходит в настороженное состояние. Следовательно в этом случае асинхронные процедуры не вызываются и поток блокируется на заданный интервал времени. Если же в этом параметре установлено значение true, to поток переходит в настороженное состояние и блокируется до тех пор, пока не выполнится одно из следующих условий: □ закончится заданный интервал времени ожидания; □ асинхронная процедура поставлена в очередь потока; □ процедура завершения асинхронной операции ввода-вывода поставлена в очередь потока. В ЛИСТИНГе 26.2 Приведена Программа, В КОТОРОЙ фунКЦИЯ WaitForSingleObjectEx используется для ожидания события или включения асинхронной процедуры в очередь потока. Листинг 26,2, Ожидание события или вызова асинхронной процедуры #define _WIN32_WINNT 0x0400 #include <windows.h> #include <iostream.h> HANDLE hEvent; // дескриптор события HANDLE hThread; // дескриптор потока DWORD IDThread; // идентификатор потока DWORD dwRet; // код возврата при установке асинхронной процедуры // процедура, которая вызывается асинхронно
Глава 26. Асинхронный вызов процедур 491 void CALLBACK a_proc(DWORD p) { int n; DWORD *ptr = (DWORD* )p; cout « "The asynchronous procedure is called." « endl; // ждем ввода целого числа cout « "Input integer: "; cin » n; // увеличиваем счетчик *ptr += n; cout « "The asynchronous procedure is returned." « endl; } // поток, в очередь которого включается асинхронная процедура DWORD WINAPI add(LPVOID ptr) { DWORD dwRet; // код возврата из функции ожидания // распечатываем начальное значение счетчика cout « "Initial count = " « *(DWORD*)ptr « endl; // ждем timeout, или наступление события, // или завершение асинхронной процедуры dwRet = WaitForSingleObjectEx(hEvent, 10, TRUE); switch (dwRet) { case WAIT_OBJECT_0: cout « "The event is signaled." « endl; break; case WAIT_IO_COMPLETION: cout « "The asynchronous procedure is done." « endl; break; case WAIT_TIMEOUT: cout « "Time out." « endl; break; default: cout « "Another case." « endl; break;
492 Часть VIII. Асинхронная обработка данных } // распечатываем конечное значение счетчика cout « "Final count = " « *(DWORD*)ptr « endl; return 0; } // главный поток int main() { char c; DWORD count = 10; // спрашиваем, что установить: событие или асинхронную процедуру cout « "Input: " « endl « "\te - to set event" « endl « "\ta - to set asynchronous procedure" « endl « "\tanother char - timeout" « endl « "->"; cin » c; // запускаем поток hThread = CreateThread(NULL, 0, add, &count, 0, ScIDThread) ; if (hThread == NULL) return GetLastError(); // создаем событие с автоматическим сбросом hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); if (hEvent == NULL) return GetLastError(); switch (c) { case 'e': // устанавливаем событие SetEvent(hEvent); break; case 'a': // устанавливаем асинхронную процедуру для потока dwRet = QueueUserAPC(a_proc, hThread, (DWORD) &count);
Глава 26. Асинхронный вызов процедур 493 if (IdwRet) { cout « "Queue user APC failed:" « dwRet « endl; return 0; } break; default: break; } // ждем, пока поток add закончит работу- Wait For Sing leObj ее t (hThread, INFINITE); // закрываем дескриптор потока CloseHandle(hThread); return 0; } Для ожидания сигнального состояния нескольких объектов синхронизации МОЖНО ИСПОЛЬЗОВатЬ фуНКЦИЮ WaitForMultipleObjectsEx, которая имеет следующий прототип: DWORD Wa i tForMu11 ipleeObj eсtsEx( DWORD nCount, // количество объектов синхронизации CONST HANDLE lpHandles, // массив дескрипторов объектов BOOL bWaitAll, // режим ожидания DWORD dwMilliseconds, // временной интервал в миллисекундах BOOL bAlertable // режим завершения ); В случае успешного завершения эта функция возвращает одно из следующих значений: □ в интервале от значения wait_object_o до значения wait_object_o + nCount - 1; □ в интервале от значения wait_abandoned до значения wait_abandoned + nCount - 1; П wait_io_completion; П WAIT_TIMEOUT. Значения wait_io_completion и wait_timeout имеют тот же смысл, что и в функции waitForsingieobjectEx, а первые два значения интерпретируются в зависимости от значения параметра bWaitAll.
494 Часть VIIL Асинхронная обработка данных Если параметр bwaitAii равен true, to любое значение (от wait_object_o до wait_object_o + ncount - l) означает, что все объекты синхронизации находятся в сигнальном состоянии. В противном случае возвращаемое значение минус wait_object_o является индексом элемента массива, заданного параметром lpHandies, и этот индекс указывает на дескриптор объекта, который перешел в сигнальное состояние. Если таких объектов несколько, то индекс имеет наименьшее из возможных значений. Далее, если значение параметра bwaitAii установлено в true, to любое значение ОТ WAIT_ABANDONED ДО WAIT_ABANDONED + nCount - 1 Означает, ЧТО ВСе объекты синхронизации находятся в сигнальном состоянии и, по крайней мере, один из них является забытым мьютексом. В противном случае возвращаемое значение минус wait_object_o является индексом элемента массива, заданного параметром lpHandies, и этот индекс указывает на забытый мьютекс. Теперь Перейдем К Параметрам фуНКЦИИ WaitForMultipleObjectsEx. Параметр ncount содержит количество объектов синхронизации, сигнальное состояние которых ожидает функция WaitForMultipleObjectsEx. Значение этого параметра не должно превышать величины maximum_wait_objects. Параметр lpHandies должен указывать на массив дескрипторов объектов синхронизации. Количество элементов этого массива должно совпадать с величиной параметра ncount. Если хотя бы один из дескрипторов закрывается ВО Время ОЖИДанИЯ фуНКЦИИ WaitForMultipleObjectsEx, TO поведение ЭТОЙ функции неопределено. Параметр bwaitAii задает режим ожидания функции WaitForMultipleObjectsEx. Если этот параметр установлен в true, то функция ждет перехода в сигнальное состояние всех объектов синхронизации. Если же значение этого параметра установлено в false, to функция ждет перехода в сигнальное состояние хотя бы одного объекта синхронизации. Параметры dwMiiiiseconds и bAiertabie имеют то же назначение, что и в фуНКЦИИ WaitForSingleObjectEx. 26.5. Оповещение и ожидание события Для оповещения о наступлении некоторого события и ожидании другого события или включения асинхронной процедуры в очередь потока используется функция signaiobjectAndwait, которая имеет следующий прототип: DWORD SignalObjectAndWait ( HANDLE hObjectToSignal, // дескриптор сигнального объекта HANDLE hObjectToWaitOn, // дескриптор ожидаемого объекта
Глава 26. Асинхронный вызов процедур 495 DWORD dwMilliseconds, // временной интервал в миллисекундах BOOL bAlertable // режим завершения ); В случае успешного завершения эта функция возвращает одно из следующих значений: □ wait_object_o — объект в сигнальном состоянии; □ wait_abandoned — забытый мьютекс; □ wait_io_completion — асинхронная процедура поставлена в очередь; □ wait_timeout — истек временной интервал. В случае неудачи функция waitForsingieobjectEx возвращает значение -1. Опишем параметры этой функции. Параметр hobjectTosignai должен содержать дескриптор объекта, сигнальное СОСТОЯНИе КОТОРОГО устанавливается функцией WaitForSingleObjectEx. Таким объектом может быть событие, мьютекс или семафор. Параметр hobjectToWaiton должен содержать дескриптор объекта, сигнальное состояние которого ожидает функция. Параметр dwMilliseconds должен содержать временной интервал, заданный в миллисекундах, в течение которого функция ожидает перехода объекта в сигнальное состояние или включение асинхронной процедуры в очередь потока. Параметр bAlertable может принимать одно из двух значений: false или true. Если установлено значение false, to поток не переходит в настороженное состояние. Следовательно, в этом случае асинхронные процедуры не вызываются и поток блокируется на заданный интервал времени. Если же в этом параметре установлено значение true, to поток переходит в настороженное состояние и блокируется до тех пор, пока не выполнится одно из следующих условий: □ закончится заданный интервал времени ожидания; □ асинхронная процедура поставлена в очередь потока; □ процедура завершения асинхронной операции ввода-вывода поставлена в очередь потока. Отметим, что функция signaiobjectAndwait работает только в операцион- ных системах Windows NT версии 4.0 и старше. В листинге 26.3 приведена программа, в которой эта функция используется для оповещения о наступлении события и одновременном ожидании наступления другого события или включения асинхронной процедуры в очередь потока.
496 Часть VIII. Асинхронная обработка данных #define _WIN32_WINNT 0x0400 #include <windows.h> #include <iostream.h> HANDLE HANDLE HANDLE DWORD DWORD hSignal; hWait; hThread; IDThread; dwRet; // дескриптор события, о котором сигнализируем // дескриптор события, которое ждем // дескриптор потока // идентификатор потока // возвращаемое значение при установке асинхронной // процедуры // процедура, которая вызывается асинхронно void CALLBACK а_ргос(DWORD p) { int n; DWORD *ptr = (DWORD*)p; cout « "The asynchronous procedure is called." « endl; // ждем ввода целого числа cout « "Input integer: "; cin » n; // увеличиваем счетчик *ptr += n; cout « "The asynchronous procedure is returned." « endl; } // поток, в очередь которого включается асинхронная процедура DWORD WINAPI add(LPVOID ptr) { DWORD dwRet; // код возврата из функции ожидания // распечатываем начальное значение счетчика cout « "Initial count = " « *(DWORD*)ptr « endl; // ждем timeout, или наступление события, // или завершение асинхронной процедуры
Глава 26. Асинхронный вызов процедур 497 dwRet = SignalObjectAndWait(hSignal, hWait, 10, TRUE); switch (dwRet) { case WAIT_OBJECT_0: cout « "The event is signaled." « endl; break; case WAIT_IO_COMPLETION: cout « "The asynchronous procedure is done." « endl; break; case WAIT_TIMEOUT: cout « "Time out." « endl; break; default: cout « "Another case." « endl; break; } // распечатываем конечное значение счетчика cout « "Final count = " « *(DWORD*)ptr « endl; return 0; } // главный поток int main() { char c; DWORD count =10; // спрашиваем, что установить: событие или асинхронную процедуру cout « "Input: " « endl « "\te - to set event" « endl « "\ta - to set asynchronous procedure" « endl « "\tanother char - timeout" « endl « "->" ; cin » c; // запускаем поток hThread = CreateThread(NULL, 0, add, &count, 0, &IDThread); if (hThread == NULL) return GetLastError();
498 Часть VIII. Асинхронная обработка данных // создаем события с автоматическим сбросом hSignal = CreateEvent(NULL, FALSE, FALSE, NULL); if (hSignal == NULL) return GetLastError(); hWait = CreateEvent(NULL, FALSE, FALSE, NULL); if (hWait == NULL) return GetLastError(); // ждем сигнала WaitForSingleObject(hSignal, INFINITE); // запрашиваем вариант работы программы switch (с) { case 'е': // устанавливаем событие SetEvent(hWait); break; case 'а': // устанавливаем асинхронную процедуру для потока dwRet = QueueUserAPC(a_proc, hThread, (DWORD) &count); if (IdwRet) { cout « "Queue user APC failed:" « dwRet « endl; return 0; } break; default: break; } // ждем, пока поток add закончит работу WaitForSingleObject(hThread, INFINITE); // закрываем дескриптор потока CloseHandle(hThread); return 0;
Глава 27 Асинхронный доступ к данным 27.1. Концепция асинхронного ввода-вывода Синхронный и асинхронный ввод-вывод концептуально нисколько не отличается от синхронного и асинхронного обмена данными, рассмотренного в гл. 14. При синхронной записи данных в файл поток, выдавший команду записи, блокируется до тех пор, пока данные не будут записаны в файл или буфер. При асинхронной записи данных в файл, после выдачи команды на запись данных, поток не блокируется, а продолжает свою работу. Соответственно, при синхронном чтении данных из файла поток, выдавший команду чтения, блокируется до тех пор, пока данные не будут прочитаны из файла. При асинхронном чтении данных такой блокировки не происходит, и поток продолжает свое исполнение. В операционных системах Windows асинхронный ввод-вывод также называется перекрывающимся (overlapped) вводом- выводом. Для асинхронного ввода-вывода данных в операционных системах Windows ИСПОЛЬЗУЮТСЯ те же фуНКЦИИ ReadFile И WriteFile, ЧТО И ДЛЯ СИН- хронного ввода-вывода. Однако в этом случае файл должен быть открыт в режиме file_flag_overlapped. Возникает вопрос: как поток узнает о завершении асинхронной операции чтения или записи? Это можно сделать двумя способами. Первый способ заключается в том, что для этого можно использовать дескриптор файла, который устанавливается в несигнальное состояние после начала каждой асинхронной операции записи или чтения и переходит в сигнальное состояние после завершения асинхронной операции записи или чтения. Однако этот способ не может быть использован, если с одним файлом работает несколько асинхронных операций ввода-вывода. Так как в этом случае невозможно определить какая из асинхронных операций ввода- вывода завершилась.
500 Часть VIII. Асинхронная обработка данных Второй способ заключается в использовании специального события, которое устанавливается операционной системой в сигнальное состояние при завершении асинхронной операции ввода-вывода. Дескриптор этого события должен находиться в структуре типа overlapped, адрес которой передается в функции асинхронного ввода-вывода. Эта структура имеет следующий формат: typedef struct „OVERLAPPED { DWORD Internal; // для использования операционной системой DWORD InternalHigh; // для использования операционной системой DWORD Offset; // младшая часть смещения относительно начала файла DWORD OffsetHigh;// старшая часть смещения относительно начала файла HANDLE hEvent; // дескриптор события } OVERLAPPED, *LPOVERLAPPED; ПОЛЯ Internal И InternalHigh ЭТОЙ структуры зарезервированы ДЛЯ ИСПОЛЬ- зования операционной системой. В полях offset и offsetHigh устанавливаются соответственно младшая и старшая части смещения относительно начала файла. Начиная с этого смещения, операционная система выполняет чтение или запись данных. В параметре hEvent устанавливается дескриптор события, которое операционная система устанавливает в сигнальное состояние после завершения асинхронной операции ввода-вывода. При одновременной работе нескольких асинхронных операций ввода- вывода для каждой из них нужно определить свою структуру типа overlapped, в которой указать дескриптор отдельного события, связанного только с этой операцией ввода-вывода. В следующих разделах будут рассмотрены примеры организации асинхронного доступа к файлам. Однако используемый подход может быть также применен и при передаче данных по именованным каналам. 27.2. Асинхронная запись данных Асинхронная запись данных в файл работает только на платформах Windows NT/2000. Выполнить асинхронную запись данных можно при помощи функции writeFile, которая имеет следующий прототип BOOL WriteFile( HANDLE hFile, // дескриптор файла LPCVOID lpBuffer, // указатель на буфер данных DWORD nNumberOfBytesToWrite, // количество записываемых байтов LPDWORD lpNumberOfBytesWritten, // количество записанных байтов LPOVERLAPPED lpOverlapped // используется при асинхронной записи
Глава 27. Асинхронный доступ к данным 501 При асинхронной записи данных в файл эта функция возвращает ненулевое значение в том случае, если запись данных в файл уже завершилась до выхода из функции writeFiie. Если же функция возвращает false, to нужно Проверить КОД Последней ОШИбки путем ВЫЗОВа фунКЦИИ GetLastError. ЕСЛИ эта функция возвращает значение error_io_pending, to это значит, что операция вывода данных еще не закончилась. Кратко опишем назначение параметров этой функции. При этом отметим, что в отличие от синхронного вывода при асинхронном выводе также используется Последний параметр lpOverlapped фунКЦИИ WriteFiie. Параметр hFiie должен содержать дескриптор файла, причем файл должен быть открыт в режиме записи. Кроме того, должен быть установлен флаг file_ flag_overlapped, который указывает на асинхронный режим ввода-вывода. Параметр lpBuffer должен указывать на область памяти, в которую будут читаться данные. Параметр nNumberofBytesTowrite должен содержать количество байт, которые предполагается записать в файл посредством вызова функции writeFiie. Параметр lpNumberofByteswritten должен содержать адрес памяти, в которую функция writeFiie поместит количество фактически записанных в файл байтов. При выполнении функции writeFiie операционная система записывает по этому адресу ноль, прежде чем выполнить запись данных в файл. При асинхронном выводе значение этого параметра может быть установлено в null. В этом случае для определения количества байт, которые функция записала В файл, МОЖНО ИСПОЛЬЗОВать функцию GetOverlappedResult. Параметр lpOverlapped должен указывать на структуру типа overlapped. Причем в этой структуре должны быть установлены поля offset и of f setHigh. В поле hEvent может быть помещен как дескриптор события, так и значение null. В первом случае система устанавливает событие в сигнальное состояние по завершении операции вывода. Во втором случае завершение операции вывода можно определить по дескриптору файла, который устанавливается в сигнальное состояние по завершении операции вывода, а при запуске новой операции вывода — сигнальное состояние файла сбрасывается. В листинге 27.1 приведена программа, которая выполняет асинхронную запись данных в файл. В этой программе для сигнализации о завершении операции вывода используется дескриптор файла. | Листинг 27.1, Асинхронная запись данных в файл с использованием дескриптора | файла \ #define _WIN32_WINNT 0x0400 #include <windows.h>
502 Часть VIII. Асинхронная обработка данных #include <iostream.h> int main() { HANDLE hFile; // дескриптор файла OVERLAPPED ovl; // структура управления асинхронным доступом к файлу // инициализируем структуру OVERLAPPED ovl.Offset =0; // младшая часть смещения равна 0 ovl.OffsetHigh =0; // старшая часть смещения равна 0 ovl.hEvent =0; // события нет // создаем файл для записи данных hFile = CreateFile( "С: \\demo_file.dat", // имя файла GENERIC_WRITE, // запись в файл FILE_SHARE_WRITE, // совместный доступ к файлу NULL, // защиты нет OPEN_ALWAYS, // открываем или создаем новый файл FILE_FLAG_OVERLAPPED, // асинхронный доступ к файлу 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 dwBytesWri te; DWORD dwRet; if (!WriteFile(
Глава 27. Асинхронный доступ к данным 503 hFile, // дескриптор файла &i, // адрес буфера, откуда идет запись sizeof(i), // количество записываемых байтов &dwBytesWrite, // количество записанных байтов &ovl // запись асинхронная )) dwRet = GetLastError(); if (dwRet == ERROR_IO_PENDING) cout « "Write file pending." « endl; else { cout « "Write file failed." « endl << "The last error code: " « dwRet « endl; return 0; } } // ждем, пока завершится асинхронная операция записи WaitForSingleObject(hFile, INFINITE); // увеличивает смещение в файле ovl.Offset += sizeof(i); } // закрываем дескриптор файла CloseHandle(hFile); cout « "The file is written." « endl; return 0; } Теперь немного изменим программу из листинга 27.1 таким образом, чтобы для сигнализации о завершении асинхронной операции вывода использовалось событие. Измененная программа приведена в листинге 27.2. #define _WIN32 WINNT 0x0400 # inc lude <windows. h> #include <iostream.h>
504 Часть VIII. Асинхронная обработка данных int main() { HANDLE hFile; // дескриптор файла HANDLE hEndWrite; // дескриптор события OVERLAPPED ovl; // структура управления асинхронным доступом к файлу // создаем события с автоматическим сбросом hEndWrite = CreateEvent(NULL, FALSE, FALSE, NULL); if (hEndWrite == NULL) return GetLastError(); // инициализируем структуру OVERLAPPED ovl.Offset =0; // младшая часть смещения равна 0 ovl.OffsetHigh =0; // старшая часть смещения равна 0 ovl.hEvent = hEndWrite; // событие для оповещения завершения записи // создаем файл для записи данных hFile = CreateFile( "С: Wdemo_file.dat", // имя файла GENERIC_WRITE, // запись в файл FILE_SHARE_WRITE, // совместный доступ к файлу NULL, // зашиты нет OPEN_ALWAYS, // открываем или создаем новый файл FILE_FLAG_OVERLAPPED, // асинхронный доступ к файлу NULL // шаблона нет ); // проверяем на успешное создание if (hFile == INVALID_HANDLE_VALUE) { cerr « "Create file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hEndWrite); cout « "Press any key to finish."; cin.getO ; return 0;
Глава 27. Асинхронный доступ к данным 505 // пишем данные в файл for (int i = 0; i < 10; ++i) { DWORD dwBytesWrite; DWORD dwRet; if (!WriteFile( hFile, // дескриптор файла &i, // адрес буфера, откуда идет запись sizeof(i), // количество записываемых байтов sdwBytesWrite, // количество записанных байтов &ovl // запись асинхронная )) { dwRet = GetLastError(); if (dwRet == ERROR_IO_PENDING) cout « "Write file pending." « endl; else { cout « "Write file failed." « endl « "The last error code: " « dwRet « endl; return 0; } } // ждем, пока завершится асинхронная операция записи WaitForSingleObject(hEndWrite, INFINITE); // увеличивает смещение в файле ovl.Offset += sizeof(i); } // закрываем дескрипторы CloseHandle(hFile); CloseHandle(hEndWrite); cout « "The file is written." « endl; return 0;
506 Часть VIII. Асинхронная обработка данных 27.3. Асинхронное чтение данных Асинхронное чтение данных из файла работает только в операционных системах Windows NT/2000. Выполнить асинхронное чтение данных можно при помощи функции ReadFile, которая имеет следующий прототип: BOOL ReadFile( HANDLE hFile, // дескриптор файла LPVOID lpBuffer, // указатель на буфер данных DWORD nNumberOfBytesToRead, // количество читаемых байтов LPDWORD lpNumberOfBytesRead, // количество прочитанных байтов LPOVERLAPPED lpOverlapped // используется при асинхронной записи ); При асинхронном чтении данных из файла эта функция возвращает ненулевое значение в том случае, если чтение данных из файла уже завершилось до выхода из функции ReadFile. Если же функция возвращает значение false, то нужно проверить код последней ошибки путем вызова функции GetLastError. Если эта функция возвращает значение error_io_pending, to это значит, что операция ввода данных еще не закончилась. Кратко опишем назначение параметров этой функции. При этом отметим, что, в отличие от синхронного ввода, при асинхронном вводе также используется последний параметр lpOverlapped функции ReadFile. Параметр hFile должен содержать дескриптор файла, причем файл должен быть открыт в режиме чтения. Кроме того, должен быть установлен флаг file_flag_overlapped, который указывает на асинхронный режим ввода- вывода. Параметр lpBuffer должен указывать на область памяти, из которой будут читаться данные. Параметр nNumberOfBytesToRead должен содержать количество байт, которые предполагается читать из файла посредством вызова функции ReadFile. Параметр lpNumberOfBytesRead должен содержать адрес памяти, в которую функция ReadFile поместит количество фактически прочитанных из файла байтов. Операционная система записывает по этому адресу ноль, прежде чем выполнить чтение данных из файла. При асинхронном вводе значение этого параметра может быть установлено в null. В этом случае для определения количества байтов, которые функция прочитала из файла, можно использовать функцию GetOverlappedResult. Параметр lpOver lapped должен указывать на структуру типа overlapped. Причем В ЭТОЙ Структуре ДОЛЖНЫ быть установлены ПОЛЯ Offset И Of fsetHigh. В поле hEvent может быть помещен как дескриптор события, так и значение null. В первом случае система устанавливает событие в сигнальное со-
Глава 27. Асинхронный доступ к данным 507 стояние по завершении операции ввода. Во втором случае завершение операции ввода можно определить по дескриптору файла, который устанавливается в сигнальное состояние по завершении операции ввода, а при запуске новой операции ввода сигнальное состояние файла сбрасывается. В листинге 27.3 приведена программа, которая выполняет асинхронное чтение данных из файла. В этой программе для сигнализации о завершении операции вывода используется дескриптор файла. Листинг 27,3, Асинхронное чтение данных m файла с использованием #include <windows.h> #include <iostream.h> int main() { HANDLE hFile; // дескриптор файла OVERLAPPED ovl; // структура управления асинхронным доступом к файлу // инициализируем структуру OVERLAPPED ovl.Offset =0; // младшая часть смещения равна 0 ovl.OffsetHigh = 0; // старшая часть смещения равна 0 ovl.hEvent =0; // события нет // открываем файл для чтения hFile = CreateFile( "С: Wdemo_file.dat", // имя файла GENERIC_READ, // чтение из файла FILE_SHARE_READ, // совместный доступ к файлу NULL, // защиты нет OPEN_EXISTING, // открываем существующий файл FILE_FLAG_OVERLAPPED/ // асинхронный ввод NULL // шаблона нет ); // проверяем на успешное открытие if (hFile == INVALID_HANDLE_VALUE) { cerr « "Create file failed." « endl « "The last error code: " « GetLastError() « endl;
508 Часть VIII. Асинхронная обработка данных cout « "Press any key to finish."; cin.getO ; return 0; } // читаем данные из файла for (;;) { DWORD dwBytesRead; DWORD dwRet; int n ; // читаем одну запись if (!ReadFile( hFile, &n, sizeof(n), &dwBytesRead, &ovl // // // // // дескриптор файла адрес буфера, куда читаем данные количество читаемых байтов количество прочитанных байтов чтение асинхронное )) { switch(dwRet = GetLastError()) { case ERROR_IO_PENDING: cout « "Read file pending." « endl; break; case ERROR_HANDLE_EOF: cout « endl « "End of the file." « endl; // закрываем дескриптор файла CloseHandle(hFile); cout « "The file is read." « endl; return 1; default: cout « "Read file failed." « endl « "The last error code: " « dwRet « endl; return 0; } } // ждем, пока завершится асинхронная операция чтения
Глава 27. Асинхронный доступ к данным 509 WaitForSingleObject(hFile, INFINITE); // печатаем число cout « n « ' ' ; // увеличивает смещение в файле ovl.Offset += sizeof(n); } } Теперь немного изменим программу из листинга 27.4 таким образом, чтобы для сигнализации о завершении асинхронной операции ввода использовалось событие. Измененная программа приведена в листинге 27.4. | Листинг 27,4 Асинхронное чтение данных из файла с использованием события j #include <windows.h> #include <iostream.h> int main () { HANDLE hFile; // дескриптор файла HANDLE hEndRead; // дескриптор события OVERLAPPED ovl; // структура управления асинхронным доступом к файлу // создаем события с автоматическим сбросом hEndRead = CreateEvent(NULL, FALSE, FALSE, NULL); if (hEndRead == NULL) return GetLastError(); // инициализируем структуру OVERLAPPED ovl.Offset =0; // младшая часть смещения равна 0 ovl.OffsetHigh = 0; // старшая часть смещения равна 0 ovl.hEvent = hEndRead; // событие для оповещения завершения чтения // открываем файл для чтения hFile = CreateFile( "С: Wdemo_file.dat", // имя файла GENERIC_READ, // чтение из файла FILE_SHARE_READ, // совместный доступ к файлу NULL, // защиты нет
510 Часть VIII. Асинхронная обработка данных OPEN_EXISTING, // открываем существующий файл FILE_FLAG_OVERLAPPED/ // асинхронный ввод NULL // шаблона нет ); // проверяем на успешное открытие if (hFile == INVALID_HANDLE_VALUE) { cerr « "Create file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hEndRead); cout « "Press any key to finish."; cin.get(); return 0; } // читаем данные из файла for (;;) { DWORD dwBytesRead; DWORD dwRet; int n ; // читаем одну запись if (!ReadFile( hFile, &n, sizeof(n), &dwBytesRead, &ovl // // // // // дескриптор файла адрес буфера, куда читаем данные количество читаемых байтов количество прочитанных байтов чтение асинхронное )) { switch(dwRet = GetLastError()) { case ERROR_IO_PENDING: cout « "Read file pending." « endl; break; case ERROR_HANDLE_EOF:
Глава 27. Асинхронный доступ к данным 511 cout « endl « "End of the file." « endl; cout « "The file is read." « endl; // закрываем дескрипторы CloseHandle(hFile); CloseHandle(hEndRead); return 1; default: cout « "Read file failed." « endl « "The last error code: " « dwRet « endl; // закрываем дескрипторы CloseHandle(hFile); CloseHandle(hEndRead); return 0; } } // ждем, пока завершится асинхронная операция чтения WaitForSingleObject(hEndRead, INFINITE); // печатаем число cout « n « ' '; // увеличиваем смещение в файле ovl.Offset += sizeof(n); 24.4. Блокирование файлов На платформе Windows NT возможно выполнить асинхронное блокирование файла для монопольного доступа. Для этих целей предназначена функция LockFiieEx, которая возвращает управление, не дожидаясь завершения операции блокирования файла. Эта функция имеет следующий прототип: BOOL LockFiieEx( HANDLE hFile, // дескриптор файла DWORD dwFalgs, // опции блокирования DWORD dwReserved, // зарезервировано
512 Часть VIII. Асинхронная обработка данных DWORD nNumberOfBytesToLockLow, // младшая часть количества байтов DWORD nNumberOfBytesTiLockHigh, // старшая часть количества байтов LPOVERLAPPED lpOverlapped // для асинхронной работы ); При асинхронном блокировании файла эта функция возвращает ненулевое значение в том случае, если блокирование файла завершилось, до выхода из функции LockFiieEx. Если же функция возвращает значение false, to нужно Проверить КОД последней ОШИбкИ Путем ВЫЗОВа фунКЦИИ GetLastError. Если эта функция возвращает значение error_io_pending, to это значит, что операция блокирования файла еще не закончилась. Теперь перейдем К параметрам фунКЦИИ LockFiieEx. Параметр hFile должен содержать дескриптор файла. Причем этот файл должен быть открыт в режиме записи или чтения. Параметр dwFiags задает режимы блокирования файла. В этом параметре может быть установлена любая комбинация из следующих двух флагов: П lockfile_fail_immediately — завершение функции в случае невозможности немедленной блокировки файла; П lockfile_exclusive_lock — монопольный доступ к области файла. Немного поясним значение флага lockfile_exclusive_lock. Если этот флаг установлен, то функция блокирует область файла для монопольного использования процессом. В противном случае область файла блокируется процессом только для записи, остальные же процессы могут читать данные из этой области файла. Параметр dwReserved не используется и должен быть установлен в о. В параметрах dwFileOffsetLow И dwFileOffsetHigh ДОЛЖНЫ быть установлены младшая и старшая части соответственно смещения от начала файла в байтах. Параметр lpOverlapped должен указывать на структуру типа lpoverlapped. Поля offset и of fsetHigh этой структуры должны содержать соответственно младшую и старшую части длины области файла, которая блокируется приложением. А в поле hEvent этой структуры может быть помещен как дескриптор события, так и значение null. В первом случае система устанавливает событие в сигнальное состояние по завершении операции блокирования. Во втором случае завершение операции блокирования можно определить по дескриптору файла, который устанавливается в несигнальное состояние перед исполнением операции блокирования и в сигнальное состояние по завершении этой операции. Для асинхронной отмены блокировки области файла используется функция uniockFiieEx, которая имеет следующий прототип: BOOL UniockFiieEx( HANDLE hFile, // дескриптор файла
Глава 27. Асинхронный доступ к данным 513 DWORD dwReserved, // зарезервировано DWORD nNumberOfBytesToLockLow, // младшая часть количества байтов DWORD nNumberOfBytesTiLockHigh, // старшая часть количества байтов LPOVERLAPPED lpOverlapped // для асинхронной работы ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — значение false. Все параметры этой функции аналогичны параметрам функции LockFiieEx. В листинге 27.5 приведена программа, которая сначала асинхронно блокирует доступ к файлу, а затем разблокирует его. Листинг 27,& Асинхронное блокирование и разблокирование файла #include <windows.h> #include <iostream.h> int main () { HANDLE hFile; DWORD dwFileSize; DWORD dwRet; OVERLAPPED ovl; // структура управления асинхронным доступом к файлу // инициализируем структуру OVERLAPPED ovl.Offset = 0; // младшая часть смещения равна 0 ovl.OffsetHigh = 0; // старшая часть смещения равна 0 ovl.hEvent =0; // события нет // открываем файл для записи hFile = CreateFile( "С: Wdemo_file.dat", // имя файла GENERIC_WRITE, // запись в файл FILE_SHARE_WRITE, // совместный доступ к файлу NULL, // защиты нет OPEN_EXISTING, // открываем существующий файл FILE_FLAG_OVERLAPPED, // асинхронный доступ к файлу NULL // шаблона нет ); // проверяем на успешное открытие 17 Зак. 514
514 Часть VIII. Асинхронная обработка данных 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; } // определяем размер файла dwFileSize = GetFileSize(hFile, NULL); if (dwFileSize == -1) { cerr « "Get file size failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.get(); return @); } // блокируем файл if (!LockFileEx( hFile, // дескриптор файла LOCKFILE_EXCLUSIVE_LOCK, // монопольный доступ к файлу 0, //не используется 0, dwFileSize, // длина области &ovl)) // асинхронная блокировка { dwRet = GetLastError(); if (dwRet == ERROR_IO_PENDING) cout « "Lock file is pending." « endl; else { cout « "Lock file failed." « endl « "The last error code: " « dwRet « endl; CloseHandle(hFile); return 0; } } // ждем, пока завершится асинхронная операция записи
Глава 27. Асинхронный доступ к данным 515 WaitForSingleObject(hFile, INFINITE); cout « "Now the file is locked." « endl « "Press any key to continue." « endl; cin.get(); // разблокируем файл if (!UnlockFileEx(hFile, 0, 0, dwFileSize, &ovl)) { cerr « "Unlock file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.getO; return 0; } cout « "Now the file is unlocked." « endl « "Press any key to continue." « endl; cin.get(); // закрываем дескриптор файла CloseHandle(hFile); return 0; } Теперь немного изменим программу из листинга 27.5 таким образом, чтобы для сигнализации о завершении асинхронной операции ввода использова- #include <windows.h> #include <iostream.h> int main() { HANDLE hFile; // дескриптор файла HANDLE hEndLock; // дескриптор события DWORD dwFileSize; DWORD dwRet;
516 Часть VIII. Асинхронная обработка данных OVERLAPPED ovl; // структура управления асинхронным доступом к файлу // создаем события с автоматическим сбросом hEndLock = CreateEvent(NULL, FALSE, FALSE, NULL); if (hEndLock == NULL) return GetLastError(); // инициализируем структуру OVERLAPPED ovl.Offset =0; // младшая часть смещения равна 0 ovl.OffsetHigh = 0; // старшая часть смещения равна 0 ovl.hEvent = hEndLock; // событие для оповещения завершения блокирования // открываем файл для записи hFile = CreateFile( "С:\\demo_file.dat", II имя файла GENERIC_WRITE, // запись в файл 0, // монопольный доступ к файлу NULL, // зашиты нет OPEN_EXISTING, // открываем существующий файл FILE_FLAG_OVERLAPPED, // асинхронный доступ к файлу NULL // шаблона нет ); // проверяем на успешное открытие if (hFile == INVALID_HANDLE_VALUE) { cerr « "Create file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.get(); return 0; } // определяем размер файла dwFileSize = GetFileSize(hFile, NULL); if (dwFileSize == -1)
Глава 27. Асинхронный доступ к данным 517 { cerr « "Get file size failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); cout « "Press any key to finish."; cin.getO ; return 0; } // блокируем файл if (!LockFileEx( hFile, // дескриптор файла LOCKFILE_EXCLUSIVE_LOCK/ // монопольный доступ к файлу 0, //не используется 0, dwFileSize, // длина области &ovl)) // асинхронная блокировка { dwRet = GetLastError(); if (dwRet == ERROR_IO_PENDING) cout « "The lock file is pending." « endl; else { cout « "Lock file failed." « endl « "The last error code: " « dwRet « endl; CloseHandle(hFile); CloseHandle(hEndLock); return 0; } } // ждем, пока завершится асинхронная операция записи WaitForSingleObject(hEndLock, INFINITE); cout « "Now the file is locked." « endl
518 Часть VIII. Асинхронная обработка данных « "Press any key to continue." « endl; cin.get(); // разблокируем файл if (!UnlockFileEx(hFile, 0, 0, dwFileSize, &ovl)) { cerr « "Unlock file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hFile); CloseHandle(hEndLock); cout « "Press any key to finish."; cin.get(); return 0; } cout « "Now the file is unlocked." « endl « "Press any key to continue." « endl; cin.get(); // закрываем дескрипторы CloseHandle(hFile); CloseHandle(hEndLock); return 0; } 27.5. Определение состояния асинхронной операции ввода-вывода Состояние, в котором находится асинхронная операция ввода-вывода, можно определить посредством функции GetoverlappedResult, которая имеет следующий прототип: BOOL GetoverlappedResult( HANDLE hFile, // дескриптор файла LPOVERLAPPED lpOverlapped, // для асинхронной работы LPDWORD lpNumberOfBytesTransferred, // количество переданных байт BOOL bWait // режим ожидания
Глава 27. Асинхронный доступ к данным 519 В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Параметр hFile должен содержать дескриптор файла, для доступа к которому используется асинхронная операция ввода-вывода посредством вызова ФУНКЦИЙ ReadFile И WriteFile. ЕСЛИ Дескриптор ОПИСЫВает именованный канал, то посредством функции GetoverlappedResult можно также определить состояние операций ConnectNamedPipe И TransactNamedPipe. Параметр lpoveriapped должен содержать адрес структуры типа overlapped, которая используется в асинхронной операции доступа к файлу. Параметр lpNumberofBytesTransferred должен указывать на переменную типа dword, в которую функция GetoverlappedResult поместит фактическое количество байт, переданных асинхронной операцией чтения или записи. Для функций ConnectNamedPipe И TransactNamedPipe ЭТО значение не определено. В параметре bwait должно быть установлено одно из двух значений true или FALSE, которое указывает на режим Ожидания функции GetoverlappedResult. ЕСЛИ значение ЭТОГО параметра равно FALSE, TO функция GetoverlappedResult сразу после определения состояния асинхронной передачи данных вернет значение false. В этом случае функция определения последней ошибки GetLastError вернет значение error_io_incomplete. Если же значение этого параметра равно true, to функция GetoverlappedResult не вернет управление до тех пор, пока не завершится асинхронная операция передачи данных. При этом функция GetoverlappedResult определяет завершение асинхронной операции передачи данных посредством ожидания перехода в сигнальное состояние события, дескриптор которого задан в структуре типа OVERLAPPED. В ЛИСТИНГе 27.7 приведена программа, В КОТОРОЙ фунКЦИЯ GetoverlappedResult используется для определения состояния асинхронной операции записи данных в файл. Г™ ~ ™ " ~ ~"~ ; '"""""" г>т~__» | Листинг 27,7, Определение состояния асинхронной операции записи данных ^ j #define _WIN32_WINNT 0x0400 #include <windows.h> #include <iostream.h> int main() { HANDLE hFile; // дескриптор файла HANDLE hEndWrite; // дескриптор события
520 Часть VIII. Асинхронная обработка данных OVERLAPPED ovl; // структура управления асинхронным доступом к файлу // создаем события с автоматическим сбросом hEndWrite = CreateEvent(NULL, FALSE, FALSE, NULL); if (hEndWrite == NULL) return GetLastError(); // инициализируем структуру OVERLAPPED ovl.Offset =0; // младшая часть смещения равна 0 ovl.OffsetHigh =0; // старшая часть смещения равна 0 ovl.hEvent = hEndWrite; // событие для оповещения завершения записи // создаем файл для записи данных hFile = CreateFile( "С: Wdemo_file.dat", // имя файла GENERIC_WRITE, // запись в файл FILE_SHARE_WRITE, // совместный доступ к файлу NULL, // защиты нет OPEN_ALWAYS, // открываем или создаем новый файл FILE_FLAG_OVERLAPPED, // асинхронный доступ к файлу NULL // шаблона нет ); // проверяем на успешное создание if (hFile == INVALID_HANDLE_VALUE) { cerr « "Create file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hEndWrite); cout « "Press any key to finish."; cin.get(); return 0; } // пишем данные в файл for (int i = 0; i < 10; ++i)
Глава 27. Асинхронный доступ к данным 521 DWORD dwBytesWri te; DWORD dwNumberOfBytesTransferred; DWORD dwRet; if (!WriteFile( hFile, // дескриптор файла &i, // адрес буфера, откуда идет запись sizeof(i), // количество записываемых байтов SdwBytesWrite, // количество записанных байтов &ovl // запись асинхронная )) { dwRet = GetLastError(); if (dwRet == ERROR_IO_PENDING) cout « "Write file pending." « endl; else { cout « "Write file failed." « endl « "The last error code: " « dwRet « endl; return 0; } } // проверяем состояние асинхронной операции записи if (IGetOverlappedResult( hFile, // дескриптор файла &ovl, // адрес структуры для асинхронной работы &dwNumberOfBytesTransferred, // количество переданных байтов FALSE //не ждать завершения операции записи )) { cout « "Get overlapped result failed." « endl « "The last error code: " « GetLastError() « endl; return 0; } else cout « "Number of bytes transferred: "
522 Часть VIII. Асинхронная обработка данных « dwNumberOfBytesTransferred « endl; // ждем завершения асинхронной операции записи WaitForSingleObject(hEndWrite, INFINITE); // увеличиваем смещение в файле ovl.Offset += sizeof(i); } // закрьюаем дескрипторы CloseHandle(hFile) ; CloseHandle(hEndWrite); cout « "The file is written." « endl; return 0; } 27.6. Отмена асинхронной операции ввода-вывода Отменить асинхронную операцию передачи данных, которая еще не завершилась, можно посредством функции canceiio, которая имеет следующий прототип: BOOL Cancello( HANDLE hFile // дескриптор файла ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Единственный параметр hFile этой функции должен содержать дескриптор файла, для которого отменяются все асинхронные операции ввода-вывода. Операция, отменяющаяся функцией cancelio, немедленно прекращается, а сама функция ввода- вывода, которая инициировала эту операцию, завершается с кодом ERROR_OPERATION_ABORTED. Отметим, что функция cancel io отменяет только те асинхронные операции ввода-вывода для заданного файла, которые были запущены в потоке, вызвавшем эту функцию. В листинге 27.8 приведена программа, в которой показано, как использовать функцию cancelю для отмены незавершенных асинхронных операций записи данных в файл.
Глава 27. Асинхронный доступ к данным 523 #define _WIN32_WINNT 0x0400 #include <windows.h> #include <iostream.h> int main () { HANDLE hFile; // дескриптор файла HANDLE hEndWrite; // дескриптор события OVERLAPPED ovl; // структура управления асинхронным доступом к файлу // создаем события с автоматическим сбросом hEndWrite = CreateEvent(NULL, FALSE, FALSE, NULL); if (hEndWrite == NULL) return GetLastError(); // инициализируем структуру OVERLAPPED ovl.Offset =0; // младшая часть смещения равна 0 ovl.OffsetHigh =0; // старшая часть смещения равна 0 ovl.hEvent = hEndWrite; // событие для оповещения завершения записи // создаем файл для записи данных hFile = CreateFile( "С: \\demo_file.dat", GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN ALWAYS, // имя файла // запись в файл // совместный доступ к файлу // защиты нет // открываем или создаем новый файл FILE_FLAG_OVERLAPPED, // асинхронный доступ к файлу NULL // шаблона нет ); // проверяем на успешное создание if (hFile == INVALID_HANDLE_VALUE) { cerr « "Create file failed." « endl
524 Часть VIII. Асинхронная обработка данных « "The last error code: " « GetLastError() « endl; CloseHandle(hEndWrite); cout « "Press any key. to finish."; cin.getO ; return 0; } // пишем данные в файл for(int i = 0; i < 10; ++i) { DWORD dwBytesWri te; if (!WriteFile( hFile, // дескриптор файла &i, // адрес буфера, откуда идет запись sizeof(i), // количество записываемых байтов SdwBytesWrite, // количество записанных байтов &ovl // запись асинхронная )) { cout « "Write file failed." « endl « "The last error code: " « GetLastError() « endl; return 0; } // увеличиваем смещение в файле ovl.Offset += sizeof(i); } // отменяем незавершенные асинхронные операции записи if (!CancelIo(hFile)) { cout « "Concel io failed." « endl « "The last error code: " « GetLastError() « endl; return 0;
Глава 27. Асинхронный доступ к данным 525 } // закрываем дескрипторы CloseHandle(hFile); CloseHandle(hEndWrite); cout « "The file is written." « endl; return 0; } Проверить завершение асинхронной операции передачи данных можно при помощи макрокоманды Hasoveriappediocompieted, которая определена следующим образом: #define Hasoveriappediocompieted(lpOverlapped) ((lpOverlapped)->Internal != STATUS_PENDING) и может рассматриваться как функция BOOL Hasoveriappediocompieted( LPOVERLAPPED lpOverlapped ); В случае завершения асинхронной операции передачи данных эта команда возвращает значение true, иначе — false. Единственным параметром макрокоманды Hasoveriappediocompieted является указатель на структуру ТИПа overlapped, которая используется для управления асинхронной операцией передачи данных. Отметим, что до вызова этой макрокоманды нужно убедиться, что асинхронная операция передачи данных уже стартовала. В листинге 27.9 приведена программа, в которой для определения завершения асинхронной операции записи в файл используется макрокоманда Hasoveriappediocompieted. ! Листинг 27,9, Определение завершения асинхронной операции записи данных #define _WIN32_WINNT 0x0400 # include <windows.h> #include <iostream.h> int main() { HANDLE hFile; HANDLE hEndWrite; // дескриптор файла // дескриптор события
526 Часть VIII. Асинхронная обработка данных OVERLAPPED ovl; // структура управления асинхронным доступом к файлу // создаем события с автоматическим сбросом hEndWrite = CreateEvent(NULL, FALSE, FALSE, NULL); if (hEndWrite == NULL) return GetLastError(); // инициализируем структуру OVERLAPPED ovl.Offset =0; // младшая часть смещения равна 0 ovl.OffsetHigh = 0/ // старшая часть смещения равна 0 ovl.hEvent = hEndWrite; // событие для оповещения завершения записи // создаем файл для записи данных hFile = CreateFile( "С:\\demo_file.dat", // имя файла GENERIC_WRITE/ // запись в файл FILE_SHARE_WRITE, // совместный доступ к файлу NULL, // защиты нет OPEN_ALWAYS, // открываем или создаем новый файл FILE_FLAG_OVERLAPPED, // асинхронный доступ к файлу NULL // шаблона нет ); // проверяем на успешное создание if (hFile == INVALID_HANDLE_VALUE) { cerr « "Create file failed." « endl « "The last error code: " « GetLastError() « endl; CloseHandle(hEndWrite); cout « "Press any key to finish."; cin.getO ; return 0; } // пишем данные в файл for (int i = 0; i < 10; ++i)
Глава 27. Асинхронный доступ к данным 527 { DWORD dwBytesWrite; hFile, &i, sizeof(i), &dwBytesWrite, &ovl // // // // // дескриптор файла адрес буфера, откуда идет запись количество записываемых байтов количество записанных байтов запись асинхронная )) { cout « "Write file failed." « endl « "The last error code: " « GetLastError() « endl; return 0; } // проверяем завершение асинхронной операции записи if (HasOverlappedloCompleted(&ovl)) cout « "Write file is completed." « endl; else cout « "Write file is not completed." « endl; // ждем завершения асинхронной операции записи WaitForSingleObject(hEndWrite, INFINITE); // увеличиваем смещение в файле ovl.Offset += sizeof(i); } // закрываем дескрипторы CloseHandle(hFile); CloseHandle(hEndWrite); cout « "The file is written." « endl; return 0;
528 Часть VIII. Асинхронная обработка данных 27.7. Процедуры завершения ввода-вывода Возможен другой подход к оповещению потока о завершении асинхронной операции записи или чтения данных из файла. В этом случае операционная система не устанавливает событие, дескриптор которого задан в структуре типа overlapped, а, по завершении асинхронной операции ввода-вывода, вызывает функцию, которая называется процедурой завершения ввода-вывода {FilelOCompletionRoutine). Такой подход к оповещению завершения асинхронных операций ввода-вывода используется в функциях writeFiieEx и ReadFiieEx. При этом отметим, что процедура завершения ввода-вывода вызывается только в том случае, если поток находится в настороженном состоянии. В противном случае процедура завершения ввода-вывода включается в очередь асинхронных процедур потока. Кроме того, процедуры завершения ввода-вывода могут вызываться в порядке отличном от порядка завершения асинхронных операций ввода-вывода. Процедура завершения ввода-вывода определяется программистом, но при этом она должна иметь следующий прототип: VOID CALLBACK процедура_завершения_ввода_вывода( DWORD dwErrorCode, // код завершения операции ввода-вывода DWORD dwNumberOfBytesTransferred, // количество переданных байтов LPOVERLAPPED IpOveriapped // информация о вводе-выводе ); Параметры процедуры ввода-вывода имеют следующее назначение. В параметре dwErrorcode процедура завершения ввода-вывода получает код завершения асинхронной операции ввода-вывода. Этот код может принимать одно из двух значений: □ о — ввод-вывод завершился успешно; □ ERROR_HANDLE_EOF — операция ReadFiieEx ДОСТИГЛа КОНЦа файла. В параметре dwNumberOfBytesTransferred процедура завершения ввода- вывода поучает количество переданных байтов в асинхронной операции ввода-вывода. Если последняя завершилась ошибкой, то значение этого параметра равно нулю. В параметре IpOveriapped процедура завершения ввода-вывода получает адрес структуры типа overlapped, которая использовалась в асинхронной операции ввода-вывода.
Глава 27. Асинхронный доступ к данным 529 27.8. Асинхронная запись данных с процедурами завершения Для асинхронной записи данных в файл, используя процедуры завершения ввода-вывода, предназначена функция writeFiieEx, которая имеет следующий прототип: BOOL WriteFiieEx( HANDLE hFile, // дескриптор файла LPCVOID lpBuffer, // буфер данных DWORD nNumberOfBytesToWrite, // количество записываемых байтов LPOVERLAPPED lpOverlapped, // асинхронная запись // адрес процедуры завершения LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. В параметре hFile должен быть установлен дескриптор файла, в который записываются данные. При этом файл должен быть открыт в режиме асинхронной записи данных, т. е. должен быть установлен флаг FILE_FLAG_OVERLAPPED. Параметр lpBuf fer должен содержать адрес буфера, в который будут записываться данные. Параметр nNumberOfBytesToWrite должен содержать количество байтов, записываемых в файл. Параметр lpoveriapped должен указывать на структуру типа overlapped, которая используется при асинхронной записи в файл. Параметр lpCompletionRoutine должен указывать на процедуру завершения асинхронной операции ввода-вывода. В листинге 27.10 приведена программа, в которой показано, как можно записать данные В фаЙЛ, ИСПОЛЬЗУЯ фуНКЦИЮ WriteFiieEx. [Листинг 27.10, Асинхронная за^&ь #define _WIN32_WINNT 0x0400 #include <windows.h> #include <iostream.h> // процедура завершения ввода-вывода
530 Часть VIII. Асинхронная обработка данных VOID CALLBACK completion_routine( DWORD dwErrorCode, // код возврата DWORD dwNumberOfBytesTransferred, // количество переданных байтов LPOVERLAPPED lpOverlapped // асинхронная передача данных ) { cout « "Completion routine parameters: " « endl « "XtErrorCode: " « dwErrorCode « endl « "\tNumber of bytes transferred: " « dwNumberOfBytesTransferred « endl « "\tOffsets: " « (*lpOverlapped).OffsetHigh « ' ' « (*lpOverlapped).Offset « endl; } // главная программа int main() { HANDLE hFile; // дескриптор файла OVERLAPPED ovl; // структура управления асинхронным доступом к файлу // инициализируем структуру OVERLAPPED ovl.Offset =0; // младшая часть смещения равна 0 ovl.OffsetHigh =0; // старшая часть смещения равна 0 // создаем файл для записи данных hFile = CreateFile( "С:\\demo_file.dat", // имя файла GENERIC_WRITE, // запись в файл FILE_SHARE_WRITE, // совместный доступ к файлу NULL, // защиты нет OPEN_ALWAYS, // открываем или создаем новый файл FILE_FLAG_OVERLAPPED, // асинхронный доступ к файлу NULL // шаблона нет ); // проверяем на успешное создание if (hFile == INVALID_HANDLE_VALUE) { cerr « "Create file failed." « endl
Глава 27. Асинхронный доступ к данным 531 « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish."; cin.get(); return 0; } // пишем данные в файл for (int i = 0; i < 10; ++i) { DWORD dwRet; if (!WriteFileEx( hFile, // дескриптор файла &i, // адрес буфера, откуда идет запись sizeof(i), // количество записываемых байтов &ovl, // запись асинхронная completion_routine // процедура завершения )) { dwRet = GetLastError(); if (dwRet == ERROR__IO_PENDING) cout « "Write file pending." « endl; else { cout « "Write file failed." « endl « "The last error code: " « dwRet « endl; return 0; } } // ждем, пока сработает асинхронная процедура // завершения операции вывода SleepEx(INFINITE, TRUE); // увеличивает смещение в файле ovl.Offset += sizeof(i);
532 Часть VIII. Асинхронная обработка данных II закрываем дескриптор файла CloseHandle(hFile); cout << "The file is written." « endl; return 0; } 27.9. Асинхронное чтение данных с процедурами завершения Для асинхронного чтения данных из файла, используя процедуры завершения ввода-вывода, предназначена функция ReadFileEx, которая имеет следующий прототип: BOOL ReadFileEx( HANDLE hFile, // дескриптор файла LPCVOID lpBuffer, // буфер данных DWORD nNumberOfBytesToRead, // количество читаемых байтов LPOVERLAPPED lpOverlapped, // асинхронная запись // адрес процедуры завершения LPOVERLAPPED_COMPLETION_ROUTINE IpCompletionRoutine ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Параметры функции ReadFileEx имеют следующее назначение. В параметре hFile должен быть установлен дескриптор файла, из которого читаются данные. При этом файл должен быть открыт в режиме асинхронного чтения данных, т. е. должен быть установлен флаг file_flag_overlapped. Параметр lpBuf fer должен содержать адрес буфера, из которого будут читаться данные. Параметр nNumberOfBytesToRead должен содержать количество байтов, читаемых из файла. Параметр ipoverlapped должен указывать на структуру типа overlapped, которая используется при асинхронном чтении из файла. Параметр IpCompletionRoutine должен указывать на процедуру завершения асинхронной операции ввода-вывода. В листинге 27.11 приведена программа, в которой показано, как можно читать данные ИЗ файла, ИСПОЛЬЗУЯ ФУНКЦИЮ ReadFileEx.
Глава 27. Асинхронный доступ к данным 533 ; Листинг 27,11 * Асинхронное чтение данных m файла ' , ";**; v > . _ ~ „ ~ . .. ~ * ~ „.^.^„ .. .. ~ ^. — j . .. « ~ ~ ~. ~ ~ V^ »- «w х.^» "S- ~ ~ ~ i #include <windows.h> #include <iostream.h> // процедура завершения ввода-вывода VOID CALLBACK completion_routine( DWORD dwErrorCode, // код возврата DWORD dwNumberOfBytesTransferred, // количество переданных байтов LPOVERLAPPED lpOverlapped // асинхронная передача данных ) { cout « "Completion routine parameters: " « endl « "\tErrorCode: " « dwErrorCode « endl « "\tNumber of bytes transferred: " « dv^SFumberOf By tesTransf erred « endl « "\tOffsets: " « (*lpOverlapped).OffsetHigh « ' ' « (*lpOverlapped).Offset « endl; } // главная программа int main() { HANDLE hFile; // дескриптор файла OVERLAPPED ovl; // структура управления асинхронным доступом к файлу // инициализируем структуру OVERLAPPED ovl.Offset =0; // младшая часть смещения равна 0 ovl.OffsetHigh =0; // старшая часть смещения равна 0 // открываем файл для чтения hFile = CreateFile( "С:\\demo_file.dat", // имя файла GENERIC_READ, // чтение из файла FILE_SHARE_READ, // совместный доступ к файлу NULL, // зашиты нет OPEN_EXISTING, // открываем существующий файл FILE_FLAG_OVERLAPPED/ // асинхронный ввод NULL // шаблона нет
534 Часть VIII. Асинхронная обработка данных // проверяем на успешное открытие 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 (;;) { DWORD dwRet; int n ; // читаем одну запись if (!ReadFileEx( hFile, // дескриптор файла &n, // адрес буфера, куда читаем данные sizeof(n), // количество читаемых байтов &ovl, // чтение асинхронное completion_routine // процедура завершения чтения )) { switch(dwRet = GetLastError()) { case ERROR_IO_PENDING: cout « "Read file pending." « endl; break; case ERROR_HANDLE_EOF: cout « endl « "End of the file." « endl; cout « "The file is read." « endl; // закрываем дескриптор файла CloseHandle(hFile); return 0; default:
Глава 27. Асинхронный доступ к данным 535 cout « "Read file failed." « endl « "The last error code: " « dwRet « endl; // закрываем дескриптор файла CloseHandle(hFile); return 0; } } // ждем, пока сработает асинхронная процедура завершения чтения SleepEx(INFINITE, TRUE); // печатаем число cout « n « endl; // увеличивает смещение в файле ovl.Offset += sizeof(n); } }
Глава 28 Порты завершения 28.1. Концепция порта завершения Порт завершения ввода-вывода — это, фактически, объект синхронизации, который оповещает параллельно работающие потоки о завершении асинхронных операций доступа к файлам или именованным каналам. В дальнейшем мы будем говорить только о файлах, подразумевая, что все сказанное справедливо и для именованных каналов. Для работы порта завершения ввода-вывода к нему необходимо подключить файлы, которые должны быть открыты в режиме асинхронного доступа. Причем для каждого файла устанавливается свой номер, который в этом случае называется ключом завершения. При завершении асинхронной операции доступа к файлу операционная система посылает в порт завершения пакет, который содержит информацию о завершившейся асинхронной операции ввода-вывода, содержащую и ключ файла, для которого завершилась эта операция. Порт содержит очередь пакетов, оповещающих о завершении асинхронных операций ввода-вывода. Обслуживается эта очередь по алгоритму FIFO (first in — first out), т. е. вошедший первым — выходит первым. Для создания портов завершения ввода-вывода и подключения к нему файлов в операционных системах Windows используется функция createioCompietionPort. Порт завершения ввода-вывода удаляется, когда закрывается его дескриптор. Поток может узнать о том, что завершилась асинхронная операция ввода- вывода, ВЫЗВав фуНКЦИЮ GetQueuedCompletionStatus, В КОТОРОЙ указывается дескриптор порта. Если очередь пакетов порта завершения не пуста, то поток получает пакет из этой очереди. В противном случае поток блокируется. Каждый порт содержит очередь заблокированных потоков, которая обслуживается по алгоритму LIFO Ast in — first out), т. е. вошедший последним — выходит первым. При этом отметим, что порт завершения может одновременно обслуживать только заданное количество потоков, которое
Глава 28. Порты завершения 537 определяется одним из параметров функции createioCompietionPort. Остальные потоки, которые превысили этот предел, блокируются системой, до тех пор, пока количество обслуживаемых потоков не станет меньше заданного предела. Другой особенностью порта завершения является то, что он может получать пакеты не только от системы, по завершении асинхронной операции ввода- вывода, но также и от приложения. Для посылки пакетов в порт завершения предназначена функция PostQueuedcompietionstatus. Этот прием может использоваться для оповещения потоков о внешних событиях. В заключение этого раздела отметим, что работа с портами завершения ввода- вывода поддерживается только в операционных системах Windows NT/2000/XP. 28.2. Создание порта завершения Для создания порта завершения ввода-вывода и подключения файлов к этому Порту ИСПОЛЬЗуетсЯ функция CreateioCompietionPort, КОТОрая имеет следующий прототип: HANDLE CreateioCompietionPort( HANDLE hFile, // дескриптор файла HANDLE hExistingCompletionPort, // дескриптор порта завершения ULONG *ulCompletionKey, // ключ завершения DWORD dwNumberOfConcurrentThreads // количество параллельных потоков ); В случае успешного завершения эта функция возвращает дескриптор созданного порта завершения ввода-вывода, а в случае неудачи — null. В параметре hFile должен содержать должен быть установлен дескриптор файла, который подключается к порту завершения ввода-вывода. Причем этот файл должен быть открыт в режиме асинхронного ввода-вывода. То есть должен быть установлен флаг file_flag_overlapped. Если создается новый порт, то в параметре hFile можно установить значение invalid_handle_value. Тогда будет создан порт без подключенного К Нему файла. В ЭТОМ Случае В Параметре hExistingCompletionPort ДОЛЖНО быть установлено значение null. Значение же параметра uicompietionKey не важно, т. к. не используется функцией. В параметре hExistingCompletionPort ДОЛЖен быть установлен дескриптор порта завершения, к которому подключается файл. Как уже было сказано, если создается новый порт завершения, то значение этого параметра должно быть установлено в null. В параметре uicompietionKey должен быть задан адрес числа без знака, которое будет обозначать номер файла. Это число называется ключом завер-
538 Часть VIII. Асинхронная обработка данных шения и используется для идентификации файла, для которого завершилась асинхронная операция ввода-вывода. В параметре dwNumberOfConcurrentThreads задается максимальное количество потоков, которые могут одновременно подключиться к порту завершения ввода-вывода. Если это значение равно нулю, то максимальное количество потоков равно количеству процессоров в системе. Каждый новый вызов функции createioCompietionPort задает новое максимальное количество потоков. Пример использования функции CreateioCompietionPort будет приведен в разд. 28.4, после изучения остальных функций, предназначенных для работы с портом завершения. 28.3. Получение пакета из порта завершения Для получения пакета из порта завершения ввода-вывода используется ФУНКЦИЯ GetQueuedCompletionStatus, Которая имеет следующий ПроТОТИП! BOOL GetQueuedCompletionStatus( HANDLE hCompletionPort, // дескриптор порта завершения LPDWORD lpNumberOfBytes, // количество переданных байтов ULONG *ulCompletionKey, // ключ завершения LPOVERLAPPED *lpOverlapped, // адрес структур управления // асинхронным доступом DWORD dwMilliseconds // интервал ожидания ); В Случае успешного завершения фуНКЦИЯ GetQueuedCompletionStatus ВОЗ- вращает ненулевое значение, а в случае неудачи — false. Параметры этой функции имеют следующее назначение. В параметре hcompietionPort должен быть установлен дескриптор порта, к которому поток обращается за пакетом. В параметре lpNumberOfBytes должен быть установлен адрес переменной, в которую в случае успешного завершения функция поместит количество переданных байт. В параметре uicompietionKey должен быть задан адрес переменной типа unsigned long, В Которую фуНКЦИЯ GetQueuedCompletionStatus поместит ключ завершения, т. е. номер файла, для которого завершилась асинхронная операции ввода-вывода. Посылку пакетов в порт завершения ввода-вывода МОГут Инициировать следующие функции: ConnectNamedPipe, DeviceloControl, LockFileEx, ReadDirectoryChangesW, ReadFile, TransactNamedPipe,
Глава 28. Порты завершения 539 waitcommEvent, writeFiie. При этом отметим, что приложение может отме- нить посылку пакета в порт завершения ввода-вывода, если установит младший бит в дескрипторе события, который задан в структуре overlapped, обрабатываемой асинхронной операцией ввода-вывода. В параметре IpOveriapped должен быть задан адрес на переменную, в которую функция GetQueuedCompletionStatus Поместит адрес структуры ТИПа overlapped, для завершенной асинхронной операции ввода-вывода. Параметр dwMiiiiseconds должен содержать количество миллисекунд, которые определяют время ожидания функцией GetQueuedCompletionStatus завершение асинхронной операции ввода-вывода. Если в течение этого времени в очередь не поставлен ни один пакет, то функция завершается неудачей, а по адресу, заданному параметром ipOveriapped, будет записано значение null. Пример Использования функции GetQueuedCompletionStatus будет Приведен В следующем разделе, ПОСЛе изучения фуНКЦИИ PostQueuedCompletionStatus. 28.4. Посылка пакета в порт завершения Для посылки приложением пакета в порт завершения ввода-вывода используется функция PostQueuedCompletionStatus, которая имеет следующий прототип: BOOL PostQueuedCompletionStatus( HANDLE hCompletionPort, // дескриптор порта завершения WORD lpNumberOfBytesTransferred, // количество переданных байтов ULONG *ulCompletionKey, // ключ завершения LPOVERLAPPED *IpOveriapped, // асинхронный доступ ); В случае успешного завершения фуНКЦИЯ PostQueuedCompletionStatus ВОЗ- вращает ненулевое значение, а в случае неудачи — false. В параметре hCompietionPort должен быть установлен дескриптор порта, в который приложение посылает пакет. Остальные параметры могут принимать произвольные значения. Эти значения будут переданы через соответствующие ВЫХОДНЫе параметры фуНКЦИИ GetQueuedCompletionStatus В ПОТОК, который получит пакет, вызвав эту функцию. В листинге 28.1 приведена программа, в которой поток main посылает пакеты в порт завершения ввода-вывода, используя функцию PostQueuedCompletionStatus, а ПОТОК thread получает пакеты ИЗ ЭТОГО Порта, ИСПОЛЬЗуя функцию GetQueuedCompletionStatus. Поясним работу этой программы чуть подробнее. Программа состоит из двух потоков, работающих с портом завершения ввода-вывода. Поток main
540 Часть VIII. Асинхронная обработка данных создает порт завершения ввода-вывода и подключает к нему файл, в который записываются данные. Поток thread получает пакеты о завершении асинхронных операций записи данных в этот файл. Отметим следующий момент, поток thread завершает свою работу после получения пакета от потока main. Для этого считаем, что пакет с ключом файла, равным нулю, оповещает о завершении операции записи данных в файл. Листинг 28.1. Работа с портом завершения ввода-вывода #define _WIN32_WINNT 0x0400 #include <windows.h> #include <iostream.h> HANDLE hCompletionPort; // порт завершения DWORD WINAPI thread(LPVOID) { int i = 0; // количество полученных пакетов DWORD dwNumberOfBytes; // количество переданных байтов ULONG ulCompletionKey; // ключ файла LPOVERLAPPED lpOverlapped; // указатель на структуру типа OVERLAPPED cout « "The thread is started." « endl; // подключаем поток к порту while (GetQueuedCompletionStatus( hCompletionPort, // дескриптор порта завершения &dwNumberOfBytes, // количество переданных байтов &ulCompletionKey, // ключ файла &lpOverlapped, // указатель на структуру типа OVERLAPPED INFINITE)) // бесконечное ожидание // { тело цикла // проверяем пакет на завершение вывода if (ulCompletionKey == 0) { cout « endl « "The thread is finished." « endl; break; } else
Глава 28. Порты завершения 541 cout « "\tPacket: " « ++i « endl « "Number of bytes: " « dwNumberOfBytes « endl « "Completion key: " « ulCompletionKey « endl; // } конец тела цикла return 0; int main() { HANDLE hFile; OVERLAPPED ovl; ULONG ulKey; HANDLE hThread; // дескриптор файла // структура управления асинхронным доступом к файлу // ключ файла // массив для дескрипторов потоков DWORD dwThreadID; // массив для идентификаторов потоков // инициализируем структуру OVERLAPPED ovl.Offset =0; // младшая часть смещения равна 0 ovl.OffsetHigh =0; // старшая часть смещения равна 0 ovl.hEvent =0; // события нет // запрашиваем ключ файла cout « "Input a number for file key (not zero) с in » ulKey; if (ulKey == 0) { cout « "The file key can't be equal to zero. « "Press any key to exit." « endl; « endl return 0; } // создаем файл для записи данных hFile = CreateFile( "С: Wdemo_file.dat", GENERIC_WRITE, FILE_SHARE_WRITE, NULL, OPEN ALWAYS, // имя файла // запись в файл // совместный доступ к файлу // защиты нет // открываем или создаем новый файл FILE_FLAG_OVERLAPPED, // асинхронный доступ к файлу
542 Часть VIII. Асинхронная обработка данных NULL // шаблона нет ); // проверяем на успешное создание if (hFile == INVALID_HANDLE_VALUE) { cerr « "Create file failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish." « endl; cin.getO ; return 0; } // создаем порт завершения и подключаем к нему файл hCompletionPort = CreateloCompletionPort( hFile, // дескриптор файла NULL, // новый порт ulKey, // ключа файла 1 // один поток ); // проверяем на успешное создание if (hCompletionPort == NULL) { cerr « "Create completion port failed." « endl « "The last error code: " « GetLastError() « endl; cout « "Press any key to finish." « endl; cin.getO ; return 0; } // запускаем поток hThread = CreateThread(NULL, 0, thread, NULL, 0, fcdwThreadlD); // пишем данные в файл for (int i = 0; i < 10; ++i) { DWORD dwBytesWri te; DWORD dwRet;
Глава 28. Порты завершения 543 if (!WriteFile( hFile, // дескриптор файла &i, // адрес буфера, откуда идет запись sizeof(i), // количество записываемых байтов &dwBytesWrite, // количество записанных байтов &ovl // запись асинхронная )) { dwRet = GetLastError(); if (dwRet == ERROR_IO_PENDING) cout « "Write file pending." « endl; else { cout « "Write file failed." « endl « "The last error code: " « dwRet « endl; return 0; } } // ждем, пока завершится асинхронная операция записи WaitForSingleObject(hFile, INFINITE); // увеличивает смещение в файле ovl.Offset += sizeof(i); } // посылаем пакет с командой на завершение потока PostQueuedCompletionStatus( hCompletionPort, // дескрипор потока 0, // нет передачи 0, // ключ завершения NULL); // нет структуры типа OVERLAPPED // ждем завершения потока WaitForSingleObject(hThread, INFINITE); // закрываем дескрипторы CloseHandle(hFile); CloseHandle(hCompletionPort); CloseHandle(hThread); cout « "The file is written." « endl; return 0;
Глава 29 Работа с ожидающим таймером 29.1. Ожидающий таймер Ожидающим таймером (waitable timer) называется объект синхронизации, который переходит в сигнальное состояние при наступлении заданного момента времени. Создаются ожидающие таймеры функцией createWaitabieTimer. После создания ожидающего таймера нужно установить тот момент времени, когда ожидающий таймер перейдет в сигнальное состояние. Для установки такого момента времени используется функция setwaitabieTimer. Момент перехода ожидающего таймера в сигнальное состояние может быть определен двумя способами: используя абсолютное системное время или временной интервал, который отсчитывается от текущего момента времени. Если ожидающий таймер ждет момента для перехода в сигнальное состояние, то говорят, что он находится в активном состоянии. В противном случае таймер находится в пассивном или неактивном состоянии. В пассивном состоянии ожидающий таймер находится после его создания. В активное состояние таймер переводится посредством вызова функции setwaitabieTimer. Для перевода таймера из активного состояния в пассивное СОСТОЯНИе ПОТОК МОЖет ИСПОЛЬЗОВать функцию CancelWaitableTimer. Для ожидания сигнального состояния ожидающего таймера поток использует обычные функции ожидания перехода объекта в сигнальное состояние. По способу перехода в несигнальное состояние ожидающие таймеры подразделяются на таймеры с ручным сбросом и таймеры с автоматическим сбросом, которые также называются таймерами синхронизации. Таймер с ручным сбросом переходит из сигнального состояния в несигнальное только посредством установки нового момента (времени) для этого таймера посредством вызова функции SetwaitabieTimer. Таймер синхронизации переходит в несигнальное состояние посредством срабатывания функции ожидания, параметром которой является дескриптор этого таймера.
Глава 29. Работа с ожидающим таймером 545 Существуют два типа ожидающих таймеров: периодические ожидающие таймеры и непериодические ожидающие таймеры. Периодические ожидающие таймеры, перейдя в сигнальное состояние, повторяют этот переход через заданные интервалы времени до тех пор, пока не будут переустанолены функцией SetWaitableTimer ИЛИ отменены функцией CancelWaitableTimer. Непериодический ожидающий таймер переходит в сигнальное состояние только один раз при наступлении заданного момента. После завершения использования таймера его дескриптор нужно закрыть При ПОМОЩИ функции CloseHandle. 29.2. Создание ожидающего таймера Создается ожидающий таймер Посредством фуНКЦИИ CreateWaitableTimer, которая имеет следующий прототип: HANDLE CreateWaitableTimer( LPSECURITY_ATTRIBUTES lpTimerAttributes, // атрибуты безопасности BOOL bManualReset, // переход в несигнальное состояние LPCTSTR lpTimerName // имя таймера ); В случае успешного завершения функция CreateWaitableTimer возвращает дескриптор вновь созданного или уже существующего таймера. В последнем случае функция GetLastError вернет значение error_already_exists. В случае неудачи функция возвращает значение null. В параметре lpTimerAttributes устанавливаются атрибуты безопасности ожидающего таймера. Если значение этого параметра установлено в null, to атрибуты безопасности устанавливаются по умолчанию и дескриптор таймера не наследуется дочерними процессами. Параметр bManualReset может принимать одно из двух значений true или false. Если установлено значение false, to создается таймер синхронизации. В противном случае создается периодический таймер. Параметр lpTimerName должен указывать на символьную строку, которая служит именем файла. Если значение этого параметра равно null, to создается безымянный таймер. В следующих разделах будут приведены примеры программ, использующих ожидающие таймеры. В этих программах и будет показано, как создаются ожидающие таймеры, ИСПОЛЬЗУЯ функцию CreateWaitableTimer. 18 Зак. 514
546 Часть VIII. Асинхронная обработка данных 29.3. Установка ожидающего таймера Для установки времени перехода таймера в сигнальное состояние и других характеристик ожидающего таймера ИСПОЛЬЗуется функция SetWaitableTimer, которая имеет следующий прототип: BOOL SetWaitableTimer( HANDLE hTimer, // дескриптор таймера const LARGE_INTEGER pDueTime, // время перехода в сигн. состояние LONG IPeriod, // период времени PTIMERAPCROUTINE pfnCompletionRoutine, // процедура завершения LPVOID lpArgToCompletionRoutine, // аргумент процедуры завершения BOOL bResume // управление питанием ); В случае успешного завершения функция SetWaitableTimer возвращает ненулевое значение, а в случае неудачи — false. Параметр hTimer должен содержать дескриптор ожидающего таймера. Параметр pDueTime должен содержать адрес структуры типа large_intrger с заданным моментом, при наступлении которого таймер перейдет в сигнальное состояние. Структура типа large_integer является целым числом длиной в 64 бита. Если это целое число имеет положительное значение, то считается, что задано абсолютное время. Если же это число отрицательное, то считается, что задан интервал от текущего значения системного времени. В обоих случаях единица измерения времени равна 100 наносекунд. Напомним, что 1 наносекунда равна 10~9 секунды. Значение параметра IPeriod определяет, является ли ожидающий таймер периодическим. Если значение этого параметра равно нулю, то таймер — не периодический. Если же значение этого параметра больше нуля, то таймер является периодическим и величина периода времени, через который таймер переходит в сигнальное состояние, равна значению этого параметра. Единица измерения времени периода равна 1 миллисекунде. Напомним, что 1 миллисекунда = 10~3 секунды. Если значение параметра IPeriod меньше нуля, то вызов функции заканчивается неудачей. Параметр pfnCompletionRoutine должен указывать на функцию завершения, которая вызывается после перехода ожидающего таймера в сигнальное состояние. При этом отметим, что в этом случае поток, вызвавший функцию SetWaitableTimer, должен находиться в настороженном состоянии. Если процедуры завершения нет, то этот параметр может быть установлен в null. Параметр lpArgToCompletionRoutine может содержать единственный аргумент, который передается функции завершения.
Глава 29. Работа с ожидающим таймером 547 В параметре bResume устанавливается режим управления питанием компьютера после перехода таймера в сигнальное состояние. Этот параметр может принимать значения false или true. Если задано значение true, to компьютер переключается в режим экономии энергии. В противном случае переключение не происходит. В листинге 29.1 приведена программа, в которой создается ожидающий таймер и устанавливается момент, определяющий переход таймера в сигнальное состояние. Листинг 29.1 Создание и установка ожидающего таймера #define _WIN32_WINNT 0x0400 #include <windows.h> #include <iostream.h> #define _SECOND 10000000 // одна секунда для ожидающего таймера HANDLE hTimer; // ожидающий таймер DWORD WINAPI thread (LPVOID) { // ждем сигнал от ожидающего таймера WaitForSingleObject (hTimer, INFINITE) ; // выводим сообщение cout « "\aThe timer is signaled." « endl; } ii { return 0; it main() HANDLE hThread; DWORD IDThread; _int64 qwTimeInterval; // время задержки для таймера // создаем ожидающий таймер
548 Часть VIII. Асинхронная обработка данных hTimer = CreateWaitableTimer(NULL, FALSE, NULL); if (hTimer == NULL) return GetLastError(); // время задержки для таймера = 3 секунды qwTimelnterval = -3 * _SECOND; // инициализируем таймер if (ISetWaitableTimer( hTimer, // дескриптор таймера (LARGE_INTEGER*)&qwTimeInterval, // временной интервал О, // непериодический таймер NULL, // процедуры завершения нет NULL, // параметров к этой процедуре нет FALSE // режим сбережения энергии не устанавливать )) { cout « "Set waitable timer failed." « endl « "The last error code: " « GetLastError() « endl; return 0; } // запускаем поток hThread = CreateThread(NULL, 0, thread, NULL, 0, &IDThread); if (hThread == NULL) return GetLastError(); // ждем, пока поток закончит работу WaitForSingleObject(hThread, INFINITE); // закрываем дескрипторы CloseHandle(hThread); CloseHandle(hTimer); return 0;
Глава 29. Работа с ожидающим таймером 549 29.4. Отмена ожидающего таймера Для перевода таймера в неактивное состояние предназначена функция canceiwaitabieTimer, которая имеет следующий прототип: BOOL CanceiwaitabieTimer ( HANDLE hTimer // дескриптор таймера ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Единственным параметром этой функции является дескриптор таймера, который переходит в неактивное состояние. В листинге 29.2 приведена программа, в которой таймер переводится в неактивное состояние посредством вызова функции CanceiwaitabieTimer. I Листинг 29.2. Перевод ожидающего таймера в неактивное состояние #define _WIN32_WINNT 0x0400 # include <windows.h> #include <iostream.h> #define _SECOND 10000000 // одна секунда для ожидающего таймера HANDLE hHandle[2]; // событие для выхода из цикла и ожидающий таймер DWORD WINAPI thread(LPVOID) { DWORD dwRetCode; // код возврата из функции ожидания for (;;) { // ждем сигнал от ожидающего таймера или на выход из цикла dwRetCode = WaitForMultipleObjectsB, hHandle, FALSE, INFINITE); // определяем индекс дескриптора dwRetCode -= WAIT_OBJECT_0 ; switch (dwRetCode)
550 Часть VIII. Асинхронная обработка данных case 0: // сработало событие cout « "The timer is canceled." « endl; return 0; case 1: // сработал таймер cout « "\aThe waitable timer is signaled." « endl; break; default: cout « "Default statement." « endl; return 0; } } } int main() { HANDLE hThread; DWORD IDThread; _int64 qwTimeInterval; // время задержки для таймера // создаем события с автоматическим сбросом hHandle[0] = CreateEvent(NULL, FALSE, FALSE, NULL); if (hHandle[0] == NULL) return GetLastError(); // создаем ожидающий таймер hHandle[l] = CreateWaitableTimer( NULL, // защиты нет FALSE, // автоматический сброс NULL // имени нет ); if (hHandle[l] == NULL) return GetLastError(); // время задержки для таймера = 2 секунды qwTimelnterval = -2 * _SECOND; // инициализируем таймер if (!SetWaitableTimer(
Глава 29. Работа с ожидающим таймером 551 hHandle[l], // дескриптор таймера (LARGE_INTEGER*)&qwTimeInterval, // временной интервал 2000, // период = 2 секунды NULL, // процедуры завершения нет NULL, // параметров к этой процедуре нет FALSE // режим сбережения энергии не устанавливать )) { cout « "Set waitable timer failed." « endl « "The last error code: " « GetLastError() « endl; return 0; } // запускаем поток hThread = CreateThread(NULL/ 0, thread, NULL, 0, &IDThread); if (hThread == NULL) return GetLastError(); // ждем команды на снятие ожидающего таймера char с; cout « "Input any char to cancel waitable timer." « endl; cin » c; // снимаем ожидающий таймер CancelWaitableTimer(hHandle[l]); // даем потоку сигнал на завершение работы SetEvent(hHandle[0]); // ждем, пока поток закончит работу WaitForSingleObject(hThread, INFINITE); // закрываем дескрипторы CloseHandle(hHandle[0]); CloseHandle(hHandle[l]); return 0;
552 Часть VIII. Асинхронная обработка данных 29.5. Открытие существующего ожидающего таймера Для открытия уже существующего ожидающего таймера используется функция openwaitabieTimer, которая имеет следующий прототип: HANDLE OpenwaitabieTimer( DWORD dwDesiredAccess, // режимы доступа BOOL bInheritHandle, // режим наследования LPCTSTR lpTimerName // имя таймера ); В случае успешного завершения функция возвращает дескриптор уже существующего таймера, а в случае неудачи — значение null. В параметре dwDesiredAccess устанавливаются режимы доступа к открываемому таймеру. В этом параметре может быть установлена любая комбинация следующих флагов: □ timer_all_access — произвольный доступ к таймеру; □ timer_modify_state — можно только изменять состояние таймера, используя функции SetWaitableTimer И CancelWaitableTimer; □ timer_query_state — не используется; □ synchronize — можно использовать таймер только в функциях ожидания. Отметим, что режим synchronize работает только в операционных системах Windows NT/2000/XP. Параметр bInheritHandle может принимать значения true или false. Если устанавливается значение true, to дескриптор таймера наследуется дочерними процессами, в противном случае дескриптор ненаследуемый. Параметр lpTimerName должен указывать на имя открываемого таймера, который должен быть предварительно создан другим процессом. В листинге 29.3 приведена программа родительского процесса, который создает ожидающий таймер и запускает дочерний процесс. | Листинг 29.3, Программа родительского процесса, который создает j ожидающий таймер #define _WIN32_WINNT 0x0400 #include <windows.h> #include <iostream.h>
Глава 29. Работа с ожидающим таймером 553 #define _SECOND 10000000 // одна секунда для ожидающего таймера int main() { HANDLE hTimer; // ожидающий таймер _int64 qwTimeInterval; // время задержки для таймера // создаем ожидающий таймер hTimer = CreateWaitableTimer(NULL, FALSE, "demo_timer"); if (hTimer == NULL) return GetLastError(); // время задержки для таймера = 2 секунды qwTime Interval = -2 * _SECOND; // инициализируем таймер if (!SetWaitableTimer( hTimer, // дескриптор таймера (LARGE_INTEGER*)&qwTimeInterval, // временной интервал 0, // непериодический таймер NULL, // процедуры завершения нет NULL, // параметров к этой процедуре нет FALSE // режим сбережения энергии не устанавливать )) { cout « "Set waitable timer failed." « endl « "The last error code: " « GetLastError() « endl; return 0; } // создаем новый консольный процесс char IpszAppName[] = "С:\\DemoProcess.exe"; STARTUPINFO si; PROCESS_INFORMATION piApp; ZeroMemory(&si, sizeof(STARTUPINFO));
554 Часть VIII. Асинхронная обработка данных si.cb = sizeof(STARTUPINFO); if (!CreateProcess(lpszAppName, NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &piApp) ) { cout « "The new process is not created." « endl « "Check a name of the process." « endl « "Press any key to finish." « endl; cin.get(); return 0; } // ждем завершения созданного процесса WaitForSingleObject(piApp.hProcess, INFINITE); // закрываем дескрипторы этого процесса в текущем процессе CloseHandle(piApp.hThread); CloseHandle(piApp.hProcess); // закрываем таймер CloseHandle(hTimer); return 0; } В листинге 29.4 приведена программа дочернего процесса, который открывает ожидающий таймер, созданный родительским процессом, и ждет перехода этого таймера в сигнальное состояние. | Листинг 28Л Открытие существующего ожидающего таймера дочерним #define _WIN32_WINNT 0x0400 #include <windows.h> #include <iostream.h> int main()
Глава 29. Работа с ожидающим таймером 555 { HANDLE hTimer; // ожидающий таймер // открываем существующий ожидающий таймер hTimer = OpenWaitableTimer ( TIMER_ALL_ACCESS, FALSE, "demo_timer" ); if (hTimer == NULL) { cout « "Open waitable timer failed. " << endl « "The last error code: " « GetLastError() « endl; return 0; } // ждем сигнал от ожидающего таймера WaitForSingleObject(hTimer, INFINITE); // выводим сообщение cout « "\aThe timer is signaled." « endl; // закрываем таймер CloseHandle(hTimer); // ждем команды на выход из процесса cout « "Press any key to exit." « endl; cin.get(); return 0; 29.6. Процедуры завершения ожидания При установке характеристик ожидающего таймера посредством функции setwaitabieTimer можно определить процедуру завершения, которая будет вызвана при переходе таймера в сигнальное состояние. Эта процедура ставится в очередь асинхронных процедур потока и вызывается только в том случае, если поток находится в настороженном состоянии. Отметим, что вызов функции canceiwaitabieTimer удаляет процедуру завершения ожидающего таймера из очереди асинхронных процедур. Подробно механизм вызова асинхронных процедур рассмотрен в гл. 26.
556 Часть VIII. Асинхронная обработка данных Процедура завершения ожидающего таймера имеет следующий прототип: VOID CALLBACK имя_процедуры_завершения ( LPVOID IpArgToCompietionRoutine, // параметр DWORD dwTimerLowValue, // младшее значение таймера DWORD dwTimerHighValue // старшее значение таймера ); Эта процедура не возвращает значения и получает три параметра. В параметре IpArgToCompietionRoutine процедура завершения получает единственный аргумент, который был задан при вызове функции setwaitabieTimer в аналогичном параметре. В параметрах dwTimerLowValue И dwTimerHighValue процедура завершения получает, соответственно, младшую и старшую части из структуры типа large_integer, которая задает момент перехода ожидающего таймера в сигнальное состояние. В листинге 29.5 приведена программа, в которой вызывается процедура завершения при переходе таймера в сигнальное состояние. Отметим, что, в этой программе для перевода потока в настороженное состояние используется функция SleepEx. ! Листинг 29.5, Пример вызова процедуры завершения ожидающего таймера #define _WIN32_WINNT 0x0400 # include <windows.h> #include <iostream.h> #define _SECOND 10000000 // одна секунда для ожидающего таймера VOID CALLBACK completion_proc(LPVOID lpArg, DWORD dwTimerLowValue, DWORD dwTimerHighValue) { // выводим сообщение cout « "\aThe timer is signaled." « endl; // распечатываем аргументы cout « "Arguments: " « (char*)lpArg « endl « "Timer low value: " « dwTimerLowValue « endl « "Timer high value: " « dwTimerHighValue « endl; } int main ()
Глава 29. Работа с ожидающим таймером 557 HANDLE hTimer; // ожидающий таймер _int64 qwTimeInterval; // время задержки для таймера // создаем ожидающий таймер hTimer = CreateWaitableTimer(NULL, FALSE, NULL); if (hTimer == NULL) return GetLastError(); // время задержки для таймера = 1 секунда qwTimelnterval = -1 * _SECOND; // инициализируем таймер if (!SetWaitableTimer( hTimer, // дескриптор таймера (LARGE_INTEGER*)&qwTimeInterval, // временной интервал О, // непериодический таймер completion_proc, // процедура завершения "Demo parameters.", // параметров к этой процедуре нет FALSE // режим сбережения энергии не устанавливать )) { cout « "Set waitable timer failed." « endl « "The last error code: " « GetLastError() « endl; return 0; } // переводим поток в настороженное состояние SleepEx(INFINITE, TRUE); // закрываем таймер CloseHandle(hTimer); return 0;
Часть IX Динамически подключаемые библиотеки Глава 30. Отображение файлов в память Глава 31. Динамически подключаемые библиотеки Глава 32. Локальная память потока
Глава 30 Отображение файлов в память 30.1. Концепция механизма отображения файлов в память Прежде чем перейти к описанию динамически подключаемых библиотек, рассмотрим механизм, который позволяет динамически выполнять это подключение. Этот механизм называется отображением содержимого файла (file mapping) в виртуальную память процесса. Как уже говорилось в гл. 20, посвященной организации виртуальной памяти, операционная система создает файлы подкачки с виртуальными страницами, которые система отображает в адресные пространства процессов. В операционных системах Windows реализован механизм, который позволяет отображать в адресное пространство процесса не только содержимое файлов подкачки, но и содержимое обычных файлов. То есть в этом случае файл или его часть рассматривается как набор виртуальных страниц процесса, которые имеют последовательные логические адреса. Файл, отображенный в адресное пространство процесса, называется представлением или видом файла (file view). После отображения файла в адресное пространство процесса доступ к виду может осуществляться с помощью указателя, как к обычным данным в адресном пространстве процесса. Несколько процессов могут одновременно отображать один и тот же файл в свое адресное пространство. В этом случае операционная система обеспечивает согласованность содержимого файла для всех процессов, если доступ к этим данным осуществляется как к области виртуальной памяти процесса. То есть для доступа к файлу, который отображен в память, не используется функция writeFiie. Такая согласованность данных, хранящихся в файле, отображенном в память несколькими процессами, называется когерентностью данных. Однако следует отметить, что когерентность данных для файла, отображенного в память, не поддерживается в том случае, если этот файл ото-
562 Часть IX. Динамически подключаемые библиотеки бражается в адресное пространство процессов, которые выполняются на других компьютерах в локальной сети. Из вышесказанного можно сделать вывод, что основным назначением механизма отображения файлов в память является загрузка программы (файла с расширением ехе) на выполнение, в адресном пространстве процесса, и динамическое подключение библиотек функций во время выполнения этой программы. Кроме того, механизм отображения файлов в память позволяет осуществлять обмен данными между процессами, принимая во внимание то, что система обеспечивает когерентность данных в файле, отображаемом в память. Теперь кратко опишем общую последовательность действий, которые необходимо выполнить для работы с отображаемым в память файлом. Эти действия могут быть разбиты на следующие шаги: □ открыть файл, который будет отображаться в память; □ создать объект ядра, который выполняет отображение файла; □ отобразить файл или его часть в адресное пространство процесса; □ выполнить необходимую работу с видом файла; □ отменить отображение файла; □ закрыть объект ядра для отображения файла; □ закрыть файл, который отображался в память. Все эти шаги и функции, необходимые для их выполнения, будут рассмотрены в следующих разделах. 30.2. Создание и открытие объекта, отображающего файл Если в память отображается существующий файл, то первым делом этот фаЙЛ ДОЛЖен быть ОТКРЫТ ДЛЯ Доступа, ИСПОЛЬЗУЯ фуНКЦИЮ CreateFile. ЭТО делается для того, чтобы получить дескриптор файла, т. к. в дальнейшем этот дескриптор используется при создании объекта, отображающего файл в память процесса. Если отображаемый файл используется просто для обмена данными между процессами, то создавать для этого специальный файл на диске не обязательно. Для этого можно использовать файлы подкачки операционной системы. Как это делается, будет рассмотрено подробно в разд. 30.4. После того как файл открыт, создается объект, отображающий этот файл в память. Под объектом, отображающим файл в память, можно понимать объект ядра операционной системы, который выполняет отображение файла в адресное пространство процесса. Можно также представить, что этот объ-
Глава 30. Отображение файлов в память 563 ект позволяет рассматривать файл, отображаемый в память, как файл подкачки. ДЛЯ создания ЭТОГО объекта ИСПОЛЬЗуетсЯ функция CreateFileMapping, которая имеет следующий прототип: HANDLE CreateFileMapping( HANDLE hFile, // дескриптор файла LPSECURITY_ATTRIBUTES, lpAttributes // атрибуты защиты DWORD flProtect, // флаги доступа к файлу DWORD dwMaximumSizeHigh, // старшее двойное слово размера объекта DWORD dwMaximumSizeLow, // младшее двойное слово размера объекта LPCTSTR lpName // имя объекта отображения ); В случае успешного завершения эта функция возвращает дескриптор объекта, отображающего файл в память, а в случае неудачи — null. Параметры этой функции имеют следующее назначение. Параметр hFile должен содержать дескриптор открытого файла, для которого будет создаваться объект, отображающий этот файл в память процесса. Параметр lpAttributes, как обычно, указывает на атрибуты защиты для объекта, отображающего файл в память. В этом разделе этот параметр будет всегда устанавливаться в null, задавая, тем самым, атрибуты защиты по умолчанию. То есть в этом случае объект отображения не является наследуемым и принадлежит создавшему его пользователю. Параметр f lProtect содержит флаги, которые задают режимы доступа к виду файла в памяти процесса. Этот параметр может принимать одно из следующих значений: □ page_readonly — из вида файла можно только читать данные; □ page_readwrite — разрешает чтение и запись данных в вид файла; □ page_writecopy — разрешает чтение и запись данных в вид файла, но при записи создается новая копия вида файла. Как можно видеть, значения этого параметра совпадают со значениями соответствующего параметра функции virtuaiAiioc, которая распределяет виртуальную память процессу. Отметим, что режимы доступа к объекту, отображающему файл в память, должны соответствовать режимам доступа к файлу, для которого создается этот объект отображения. Кроме того, в параметре flProtect может быть установлена любая комбинация флагов, которые определяют атрибуты секций исполняемых файлов, заданных в переносимом формате (portable executable files). Эти флаги рассматриваться не будут. Параметры dwMaximumSizeHigh И dwMaximumSizeLow определяют соответственно значение старшей и младшей частей, которые в совокупности задают
564 Часть IX. Динамически подключаемые библиотеки размер объекта, отображающего файл в память. Если эти значения установлены в 0, то объект, отображающий файл в память, имеет размер, равный размеру файла. Отметим, что если размер этого объекта будет меньше размера файла, то система не сможет отобразить весь файл в память. Если же размер объекта, отображающего файл, больше чем размер файла, то размер файла увеличивается до размера объекта. Последний параметр lpName используется для задания имени объекта, отображающего файл в память. Как обычно, это имя используется для доступа к одному и тому же объекту в разных процессах. Если процесс пытается получить доступ к уже созданному объекту, отображающему файл, то флаги доступа, установленные в параметре fiProtect, должны соответствовать флагам доступа, уже установленным в существующем объекте, отображающем файл. Пример использования функции createFiieMapping будет приведен в сле- дующем разделе. А в заключение этого раздела отметим, что, как обычно, после завершения работы с объектом, отображающим файл в память, его ДескрИПТОр НУЖНО закрыть, ИСПОЛЬЗУЯ фуНКЦИЮ CloseHandle. 30.3. Отображение файла в память После того как создан объект, отображающий файл в память, файл или его часть должна быть отображена в память процесса. Другими словами, должен быть создан вид файла или его части в адресном пространстве процесса. Для отображения файла или его части в адресное пространство процесса используется функция MapviewOfFile, которая имеет следующий прототип: LPVOID MapViewOfFile( HANDLE hFileMappingObject, // дескриптор объекта, отображающего файл DWORD dwDesiredAccess, // режим доступа DWORD dwFileOffsetHigh, // старшее двойное слово смещения DWORD dwFileOffsetLow, // младшее двойное слово смещения SIZE_T dwNumberOfBytesToMap // количество отображаемых байт ); В случае успешного завершения функция возвращает указатель на вид файла в адресном пространстве процесса, а случае неудачи — null. Параметры этой функции имеют следующее назначение. Параметр hFileMappingObject должен содержать дескриптор объекта, отображающего файл в память, который был предварительно создан функцией CreateFiieMapping. Параметр dwDwsiredAccess задает режим доступа к виду файла и может принимать одно из следующих значений: □ file_map_write — чтение и запись в вид файла;
Глава 30. Отображение файлов в память 565 П file_map_read — только чтение из вида файла; □ file_map_all_access — чтение и запись в вид файла; □ file_map_copy — при записи в вид файла создается его копия, а исходный файл не изменяется. Следует отметить, что установленное в этом параметре значение должно соответствовать режиму доступа, который установлен для объекта, отображающего файл в память. Параметры dwFiieof f setHigh и dwFiieof f setLow задают смещение от начала файла или, другими словами, первый байт файла, начиная с которого файл отображается в память. Это смещение задается в байтах и должно быть кратно гранулярности распределения виртуальной памяти в системе (allocation granularity), которая может быть определена при помощи вызова функции Getsysteminfo. Единственным параметром этой функции является указатель на структуру SYSTEM_INFO, В ПОЛе dwAllocationGranularity КОТОРОЙ функция Getsysteminfo помещает гранулярность (в байтах). Это значение зависит от архитектуры компьютера и в большинстве случаев равно 64 Кбайт. Параметр dwNumberofBytesToMap задает количество байт, которые будут отображаться в память из файла. Если значение этого параметра равно нулю, то в память будет отображен весь файл. Если необходимо отобразить файл в адресное пространство процесса, начиная с некоторого заданного виртуального адреса, то для этой цели нужно использовать функцию MapviewofFiieEx, которая имеет следующий прототип: LPVOID MapViewOfFileEx( HANDLE hFileMappingObject, // дескриптор объекта, отображающего файл DWORD dwDesiredAccess, // режим доступа DWORD dwFileOffsetHigh, // старшее двойное слово смещения DWORD dwFileOffsetLow, // младшее двойное слово смещения SIZE_T dwNumberOfBytesToMap, // количество отображаемых байтов LPVOID lpBaseAddress // начальный адрес отображения файла ); Последний параметр этой функции указывает на начальный виртуальный адрес вида отображаемого файла. Этот параметр может быть установлен в null. В этом случае система сама выберет начальный адрес загрузки. Остальные параметры этой функции соответствуют параметрам функции MapViewOfFile. После окончания работы с видом файла в памяти нужно отменить отображение файла в адресное пространство процесса. Отмена отображения файла освобождает виртуальные адреса процесса. Следует особо отметить, что если
566 Часть IX. Динамически подключаемые библиотеки отображение файла в адресное пространство процесса не отменено, то система продолжает держать отображаемый файл открытым до тех пор, пока существует его вид, независимо от того, закрыт этот файл функцией closeHandie или нет. Для отмены отображения файла в память используется функция unmapviewofFile, которая имеет следующий прототип: BOOL UnmapViewOfFile (LPCVOID lpBaseAddress) ; В случае успешного завершения возвращает ненулевое значение, а в случае неудачи — false. Единственным параметром этой функции является начальный адрес вида файла. Этот адрес должен быть предварительно получен ОДНОЙ ИЗ функций MapViewOfFile ИЛИ MapViewOfFileEx. В листинге 30.1 приведена программа, которая отображает файл в память. Предварительно создадим файл, который представляет собой массив целых чисел. После этого увеличим значения элементов этого массива на десять, используя вид файла. Затем выведем содержимое измененного файла на экран. | Листинг ЗОИ * Работа с видом файла #include <windows.h> #include <fstream.h> int main () { int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; char f i 1 e_name [ ] = " Demo. bin" ; HANDLE hFile, hMapping; // дескрипторы файла и объекта отображения int *ptr; // для указателя на массив // открываем файл для вывода ofstream out(file_name, ios::out | ios::binary); if (lout) { cerr « "File constructor failed." « endl; return 0; } // выводим исходный массив в файл и на консоль cout « "Initial array: "; for (int i = 0; i < 10; ++i) { out.write((char*)&a[i], sizeof(int));
Глава 30. Отображение файлов в память 567 cout « a[i] « ' ' ; } cout « endl; // закрываем выходной файл out.close(); // // открываем файл для отображения в память hFile = CreateFile(file_name/ GENERIC_READ | GENERIC_WRITE, О, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { cerr « "Create file failed." « endl; return GetLastError(); } // открываем объект, отображающий файл в память hMapping = CreateFileMapping( hFile, // дескриптор открытого файла NULL, // атрибуты защиты по умолчанию PAGE_READWRITE, // режим доступа О, 0, // размер объекта отображения равен размеру файла NULL); // имя не используем if (!hMapping) { cerr « "Create file mapping failed." « endl; return GetLastError(); } // создаем вид файла ptr = (int*)MapViewOfFile( hMapping, // дескриптор объекта отображения FILE_MAP_WRITE, // режим доступа к виду О, 0, // отображаем файл с начала 0); // отображаем весь файл // изменяем значения элементов массива for (i = 0; i < 10; ++i) ptr[i] += 10; // отменяем отображение файла в память if (!UnmapViewOfFile(ptr))
568 Часть IX. Динамически подключаемые библиотеки { cerr « "Unmap view of file failed." « endl; return GetLastError(); } // закрываем объект отображения файла в память if (!CloseHandle(hMapping)) { cerr « "Close file failed." « endl; return GetLastError(); } // закрываем файл if (!CloseHandle(hFile)) { cerr « "Close file failed." « endl; return GetLastError(); } // // открываем файл для ввода ifstream in(file_name/ ios::in | ios::binary); if (!in) { cerr « "File constructor failed." « endl; return 0; } // вводим финальный массив из файла и выводим на консоль cout « "Final array: "; for (i = 0; i < 10; ++i) { in.read((char*)&a[i], sizeof(int)); cout « a[i] « ' '; } cout « endl; // закрываем входной файл in.close(); return 0;
Глава 30. Отображение файлов в память 569 30.4. Обмен данными между процессами через отображаемый в память файл Так как один и тот же файл может быть отображен в память несколькими процессами, и система поддерживает когерентность таких отображений, то механизм отображения файлов в память может использоваться для обмена данными между процессами. Кроме того, в операционных системах Windows все остальные механизмы обмена данными между процессами базируются на отображении файлов в память. Поэтому можно сказать, что отображение файлов в память обеспечивает наилучшую производительность по сравнению со всеми остальными способами обмена данными между процессами. Так как безразлично, какой файл используется для обмена данными между процессами, то в этом случае лучше использовать файл подкачки страниц. В программах из листингов 30.2, 30.3 показывается, как передать данные другому процессу через файл подкачки страниц. Первая программа создает объект, отображающий файл подкачки, а затем записывает в вид файла подкачки массив целых чисел. После этого она запускает вторую программу, которая будет читать созданный массив, через отображаемый в память файл, используя тот же объект, отображающий файл в память. Отметим, что для обращения к одному и тому же объекту, отображающему файл, используется имя этого объекта. I Листинг 30.2. Передача данных через отображаемый в память файл #include <windows.h> #include <fstream.h> int main() { char MappingName [ ] = "MappingName"; HANDLE hMapping; // дескриптор объекта, отображающего файл int *ptr; // для указателя на массив const int n = 10; // размерность массива cout « "This is a parent process." « endl; // открываем объект отображения файла в память hMapping = CreateFileMapping( INVALID_HANDLE_VALUE/ // файл подкачки страниц NULL, // атрибуты защиты по умолчанию PAGE_READWRITE, // режим доступа: чтение и запись
570 Часть IX. Динамически подключаемые библиотеки О, // старшее слово = О п * sizeof(int), // младшее слово = длине массива MappingName); // имя объекта отображения if (IhMapping) { cerr « "Create file mapping failed." « endl; return GetLastError(); } // создаем вид файла ptr = (int*)MapViewOfFile( hMapping, // дескриптор объекта отображения FILE_MAP_WRITE, // режим доступа к виду 0,0, // отображаем файл с начала 0); // отображаем весь файл // инициализируем массив и выводим его на консоль cout « "Array: "; for (int i = 0; i < n; ++i) { ptr[i] = i; cout « ptr[i] « ' '; } cout « endl; // // создаем процесс, который будет читать данные из отображаемого //в память файла char lps zAppName [] = "С:\\ConsoleProcess.ехе"; 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)) { cerr « "Create process failed." « endl; return GetLastError();
Глава 30. Отображение файлов в память 571 } // ждем завершения созданного прцесса WaitForSingleObject(piApp.hProcess, INFINITE); // закрываем дескрипторы этого процесса в текущем процессе С1оseHandlе(рiApp.hThread); CIoseHandle(piApp.hProcess); // // отменяем отображение файла в память if (!UnmapViewOfFile(ptr)) { cerr « "Unmap view of file failed." « endl; return GetLastError(); } // закрываем объект отображения файла в память if (!CloseHandle(nMapping)) { cerr « "Close file failed." « endl; return GetLastError(); } // ждем команду на завершение процесса char с; cout « "Input any char to exit: "; cin » c; return 0; j Листинг ЗОЛ, Прием данных через отображаемый в память файп #include <windows.h> #include <fstream.h> int main'O { char MappingName [ ] = "MappingName"; HANDLE nMapping; // дескриптор объекта, отображающего файл int *ptr; // для указателя на массив const int n = 10; // размерность массива
572 Часть IX. Динамически подключаемые библиотеки cout « "This is a child process." « endl; // открываем объект отображения файла в память hMapping = CreateFileMapping( IWALID_HANDLE_VALUE, // файл подкачки страниц NULL, // атрибуты защиты по умолчанию PAGE_READWRITE, // режим доступа: чтение и запись О, // старшее слово = О п * sizeof(int), // младшее слово = длине массива MappingName); // имя объекта отображения if (!hMapping) { cerr « "Create file mapping failed." « endl; return GetLastError(); } // создаем вид файла ptr = (int*)MapViewOfFile( hMapping, // дескриптор объекта отображения FILE_MAP_WRITE, // режим доступа к виду 0,0, // отображаем файл с начала 0); // отображаем весь файл // выводим массив из вида на консоль cout « "Array: "; for (int i = 0; i < n; ++i) cout « ptr[i] « ' '; cout « endl; // отменяем отображение файла в память if ( lUnmapViewOfFile (ptr) ) { cerr « "Unmap view of file failed." « endl; return GetLastError(); } // закрываем объект отображения файла в память if (ICloseHandle(hMapping)) { cerr « "Close file failed." « endl; return GetLastError(); } // ждем команду на завершение процесса
Глава 30. Отображение файлов в память 573 cout << "Input any char to exit: "; cin » c/ return 0; } 30.5. Сброс вида в файл Для записи на диск содержимого вида файла используется функция FlushviewOfFile, которая имеет следующий прототип: BOOL FlushviewOfFile( LPCVOID lpBaseAddress, // базовый адрес вида SIZE_T dwNumberOfButesToFlush // количество записываемых байтов ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. Параметр lpBaseAddress этой функции должен содержать начальный адрес, который принадлежит диапазону адресов одного из видов и, начиная с которого, содержимое вида записывается на диск, а параметр dwNumberOfButesToFlush — количество записываемых байт. Если значение второго параметра равно нулю, то на диск записывается весь вид файла, начиная с базового адреса и до конца этого вида. Следует отметить, что функция FlushviewOfFile гарантирует, что данные были записаны на диск локального компьютера, но не гарантирует запись этих данных в вид на удаленном компьютере в локальной сети. Далее, в листингах 30.4, 30.5, приведены две программы, которые иллюстрируют использование функции FlushviewOfFile. Первая программа создает файл, затем отображает его в память и изменяет его содержимое. После этого запускается вторая программа, которая читает содержимое отображенного в память файла. Чтобы вторая программа прочитала измененный файл, вид перед ее запуском сбрасывается на диск при помощи вызова функции FlushviewOfFile. | Листинг 30А Сброс вида в файл на диске #include <windows.h> #include <fstream.h> int main ()
574 Часть IX. Динамически подключаемые библиотеки int а[10] = {О, 1, 2, 3, 4, 5, б, 7, 8, 9}; char f ile_name[] = "С:\\Demo.bin"; char mapping_name [ ] = "MappingName"; HANDLE hFile, hMapping; // дескрипторы файла и объекта отображения int *ptr; // для указателя на массив // открьшаем файл для вывода ofstream out(file_name, ios::out | ios::binary); if (lout) { cerr « "File constructor failed." « endl; return 0; } // выводим исходный массив в файл и на консоль cout « "Initial array: "; for (int i = 0; i < 10; ++i) { out.write((char*)&a[i], sizeof(int)); cout « a[i] « ' '; } cout « endl; // закрываем выходной файл out.close(); // // открываем файл для отображения в память hFile = CreateFile(file_name, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { cerr « "Create file failed." « endl; return GetLastError(); } // открываем объект, отображающий файл в память hMapping = CreateFileMapping( hFile, // дескриптор открытого файла NULL, // атрибуты защиты по умолчанию PAGE_READWRITE, // режим доступа 0, 0, // размер объекта отображения равен размеру файла
Глава 30. Отображение файлов в память 575 mapping_name); // имя объекта отображения if (IhMapping) { cerr « "Create file mapping failed." « endl; return GetLastError(); } // создаем вид файла ptr = (int*)MapViewOfFile( hMapping, // дескриптор объекта отображения FILE_MAP_WRITE, // режим доступа к виду 0,0, // отображаем файл с начала 0); // отображаем весь файл // изменяем значения элементов массива for (i = 0; i < 10; ++i) ptr[i] += 10; // сбрасываем весь вид на диск if (IFlushViewOfFile(ptr, 0)) { cerr « "Flush view of file failed." « endl; return GetLastError(); } // // создаем процесс, который будет читать данные из отображаемого //в память файла char IpszAppName[] = "С:\\ConsoleProcess.ехе"; STARTUPINFO si; PROCESS_INFORMATION piApp; ZeroMemory(&si, sizeof(STARTUPINFO)); si.cb = sizeof(STARTUPINFO); // создаем новый консольный процесс if (!CreateProcess(IpszAppName, NULL, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &piApp)) { cerr « "Create process failed." « endl; return GetLastError();
576 Часть IX. Динамически подключаемые библиотеки // ждем завершения созданного прцесса WaitForSingleObj ect(piApp.hProcess, INFINITE); // закрываем дескрипторы этого процесса в текущем процессе CloseHandle(piApp.hThread); CloseHandle(piApp.hProcess); // // отменяем отображение файла в память if ( !UnmapViewOfFile(ptr) ) { cerr « "Unmap view of file failed." « endl; return GetLastError(); } // закрываем объкт отображения файла в память if (!CloseHandle(nMapping)) { cerr « "Close file failed." « endl; return GetLastError(); } // закрываем файл if (!CloseHandle(hFile)) { cerr « "Close file failed." « endl; return GetLastError(); } // ждем команду на завершение процесса char с; cout « "Input any char to exit: "; cin » c; return 0; Листинг 30.5. Чтение данных из файла, который отображен а память другим процессом #include <windows.h> #include <fstream.h> int main()
Глава 30. Отображение файлов в память 577 char f ile_name [ ] = " С: \ \Demo.bin" ; int а[10]; // открываем файл для ввода ifstream in(f ile__name, ios::in | ios::binary); if (!in) { cerr « "File constructor failed." « endl; return 0; } // вводим финальный массив из файла и выводим на консоль cout « "Final array: "; for (int i = 0; i < 10; ++i) { in.read((char*)&a[i], sizeof(int)); cout « a[i] « ' '; } cout « endl; // закрываем входной файл in.close(); // ждем команду на завершение процесса char с; cout « "Input any char to exit: "; cin » c; return 0; 19 Зак. 514
Глава 31 Динамически подключаемые библиотеки 31.1. Концепция динамически подключаемых библиотек Динамически подключаемая библиотека (DLL, Dynamic Link Library) представляет собой программный модуль, который может быть загружен в виртуальную память процесса как статически, во время создания исполняемого модуля процесса, так и динамически, во время исполнения процесса операционной системой. Программный модуль, оформленный в виде DLL, хранится на диске в виде файла, который имеет расширение dll, и может содержать как функции, так и данные. Для загрузки DLL в память используется механизм отображения файлов в память. Динамически подключаемые библиотеки предназначены, главным образом, для разработки функционально-замкнутых библиотек функций, которые могут использоваться разными приложениями. Это позволяет снизить затраты на разработку программного обеспечения, т. к. один и тот же программный код может использоваться разными разработчиками. Кроме того, динамически подключаемые библиотеки позволяют уменьшить объем используемой физической памяти при одновременной работе нескольких приложений, которые используют одну и ту же библиотеку. Это достигается благодаря механизму проецирования DLL в виртуальную память процессов, т. к. в этом случае все приложения разделяют один и тот же экземпляр исполняемого кода DLL, загруженный в физическую память. В связи с последним предложением следует сделать небольшое замечание. Исполняемые файлы и файлы динамических библиотек, т. е. файлы с расширениями ехе и dll соответственно разбиты на разделы, каждый из которых содержит данные только определенного типа. Один из этих разделов содержит только исполняемый код приложения или динамически подключаемой библиотеки. Вот этот раздел и хранится в физической памяти в одном
Глава 31. Динамически подключаемые библиотеки 579 экземпляре и отображается в адресное пространство всех процессов. Те же разделы, которые содержат данные, хранятся для каждого процесса в отдельном экземпляре. Поэтому получается, что процессы совместно разделяют исполняемый код из динамически подключаемой библиотеки, но каждый процесс имеет свой набор переменных из этой библиотеки. Как сделать переменные разделяемыми между процессами можно прочитать, например, в книге Джеффри Рихтера "Windows для профессионалов". 31.2. Создание DLL Создаются DLL подобно обычным исполняемым модулям, но при этом в среде разработки Visual Studio необходимо выбрать проект типа Win32 Dynamic-Link Library. Как и каждая программа на языке программирования C++, динамически подключаемая библиотека должна иметь главную функцию, которая отмечает точку входа в программу при ее исполнении операционной системой. В отличие от исполняемых модулей, в которых эта функция называется main, в DLL главная функция называется DllMain и вызывается операционной системой при загрузке DLL в адресное пространство процесса и при создании этим процессом нового потока. Главное назначение функции DllMain заключается в инициализации DLL при ее загрузке, а также захвате и освобождении необходимых ресурсов при создании и завершении нового потока в процессе. Эта функция имеет следующий прототип: BOOL WINAPI DllMain( HINSTANCE hinstDLL, // дескриптор DLL DWORD fdwReason, // флаг причины вызова функции DllMain LPVOID lpvReserved // зарезервировано Windows ); При успешном завершении функция DllMain должна вернуть значение true, а в случае неудачи — значение false. Параметры функции DllMain имеют следующее назначение. В параметре hinstDLL операционная система Windows передает дескриптор DLL, который фактически равен виртуальному адресу, по которому загружена DLL. Параметр fdwReason может иметь одно из следующих значений, которое указывает на причину, по которой операционная система вызывает функцию DllMain: □ dll_process_attach — DLL загружена в адресное пространство процесса; □ DLL_THREAD_ATTACH — В Процессе СОЗДаН НОВЫЙ ПОТОК И фуНКЦИЯ DllMain вызывается в контексте этого потока;
580 Часть IX. Динамически подключаемые библиотеки П DLL_THREAD_DETACH — В Процессе завершается ПОТОК И фуНКЦИЯ DllMain вызывается в контексте этого потока; □ dll_process_detach — DLL выгружается из адресного пространства процесса. Параметр lpvReserved совместно со значением параметра fdwReason отмечает способ загрузки и выгрузки DLL из адресного пространства процесса. Если значение параметра fdwReason равно dll_process_attach, то значение параметра lpvReserved, равное null, указывает на то, что DLL загружается динамически при ПОМОЩИ ОДНОЙ ИЗ фуНКЦИЙ LoadLibrary ИЛИ LoadLibraryEx. Любое другое значение параметра lpvReserved в этом случае указывает на то, что DLL загружается статически. Если значение параметра fdwReason равно dll_process_detach, то значение параметра lpvReserved, равное null, указывает на то, что DLL выгружается динамически при помощи функции FreeLibrary. Любое другое значение параметра lpvReserved в этом случае указывает на то, что DLL выгружается при завершении процесса. Сделаем следующее замечание относительно функции DllMain. Если эта функция не реализована в DLL, то компилятор автоматически сгенерирует эту функцию, которая всегда будет возвращать значение true. Теперь рассмотрим, как оформляются функции и переменные, которые DLL предоставляет в использование своим клиентам. Во-первых, заметим, что такие функции и переменные называются экспортируемыми. Для того чтобы сделать функцию или переменную экспортируемой, нужно определить ИХ С Модификатором extern "С" И КВЭЛИфикатороМ declspec(dllexport). Кроме того, экспортируемая переменная должна быть инициализирована. Модификатор extern "С" указывает компилятору на то, что функция или переменная должна иметь имя в стиле языка программирования С. То есть имя функции или переменной не будет искажаться путем добавления к нему обозначений типов данных из сигнатуры функции или определения переменной. Модификатор decispec(diiexport) указывает компилятору на то, что данная функция или переменная будет экспортироваться из DLL. В листинге 31.1 приведен код DLL, в которой определяются переменная count и две функции Add и Sub. Переменная count исполняет роль счетчика, а функции Add и sub соответственно прибавляют и вычитают из счетчика некоторое число. j Листинг 31 Л* Создание DLL # inc lude <windows . h> // главная функция
Глава 31. Динамически подключаемые библиотеки 581 BOOL WINAPI DllMain(HINSTANCE hDll, DWORD dwReason, LPVOID lpReserved) { switch (dwReason) { case DLL_PROCESS_ATTACH: // загрузка DLL break; case DLL_THREAD_ATTACH: // создание потока break; case DLL_THREAD_DETACH: // завершение потока break; case DLL_PROCESS_DETACH: // отключение DLL break; } return TRUE; } extern "C" declspec(dllexport) int count = 0; extern "C" declspec (dllexport) int Add(int n) { count += n; return count; } extern "C" declspec(dllexport) int Sub(int n) { count -= n; return count; } 31.3. Динамическая загрузка и отключение DLL Для динамической загрузки DLL в виртуальную память процесса используются Две функции LoadLibrary И LoadLibraryEx. ФунКЦИЯ LoadLibraryEx ОТЛИЧаетСЯ ОТ ФУНКЦИИ LoadLibrary ТОЛЬКО Тем, ЧТО ПОЗВОЛЯеТ упраВЛЯТЬ режимом загрузки библиотеки. Рассмотрим эти функции подробнее.
582 Часть IX. Динамически подключаемые библиотеки Для загрузки динамически подключаемой библиотеки используется функция LoadLibrary, которая имеет следующий прототип: HMODULE LoadLibrary( LPCTSTR lpFileName // имя файла ); В случае успешного завершения эта функция возвращает дескриптор загруженного модуля, а в случае неудачи — null. Единственный параметр этой функции указывает на строку, содержащую имя загружаемого файла. Если эта строка содержит полный путь к файлу, то именно этот файл и загружается. Если же указано только имя файла, то сначала система определяет, не был ли загружен этот модуль ранее. Если да, то система возвращает дескриптор уже загруженного модуля, в противном случае — для загрузки используется следующий алгоритм поиска. 1. Просматривается каталог, из которого запущено приложение. 2. Просматривается текущий каталог процесса. 3. Просматривается системный каталог Windows. 4. Просматривается каталог Windows. 5. Просматриваются каталоги, которые указаны в переменной окружения PATH. При этом следует учитывать, что если тип файла не указан, то система по умолчанию считает, что этот файл имеет расширение dll. Для того чтобы отметить, что файл не имеет типа, нужно после имени файла просто указать точку. Если поиск DLL завершился успехом, то библиотека загружается в виртуальную память процесса, а затем вызывается ее функция DiiMain с параметром fdwReason, равным значению dll_process_attach. Если эта функция возвращает значение false, to она опять вызывается системой, но со значением параметра fdwReason равным dll_process_detach. После этого DLL выгружается. Для загрузки динамически подключаемых библиотек, а также исполняемых модулей используется функция LoadLibraryEx, которая имеет следующий прототип: HMODULE LoadLibraryEx( LPCTSTR lpFileName, // имя файла HANDLE hFile, // зарезервировано DWORD dwFlags // флаги управления загрузкой ); В случае успешного завершения возвращает дескриптор загруженного модуля, а в случае неудачи — null.
Глава 31. Динамически подключаемые библиотеки 583 Как и в случае с функцией LoadLibrary, параметр lpFiieName должен содержать указатель на имя файла. Поиск загружаемого модуля выполняется по тому же алгоритму, что и функции LoadLibrary, при условии, что в параметре dwFiags не задан флаг, указывающий на альтернативный алгоритм поиска загружаемого модуля. Параметр hFile зарезервирован для дальнейшего использования системой и должен быть всегда установлен в null. Параметр dwFiags задает флаги управления загрузкой модуля и может принимать одно из следующих значений: □ dont_resolve_dll_references — в операционных системах Windows NT/2000 после загрузки DLL не вызывается функция DiiMain, а также не загружаются дополнительные модули, которые требуются для выполнения этой DLL; □ load_library_as_datafile — в этом случае модуль загружается как файл данных, при этом не выполняются никакие действия по настройке и вызову программ. Отметим, что, в этом случае операционная система Windows 98 разрешает использовать возвращаемый дескриптор модуля только в функциях управления ресурсами; □ load_with_altered_search_path — в этом случае при поиске загружае- мого модуля в алгоритме поиска не просматривается каталог, из которого запущено приложение. ДЛЯ ОТКЛЮЧеНИЯ DLL ОТ Процесса ИСПОЛЬЗуеТСЯ фуНКЦИЯ FreeLibrary, КО- торая имеет следующий прототип: BOOL FreeLibrary( HMODULE hModule // дескриптор DLL ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — значение false. В единственном параметре hModule этой функции должен быть установлен дескриптор выгружаемого модуля, который был предварительно получен ФУНКЦИЯМИ LoadLibrary ИЛИ LoadLibraryEx. Перед выгрузкой DLL из адресного пространства процесса операционная система вызовет функцию DiiMain с параметром fdwReason равным значению DLL_PROCESS_ATTACH. Для одновременного завершения потока и отключения DLL используется ФУНКЦИЯ FreeLibraryAndExitThread, КОТОрая имеет Следующий ПроТОТИП: VOID FreeLibraryAndExitThread( HMODULE hModule, // дескриптор DLL DWORD dwExitCode // код возврата для потока
584 Часть IX. Динамически подключаемые библиотеки Эта функция эквивалентна последовательному вызову следующих двух функций: FreeLibrary(hModule); Exi tThread(dwExi tCode); Вызов этой функции имеет смысл только в потоке динамической библиотеки, который одновременно завершает свою работу и выгружает саму эту библиотеку. Сделаем следующее замечание о функциях загрузки и отключения DLL. В принципе, функция загрузки одной и той же DLL может вызываться в приложении несколько раз, что имеет смысл в многопоточном приложении. В этом случае при каждом новом вызове функции загрузки одной и той же DLL эта библиотека не зафужается вновь, а используется счетчик ссылок на библиотеку, который при каждом вызове функции увеличивается на единицу. Соответственно, при каждом отключении этой DLL счетчик ссылок уменьшается на единицу. Динамически подключаемая библиотека выгружается только в том случае, если счетчик ссылок на эту библиотеку становится равным нулю. Пример использования некоторых из рассмотренных функций будет приведен в разд. 31.4, в котором будет показано использование DLL, разработанной в предыдущем разделе. 31.4. Использование DLL Если программа использует некоторые функции и переменные из DLL, то говорят, что она импортирует их из DLL. Для обеспечения доступа к импортируемым из DLL функциям и переменным используется функция GetProcAddress, которая имеет следующий прототип: FARPROC GetProcAddress( HMODULE hModule, // дескриптор DLL LPCSTR lpProcName // имя функции ); В случае успешного завершения возвращает адрес экспортируемой из библиотеки функции, а в случае неудачи null. Теперь опишем параметры этой функции. Параметр hModule должен содержать дескриптор DLL, который был предварительно ПОЛучен функцией LoadLibrary ИЛИ LoadLibraryEx. Параметр lpProcName должен указывать на имя импортируемой функции или ее порядковый номер. В последнем случае для задания порядкового номера функции лучше всего использовать макрос MAKEiNTRESOURCE(n), где п задает порядковый номер функции.
Глава 31. Динамически подключаемые библиотеки 585 ОтмеТИМ, ЧТО фуНКЦИЯ GetProcAddress МОЖет также ИСПОЛЬЗОВаТЬСЯ И ДЛЯ доступа к экспортируемым переменным из DLL. В листинге 31.2 приведена программа, которая импортирует функции из DLL, рассмотренной в разд. 31.2. i ' " ' ""' ,_ *'"' '"'" ' ■ ' ""'"" '" "" ' '"'"""" ' :"""" vv \ Листинг 31.2. Импорт функций и переменных из DLL с использованием их имен # include <windows.h> #include <iostream.h> int main() { HMODULE hDll; // дескриптор DLL int (*Add)(int); // указатель на функцию Add из DLL int (*Sub)(int); // указатель на функцию Sub из DLL int *pCount; // указатель на переменную count из DLL // загружаем динамически подключаемую библиотеку hDll = LoadLibrary("Count.dll"); if (IhDll) { cerr « "Load library failed." « endl; return GetLastError(); } // настраиваем адрес переменной count из DLL pCount = (int *)GetProcAddress(hDll, "count"); if (IpCount) { cerr « "Get variable address failed." « endl; return GetLastError(); } // выводим начальное значение переменной count cout « "Initial count = " << (*pCount) « endl; // настраиваем адреса функций Add = (int (*)(int))GetProcAddress(hDll, "Add"); Sub = (int (*)(int))GetProcAddress(hDll, "Sub"); if (!Add || !Sub) { cerr « "Get procedure address failed." « endl; return GetLastError();
586 Часть IX. Динамически подключаемые библиотеки } // изменяем значение счетчика cout « "count = " « AddB0) « endl; cout « "count = " « SubA5) « endl; // отключаем библиотеку if (IFreeLibrary(hDll)) { cerr « "Free library failed." « endl; return GetLastError(); } // выходим из программы cout « "Press any key to exit "; cin.getO ; return 0; } Теперь приведем, в листинге 31.3, эту же программу, но функции и переменные из DLL будут импортироваться, используя их порядковые номера. ^..„,^..„.?„.,Г£ :.^.^.^,..г „^..„ .,._ .,.,. ■^исткйгЩ&,^п^^^УН^Цйй и переменных из DLL с использованием их порядковых нЬмеров # include <windows.h> #include <iostream.h> int main() { HMODULE hDll; // дескриптор DLL int (*Add)(int); // указатель на функцию Add из DLL int (*Sub)(int); // указатель на функцию Sub из DLL int *pCount; // указатель на переменную count из DLL // загружаем динамически подключаемую библиотеку hDll = LoadLibrary("Count.dll"); if (IhDll) { cerr « "Load library failed." « endl; return GetLastError();
Глава 31. Динамически подключаемые библиотеки 587 // настраиваем адрес переменной count из DLL pCount = (int *)GetProcAddress(hDll, MAKEINTRESOURCEC)); if (IpCount) { cerr « "Get variable address failed." « endl; return GetLastError(); } // выводим начальное значение переменной count cout « "Initial count = " « (*pCount) « endl; // настраиваем адреса функций Add = (int (*)(int))GetProcAddress(hDll, MAKEINTRESOURCEA)); Sub = (int (*)(int))GetProcAddress(hDll, MAKEINTRESOURCEB)); if (!Add || !Sub) { cerr « "Get procedure address failed." « endl; return GetLastError(); } // изменяем значение счетчика cout « "count = " « AddB0) « endl; cout « "count = " « SubA5) « endl; // отключаем библиотеку if (!FreeLibrary(hDll)) { cerr « "Free library failed." « endl; return GetLastError(); } // выходим из программы cout « "Press any key.to exit"; cin.get(); return 0; } Отметим, что в этом случае порядковые номера функций начинаются с единицы, а только потом задаются переменные. В связи с этим сделаем следующее замечание. Хотя использование порядковых номеров функций и переменных В функции GetProcAddress И ПОЗВОЛЯеТ ВЫПОЛНЯТЬ Эту функ- цию быстрее, но использование символических имен более надежно, и, следовательно, более предпочтительно.
588 Часть IX. Динамически подключаемые библиотеки В заключение этого раздела сделаем важное замечание. Для того чтобы система смогла найти используемую DLL, ее нужно поместить в каталог, из которого запускается приложение. 31.5. Использование файла определений Для определения экспортируемых из DLL функций и переменных можно использовать файл определения модуля, который должен иметь расширение def. Такие файлы включают инструкции, которые используются компоновщиком для создания исполняемого модуля или DLL. Однако эти файлы не обязательны для компоновки, т. к. большинство их параметров может быть задано при помощи определения опций компоновщика. При создании DLL def-файл содержит инструкции, которые определяют имена экспортируемых функций и переменных. Отметим, что если для описания экспорта из DLL используется файл определений, то квалификатор decispec(diiexport) перед именами функций и переменных в самой DLL использовать не нужно. Для описания экспорта из DLL через файл определений достаточно использовать инструкции двух типов: library и exports. Кратко опишем синтаксис этих инструкций. Инструкция library говорит компоновщику LINK о том, что создается динамическая библиотека. Эта инструкция имеет следующий синтаксис: LIBRARY [library] [BASE = address] Параметр library обозначает имя библиотеки. Это имя не используется как имя DLL модуля, а служит только для внутреннего именования библиотеки при сборке модуля. Параметр base обозначает базовый адрес, начиная с которого операционная система будет загружать DLL. По умолчанию операционная система загружает DLL по адресу Oxiooooooo. Инструкция exports определяет экспортируемые имена. Эта инструкция имеет следующий синтаксис: EXPORTS entryname [ = internalname] [©ordinal [NONAME] ] [PRIVATE] [DATA] Параметр entryname в этой инструкции обозначает имя экспортируемой функции или переменной. Если это имя отличается от внутреннего имени функции или переменной в DLL, то это внутреннее имя задается параметром internalname, который не является обязательным. Все остальные параметры также не являются обязательными. Параметр ©ordinal задает порядковый номер экспортируемого имени. Атрибут noname указывает на то, что в таблице экспорта не будет храниться данное экспортируемое имя. В этом случае импорт адреса возможен только по его порядковому номеру.
Глава 31. Динамически подключаемые библиотеки 589 Ключевое слово private запрещает размещение экспортируемого имени в библиотеке импорта. О библиотеках импорта будет рассказано более подробно в следующем разделе. Атрибут data указывает на то, что экспортируемое имя является именем переменной. Сделаем следующее замечание относительно использования файлов определения модуля. Эти файлы удобно использовать тогда, когда DLL содержит большое число экспортируемых функций и переменных. В этом случае поиск экспортируемых имен занимает очень много времени. Поэтому для более быстрой работы приложения функции и переменные импортируются по их порядковым номерам. Чтобы сделать эти порядковые номера фиксированными, т. е. независимыми от расположения функции или переменной в DLL, используют файл определения модуля. Теперь приведем пример использования файла определения модуля для создания DLL, которая была рассмотрена в начале главы. В этом случае текст самой DLL будет выглядеть так, как это показано в листинге 31.4. Файл определения модуля для этой DLL приведен в листинге 31.5. { Листинг 31А Создание DLL #include <windows.h> // главная функция BOOL WINAPI DllMain(HINSTANCE hDll, DWORD dwReason, LPVOID lpReserved) { switch (dwReason) { case DLL_PROCESS_ATTACH: // загрузка DLL break; case DLL_THREAD_ATTACH: // создание потока break; case DLL_THREAD_DETACH: // завершение потока break; case DLL_PROCESS_DETACH: // отключение DLL break; } return TRUE;
590 Часть IX. Динамически подключаемые библиотеки extern "С" int count = -5; extern "С" int Add(int n) { count += n; return count; } extern "C" int Sub(int n) { count -= n; return count; } » Листинг 31.5. Файл определения модул» для динамически подключаемой ». библиотеки Count 1^-..'.---'1% s.-Jbli ^ ь LIBRARY "Count" EXPORTS count @1 DATA Add @2 Sub @3 Сделаем несколько замечаний относительно работы с файлом определения модуля. Во-первых, такой файл может быть набран в любом текстовом редакторе. Во-вторых, этот файл должен иметь расширение def. И в-третьих, перед компоновкой модуля необходимо подключить этот файл к проекту в среде Visual Sudio. В листинге 31.6 приведена программа, использующая созданную DLL. Эта программа отличается от программы 31.3 только порядковыми номерами переменной и функций, импортируемых из DLL. (Листинг 31.6. Использование DLL #include <windows.h> #include <iostream.h> int main()
Глава 31. Динамически подключаемые библиотеки 591 { HMODULE hDll; // дескриптор DLL int (*Add)(int); // указатель на функцию Add из DLL int (*Sub)(int); // указатель на функцию Sub из DLL int *pCount; // указатель на переменную count из DLL // загружаем динамически подключаемую библиотеку hDll = LoadLibrary("Count.dll"); if (IhDll) { cerr « "Load library failed." « endl; return GetLastError(); } // настраиваем адрес переменной count из DLL pCount = (int *)GetProcAddress(hDll, MAKEINTRESOURCEA)); if (IpCount) { cerr « "Get variable address failed." « endl; return GetLastError(); } // выводим начальное значение переменной count cout « "Initial count = " « (*pCount) « endl; // настраиваем адреса функций Add = (int (*) (int) )GetProcAddress (hDll, MAKEINTRESOURCEB) ) ; Sub = (int (*)(int))GetProcAddress(hDll, MAKEINTRESOURCEC) ) ; if (!Add || !Sub) { cerr « "Get procedure address failed." « endl; return GetLastError(); } // изменяем значение счетчика cout « "count = " « AddB0) « endl; cout « "count = " « SubA5) « endl; // отключаем библиотеку if (!FreeLibrary(hDll)) { cerr « "Free library failed." « endl; return GetLastError();
592 Часть IX. Динамически подключаемые библиотеки II выходим из программы cout « "Press any key to exit"; cin.get(); return 0; } 31.6. Статическая загрузка DLL Статическая загрузка DLL отличается от динамической тем, что выполняется на этапе компоновки исполняемого модуля, а не на этапе его исполнения. Для этого компоновщик использует библиотеку импорта. Библиотека импорта — это файл с расширением lib, который создается компоновщиком при создании динамической библиотеки. В библиотеке импорта содержатся ссылки на все экспортируемые из динамической библиотеки имена. Для статической загрузки DLL необходимо выполнить следующую последовательность действий. 1. Создать любым из вышеуказанных способов DLL. 2. Поместить саму библиотеку и файл импорта этой библиотеки в каталог, из которого запускается приложение. 3. В меню Project выбрать пункт Settings, на котором выбрать закладку Link. После этого в окне Object/library modules ввести имя используемой библиотеки импорта. 4. Описать импортируемые из DLL имена в приложении. Отметим, что импортируемые из DLL имена должны иметь модификатор extern и квалификатор decispec(diiimport). Использование квалифика- тора decispec(diiimport) с прототипами функций не обязательно, но его нужно обязательно использовать при импорте имен переменных. Однако использование этого квалификатора с именами функций делает описание функций более понятным и ускоряет работу компоновщика. В листинге 31.7 приведена программа, которая использует разработанную ранее DLL, при условии, что эта DLL загружается статически. I Листинг $1.7. Использование DLL # include <windows.h> #include <iostream.h> extern "C" declspec(dllimport) int count;
Глава 31. Динамически подключаемые библиотеки 593 extern "С" dec 1 spec(dllimport) int Add(int n) ; extern "C" declspec(dllimport) int Sub(int n); int main() { // выводим начальное Значение переменной count cout « "Initial count = " « count « endl; // изменяем значение счетчика cout « "count = " « AddB0) « endl; cout « "count = " « SubA5) « endl; // выходим из программы cout « "Press any key to exit"; cin.getO ; return 0; }
Глава 32 Локальная память потока 32.1. Динамическая локальная память потока Представим себе такую ситуацию: разрабатывается DLL, одна из функций которой динамически захватывает память для обслуживания приложения. Если эту DLL использует только один поток приложения, то для сохранения указателя на динамически распределенную память достаточно определить в DLL одну переменную. Теперь допустим, что эта DLL используется несколькими потоками одного приложения. В этом случае возникают следующие проблемы. Во-первых, нужно где-то хранить указатели на захваченную память, ведь заранее неизвестно, сколько потоков будут параллельно использовать DLL. Во-вторых, функция из DLL должна как-то узнать — какой поток ее вызывает, чтобы сопоставить каждому потоку свой указатель. Для решения подобных задач и предназначена динамическая локальная память потока. Динамическая локальная память потока (thread local storage, TLS) представляет собой массив указателей, доступ к которым осуществляется через индексы при помощи специальных функций Win32 API. Этот массив указателей автоматически поддерживается операционной системой для каждого потока и содержит, по меньшей мере, tls_minimum_available указателей. Порядок работы с локальной памятью потока следующий: □ распределение указателя; □ работа с указателем; □ освобождение указателя. Прежде чем использовать какой-либо указатель, его нужно распределить. Это делается при помощи функции TisAiioc. После распределения указателя для каждого потока становится доступной своя копия этого указателя.
Глава 32. Локальная память потока 595 Для работы с указателями локальной памяти потока используются функции TisSetvalue и TisGetvalue, которые соответственно записывают некоторое значение и читают значение из локальной памяти потока. После окончания работы с указателем его нужно освободить, вызвав функцию TisFree. В следующих далее разделах будет описана подробно работа со всеми этими функциями. 32.2. Распределение и освобождение локальной памяти потока Прежде чем использовать какой-либо указатель из локальной памяти потока, его нужно распределить. Для этой цели используется функция TisAiioc, которая имеет следующий прототип: DWORD TlsAlloc(VOID); Эта функция не имеет параметров и в случае успешного завершения возвращает индекс распределенного указателя из локальной памяти потока. При этом распределенный указатель инициализирован в ноль. Если функция закончилась неудачей, то она возвращает -1. Неудача при выполнении этой функции, скорее всего, означает, что локальная память потока исчерпана. Для освобождения распределенного указателя из локальной памяти потока используется функция TisFree, которая имеет следующий прототип: BOOL TisFree( DWORD dwTlsIndex // TLS-индекс ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Единственным параметром этой функции является индекс освобождаемого указателя из локальной памяти потока. Примеры вызова этих функций приведены в следующем разделе, который содержит программы, иллюстрирующие работу с динамической локальной памятью потока. 32.3. Запись и чтение из локальной памяти потока Для записи значения в локальную память потока используется функция TisSetvalue, которая имеет следующий прототип: BOOL TisSetvalue( DWORD dwTlsIndex, // TLS-индекс
596 Часть IX. Динамически подключаемые библиотеки LPVOID lpTlsValue // запоминаемое значение ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Параметр dwTis index этой функции должен содержать индекс элемента в массиве указателей, а само значение, запоминаемое в этом элементе массива, передается через параметр lpTlsValue. Для чтения значения из динамической локальной памяти потока используется функция TisGetvalue, которая имеет следующий прототип: LPVOID TlsGetValue( DWORD dwTisIndex // TLS-индекс ); В случае успешного завершения эта функция возвращает значение, хранимое в локальной памяти потока с индексом dwTisindex, а в случае неудачи — ноль. В листинге 32.1 приведена программа, которая иллюстрирует работу с рассмотренными функциями. Эта программа распределяет все указатели из локальной памяти потока, а затем освобождает их. При этом для запоминания индексов распределенных указателей используются сами указатели. | Листинг 32,1» Работа с динамической локальной памятью потока # include <windows.h> #include <iostream.h> DWORD thread(void) { char с; DWORD dwFirstTlsIndex, dwPrevTlsIndex, dwNextTlsIndex; // распределяем первый TLS-индекс dwFirstTls Index = TlsAllocO; if (dwFirstTlsIndex == -1) { cerr « "Tls allocation failed." « endl; return GetLastError(); } // устанавливаем предыдущий TLS-индекс равным первому индексу dwPrevTlsIndex = dwFirstTlsIndex;
Глава 32. Локальная память потока 597 // двигаемся дальше по TLS-индексам for (;;) { // выводим на экран предьщущий TLS-индекс cout « "Alloc tls index = " « dwPrevTlsIndex « endl; SleepE0); // распределяем следующий TLS-индекс dwNextTls Index = TlsAllocO; // если больше индексов нет, то выходим из цикла if (dwNextTlsIndex == -1) break; // запомним следующий TLS-индекс в предыдущем индексе if (!TlsSetValue(dwPrevTlsIndex, (LPVOID)dwNextTlsIndex)) { cerr « "Tls set value failed." « endl; return GetLastError(); } // продвигаем индексы dwPrevTlsIndex = dwNextTlsIndex; } // ждем команду на освобождение индексов cout « "Input any char to free tls indexes: "; cin » c; // устанавливаем предьщущий TLS-индекс в первый dwPrevTlsIndex = dwFirstTlsIndex; // двигаемся дальше по tls индексам for (;;) { // выводим на экран предьщущий TLS-индекс cout « "Free tls index = " « dwPrevTlsIndex « endl; SleepE0); // получаем следующий TLS-индекс dwNextTlsIndex = (DWORD)TlsGetValue(dwPrevTlsIndex); if (!dwNextTlsIndex) break; // освобождаем предыдущий TLS-индекс if (!TlsFree(dwPrevTlsIndex))
598 Часть IX. Динамически подключаемые библиотеки { cerr « "Tls free failed." « endl; return GetLastError(); } // продвигаем индексы dwPrevTlsIndex = dwNextTlsIndex; } // ждем команду на выход из потока cout « "Input any char to exit: "; cin » c; return 0; int main() { HANDLE hThread; DWORD IDThread; // запускаем поток hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE) thread, NULL, 0, &IDThread); if (hThread == NULL) { cerr « "Create thread failed." « endl; return GetLastError(); } // ждем, пока поток Add закончит работу . WaitForSingleObjееt(hThread, INFINITE); // закрываем дескриптор потока CloseHandle(hThread); return 0; Теперь приведем пример использования локальной памяти потока в DLL. Для этого создадим DLL, в которой всего одна функция Add. Эта функция увеличивает значение отдельного счетчика для каждого потока, при этом
Глава 32. Локальная память потока 599 счетчики создаются динамически. Так как в DLL заранее неизвестно, сколько потоков будут использовать эту функцию, то для хранения указателей на счетчики используется локальная память потоков. В листинге 32.2 приведен текст программы, в которой создается DLL. ; Листинг 32,2, Пример DLL, использующей динамическую память потока Kw # inc lude <windows. h> #include <iostream.h> DWORD dwTlsIndex; // индекс динамической локальной памяти потока // главная функция BOOL WINAPI DllMain(HINSTANCE hDll, DWORD dwReason, LPVOID lpReserved) { BOOL retVal = TRUE; // возвращаемое значение int *pCount; // указатель на счетчик switch (dwReason) { case DLL_PROCESS_ATTACH: // загрузка DLL // распределяем локальную память потоков dwTlsIndex = TlsAllocO; if (dwTlsIndex == -1) { cerr « "Tls allocation failed." « endl; retVal = FALSE; } break; case DLL_THREAD_ATTACH: // создание потока // захватываем память под счетчик pCount = new int@); // запоминаем указатель в индексе if (ITlsSetValue(dwTlsIndex, (LPVOID)pCount)) { cerr « "Tls set value failed." « endl; retVal = FALSE; } break; case DLL_THREAD_DETACH: // завершение потока
600 Часть IX. Динамически подключаемые библиотеки // получаем указатель из индекса pCount = (int*)TlsGetValue(dwTlsIndex); // если ошибка, то выдаем сообщение if (!pCount) { cerr « "Tls get value failed." « endl; retVal = FALSE; } // иначе освобождаем память else delete pCount; break; case DLL_PROCESS_DETACH: // отключение DLL // освобождаем локальную память потока if (!TlsFrее(dwTlsIndex)) { cerr « "Tls free failed." « endl; retVal = FALSE; } break; return retVal; } extern "C" declspec(dllexport) int Add(int n) { int *pCount; // получаем указатель на локальный счетчик pCount = (int*)TlsGetValue(dwTlsIndex); if (!pCount) { cerr « "Tls get value failed." « endl; return GetLastError(); } // увеличиваем значение счетчика *pCount += n; return *pCount;
Глава 32. Локальная память потока 601 Теперь в листинге 32.3 приведем программу, которая использует DLL, созданную в предыдущем примере. В этой программе функцию Add вызывают два создаваемых потока. I Листинг 32.3, Использование DLL в многопоточном приложении #include <windows.h> #include <iostream.h> int (*Add) (int) ; // указатель на функцию Add из DLL DWORD WINAPI thread(LPVOID iNum) { for (int i = 0; i < (int)iNum; ++i) { cout « "count = " « Add((int)iNum) « endl; SleepA5); } return 0; int main() { char с; HMODULE hDll; // дескриптор DLL HANDLE hThread[2]; // дескрипторы потоков DWORD IDThread[2]; // идентификаторы потоков // загружаем динамически подключаемую библиотеку hDll = LoadLibrary("Count.dll"); if (!hDll) { cerr « "Load library failed." « endl; return GetLastError(); } // настраиваем адрес функци Add = (int (*)(int))GetProcAddress(hDll, "Add"); if (!Add)
602 Часть IX. Динамически подключаемые библиотеки { cerr « "Get procedure address failed." « endl; return GetLastError(); } // запускаем потоки hThread[0] = CreateThread(NULL, 0, thread, (void*K, 0, &IDThread[0]); hThread[l] = CreateThread(NULL, 0, thread, (void*M, 0, &IDThread[l]); if (hThread[0] == NULL || hThread[l] == NULL) { cerr « "Create thread failed." « endl; return GetLastError(); } // ждем завершения потоков WaitForMultipleObjectsB/ hThread, TRUE, INFINITE); // отключаем библиотеку if (!FreeLibrary(hDll)) { cerr « "Free library failed." « endl; return GetLastError(); } // выходим из программы cout « "Input any char to exit: "; cin » c; return 0; 32.4. Статическая локальная память потока Если DLL загружается статически, то динамический вызов функций является не лучшим подходом к работе с локальной памятью потока, т. к. замедляет работу приложения. В случае статической загрузки DLL информация о необходимой локальной памяти для каждого потока может быть получена на эта[пе компиляции DLL. Для этого в компиляторе Visual C++ фирмы Microsoft введен спецификатор памяти declspec(thread), который может использоваться только с глобальными или статическими переменными. Естественно, что этот спецификатор может использоваться и в обычных программах, но там его использование не имеет смысла, т. к. в этом случае в каждом потоке можно определить локальные переменные как в обычных функциях. Если спецификатор declspec (thread) используется при объявле-
Глава 32. Локальная память потока 603 нии некоторой переменной, то система создает отдельный экземпляр этой переменной для каждого потока приложения, в котором используется эта переменная. Память, выделяемая под такие переменные, называется статической локальной памятью потока. В листинге 32.4 приведен текст рассмотренной в предыдущем разделе DLL, в которой счетчик объявлен глобально, используя локальную статическую память потока. f Листинг 32А Пример DLL, использующей статическую память потока #include <windows.h> dec1spec(thread) int count = 0; // главная функция BOOL WINAPI DllMain(HINSTANCE hDll, DWORD dwReason, LPVOID lpReserved) { switch (dwReason) { case DLL_PROCESS_ATTACH: // загрузка DLL break; case DLL_THREAD_ATTACH: // создание потока break; case DLL_THREAD_DETACH: // завершение потока break; case DLL_PROCESS_DETACH: // отключение DLL break; } return TRUE; } extern "C" declspec(dllexport) int Add(int n) { count += n; return count; } Теперь в листинге 32.5 приведем текст многопоточного приложения, которое использует эту DLL. Отметим, что в этом приложении один экземпляр
604 Часть IX. Динамически подключаемые библиотеки счетчика используется главным потоком, а второй экземпляр — потоком, создаваемым динамически. | Листинг 32.5. Использование DLL в многопоточном приложении j #include <windows.h> #include <iostream.h> extern "С" declspec (dllimport) int Add(int n) ; DWORD WINAPI thread(LPVOID iNum) { cout « "count from thread = " « Add( (int) iNum) « endl; return 0; } int main() { HANDLE hThread; // дескрипторы потоков DWORD IDThread; // идентификаторы потоков // изменяем значение счетчика cout « "count from main = " « AddB0) « endl; // запускаем поток hThread = CreateThread(NULL/ 0, thread, (void*I0, 0, &IDThread); if (hThread == NULL) { cerr « "Create thread failed." « endl; return GetLastError(); } // ждем завершения потока WaitForSingleObject(hThread, INFINITE); // выходим из программы cout « "Press any key to exit"; cin.get(); return 0; } В заключение этого раздела отметим, что локальная память потоков не работает в DLL, которые подключаются динамически.
Часть X Разработка сервисов в Windows Глава 33. Сервисы в Windows Глава 34. Работа с сервисами в Windows
Глава 33 Сервисы в Windows 33.1. Концепция сервиса Сервис это процесс, который выполняет служебные функции. Сервисы являются аналогами резидентных программ, которые использовались в операционных системах, предшествующих операционной системе Windows NT. То есть сервис это такая программа, которая запускается при загрузке операционной системы или в процессе ее работы по специальной команде и заканчивает свою работу при завершении работы операционной системы или по специальной команде. Обычно сервисы выполняют определенные служебные функции, необходимые для работы приложений или какого-то конкретного приложения. Примером сервиса может служить фоновый процесс, который обеспечивает доступ к базе данных — такие сервисы также называются серверами. Другой тип сервисов — это программы, обеспечивающие доступ к внешним устройствам, такие сервисы называются драйверами. Как сервис также может быть реализован процесс, отслеживающий работу некоторого приложения, такие сервисы также называются мониторами. Сервисы реализованы только на платформах Windows NT/2000/XP. Управляет работой сервисов специальная программа операционной системы, которая называется менеджер сервисов (Service Control Manager, SCM). Ниже перечислены функции, которые выполняет менеджер сервисов: П поддержка базы данных установленных сервисов; П запуск сервисов при загрузке операционной системы; П поддержка информации о состоянии работающих сервисов; П передача управляющих запросов работающим сервисам; П блокировка и разблокирование базы данных сервисов. Для того чтобы менеджер сервисов знал о существовании определенного сервиса — его нужно установить. Информация обо всех установленных сервисах
608 Часть X. Разработка сервисов в Windows хранится в реестре операционной системы Windows под ключом HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services. Сервис может запускаться как операционной системой при загрузке, так и программно — из приложения. Если сервис больше не нужен, то его нужно удалить из базы данных операционной системы. Для работы с сервисами в операционных системах Windows предназначены специальные функции из Win32 API. Управлять работой сервисов можно также и через панель управления, а именно Control Panel | Administrative Tools | Services (Панель управления | Администрирование | Службы). В этой главе будут рассмотрены только программные средства для управления сервисами. В заключение этого раздела скажем об отладке сервисов. Так как сервисы запускаются операционной системой, то невозможно выполнить отладку сервиса как обычного консольного приложения. В этом случае можно использовать отладчик так, как это описано в справочной системе MSDN (в разделе Windows Development | Windows Base Services | DLL, Processes and Threads | SDK Documentation | DLL, Processes and Threads | Services | About Services j Debugging a Service). Но можно использовать для отладки сервиса старые методы отладки программ. А именно, создать так называемый лог-файл и записывать в него протокол работы сервиса и его состояние, включающее контекст памяти, в контрольных точках. 33.2. Структура сервиса Так как сервисы работают под управлением менеджера сервисов, то они должны удовлетворять определенным соглашениям, которые определяют интерфейс сервиса. Сам сервис может быть как консольным приложением, так и приложением с графическим интерфейсом. Это не имеет значения, т. к. сервисы могут взаимодействовать с пользователем только через рабочий стол или через окно сообщений. Поэтому обычно сервисы оформляются как консольные приложения. Кроме того, каждый сервис должен содержать две функции обратного вызова, которые вызываются операционной системой. Одна из этих функций определяет точку входа сервиса, т. е., собственно, и является сервисом, а вторая — должна реагировать на управляющие сигналы от операционной системы. Отсюда следует, что если консольное приложение определяет один сервис, то оно должно иметь следующую структуру: // главная функция консольного приложения int main(int argc, char *argv) { ... } // точка входа сервиса VOID WINAPI ServiceMain(DWORD dwArgc, LPTSTR *lpszArgv) { ... } // обработчик запросов VOID WINAPI ServiceCtrlHandler (DWORD dwControl) { ... }
Глава 33. Сервисы в Windows 609 Конечно, допускается присутствие и других функций, но эти три функции должны быть обязательно. Главной задачей функции main является запуск диспетчера сервиса, который является потоком и управляет этим сервисом. Диспетчер сервиса получает управляющие сигналы от менеджера сервисов по именованному каналу и передает эти запросы функции ServicectriHandier, которая обрабатывает эти управляющие запросы. Если в приложении несколько сервисов, то для каждого сервиса запускается свой диспетчер и для каждого диспетчера определяется своя функция обработки управляющих запросов, которая выполняется в контексте соответствующего диспетчера сервисов. Запуск диспетчеров сервисов выполняется При ПОМОЩИ ВЫЗОВа функции StartServiceCtrlDispatcher. Для подключения обработчика запросов к сервису используется функция RegisterServiceCtrlHandler. Функция, определяющая точку входа сервиса, должна иметь следующий прототип: VOID WINAPI имя_точки_входа(DWORD dwArgc, LPTSTR *lpszArgv); Если определяется только один сервис, то эта функция обычно называется serviceMain, хотя возможны и другие, более подходящие по смыслу имена точек входа сервисов. Если же в приложении определяется несколько сервисов, то естественно каждый из них должен иметь свое имя. Эта функция содержит два параметра, которые аналогичны параметрам функции main консольного приложения. Параметр dwArgc содержит количество аргументов в массиве lpszArgv, а сам этот массив содержит адреса строк. Причем первый из этих адресов указывает на строку, содержащую имя сервиса, а последующие аргументы передаются из функции startservice. Функция, определяющая обработчик управляющих запросов, должна иметь следующий прототип: VOID WINAPI имя_обработчика_запросов(DWORD dwControl); Если определяется только один сервис, то эта функция обычно называется ServicectriHandier. Если же в приложении определяется несколько сервисов, то естественно, что обработчик запросов для каждого сервиса должен иметь свое имя. Эта функция содержит только один параметр, который содержит код управляющего сигнала. Более подробно возможные коды сигналов будут рассмотрены в разд. 33.5, посвященном обработчику запросов. Так как обработчик запросов вызывается диспетчером сервиса, то и коды управляющих сигналов он получает от своего диспетчера. 33.3. Организация функции main Как уже было сказано, главной задачей функции main является запуск диспетчера сервиса для каждого из сервисов. Для запуска диспетчера используется функция StartServiceCtrlDispatcher, которая ДОЛЖНа быть вызвана 20 Зак. 514
610 Часть X. Разработка сервисов в Windows в течение 30 секунд с момента запуска программы main. Если в течение этого Промежутка времени фуНКЦИЯ StartServiceCtrlDispatcher вызвана не будет, то последующий вызов этой функции закончится неудачей. Поэтому всю необходимую инициализацию сервиса нужно делать в самом сервисе, т. е. в теле функции serviceMain. Если же необходимо выполнить инициализацию глобальных переменных, то для этой цели лучше запустить из ФУНКЦИИ main ОТДеЛЬНЫЙ ПОТОК. ФуНКЦИЯ StartServiceCtrlDispatcher имеет следующий ПроТОТИП! BOOL StartServiceCtrlDispatcher( CONST LPSERVICE_TABLE_ENTRY IpServiceTable // таблица сервисов ); В случае успешного завершения фуНКЦИЯ StartServiceCtrlDispatcher ВОЗ- вращает ненулевое значение, а в случае неудачи — false. Функция имеет единственный параметр, который указывает на таблицу сервисов, которая представляет собой массив. Каждый элемент этого массива имеет следующий тип: typedef struct _SERVICE_TABLE_ENTRY { LPSTR IpServiceName; // имя сервиса LPSERVICE_MAIN_FUNCTION IpServiceProc; // функция сервиса } SERVICE_TABLE_ENTRY/ *LPSERVICE_TABLE_ENTRY; Поле IpServiceName содержит указатель на строку, содержащую имя сервиса, а поле IpServiceProc указывает на функцию самого сервиса. При этом отметим, что последний элемент таблицы сервисов должен содержать пустую структуру, т. е. структуру с пустыми указателями. Ниже в листинге 33.1 приведен пример программы main, которая запускает диспетчер для одного сервиса. Перед телом функции объявлены все глобальные имена, которые в дальнейшем используются в сервисе. Г Листинг 33,1. Пример функции main сервиса #include <windows.h> #include <fstream.h> char service_name [ ] = "DemoService"; // имя сервиса SERVICE_STATUS service_status; SERVICE_STATUS_HANDLE hServiceStatus; VOID WINAPI ServiceMain (DWORD dwArgc, LPTSTR *lpszArgv) ;
Глава 33. Сервисы в Windows 611 VOID WINAPI ServiceCtrlHandler(DWORD dwControl); ofstream out; // выходной файл для протокола работы сервиса int nCount; // счетчик // главная функция приложения int main() { // инициализируем структуру сервисов SERVICE_TABLE_ENTRY service_table[] = { {service_name, ServiceMain}, // имя сервиса и функция сервиса { NULL, NULL } // больше сервисов нет }/ // запускаем диспетчер сервиса if (IStartServiceCtrlDispatcher(service_table)) { out.open("С:\\ServiceFile.log"); out « "Start service control dispatcher failed."; out.close(); return 0; } return 0; } 33.4. Организация функции ServiceMain Как уже говорилось, функция, определяющая точку входа сервиса, должна иметь следующий прототип: VOID WINAPI имя_точки_входа(DWORD dwArgc, LPTSTR *lpszArgv); Если в приложении определен только один сервис, то эта функция обычно называется serviceMain. В противном случае точка входа каждого сервиса должна иметь уникальное имя. Здесь параметр dwArgc содержит количество аргументов в массиве lpszArgv, а сам этот массив содержит адреса строк. Причем первый из этих адресов указывает на строку, содержащую имя сервиса, а последующие аргументы передаются из функции startservice.
612 Часть X. Разработка сервисов в Windows Функция serviceMain должна выполнить следующую последовательность действий: 1. Немедленно запустить обработчик управляющих команд от менеджера сервисов, Вызвав фуНКЦИЮ RegisterServiceCtrlHandler. 2. Установить стартующее состояние сервиса service_start_pending посредством ВЫЗОВа функции SetServiceStatus. 3. Провести локальную инициализацию сервиса. 4. Установить рабочее состояние сервиса service_running посредством вызова функции SetServiceStatus. 5. Выполнять работу сервиса, учитывая состояния сервиса, которые могут изменяться обработчиком управляющих команд от менеджера сервисов. 6. После перехода в состояние останова service_stopped выполнить освобождение захваченных ресурсов и закончить работу. Опишем подробно функции, которые должны использоваться в сервисе. Для запуска обработчика управляющих команд от менеджера сервисов сервис ДОЛЖен ИСПОЛЬЗОВать фуНКЦИЮ RegisterServiceCtrlHandler, которая имеет следующий прототип: SERVICE_STATUS_HANDLE RegisterServiceCtrlHandler( LPCTSTR IpServiceName, // имя сервиса LPHANDLER_FUNCTION lpHandlerProc // функция обработчика ); В случае успешного завершения функция возвращает дескриптор состояния сервиса, а в случае неудачи — ноль. Параметр IpServiceName этой функции должен указывать на строку, которая содержит имя сервиса, а параметр lpHandlerProc — на процедуру обработки управляющих команд от менеджера сервисов. Для запуска обработчика управляющих команд может также использоваться ФУНКЦИЯ RegisterServiceCtrlHandlerEx, которая Имеет более широкие ВОЗМОЖНОСТИ ПО сравнению С функцией RegisterServiceCtrlHandler. ДЛЯ Изменения СОСТОЯНИЯ сервиса ИСПОЛЬЗуеТСЯ фуНКЦИЯ SetServiceStatus, которая имеет следующий прототип: BOOL SetServiceStatus( SERVICE_STATUS_HANDLE hServiceStatus, // дескриптор состояния сервиса LPSERVICE_STATUS IpServiceStatus // структура состояния ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false.
Глава 33. Сервисы в Windows 613 Параметр hservicestatus должен содержать дескриптор состояния сервиса, КОТОРЫЙ был получен функцией RegisterServiceCtrlHandler. Параметр lpservicestatus должен указывать на структуру типа service_status, которая содержит информацию о состоянии сервиса. Эта структура имеет следующий формат: typedef struct _SERVICE_STATUS { DWORD dwServiceType; // тип сервиса DWORD dwCurrentState; // текущее состояние сервиса DWORD dwControlsAccepted; // допускаемое управление DWORD dwWin32ExitCode; // код возврата для Win32 DWORD dwServiceSpecificExitCode; // специфический код возврата // сервиса DWORD dwCheckPoint; // контрольная точка DWORD dwWaitHint; // период ожидания команды управления } SERVICE_STATUS, *LPSERVICE_STATUS; Поле dwServiceType содержит тип сервиса и может принимать следующие значения: □ service_win32_own_process — сервис является самостоятельным процессом; □ service_win32_share_process — сервис разделяет процесс с другими сервисами; □ service_kernel_driver — сервис является драйвером устройства; □ service_file_system_driver — сервис является драйвером файловой системы. Кроме того, первые два флага могут быть установлены совместно с флагом service_interactive_process — сервис может взаимодействовать с рабочим столом. То есть сервисы, которые не являются драйверами, могут взаимодействовать с рабочим столом. Поле dwCurrentState содержит текущее состояние сервиса. Это поле может принимать одно из следующих значений: □ service_stopped — сервис остановлен; □ service_start_pending — сервис стартует; □ service_stop_pending — сервис останавливается; □ service_running — сервис работает; □ service_continue_pending — сервис переходит в рабочее состояние; □ service_pause_pending — сервис переходит в состояние ожидания; □ service_paused — сервис находится в состоянии ожидания.
614 Часть X. Разработка сервисов в Windows Поле dwControlsAccepted содержит коды управляющих команд, которые могут быть переданы обработчику этих команд, определенному в приложении. В этом поле может быть установлена любая комбинация следующих флагов: П service_accept_stop — можно остановить сервис; П SERVICE_ACCEPT_PAUSE_CONTINUE — МОЖНО ПрИОСТаНОВИТЬ И ВОЗОбнОВИТЬ сервис; П service_accept_shutdown — сервис информируется о выключении системы. Кроме того, в операционной системе Windows 2000 в этом поле может быть дополнительно установлена любая комбинация следующих флагов: П service_accept_paramchange — сервис может вновь прочитать свои параметры; П service_accept_netbindchange — сервис является сетевой компонентой; П service_accept_hardwareprofilechange — сервер информируется об изменении аппаратного обеспечения компьютера; П service_accept_powerevent — сервис информируется об изменении состояния питания компьютера. Если установлен флаг service_accept_netbindchange, to обработчик управляющих команд сервиса может также получить следующие команды: □ service_control_netbindadd — к сети подключен новый компонент; П service_control_netbindremove — из сети удален компонент; П service_control_netbindenable — одна из связей стала доступна; П service_control_netbinddisable — одна из связей стала недоступна. Обработчик управляющих команд может получить команду service_accept_powerevent только в том случае, если он был установлен функцией RegisterServiceCtrlHandlerEx. Кроме того, по умолчанию предполагается, что каждый обработчик может получить команду: П service_control_interrogate — немедленно обновить статус сервиса. Поле dwwin3 2Exitcode может содержать одно из следующих двух значений: П error_service_specific_error — ошибка во время запуска или остановки сервиса; П no_error — ошибки нет. Если в этом поле установлено значение error_service_specific_error, to КОД самой ОШИбки находится В следующем ПОЛе dwServiceSpecif icExitCode. ЭТОТ КОД МОЖеТ быть ПОЛучеН функцией GetLastError.
Глава 33. Сервисы в Windows 615 Поле dwServiceSpecificExitcode содержит код возврата из сервиса, этот код действителен только в том случае, если в поле dwwin32Exitcode установлено значение error_service_specific_error. Поле dwCheckPoint содержит значение, которое сервис должен периодически увеличивать на единицу, сообщая о продвижении своей работы во время инициализации и длительных переходов из состояния в состояние. Это значение может использоваться программой пользователя, которая отслеживает работу сервиса. Если это значение не используется пользовательской программой и переход из состояния в состояние занимает менее 30 секунд, то оно может быть установлено в о. Поле dwWaitHint содержит интервал времени в миллисекундах, в течение которого сервис переходит из состояния в состояние перед вызовом функции установки состояния setservicestatus. Если в течение этого интервала не произошло изменение состояния сервиса в поле dwServicestate или не изменилось значение поля dwCheckPoint, то менеджер сервисов считает, что в сервисе произошла ошибка. В листинге 33.2 приведен пример функции serviceMain сервиса. | ЛИСТИНГ 33.2, Пример функции ServiceMain сервиса VOID WINAPI ServiceMain (DWORD dwArgc, LPTSTR *lpszArgv) { // регистрируем обработчик управляющих команд для сервиса hServiceStatus = RegisterServiceCtrlHandler( service_name, // имя сервиса ServiceCtrlHandler // обработчик управляющих команд ); if (!hServiceStatus) { out.open("С:\\ServiceFile.log"); out « "Register service control handler failed."; out.close(); return; } // инициализируем структуру состояния сервиса service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS; service_status . dwCurrentState = SERVICE_START_PENDING; service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP |
616 Часть X. Разработка сервисов в Windows SERVICE_ACCEPT_SHUTDOWN; service_status.dwWin32ExitCode = ERROR_SERVICE_SPECIFIC_ERROR; service_status.dwServiceSpecificExitCode = 0; service_status.dwCheckPoint = 0; service_status.dwWaitHint = 5000; // устанавливаем состояние сервиса if (!SetServiceStatus(hServiceStatus, &service_status)) { out.open("C:\\ServiceFile.log"); out « "Set service status 'SERVICE_START_PENDING' failed."; out.close(); return; } // определяем сервис как работающий service_status.dwCurrentState = SERVICE_RUNNING; // нет ошибок service_status.dwWin32ExitCode = NO_ERROR; // устанавливаем новое состояние сервиса if (ISetServiceStatus(hServiceStatus, &service_status)) { out.open("C:\\ServiceFile.log"); out « "Set service status 'START_PENDING' failed."; out.close(); return; } // открываем файл протокола работы сервиса out.open("С:\\ServiceFile.log"); out « "The service is started." « endl « flush; out « "My name is: " « lpszArgv[0] « endl « flush; for (int i = 1; i < (int)dwArgc; ++i) out « "My " « i « " parameter = " « lpszArgv[i] « endl « flush; // рабочий цикл сервиса while (service_status.dwCurrentState == SERVICE_RUNNING)
Глава 33. Сервисы в Windows 617 { ++nCount; SleepC000); } } 33.5. Организация обработчика управляющих команд Функция-обработчик управляющих запросов, должна иметь следующий прототип: VOID WINAPI имя_обработчика_запросов(DWORD dwControl); Если приложение содержит только один сервис, то эта функция обычно называется servicectriHandier. В противном случае обработчик запросов для каждого сервиса должен иметь свое имя. Эта функция содержит только один параметр, который содержит код управляющего сигнала. Возможны следующие управляющие коды: □ service_control_stop — остановить сервис; П service_control_pause — приостановить сервис; П service_control_continue — возобновить сервис; П service_control_interrogate — обновить состояние сервиса; П service_control_shutdown — закончить работу сервиса. Дополнительно в операционной системе Windows 2000 допускаются следующие коды управляющих команд: П service_control_paramchange — изменились параметры сервиса; П service_control_netbindadd — соединиться с новым компонентом сети; П service_control_netbindremove — отсоединиться от компонента сети; П SERVICE_CONTROL_NETBINDENABLE — СВЯЗЬ С КОМПОНеНТОМ СеТИ ВОССТаНОВ- лена; □ service_control_netbinddisable — связь с компонентом сети разорвана. Но обработчик должен обрабатывать только те команды, которые допускаются в поле dwControisAccepted структуры типа service_status, рассмотренной в предыдущем разделе. Напомним, что по соглашению обработчик всегда получает сигнал с кодом service_control_interrogate, по которому он должен немедленно обновить состояние сервиса. Для обновления состояния сервиса используется функция setservicestatus, которая была рассмотрена в предыдущем разделе. Кроме того, обработчик может получать
618 Часть X. Разработка сервисов в Windows коды, определенные пользователем. Для кодов пользователя зарезервирован диапазон от 128 до 255. Вызывается обработчик управляющих запросов диспетчером сервиса и, следовательно, выполняется в его контексте. Обработчик должен изменить состояние сервиса в течение 30 секунд, в противном случае диспетчер сервисов считает, что произошла ошибка. Если для изменения состояния сервиса требуется более продолжительный интервал времени, то для этой цели нужно запустить отдельный поток. Для обработки кода service_control_shutdown сервису отводится 20 секунд, в течение которых он должен освободить захваченные им ресурсы. В листинге 33.3 приведена функция обработки управляющих команд для сервиса из предыдущего раздела. [Листинг 33.3. Пример функции обработки управляющих команд VOID WINAPI ServiceCtrlHandler(DWORD dwControl) { switch(dwControl) { case SERVICE_CONTROL_STOP: // остановить сервис // записываем конечное значение счетчика out « "Count = " « nCount « endl; out « "The service is finished." « endl << flush; // закрываем файл out.close(); // устанавливаем состояние остановки service_status.dwCurrentState = SERVICE_STOPPED; // изменить состояние сервиса SetServiceStatus(hServiceStatus, &service_status); break; case SERVICE_CONTROL_SHUTDOWN: // завершить сервис service_status.dwCurrentState = SERVICE_STOPPED; // изменить состояние сервиса SetServiceStatus(hServiceStatus, &service_status); break; default:
Глава 33. Сервисы в Windows 619 // увеличиваем значение контрольной точки ++service_status.dwCheckPoint; // оставляем состояние сервиса без изменения SetServiceStatus(hServiceStatus, &service_status); break; } return; } В заключение этого раздела отметим, что если обработчик регистрируется функцией RegisterServiceCtrlHandlerEx, TO ОН ДОЛЖеН ИМеТЬ Другой про- тотип, который предоставляет более широкие возможности по обработке управляющих команд. В этом случае функция обработки обычно называется HandierEx, а ее прототип можно найти в справочной документации.
Глава 34 Работа с сервисами в Windows 34.1. Открытие доступа к базе данных сервисов Для установки связи с менеджером сервисов и открытия доступа к базе данных сервисов используется функция OpenSCManager, которая имеет следующий прототип: SC_HANDLE OpenSCManager( LPCTSTR lpMachineName, II имя компьютера LPCTSTR lpDatabaseName, // имя базы данных сервисов DWORD dwDesiredAccess // тип доступа ); В случае успешного завершения функция возвращает дескриптор базы данных сервисов, а в случае неудачи — null. Теперь опишем назначение параметров функции OpenSCManager. Параметр lpMachineName должен содержать адрес строки с именем компьютера, на котором находится база данных сервисов. Имени компьютера должны предшествовать символы \\. Если этот параметр содержит null или указывает на пустую строку, то предполагается, что база данных расположена на локальном компьютере. Параметр lpDatabaseName должен содержать адрес строки с именем базы данных сервисов. Если этот параметр равен null, to открывается активная база данных сервисов. Параметр dwDesiredAccess должен содержать тип доступа к базе данных. Этот параметр может принимать любую комбинацию следующих флагов: □ sc_manager_all_access — любой из нижеперечисленных доступов; П sc_manager_connect — связь с менеджером сервисов; □ sc_manager_create_service — установка сервиса;
Глава 34. Работа с сервисами в Windows 621 О sc_manager_enumerate_service — просмотр сервисов в базе данных; □ sc_manager_lock — запирание базы данных сервисов; □ sc_manager_query_lock_status — определение состояния базы данных. При этом отметим, что флаг sc_manager_connect считается установленным по умолчанию, а флаг sc_manager_all_access включает в себя флаг STANDARD_RIGHTS_REQUIRED. Для использования в этом параметре также определены флаги, обозначающие следующие родовые права доступа к базе данных сервисов: □ generic_read — включает следующие флаги: standard_rights_read, SC_MANAGER_ENUMERATE_SERVICE И SC_MANAGER_QUERY_LOCK_STATUS; □ GENERIC_WRITE — ВКЛЮЧаеТ флаГИ STANDARD_RIGHTS_WRITE И sc_manager_create_service; □ generic_execute — включает следующие флаги: standard_rights_execute, SC_MANAGER_CONNECT И SC_MANAGER_LOCK. Возможна установка любой комбинации этих флагов. После окончания работы с менеджером сервисов или сервисом нужно закрыть его дескриптор. Для этой цели предназначена функция CloseServiceHandle, КОТОраЯ имеет следующий ПроТОТИП: BOOL CloseServiceHandle( SC_HANDLE hSCObject // дескриптор менеджера сервисов или сервиса ); В случае успешного завершения эта функция возвращает ненулевое значение, а в случае неудачи — false. Единственным параметром этой функции является дескриптор менеджера сервисов или дескриптор сервиса. Пример использования функций OpenSCManager И CloseServiceHandle будет приведен в следующем разделе, в котором будет рассмотрена программа установки сервиса. В заключение этого раздела отметим, что процесс, связывающийся с менеджером сервисов и открывающий базу данных сервисов, должен иметь права и привилегии администратора. 34.2. Установка сервиса Для установки сервисов в базу данных используется функция createService, которая имеет следующий прототип: SC_HANDLE CreateService( SC_HANDLE hSCManager // дескриптор базы данных сервисов LPCTSTR IpServiceName // внутреннее имя сервиса
622 Часть X. Разработка сервисов в Windows LPCTSTR lpDisplayName DWORD DWORD DWORD DWORD LPCTSTR LPCTSTR LPDWORD LPCTSTR LPCTSTR LPCTSTR // внешнее имя сервиса // разрешаемый доступ к сервису // тип сервиса // способ запуска сервиса // режим обработки ошибок // имя бинарного файла сервиса // имя группы загрузки сервисов // тег сервиса в группе // массив имен сервисов IpServiceStartName // имя пользователя lpPassword // пароль пользователя dwDesiredAccess dwServiceType dwStartType dwErrorControl lpBinaryPathName lpLoadOrderGroup lpdwTagld lpDependencies ); В случае успешного завершения функция возвращает дескриптор установленного сервиса, а в случае неудачи — null. Рассмотрим назначение параметров этой функции. Параметр hscManager должен содержать дескриптор менеджера сервисов, КОТОРЫЙ был Предварительно ОТКРЫТ функцией OpenSCManager. Параметр lpServiceName должен указывать на строку с именем сервиса, которое используется менеджером сервисов. Имена сервисов не чувствительны к верхнему и нижнему регистрам. Длина имени не может превышать 256 символов. Имя сервиса не может содержать символы / и \. Параметр lpDisplayName должен указывать на имя сервиса, которое используется в панели управления Windows для ссылок на сервис. В этом имени различаются верхний и нижний регистры, а его длина не может превышать 256 символов. В параметре dwDesiredAccess должны быть установлены флаги, которые определяют доступ к сервису. Возможна любая комбинация следующих флагов: □ delete — разрешено удаление сервиса; □ service_change_config — разрешено изменять конфигурацию сервиса; □ service_enumerate_dependents — разрешено просматривать зависимые сервисы; □ service_interrogate — разрешено запрашивать состояние сервиса; □ service_pause_continue — разрешено приостанавливать и возобновлять сервис; □ service_query_config — разрешено запрашивать конфигурацию сервиса; □ service_query_status — разрешено запрашивать состояние сервиса; □ service_start — разрешено запускать сервис;
Глава 34. Работа с сервисами в Windows 623 О service_stop — разрешено останавливать сервис; □ service_user_defined_control — разрешено использовать управляющие команды пользователя. Кроме того, для обеспечения возможности работы с атрибутами защиты сервиса нужно установить комбинированный флаг standard_rights_required — стандартные права доступа, который также включает флаг delete. Также можно использовать флаг service_all_access — полный доступ, который включает флаг standard_rights_required и все перечисленные выше флаги. Дополнительно существуют следующие родовые права доступа, которые можно установить в этом параметре: □ generic_read — включает флаги standard_rights_read, service_ query_config, service_query_status, service_interrogate и service_ enumerate_dependents; d generic_write — включает флаги standard_rights_write и service_ change_config; □ generic_execute — включает флаги standard_rights_execute, service_start, SERVICE_STOP, SERVICE_PAUSE_CONTINUE И SERVICE_USER_DEFINED_CONTROL. В параметре dwServiceType задается тип сервиса. Этот параметр может принимать одно из следующих значений: □ service_win32_own_process — отдельный процесс; □ service_win32_share_process — разделяет процесс с другими сервисами; О service_kernel_driver — драйвер ядра; П service_file_system_driver — драйвер файловой системы. В случае если сервис не является драйвером, то может быть также установлен флаг service_interactive_process — интерактивный процесс, который разрешает сервису взаимодействовать с рабочим столом. В параметре dwstartType должен быть установлен режим запуска сервиса. Этот параметр может принимать одно из следующих значений: □ service_boot_start — запуск драйвера при загрузке системы; О service_system_start — запуск драйвера функцией ioinitsystem; □ service_auto_start — запуск сервиса менеджером сервисов при загрузке системы; О service_demand_start — запуск сервиса функцией startservice; □ service_disabled — сервис не может быть запущен. Заметим, что первые два флага могут быть установлены только для драйверов. В параметре dwErrorcontroi устанавливается режим обработки ошибок, произошедших во время запуска сервиса, если этот сервис запускается во
624 Часть X. Разработка сервисов в Windows время загрузки системы. В этом параметре должно быть установлено одно из следующих значений: □ SERVICE_ERROR_IGNORE — Игнорировать Ошибку; □ SERVICE_ERROR_NORMAL — Игнорировать ОШИбку И СООбщИТЬ об ЭТОМ В ОКНе ошибок; □ service_error_severe — продолжить или перезагрузиться в безопасной конфигурации; □ service_error_critical — прервать загрузку и перезагрузиться в безопасной конфигурации. В последнем случае если ошибка произошла при загрузке системы в безопасной конфигурации, то загрузка прекращается. Параметр lpBinaryPathName должен указывать на имя исполняемого ехе- файла, который содержит сервис. Рекомендуется, чтобы это имя заканчивалось символом \. Параметр lpLoadOrderGroup должен указывать на имя группы, содержащей порядок загрузки сервисов и в которую входит сервис. Если сервис не зависит от других сервисов, то в этом параметре должно быть установлено значение NULL. В параметре lpdwTagid должен быть задан адрес переменной типа dword, по которому функция помещает тег сервиса в группе загрузки. Этот тег может в дальнейшем использоваться для определения порядка запуска сервисов. Если тег не используется, то этот параметр нужно установить в null. При этом отметим, что тег может использоваться только для драйверов, для которых задан режим загрузки service_boot_start или service_system_start. Параметр lpDependencies должен указывать на массив строк, каждая из которых содержит имя сервиса или имя группы сервисов, которые система должна запустить перед запуском устанавливаемого сервиса. Если сервис не зависит от других сервисов, то в этом параметре должно быть установлено значение null. Отметим, что имя группы должно начинаться с префикса sc_group_identifier, чтобы отличить это имя от имени сервиса. Параметр lpServicestartName должен указывать на строку с именем сервиса. Если сервис является процессом, то этот параметр должен указывать на имя пользователя, под которым будет запущен сервис. Если сервис запускается под текущим локальным именем, то этот параметр должен быть установлен в null. Сервис, взаимодействующий с рабочим столом, должен быть запущен только под текущим именем пользователя. Параметр lpPassword должен указывать на строку с паролем пользователя, под именем которого запускается сервис. Если сервис запускается под именем текущего пользователя, то в этом параметре должно быть установлено значение null.
Глава 34. Работа с сервисами в Windows 625 В листинге 34.1 приведена профамма, в которой устанавливается сервис, рассмотренный в гл. 33. При этом заметим, что исполняемый файл DemoService.exe содержит коды функций, представленных в листингах 33.1, 33.2 и 33.3. Листинг 34,1. Установка сервиса #include <windows.h> #include <iostream.h> int main() { SC_HANDLE hServiceControlManager, hService; // связываемся с менеджером сервисов hServiceControlManager = OpenSCManager( NULL, // локальная машина NULL, // активная база данных сервисов SC_MANAGER_CREATE_SERVICE // возможно создание сервиса ); if (hServiceControlManager == NULL) { cout « "Open service control manager failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.get(); return 0; } cout << "Service control manager is opened." « endl « "Press any key to continue." « endl; cin.get(); // устанавливаем новый сервис hService = CreateService( hServiceControlManager, // дескриптор менеджера сервисов "DemoService", // внутреннее имя сервиса, используемое SCM "Demo_Service", // внешнее имя сервиса в панели управления SERVICE_ALL_ACCESS, // полный контроль над сервисом
626 Часть X. Разработка сервисов в Windows SERVICE_WIN32_OWN_PROCESS, // сервис является процессом SERVICE JDEMAND_START, // запускается сервис по требованию SERVICE_ERROR_NORMAL, // обработка ошибок нормальная "С: WDemoService.exe", // путь к сервису NULL, // сервис не принадлежит к группе NULL, // тег группы не изменяется NULL, // сервис не зависит от других сервисов NULL, // имя совпадает с текущим именем учетной записи NULL // пароля нет ); if (hService == NULL) { cout « "Create service failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.getO ; // закрываем дескриптор менеджера сервисов CloseServiceHandle(hServiceControlManager); return 0; } cout « "Service is installed." « endl « "Press any key to exit." « endl; cin.getO ; // закрываем дескрипторы CloseServiceHandle(hService); CloseServiceHandle(hServiceControlManager); return 0; } Напомним, что после окончания работы с сервисом нужно закрыть его дескриптор, ИСПОЛЬЗуЯ ДЛЯ ЭТОГО фуНКЦИЮ CloseServiceHandle, КОТОрая была рассмотрена в предыдущем разделе. Отметим, что эта функция не уничтожает сервис, а только разрывает связь прикладной программы с этим сервисом. Для удаления сервиса из базы данных нужно использовать функцию DeleteService.
Глава 34. Работа с сервисами в Windows 627 34.3. Открытие доступа к сервису Для открытия доступа к уже установленному сервису используется функция openservice, которая имеет следующий прототип: SC_HANDLE OpenService( SC_HANDLE hSCManager, // дескриптор базы данных сервисов LPCTSTR IpServiceName, // имя сервиса DWORD dwDesiredAccess // требуемый доступ к сервису ); В случае успешного завершения функция возвращает дескриптор сервиса, а в случае неудачи — null. Параметр hscManager должен содержать дескриптор базы данных сервисов, связь с которой была предварительно установлена вызовом функции OpenSCManager. Параметр lpServiceName должен указывать на строку с именем сервиса. В параметре dwDesiredAccess устанавливаются флаги доступа к сервису. Флаги доступа, которые можно установить в этом параметре, совпадают С флагами, КОТОрые МОЖНО установить В параметре dwDesiredAccess фуНК- ции createservice, рассмотренной в предыдущем разделе. В следующем разделе приведен пример программы, в которой открывается доступ к уже установленному сервису для запуска этого сервиса из приложения. 34.4. Запуск сервиса Для запуска сервиса используется функция startservice, которая имеет следующий прототип: BOOL Startservice( SC_HANDLE hService, // дескриптор сервиса DWORD dwNumServiceArgs, // количество аргументов LPCTSTR *lpServiceArgVectors // массив аргументов В случае удачного завершения функция возвращает ненулевое значение, а в случае неудачи — false. В параметре hservice должен быть установлен дескриптор сервиса, который был Предварительно ПОЛучен ФУНКЦИЯМИ CreateService ИЛИ OpenService. Параметр dwNumServiceArgs должен содержать количество аргументов, которые передаются сервису. Каждый из аргументов является символьной строкой. Если аргументов нет, то в параметре должно быть установлено значение ноль.
628 Часть X. Разработка сервисов в Windows Параметр lpServiceArgVectors должен содержать адрес массива, каждый элемент которого является указателем на строку. Эти строки и являются аргументами, которые передаются функции сервиса serviceMain. Если аргументов нет, то этот параметр должен быть установлен в null. Отметим, что во время работы функции startservice менеджер сервисов устанавливает следующие состояния сервиса в структуре service_status: □ в поле dwcurrentstate устанавливает значение service_start_pending; □ в поле dwControisAccepted устанавливает значение ноль, что запрещает обработку управляющих команд; □ в поле dwcheckPoint устанавливает значение ноль; □ в поле dwWaitHint устанавливает значение, равное двум секундам. Кроме того, во время работы функции startservice менеджер запросов блокирует базу данных сервисов. В листинге 34.2 приведен пример программы, которая открывает доступ к сервису, а затем запускает его. Этот сервис должен быть предварительно установлен программой, которая приведена в листинге 34.1. |, Листинг 34.2* Открытие и запуск сервиса # include <windows.h> #include <iostream.h> int main () { char service_name [ ] = "DemoService"; // имя сервиса char *param[] = { "pi", "p2", "p3" }; // список параметров SC_HANDLE hServiceControlManager, hService; // связываемся с менеджером сервисов hServiceControlManager = OpenSCManager( NULL, // локальная машина NULL, // активная база данных сервисов SC_MANAGER_CONNECT // соединение с менеджером сервисов ); if (hServiceControlManager == NULL) { cout « "Open service control manager failed." « endl « "The last error code: " « GetLastError() « endl
Глава 34. Работа с сервисами в Windows 629 « "Press any key to continue." « endl; cin.get(); return 0; } cout « "Service control manager is opened." « endl « "Press any key to continue." « endl; cin.getO ; // открываем сервис hService = OpenService( hServiceControlManager, // дескриптор менеджера сервисов- service_name, // имя сервиса SERVICE_ALL_ACCESS // полный доступ к сервису ); if (hService == NULL) { cout « "Open service failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.get(); // закрываем дескриптор менеджера сервисов CloseServiceHandle(hServiceControlManager); return 0; } cout « "Service is opened." « endl « "Press any key to continue." « endl; cin.get(); // стартуем сервис if (!StartService( hService, // дескриптор сервиса 3, // три параметра (const char**)param // указатель на массив параметров
630 Часть X. Разработка сервисов в Windows { cout « "Start service failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.get(); // закрьшаем дескрипторы CloseServiceHandle(hServiceControlManager); CloseServiceHandle(hService); return 0; } cout « "The service is started." « endl « "Press any key to exit." « endl; cin.get(); // закрываем дескрипторы CloseServiceHandle(hServiceControlManager); CloseServiceHandle(hService); return 0; } 34.5. Определение и изменение состояния сервиса Для определения состояния сервиса используется функция QueryServicestatus, которая имеет следующий прототип: BOOL QueryServicestatus( SC_HANDLE hService, // дескриптор сервиса LPSERVICE_STATUS IpServiceStatus // адрес структуры SERVICE_STATUS ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. В параметре hservice должен быть установлен дескриптор сервиса, который Предварительно получен функцией CreateService ИЛИ OpenService.
Глава 34. Работа с сервисами в Windows 631 Параметр ipservicestatus должен указывать на структуру типа service_status, которая содержит информацию о состоянии сервиса. Эта структура имеет следующий формат: typedef struct _SERVICE_STATUS { DWORD dwServiceType; // тип сервиса DWORD dwCurrentState; // текущее состояние сервиса DWORD dwControlsAccepted; // допускаемое управление DWORD dwWin32ExitCode; // код возврата для Win32 DWORD dwServiceSpecificExitCode; // специфический код возврата // сервиса DWORD dwCheckPoint; // контрольная точка DWORD dwWaitHint; // период ожидания команды управления } SERVICE_STATUS, * LPSERVICE_STATUS; Подробнее назначение полей этой структуры было описано в разд. 33.4, посвященном организации ФУНКЦИИ ServiceMain. В листинге 34.3 приведена программа, в которой определяется состояние сервиса посредством ВЫЗОВа фуНКЦИИ QueryServiceStatus. | Листинг 34.3. Определение состояния сервиса \ # include <windows.h> #include <iostream.h> int main() { char service_name [ ] = "DemoService"; // имя сервиса SC_HANDLE hServiceControlManager, hService; SERVICE_STATUS service_status; // связываемся с менеджером сервисов hServiceControlManager = OpenSCManager( NULL, // локальная машина NULL, // активная база данных сервисов SC_MANAGER_CONNECT // соединение с менеджером сервисов ); if (hServiceControlManager == NULL) { cout « "Open service control manager failed." « endl « "The last error code: " « GetLastError() « endl
632 Часть X. Разработка сервисов в Windows « "Press any key to continue." « endl; cin.get(); return 0; } cout « "Service control manager is opened." « endl « "Press any key to continue." « endl; cin.get(); // открываем сервис hService = OpenService( hServiceControlManager, // дескриптор менеджера сервисов service_name, // имя сервиса SERVICE_ALL_ACCESS // любой доступ к сервису ); if (hService == NULL) { cout « "Open service failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.get(); // закрываем дескриптор менеджера сервисов CloseServiceHandle(hServiceControlManager); return 0; } cout « "Service is opened." « endl « "Press any key to continue." « endl; cin.get(); // определение состояния сервиса if (IQueryServiceStatus( hService, // дескриптор сервиса &service_status // адрес структуры состояния сервиса
Глава 34. Работа с сервисами в Windows 633 cout « "Query service sataus failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.get(); // закрываем дескрипторы CloseServiceHandle(hServiceControlManager); CloseServiceHandle(hService); return 0; } switch (service_status.dwCurrentState) { case SERVICE_STOPPED: cout « "The service is stopped." « endl; break; case SERVICE_RUNNING: cout « "The service is running." « endl; break; default: cout « "The service status: " « service_status .dwCurrentState « endl; break; } cout « "Press any key to exit." « endl; cin.getO ; // закрываем дескрипторы CloseServiceHandle(hServiceControlManager); CloseServiceHandle(hService); return 0; } ДЛЯ Изменения СОСТОЯНИЯ сервиса ИСПОЛЬЗуетсЯ фуНКЦИЯ SetServiceStatus, которая была подробно рассмотрена в разд. 33.4, посвященном функции ServiceMain.
634 Часть X. Разработка сервисов в Windows 34.6. Определение и изменение конфигурации сервиса Под конфигурацией сервиса понимается информация, которая описывает внутреннее и внешнее имена сервиса, режимы его запуска и работы. Для определения конфигурации сервиса используется функция QueryServiceConf ig, которая имеет следующий прототип: BOOL QueryServi сеСon f ig( SC_HANDLE hService, // дескриптор сервиса LPQUERY_SERVICE_CONFIG IpServiceConfig, // указатель на буфер DWORD dwBufferSize, // размер буфера LPDWORD lpdwBytesNeeded // необходимая длина буфера ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. Параметры функции имеют следующее назначение. Параметр hService должен содержать дескриптор сервиса, который был предварительно ОТКРЫТ при ПОМОЩИ функции CreateService ИЛИ OpenService. Параметр IpServiceConf ig должен указывать на область памяти, в которую функция поместит информацию о конфигурации сервиса в случае своего успешного завершения. Эта информация будет храниться в формате, который определяется следующей структурой: typedef struct _QUERY_SERVICE_CONFIG { // тип сервиса // режим запуска сервиса // режим обработки ошибок при запуске // имя бинарного файла сервиса // информация о порядке загрузки сервисов // тег сервиса в группе загрузки // информация о порядке запуска сервисов // имя пользователя, который запускает // сервис // имя сервиса в окне управления сервисами LPQUERY_SERVICE_CONFIG; DWORD DWORD DWORD LPSTR LPSTR DWORD LPSTR LPSTR dwServiceType; dwStartType; dwErrorControl; lpBinaryPathName; lpLoadOrderGroup; dwTagld; lpDependencies ; IpServiceStartName LPSTR lpDisplayName; } QUERY_SERVICE_CONFIG, В поля этой структуры записываются те значения, которые были установлены В соответствующих параметрах При ВЫЗОВах функций CreateService И OpenService.
Глава 34. Работа с сервисами в Windows 635 В листинге 34.4 приведена программа, в которой при помощи функции Queryserviceconf ig определяется конфигурация сервиса. I Листинг 34.4. Определение конфигурации сервиса # include <windows.h> #include <iostream.h> int main() { char service_name[] = "DemoService"; // имя сервиса SC_HANDLE hServiceControlManager, hService; // указатель на буфер для информации о конфигурации сервиса QUERY_SERVICE_CONFIG* lpQueryServiceConfig; // переменная для размера структуры в случае неудачи функции DWORD dwBytesNeeded; // связываемся с менеджером сервисов hServiceControlManager = OpenSCManager( NULL, // локальная машина NULL, // активная база данных сервисов SC_MANAGER_CONNECT // соединение с менеджером сервисов ); if (hServiceControlManager == NULL) { cout « "Open service control manager failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to continue." « endl; cin.get(); return 0; } cout « "Service control manager is opened." « endl « "Press any key to continue." « endl; cin.get(); // открываем сервис
636 Часть X. Разработка сервисов в Windows hService = OpenService( hServiceControlManager, // дескриптор менеджера сервисов service_name, // имя сервиса SERVICE_ALL_ACCESS // любой доступ к сервису ); if (hService == NULL) { cout « "Open service failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.get(); // закрываем дескриптор менеджера сервисов CloseServiceHandle(hServiceControlManager); return 0; } cout « "Service is opened." « endl « "Press any key to continue." « endl; cin.get(); // захватываем память под буфер IpQueryServiceConfig = (LPQUERY_SERVICE_CONFIG)new char[4096]; // определяем конфигурацию сервиса if (!QueryServiceConfig( hService, // дескриптор сервиса IpQueryServiceConfig, // адрес структуры конфигурации сервиса 4096, // размер этой структуры &dwBytesNeeded // необходимое количество байтов )) { cout « "Query service configuartion failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.get(); // закрываем дескрипторы
Глава 34. Работа с сервисами в Windows 637 CloseServiceHandle(hServiceControlManager); CloseServiceHandle(hService); return 0; } // распечатываем информацию о конфигурации cout « "Service type: " « lpQueryServiceConfig->dwServiceType « endl « "Start type: " « lpQueryServiceConfig->dwStartType « endl « "Error control: " « lpQueryServiceConfig->dwErrorControl << endl « "Binary path name: " « lpQueryServiceConfig->lpBinaryPathName « endl « "Service start name: " « lpQueryServiceConfig->lpServiceStartName « endl « "Display name: " « lpQueryServiceConfig->lpDisplayName « endl « endl; // освобождаем память delete[] lpQueryServiceConfig; cout « "Press any key to exit." « endl; cin.getO ; // закрываем дескрипторы CloseServiceHandle(hServiceControlManager); CloseServiceHandle(hService); return 0; } Получить информацию о реакции диспетчера сервиса на завершение работы сервиса без установки состояния service_stopped можно при помощи вызова функции QueryServiceConfig2. Для изменения конфигурации сервиса можно использовать функцию ChangeServiceConf ig, которая Имеет следующий ПРОТОТИП! BOOL ChangeServiceConfig( SC_HANDLE hService, // дескриптор сервиса DWORD dwServiceType, // тип сервиса DWORD dwStartType, // режим запуска сервиса DWORD dwErrorControl, // режим обработки ошибок при запуске LPCSTR lpBinaryPathName, // имя бинарного файла сервиса
638 Часть X. Разработка сервисов в Windows LPCSTR lpLoadOrderGroup, // информация о порядке загрузки сервисов LPDWORD lpdwTagld, // тег сервиса в группе загрузки LPCSTR lpDependencies, // информация о порядке запуска сервисов LPCSTR IpServiceStartName, // имя пользователя LPCSTR lpPassword, // пароль пользователя LPCSTR lpDisplayName // имя сервиса в окне управления сервисами ); В случае удачного завершения функция возвращает ненулевое значение, а в случае неудачи — false. Параметры этой функции имеют тот же смысл, что И соответствующие параметры В ФУНКЦИЯХ CreateService И OpenService. В ЛИСТИНГе 34.5 приведен Пример, В КОТОРОМ фунКЦИЯ ChangeServiceConf ig используется для изменения режима запуска сервиса. Листинг 34.5. Изменение конфигурации сервиса #include <windows.h> #include <iostream.h> int main() { char service_name [ ] = "DemoService"; // имя сервиса SC_HANDLE hServiceControlManager, hService; // указатель на буфер для информации о конфигурации сервиса QUERY_SERVICE_CONFIG* lpQueryServiceConfig; // переменная для размера структуры в случае неудачи функции DWORD dwBytesNeeded; // связываемся с менеджером сервисов hServiceControlManager = OpenSCManager( NULL, // локальная машина NULL, // активная база данных сервисов SC_MANAGER_CONNECT // соединение с менеджером сервисов ); if (hServiceControlManager == NULL) { cout « "Open service control manager failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to continue." « endl;
Глава 34. Работа с сервисами в Windows 639 cin.get(); return 0; } cout « "Service control manager is opened." « endl « "Press any key to continue." « endl; cin.get(); // открываем сервис hService = OpenService( hServiceControlManager, // дескриптор менеджера сервисов service_name/ // имя сервиса SERVICE_ALL_ACCESS // любой доступ к сервису ); if (hService == NULL) { cout « "Open service failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.get(); // закрываем дескриптор менеджера сервисов CloseServiceHandle(hServiceControlManager); return 0; } cout « "Service is opened." « endl « "Press any key to continue." « endl; cin.get(); // захватываем память под буфер lpQueryServiceConfig = (LPQUERY_SERVICE_CONFIG)new char[4096]; // определям конфигурацию сервиса QueryServiceConfig( hService, lpQueryServiceConfig, 4096, ScdwBytesNeeded) ; // распечатываем информацию о режиме запуска сервиса
640 Часть X. Разработка сервисов в Windows cout « "Old start type: " « IpQueryServiceConfig->dwStartType « endl; // изменяем режим запуска сервиса if (!ChangeServiceConfig( hService, // дескриптор сервиса SERVICE_NO_CHANGE, // тип сервиса не изменяем SERVICE_AUTO_START, // запуск во время загрузки системы SERVICE_NO_CHANGE, // режим обработки ошибок не изменяем NULL, NULL, NULL, NULL, NULL, NULL, NULL // все остальные параметры // конфигурации не изменяем )) { cout « "Change service configuration failed." « endl <r< "The last error code: " « GetLastError () « endl « "Press any key to exit." « endl; cin.getO; // закрываем дескрипторы CloseServiceHandle(hServiceControlManager); CloseServiceHandle(hService); return 0; } // определям конфигурацию сервиса QueryServiceConfig( hService, IpQueryServiceConfig, 4096, ScdwBytesNeeded) ; // распечатываем информацию о режиме запуска сервиса cout « "New start type: " « IpQueryServiceConfig->dwStartType « endl; // восстанавливаем режим запуска сервиса ChangeServiceConfig( hService, // дескриптор сервиса SERVICE_NO_CHANGE, // тип сервиса не изменяем SERVICE_DEMAND_START, // запуск по требованию SERVICE_NO_CHANGE, // режим обработки ошибок не изменяем NULL, NULL, NULL, NULL, NULL, NULL, NULL // все остальные параметры
Глава 34. Работа с сервисами в Windows 641 // конфигурации не изменяем ); // определям конфигурацию сервиса QueryServiceConfig( hService, lpQueryServiceConfig, 4096, &dwBytesNeeded); // распечатываем информацию о режиме запуска сервиса cout « "Old start type: " « lpQueryServiceConfig->dwStartType « endl; // освобождаем память delete[] lpQueryServiceConfig; cout « "Press any key to exit." « endl; cin.getO ; // закрываем дескрипторы CloseServiceHandle(hServiceControlManager); CloseServiceHandle(hService); return 0; } Изменить информацию о реакции диспетчера сервиса на завершение работы сервиса без установки состояния service_stopped можно при помощи ВЫЗОВа функции ChangeServiceConf ig2. 34.7. Определение имени сервиса Назовем внутренним именем сервиса то имя, под которым сервис хранится в базе данных сервисов, а также используется менеджером сервисов для ссылок на этот сервис. Узнать внутреннее имя сервиса можно при помощи ФУНКЦИИ GetServiceKeyName, Которая Имеет следующий ПрОТОТИП! BOOL GetServiceKeyName ( SC_HANDLE hSCManager, // дескриптор базы данных сервисов LPCSTR lpDisplayName, // указатель на внешнее имя сервиса LPSTR IpServiceName, // указатель на внутреннее имя сервиса LPDWORD lpdwBuffer // размер памяти для внутреннего имени ); 21 Зак. 514
642 Часть X. Разработка сервисов в Windows В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. Параметры функции имеют следующее назначение. В параметре hscManager должен быть установлен дескриптор базы данных сервисов, Которая была Предварительно открыта функцией OpenSCManager. Параметр lpDispiayName должен указывать на строку с именем сервиса, которое используется в окне управления сервисами, вызываемом из Start | Settings | Control Panel | Administrative Tools | Services (Пуск | Настройка j Панель управление | Администрирование | Службы). Параметр lpServiceName должен указывать на область памяти, куда функция запишет внутреннее имя сервиса, т. е. имя, которое используется для ссылок менеджером сервисов. В случае удачного завершения функция записывает в эту память внутреннее имя сервиса. Параметр lpdwBuffer должен указывать на двойное слово, в котором установлена длина области памяти, на которую указывает параметр lpServiceName. Функция записывает по этому адресу фактическую длину внутреннего имени сервиса, исключая завершающий пустой символ. В листинге 34.6 приведена программа, в которой показано, как определить внутреннее имя сервиса по его внешнему имени при помощи функции GetServiceKeyName. # inc lude <windows. h> #include <iostream.h> int main() { char service_name [ ] = "DemoService"; // имя сервиса SC_HANDLE hServiceControlManager; // дескриптор базы данных сервисов // указатель на буфер для внутреннего имени сервиса char* IpszServiceName; // переменная для длины буфера в случае неудачи функции DWORD dwBufferSize; // связываемся с менеджером сервисов hServiceControlManager = OpenSCManager( NULL, // локальная машина NULL, // активная база данных сервисов
Глава 34. Работа с сервисами в Windows 643 SC_MANAGER_CONNECT // соединение с менеджером сервисов ); if (hServiceControlManager == NULL) { cout « "Open service control manager failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to continue." « endl; cin.getO ; return 0; } cout « "Service control manager is opened." « endl « "Press any key to continue." « endl; cin.getO ; // захватываем память под буфер dwBufferSize = 256; IpszServiceName = (char*)new char[dwBufferSize]; // определение внутреннего имени сервиса if (!GetServiсeKeyName( hServiceControlManager, // дескриптор базы данных сервисов "Demo_Service", // внешнее имя сервиса IpszServiceName, // буфер для имени сервиса &dwBufferSize // количество необходимых байтов )) { cout « "Get service key name failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.getO ; // закрываем дескриптор базы данных сервисов CloseServiceHandle(hServiceControlManager); return 0; } // выводим на консоль внутреннее имя сервиса
644 Часть X. Разработка сервисов в Windows cout « "Service key name: " « IpszServiceName « endl; cout « "Press any key to exit." « endl; cin.get(); // закрьшаем дескриптор базы данных сервисов CloseServiceHandle(hServiceControlManager); return 0; } Назовем внешним именем сервиса то имя, которое используется для ссылок на сервис в окне управления сервисами. Узнать внешнее имя сервиса можно С ПОМОЩЬЮ фуНКЦИИ GetServiceDisplayName, КОТОраЯ Имеет следующий прототип: BOOL GetServiceDisplayName{ SC_HANDLE hSCManager, // дескриптор базы данных сервисов LPCSTR IpServiceName, // указатель на внутреннее имя сервиса LPSTR lpDisplayName, // указатель на внешнее имя сервиса LPDWORD lpdwBuffer // размер памяти для внешнего имени ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. Параметры функции имеют следующее назначение. В параметре hscManager должен быть установлен дескриптор базы данных сервисов, КОТОраЯ была Предварительно открыта функцией OpenSCManager. Параметр IpServiceName должен указывать на строку с внутренним именем сервиса, под которым сервис хранится в базе данных. Параметр lpDisplayName должен указывать на область памяти, куда функция запишет внешнее имя сервиса, т. е. имя, которое используется для ссылок на сервис в окне управления сервисами. Параметр lpdwBuffer должен указывать на переменную типа dword, в котором установлена длина области памяти, на которую указывает параметр lpDisplayName. Функция записывает по этому адресу фактическую длину внешнего имени сервиса, исключая завершающий пустой символ. В листинге 34.7 приведена программа, в которой показано, как определить внутренне имя сервиса по его внешнему имени при помощи функции GetServiceDisplayName.
Глава 34. Работа с сервисами в Windows 645 £ * -' ,.*.~._^ jZ.^ а : £& I.J..-....1.1 :Гг~..^..^1^ #include <windows.h> #include <iostream.h> int main() { char service_name[] = "DemoService"; // имя сервиса SC_HANDLE hServiceControlManager; // дескриптор базы данных сервисов // указатель на буфер для внутренненего имени сервиса char* IpszServiceName; // переменная для длины буфера в случае неудачи функции DWORD dwBufferSize; // связываемся с менеджером сервисов hServiceControlManager = OpenSCManager( NULL, // локальная машина NULL, // активная база данных сервисов SC_MANAGER_CONNECT // соединение с менеджером сервисов ); if (hServiceControlManager == NULL) { cout « "Open service control manager failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to continue." « endl; cin.getO ; return 0; } cout « "Service control manager is opened." « endl « "Press any key to continue." « endl; cin.getO ; // захватываем память под буфер dwBufferSize = 256; IpszServiceName = (char*)new char[dwBufferSize];
646 Часть X. Разработка сервисов в Windows // определение внешнего имени сервиса if (!GetServiceDisplayName( hServiceControlManager, // дескриптор базы данных сервисов "DemoService", // внутреннее имя сервиса IpszServiceName, // буфер для имени сервиса &dwBufferSize // количество необходимых байтов )) { cout « "Get service display name failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.get(); // закрываем дескриптор базы данных сервисов CloseServiceHandle(hServiceControlManager); return 0; } // выводим на консоль внутреннее имя сервиса cout « "Service display name: " « IpszServiceName « endl; cout « "Press any key to exit." « endl; cin.get(); // закрываем дескриптор базы данных сервисов CloseServiceHandle(hServiceControlManager); return 0; 34.8. Управление сервисом Приложение может послать сервису управляющую команду, которая будет передана обработчику управляющих команд сервиса. Для этой цели используется функция controiservice, которая имеет следующий прототип: BOOL Controiservice( SC_HANDLE hService, // дескриптор сервиса
Глава 34. Работа с сервисами в Windows 647 DWORD dwControl, // управляющий код LPSERVICE_STATUS IpServiceStatus // адрес структуры SERVICE_STATUS ); В случае успешного завершения функция controiservice возвращает ненулевое значение, а в случае неудачи — false. Параметры функции имеют следующее назначение. Параметр hservice должен содержать дескриптор сервиса, который был предварительно получен ВЫЗОВОМ функции CreateService ИЛИ OpenService. Параметр dwcontroi должен содержать код управляющей команды, который может принимать одно из следующих значений: □ service_control_stop — остановить сервис; □ service_control_pause — приостановить сервис; □ SERVICE_CONTROL_CONTINUE — ВОЗОбнОВИТЬ СерВИС, □ SERVICE_CONTROL_INTERROGATE — обнОВИТЬ СОСТОЯНИе Сервиса. Дополнительно, в операционной системе Windows 2000 возможны следующие коды управляющих команд: □ service_control_paramchange — параметры запуска изменились; □ service_control_netbindchange — параметры сети изменились. Кроме того, сервису можно послать управляющие команды, коды которых заданы пользователем. Для задания таких команд зарезервирован диапазон от 128 до 255. Сервис может обрабатывать такие команды только в том случае, если при создании сервиса в параметре dwDesiredAccess установлен флаг SERVICE_USER_DEFINED_CONTROL. В листинге 34.8 приведена программа, в которой функция controiservice используется для остановки сервиса. #include <windows.h> #include <iostream.h> int main () { char service_name [ ] = "DemoService"; // имя сервиса SC_HANDLE hServiceControlManager, hService; SERVICE_STATUS service_status; // связываемся с менеджером сервисов
648 Часть X. Разработка сервисов в Windows hServiceControlManager = OpenSCManager( NULL, // локальная машина NULL, // активная база данных сервисов SC_MANAGER_CONNECT // соединение с менеджером сервисов ); if (hServiceControlManager == NULL) { cout « "Open service control manager failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to continue." « endl; cin.getO ; return 0; } cout « "Service control manager is opened." « endl « "Press any key to continue." « endl; cin.get(); // открываем сервис hService = OpenService( hServiceControlManager, // дескриптор менеджера сервисов service_name/ // имя сервиса SERVICE_ALL_ACCESS // любой доступ к сервису ); if (hService == NULL) { cout « "Open service failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.getO ; // закрываем дескриптор менеджера сервисов CloseServiceHandle(hServiceControlManager); return 0; } cout « "Service is opened." « endl
Глава 34. Работа с сервисами в Windows 649 « "Press any key to continue." « endl; cin.get(); // останавливаем сервис if (!ControlService( hService, // дескриптор сервиса SERVICE_CONTROL_STOP, // управляющая команда &service_status // адрес структуры состояния сервиса )) { cout « "Control service failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.get(); // закрываем дескрипторы CloseServiceHandle(hServiceControlManager); CloseServiceHandle(hService); return 0; } cout « "The service is stopped." « endl « "Press any key to exit." « endl; cin.get(); // закрываем дескрипторы CloseServiceHandle(hServiceControlManager); CloseServiceHandle(hService); return 0; } 34.9. Удаление сервисов Для удаления сервиса из базы данных сервисов служит функция DeleteService, которая имеет следующий прототип: BOOL DeleteService( SC_HANDLE hService // дескриптор сервиса
650 Часть X. Разработка сервисов в Windows В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. Единственный параметр hservice этой функции должен содержать дескриптор сервиса, который был предварительно ПОЛучен ВЫЗОВОМ фуНКЦИИ CreateService ИЛИ OpenService. ЕСЛИ сервис работает, то он не удаляется из базы данных до тех пор, пока не будет остановлен. В листинге 34.9 приведена программа, в которой показано, как удалить сервис, ИСПОЛЬЗуя функцию DeleteService. #include <windows.h> #include <iostream.h> int main () { char service_name [ ] = "DemoService"; // имя сервиса SERVICE_STATUS service_status; // состояние сервиса SC_HANDLE hServiceControlManager, hService; // связываемся с менеджером сервисов hServiceControlManager = OpenSCManager( NULL, // локальная машина NULL, // активная база данных сервисов SC_MANAGER_CONNECT // соединение с менеджером сервисов ); if (hServiceControlManager == NULL) { cout « "Open service control manager failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to continue." « endl; cin.getO ; return 0; } cout « "Service control manager is opened." « endl « "Press any key to continue." « endl;
Глава 34. Работа с сервисами в Windows 651 cin.getO ; // открываем сервис hService = OpenService( hServiceControlManager, // дескриптор менеджера сервисов service_name/ // имя сервиса SERVICE_ALL_ACCESS | DELETE // любой доступ к сервису //и удаление из базы данных ); if (hService == NULL) { cout « "Open service failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.getO ; // закрываем дескриптор менеджера сервисов CloseServiceHandle(hServiceControlManager); return 0; } cout « "Service is opened." « endl « "Press any key to continue." « endl; cin.get(); // получаем состояние сервиса if (IQueryServiceStatus(hService, &service_status)) { cout « "Query service status failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.getO ; // закрываем дескрипторы CloseServiceHandle(hServiceControlManager); CloseServiceHandle(hService); return 0;
652 Часть X. Разработка сервисов в Windows // если сервис работает, то останавливаем его if (service_status.dwCurrentState != SERVICE_STOPPED) { cout « "Service is working. It will be stoped" « endl; if ( !ControlService(hService/ SERVTCE_CONTROL_STOP, &service_status)) { cout « "Control service failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.get(); // закрываем дескрипторы CloseServiceHandle(hServiceControlManager); CloseServiceHandle(hService); return 0; } // ждем, пока сервис остановится SleepE00); } // удаляем сервис if (!DeleteService(hService)) { cout « "Delete service failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.get(); // закрываем дескрипторы CloseServiceHandle(hServiceControlManager); CloseServiceHandle(hService); return 0; } cout « "The service is deleted." « endl
Глава 34. Работа с сервисами в Windows 653 « "Press any key to exit." « endl; cin.get(); // закрываем дескрипторы CloseServiceHandle(hServiceControlManager); CloseServiceHandle(hService); return 0; } 34.10. Блокирование базы данных сервисов Для получения монопольного доступа к базе данных сервисов процесс может заблокировать доступ других процессов к этой базе данных, используя функцию LockserviceDatabase, которая имеет следующий прототип: SC_LOCK LockserviceDatabase( SC_HMIDLE hSCManager // дескриптор базы данных сервисов ); В случае успешного завершения функция возвращает дескриптор блокирования базы данных сервисов, а в случае неудачи — null. Единственный параметр hSCManager этой функции должен содержать дескриптор базы данных сервисов, доступ к которой блокируется функцией. Эта база данных сервисов должна быть предварительно открыта для доступа вызовом функции openscManager. Причем база данных сервисов должна быть открыта С КЛЮЧОМ SC_MANAGER_LOCK. Для разблокирования доступа к базе данных сервисов используется функция unockserviceDatabase, которая имеет следующий прототип: BOOL UnlockServiceDatabase( SC_LOCK hSCLock // дескриптор блокирования базы данных сервисов ); В случае успешного завершения функция возвратит ненулевое значение, а в случае неудачи — false. Единственный параметр этой функции должен содержать дескриптор блокирования базы данных сервисов, который был предварительно получен ВЫЗОВОМ функции LockserviceDatabase. Для того чтобы узнать заблокирована база данных сервисов или нет, используется функция QueryServiceLockstatus, которая имеет следующий прототип: BOOL QueryServiceLockstatus( SC_HANDLE hSCManager, // дескриптор базы данных сервисов
654 Часть X. Разработка сервисов в Windows LPQUERY_SERVICE_LOCK_STATUS IpLockstatus, // информация о блокировке DWORD dwBufSize, // размер буфера для информации LPDWORD lpdwBytesNeeded // количество необходимых байтов ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. Параметры этой функции имеют следующее назначение. Параметр hscManager должен содержать дескриптор базы данных сервисов, КОТОРЫЙ был Предварительно ПОЛучеН ВЫЗОВОМ фуНКЦИИ OpenSCManager. Причем база данных сервисов должна быть открыта с ключом SC_MANAGER_QUERY_LOCK_STATUS. Параметр IpLockstatus должен указывать на область памяти, куда система поместит информацию о блокировании базы данных сервисов. Эта информация будет записана в структуру типа query_service_lock_status, которая имеет следующий формат: typedef Struct _QUERY_SERVICE_LOCK_STATUSA { DWORD flsLocked; // состояние блокировки LPSTR lpLockOwner; // владелец блокировки DWORD dwLockDuration; // время блокировки } QUERY_SERVICE_LOCK_STATUS, *LPQUERY_SERVICE_LOCK_STATUS; Поля этой структуры имеют следующее назначение. В поле flsLocked хранится состояние блокировки базы данных сервисов. Если значение этого поля равно нулю, то база данных не заблокирована. В противном случае база данных заблокирована. Если база данных сервисов заблокирована, то в поле lpLockOwner записывается адрес строки, которая содержит имя пользователя, заблокировавшего базу данных. Если база данных сервисов заблокирована, то в поле dwLockDuration хранится целое число без знака, которое определяет продолжительность блокирования базы данных в секундах. В параметре dwBufsize должен быть установлен размер памяти, на которую указывает параметр ipLockstatus. Параметр lpdwBytesNeeded должен указывать на двойное слово, в которое функция в случае своего неудачного завершения поместит необходимый размер буфера для информации о блокировании базы данных сервисов. В листинге 34.10 приведена программа, в которой показано, как блокировать и разблокировать базу данных сервисов, а также определить заблокирована база данных сервисов или нет.
Глава 34. Работа с сервисами в Windows 655 #include <windows.h> #include <iostream.h> int main() { char service_name [ ] = "DemoService"; // имя сервиса SC_HANDLE hServiceControlManager; // дескриптор базы данных // сервисов SC_LOCK hScLock; // дескриптор блокировки базы данных сервисов // указатель на буфер для состояния о блокировке LPQUERY_SERVICE_LOCK_STATUS lpLockStatus; // длина буфера DWORD dwBufferSize; // переменная для длины буфера в случае неудачи функции DWORD dwBytesNeeded; // связываемся с менеджером сервисов hServiceControlManager = OpenSCManager( NULL, // локальная машина NULL, // активная база данных сервисов SC_MANAGER_LOCK | // разрешена блокировка базы данных SC_MANAGER_QUERY_LOCK_STATUS //и определения состояния блокировки ); if (hServiceControlManager == NULL) { cout « "Open service control manager failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to continue." « endl; cin.getO ; return 0; } cout « "Service control manager is opened." « endl « "Press any key to continue." « endl;
656 Часть X. Разработка сервисов в Windows cin.getO ; // блокируем базу данных сервисов hScLock = LockServlceDatabase(hServiceControlManager); if (hScLock == NULL) { cout « "Lock service database failed." « endl « "The last error code: " « GetLastError() « endl « "Press any key to exit." « endl; cin.getO ; // закрываем дескриптор базы данных сервисов CloseServiceHandle(hServiceControlManager); return 0; } cout « "\tThe service database is locked." « endl; // захватываем память под буфер dwBufferSize = 4096; IpLockStatus = (LPQUERY_SERVICE_LOCK_STATUS)new char[dwBufferSize]; // определяем состояние блокировки QueryServiceLockStatus(hServiceControlManager, IpLockStatus, dwBufferSize, ScdwBytesNeeded) ; // выводим на консоль состояние блокировки cout « "Lock state: " « IpLockStatus->fIsLocked « endl « "Lock owner: " « IpLockStatus->lpLockOwner « endl « "Lock duration: " « IpLockStatus->dwLockDuration « endl; // разблокируем базу данных сервисов UnlockServiceDatabase(hScLock); cout « "\tThe service database is unlocked." « endl; // определяем состояние блокировки QueryServiceLockStatus(hServiceControlManager, IpLockStatus, dwBufferSize, ScdwBytesNeeded) ;
Глава 34. Работа с сервисами в Windows 657 // выводим на консоль состояние блокировки cout « "Lock state: " « lpLockStatus->fIsLocked « endl « "Lock owner: " « lpLockStatus->lpLockOwner « endl « "Lock duration: " « lpLockStatus->dwLockDuration « endl; cout « "Press any key to exit." « endl; cin.get(); // закрываем дескрипторы CloseServiceHandle (hServiceControlManager) ; CloseServiceHandle(hScLock); return 0;
Часть XI Управление безопасностью в Windows Глава 35. Система информационной безопасности Глава 36. Управление безопасностью в Windows Глава 37. Управление пользователями Глава 38. Управление группами Глава 39. Работа с идентификаторами безопасности Глава 40. Работа с дескрипторами безопасности Глава 41. Работа со списками управления доступом на высоком уровне Глава 42. Работа с привилегиями Глава 43. Работа с маркерами доступа Глава 44. Работа со списками управления доступом на низком уровне Глава 45. Управление безопасностью объектов на низком уровне
Глава 35 Система информационной безопасности 35.1. Контроль доступа к ресурсам Системы информационной безопасности обеспечивают защиту информационных ресурсов от несанкционированного доступа со стороны пользователей. Здесь под пользователями понимаются процессы, которые выполняются от имени пользователей компьютерной системы. Безопасность ресурсов обеспечивается посредством контроля доступа к этим ресурсам. Контроль доступа к ресурсам это некоторый механизм, который разрешает доступ к ресурсу только авторизованным пользователям этого ресурса, т. е. только тем пользователям, которым разрешен доступ к этому ресурсу. Часто механизм авторизации пользователя включает также и его аутентификацию, т. е. установление подлинности пользователя или, другими словами, определение того, не выдает ли пользователь системы себя за кого-то другого. Более формально в системах информационной безопасности все ресурсы разбиваются на две категории: объекты и субъекты. Объекты представляют собой пассивные ресурсы, которые требуется защитить. К таким ресурсам относятся, например, файлы, каналы передачи данных, оперативная память, принтеры и другие внешние устройства для хранения и отображения информации. Для каждого объекта определяется множество операций, которые можно выполнить над этим объектом. С другой стороны, субъекты представляют собой активные ресурсы, которые выполняют операции над объектами и представляются процессами, исполняемыми от имени пользователей информационной системы. Выполнение субъектами операций над объектами называется доступом субъектов к объектам. Чтобы контролировать доступ субъектов к объектам для каждой пары (субъект, объект) определяется множество операций, которые данный субъект может выполнять над данным объектом. Очевидно, что это множество является подмножеством множества всех операций, заданных над объектом.
662 Часть XI. Управление безопасностью в Windows В системе информационной безопасности существует субъект, который контролирует доступ других субъектов к объектам, руководствуясь при этом некоторыми правилами. Совокупность этих правил называется политикой безопасности, а сам субъект, реализующий эту политику безопасности, называется менеджером или монитором безопасности. Предполагается, что менеджер безопасности всегда находится в активном состоянии, а для каждого пользователя создается, по крайней мере, один субъект, при его входе в систему. Кроме того, субъекты могут создаваться из объектов другими субъектами, работающими от имени пользователей, путем выполнения операции создания субъекта. Например, исполняемый файл, хранящийся на диске, может запускаться на выполнение, превращаясь при этом из объекта в субъект. Следует отличать менеджера безопасности, который является программой, от администратора системы информационной безопасности, который является человеком. Вообще, администратор системы информационной безопасности занимается регистрацией пользователей информационной системы, наделением их определенными правами и привилегиями, а также отслеживанием работы системы безопасности. Здесь уместно сделать различие между правами и привилегиями. Правом называется возможность субъекта выполнять некоторые операции над объектами. Например, пользователь имеет право или, другими словами, ему разрешается читать некоторый файл. Привилегия это более общее понятие, которое позволяет пользователю выполнять действия в отношении других объектов и субъектов системы информационной безопасности. Как правило, привилегия выделяет пользователя из числа обычных пользователей системы. Например, пользователь имеет привилегию блокировать доступ к системе других пользователей. 35.2. Политика безопасности Политикой безопасности называется набор требований, выполнение которых обеспечивает безопасную работу информационной системы. Следует отличать политику безопасности от способа реализации этой политики, который обычно называется механизмом реализации политики безопасности и представляет собой комплекс программных и аппаратных средств. Вообще понятие политики безопасности довольно общее и используется не только при разработке систем информационной безопасности, но и при разработке систем безопасности любых организаций. Обычно разработка политики безопасности для системы разбивается на два этапа. На первом этапе проводится анализ угроз для системы информационной безопасности. При этом подробно изучаются следующие два вопроса: □ определяются информационные ресурсы, которые должны быть защищены; □ определяются возможные угрозы этим информационным ресурсам.
Глава 35. Система информационной безопасности 663 Причем угрозы рассматриваются с двух точек зрения: □ внутренние угрозы, т. е. угрозы от программного обеспечения; □ внешние угрозы, т. е. угрозы от пользователей. Первый этап разработки политики безопасности также называют анализом рисков. После определения защищаемых ресурсов и возможных угроз этим ресурсам переходят ко второму этапу, на котором разрабатываются средства защиты от возможных угроз безопасности информационной системы. При этом средства защиты должны предусматривать работу в трех режимах: □ обработка рутинных задач информационной безопасности системы; □ обработка исключительных ситуаций, как, например, обнаружение атаки вируса; □ обработка аварийных ситуаций, таких как, например, обнаружение вируса в системе. Второй этап разработки политики безопасности также называют управлением рисками. 35.3. Модель безопасности После разработки политики безопасности строят модель безопасности, которая является формальным описанием разработанной политики безопасности, хотя часто понятия политики и модели безопасности не различают. При этом модель безопасности рассматривается как система, которая включает следующие компоненты: □ пассивные ресурсы, которые называются объектами; □ наборы операций, которые можно выполнять над каждым объектом; □ активные ресурсы, которые выполняют операции над объектами; □ атрибуты защиты объектов, которые описывают права доступа субъектов к объектам. Контроль доступа субъектов к объектам выполняет менеджер безопасности, используя для этого атрибуты безопасности объектов и соблюдая при этом некоторые правила, которые называются правилами управления доступом. Формально правила управления доступом могут быть выражены некоторыми предикатами (условиями) над компонентами системы, которые должны выполняться при доступе субъекта к объекту. Состояние объектов, наборов операций, субъектов и атрибутов защиты объектов называется состоянием системы безопасности. Состояние системы безопасности называется безопасным, если в этом состоянии нет несанкционированного доступа субъекта к объекту. При доступе субъекта к объекту
664 Часть XI. Управление безопасностью в Windows система безопасности переходит из одного состояния в другое. Задача системы безопасности состоит в том, чтобы обеспечить переход из безопасного состояния в безопасное же состояние. Однако обеспечение такого перехода не обязательно гарантирует то, что все возможные дальнейшие состояния системы также будут безопасными. Поэтому общая задача безопасности системы может быть сформулирована следующим образом: □ начальное состояние системы безопасное; □ правила управления доступом обеспечивают переход из безопасного состояния системы в безопасное состояние системы; □ любое состояние системы, достижимое из ее начального состояния, является безопасным состоянием. Для решения этой задачи используются методы математической логики. В общем случае доказательство безопасности системы является трудно решаемой задачей. 35.4. Дискреционная политика безопасности Дискреционная политика безопасности основывается на следующих принципах: □ для каждого объекта определяется набор операций, которые можно выполнять над этим объектом; □ субъект может выполнить операцию над объектом при условии, если он имеет право на выполнение этой операции; □ субъект, который имеет права на выполнение некоторых операций над объектом, может передать эти права другому субъекту. Суть дискреционной политики безопасности заключается в том, что права доступа субъекта к объекту определяются другим субъектом, который имеет эти права. Другими словами, доступ субъектов к объектам разрешается другими субъектами или можно сказать, что оставлен на усмотрение другим субъектам, которые обладают этими правами. Менеджер безопасности контролирует доступ субъекта к объекту следующим образом: он проверяет, есть ли у субъекта разрешение на выполнение затребованной операции над объектом. Если такое разрешение есть, то выполнение затребованной операции разрешается, в противном случае выполнение затребованной операции запрещается. В приведенном выше варианте дискреционной политики безопасности существует очевидная проблема. А именно, субъект может передать права на доступ к объекту другому субъекту, который не имеет таких прав, без контроля менеджера безопасности системы. Поэтому такая дискреционная политика безопасности называется либеральной. Чтобы разрешить указанную проблему, последний принцип в дискреционной политике безопасности
Глава 35. Система информационной безопасности 665 обычно изменяют следующим образом: субъект может передать права на выполнение некоторых операций над объектом другому субъекту только при условии, если он наделен такими полномочиями. Полученную политику безопасности называют строгой дискреционной политикой безопасности. Очевидно, что такая политика повышает безопасность системы в целом. 35.5. Дискреционная модель безопасности Для построения дискреционной модели безопасности поступают следующим образом. Идентифицируются все объекты и все субъекты системы. Для каждого объекта определяются операции, которые субъекты могут выполнять над этим объектом. После этого строится матрица, каждая строка которой соответствует одному субъекту и каждый столбец которой соответствует одному объекту системы. Тогда каждая клетка матрицы однозначно идентифицируется субъектом и объектом системы. В клетки полученной матрицы записывают права, которые субъект, соответствующий данной строке матрицы, имеет по отношению к объекту, соответствующему данному столбцу матрицы. Полученная матрица называется матрицей управления доступами. Например, матрица управления доступами может быть такой, как это показано в табл. 35.1, где обозначения для прав субъектов по отношению к объектам приведены в табл. 35.2, 35.3 соответственно. Таблица 35.1. Пример матрицы управления доступами Kimjile Donjile PayrolM Payroll_2 Martinjile Kim R, W Joe Don Jones Martin R, W Manager CP Maria Права доступа субъекта к объекту, как правило, делятся на две категории: □ операции, которые субъект может выполнять над объектом; □ права управления объектом. R R R, W СР R, W R R С R, W R R С R, W
666 Часть XI. Управление безопасностью в Windows Операции, которые разрешается выполнять над объектами, также называются режимами доступа к объекту. Права управления объектом также называются режимами управления объектом. Рассмотрим подробнее возможные операции над объектами. В системах информационной безопасности под объектами обычно понимаются файлы (хотя это могут быть и другие объекты с очень похожими операциями доступа). Поэтому остановимся на объектах подобных файлам, над которыми разрешается выполнять операции, или, другими словами, для которых определены режимы доступа, представленные в табл. 35.2. Как правило, файловые системы операционных систем имеют древовидную структуру, в которой листья дерева соответствуют файлам, а некорневые вершины — каталогам (директориям, папкам). В этом случае возможны три варианта управления доступом к каталогам и файлам: □ запрашивается доступ к файлу, но не к каталогу; □ запрашивается доступ к каталогу, но не к файлу; □ запрашивается доступ к каталогу и файлу. В первом случае субъект указывает полное имя файла, к которому он хочет получить доступ. В этом случае доступ к другим файлам из этого каталога данному субъекту запрещен. Во втором случае субъект указывает только имя каталога, к которому он хочет получить доступ. Он получает доступ ко всем файлам, которые находятся в этом каталоге. В третьем случае субъект для доступа к файлу должен указать как имя файла, к которому он хочет получить доступ, так и имя каталога, в котором находится этот файл. Таблица 35.2. Режимы доступа к объектам Название Краткое Описание режима доступа режима доступа обозначение READ R Разрешается чтения содержимого объекта, как правило, в этом режиме также разрешено и копирование файла WRITE W Разрешается любая модификация объекта WRITE_APPEND WA Разрешается только добавление данных в объект WRITE_CHANGE WC Разрешается только изменять или удалять данные из объекта, не разрешается добавлять данные в объект
Глава 35. Система информационной безопасности 667 Таблица 35.2 (окончание) Название Краткое Описание режима доступа режима доступа обозначение WRITEJJPDATE WU Разрешается только изменять данные из объекта, не разрешается удалять или добавлять данные в объект DELETE D Разрешается удаление объекта EXECUTE E Разрешается исполнение объекта NULL N Нет доступа к объекту Теперь перейдем к режимам управления доступом к объектам. Режимы управления объектом определяют субъектов, которые могут изменять права доступа других субъектов к объекту или передавать права управления этим объектом другим субъектам. Как правило, существует два режима управления, которые представлены в табл. 35.3. Таблица 35.3. Режимы управления объектами Название режима Краткое Описание режима доступа доступа обозначение CONTROL С Разрешается устанавливать режимы доступа к объекту для субъектов, не разрешается передавать режим управления другому субъекту CONTROL WITH CP Разрешается устанавливать режимы доступа к PASSING ABILITY объекту для субъектов и передавать режимы управления объектом другим субъектом Так как субъекты могут передавать права управления объектами другим субъектам, то должны быть определены правила, которым подчиняются субъекты при передаче таких прав. Набор таких правил определяет модель управления в дискреционной модели безопасности. Существуют четыре модели управления, которые могут использоваться в дискреционной модели безопасности: □ иерархическое управление (hierarchical control); □ управление правами доступа владельцем объекта (concept of ownership). □ либеральное управление (laissez-fair); □ централизованное управление (centralized control). Кратко опишем каждую из этих моделей управления.
668 Часть XI. Управление безопасностью в Windows В иерархической модели управления все субъекты, которые могут управлять правами доступа к объектам, упорядочиваются (иерархически), организуя при этом древовидную структуру. В вершине этой структуры находится администратор системы безопасности, который может управлять правами доступа ко всем объектам и наделять такими правами любого субъекта. Для остальных субъектов предполагается, что они могут управлять правами доступа только субъектов, которые находятся ниже их в иерархии. В модели управления, использующей понятие владельца объекта, управление правами доступа к объекту для всех субъектов выполняется именно владельцем этого объекта. Владельцем объекта, как правило, считается тот субъект, который создает этот объект. В такой модели управления также присутствует администратор системы безопасности, который может ограничить права владельца объекта на передачу прав доступа к объекту другим субъектам. Фактически в этом случае модель управления представляет собой двухуровневую, иерархическую модель управления. В либеральной модели управления передача прав доступа к объекту от субъекта, который обладает этими правами, к другому субъекту никак не контролируется. Как уже говорилось, такая модель управления является самой ненадежной. Здесь же можно сказать, что laissez-fair является сокращением французского выражения "Laissez fair, laissez passer", которое может быть переведено, как "Пусть все идет своим чередом". В централизованной модели управления правами управления доступом к объектам обладает только один субъект, как правило, это администратор системы. Таким образом, все вопросы по разрешению доступа субъектов к объектам системы разрешаются только администратором. Централизованная модель управления является наиболее надежной из рассмотренных моделей управления доступами к объектам. В заключение этого раздела отметим, что для каждой из рассмотренных моделей управления существует формальное описание, которое позволяет анализировать надежность дискреционной модели безопасности. 35.6. Реализация дискреционной модели безопасности В дискреционной модели безопасности при обработке запроса субъекта на доступ к объекту менеджер безопасности должен проверить содержимое соответствующей клетки матрицы управления доступами, чтобы выяснить, имеет ли этот субъект право доступа к объекту, которое он запрашивает, или нет. Так как на практике матрица управления доступами довольно разреженная и имеет большой размер, то она редко хранится непосредственно в памяти компьютера. Как правило, для хранения матрицы управления доступами используют один из следующих двух подходов.
Глава 35. Система информационной безопасности 669 При первом подходе матрицу управления доступами рассматривают по строкам. Так как каждая строка этой матрицы соответствует одному субъекту, то можно сказать, что вся строка матрицы доступов описывает все объекты, к которым этот субъект имеет доступ, и режимы доступа к этим объектам. Эта информация называется возможностями (capabilities) субъекта. Для каждого пользователя информационной системы администратором информационной системы задаются его возможности. При этом менеджер системы безопасности работает так, что каждому субъекту разрешается доступ к ресурсам только в соответствии с его возможностями. Поэтому для процесса, работающего от имени некоторого пользователя, доступ к ресурсам также ограничен только его возможностями. Такой подход повышает безопасность системы безопасности, т. к. в случае, если от имени пользователя работает несанкционированная программа (например, вирус), то эта программа будет иметь доступ только к ограниченному множеству объектов. Для реализации такого подхода, как правило, поступают следующим образом. Возможности каждого пользователя хранят в виде списка, каждый элемент которого содержит имена объектов и режимы доступа к этим объектам. Такой список называется профилем (profile) пользователя. Тогда субъект может открыть заданный режим доступа к объекту только в том случае, если этот объект и заданный режим присутствует в его профиле. При работе с профилями возникают следующие проблемы: □ при удалении объекта нужно корректировать профили всех пользователей, которые имели доступ к этому объекту; □ при изменении режимов доступа к объекту также нужно корректировать профили всех пользователей, которые имели доступ к этому объекту. При втором подходе матрицу управления доступами рассматривают по столбцам. Так как каждый столбец матрицы соответствует одному объекту, то можно сказать, что весь столбец матрицы доступов описывает всех субъектов, которые имеют доступ к этому объекту, и режимы доступа каждого субъекта. В этом случае при реализации столбец матрицы управления доступами хранится в виде списка, который называется список управления доступами (access control list). На практике такие списки обычно сокращенно называют ACL, а элементы этих списков — АСЕ (access control elements). Каждый элемент списка управления доступами содержит имя пользователя и разрешенные этому пользователю режимы доступа к объекту. Список управления доступами объекта обычно создается владельцем объекта или администратором информационной системы. При этом менеджер системы безопасности открывает субъекту требуемый доступ к объекту только в том случае, если его имя и требуемый режим доступа находятся в списке управления доступами для этого объекта. Очевидно, что при такой реализации матрицы управления доступами легко решаются вопросы корректировки доступа к объектам и удаления объектов,
670 Часть XI. Управление безопасностью в Windows однако возникают сложности с корректировкой информации о пользователях системы. Так, например, если пользователю запрещается доступ к ресурсам информационной системы, то его имя и режимы доступа нужно удалить из всех списков управления доступами для всех объектов, к которым этот пользователь имел доступ. Для упрощения решения этой задачи всех пользователей информационной системы разбивают на группы, предполагая, что каждая из этих групп может работать только с определенными объектами в заданных режимах доступа. Тогда в списки управления доступами вносят имена групп пользователей с соответствующими режимами доступа. Это значительно сокращает объем списков и упрощает их корректировку. При запросе субъекта на доступ к объекту менеджер системы безопасности проверяет принадлежность этого субъекта к какой-либо группе, а затем сверяется со списком управления доступами, чтобы разрешить или запретить доступ этого субъекта к объекту.
Глава 36 Управление безопасностью в Windows 36.1. Модель безопасности в Windows В операционных системах Windows NT/2000/XP реализована дискреционная модель безопасности. В качестве активных субъектов этой модели безопасности рассматриваются процессы и потоки, каждый из которых работает от имени некоторого пользователя. Когда пользователь регистрируется и входит в систему, то для него создается маркер доступа (access token), который идентифицирует этого пользователя и содержит его привилегии. Каждый процесс, исполняемый от имени пользователя, имеет маркер доступа этого пользователя. Маркер доступа используется для контроля доступа процесса к объектам, которые называются в Windows охраняемыми объектами (securable objects). К охраняемым объектам относятся все объекты Windows, которые могут иметь имя. Кроме того, к охраняемым объектам относятся также потоки и процессы. Каждый охраняемый объект имеет дескриптор безопасности (security descriptor), который создается вместе с охраняемым объектом и содержит информацию, необходимую для защиты объекта от несанкционированного доступа. В дескрипторе безопасности идентифицируется владелец объекта, определяются пользователи и группы пользователей, которым разрешен или запрещен доступ к охраняемому объекту, а также информация для аудита доступа к объекту. Изменять информацию, заданную в дескрипторе безопасности, может только владелец объекта, которым по умолчанию является создатель этого объекта. При доступе к охраняемому объекту система сверяет информацию о пользователе, заданную в маркере доступа, с информацией, заданной в дескрипторе безопасности. Если в дескрипторе безопасности указано, что пользователю разрешен доступ к объекту, то процесс получает запрашиваемый доступ, в противном случае в доступе отказывается. Для хранения информации о пользователях, которым разрешен или запрещен доступ к охраняемым объектам, каждый дескриптор безопасности
672 Часть XI. Управление безопасностью в Windows содержит список управления дискреционным доступом (Discretionary Access- Control List, DACL). Для управления аудитом доступа к объекту в дескрипторе безопасности хранится список управления системным доступом (System Access-Control List, SACL). Общее название для этих списков — списки управления доступом (Access-Control Lists) или сокращенно ACL. Таким образом, можно сказать, что в операционных системах Windows NT/2000/XP реализована дискреционная модель безопасности, в которой управление правами доступа к объекту выполняет владелец этого объекта. В следующих разделах более подробно рассмотрена структура компонент системы управления безопасностью в операционных системах Windows и работа с функциями, которые выполняют настройку этих компонент. В заключение этого раздела скажем, что реализованная в Windows модель безопасности удовлетворяет уровню безопасности С-2, который определен министерством обороны США. Перечислим наиболее важные требования, которым должна удовлетворять система, поддерживающая безопасность на этом уровне: □ владелец ресурса должен управлять доступом к ресурсу; □ операционная система должна защищать ресурсы от случайного повторного использования, например, содержимое памяти завершившегося процесса должно быть недоступно другому процессу; □ каждый пользователь системы должен при входе в систему указывать свое имя и пароль, система должна иметь средства для отслеживания работы пользователя; □ администратор системы должен иметь средства для проведения аудита событий, связанных с безопасностью системы, доступ к полученным при аудите данным должен иметь только авторизованный администратор системы; □ система должна защищать себя от постороннего вмешательства во время своей работы, например, система должна защищать себя от модификации извне во время своей работы. 36.2. Учетные записи Прежде чем пользователь сможет работать в среде операционных систем Windows NT/2000/XP, он должен быть зарегистрирован администратором системы. При регистрации пользователя администратором создается учетная запись пользователя (user account), которая хранится в базе данных менеджера учетных записей (Security Account Manager, SAM). Эта база данных является частью реестра Windows и доступ к ней имеет только администратор системы. Регистрация пользователя выполняется на уровне домена локальной сети, что упрощает управление доступом к ресурсам этой сети. Вообще
Глава 36. Управление безопасностью в Windows 673 доменом называется группа компьютеров в локальной сети, которые разделяют общую базу данных учетных записей пользователей. Более подробно о доменах будет сказано в следующем разделе. В учетной записи хранится следующая информация: □ имя пользователя для входа в систему (username); □ пароль (password); □ полное имя пользователя (full name); □ допустимое время работы в системе (logon hours); □ допустимые компьютеры для входа в систему (logon workstations); □ дата окончания срока действия учетной записи (expiration date); □ рабочий каталог пользователя (home directory); □ действия, выполняемые при загрузке (logon script); □ профиль пользователя, который содержит информацию для настройки рабочего стола пользователя (profile); □ тип учетной записи (account type). Существуют четыре типа учетных записей, а именно: □ учетная запись пользователя; □ учетная запись группы пользователей; □ учетная запись компьютера; □ учетная запись домена. Учетная запись компьютера используется для регистрации компьютеров в домене и создается при подключении компьютера к домену. Также как пользователь должен войти в систему на компьютере, так и компьютер должен при включении войти в домен. Учетная запись домена используется для регистрации домена в других доменах. Учетные записи пользователя, компьютера и домена имеют пароли. Доступ к паролю пользователя имеют администратор и сам пользователь. Паролем домена управляет администратор, а паролем компьютера управляет контроллер доменов (Primary Domain Controller, PDC). После того как для пользователя заведена учетная запись, он может работать на любом компьютере, входящем в домен. При входе пользователя в операционную систему он должен ввести свое имя (user name) и пароль (password). После этого система безопасности операционной системы Windows проверяет базу данных учетных записей на наличие учетной записи данного пользователя. Если пользователь зарегистрирован в системе, то ему разрешается доступ в систему, в противном случае доступ в систему запрещается. 22 Зак. 514
674 Часть XI. Управление безопасностью в Windows По умолчанию операционная система Windows NT создает три учетных записи: □ Administrator — администратор; □ Guest — гость; □ System — система. Учетная запись администратора предназначена для управления локальной системой, установленной на одном компьютере. По умолчанию эта учетная запись создается без пароля. Желательно, чтобы в дальнейшем администратор системы изменил имя пользователя и установил пароль для этой учетной записи. Учетную запись администратора системы нельзя заблокировать. Администратор системы может создавать новые учетные записи. Учетная запись гостя предназначена для обычного пользователя системы, который не имеет административных полномочий. Эта учетная запись также создается без пароля. Она не может быть удалена, но может быть переименована. Учетная запись системы используется самой операционной системой для выполнения различных задач, которые требуют аутентификации. Эта учетная запись также имеет административные привилегии. В операционной системе Windows XP дополнительно создаются учетные записи: □ HelpAssistant — используется удаленным экспертом для регистрации на локальном компьютере, чтобы оказать помощь пользователю этого компьютера; □ SUPPORT_xxxxxxxx — предназначен для сервисной поддержки и обслуживания аппаратного и программного обеспечения его производителями в режиме удаленного доступа. В заключение этого раздела еще раз отметим, что создание, удаление и модификацию учетных записей могут производить только пользователи, которые входят в группу администраторов системы или группу операторов учетных записей. 36.3. Домены Домен это группа компьютеров в локальной сети, которые поддерживают одну политику безопасности и разделяют общую базу данных учетных записей пользователей. Эта политика безопасности и база данных учетных записей пользователей хранятся в базе данных домена (Domain Directory Database). Домен включает серверы и рабочие станции. Один из серверов выбирается контроллером домена. На этом сервере хранится база данных учетных записей домена. Основная функция контроллера домена — выполнять
Глава 36. Управление безопасностью в Windows 675 аутентификацию пользователей при входе в домен на одном из компьютеров домена. Администратор управляет доменом через контроллер домена. В домене можно определить один первичный контроллер домена (Primary Domain Controller, PDC) и несколько вторичных контроллеров (Backup Domain Controller, BDC). На вторичных контроллерах домена хранится копия базы данных учетных записей. Поэтому вторичные контроллеры домена также могут выполнять аутентификацию пользователей. Так как пользователь регистрируется в домене, то будет уместно пояснить организацию доменов на платформе Windows. Понятие домена локальной сети было введено для того, чтобы упростить управление безопасностью объектов в локальной сети. То есть зарегистрированный в домене пользователь может работать на любом компьютере, входящем в домен и отмеченном в учетной записи пользователя, а также имеет доступ к ресурсам, расположенным на компьютерах домена. Конечно, доступ к этим ресурсам может быть выполнен только в том случае, если у пользователя есть соответствующие полномочия. В локальной сети может быть несколько доменов. Для того чтобы пользователь, зарегистрированный в одном домене, имел доступ к ресурсам другого домена, между этими доменами должно быть установлено отношение доверия (trust relationship). Отношение доверия характеризуется двумя свойствами: направлением и транзитивностью. С точки зрения свойства направление отношения доверия может быть односторонним и двусторонним. В одностороннем отношении доверия между двумя доменами один из них разрешает пользователям другого домена доступ к своим ресурсам, но второй домен не разрешает этого первому. В двустороннем отношении доверия оба домена разрешают друг другу доступ к своим ресурсам. Свойство транзитивности определяет возможность передачи прав на доступ к ресурсам одного домена транзитивно через другой домен. Если отношение доверия транзитивно, то права на доступ к ресурсам домена, который передает эти права, могут быть переданы другому домену через домен, который является получателем этих прав. В противном случае такая передача права на доступ к ресурсам другому домену запрещается. Можно сказать, что отношение доверия связывает домены в древовидные структуры. А множество всех доменов локальной сети образует лес. Операционная система Windows NT поддерживает односторонние и двусторонние нетранзитивные отношения доверия. Операционная система Windows 2000 поддерживает следующие типы отношений доверия: □ транзитивное двухстороннее отношение доверия; □ нетранзитивное одностороннее отношение доверия. Двустороннее, нетранзитивное отношение доверия должно моделироваться, используя два односторонних нетранзитивных отношения доверия.
676 Часть XL Управление безопасностью в Windows 36.4. Группы Теперь рассмотрим группы пользователей. Это понятие также было введено для того, чтобы упростить управление безопасностью объектов. Группа (group) это просто набор учетных записей пользователей, которые объединены по какому-либо признаку, например, пользователи одной группы могут работать в одном отделе. При этом отметим, что одна учетная запись пользователя может входить более чем в одну группу. Каждая группа имеет свою учетную запись и наделена своими правами и полномочиями. Эти права и полномочия передаются каждому члену группы. Однако администратор системы может изъять некоторые права и полномочия у некоторых членов группы. Максимальное количество групп, в которые может входить пользователь, равно 1000. На платформе Windows NT различают три типа групп: □ глобальные группы; □ локальные группы; □ специальные группы. Локальные и глобальные группы создаются администратором системы. Специальные группы создаются системой по умолчанию. Рассмотрим каждый из этих типов подробнее. Глобальная группа используется для организации пользователей с целью упорядочения их доступа к ресурсам, находящимся в локальной сети вне домена, в котором создана эта группа. Глобальная группа имеет следующие характеристики: □ может содержать учетные записи только пользователей из домена, в котором она создается; □ может быть создана только на первичном контроллере домена; □ по умолчанию не имеет прав и привилегий. По умолчанию система создает следующие глобальные группы: □ Domain Admins — эта группа включает учетные записи всех администраторов внутри домена; □ Domain Guests — эта группа включает учетные записи всех гостей домена; □ Domain Users — эта группа учетных записей всех пользователей домена. Локальная группа используется для организации доступа пользователей к ограниченному множеству ресурсов внутри домена. Локальная группа имеет следующие характеристики: □ может содержать учетные записи пользователей и глобальных групп из разных доменов; □ может быть создана на любом контроллере домена;
Глава 36. Управление безопасностью в Windows 677 □ обычно получает права и привилегии на доступ к некоторым ресурсам домена. По умолчанию система создает следующие локальные группы: □ Account Operators (операторы учетных записей) — члены этой группы могут создавать новые учетные записи пользователей; □ Administrators (администраторы) — члены этой группы наделяются неограниченными полномочиями администратора системы; □ Backup Operators (операторы резервирования) — членам этой группы разрешается создавать резервные копии файлов системы; □ Guests (гости) — эта группа предназначена для ограничения полномочий разовых пользователей системы, члены этой группы имеют такие же права, как и члены группы Users; □ Print Operators (операторы принтеров) — члены этой группы ответственны за управление принтерами системы: они могут добавлять, удалять и изменять принтеры (на сервере), используемые пользователями сети; □ Power Users (полномочные пользователи) — члены этой группы обладают широкими правами, которые можно сравнить с административными правами, но не имеют власти над администраторами системы; □ Replicator (репликаторы) — членам этой группы разрешается выполнять копирование каталогов, содержащих управляющую информацию, между компьютерами локальной сети; □ Server Operator (операторы сервера) — члены этой группы могут управлять сервером, однако они не имеют полномочий, сравнимых с администраторами системы; □ Users (пользователи) — члены этой группы являются зарегистрированными пользователями системы, они могут создавать другие локальные группы пользователей и управлять ими. В операционной системе Windows XP дополнительно создаются следующие группы: □ Network Configuration Operators (операторы сети) — могут устанавливать компоненты и изменять конфигурацию локальной сети; □ Remote Desktop Users (удаленные пользователи) — обеспечивают доступ к компьютеру через Remote Desktop Connection; □ Help Services Group (группа обслуживающего персонала) — разрешают обслуживающему персоналу подключиться к компьютеру. Специальные группы создаются системой для управления доступом к ресурсам. Членство в этих группах предопределено и не может быть изменено. Существуют следующие специальные группы: □ Creator/Owner (создатели и владельцы) — членами этой группы являются владельцы объектов;
678 Часть XI. Управление безопасностью в Windows П Everyone (любые учетные записи) — членами этой группы являются все учетные записи, зарегистрированные в системе, включая анонимную и пустую учетные записи; □ Interactive (интерактивные пользователи) — членами этой группы являются учетные записи, которые соответствуют пользователям, интерактивно работающим в системе; □ Network (сетевые пользователи) — членами этой группы являются учетные записи пользователей, которые работают с системой через локальную сеть; □ System (системные процессы) — эта специальная группа используется самой системой. 36.5. Идентификаторы безопасности Для каждой учетной записи операционная система создает идентификатор безопасности (Security Identifier, SID), который хранится в базе данных менеджера учетных записей SAM. Идентификатор безопасности является бинарным представлением учетной записи и используется системой безопасности при своей работе для идентификации учетных записей. Фактически идентификатор безопасности идентифицирует пользователя на уровне системы безопасности. Использование идентификатора безопасности ускоряет работу системы безопасности, т. к. в этом случае система при идентификации пользователей работает с числовыми, а не символьными данными. Символически структура идентификатора безопасности может быть описана следующим образом: S - R - I - SAo - SAi - SA2 -SA3 - SA4 ... Здесь каждый символ обозначает группу бит, имеющих определенное значение, а именно: □ s — представляет символ S, который обозначает, что дальнейшее числовое значение является идентификатором безопасности; □ r — представляет версию (Revision Level) формата идентификатора безопасности, начиная с операционной системы Windows NT версии 3.1, формат идентификатора безопасности не изменялся и поэтому значение R всегда равно 1; □ i — представляет 48-битное число, которое обозначает уровень авторизации учетной записи (Top-level Authority или Identifier Authority), которая связана с данным идентификатором безопасности. Это значение также называется идентификатором авторизации учетной записи; П sa — представляет 32-битное число, которое уточняет уровень авторизации учетной записи (Subauthority), связанной с данным идентификато-
Глава 36. Управление безопасностью в Windows 679 ром безопасности. Это число также называется относительным идентификатором учетной записи (Relative Identifier, RID). Здесь под уровнем авторизации понимается уровень, на котором была создана учетная запись, или, другими словами, множество учетных записей, которому принадлежит учетная запись. Предопределенные значения уровней авторизации учетных записей приведены в табл. 36.1. В свою очередь, относительные идентификаторы учетной записи предназначены для конкретизации или, другими словами, однозначной идентификации учетных записей. В общем случае количество битовых полей типа sa в идентификаторе безопасности может быть произвольным. В нашем конкретном случае рассмотрим структуру идентификатора безопасности, который имеет только пять битовых полей типа sa: S - R - I - SAo - SAi - SA2 -SA3 - SA4 Такие идентификаторы безопасности создаются для пользователей и групп. В этом случае битовые поля типа sa имеют следующий смысл: □ поле SAo уточняет авторизацию учетной записи, которая связана с данным идентификатором безопасности; □ поля sai, sa2 и sa3 представляют уникальный 96-битовый идентификатор компьютера, на котором установлена система; □ поле SA4 нумерует идентификаторы безопасности, создаваемые внутри системы. Номера от 0 до 999 зарезервированы для использования системой. А номера, начиная с 1000, присваиваются новым идентификаторам безопасности, при этом значение SA4 увеличивается на 1 при создании каждого нового идентификатора безопасности для пользователя или группы внутри системы. В табл. 36.1—36.4 приведены символьные представления некоторых предопределенных констант и идентификаторов безопасности, используемых в операционных системах Windows. При этом отметим, что идентификаторы безопасности предопределенных групп обычно имеют следующую структуру: S - R - I - SAo В табл. 36.1 представлены предопределенные универсальные идентификаторы безопасности. Здесь слово "универсальный" означает, что эти идентификаторы безопасности имеют смысл в любой системе, которая использует дискреционную модель безопасности. В табл. 36.2 представлены предопределенные универсальные идентификаторы учетной записи (Identifier Authority), которые определены в системах с дискреционной политикой безопасности.
680 Часть XI. Управление безопасностью в Windows Предопределенные универсальные SID Null SID World Local Creator Owner ID Creator Group ID Таблица Символьное значение S-1-0-0 S-1-1-0 S-1-2-0 S-1-3-0 S-1-3-1 36.1. Предопределенные универсальные SID Уровень авторизации учетной записи Группа без членов Группа, которая включает всех пользователей Локальные пользователи системы Идентификатор безопасности владельца объекта Идентификатор безопасности группы, которой принадлежит владелец объекта Таблица 36.2. Предопределенные значения типа учетной записи Идентификаторы учетной записи Значение Префикс SID SECURITY_NULL_SID_AUTHORITY SECURITY_WORLD_SID_AUTHORITY SECURITY_LOCAL_SID_AUTHORITY SECURITY_CREATOR_SID_AUTHORITY SECURITY NT AUTHORITY 0 1 2 3 5 S-1-0 S-1-1 S-1-2 S-1-3 S-1-5 Первые четыре идентификатора учетной записи из табл. 36.2 используются в предопределенных универсальных дескрипторах безопасности. Последний идентификатор используется в дескрипторах безопасности, известных на платформах Windows. Эти дескрипторы безопасности приведены в табл. 36.3. Таблица 36.3 представляет символьные обозначения для относительных идентификаторов (RID), используемых с предопределенным идентификатором безопасности SECURITY_NT_AUTHORITY (S-1-5) для создания SID, которые не являются универсальными, но известны на платформе Windows. Таблица 36.3. Дескрипторы безопасности, известные на платформах Windows Предопределенные RID Известные SID Уровень авторизации учетной записи SECURITY DIALUP RID S-1-5-1 Группа, члены которой вошли в систему с терминалов, используя dial-up модем
Глава 36. Управление безопасностью в Windows 681 Таблица 36.3 (окончание) Предопределенные RID Известные SID Уровень авторизации учетной записи SECURITY NETWORK RID SECURITY BATCH RID SECURITY INTERACTIVE RID SECURITY LOGON IDS RID SECURITY_SERVICE_RID SECURITY_ANONYMOUS_LOGON_RID SECURITY_PROXY_RID SECURITY_ENTERPRISE_ CONTROLLERS_RID SECURITY PRINCIPAL SELF RID SECURITY_AUTHENTICATED_ USER_RID SECURITY_RESTRICTED_CODE_RID SECURITY TERMINAL SERVER RID SECURITY_LOCAL_SYSTEM_RID SECURITY_NT_NON_UNIQUE SECURITY BUILTIN DOMAIN RID S-1-5-2 Группа, члены которой вошли в систему через сеть S-1-5-3 Группа, члены которой могут исполнять пакетные задания S-1-5-4 Группа, членам которой разрешается выполнять интерактивные операции S-1-5-5-X-Y Учетная запись для идентификации сессии пользователя, взаимодействующего с системой (logon session). Значения X и Y отличаются для каждой сессии S-1-5-6 Учетная запись сервиса S-1-5-7 Анонимная или пустая учетная запись S-1-5-8 Прокси S-1-5-9 Контроллер предприятия S-1-5-10 Используется для замещения идентификатора безопасности владельца объекта при его наследовании S-1 -5-11 Учетная запись аутентифи- цированного пользователя S-1-5-12 Ограниченный код S-1-5-13 Учетная запись пользователя, который вошел на терминальный сервер S-1-5-18 Специальная учетная запись, используемая операционной системой S-1-5-21 S-1-5-32 Учетная запись встроенного системного домена
682 Часть XI. Управление безопасностью в Windows В табл. 36.4 приведены предопределенные относительные идентификаторы учетной записи, которые используются в универсальных дескрипторах безопасности SID. Таблица 36.4. Предопределенные относительные идентификаторы учетной записи Относительные идентификаторы Значение Идентификаторы учетной записи учетной записи SECURITY_NULL_RID О S-1-0 SECURITY_WORLD_RID О S-1-1 SECURITY_LOCAL_RID О S-1-2 SECURITY_CREATOR_OWNER_RID О S-1-3 SECURITY_CREATOR_GROUP_RID 1 S-1-3 В заключение этого раздела скажем, что текстовые представления установленных идентификаторов безопасности можно просмотреть в ключе реестра HKLM\SOFTWARE\Microsoft\WindowsNT\CurrentVersion\ProfileList. Щелкнув кнопкой мыши на нужном идентификаторе безопасности, можно увидеть учетную запись, связанную с этим идентификатором безопасности. 36.6. Дескрипторы безопасности Как уже говорилось в начале главы, каждый охраняемый объект имеет дескриптор безопасности (security descriptor) или сокращенно SD, который создается вместе с охраняемым объектом и содержит информацию, необходимую для защиты объекта от несанкционированного доступа. Дескриптор безопасности включает заголовок (header), который содержит управляющие флаги и указатели на следующие компоненты: □ SID владельца объекта; □ SID первичной группы владельца объекта; □ указатель на список управления дискреционным доступом — DACL; □ указатель на список управления системным доступом — SACL. Идентификаторы безопасности владельца объекта и первичной группы, идентифицируют пользователя, который создает объект. Структура идентификаторов безопасности была рассмотрена в предыдущем разделе. Здесь же только поясним, что такое первичная группа. Каждый пользователь, т. е. его учетная запись, может принадлежать нескольким группам, но одна из них должна быть выбрана в качестве первичной группы пользователя (primary group). Это сделано для того, чтобы ресурсы, создаваемые пользователем, были доступны другим членам группы,
Глава 36. Управление безопасностью в Windows 683 к которой принадлежит этот пользователь. Такая политика доступа к ресурсам определяется в приложениях, совместимых со стандартом POSIX, или работающих на базе компьютеров Macintosh, которые подключены к локальной сети, работающей под управлением сервера Windows. Поэтому дескриптор безопасности субъекта и содержит идентификатор безопасности первичной группы. В операционных системах Windows в качестве первичной группы может быть выбрана любая глобальная группа домена или просто пользователь этого домена. По умолчанию для каждого пользователя система в качестве первичной группы определяет глобальную группу Domain Users. Поэтому чтобы удалить пользователя из этой группы, нужно сначала включить его в другую глобальную группу, а затем сделать эту группу его первичной группой. Отметим, что в операционных системах Windows для работы с дескрипторами безопасности охраняемых объектов ИСПОЛЬЗУЮТСЯ функции SetNamedSecuritylnfо, SetSecuritylnfo, GetNamedSecuritylnfo И GetSecuritylnfo. Небольшое ограничение существует только для объектов ядра, а именно, функции GetNamedSecuritylnfo И SetNamedSecuritylnf о работают ТОЛЬКО СО следующими объектами ядра: семафорами, событиями, мьютексами, ожидающими таймерами и отображениями файлов в память. В заключение этого раздела скажем, что списки управления дискреционным и системным доступом будут более подробно рассмотрены в следующем разделе. 36.7. Списки управления доступом ACL Список управления доступом (ACL) содержит элементы, которые называются входами управления доступом (Access-Control Entries, АСЕ). Каждый элемент АСЕ содержит следующую информацию: □ идентификатор безопасности субъекта, которому разрешен или запрещен доступ к охраняемому объекту; □ маску доступа, которая специфицирует права доступа субъекта к охраняемому объекту; □ флаг, который определяет тип элемента АСЕ; □ флаги, которые определяют свойства наследования данного элемента АСЕ; □ флаги, которые управляют аудитом доступа к охраняемому объекту. Идентификаторы безопасности определяют учетную запись пользователя, от имени которого выступает субъект. Структура идентификаторов безопасности была рассмотрена в предыдущем разделе. Теперь рассмотрим подробно остальные компоненты элемента АСЕ из списка управления дискреционным доступом.
684 Часть XI. Управление безопасностью в Windows Маска доступа представляет собой 32-битовое поле, в котором каждый бит специфицирует определенное право доступа субъекта к охраняемому объекту, за исключением 24—27 битов, которые зарезервированы для дальнейшего использования. Каждое право доступа представляет собой набор операций, которые субъекту разрешается или запрещается выполнять над охраняемым объектом. В Windows права доступа также называются разрешениями (permissions). В операционных системах Windows NT определены следующие четыре категории прав доступа: □ Specific Access Rigths — специфические права доступа; □ Standard Access Rights — стандартные права доступа; □ Generic Access Rights — родовые права доступа; □ SACL Access Right — право доступа к списку управления системным доступом (SACL); Рассмотрим каждую из этих категорий прав доступа к охраняемому объекту более подробно. Специфические права доступа определяют операции, которые могут выполняться только над объектом заданного типа. Поэтому для каждого объекта определяется свой набор специфических прав доступа. Специфические права доступа устанавливаются в 0—15 битах маски доступа. Стандартные права доступа содержат такие операции, которые можно выполнять над большинством охраняемых объектов. Права этого типа включают следующие права: □ read_control — право читать информацию из дескриптора безопасности объекта, за исключением информации из списка SACL; □ write_dac — право модифицировать список DACL в дескрипторе безопасности охраняемого объекта; □ write_owner — право изменить владельца объекта в дескрипторе безопасности охраняемого объекта; □ delete — право удалять охраняемый объект; □ synchronize — право использовать объект для синхронизации. Стандартные права доступа устанавливаются в 16—22 битах маски доступа. Родовые права включают следующие четыре права: □ generic_read — право читать содержимое объекта; □ generic_read — право записи в объект; □ generic_execute — право исполнения объекта; □ generic_all — объединяет три предыдущих права.
Глава 36. Управление безопасностью в Windows 685 Каждое из этих прав отображается в подходящие стандартные и специфические права доступа для каждого конкретного охраняемого объекта. Родовые права доступа устанавливаются в 28—31 битах маски доступа. Для списка управления системным доступом (SACL) определено единственное право: □ access_system_security — право доступа к списку SACL дескриптора безопасности. Оно дает субъекту возможность просматривать и изменять содержимое списка SACL. Этот флаг устанавливается в 23-ем бите маски доступа. В операционной системе Windows 2000 также определены права для доступа к объектам активного каталога (Active Directory), которые здесь рассматриваться не будут. Теперь перейдем к флагу, который определяет тип элемента списка управления доступом. Элементы списка DACL могут быть двух типов: □ access_allowed_ace — элемент, разрешающий доступ субъекта к охраняемому объекту; □ access_denied_ace — элемент, запрещающий доступ субъекта к охраняемому объекту. Причем субъект, доступ которого к охраняемому объекту контролирует элемент списка DACL, задается идентификатором безопасности, который хранится в этом же элементе списка АСЕ. А операции, которые субъекту разрешается или запрещается выполнять над объектом, определяются флагами, отмечающими права доступа. Список SACL может содержать только элементы типа: □ system_audit_ace — элемент списка SACL. Каждый из этих элементов указывает системе, что нужно создать аудиторскую запись при попытке выполнения над объектом операции, разрешенной специфицированными в этом же элементе списка правами доступа к охраняемому объекту. Последняя компонента элемента списка управления доступом содержит флаги, которые задают свойства наследования данного элемента АСЕ. То есть эти флаги определяют, наследуется ли элемент АСЕ списка управления доступом для вновь создаваемого охраняемого объекта, который является дочерним по отношению к текущему охраняемому объекту. Например, пусть в качестве охраняемого объекта выступает каталог, и элемент АСЕ списка управления доступов в дескрипторе безопасности этого объекта является наследуемым. Тогда копия этого элемента будет помещена в соответствующий список управления доступом вновь создаваемого подкаталога в рассматриваемом каталоге. Если же элемент АСЕ ненаследуемый, то этого
686 Часть XI. Управление безопасностью в Windows не произойдет. Для управления наследованием элементов списка управления доступом используются следующие флаги: □ object_inherit_ace — элемент наследуется неконтейнерным дочерним объектом. Если не установлен флаг no_propagate_inherit_ace, to элемент также наследуется и контейнерным дочерним объектом, но при этом в дочернем объекте устанавливается флаг inherit_only_ace; □ container_inherit_ace — элемент наследуется только контейнерным дочерним объектом; □ no_propagate_inherit_ace — элемент был унаследован от родительского объекта, но флаги object_inherit_ace и container_inherit_ace в элементе сбрасываются, что отменяет дальнейшее наследование этого элемента; □ inherit_only_ace — отмечает, что элемент был унаследован от родительского объекта; этот элемент не участвует в контроле доступа к объекту; □ inherited_ace — отмечает, что элемент был унаследован от родительского объекта. При этом сделаем следующее замечание — операционная система Windows 2000 автоматически выполняет распространение наследуемых элементов списка управления доступом в списки управления доступом дочерних объектов, если эти элементы включаются в список управления доступом при ПОМОЩИ функций SetNamedSecuritylnfo И SetSecuritylnfo. To есть если элемент с установленным флагом наследования container_inherit_ace включается в список управления доступом к каталогу, то этот элемент будет включен также в списки управления доступом всех подкаталогов этого каталога. Теперь приведем правила, следуя которым выполняется автоматическое наследование элементов списка управления DACL: □ если дочерний объект имеет список DACL, то в этот список включаются наследуемые элементы списка DACL родительского объекта; □ если дочерний объект не имеет списка DACL, то этот список создается и в него включаются наследуемые элементы списка DACL родительского объекта; □ если наследуемый элемент списка DACL удаляется из родительского объекта, то он удаляется также из списков DACL дочерних объектов; □ если список DACL дочернего объекта содержит только наследуемые элементы и все эти элементы удаляются из списка, то сам список не удаляется, а становится пустым. Особенно стоит обратить внимание на последнее правило, т. к. при доступе субъекта к объекту система различает случаи отсутствия списка DACL и наличие пустого списка DACL, т. е. списка DACL, который не содержит элементов. В первом случае доступ к объекту открыт для всех субъектов, а во втором — доступ к объекту закрыт для всех субъектов. Более подроб-
Глава 36. Управление безопасностью в Windows 687 но о контроле доступа субъектов к охраняемому объекту будет сказано в разд. 36.9. В заключение этого раздела рассмотрим флаги, которые управляют аудитом доступа к охраняемому объекту. Определены два флага, управляющие аудитом, а именно: □ successful_access_ace_flag — отмечает, что нужно генерировать аудиторское сообщение при успешном открытии доступа к объекту; □ failed_access_ace_flag — отмечает, что нужно генерировать аудиторское сообщение при неудачной попытке открытия доступа к объекту. Отметим, что эти флаги используются только в элементах, принадлежащих спискам управления доступом SACL, и могут быть установлены как по отдельности, так и совместно. 36.8. Маркеры доступа Каждый субъект, которым в Windows может быть поток или процесс, имеет маркер доступа, который идентифицирует субъекта при попытке его доступа к объекту. Поэтому также говорят, что маркер доступа описывает контекст безопасности субъекта, т. е. ограничивает доступ субъекта к объектам. В маркере доступа хранится информация, идентифицирующая пользователя, от имени которого исполняется поток или процесс, и привилегии, которыми обладает субъект. Вообще привилегией называется право пользователя выполнить некоторое действие по отношению к некоторым объектам или субъектам системы. Главное отличие привилегий от прав доступа заключается в том, что, во-первых, привилегии касаются субъектов, а не охраняемых объектов системы; а во-вторых, привилегии назначаются субъектам администратором системы, а правами доступа к объекту управляет владелец этого объекта. В табл. 36.5 перечислены привилегии и встроенные учетные записи, которым эти привилегии назначаются по умолчанию в операционных системах Windows. Таблица 36.5. Привилегии, назначаемые учетным записям по умолчанию Привилегия Строковая константа, Учетные записи, Описание привилегии именующая привиле- которым при- гию вилегия назначается по умолчанию Replace a SeAssignPrimaryToken Empty Разрешает пользовате- process-level Privilege лю заменять маркер token доступа процесса
688 Часть XI. Управление безопасностью в Windows Таблица 36.5 (продолжение) Привилегия Generate Security Audits Back up Files and Directories Log on as a Batch Job Bypass Traverse Checking Строковая константа, именующая привилегию SeAuditPrivilege SeBackupPrivilege SeBatchLogonRight SeChangeNot i fyPrivilege Учетные записи, которым привилегия назначается по умолчанию Empty Administrators Backup Operators Server Operators Empty Administrators Power Users Backup Operators Users Everyone Описание привилегии Разрешает пользователю создавать элементы в списке SACL для аудита доступа к объекту Разрешает пользователю создавать резервные копии файлов и каталогов Разрешает пользователю запустить пакетное задание из очереди заданий Разрешает пользователю получать извещения об изменениях в каталогах или файлах. Разрешает пользователю продвигаться по каталогам, чтобы достичь каталога или файла, к которому он имеет доступ. Содержимое каталогов во время продвижения просматривать нельзя Create a Pagefile Create Permanent Shared Objects Create a Token Object SeCreatePagefile Privilege SeCreatePermanent Privilege SeCreateTokeriftrivilege Administrators Empty Local System Разрешает пользователю создавать и изменять размеры файла подкачки страниц Разрешает пользователю создать постоянный объект, который может совместно использоваться другими пользователями Разрешает процессу, работающему от имени учетной записи пользователя, создавать маркер доступа, который разрешает доступ к любым ресурсам на локальном компьютере
Глава 36. Управление безопасностью в Windows 689 Привилегия Debug Programs Increase Scheduling Priority Increase Quotas Строковая константа, именующая привилегию SeDebugPrivilege SelncreaseBasePriority Privilege SelncreaseQuotaPrivilege Таблица 36.5 (продолжение) Учетные записи, которым привилегия назначается по умолчанию Administrators Administrators Administrators Описание привилегии Разрешает пользователю присоединять отладчик к любому процессу Разрешает процессу увеличить приоритет другого процесса Разрешает процессу увеличивать квоту про- Log on Locally Load and Unload Device Drivers Lock Pages in Memory Add Workstations to the Domain Access this Computer from the Network SelnteractiveLogonRight Account Operators Administrators Backup Operators Print Operators Server Operators Everyone Guests Power Users Users SeLoadDriverPrivilege Ad m inistrators SeLockMemoryPrivilege Empty цессорного времени, выделяемого другому процессу Разрешает пользователю интерактивный вход в систему SeMachineAccount Privilege SeNetworkLogonRight Empty Administrators Power Users Backup Operators Users Everyone Разрешает пользователю устанавливать и удалять драйверы устройств Разрешает процессу запирать виртуальные страницы в реальной памяти. Эта привилегия считается устаревшей Разрешает пользователю добавлять компьютер к домену Разрешает доступ к компьютеру из локальной сети
690 Часть XI. Управление безопасностью в Windows Таблица 36.5 (продолжение) Привилегия Строковая константа, именующая привилегию Учетные записи, которым привилегия назначается по умолчанию Описание привилегии Profile Single Process Force Shutdown from a Remote System Restore files and directories Manage Auditing and Security Log Log on as a Service Shut down the system SeProfSingleProcess Administrators Modify Firmware Environment Variables Profile System Performance SeRemoteShutdown Privilege SeRestorePrivilege SeSecurityPrivilege SeShutdownPrivilege SeSys temEnvironment Privilege SeSystemProfile Privilege Administrators Server Operators Power Users Administrators Backup Operators Server Operators Administrators SeServiceLogonRight Empty Account Operators Administrators Backup Operators Print Operators Server Operators Everyone Guests Poser Users Users Administrators Administrators Разрешает пользователю настраивать производительность процесса Разрешает пользователю выключать компьютер с другого компьютера в сети Разрешает пользователю восстанавливать содержимое сохраненных файлов и каталогов Разрешает пользователю проводить аудит доступа к объектам Разрешает сервису работать от имени пользователя Разрешает пользователю выключать систему Разрешает пользователю изменять значения переменных системного окружения Разрешает настраивать производительность системы
Глава 36. Управление безопасностью в Windows 691 Таблица 36.5 (окончание) Привилегия Строковая константа, именующая привилегию Учетные записи, которым привилегия назначается по умолчанию Описание привилегии Change the System Time Take ownership of files or other objects Act as Part of the Operating System SeSystemTimePrivilege SeTakeOwnership Privilege SeTcbPrivilege Administrators Power Users Server Operators Administrators Local System Разрешает пользователю устанавливать системное время и дату Разрешает пользователю становиться владельцем объекта без разрешения текущего владельца этого объекта Разрешает процессу работать от имени любого пользователя и иметь доступ к его ресурсам Теперь рассмотрим структуру маркера доступа. В маркере доступа хранится следующая информация: □ SID учетной записи пользователя; □ список SID учетных записей групп, которым принадлежит пользователь; □ идентификатор безопасности текущей сессии (logon session); □ список привилегий, которыми обладает пользователь и группы, в которые он входит, на локальном компьютере; □ идентификатор безопасности пользователя или группы, который используется по умолчанию для задания владельца вновь создаваемого или существующего охраняемого объекта; □ идентификатор безопасности первичной группы пользователя; □ список DACL, которую система использует по умолчанию при создании охраняемого объекта; □ идентификатор процесса, который вызвал создание маркера доступа; □ значение, определяющее тип маркера доступа: первичный маркер или маркер, замещающий первичный маркер маркером другого пользователя (impersonation token); □ список SID, которые ограничивают доступ потока к охраняемым объектам, так называемые ограничивающие SID (restricting SID);
692 Часть XI. Управление безопасностью в Windows □ значение, которое отмечает уровни замещения сервером маркера доступа клиента (impersonation levels); □ статистическая информация о маркере доступа. Поясним два понятия, которые встречаются в описании маркера доступа, а именно, работа одного потока в контексте безопасности другого потока и ограничение контекста безопасности процесса. О первичной группе пользователя было сказано в параграфе, посвященном дескрипторам безопасности. Прежде всего скажем о персонификации маркеров доступа. Каждый поток имеет свой маркер доступа, который, как уже было сказано, определяет контекст безопасности этого потока или, другими словами, охраняемые объекты, к которым поток имеет доступ. Теперь представим себе, что поток- клиент посылает по сети потоку-серверу запрос на выполнение некоторой операции. Для того чтобы сервер не вышел за пределы контекста безопасности потока-клиента, он должен работать от имени клиента, а не от своего имени. Такая замена контекста безопасности потока называется подменой контекста безопасности. Подмена контекста безопасности потока может выполняться только при помощи подмены маркера доступа потока-сервера на маркер доступа потока-клиента. Поэтому маркер доступа и содержит информацию о том, является ли он первичным или замещается маркером другого пользователя. Подмену маркера доступа потока-сервера на маркер доступа потока-клиента будем также назвать замещением маркера доступа (impersonation) Исходя из этих рассуждений, можно также сказать, что замещение маркера доступа это способность одного процесса использовать атрибуты защиты другого процесса. Поток-сервер может обрабатывать запросы потока-клиента на четырех уровнях безопасности, каждый их которых указывает серверу, до какой степени он может исполнять роль клиента. Далее перечислены эти уровни: □ Security Anonymous level — анонимный уровень, т. е. сервер не может идентифицировать клиента, который остается анонимным; □ Security Identification level — идентифицирующий уровень, на этом уровне клиент может идентифицировать клиента, т. е. может получить любую информацию из идентификатора безопасности (SID) клиента, но не может выполнять от имени клиента никаких действий; □ Security Impersonation level — уровень подмены контекста безопасности, в этом случае поток-сервер может работать от имени потока-клиента. Если поток-сервер работает на удаленном компьютере, то он имеет доступ только к локальным ресурсам на этом компьютере. В противном случае поток-сервер имеет доступ как к локальным, так и к сетевым ресурсам, входящим в контекст безопасности потока-клиента; □ Security Delegation level — уровень делегирования, на этом уровне поток- клиент полностью делегирует свои полномочия потоку-серверу, который может распоряжаться ими как на локальном, так и на удаленном компь-
Глава 36. Управление безопасностью в Windows 693 ютере. Этот уровень передачи полномочий реализован только в операционных системах Windows 2000/XP. Теперь рассмотрим ограничивающие SID (restricting SID), само название которых говорит о том, что они используются для ограничения доступа субъекта к объектам. Это ограничение выполняется следующим образом. Система управления безопасностью при контроле доступа субъекта к охраняемому объекту проверяет не только идентификатор безопасности этого субъекта, но и его ограничивающие идентификаторы безопасности. В заключение этого раздела скажем, что маркеры доступа сами являются охраняемыми объектами и поэтому для них также нужно задавать атрибуты безопасности. Доступ к дескриптору безопасности маркера доступа осуществляется так же, как и доступ к объекту ядра при помощи функций SetKernelObjectSecurity И GetKernelObjectSecurity. 36.9. Создание новых объектов При создании нового объекта поток, создающий этот объект, может предоставить атрибуты безопасности этого объекта, а может и не предоставить. Атрибуты безопасности объекта задаются структурой security_attributes, которая имеет следующий формат: typedef struct _SECURITY_ATTRIBUTES { DWORD nLength; // длина структуры в байтах LPVOID IpSecurityDescriptor; // указатель на дескриптор безопасности BOOL blnheritHandle; // признак наследования } SECURITY_ATTRIBUTES; *PSECURITY_ATTRIBUTES/ *LPSECURITY_ATTRIBUTES; Адрес этой структуры передается как параметр функции создания нового охраняемого объекта. Как видно из определения, структура security_attributes содержит только адрес дескриптора безопасности объекта и флаг наследования этого объекта дочерними процессами. О наследовании объектов было рассказано в разд. 4.4. Теперь же поговорим о том, как система устанавливает дескриптор безопасности объекта. Если структура типа security_attributes содержит указатель на дескриптор безопасности, то этот дескриптор безопасности и устанавливается для нового объекта. Если же дескриптор безопасности не задан, то функция создания объекта все равно строит дескриптор безопасности этого объекта, но его содержимое определяется системой. Для этого и предназначены идентификаторы безопасности владельца объекта, первичной группы владельца объекта и список DACL из маркера доступа, которые используются по умолчанию при создании новых объектов. Кратко рассмотрим, как назначаются владелец объекта и список доступа DACL объекта, если они не заданы явно в дескрипторе доступа при создании объекта.
694 Часть XI. Управление безопасностью в Windows Если владелец объекта не задан, то владельцем объекта становится пользователь, заданный как владелец объекта по умолчанию в маркере доступа, при условии, что учетная запись этого пользователя связана с маркером доступа И ДЛЯ ЭТОГО Пользователя установлена привилегия SeTakeOwnershipPrivilege. В противном случае владельцем объекта становится учетная запись, от имени которой работает поток, создающий объект. Если объект имеет имя, то, начиная с операционной системы Windows 2000, список управления DACL этого объекта всегда содержит наследуемые элементы из списка управления доступом родительского контейнерного объекта. Если наследуемых элементов нет или объект не имеет имени, то в дескриптор безопасности объекта включается список DACL по умолчанию, который задан в маркере доступа потока, создающего объект. Если такого списка нет, то дескриптор безопасности объекта также не содержит список DACL, что открывает всем субъектам неограниченный доступ к этому объекту. 36.10. Контроль доступа к охраняемому объекту Опишем общую схему контроля доступа субъектов к объектам. Для этого напомним, что в операционных системах Windows в качестве субъекта выступает процесс или поток, который исполняется от имени некоторого пользователя. С каждым процессом ассоциируется маркер доступа, который идентифицирует учетную запись пользователя, от имени которого выполняется этот процесс. Маркер доступа связывается с потоком во время его запуска при входе пользователя в систему. Но фактически доступ к объектам осуществляет поток, который выполняется в контексте этого процесса. Каждый поток также имеет свой маркер доступа, который может быть первичным, т. е. совпадать с маркером доступа процесса, в контексте которого выполняется этот поток, или заимствованным у другого процесса, в случае если произошла подмена контекста безопасности (impersonation). Доступ к охраняемому объекту выполняется из потока. Поэтому под субъектом часто и понимается исполняемый поток, который при контроле доступа представляется маркером доступа этого потока. В свою очередь охраняемый объект, доступ к которому контролируется системой управления безопасностью, представляется при контроле доступа дескриптором безопасности этого объекта. Теперь можно перейти к общей схеме контроля доступа субъектов к объектам. Контроль доступа субъекта к охраняемому объекту выполняется следующим образом. При открытии субъектом доступа к охраняемому объекту, что обычно выполняется посредством функций типа create или open, система управления безопасностью просматривает список DACL этого охраняемого объекта для поиска элемента, в котором хранится идентификатор безопасности субъекта. Если такой элемент в списке DACL не найден.
Глава 36. Управление безопасностью в Windows 695 то поток получает отказ в доступе к объекту. Если же такой элемент найден, то система проверяет тип этого элемента. Если найденный элемент имеет тип access_allowed_ace, то система безопасности проверяет, установлены ли в этом элементе флаги прав доступа субъекта, которые соответствуют запрашиваемому потоком доступу. Если такие флаги установлены, то поток получает доступ к охраняемому объекту, в противном случае поток получает отказ в доступе к охраняемому объекту. Если же найденный элемент имеет тип access_denied_ace, то система безопасности проверяет, установлены ли флаги прав доступа субъекта, которые соответствуют запрашиваемому потоком доступу. Если хотя бы один из таких флагов установлен, то поток получает отказ в доступе к охраняемому объекту, в противном случае система продолжает просмотр списка управления доступом DACL. Как видно из изложенного, доступ потока к охраняемому объекту зависит от порядка расположения элементов в списке управления доступом DACL этого объекта. Система управления безопасностью включает элементы типа access_denied_ace в начало списка, если это включение выполняется при помощи функции setEntriesinAcl. Однако низкоуровневые функции для работы СО СПИСКамИ Доступа, такие как AddAccessAllowedAce, AddAccessDeniedAce и AddAce, не обеспечивают такую последовательность элементов в списках. При работе с такими функциями программа сама должна контролировать порядок элементов в списках управления доступом. Кроме того, доступ к охраняемому объекту также зависит от того, в каком порядке в этот список DACL включаются наследуемые элементы из родительских объектов. В операционной системе Windows 2000 принят следующий порядок включения элементов в список DACL при автоматическом наследовании элементов. Наследуемые элементы типа access_allowed_ace включаются в самый конец списка DACL, после ненаследованных элементов такого же типа. Наследуемые элементы типа access_denied_ace включаются после всех ненаследуемых элементов такого типа, но перед всеми элементами типа access_allowed_ace. Заметим, что при определении права доступа потока к охраняемому объекту следует различать две ситуации: □ отсутствие списка DACL у объекта (Null DACL) — в этом случае доступ к объекту не ограничен и разрешен для любого потока; □ пустой список DACL у объекта (Empty DACL) — в этом случае доступ к объекту запрещен для всех потоков. Такая интерпретация ситуации, в которой отсутствует список DACL, позволяет избежать затрат, связанных с работой системы безопасности, если пользователь не использует возможности, предоставляемые операционной системой для обеспечения безопасности объекта от несанкционированного доступа.
696 Часть XI. Управление безопасностью в Windows 36.11. Аудит доступа к охраняемому объекту Теперь перейдем к аудиту доступа субъектов к охраняемым объектам. Аудит доступа субъекта к охраняемому объекту выполняется следующим образом. При открытии субъектом доступа к охраняемому объекту, что обычно выполняется посредством функций типа create, система управления безопасностью просматривает список управления доступом SACL этого объекта. Если в списке SACL найден элемент, который содержит идентификатор безопасности, совпадающий с идентификатором безопасности субъекта, и в этом элементе установлены флаги successful_access_ace_flag или failed_access_ace_flag (или оба флага вместе), то система проверяет флаги управления доступом, установленные в этом элементе. Если запрашиваемый субъектом доступ соответствует правам пользователя, т. е. субъект получает доступ к охраняемому объекту, и в элементе списка SACL для затребованных субъектом прав доступа установлен флаг successful_access_ace_flag, то система генерирует аудиторское сообщение об успешном доступе субъекта к объекту. Или если субъекту отказано в доступе к охраняемому объекту и в элементе списка SACL для затребованных субъектом прав доступа установлен флаг failed_access_ace_flag, то система генерирует аудиторское сообщение о неудачном доступе субъекта к объекту. В других случаях аудиторские сообщения не генерируются. При доступе субъекта к охраняемым объектам возможны следующие аудиторские сообщения: □ Object Open — объект открыт; □ Object Open for Delete — объект открыт для удаления; □ Object Deleted — объект удален. Каждое сообщение также содержит информацию о субъекте, который выполнил действие, отмеченное этим сообщением. Аудиторские сообщения записываются в журнал сообщений (Event log). Доступ к этому журналу имеют администраторы системы посредством просмотра соответствующих ключей реестра. 36.12. Структура системы безопасности В заключительном разделе этой главы кратко рассмотрим структуру системы безопасности операционных систем Windows. На рис. 36.1 приведена общая схема взаимодействия основных компонент системы безопасности операционных систем Windows. Дадим краткое описание и функциональное назначение каждой из компонент системы безопасности.
Глава 36. Управление безопасностью в Windows 697 Winlogon LSA Policy Database ~^f ^7 Local Security Authority z~\ <^ Security Reference Monitor ^x Iz Security Account Manager ^V Рис. 36.1. Схема взаимодействия компонент системы безопасности Сначала перечислим ключи реестра, в которых хранятся базы данных системы безопасности: □ Local Security Authority Policy Database (HKEY_LOCAL_MACHINE\ SECURITY) — защищенная база данных, в которой хранятся данные, определяющие политику безопасности на локальной машине; □ Security Accounts Database (HKEY_LOCAL_MACHINE\SAM) — защищенная база данных, в которой хранятся учетные записи пользователей и групп, а также связанная с ними информация, такая как, например, пароли и идентификаторы безопасности. Теперь перечислим основные программные компоненты системы безопасности и кратко опишем их функциональность: □ Security Reference Monitor (компонент NTOSKRNL.EXE) — контролирует доступ субъектов к охраняемым объектам, поддерживает аудит, является компонентом ядра операционной системы и работает в защищенном режиме (режиме ядра);
698 Часть XI. Управление безопасностью в Windows П Local Security Authority (компонент LSASS.EXE) — определяет, может ли пользователь войти в систему, и отвечает за исполнение политики безопасности на локальном компьютере; □ Security Accounts Manager (компонент LSASS.EXE) — является компонентом Local Security Authority и поддерживает базу данных учетных записей. Кроме того, укажем вспомогательные процессы, которые участвуют в работе системы безопасности: □ Winlogon (WINLOGON.EXE) — запускается во время загрузки системы и управляет процессом входа пользователей в систему; □ Authentication Package (MSV1_0.DLL) — используется для верификации полномочий пользователя при доступе субъектов к объектам; □ Netlogon (SERVICES.EXE) — контролирует вход в сеть, обеспечивает безопасную передачу данных о пользователе контроллеру домена.
Глава 37 Управление пользователями 37.1. Создание учетной записи пользователя Для создания учетной записи пользователя используется функция NetuserAdd, которая имеет следующий прототип: NET_API_STATUS NetUserAdd ( LPCWSTR servername, // имя сервера DWORD level, // уровень информации LPBYTE buf, // указатель на информацию LPDWORD parm_err // индексация структуры с информацией ); Отметим, что успешно эту функцию могут выполнить только те пользователи, которые являются администраторами или операторами учетных записей. В случае успешного завершения функция NetuserAdd возвращает значение NERR_success, а в случае неудачи возможны следующие коды завершения: □ error_access_denied — пользователю отказано в доступе; □ NERR_invaiidcomputer — неправильное имя компьютера; □ NERR_NotPrimary — операция может выполняться только на первичном контроллере домена; □ NERR_GroupExists — группа уже существует; □ NERR_userExists — учетная запись уже существует; □ NERR_PasswordTooShort — пароль короче, чем требуется. Параметры функции NetuserAdd имеют следующее назначение. Параметр servername должен указывать на строку с именем сервера, на котором будет исполняться функция. Эта строка должна иметь кодировку
700 Часть XI. Управление безопасностью в Windows Unicode и начинаться с символов \\. Если функция должна исполняться на локальном компьютере, то этот параметр должен иметь значение null. Параметр level указывает тип структуры, которая содержит информацию о пользователе. Этот параметр может принимать одно из следующих значений: □ 1 — используется структура типа user_info_i; □ 2 — используется структура типа user_info_2; □ 3 — используется структура типа user_info_3; □ 4 — используется структура типа user_info_4. В примере будет использована структура типа user_info_i, поэтому после описания функции приведем тип и опишем назначение полей этой структуры. Описание остальных структур может быть найдено в MSDN. Параметр buf должен указывать на буфер с информацией о пользователе. Информация должна храниться в структуре, тип которой задан параметром level. В параметр parm_err функция NetuserAdd помещает индекс первого поля в структуре с информацией о пользователе, которое содержит неправильные данные и, как следствие, вызвало завершение функции NetuserAdd с ошибкой. В этом параметре может быть установлено значение null. В этом случае индекс поля с неправильными данными не возвращается. Теперь опишем структуру user_info_i, которая будет использоваться в примере. Эта структура имеет следующий тип: typedef struct _USER_INFO_l { LPWSTR lpwstr DWORD DWORD LPWSTR LPWSTR DWORD LPWSTR usril name; usril password; usril_password_ usril priv; us r i l_home_di r ; usril_comment ; usril_flags; _age ; usril_script_path; // // // // // // // // имя пользователя пароль пользователя возраст пароля уровень привилегий домашний каталог комментарий флаги путь к скрипту }USER_INFO_l, *PUSER_INFO_l, *LPUSER_INFO_l; Поля этой структуры имеют следующее назначение. В поле usrii_name хранится указатель на строку с именем пользователя. Длина строки не должна превышать unlen байтов, а сама строка должна иметь кодировку Unicode. В поле usrii_password хранится указатель на строку с паролем. Длина строки не должна превышать pwlen байтов, а сама строка должна иметь кодировку Unicode.
Глава 37. Управление пользователями 701 Поле usrii_password_age предназначено для хранения количества секунд, которые истекли с момента последнего изменения пароля. Поле usrii_priv предназначено для хранения уровня привилегий, которые получает пользователь. В этом поле может храниться одно из следующих значений: □ USER_PRIV_GUEST — ГОСТЬ; □ user_priv_user — пользователь; □ user_priv_admin — администратор. При вызове функции NetuserAdd это поле может содержать только значение USER_PRIV_USER. В поле usrii_home_dir хранится указатель на строку с именем домашнего каталога пользователя. Строка должна иметь кодировку Unicode. Это поле может содержать значение null. В поле usrii_coiranent хранится указатель на строку с комментариями о пользователе. Строка должна иметь кодировку Unicode. Это поле может содержать значение null. Поле usrii_fiags предназначено для хранения флагов, определяющих тип учетной записи и ее свойства. В этом поле может быть установлен один из следующих флагов: □ uf_normal_account — учетная запись обычного пользователя; □ uf_temp_duplicate_account — временная учетная запись, действительная только в домене; □ uf_workstation_trust_account — учетная запись компьютера в домене как рабочей станции, так и сервера; □ uf_server_trust_account — учетная запись вторичного контроллера домена; □ uf_interdomain_trust_account — учетная запись в доверительном домене. Кроме того, в этом поле может быть установлена любая комбинация следующих флагов: □ uf_script — исполнять скрипт (сценарий) при входе в систему; □ uf_accountdisable — учетная запись недействительна; □ uf_homedir_required — требуется домашний каталог; □ uf_passwd_notreqd — пароль не требуется; □ uf_passwd_cant_change — пароль нельзя изменять; □ uf_lockout — учетная запись заблокирована; □ uf_dont_expire_passwd — пароль не нужно изменять;
702 Часть XI. Управление безопасностью в Windows П UF_ENCRYPTED_TEXT_PASSWORD_ALLOWED — парОЛЬ Сохранен В ЗаШИфрО- ванном виде в Active Directory; □ uf_not_delegated — учетная запись не может делегироваться другим пользователям; □ uf_smartcard_required — для входа в систему требуется смарт-карта; □ uf_use_des_key_only — использовать только DES-стандарт шифрования; □ uf_dont_require_preauth — не требуется Kerberos-аутентификация при входе в систему; □ uf_trusted_for_delegation — учетная запись может делегироваться; □ uf_password_expired — время действия пароля истекло. Отметим, что, начиная с флага uf_dont_expire_passwd, все последующие флаги действительны только в версиях Windows 2000 и выше. В поле usrii_script_path хранится указатель на строку с именем файла, содержащего скрипт, который выполняется перед входом пользователя в систему. Строка должна иметь кодировку Unicode. Это поле может содержать значение null. В листинге 37.1 приведена программа, которая создает учетную запись пользователя, ИСПОЛЬЗУЯ ДЛЯ ЭТОГО фуНКЦИЮ NetUserAdd. #include <stdio.h> #include <windows.h> #include <lm.h> #pragma comment ( lib, "netapi32 .lib" ) // подключаем сетевую библиотеку int main () { wchar_t s erver_name[256] = L" \ \ \ \"; wchar_t user_name[UNLEN] ; wchar_t user_j?assword[PWLEN] ; wchar_t user_comment [256] ; USER_INFO_l ui; NET_API_STATUS ret_s tatus; printf (" Input server name: ") ; // формируем имя сервера wscanf (L"%s", server_name + wcslen(server_name) ) ; // имя сервера // имя пользователя // пароль пользователя // комментарии о пользователе // информация о пользователе // код возврата из функции
Глава 37. Управление пользователями 703 printf("Input user name: "); // читаем имя пользователя wscanf(L"%s", user_name); // читаем имя пользователя ui.usril_name = user_name; // устанавливаем имя пользователя printf("Input user password: ") ; wscanf(L"%s", user_password); // читаем пароль пользователя ui.usril_password = user_password; // устанавливаем пароль пользователя ui.usril_priv = USER_PRIV_USER; // обычный пользователь ui.usril_home_dir = NULL; // домашнего каталога нет printf("Input user comment: ") ; getwchar(); // очищаем поток _getws (user_comment) ; // читаем комментарии о пользователе ui.usril_comment = user_comment; // устанавливаем комментарии ui.usril_flags = UF_SCRIPT; // исполнять скрипт при входе // пользователя в систему ui.usril_script_path = NULL; // пока файл со скриптом не определяем // добавляем пользователя ret_status = NetUserAdd( server_name, // имя сервера 1, // уровень информации 1 (LPBYTE)&ui, // адрес информации о пользователе NULL); // индексирования в структуре данных нет // проверяем на успешное завершение if (ret_status != NERR_Success) { printf("Net user add failed.\n"); printf("Error code: %u\n", ret_status); return ret_status; } printf("The user is added.\n"); return 0;
704 Часть XI. Управление безопасностью в Windows 37.2. Получение информации о пользователе Для получения информации о пользователе из его учетной записи используется функция NetuserGetinf о, которая имеет следующий прототип: NET_API_STATUS NetUserGetlnfо( LPCWSTR servername, // имя сервера LPCWSTR username, // имя пользователя DWORD level, // уровень информации LPBYTE *bufptr // указатель на информацию ); В случае успешного завершения функция NetuserGetinfo возвращает значение NERR_success, а в случае неудачи возможны следующие коды завершения: □ error_access_denied — пользователю отказано в доступе; □ NERR_invaiidcomputer — неправильное имя компьютера; □ NERR_userNotFound — пользователь не найден. Параметры функции NetuserGetinfo имеют следующее назначение. Параметр servername должен указывать на строку с именем сервера, на котором будет исполняться функция. Эта строка должна иметь кодировку Unicode и начинаться с символов \\. Если функция должна исполняться на локальном компьютере, то этот параметр должен иметь значение null. Параметр username должен указывать на строку с именем пользователя, информацию о котором возвращает функция. Эта строка должна иметь кодировку Unicode. Параметр level указывает тип структуры, в которую будет записана информация о пользователе. Этот параметр может принимать одно из следующих значений: □ 1 — используется структура типа user_info_i; □ 2 — используется структура типа user_info_2; □ 3 — используется структура типа user_info_3; □ 4 — используется структура типа user_info_4; □ 10 — используется структура типа user_info_io; □ 11 — используется структура типа user_info_h; □ 20 — используется структура типа user_info_2o; □ 23 — используется структура типа user_info_23.
Глава 37. Управление пользователями 705 В примере будет использована структура типа user_info_i, тип которой описан в предыдущем разделе. Описание типов остальных структур можно найти в MSDN. Параметр bufptr должен содержать адрес указателя на массив байтов. В этот указатель функция запишет адрес буфера с информацией о пользователе. Память под буфер выделяет система. Поэтому после прочтения информации о пользователе из буфера эта память должна быть освобождена посредством вызова функции NetApiBuf f erFree, которая рассмотрена далее. Для освобождения памяти, которая резервируется функциями из библиотеки netapi32.1ib, используется функция NetApiBufferFree, которая имеет следующий прототип: NET_API_STATUS NetApiBuf ferFree ( LPVOID buffer // указатель на буфер ); В случае успешного завершения функция NetApiBuf ferFree возвращает значение NERR_success, а в случае неудачи код ошибки. Единственный параметр этой функции должен указывать на буфер, память для которого была предварительно распределена одной из сетевых функций. В листинге 37.2 приведена программа, которая получает информацию о пользователе при ПОМОЩИ функции NetUserGetlnfо. -и -'■- j^f^^ 7Г':'.' ™"™ , """'""Г Листинг37.2. Получение информации о пользователе #include <stdio.h> #include <windows.h> #include <lm.h> #pragma comment( lib, "netapi32.1ib" ) // подключаем сетевую библиотеку int main () { wchar_t server_name[256] =L"\\\\"; // имя сервера wchar_t user_name [UNLEN]; // имя пользователя USER_INF0_1 *ui; // информация о пользователе NET_API_STATUS ret_status; // код возврата из функции printf( "Input server name: "); // вводим имя сервера // формируем имя сервера ws can f(L"% s", server_name + wc s1en(server_name)); 23 Зак. 514
706 Часть XI. Управление безопасностью в Windows printf("Input user name: "); wscanf(L"%s", user_name); // читаем имя пользователя // получаем информацию о пользователе ret_status = NetUserGetInfo( server_name/ // имя сервера user_name, // имя пользователя 1, // уровень информации 1 (LPBYTE*)&ui); // адрес информации о пользователе // проверяем на успешное завершение if (ret_status != NERR_Success) { printfC'Net user get info failed. \n") ; return ret_status/ } wprintf(L"User name: %s\n", ui->usril_name); wprintf(L"User commemnt: %s\n", ui->usril_comment) ; NetApiBufferFree(ui); // освобождаем буфер return 0; } 37.3. Перечисление пользователей Для того чтобы перечислить учетные записи всех пользователей, зарегистрированных на сервере, используется функция NetuserEnum, которая имеет следующий прототип: NET_API_STATUS NetuserEnum( // имя сервера // уровень информации // фильтр на учетные записи // указатель на буфер с информацией // длина буфера с информацией // количество прочитанных структур LPCWSTR DWORD DWORD LPBYTE DWORD LPDWORD servername, level, filter, *bufptr, prefmaxlen, entriesread,
Глава 37. Управление пользователями 707 LPDWORD totalentries, // общее количество структур LPDWORD resume_handle // дескриптор для продолжения чтения ); В случае успешного завершения функция NetuserEnum возвращает значение NERR_success, а в случае неудачи возможны следующие коды завершения: □ error_access_denied — пользователю отказано в доступе; □ NERR_invaiidcomputer — неправильное имя компьютера; □ error_more_data — не все данные прочитаны. Параметры функции NetuserEnum имеют следующее назначение. Параметр servemame должен указывать на строку с именем сервера, на котором будет исполняться функция. Эта строка должна иметь кодировку Unicode и начинаться с символов \\. Если функция должна исполняться на локальном компьютере, то этот параметр должен иметь значение null. Параметр level указывает тип структуры, в которую будет записана информация о пользователе. Этот параметр может принимать одно из следующих значений: □ 1 — используется структура типа user_info_i; П 2 — используется структура типа user_info_2; □ 3 — используется структура типа user_info_3; □ 4 — используется структура типа user_info_4; □ 10 — используется структура типа user_info_io; □ 11 — используется структура типа user_info_ii; □ 20 — используется структура типа user_info_2o; □ 23 — используется структура типа user_info_23. Отметим, что если установлен один из первых четырех уровней, то функция не возвращает пароль пользователя. В примере будет использована структура типа user_info_i, тип которой описан в разд. 37.1. Описание типов остальных структур можно найти в MSDN. Параметр filter задает типы учетных записей, которые должны быть перечислены. Этот параметр может принимать одно из следующих значений: □ filter_temp_duplicate_account — перечисляются локальные учетные записи; □ filter_normal_account — перечисляются глобальные учетные записи; □ filter_interdomain_trust_account — перечисляются доверительные учетные записи домена; □ filter_workstation_trust_account — перечисляются учетные записи рабочих станций и серверов домена;
708 Часть XI. Управление безопасностью в Windows □ filter_server_trust_account — перечисляются учетные записи контроллеров домена. Параметр bufptr должен содержать адрес указателя, в который система запишет адрес буфера с информацией об учетных записях. Память под буфер распределяется системой. Поэтому после прочтения информации из буфера эту память нужно освободить при помощи вызова функции NetApiBuf f erFree, которая была описана в предыдущем разделе. Эту память нужно освободить также и в том случае, если функция вернула код ошибки ERROR_MORE_DATA. Параметр prefmaxien должен содержать предпочтительную длину буфера в байтах. Если памяти для данных не хватит, то функция вернет код ошибки error_more_data. В этом параметре можно установить значение max_preferred_length. В этом случае система сама определит необходимую длину буфера для информации об учетных записях. Параметр entriesread должен указывать на двойное слово, в которое функция запишет количество учетных записей, информация о которых записана в буфер данных. Параметр totaientries должен указывать на переменную типа dword, в которую функция запишет общее количество учетных записей, удовлетворяющих условию отбора. Параметр resume_handie должен указывать на переменную типа dword, в которую функция запишет дескриптор для продолжения перечисления учетных записей. При первом вызове функции это двойное слово должно содержать значение 0. Если продолжение чтения информации об учетных записях не предполагается, то в этом параметре нужно установить значение null. В листинге 37.3 приведена программа, которая перечисляет учетные записи Пользователей, ИСПОЛЬЗУЯ фуНКЦИЮ NetUserEnum. [Листинг 37& Перечисление учетных записей пользователей #include <stdio.h> #include <windows.h> #include <lm.h> #pragma comment( lib, "netapi32.lib" ) // подключаем сетевую библиотеку int main () { wchar_t server_name [256] = L"\\\\"; // имя сервера USER_INFO_0 *ui; // информация о пользователе
Глава 37. Управление пользователями 709 DWORD entries_read; // количество прочитанных пользователей DWORD total_entries; // общее количество пользователей NET_API_STATUS ret_status; // код возврата из функции printf("Input server name: "); // читаем имя сервера // формируем имя сервера ws can f(L"%s", s erver_name + wcs1en(server_name)); // получаем информацию о пользователях ret_status = NetUserEnum( server_name, // имя сервера О, // узнаем только имена пользователей FILTER_NORMAL_ACCOUNT, // перечисляем пользователей, // зарегистрированных на компьютере (LPBYTE*)&ui, // адрес информации о пользователях MAX_PREFERRED_LENGTH, // перечисляем всех пользователей &entries_read, // количество прочитанных пользователей &total_entries, // общее количество пользователей NULL); // индексации нет // проверяем на успешное завершение if (ret_status != NERR_Success) { printf("Net user get info failed.\n"); printf("Net API Status: %d\n'\ ret_status); NetApiBufferFree(ui); // освобождаем буфер return ret_status; } for (DWORD i = 0; i < entries_read; ++i) wprintf(L"User name: %s\n"/ ui[i].usriO_name); NetApiBufferFree(ui); // освобождаем буфер return 0;
710 Часть XI. Управление безопасностью в Windows 37.4. Перечисление групп, которым принадлежит пользователь Для перечисления глобальных групп, которым принадлежит пользователь, используется функция NetuserGetGroups, которая имеет следующий прототип: NET_API_STATUS NetuserGetGroups( LPCWSTR LPCWSTR DWORD LPBYTE DWORD LPDWORD LPDWORD servername, username, level, *bufptr, prefmaxlen, entriesread, totalentries // // // // // // // имя сервера имя пользователя уровень информации адрес указателя на буфер с информацией длина буфера с информацией количество прочитанных структур общее количество структур ); В случае успешного завершения функция NetuserGetGroups возвращает значение NERR_success, а в случае неудачи возможны следующие коды завершения: □ error_access_denied — пользователю отказано в доступе; □ error_more_data — не все данные прочитаны; □ NERR_invalidComputer — неправильное имя компьютера; □ NERR_userNotFound — имя пользователя не найдено. Параметры функции NetuserGetGroups имеют следующее назначение. Параметр servemame должен указывать на строку с именем сервера, на котором будет исполняться функция. Эта строка должна иметь кодировку Unicode и начинаться с символов \\. Если функция должна исполняться на локальном компьютере, то этот параметр должен иметь значение null. Параметр username должен указывать на строку с именем пользователя, которое проверяется на принадлежность глобальным группам для их перечисления. Параметр level указывает тип структуры, в которую будет записана информация о глобальных группах. Этот параметр может принимать только значение 0, которое обозначает, что для записи информации о локальной группе будет использоваться структура group_users_info_o, которая имеет следующий тип: typedef struct _GROUP_USERS_INFO_0 { LPWSTR gruiO_name; // имя пользователя } GROUP_USERS_INFO_0, *PGROUP_USERS_INFO_0/ *LPGROUP_USERS_INFO_0;
Глава 37. Управление пользователями 711 Параметр bufptr должен содержать адрес указателя, в который система запишет адрес буфера с информацией о глобальных группах. Память под буфер распределяется системой. Поэтому после прочтения информации из буфера эту память нужно освободить при помощи вызова функции NetApiBuf f erFree, которая была описана в разд. 37.2. Эту память нужно освободить также и в том случае, если функция вернула код ошибки ERROR_MORE_DATA. Параметр prefmaxien должен содержать предпочтительную длину буфера в байтах. Если памяти для данных не хватит, то функция вернет код ошибки error_more_data. В этом параметре можно установить значение max_preferred_length. В этом случае система сама определит необходимую длину буфера для информации об учетных записях. Параметр entriesread должен указывать на переменную типа dword, в которую функция запишет количество глобальных групп, имена которых записаны в буфер данных. Параметр totaientries должен указывать на переменную типа dword, в которую функция запишет общее количество глобальных групп, которым принадлежит пользователь. В листинге 37.4 приведена программа, которая перечисляет глобальные группы, которым принадлежит пользователь, используя функцию NetUserGetGroups. | Листинг 37,4, Перечисление глобальных групп» которым принадлежит \ ъ \ | пользователь ^ ч " /\ i ..........—.......~........... ~ „ . . . . .: .. 1 ; I #include <stdio.h> # include <windows.h> #include <lm.h> #pragma comment( lib, "netapi32.lib" ) // подключаем сетевую библиотеку int main() { wchar_t server_name[256] = L"\\\\"; // имя сервера wchar_t user_name [UNLEN] ; // имя пользователя GROUP_USERS_INFO_0 *ui; // информация о группах DWORD entries_read; // количество прочитанных групп DWORD total_entries; // общее количество групп NET_API_STATUS ret_status; // код возврата из функции
712 Часть XL Управление безопасностью в Windows printf("Input server name: "); // формируем имя сервера ws can f (L" % s", s erver_name + wcs1en(server_name)); printf("Input user name: "); wscanf(L"%s", user_name); // читаем имя пользователя // получаем информацию о группах, в которые входит пользователь ret_status = NetUserGetGroups( server_name/ // имя сервера user_name, // имя пользователя О, // узнаем имена групп (LPBYTE*)&ui, // адрес информации о группах MAX_PREFERRED_LENGTH, // перечисляем все группы &entries_read, // количество прочитанных групп &total_entries); // общеее количество групп // проверяем на успешное завершение if (ret_status != NERR_Success) { printf("Net user get groups failed.\n"); printf("Net API Status: %d\n", ret_status); NetApiBufferFree(ui); // освобождаем буфер return ret_status; } for (DWORD i = 0; i < entries_read; ++i) wprintf(L"Global group name: %s\n"/ ui[i].gruiO_name); NetApiBufferFree(ui); // освобождаем буфер return 0; } Для перечисления локальных групп, которым принадлежит пользователь, ИСПОЛЬЗуетСЯ фунКЦИЯ NetuserGetLocaiGroups, которая имеет следующий прототип: NET_API_STATUS NetuserGetLocaiGroups ( LPCWSTR servername, // имя сервера
Глава 37. Управление пользователями 713 LPCWSTR username, // имя пользователя DWORD level, // уровень информации DWORD flags, // флаг косвенного поиска LPBYTE *bufptr, // адрес указателя на буфер с информацией DWORD prefmaxlen, // длина буфера с информацией LPDWORD entriesread, // количество прочитанных структур LPDWORD totalentries // общее количество структур ); В случае успешного завершения функция NetuserGetLocaiGroups возвращает значение NERR_success, а в случае неудачи возможны такие же коды завершения, как и в случае функции NetuserGetGroups. Параметры функции NetuserGetLocaiGroups имеют такое же назначение, как и параметры функции NetuserGetGroups. Существуют только небольшие дополнения, о которых и сказано далее. Если имя пользователя в строке, на которую указывает параметр username, задано в виде имя_домена\имя_пользователя, то имя пользователя ищется в домене. Если же имя пользователя задано только как имяпользователя, то оно ищется на сервере, имя которого задано первым параметром. Параметр flags может принимать значение lg_include_indirect. Если это значение установлено, то перечисляются как локальные группы, которым пользователь принадлежит непосредственно, так и локальные группы, которым пользователь принадлежит косвенно. То есть через членство в глобальных группах, которые принадлежат локальной группе. В противном случае перечисляются только локальные группы, которым пользователь принадлежит только непосредственно. В листинге 37.5 приведена программа, которая перечисляет локальные группы, которым принадлежит пользователь, используя для этого функцию NetuserGetLocaiGroups. Листинг 37.5. Перечисление локальных групп, которым принадлежит пользователь , ,~ -* s* ^-' ^* :^ *,-- » ''^ 'V* #include <stdio.h> #include <windows.h> #include <lm.h> #pragma comment ( lib, "netapi32 . lib" ) // подключаем сетевую библиотеку int main ()
714 Часть XL Управление безопасностью в Windows wchar_t server_name[256] = L"\\\\"; // имя сервера wchar_t user_name [UNLEN] ; // имя пользователя LOCALGROUP_USERS_INFO_0 *ui; // информация о группах DWORD entries_read; // количество прочитанных групп DWORD total_entries; // общее количество групп NET_API_STATUS ret_status; // код возврата из функции printf("Input server name: "); // -формируем имя сервера ws can f(L"%s", server_name + wcs1en(server_name)); printf("Input user name: "); wscanf(L"%s", user_name); // читаем имя пользователя // получаем локальные группы, в которые входит пользователь ret_status = NetUserGetLocalGroups( server_name, // имя сервера user_name, // имя пользователя О, // узнаем только имена групп LG_INCLUDE_INDIRECT, // включая косвенную принадлежность (LPBYTE*)&ui, // адрес информации о группе MAX_PREFERRED_LENGTH, // перечисляем все группы &entries_read, // количество прочитанных групп &total_entries); // общее количество групп // проверяем на успешное завершение if (ret_status != NERR_Success) { printfC'Net user get groups failed.\n"); printfC'Net API Status: %d\n"/ ret_status); NetApiBufferFree(ui); // освобождаем буфер return ret_status; } for (DWORD i = 0; i < entries_read; ++i)
Глава 37. Управление пользователями 715 wprintf(L"Local group name: %s\n"/ ui[i].lgruiO_name); NetApiBufferFree(ui); // освобождаем буфер return 0; } 37.5. Изменение учетной записи пользователя Для изменения содержимого учетной записи пользователя используется функция Netusersetinf о, которая имеет следующий прототип: NET_API_STATUS NetUserSetlnfо( LPCWSTR servername, // имя сервера LPCWSTR username, // имя пользователя DWORD level, // уровень информации LPBYTE buf, // буфер с информацией об учетной записи LPDWORD parm_err // индекс поля с неправильными данными ); Отметим, что успешно эту функцию могут выполнить только пользователи, которые являются администраторами или операторами учетных записей. В случае успешного завершения функция Netusersetinfo возвращает значение NERR_success, а в случае неудачи возможны следующие коды завершения: □ error_access_denied — пользователю отказано в доступе; □ error_invalid_parameter — неправильный параметр; □ NERR_invaiidconputer — неправильное имя компьютера; □ NERR_NotPrimary — не первичный контроллер домена; □ NERR_speGroupOp — не разрешается выполнять над членами специальной группы; □ NERR_LastAdmin — не разрешается выполнять над учетной записью администратора; □ NERR_BadPassword — Неправильный Пароль; □ NERR_PasswordTooShort — пароль короче, чем требуется; □ NERR_userNotFound — имя пользователя не найдено. Параметры этой функции имеют следующее назначение.
716 Часть XI. Управление безопасностью в Windows Параметр servemame должен указывать на строку с именем сервера, на котором будет исполняться функция. Эта строка должна иметь кодировку Unicode и начинаться с символов \\. Если функция должна исполняться на локальном компьютере, то этот параметр должен иметь значение NULL. Параметр usemame должен указывать на строку с именем учетной записи, содержимое которой модифицируется. Эта строка должна иметь кодировку Unicode. Параметр level указывает тип структуры, которая содержит новую информацию об учетной записи пользователя. Этот параметр может принимать одно из следующих значений: □ О — используется структура типа user_info_o; □ 1 — используется структура типа user_info_i; □ 2 — используется структура типа user_info_2; □ 3 — используется структура типа user_info_3; □ 4 — используется структура типа user_info_4; □ 21 — используется структура типа user_info_2i; □ 22 — используется структура типа user_info_22; □ 1003 — используется структура типа user_info_1003; □ 1005 — используется структура типа user_info_1005; □ 1006 — используется структура типа user_info_ioo6; □ 1007 — используется структура типа user_info_ioov, □ 1008 — используется структура типа user_info_1008; □ 1009 — используется структура типа user_info_ioo9; □ 1010 — используется структура типа user_info_ioio; □ 1011 — используется структура типа user_info_ioii; □ 1012 — используется структура типа user_info_1012; □ 1014 — используется структура типа user_info_1014; □ 1017 — используется структура типа user_info_1017; □ 1020 — используется структура типа user_info_io2o; □ 1024 — используется структура типа user_info_1024; □ 1051 — используется структура типа user_info_105i; □ 1052 используется структура типа user_info_1052; □ 1053 используется структура типа user_info_1053.
Глава 37. Управление пользователями 717 В примере будет использована структура user_info_o, которая имеет следующий тип: typedef struct _USER_INFO_0 { LPWSTR usriO_name; // имя пользователя }USER_INFO_0, *PUSER_INFO_0, *LPUSER_INFO_0; Эта структура используется в том случае, если при помощи функции NetuserSetinfo изменяется имя пользователя. Остальные структуры имеют также конкретное назначение. Например, структура типа user_info_ioo8 используется для установки флагов, которые управляют свойствами учетной записи пользователя. Описание типов остальных структур можно найти в MSDN. Параметр buf должен указывать на структуру, которая содержит информацию об учетной записи пользователя. Тип этой структуры задается параметром level. Параметр parm_err должен указывать на переменную типа dword, в которую функция NetuserSetinfo поместит индекс поля в структуре с информацией о пользователе, содержимое которого вызвало неудачное завершение функции. В этом случае функция NetuserSetinfo вернет значение ERROR_INVALID_PARAMETER. В листинге 37.6 приведена программа, которая изменяет имя пользователя, ИСПОЛЬЗуя фунКЦИЮ NetuserSetinfo. Листинг 37.$. Изменение имени пользователя ; ^. \ #include <stdio.h> # include <windows.h> #include <lm.h> #pragma comment ( lib, "netapi32 .lib" ) // подключаем сетевую библиотеку int main() { wchar_t server_name[256] = L"\\\\"; // имя сервера wchar_t old_name[UNLEN]; // старое имя пользователя wchar_t new_name[UNLEN]; // новое имя пользователя USER_INFO_0 ui; // информация о пользователе NET_API_STATUS ret_status; // код возврата из функции printf("Input server name: ");
718 Часть XI. Управление безопасностью в Windows II формируем имя сервера wscanf (L"%s", server_name + wcslen(server_name)); printf("Input old user name: "); wscanf(L"%s", old_name); // читаем старое имя пользователя printf("Input new user name: "); wscanf(L"%s", new_name); // читаем новое имя пользователя ui.usriO_name = new_name; // устанавливаем новое имя пользователя // устанавливаем информацию о пользователе ret_status = NetUserSetlnfо( server_name, // имя сервера old_name, // имя пользователя 0, // изменяем имя пользователя (LPBYTE)&ui, // адрес информации о пользователе NULL); // нет индексации // проверяем на успешное завершение if (ret_status != NERR_Success) { printf("Net user set info failed.\n"); printf("Net API Status: %d\n", ret_status); return ret_status; } printf("User name is changed.\n"); USER_INF0_1 *uix; // информация о пользователе // получаем информацию о пользователе ret_status = NetUserGetlnfо( server_name, // имя сервера new_name, // имя пользователя 1, // уровень информации 1 (LPBYTE*)&uix); // адрес информации о пользователе // проверяем на успешное завершение if (ret_status != NERR_Success)
Глава 37. Управление пользователями 719 { printf("Net user get info failed.\n"); return ret_status; } wprintf(L"User name: %s\n", uix->usril_name); wprintf(L"User coramemnt: %s\n", uix->usril_comment); return 0; } 37.6. Изменение пароля пользователя Для изменения пароля пользователя используется функция NetUserChangePassword, КОТОрая имеет следующий ПроТОТИП! NET_API_STATUS NetUserChangePassword( LPCWSTR domainname, // имя домена LPCWSTR username, // имя пользователя LPCWSTR oldpassword, // старый пароль LPCWSTR newpassword // новый пароль ); Отметим, что сервер может быть сконфигурирован таким образом, что успешно эту функцию могут выполнить только пользователи, которые являются администраторами или операторами учетных записей, или пользователь, который изменяет свой пароль. В случае успешного завершения функция NetUserChangePassword ВОЗВраща- ет значение NERR_success, а в случае неудачи возможны следующие коды завершения: □ error_access_denied — пользователю отказано в доступе; П error_invalid_password — неправильный пароль; □ NERR_invaiidComputer — неправильное имя компьютера; □ NERR_NotPrimary — не первичный контроллер домена; □ NERR_PasswordTooshort — пароль короче, чем требуется; □ NERR_userNotFound — имя пользователя не найдено. Параметры этой функции имеют следующее назначение. Параметр domainname должен указывать на строку с именем сервера или домена, в котором зарегистрирован пользователь. Эта строка должна иметь
720 Часть XI. Управление безопасностью в Windows кодировку Unicode и начинаться с символов \\. Если этот параметр равен null, то имя пользователя ищется в домене, в котором зарегистрирована учетная запись, от имени которой вызвана функция. Параметр username должен указывать на строку с именем учетной записи, пароль которой изменяется. Эта строка должна иметь кодировку Unicode. Если значение этого параметра равно null, to используется имя пользователя, указанное при входе в систему. Параметр oidpassword должен указывать на строку со старым паролем пользователя. Эта строка должна иметь кодировку Unicode. Параметр newpassword должен указывать на строку с новым паролем пользователя. Эта строка должна иметь кодировку Unicode В листинге 36.7 приведена программа, которая изменяет пароль пользователя, ИСПОЛЬЗУЯ фунКЦИЮ NetUserChangePassword. ..-...„,, J.-, _...,. -_гг. -_.._. ...... ^_т .;. ......... _.. .... .-... -., rjftZTi Шцёиение пароля Шй^йрагшйя. — * ;' /£~- */ "* | #include <stdio.h> #include <windows.h> #include <lm.h> #pragma comment( lib, "netapi32.lib" ) // подключаем сетевую библиотеку int main() { wchar_t domain_name[256] = Ь"\\\\' wchar_t user_name [UNLEN] ; wchar_t old_password[PWLEN] ; wchar_t new_password[PWLEN]; // имя сервера // имя пользователя // старый пароль пользователя // новый пароль пользователя NET_API_STATUS ret_status; // код возврата из функции printf("Input server or domain name: "); wscanf(L"%s", domain__name); // читаем имя домена printf("Input user name: "); wscanf(L"%s", user name); // читаем имя пользователя printf("Input old password: "); wscanf(L"%s", old_password); // читаем имя пользователя
Глава 37. Управление пользователями 721 printf("Input new password: "); wscanf(L"%s", new_password); // читаем имя пользователя // изменяем пароль пользователя ret_status = NetUserChangePassword( domain_name, // имя домена user_name, // имя пользователя old_password, // старый пароль new_password); // новый пароль // проверяем на успешное завершение if (ret_status != NERR_Success) { printf("Net user change password failed.\n"); printf("Net API Status: %d\n", ret_status); return ret_status; } printf("The password was changed.\n"); return 0; } 37.7. Удаление учетной записи пользователя Для удаления учетной записи пользователя из базы данных менеджера учетных записей используется функция NetuserDei, которая имеет следующий прототип: NET_API_STATUS NetUserDel( LPCWSTR servername, // имя сервера LPCWSTR username // имя пользователя ); Отметим, что успешно эту функцию могут выполнить только пользователи, которые являются администраторами или операторами учетных записей. А учетную запись администратора может удалить только другой администратор.
722 Часть XI. Управление безопасностью в Windows В случае успешного завершения функция NetuserDei возвращает значение NERR_success, а в случае неудачи возможны следующие коды завершения: □ error_access_denied — пользователю отказано в доступе; □ NERR_invaiidComputer — неправильное имя компьютера; □ NERR_NotPrimary — не первичный контроллер домена; □ NERR_userNotFound — имя пользователя не найдено. Параметры этой функции имеют такое же назначение, как в других функциях управления пользователями. Параметр servername должен указывать на строку с именем сервера, на котором будет исполняться функция. Эта строка должна иметь кодировку Unicode и начинаться с символов \\. Если функция должна исполняться на локальном компьютере, то этот параметр должен иметь значение null. Параметр username должен указывать на строку с именем учетной записи, которая удаляется из базы данных менеджера учетных записей. Эта строка должна иметь кодировку Unicode. В листинге 37.8 приведена программа, которая удаляет учетную запись пользователя, ИСПОЛЬЗУЯ фуНКЦИЮ NetUserDel. Листинг $7Ж Удаление учетной записи пользователя | #include <stdio.h> # include <windows.h> #include <lm.h> #pragma comment ( lib, "netapi32 .lib" ) // подключаем сетевую библиотеку int main () { wchar_t server_name[256] =L"\\\\"; // имя сервера wchar_t user_name[UNLEN]; // имя пользователя NET_API_STATUS ret_status; // код возврата из функции printf("Input server name: "); wscanf(L"%s", server_name) ; // читаем имя домена printf("Input user name: ");
Глава 37. Управление пользователями 723 wscanf(L"%s", user_name); // читаем имя пользователя // удалаем пользователя ret_status = NetUserDel( server_name/ // имя домена user_name); // имя пользователя // проверяем на успешное завершение if (ret_status != NERR_Success) { printf("Net user del failed.\n"); printf("Net API Status: %d\n", ret_status); return ret_status; } printf("The user is deleted.\n"); return 0;
Глава 38 Управление группами В этой главе будут рассмотрены только функции, предназначенные для работы с локальными группами. Функции, предназначенные для работы с глобальными группами, рассматриваться не будут. Для этого существуют две причины. Во-первых, сигнатура и правила использования функций для работы с глобальными группами очень похожи соответственно на сигнатуру и правила использования функций для работы с локальными группами. Поэтому после изучения этого раздела программист сможет самостоятельно разобраться с функциями, предназначенными для управления глобальными группами. Во-вторых, этот материал предназначен для начинающих программистов. Поэтому маловероятно, что такие программисты будут экспериментировать с глобальными группами локальной сети. А для этого достаточно научиться управлять только локальными группами. 38.1. Создание локальной группы Для создания учетной записи локальной группы в базе данных менеджера учетных записей используется функция NetLocaiGroupAdd, которая имеет следующий прототип: NET_API_STATUS NetLocaiGroupAdd( LPCWSTR servername, // имя сервера DWORD level, // уровень информации LPBYTE buf, // буфер с информацией LPDWORD parm_err // индексирование ошибки ); Отметим, что успешно эту функцию могут выполнить только пользователи, которые являются администраторами или операторами учетных записей.
Глава 38. Управление группами 725 В случае успешного завершения функция NetLocaiGroupAdd возвращает значение NERR_Success, а в случае неудачи возможны следующие коды завершения: □ error_access_denied — пользователю отказано в доступе; □ NERR_invaiidcomputer — неправильное имя компьютера; □ NERR_NotPrimary — операция может выполняться только на первичном контроллере домена; □ NERR_GroupExists — группа уже существует; □ error_alias_exists — группа уже существует. Параметры функции NetLocaiGroupAdd имеют следующее назначение. Параметр servername должен указывать на строку с именем сервера, на котором будет исполняться функция. Эта строка должна иметь кодировку Unicode и начинаться с символов \\. Если функция должна исполняться на локальном компьютере, то этот параметр должен иметь значение null. Параметр level указывает тип структуры, которая содержит информацию о локальной группе. Этот параметр может принимать одно из следующих значений: □ 0 — используется структура типа localgroup_info_o; □ 1 — используется структура типа localgroup_info_i. В примере будет использована структура типа localgroup_info_i, описание которой и будет приведено после описания функции. Описание типа структуры localgroup_info_o может быть найдено в MSDN. Параметр buf должен указывать на буфер с информацией о локальной группе. Информация должна храниться в структуре, тип которой задан параметром level. В переменную, на которую указывает параметр parm_err, функция NetLocaiGroupAdd помещает индекс первого поля в структуре с информацией о локальной группе, которое содержит неправильные данные и, как следствие, вызывает завершение функции NetLocaiGroupAdd с ошибкой. В этом параметре может быть установлено значение null. В этом случае индекс поля с неправильными данными не возвращается. Так как в примере будет использована структура типа localgroup_info_i, то приведем тип и опишем назначение полей этой структуры. Структура localgroup_info_i имеет следующий тип: typedef struct _LOCALGROUP_INFO_l { LPWSTR lgrpil_name; // имя группы LPWSTR lgrpil_coiranent; // комментарии }LOCALGROUP_INFO_l, * PLOCALGROUP_INFO_l, * LPLOCALGROUP_INFO_l;
726 Часть XI. Управление безопасностью в Windows Поля этой структуры имеют следующее назначение. В поле igrpii_name хранится указатель на строку с именем локальной группы. Длина строки не должна превышать gnlen байт, а сама строка должна иметь кодировку Unicode. В поле igrpii_comment хранится указатель на строку с комментариями относительно локальной группы. Длина строки не должна превышать maxcommentsz байтов, а сама строка должна иметь кодировку Unicode. В листинге 38.1 приведена программа, которая создает локальную группу, ИСПОЛЬЗуя ДЛЯ ЭТОГО фунКЦИЮ NetLocalGroupAdd. [Листинг 38,1, Создание шкальной группы С | #include <stdio.h> # include <windows.h> #include <lm.h> #pragma comment ( lib, "netapi32 .lib" ) // подключаем сетевую библиотеку int main() { wchar_t server_name[256] = L"WW"; wchar_t group_name[GNLEN]; wchar_t comment [MAXCOMMENTSZ] ; LOCALGROUP_INFO_l group_infо; NET_API_STATUS ret_status; printf("Input server name: ") ; // формируем имя сервера wscanf(L"%s", server_name + wcslen(server_name)); printf("Input a name for the local group: "); wscanf(L"%s", group_name); // вводим имя группы group_info.lgrpil_name = group_name; // устанавливаем адрес имени //в структуру printf("Input group comments: "); getwchar(); // очищаем поток _getws(comment); // читаем комментарий о локальной группе // имя сервера // имя локальной группы // комментарий о группе // информация о группе // код возврата из функции
Глава 38. Управление группами 727 group_info.lgrpil_corament = comment; // устанавливаем комментарий // создаем локальную группу ret_status = NetLocalGroupAdd( server_name, // имя сервера 1, // уровень входных данных (LPBYTE)&group_info, // имя группы и комментарии NULL); // индексацию данных не используем if (ret_status != NERR_Success) { printf("Net local group add failed.\n"); return ret_status; } printf("The group is created.\n"); return 0; } 38.2. Получение информации о локальной группе Для получения информации о локальной группе используется функция NetLocaiGroupGetinfo, которая имеет следующий прототип: NET_API_STATUS NetLocaiGroupGetinfo( LPCWSTR servername, // имя сервера LPCWSTR groupname, // имя группы DWORD level, // уровень информации LPBYTE *bufptr // буфер для информации ); Отметим, что успешно эту функцию могут выполнить только пользователи, которые являются администраторами или операторами учетных записей. В случае успешного завершения функция NetLocaiGroupGetinfo возвращает значение NERR_success, а в случае неудачи возможны следующие коды завершения: □ error_access_denied — пользователю отказано в доступе; □ NERR_invaiidComputer — неправильное имя компьютера; □ error_no_such_alias — группа с таким именем не существует.
728 Часть XI. Управление безопасностью в Windows Параметры функции NetLocaiGroupGetinfo имеют следующее назначение. Параметр servername должен указывать на строку с именем сервера, на котором будет исполняться функция. Эта строка должна иметь кодировку Unicode и начинаться с символов \\. Если функция должна исполняться на локальном компьютере, то этот параметр должен иметь значение null. Параметр groupname должен указывать на строку с именем локальной группы, о которой получают информацию. Эта строка должна иметь кодировку Unicode. Параметр level указывает тип структуры, которая содержит информацию о локальной группе. Этот параметр может принимать только одно значение — 1, указывающее на то, что используется структура типа localgroup_info_i; В этом случае будет использована структура localgroup_info_i, тип которой был описан в предыдущем разделе. Параметр bufptr должен содержать адрес указателя, в который система запишет адрес буфера с информацией о локальных группах. Память под буфер распределяется системой. Поэтому после прочтения информации из буфера Эту память НУЖНО ОСВОбоДИТЬ При ПОМОЩИ ВЫЗОВа фунКЦИИ NetApiBuf ferFree, которая была описана в разд. 37.2. В листинге 38.2 приведена программа, которая получает информацию о локальной Группе, ИСПОЛЬЗУЯ ДЛЯ ЭТОГО фуНКЦИЮ NetLocaiGroupGetinfo. { Листинг %ЪЛ* Получение информации о локальной группе ; #include <stdio.h> # include <windows.h> #include <lm.h> #pragma comment( lib, "netapi32.lib" ) // подключаем сетевую библиотеку int main() { wchar_t s erver_name[256] = L"\\\\"; wchar_t group_name[GNLEN] ; LOCALGROUP_INFO_l *buf__ptr = NULL; NET_API_STATUS ret_status; printf ("Input server name: "■) ; // формируем имя сервера wscanf(L"%s", server_name + wcslen(server_name)); // имя сервера // имя локальной группы // адрес буфера для данных // код возврата из функции
Глава 38. Управление группами 729 printf("Input name for the local group: "); wscanf(L"%s", group_name); // вводим имя группы // получаем информацию о локальной группе ret_status = NetLocalGroupGetlnfо( server_name, // имя сервера group_name, // имя группы 1, // получаем имя группы и комментарий (LPBYTE*)&buf_ptr); // указатель на адрес буфера для данных // проверяем завершение функции if (ret_status != NERR_Success) { printf("Net local group get info failed.\n"); NetApiBufferFree(buf_ptr); // освобождаем буфер для данных return ret_status; } // выводим на консоль имя локальной группы и комментарий wprintf(L"Local group name: %s\n", buf_ptr->lgrpil_name); wprintf(L"Comment: %s\n", buf__ptr->lgrpil_comment); NetApiBufferFree(buf_ptr); // освобождаем буфер для данных return 0; } 38.3. Перечисление локальных групп Для перечисления локальных групп, учетные записи которых зарегистриро- ванны на заданном сервере, используется функция NetLocaiGroupEnum, которая имеет следующий прототип: NET_API_STATUS NetLocaiGroupEnum( LPCWSTR servername, // имя сервера DWORD level, // уровень информации LPBYTE *bufptr, // буфер для информации DWORD prefmaxlen, // длина буфера LPDWORD entriesread, // количество прочитанных структур LPDWORD totalentries, // общее количество групп
730 Часть XI. Управление безопасностью в Windows LPDWORD resumehandle // дескриптор для продолжения перечисления ); В случае успешного завершения функция NetLocaiGroupEnum возвращает значение NERR_success, а в случае неудачи возможны следующие коды завершения: □ error_access_denied — пользователю отказано в доступе; □ error_more_data — не все данные прочитаны; □ NERR_invaiidcomputer — неправильное имя компьютера; □ NERR_BufTooSmaii — буфер очень мал. Опишем параметры функции NetLocaiGroupEnum. Параметр servername должен указывать на строку с именем сервера, на котором будет исполняться функция. Эта строка должна иметь кодировку Unicode и начинаться с символов \\. Если функция должна исполняться на локальном компьютере, то этот параметр должен иметь значение null. Параметр level указывает тип структуры, которая содержит информацию о локальной группе. Этот параметр может принимать одно из следующих значений: □ 0 — используется структура типа localgroup_info_o; □ 1 — используется структура типа localgroup_info_i. В примере будет использована структура типа localgroup_info_i, тип которой приведен в разд. 38.1. Описание типа структуры localgroup_info_o может быть найдено в MSDN. Параметр bufptr должен содержать адрес указателя, в который система запишет адрес буфера с информацией о локальных группах. Память под буфер распределяется системой. Поэтому после прочтения информации из буфера эту память нужно освободить при помощи вызова функции NetApiBuf f erFree, которая была описана в разд. 37.2. Эту память нужно освободить также и в том случае, если функция вернула код ошибки ERROR_MORE_DATA. Параметр prefmaxien должен содержать предпочтительную длину буфера в байтах. Если памяти для данных не хватит, то функция вернет код ошибки error_more_data. В этом параметре можно установить значение max_preferred_length. Тогда система сама определит необходимую длину буфера для информации об учетных записях. Параметр entriesread должен указывать на переменную типа dword, в которую функция запишет количество локальных групп, информация о которых записана в буфер данных.
Глава 38. Управление группами 731 Параметр totaientries должен указывать на переменную типа dword, в которую функция запишет общее количество локальных групп, зарегистрированных на сервере. Параметр resume_handie должен указывать на переменную типа dword, в которую функция запишет дескриптор для продолжения перечисления локальных групп. При первом вызове функции этот параметр должен быть равен 0. Если продолжение чтения информации об учетных записях не предполагается, то в этом параметре нужно установить значение null. В листинге 38.3 приведена программа, которая перечисляет локальные группы, зарегистрированные на сервере, используя для этого функцию NetLocalGroupEnum. ^Листинг 38*3* Перечисление локальных групп #include <stdio.h> #include <windows.h> #include <lm.h> #pragma comment ( lib, "netapi32 . lib" ) // подключаем сетевую библиотеку int main() { DWORD entries_read; // количество элементов DWORD total_entries; // нумерация элементов LOCALGROUP_INFO_l *buf_ptr = NULL; // адрес буфера для данных NET_API_STATUS ret_status; // код возврата из функции // перечисляем локальные группы ret_status = NetLocalGroupEnum( NULL, // локальный компьютер 1, // получаем имя группы и комментарий (LPBYTE*)&buf_ptr, // указатель на адрес буфера для данных MAX_PREFERRED_LENGTH, // длина буфера по требованию &entries_read, // указатель на количество элементов &total_entries, // указатель на нумерацию элементов NULL); // проверяем завершение функции if (ret_status != NERR_Success)
732 Часть XI. Управление безопасностью в Windows printf("Net local group enumeration failed.\n"); NetApiBufferFree(buf_ptr); // освобождаем буфер для данных return ret_status; } printf("Local groups enumeration:\n"); // выводим на консоль имена локальных групп и комментарии for (DWORD i = 0; i < entries_read; ++i) { wprintf(L"Name: %s\n", buf_ptr[i].lgrpil_name); wprintf(L"Comment: %s\n", buf_ptr [i] . lgrpil_comment) ; } NetApiBufferFree(buf_ptr); // освобождаем буфер для данных return 0; } 38.4. Изменение информации о локальной группе Для изменения имени локальной группы и комментариев о локальной группе ИСПОЛЬЗуетСЯ функция NetLocaiGroupSetinfo, Которая Имеет следующий прототип: NET_API_STATUS NetLocaiGroupSetinfo( LPCWSTR servername, // имя сервера LPCWSTR groupname, // имя группы DWORD level, // уровень информации LPBYTE buf, // буфер с информацией LPDWORD parm_err // индексирование ошибки ); Отметим, что успешно эту функцию могут выполнить только пользователи, которые являются администраторами или операторами учетных записей. В случае успешного завершения функция NetLocaiGroupSetinfo возвращает значение NERR_success, а в случае неудачи возможны следующие коды завершения: П error_access_denied — пользователю отказано в доступе; □ error_invalid_parameter — неправильный параметр;
Глава 38. Управление группами 733 П error_no_such_alias — не существует указанной локальной группы; □ NERR_invaiidComputer — неправильное имя компьютера; □ NERR_NotPrimary — операция может выполняться только на первичном контроллере домена. Параметры функции NetLocaiGroupSetmfо имеют следующее назначение. Параметр servername должен указывать на строку с именем сервера, на котором будет исполняться функция. Эта строка должна иметь кодировку Unicode и начинаться с символов \\. Если функция должна исполняться на локальном компьютере, то этот параметр должен иметь значение null. Параметр groupname должен указывать на строку с именем группы, информация о которой изменяется. Эта строка должна иметь кодировку Unicode. Параметр level указывает тип структуры, которая содержит информацию о локальной группе. Этот параметр может принимать одно из следующих значений: ПО — используется структура типа localgroup_info_o; □ 1 — используется структура типа localgroup_info_i; □ 1002 — используется структура типа localgroup_info_1002. В примере будут использованы структуры типов localgroup_info_o и LOCALGROUP_INFO_l. ТИП Структуры LOCALGROUP_INFO_l бЫЛ ОПИСаН В разд. 38. /, поэтому здесь после описания функции приведем тип структуры localgroup_info_o и опишем назначение полей этой структуры. Описание типа структуры localgroup_info_1002 может быть найдено в MSDN. Параметр buf должен указывать на буфер с информацией о локальной группе. Информация должна храниться в структуре, тип которой задан параметром level. В переменную, на которую указывает параметр parm_err, функция NetLocaiGroupSetinfo помещает индекс первого поля в структуре с информацией о локальной группе, которое содержит неправильные данные и, как следствие, вызвало завершение фуНКЦИИ NetLocaiGroupSetinfo С ошибкой. В двойное слово, на которое указывает параметр, функция NetLocaiGroupSetinfo может записать одно из следующих значений: □ localgroup_name_parmnum — неправильное имя группы; □ localgroup_comment_parmnum — неправильный комментарий. В этом параметре может быть установлено значение null. В этом случае индекс поля с неправильными данными не возвращается. Теперь опишем структуру localgroup_info_o, которая имеет следующий тип: typedef struct _LOCALGROUP_INFO_0 { LPWSTR lgrpiO_name; // имя группы }LOCALGROUP_INFO_0, * PLOCALGROUP_INFO_0, * LPLOCALGROUP_INFO_0;
734 Часть XI. Управление безопасностью в Windows В единственном поле igrpio_name хранится указатель на строку с именем локальной группы. Длина строки не должна превышать gnlen байт, а сама строка должна иметь кодировку Unicode. В листинге 38.4 приведена программа, которая изменяет имя локальной ГруППЫ, ИСПОЛЬЗУЯ ДЛЯ ЭТОГО фуНКЦИЮ NetLocalGroupSetlnfo. #include <stdio.h> # include <windows.h> #include <lm.h> #pragma comment ( lib, "netapi32 .lib" ) // подключаем сетевую библиотеку int main() { wchar_t server_name[256] = L"\\\\"; // имя сервера wchar_t old_name[GNLEN]; // старое имя локальной группы wchar_t new_name[GNLEN]; // новое имя локальной группы wchar_t comment [MAXCOMMENTSZ ] ; / / новый комментарий LOCALGROUP_INFO_0 group_inf o_0; // информация об имени группы L0CALGR0UP_INF0_1 group_infо_1; // описание новой группы NET_API_STATUS ret_status; // код возврата из функции printf("Input a server name: "); // формируем имя сервера wscanf(L"%s", server_name + wcslen(server_name)); printf("Input a local group name: "); wscanf(L"%s", old_name); // вводим имя группы printf("Input a new name for the group: "); wscanf(L"%s", new_name); // вводим новое имя группы // устанавливаем адрес имени в структуру group_info_0.1grpi0_name = new_name;
Глава 38. Управление группами 735 II изменяем имя группы ret_status = NetLocalGroupSetlnfо( server_name/ // имя сервера old_name, // имя группы 0, // новое имя группы (LPBYTE)&group_info_0, // новая информация о группе NULL); // индексирования информации нет // проверяем завершение.функции if (ret_status != NERR_Success) { printfC'Net local group set name failed.\n"); return ret_status; } printf("Local group name was changed.\n"); printf("Input group comments: ") ; getwchar(); // очищаем поток _getws(comment); // читаем комментарии о пользователе // устанавливаем новый комментарий group_info_l.lgrpil_comment = comment; // изменяем описание группы ret_status = NetLocalGroupSetlnfо( server_name/ // имя сервера new_name, // имя группы 1, II получаем имя группы и комментарий (LPBYTE)&group_info_l, // новое описание группы NULL); // индексирования информации нет // проверяем завершение функции if (ret_status != NERR_Success) { printfC'Net local group set info failed.\n"); return ret_status; } printf("Local group comment was changed.\n"); return 0;
736 Часть XI. Управление безопасностью в Windows 38.5. Добавление членов локальной группы Для добавления новых членов в локальную группу используется функция NetLocalGroupAddMembers, Которая имеет следующий ПроТОТИП! NET_API_STATUS NetLocalGroupAddMembers( LPCWSTR servername, // имя сервера LPCWSTR groupname, // имя группы DWORD level, // уровень информации LPBYTE buf, // буфер с информацией DWORD totalentries // количество включаемых членов ); Отметим, что успешно эту функцию могут выполнить только те пользователи, которые являются администраторами или операторами учетных записей. В случае успешного завершения функция NetLocalGroupAddMembers ВОЗ- вращает значение NERR_success, а в случае неудачи возможны следующие коды завершения: □ NERR_GroupNotFound — группа не найдена; □ error_no_such_member — не существует членов, которые добавляются в группу; □ error_member_in_alias — некоторые члены уже являются членами группы; □ error_invalid_member — неправильный тип учетной записи добавляемого члена. Параметры функции NetLocalGroupAddMembers имеют следующее назначение. Параметр servername должен указывать на строку с именем сервера, на котором будет исполняться функция. Эта строка должна иметь кодировку Unicode и начинаться с символов \\. Если функция должна исполняться на локальном компьютере, то этот параметр должен иметь значение null. Параметр groupname должен указывать на строку с именем группы, в которую добавляются новые члены. Эта строка должна иметь кодировку Unicode. Параметр level указывает тип структуры, которая содержит информацию о добавляемых в группу членах. Этот параметр может принимать одно из следующих значений: ПО — используется структура типа localgroup_members_info_o; П 3- используется структура типа localgroup_members_info_3. Описание этих структур будет приведено после описания функции.
Глава 38. Управление группами 737 Параметр buf должен указывать на буфер с информацией о членах, которые добавляются в локальную группу. Информация должна храниться в структурах, тип которых задан параметром level. Параметр totaientries должен содержать размерность массива структур, на который указывает параметр buf. Теперь приведем типы структур, на которые может указывать параметр level. Структура localgroup_members_info_o имеет следующий тип: typedef struct _LOCALGROUP_MEMBERS_INFO_0 { PSID lgrmiO_sid; // указатель на идентификатор безопасности } LOCALGROUP_MEMBERS_INFO_0, * PLOCALGROUP_MEMBERS_INFO_0, *LPLOCALGROUP_MEMBERS_INFO_0 ; В единственном поле igrmio_sid этой структуры хранится указатель на идентификатор безопасности члена, который добавляется в локальную группу. Структура localgroup_members_info_3 имеет следующее определение: typedef struct _LOCALGROUP_MEMBERS_INFO_3 { LPWSTR lgrmi3_domainandname; // указатель на имя учетной записи } LOCALGROUP_MEMBERS_INFO_3 , * PLOCALGROUP_MEMBERS_INFO_3 , *LPLOCALGROUP_MEMBERS_INFO_3 ; В единственном поле igrmi3_domainandname этой структуры хранится указатель на строку с именем учетной записи, которая становится членом группы. Эта строка должна иметь кодировку Unicode, а имя должно быть задано В формате: <имя_домена>\<имя_учетной_записи>. Теперь перейдем к примерам. Сначала, в листинге 38.5, приведем программу, которая добавляет члена локальной группы по его имени, используя для ЭТОГО функцию NetLocalGroupSetlnf о. I ~\ '"" ~ "" ' " '' " 1 j Листинг 38.5. Добавление члена локальной группы по имени #include <stdio.h> # include <windows.h> #include <lm.h> #pragma comment( lib, "netapi32.lib" ) // подключаем сетевую библиотеку int main() { ' 24 Зак. 514
738 Часть XI. Управление безопасностью в Windows wchar_t server_name[256] = L"\\\\"; // имя сервера wchar_t group_name[GNLEN]; // имя локальной группы wchar_t user_name [UNLEN] ; // имя пользователя L0CALGR0UP_MEMBERS_INF0_3 member_info; // информация о члене группы NET_API_STATUS ret_status; // код возврата из функции printf("Input server name: "); // формируем имя сервера wscanf(L"%s", server_name + wcslen(server_name)); printf("Input a local group name: "); wscanf(L"%s", group__name); // вводим имя группы printf("Input a domain name: "); wscanf(L"%s", user_name) ; // вводим имя домена wcscat(user_name/ L"\\"); // присоединяем символ 'V printf("Input a user name: "); // вводим имя пользователя wscanf(L"%s", user_name + wcslen(user_name)); // устанавливаем информацию о пользователе member_inf о. lgrmi3_domainandname = user_name; // добавляем пользователя в локальную группу ret_status = NetLocalGroupAddMembers ( server_name/ // имя сервера group_name, // имя группы 3, // уровень информации (LPBYTE) bmembe^info, // имя учетной записи 1) ; // добавляем одного члена группы if (ret_status != NERR_Success) { printf("Net local group add members failed.\n"); return ret_status;
Глава 38. Управление группами 739 printfC'The member is added. \n"); return 0; } Теперь, в листинге 38.6, приведем программу, которая добавляет члена локальной группы по его идентификатору безопасности, используя для этого функцию NetLocaiGroupSetmfo. Заметим, что в этой программе идентификатор безопасности учетной записи определяется при помощи функции LookupAccountName, которая будет подробно рассмотрена в гл. 39, посвященной работе с идентификаторами безопасности. #ifndef UNICODE #define UNICODE #endif #include <stdio.h> #include <windows.h> #include <lm.h> :'^^^^^^^^^l #pragma comment ( lib, "netapi32 .lib" // подключаем сетевую библиотеку int main () { wchar_t server_name[256] = L"\\\\"; wchar_t group_name [GNLEN] ; wchar_t user_name [UNLEN] ; // имя сервера // имя локальной группы // имя пользователя LOCALGROUP_MEMBERS_INFO_0 member_infо; // информация о члене группы NET_API_STATUS ret_s ta tus; DWORD dwErrCode; // код возврата из функции DWORD dwLengthOfSID = 0; DWORD dwLengthOfDomainName PSID IpSID = NULL; // длина SID 0; // длина имени домена // указатель на SID
740 Часть XI. Управление безопасностью в Windows LPTSTR lpDomainName = NULL; SID_NAME_USE type_of_SID; // указатель на имя домена // тип учетной записи printf("Input server name: "); // формируем имя сервера wscanf(L"%s", server_name + wcslen(server_name)); printf("Input a local group name: "); wscanf(L"%s", group_name); // вводим имя группы printf("Input a user name: "); wscanf(L"%s", user_name); // вводим имя пользователя // определяем длину SID пользователя LookupAccountName ( NULL, user_name, NULL, &dwLengthOfSID, NULL, // ищем имя на локальном компьютере // имя пользователя // определяем длину SID // длина SID // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID); // тип учетной записи // проверяем, вернула ли функция длину SID if (dwLengthOfSID == 0) { dwErrCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n"/ dwErrCode); return dwErrCode; } // распределяем память для SID и имени домена IpSID = (PSID) new char[dwLengthOfSID]; lpDomainName = (LPTSTR) new char [dwLengthOfDomainName] ; // определяем SID и имя домена пользователя if(!LookupAccountName(
Глава 38. Управление группами 741 NULL, // ищем имя на локальном компьютере user_name, // имя пользователя IpSID, // определяем длину SID &dwLengthOfSID, // длина SID lpDomainName, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи dwErrCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode; } // распечатываем имя домена wprintf(L"%s\n", lpDomainName) // устанавливаем SID в информацию о члене группы member_info.lgrmiO_sid = IpSID; // добавляем пользователя в локальную группу ret_status = NetLocalGroupAddMembers( server_name, // имя сервера group_name, // имя группы О, // уровень информации (LPBYTE) &member_info, // информация о SID 1); // добавляем одного члена группы if (ret_status != NERR_Success) { printf("Net local group add members failed.\n"); return ret_status; } printf("The member is added.\n"); return 0;
742 Часть XI. Управление безопасностью в Windows 38.6. Установка членов локальной группы Под установкой членов локальной группы понимаются следующие действия. Задается список членов, которые должны входить в локальную группу. После этого из локальной группы исключаются все члены, которые не принадлежат этому списку, и включаются только члены из списка. Для установки членов ЛОКалЬНОЙ ГРУППЫ ИСПОЛЬЗуетСЯ функция NetLocalGroupSetMembers, которая имеет следующий прототип: NET_API_STATUS NetLocalGroupSetMembers ( LPCWSTR LPCWSTR DWORD LPBYTE DWORD servername, groupname, level, buf, totalentries // // // // // имя сервера имя группы уровень информации буфер с информацией количество структур в буфере Отметим, что успешно эту функцию могут выполнить только пользователи, которые являются администраторами или операторами учетных записей. В случае успешного завершения фуНКЦИЯ NetLocalGroupSetMembers ВОЗ- вращает значение NERR_success, а в случае неудачи возможны следующие коды завершения: □ NERR_GroupNotFound — группа не найдена; □ error_no_such_member — не существует членов, которые добавляются в группу; □ error_invalid_member — неправильный тип учетной записи добавляемого члена. Параметры функции NetLocalGroupAddMembers имеют следующее назначение. Параметр servername должен указывать на строку с именем сервера, на котором будет исполняться функция. Эта строка должна иметь кодировку Unicode и начинаться с символов \\. Если функция должна исполняться на локальном компьютере, то этот параметр должен иметь значение null. Параметр groupname должен указывать на строку с именем группы, в которую добавляются новые члены. Эта строка должна иметь кодировку Unicode. Параметр level указывает тип структуры, которая содержит информацию о членах, которые должны входить в локальную группу. Этот параметр может принимать одно из следующих значений: □ О — используется структура типа localgroup_members_info_o; □ 3 — используется структура типа localgroup_members_info_3. Типы этих структур были приведены в предыдущем разделе.
Глава 38. Управление группами 743 Параметр buf должен указывать на буфер с информацией о членах, которые должны входить в локальную группу. Информация должна храниться в структурах, тип которых задан параметром level. Параметр totaientries должен содержать размерность массива, на который указывает параметр buf. В листинге 38.7 приведена программа, которая устанавливает членов локальной Группы, ИСПОЛЬЗуЯ ДЛЯ ЭТОГО фуНКЦИЮ NetLocalGroupSetMembers. #include <stdio.h> # include <windows.h> #include <lm.h> #pragma comment ( lib, "netapi32. lib" ) // подключаем сетевую библиотеку int main() { wchar_t server_name[256] = L"\\\\"; // имя сервера wchar_t group_name [GNLEN] ; // имя локальной группы LOCALGROUP_MEMBERS_INFO_3 *member_inf о; // информация о членах группы DWORD n; // количество остающихся членов локальной группы NET_API_STATUS ret_status; // код возврата из функции printf("Input server name: "); // формируем имя сервера wscanf(L"%s", server_name + wcslen(server_name)); printf("Input a local group name: "); wscanf(L"%s", group_name); // вводим имя группы printf("Input number of members: "); scanf("%d", &n) ; // захватываем память для информации о пользователях member_inf о = new L0CALGR0UP_MEMBERS_INF0_3 [n] ; for (DWORD i = 0; i < n; ++i)
744 Часть XI. Управление безопасностью в Windows // захватываем память под имя пользователя member_info[i] . lgrmi3_domainandname = new wchar_t [256] ; printf("Input a domain name: "); // вводим имя домена wscanf (L" %s", member_inf о [ i ] . lgrmi3_domainandname) ; // присоединяем символ '\' - wcscat (member_inf о [ i ] . lgrmi3_domainandname, L" \ \") ; printf("Input a user name: "); // вводим имя пользователя wscanf (L" %s", member_inf о [i] . lgrmi3_domainandname + wcslen(member_info[i] .lgrmi3_domainandname) ) ; // вводим имя домена for (i = 0; i < n; ++i) wprintf (L"%s\n", member_infо [i] . lgrmi3_domainandname) ; // устанавливаем членов в локальной группе ret_status = NetLocalGroupSetMembers( server_name/ // имя сервера group_name, // имя группы 3, // уровень информации (LPBYTE)member_info, // информация о SID п) ; // количество остающихся членов группы if (ret_status != NERR_Success) { printfC'Net local group set members failed.\n"); return ret_status; } printf("The members are set.\n"); return 0;
Глава 38. Управление группами 745 38.7. Перечисление членов локальной группы Для перечисления членов локальной группы используется функция NetLocalGroupGetMembers, Которая Имеет следующий ПроТОТИП! NET_API_STATUS NetLocalGroupGetMembers( LPCWSTR LPCWSTR DWORD LPBYTE DWORD LPDWORD LPDWORD LPDWORD servername, localgroupname, level, *bufptr, prefmaxlen, entriesread, totalentries, resumehandle // // // // // // // // имя сервера имя локальной группы уровень информации буфер для информации длина буфера количество прочитанных структур общее количество структур дескриптор для продолжения чтения ); В случае успешного завершения функция NetLocalGroupGetMembers ВОЗ- вращает значение NERR_success, а в случае неудачи возможны следующие коды завершения: □ error_access_denied — пользователю отказано в доступе; □ NERR_invaiidComputer — неправильное имя компьютера; □ error_more_data — не все данные прочитаны; □ error_no_such_alias — не существует указанной локальной группы. Параметры функции NetLocalGroupGetMembers имеют следующее назначение. Параметр servername должен указывать на строку с именем сервера, на котором будет исполняться функция. Эта строка должна иметь кодировку Unicode и начинаться с символов \\. Если функция должна исполняться на локальном компьютере, то этот параметр должен иметь значение null. Параметр localgroupname должен указывать на строку с именем группы, члены которой перечисляются. Эта строка должна иметь кодировку Unicode. Параметр level указывает тип структуры, которая содержит информацию о перечисляемых членах группы. Этот параметр может принимать одно из следующих значений: □ 0 — используется структура типа localgroup_members_info_o; □ 1 — используется структура типа localgroup_members_info_i; □ 2 — используется структура типа localgroup_members_info_2; □ 3 — используется структура типа localgroup_members_info_3.
746 Часть XI. Управление безопасностью в Windows В примере, который приведен в листинге 38.8, будет использована структура localgroup_members_info_i, тип которой будет приведен после описания функции. Типы остальных структур можно найти в MSDN. Параметр bufptr должен содержать адрес указателя, в который система запишет адрес буфера с информацией о членах локальной группы. Память под буфер распределяется системой, поэтому после прочтения информации из буфера ЭТУ ПаМЯТЬ НУЖНО ОСВОбоДИТЬ При ПОМОЩИ фуНКЦИИ NetApiBufferFree, которая была описана в разд. 37.2. Эту память нужно освободить также и в том случае, если функция вернула код ошибки error_more_data. Параметр prefmaxien должен содержать предпочтительную длину буфера в байтах. Если памяти для данных не хватит, то функция вернет код ошибки error_more_data. В этом параметре можно установить значение max_preferred_length — тогда система сама определит необходимую длину буфера для информации об учетных записях. Параметр entriesread должен указывать на переменную типа dword, в которую функция запишет количество учетных записей, информация о которых записана в буфер данных. Параметр totaientries должен указывать на переменную типа dword, в которую функция запишет общее количество учетных записей, которые являются членами локальной группы. Параметр resume_handie должен указывать на переменную типа dword, в которую функция запишет дескриптор для продолжения перечисления учетных записей членов локальной группы. При первом вызове функции этот параметр должен содержать значение 0. Если продолжение чтения информации об учетных записях не предполагается, то в этом параметре нужно установить значение null. Теперь опишем тип структуры localgroup_members_info_i, которая будет использоваться в листинге 38.8. typedef struct _LOCALGROUP_MEMBERS_INFO_l { PSID lgrmil_sid; // указатель на идентификатор безопасности SID_NAME_USE lgrmil_sidusage; // тип учетной записи LPWSTR lgrmil_name; // имя учетной записи } LOCALGROUP_MEMBERS_INFO_l, *PLOCALGROUP_MEMBERS_INFO_l, *LPLOCALGROUP_MEMBERS_INFO_l ; Поля этой структуры имеют следующее назначение. Поле igrmii_sid предназначено для хранения указателя на идентификатор безопасности члена локальной группы. В поле igrmii_sidusage хранится тип учетной записи члена локальной группы. Этот тип может принимать одно из следующих значений, принадлежащих перечислимому типу sid_name_use: □ sidTypeUser — учетная запись пользователя;
Глава 38. Управление группами 747 П sidTypeGroup — учетная запись группы; □ sidTypeweiiKnownGroup — учетная запись предопределенной фуппы; □ sidTypeDeietedAccount — учетная запись удалена; □ sidTypeUnknown — тип учетной записи не определен. В поле igrmii_name хранится указатель на имя учетной записи. Это имя не должно содержать имя домена. В листинге 38.8 приведена профамма, которая перечисляет членов локальной фуППЫ, ИСПОЛЬЗуЯ ДЛЯ ЭТОГО фуНКЦИЮ NetLocalGroupGetMembers. #include <stdio.h> #include <windows.h> #include <lm.h> #pragma comment ( lib, "netapi32.lib" ) // подключаем сетевую библиотеку // имя сервера // имя локальной группы int main() { wchar_t server_name[256] = L"\\\\"; wchar_t group_name [GNLEN] ; // информация о членах группы L0CALGR0UP_MEMBERS_INF0_1 *member_info = NULL; DWORD entries_read; // прочитанное количество членов группы DWORD total_entries; // общее количество членов группы NET_API_STATUS ret_s ta tus ; // код возврата из функции printf ("Input server name: "); // формируем имя сервера wscanf (L"%s", server_name + wcslen(server_name) ) ; printf("Input a local group name: "); wscanf(L"%s", group_name); // вводим имя группы // получаем информацию о членах локальной группы ret_status = NetLocalGroupGetMembers ( server_name, // имя сервера
748 Часть XI. Управление безопасностью в Windows group_name, // имя группы 1, // уровень информации (LPBYTE*)&member_info, // информация о пользователях MAX_PREFERRED_LENGTH, // читаем информацию о всех членах группы &entries_read, // прочитанное количество членов группы &total_entries, // полное количество членов группы NULL); // читаем в один прием if (ret_status != NERR_Success) { printf("Net local group get members failed.\n"); NetApiBufferFree(member_info); // освобождаем буфер return ret_status; } // распечатываем пользователей for (DWORD i = 0; i < entries_read; ++i) wprintf (L" The group member: %s\n"/ member_info[i] . lgrmil_name) ; NetApiBuf ferFree (member_inf о) ; // освобождаем буфер return 0; } 38.8. Удаление членов локальной группы Для удаления членов локальной группы используется функция NetLocalGroupDelMembers, которая Имеет следующий проТОТИП! NET_API_STATUS NetLocalGroupDelMembers( LPCWSTR servername, // имя сервера LPCWSTR groupname, // имя группы DWORD level, // уровень информации LPBYTE buf, // буфер с информацией DWORD totalentries // количество структур в буфере ); Отметим, что успешно эту функцию могут выполнить только пользователи, которые являются администраторами или операторами учетных записей.
Глава 38. Управление группами 749 В случае успешного завершения функция NetLocalGroupDelMembers ВОЗ- вращает значение NERR_success, а в случае неудачи возможны следующие коды завершения: □ NERR_GroupNotFound — группа не найдена; □ error_no_such_member — не существует членов, которые удаляются из группы; □ error_member_in_alias — некоторые учетные члены не являются членами локальной группы, поэтому из группы не был удален ни один член. Параметры функции NetLocalGroupDelMembers имеют следующее назначение. Параметр servername должен указывать на строку с именем сервера, на котором будет исполняться функция. Эта строка должна иметь кодировку Unicode и начинаться с символов \\. Если функция должна исполняться на локальном компьютере, то этот параметр должен иметь значение null. Параметр groupname должен указывать на строку с именем группы, из которой удаляются члены. Эта строка должна иметь кодировку Unicode. Параметр level указывает тип структуры, которая содержит информацию о членах, которые должны быть удалены из локальной группы. Этот параметр может принимать одно из следующих значений: □ 0 — используется структура типа localgroup_members_info_o; □ 3 — используется структура типа localgroup_members_info_3; Типы этих структур были приведены в разд. 38.5. Параметр buf должен указывать на буфер с информацией о членах, которые должны быть удалены из локальной группы. Информация должна храниться в структурах, тип которых задан параметром level. Параметр totaientries должен содержать размерность массива структур, на который указывает параметр buf. В листинге 38.9 приведена программа, которая удаляет члена локальной ГРУППЫ ПО ИМеНИ При ПОМОЩИ фуНКЦИИ NetLocalGroupDelMembers. [ Листинг 38А Удаление члена локальной группы по wmw #include <stdio.h> #include <windows.h> #include <lm.h> #pragma comment ( lib, "netapi32. lib" ) // подключаем сетевую библиотеку int main()
750 Часть XI. Управление безопасностью в Windows { wchar_t server_name[256] =L"\\\\"; // имя сервера wchar_t group_name [GNLEN] ; // имя локальной группы wchar_t user_name [UNLEN] ; // имя пользователя L0CALGR0XJP_MEMBERS_INF0_3 member_info; // информация о члене группы NET_API_STATUS ret_status; // код возврата из функции printf("Input server name: ") ; // формируем имя сервера wscanf(L"%s", server_name + wcslen(server_name)); printf("Input a local group name: "); wscanf(L"%s", group_name); // вводим имя группы printf("Input a domain name: "); wscanf(L"%s", user_name); // вводим имя домена wcscat(user_name, L"\\"); // присоединяем символ '\' printf("Input a user name: "); // вводим имя пользователя wscanf(L"%s", user_name + wcslen(user_name)); // устанавливаем информацию о пользователе member_inf о. lgrmi3_domainandname = user_name; // удаляем пользователя из локальной группы ret_status = NetLocalGroupDelMembers( server_name, // имя сервера group__name, // имя группы 3, // уровень информации (LPBYTE)&member_info, // информация о SID 1); // добавляем одного члена группы if (ret_status != NERR_Success) { printf("Net local group del members.\n"); return ret_status;
Глава 38. Управление группами 751 printf("The member is deleted.\n" return 0; } В листинге 38.10 приведена профамма, в которой член локальной фуппы удаляется по идентификатору безопасности при помощи функции NetLocaiGroupDeiMembers. При этом отметим, что идентификатор безопас- НОСТИ Пользователя определяется при ПОМОЩИ фуНКЦИИ LookupAccountName, которая будет подробно рассмотрена в гл. 39, посвященной работе с идентификаторами безопасности. № #ifndef UNICODE #def ine UNICODE #endif #include <stdio.h> # include <windows.h> #include <lm.h> #pragma comment ( lib, "netapi32. lib" ) // подключаем сетевую библиотеку int main () { wchar_t server_name[256] = L"\\\\" wchar_t group_name[GNLEN]; wchar_t user_name [UNLEN] ; // имя сервера // имя локальной группы // имя пользователя LOCALGROUP_MEMBERS_INFO_0 member_info; // информация о члене группы NET_API_STATUS ret_status; DWORD dwErrCode; // код возврата из функции DWORD dwLengthOfSID = 0; DWORD dwLengthOfDomainName = 0; PSID IpSID = NULL; // длина SID // длина имени домена // указатель на SID
752 Часть XI. Управление безопасностью в Windows LPTSTR lpDomainName = NULL; // указатель на имя домена SID_NAME_USE type_of_SID; // тип учетной записи printf("Input server name: "); // формируем имя сервера ws can f(L"% s", s erver_name + wcs1en(s erver_name)); printf("Input a local group name: "); wscanf(L"%s", group_name); // вводим имя группы printf("Input a user name: "); wscanf(L"%s", user_name); // вводим имя пользователя // определяем длину SID пользователя LookupAccountName ( NULL, // ищем имя на локальном компьютере user_name, // имя пользователя NULL, // определяем длину SID &dwLengthOfSID, // длина SID NULL, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID); // тип учетной записи // проверяем, вернула ли функция длину SID if (dwLengthOfSID == 0) { dwErrCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode; } // распределяем память для SID и имени домена IpSID = (PSID) new char[dwLengthOfSID]; lpDomainName = (LPTSTR) new char [dwLengthOfDomainName] ; // определяем SID и имя домена пользователя if(!LookupAccountName( NULL, // ищем имя на локальном компьютере
Глава 38. Управление группами 753 user_name, // имя пользователя IpSID, // определяем длину SID &dwLengthOfSID, // длина SID lpDomainName, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи dwErrCode = GetLastError(); printf{"Lookup account name failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode; } // распечатываем имя домена wprintf(L"%s\n", lpDomainName); // устанавливаем SID в информацию о члене группы member_info.lgrmiO_sid = IpSID; // удаляем члена локальной группы ret_status = NetLocalGroupDelMembers( server_name, // имя сервера group_name, // имя группы О, // уровень информации (LPBYTE)&member_info, // информация о SID 1); // добавляем одного члена группы if (ret_status != NERR_Success) { printf("Net local group del members failed.\n"); return ret_status; } printf("The member is deleted.\n"); return 0;
754 Часть XI. Управление безопасностью в Windows 38.9. Удаление локальной группы Для удаления учетной записи локальной группы из базы данных менеджера учетных записей используется функция NetLocalGroupDel, которая имеет следующий прототип: NET_API_STATUS NetLocalGroupDel ( LPCWSTR servername, // имя сервера LPCWSTR groupname // имя группы ); Отметим, что успешно эту функцию могут выполнить только пользователи, которые являются администраторами или операторами учетных записей. В случае успешного завершения функция NetLocalGroupDel возвращает значение NERR_Success, а в случае неудачи возможны следующие коды завершения: □ error_access_denied — пользователю отказано в доступе; □ NERR_invalidcomputer — неправильное имя компьютера; □ NERR_NotPrimary — операция может выполняться только на первичном контроллере домена; □ NERR_GroupNotFound — группа не найдена; □ error_no_such_alias — группа не существует. Параметры функции NetLocaiGroupDeiMembers имеют следующее назначение. Параметр servername должен указывать на строку с именем сервера, на котором будет исполняться функция. Эта строка должна иметь кодировку Unicode и начинаться с символов \\. Если функция должна исполняться на локальном компьютере, то этот параметр должен иметь значение null. Параметр groupname должен указывать на строку с именем группы, которая удаляется из базы данных менеджера учетных записей. Эта строка должна иметь кодировку Unicode. В листинге 38.11 приведена профамма, которая удаляет локальную группу При ПОМОЩИ фуНКЦИИ NetLocalGroupDel. I Листинг 38.11. Удаление локальной группы #include <stdio.h> # include <windows.h> #include <lm.h> #pragma comment ( lib, "netapi32. lib" ) // подключаем сетевую библиотеку
Глава 38. Управление группами 755 int main () { wchar_t server_name [256] = L" \ \ \ \"; / / имя сервера wchar_t group_name[GNLEN] ; // имя удаляемой локальной группы NET_API_STATUS ret_status; // код возврата из функции printf("Input server name: "); // формируем имя сервера wscanf(L"%s", server_name + wcslen(server_name)); printf("Input a name of the local group: "); wscanf(L"%s", group_name); // вводим имя группы // удаляем локальную группу ret_status = NetLocalGroupDel(server_name, group_name); // проверяем код завершения if (ret_status != NERR_Success) { printfC'Net local group del failed.\n"); printf("Error code: %d\n", ret_status); return ret_status; } printf("The local group is deleted.\n"); return 0;
Глава 39 Работа с идентификаторами безопасности 39.1. Структура идентификатора безопасности Как было уже сказано в разд. 36.5, идентификатор безопасности (SID) является бинарным представлением учетной записи, которое используется операционной системой для управления доступом к объектам. В операционных системах Windows идентификаторы безопасности представляются структурой следующего типа: typedef struct _SID { BYTE Revision; // версия SID BYTE SubAuthorityCount; // количество относительных идентификаторов SID_IDENTIFIER_AUTHORITY IdentifierAuthority; // авторизация DWORD SubAuthority[ANYSIZE_ARRAY]; // относительные идентификаторы } SID; Отсюда видно, что структура sid имеет переменную длину, которая зависит от количества относительных идентификаторов учетной записи. Идентификатор безопасности должен содержать идентификатор учетной записи, который ХраНИТСЯ В ПОЛе IdentifierAuthority И, ПО Крайней Мере, ОДИН OTHO- сительный идентификатор учетной записи. Идентификатор учетной записи, который определяет ее уровень авторизации или, другими словами, принадлежность учетной записи какому-то множеству учетных записей, задается структурой типа: typedef struct _SID_IDENTIFIER_AUTHORITY { BYTE Value[6]; // значение идентификатора учетной записи } SID.IDENTIFIER.AUTHORITY, *PSID_IDENTIFIER_AUTHORITY;
Глава 39. Работа с идентификаторами безопасности 757 Например, идентификатор безопасности, указывающий на локальный уровень авторизации, определяется следующим образом: #define SECURITY_LOCAL_SID_AUTHORITY {0,0,0,0,0,2} В операционных системах Windows предопределены следующие идентификаторы безопасности: □ SECURITY_NULL_SID_AUTHORITY — 0; □ SECURITY_WORLD_SID_AUTHORITY — 1; □ SECURITY_LOCAL_SID_AUTHORITY — 2; □ SECURITY_CREATOR_SID_AUTHORITY - 3; □ SECURITY_NON_UNIQUE_AUTHORITY — 4; □ SECURITY_NT_AUTHORITY — 5. Каждый из них, естественно, определяется как массив байт. 39.2. Создание идентификатора безопасности В ЭТОМ разделе будут рассмотрены следующие фуНКЦИИ: GetSidLengthRequired, InitializeSid, GetSidSubAuthority, AiiocateAndinitiaiizeSid, IsValidSid, которые используются при создании идентификаторов безопасности. Прежде чем переходить к описанию этих функций, кратко расскажем о порядке создания идентификатора безопасности. Так как структура sid имеет переменную длину, зависящую от количества относительных идентификаторов учетной записи, то, прежде чем создавать идентификатор безопасности, нужно определиться с количеством относительных идентификаторов, а затем зарезервировать память для структуры sid. Для определения длины структуры sid, учитывая количество относительных идентификаторов, используется фуНКЦИЯ GetSidLengthRequired. После ЭТОГО резервируется Па- мять для структуры sid. Сама структура sid инициализируется при помощи функции InitializeSid, причем эта функция инициализирует только первых три члена структуры sid. Относительные идентификаторы должны инициализироваться отдельно, вручную. Для вычисления адреса относительного идентификатора безопасности по его индексу в структуре sid используется функция GetSidSubAuthority. После инициализации sid нужно проверить его структуру — для этого используется функция isvaiidsid. Все вышеперечисленные действия можно также выполнить одной функцией AiiocateAndinitiaiizeSid, КОТОрая резервирует память ДЛЯ структуры ТИПа sid и инициализирует ее. Однако в этом случае зарезервированную память НУЖНО ОСВОбоЖДаТЬ, ИСПОЛЬЗУЯ фуНКЦИЮ FreeSid.
758 Часть XI. Управление безопасностью в Windows Для определения длины буфера памяти, который потребуется для создания Идентификатора безопасности, ИСПОЛЬЗуеТСЯ фуНКЦИЯ GetSidLengthRequired, которая имеет следующий прототип: DWORD GetSidLengthRequired( UCHAR nSubAuthorityCount // количество относительных идентификаторов ); Единственный параметр этой функции должен содержать количество относительных идентификаторов, которые предполагается записать в идентификатор безопасности. Функция GetSidLengthRequired всегда успешно завершает свою работу и возвращает количество байт, требуемых для хранения идентификатора безопасности. Пример использования функции GetSidLengthRequired будет приведен далее, в программе из листинга 39.1, которая инициализирует идентификатор безопасности при помощи функции InitializeSid. Для инициализации идентификатора безопасности используется функция InitializeSid, которая имеет следующий прототип: BOOL InitializeSid( PSID pSid, // указатель на идентификатор безопасности // указатель на идентификатор учетной записи PSID_IDENTIFIER_AUTHORITY pldent i f ierAuthori ty, BYTE nSubAuthorityCount // количество относительных идентификаторов ); В случае успешного завершения функция InitializeSid возвращает ненулевое значение, а в случае неудачи — false. При неудаче код ошибки можно определить посредством вызова функции GetLastError. Параметры функции InitializeSid имеют следующее назначение. Параметр psid должен указывать на структуру типа sid, которая будет инициализироваться. Память под структуру должна быть зарезервирована вызывающей программой. Параметр pidentifierAuthority должен указывать на структуру типа sid_identifier_authority, которая определяет идентификатор учетной записи. Значение, заданное в этой структуре, будет записано в идентификатор безопасности, на который указывает параметр psid. Параметр nSubAuthorityCount должен указывать на количество относительных идентификаторов учетной записи, которые будут записаны в идентификатор безопасности, соответствующий этой учетной записи. Функция InitializeSid записывает это значение в идентификатор безопасности. Сами значения относительных идентификаторов должны быть записаны в идентификатор безопасности отдельно. Для этого может использоваться функция GetSidSubAuthority.
Глава 39. Работа с идентификаторами безопасности 759 Для определения адреса относительного идентификатора учетной записи В Идентификаторе беЗОПаСНОСТИ ИСПОЛЬЗуеТСЯ фунКЦИЯ GetSidSubAuthority, которая имеет следующий прототип: PDWORD GetSidSubAuthority( PSID pSid, // указатель на идентификатор безопасности DWORD nSubAuthority // номер относительного идентификатора ); В случае успешного завершения функция GetSidSubAuthority возвращает указатель на относительный идентификатор безопасности. Функция GetSidSubAuthority может завершиться неудачей в одном из двух случаев, а именно, если идентификатор безопасности имеет неправильную структуру или индекс относительного идентификатора выходит за пределы допустимых Значений. В случае Неудачного завершения фуНКЦИЯ GetSidSubAuthority вернет неопределенное значение. При неудаче код ошибки можно определить Посредством ВЫЗОВа фуНКЦИИ GetLastError. Параметры функции GetSidSubAuthority имеют следующее назначение. Параметр psid должен указывать на структуру типа sid, в которой будет определяться адрес относительного идентификатора безопасности. Параметр nSubAuthorityCount должен указывать на номер относительного идентификатора, адрес которого будет определяться. Для проверки того, правильно ли в созданном идентификаторе безопасности установлена версия и не выходит ли количество относительных идентификаторов за допустимые пределы, используется функция isvaiidsid, которая имеет следующий прототип: BOOL IsValidSid( PSID pSid // указатель на идентификатор безопасности ); Единственный параметр этой функции должен указывать на идентификатор безопасности, который проверяется на правильность версии и допустимого количества относительных идентификаторов. В случае успешного завершения функция initiaiizesid возвращает значение true, а в случае неудачи — false. При неудаче код ошибки можно определить посредством вызова ФУНКЦИИ GetLastError. В листинге 39.1 приведена программа, в которой инициализируется идентификатор безопасности, используя функцию initiaiizesid. Причем предварительно При ПОМОЩИ фуНКЦИИ GetSidLengthRequired Определяется ДЛИНа идентификатора безопасности. После инициализации идентификатора безопасности при помощи функции GetSidSubAuthority определяется индекс относительного идентификатора, после чего устанавливается его значение. Затем проверяется правильность идентификатора безопасности при помощи ВЫЗОВа фуНКЦИИ IsValidSid.
760 Часть XI. Управление безопасностью в Windows Ь АистйнГМ?* ^йициаШзацйя идентификатора безопасности L &. Lz.te..-..^ ±. *..„.>. sljl & ; j #include <stdio.h> # inc lude <windows . h> int main () { DWORD dwSidLength; // длина памяти для SID SID *pSid = NULL; // указатель на SID // указатель на относительный идентификатор учетной записи DWORD *pSuhAuthority = NULL; // идентификатор учетной записи SID_IDENTIFIER_AUTHORITY sia = SECURITY_LOCAL_SID_AUTHORITY; // определеем длину SID с одним относительным идентификатором учетной записи dwSidLength = GetSidLengthRequired(l); printf("SID length: %u\n", dwSidLength); // захватываем память под SID pSid = (SID*) new UCHAR [dwSidLength] ; // инициализируем SID if(!InitializeSid( pSid, // указатель на SID &sia, // идентификатор учетной записи 1) ) // количество относительных идентификаторов { printf( "Initialized SID failed.\n"); return 1; } printf("SID is initialized.\n"); // определяем указатель на относительный идентификатор pSuhAuthority = GetSidSubAuthority(pSid, 0); // устанавливаем значение относительного идентификатора
Глава 39. Работа с идентификаторами безопасности 761 *pSubAuthority = SECURITY_LOCAL_RID; // проверяем достоверность SID if(!IsValidSid(pSid)) { printf( "The SID is not valid.\n"); return 1; } printfC'The SID is valid.\n"); // освобождаем SID delete[] pSid; return 0; } Распределить память под идентификатор безопасности и инициализировать его ПОЛЯ МОЖНО ОДНОЙ функцией AllocateAndlnitializeSid, КОТОрая имеет следующий прототип: BOOL AllocateAndlnitializeSid ( PSID_IDENTIFIER_AUTHORITY pidentifierAuthority, BYTE nSubAuthorityCount, DWORD nSubAuthorityO, DWORD nSubAuthor i ty1, DWORD nSubAuthority2, DWORD nSubAuthority3, DWORD nSubAuthority4, DWORD nSubAuthority5, DWORD nSubAuthority6, DWORD nSubAuthority77 PSID *pSid ); В случае успешного завершения функция AllocateAndlnitializeSid ВОЗ- вращает ненулевое значение, а в случае неудачи — false. При неудаче код ошибки можно определить посредством вызова функции GetLastError. Параметры функции AllocateAndlnitializeSid имеют следующее назначение. Параметр pidentifierAuthority должен указывать на структуру типа sid_identifier_authority, которая определяет идентификатор учетной
762 Часть XI. Управление безопасностью в Windows записи. Значение, заданное в этой структуре, будет записано в создаваемый идентификатор безопасности. Параметр nSubAuthorityCount должен содержать количество относительных идентификаторов учетной записи, которые будут записаны в идентификатор безопасности, соответствующий этой учетной записи. В параметрах nSubAuthorityO, ..., nSubAuthority7 ДОЛЖНЫ быть установлены значения относительных идентификаторов, которые функция запишет в идентификатор безопасности учетной записи. В идентификатор безопасности будут записаны ТОЛЬКО первые nSubAuthorityCount относительных идентификаторов. Параметр psid должен содержать адрес переменной, в которую функция AiiocateAndinitiaiizeSid поместит указатель на созданную ей структуру типа sid. Память под структуру резервируется самой функцией. После окончания работы с идентификатором безопасности, созданным функцией AiiocateAndinitiaiizeSid, ЭТу память НуЖНО ОСВОбоДИТЬ Посредством вызова функции Freesid, которая имеет следующий прототип: PVOID FreeSid( PSID pSid // указатель на идентификатор безопасности ); В листинге 39.2 приведена программа, в которой идентификатор безопасности создается при ПОМОЩИ функции AiiocateAndinitiaiizeSid. #include <stdio.h> #include <windows.h> int main() { int n; // количество идентификаторов // указатель на идентификатор учетной записи SID_IDENTIFIER_AUTHORITY *sia; DWORD dwSubAuthority; // относительный идентификатор учетной записи PSID pSid = NULL; // указатель на SID // предопределенные идентификаторы учетной записи SID_IDENTIFIER_AUTHORITY sO = SECURITY.NULL.SID.AUTHORITY; SID_IDENTIFIER_AUTHORITY si = SECURITY_WORLD_SID_AUTHORITY; SID_IDENTIFIER_AUTHORITY s2 = SECURITY_LOCAL_SID_AUTHORITY;
Глава 39. Работа с идентификаторами безопасности 763 SID_IDENTIFIER_AUTHORITY s5 = SECURITY_NT_AUTHORITY; // ввод идентификатора безопасности printf("Choose one of the predefined SID.\n"); printf("Null SID:\t\tO\n"); printf("World SID:\t\tl\n") . printf("Local SID:\t\t2\n"); printf("Anonimous logon SID:\t5\n\n"); printf("SID = "); scanf("%d", &n); // читаем идентификатор учетной записи switch (n) { case 0: sia = &s0; dwSuhAuthority = SECURITY_NULL_RID ; break; case 1: sia = &sl; dwSuhAuthority = SECURITY_NULL_RID ; break; case 2: sia = &s2; dwSuhAuthority = SECURITY_NULL_RID ; break; case 5: sia = &s5; dwSuhAuthority = SECURITY_NULL_RID ; break; default: printf("Unknown uiniversal SID.\n"); return 1; } // создаем SID if(!AllocateAndInitializeSid( sia, // идентификатор учетной записи 1, // количество относительных идентификаторов учетной записи
764 Часть XL Управление безопасностью в Windows dwSubAuthority, // первый RID О, О, О, О, О, О, О, // остальные RID равны О &pSid) ) { DWORD dwRetCode = GetLastError(); printf( "Allocate and initialize sid failed %u\n", dwRetCode); return dwRetCode; } printf("The SID is allocated and initialized.\n"); // освобождаем SID FreeSid(pSid); printf("The SID is freed.\n"); return 0; } 39.3. Определение учетной записи по идентификатору безопасности Для определения имени учетной записи по ее идентификатору безопасности используется функция LookupAccountsid, которая имеет следующий прототип: BOOL LookupAccountsid( LPCSTR IpSystemName, // имя компьютера PSID pSid, // указатель на SID LPSTR lpName, // имя учетной записи LPDWORD cbName, // длина имени учетной записи LPSTR ReferencedDomainName, // имя домена LPDWORD cbReferencedDomainName, // длина имени домена PSID_NAME_USE peUse // тип идентификатора безопасности ); В случае успешного завершения функция LookupAccountsid возвращает ненулевое значение, а в случае неудачи — false. При неудаче код ошибки МОЖНО определить ПОСреДСТВОМ ВЫЗОВа фуНКЦИИ GetLastError.
Глава 39. Работа с идентификаторами безопасности 765 Функция LookupAccountsid кроме имени учетной записи возвращает также и имя домена, в котором найдена учетная запись. Поиск учетной записи выполняется в следующем порядке. Сначала проверяются предопределенные общеизвестные учетные записи. Затем проверяются учетные записи, зарегистрированные на локальном компьютере. После этого проверяются учетные записи, зарегистрированные в первичном домене. И, в последнюю очередь, проверяются учетные записи из доверительных доменов. Параметры функции LookupAccountsid имеют следующее назначение. Параметр lpSystemName должен указывать на строку с именем компьютера, на котором ищется имя учетной записи. Если этот параметр имеет значение null, то учетная запись ищется на локальном компьютере. Параметр psid должен указывать на структуру типа sid, которая содержит идентификатор безопасности учетной записи, имя которой нужно определить. Параметр lpName должен указывать на буфер, в который функция LookupAccountsid запишет имя учетной записи, соответствующей идентификатору безопасности, заданному параметром psid. Параметр cbName должен указывать на переменную типа dword, которая содержит длину буфера для имени учетной записи, на который указывает параметр lpName. Если длина буфера недостаточна, то выполнение функции закончится неудачей, а в двойное слово, на которое указывает параметр cbName, функция запишет необходимую длину буфера. Параметр Ref erencedDomainName ДОЛЖен содержать адрес буфера, В Который функция запишет имя домена, в котором найдена учетная запись. Если установить значение этого параметра в null, to функция запишет в двойное СЛОВО, На КОТОрое указывает Параметр cbReferencedDomainName, необходимую длину буфера для имени домена. Отметим, что строка с именем домена имеет кодировку Unicode. Параметр cbReferencedDomainName ДОЛЖен указывать на переменную ТИПа dword, которая содержит длину буфера для имени домена, на который указывает параметр ReferencedDomainName. ЕСЛИ ДЛИНа буфера Недостаточна, ТО выполнение функции закончится неудачей, а в переменную, на которую указывает параметр cbReferencedDomainName, функция запишет необходимую длину буфера. Параметр peuse должен указывать на переменную типа перечисление sid_name_use, в которую функция запишет тип учетной записи. Типы учетных записей определяются как значения этого перечисления, которое определено следующим образом: typedef enum _SID_NAME_USE { SidTypeUser = 1, II SID пользователя SidTypeGroup, // SID группы SidTypeDomain, // SID домена
766 Часть XL Управление безопасностью в Windows SidTypeAlias, // SID синонима SidTypeWellKnownGroup, // SID предопределенной группы SidTypeDeletedAccount, // SID удаленной учетной записи SidTypelnvalid, // недействительный SID SidTypeUnknown, // неизвестный SID SidTypeComputer // SID компьютера } SID_NAME_USE, *PSID_NAME_USE; В листинге 39.3 приведена программа, которая определяет имя учетной записи и имя домена по идентификатору безопасности учетной записи, используя ДЛЯ ЭТОГО фунКЦИЮ LookupAccountSid. #ifndef UNICODE #define UNICODE #endif #include <stdio.h> # inc lude <windows. h> #include <lm.h> int main () { wchar_t user_name [UNLEN] ; // имя пользователя HANDLE hProcess; // дескриптор процесса HANDLE hTokenHandle; // дескриптор маркера доступа DWORD dwErrCode; // код возврата TOKEN_OWNER *lpTokenOwner = NULL; // буфер для информации DWORD dwLength= 0; // длина буфера DWORD dwLengthOfUserName = UNLEN; DWORD dwLengthOfDomainName = 0; LPTSTR lpDomainName = NULL; SID_NAME_USE type_of_SID; // длина имени учетной записи // длина имени домена // указатель на имя домена // тип учетной записи // получаем маркер доступа процесса
Глава 39. Работа с идентификаторами безопасности 767 hProcess = GetCurrentProcess(); // получить маркер доступа процесса if (!OpenProcessToken( hProcess, // дескриптор процесса TOKEN_QUERY, // запрос информации из маркера &hTokenHandle)) // дескриптор маркера { dwErrCode = GetLastError(); printf( "Open process token failed: %u\n", dwErrCode); return dwErrCode; } // определяем длину SID if (!GetTokenlnformation( hTokenHandle, // дескриптор маркера доступа TokenOwner, // получаем SID владельца NULL, // нужна длина буфера О, // пока длина равна О SdwLength)) // для длины буфера { dwErrCode = GetLastError(); if (dwErrCode == ERROR_INSUFFICIENT_BUFFER) // захватываем память под SID lpTokenOwner = (TOKEN_OWNER*)new char[dwLength]; else { // выходим из программы printf( "Get token information for length failed: %u\n" dwErrCode); return dwErrCode; } // определяем имя владельца if (!GetTokenlnformation( hTokenHandle, // дескриптор маркера доступа TokenOwner, // нужен SID маркера доступа lpTokenOwner, // адрес буфера для SID dwLength, // длина буфера
768 Часть XI. Управление безопасностью в Windows &dwLength)) // длина буфера { dwErrCode = GetLastError(); printf( "Get token information failed: %u\n", dwErrCode); return dwErrCode; } // определяем длину имени домена if(!LookupAccountSid( NULL, // ищем на локальном компьютере lpTokenOwner->Owner, // указатель на SID user_name, // имя пользователя &dwLengthOfUserName, // длина имени пользователя lpDomainName, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwErrCode = GetLastError(); if (dwErrCode == ERROR_INSUFFICIENT_BUFFER) // распределяем память под имя домена lpDomainName = (LPTSTR) new wchar_t [dwLengthOfDomainName] , else { printf("Lookup account SID for length failed.\n"); printf("Error code: %d\n"/ dwErrCode); return dwErrCode; } } // определяем имя учетной записи по SID if(!LookupAccountSid( NULL, // ищем на локальном компьютере lpTokenOwner->Owner, // указатель на SID user_name, // имя пользователя &dwLengthOfUserName, // длина имени пользователя lpDomainName, // определяем имя домена
Глава 39. Работа с идентификаторами безопасности 769 &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwErrCode = GetLastError(); printf("Lookup account SID failed.\n"); printf("Error code: %d\n"/ dwErrCode); return dwErrCode; } wprintf(L"User name: %s\n", user_name) ; wprintf(L"Domain name: %s\n"/ lpDomainName); // освобождаем память delete[] lpDomainName; delete[] lpTokenOwner; return 0; } Отметим, что в этой программе используются функции для работы с маркерами доступа. Более подробно работа с этими функциями будет рассмотрена в гл. 41, посвященной работе с маркерами доступа. 39.4. Определение идентификатора безопасности по имени учетной записи Для определения идентификатора безопасности по имени учетной записи ИСПОЛЬЗуетСЯ фуНКЦИЯ LookupAccountName, Которая имеет следующий npOTO- тип: BOOL LookupAccountName ( LPCSTR IpSystemName, // имя системы LPCSTR lpAccountName, // имя учетной записи PSID pSid, // буфер для идентификатора безопасности LPDWORD cbSid, // длина буфера для идентификатора LPSTR ReferencedDomainName, // буфер для имени домена LPDWORD cbReferencedDomainName, // длина буфера для имени домена PSID_NAME_USE peUse // тип учетной записи ); 25 Зак. 514
770 Часть XI. Управление безопасностью в Windows В случае успешного завершения функция LookupAccountName возвращает ненулевое значение, а в случае неудачи — false. При неудаче код ошибки МОЖНО определить ПОСреДСТВОМ ВЫЗОВа фуНКЦИИ GetLastError. Отметим, ЧТО Кроме Идентификатора безопасности ФУНКЦИЯ LookupAccountName возвращает также имя домена, в котором найдена учетная запись. Поиск учетной записи выполняется в следующем порядке: сначала проверяются предопределенные общеизвестные учетные записи, затем проверяются учетные записи, зарегистрированные на локальном компьютере, после этого проверяются учетные записи, зарегистрированные в первичном домене, и, в последнюю очередь, проверяются учетные записи из доверительных доменов. Параметры функции LookupAccountName имеют следующее назначение. Параметр lpSystemName должен указывать на строку с именем компьютера, на котором ищется имя учетной записи. Если этот параметр имеет значение null, то учетная запись ищется на локальном компьютере. Параметр lpAccountName должен указывать на строку с именем учетной записи, для которой определяется идентификатор безопасности и имя домена. Параметр psid должен содержать адрес буфера, в который функция запишет структуру sid. Если установить значение этого параметра в null, to функция запишет в двойное слово, на которое указывает параметр cbsid, необходимую длину буфера для идентификатора безопасности. Параметр cbsid должен указывать на переменную типа dword, которая содержит длину буфера для идентификатора безопасности, на который указывает параметр psid. Если длина буфера недостаточна, то выполнение функции закончится неудачей, а в двойное слово, на которое указывает параметр cbsid, функция запишет необходимую длину буфера. Если в параметре psid установлено значение null, to двойное слово, на которое указывает параметр cbsid, должно содержать 0. Параметр ReferencedDomainName ДОЛЖен содержать адрес буфера, В Который функция запишет имя домена, в котором найдена учетная запись. Если установить значение этого параметра в null, to функция запишет в переменную ТИПа DWORD, На КОТОРУЮ указывает параметр cbReferencedDomainName, необходимую длину буфера для имени домена. Отметим, что строка с именем домена имеет кодировку Unicode. Параметр cbReferencedDomainName ДОЛЖен указывать на переменную ТИПа dword, которая содержит длину буфера для имени домена, на который указывает параметр ReferencedDomainName. ЕСЛИ ДЛИНа буфера недостаточна, ТО выполнение функции закончится неудачей, а в переменную, на которую указывает параметр cbReferencedDomainName, функция запишет необходимую ДЛИНУ буфера. ЕСЛИ В параметре ReferencedDomainName установлено значение null, to переменная, на которую указывает параметр cbReferencedDomainName, ДОЛЖНа содержать 0.
Глава 39. Работа с идентификаторами безопасности 771 Параметр peuse должен указывать на переменную типа sid_name_use, в которую функция запишет тип учетной записи. Возможные значения этого перечисления были приведены в предыдущем разделе. В листинге 39.8 приведена программа, которая определяет идентификатор безопасности и имя домена по имени учетной записи, используя для этого ФУНКЦИЮ LookupAccountName. еделение идот^ифдащ #ifndef UNICODE #define UNICODE #endif #include <stdio.h> #include <windows.h> #include <lm.h> int main() { wchar_t user_name[UNLEN] ; // имя пользователя DWORD dwErrCode; // код возврата DWORD dwLengthOfSID =0; // длина SID DWORD dwLengthOfDomainName = 0; // длина имени домена DWORD dwLengthOfUserName = UNLEN; // длина имени учетной записи SID *lpSID = NULL; // указатель на SID LPTSTR lpDomainName = NULL; // указатель на имя домена SID_NAME_USE type_of_SID; // тип учетной записи printf("Input a user name: "); wscanf(L"%s", user_name); // вводим имя пользователя // определяем длину SID пользователя if (!LookupAccountName( NULL, // ищем имя на локальном компьютере user_name, // имя пользователя NULL, // определяем длину SID
772 Часть XI. Управление безопасностью в Windows { ScdwLengthOfSID, // длина SID NULL, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи dwErrCode = GetLastError(); if (dwErrCode == ERROR_INSUFFICIENT_BUFFER) { // распределяем память для SID и имени домена IpSID = (SID*) new char[dwLengthOfSID]; lpDomainName = (LPTSTR) new wchar_t [dwLengthOfDomainName] ; } else { // выходим из программы printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode; } } // определяем SID и имя домена пользователя if(!LookupAccountName( NULL, // ищем имя на локальном компьютере user_name, // имя пользователя IpSID, // указатель на SID SdwLengthOfSID, // длина SID lpDomainName, // указатель на имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwErrCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode;
Глава 39. Работа с идентификаторами безопасности 773 } // выводим SID на консоль printf("SID revision: %u\n", lpSID->Revision); printf("SubAuthorityCount: %u\n", lpSID->SubAuthorityCount); printf("IdentifierAuthority: "); for (int i = 0; i < 6; ++i) printf("%u ", lpSID->IdentifierAuthority.Value[i]); printf("\n") ; printf("SubAuthorities: "); for (i = 0; i < lpSID->SubAuthorityCount; ++i) printf("%u ", lpSID->SubAuthority[i]); printf("\n") ; // выводим имя домена wprintf(L"Domain name: %s\n", lpDomainName); delete[] lpDomainName; delete[] IpSID; return 0; 39.5. Получение характеристик идентификатора безопасности Для определения длины идентификатора безопасности используется функция GetLengthsid, которая имеет следующий прототип: DWORD GetLengthSid ( PSID pSid // указатель на SID ); Единственным параметром этой функции является указатель на идентификатор безопасности. Если идентификатор безопасности имеет правильную структуру, то функция возвращает длину этого идентификатора безопасности, в противном случае — возвращаемое значение не определено. Поэтому Перед ВЫЗОВОМ фуНКЦИИ GetLengthSid НуЖНО ВЫЗЫВаТЬ фуНКЦИЮ IsValidSid, которая проверяет структуру идентификатора безопасности.
774 Часть XI. Управление безопасностью в Windows Для определения указателя на идентификатор авторизации в структуре sid ИСПОЛЬЗуетСЯ фуНКЦИЯ GetSidldentifierAuthority, которая имеет следующий прототип: PSID_IDENTIFIER_AUTHORITY GetSidldentifierAuthority ( PSID pSid // указатель на SID ); Единственным параметром этой функции является указатель на идентификатор безопасности. Если идентификатор безопасности имеет правильную Структуру, ТО фуНКЦИЯ Возвращает указатель на ПОЛе IdentifierAuthority в идентификаторе безопасности, в противном случае — возвращаемое значение Не определено. ПОЭТОМУ перед ВЫЗОВОМ функции GetSidldentifierAuthority нужно вызывать функцию isVaiidsid, которая проверяет структуру идентификатора безопасности. Для определения количества относительных идентификаторов в идентификаторе безопасности используется функция GetsidsubauthorityCount, которая имеет следующий прототип: PUCHAR GetSidSubAuthorityCount( PSID pSid // указатель на SID ); Единственным параметром этой функции является указатель на идентификатор безопасности. Если идентификатор безопасности имеет правильную структуру, то функция возвращает указатель на поле SubAuthorityCount в идентификаторе безопасности, в противном случае — возвращаемое значение не определено. Поэтому перед вызовом функции GetsidsubauthorityCount нужно вызывать функцию IsValidSid, Которая проверяет структуру идентификатора безопасности. После того как получено количество относительных идентификаторов, адрес конкретного относительного идентификатора можно определить посредством вызова функции GetsidsubAuthority, которая была подробно рассмотрена в разд. 39.2. В листинге 39.5 приведена программа, которая выводит на консоль содержимое идентификатора безопасности, используя описанные ранее функции. ^T.w„,^„VKv4^.^^^ ™.T,v.„„..v..r.~.,v...v,..,,..,v v...v ,...v v. ~ 5 ; Листинг 39Ж Получение характеристик идентификатора безопасности #ifndef UNICODE #define UNICODE #endif #include <stdio.h>
Глава 39. Работа с идентификаторами безопасности 775 #include <windows.h> #include <lm.h> int main () { wchar_t user_name [UNLEN] ; // имя пользователя DWORD dwErrCode; // код возврата DWORD dwLengthOfSID = 0; DWORD dwLengthOfDomainName = 0; DWORD dwLengthOfUserName = UNLEN; SID *lpSID = NULL; LPTSTR lpDomainName = NULL; SID_NAME_USE type_of_SID; // длина SID // длина имени домена // длина имени учетной записи // указатель на SID // указатель на имя домена // тип учетной записи // указатель на идентификатор авторизации SID PSID_IDENTIFIER_AUTHORITY IpSia; // указатель на количество относительных идентификаторов PUCHAR IpSubAuthorityCount; // указатель на относительный идентификатор DWORD *lpSubAuthority; printf("Input a user name: "); wscanf(L"%s", user_name); // вводим имя пользователя // определяем длину SID пользователя LookupAccountName( // ищем имя на локальном компьютере // имя пользователя // определяем длину SID // длина SID // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID); // тип учетной записи NULL, user_name, NULL, &dwLengthOfSID, NULL, // проверяем, вернула ли функция длину SID if (dwLengthOfSID == 0)
776 Часть XI. Управление безопасностью в Windows dwErrCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode; } // распределяем память для SID и имени домена IpSID = (SID*) new char[dwLengthOfSID]; lpDomainName = (LPTSTR) new wchar_t [dwLengthOfDomainName] ; // определяем SID и имя домена пользователя if(!LookupAccountName ( NULL, // ищем имя на локальном компьютере user_name, // имя пользователя IpSID, // указатель на SID ScdwLengthOfSID, // длина SID lpDomainName, // указатель на имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwErrCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode; } // получаем характеристики SID и выводим их на консоль dwLengthOfSID = GetLengthSid(IpSID); printf("Length of SID: %u\n", dwLengthOfSID); IpSia = GetSidIdentifierAuthority(IpSID); printf("IdentifierAuthority: "); for (int i = 0; i < 6; ++i) printf("%u ", lpSia->Value[i]); printf("\n");
Глава 39. Работа с идентификаторами безопасности 777 IpSubAuthorityCount = GetSidSubAuthorityCount(IpSID); printf("SuhAuthorityCount: %u\n", *IpSubAuthorityCount); printf("SubAuthorities: "); for (i = 0; i < *IpSubAuthorityCount; ++i) { IpSubAuthority = GetSidSubAuthority(IpSID, i); printf("%u ", *IpSubAuthority); } printf("\n"); delete[] lpDomainName; delete[] IpSID; return 0; } 39.6. Копирование и сравнение идентификаторов безопасности Для копирования идентификатора безопасности используется функция copysid, которая имеет следующий прототип: BOOL CopySid( DWORD nDestinationSidLength, // длина буфера для SID PSID pDestinationSid, // указатель на буфер PSID pSourceSid // указатель на SID ); В случае успешного завершения функция copysid возвращает ненулевое значение, а в случае неудачи — false. При неудаче код ошибки можно определить посредством вызова функции GetLastError. Параметры функции copysid имеют следующее назначение. Параметр nDestinationSidLength ДОЛЖен содержать ДЛИНУ буфера, В КОТОРЫЙ будет копироваться sid. Параметр Destinationsid должен указывать на буфер, в который будет копироваться SID. Параметр psourcesid должен указывать на sid, который будет копироваться в буфер.
778 Часть XI. Управление безопасностью в Windows Для сравнения двух идентификаторов безопасности используется функция Equaisid, которая имеет следующий прототип: BOOL EqualSid( PSID pSidl, // указатель на первый сравниваемый SID PSID pSid2 // указатель на второй сравниваемый SID ); Параметрами функции являются указатели на идентификаторы безопасности, которые будут сравниваться. Если сравниваемые идентификаторы безопасности совпали, то функция Equaisid возвратит ненулевое значение, в противном случае — false. При неудаче код ошибки можно определить посредством вызова функции GetLastError. Если хотя бы один из сравниваемых идентификаторов безопасности имеет неправильную структуру, то возвращаемое функцией значение не определено. Идентификатор безопасности без последнего относительного идентификатора называется префиксом идентификатора безопасности. Сравнение префиксов используется для определения принадлежности двух учетных записей одной области авторизации. Например, учетные записи групп, принадлежащих одному домену, будут иметь одинаковые префиксы: S - R - I - SAo Для сравнения префиксов двух идентификаторов безопасности используется функция EquaiPref ixsid, которая имеет следующий прототип: BOOL EqualPrefixSid( PSID pSidl, PSID pSid2 ); Параметрами функции являются указатели на идентификаторы безопасности, префиксы которых будут сравниваться. Если сравниваемые префиксы совпали, то функция EquaiPref ixsid возвратит ненулевое значение, в противном случае — false. При неудаче код ошибки можно определить посредством ВЫЗОВа функции GetLastError. В листинге 39.6 приведена программа, которая сравнивает префиксы двух Групп, ИСПОЛЬЗУЯ фуНКЦИЮ EquaiPref ixSid. j—™ V-—„.«...-..- v.-; N_.~ .-.-- -•- ..- + [ Листинг 39.6. Сравнение префиксов идентификаторов безопасности #ifndef UNICODE #define UNICODE #endif #include <stdio.h>
Глава 39. Работа с идентификаторами безопасности 779 # include <windows.h> #include <lm.h> int main() { wchar_t group_name[GNLEN]; // имя группы DWORD dwErrCode; // код возврата DWORD dwLengthOfSID = 0; DWORD dwLengthOfDomainName = 0; DWORD dwLengthOfUserName = UNLEN; SID *lpSID_l = NULL; SID *lpSID_2 = NULL; LPTSTR lpDomainName_l = NULL; LPTSTR lpDomainName_2 = NULL; SID_NAME_USE type_of_SID; // длина SID // длина имени домена // длина имени учетной записи // указатель на SID первой группы // указатель на SID второй группы // указатель на имя домена // указатель на имя домена // тип учетной записи printf("Input a name of the first group: "); wscanf(L"%s", group_name); // вводим имя группы // определяем длину SID группы LookupAccountName( NULL, group_name, NULL, SdwLengthOfSID, NULL, // ищем имя на локальном компьютере // имя пользователя // определяем длину SID // длина SID // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID); // тип учетной записи // проверяем, вернула ли функция длину SID if (dwLengthOfSID == 0) { dwErrCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode;
780 Часть XI. Управление безопасностью в Windows } // распределяем память для SID и имени домена IpSIDJ. = (SID*) new char[dwLengthOfSID]; lpDomainName_l = (LPTSTR) new wchar_t [dwLengthOfDomainName] ; // определяем SID и имя домена группы if(!LookupAccountName( NULL, // ищем имя на локальном компьютере group_name, // имя группы lpSID_l, // указатель на SID SdwLengthOfSID, // длина SID lpDomainName_l, // указатель на имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwErrCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode; } // проверяем тип учетной записи if (type_of_SID != SidTypeAlias) { printf("This is not a group.\n"); return 1; } printf("Input a name of the second group: "); wscanf(L"%s", group_name); // вводим имя группы // определяем длину SID группы LookupAccountName ( NULL, // ищем имя на локальном компьютере group_name, // имя пользователя NULL, // определяем длину SID
Глава 39. Работа с идентификаторами безопасности 781 &dwLengthOfSID, // длина SID NULL, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID); // тип учетной записи // проверяем, вернула ли функция длину SID if (dwLengthOfSID == 0) { dwErrCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode; } // распределяем память для SID и имени домена lpSID_2 = (SID*) new char[dwLengthOfSID]; lpDomainName_2 = (LPTSTR) new wchar_t [dwLengthOfDomainName] ; // определяем SID и имя домена группы if(!LookupAccountName( NULL, // ищем имя на локальном компьютере group_name, // имя группы lpSID_2, // указатель на SID SdwLengthOfSID, // длина SID lpDomainName_2, // указатель на имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwErrCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode; } // проверяем тип учетной записи
782 Часть XI. Управление безопасностью в Windows if (type_of_SID != SidTypeAlias) { printf("This is not a group.\n"); return 1; } // сравниваем префиксы SID групп if (EqualPrefixSid(lpSID_l, lpSID_2)) printf("SID prefixes are equal\n"); else printf("SID prefixes are not equal\n"); delete[] lpDomainName_l; de1ete[] lpDomainName_2; delete[] lpSID_l; delete[] lpSID_2; return 0; > 39.7. Строковое представление идентификатора безопасности Для преобразования информации, хранящейся в структуре sid, в строку символов используется функция ConvertsidTostringsid, которая имеет следующий прототип: BOOL ConvertsidTostringsid( PSID pSid, // указатель на SID LPSTR *StringSid // адрес указателя на строку ); В случае успешного завершения функция возвратит ненулевое значение, а в случае неудачи — false. При неудаче код ошибки можно определить посредством вызова функции GetLastError, которая может вернуть один из двух возможных кодов: □ error_not_enough_memory — недостаточно памяти; □ error_none_mapped — не найдена учетная запись, соответствующая sid. Параметры функции имеют следующее назначение.
Глава 39. Работа с идентификаторами безопасности 783 Параметр psid должен указывать на идентификатор безопасности, который будет преобразован в строковое представление. Параметр stringsid должен содержать адрес указателя, в который функция поместит адрес на строку с символьным представлением идентификатора безопасности. Память под строку резервирует сама функция. После завершения работы со строковым представлением идентификатора безопасности ЭТУ ПаМЯТЬ НУЖНО ОСВОбоДИТЬ При ПОМОЩИ фуНКЦИИ LocalFree. В листинге 39.5 приведена программа, в которой идентификатор безопасности преобразуется в строковое представление, используя функцию ConvertSidToStringSid. #define _WIN32_WINNT 0x0500 #ifndef UNICODE #define UNICODE #endif #include <stdio.h> # include <windows.h> #include <lm.h> #include <sddl.h> int main() { wchar_t user_name [UNLEN] ; // имя пользователя DWORD dwErrCode; // код возврата DWORD dwLengthOfSID =0; // длина SID DWORD dwLengthOfDomainName = 0; // длина имени домена PSID IpSID = NULL; // указатель на SID LPTSTR lpDomainName = NULL; // указатель на имя домена SID_NAME_USE type_of_SID; // тип учетной записи LPTSTR IpStringSID = NULL; // указатель на строку с SID printf("Input a user name: ");
784 Часть XI. Управление безопасностью в Windows wscanf(L"%sM, user_name); // вводим имя пользователя // определяем длину SID пользователя LookupAccountName ( NULL, // ищем имя на локальном компьютере user_name, // имя пользователя NULL, // определяем длину SID &dwLengthOfSID, // длина SID NULL, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID); // тип учетной записи // проверяем, вернула ли функция длину SID if (dwLengthOfSID == 0) { dwErrCode = GetLastError(); printf("Lookup account name failed.\n") ; printf("Error code: %d\n", dwErrCode); return dwErrCode; } // распределяем память для SID и имени домена IpSID = (PSID) new wchar_t[dwLengthOfSID] ; lpDomainName = (LPTSTR) new wchar_t [dwLengthOfDomainName] ; // определяем SID и имя домена пользователя if(!LookupAccountName( NULL, // ищем имя на локальном компьютере user_name, // имя пользователя IpSID, // указатель на SID &dwLengthOfSID, // длина SID lpDomainName, // указатель на имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwErrCode = GetLastError();
Глава 39. Работа с идентификаторами безопасности 785 printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode; } // преобразуем SID в строку if (!ConvertSidToStringSid(lpSID, klpStringSID)) { printf("Convert SID to string SID failed."); return GetLastError(); } // распечатываем SID wprintf(L"SID: %s\n", IpStringSID); // распечатываем имя домена wprintf(L"Domain name: %s\n", lpDomainName); // освобождаем память LocalFree(IpStringSID); de1e te[] lpDomainName; delete[] IpSID; return 0; } Для преобразования символьного представления идентификатора безопасности В структуру ТИПа SID ИСПОЛЬЗуеТСЯ фуНКЦИЯ ConvertStringSidToSid, которая имеет следующий прототип: BOOL ConvertStringSidToSid( LPCSTR StringSid, // указатель на строку PSID *pSid // адрес указателя на SID ); В случае успешного завершения функция возвратит ненулевое значение, а в случае неудачи — false. При неудаче код ошибки можно определить посредством вызова функции GetLastError, которая может вернуть один из двух возможных кодов: □ error_invalid_parameter — неверный параметр; □ ERROR_INVALID_SID — НеверНЫЙ SID.
786 Часть XI. Управление безопасностью в Windows Параметры функции имеют следующее назначение. Параметр strings id должен указывать на строку с символьным представлением идентификатора безопасности. Параметр psid должен содержать адрес указателя, в который функция поместит адрес на структуру типа sid. Память под структуру резервирует сама функция. После завершения работы со структурой эту память нужно освободить при ПОМОЩИ фуНКЦИИ LocalFree. В листинге 39.8 приведена профамма, которая преобразует символьное представление идентификатора безопасности в структуру типа sid. ние отроки с идентификатором безопасности ; i ? '- ^ - '-'-,4 #define _WIN32_WINNT 0x0500 #include <stdio.h> #include <windows.h> #include <lm.h> # include < sddl.h> int main () { int n; // количество относительных идентификаторов учетной записи LPTSTR IpStringSID = NULL; // указатель на строку с SID PSID IpSID = NULL; // указатель на SID LPTSTR IpString; // текущий указатель на SID printf("Input a number of Sub Authorities: "); scanf("%d"# &n); IpStringSID = (LPTSTR) new char[10 + 5 * n] ; IpStringSID[0] = 'S' IpStringSID[1] = '-' IpStringSID[2] = '1' IpStringSID[3] = '-' // признак строки SID // версия 1 // устанавливаем указатель на идентификатор учетной записи IpString = IpStringSID + 4; printf("Input an Identifier Authority: "); scanf("%s", IpString); // вводим идентификатор учетной записи
Глава 39. Работа с идентификаторами безопасности 787 IpString += strlen(IpString); *lpString++ = '-'; for (int i = 0; i < n - 1; ++i) { printf("Input Sub Authority #%d: ", i); // вводим относительный идентификатор учетной записи scanf("%s", IpString); IpString += strlen(IpString); *lpString++ = '-'; } printf("Input Sub Authority #%d: ", n - 1); // вводим последний относительный идентификатор учетной записи scanf("%s", IpString); // распечатываем полученный SID printf("SID: %s\n", IpStringSID); // преобразуем строку в SID if (!ConvertstringSidToSid(IpStringSID, &lpSID)) { DWORD dwErrCode = GetLastError(); printf("Convert string SID to SID failed.\n"); printf("Last error code: %u\n"/ dwErrCode); return dwErrCode; } // проверяем SID на достоверность if(IIsValidSid(lpSID)) { printf( "The SID is not valid.\n"); return 1; } printf("The SID is valid.\n"); // освобождаем память LocalFree(lpSID); return 0;
Глава 40 Работа с дескрипторами безопасности 40.1. Форматы дескрипторов безопасности Как уже говорилось в гл. 36, каждый охраняемый объект в операционных системах Windows имеет дескриптор безопасности, который используется операционной системой для ограничения доступа к этому объекту. Дескриптор безопасности может храниться в двух форматах: абсолютном и относительном. В обоих случаях дескриптор безопасности описывается структурой типа: typedef struct _SECURITY_DESCRIPTOR { BYTE Revision; // версия BYTE Sbzl; // 0 для выравнивания на границу 16 бит SECURITY_DESCRIPTOR_CONTROL Control; // управляющие флаги PSID Owner; // указатель на SID владельца объекта PSID Group; // указатель на SID первичной группы владельца PACL Sacl; // указатель на список SACL PACL Dacl; // указатель на список DACL } SECURITY_DESCRIPTOR, *PISECURITY_DESCRIPTOR; Тип security_descriptor_control определяется следующим образом: typede f WORD SECURITY_DESCRIPTOR_CONTROL; To есть является символическим именем типа word, который определяет слово памяти. В этом слове могут устанавливаться следующие флаги, управляющие состоянием дескриптора безопасности: □ se_owner_defaulted — владелец задан по умолчанию; □ se_group_defaulted — первичная фуппа владельца задана по умолчанию;
Глава 40. Работа с дескрипторами безопасности 789 □ se_dacl_present — присутствует список DACL; □ se_dacl_defaulted — список DACL задан по умолчанию; □ se_sacl_present — присутствует список SACL; □ se_sacl_defaulted — список SACL задан по умолчанию; □ se_self_relative — дескриптор безопасности в относительной форме. Кроме того, начиная с версии Windows 2000, в дескрипторе безопасности могут быть установлены следующие флаги: □ se_dacl_auto_inherited — DACL установлен механизмом наследования; □ se_sacl_auto_inherited — SACL установлен механизмом наследования; □ se_dacl_protected — DACL защищен от наследования; □ se_sacl_protected — SACL защищен от наследования. Различие между абсолютным и относительным форматами дескриптора безопасности заключается в том, что в случае абсолютного формата дескриптор безопасности содержит только указатели на данные, которые описывают ограничения на доступ к объекту, а не сами данные. А при относительном формате дескриптор безопасности также содержит и данные, которые описывают ограничения доступа. Эти данные хранятся вслед за структурой в одном буфере, хотя порядок хранения этих данных не определен. В случае абсолютного формата структура типа security_descriptor также содержит адреса данных, но сами данные могут храниться в любой области памяти. Таким образом, можно сказать, что дескриптор безопасности в относительном формате содержит как указатели на данные, так и сами данные, а дескриптор безопасности в абсолютном формате содержит только адреса данных. Если функция Win32 API возвращает дескриптор безопасности, то этот дескриптор безопасности представлен в относительном формате. В качестве параметра функции дескриптор безопасности может передаваться как в абсолютном, так и в относительном формате. Возможно преобразование дескриптора безопасности из одного формата в другой. Для преобразования дескриптора безопасности из абсолютного формата В ОТНОСИТелЬНЫЙ ИСПОЛЬЗуетсЯ фунКЦИЯ MakeSelfRelativeSD, KOTO- рая имеет следующий прототип: BOOL MakeSelfRelativeSD( PSECURITY_DESCRIPTOR pAbsoluteSD, // адрес абсолютного SD PSECURITY_DESCRIPTOR pSelfRelativeSD, // адрес относительного SD LPDWORD lpdwBufferLength // адрес длины буфера ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. При неудачном завершении функции код
790 Часть XI. Управление безопасностью в Windows ошибки можно получить посредством вызова функции GetLastError. Параметры функции MakeseifReiativeSD имеют следующее назначение. Параметр pAbsoiuteSD должен содержать адрес исходного дескриптора безопасности в абсолютном формате. Параметр pSeifReiativeSD должен указывать на буфер, в который функция запишет дескриптор безопасности в относительном формате, полученный из дескриптора безопасности в абсолютном формате. Параметр lpdwBufferLength должен указывать на переменную типа dword, в которой хранится длина буфера для дескриптора безопасности в относительном формате. Если эта длина меньше необходимой, то функция закончится неудачей и запишет по адресу lpdwBufferLength необходимую длину буфера. Для преобразования дескриптора безопасности из относительного формата в абсолютный используется функция MakeAbsoiuteSD, которая имеет следующий прототип: BOOL MakeAbsoiuteSD( PSECURITY_DESCRIPTOR pSeifReiativeSD, // адрес относительного SD PSECURITY_DESCRIPTOR pAbsoiuteSD, // адрес абсолютного SD LPDWORD IpdwAbsoiuteSDsize, // указатель на длину абсолютного SD // адрес DACL // адрес длины DACL // адрес SACL // адрес длины SACL // адрес SID владельца // адрес длины SID владельца // адрес SID первичной группы LPDWORD lpdwPrimaryGroupSize // адрес длины SID первичной группы PACL LPDWORD PACL LPDWORD PSID LPDWORD PSID pDacl, lpdwDaclSize, pSacl, lpdwSaclSize, pOwner, lpdwOwnerSize, pPrimaryGroup, В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. При неудачном завершении функции код ошибки можно получить посредством вызова функции GetLastError. Параметры функции MakeAbsoiuteSD имеют следующее назначение. Параметр pSeifReiativeSD должен указывать на исходный дескриптор безопасности в относительном формате. Параметр pAbsoiuteSD должен указывать на буфер, в который функция запишет дескриптор безопасности в абсолютном формате, полученный из дескриптора безопасности в относительном формате. Параметр IpdwAbsoiuteSDsize должен указывать на переменную типа dword, в которой хранится длина буфера для дескриптора безопасности
Глава 40. Работа с дескрипторами безопасности 791 в абсолютном формате. Если эта длина меньше необходимой, то функция закончится неудачей и запишет по адресу lpdwAbsoiuteSDsize необходимую длину буфера. Параметр pDaci должен указывать на буфер, куда функция запишет список DACL. Параметр lpdwDacisize должен указывать на переменную типа dword, в которой хранится длина буфера для списка DACL. Если эта длина меньше необходимой, то функция закончится неудачей и запишет необходимую длину буфера ПО адресу lpdwDacisize. Параметр psaci должен указывать на буфер, куда функция запишет список SACL. Параметр lpdwsacisize должен указывать на переменную типа dword, в которой хранится длина буфера для списка SACL. Если эта длина меньше необходимой, то функция закончится неудачей и запишет по адресу lpdwsacisize необходимую длину буфера. Параметр powner должен указывать на буфер, куда функция запишет идентификатор безопасности владельца объекта. Параметр lpdwOwnersize должен указывать на переменную типа dword, в которой хранится длина буфера для идентификатора безопасности владельца объекта. Если эта длина меньше необходимой, то функция закончится неудачей и запишет по адресу lpdwOwnersize необходимую длину буфера. Параметр pPrimaryGroup должен указывать на буфер, куда функция запишет идентификатор безопасности первичной группы владельца объекта. Параметр IpdwPrimaryGroupSize должен указывать на переменную типа dword, в которой хранится длина буфера для идентификатора безопасности первичной группы владельца объекта. Если эта длина меньше необходимой, ТО фунКЦИЯ закончится неудачей И запишет ПО адресу IpdwPrimaryGroupSize необходимую длину буфера. В разд. 40.3 приведена программа, в которой дескриптор из абсолютного формата преобразуется в дескриптор в относительном формате, используя ФУНКЦИЮ MakeSelfRelativeSD. 40.2. Создание нового дескриптора безопасности Сначала кратко опишем порядок создания дескриптора безопасности, а затем рассмотрим функции, которые используются при его создании. Очевидно, что дескриптор безопасности создается в абсолютной форме, т. к. данные, описывающие ограничения на доступ к объекту, обычно хранятся в различных областях памяти. Первым делом нужно зарезервировать память
792 Часть XI. Управление безопасностью в Windows для дескриптора безопасности. Если память резервируется статически, то нужно просто объявить переменную типа security_descriptor. При динамическом распределении памяти необходимо зарезервировать блок памяти, длина которого равна, по крайней мере, security_descriptor_min_length байт. После этого нужно инициализировать поля дескриптора безопасности. Для этого используется следующие функции. Версия дескриптора безопасности устанавливается при ПОМОЩИ функции InitializeSecurityDesriptor. Владелец дескриптора безопасности устанавливается посредством вызова функции setsecurityDescriptorOwner. Первичная группа владельца дескриптора безопасности устанавливается посредством вызова функции setsecurityDescriptorGroup. Списки управления доступом к объекту устанавливаются ПОСреДСТВОМ ВЫЗОВа функций SetSecurityDescriptorDacl И setsecurityDescriptorsaci. После инициализации дескриптора безопасности можно проверить его структуру. Для этого используется функция IsValidSecurityDescriptor. В этом разделе создадим дескриптор безопасности, который только содержит владельца объекта и его первичную группу. Создание списков управления доступом к объекту и подключение их к дескриптору безопасности будет рассмотрено в следующем разделе. Там же будут подробно рассмотрены и функции подключения этих списков к дескриптору безопасности, а именно функции SetSecurityDescriptorDacl И SetSecurityDescriptorSacl. Теперь же перейдем к подробному описанию остальных упомянутых функций. Для инициализации версии дескриптора безопасности используется функция InitializeSecurityDesriptor, КОТОрая Имеет следующий прОТОТИП! BOOL InitializeSecurityDescriptor( PSECURITY_DESCRIPTOR pSecurityDescriptor, // указатель на SD DWORD dwRevision // версия SD ); В случае успешного завершения функция возвращает ненулевое значение, в противном случае — false. При неудачном завершении функции код ошибки можно получить посредством вызова функции GetLastError. Параметры функции InitializeSecurityDesriptor имеют следующее назначение. Параметр pSecurityDescriptor должен указывать на структуру типа security_descriptor, версия которой инициализируется. Параметр dwRevision должен содержать значение security_descriptor_revision. Для установки владельца дескриптора безопасности используется функция SetsecurityDescriptorOwner, КОТОрая Имеет следующий ПрОТОТИП! BOOL SetsecurityDescriptorOwner( PSECURITY_DESCRIPTOR pSecurityDescriptor, // указатель на SD PSID pOwner, // указатель на SID владельца
Глава 40. Работа с дескрипторами безопасности 793 BOOL bOwnerDefaulted // флаг задания владельца по умолчанию ); В случае успешного завершения функция возвращает ненулевое значение, в противном случае — false. При неудачном завершении функции код ошибки можно получить посредством вызова функции GetLastError. Параметры функции setsecurityDescriptorOwner имеют следующее назначение. Параметр pSecurityDescriptor должен указывать на структуру типа security_descriptor, в которую устанавливается владелец. Параметр powner должен указывать на идентификатор безопасности учетной записи, которая будет владельцем объекта. В параметре bOwnerDefaulted должно быть установлено значение true или false. Если установлено значение true, то в поле control дескриптора безопасности будет установлен флаг se_owner_defaulted. В противном случае этот флаг будет сброшен. Если в дескрипторе безопасности установлен флаг se_owner_defaulted, то для определения владельца объекта система будет использовать механизм, заданный по умолчанию. В противном случае владелец объекта будет определяться из дескриптора безопасности. Для установки первичной группы владельца дескриптора безопасности используется фунКЦИЯ SetSecurityDescriptorGroup, КОТОрая Имеет СЛедуЮ- щий прототип: BOOL SetSecurityDescriptorGroup( PSECURITY_DESCRIPTOR pSecurityDescriptor, // указатель на SD PS ID pGroup, // указатель на SID первичной группы BOOL bGroupDefaulted // флаг первичной группы по умолчанию ); В случае успешного завершения функция возвращает ненулевое значение, в противном случае — false. При неудачном завершении функции код ОШИбкИ МОЖНО ПОЛУЧИТЬ ПОСреДСТВОМ ВЫЗОВа фунКЦИИ GetLastError. Опишем параметры функции SetSecurityDescriptorGroup. Параметр pSecurityDescriptor должен указывать на структуру ТИПа security_descriptor, в которую устанавливается группа-владелец. Параметр pGroup должен указывать на идентификатор безопасности учетной записи первичной группы владельцем объекта. Значение этого параметра может быть равно null. В этом случае предполагается, что владелец объекта не имеет первичной группы. В параметре bGroupDefaulted должно быть установлено значение true или false. Если установлено значение true, то в поле control дескриптора безопасности будет установлен флаг se_group_defaulted. В противном случае этот флаг будет сброшен. Если в дескрипторе безопасности установлен
794 Часть XI. Управление безопасностью в Windows флаг se_group_defaulted, то для определения первичной группы владельца объекта система будет использовать механизм, заданный по умолчанию. В противном случае первичная группа владельца объекта будет определяться из дескриптора безопасности. После инициализации дескриптора безопасности нужно проверить его структуру. ДЛЯ ЭТОГО ИСПОЛЬЗуеТСЯ фуНКЦИЯ IsValidSecurityDescriptor, которая имеет следующий прототип: BOOL IsValidSecurityDescriptor( PSECURITY_DESCRIPTOR pSecurityDescriptor // указатель на SD ); В случае успешного завершения функция возвращает ненулевое значение, в противном случае — false. При неудачном завершении функции код ОШИбкИ МОЖНО ПОЛУЧИТЬ ПОСреДСТВОМ ВЫЗОВа фуНКЦИИ GetLastError. ЕдИН- ственный параметр этой функции должен указывать на дескриптор безопасности, структура которого проверяется. В листинге 40.1 приведена программа, в которой инициализируется дескриптор безопасности для нового каталога. При этом владелец каталога и первичная группа владельца каталога определяются операционной системой, используя механизм, заданный по умолчанию. В этом случае владельцем каталога является пользователь, от имени которого запущена программа. Так как списки управления доступом создаются пустыми, то доступ к каталогу разрешен всем пользователям. Подробно работа со списками доступа будет рассмотрена в следующем разделе. #include <windows.h> #include <stdio.h> #include <lm.h> int main() { char chDirName[248] ; // имя каталога SECURITY_DESCRIPTOR sd; // дескриптор безопасности каталога SECURITY_ATTRIBUTES sa; // атрибуты защиты каталога DWORD dwErrCode; // код возврата // инициализируем версию дескриптора безопасности if (!InitializeSecurityDescriptor(
Глава 40. Работа с дескрипторами безопасности 795 &sd, SECURITY_DESCRIPTOR_REVISION)) { dwErrCode = GetLastError(); printf("Initialize security descroptor failed.\n"); printf("Error code: %d\n"/ dwErrCode); return dwErrCode; } // устанавливаем SID владельца объекта if (!SetSecurityDescriptorOwner( &sd, // адрес дескриптора безопасности NULL, //не задаем владельца SE_OWNER_DEFAULTED)) // определить владельца по умолчанию { dwErrCode = GetLastError(); perror("Set security descriptor owner failed.\n"); printf("The last error code: %u\n"/ dwErrCode); return dwErrCode; } // устанавливаем SID первичной группы владельца if (!SetSecurityDescriptorGroup( &sd, // адрес дескриптора безопасности NULL, //не задаем первичную группу SE_GROUP_DEFAULTED)) // определить первичную группу по умолчанию { dwErrCode = GetLastError(); perror("Set security descriptor group failed.\n"); printf("The last error code: %u\n"/ dwErrCode); return dwErrCode; } // проверяем структуру дескриптора безопасности if (!IsValidSecurityDescriptor(&sd))
796 Часть XI. Управление безопасностью в Windows dwErrCode = GetLastError(); perror("Security descriptor is invalid.\n"); printf("The last error code: %u\n"/ dwErrCode); return dwErrCode; } // инициализируем атрибуты безопасности sa.nLength = sizeof(sa); // устанавливаем длину атрибутов защиты sa.lpSecurityDescriptor = &sd; // устанавливаем адрес SD sa.blnheritHandle = FALSE; // дескриптор каталога ненаследуемый printf("Input a directory name: "); scanf("%s", chDirName); // вводим имя каталога // создаем каталог if (!CreateDirectory(chDirName, &sa)) { dwErrCode = GetLastError(); perror("Security descriptor is invalid.\n"); printf("The last error code: %u\n", dwErrCode); return dwErrCode; } printf("The directory is created.\n"); return 0; } Для того чтобы эта программа создала новый каталог, нужно, чтобы процесс обладал привилегией se_take_ownership. Отметим, что, если процесс, создающий новый объект, пытается установить владельцем этого объекта пользователя, отличного от пользователя, от имени которого запущен процесс, то система может выдать ошибку. Это произойдет потому, что данный процесс обычно обладает только привилегией se_take_ownership. А для того, чтобы процесс мог установить владельцем объекта любого пользователя, который может быть владельцем объекта, этот процесс должен обладать привилегией se_restore_name. Владельцем же объекта может быть только пользователь, который наделен привилегией se_take_ownership_name. Подробно о работе с привилегиями будет рассказано в гл. 43.
Глава 40. Работа с дескрипторами безопасности 797 40.3. Определение длины дескриптора безопасности Для определения длины дескриптора безопасности используется функция GetSecurityDescriptorLength, Которая Имеет Следующий ПРОТОТИП! DWORD GetSecurityDescriptorLength( PSECURITY_DESCRIPTOR pSecurityDescriptor // указатель на SD ); Если дескриптор безопасности имеет правильную структур, то функция возвратит его длину, включая длину всех связанных с этим дескриптором полей, в противном случае — неопределенное значение. Поэтому перед вызовом функции GetSecurityDescriptorLength нужно вызвать функцию isVaiidsecurityDescriptor. Единственным параметром этой функции является указатель на дескриптор безопасности, длину которого определяет функция. Можно сказать, что вызов этой функции позволяет определить длину буфера, необходимую для преобразования дескриптора из абсолютной формы в относительную форму. В листинге 40.2 приведена программа, которая определяет длину дескриптора безопасности, ИСПОЛЬЗуя фунКЦИЮ GetSecurityDescriptorLength. Листинг 40,2, Определение длины дескриптора безопасности и преобразование дескриптора безопасности из абсолютной в относительную форму #include <windows.h> #include <iostream.h> int main () { HANDLE hProcess; // дескриптор процесса HANDLE hTokenHandle; // дескриптор маркера доступа TOKEN_OWNER *lpOwnerSid = NULL; // указатель на буфер DWORD dwOwnerLength= 0; // длина буфера TOKEN_OWNER *lpPrimaryGroupSid = NULL; // указатель на буфер DWORD dwPrimaryGroupLength= 0; // длина буфера SECURITY_DESCRIPTOR sd; // SD в абсолютной форме SECURITY_DESCRIPTOR *lpSd; // указатель на SD в относительной форме
798 Часть XI. Управление безопасностью в Windows DWORD dwErrCode; // код возврата DWORD dwLength; // длина дескриптора безопасности // получить дескриптор процесса hProcess = GetCurrentProcess(); // получить маркер доступа процесса if (!OpenProcessToken( hProcess, // дескриптор процесса TOKEN_QUERY, // запрос информации из маркера &hTokenHandle)) // дескриптор маркера { dwErrCode = GetLastError(); cout « "Open process token failed: " « dwErrCode « endl; return dwErrCode; } // определяем длину SID владельца if (!GetTokenlnformation( hTokenHandle, // дескриптор маркера доступа TokenOwner, // получаем SID владельца NULL, // нужна длина буфера О, // пока длина равна О &dwOwnerLength)) // для длины буфера { if (dwOwnerLength != 0) // захватываем память под SID lpOwnerSid = (TOKEN_OWNER*)new char[dwOwnerLength]; else { // выходим из программы dwErrCode = GetLastError(); cout « "Get token information for length failed: " « dwErrCode « endl; return dwErrCode; } } // определяем SID владельца
Глава 40. Работа с дескрипторами безопасности 799 if (!GetTokenInformation( hTokenHandle, // дескриптор маркера доступа TokenOwner, // нужен SID маркера доступа lpOwnerSid, // адрес буфера для SID dwOwnerLength, // длина буфера &dwOwnerLength)) // длина буфера { dwErrCode = GetLastError(); cout « "Get token information failed: " « dwErrCode « endl; return dwErrCode; } // определяем длину SID первичной группы владельца if (!GetTokenInformation( hTokenHandle, // дескриптор маркера доступа TokenPrimaryGroup, // получаем SID первичной группы NULL, // нужна длина буфера О, // пока длина равна О &dwPrimaryGroupLength)) // для длины буфера { if (dwPrimaryGroupLength != 0) // захватываем память под SID lpPrimaryGroupSid = (TOKEN_OWNER*)new char [dwPrimaryGroupLength] ; else { // выходим из программы dwErrCode = GetLastError(); cout « "Get token information for length failed: " « dwErrCode « endl; return dwErrCode; } } // определяем SID первичной группы владельца if (!GetTokenlnformation( hTokenHandle, // дескриптор маркера доступа TokenPrimaryGroup, // нужен SID первичной группы
800 Часть XI. Управление безопасностью в Windows lpPrimaryGroupSid, // адрес буфера для SID dwPrimaryGroupLength, // длина буфера &dwPrimaryGroupLength)) // длина буфера { dwErrCode = GetLastError(); cout « "Get token information failed: " « dwErrCode « endl; return dwErrCode; } // инициализируем дескриптор безопасности if (!InitializeSecurityDescriptor( // инициализируем версию &sd, SECTJRITY_DESCRIPTOR_REVISION)) { dwErrCode = GetLastError(); cerr « "Initialize security descriptor failed." « endl « "The last error code: " « dwErrCode « endl; cout « "Press any key to finish." « endl; cin.get(); return dwErrCode; } // устанавливаем SID владельца объекта if (!SetSecurityDescriptorOwner( &sd, lpOwnerSid->Owner, FALSE)) { dwErrCode = GetLastError(); cerr « "Set security descriptor owner failed." « endl « "The last error code: " « dwErrCode « endl; cout « "Press any key to finish." « endl; cin.getO; return dwErrCode;
Глава 40. Работа с дескрипторами безопасности 801 // устанавливаем SID первичной группы владельца if (!SetSecurityDescriptorGroup( &sd, lpPrimaryGroupSid->Owner, FALSE)) { dwErrCode = GetLastError(); cerr « "Set security descriptor group failed." « endl « "The last error code: " « dwErrCode « endl; cout « "Press any key to finish." « endl; cin.getO ; return dwErrCode; } // проверяем структуру дескриптора безопасности if (!IsValidSecurityDescriptor(&sd)) { dwErrCode = GetLastError0; cerr « "The security descriptor is invalid." « endl « "The last error code: " « dwErrCode « endl; cout « "Press any key to finish." « endl; cin.getO ; return dwErrCode; } // печатаем длину структуры SECURITY_DESCRIPTOR cout « "Length of SECURITY_DESCRIPTOR structure: " « sizeof(sd) « endl; // получаем и печатаем длину дескриптора безопасности //в абсолютной форме dwLength = GetSecurityDescriptorLength(&sd); cout « "Length of security descriptor in absolute form: " « dwLength « endl; // создаем дескриптор безопаснотсти в относительной форме IpSd = (SECURITY_DESCRIPTOR*)new char[dwLength]; 26 Зак. 514
802 Часть XI. Управление безопасностью в Windows if (!MakeSelfRelativeSD(&sd, IpSd, &dwLength)) { dwErrCode = GetLastError(); cerr « "Make self relative SD failed." « endl « "The last error code: " « dwErrCode « endl; cout « "Press any key to finish." « endl; cin.getO ; return dwErrCode; } cout « "The self realtive security descriptor is made." « endl; // освобождаем память delete[] lpOwnerSid; delete[] lpPrimaryGroupSid; delete[] IpSd; // закрываем дескрипторы CloseHandle(hTokenHandle); return 0; } 40.4. Получение дескриптора безопасности по имени объекта Для получения дескриптора безопасности объекта по имени этого объекта ИСПОЛЬЗуетСЯ фуНКЦИЯ GetNamedSecuritylnfo, КОТОрая имеет следующий прототип: DWORD GetNamedSecuritylnfo ( LPSTR pObjectName, // указатель на имя объекта SE_OBJECT_TYPE ObjectType, // тип объекта SECURITY_INFORMATION Security Info, // управляющие флаги PSID *ppsidOwner, // адрес указателя на SID владельца PSID *ppsidGroup, // адрес указателя на SID группы PACL *ppDacl, // адрес указателя на DACL PACL *ppSacl, // адрес указателя на SACL PSECURITY_DESCRIPTOR *ppSecurityDescriptor // адрес указателя на SD
Глава 40. Работа с дескрипторами безопасности 803 В случае успешного завершения функция возвращает значение error^success, а в случае неудачи — код ошибки. Параметры функции GetNamedSecuritylnfo имеют следующее назначение. Параметр pobjectName должен указывать на имя объекта, дескриптор безопасности которого возвращает функция. Параметр objectType должен содержать тип объекта, дескриптор безопасности которого возвращает функция. Тип объекта должен быть одним из значений перечисления se_object_type: typedef enum _SE_OBJECT_TYPE { SE_UNKNOWN_OBJECT_TYPE =0, // неизвестный тип SE_FILE_OBJECT/ // файл SE_SERVICE, // сервис SE_PRINTER, // принтер SE_REGISTRY_KEY/ // ключ регистра SE_LMSHARE, // разделяемый объект локальной сети SE_KERNEL_OBJECT, // объект ядра SE_WINDOW_OBJECT, // оконная станция на локальном компьютере SE_DS_OBJECT, // сервис каталогов SE_DS_OBJECT_ALL, // сервис каталогов с их свойствами SE_PROVIDER_DEFINED_OBJECT // Windows Management Instrumentation } SE_OBJECT_TYPE; Отметим, что последние три значения определены только для версий старше Windows 2000 (включительно). Параметр securityinfo должен содержать переменную типа security_information, в которой установлены флаги, отмечающие информацию, которую должен содержать дескриптор безопасности. Тип security_information определен следующим образом: typedef DWORD SECmiTY_INFORMATION, *PSECTJRITY_INFORMATION; To есть представляет собой переменную типа dword. При работе с функцией GetNamedSecuritylnfo в этой переменной могут быть установлены любые комбинации следующих флагов: □ owner_security_information — получить владельца объекта; □ group_security_information — получить первичную группу владельца; □ dacl_security_information — получить список DACL; □ sacl_security_information — получить список SACL. Параметр ppsidowner должен содержать адрес указателя, в который функция запишет указатель на идентификатор безопасности владельца объекта.
804 Часть XI. Управление безопасностью в Windows Этот указатель будет записан только в том случае, если в параметре securityinfo установлен флаг owner_security_information. Если sid владельца объекта не нужен, то значение этого параметра может быть установлено В NULL. Параметр ppsidGroup должен содержать адрес указателя, в который функция запишет указатель на идентификатор безопасности первичной группы владельца объекта. Этот указатель будет записан только в том случае, если в параметре securityinfo установлен флаг group_security_information. Если sid первичной группы владельца объекта не нужен, то значение этого параметра может быть установлено в null. Параметр ppDaci должен содержать адрес указателя, в который функция запишет указатель на список DACL из дескриптора безопасности объекта. Этот указатель будет записан только в том случае, если в параметре securityinfo установлен флаг dacl_security_information. Если список DACL объекта не нужен, то значение этого параметра может быть установлено В NULL. Параметр ppsaci должен содержать адрес указателя, в который функция запишет указатель на список SACL из дескриптора безопасности объекта. Этот указатель будет записан только в том случае, если в параметре securityinfo установлен флаг sacl_security_information. Если список SACL объекта не нужен, то значение этого параметра может быть установлено В NULL. ( Замечание J Для получения идентификаторов безопасности владельца объекта и первичной группы владельца объекта, а также списка DACL необходимо, чтобы процесс открыл доступ к объекту в режиме read_control. Для этого нужно, чтобы процесс имел соответствующие права доступа к объекту, которые должны быть установлены в списке DACL. Кроме того, для получения списка SACL процесс должен обладать привилегией se_security_name. В листинге 40.3 приведена программа, в которой функция GetNamedsecurityinfo используется для получения идентификаторов безопасности владельца объекта и первичной группы владельца объекта по имени объекта. уЛиотйрг 40.3. Получение идентификаторов безопасности владельца объекта Ри первичной группы по имени объекта j #define _WIN32_WINNT 0x0500 #include <stdio.h> #include <windows.h>
Глава 40. Работа с дескрипторами безопасности 805 #include <lm.h> #include <aclapi.h> #include <sddl.h> int main() { char chFileName[256]; // имя файла PSID pSidOwner; // указатель на SID владельца объекта PSID pSidGroup; // указатель на SID первичной группы объекта PSECURITY_DESCRIPTOR pSecurityDescriptor; // указатель на SD LPTSTR IpStringSid; // указатель на строку SID DWORD dwRetCode; // код возврата // вводим имя файла, например: C:\Xtest.txt printf("Input a full path to your file: "); gets(chFileName); // получаем дескриптор безопасности файла dwRetCode = GetNamedSecuritylnfо( chFileName, // имя файла SE_FILE_OBJECT/ // объект файл GROUP_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION/ &pSidOwner, // адрес указателя на SID владельца &pSidGroup, // адрес указателя на первичную группу NULL, // указатель на DACL не нужен NULL, // указатель на SACL не нужен &pSecurityDescriptor); // адрес указателя на SD if (dwRetCode != ERROR_SUCCESS) { printf("Get named security info failed.\n"); printf("Error code: %u\n", dwRetCode); return dwRetCode;
806 Часть XI. Управление безопасностью в Windows // преобразуем SID владельца в строку if (!ConvertSidToStringSid(pSidOwner/ SclpStringSid)) { printf("Convert SID to string SID failed."); dwRetCode = GetLastError(); return dwRetCode; } // печатаем SID владельца printf("Owner SID: %s\n", IpStringSid); // освобождаем память для строки LocalFree(IpStringSid); // преобразуем SID первичной группы в строку if (!ConvertSidToStringSid(pSidGroup, SclpStringSid)) { printf("Convert SID to string SID failed."); ' dwRetCode = GetLastError(); return dwRetCode; } // печатаем SID первичной группы printf("Group SID: %s\n", IpStringSid); // освобождаем память для строки LocalFree(IpStringSid); // освобождаем память для дескриптора LocalFree(pSecurityDescriptor); return 0; 40.5. Получение дескриптора безопасности по дескриптору объекта Для получения дескриптора безопасности объекта по дескриптору этого объекта используется функция Getsecurityinfo, которая имеет следующий прототип: DWORD Getsecurityinfo( HANDLE handle, // дескриптор объекта
Глава 40. Работа с дескрипторами безопасности 807 SE_OBJECT_TYPE ObjectType, // тип объекта SECURITY_INFORMATION Securitylnfo, // управляющие флаги PSID *ppsidOwner, // адрес указателя на SID владельца PSID *ppsidGroup, // адрес указателя на SID группы PACL *ppDacl, // адрес указателя на DACL PACL *ppSacl, // адрес указателя на SACL PSECURITY_DESCRIPTOR *ppSecurityDescriptor // адрес указателя на SD ); В случае успешного завершения функция возвращает значение error_success, а в случае неудачи — код ошибки. Все параметры этой функции за исключением первого имеют то же назначение, что и соответствующие параметры функции GetNamedsecurityinfo. Первый же параметр handle должен содержать дескриптор объекта, для которого функция GetSecuritylnfo получает дескриптор безопасности. В ЛИСТИНГе 40.4 приведена программа, В КОТОРОЙ фуНКЦИЯ GetSecuritylnfo используется для получения идентификаторов безопасности владельца объекта и первичной группы владельца объекта по дескриптору объекта. Программа 40.4. Получение идентификаторов безопасности владельца объекта и первичной группы по дескриптору объекта ; ^ ^ _ ^;; , w | #define _WIN32_WINNT 0x0500 #include <stdio.h> #include <windows.h> #include <lm.h> #include <aclapi.h> #include <sddl.h> int main() { char chFileName[256] ; // имя файла HANDLE hFile; // дескриптор файла PSID pSidOwner; // указатель на SID владельца объекта PSID pSidGroup; // указатель на SID первичной группы объекта PSECURITY_DESCRIPTOR pSecurityDescriptor; // указатель на SD LPTSTR IpStringSid; // указатель на строку SID
808 Часть XI. Управление безопасностью в Windows DWORD dwRetCode; // код возврата // вводим полное имя файла, например: С:\\test.txt printf("Input a full path to your file: "); gets (chFileName) ; // открываем файл hFile = CreateFile( chFileName, // имя файла READ_CONTROL, // разрешаем доступ к дескриптору безопасности О, //не разделяем доступ NULL, // ненаследуемый дескриптор OPEN_EXISTING, // открываем существующий файл FILE_ATTRIBUTE_NORMAL, // обычный файл NULL); // шаблона нет if(hFile == INVALID_HANDLE_VALUE) { dwRetCode = GetLastError(); printf("Create file failed.\n"); printf("Error code: %u\n", dwRetCode); return dwRetCode; } // получаем дескриптор безопасности файла dwRetCode = GetSecurityInfo( hFile, // дескриптор файла SE_FILE_OBJECT, // объект файл GROUP_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION, // тип информации &pSidOwner, // адрес указателя на SID владельца &pSidGroup, // адрес указателя на первичную группу NULL, // указатель на DACL не нужен NULL, // указатель на SACL не нужен SpSecurityDescriptor); // адрес указателя на SD if (dwRetCode != ERROR_SUCCESS) { printf("Get named security info failed.\n"); printf("Error code: %u\n", dwRetCode);
Глава 40. Работа с дескрипторами безопасности 809 return dwRetCode; } // преобразуем SID владельца в строку if (!ConvertsidToStringSid(pSidOwner, &lpStringSid)) { printf("Convert SID to string SID failed."); dwRetCode = GetLastError(); return dwRetCode; } // печатаем SID владельца printf("Owner SID: %s\n", IpStringSid); // освобождаем память для строки LocalFree(IpStringSid); // преобразуем SID первичной группы в строку if (!ConvertSidToStringSid(pSidGroup, ^IpStringSid)) { printf("Convert SID to string SID failed."); dwRetCode = GetLastError(); return dwRetCode; } // печатаем SID первичной группы printf("Group SID: %s\n", IpStringSid); // освобождаем память для строки LocalFree(IpStringSid); // освобождаем память для дескриптора LocalFree(pSecurityDescriptor); // закрываем файл CloseHandle(hFile); return 0;
810 Часть XI. Управление безопасностью в Windows 40.6. Получение данных из дескриптора безопасности Из дескриптора безопасности можно получить следующие данные: идентификаторы безопасности владельца объекта и первичной группы владельца объекта, а также списки управления доступом к объекту DACL и SACL. Для получения этих данных используются соответственно функции GetSecurityDescriptorOwner, GetSecurityDescriptorGroup, GetSecurityDescriptorEacl и GetsecurityDescriptorSacl. В этом разделе будут рассмотрены только первые две из перечисленных выше функций. Функции для получения из дескриптора безопасности информации о списках DACL и SACL будут рассмотрены в гл. 41. Для получения из дескриптора безопасности информации о владельце объекта ИСПОЛЬЗуется функция GetSecurityDescriptorOwner, которая имеет следующий прототип: BOOL GetSecurityDescriptorOwner( PSECURITY_DESCRIPTOR pSecurityDescriptor, // указатель на SD PSID *pOwner, // указатель на SID владельца объекта LPBOOL lpbOwnerDefaulted // указатель на флаг ); В случае успешного завершения функция возвращает ненулевое значение, в противном случае — false. При неудачном завершении функции код ошибки можно получить посредством вызова функции GetLastError. Параметры функции GetSecurityDescriptorOwner имеют следующее назначение. Параметр pSecurityDescriptor ДОЛЖен указывать на структуру типа security_descriptor, из которой определется владелец объекта. Параметр powner должен содержать адрес переменной, в которую функция запишет указатель на идентификатор безопасности владельца объекта. Если дескриптор не содержит идентификатор безопасности владельца объекта, то функция устанавливает значение переменной в null и параметр lpbOwnerDefaulted игнорируется. Параметр lpbOwnerDefaulted должен указывать на булеву переменную. Если в дескрипторе безопасности установлен флаг se_owner_defaulted, to функция установит значение этой булевой переменной в true, в противном случае — В FALSE. Для получения из дескриптора безопасности информации о первичной Группе владельца Объекта ИСПОЛЬЗуется фуНКЦИЯ GetSecurityDescriptorGroup, которая имеет следующий прототип: BOOL GetSecurityDescriptorGroup( PSECURITY_DESCRIPTOR pSecurityDescriptor, // указатель на SD
Глава 40. Работа с дескрипторами безопасности 811 PSID *pGroup, // указатель на SID владельца LPBOOL lpbGroupDefaulted // указатель на флаг ); В случае успешного завершения функция возвращает ненулевое значение, в противном случае — false. При неудачном завершении функции код ошибки можно получить посредством вызова функции GetLastError. Параметры функции GetSecurityDescriptorGroup имеют следующее назначение. Параметр pSecurityDescriptor должен указывать на структуру типа security_descriptor, из которой определяется первичная группа владельца объекта. Параметр pGroup должен содержать адрес переменной, в которую функция запишет указатель на идентификатор безопасности первичной группы владельца объекта. Если дескриптор не содержит идентификатор безопасности первичной группы владельца объекта, то функция устанавливает значение переменной в null и параметр lpbGroupDe faulted игнорируется. Параметр lpbGroupDe faulted должен указывать на булеву переменную. Если в дескрипторе безопасности установлен флаг se_group_defaulted, to функция установит значение этой булевой переменной в true, в противном случае — В FALSE. В листинге 40.5 приведена программа, которая получает из дескриптора безопасности файла указатели на идентификаторы безопасности владельца этого файла и его первичной группы, используя для этого функции GetSecurityDescriptorOwner И GetSecurityDescriptorGroup. В СВЯЗИ С ЭТОЙ программой отметим, что функция GetFiieSecurity вернет дескриптор безопасности файла только в том случае, если программа запущена от имени владельца файла. | Листинг 40*5. Получение идентификаторов безопасности владельца объекта [и его первичной группы ' ,,;_;; ^^^^^_л:. ь _ ; J #define _WIN32_WINNT 0x0500 #include <stdio.h> # include <windows.h> #include <sddl.h> int main() { char chFileName[256]; // имя файла PSID pSidOwner = NULL; // указатель на SID владельца объекта
812 Часть XI. Управление безопасностью в Windows PSID pSidGroup = NULL; // указатель на SID первичной группы объекта PSECURITY_DESCRIPTOR pSD = NULL; // указатель на SD BOOL bOwnerDefaulted = FALSE; // флаг владельца по умолчанию BOOL bGroupDefaulted = FALSE; // флаг первичной группы по умолчанию LPTSTR IpStringSid; // указатель на строку SID DWORD dwLength =0; // длина дескриптора безопасности DWORD dwRetCode; // код возврата // вводим имя файла, к которому вы имеете доступ printf("Input a full path to your file or directory: "); gets(chFileName); // получаем длину дескриптора безопасности файла if (!GetFileSecurity( chFileName, // имя файла GROUP_SECURITY_INFORMATION | OWNER_SECURITY_INFORMATION, // информация, которую нужно получить pSD, // адрес буфера для дескриптора безопасности dwLength, // длина буфера &dwLength)) // необходимая длина if (dwLength != 0) // захватываем память для дескриптора безопасности pSD = (PSECURITY_DESCRIPTOR) new char[dwLength]; else { printf("Get file security for length failed.\n"); dwRetCode = GetLastError(); printf("Error code: %u\n"/ dwRetCode); return dwRetCode; } // получаем дескриптор безопасности файла if (!GetFileSecurity( chFileName, // имя файла GROUP_SECURITY_INFORMATION |
Глава 40. Работа с дескрипторами безопасности 813 OWNER_SECURITY_INFORMATION, // информация, которую нужно получить pSD, // адрес буфера для дескриптора безопасности dwLength, // длина буфера &dwLength)) // необходимая длина { printf("Get file security failed.\n"); dwRetCode = GetLastError(); printf("Error code: %u\n"/ dwRetCode); return dwRetCode; } // получаем идентификатор безопасности владельца объекта if (IGetSecurityDescriptorOwner( pSD, &pSidOwner, &bOwnerDefaulted)) { printf("Get security descriptor owner failed.\n"); dwRetCode = GetLastError(); printf("Error code: %u\n"/ dwRetCode); return dwRetCode; } // получаем SD первичной группы владельца объекта if (!GetSecurityDescriptorGroup( pSD, &pSidGroup, &bGroupDefaulted)) { printf("Get security descriptor group failed.\n"); dwRetCode = GetLastError(); printf("Error code: %u\n"/ dwRetCode); return dwRetCode; } // преобразуем SID владельца в строку
814 Часть XI. Управление безопасностью в Windows if (!ConvertSidToStringSid(pSidOwner, SlpStringSid)) { printf("Convert SID to string SID failed."); dwRetCode = GetLastError(); return dwRetCode; } // печатаем SID владельца printf("Owner SID: %s\n", IpStringSid); // освобождаем память для строки LocalFree(IpStringSid); // преобразуем SID первичной группы в строку if (!ConvertSidToStringSid(pSidGroup, SlpStringSid)) { printf("Convert SID to string SID failed."); dwRetCode = GetLastError(); return dwRetCode; } // печатаем SID первичной группы printf("Group SID: %s\n", IpStringSid); // освобождаем память для строки LocalFree(IpStringSid); // освобождаем память для дескриптора delete[] pSD; return 0; } В заключение ЭТОГО раздела ОТМетИМ, ЧТО функции GetSecurityDescriptorOwner, GetSecurityDescriptorGroup, GetSecurityDescriptorDacl И GetSecurityDescriptorSaci используются совместно с функциями, которые возвращают только дескриптор безопасности объекта без указателей на данные, хранящиеся в этом дескрипторе безопасности. К ним относятся еле-
Глава 40. Работа с дескрипторами безопасности 815 дующие функции: GetFileSecurity, GetKernelObj ectSecur ity, GetUserObjectSecurity, QueryServiceObjectSecurity, RegGetKeySecurity. Как видно из названий этих функций, они предназначены для извлечения дескриптора безопасности из объектов определенного типа, а именно — файлов, объектов ядра, сервисов и ключей регистра соответственно. При этом заметим, что вместо всех этих функций могут использоваться функции GetNamedSecuritylnfo И GetSecuritylnfo, которые сразу возвращают Деск- риптор безопасности и указатели на данные, хранящиеся в нем. Небольшое Офаничение существует ДЯЯ объектов Ядра, Т. е. функции GetNamedSecuritylnfo и setNamedSecurityinfo работают только со следующими объектами ядра: семафорами, событиями, мьютексами, ожидающими таймерами и отображениями файлов в памяти. Поэтому можно сказать, что функции для получения дескрипторов безопасности из объектов конкретного типа являются устаревшими. 40.7. Получение состояния управляющих флагов дескриптора безопасности Для получения информации о состоянии управляющих флагов дескриптора безопасности ИСПОЛЬЗуется функция GetSecurityDescriptorControl, которая имеет следующий прототип: BOOL GetSecurityDescriptorControl( PSECURITY_DESCRIPTOR pSecurityDescriptor, // указатель на SD PSECURITY_DESCRIPTOR_CONTROL pControl, // управляющие флаги SD LPDWORD lpdwRevision // версия SD ); В случае успешного завершения функция возвращает ненулевое значение, в противном случае — false. При неудачном завершении функции код ошибки можно получить посредством вызова функции GetLastError. Параметры функции GetSecurityDescriptorControl имеют следующее назначение. Параметр pSecurityDescriptor должен указывать на структуру типа security_descriptor, из которой определяются состояния управляющих флагов. Параметр pControi должен указывать на переменную типа SECURITY_DESCRIPTOR_CONTROL, В КОТОруЮ фуНКЦИЯ Запишет СОСТОЯНИЯ управляющих флагов. Тип security_descriptor_control определяется следующим образом: typedef WORD SECURITY_DESCRIPTOR_CONTROL; То есть фактически является символическим именем типа word.
816 Часть XI. Управление безопасностью в Windows Параметр lpdwRevision должен указывать на переменную типа dword, в которую функция запишет версию дескриптора безопасности. В листинге 40.6 приведена программа, которая получает управляющие флаги из дескриптора безопасности, используя для этого функцию GetsecurityDescriptorcontrol. После этого в программе проверяются состояния управляющих флагов, характеризующих список управления доступом DACL. Полный список управляющих флагов, которые могут использоваться в дескрипторе безопасности, приведен в разд. 40.10. 40.6. Получение сое юности > ч&К мл*й sjL^С. #define _WIN32_WINNT 0x0500 #include <stdio.h> # include <windows.h> #include <aclapi.h> ^управляющих флагов 1их флагов и|шскриптора * ^ s int main() { char chFileName[256]; HANDLE hFile; // имя файла // дескриптор файла PSECURITY_DESCRIPTOR pSecurityDescriptor; // указатель на SD SECURITY_DESCRIPTOR_CONTROL wControl; // управляющие флаги из SD DWORD dwRevision; // версия дескриптора безопасности DWORD dwRetCode; // код возврата // вводим полное имя файла, например: C:\Xtest.txt printf("Input a full path to your file: "); gets(chFileName); // открываем файл hFile = CreateFile( chFileName, // имя файла READ_CONTROL, // разрешаем доступ к дескриптору безопасности 0, //не разделяем доступ NULL, // ненаследуемый дескриптор
Глава 40. Работа с дескрипторами безопасности 817 QPEN_EXISTING, // открываем существующий файл FILE_ATTRIBUTE_NORMAL, // обычный файл NULL); // шаблона нет if(hFile == INVALID_HANDLE_VALUE) { dwRetCode = GetLastError(); printf("Create file failed.\n"); printf("Error code: %u\n", dwRetCode); return dwRetCode; } // получаем дескриптор безопасности файла dwRetCode = GetSecuritylnfо( hFile, // дескриптор файла SE_FILE_OBJECT, // объект файл DACL_SECURITY_INFORMATION, // тип информации NULL, // указатель на SID владельца не нужен NULL, // указатель на первичную группу не нужен NULL, // указатель на DACL не нужен NULL, // указатель на SACL не нужен &pSecurityDescriptor); // адрес указателя на SD if (dwRetCode != ERROR_SUCCESS) { printfC'Get named security info failed.\n"); printf("Error code: %u\n", dwRetCode); return dwRetCode; } // получаем управляющую информацию из дескриптора безопасности if(IGetSecurityDescriptorControl( pSecurityDescriptor, &wControl, &dwRevision)) { dwRetCode = GetLastError(); printfC'Get security descriptor control failed.\n"); printf("Error code: %u\n", dwRetCode);
818 Часть XI. Управление безопасностью в Windows return dwRetCode; } printf("The following flags are set: \n"); // определяем информацию из управляющего слова if(wControl & SE_DACL_AUTO_INHERITED) printf("SE_DACL_AUTO_INHERITED\n"); if(wControl & SE_DACL_DEFAULTED) printf("SE_DACL_DEFAULTED\n"); if(wControl & SE_DACL_PRESENT) printf("SE.DACL.PRESENTXn"); if(wControl & SE_DACL_PROTECTED) printf("SE_DACL_PROTECTED\n"); // выводим на печать версию дескриптора безопасности printf("Descriptor revision: %u\n", dwRevision); // освобождаем память для дескриптора LocalFree(pSecurityDescriptor); // закрываем файл CloseHandle(hFile); return 0; 40.8. Изменение дескриптора безопасности по имени объекта Для изменения дескриптора безопасности по имени объекта используется функция SetNamedSecuritylnfo, которая имеет следующий прототип: DWORD SetNamedSecuritylnfo( LPSTR pObjectName, // указатель на имя объекта SE_OBJECT_TYPE ObjectType, // тип объекта SECURITY_INFORMATION Securitylnfo, // управляющие флаги PSID psidOwner, // указатель на SID владельца PSID psidGroup, // указатель на SID группы
Глава 40. Работа с дескрипторами безопасности 819 PACL pDacl, // указатель на список DACL PACL pSacl // указатель на список SACL ); В случае успешного завершения функция возвращает значение error_success, а в случае неудачи — код ошибки. Параметры функции SetNamedSecurityinfo имеют следующее назначение. Параметр pobjectName должен указывать на имя объекта, дескриптор безопасности которого изменяет функция. Параметр objectType должен содержать тип объекта, дескриптор безопасности которого изменяет функция. Тип объекта должен быть одним из значений перечисления se_object_type, которое имеет следующий тип typedef enum _SE_OBJECT_TYPE { SE_UNKNOWN_OBJECT_TYPE =0, // неизвестный тип SE_FILE_OBJECT, // файл SE_SERVICE, // сервис SE_PRINTER, // принтер SE_REGISTRY_KEY, // ключ регистра SE_LMSHARE, // разделяемый объект локальной сети SE_KERNEL_OBJECT, // объект ядра SE_WINDOW_OBJECT, // оконная станция на локальном компьютере SE_DS_OBJECT, // сервис каталогов SE_DS_OBJECT_ALL, // сервис каталогов с их свойствами SE_PROVIDER_DEFINED_OBJECT // Windows Management Instrumentation } SE_OBJECT_TYPE; Отметим, что последние три значения определены, начиная с версии Windows 2000 и старше. Параметр securityinfo должен содержать переменную типа security_information, в которой установлены флаги, отмечающие информацию, которую должна изменить функция в дескрипторе безопасности. Тип security_information определен следующим образом: typedef DWORD SECURITY_INFORMATION, *PSECURITY_INFORMATION; т. е. представляет собой двойное слово (тип dword). При работе с функцией SetNamedSecurityinfo в этом двойном слове может быть установлена любая комбинация следующих флагов: □ owner_security_information — получить владельца объекта; □ group_security_information — получить первичную фуппу владельца; □ DACL_SECURITY_INFORMATION — ПОЛУЧИТЬ СПИСОК DACL;
820 Часть XI. Управление безопасностью в Windows D sacl_security_information — получить список SACL; П PROTECTED_DACL_SECURITY_INFORMATION — ЗЭЩИТа СПИСКЭ DACL ОТ На- следования; □ protected_sacl_security_information — защита списка SACL от насле- дования. Отметим, что последние два флага используются, только начиная с версии Windows 2000. Если установлен флаг dacl_security_information, to флаг protected_dacl_security_information игнорируется. В свою очередь, если установлен флаг sacl_security_information, to игнорируется флаг PROTECTED_SACL_SECURITY_INFORMATION. Параметр psidowner должен указывать на идентификатор безопасности нового владельца объекта. Причем процесс, в котором выполняется функция, должен иметь режим доступа write_owner к объекту, владельца которого он изменяет. Или новый владелец должен иметь привилегию se_take_ownership_name, которая позволяет стать новым владельцем объекта без разрешения старого владельца объекта. Функция будет изменять владельца объекта только в том случае, если в параметре securityinfo установлен флаг owner_security_information. Если изменять владельца объекта не нужно, то значение этого параметра может быть установлено в null. Параметр psidGroup должен указывать на идентификатор безопасности новой первичной группы владельца объекта. Функция изменит первичную группу владельца объекта только в том случае, если в параметре securityinfo установлен флаг group_security_information. Если изменять первичную группу владельца объекта не нужно, то значение этого параметра может быть установлено в null. Параметр pDaci должен указывать на новый список DACL объекта. Причем процесс, в котором выполняется функция, должен иметь режим доступа write_dac к объекту, список DACL которого он заменяет. Или процесс должен исполняться от имени владельца объекта. Новый список DACL будет записан только в том случае, если в параметре securityinfo установлен флаг dacl_security_information. Если заменять список DACL объекта не нужно, то значение этого параметра может быть установлено в null. Параметр psaci должен указывать на новый список SACL объекта. Причем пользователь, от имени которого вьтолняется процесс, должен иметь привилегию se_security_name. Новый список SACL будет записан только в том случае, если в параметре securityinfo установлен флаг sacl_security_information. Если заменять список SACL объекта не нужно, то значение этого параметра может быть установлено в null. В листинге 40.7 приведена программа, в которой функция setNamedSecurityinfo используется для изменения владельца файла.
Глава 40. Работа с дескрипторами безопасности 821 Программа 40.7. Изменение владельца файла по имени файла #ifndef UNICODE #def ine UNICODE #endif #include <stdio.h> #include <windows.h> #include <lm.h> #include <aclapi.h> int main() { wchar_t wchF i 1 eName [256] ; wchar_t wchUserName [UNLEN] ( // имя файла // имя нового владельца файла PSID IpSID = NULL; // указатель на SID LPTSTR lpDomainName = NULL; // указатель на имя домена DWORD dwLengthOfSID =0; // длина SID DWORD dwLengthOfDomainName = 0; // длина имени домена DWORD dwRetCode; // код возврата SID_NAME_USE typeOfSID; // тип учетной записи // вводим имя файла, например: C:\Xtest.txt printf("Input a full path to your file: "); _getws (wchF i 1 eName) ; // вводим имя нового владельца объекта printf("Input a user name: "); ws can f (L" % s", wchUs erName) ; // определяем длину SID нового владельца файла LookupAccountName( NULL, // ищем имя на локальном компьютере wchUserName, // имя нового владельца файла
822 Часть XL Управление безопасностью в Windows NULL, // определяем длину SID SdwLengthOfSID, // длина SID NULL, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &typeOfSID); // тип учетной записи // проверяем, вернула ли функция длину SID if (dwLengthOfSID == 0) { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // распределяем память для SID и имени домена IpSID = (PSID) new wchar_t[dwLengthOfSID]; lpDomainName = (LPTSTR) new wchar_t [dwLengthOfDomainName] j // определяем SID и имя домена пользователя if(!LookupAccountName( NULL, // ищем имя на локальном компьютере wchUserName, // имя пользователя IpSID, // указатель на SID SdwLengthOfSID, // длина SID lpDomainName, // указатель на имя домена &dwLengthOfDomainName, // длина имени домена StypeOfSID)) // тип учетной записи { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode;
Глава 40. Работа с дескрипторами безопасности 823 // устанавливаем нового владельца файла dwRetCode = SetNamedSecuritylnfо( wchFileName, // имя файла SE_FILE_OBJECT/ // объект файл OWNER_SECURITY_INFORMATION, // изменяем только имя владельца файла IpSID, // адрес на SID нового владельца NULL, // первичную группу не изменяем NULL, // DACL не изменяем NULL); // SACL не изменяем if (dwRetCode != ERROR_SUCCESS) { printf("Set named security info failed.\n"); printf("Error code: %u\n", dwRetCode); return dwRetCode; } printf("The new owner of the file is set.\n"); // освобождаем память delete[] IpSID; delete[] lpDomainName; return 0; } В заключение этого раздела отметим, что в данной программе функция SetNamedSecuritylnf о использовалась только для изменения имени владельца файла. ЕСЛИ же функция SetNamedSecuritylnf о изменяет также СПИСКИ управления доступом к охраняемому объекту, то в наследуемые элементы этих списков включаются также списки управления дочерних объектов. Более подробно о наследовании элементов списков управления доступом сказано в разд. 36.7, 36.9. 40.9. Изменение дескриптора безопасности по дескриптору объекта Для изменения дескриптора безопасности по дескриптору объекта используется функция setsecurityinfo, которая имеет следующий прототип: DWORD Setsecurityinfo( HANDLE handle, // дескриптор объекта SE_OBJECT_TYPE ObjectType, // тип объекта
824 Часть XL Управление безопасностью в Windows SECURITY_INFORMATION Securitylnfo, // управляющие флаги PSID psidOwner, // указатель на SID владельца PSID psidGroup, // указатель на SID первичной группы PACL pDacl, // указатель на список DACL PACL pSacl // указатель на список SACL ); В случае успешного завершения функция возвращает значение error_success, а в случае неудачи — код ошибки. Все параметры этой функции за исключением первого имеют то же назначение, что и соответствующие параметры функции SetNamedSecurityinfo. Первый же параметр handle должен содержать дескриптор объекта, в котором функция setsecurityinfo изменяет дескриптор безопасности. В листинге 40.8 приведена программа, в которой функция setsecurityinfo используется для изменения владельца файла. Листинг 40.8. Изменение владельца файла по дескриптору файла #ifndef UNICODE #define UNICODE #endif #include <stdio.h> # include <windows.h> #include <lm.h> #include <aclapi.h> int main() { wchar_t wchFileName[256]; // имя файла wchar_t wchUserName[UNLEN] ; // имя нового владельца файла HANDLE hFile; // дескритпор файла PSID IpSID = NULL; // указатель на SID LPTSTR lpDomainName = NULL; // указатель на имя домена DWORD dwLengthOfSID =0; // длина SID DWORD dwLengthOfDomainName = 0; // длина имени домена DWORD dwRetCode; // код возврата
Глава 40. Работа с дескрипторами безопасности 825 SID_NAME_USE typeOfSID; // тип учетной записи // вводим имя файла, например: C:\Xtest.txt printf("Input a full path to your file: "); _getws (wchFileName) ; // вводим имя нового владельца объекта printf("Input a user name: "); ws canf(L"% s", wchUs erName); // открываем файл hFile = CreateFile( wchFileName, // имя файла WRITE_OWNER, // разрешаем изменение владельца файла О, //не разделяем доступ NULL, // ненаследуемый дескриптор OPEN_EXISTING, // открываем существующий файл FILE_ATTRIBUTE_NORMAL, // обычный файл NULL); // шаблона нет if(hFile == INVALID_HANDLE_VALUE) { dwRetCode = GetLastError(); printf("Create file failed.\n"); printf("Error code: %u\n", dwRetCode); return dwRetCode; } // определяем длину SID нового владельца файла LookupAccountName ( NULL, // ищем имя на локальном компьютере wchUserName, // имя нового владельца файла NULL, // определяем длину SID &dwLengthOfSID, // длина SID NULL, // определяем имя домена &dwLengthOfDomainName, // длина имени домена StypeOfSID); // тип учетной записи // проверяем, вернула ли функция длину SID
826 Часть XI. Управление безопасностью в Windows if (dwLengthOfSID == 0) { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // распределяем память для SID и имени домена IpSID = (PSID) new wchar_t[dwLengthOfSID]; lpDomainName = (LPTSTR) new wchar_t [dwLengthOfDomainName] ; // определяем SID и имя домена пользователя if(!LookupAccountName( NULL, // ищем имя на локальном компьютере wchUserName, // имя пользователя IpSID, // указатель на SID &dwLengthOfSID, // длина SID lpDomainName, // указатель на имя домена &dwLengthOfDomainName, // длина имени домена &typeOfSID)) // тип учетной записи { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // устанавливаем нового владельца файла dwRetCode = SetSecuritylnfo( hFile, // дескриптор файла . SE_FILE_OBJECT/ // объект файл OWNER_SECURITY_INFORMATION, // изменяем только имя владельца файла IpSID, // адрес на SID нового владельца NULL, // первичную группу не изменяем NULL, // DACL не изменяем NULL); // SACL не изменяем if (dwRetCode != ERROR_SUCCESS)
Глава 40. Работа с дескрипторами безопасности 827 { printf("Set named security info failed.\n"); printf("Error code: %u\n", dwRetCode); return dwRetCode; } printf("The new owner of the file is set.\n"); // освобождаем память delete[] IpSID; delete[] lpDomainName; // закрываем файл CloseHandle(hFile) ; return 0; } В заключение этого раздела сделаем следующее замечание. В данной программе функция setsecurityinfo использовалась только для изменения имени владельца файла. Если же эта функция изменяет также списки управления доступом к охраняемому объекту, то в наследуемых элементы этих списков включаются также списки управления дочерних объектов. Более подробно о наследовании элементов списков управления доступом сказано в разд. 36.7, 36.9. 40.10. Изменение состояния управляющих флагов дескриптора безопасности Для изменения состояния управляющих флагов дескриптора безопасности ИСПОЛЬЗуетсЯ функция SetSecurityDescriptorControl, Которая Имеет СЛе- дующий прототип: BOOL SetSecurityDescriptorControl( PSECURITY_DESCRIPTOR pSecurityDescriptor, // указатель на SD SECURITY_DESCRIPTOR_CONTROL ControlBitsOfInterest, // флаги SECURITY_DESCRIPTOR_CONTROL ControlBitsToSet // новые флаги ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного завершения ФУНКЦИИ МОЖНО ПОЛУЧИТЬ ПОСреДСТВОМ ВЫЗОВа фуНКЦИИ GetLastError. Параметры фуНКЦИИ SetSecurityDescriptorControl имеют следующее назначение.
828 Часть XI. Управление безопасностью в Windows Параметр pSecurityDescriptor должен указывать на дескриптор безопасности, состояния управляющих флагов которого будут изменяться. В параметре controiBitsofinterest должны быть установлены управляющие флаги, значения которых будут изменяться. В параметре ControiBitsToSet должны быть установлены новые значения управляющих флагов, которые будут изменяться. Два последних параметра имеют тип security_descriptor_control, который определяется следующим образом: typedef WORD SECURITY_DESCRIPTOR_CONTROL; т. е. фактически является символическим именем типа word, описывающего слово в памяти компьютера. При использовании функции setsecurityDescriptorControi в этом слове могут устанавливаться сле- дующие управляющие флаги: □ se_dacl_auto_inherit_req — выполнить механизм наследования DACL; □ se_sacl_auto_inherit_req — выполнить механизм наследования SACL; □ se_dacl_auto_inherited — список DACL установлен механизмом наследования; □ se_sacl_auto_inherited — список SACL установлен механизмом наследования; □ se_dacl_protected — список DACL защищен от наследования; □ se_sacl_protected — список SACL защищен от наследования. В листинге 40.9 приведена программа, в которой устанавливается флаг защиты дескриптора безопасности от механизма наследования списка DACL, ИСПОЛЬЗУЯ фуНКЦИЮ SetsecurityDescriptorControi. ; Листинг 40.9» Изменение флага защиты от наследования списка DACL #define _WIN32_WINNT 0x0500 # i fndef PROTECTED_DACL_SECURITY_INFORMATION #define PROTECTED_DACL_SECURITY_INFORMATION 0x80000000L #endif #include <stdio.h> #include <windows.h> #include <lm.h> #include <aclapi.h> # include < sddl.h>
Глава 40. Работа с дескрипторами безопасности 829 int main() { char chFileName[256]; // имя файла HANDLE hFile; // дескриптор файла PSECURITY_DESCRIPTOR pSecurityDescriptor; // указатель на SD PACL pDacl; // указатель на DACL DWORD dwRetCode; // код возврата // вводим полное имя файла, например: C:\Xtest.txt printf("Input a full path to your file: "); gets(chFileName); // открываем файл hFile = CreateFile( chFileName, // имя файла READ_CONTROL | WRITE_DAC, // доступ к дескриптору безопасности О, //не разделяем доступ NULL, // ненаследуемый дескриптор OPEN_EXISTING, // открываем существующий файл FILE_ATTRIBUTEJNORMAL, // обычный файл NULL); // шаблона нет if(hFile == INVALID_HANDLE_VALUE) { dwRetCode = GetLastError(); printf("Create file failed.\n"); printf("Error code: %u\n", dwRetCode); return dwRetCode; } // получаем дескриптор безопасности файла dwRetCode = GetSecuritylnfо( hFile, // дескриптор файла SE_FILE_OBJECT, // объект файл DACL_SECURITY_INFORMATION, // тип информации NULL, // указатель на SID владельца не нужен NULL, // указатель на первичную группу не нужен
830 Часть XI. Управление безопасностью в Windows &pDacl, // указатель на DACL NULL, // указатель на SACL не нужен SpSecurityDescriptor); // адрес указателя на SD if (dwRetCode I= ERROR_SUCCESS) { printf("Get named security info failed.\n"); printf("Error code: %u\n", dwRetCode); return dwRetCode; } // сбрасьшаем флаг автоматического наследования DACL if(!SetSecurityDescriptorControl( pSecurityDescriptor, SE_DACL_PROTECTED, 0)) { dwRetCode = GetLastError(); printfC'Set security descriptor control failed."); printf("Error code: %u\n"/ dwRetCode); return dwRetCode; } // устанавливаем новые свойства DACL dwRetCode = SetSecuritylnfо( hFile, // дескриптор файла SE_FILE_OBJECT/ // объект файл DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION/ // изменяем информацию о DACL NULL, // владельца не изменяем NULL, // первичную группу не изменяем pDacl, // DACL изменяем NULL); // SACL не изменяем if (dwRetCode != ERROR_SUCCESS) { printfC'Set named security info failed.\n"); printf("Error code: %u\n", dwRetCode);
Глава 40. Работа с дескрипторами безопасности 831 return dwRetCode; } // освобождаем память для дескриптора LocalFree(pSecurityDescriptor); printf("The flag SE_DACL_PROTECTED is reset.\n"); // закрываем файл CloseHandle(hFile); return 0; } 40.11. Строковое представление дескрипторов безопасности Дескриптор безопасности может быть преобразован в строку, которая имеет специальный формат. Формат этой строки описывается при помощи специального языка, который называется SDDL (Security Descriptor Definition Language) — язык описания дескриптора безопасности. Далее приведено краткое описание этой нотации. Строка, описывающая дескриптор безопасности, может быть разбита на четыре блока, которые последовательно описывают владельца объекта, первичную группу владельца объекта, списки SACL и DACL по аналогии со структурой security_descriptor. Эти описания имеют следующие форматы: П О: ЗЮ_владелыда_объекта; П G: ЗЮ_первичной_группы_владелыда_объекта; П D: флаги_БАСЬ(элемент_БАСЬ) ... (элемент_БАСЬ) ; П S: флаги_ЗАСЬ(элемент_ЗАСЬ) ... (элемент_ЗАСЬ) . Сначала кратко рассмотрим форматы каждого из этих описаний. Описание владельца объекта начинается с символов о:, за которыми следует символьное представление идентификатора безопасности владельца объекта. Описание первичной группы владельца объекта начинается с символов G-., за которыми следует символьное представление идентификатора безопасности первичной группы владельца объекта. Описание списка DACL начинается с символов D:, за которыми следуют символьные обозначения флагов, а затем перечисляются элементы списка DACL.
832 Часть XI. Управление безопасностью в Windows Описание списка SACL начинается с символов S:, за которыми следуют символьные обозначения флагов, а затем перечисляются элементы списка SACL. Теперь более подробно опишем форматы подстрок, входящих в каждый блок строкового представления дескриптора безопасности. Символьное представление идентификаторов безопасности Напомним, что символьное представление идентификаторов безопасности рассматривалось в разд. 36.5. Эти же символьные представления используются в двух первых блоках строки для описания идентификаторов безопасности владельца объекта и первичной группы владельца объекта. В языке SDDL определены следующие символьные обозначения для общеизвестных идентификаторов безопасности: □ до — sddl_account_operators — операторы учетных записей; □ до — sddl_anonymous — анонимный пользователь; □ au — sddl_authenticated_users — аутентифицированные пользователи; □ ва — sddl_builtin_administrators — встроенные локальные администраторы; □ bg — sddl_builtin_guests — встроенные локальные гости; □ во — sddl_backup_operators — операторы резервных копий; □ ви — sddl_builtin_users — встроенные пользователи; □ са — _cert_serv_administrators — сертифицированные администраторы сервера; □ cg — sddl_creator_group — создатель фуппы; □ со — sddl_creator_owner — создатель владельца; □ da — sddl_domain_administrators — администраторы домена; □ dc — sddl_domain_computers — компьютеры домена; □ DD — SDDL_DOMAIN_DOMAIN_CONTROLLERS — КОНТрОЛЛерЫ ДОМена; □ dg — sddl_domain_guests — гости домена; □ du — sddl_domain_users — пользователи домена; □ еа — sddl_enterprise_admins — администраторы предприятия; □ ED — SDDL_ENTERPRISE_DOMAIN_CONTROLLERS — КОНТрОЛЛерЫ ДОМена предприятия; □ iu — sddl_interactive — интерактивные пользователи; □ la — sddl_local_admin — локальные администраторы; □ LG — SDDL_LOCAL_GUEST — ЛОКЭЛЬНЫЙ ГОСТЬ; □ LS — SDDL_LOCAL_SERVICE — ЛОКЭЛЬНЫЙ СерВИС;
Глава 40. Работа с дескрипторами безопасности 833 П no- sddl_network_configuration_ops — оператор конфигурации сети; П ns- sddl_network_service — сетевой сервис; П nu- sddl_network — сетевой пользователь; □ pa — sddl_group_policy_admins — администратор политик групп; □ ро — sddl_printer_operators — операторы принтеров; П PS- SDDL_PERSONAL_SELF — ПерСОНЭЛЬНЫЙ; □ pu — sddl_power_users — полномочные пользователи; П rc- sddl_restricted_code — ограниченный код; П rd- sddl_remote_desktop — удаленный пользователь; □ re- sddl_replicator — репликатор; □ rs — sddl_ras_servers — сервер удаленных запросов; □ ru — sddl_alias_prew2kcompacc — для совместимости с Windows 2000; П sa- sddl_schema_administrators — администраторы схемы; □ so — sddl_server_operators — операторы сервера; □ su — sddl_service — сервис; П sy- sddl_local_system — локальная система; П wd- sddl_everyone — любой пользователь (World). Символьное представление флагов Флаги, управляющие списком DACL, имеют следующие символьные обозначения: □ р — sddl_protected — установлен флаг se_dacl_protected; □ ar — sddl_auto_inherit_req — установлен флаг se_dacl_auto_inherit_req; □ AI — SDDL_AUTO_INHERITED — установлен флаг SE_DACL_AUTO_INHERITED. Такие же символьные обозначения имеют и флаги, управляющие списком SACL: □ р — sddl_protected — установлен флаг se_sacl_protected; □ ar — sddl_auto_inherit_req — установлен флаг se_sacl_auto_iisiherit_req; □ AI — SDDL_AUTO_INHERITED — установлен флаг SE_SACL_AUTO_INHERITED. Заметим, что более подробно эти флаги рассматривались в предыдущем разделе. Символьное представление элементов списков Элементы списков управления доступом имеют следующее символьное представление: тип_элемента; флаги; права; Сию_объекта; СиЮ_объекта_предка; ЗЮ_учетной_записи 27 Зак. 514
834 Часть XI. Управление безопасностью в Windows Здесь guid является аббревиатурой для globally unique identifier, что переводится как "глобальный уникальный идентификатор". Такие идентификаторы используются для идентификации объектов. Теперь рассмотрим обозначения, используемые в языке SDDL для описания каждой подстроки из строки, описывающей элемент списка. Подстрока типэлемента описывает тип элемента списка DACL или SACL. Для типа элемента списка в языке SDDL используются следующие символьные обозначения: □ а — sddl_access_allowed — доступ разрешен; □ d — sddl_access_denied — доступ запрещен; □ au — sddl_audit — аудит; □ al — sddl_alarm — тревога. Подстрока флаги описывает управляющие флаги, установленные в элементе списка управления доступом. В языке SDDL управляющие флаги обозначаются следующим образом: □ ci — sddl_container_inherit — наследуется контейнером; □ 01 — sddl_object_inherit — наследуется не контейнером; □ np — sddl_no_propagate — наследование не распространять; □ ю — sddl_inherit_only — только наследование, управления доступом нет; □ id — sddl_inherited — наследуемый элемент; □ sa — sddl_audit_success — успешный аудит; □ fa — ddl_audit_failure — неудачный аудит. Подстрока флаги представляет собой набор вышеперечисленных символьных обозначений для управляющих флагов. Подстрока права описывает права доступа к объекту, которые контролирует данный элемент списка управления доступом. В языке SDDL права доступа обозначаются следующим образом. Родовые права доступа: □ ga — sddl_generic_all — чтение, запись и исполнение; □ gr — sddl_generic_read — только чтение; □ gw — sddl_generic_write — только запись; □ GX — SDDL_GENERIC_EXECUTE — ТОЛЬКО ИСПОЛНеНИС Стандартные права доступа: □ rc — sddl_read_control — чтение управляющих флагов; □ wd — sddl_write_dac — запись в список DACL;
Глава 40. Работа с дескрипторами безопасности 835 П wo- sddl_write_owner — запись владельца; □ sd — sddl_standard_delete — стандартное удаление. Права доступа к файлу: □ fa- sddl_file_all — полный доступ к файлу; □ ufr — sddl_file_read — чтение файла; П fw- sddl_file_write — запись в файл; П fx- sddl_file_execute — исполнение файла. Права доступа к ключу регистра: П ка- sddl_key_all — полный доступ к ключу регистра; □ кр — sddl_key_read — чтение ключа регистра; □ kw — sddl_key_write — запись в ключ регистра; □ кх — sddl_key_execute — исполнение ключа регистра Глобальные уникальные идентификаторы объекта и объекта предка представляют собой символьные строки для обозначения 128-битовых уникальных значений. Такая строка имеет следующий формат: Х0Х1Х2Х3Х4Х5Х6Х7-Х8Х9Х10ХЦ-Х12Х13Х14Х15—X16X17X18X19-X20X21X22X23X24X25X26X27X28X29X30X31 Символ х обозначает шестнадцатеричную цифру. Подстрока эюучетной записи описывает идентификатор безопасности учетной записи, для которой элемент списка контролирует доступ к объекту. Формат этой подстроки был описан в разд 36.5, посвященном идентификаторам безопасности. Символьные обозначения для общеизвестных идентификаторов безопасности, которые используются в языке SDDL, были рассмотрены также в разд. 36.5. Теперь перейдем к рассмотрению функций, которые используются для преобразования дескриптора безопасности в строковое представление и наоборот. Для преобразования дескриптора безопасности в строку используется функция ConvertSecurityDescriptorToStringSecurityDescriptor, Которая имеет следующий прототип: BOOL ConvertSecurityDescriptorToStringSecurityDescriptor( PSECURITY_DESCRIPTOR SecurityDescriptor, // адрес SD DWORD RequestedStringSDRevision, // версия SECURITY_INFORMATION Securitylnformation, // тип информации LPSTR *StringSecurityDescriptor/ // строка SD PULONG StringSecurityDescriptorLen // длина строки )/ В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. При неудаче код ошибки можно получить при
836 Часть XI. Управление безопасностью в Windows помощи функции GetLastError, которая может вернуть одно из следующих значений: □ error_invalid_parameter — неправильный параметр; □ error_unknown_revision — неизвестная версия дескриптора безопасности; □ error_none_mapped — неверный SID в дескрипторе безопасности; □ ERROR_INVALID_ACL — установлен флаг SE_DACL_PRESENT, НО СаМОГО СПИ- ска DACL нет. Параметры функции ConvertSecurityDescriptorToStringSecurityDescriptor имеют следующее назначение. Параметр securityDescriptor должен указывать на дескриптор безопасности, который может находиться как в абсолютном формате, так и в относительном формате. Параметр RequestedstringSDRevision должен содержать версию формата выходной строки, в которую будет преобразован дескриптор безопасности. В настоящее время в этом параметре должно быть установлено значение SDDL_REVISION_l. В параметре securityinformation должна быть установлена любая комбинация следующих флагов: □ OWNER_SECURITY_INFORMATION — ВКЛЮЧИТЬ В СТрОКу Информацию О ВЛа- дельце; П GROUP_SECURITY_INFORMATION — ВКЛЮЧИТЬ В СТрОКу Информацию О Пер- вичной группе; □ DACL_SECURITY_INFORMATION — ВКЛЮЧИТЬ В СТрОКу Информацию О СПИСКе DACL; П sacl_security_information — включить в строку информацию о списке SACL. Флаги указывают на то, какую информацию из дескриптора безопасности нужно включить в его строковое представление. Параметр stringsecurityDescriptor должен содержать адрес указателя на строку. В этот указатель функция поместит адрес строки, в которую был преобразован дескриптор безопасности. После завершения работы со строкой программа должна освободить занимаемую этой строкой память при ПОМОЩИ функции LocalFree. Параметр stringSecurityDescriptorLen должен указывать на переменную, в которую функция запишет длину памяти, которую занимает строковое представление дескриптора безопасности. Если эта длина не нужна, то в этом параметре может быть установлено значение null.
Глава 40. Работа с дескрипторами безопасности 837 В листинге 40.10 приведена программа, которая выводит на консоль строку, описывающую дескриптор безопасности файла, используя для получения ЭТОЙ строки функцию ConvertSecurityDescriptorToStringSecurityDescriptor. | Листинг 40.10. Преобразование дескриптора безопасности в строковое | представление #define _WIN32_WINNT 0x0500 #include <stdio.h> #include <windows.h> #include <lm.h> #include <aclapi.h> #include <sddl.h> int main() { char chFileName[256]; // имя файла PSECURITY_DESCRIPTOR pSecurityDescriptor; // указатель на SD LPSTR StringSecurityDescriptor; // строка с SD DWORD StringSecurityDescriptorLen; // длина строки с SD DWORD dwRetCode; // код возврата // вводим имя файла, например: C:\Xtest.txt printf("Input a full path to your file: "); gets (chFileName) ; // получаем дескриптор безопасности файла dwRetCode = GetNamedSecuritylnfо( chFileName, // имя файла SE_FILE_OBJECT, // объект файл DACL_SECURITY_INFORMATION, // читаем список DACL NULL, // указатель на владельца не нужен NULL, // указатель на первичную группу не нужен NULL, // указатель на DACL не нужен NULL, // указатель на SACL не нужен &pSecurityDescriptor); // адрес указателя на SD
838 Часть XI. Управление безопасностью в Windows if (dwRetCode != ERROR_SUCCESS) { printf("Get named security info failed.\n"); printf("Error code: %u\n", dwRetCode); return dwRetCode; } // преобразуем дескриптор безопасности в строку if(IConvertSecurityDescriptorToStringSecurityDescriptor( pSecurityDescriptor, // адрес дескриптора безопасности SDDL_REVISI0N_1, // версия языка описания OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION | SACL_SECURITY_INFORMATION, • ScStringSecurityDescriptor, // буфер для строки &StringSecurityDescriptorLen)) // длина буфера { dwRetCode = GetLastError(); printf("Convert security descriptor to string security descriptor failed."); printf("Error code: %u\n", dwRetCode); return dwRetCode; } printf("String security descriptor length: %u\n", StringSecurityDescriptorLen); printf("String security desriptor: %s\n"/ StringSecurityDescriptor); // освобождаем память LocalFree(pSecurityDescriptor); LocalFree(StringSecurityDescriptor); return 0;
Глава 40. Работа с дескрипторами безопасности 839 Для преобразования дескриптора безопасности из строкового представления в бинарное представление используется функция ConvertStringSecurityDescriptorToSecurityDescriptor, которая имеет следующий прототип: BOOL ConvertStringSecurityDescriptorToSecurityDescriptor( LPCSTR StringSecurityDescriptor, // строка с SD DWORD StringSDRevision, // версия дескриптора безопасноти PSECURITY_DESCRIPTOR *SecurityDescriptor, // адрес буфера для SD PULONG SecurityDescriptorSize // длина буфера ); В случае успешного завершения функция возвращает ненулевое значение, а в случае неудачи — false. При неудаче код ошибки можно получить при помощи функции GetLastError, которая может вернуть одно из следующих значений: □ error_invalid_parameter — неправильный параметр; □ error_unknown_revision — неизвестная версия дескриптора безопасности; □ error_none_mapped — неверный SID в дескрипторе безопасности. Параметры функции ConvertStringSecurityDescriptorToSecurityDescriptor имеют следующее назначение. Параметр stringSecurityDescriptor должен указывать на строку с описанием дескриптора безопасности в формате, приведенном в начале раздела. Параметр StringSDRevision должен содержать версию формата входной строки, из которой будет получено бинарное представление дескриптора безопасности. В настоящее время в этом параметре должно быть установлено значение sddl_revision_i. Параметр securityDescriptor должен содержать адрес указателя на структуру типа security_descriptor. В этот указатель функция поместит адрес дескриптора безопасности, в который была преобразована строка. Дескриптор безопасности будет создан в относительном формате. После завершения работы с дескриптором безопасности программа должна освободить занимаемую дескриптором безопасности память при помощи функции LocalFree. Параметр SecurityDescriptorSize ДОЛЖен указывать на переменную, В КО- торую функция запишет длину памяти, которую занимает дескриптор безопасности. Если эта длина не нужна, то в этом параметре может быть установлено значение mjll.
Глава 41 Работа со списками управления доступом на высоком уровне В этой главе будут рассмотрены функции для работы со списками управления доступом на высоком уровне. То есть доступ к элементам списка будет осуществляться при помощи специальных структур trustee и explicit_access, которые содержат информацию об элементах этих списков. А именно, структура типа trustee используется для идентификации учетных записей, которым соответствуют элементы списка управления доступом. А структуры типа explicit_access используются для доступа к самим элементам списка управления доступом. При этом структура типа explicit_access содержит поле, которое имеет тип trustee. Работа со списками управления доступом на низком уровне будет рассмотрена в гл. 44. 41.1. Структура TRUSTEE Для идентификации учетных записей, которым соответствуют элементы из списка управления доступом, используется структура типа trustee. Тип этой структуры определен следующим образом: typedef struct _TRUSTEE { struct _TRUSTEE *pMultipleTrustee; //не поддерживается MULTIPLE_TRUSTEE_OPERATION MultipleTrusteeOperation; // замещение TRUSTEE_FORM TrusteeForm; // тип имени учетной записи TRUSTEE_TYPE TrusteeType; // тип учетной записи LPSTR ptstrName; // указатель на имя или SID учетной записи } TRUSTEE_A, *PTRUSTEE_A, TRUSTEEA, *PTRUSTEEA; Поле pMuitipieTrustee должно указывать на структуру trustee, описывающую учетную запись сервера, которая может замещать (impersonates) данную структуру trustee. В настоящее время эта возможность не поддерживается и поэтому в поле pMuitipieTrustee должно быть установлено значение null.
Глава 41. Работа со списками управления доступом на высоком уровне 841 Поле MultipieTrusteeOperation должно содержать одно из значений перечисления типа: typedef enum _MULTIPLE_TRUSTEE_OPERATION { // учетная запись не замещена учетной записью сервера NO_MULTIPLE_TRUSTEE, // учетная запись замещена учетной записью сервера TRUSTEE_IS_IMPERSONATE, } MULTIPLE_TRUSTEE_OPERATION ; Если в этом поле установлено значение trustee_is_impersonate, to поле pMuitipieTrustee должно содержать информацию о структуре trustee сервера, которая замещает (impersonates) данную структуру trustee. Так как в настоящее время замещение структур trustee не поддерживается, то в этом поле должно быть установлено значение no_multiple_trustee. Поле TrusteeForm должно содержать одно из значений перечисления типа: typedef enum _TRUSTEE_FORM { // указатель ptstrName содержит адрес SID учетной записи TRUSTEE_IS_SID, // указатель ptstrName содержит адрес имени учетной записи TRUSTEE_IS_NAME, TRUSTEE_BAD_FORM // недействительная форма } TRUSTEE_FORM; Оно определяет тип информации, на которую указывает указатель ptstrName. Поле TrusteeType должно содержать одно из значений перечисления типа: typedef enum _TRUSTEE_TYPE { TRUSTEE_IS_UNKNOWN, // тип не известен TRUSTEE_IS_USER, // пользователь TRUSTEE_IS_GROUP, // группа TRUSTEE_IS_DOMAIN, // домен TRUSTEE_IS_ALIAS/ // псевдоним TRUSTEE_IS_WELL_KNOWN_GROUP/ // общеизвестная группа TRUSTEE_IS_DELETED/ // учетная запись удалена TRUSTEE_IS_INVALID, // недействительный тип } TRUSTEE_TYPE; Оно определяет тип учетной записи, которую идентифицирует данная структура trustee. Поле ptstrName должно содержать адрес имени или идентификатора учетной записи, который идентифицирует данная структура trustee, в зависи-
842 Часть XI. Управление безопасностью в Windows МОСТИ ОТ значения ПОЛЯ TrusteeForm. ЕСЛИ В ПОЛе TrusteeForm установлено значение trustee_is_sid, to поле ptstrName указывает на идентификатор учетной записи с одним из следующих типов SID: П SID учетной записи пользователя; □ SID учетной записи группы; П универсальный SID; П общеизвестный SID. Если же в поле TrusteeForm установлено значение trustee_is_name, то поле ptstrName указывает на имя учетной записи. Причем имя учетной записи может быть задано в одном из следующих форматов: П полное имя учетной записи: • "имя_домена\имя_пользователя". □ имя предопределенной группы, например: • "EVERYONE"; • "GUEST". П одно из следующих специальных имен: • "creator group" — первичная группа создателя объекта; • "creator owner" — создатель объекта; • "current_user" — текущий процесс или поток. Для работы со структурами типа trustee используются следующие функции: BuildTrusteeWithName, BuildTrusteeWithSid, GetTrusteeForm, GetTrusteeType И GetTrusteeName. Первые две ИЗ ЭТИХ функций предназначены для инициализации структур типа trustee, а последние три — для получения информации из таких структур. Работа с этими функциями будет рассмотрена в последующих разделах этой главы. 41.2. Инициализация структуры TRUSTEE Для инициализации структуры типа trustee, используя имя учетной записи, Предназначена фунКЦИЯ BuildTrusteeWithName, КОТОрая Имеет следующий прототип: VOID BuildTrusteeWithName( PTRUSTEE pTrustee, // указатель на структуру типа TRUSTEE LPSTR pName // указатель на имя ); Эта функция не возвращает значения. Первый параметр pTrustee этой функции должен указывать на структуру типа trustee, которая будет ини-
Глава 41. Работа со списками управления доступом на высоком уровне 843 циализироваться. А параметр pName должен указывать на имя учетной записи, КОТОРЫМ будет Инициализирована Структура. ФУНКЦИЯ BuildTrusteeWithName устанавливает следующие значения в поля инициализируемой структуры trustee: // структуры TRUSTEE для сервера нет pMultipleTrustee = NULL; // замещения TRUSTEE нет MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE; // ptstrName указывает на имя учетной записи TrusteeForm = TRUSTEE_IS_NAME; // тип учетной записи не известен TrusteeType = TRUSTEE_IS_UNKNOWN; ptstrName = pName; // указатель на имя учетной записи В листинге 41.1 приведена программа, в которой структура типа trustee инициализируется, используя специальное имя, отмечающее текущего пользователя. #include <stdio.h> #include <windows.h> # include <aclapi.h> int main() { TRUSTEE Trustee; char trustee_name[] = "CURRENT_USER"; // имя пользователя // строим структуру TRUSTEE по имени BuildTrus teeWithName (&Trustee, trustee_name) ; // проверяем значения полей if (Trustee.pMultipleTrustee == NULL) printf("Server trustee: NULL\n"); if (Trustee.MultipleTrusteeOperation == NO_MULTIPLE_TRUSTEE) printf("Multiple trustee: NO_MULTIPLE_TRUSTEE\n"); if (Trustee.TrusteeForm == TRUSTEE_IS_NAME) printf("Trustee form: TRUSTEE_IS_NAME\n");
844 Часть XI. Управление безопасностью в Windows if (Trustee.TrusteeType == TRUSTEE_IS_UNKNOWN) printf("Trustee type: TRUSTEE_IS_UNKNOWN\n"); printf("Trustee name: %s\n"/ Trustee.ptstrName); return 0; } Для инициализации структуры типа trustee, используя идентификатор учетной записи, предназначена функция BuildTrusteewithsid, которая имеет следующий прототип: VOID BuildTrusteewithsid( PTRUSTEE pTrustee, // указатель на структуру типа TRUSTEE PSID pSid // указатель на идентификатор безопасности ); Единственное ОТЛИЧИе ЭТОЙ фуНКЦИИ ОТ фуНКЦИИ BuildTrusteeWithName состоит в том, что второй параметр этой функции должен указывать на идентификатор безопасности учетной записи и, соответственно, значения полей TrusteeForm и ptstrName устанавливаются следующим образом: // ptstrName указывает на SID учетной записи TrusteeForm = TRUSTEE_IS_SID; // указатель на SID учетной записи ptstrName = pSid; Ниже приведена программа, в которой структура типа trustee инициализируется, используя идентификатор безопасности. i Листинг 41 .2. Инициализация структуры типа trustee с использованием j идентификатора безопасности #define _WIN32_WINNT 0x0500 #include <stdio.h> # include <windows.h> #include <aclapi.h> #include <sddl.h> int main () { TRUSTEE Trustee; // информация об учетной записи
Глава 41. Работа со списками управления доступом на высоком уровне 845 II идентификатор учетной записи SID_IDENTIFIER_AUTHORITY sia = SECURITY_WORLD_SID_AUTHORITY; // относительный идентификатор учетной записи DWORD dwSubAuthority = SECURITY_WORLD_RID; PSID IpSid = NULL; // указатель на SID LPTSTR IpStringSid = NULL; // указатель на строку с SID // создаем SID for Everyone if(!AllocateAndInitializeSid( &sia, // адрес идентификатора учетной записи 1, // количество относительных идентификаторов учетной записи dwSubAuthority, // первый RID О, 0, 0, 0, 0, 0, 0, // остальные RID равны О &lpSid) ) { DWORD dwRetCode = GetLastError(); printf( "Allocate and initialize sid failed %u\n"/ dwRetCode); return dwRetCode; } // строим структуру TRUSTEE по идентификатору безопасности BuildTrusteeWithSid(&Trustee, IpSid); // проверяем значения полей if (Trustee.pMultipleTrustee == NULL) printf("Server trustee: NULL\n"); if (Trustee.MultipleTrusteeOperation == NO_MULTIPLE_TRUSTEE) printf("Multiple trustee: NO_MULTIPLE_TRUSTEE\n"); if (Trustee.TrusteeForm == TRUSTEE_IS_SID) printf("Trustee form: TRUSTEE_IS_SID\n"); if (Trustее.TrusteeType == TRUSTEE_IS_UNKNOWN) printf("Trustee type: TRUSTEE_IS_UNKNOWN\n"); // преобразуем SID в строку if (!ConvertSidToStringSid(Trustee.ptstrName, SclpStringSid))
846 Часть XI. Управление безопасностью в Windows { printf("Convert SID to string SID failed."); return GetLastError(); } // распечатываем SID printf("SID: %s\n", IpStringSid); // освобождаем SID FreeSid(lpSid); return 0; } 41.3. Структура EXPLICIT_ACCESS Структуры типа trustee идентифицируют учетные записи, для которых существуют элементы в списках управления доступом к охраняемым объектам. Сами же элементы этих списков для доступа на высоком уровне описываются структурами типа explicit_access. Тип этой структуры определен следующим образом: typedef struct _EXPLICIT_ACCESS { DWORD grfAccessPermissions; // права доступа ACCESS_MODE grfAccessMode; // режим доступа DWORD grfInheritance; // наследование элемента TRUSTEE Trustee; // учетная запись } EXPLICIT_ACCESS, *PEXPLICIT_ACCESS; В поле grfAccessPermissions устанавливаются права доступа к объекту, которыми владеет или нет учетная запись, связанная с элементом списка управления доступом. При доступе субъекта к охраняемому объекту система сверяет затребованные субъектом режимы доступа к объекту с правами доступа, которые устанавливаются в этом поле. То есть эти права доступа контролируют доступ потоков к охраняемому объекту. Все права доступа к охраняемому объекту делятся на четыре категории: □ специфические права доступа (specific access rights), т. е. такие права, которые присущи только данному объекту; □ стандартные права доступа (standard access rights), т. е. такие права, которые присущи всем объектам;
Глава 41. Работа со списками управления доступом на высоком уровне 847 □ родовые права доступа (generic access rights), т. е. такие права, которые используются всеми объектами, но при их реализации отображаются в специфические права для каждого объекта; □ права доступа к списку SACL. Ниже перечислены флаги, соответствующие правам доступа из каждой категории, исключая специфические права доступа, которые зависят от типа охраняемого объекта. Правда, для специфических прав доступа определена символьная константа specific_rights_all, которая включает все права доступа. Эта константа устанавливает для охраняемого объекта флаги всех специфических прав доступа. Специфические права доступа к конкретным объектам перечислены в гл. 45 (при рассмотрении управлением безопасностью объектов на низком уровне). Стандартные права доступа задаются следующими символьными константами: □ delete — право удаления объекта; □ read_control — право чтения управляющей информации из дескриптора безопасности, исключая содержимое списка DACL; □ synchronize — право использования объекта для синхронизации; □ write_dac — право записи в список DACL объекта; □ write_owner — право записи владельца объекта; Кроме того, для стандартных прав доступа определены символьные константы, которые включают несколько основных прав доступа: □ standard_rights_all — включает все права доступа; □ standard_rights_required — включает права delete, read_control, write_dac и write_owner; □ standard_rights_read — эквивалентно read_control; □ standard_rights_write — эквивалентно read_control; □ standard_rights_execute — эквивалентно read_control. Родовые права доступа: □ generic_read — право читать объект; □ generic_write — право записывать объект; □ generic_execute — право исполнять объект. Следующее родовое право включает все основные родовые права доступа: generic_all — включает права generic_read, generic_write и GENERIC_EXECUTE. Права доступа к списку SACL включают единственное право access_system_security — право доступа к списку SACL.
848 Часть XI. Управление безопасностью в Windows Если структура типа explicit_access используется для доступа к элементам ИЗ СПИСКа DACL, ТО В ПОЛе grfAccessPermissions МОЖет быть установлена любая комбинация флагов, отмечающих специфические, стандартные и родовые права доступа. Если структура типа explicit_access используется ДЛЯ доступа К Элементам ИЗ СПИСКа SACL, TO В ПОЛе grfAccessPermissions имеет смысл устанавливать только флаг доступа access_system_security. Теперь перейдем к полю grfAccessMode структуры типа explicit_access. В этом поле может быть установлена одна из констант, которая принадлежит следующему перечислению: typedef enum _ACCESS_MODE { NOT_USED_ACCESS =0, //не используется GRANT_ACCESS, // добавить права доступа к существующим правам SET_ACCESS, // установить права доступа, удалив существующие права DENY_ACCESS, // запретить права доступа REVOKE_ACCESS, // удалить все элементы из списка управления доступом SET_AUDIT_SUCCESS,// установить флаг генерации сообщения при // успешном открытии доступа к охраняемому объекту SET_AUDIT_FAILURE // установить флаг генерации сообщения при // неудачном открытии доступа к охраняемому объекту } ACCESS_MODE; Она указывает на режим использования прав доступа, установленных в поле grfAccessPermissions. To есть добавляет, запрещает или удаляет права доступа к данному охраняемому объекту для учетной записи, идентифицируемой полем Trustee этой же структуры. Отметим, что константы set_audit_success и set_audit_failure имеет смысл устанавливать только в структурах explicit_access, которые используются для доступа к элементам списка управления доступом SACL. Причем эти константы могут быть установлены одновременно, обращаясь с ними как с управляющими флагами. В поле grfinheritance структуры explicit_access устанавливаются флаги, которые управляют наследованием элементов из списка управления доступом. Возможно установить любую комбинацию следующих флагов: □ container_inherit_ace — элемент наследуется только контейнерными дочерними объектами; □ object_inherit_ace — элемент наследуется только не контейнерными дочерними объектами; □ sub_containers_only_inherit — тоже, что и container_inherit_ace; □ sub_objects_only_inherit — тоже, что и object_inherit_ace; □ sub_containers_and_objects_inherit — элемент наследуется как контейнерными, так и неконтейнерными дочерними объектами;
Глава 41. Работа со списками управления доступом на высоком уровне 849 □ inherit_only_ace — отмечает, что элемент был унаследован от родительского объекта, но этот элемент не участвует в контроле доступа к данному объекту; □ no_propagate_inherit_ace — элемент наследуется, но флаги object_imherit_ace и container_imherit_ace не устанавливаются в наследованных элементах. Последнее поле — Trustee — имеет тип trustee и идентифицирует учетную запись пользователя, для которой создается данный элемент списка управления доступом. Структура типа trustee и ее инициализация были рассмотрены в разд. 41.1, 41.2. 41.4. Инициализация структуры EXPLICIT_ACCESS Для инициализации структуры типа explicit_access используется функция BuildExplicitAccessWithName, КОТОраЯ Имеет следующий ПрОТОТИП! VOID BuildExplicitAccessWithName ( PEXPLICIT_ACCESS pExplicitAccess, // адрес структуры EXPLICIT_ACCESS LPSTR pTrusteeName, // имя учетной записи DWORD AccessPermissions, // флаги доступа ACCESS_MODE AccessMode, // режим доступа DWORD Inheritance // режим наследования ); Эта функция не возвращает ничего. Параметры функции BuildExplicitAccessWithName имеют следующее назначение. Параметр pExplicitAccess должен указывать на структуру типа explicit_access, которую будет инициализировать функция. Параметр pTrusteeName должен указывать на имя учетной записи, которым будет инициализирована структура типа trustee, входящая в структуру типа explicit_access, заданную параметром pExplicitAccess. Функция инициализирует поля этой структуры trustee таким же образом, как это делает ФУНКЦИЯ BuildTrusteeWithName, описанная В разд. 41.2. Параметр AccessPermissions ДОЛЖен содержать маску установленных фла- гов, которая будет перенесена в поле grfAccessPermissions структуры explicit_access, на которую указывает параметр pExplicitAccess. Для ус- тановки флагов нужно использовать символьные константы, которые определяют права доступа к объекту и были рассмотрены в предыдущем разделе При описании ПОЛЯ grfAccessPermissions структуры EXPLICIT_ACCESS.
850 Часть XI. Управление безопасностью в Windows Параметр AccessMode должен содержать режим доступа к элементу списка управления доступом. В этом параметре должно быть установлено одно из значений перечислимой константы типа access_mode, который был рассмотрен в предыдущем разделе при описании поля grfAccessMode структуры типа explicit_access. Параметр inheritance должен содержать режим наследования элемента списка управления доступом, для которого инициализируется структура типа explicit_access. В этом параметре можно установить любую комбинацию флагов, которые были рассмотрены в предыдущем разделе при описании ПОЛЯ grf Inheritance структуры ТИПа EXPLICIT_ACCESS. Программа, в которой структура типа explicit_access инициализируется, ИСПОЛЬЗУЯ фуНКЦИЮ BuildExplicitAccessWithName, Приведена В разд. 41.5. 41.5. Создание нового списка управления доступом Как для создания новых списков управления доступом, так и для модификации существующих списков управления доступом предназначена функция setEntriesinAci, которая имеет следующий прототип: DWORD SetEntriesinAci( ULONG cCountOfExplicitEntries, // количество элементов в массиве PEXPLICIT_ACCESS pListOfExplicitEntries, // массив структур PACL OldAcl, // адрес старого списка PACL *NewAcl // адрес указателя на новый список ); В случае успешного завершения функция возвращает значение error_success, а в случае неудачи — код ошибки. Параметры функции имеют следующее назначение. Параметр cCountOfExplicitEntries должен содержать количество элементов в массиве структур типа explicit_access, на который указывает параметр pListOfExplicitEntries. Параметр pListOfExplicitEntries должен указывать на массив структур типа explicit_access, которые содержат информацию о добавляемых в список управления доступом элементах. Параметр oldAci должен указывать на старый список управления доступом. Если в этом параметре установлено значение null, to система создает новый список управления доступом. Параметр *NewAci должен содержать адрес указателя на список управления доступом. В случае успешного завершения в этот указатель функция запи-
Глава 41. Работа со списками управления доступом на высоком уровне 851 шет адрес нового списка управления доступом, который создается системой путем присоединения элементов из массива pListofExpiicitEntries к старому списку управления доступом, на который указывает параметр oidAci. Память под новый список управления доступом резервирует система. Поэтому после использования нового списка управления доступом эту память НУЖНО ОСВОбоДИТЬ Путем ВЫЗОВа фуНКЦИИ LocalFree. Сделаем важное замечание относительно порядка элементов в новом списке управления доступом, который создает функция SetEntriesinAci. Элемен- ты типа access_denied_ace, т. е. запрещающие доступ к объекту, включаются в начало нового списка управления доступом. Элементы типа access_allowed_ace, т. е. запрещающие доступ к объекту, включаются в новый список управления доступом после всех запрещающих доступ элементов, но перед всеми разрешающими доступ элементами из старого списка управления доступом к охраняемому объекту. В листинге 41.3 приведена программа, в которой создается новый список управления доступом DACL, используя функцию SetEntriesinAci. После создания список DACL устанавливается в дескриптор безопасности объекта. Затем создается новый каталог, атрибуты защиты которого содержат адрес созданного дескриптора безопасности. В результате к созданному каталогу разрешается доступ только тому пользователю, для учетной записи которого был создан элемент в списке управления доступом DACL. | Листинг 41 »3« Создание нового списка управления доступом DACL \ #ifndef UNICODE #define UNICODE #endif #include <windows.h> #include <stdio.h> #include <lm.h> #include <aclapi.h> #include <accctrl.h> int main() { wchar_t wchDirName[248]; // имя каталога wchar_t wchAccountName [UNLEN +255]; // имя учетной записи SECURITY_DESCRIPTOR sd; // дескриптор безопасности каталога
852 Часть XI. Управление безопасностью в Windows SECURITY_ATTRIBUTES sa; // атрибуты защиты каталога EXPLICIT_ACCESS ea; // информация для элемента списка DACL PACL lpNewDacl; // указатель на список DACL DWORD dwErrCode; // код возврата // инициализируем версию дескриптора безопасности if (!InitializeSecurityDescriptor( &sd, SECURITY_DESCRIPTOR_REVISION)) { dwErrCode = GetLastError(); printf("Initialize security descroptor failed.\n"); printf("Error code: %d\n"/ dwErrCode); return dwErrCode; } // устанавливаем SID владельца объекта if (!SetSecurityDescriptorOwner( &sd, // адрес дескриптора безопасности NULL, //не задаем владельца SE_OWNER_DEFAULTED)) // определить владельца по умолчанию { dwErrCode = GetLastError(); perror("Set security descriptor owner failed.\n"); printf("The last error code: %u\n"/ dwErrCode); return dwErrCode; } // устанавливаем SID первичной группы владельца if (!SetSecurityDescriptorGroup( &sd, // адрес дескриптора безопасности NULL, //не задаем первичную группу SE_GROUP_DEFAULTED)) // определить первичную группу по умолчанию { dwErrCode = GetLastError();
Глава 41. Работа со списками управления доступом на высоком уровне 853 perror("Set security descriptor group failed.\n"); printfC'The last error code: %u\n"/ dwErrCode); return dwErrCode; } // вводим имя домена и пользователя printfC'The following user will have full access to directory. \n") printf("Input domain account (domain_name\\user_name): "); wscanf (L"%s", wchAccountName) ; // вводим имя учетной записи // строим структуру EXPLICIT_ACCESS по имени BuildExplicitAccessWithName ( &еа, // адрес структуры ExplicitAccess wchAccountName, // имя учетной записи GENERIC_ALL, // полный доступ к объекту SET_ACCESS, // установить доступ NO_INHERITANCE // нет наследования ); // создаем список DACL dwErrCode = SetEntriesInAcl( 1/ // один элемент &еа, // адрес структуры ExplicitAccess NULL, // старого DACL нет SlpNewDacl); // адрес нового DACL if (dwErrCode != ERROR_SUCCESS) { perror("Set entries in DACL failed.\n"); printfC'The last error code: %u\n", dwErrCode); return dwErrCode; } // устанавливаем DACL в дескриптор безопасности if (!SetSecurityDescriptorDacl( &sd, // адрес дескриптора безопасности TRUE, // DACL присутствует lpNewDacl, // указатель на DACL
854 Часть XI. Управление безопасностью в Windows FALSE)) // DACL не задан по умолчанию { dwErrCode = GetLastError(); perrorC'Set security descriptor group failed.\n"); printf("The last error code: %u\n"/ dwErrCode); return dwErrCode; } // проверяем структуру дескриптора безопасности if (!IsValidSecurityDescriptor(&sd)) { dwErrCode = GetLastError(); perror("Security descriptor is invalid.\n"); printf("The last error code: %u\n"/ dwErrCode); return dwErrCode; } // инициализируем атрибуты безопасности sa.nLength = sizeof(sa); // устанавливаем длину атрибутов защиты sa.IpSecurityDescriptor = &sd; // устанавливаем адрес дескриптора // безопасности sa.blnheritHandle = FALSE; // дескриптор каталога ненаследуемый // читаем имя создаваемого каталога printf("Input a directory name: "); wscanf(L"%s", wchDirName); // вводим имя каталога // создаем каталог if (ICreateDirectory(wchDirName, &sa)) { dwErrCode = GetLastError(); perror("Security descriptor is invalid.\n"); printf("The last error code: %u\n"/ dwErrCode); return dwErrCode;
Глава 41. Работа со списками управления доступом на высоком уровне 855 // освобождаем память, захваченную под DACL LocalFree(lpNewDacl); printf("The directory is created.\n"); return 0; } В листинге 41.4 приведена программа, в которой показано, как можно создать новый список SACL, используя функцию setEntriesinAci. Фактически эта программа является расширением программы из листинга 41.3. Отличие заключается в том, что для пользователя, которому предоставляется полный доступ к каталогу, в список SACL включается элемент, который управляет аудитом доступа этого пользователя к создаваемому каталогу. Сделаем следующие замечания. Для создания списка SACL требуется, чтобы пользователь имел действующую привилегию se_security_name. Такую привилегию имеют администраторы системы, но по умолчанию она недействительна. Поэтому прежде чем работать со списком SACL, эта привилегия должна быть активизирована. Для этого используется функция AdjustTokenPriviieges. Подробнее работа с привилегиями рассмотрена в гл. 43. | Листинг 41.4. Создание новых списков управления доступом DACL и SACL , j #ifndef UNICODE #define UNICODE #endif #include <windows.h> #include <stdio.h> #include <lm.h> #include <aclapi.h> int main() { HANDLE hProcess; // дескриптор процесса HANDLE hTokenHandle; // дескриптор маркера доступа TOKEN_PRIVILEGES tp; // привилегии маркера доступа wchar_t wchDirName[248]; // имя каталога
856 Часть XI. Управление безопасностью в Windows wchar_t wchAccountName [UNLEN + 255]; // имя учетной записи SECURITY_DESCRIPTOR sd; // дескриптор безопасности каталога SECURITY_ATTRIBUTES sa; // атрибуты защиты каталога EXPLICIT_ACCESS eaDacl; // информация для элемента списка DACL PACL lpNewDacl; // указатель на список DACL EXPLICIT_ACCESS eaSacl; // информация для элемента списка SACL PACL lpNewSacl; // указатель на список DACL DWORD dwErrCode; // код возврата // получаем дескриптор процесса hProcess = GetCurrentProcess(); // получаем маркер доступа процесса if (!OpenProcessToken( hProcess, // дескриптор процесса TOKEN_ALL_ACCESS, // полный доступ к маркеру доступа &hTokenHandle)) // дескриптор маркера { dwErrCode = GetLastError(); printf( "Open process token failed: %u\n", dwErrCode); return dwErrCode; } // устанавливаем общее количество привилегий tp.PrivilegeCount = 1; // определяем идентификатор привилегии для установки аудита if (!LookupPrivi1egeValue( NULL, // ищем идентификатор привилегии на локальном компьютере SE_SECTJRITY_NAME, // привилегия для аудита &(tp.Privileges[0].Luid))) { dwErrCode = GetLastError(); printf("Lookup privilege value failed.\n");
Глава 41. Работа со списками управления доступом на высоком уровне 857 printf("Error code: %d\n", dwErrCode); return dwErrCode; } // разрешаем привилегию аудита tp. Privileges [0] .Attributes = SE_PRIVILEGE_ENABLED; // разрешаем привилегию для установки аудита if (!AdjustTokenPrivileges( hTokenHandle, // дескриптор маркера доступа процесса FALSE, //не запрещаем все привилегии &tp, // адрес привилегий 0, // длины буфера нет NULL, // предыдущее состояние привилегий не нужно NULL)) // длина буфера не нужна { dwErrCode = GetLastError(); printf("Lookup privilege value failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode; } // инициализируем версию дескриптора безопасности if (!InitializeSecurityDescriptor( &sd, SECURITY_DESCRIPTOR_REVISION)) { dwErrCode = GetLastError(); printf("Initialize security descroptor failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode; } // устанавливаем SID владельца объекта if (!SetSecurityDescriptorOwner( &sd, // адрес дескриптора безопасности
858 Часть XI. Управление безопасностью в Windows NULL, //не задаем владельца SE_OWNER_DEFAULTED)) // определить владельца по умолчанию { dwErrCode = GetLastError(); perror("Set security descriptor owner failed.\n"); printf("The last error code: %u\n", dwErrCode); return dwErrCode; } // устанавливаем SID первичной группы владельца if (!SetSecurityDescriptorGroup( &sd, // адрес дескриптора безопасности NULL, //не задаем первичную группу SE_GROUP_DEFAULTED)) // определить первичную группу по умолчанию { dwErrCode = GetLastError(); perror("Set security descriptor group failed.\n"); printf("The last error code: %u\n", dwErrCode); return dwErrCode; } // вводим имя домена и пользователя printf("The following user will have full access to directory.\n"); printf("Input domain account (domain_name\\user_name): "); wscanf (L"%s", wchAccountName) ; // вводим имя учетной записи // строим структуру EXPLICIT_ACCESS по имени для списка DACL BuildExplicitAccessWithName( &eaDacl, // адрес структуры ExplicitAccess wchAccountName, // имя учетной записи GENERIC_ALL, // полный доступ к объекту SET_ACCESS, // установить доступ NO_INHERITANCE // нет наследования ); // создаем список DACL dwErrCode = SetEntriesInAcl(
Глава 41. Работа со списками управления доступом на высоком уровне 859 1, // один элемент &eaDacl, // адрес структуры ExplicitAccess NULL, // старого DACL нет SlpNewDacl); // адрес нового DACL if (dwErrCode != ERROR_SUCCESS) { perrorC'Set entries in DACL failed. \n") ; printf("The last error code: %u\n", dwErrCode); return dwErrCode; } // устанавливаем DACL в дескриптор безопасности if (!SetSecurityDescriptorDacl( &sd, // адрес дескриптора безопасности TRUE, // DACL присутствует lpNewDacl, // указатель на DACL FALSE)) // DACL не задан по умолчанию { dwErrCode = GetLastError(); perrorC'Set security descriptor group failed.\n"); printf("The last error code: %u\n", dwErrCode); return dwErrCode; } // строим структуру EXPLICIT_ACCESS по имени для списка SACL BuildExplicitAccessWithName ( &eaSacl, // адрес структуры ExplicitAccess wchAccountName, // имя учетной записи GENERIC_ALL, // полный доступ к объекту SET_AUDIT_SUCCESS, // установить доступ NO_INHERITANCE // нет наследования ); // создаем список SACL dwErrCode = SetEntriesInAcl( 1, // один элемент &eaSacl, // адрес структуры ExplicitAccess
860 Часть XI. Управление безопасностью в Windows NULL, // старого SACL нет SlpNewSacl); // адрес нового SACL if (dwErrCode != ERROR_SUCCESS) { perrorC'Set entries in DACL failed.\n"); printf("The last error code: %u\n", dwErrCode); return dwErrCode; } // устанавливаем SACL в дескриптор безопасности if (ISetSecurityDescriptorSacl( &sd, // адрес дескриптора безопасности TRUE, // SACL присутствует lpNewSacl, // указатель на SACL FALSE)) // SACL не задан по умолчанию { dwErrCode = GetLastError(); perrorC'Set security descriptor group failed.\n"); printfC'The last error code: %u\n", dwErrCode); return dwErrCode; } // проверяем структуру дескриптора безопасности if (!IsValidSecurityDescriptor(&sd)) { dwErrCode = GetLastError(); perror("Security descriptor is invalid.\n"); printfC'The last error code: %u\n", dwErrCode); return dwErrCode; } // инициализируем атрибуты безопасности sa.nLength = sizeof(sa); // устанавливаем длину атрибутов защиты // устанавливаем адрес дескриптора безопасности sa.lpSecurityDescriptor = &sd;
Глава 41. Работа со списками управления доступом на высоком уровне 861 // дескриптор каталога ненаследуемый sa.blnheritHandle = FALSE; // читаем имя создаваемого каталога printf("Input a directory name: "); wscanf(L"%s", wchDirName); // вводим имя каталога // создаем каталог if (!CreateDirееtory(wchDirName, &sa)) { dwErrCode = GetLastError(); perror("Security descriptor is invalid.\n"); printf("The last error code: %u\n", dwErrCode); return dwErrCode; } // запрещаем привилегию аудита tp.Privileges[0].Attributes = 0; // запрещаем привилегию для установки аудита if (!AdjustTokenPrivi1eges( hTokenHandle, // дескриптор маркера доступа процесса FALSE, //не запрещаем все привилегии &tp, // адрес привилегий 0, // длины буфера нет NULL, // предыдущее состояние привилегий не нужно NULL)) // длина буфера не нужна { dwErrCode = GetLastError(); printf("Lookup privilege value failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode; } // освобождаем память, захваченную под списки DACL и SACL LocalFree(lpNewDacl);
862 Часть XI. Управление безопасностью в Windows LocalFree(lpNewSacl); printf("The directory is created.\n") ; return 0; } 41.6. Модификация списка управления доступом Для модификации списков управления доступом, так же как и для создания НОВОГО СПИСКа, ИСПОЛЬЗуеТСЯ фуНКЦИЯ SetEntriesInAcl, ПроТОТИП КОТОроЙ был рассмотрен в разд. 41.5. В листинге 41.5 приведена программа, в которой эта функция используется для добавления элемента в список управления доступом DACL существующего каталога. Каталог можно создать, используя программу из листинга 41.3. | Листинг 41.5. Добавление элемента & список управления доступом DACL #ifndef UNICODE #define UNICODE #endif # include <windows.h> #include <stdio.h> #include <lm.h> #include <aclapi.h> int main() { wchar_t wchDirName[248] ; // имя каталога wchar_t wchAccountName [UNLEN + 255]; // имя учетной записи EXPLICIT_ACCESS ea; // информация для элемента списка DACL PACL lpOldDacl; // указатель на старый список DACL PACL lpNewDacl; // указатель на новый список DACL PSECURITY_DESCRIPTOR IpSD; // указатель на дескриптор безопасности
Глава 41. Работа со списками управления доступом на высоком уровне 863 DWORD dwErrCode; // код возврата // читаем имя созданного каталога printf("Input a directory name: "); wscanf(L"%s", wchDirName); // вводим имя каталога // получаем SD этого каталога dwErrCode = GetNamedSecurityInfo( wchDirName, // имя каталога SE_FILE_OBJECT/ // объект файл DACL_SECimiTY_INFORMATION, // получаем DACL NULL, // адрес указателя на SID владельца NULL, // адрес указателя на первичную группу &lp01dDacl, // указатель на DACL NULL, // указатель на SACL не нужен &lpSD); // адрес указателя на дескриптор безопасности if (dwErrCode != ERROR_SUCCESS) { printf("Get named security info failed.\n"); printf("Error code: %u\n", dwErrCode); return dwErrCode; } // вводим имя домена и пользователя для нового элемента списка DACL printf("The following user will be added in DACL.\n"); printf("Input domain account (domain_name\\user_name): "); wscanf(L"%s", wchAccountName); // вводим имя учетной записи // строим структуру EXPLICIT_ACCESS по имени BuildExplicitAccessWithName( &еа, // адрес структуры ExplicitAccess wchAccountName, // имя учетной записи GENERIC_ALL, // полный доступ SET_ACCESS, // установить доступ NO_INHERITANCE // нет наследования ); // создаем список DACL
864 Часть XI. Управление безопасностью в Windows dwErrCode = SetEntriesInAcl( 1, // добавляем один элемент в список DACL &еа, // адрес структуры ExplicitAccess lpOldDacl, // адрес старого списка DACL SlpNewDacl); // адрес указателя на новый список DACL if (dwErrCode != ERROR_SUCCESS) { perror("Set entries in DACL failed.\n"); printf("The last error code: %u\n'\ dwErrCode); return dwErrCode; } // устанавливаем новый список DACL dwErrCode = SetNamedSecurityInfo( wchDirName, // имя файла SE_FILE_OBJECT/ // объект файл DACL_SECURITY_INFORMATION/ // изменяем список DACL NULL, // владельца не изменяем NULL, // первичную группу не изменяем lpNewDacl, // новый DACL NULL); // SACL не изменяем if (dwErrCode != ERROR_SUCCESS) { printf("Set named security info failed.\n"); printf("Error code: %u\n"/ dwErrCode); return dwErrCode; } // освобождаем память LocalFree(lpSD); LocalFree(lpNewDacl); printf("The DACL of directory is modified.\n"); return 0;
Глава 41. Работа со списками управления доступом на высоком уровне 865 Теперь, в листинге 41.6, приведем программу, в которой функция SetEntriesinAcl используется для добавления по одному элементу в списки управления доступом DACL и SACL существующего каталога. Каталог можно создать, используя программу из листинга 41.4. Относительно следующей программы, как и в случае с программой из листинга 41.4, заметим, что для модификации списка SACL требуется, чтобы пользователь имел действующую привилегию se_security_name. Такую привилегию имеют администраторы системы, но по умолчанию она недействительна. Поэтому прежде чем модифицировать список SACL, эта привилегия должна быть активизирована. Для ЭТОГО ИСПОЛЬЗуетсЯ функция AdjustTokenPrivileges. Более подробно работа с привилегиями рассмотрена в гл. 43. Листинг 41.6, Добавление элементов в списки управления доступом DACL и SACL #ifndef UNICODE #define UNICODE #endif # include <windows.h> #include <stdio.h> #include <lm.h> #include <aclapi.h> int main() { HANDLE hProcess; HANDLE hTokenHandle; // дескриптор процесса // дескриптор маркера доступа TOKEN_PRIVILEGES tp; // привилегии маркера доступа wchar_t wchDirName[248]; // имя каталога wchar_t wchAccountName [UNLEN +255]; // имя учетной записи EXPLICIT_ACCESS eaSacl; EXPLICIT ACCESS eaDacl; // информация для элемента списка DACL // информация для элемента списка DACL PACL lpOldDacl; PACL lpNewDacl; PACL lpOldSacl; PACL lpNewSacl; // указатель на старый список DACL // указатель на новый список DACL // указатель на старый список SACL // указатель на новый список SACL 28 Зак. 514
866 Часть XI. Управление безопасностью в Windows PSECURITY_DESCRIPTOR IpSD; // указатель на дескриптор безопасности DWORD dwErrCode; // код возврата // получаем дескриптор процесса hProcess = GetCurrentProcess(); // получаем маркер доступа процесса if (!OpenProcessToken( hProcess, // дескриптор процесса TOKEN_ALL_ACCESS, // полный доступ к маркеру доступа fchTokenHandle)) // дескриптор маркера { dwErrCode = GetLastError(); printf( "Open process token failed: %u\n", dwErrCode); return dwErrCode; } // устанавливаем общее количество привилегий tp.PrivilegeCount = 1; // определяем идентификатор привилегии для установки аудита if (!LookupPrivilegeValue( NULL, // ищем идентификатор привилегии на локальном компьютере SE_SECURITY_NAME/ // привилегия для аудита &(tp.Privileges[0].Luid))) { dwErrCode = GetLastError(); printf("Lookup privilege value failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode; } // разрешаем привилегию аудита tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; // разрешаем привилегию для установки аудита
Глава 41. Работа со списками управления доступом на высоком уровне 867 if (!AdjustTokenPrivileges( hTokenHandle, // дескриптор маркера доступа процесса FALSE, //не запрещаем все привилегии &tp, // адрес привилегий О, // длины буфера нет NULL, // предыдущее состояние привилегий не нужно NULL)) // длина буфера не нужна { dwErrCode = GetLastError(); printf("Lookup privilege value failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode; } // читаем имя созданного каталога printf("Input a directory name: "); wscanf(L"%s", wchDirName); // вводим имя каталога // получаем SD этого каталога dwErrCode = GetNamedSecuritylnfо( wchDirName, // имя каталога SE_FILE_OBJECT/ // объект файл DACL_SECURITY_INFORMATION | // получаем DACL и SACL SACL_SECURITY_INFORMATION, NULL, // адрес указателя на SID владельца NULL, // адрес указателя на первичную группу SlpOldDacl, // указатель на DACL &lp01dSacl, // указатель на SACL &lpSD); // адрес указателя на дескриптор безопасности if (dwErrCode != ERROR_SUCCESS) { printf("Get named security info failed.\n"); printf("Error code: %u\n"/ dwErrCode); return dwErrCode; } // вводим имя домена и пользователя для нового элемента списка DACL
868 Часть XI. Управление безопасностью в Windows printf ("The following user will be added in DACL and SACL.\n"); printf("Input domain account (domain_name\\user_name): "); wscanf(L"%s", wchAccountName); // вводим имя учетной записи // строим структуру EXPLICIT_ACCESS для DACL по имени BuildExplicitAccessWithName( &eaDacl, // адрес структуры ExplicitAccess wchAccountName, // имя учетной записи GENERIC_READ, // только чтение SET_ACCESS, // установить доступ NO_INHERITANCE // нет наследования ); // создаем список DACL dwErrCode = SetEntriesInAcl( 1, // добавляем один элемент в список DACL SeaDacl, // адрес структуры ExplicitAccess lpOldDacl, // адрес старого списка DACL &lpNewDacl); // адрес указателя на новый список DACL if (dwErrCode != ERROR_SUCCESS) { perror("Set entries in DACL failed.\n"); printf("The last error code: %u\n", dwErrCode); return dwErrCode; } // строим структуру EXPLICIT_ACCESS для SACL по имени BuildExplicitAccessWithName( &eaSacl, // адрес структуры ExplicitAccess wchAccountName, // имя учетной записи GENERIC_READ, // только чтение SET_AUDIT_SUCCESS, // аудиторское сообщение при успешном доступе NO_INHERITANCE // нет наследования ); // создаем список SACL dwErrCode = SetEntriesInAcl( 1, // добавляем один элемент в список DACL
Глава 41. Работа со списками управления доступом на высоком уровне 869 SeaSacl, // адрес структуры ExplicitAccess lpOldSacl, // адрес старого списка DACL SclpNewSacl); // адрес указателя на новый список DACL if (dwErrCode != ERROR_SUCCESS) { perror("Set entries in SACL failed.\n"); printfC'The last error code: %u\n"/ dwErrCode); return dwErrCode; > // устанавливаем новые списки DACL и SACL dwErrCode = SetNamedSecuritylnfо( wchDirName, // имя файла SE_FILE_OBJECT/ // объект файл DACL_SECURITY_INFORMATION | // изменяем списки DACL SACL.SECURITY.INFORMATION, //и SACL NULL, // владельца не изменяем NULL, // первичную группу не изменяем lpNewDacl, // новый DACL lpNewSacl); // новый SACL if (dwErrCode != ERROR_SUCCESS) { printf("Set named security info failed.\n"); printf("Error code: %u\n"/ dwErrCode); return dwErrCode; } // запрещаем привилегию аудита tp.Privileges[0].Attributes = 0; // разрешаем привилегию для установки аудита if (!AdjustTokenPrivileges( hTokenHandle, // дескриптор маркера доступа процесса FALSE, //не запрещаем все привилегии &tp, // адрес привилегий 0, // длины буфера нет NULL, // предыдущее состояние привилегий не нужно
870 Часть XI. Управление безопасностью в Windows NULL)) // длина буфера не нужна { dwErrCode = GetLastError(); printf("Lookup privilege value failed.\n"); printf("Error code: %d\n", dwErrCode); return dwErrCode; } // освобождаем память LocalFree(lpSD); LocalFree(lpNewDacl); LocalFree(lpNewSacl); printf("The DACL and SACL of the directory are modified.\n"); return 0; } 41.7. Получение элементов из списка управления доступом Для получения информации, содержащейся в элементах списка управления ДОСТУПОМ, ИСПОЛЬЗуеТСЯ фуНКЦИЯ GetExplicitEntriesFromAcl, КОТОрая ИМе- ет следующий прототип: DWORD GetExplicitEntriesFromAcl( PACL pAcl, // указатель на список управления доступом PULONG pcCountofExpiicitEntries, // количество элементов в списке PEXPLICIT_ACCESS *pListOfExplicitEntries // адрес указателя на буфер ); В случае успешного завершения функция возвращает значение error_success, а в случае неудачи — код ошибки. Параметры функции имеют следующее назначение. Параметр pAcl должен указывать на список управления доступом, из которого извлекается информация, хранящаяся в элементах этого списка. Параметр pcCountofExpiicitEntries должен указывать на переменную типа ulong, в которую функция запишет количество элементов, находящихся в списке управления доступом.
Глава 41. Работа со списками управления доступом на высоком уровне 871 Параметр pListofExpiicitEntries должен содержать адрес указателя типа pexplicit_access. В случае успешного завершения функция запишет в этот указатель адрес массива структур типа explicit_access, в котором будет храниться информация из элементов списка управления доступом. Количество элементов в массиве равно числу, записанному функцией по адресу, заданному параметром pcCountofExpiicitEntries. Каждый элемент массива соответствует одному элементу из списка управления доступом. При этом заметим, что, начиная с операционной системы Windows 2000, функция не возвращает элементы из списка управления доступом, которые были наследованы от родительских объектов. Так как система сама распределяет память под массив структур типа explicit_access, to после использования ЭТУ ПаМЯТЬ НУЖНО ОСВОбоДИТЬ ПОСреДСТВОМ ВЫЗОВа фуНКЦИИ LocalFree. Программа, в которой для получения элементов из списка управления доступом DACL ИСПОЛЬЗуеТСЯ фуНКЦИЯ GetExplicitEntriesFromAcl, Приведена в листинге 41.7. 41.8. Получение информации из структуры TRUSTEE Для получения формы и типа структуры типа trustee используются функции GetTrusteeForm И GetTrusteeType соответственно. Для Получения ИЗ структуры trustee имени или идентификатора учетной записи используется ФУНКЦИЯ GetTrusteeName. ПроТОТИПЫ ЭТИХ фуНКЦИЙ ОПИСаНЫ НИЖС ФуНКЦИЯ GetTrusteeForm имеет следующий ПроТОТИП! TRUSTEE_FORM GetTrusteeForm ( PTRUSTEE pTrustee // указатель на структуру TRUSTEE ); Единственным параметром этой функции является указатель на структуру типа trustee, форму которой вернет функция. Функция возвращает одно из значений перечисления типа trustee_form, которое было подробно рассмотрено в разд. 41.1. ФуНКЦИЯ GetTrusteeType Имеет следующий ПроТОТИП! TRUSTEE_TYPE Ge tTrus teeType( PTRUSTEE pTrustee // указатель на структуру TRUSTEE ); Единственным параметром этой функции является указатель на структуру типа trustee, тип которой вернет функция. Функция возвращает одно из значений перечисления типа trustee_type, которое было подробно рассмотрено в разд. 41.1.
872 Часть XI. Управление безопасностью в Windows ФуНКЦИЯ GetTrusteeName имеет СЛедуЮЩИЙ ПроТОТИП! LPSTR GetTrusteeName( PTRUSTEE pTrustee // указатель на структуру TRUSTEE ); Единственным параметром этой функции является указатель на структуру типа trustee, из которой функция вернет указатель на имя учетной записи. Это имя может быть как символьной строкой, так и идентификатором безопасности в зависимости от формы структуры trustee. Подробнее об имени структуры trustee рассказано в разд. 41.1. В листинге 41.7 приведена программа, которая сначала получает элементы из списка управления доступом DACL, используя для этого функцию GetExplicitEntriesFromAcl. А затем получает информацию о структурах типа trustee, которые являются полями полученных структур типа EXPLICIT_ACCESS. \ Листинг 41 Л\ Получение элементов из списка управления доступом DACL I и получение информации из структуры типа trustee #define _WIN32_WINNT 0x0500 ttifndef UNICODE ttdefine UNICODE #endif # include <windows.h> #include <stdio.h> #include <aclapi.h> ttinclude <sddl.h> int main() { wchar_t wchDirName[248] ; // имя каталога PSECURITY_DESCRIPTOR IpSd; // указатель на дескриптор безопасности PACL lpDacl; // указатель на список DACL PEXPLICIT_ACCESS IpEa; // указатель на массив элементов типа // EXPLICIT_ACCESS ULONG ulCount; // количество элементов в массиве LPTSTR IpStringSid = NULL; // указатель на строку с SID
Глава 41. Работа со списками управления доступом на высоком уровне 873 DWORD dwErrCode; // код возврата // читаем имя файла printf("Input a file name: "); wscanf (L"%s", wchDirName) ; // вводим имя файла // получаем SD этого файла dwErrCode = GetNamedSecuritylnfо( wchDirName, // имя файла SE_FILE_OBJECT, // объект файл DACL_SECraiTY_INFORMATION, // получаем DACL NULL, // адрес указателя на SID владельца NULL, // адрес указателя на первичную группу &lpDacl, // указатель на DACL NULL, // указатель на SACL не нужен &lpSd); // адрес указателя на дескриптор безопасности if (dwErrCode != ERROR_SUCCESS) { printfC'Get named security info failed.\n") ; printf("Error code: %u\n", dwErrCode); return dwErrCode; } // читаем элементы из списка DACL dwErrCode = GetExplicitEntriesFromAcl( lpDacl, // адрес списка DACL &ulCount, // адрес для количества элементов &1рЕа); // адрес указателя на буфер if (dwErrCode != ERROR_SUCCESS) { printfC'Get explicit entries from ACL failed.\n"); printf("Error code: %u\n", dwErrCode); return dwErrCode; } printf("Number of elements in DACL = %u\n", ulCount); // получаем информацию из структур типа TRUSTEE
874 Часть XI. Управление безопасностью в Windows for (unsigned i = 0; i < ulCount; ++i) { if (GetTrusteeForm(&:(lpEa->T2rustee) ) == TRUSTEE_IS_NAME) wprintf (L"Trustee name: %s", GetTrusteeName (&(lpEa->Trustee) ) ) ; if (GetTrusteeForm(&(lpEa->Trustee)) == TRUSTEE_IS_SID) { // преобразуем SID в строку if (!ConvertSidToStringSid(lpEa->Trustee.ptstrName, SdpStringSid)) { printf("Convert SID to string SID failed."); return GetLastError(); } // распечатываем SID wprintf(L"Trustee SID: %s\n", IpStringSid); // освобождаем память, занятую строкой LocalFree(IpStringSid); } ++1рЕа; // продвигаемся по массиву } // освобождаем память LocalFree(lpSd); LocalFree(lpEa); return 0; } 41.9. Получение прав доступа из списка управления доступом Не все права доступа, установленные для субъекта в разрешающем элементе списка управления доступом DACL, который соответствует этому субъекту, могут быть субъектом реализованы. Некоторые из этих прав могут быть установлены также и в запрещающем элементе списка управления доступом, который соответствует этому же субъекту. Так как запрещающие элементы списка DACL имеют приоритет над разрешающими элементами этого списка,
Глава 41. Работа со списками управления доступом на высоком уровне 875 то в последних могут быть установлены права, которые пользователь не может реализовать. Под эффективными правами доступа субъекта к объекту понимаются такие права, которые субъект действительно может реализовать при доступе к объекту. Для получения эффективных прав доступа к объекту заданной учетной записи ИСПОЛЬЗуетСЯ функция GetEffectiveRightsFromAcl, Которая имеет следующий прототип: DWORD GetEffectiveRightsFromAcl( PACL pAcl, // указатель на список управления доступом PTRUSTEE pTrustee, // указатель на структуру TRUSTEE PACCESS_MASK pAccessRights // указатель на маску доступа ); В случае успешного завершения функция возвращает значение error_success, а в случае неудачи — код ошибки. Параметр pAcl должен указывать на список управления доступом, из которого извлекается информация об эффективных правах доступа учетной записи к объекту. Параметр pTrustee должен указывать на структуру типа trustee, для которой определяются эффективные права доступа к объекту. Структура trustee может быть инициализирована именем или идентификатором безопасности учетной записи. Параметр pAccessRights должен указывать на переменную типа access_mask, в которую функция запишет эффективные права доступа для Структуры TRUSTEE, заданной Параметром pTrustee. В листинге 41.8 приведена программа, в которой для определения эффективных прав доступа пользователя к файлу или каталогу используется функция GetEffectiveRightsFromAcl. I Листинг 41 Л* Получение элементов из списщ управления* доступом DACL | \ и получение ййф»рш1|^ *** :\| ttdefine _WIN32_WINNT 0x0500 ttifndef UNICODE #define UNICODE #endif # inc lude <windows . h> #include <stdio.h>
876 Часть XI. Управление безопасностью в Windows #include <lm.h> ttinclude <aclapi.h> int main() { TRUSTEE Trustee; // информация об учетной записи wchar_t wchFileName[248]; // имя файла или каталога wchar_t wchAccountName[UNLEN +255]; // имя учетной записи PSECURITY_DESCRIPTOR IpSd; // указатель на дескриптор безопасности PACL lpDacl; // указатель на список DACL ACCESSJMASK dwAccessRights =0; // права доступа DWORD dwErrCode; // код возврата // читаем имя файла printf("Input a file name: "); wscanf(L"%s", wchFileName); // вводим имя файла или каталога // получаем SD этого файла dwErrCode = GetNamedSecurityInfo( wchFileName, // имя файла SE_FILE_OBJECT, // объект файл DACL_SECURITY_INFORMATION, // получаем DACL NULL, // адрес указателя на SID владельца NULL, // адрес указателя на первичную группу &lpDacl, // указатель на DACL NULL, // указатель на SACL не нужен &lpSd); // адрес указателя на дескриптор безопасности if (dwErrCode != ERROR_SUCCESS) { printf("Get named security info failed.\n"); printf("Error code: %u\n", dwErrCode); return dwErrCode; } // вводим имя домена и пользователя printf("Input domain account (domain_name\\user_name): "); wscanf(L"%s", wchAccountName); // вводим имя учетной записи
Глава 41. Работа со списками управления доступом на высоком уровне 877 // строим структуру TRUSTEE по имени Bui IdTrus teeWi thName (&Trus t ее, wchAccountName) ; // получаем права доступа для заданного имени dwErrCode = GetEffectiveRightsFromAcl( lpDacl, // указатель на список DACL &Trustee, // адрес структуры TRUSTEE &dwAccessRights); // адрес маски с флагами if (dwErrCode != ERROR_SUCCESS) { printf("Get effective rights from ACL failed.\n"); printf("Error code: %u\n"/ dwErrCode); return dwErrCode; } // выводим на консоль права доступа printf("ACCESS_MASK: %x\n", dwAccessRights); // отображаем права доступа if ((dwAccessRights & SPECIFIC_RIGHTS_ALL) == SPECIFIC_RIGHTS_ALL) printf("SPECIFIC_RIGHTS_ALL is set.\n"); else printf("SPECIFIC_RIGHTS_ALL is not set.\n"); if ((dwAccessRights & STANDARD_RIGHTS_ALL) == STANDARD_RIGHTS_ALL) printf("STANDARD_RIGHTS_ALL is set.\n"); else printf("STANDARD_RIGHTS_ALL is not set.\n"); if ((dwAccessRights & GENERIC_ALL) == GENERIC_ALL) printf("GENERIC_ALL is set.\n"); else printf("GENERIC_ALL is not set.\n"); // освобождаем память LocalFree(lpSd); return 0;
878 Часть XI. Управление безопасностью в Windows 41.10. Получение из списка управления доступом прав, которые подвергаются аудиту Для получения из списка управления доступом SACL прав субъекта на доступ к объекту, которые подвергаются аудиту со стороны системы безопасности, ИСПОЛЬЗуется функция GetAuditedPermissionsFromAcl, которая Имеет следующий прототип: DWORD GetAuditedPermissionsFromAcl( PACL pAcl, // адрес списка SACL PTRUSTEE pTrustee, // адрес структуры TRUSTEE PACCESS_MASK pSuccessfulAuditedRights, // маска удачного доступа PACCESS_MASK pFailedAuditRights // маска неудачного доступа ); В случае успешного завершения функция возвращает значение error_success, а в случае неудачи — код ошибки. Параметр pAcl должен указывать на список управления доступом, из которого извлекается информация о правах доступа учетной записи к объекту, которые подвергаются аудиту со стороны системы безопасности. Параметр pTrustee должен указывать на структуру типа trustee, для которой определяются права доступа к объекту, подверженные аудиту. Структура trustee может быть инициализирована именем или идентификатором безопасности учетной записи. Параметр pSuccessfulAuditedRights должен указывать на переменную типа access_mask, в которую функция запишет подвергаемые аудиту права доступа для структуры trustee, заданной параметром pTrustee. Причем для этих прав генерируется аудиторское сообщение только в случае успешного разрешения доступа к объекту. Параметр pFailedAuditRights должен указывать на переменную типа access_mask, в которую функция запишет подвергаемые аудиту права доступа для структуры trustee, заданной параметром pTrustee. Причем для этих прав генерируется аудиторское сообщение только в случае неудачной попытки получения доступа к объекту. В листинге 41.9 приведена программа, определяющая права доступа пользователя к файлу или каталогу, которые подвергаются аудиту, используя для ЭТОГО функцию GetAuditedPermissionsFromAcl. Как И ДЛЯ Всех программ, работающих со списком управления доступом SACL, требуется, чтобы пользователь имел действующую привилегию se_security_name. Такую привилегию имеют администраторы системы, но по умолчанию она недействительна. Поэтому, прежде чем получать информацию из списка SACL объекта, эта привилегия будет активизирована посредством вызова функции
Глава 41. Работа со списками управления доступом на высоком уровне 879 Adj us tTokenPrivi leges. Более подробно работа с привилегиями рассмотрена в гл. 43. #ifndef UNICODE #define UNICODE #endif #define _WIN32_WINNT 0x0500 #include <stdio.h> #include <windows.h> #include <lm.h> #include <aclapi.h> #include <sddl.h> int main() { HANDLE hProcess; HANDLE hTokenHandle; // дескриптор процесса // дескриптор маркера доступа TOKEN_PRIVILEGES tp; // привилегии маркера доступа DWORD dwLengthOfSID =0; // длина SID DWORD dwLengthOfDomainName =0; // длина имени домена DWORD dwLengthOfUserName = UNLEN; // длина имени учетной записи SID *lpSid = NULL; // указатель на SID LPTSTR lpDomainName = NULL; SID_NAME_USE type_of_SID; // указатель на имя домена // тип учетной записи wchar_t wchF i 1 eName [248] ; wchar_t wchUserName [UNLEN] ; // имя каталога // имя учетной записи TRUSTEE Trustee; // информация об учетной записи PACL pSacl = NULL; // указатель на список SACL PSECURITY_DESCRIPTOR pSd = NULL; // указатель на дескриптор безопасности
880 Часть XI. Управление безопасностью в Windows ACCESS_MASK amSuccess =0; // маска для аудита успешного доступа ACCESS_MASK amFailed =0; // маска для аудита неудачного доступа DWORD dwRetCode; // код возврата // получаем дескриптор процесса hProcess = GetCurrentProcess(); // получаем маркер доступа процесса if (!OpenProcessToken( hProcess, // дескриптор процесса TOKEN_ALL_ACCESS, // полный доступ к маркеру доступа ShTokenHandle)) // дескриптор маркера { dwRetCode = GetLastError(); printf( "Open process token failed: %u\n"/ dwRetCode); return dwRetCode; } // устанавливаем общее количество привилегий tp.PrivilegeCount = 1; // определяем идентификатор привилегии для установки аудита if (!LookupPrivilegeValue( NULL, // ищем идентификатор привилегии на локальном компьютере SE_SECURITY_NAME/ // привилегия для аудита &(tp.Privileges[0].Luid))) { dwRetCode = GetLastError(); printf("Lookup privilege value failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // разрешаем привилегию аудита tp.Privileges [0] .Attributes = SE_PRIVILEGE_ENABLED; // разрешаем привилегию для установки аудита
Глава 41. Работа со списками управления доступом на высоком уровне 881 if (!AdjustTokenPrivileges( hTokenHandle, // дескриптор маркера доступа процесса FALSE, //не запрещаем все привилегии &tp, // адрес привилегий О, // длины буфера нет NULL, // предыдущее состояние привилегий не нужно NULL)) // длина буфера не нужна { dwRetCode = GetLastError(); printf("Lookup privilege value failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // вводим имя файла или каталога, например: С:\\test.txt printf("Input a full path to your file: "); _ge tws (wchF i 1 eName) ; // получаем дескриптор безопасности файла dwRetCode = GetNamedSecurityInfo( wchFileName, // имя файла SE_FILE_OBJECT/ // объект файл SACL_SECURITY_INFORMATION, // получаем сведения об аудите NULL, // SID владельца не нужен NULL, // SID первичной группы не нужен NULL, // DACL не нужен SpSacl, // указатель на SACL &pSd); // адрес указателя на дескриптор безопасности if (dwRetCode != ERROR_SUCCESS) { printf("Get named security info failed.\n"); printf("Error code: %u\n"/ dwRetCode); return dwRetCode; } // вводим имя пользователя printf("Input user_name: ");
882 Часть XL Управление безопасностью в Windows _getws(wchUserName); // вводим имя учетной записи // получаем SID учетной записи // определяем длину SID пользователя LookupAccountName( NULL, // ищем имя на локальном компьютере wchUserName, // имя пользователя NULL, // определяем длину SID &dwLengthOfSID, // длина SID NULL, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID); // тип учетной записи // проверяем, вернула ли функция длину SID if (dwLengthOfSID == 0) { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // распределяем память для SID и имени домена IpSid = (SID*) new char[dwLengthOfSID]; lpDomainName = (LPTSTR) new wchar_t [dwLengthOfDomainName] ; // определяем SID и имя домена пользователя if(!LookupAccountName( NULL, // ищем имя на локальном компьютере wchUserName, // имя пользователя IpSid, // указатель на SID SdwLengthOfSID, // длина SID lpDomainName, // указатель на имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwRetCode = GetLastError();
Глава 41. Работа со списками управления доступом на высоком уровне 883 printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // строим структуру TRUSTEE по идентификатору безопасности BuildTrusteeWithSid(&Trustee, IpSid); // получаем аудиторские права пользователя dwRetCode = GetAuditedPermissionsFromAcl( pSacl, // адрес списка SACL STrustee, // информация об учетной записи SamSuccess, // адрес маски прав успешного доступа SamFailed); // адрес маски прав неудачного доступа if (dwRetCode != ERROR_SUCCESS) { printf("Get audited permissions from ACL failed.\n"); printf("Error code: %u\n"/ dwRetCode); return dwRetCode; } // распечатываем права доступа printf("Success audit rights: %x\n"/ amSuccess); printf("Failed audit rights: %x\n"/ amFailed); // запрещаем привилегию аудита tp.Privileges[0].Attributes = 0; // разрешаем привилегию для установки аудита if (!AdjustTokenPrivileges( hTokenHandle, // дескриптор маркера доступа процесса FALSE, //не запрещаем все привилегии &tp, // адрес привилегий 0, // длины буфера нет NULL, // предыдущее состояние привилегий не нужно NULL)) // длина буфера не нужна
884 Часть XI. Управление безопасностью в Windows dwRetCode = GetLastError(); printf("Lookup privilege value failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // освобождаем память для дескриптора LocalFree(pSd); delete[] lpDomainName; delete[] IpSid; return 0; } В СВЯЗИ С работой функции GetAuditedPermissionsFromAcl сделаем следующее замечание. Похоже, что в операционной системе Windows 2000 функция возвращает права доступа, для которых генерируется аудиторское сообщение в случае успешного доступа к объекту в переменную, на которую указывает параметр pFaiiedAuditRights. И, наоборот, в переменную, на которую указывает параметр pSuccessfulAuditedRights, функция записывает права доступа, ДЛЯ КО- торых генерируется аудиторское сообщение в случае неудачного доступа к объекту. То есть ИЛИ при реализации функции GetAuditedPermissionsFromAcl перепу- таны параметры pSuccessfulAuditedRights И pFaiiedAuditRights, ИЛИ ЭТИ же параметры перепутаны при описании функции в документации.
Глава 42 Работа с привилегиями 42.1. Локальные идентификаторы привилегий Привилегии, которыми могут обладать учетные записи, были рассмотрены в разд. 36.8, посвященном маркерам доступа. Назначаются привилегии при создании учетных записей. При создании маркера доступа он получает привилегии той учетной записи, которая его создала. Далее приведены символические имена для привилегий, которые используются в операционных системах Windows NT. □ se_assignprimarytoken_name — SeAssignPrimaryTokenPHvilege; □ se_audit_name — SeAuditPrivilege; □ se_backup_name — SeBackupPrivilege; □ se_change_notify_name — SeChaiigeNotifyPrivilege; □ se_create_pagefile_name — SeCreatePagefilePrivilege; □ se_create_permanent_name — SeCreatePermaneiitPrivilege; □ se_create_token_name — SeCreateTokenPrivilege; □ se_debug_name — SeDebugPrivilege; □ se_inc_base_priority_name — SeliicreaseBasePriorityPrivilege; □ se_increase_quota_name — SelncreaseQuotaPrivilege; □ se_load_driver_name — SeLoadDriverPrivilege; □ se_lock_memory_name — SeLockMemoryPrivilege; □ se_machine_account_name — SeMachineAccountPrivilege; □ se_prof_single_process_name — SeProfileSingleProcessPrivilege;
886 Часть XI. Управление безопасностью в Windows П se_remote_shutdown_name — SeRemoteShutdownPrivilege; □ se_restore_name — SeRestorePrivilege; □ se_security_name — SeSecurityPrivilege; □ se_shutdown_name — SeShutdownPrivilege; □ se_system_environment_name — SeSystemEnvironmentPrivilege; □ se_system_profile_name — SeSystemProfilePrivilege; □ se_systemtime_name — SeSystemtimePrivilege; □ se_take_ownership_name — SeTakeOwnershipPrivilege; □ se_tcb_name — SeTcb Privilege; □ se_unsolicited_input_name — SeUnsolicitedlnputPrivilege. При работе с привилегиями используются специальные числовые значения, которые называются локальными идентификаторами. Локальные идентификаторы действительны только на локальной машине от момента запуска системы и до момента ее выключения. При последующих запусках системы локальные идентификаторы, используемые системой, генерируются повторно. Так как локальные идентификаторы генерируются локально, то на каждой машине для идентификации одинаковых объектов используются различные локальные идентификаторы. Например, для обозначения одной и той же привилегии на разных машинах могут использоваться различные локальные идентификаторы. Локальные идентификаторы определяются структурами следующего типа: typedef struct _LUID { DWORD LowPart; // младшая часть LONG HighPart; // старшая часть } LUID, *PLUID; При работе с локальными идентификаторами обычно используются структуры типа: typedef struct _LUID_AND_ATTRIBUTES { LUID Luid; // локальный идентификатор DWORD Attributes; // атрибуты локального идентификатора } LUID_AND_ATTRIBUTES, * PLUID_AND_ATTRIBUTES; Здесь поле Attributes используется для установки различных флагов, характеризующих локальный идентификатор, а, следовательно, и объект, который этот локальный идентификатор представляет. В последующих разделах будут рассмотрены функции для работы с локальными идентификаторами, которые представляют привилегии. Но в общем случае локальные идентификаторы могут использоваться и для других целей.
Глава 42. Работа с привилегиями 887 42.2. Инициализация локального идентификатора Для инициализации локального идентификатора используется функция AllocateLocallyUniqueld, которая Имеет следующий ПроТОТИП! BOOL AllocateLocallyUniqueld( PLUID Luid // указатель на локальный идентификатор ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции можно получить посредством вызова функции GetLastError. Единственный параметр функции должен указывать на структуру типа luid, описанную в разд. 42. /, в которую функция запишет сгенерированное значение локального идентификатора. В листинге 42.1 приведена программа, которая создает новый локальный идентификатор Посредством ВЫЗОВа функции AllocateLocallyUniqueld. Листинг 42.1. Инициализация локального иден^фикатора # include <windows.h> #include <stdio.h> int main() { LUID luid; // локальный идентификатор DWORD dwRetCode; // код возврата // распределяем локальный уникальный идентификатор if (!AllocateLocallyUniqueld( &luld)) // адрес локального идентификатора { dwRetCode = GetLastError(); perror("Lookup privilege value failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // распечатываем LUID
888 Часть XI. Управление безопасностью в Windows printf("Locally unique identifier.\n"); printf("\tHigh part: %x, Low part: %x\n"/ luId.HighPart, luId.LowPart) ; return 0; } 42.3. Получение локального идентификатора привилегии Для получения локального идентификатора привилегии по имени привилегии используется функция LookupPriviiegeValue, которая имеет следующий прототип: BOOL LookupPriviiegeValue( LPCWSTR IpSystemName, // имя системы LPCWSTR lpName, // имя привилегии PLUID IpLuid // адрес локального идентификатора ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ Посредством ВЫЗОВа фуНКЦИИ GetLastError. Параметр IpSystemName должен указывать на имя компьютера, на котором ищется локальный идентификатор привилегии. Если в этом параметре установлено значение null, to локальный идентификатор привилегии ищется на локальном компьютере. Параметр lpName должен указывать на имя привилегии, для которой ищется ее локальный идентификатор. Параметр ipLuid должен указывать на структуру типа luid, в которую функция поместит локальный идентификатор привилегии. Программа, в которой локальный идентификатор привилегии определяется посредством ВЫЗОВа фуНКЦИИ LookupPriviiegeValue, приведена В ЛИСТИНГе 42.2. 42.4. Получение имени привилегии Для определения имени привилегии по ее локальному идентификатору используется фуНКЦИЯ LookupPrivilegeName, Которая Имеет следующий ПроТОТИП! BOOL LookupPrivilegeName( LPCWSTR IpSystemName, // имя системы PLUID IpLuid, // адрес локального идентификатора
Глава 42. Работа с привилегиями 889 LPWSTR lpName, // имя привилегии LPDWORD cbName // длина имени привилегии ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ ПОСреДСТВОМ ВЫЗОВа фуНКЦИИ GetLastError. Параметры функции имеют следующее назначение. Параметр lpSystemName должен указывать на имя компьютера, на котором ищется имя привилегии, соответствующей локальному идентификатору, на который указывает параметр IpLuid. Если в этом параметре установлено значение null, to имя привилегии ищется на локальном компьютере. Параметр IpLuid должен указывать на структуру типа luid, которая содержит локальный идентификатор привилегии, имя которой определяется. Параметр lpName должен указывать на буфер памяти, в который функция запишет имя привилегии. Параметр cbName должен указывать на переменную типа dword, которая содержит длину буфера для имени привилегии. Если эта длина меньше необходимой, то функция закончится неудачей и в эту переменную будет записана требуемая длина буфера для имени привилегии. В листинге 42.2 приведена программа, которая сначала определяет локальный идентификатор привилегии, используя для этого функцию LookupPrivilegevalue, а затем определяет имя привилегии по ее локальному идентификатору, ИСПОЛЬЗуя ДЛЯ ЭТОГО функцию LookupPrivilegeName. Листинг 42.2» Определение локального идентификатора и имени привилегии #include <windows . h> #include <stdio.h> int main() { LUID luid; // локальный идентификатор для привилегии DWORD dwLength =0; // длина имени привилегии LPTSTR lpPrivName = NULL; // адрес имени привилегии DWORD dwRetCode; // код возврата // определяем локальный идентификатор привилегии if (!LookupPrivilegevalue(
890 Часть XI. Управление безопасностью в Windows NULL, // ищем привилегии на локальной машине SE_DEBUG_NAME, // привилегия выполнять отладку процесса &luld)) // адрес локального идентификатора привилегии { dwRetCode = GetLastError(); perror("Lookup privilege value failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // распечатываем LUID printf("Luid of SE_DEBUG_NAME privilege. "); printf("High part: %x, Low part: %x\n", luld.HighPart, luld.LowPart) ; // определяем длину имени привилегии if (!LookupPrivilegeName( NULL, // локальная машина &luld, // адрес локального уникального идентификатора lpPrivName, // адрес имени привилегии SdwLength)) // адрес длины буфера { dwRetCode = GetLastError(); if (dwRetCode != ERROR_INSUFFICIENT_BUFFER) { perror("Lookup privilege name failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // захватываем память для имени привилегии lpPrivName = new CHAR[dwLength + 1]; } // определяем имя привилегии if (!LookupPrivilegeName( NULL, // локальная машина &luld, // адрес локального уникального идентификатора
Глава 42. Работа с привилегиями 891 lpPrivName, // адрес имени привилегии &dwLength)) // адрес длины буфера { dwRetCode = GetLastError(); perror("Lookup privilege name failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // распечатываем имя привилегии printf("Name of the Luid: %s\n", lpPrivName); delete[] lpPrivName; return 0; } 42.5. Получение имени привилегии для отображения Для определения имени привилегии, которое имеет вид, пригодный для чтения непрофессиональным пользователем, используется функция LookupPrivilegeDisplayName, Которая Имеет следующий проТОТИП: BOOL LookupPrivilegeDisplayName( // имя системы // имя привилегии // имя привилегии для отображения // длина имени привилегии для отображения // идентификатор языка В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ ПОСреДСТВОМ ВЫЗОВа фуНКЦИИ GetLastError. Параметр IpSystemName должен указывать на имя компьютера, на котором ищется имя привилегии для отображения, соответствующее имени привилегии, на которое указывает параметр lpName. Если в этом параметре установлено значение null, to имя привилегии ищется на локальном компьютере. Параметр lpName должен указывать имя привилегии, для которого определяется удобное для отображения имя. LPCSTR LPCSTR LPSTR LPDWORD LPDWORD ); IpSys temName, lpName, lpDisplayName, cbDisplayName, lpLanguageld
892 Часть XI. Управление безопасностью в Windows Параметр IpDisplayName должен указывать на буфер, в который функция запишет удобное для отображения имя привилегии. Параметр cbDispiayName должен указывать на переменную типа dword, которая содержит длину буфера для удобного для отображения имени привилегии. Если эта длина меньше необходимой, то функция закончится неудачей и в эту переменную будет записана требуемая длина буфера для удобного для отображения имени привилегии. Параметр lpLanguageid должен указывать на переменную типа dword, в которую функция запишет идентификатор языка, используемого при написании имени привилегии, удобного для отображения. В листинге 42.3 приведена программа, которая определяет удобное для отображения имя привилегии по ее внутреннему имени, используя для этого ФУНКЦИЮ LookupPrivilegeDisplayName. | Листинг 42.3, Определение удобного для отображения имени привилегии #include <windows.h> #include <stdio.h> int main () { LPTSTR lpPrivDisplayName = NULL; // адрес имени привилегии // для отображения DWORD dwLength =0; // длина имени привилегии DWORD dwLangld; // идентификатор языка DWORD dwRetCode; // код возврата // определяем длину имени привилегии для отображения if (!LookupPrivilegeDisplayName ( NULL, // локальная машина SE_SHUTDOWN_NAME, // имя привилегии lpPrivDisplayName, // адрес для имени привилегии &dwLength, // адрес длины буфера SdwLangld)) // адрес идентификатора языка { dwRetCode = GetLastError(); if (dwRetCode != ERROR_INSUFFICIENT_BUFFER)
Глава 42. Работа с привилегиями 893 { perror("Lookup privilege display name failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // захватываем память для имени привилегии lpPrivDisplayName = new char[dwLength + 1]; } // определяем имя привилегии для отображения if (!LookupPrivilegeDisplayName ( NULL, // локальная машина SE_SHUTDOWN_NAME/ // имя привилегии lpPrivDisplayName, // адрес для имени привилегии sdwLength, // адрес длины буфера sdwLangld)) // адрес идентификатора языка { dwRetCode = GetLastError(); perror("Lookup privilege display name failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // распечатываем имя привилегии и идентификатор языка print f ("Display name of the privilege SE_SHUTDOWN_NAME: \n\t%s\n", lpPrivDisplayName); printf("Language identifier: %x\n"/ dwLangld); delete[] lpPrivDi splayName; return 0; }
Глава 43 Работа с маркерами доступа 43.1. Открытие маркера доступа процесса Прежде чем с маркером доступа можно будет производить какие-либо действия, его нужно открыть. Для открытия маркера доступа процесса используется функция openProcessToken, которая имеет следующий прототип: BOOL OpenProcessToken( HANDLE ProcessHandie, // дескриптор процесса DWORD DesiredAccess, // режимы доступа к маркеру доступа процесса PHANDLE TokenHandle // адрес маркера доступа процесса ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ ПОСреДСТВОМ ВЫЗОВа фунКЦИИ GetLastError. Параметр ProcessHandie должен содержать дескриптор процесса, маркер доступа которого открывается. В параметре DesiredAccess должны быть установлены флаги, отмечающие затребованные права доступа к маркеру доступа процесса. Ниже перечислены флаги, которые можно установить в этом поле маркера доступа. Специфические для маркера доступа права доступа устанавливаются следующим флагами: □ token_assign_primary — право присоединить первичный маркер доступа к процессу; □ token_duplicate — право дублировать маркер доступа; □ token_impersonate — право замещать маркер доступа процесса; □ token_query — право получать информацию из маркера доступа;
Глава 43. Работа с маркерами доступа 895 □ token_query_source — право получить информацию об источнике маркера доступа; □ token_adjust_privileges — право настраивать привилегии маркера доступа; □ token_adjust_groups — право настраивать свойства групп в маркере доступа; □ token_adjust_default — право изменять информацию для установки в дескриптор объекта по умолчанию; □ token_adjust_sessionid — право настраивать идентификатор сессии в маркере доступа. Для стандартных прав доступа можно установить следующие флаги: □ read_control — право читать информацию из маркера доступа, за исключением информации из списка SACL; □ write_dac — право модифицировать список DACL в маркере доступа; □ write_owner — право изменить владельца маркера доступа; □ delete — право удалять маркер доступа. Кроме того, можно использовать следующие символические константы, применяемые для обозначения комбинации нескольких различных флагов доступа: □ token_all_access — включает все права доступа к маркеру доступа; □ token_read — включает права доступа standard_rights_read и token_query; □ token_write — включает права standard_rights_write, TOKEN_ADJUST_PRIVILEGES, TOKEN_ADJUST_GROUPS и token_adjust_default; □ token_execute — включат право standard_rights_execute. Для доступа к списку управления доступом SACL маркера доступа нужно установить флаг access_system_security — доступ к списку управления доступом SACL. Теперь перейдем К Последнему параметру функции OpenProcessToken — TokenHandie. Этот параметр должен указывать на переменную типа handle, в которую функция запишет дескриптор маркера доступа процесса. Так как этот дескриптор является обычным дескриптором, описывающим объект ядра операционной системы, то после завершения работы с маркером доступа ЭТОТ дескриптор НУЖНО закрыть При ПОМОЩИ фуНКЦИИ CloseHandle. Примеры использования функции OpenProcessToken будут приведены в сле- дующих параграфах, посвященных работе с маркерами доступа.
896 Часть XI. Управление безопасностью в Windows 43.2. Открытие маркера доступа потока Прежде чем работать с маркером доступа потока его необходимо открыть при помощи функции openThreadToken, которая имеет следующий прототип: BOOL OpenThreadToken( HANDLE ThreadHandle, // дескриптор потока DWORD DesiredAccess, // режимы доступа к маркеру доступа потока BOOL OpenAsSelf, // выбор контекста безопасности при открытии PHANDLE TokenHandle // адрес маркера доступа потока ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ Посредством ВЫЗОВа функции GetLastError. Параметр ThreadHandie должен содержать дескриптор потока, маркер доступа которого открывается. В параметре DesiredAccess должны быть установлены флаги, отмечающие затребованные права доступа к маркеру доступа потока. Флаги, которые можно установить в этом поле маркера доступа, перечислены в предыдущем разделе При рассмотрении соответствующего Параметра фунКЦИИ OpenProcessToken. В параметре OpenAsSelf должно быть установлено одно из значений false или true. Если в параметре установлено значение false, to функция работает в контексте безопасности потока, вызвавшего функцию. Это дает возможность исполнять функцию в контексте процесса клиента, если данный поток запущен на сервере от имени клиента. Если же в параметре установлено значение true, to функция работает в контексте безопасности процесса, вызвавшего функцию. Параметр TokenHandle должен указывать на переменную типа handle, в которую функция запишет дескриптор маркера доступа потока. Так как этот дескриптор является обычным дескриптором, описывающим объект ядра операционной системы, то после завершения работы с маркером доступа ЭТОТ ДеСКрИПТОр НУЖНО закрЫТЬ При ПОМОЩИ фуНКЦИИ CloseHandle. Пример использования функции OpenThreadToken будет приведен в разд. 43.11. 43.3. Структуры, используемые для работы с маркером доступа Программы не имеют непосредственного доступа к внутренней структуре маркера доступа. Поэтому при работе с маркерами доступа функции используют специальные структуры или перечисления, которые позволяют
Глава 43. Работа с маркерами доступа 897 получать информацию из маркера доступа или изменять требуемую информацию в маркере доступа. Далее приведены типы структур и перечислений, которые используются при работе с маркерами доступа. Для получения информации о пользователе, связанном с заданным маркером доступа, используется структура типа: typedef struct _TOKEN_USER { SID_AND_ATTRIBUTES User; // SID пользователя и атрибуты } TOKEN_USER, *PTOKEN_USER; Структура типа sid_and_attributes определена следующим образом: typedef struct _SID_AND_ATTRIBUTES { PSID Sid; // указатель на SID пользователя DWORD Attributes; // атрибуты SID } SID_AND_ATTRIBUTES, * PSID_AND_ATTRIBUTES; Атрибуты идентификатора безопасности задаются флагами, каждый из которых определяет некоторое свойство идентификатора безопасности. В настоящее время для идентификатора безопасности пользователя не определены атрибуты, поэтому это поле не используется. Для получения информации о группах, связанных с заданным маркером доступа, используется структура типа: typedef struct _TOKEN_GROUPS { DWORD GroupCount; // количество групп SID_AND_ATTRIBUTES Groups [ANYSIZE_ARRAY] ; // SID и атрибуты групп } TOKEN_GROUPS, *PTOKEN_GROUPS; В ПОЛе Attributes Структуры ТИПа SID_AND_ATTRIBUTES МОГуТ быТЬ уСТаНОВ- лены следующие флаги: □ se_group_mandatory — нельзя сбросить флаг se_group_enabled; □ se_group_enabled_by_default — флаг проверки доступа установлен по умолчанию; □ se_group_enabled — группа проверяется при доступе к объекту; □ se_group_owner — пользователь, связанный с маркером доступа, является владельцем группы; или группа может быть владельцем объекта; □ se_group_use_for_deny_only — фуппа используется только при проверке запрещения доступа к объекту; □ se_group_logon_id — sid идентифицирует сессию, выполнившую вход в систему для пользователя, связанного с группой; □ se_group_resource — локальная группа домена. 29 Зак. 514
898 Часть XI. Управление безопасностью в Windows Напомним, что структура sid_and_attributes была описана выше, при описании структуры типа token_user. Для получения информации о привилегиях, которыми обладает маркер доступа, используется структура типа: typedef struct _TOKEN_PRIVILEGES { DWORD PrivilegeCount; LUID_AND_ATTRIBUTES Privileges [ANYSIZE_ARRAY] ; } TOKEN_PRIVILEGES, * PTOKEN_PRIVILEGES ; В ПОЛе Attributes Структуры ТИПа LUID_AND_ATTRIBUTES МОГуТ 6bITb уста- новлены следующие флаги: □ SE_PRIVILEGE_ENABLED_BY_DEFAULT — ПрИВИЛеГИЯ установлена ПО уМОЛ- чанию; □ se_privilege_enabled — привилегия установлена; □ se_privilege_used_for_access — привилегия использовалась для доступа к объекту или сервису. Напомним, что структура luid_and_attributes была описана в разд. 42.1. Для получения информации об идентификаторе безопасности, который будет использоваться системой в алгоритме установки по умолчанию владельца нового объекта, создаваемого процессом с данным маркером доступа, используется структура типа: typedef struct _TOKEN_OWNER { PSID Owner; // SID владельца объекта по умолчанию } TOKEN_OWNER, * PTOKEN_OWNER; Отметим, что в этом случае идентификатор безопасности должен быть идентификатором безопасности одного из пользователей или групп, связанных с данным маркером доступа. Для получения информации об идентификаторе безопасности, который будет использоваться системой в алгоритме установки по умолчанию первичной группы нового объекта, создаваемого процессом с данным маркером доступа, используется структура типа: typedef struct _TOKEN_PRIMARY_GROUP { PSID PrimaryGroup; // SID первичной группы по умолчанию } TOKEN_PRIMARY_GROUP, *PTOKEN_PRIMARY_GROUP; Отметим, что в этом случае идентификатор безопасности должен быть идентификатором безопасности одной из групп, связанных с данным маркером доступа. Для получения информации о списке управления доступом DACL, который будет использоваться в алгоритме установки по умолчанию списка DACL
Глава 43. Работа с маркерами доступа 899 нового объекта, создаваемого процессом с данным маркером доступа, используется структура типа: typedef struct _TOKEN_DEFAULT_DACL { PACL DefaultDacl; // указатель на список DACL по умолчанию } TOKEN_DEFAULT_DACL, *PTOKEN_DEFAULT_DACL; Для получения информации об источнике, вызвавшем создание данного маркера доступа, используется структура типа: typedef struct _TOKEN_SOURCE { CHAR SourceName[TOKEN_SOURCE_LENGTH] ; // имя источника LUID Sourceldentifier; // локальный идентификатор источника } TOKEN_SOURCE, * PTOKEN_SOURCE ; Заметим, что поле sourceName является простым символьным массивом, содержащим имя источника маркера доступа. Информация о типе маркера доступа представляется перечислимой константой следующего типа: typedef enum _TOKEN_TYPE { TokenPrimary = 1, // первичный маркер доступа Tokenlmpersonation // замещающий маркер доступа } TOKEN_TYPE; Информация об уровне замещения первичного маркера доступа представляется перечислимой константой следующего типа: typedef enum _SECURITY_IMPERSONATION_LEVEL { SecurityAnonymous, // анонимный уровень замещения Securityldentification, // идентифицирующий уровень замещения Securitylmpersonation, // уровень подмены контекста безопасности SecurityDelegation // уровень делегирования полномочий } SECURITY_IMPERSONATION_LEVEL, * PSECURITY_IMPERSONATION_LEVEL ; Более подробно об уровнях замещения маркеров доступа рассказано в разд. 36.8. Для получения статистических данных о маркере доступа используется структура следующего типа: typedef struct _TOKEN_STATISTICS { LUID Tokenld; // локальный идентификатор маркера доступа LUID Authenticationld; // локальный идентификатор сессии, // которую представляет маркер доступа LARGE_INTEGER ExpirationTime; //не поддерживается TOKEN_TYPE TokenType; // тип маркера доступа SECURITY_IMPERSONATION_LEVEL ImpersonationLevel;// уровень замещения
900 Часть XI. Управление безопасностью в Windows DWORD DynamicCharged; // объем буфера, используемого для хранения // информации о защите по умолчанию и идентификаторе // безопасности первичной группы маркера доступа DWORD DynamicAvailable; // объем свободной памяти в буфере DWORD GroupCount; // количество групп, связанных с маркером доступа DWORD PrivilegeCount; // количество привилегий, доступных в // маркере доступа LUID Modifiedld; // локальный идентификатор, который изменяется // каждый раз при модификации маркера доступа } TOKEN_STATISTICS, *PTOKEN_STATISTICS; Для получения информации об ограничивающих идентификаторах безопасности используется структура типа token_groups, которая была описана ранее. Для получения информации об идентификаторе сессии, в которой был создан маркер доступа, используется переменная типа dword. Этот идентификатор используется только в том случае, если маркер доступа был создан для терминальной сессии. В остальных случаях значение этого идентификатора равно 0. 43.4. Получение информации из маркера доступа Для получения информации из маркера доступа используется функция GetTokeninf ormation, которая имеет следующий прототип: BOOL GetTokeninformation( HANDLE TokenHandle, // дескриптор маркера доступа TOKEN_INFORMATION_CLASS TokenlnformationClass, // тип информации LPVOID Tokenlnformation, // указатель на буфер для информации DWORD TokenInformationLength, // длина буфера PDWORD ReturnLength // требуемая длина буфера ); В случае удачного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного завершения функции можно получить посредством вызова функции GetLastError. Параметры функции GetTokeninf ormation имеют следующее назначение. Параметр TokenHandie должен содержать дескриптор маркера доступа, из которого извлекается информация. Причем этот дескриптор должен быть открыт в режиме token_query_source, если извлекается информация об источнике маркера доступа и в режиме token_query для извлечения информации другого типа.
Глава 43. Работа с маркерами доступа 901 Параметр Tokeninformationciass должен содержать одно из значений перечисления типа token_information_class, которое указывает, какую информацию из маркера доступа должна вернуть функция. Перечисление типа token_information_class определено следующим образом: typedef enum _TOKEN_INFORMATION_CLASS { TokenUser = 1, // информация о пользователе TokenGroups, // информация о группах, связанных с маркером доступа TokenPrivileges, // информация о привилегиях TokenOwner, // информация о владельце объекта по умолчанию TokenPrimaryGroup, // информация о первичной группе владельца // объекта по умолчанию TokenDefaultDacl, // информация о списке DACL объекта по умолчанию TokenSource, // источник маркера доступа TokenType, // тип маркера доступа TokenlmpersonationLevel, // уровень замещения маркера доступа Tokenstatistics, // статистика TokenRestrictedSids, // список ограничивающих идентификаторов // безопасности TokenSessionld // идентификатор сессии } TOKEN_INFORMATION_CLASS, * PTOKEN_INFORMATION_CLASS ; Параметр Tokeninformation должен указывать на буфер, в который функция поместит информацию, извлекаемую из маркера доступа. В зависимости от значения, заданного параметром Tokeninformationciass, информация о маркере доступа записана в структурном виде или имеет значение некоторого перечислимого типа. Ниже приведено соответствие значений перечислимой константы типа token_information_class и типов, используемых для представления соответствующей информации о маркере доступа: □ TokenUser — tokenjjser; □ TokenGroups — token_groups; □ TokenPrivileges — token_privileges; □ TokenOwner — token_owner; □ TokenPrimaryGroup — token_primary_group; □ TokenDefaultDacl — token_default_dacl; □ TokenSource — token_source; □ TokenType — token_type; □ TokenlmpersonationLevel — security_impersonation_level; □ TokenStatistics — token_statistics;
902 Часть XI. Управление безопасностью в Windows П TokenRestrictedSids — token_groups; □ TokenSessionld — dword. В параметре Tokeninformation может быть также установлено значение null. В этом случае функция запишет по адресу, заданному последним параметром, необходимую длину буфера. Параметр TokeninformationLength должен содержать длину буфера, на который указывает параметр Tokeninformation. Если в параметре Tokeninformation установлено значение null, to в параметре TokeninformationLength должно быть установлено значение 0. В параметре ReturnLength должен быть установлен адрес двойного слова, в которое функция вернет требуемую длину буфера, если в параметре Tokeninformation установлено значение null или заданная длина буфера недостаточна для записи требуемой информации. В ЛИСТИНГе 43.1 Приведена программа, В КОТОРОЙ функция GetTokenlnformation используется для определения источника маркера доступа. Пистинг «X Определение источника маркера доступа &&..L. г*ь_-£ :..! ^l.^.^.^JLJL.j*~.i^:i. L-s I * J # include <windows.h> #include <stdio.h> int main () { HANDLE hProcess; // дескриптор процесса HANDLE hTokenHandle; // дескриптор маркера доступа TOKEN_SOURCE ts; // источник маркера доступа DWORD dwRetLength; // возвращаемая длина буфера DWORD dwRetCode; // код возврата // получаем дескриптор процесса hProcess = GetCurrentProcess(); // открываем маркер доступа процесса if (!OpenProcessToken( hProcess, // дескриптор процесса TOKEN_QUERY_SOURCE, // доступ к источнику маркера
Глава 43. Работа с маркерами доступа 903 ShTokenHandle)) // дескриптор маркера { dwRetCode = GetLastError(); printf( "Open process token failed: %u\n"/ dwRetCode); return dwRetCode; } // получаем источник маркера доступа if (!GetTokenlnformation( hTokenHandle, // дескриптор маркера доступа TokenSource, // получаем источник маркера доступа &ts, // адрес буфера для источника sizeof(ts), // размер буфера &dwRetLength)) // требуемый размер буфера в случае неудачи { dwRetCode = GetLastError(); printf( "Get token information failed: %u\n"/ dwRetCode); return dwRetCode; } // распечатываем источник маркера доступа printf("Source name: "); for (int i = 0; i < TOKEN_SOURCE_LENGTH; ++i) putchar(ts.SourceName[i]); putchar('\n'); printf("Source identifier: %X %X\n"/ ts.Sourceldentifier.HighPart, ts.Sourceldenti fier.LowPart); С1oseHandle(hTokenHandle); return 0; } Теперь приведем ПрОфамму, В КОТОРОЙ функция GetTokenlnformation ИС- пользуется для определения всех привилегий маркера доступа. Эта программа приведена в листинге 43.2.
904 Часть XI. Управление безопасностью в Windows #include <windows.h> #include <stdio.h> int main () { HANDLE hProcessToken; // дескриптор маркера доступа PTOKEN_PRIVILEGES lpTokenPrivileges = NULL; LPTSTR lpPrivName = NULL; LPTSTR lpPrivDisplayName = NULL; // адрес имени привилегии // адрес имени привилегии для // отображения DWORD dwLangld; DWORD dwLength; DWORD dwRetCode; // код возврата // дескриптор процесса // чтение информации из маркера доступа // дескриптор маркера доступа if (!OpenProcessToken( GetCurrentProcess(), TOKEN_QUERY, ShProcessToken )) { dwRetCode = GetLastError(); perrorC'Set security descriptor owner failed.\n") printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // получаем длину буфера для привилегий if (!GetTokenlnformation( hProcessToken, TokenPrivileges, lpTokenPrivileges, 0, &dwLength)) // дескриптор маркера доступа // получаем привилегии // адрес буфера // длина буфера // требуемая длина
Глава 43. Работа с маркерами доступа 905 dwRetCode = GetLastError(); if (dwRetCode != ERROR_INSUFFICIENT_BUFFER) { perrorC'Get token information for length failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // захватываем память для имени привилегии IpTokenPrivi leges = (TOKEN_PRIVTLEGES *) new char [ dwLength ] ; } // получаем привилегии маркера доступа if (!GetTokenInformation( hProcessToken, // дескриптор маркера доступа TokenPrivileges, // получаем привилегии IpTokenPrivileges, // адрес буфера dwLength, // длина буфера &dwLength)) // требуемая длина { dwRetCode = GetLastError(); perrorC'Get token information failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // распечатываем привилегии printf( "User privileges: \n" ); for( unsigned i = 0; i < lpTokenPrivileges->PrivilegeCount; ++i) { // печатаем состояние привилегии if ((IpTokenPrivileges->Privileges[i].Attributes & SE_PRIVILEGE_ENABLED) == SE_PRIVILEGE_ENABLED) printf("SE_PRIVILEGE_ENABLED:\n"); if ((IpTokenPrivileges->Privileges[i].Attributes & SE_PRIVILEGE_ENABLED_BY_DEFAULT) == SE_PRIVILEGE_ENABLED_BY_DEFAULT)
906 Часть XI. Управление безопасностью в Windows print f ( "SE_PRIVILEGE_ENABLED_BY_DEFAULT: \n") ; if ((IpTokenPrivileges->Privileges[i].Attributes & SE_PRIVILEGE_USED_FOR_ACCESS) == SE_PRIVILEGE_USED_FOR_ACCESS) printf("SE_PRIVILEGE_USED_FOR_ACCESS:\n"); if (!IpTokenPrivileges->Privileges[i].Attributes) printf("The privilege is disabled:\n"); // определяем длину имени привилегии dwLength = 0; if (!LookupPrivilegeName( NULL, // локальная машина &(IpTokenPrivileges->Privileges[i].Luid), // адрес LUID lpPrivName, // адрес имени привилегии &dwLength)) // адрес длины буфера { dwRetCode = GetLastError(); if (dwRetCode != ERROR_INSUFFICIENT_BUFFER) { perror("Lookup privilege name for length failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // захватываем память для имени привилегии lpPrivName = new char [dwLength + 1] ; } // определяем имя привилегии if (!LookupPrivilegeName( NULL, // локальная машина &(IpTokenPrivileges->Privileges[i].Luid), // адрес LUID lpPrivName, // адрес имени привилегии &dwLength)) // адрес длины буфера { dwRetCode = GetLastError(); perror("Lookup privilege name failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode;
Глава 43. Работа с маркерами доступа 907 // определяем длину имени привилегии для отображения dwLength = 0; if (ILookupPrivilegeDisplayName ( NULL, // локальная машина lpPrivName, // имя привилегии lpPrivDisplayName, // адрес для имени привилегии &dwLength, // адрес длины буфера SdwLangld)) // адрес идентификатора языка { dwRetCode = GetLastError(); if (dwRetCode != ERROR_INSUFFICIENT_BUFFER) { perror("Lookup privilege display name for length failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // захватываем память для имени привилегии lpPrivDisplayName = new char[dwLength + 1]; } // определяем имя привилегии для отображения if (ILookupPrivilegeDisplayName ( NULL, // локальная машина lpPrivName, // имя привилегии lpPrivDisplayName, // адрес для имени привилегии &dwLength, // адрес длины буфера SdwLangld)) // адрес идентификатора языка { dwRetCode = GetLastError(); perror("Lookup privilege display name failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // распечатываем имя привилегии и идентификатор языка printf("\t%s\n", lpPrivDisplayName);
908 Часть XI. Управление безопасностью в Windows II освобождаем память delete[] lpPrivName; delete[] lpPrivDisplayName; lpPrivName = NULL; lpPrivDisplayName = NULL; } CloseHandle(hProcessToken); return 0; } 43.5. Изменение информации в маркере доступа Для изменения в маркере доступа информации, используемой в алгоритме для задания по умолчанию атрибутов безопасности нового объекта, применяется функция SetTokeninf ormation, которая имеет следующий прототип: BOOL SetTokenInformation( HANDLE TokenHandle, // дескриптор маркера доступа TOKEN_INFORMATION_CLASS TokenlnformationClass, // тип информации LPVOID Tokenlnformation, // указатель на буфер с информацией DWORD TokenlnformationLength // длина буфера с информацией ); В случае удачного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного завершения функции можно получить посредством вызова функции GetLastError. Параметры функции SetTokeninf ormation имеют следующее назначение. Параметр TokenHandie должен содержать дескриптор маркера доступа, из которого извлекается информация. Причем этот маркер доступа должен быть открыт в режиме token_adjust_default. Параметр Tokeninformationciass должен содержать одно из значений перечисления типа token_information_class, которое указывает, какую информацию в маркере доступа изменяет функция. Все константы, принадлежащие перечислению типа token_information_class, были приведены В Предыдущем параграфе. При ИСПОЛЬЗОВанИИ фуНКЦИИ SetTokenlnformation в этом параметре могут быть установлены только следующие константы: □ Tokenowner — информация о владельце объекта по умолчанию; □ TokenPrimaryGroup — информация о первичной группе владельца объекта по умолчанию; □ TokenDefauitDaci — информация о списке DACL объекта по умолчанию.
Глава 43. Работа с маркерами доступа 909 Параметр Tokeninformation должен указывать на буфер с информацией, которая будет установлена в маркер доступа. В зависимости от значения, заданного параметром Tokeninformationciass, информация в буфере должна храниться в структуре одного из следующих типов: □ TokenPrimaryGroup — TOKEN_PRIMARY_GROUP; □ TokenDefaultDacl — TOKEN_DEFAULT_DACL; □ TokenSource — TOKEN_SOURCE. Параметр TokeninformationLength должен содержать длину буфера, на который указывает параметр Tokeninformation. В ЛИСТИНГе 43.3 Приведена Программа, В КОТОРОЙ фуНКЦИЯ SetTokenInf ormation изменяет идентификатор безопасности владельца объекта, который используется в алгоритме установки владельца нового объекта по умолчанию. лиг 43.3. (^писка DACL объекта по умолчанию #define _WIN32_WINNT 0x0500 #include <windows.h> #include <stdio.h> #include <lm.h> #include <sddl.h> int main() { HANDLE hMutex; HANDLE hProcess; HANDLE hTokenHandle; // дескриптор мьютекса // дескриптор процесса // дескриптор маркера доступа ACL *lpDacl; void *1рАсе = NULL; // указатель на список доступа // указатель на элемент списка BOOL bDaclPresent; BOOL bDaclDefaulted; // признак присутствия списка DACL // признак списка DACL по умолчанию char chUserName[UNLEN]; // имя пользователя DWORD dwLengthOfDomainName =0; // длина имени домена DWORD dwLengthOfSid =0; // длина разрешающего SID
910 Часть XI. Управление безопасностью в Windows DWORD dwDaclLength =0; // длина списка доступа DWORD dwRetLength =0; // возвращаемая длина буфера SID *lpSid = NULL; // указатель на разрешающий SID LPTSTR lpDomainName = NULL; // указатель на имя домена SID_NAME_USE typeOfSid; // тип учетной записи PSECURITY_DESCRIPTOR IpSd = NULL; // указатель на SD DWORD dwLengthOfSd =0; // длина дескриптора безопасности LPTSTR StringSid; // указатель на строку SID DWORD dwRetCode; // код возврата // получаем дескриптор процесса hProcess = GetCurrentProcess(); // открываем маркер доступа процесса if (!OpenProcessToken( hProcess, // дескриптор процесса TOKEN_QUERY | // получаем информацию из маркера доступа TOKEN_ADJUST_DE FAULT, //и измененяем список DACL маркера доступа ShTokenHandle)) // дескриптор маркера { dwRetCode = GetLastError(); printf( "Open process token failed: %u\n", dwRetCode); return dwRetCode; } // вводим имя пользователя, которому разрешим доступ к каталогу printf("Input a user name: "); gets(chUserName); // определяем длину SID пользователя if (!LookupAccountName( NULL, // ищем имя на локальном компьютере chUserName, // имя пользователя NULL, // определяем длину SID
Глава 43. Работа с маркерами доступа 911 { SdwLengthOfSid, // длина SID NULL, // определяем имя домена &dwLengthOfDomainName, // длина имени домена StypeOfSid)) // тип учетной записи dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) { // распределяем память для SID и имени домена IpSid = (SID*) new char[dwLengthOfSid]; lpDoma inName = (LPTSTR) new char[dwLengthOfDomainName]; } else { // выходим из программы printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } } // определяем SID и имя домена пользователя if(!LookupAccountName( NULL, // ищем имя на локальном компьютере chUserName, // имя пользователя IpSid, // указатель на SID SdwLengthOfSid, // длина SID lpDomainName, // указатель на имя домена SdwLengthOfDomainName, // длина имени домена StypeOfSid)) // тип учетной записи { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode;
912 Часть XL Управление безопасностью в Windows } // преобразуем SID в строку if (!ConvertSidToStringSid( IpSid, &StringSid)) { dwRetCode = GetLastError(); printf("Convert sid to string sid failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // печатем SID printf (,,%s\n"/StringSid) ; LocalFree(StringSid); // определяем длину списка DACL dwDaclLength = sizeof(ACL) + sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD) + dwLengthOfSid; // распределяем память под DACL lpDacl = (ACL*)new char[dwDaclLength]; // инициализируем список DACL if (UnitializeAcl( lpDacl, // адрес DACL dwDaclLength, // длина DACL ACL_REVISION)) // версия DACL { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // добавляем разрешающий элемент в список DACL
Глава 43. Работа с маркерами доступа 913 if (! AddAccessAl 1 owedAce ( lpDacl, // адрес DACL ACL_REVISION, // версия DACL GENERIC_ALL, // разрешаем все родовые права доступа IpSid)) // адрес SID { dwRetCode = GetLastError(); perrorC'Add access allowed ace failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // устанавливаем новый DACL по умолчанию в маркер доступа if (!SetTokenlnformation( hTokenHandle, // дескриптор маркера доступа TokenDefaultDacl, У/ устанавливаем список DACL &lpDacl, // адрес буфера с новым списком DACL dwDaclLength)) // размер буфера { dwRetCode = GetLastError(); printf( "Set token information failed: %u\n", dwRetCode) return dwRetCode; } // создаем мьютекс hMutex = CreateMutex(NULL, FALSE, "DemoMutex"); if (hMutex == NULL) { dwRetCode = GetLastError(); perror("Create mutex failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } printf("The mutex is created.\n"); // освобождаем память
914 Часть XI. Управление безопасностью в Windows delete[] IpSid; de1еte[] lpDomainName; delete[] lpDacl; // теперь проверяем список DACL мьютекса // определяем требуемую длину буфера if (!GetKernelObjectSecurity( hMutex, // дескриптор мьютекса DACL_SECURITY_INFORMATION, // получаем список DACL мьютекса IpSd, // адрес SD О, // определяем длину буфера &dwLengthOfSd)) // требуемая длина буфера { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) // распределяем память для буфера IpSd = (SECURITY_DESCRIPTOR*) new char[dwLengthOfSd]; else { // выходим из программы printfC'Get kernel object security failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } } // читаем идентификатор безопасности мьютекса if (!GetKernelObjectSecurity( hMutex, // дескриптор мьютекса DACL_SECURITY_INFORMATION, // получаем список DACL мьютекса IpSd, // адрес SD dwLengthOfSd, // длина буфера &dwLengthOfSd)) // требуемая длина буфера { dwRetCode = GetLastError(); printfC'Get kernel object security failed.\n");
Глава 43. Работа с маркерами доступа 915 printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // получаем список DACL из дескриптора безопасности if (IGetSecurityDescriptorDacl( IpSd, // адрес дескриптора безопасности SbDaclPresent, // признак присутствия списка DACL SlpDacl, // адрес указателя на DACL SbDaclDefaulted)) // признак списка DACL по умолчанию { dwRetCode = GetLastError(); printf("Get security descriptor DACL failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // проверяем, есть ли DACL if (IbDaclPresent) { printf("Dacl is not present.\n"); return 0; } else printf("Dacl is present.\n"); // проверяем, установлен ли DACL пользователем if (bDaclDefaulted == SE_DACL_DEFAULTED) printf("Dacl is defaulted.\n"); else printf("Dacl is not defaulted.\n"); // печатаем количество элементов printf("Ace count: %u\n", lpDacl->AceCount); // получаем элементы списка DACL for (unsigned i = 0; i < lpDacl->AceCount; ++i)
916 Часть XL Управление безопасностью в Windows { if (!GetAce( lpDacl, // адрес DACL i, // индекс элемента &1рАсе)) // указатель на элемент списка { dwRetCode = GetLastError(); printf("Get ace failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // выводим на печать тип элемента и SID if (((ACE_HEADER*)lpAce)->AceType == ACCESS_ALLOWED_ACE_TYPE) { printf("ACCESS_ALLOWED_ACE_TYPE\n"); // преобразуем SID в строку if (!ConvertSidToStringSid( &((ACCESS_ALLOWED_ACE*)lpAce)->SidStart, &StringSid)) { dwRetCode = GetLastError(); printf("Convert sid to string sid failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // печатаем SID printf("%s\nn,StringSid); LocalFree(StringSid); } } // освобождаем память delete[] IpSd; CloseHandle(hMutex); CloseHandle(hTokenHandle); return 0;
Глава 43. Работа с маркерами доступа 917 43.6. Настройка привилегий Теперь рассмотрим, как настраивать привилегии в маркере доступа. Вообще привилегии даются пользователю при регистрации этого пользователя в системе. Каждый пользователь имеет свой набор привилегий, некоторые из которых могут быть установлены или действительны, а некоторые — сброшены или отменены. Маркер доступа получает свой набор привилегий из учетной записи пользователя, которая явилась источником создания данного маркера доступа. Добавить новые или удалить существующие привилегии в маркер доступа невозможно. Можно только сделать привилегию действительной или отмененной. Или, другими словами, установить или сбросить привилегию соответственно. Назовем такую работу с привилегиями настройкой привилегий в маркере доступа. Для настройки привилегий, доступных в маркере доступа, используется функция AdjustTokenPrivileges, которая имеет следующий прототип: BOOL AdjustTokenPrivileges( HANDLE TokenHandle, // дескриптор маркера доступа BOOL DisableAllPrivileges, // флаг сброса всех привилегий PTOKEN_PRIVILEGES NewState, // новое состояние привилегий DWORD BufferLength, // длина буфера PreviousState PTOKEN_PRIVILEGES PreviousState, // старое состояние привилегий PDWORD ReturnLength // требуемая длина буфера PreviousState ); В случае удачного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного завершения функции можно получить посредством вызова функции GetLastError. Параметры функции AdjustTokenPrivileges имеют следующее назначение. Параметр TokenHandle должен содержать дескриптор маркера доступа, в котором настраиваются привилегии. Причем маркер доступа должен быть открыт в режиме token_adjust_privileges. В параметре DisabieAiiPriviieges должно быть установлено одно из значений: false или true. Если установлено значение false, to функция настраивает привилегии в соответствии с информацией, заданной параметром NewState. Если же в этом параметре установлено значение true, to функция отменяет все привилегии в заданном маркере доступа. Параметр NewState должен указывать на структуру типа token_privileges, которая содержит привилегии и новые состояния для каждой из перечисленных привилегий. Если для привилегии установлен флаг se_privilege_enabled, то привилегия становится действительной, в противном случае — привилегия отменяется. Подробно структура типа token_privileges была рассмотрена в разд. 43.3.
918 Часть XI. Управление безопасностью в Windows Параметр Buf ferLength должен содержать размер буфера памяти, на который указывает параметр Previousstate. Если в Previousstate установлено значение null, то в этом параметре может быть установлено значение 0. Параметр Previousstate должен указывать на буфер, в который функция запишет структуру типа token_privileges. Эта структура будет содержать предыдущее состояние привилегий маркера доступа. В этом параметре может быть установлено значение null. В этом случае функция не вернет предыдущее состояние привилегий маркера доступа. Если длина буфера недостаточна для записи предыдущего состояния привилегий, то функция закончится неудачей, а требуемая длина буфера будет записана в переменную, на которую указывает параметр ReturnLength. При этом заметим, что предыдущее состояние привилегий маркера доступа используется для восстановления привилегий в первоначальное состояние. Параметр ReturnLength должен указывать на переменную типа pdword, в которую функция запишет требуемый размер буфера для предыдущего состояния привилегий маркера доступа. Если размер, заданный параметром Buf ferLength, меньше необходимого. Если в параметре Previousstate установлено значение null, то в этом параметре также может быть установлено значение null. Примеры Использования функции AdjustTokenPrivileges ДЛЯ настроЙКИ в маркере доступа состояния привилегии se_security_name, которая разрешает доступ к списку DACL, содержатся в программах из листингов 41.4 и 41.6, которые приведены в разд. 41.5 и 41.6 соответственно. 43.7. Настройка групп Под настройкой групп в маркере доступа понимаем установку или сброс флага se_group_enabled для каждой группы, связанной с заданным маркером доступа. При этом отметим, что этот флаг не может быть установлен для группы с установленным флагом se_group_use_for_deny_only. Кроме того, заметим, что флаг se_group_enabled не может быть сброшен для группы с установленным флагом se_group_mandatory. Подробно флаги, задающие атрибуты группы, рассмотрены в разд. 43.3, при описании возможных Значений ПОЛЯ Attributes структуры ТИПа TOKEN_GROUPS. ДЛЯ НастроЙКИ групп ИСПОЛЬЗуетСЯ функция AdjustTokenGroups, которая имеет следующий прототип: BOOL Adjus tTokenGroups( HANDLE TokenHandle, // дескриптор маркера доступа BOOL ResetToDefault, // сбросить в состояние по умолчанию PTOKEN_GROUPS NewState, // новое состояние групп
Глава 43. Работа с маркерами доступа 919 DWORD BufferLength, // длина буфера PreviousState PTOKEN_GROUPS PreviousState, // предыдущее состояние групп PDWORD ReturnLength // требуемая длина буфера PreviousState ); В случае удачного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного завершения функции можно получить посредством вызова функции GetLastError. Параметры функции AdjustTokenPriviieges имеют следующее назначение. Параметр TokenHandie должен содержать дескриптор маркера доступа, в котором настраиваются группы. Причем маркер доступа должен быть открыт в режиме token_adjust_groups. В параметре ResetToDefauit должно быть установлено одно из значений: false или true. Если установлено значение false, to функция настраивает группы в соответствии с информацией, заданной параметром Newstate. Если же в этом параметре установлено значение true, to функция переводит все группы в состояния, которые были заданы по умолчанию. Параметр Newstate должен указывать на структуру типа token_groups, которая содержит группы и новые флага состояния se_group_enabled для каждой из перечисленных групп. Если для группы установлен флаг se_group_enabled, то этот флаг будет также установлен и для соответствующей группы в маркере доступа, в противном случае этот флаг будет сброшен для соответствующей группы в маркере доступа. Подробно структура типа token_groups была рассмотрена в разд. 43.3. Параметр Buf ferLength должен содержать размер буфера памяти, на который указывает параметр PreviousState. Если В параметре PreviousState установлено значение null, то в этом параметре может быть установлено значение 0. Параметр PreviousState должен указывать на буфер, в который функция запишет структуру типа token_groups. Эта структура будет содержать предыдущее состояние групп, связанных с маркером доступа. В этом параметре может быть установлено значение null. В этом случае функция не вернет предыдущее состояние групп из маркера доступа. Если длина буфера недостаточна для записи предыдущего состояния групп, то функция закончится неудачей, а требуемая длина буфера будет записана в переменную, на которую указывает параметр ReturnLength. При этом заметим, что предыдущее состояние групп используется для восстановления состояния групп в начальное состояние. Параметр ReturnLength должен указывать на переменную типа pdword, в которую функция запишет требуемую длину буфера для предыдущего состояния групп маркера доступа, если длина заданная параметром BufferLength меньше необходимой. ЕСЛИ В параметре PreviousState уста-
920 Часть XI. Управление безопасностью в Windows новлено значение null, то в этом параметре также может быть установлено значение null. 43.8. Создание маркера ограниченного доступа Маркером ограниченного доступа называется маркер доступа, который создается из существующего маркера доступа, но может иметь по сравнению с оригиналом следующие офаничения: □ некоторые идентификаторы безопасности, которые в исходном маркере доступа использовались для проверки разрешения доступа к охраняемому объекту, в офаничивающем маркере доступа будут использоваться для проверки запрещения доступа к объекту. То есть идентификатор безопасности, имеющий атрибут se_group_enabled в исходном маркере доступа, будет иметь атрибут se_group_use_for_deny_only в маркере офаниченного доступа; □ некоторые привилегии могут быть отменены в маркере офаниченного доступа, хотя они были действительны в исходном маркере доступа; □ маркер ограниченного доступа может иметь дополнительные идентификаторы безопасности, которые используются для проверки разрешения доступа субъекта, который представляется этим маркером доступа, к охраняемому объекту; такие идентификаторы безопасности называются ограничивающими идентификаторами безопасности. То есть маркер офаниченного доступа имеет два множества связанных с ним идентификаторов безопасности. Одно множество идентификаторов безопасности наследуется от исходного маркера доступа, а второе — включает офаничивающие идентификаторы безопасности, которые добавлены в маркер ограниченного доступа при его создании. Офаничение доступа к охраняемому объекту при помощи маркера офаниченного доступа выполняется посредством того, что система управления безопасностью конфолирует доступ, используя как идентификаторы безопасности, наследуемые маркером офаниченного доступа от исходного маркера, так и офаничивающие маркеры, которые были добавлены в маркер офаниченного доступа. По сути, маркер офаниченного доступа и определяется наличием в нем списка офаничивающих идентификаторов безопасности. Если этот список пуст, то маркер доступа не считается ограниченным в доступе к охраняемым объектам. Маркеры ограниченного доступа используются, во-первых, для создания НОВОГО процесса посредством ВЫЗОВа функции CreateProcessAsUser, КОТОРЫЙ имеет ограниченный контекст безопасности и может иметь свой рабочий стол; во-вторых, для офаничения контекста безопасности текущего Процесса Посредством ВЫЗОВа функции ImpersonateLoggedOnUser.
Глава 43. Работа с маркерами доступа 921 Для создания маркера офаниченного доступа используется функция CreateRestrictedToken, Которая Имеет следующий ПРОТОТИП! BOOL CreateRestrictedToken( HANDLE ExistingTokenHandle, // дескриптор исходного маркера доступа DWORD Flags, // флаг сброса привилегий DWORD DisableSidCount, // количество запрещающих SID PSID_AND_ATTRIBUTES SidsToDisable, // массив запрещающих SID DWORD DeletePrivilegeCount, // количество отменяемых привилегий PLUID_AND_ATTRIBUTES PrivilegesToDelete, // массив отменяемых // привилегий DWORD RestrictedSidCount, // количество ограничивающих SID PSID_AND_ATTRIBUTES SidsToRestrict, // массив ограничивающих SID PHANDLE NewTokenHandle // указатель на дескриптор нового // маркера доступа ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного завершения функции можно получить посредством вызова функции GetLastError. Параметры функции CreateRestrictedToken имеют следующее назначение. Параметр ExistingTokenHandle ДОЛЖен содержать дескриптор ИСХОДНОГО маркера доступа, который может быть первичным или замещающим маркером доступа, а также маркером офаниченного доступа. Данный маркер должен быть открыт в режиме token_duplicate. В параметре Flags может быть установлен флаг disable_max_privilege, который отменяет все привилегии в маркере офаниченного доступа. Если этот флаг установлен, ТО функция Игнорирует параметры DeletePrivilegeCount И PrivilegesToDelete. Параметр DisableSidCount должен содержать количество запрещающих идентификаторов безопасности в массиве, на который указывает следующий параметр SidsToDisable. Параметр SidsToDisable должен указывать на массив структур типа sid_and_attributes, каждая из которых содержит запрещающий идентификатор безопасности, для которого будет установлен режим se_group_use_ for_deny_only в маркере офаниченного доступа. Отметим, что эти запрещающие идентификаторы должны быть связаны с исходным маркером доступа. Функция игнорирует идентификаторы безопасности, которые не связаны с исходным маркером доступа. Функция также игнорирует атрибуты идентификатора безопасности, которые установлены в поле Attributes структуры sid_and_attributes. Если функция не изменяет атрибуты дескрипторов безопасности, которые связаны с исходным маркером доступа,
922 Часть XI. Управление безопасностью в Windows то в этом параметре можно установить значение null. Подробное описание структуры типа sid_and_attributes, которая описывает элементы массива sidsToDisabie, было приведено в разд. 43.3. Параметр DeietePriviiegeCount должен содержать количество отменяемых привилегий в массиве, на который указывает следующий параметр PrivilegesToDelete. Параметр PrivilegesToDelete должен указывать на массив структур типа luid_and_attributes, каждая из которых содержит локальный идентификатор привилегии, которая должна быть отменена в маркере ограниченного доступа. Функция игнорирует все привилегии, которые не входят в исходный маркер доступа. Функция также игнорирует атрибуты привилегий, которые установлены В ПОЛе Attributes Структуры ТИПа LUID_AND_ATTRIBUTES. Если функция не отменяет привилегии исходного маркера доступа, то в этом параметре можно установить значение null. Подробное описание структуры типа luid_and_attributes было приведено в разд. 42.1. Параметр Restrictedsidcount должен содержать количество ограничивающих идентификаторов в массиве, на который указывает следующий параметр SidsToRestrict. Параметр SidsToRestrict должен указывать на массив структур типа sid_and_attributes, каждая из которых содержит ограничивающий идентификатор безопасности, который будет связан с новым маркером ограниченного доступа. Ограничивающие идентификаторы безопасности не связаны с исходным маркером доступа и используются только для проверки разрешения доступа маркера ограниченного доступа к охраняемому объекту. Поле Attributes структуры sid_and_attributes должно содержать нулевое значение для каждого ограничивающего маркера доступа. Если функция не связывает с маркером ограниченного доступа ограничивающих идентификаторов безопасности, то в этом параметре можно установить значение null. Если исходным маркером доступа является маркер ограниченного доступа, то с новым маркером ограниченного доступа будут связаны только те ограничивающие идентификаторы безопасности, которые одновременно связаны с исходным маркером ограниченного доступа и находятся в массиве, на который указывает параметр SidsToRestrict. Подробное описание структуры типа sid_and_attributes, которая описывает элементы массива SidsToRestrict, было приведено в разд. 43.3. Параметр NewTokenHandie должен содержать адрес переменной типа handle, в которую функция запишет дескриптор созданного маркера ограниченного доступа. Созданный маркер доступа имеет тот же тип, что и исходный маркер доступа, т. е. может быть первичным или замещающим маркером доступа. А также для нового маркера доступа установлены те же режимы доступа, что и для исходного маркера доступа.
Глава 43. Работа с маркерами доступа 923 Для проверки является ли заданный маркер доступа маркером ограниченного доступа используется функция isTokenRestricted, которая имеет следующий прототип: BOOL IsTokenRestricted( HANDLE TokenHandie // дескриптор маркера доступа ); Единственный параметр этой функции должен содержать дескриптор маркера доступа, который подвергается проверке на ограничения по доступу к охраняемым объектам. Если маркер доступа, который содержится в параметре TokenHandie, содержит список ограничивающих идентификаторов безопасности, то функция вернет ненулевое значение, в противном случае — false. Код ошибки в случае неудачного завершения функции можно получить Посредством ВЫЗОВа фуНКЦИИ GetLastError. В листинге 43.4 приведена программа, в которой при помощи функции CreateRestrictedToken создается маркер ограниченного доступа. После этого этот маркер проверяется функцией isTokenRestricted на ограниченность. Листинг 43Ж Создание и проверка маркера ограниченного доступа I #include <windows.h> #include <stdio.h> #include <lm.h> int main() { HANDLE hProcess; HANDLE hToken; HANDLE hRestrict; // дескриптор процесса // дескриптор маркера доступа // дескриптор ограничивающего маркера доступа char chUserName[UNLEN]; // имя пользователя DWORD dwLengthOfUserName = UNLEN; // длина имени учетной записи DWORD dwLengthOfSID =0; // длина SID DWORD dwLengthOfDomainName =0; // длина имени домена SID *lpSid = NULL; // указатель на SID LPTSTR lpDomainName = NULL; // указатель на имя домена
924 Часть XI. Управление безопасностью в Windows SID_NAME_USE type_of_SID; // тип учетной записи SID_AND_ATTRIBUTES RestrictingSid; // ограничивающий SID DWORD dwRetCode; // код возврата // получаем дескриптор процесса hProcess = GetCurrentProcess(); // получаем маркер доступа процесса if (!OpenPrосessToken( hProcess, // дескриптор процесса TOKEN_ALL_ACCESS, // полный доступ к маркеру доступа &hToken)) // дескриптор маркера { dwRetCode = GetLastError(); printf( "Open process token failed: %u\n"/ dwRetCode); return dwRetCode; } printf("Input a user name: "); gets(chUserName); // вводим имя пользователя // определяем длину SID пользователя if (!LookupAccountName( NULL, // ищем имя на локальном компьютере chUserName, // имя пользователя NULL, // определяем длину SID SdwLengthOfSID, // длина SID NULL, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) { // распределяем память для SID и имени домена IpSid = (SID*) new char[dwLengthOfSID];
Глава 43. Работа с маркерами доступа 925 lpDomainName = (LPTSTR) new wchar_t [dwLengthOfDomainName] ; } else { // выходим из программы printf("Lookup account name failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } } //определяем SID и имя домена пользователя if(!LookupAccountName( NULL, // ищем имя на локальном компьютере chUserName, // имя пользователя IpSid, // указатель на SID SdwLengthOfSID, // длина SID lpDomainName, // указатель на имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // инициализируем ограничивающий SID RestrictingSid.Sid = IpSid; RestrictingSid.Attributes = 0; // создаем ограничивающий маркер доступа if (!CreateRestrictedToken( hToken, // дескриптор исходного маркера доступа 0, // привилегии не изменяем 0, NULL, // флаг SE_GROUP_USE_FOR_DENY_ONLY не устанавливаем
926 Часть XI. Управление безопасностью в Windows 0, NULL, // привилегии не удаляем 1, // один ограничивающий SID &RestrictingSidf // адрес ограничивающего SID &hRestrict)) // адрес ограничивающего маркера доступа { dwRetCode = GetLastError(); printf("Create restricted token failed.\n"); printf("Error code: %d\n'\ dwRetCode); return dwRetCode; } V/ проверяем замещение маркера доступа if (IsTokenRestricted(hRestrict)) printf("The restricted token is created.\n"); else { dwRetCode = GetLastError(); printf("Is Token Restricted failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // замещаем контекст безопасности потока if (! ImpersonateLoggedOnUser(hRestrict)) { dwRetCode = GetLastError(); printf("Impersonate logged on user failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // здесь делаем какую-то работу
Глава 43. Работа с маркерами доступа 927 printf("Some work is done.\n"); // возвращаем исходный контекст if (IRevertToSelf()) { dwRetCode = GetLastError(); printf("Revert to self failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } CloseHandle(hToken); return 0; } 43.9. Дублирование маркеров доступа Под дублированием маркера доступа понимается создание нового маркера доступа, который может отличаться от исходного маркера доступа установленными режимами доступа, дескриптором безопасности и может быть как первичным, так и замещающим маркером доступа. В дублированном маркере доступа остается без изменения список идентификаторов безопасности, связанных с исходным маркером доступа. ДЛЯ Дублирования Маркеров ДОСТупа ИСПОЛЬЗуеТСЯ фуНКЦИЯ DuplicateToken, которая имеет следующий прототип: BOOL DuplicateToken( HANDLE ExistingTokenHandle, // дескриптор исходного маркера доступа SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,// уровень замещения PHANDLE DuplicateTokenHandle // дескриптор нового маркера доступа ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного завершения функции можно получить посредством вызова функции GetLastError. Параметры функции DupiicateToken имеют следующее назначение. Параметр ExistingTokenHandle должен содержать дескриптор исходного маркера доступа, который будет продублирован. Исходный маркер доступа должен быть открыт в режиме доступа token_duplicate.
928 Часть XI. Управление безопасностью в Windows В параметре impersonationLevel должно быть установлено одно из значений перечисления типа security_impersonation_level, которое определяет уровень замещения дублированным маркером доступа исходного маркера доступа. Перечисление типа security_impersonation_level было определено в разд. 43.3. Подробнее об уровнях замещения маркеров доступа было рассказано в разд. 36.8. Параметр DupiicateTokenHandie должен указывать на переменную типа handle, в которую функция запишет дескриптор нового маркера доступа, полученного дублированием исходного маркера доступа. Для нового маркера доступа будут установлены режимы доступа token_impersonate и TOKEN_QUERY. Программа, в которой создается дубликат маркера доступа, используя для этого функцию DupiicateToken, приведена в листинге 43.5. Для дублирования маркера доступа с возможностью изменения в новом маркере доступа свойств, установленных в исходном маркере доступа, используется функция DupiicateTokenEx, которая имеет следующий прототип: BOOL DupiicateTokenEx( HANDLE hExistingToken, // дескриптор исходного маркера доступа DWORD dwDesiredAccess, // права доступа LPSECURITY_ATTRIBUTES lpTokenAttributes, // атрибуты безопасности SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,// уровень замещения TOKEN_TYPE TokenType, // тип маркера доступа PHANDLE phNewToken // дескриптор нового маркера доступа ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного завершения функции можно получить посредством вызова функции GetLastError. Параметры функции DupiicateTokenEx имеют следующее назначение. Параметр hExistingToken должен содержать дескриптор исходного маркера доступа, который будет продублирован. Исходный маркер доступа должен быть открыт в режиме доступа token_duplicate. Параметр dwDesiredAccess ДОЛЖен Содержать реЖИМЫ Доступа К НОВОМУ маркеру доступа. При установке новых режимов доступа функция DupiicateTokenEx проверяет список DACL исходного маркера доступа, чтобы определить разрешены или запрещены эти режимы для учетной записи, от ИМеНИ КОТОРОЙ ВЫПОЛНЯеТСЯ фуНКЦИЯ DupiicateTokenEx. ЕСЛИ реЖИМЫ ДОСТупа существующего маркера доступа нужно продублировать в новом маркере доступа, то в этом параметре должно быть установлено значение 0. Если в новом маркере доступа нужно установить максимально возможные режимы доступа, то в этом параметре должно быть установлено значение maximum_allowed.
Глава 43. Работа с маркерами доступа 929 Параметр lpTokenAttributes должен указывать на атрибуты безопасности нового маркера доступа. Если в этом параметре установлено null, to в новом маркере доступа устанавливается дескриптор безопасности, определяемый по умолчанию. В параметре impersonationLevel должно быть установлено одно из значений перечисления типа security_impersonation_level, которое определяет уровень замещения дублированным маркером доступа исходного маркера доступа. Определение перечисления типа security_impersonation_level было приведено в разд. 43.3. Подробнее об уровнях замещения маркеров доступа было рассказано в разд. 36.8. В параметре токептуре должно быть установлено одно из значений перечисления token_type, которое определяет, является новый маркер доступа первичным или замещает первичный маркер доступа. Определение перечисления типа token_type было приведено в разд. 43.3. Подробнее о замещении маркеров доступа было рассказано в разд. 36.8. Параметр DupiicateTokenHandie должен указывать на переменную типа handle, в которую функция запишет дескриптор нового маркера доступа, полученного дублированием исходного маркера доступа. 43.10. Замещение маркеров доступа потока Замещением маркера доступа будем называть подмену маркера доступа одного потока на маркер доступа другого потока. При этом маркер доступа, который замещает исходный маркер доступа, должен иметь тип Tokenlmpersonation. Напомним, ЧТО значение Tokenlmpersonation является перечислимой константой, которая принадлежит перечислению типа token_type. Определение этого перечисления было дано в разд. 43.3. Для замещения маркера доступа потока используется функция setThreadToken, которая имеет следующий прототип: BOOL SetThreadToken( PHANDLE Thread, // указатель на дескриптор потока HANDLE Token // дескриптор замещающего маркера доступа ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного завершения функции можно получить посредством вызова функции GetLastError. Параметры функции setThreadToken имеют следующее назначение. Параметр Thread должен содержать адрес дескриптора потока, для которого будет замещен маркер доступа. Если в этом параметре установлено значение 30 Зак. 514
930 Часть XI. Управление безопасностью в Windows null, то маркер доступа будет замещен для потока, вызвавшего функцию SetThreadToken. Параметр Token должен содержать дескриптор маркера доступа, который заместит маркер доступа в потоке, дескриптор которого задан первым параметром. Если в параметр Token установлено значение null, to функция завершит поток, используя замещающий маркер доступа. В листинге 43.5 приведена программа, в которой создается дубликат маркера ДОСТупа, ИСПОЛЬЗУЯ ДЛЯ ЭТОГО фуНКЦИЮ DuplicateToken. После ЭТОГО При помощи функции SetThreadToken созданный дубликат замещает маркер доступа потока. # include <windows. h> #include <stdio.h> volatile UINT count; volatile BOOL flag = TRUE; void thread() { while(flag) { COUnt++; printf("count = %u\n"/ count); SleepE00); } } int main() { HANDLE hThread; DWORD IDThread; HANDLE hProcess; HANDLE hToken; HANDLE hDuplicate; // дескриптор процесса // дескриптор маркера доступа // дескриптор дубликата маркера доступа DWORD dwRetCode; // код возврата
Глава 43. Работа с маркерами доступа 931 II запускаем поток hThread = CreateThread( NULL, // защита по умолчанию О, //размер стека по умолчанию (LPTHREAD_START_ROUTINE) thread, // адрес программы потока NULL, // параметров нет CREATE_SUSPENDED, // подвешенное состояние потока &IDThread); // идентификатор потока if (hThread == NULL) { printf("Create thread failed.\n"); return GetLastError(); } // получаем дескриптор процесса hProcess = GetCurrentProcess(); // получаем маркер доступа потока if (!OpenProcessToken( hProcess, // дескриптор процесса TOKEN_DUPLICATE, // дублирование маркера доступа &hToken)) // дескриптор маркера { dwRetCode = GetLastError(); printf( "Open process token failed: %u\n", dwRetCode); return dwRetCode; } // дублируем маркер доступа if (!DuplicateToken( hToken, // маркер доступа SecurityImpersonation, // уровень замещения &hDuplicate)) // адрес дубликата маркера доступа { dwRetCode = GetLastError(); printf( "Duplicate token failed: %u\n", dwRetCode); return dwRetCode;
932 Часть XI. Управление безопасностью в Windows } // устанавливаем замещающий маркер доступа для потока if (!SetThreadToken( &hThread, // адрес дескриптора потока hDuplicate)) // дубликат маркера доступа { dwRetCode = GetLastError(); printf( "Set thread token failed: %u\n'\ dwRetCode); return dwRetCode; } printf("Press any key to exit.Xn"); // возобновляем поток ResumeThread(hThread); // ждем команду на завершение потока getchar(); flag = FALSE; // завершить поток CloseHandle(hThread); CloseHandle(hToken); return 0; } 43.11. Проверка идентификатора безопасности на принадлежность маркеру доступа Действительными идентификаторами безопасности в маркере доступа называются такие идентификаторы, для которых установлен флаг se_group_enabled. Для проверки, является ли заданный идентификатор действительным в маркере доступа, который замещает первичный маркер доступа, используется ФУНКЦИЯ CheckTokenMembership, которая имеет следующий ПроТОТИП! BOOL CheckTokenMembership ( HANDLE TokenHandle, // дескриптор маркера доступа
Глава 43. Работа с маркерами доступа 933 PSID SidToCheck, // указатель на идентификатор безопасности PBOOL IsMember // результат проверки ); В случае удачного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного завершения функции можно получить посредством вызова функции GetLastError. Параметры функции checkTokenMembership имеют следующее назначение. Параметр TokenHandle должен содержать дескриптор маркера доступа, который замещает первичный маркер доступа. Маркер доступа должен быть открыт в режиме token_query. Функция проверяет связанные с этим маркером доступа идентификаторы безопасности, чтобы определить, является ли заданный идентификатор безопасности действительным. Если в этом параметре установлено значение null, to функция проверяет идентификаторы безопасности в замещающем маркере доступа потока, вызвавшего эту функцию. Если этот маркер доступа не является замещающим, то функция дублирует его, чтобы создать замещающий маркер доступа, а затем проводит проверку. Параметр sidTocheck должен указывать на идентификатор безопасности, для которого функция выполняет проверку на действительность. Параметр IsMember должен указывать на переменную типа bool, в которую функция запишет результат проверки. Если идентификатор безопасности, на который указывает параметр sidTocheck, связан с маркером доступа и для этого идентификатора установлен флаг se_group_enabled, to функция запишет по адресу IsMember значение true, иначе — false. В листинге 43.6 приведена программа, в которой замещается маркер доступа потока thread, а потом этот поток проверяет — установлен ли атрибут se_group_enabled для заданной группы, имя которой вводится с консоли. [ Листинг 43.6. Проверка состояния атрибута se_group_enabled #include <windows.h> #include <stdio.h> #include <lm.h> extern "C" BOOL WINAPI CheckTokenMembership (HANDLE, PSID, PBOOL); DWORD WINAPI thread(LPVOID IpSid) { HANDLE hlmperson; // дескриптор замещенного потока BOOL blsMember; // признак присутствия SID
934 Часть XI. Управление безопасностью в Windows DWORD dwRetCode; // код возврата // получаем маркер доступа потока if (!OpenThreadToken( GetCurrentThread(), TOKEN_QUERY, FALSE, &hImperson)) { dwRetCode = GetLastError(); printf("Open Thread Token failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // проверяем принадлежность маркера доступа if (!CheckTokenMembership( hlmperson, IpSid, ficblsMember)) { dwRetCode = GetLastError(); printf("Check token membership failed.\n"); printf("Error code: %d\n", dwRetCode); CloseHandle(hlmperson); return dwRetCode; } if (blsMember) printf("The group is enabled.\n"); else printf("The group is disabled or not present.\n"); CloseHandle(hlmperson);
Глава 43. Работа с маркерами доступа 935 return 0; } int main() { HANDLE hThread; DWORD IDThread; HANDLE hToken; // дескриптор маркера доступа HANDLE hDuplicate; // дескриптор дубликата маркера доступа char chGroupName[GNLEN]; // имя пользователя DWORD dwLengthOfUserName = GNLEN; // длина имени учетной записи DWORD dwLengthOfSID =0; // длина SID DWORD dwLengthOfDomainName = 0; // длина имени домена PSID IpSid = NULL; // указатель на проверяемый SID LPTSTR lpDomainName = NULL; // указатель на имя домена SID_NAME_USE typeOfSid; // тип учетной записи DWORD dwRetCode; // код возврата printf("Input a group name: "); gets(chGroupName); // вводим имя пользователя // определяем длину SID пользователя if (!LookupAccountName( NULL, // ищем имя на локальном компьютере chGroupName, // имя группы NULL, // определяем длину SID SdwLengthOfSID, // длина SID NULL, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &typeOfSid)) // тип учетной записи { dwRetCode = GetLastError();
936 Часть XI. Управление безопасностью в Windows if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) { // распределяем память для SID и имени домена IpSid = (SID*) new char[dwLengthOfSID]; lpDomainName = (LPTSTR) new char [dwLengthOfDomainName] ; } else { // выходим из программы printf("Lookup account name failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } } // определяем SID и имя домена пользователя if(!LookupAccountName( NULL, // ищем имя на локальном компьютере chGroupName, // имя пользователя IpSid, // указатель на SID SdwLengthOfSID, // длина SID lpDomainName, // указатель на имя домена &dwLengthOfDomainName, // длина имени домена &typeOfSid)) // тип учетной записи { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // проверяем тип идентификатора безопасности if (typeOfSid != SidTypeAlias) { printf("This is not an alias.\n"); return 0;
Глава 43. Работа с маркерами доступа 937 // получаем маркер доступа процесса if (!OpenProcessToken( GetCurrentProcess(), // дескриптор процесса TOKEN_DUPLICATE, // дублирование маркера доступа ShToken)) // дескриптор маркера { dwRetCode = GetLastError(); printf( "Open process token failed: %u\n", dwRetCode); return dwRetCode; } // дублируем маркер доступа if (!DuplicateToken( hToken, // маркер доступа Securitylmpersonation, // уровень замещения ShDuplicate)) // адрес дубликата маркера доступа { dwRetCode = GetLastError(); printf( "Duplicate token failed: %u\n", dwRetCode); return dwRetCode; } // запускаем поток hThread = CreateThread( NULL, // защита по умолчанию 0, // размер стека по умолчанию thread, // адрес программы потока (LPVOID) IpSid, // адрес проверяемого SID CREATE_SUSPENDED, // подвешенное состояние потока SIDThread); // идентификатор потока if (hThread == NULL) { printf("Create thread failed.\n"); return GetLastError();
938 Часть XI. Управление безопасностью в Windows // устанавливаем замещающий маркер доступа для потока if (!SetThreadToken( &hThread, // адрес дескриптора потока hDuplicate)) // дубликат маркера доступа { dwRetCode = GetLastError(); printf( "Set thread token failed: %u\n", dwRetCode); return dwRetCode; } // возобновляем поток ResumeThread(hThread); // ждем завершение потока WaitForSingleObject(hThread/ INFINITE); CloseHandle(hToken); CloseHandle(hThread); return 0;
Глава 44 Работа со списками управления доступом на низком уровне В этой главе будут рассмотрены функции для работы со списками управления доступом на низком уровне. То есть программа сама должна инициализировать список управления доступом, элементы списка управления доступом и самостоятельно определять порядок включения элементов в список управления доступом. Работа со списками управления доступом на высоком уровне была рассмотрена в гл. 41. 44.1. Структура списка управления доступом При работе со списком управления доступом на низком уровне сам список хранится в непрерывной области памяти, которая структурирована следующим образом. Начинается список управления доступом с заголовка, за которым следуют элементы, контролирующие доступ к объекту. Заголовок списка управления доступом имеет следующую структуру: typedef struct _ACL { // версия списка управления доступом // равен О // длина списка управления доступом в байтах // количество элементов в списке // равен О BYTE AclRevision; BYTE Sbzl; WORD AclSize; WORD AceCount; WORD Sbz2 ; } ACL; Также определяется тип: typedef ACL *PACL; В настоящее время версия списка управления доступом определяется символической константой acl_revision, которая и должна быть установлена В ПОЛе AclRevision.
940 Часть XI. Управление безопасностью в Windows Структура элементов списка управления доступом будет описана в следующем разделе. 44.2. Структура элемента списка управления доступом Система безопасности операционной системы Windows 2000 поддерживает следующие три типа элементов, которые могут храниться в списках управления доступом любого охраняемого объекта: □ access_allowed_ace — разрешает доступ к объекту, хранится в списке DACL; □ access_denied_ace — запрещает доступ к объекту, хранится в списке DACL; □ system_audit_ace — обеспечивает генерацию аудиторской записи при доступе к объекту, хранится в списке SACL Каждому из этих типов соответствует определенный тип структуры. Определения типов этих структур следующие: typedef struct _ACCESS_ALLOWED_ACE { ACE_HEADER Header; // заголовок элемента ACCESS_MASK Mask; // маска доступа DWORD SidStart; // начало SID } ACCESS_ALLOWED_ACE; typedef struct _ACCESS_DENIED_ACE { ACE_HEADER Header; // заголовок элемента ACCESS_MASK Mask; // маска доступа DWORD SidStart; // начало SID } ACCESS_DENIED_ACE; typedef struct _SYSTEM_AUDIT_ACE { ACE_HEADER Header; // заголовок элемента ACCESS_MASK Mask; // маска доступа DWORD SidStart; // начало SID } SYSTEM_AUDIT_ACE; Как видим, хотя все эти структуры имеют разные типы, но их внутренняя структура одинакова. Опишем кратко назначение каждого поля этих структур. В поле Header элемента списка управления хранятся тип, управляющие флаги и размер элемента списка управления доступом. Это поле имеет тип структуры ace_header, которая будет подробно рассмотрена далее.
Глава 44. Работа со списками управления доступом на низком уровне 941 В поле Mask элемента списка управления доступом устанавливаются права доступа к охраняемому объекту, которые контролирует данный элемент. Само поле имеет тип: typedef DWORD ACCESS_MASK; То есть является типом dword, в котором установлены управляющие флаги, определяющие права доступа. Все права доступа к объекту делятся на три категории: специфические права доступа, стандартные права доступа и родовые права доступа. В этом поле может быть установлена любая комбинация флагов, рассмотренных далее. Для установки специфических прав доступа используется флаг specific_rights_all, который включает все специфические права доступа. Остальные флаги, используемые для специфических прав доступа, определяются конкретно для каждого типа объекта. Стандартные права доступа задаются следующими символьными константами: □ delete — право удаления объекта; □ read_control — право чтения управляющей информации из дескриптора безопасности, исключая содержимое списка DACL; □ synchronize — право использования объекта для синхронизации; □ write_dac — право записи в список DACL объекта; □ write_owner — право записи владельца объекта. Кроме того, для стандартных прав доступа определены символьные константы, которые включают несколько основных прав доступа: □ standard_rights_all — включает все права доступа; □ standard_rights_required — включает права delete, read_control, write_dac, write_owner; □ standard_rights_read — эквивалентно read_control; □ standard_rights_write — эквивалентно read_control; □ standard_rights_execute — эквивалентно read_control. Для установки родовых прав доступа используются следующие символьные константы: П generic_read — право читать объект; □ generic_write — право записывать объект; □ generic_execute — право исполнять объект. Следующее родовое право включает все основные родовые права доступа: □ generic_all — включает права generic_read, generic_write и GENERIC_EXECUTE.
942 Часть XI. Управление безопасностью в Windows Права доступа к списку SACL включают единственное право access_system_security — право доступа к списку SACL. Поле sidstart представляет собой переменную типа dword, которая определяет начало идентификатора безопасности, доступ которого к объекту контролирует данный элемент списка управления доступом. То есть это поле фактически содержит первые 32 бита идентификатора безопасности из элемента списка управления доступом. Теперь подробно опишем структуру типа ace_header, которая служит для описания заголовка элемента списка управления доступом. Тип этой структуры определяется следующим образом: typedef struct _ACE_HEADER { BYTE AceType; // тип элемента BYTE AceFlags; // флаги наследования и аудита WORD АсеSize; // длина элемента } ACE_HEADER; Кратко опишем назначение каждого поля этой структуры. В поле Асетуре должен быть установлен тип элемента списка управления доступом. Этот тип задается одной из следующих констант: □ access_allowed_ace_type — элемент типа access_allowed_ace; □ access_denied_ace_type — элемент типа access_denied_ace; □ SYSTEM_AUDIT_ACE_TYPE — ЭЛемеНТ ТИПа SYSTEM_AUDIT_ACE. В поле AceFlags устанавливаются флаги, управляющие наследованием элемента списка управления доступом при создании дочернего охраняемого объекта и аудитом доступа к охраняемому объекту. В этом поле может быть установлена комбинация из следующих флагов, управляющих наследованием элементов списка управления доступом: □ object_inherit_ace — элемент наследуется не контейнерным дочерним объектом; если не установлен флаг no_propagate_inherit_ace, to элемент также наследуется и контейнерным дочерним объектом, но при этом в дочернем объекте устанавливается флаг inherit_only_ace; □ container_inherit_ace — элемент наследуется контейнерным дочерним объектом; □ no_propagate_inherit_ace — элемент не наследуется; □ inherit_only_ace — отмечает, что элемент был унаследован от родительского объекта; этот элемент не участвует в контроле доступа к объекту; □ inherited_ace — отмечает, что элемент был унаследован от родительского объекта.
Глава 44. Работа со списками управления доступом на низком уровне 943 И флагов управляющих аудитом доступа к охраняемому объекту: □ successful_access_ace_flag — отмечает, что нужно генерировать аудиторское сообщение при успешном доступе к объекту; □ failed_access_ace_flag — отмечает, что нужно генерировать аудиторское сообщение при неудачной попытке доступа к объекту. Отметим, что два последних флага используются только в элементах, принадлежащих спискам управления доступом SACL. В поле Acesize хранится длина (в байтах) самого элемента из списка управления доступом. 44.3. Инициализация списка управления доступом Инициализация списка управления доступом заключается в задании размера буфера памяти, которую может занять список управления доступом, и установки версии списка управления доступом. Фактически инициализация списка управления доступом заключается в заполнении данными заголовка этого списка. Для инициализации списка управления доступом используется функция initiaiizeAci, которая имеет следующий прототип: BOOL InitiaiizeAci( PACL pAcl, // адрес буфера для ACL DWORD nAclLength, // длина буфера DWORD dwAclRevision // версия ACL ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции можно получить посредством вызова функции GetLastError. Параметры функции имеют следующее назначение. Параметр pAci должен указывать на область памяти, в которой будет храниться список управления доступом. В начало этой памяти будет записан заголовок списка управления доступом. Параметр nAciLength должен содержать длину памяти, выделенной для хранения списка управления доступом. Параметр dwAclRevision должен содержать версию списка управления доступом. В настоящее время эта версия задается символьной константой ACL_REVISION. В заключение этого раздела сделаем несколько замечаний относительно вычисления длины памяти, необходимой для списка управления доступом.
944 Часть XI. Управление безопасностью в Windows Так как список управления доступом содержит заголовок и элементы, то его длина соответственно равна сумме длин заголовка и всех элементов. Но при этом надо учитывать, что длина каждого элемента зависит от длины идентификатора безопасности, который хранится в этом элементе. В свою очередь длина идентификатора безопасности должна быть уменьшена на длину типа dword, т. е. длину поля sidstart, которое определяет в элементе списка управления доступом начала идентификатора безопасности. Например, длина списка управления доступом, который содержит два элемента (один из которых контролирует разрешение доступа, а второй — запрещение доступа), может вычисляться следующим образом: dwAclLength = sizeof(ACL) + sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD) + dwLengthOfSidAllow + sizeof(ACCESS_DENIED_ACE) - sizeof(DWORD) + dwLengthOfSidDeny; Программа, в которой для инициализации списка управления доступом используется функция initializeAcl, приведена в разд. 44.5, посвященном добавлению элементов в список управления доступом. 44.4. Проверка достоверности списка управления доступом Под проверкой достоверности списка управления доступом будем понимать проверку достоверности версии и длины этого списка. Длина списка управления доступом должна соответствовать количеству включенных в этот список элементов. Для проверки достоверности списка управления доступом используется функция isvaiidAcl, которая имеет следующий прототип: BOOL IsValidAcl( PACL pAcl // адрес списка DACL ); Если проверка списка на достоверность прошла успешно, то функция вернет ненулевое значение, в противном случае — false. Код ошибки в случае неудачного выполнения функции можно получить посредством вызова функции GetLastError. Единственный параметр pAcl этой функции должен указывать на список управления доступом, достоверность которого будет проверяться. Программа, в которой посредством функции isvaiidAcl проверяется достоверность списка управления доступом, приведена в разд. 44.5, посвященном добавлению элементов в список управления доступом.
Глава 44. Работа со списками управления доступом на низком уровне 945 44.5. Добавление элементов в список управления доступом В интерфейсе Win32 API существует семь функций, предназначенных для добавления элементов в список управления доступом на низком уровне. Уровень этих функций называется низким потому, что система не контролирует порядок элементов в списке управления доступом при включении нового элемента, используя одну из этих функций. Как правило, новый элемент включается в начало или конец списка управления доступом. Поэтому программа сама должна контролировать порядок, в котором элементы включаются в список управления доступом. Прежде чем перейти к описанию функций, просто перечислим их, попутно определяя их функциональное назначение. Затем подробно опишем каждую из функций. Для добавления в список управления доступом DACL элементов, разрешающих ДОСТУП, ИСПОЛЬЗУЮТСЯ фуНКЦИИ AddAccessAllowedAce И AddAccessAllowedAceEx. Для добавления в список управления доступом DACL элементов, запрещающих ДОСТУП, ИСПОЛЬЗУЮТСЯ фуНКЦИИ AddAccessDeniedAce И AddAccessDeniedAceEx. Для добавления в список управления доступом SACL элементов, контролирующих ДОСТУП, ИСПОЛЬЗУЮТСЯ фуНКЦИИ AddAuditAccessAce И AddAuditAccessAceEx. Для копирования элементов из одного списка управления доступом в другой СПИСОК управления ДОСТУПОМ ИСПОЛЬЗуетСЯ фуНКЦИЯ AddAce. Теперь перейдем к описанию каждой из перечисленных функций и примерам их использования. Для добавления в конец списка управления доступом элемента, разрешающего ДОСТУП К Объекту, ИСПОЛЬЗуетСЯ фуНКЦИЯ AddAccessAllowedAce, KOTO- рая имеет следующий прототип: BOOL AddAccessAllowedAce ( PACL pAcl, // адрес списка управления доступом DWORD dwAceRevision, // версия элемента списка управления доступом DWORD AccessMask, // маска доступа PSID pSid // адрес идентификатора безопасности ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ ПОСреДСТВОМ ВЫЗОВа фуНКЦИИ GetLastError. ВОЗМОЖНЫ следующие коды ошибок, возвращаемые функцией GetLastError, после завершения работы фуНКЦИИ AddAccessAllowedAce: □ error_allotted_space_exceeded — недостаточная длина буфера для ACL; □ error_invalid_acl — неправильный ACL;
946 Часть XI. Управление безопасностью в Windows П error_invalid_sid — неправильный SID; □ error_revision_mismatch — неправильная версия АСЕ; □ error_success — удачное завершение функции. Параметр pAci должен указывать на список управления доступом, в который добавляется элемент, контролирующий разрешение доступа субъекта к объекту Параметр dwAciRevision должен содержать версию элемента, включаемого в список управления доступом. В настоящее время эта версия задается символьной КОНСТаНТОЙ ACL_REVISION. Параметр AccessMask должен содержать разрешенные для учетной записи права доступа к охраняемому объекту, с которым связан данный список управления доступом. Идентификатор безопасности учетной записи задается параметром psid. Подробно возможные значения флагов для стандартных и родовых прав доступа описаны в разд. 36.7, 41.3. Специфические права доступа к объектам описаны в соответствующих разделах гл. 45, посвященных защите этих объектов. Параметр psid должен указывать на идентификатор безопасности учетной записи, которая наделяется этими правами. Пример ИСПОЛЬЗОВаНИЯ фуНКЦИИ AddAccessAllowedAce показан В Программе из листинга 44.1. В этой программе создается новый каталог со списком управления доступом DACL. Для добавления в список элемента, контролирующего разрешение доступа к каталогу, используется функция AddAccessAllowedAce. Для добавления в конец списка управления доступом элемента, запрещающего ДОСТУП К Объекту, ИСПОЛЬЗуеТСЯ фуНКЦИЯ AddAccessDeniedAce, КОТОраЯ имеет следующий прототип: BOOL AddAccessDeniedAce( PACL pAcl, // адрес списка управления доступом DWORD dwAceRevision, // версия элемента списка управления доступом DWORD AccessMask, // маска доступа PSID pSid // адрес идентификатора безопасности ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ ПОСредСТВОМ ВЫЗОВа фуНКЦИИ GetLastError. В ЭТОМ случае функция GetLastError возвращает такие же коды, как и после завершения работы фуНКЦИИ AddAccessAllowedAce. Параметры функции AddAccessDeniedAce имеют такое же назначение, как и соответствующие параметры фуНКЦИИ AddAccessAllowedAce. Но при ЭТОМ
Глава 44. Работа со списками управления доступом на низком уровне 947 флаги, установленные в параметре AccessMask, отмечают запрещенные для учетной записи права доступа к охраняемому объекту. В листинге 44.1 приведена программа, демонстрирующая работу функций AddAccessAllowedAce И AddAccessDeniedAce. Для ЭТОГО программа создает каталог, владелец которого устанавливается по умолчанию. Для каталога создается список управления доступом DACL, который включает два элемента: один контролирующий разрешение, а второй — запрещение доступа к каталогу для учетной записи пользователя, имя которого вводится с консоли. Этот пользователь наделяется полными родовыми правами доступа к каталогу, но ему запрещается изменять владельца каталога. #ifndef UNICODE #define UNICODE #endif # include <windows.h> #include <stdio.h> #include <lm.h> int mainO { ACL *lpDacl; DWORD dwDaclLength; // указатель на список доступа // длина списка доступа wchar_t wchUserName [UNLEN] ; // имя пользователя wchar_t wchDirName[248]; // имя каталога DWORD dwLengthOfDomainName = 0; // длина имени домена DWORD dwLengthOfSid =0; // длина SID SID *lpSid = NULL; // указатель на SID LPTSTR lpDomainName = NULL; // указатель на имя домена SID_NAME_USE typeOfSid; // тип учетной записи SECURITY_DESCRIPTOR sd; // дескриптор безопасности каталога SECURITY_ATTRIBUTES sa; // атрибуты защиты каталога
948 Часть XI. Управление безопасностью в Windows DWORD dwRetCode; // код возврата // вводим имя пользователя, которому разрешим доступ к каталогу printf("Input a user name: "); _getws (wchUserName) ; // определяем длину SID пользователя if (!LookupAccountName( NULL, // ищем имя на локальном компьютере wchUserName, // имя пользователя NULL, // определяем длину SID &dwLengthOfSid, // длина SID NULL, // определяем имя домена &dwLengthOfDomainName, // длина имени домена SctypeOfSid) ) // тип учетной записи { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) { // распределяем память для SID и имени домена IpSid = (SID*) new char[dwLengthOfSid]; lpDoma inName = (LPTSTR) new wchar_t [ dwLengthOf Doma inName ] ; } else { // выходим из программы printf("Lookup account name failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } } // определяем SID и имя домена пользователя if(!LookupAccountName( NULL, // ищем имя на локальном компьютере wchUserName, // имя пользователя IpSid, // указатель на SID
Глава 44. Работа со списками управления доступом на низком уровне 949 &dwLengthOfSid, // длина SID lpDomainName, // указатель на имя домена &dwLengthOfDomainName, // длина имени домена StypeOfSid)) // тип учетной записи { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // определем длину списка DACL dwDaclLength = sizeof(ACL) + sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD) + dwLengthOfSid + sizeof(ACCESS_DENIED_ACE) - sizeof(DWORD) + dwLengthOfSid; // распределяем память под DACL lpDacl = (ACL*)new char[dwDaclLength]; // инициализируем список DACL if (!InitializeAcl( lpDacl, // адрес DACL dwDaclLength, //длина DACL ACL_REVISION)) // версия DACL { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // добавляем запрещающий элемент в список DACL if (!AddAccessDeniedAce( lpDacl, // адрес DACL ACL_REVISION, // версия DACL
950 Часть XI. Управление безопасностью в Windows WRITE_OWNER, // запрещаем изменять владельца объекта IpSid)) // адрес SID { dwRetCode = GetLastError(); perror("Add access denied ace failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // добавляем разрешающий элемент в список DACL if (!AddAccessAllowedAce( lpDacl, // адрес DACL ACL_REVISION, // версия DACL GENERIC_ALL, // разрешаем все родовые права доступа IpSid)) // адрес SID { dwRetCode = GetLastError(); perror("Add access allowed ace failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // инициализируем версию дескриптора безопасности if (!InitializeSecurityDescriptor( &sd, SECURITY_DESCRIPTOR_REVISION)) { dwRetCode = GetLastError(); printf("Initialize security descroptor failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // устанавливаем SID владельца объекта if (ISetSecurityDescriptorOwner( &sd, // адрес дескриптора безопасности
Глава 44. Работа со списками управления доступом на низком уровне 951 NULL, //не задаем владельца SE_OWNER_DEFAULTED)) // определить владельца по умолчанию { dwRetCode = GetLastError(); perrorC'Set security descriptor owner failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // устанавливаем SID первичной группы владельца if (!SetSecurityDescriptorGroup( &sd, // адрес дескриптора безопасности NULL, //не задаем первичную группу SE_GROUP_DEFAULTED)) // определить первичную группу по умолчанию { dwRetCode = GetLastError(); perrorC'Set security descriptor group failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // устанавливаем DACL в дескриптор безопасности if (!SetSecurityDescriptorDacl( &sd, // адрес дескриптора безопасности TRUE, // DACL присутствует lpDacl, // указатель на DACL FALSE)) // DACL не задан по умолчанию { dwRetCode = GetLastError(); perrorC'Set security descriptor group failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // проверяем структуру дескриптора безопасности if (!IsValidSecurityDescriptor(&sd)) { dwRetCode = GetLastError();
952 Часть XI. Управление безопасностью в Windows perror("Security descriptor is invalid.\n"); printf ("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // инициализируем атрибуты безопасности sa.nLength = sizeof(sa); // устанавливаем длину атрибутов защиты sa.lpSecurityDescriptor = &sd; // устанавливаем адрес SD sa.blnheritHandle = FALSE; // дескриптор каталога ненаследуемый // читаем имя создаваемого каталога printf("Input a directory name: "); _getws(wchDirName); // вводим имя каталога // создаем каталог if (!CreateDirееtory(wchDirName, &sa)) { dwRetCode = GetLastError(); perror("Security descriptor is invalid.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } printf("The directory is created.\n"); // освобождаем память delete[] IpSid; delete[] lpDomainName; delete[] lpDacl; return 0; } Для добавления в конец списка управления доступом элемента, разрешающего ДОСТУП К объекту, МОЖеТ Также ИСПОЛЬЗОВаТЬСЯ фуНКЦИЯ AddAccessAllowedAceEx, которая имеет следующий прототип: BOOL AddAccessAllowedAceEx ( PACL pAcl, // адрес списка управления доступом
Глава 44. Работа со списками управления доступом на низком уровне 953 DWORD dwAceRevision, // версия элемента списка управления доступом DWORD AceFiags, // флаги наследования элемента DWORD AccessMask, // маска доступа PSID pSid // адрес идентификатора безопасности ); Эта функция отличается от функции AddAccessAiiowedAce только тем, что позволяет также устанавливать флаги, управляющие наследованием включаемого в список управления доступом элемента. Флаги управления наследованием элемента устанавливаются в параметре AceFiags. Символьные константы, используемые для флагов, управляющих наследованием, были подробно рассмотрены в разд. 36.7, 41.3. Функция AddAccessAiiowedAceEx работает точно также же, как и функция AddAccessAiiowedAce, но в случае неудачного завершения она может вернуть дополнительный код ошибки error_invalid_flags — неправильные флаги В параметре AceFiags. Кроме того, заметим, что эта функция работает только в операционных системах Windows, начиная с версии Windows 2000. Пример использования функции AddAccessAiiowedAceEx показан в программе из листинга 44.2. В этой программе создается новый каталог со списком управления доступом DACL, в который включается один элемент, контролирующий разрешение на доступ к каталогу. Для включения этого Элемента В СПИСОК ИСПОЛЬЗуеТСЯ функция AddAccessAiiowedAceEx. Для добавления в конец списка управления доступом элемента, запрещающего доступ к объекту, может также использоваться функция AddAccessDeniedAceEx, которая имеет Следующий ПроТОТИП: BOOL AddAccessDeniedAceEx ( PACL pAcl, // адрес списка управления доступом DWORD dwAceRevision, // версия элемента списка управления доступом DWORD AceFiags, // флаги наследования элемента DWORD AccessMask, // маска доступа PSID pSid // адрес идентификатора безопасности ); Эта функция ОТЛИЧаеТСЯ ОТ функции AddAccessDeniedAce ТОЛЬКО тем, ЧТО позволяет также устанавливать флаги, управляющие наследованием включаемого в список управления доступом элемента. Флаги управления наследованием элемента устанавливаются в параметре AceFiags. Символьные константы, используемые для флагов, управляющих наследованием, были подробно рассмотрены в разд. 36.7, 41.3. Функция AddAccessDeniedAceEx работает точно также же, как и функция AddAccessDeniedAce, но в случае неудачного завершения она может вернуть
954 Часть XI. Управление безопасностью в Windows дополнительный код ошибки error_invalid_flags — неправильные флаги В параметре AceFlags. Кроме того, заметим, что эта функция работает только в операционных системах Windows, начиная с версии Windows 2000. В листинге 44.2 приведена программа, демонстрирующая работу функций AddAccessAiiowedAceEx и AddAccessDeniedAceEx. Функционально эта программа ничем не отличается от программы из листинга 44.1, за исключением того, что для включения в список управления доступом DACL вместо ФУНКЦИЙ AddAccessAllowedAce И AddAccessDeniedAce ИСПОЛЬЗуЮТСЯ фуНК- ЦИИ AddAccessAiiowedAceEx И AddAccessDeniedAceEx соответственно. #define _WIN32_WINNT 0x0500 #ifndef UNICODE #define UNICODE #endif #include <windows.h> #include <stdio.h> #include <lm.h> int main() { ACL *lpDacl; //указатель на список доступа DWORD dwDaclLength; // длина списка доступа wchar_t wchUserName[UNLEN]; // имя пользователя wchar_t wchDirName [248]; // имя каталога DWORD dwLengthOfDomainName = 0; // длина имени домена DWORD dwLengthOfSid =0; // длина SID SID *lpSid = NULL; // указатель на SID LPTSTR lpDomainName = NULL; // указатель на имя домена SID_NAME_USE typeOfSid; // тип учетной записи SECURITY_DESCRIPTOR sd; // дескриптор безопасности каталога
Глава 44. Работа со списками управления доступом на низком уровне 955 SECURITY_ATTRIBUTES sa; // атрибуты зашиты каталога DWORD dwRetCode; // код возврата // вводим имя пользователя, которому разрешим доступ к каталогу printf("Input a user name: "); getws (wchUserName) ; // определяем длину SID пользователя if (!LookupAccountName ( NULL, // ищем имя на локальном компьютере wchUserName, // имя пользователя NULL, // определяем длину SID &dwLengthOfSid, // длина SID NULL, // определяем имя домена &dwLengthOfDomainName, // длина имени домена StypeOfSid)) // тип учетной записи { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) { // распределяем память для SID и имени домена IpSid = (SID*) new chartdwLengthOfSid]; lpDomainName = (LPTSTR) new wchar_t [dwLengthOfDomainName] ; } else { // выходим из программы printf("Lookup account name failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } } // определяем SID и имя домена пользователя if(!LookupAccountName( NULL, // ищем имя на локальном компьютере
956 Часть XI. Управление безопасностью в Windows wchUserName, // имя пользователя IpSid, // указатель на SID SdwLengthOfSid, // длина SID lpDomainName, // указатель на имя домена &dwLengthOfDomainName, // длина имени домена StypeOfSid)) // тип учетной записи dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // определем длину списка DACL dwDaclLength = sizeof(ACL) + sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD) + dwLengthOfSid + sizeof(ACCESS_DENIED_ACE) - sizeof(DWORD) + dwLengthOfSid; // распределяем память под DACL lpDacl = (ACL*)new char[dwDaclLength]; // инициализируем список DACL if (!InitializeAcl( lpDacl, // адрес DACL dwDaclLength, // длина DACL ACL_REVISION)) // версия DACL { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // добавляем запрещающий элемент в список DACL if (!AddAccessDeniedAceEx(
Глава 44. Работа со списками управления доступом на низком уровне 957 lpDacl, // адрес DACL ACL_REVISION, // версия DACL CONTAINER_INHERIT_ACE, // контейнерные объекты наследуют элемент WRITE_OWNER, // запрещаем изменять владельца объекта IpSid)) // адрес SID { dwRetCode = GetLastError(); perror("Add access denied ace failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // добавляем разрешающий элемент в список DACL if ( ! AddAccessAllowedAceEx ( lpDacl, // адрес DACL ACL_REVISION, // версия DACL CONTAINER_INHERIT_ACE, // контейнерные объекты наследуют элемент GENERIC_ALL, // разрешаем все родовые права доступа IpSid)) // адрес SID { dwRetCode = GetLastError(); perror("Add access allowed ace failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // инициализируем версию дескриптора безопасности if (!InitializeSecurityDescriptor( &sd, SECURITY_DESCRIPTOR_REVISION)) { dwRetCode = GetLastError(); printf("Initialize security descroptor failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode;
958 Часть XI. Управление безопасностью в Windows // устанавливаем SID владельца объекта if (!SetSecurityDescriptorOwner( &sd, // адрес дескриптора безопасности NULL, //не задаем владельца SE_OWNER_DEFAULTED)) // определить владельца по умолчанию { dwRetCode = GetLastError(); perrorC'Set security descriptor owner failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // устанавливаем SID первичной группы владельца if (!SetSecurityDescriptorGroup( &sd, // адрес дескриптора безопасности NULL, //не задаем первичную группу SE_GROUP_DEFAULTED)) // определить первичную группу по умолчанию { dwRetCode = GetLastError(); perrorC'Set security descriptor group failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // устанавливаем DACL в дескриптор безопасности if (ISetSecurityDescriptorDacl( &sd, // адрес дескриптора безопасности TRUE, // DACL присутствует lpDacl, // указатель на DACL FALSE)) // DACL не задан по умолчанию { dwRetCode = GetLastError(); perrorC'Set security descriptor group failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // проверяем структуру дескриптора безопасности
Глава 44. Работа со списками управления доступом на низком уровне 959 if (!IsValidSecurityDescriptor(&sd)) { dwRetCode = GetLastError(); perror("Security descriptor is invalid.\n"); printfC'The last error code: %u\n"/ dwRetCode); return dwRetCode; } // инициализируем атрибуты безопасности sa.nLength = sizeof(sa); // устанавливаем длину атрибутов защиты sa.lpSecurityDescriptor = &sd; // устанавливаем адрес SD sa.MnheritHandle = FALSE; // дескриптор каталога ненаследуемый // читаем имя создаваемого каталога printf("Input a directory name: "); _getws(wchDirName); // вводим имя каталога // создаем каталог if (ICreateDirectory (wchDirName, &sa)) { dwRetCode = GetLastError(); perror("Security descriptor is invalid.\n"); printfC'The last error code: %u\n", dwRetCode); return dwRetCode; } printf("The directory is created.\n"); // освобождаем память delete[] IpSid; delete[] lpDoma inName; delete[] lpDacl; return 0;
960 Часть XI. Управление безопасностью в Windows Для добавления в список управления доступом элемента, управляющего аудитом доступа к объекту, используется функция AddAudit Access Асе, которая имеет следующий прототип: BOOL AddAudi tAc ces sAce( PACL DWORD DWORD PSID BOOL BOOL pAcl, dwAceRevision, AccessMask, pSid bAuditSuccess, bAuditFailure // // // // // // адрес списка управления доступом версия элемента списка управления доступом маска доступа адрес идентификатора безопасности признак аудита успешного доступа признак аудита неудачного доступа В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ ПОСреДСТВОМ ВЫЗОВа фуНКЦИИ GetLastError. ВОЗМОЖНЫ следующие коды ошибок, возвращаемые функцией GetLastError, после завершения работы фуНКЦИИ AddAccessAllowedAce: □ error_allotted_space_exceeded — недостаточная длина буфера для ACL; □ error_invalid_acl — неправильный ACL; □ error_invalid_sid — неправильный SID; □ error_revision_mismatch — неправильная версия АСЕ; □ error_success — удачное завершение функции. Параметры функции имеют следующее назначение. Параметр pAcl должен указывать на список управления доступом, в который добавляется элемент, контролирующий разрешение доступа субъекта к объекту. Параметр dwAciRevision должен содержать версию элемента, включаемого в список управления доступом. В настоящее время эта версия задается символьной КОНСТаНТОЙ ACL_REVISION. В параметре AccessMask должны быть установлены права доступа к охраняемому объекту, которые подвергаются аудиту в случае доступа учетной записи к объекту. Идентификатор безопасности учетной записи задается параметром psid. Подробно возможные значения флагов для стандартных и родовых прав доступа описаны в разд. 36.7, 41.3. Специфические права доступа к объектам описаны в соответствующих разделах гл. 45, посвященных защите этих объектов. Параметр psid должен указывать на идентификатор безопасности учетной записи, доступ которой к охраняемому объекту подвергается аудиту. В параметре bAuditsuccess должно быть установлено одно из булевых значений true или false. Если установлено значение true, to элемент контро-
Глава 44. Работа со списками управления доступом на низком уровне 961 лирует успешный доступ учетной записи к объекту, с которым связан список SACL. В противном случае аудит успешного доступа к объекту не проводится. В параметре bAuditFaiiure также должно быть установлено одно из булевых значений true или false. Если установлено значение true, to элемент контролирует неудачный доступ учетной записи к объекту, с которым связан список SACL. В противном случае аудит неудачного доступа к объекту не проводится. В листинге 44.3 приведена программа, в которой создается новый список управления аудитом SACL. Для включения элемента в список управления аудитом используется функция AddAccessAiiowedAce. Фактически эта программа является расширением предыдущей программы. Отличие заключается в том, что для пользователя, которому запрещается устанавливать нового владельца каталога, в список SACL включается элемент, который управляет аудитом неудачных попыток изменения владельца каталога этим пользователем. В связи с этой программой сделаем следующие замечания. Для создания списка SACL требуется, чтобы пользователь имел действующую привилегию se_security_name. Такую привилегию имеют администраторы системы, но по умолчанию она недействительна. Поэтому прежде чем работать со списком SACL эта привилегия должна быть активизирована. Для этого используется функция Adj us tTokenPrivi leges. Более подробно работа с привилегиями рассмотрена в гл. 43. 'V4„! #ifndef UNICODE #define UNICODE #endif #include <windows.h> #include <stdio.h> #include <lm.h> int main() { HANDLE hProcess; // дескриптор процесса HANDLE hTokenHandle; // дескриптор маркера доступа TOKEN_PRIVILEGES tp; // привилегии маркера доступа ACL *lpDacl; // указатель на список доступа DACL 31 Зак. 514
962 Часть XI. Управление безопасностью в Windows DWORD dwDaclLength; // длина списка доступа DACL ACL *lpSacl; // указатель на список доступа SACL DWORD dwSaclLength; // длина списка доступа SACL wchar_t wchUserName[UNLEN]; // имя пользователя wchar_t wchDirName[248]; // имя каталога DWORD dwLengthOfDomainName = 0; // длина имени домена DWORD dwLengthOfSid =0; // длина SID SID *lpSid = NULL; // указатель на SID LPTSTR lpDomainName = NULL; // указатель на имя домена SID_NAME_USE typeOfSid; // тип учетной записи SECURITY_DESCRIPTOR sd; // дескриптор безопасности каталога SECURITY_ATTRIBUTES sa; // атрибуты защиты каталога DWORD dwRetCode; // код возврата // получаем дескриптор процесса hProcess = GetCurrentProcess(); // получаем маркер доступа процесса if (!OpenProcessToken( hProcess, // дескриптор процесса TOKEN_ALL_ACCESS, // полный доступ к маркеру доступа ShTokenHandle)) // дескриптор маркера доступа { dwRetCode = GetLastError(); printf( "Open process token failed: %u\n", dwRetCode); return dwRetCode; } // устанавливаем общее количество привилегий tp.PrivilegeCount = 1; // определяем идентификатор привилегии для установки аудита
Глава 44. Работа со списками управления доступом на низком уровне 963 if (!LookupPrivilegeValue( NULL, // ищем идентификатор привилегии на локальном компьютере SE_SECURITY_NAME, // привилегия для аудита &(tp.Privileges[0].Luid))) { dwRetCode = GetLastError(); printf("Lookup privilege value failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // разрешаем привилегию аудита tp. Privileges [0] .Attributes = SE_PRIVILEGE_ENABLED; // разрешаем привилегию для установки аудита if (!AdjustTokenPrivileges( hTokenHandle, // дескриптор маркера доступа процесса FALSE, //не запрещаем все привилегии &tp, // адрес привилегий 0, // длины буфера нет NULL, // предыдущее состояние привилегий не нужно NULL)) // длина буфера не нужна { dwRetCode = GetLastError(); printf("Lookup privilege value failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // вводим имя пользователя, которому разрешим доступ к каталогу printf("Input a user name: "); getws (wchUserName) ; // определяем длину SID пользователя if (!LookupAccountName( NULL, // ищем имя на локальном компьютере wchUserName, // имя пользователя
964 Часть XI. Управление безопасностью в Windows { NULL, // определяем длину SID &dwLengthOfSid, // длина SID NULL, // определяем имя домена SdwLengthOfDomainName, // длина имени домена &typeOfSid)) // тип учетной записи dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) { // распределяем память для SID и имени домена IpSid = (SID*) new char[dwLengthOfSid]; lpDoma inName = (LPTSTR) new wchar_t [dwLengthO f Doma inName ] ; } else { // выходим из программы printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } } // определяем SID и имя домена пользователя if(!LookupAccountName( NULL, // ищем имя на локальном компьютере wchUserName, // имя пользователя IpSid, // указатель на SID SdwLengthOfSid, // длина SID lpDomainName, // указатель на имя домена SdwLengthOfDomainName, // длина имени домена StypeOfSid)) // тип учетной записи { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode;
Глава 44. Работа со списками управления доступом на низком уровне 965 } // определяем длину списка DACL dwDaclLength = sizeof(ACL) + sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD) + dwLengthOfSid + sizeof(ACCESS_DENIED_ACE) - sizeof(DWORD) + dwLengthOfSid; // распределяем память под DACL lpDacl = (ACL*)new char[dwDaclLength]; // инициализируем список DACL if (ilnitializeAcl( lpDacl, // адрес DACL dwDaclLength, // длина DACL ACL_REVISION)) // версия DACL { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // добавляем запрещающий элемент в список DACL if (!AddAccessDeniedAce( lpDacl, // адрес DACL ACL_REVISION, // версия DACL WRITE_OWNER, // запрещаем изменять владельца объекта IpSid)) // адрес SID { dwRetCode = GetLastError(); perror("Add access denied ace failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // добавляем разрешающий элемент в список DACL
966 Часть XI. Управление безопасностью в Windows if (! AddAccessAllowedAce ( lpDacl, // адрес DACL ACL_REVISION, // версия DACL GENERIC_ALL, // разрешаем все родовые права доступа IpSid)) // адрес SID { dwRetCode = GetLastError(); perrorC'Add access allowed ace failed.\n"); printfC'The last error code: %u\n"/ dwRetCode); return dwRetCode; } // определяем длину списка SACL dwSaclLength = sizeof(ACL) + sizeof(SYSTEM_AUDIT_ACE ) - sizeof(DWORD) + dwLengthOfSid; // распределяем память под SACL lpSacl = (ACL*)new char[dwSaclLength]; // инициализируем список SACL if (!InitializeAcl( lpSacl, // адрес SACL dwSaclLength, // длина SACL ACL_REVISION) ) // версия SACL { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // добавляем элемент в список SACL if (!AddAuditAccessAce( lpSacl, // адрес DACL ACL_REVISION, // версия DACL WRITE_OWNER, // запрещаем изменять владельца объекта
Глава 44. Работа со списками управления доступом на низком уровне 967 IpSid, // адрес SID FALSE, //не нужен аудит удачного доступа TRUE)) // нужен аудит неудачного доступа { dwRetCode = GetLastError(); perror("Add audit access ace failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // инициализируем версию дескриптора безопасности if (!InitializeSecurityDescriptor( &sd, SECURITY_DESCRIPTOR_REVISION)) { dwRetCode = GetLastError(); printf("Initialize security descroptor failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // устанавливаем SID владельца объекта if (!SetSecurityDescriptorOwner( &sd, // адрес дескриптора безопасности NULL, //не задаем владельца SE_OWNER_DEFAULTED)) // определить владельца по умолчанию { dwRetCode = GetLastError(); perror("Set security descriptor owner failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // устанавливаем SID первичной группы владельца if (!SetSecurityDescriptorGroup( &sd, // адрес дескриптора безопасности
968 Часть XI. Управление безопасностью в Windows NULL, //не задаем первичную группу SE_GROUP_DEFAULTED)) // определить первичную группу по умолчанию { dwRetCode = GetLastError(); perrorC'Set security descriptor group failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // устанавливаем DACL в дескриптор безопасности if (ISetSecurityDescriptorDacl( &sd, // адрес дескриптора безопасности TRUE, // DACL присутствует lpDacl, // указатель на DACL FALSE)) // DACL не задан по умолчанию { dwRetCode = GetLastError(); perrorC'Set security descriptor group failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // устанавливаем SACL в дескриптор безопасности if (!SetSecurityDescriptorSacl( &sd, // адрес дескриптора безопасности TRUE, // SACL присутствует lpSacl, // указатель на SACL FALSE)) // SACL не задан по умолчанию { dwRetCode = GetLastError(); perrorC'Set security descriptor group failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // проверяем структуру дескриптора безопасности if (!IsValidSecurityDescriptor(&sd))
Глава 44. Работа со списками управления доступом на низком уровне 969 dwRetCode = GetLastError(); perror("Security descriptor is invalid.\n"); printfC'The last error code: %u\n"/ dwRetCode); return dwRetCode; } // инициализируем атрибуты безопасности sa.nLength = sizeof(sa); // устанавливаем длину атрибутов защиты sa.lpSecurityDescriptor = &sd; // устанавливаем адрес дескриптора // безопасности sa.blnheritHandle = FALSE; // дескриптор каталога не наследуемый // читаем имя создаваемого каталога printf("Input a directory name: "); _getws(wchDirName); // вводим имя каталога // создаем каталог if (!CreateDirectory(wchDirName, &sa)) { dwRetCode = GetLastError(); perror("Security descriptor is invalid.\n"); printfC'The last error code: %u\n"/ dwRetCode); return dwRetCode; } printfC'The directory is created.\n"); // запрещаем привилегию аудита tp.Privileges[0].Attributes = 0; // разрешаем привилегию для установки аудита if (!AdjustTokenPrivi1eges( hTokenHandle, // дескриптор маркера доступа процесса FALSE, //не запрещаем все привилегии &tp, // адрес привилегий О, // длины буфера нет NULL, // предыдущее состояние привилегий не нужно
970 Часть XI. Управление безопасностью в Windows NULL)) // длина буфера не нужна { dwRetCode = GetLastError(); printf("Lookup privilege value failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // освобождаем память delete[] IpSid; de1ete[] lpDomainName; delete[] lpDacl; return 0; } Для добавления в список управления доступом элемента, управляющего аудитом доступа к объекту, может также использоваться функция AddAuditAccessAceEx, КОТОрая Имеет Следующий ПрОТОТИП! BOOL AddAuditAccessAceEx ( PACL DWORD DWORD DWORD PSID BOOL BOOL pAcl, dwAceRevision, AceFlags, AccessMask, pSid bAuditSuccess, bAuditFailure // // // // // // // адрес списка управления доступом версия элемента списка управления доступом флаги наследования и аудита маска доступа адрес идентификатора безопасности признак аудита успешного доступа признак аудита неудачного доступа ); Эта функция отличается от функции AddAudit Access Асе только тем, что позволяет также устанавливать флаги, управляющие наследованием включаемого в список управления доступом элемента. Флаги управления наследованием элемента устанавливаются в параметре AceFlags. Символьные константы, используемые для флагов, управляющих наследованием, были подробно рассмотрены в разд. 36.7, 41.3. В этом параметре также могут быть установлены флаги, управляющие аудитом. Таким образом, флаги аудита успешного или неудачного доступа к объекту могут быть установлены как при помощи параметров bAuditsuccess и bAuditFailure, так и параметра AceFlags.
Глава 44. Работа со списками управления доступом на низком уровне 971 Функция AddAuditAccessAceEx работает точно также же, как и функция AddAuditAccessAce, но в случае неудачного завершения она может вернуть дополнительный код ошибки: □ ERROR_INVALID_FLAGS — Неправильные флаГИ В Параметре AceFlags. Кроме того, отметим, что эта функция работает только в операционных системах Windows, начиная с версии Windows 2000. Для копирования элементов из одного списка управления доступом в другой список управления доступом используется функция AddAce, которая имеет следующий прототип: BOOL AddAce( PACL pAcl, // адрес списка управления доступом DWORD dwAceRevision, // версия списка управления доступом DWORD dwStartingAceIndex, // индекс первого включаемого элемента LPVOID pAceList, // список включаемых элементов DWORD nAceListLength // длина списка включаемых элементов ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции можно получить посредством вызова функции GetLastError. Возможны следующие коды ошибок, возвращаемые функцией GetLastError, после завершения работы фуНКЦИИ AddAce! □ error_allotted_space_exceeded — недостаточная длина буфера для ACL; □ error_invalid_acl — неправильный ACL; □ error_invalid_sid — неправильный SID; □ error_revision_mismatch — неправильная версия АСЕ; □ error_success — удачное завершение функции. Параметр pAcl должен указывать на список управления доступом, в который добавляются элементы, контролирующие доступ субъекта к охраняемому объекту Параметр dwAciRevision должен содержать версию списка управления доступом, в который добавляются элементы. В настоящее время эта версия задается символьной константой acl_revision. Параметр dwstartingAceindex должен содержать индекс, с которого начинают добавляться элементы в список управления доступом. Если в этом параметре установлено значение 0, то элементы включаются в начало списка. Если в этом параметре установлено значение maxdword, to элементы добавляются в конец списка. Параметр pAceList должен указывать на список элементов, которые включаются в список управления доступом. Список элементов должен храниться
972 Часть XI. Управление безопасностью в Windows в последовательной области памяти. Отметим, что параметр pAceList должен указывать не на структуру типа ACL, а на элемент в списке управления доступом. Этот элемент может быть произвольным элементом списка управления доступом. Параметр nAceListLength должен содержать длину списка элементов, на который указывает параметр pAceList. В разд. 45.5 приведена программа, в которой функция AddAce используется для копирования элементов одного списка управления доступом в другой список. 44.6. Получение элементов из списка управления доступом Для получения адреса элемента списка управления доступом используется функция Get Асе, которая имеет следующий прототип: BOOL GetAce ( PACL pAcl, // адрес списка управления доступом DWORD dwAceIndex, // индекс элемента LPVOID *рАсе // указатель на переменную для адреса элемента ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции можно получить посредством вызова функции GetLastError. Параметры функции имеют следующее назначение. Параметр pAcl должен указывать на область памяти, в которой хранится список управления доступом. Параметр dwAceindex должен содержать индекс элемента из списка управления доступом, адрес которого определяет функция. Параметр рАсе должен содержать адрес указателя, в который функция запишет адрес элемента с индексом dwAceindex из списка управления доступом. Напомним, что количество элементов, находящихся в списке управления доступом, хранится в поле Acecount структуры типа ACL, которая была рассмотрена в разд. 44.1. Для определения количества элементов, включенных в список управления доступом, может также использоваться функция GetAciinformation, которая описана в разд. 44.8. В листинге 44.4 приведена программа, которая перечисляет элементы списка управления ДОСТУПОМ DACL, ИСПОЛЬЗУЯ ДЛЯ ЭТОГО фуНКЦИЮ GetAce.
Глава 44. Работа со списками управления доступом на низком уровне 973 #define _WIN32_WINNT 0x0500 #include <windows.h> #include <stdio.h> #include <sddl.h> int main() { char chDirName[248] ; // имя файла PSECURITY_DESCRIPTOR IpSd = NULL; // указатель на SD PACL lpDacl = NULL; // указатель на список управления доступом BOOL bDaclPresent; // признак присутствия списка DACL BOOL bDaclDefaulted; // признак списка DACL по умолчанию void *1рАсе = NULL; LPTSTR StringSid; // указатель на элемент списка // указатель на строку SID DWORD dwLength; DWORD dwRetCode; // длина дескриптора безопасности // код возврата // вводим имя файла printf("Input a directory or file name: "); gets (chDirName) ; // получаем длину дескриптора безопасности if (!GetFileSecurity( chDirName, // имя файла DACL_SECXTRITY_INFORMATION, // получаем DACL IpSd, // адрес дескриптора безопасности 0, // определяем длину буфера &dwLength)) // адрес для требуемой длины { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER)
974 Часть XI. Управление безопасностью в Windows // распределяем память для буфера IpSd = (SECURITY_DESCRIPTOR*) new char[dwLength]; else { // выходим из программы printf("Get file security failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } } // распределяем память для дескриптора безопасности IpSd = (PSECURITY_DESCRIPTOR) new char[dwLength]; // читаем дескриптор безопасности if (!GetFileSecurity( chDirName, // имя файла DACL_SECraiTY_INFORMATION, // получаем DACL IpSd, // адрес дескриптора безопасности dwLength, // длину буфера SdwLength)) // адрес для требуемой длины { dwRetCode = GetLastError(); printf("Get file security failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // получаем список DACL из дескриптора безопасности if (IGetSecurityDescriptorDacl( IpSd, // адрес дескриптора безопасности &bDaclPresentf // признак присутствия списка DACL &lpDacl, // адрес указателя на DACL SbDaclDefaulted)) // признак списка DACL по умолчанию
Глава 44. Работа со списками управления доступом на низком уровне 975 dwRetCode = GetLastError(); printf("Get security descriptor DACL failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // проверяем, есть ли DACL if (!bDaclPresent) { printf("Dacl is not present."); return 0; } // печатаем количество элементов printf("Ace count: %u\n", lpDacl->AceCount); // получаем элементы списка DACL for (unsigned i = 0; i < lpDacl->AceCount; ++i) { if (!GetAce( lpDacl, // адрес DACL i, // индекс элемента &1рАсе)) // указатель на элемент списка { dwRetCode = GetLastError(); printf("Get ace failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // вьшодим на печать тип элемента и SID if (((ACE_HEADER*)lpAce)->AceType == ACCESS_ALLOWED_ACE_TYPE) { printf("ACCESS_ALLOWED_ACE_TYPE\n"); // преобразуем SID в строку
976 Часть XI. Управление безопасностью в Windows if (.'ConvertsidToStringSid( & ( (ACCESS_ALLOWED_ACE*) lpAce) ->SidStart, &StringSid)) { dwRetCode = GetLastError(); printf("Convert sid to string sid failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } printf("%s\n", StringSid); LocalFree(StringSid); } if ( ( (ACE_HEADER*)lpAce)->AceType == ACCESS_DENIED_ACE_TYPE) { printf("ACCESS_DENIED_ACE_TYPE\n"); // преобразуем SID в строку if (!ConvertSidToStringSid( & ( (ACCESS_DENIED_ACE*) lpAce) ->SidStart, &StringSid)) { dwRetCode = GetLastError(); printf("Convert sid to string sid failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } printf("%s\n", StringSid); LocalFree(StringSid); } } // освобождаем память delete[] IpSd; return 0;
Глава 44. Работа со списками управления доступом на низком уровне 977 44.7. Удаление элементов из списка управления доступом Для удаления элемента из списка управления доступом используется функция DeieteAce, которая имеет следующий прототип: BOOL DeieteAce( PACL pAcl, // адрес списка управления доступом DWORD dwAcelndex, // индекс элемента ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции можно получить посредством вызова функции GetLastError. Параметры функции имеют следующее назначение. Параметр pAcl должен указывать на область памяти, в которой хранится список управления доступом. Параметр dwAceindex должен содержать индекс элемента, который удаляется из списка управления доступом. Напомним, что количество элементов, находящихся в списке управления доступом, хранится в поле Acecount структуры типа ACL, которая была рассмотрена в разд. 44.1. Для определения количества элементов, включенных в список управления доступом, может также использоваться функция GetAcimf ormation, которая описана в следующем разделе. В листинге 44.5 приведена программа, которая удаляет элемент из списка управления ДОСТУПОМ DACL, ИСПОЛЬЗУЯ ДЛЯ ЭТОГО фуНКЦИЮ DeieteAce. Листинг 44.5^ Удаление элементов из списка у правления доступом I #include <windows.h> #include <stdio.h> int main() { char chDirName[248] ; // имя файла PSECURITY_DESCRIPTOR IpSd = NULL; // указатель на SD PACL lpDacl = NULL; // указатель на список управления доступом BOOL bDaclPresent; // признак присутствия списка DACL BOOL bDaclDefaulted; // признак списка DACL по умолчанию
978 Часть XI. Управление безопасностью в Windows void *1рАсе = NULL; // указатель на элемент списка DWORD dwLength; // длина дескриптора безопасности DWORD dwRetCode; // код возврата // вводим имя файла printf("Input a directory or file name: "); gets(chDirName); // получаем длину дескриптора безопасности if (!GetFileSecurity( chDirName, // имя файла DACL_SECURITY_INFORMATION, // получаем DACL IpSd, // адрес дескриптора безопасности О, // определяем длину буфера &dwLength)) // адрес для требуемой длины { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) // распределяем память для буфера IpSd = (SECURITY_DESCRIPTOR*) new char[dwLength]; else { // выходим из программы printf("Get file security failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } } // распределяем память для дескриптора безопасности IpSd = (PSECURITY_DESCRIPTOR) new char[dwLength]; // читаем дескриптор безопасности if (!GetFileSecurity( chDirName, // имя файла DACL_SECURITY_INFORMATION, // получаем DACL
Глава 44. Работа со списками управления доступом на низком уровне 979 IpSd, // адрес дескриптора безопасности dwLength, // длина буфера sdwLength)) // адрес для требуемой длины { dwRetCode = GetLastError(); printf("Get file security failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // получаем список DACL из дескриптора безопасности if (IGetSecurityDescriptorDacl( IpSd, // адрес дескриптора безопасности SbDaclPresent, // признак присутствия списка DACL SlpDacl, // адрес указателя на DACL ScbDaclDefaulted)) // признак списка DACL по умолчанию { dwRetCode = GetLastError(); printf("Get security descriptor DACL failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // проверяем, есть ли DACL if (IbDaclPresent) { printf("Dacl is not present."); return 0; } // удаляем запрещающие доступ элементы списка DACL for (unsigned i = 0; i < lpDacl->AceCount; ++i) { // получить элемент списка DACL if (!GetAce( lpDacl, // адрес DACL i, // индекс элемента
980 Часть XI. Управление безопасностью в Windows SlpAce)) // указатель на элемент списка { dwRetCode = GetLastError(); printf("Get асе failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // проверяем тип элемента if (( (ACE_HEADER*)lpAce)->AceType == ACCESS_DENIED_ACE_TYPE) // удаляем элемент из списка DACL if (!DeleteAce( lpDacl, i)) { dwRetCode = GetLastError(); printf("Delete ace failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } } // устанавливаем новый дескриптор безопасности if (!SetFileSecurity( chDirName, // имя файла DACL_SEOmiTY_INF0RMATI0N, // устанавливаем DACL IpSd)) // адрес дескриптора безопасности { dwRetCode = GetLastError(); printf("Set file security failed.Xn"); print f("Error code: %d\n", dwRetCode); return dwRetCode; } printf("Denied elements are deleted from the DACL.Xn"); // освобождаем память delete[] IpSd; return 0;
Глава 44. Работа со списками управления доступом на низком уровне 981 44.8. Получение информации о списке управления доступом Для получения информации о версии или размерах списка управления доступом используется функция GetAclinformation, которая имеет следующий прототип: BOOL GetAclinformation( PACL pAcl, // адрес списка управления доступом LPVOID pAclInformation, // адрес буфера для информации DWORD nAclInformationLength, // длина буфера ACL_INFORMATION_CLASS dwAclInformationClass // тип информации ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции можно получить посредством вызова функции GetLastError. Параметры функции имеют следующее назначение. Параметр pAcl должен указывать на область памяти, в которой хранится список управления доступом. Параметр pAciinf ormation должен указывать на область памяти, в которую функция запишет затребованную информацию. Параметр nAcimformationLength должен содержать длину области памяти, на которую указывает параметр pAclInformation. В параметре dwAcimf ormationciass должна быть установлена одна из перечислимых констант, принадлежащая перечислению типа: typedef enum _ACL_INFORMATION_CLASS { AclRevisionlnformation = 1, II версия ACL AclSizelnformation // длина ACL } ACL_INFORMATION_CLASS; Если в параметре dwAciinformationciass установлено значение AclRevisionlnformation, то функция запишет в область памяти, на которую указывает параметр pAclInformation, информацию о версии списка управления доступом в формате структуры типа: typedef struct _ACL_REVISION_INFORMATION { DWORD AclRevision; // версия ACL } ACL_REVI S ION_INFORMAT ION ; тип указателя на которую определяется также как typedef ACL_REVISION_INFORMATION *PACL_REVISION_INFORMATION;
982 Часть XI. Управление безопасностью в Windows Если же в параметре dwAciinformationciass установлено значение Acisizeinformation, то функция запишет в область памяти, на которую указывает параметр pAcimformation, информацию о размерах списка управления доступом в формате структуры типа: typedef struct _ACL_SIZE_INFORMATION { DWORD AceCount; // количество элементов в списке DWORD AclBytesInUse; // количество используемых байтов DWORD AclBytesFree; // количество свободных байтов } ACL_SIZE_INFORMATION; Тип указателя на которую определяется как: typedef ACL_SIZE_INFORMATION *PACL_SIZE_INFORMATION; В листинге 44.6 приведена программа, которая получает информацию из списка управления доступом DACL, используя для этого функцию GetAclInformation. ; Листинг 44,6. Получение информации из списка управления доступом DACL #include <windows.h> #include <stdio.h> int main() { char chDirName[248]; // имя файла PSECURITY_DESCRIPTOR IpSd = NULL; // указатель на SD PACL lpDacl = NULL; // указатель на список управления доступом BOOL bDaclPresent; // признак присутствия списка DACL BOOL bDaclDefaulted; // признак списка DACL по умолчанию ACL_REVISION_INFORMATION ari; // версия списка DACL ACL_SIZE_INFORMATION asi; // размер списка DACL DWORD dwLength; // длина дескриптора безопасности DWORD dwRetCode; // код возврата // вводим имя файла printf("Input a directory or file name: ");
Глава 44. Работа со списками управления доступом на низком уровне 983 gets (chDirName) ; // получаем длину дескриптора безопасности if (!GetFileSecurity( chDirName, // имя файла DACL_SEOTRITY_INFORMATION, // получаем DACL IpSd, // адрес дескриптора безопасности О, // определяем длину буфера &dwLength)) // адрес для требуемой длины { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) // распределяем память для буфера IpSd = (SECURITY_DESCRIPTOR*) new char[dwLength]/ else { // выходим из программы printf("Get file security failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } } // распределяем память для дескриптора безопасности IpSd = (PSECURITY_DESCRIPTOR) new char[dwLength]; // читаем дескриптор безопасности if (!GetFileSecurity( chDirName, // имя файла DACL.SECURITY.INFORMATION, // получаем DACL IpSd, // адрес дескриптора безопасности dwLength, // длину буфера SdwLength)) // адрес для требуемой длины { dwRetCode = GetLastError(); printf("Get file security failed.\n"); printf("Error code: %d\n"/ dwRetCode);
964 Часть XI. Управление безопасностью в Windows return dwRetCode; } // получаем список DACL из дескриптора безопасности if (IGetSecurityDescriptorDacl( IpSd, // адрес дескриптора безопасности SbDaclPresent, // признак присутствия списка DACL SclpDacl, // адрес указателя на DACL SbDaclDefaulted)) // признак списка DACL по умолчанию { dwRetCode = GetLastError(); printf("Get security descriptor DACL failed.\n")/ printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // проверяем, есть ли DACL if (ibDaclPresent) { printf("Dacl is not present."); return 0; } // получаем версию списка DACL GetAclInformation( lpDacl, // адрес DACL &ari, // адрес буфера sizeof(ari), // размер буфера AclRevisionlnformation); // информация о версии // печатаем версию списка DACL printf("Acl Revision Information: %u\n"/ ari.AclRevision)t // получаем размеры списка DACL GetAclInformation( lpDacl, // адрес DACL &asi, // адрес буфера
Глава 44. Работа со списками управления доступом на низком уровне 985 sizeof(asi)/ // размер буфера AclSizelnformation); // информация о размерах DACL // печатаем информацию о размерах списка DACL printf("Ace Count: %u\n"/ asi.AceCount); printf("AclBytesInUse: %u\n", asi.AclBytesInUse); printf("Acl Bytes Free: %u\n"/ asi.AclBytesFree); // освобождаем память delete[] IpSd; return 0; } 44.9. Установка версии списка управления доступом Для установки версии списка управления доступом используется функция setAclinformation, которая имеет следующий прототип: BOOL SetAclinformation( PACL pAcl, // адрес списка управления доступом LPVOID pAclInformation, // адрес буфера с информацией DWORD nAclInformationLength, // длина буфера ACL_INFORMATION_CLASS dwAclinformationciass // тип информации ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ Посредством ВЫЗОВа фуНКЦИИ GetLastError. Параметр pAcl должен указывать на область памяти, в которой хранится список управления доступом. Параметр pAcl information должен указывать на область памяти, в которой хранится версия списка управления доступом. Параметр nAciinformationLength должен содержать длину области памяти, На Которую указывает параметр pAclInformation. В параметре dwAclinformationciass должно быть установлено значение AciRevisioninformation, принадлежащее перечислению типа acl_information_class. Это перечисление было рассмотрено в разд. 44.8.
986 Часть XI. Управление безопасностью в Windows 44.10. Определение доступной памяти Для определения адреса первого свободного байта в области памяти, хранящей СПИСОК управления ДОСТУПОМ, предназначена фуНКЦИЯ FindFirstFreeAce, которая имеет следующий прототип: BOOL FindFirstFreeAce( PACL pAcl, // адрес списка управления доступом LPV0ID *рАсе // указатель на переменную для адреса первого // свободного байта ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции можно получить посредством вызова функции GetLastError. Параметры функции имеют следующее назначение. Параметр pAcl должен указывать на область памяти, в которой хранится список управления доступом. Параметр рАсе должен указывать на переменную, в которую функция запишет адрес первого свободного байта в области памяти, на которую указывает параметр pAcl. Если структура этой памяти не соответствует структуре списка управления доступом, то функция запишет в переменную значение null. Если память заполнена полностью, то функция запишет в переменную адрес байта, следующего за списком управления доступом.
Глава 45 Управление безопасностью объектов на низком уровне В этой главе будут рассмотрены функции, используемые для управления безопасностью объектов конкретного типа. Прежде чем переходить к самим функциям скажем, что для этих же целей могут использоваться функции GetSecuritylnfo, GetNamedSecuritylnfо, SetNamedSecuritylnfо И setNamedSecurityinfo, которые позволяют работать с атрибутами безопасности объектов на более высоком уровне. Эти функции, а также другие функции, предназначенные для извлечения и установки информации в дескриптор безопасности объекта, были рассмотрены в гл. 40. Кроме того, отметим, что при работе с функциями, которые получают доступ к дескриптору безопасности объекта, нужно различать абсолютный и относительный форматы дескрипторов безопасности. То есть функции чтения атрибутов безопасности объекта получают дескриптор безопасности этого объекта в относительном формате. Нельзя изменять атрибуты безопасности в дескрипторе, который получен функцией чтения атрибутов безопасности объекта, т. к. это нарушит относительный формат дескриптора безопасности. Поэтому для изменения атрибутов безопасности объекта нужно создать новый дескриптор безопасности в абсолютном формате и установить в нем новые атрибуты безопасности объекта. После этого можно изменять атрибуты безопасности объекта посредством вызова функции установки новых атрибутов безопасности. Подробнее о форматах дескриптора безопасности было рассказано в разд. 40.1. В заключение нашего введения также скажем, что функции, которые используются для доступа к информации из дескриптора безопасности, рассматривались также в гл. 40, 44. Там же приведены другие примеры использования этих функций.
988 Часть XI. Управление безопасностью в Windows 45.1. Доступ к информации о владельце объекта Для установки в дескриптор безопасности объекта сведений о владельце объекта ИСПОЛЬЗуетСЯ функция SetSecurityDescriptorOwner, которая имеет следующий прототип: BOOL SetSecurityDescriptorOwner( PSECURITY_DESCRIPTOR pSecurityDescriptor, // SD объекта PSID pOwner, // SID нового владельца объекта BOOL bOwnerDefaulted // признак владельца объекта по умолчанию ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ Посредством ВЫЗОВа фунКЦИИ GetLastError. Параметр pSecurityDescriptor должен указывать на дескриптор безопасности, в который функция устанавливает нового владельца объекта. Параметр pOwner должен указывать на идентификатор безопасности учетной записи, которая становится владельцем объекта. Если в этом параметре установлено значение null, to функция удаляет старого владельца объекта, а нового не устанавливает. То есть в этом случае объект не имеет владельца. В параметре bOwnerDefaulted должно быть установлено одно из значений false или true. Если установлено значение false, to функция сбрасывает в дескрипторе безопасности флаг se_owner_defaulted. Если же этот параметр содержит значение true, to владелец объекта определяется системой, при этом в дескрипторе безопасности устанавливается флаг se_owner_defaulted. Для получения из дескриптора безопасности информации о владельце объекта ИСПОЛЬЗуетСЯ ФУНКЦИЯ GetSecurityDescriptorOwner, которая имеет следующий прототип: BOOL GetSecurityDescriptorOwner( PSECURITY_DESCRIPTOR pSecurityDescriptor, // на SD объекта PSID *pOwner, // SID владельца объекта LPBOOL lpbOwnerDefaulted // признак владельца объекта по умолчанию ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ посредством ВЫЗОВа фунКЦИИ GetLastError. Параметр pSecurityDescriptor должен указывать на дескриптор безопасности, из которого функция получает информацию о владельце объекта.
Глава 45. Управление безопасностью объектов на низком уровне 989 Параметр pOwner должен содержать адрес указателя, в который функция запишет адрес идентификатора безопасности владельца объекта. Если объект не имеет владельца, то функция запишет по этому адресу значение null. Параметр lpbOwnerDefauited должен указывать на булеву переменную, в которую функция запишет значение флага se_owner_defaulted. В листинге 45.1 приведена программа, которая изменяет владельца файла, используя для доступа к информации о владельце файла функции GetSecurityDescriptorOwner И SetSecurityDescriptorOwner. #ifndef UNICODE #define UNICODE #endif # inc lude <windows. h> #include <stdio.h> #include <lm.h> int main () { wchar_t wchDirName [248]; wchar_t wchUserName [UNLEN] ; // имя каталога // имя пользователя DWORD dwSdLength =0; // длина SD DWORD dwSidLength =0; // длина SID DWORD dwLengthOfDomainName = 0; // длина имени домена PSID IpSid = NULL; LPTSTR lpDomainName = NULL; // указатель на SID нового владельца // указатель на имя домена SID_NAME_USE typeOfSid; // тип учетной записи SECURITY_DESCRIPTOR sdAbsoluteSd; // абсолютный формат дескриптора // безопасности DWORD dwRetCode; // код возврата // читаем имя файла или каталога
990 Часть XI. Управление безопасностью в Windows printf("Input a file or directory name: "); qetws (wchDirName) ; // вводим имя пользователя, который будет новым владельцем файла printf("Input a user name: "); qetws (wchUserName) ; // определяем длину SID пользователя if (!LookupAccountName( NULL, // ищем имя на локальном компьютере wchUserName, // имя пользователя NULL, // определяем длину SID SdwSidLength, // длина SID NULL, // определяем имя домена &dwLengthOfDomainName, // длина имени домена StypeOfSid)) // тип учетной записи { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT__BUFFER) { // распределяем память для SID IpSid = (SID*) new char[dwSidLength]; lpDomainName = (LPTSTR) new wchar_t [dwLengthOfDomainName] ; } else { // выходим из программы printf("Lookup account name length failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } } // определяем SID if(!LookupAccountName( NULL, // ищем имя на локальном компьютере wchUserName, // имя пользователя
Глава 45. Управление безопасностью объектов на низком уровне 991 IpSid, // указатель на SID &dwSidLength, // длина SID lpDomainName, // указатель на имя домена &dwLengthOfDomainName, // длина имени домена &typeOfSid)) // тип учетной записи { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // создаем дескриптор безопасности if (!InitializeSecurityDescriptor( fcsdAbsoluteSd, // адрес структуры SD SECURITY_DESCRIPTOR_REVISION)) { dwRetCode = GetLastError(); perror("Initialize security descriptor failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // устанавливаем нового владельца в дескриптор безопасности if (!SetSecurityDescriptorOwner( SsdAbsoluteSd, // адрес дескриптора безопасности IpSid, // указатель на SID FALSE)) // SID не задан по умолчанию { dwRetCode = GetLastError(); perror("Set security descriptor owner failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // проверяем структуру дескриптора безопасности
992 Часть XI. Управление безопасностью в Windows if (!IsValidSecurityDescriptor(fcsdAbsoluteSd)) { dwRetCode = GetLastError(); perror("Security descriptor is invalid.\n"); printfC'The last error code: %u\n", dwRetCode); return dwRetCode; } // устанавливаем новый дескриптор безопасности if (!SetFileSecurity( wchDirName, // имя файла OWNER_SECURITY_INFORMATION, // устанавливаем SID &sdAbsoluteSd)) // адрес дескриптора безопасности { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INVALID_OWNER) printfC'The user can not be the owner of the file."); printf("Set file security failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // освобождаем память delete[] IpSid; de1ete[] lpDomainName; return 0; } 45.2. Доступ к информации о первичной группе владельца объекта Для установки в дескриптор безопасности объекта сведений о первичной группе владельца Объекта ИСПОЛЬЗуетсЯ фунКЦИЯ SetsecurityDescriptorGroup, KOTO- рая имеет следующий прототип: BOOL SetsecurityDescriptorGroup( PSECURITY_DESCRIPTOR pSecurityDescriptor, // на SD объекта
Глава 45. Управление безопасностью объектов на низком уровне 993 PSID pGroup, // указатель на SID первичной группы BOOL bGroupDefaulted // признак группы по умолчанию ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ ПОСреДСТВОМ ВЫЗОВа фуНКЦИИ GetLastError. Параметр pSecurityDescriptor должен указывать на дескриптор безопасности, в который функция устанавливает новую первичную группу владельца объекта. Параметр pGroup должен указывать на идентификатор безопасности учетной записи группы, которая становится первичной группой владельца объекта. Если в этом параметре установлено значение null, to функция удаляет старую первичную группу, а новую первичную группу не устанавливает. То есть в этом случае объект не имеет первичной группы владельца объекта. В параметре bOwnerDefaulted должно быть установлено одно из значений false или true. Если установлено значение false, to функция сбрасывает в дескрипторе безопасности флаг se_group_defaulted. Если же этот параметр содержит значение true, to первичная группа владельца объекта определяется системой, при этом в дескрипторе безопасности устанавливается флаг SE_GROUP_DEFAULTED. Для получения из дескриптора безопасности информации о первичной группе владельца Объекта ИСПОЛЬЗуетсЯ фунКЦИЯ GetSecurityDescriptorGroup, которая имеет следующий прототип: BOOL GetSecurityDescriptorGroup( PSECURITY_DESCRIPTOR pSecurityDescriptor, // SD объекта PSID * pGroup, // адрес указателя на SID первичной группы LPBOOL lpbGroupDefaulted // признак первичной группы по умолчанию ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции можно получить посредством вызова функции GetLastError. Параметры функции имеют следующее назначение. Параметр pSecurityDescriptor должен указывать на дескриптор безопасно- сти, из которого функция получает информацию о первичной группе владельца объекта. Параметр powner должен содержать адрес указателя, в который функция запишет адрес идентификатора безопасности первичной группы владельца объекта. Если объект не имеет первичной группы, то функция запишет по этому адресу значение null. 32 Зак. 514
994 Часть XI. Управление безопасностью в Windows Параметр ipbownerDefauited должен указывать на булеву переменную, в которую функция запишет значение флага se_group_defaulted. В листинге 45.2 приведена программа, которая изменяет первичную группу владельца файла, ИСПОЛЬЗУЯ ДЛЯ ЭТОГО фуНКЦИИ GetSecurityDescriptorOwner И SetSecurityDescriptorOwner. #define _WIN32_WINNT 0x0500 #include <windows.h> #include <stdio.h> #include <sddl.h> int main() { char chDirName[248] ; // имя файла PSECURITY_DESCRIPTOR IpSd = NULL; // указатель на SD PSID lpGroup; LPTSTR Stringsid; BOOL bGroupDefaulted; // указатель на SID первичной группы // указатель на строку SID // признак группы по умолчанию DWORD dwLength; // длина дескриптора безопасности DWORD dwRetCode; // код возврата // вводим имя файла printf("Input a directory or file name: "); gets (chDirName) ; // получаем длину дескриптора безопасности if (!GetFileSecurity( chDirName, // имя файла GROUP_SECURITY_INFORMATION, // получаем SID первичной группы IpSd, // адрес дескриптора безопасности 0, // определяем длину буфера &dwLength)) // адрес для требуемой длины
Глава 45. Управление безопасностью объектов на низком уровне 995 { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) // распределяем память для буфера IpSd = (SECURITY_DESCRIPTOR*) new char[dwLength]; else { // выходим из программы printf("Get file security failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } } // распределяем память для дескриптора безопасности IpSd = (PSECURITY_DESCRIPTOR) new char[dwLength]; // читаем дескриптор безопасности if (!GetFileSecurity( chDirName, // имя файла GROUP_SECURITY_INFORMATION, // получаем SID первичной группы IpSd, // адрес дескриптора безопасности dwLength, // длина буфера sdwLength)) // адрес для требуемой длины { dwRetCode = GetLastError(); printf("Get file security failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // получаем SID первичной группы if (!GetSecurityDescriptorGroup( IpSd, // адрес дескриптора безопасности &lpGroup, // адрес указателя на SID первичной группы
996 Часть XI. Управление безопасностью в Windows sbGroupDefaulted)) // признак первичной группы по умолчанию { dwRetCode = GetLastError(); printf("Get security descriptor group failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // проверяем, есть ли первичная группа if (lpGroup == NULL) { printf("The primary group is not present.\n"); return 0; } // преобразуем SID в строку if (!ConvertSidToStringSid( lpGroup, // указатель на SID первичной группы &StringSid)) // строка с SID { dwRetCode = GetLastError(); printf("Convert sid to string sid failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } printf("%s\n",StringSid); // освобождаем память LocalFree(StringSid); delete[] IpSd; return 0;
Глава 45. Управление безопасностью объектов на низком уровне 997 45.3. Доступ к списку DACL Для установки в дескриптор безопасности объекта списка управления доступом DACL ИСПОЛЬЗуеТСЯ фуНКЦИЯ SetSecurityDescriptorDacl, КОТОрая имеет следующий прототип: BOOL SetSecurityDescriptorDacl( PSECURITY_DESCRIPTOR pSecurityDescriptor, // SD объекта BOOL bDaclPresent, // признак присутствия DACL PACL pDaci, // указатель на список DACL BOOL bDaciDefauited // признак DACL, заданного по умолчанию ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ ПОСреДСТВОМ ВЫЗОВа фуНКЦИИ GetLastError. Параметр pSecurityDescriptor должен указывать на дескриптор безопасности, в который функция устанавливает новый список управления доступом DACL. В параметре bDaclPresent должно быть установлено одно из значений false или true. Если установлено значение false, to функция сбрасывает флаг безопасности se_dacl_present в дескрипторе, и, в этом случае, параметры pDaci и bDaciDefauited игнорируются. Если же в этом параметре задано значение true, to в дескрипторе безопасности устанавливается флаг SE_DACL_PRESENT. Параметр pDaci должен указывать на список управления доступом DACL, который устанавливается в дескриптор безопасности объекта. Если в этом параметре установлено значение null, to функция устанавливает значение указателя на список управления доступом DACL в дескрипторе безопасности также в null. Это означает отсутствие списка DACL у объекта, что разрешает полный доступ к объекту всем учетным записям. В параметре bDaciDefauited должно быть установлено одно из значений false или true. Если установлено значение false, to функция сбрасывает флаг безопасности se_dacl_defaulted в дескрипторе. Если же этот параметр содержит значение true, to список управления доступом объекта определяется системой, при этом в дескрипторе безопасности устанавливается флаг SE_DACL_DEFAULTED. Для получения из дескриптора безопасности информации о списке управления ДОСТУПОМ DACL К Объекту ИСПОЛЬЗуеТСЯ фуНКЦИЯ GetSecurityDescriptorDacl, которая имеет следующий прототип: BOOL GetSecurityDescriptorDacl( PSECURITY_DESCRIPTOR pSecurityDescriptor, // SD объекта
998 Часть XI. Управление безопасностью в Windows LPBOOL IpbDaciPresent, // указатель на признак присутствия DACL PACL *pDacl, // адрес указателя на DACL LPBOOL lpbDaclDefaulted // признак DACL, заданного по умолчанию ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ С ПОМОЩЬЮ фуНКЦИИ GetLastError. Параметр pSecurityDescriptor должен указывать на дескриптор безопасности, из которого функция получает информацию о списке управления доступом DACL. Параметр IpbDaciPresent должен содержать адрес переменной типа bool. Если дескриптор безопасности объекта содержит список управления доступом DACL, то в эту переменную функция запишет значение true. В противном случае в эту переменную будет записано значение false. Параметр pDaci должен содержать адрес указателя, в который функция запишет адрес списка DACL. Если объект не имеет списка DACL, то значение указателя не изменится. Параметр lpbDaciDefaulted должен указывать на переменную типа bool, в которую функция запишет значение флага se_dacl_defaulted. Примеры Использования функций SetSecurityDescriptorDacl И GetsecurityDescriptorDaci для доступа к списку управления DACL можно посмотреть в программах, приведенных в предыдущей главе. Теперь же приведем в листинге 45.3 программу, которая создает мьютекс с атрибутами безопасности. #ifndef UNICODE #define UNICODE #endif # include <windows.h> #include <stdio.h> #include <lm.h> int main () { HANDLE hMutex; // дескриптор мьютекса ACL *lpDacl; // указатель на список доступа
Глава 45. Управление безопасностью объектов на низком уровне 999 DWORD dwDaс1Length; // длина списка доступа wchar_t wchUserName[UNLEN]; // имя пользователя DWORD dwLengthOfDomainName =0; // длина имени домена DWORD dwLengthOfSid =0; // длина SID SID *lpSid = NULL; // указатель на SID LPTSTR lpDomainName = NULL; // указатель на имя домена SID_NAME_USE typeOfSid; // тип учетной записи SECURITY_DESCRIPTOR sd; // дескриптор безопасности мьютекса SECURITY_ATTRIBUTES sa; // атрибуты защиты мьютекса DWORD dwRetCode; // код возврата // вводим имя пользователя, которому разрешим доступ к мьютексу printf("Input a user name: ") ; _getws (wchUserName) ; // определяем длину SID пользователя if (!LookupAccountName( NULL, // ищем имя на локальном компьютере wchUserName, // имя пользователя NULL, // определяем длину SID ScdwLengthOfSid, // длина SID NULL, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &typeOfSid)) // тип учетной записи { dwRetCode = GetLastErrorO; if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) { // распределяем память для SID и имени домена IpSid = (SID*) new char[dwLengthOfSid]; lpDomainName = (LPTSTR) new wchar_t[dwLengthOfDomainName] ;
1000 Часть XI. Управление безопасностью в Windows else { // выходим из программы printf("Lookup account name failed.\п"), printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } } // определяем SID и имя домена пользователя if(!LookupAccountName( NULL, // ищем имя на локальном компьютере wchUserName, // имя пользователя IpSid, // указатель на SID &dwLengthOfSid, // длина SID lpDomainName, // указатель на имя домена &dwLengthOfDomainName, // длина имени домена StypeOfSid)) // тип учетной записи { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // проверяем тип SID if (typeOfSid != SidTypeUser) { printf("This is not a user name.\n"); return 1; } // определем длину списка DACL dwDaclLength = sizeof(ACL) + sizeof(ACCESS_ALLOWED_ACE) - sizeof(DWORD) + dwLengthOfSid + sizeof (ACCESS_DENIED_ACE) - sizeof (DWORD) + dwLengthOfSid;
Глава 45. Управление безопасностью объектов на низком уровне 1001 // распределяем память под DACL lpDacl = (ACL*)new char[dwDaclLength]; // инициализируем список DACL if (!InitializeAcl( lpDacl, // адрес DACL dwDaclLength, // длина DACL ACL_REVISION)) // версия DACL { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // добавляем запрещающий элемент в список DACL if (!AddAccessDeniedAce( lpDacl, // адрес DACL ACL_REVISION, // версия DACL WRITE_OWNER, // запрещаем изменять владельца объекта IpSid)) // адрес SID { dwRetCode = GetLastError(); perror("Add access denied ace failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // добавляем разрешающий элемент в список DACL if (!AddAccessAllowedAce( lpDacl, // адрес DACL ACL_REVISION, // версия DACL GENERIC_ALL, // разрешаем все родовые права доступа IpSid)) // адрес SID { dwRetCode = GetLastError();
1002 Часть XI. Управление безопасностью в Windows perror("Add access allowed ace failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // инициализируем версию дескриптора безопасности if (!InitializeSecurityDescriptor( &sd, SECURITY_DESCRIPTOR_REVISION)) { dwRetCode = GetLastError(); printf("Initialize security descroptor failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // устанавливаем SID владельца объекта if (!SetSecurityDescriptorOwner( &sd, // адрес дескриптора безопасности NULL, //не задаем владельца SE_OWNER_DEFAULTED)) // определить владельца по умолчанию { dwRetCode = GetLastError(); perrorC'Set security descriptor owner failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // устанавливаем SID первичной группы владельца if (!SetSecurityDescriptorGroup( &sd, // адрес дескриптора безопасности NULL, //не задаем первичную группу SE_GROUP_DEFAULTED)) // определить первичную группу по умолчанию { dwRetCode = GetLastError(); perrorC'Set security descriptor group failed.\n");
Глава 45. Управление безопасностью объектов на низком уровне 1003 printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // устанавливаем DACL в дескриптор безопасности if (!SetSecurityDescriptorDacl( &sd, // адрес дескриптора безопасности TRUE, // DACL присутствует lpDacl, // указатель на DACL FALSE)) // DACL не задан по умолчанию { dwRetCode = GetLastError(); perrorC'Set security descriptor group failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // проверяем структуру дескриптора безопасности if (!IsValidSecurityDescriptor(&sd)) { dwRetCode = GetLastError(); perror("Security descriptor is invalid. \n") ; printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // инициализируем атрибуты безопасности sa.nLength = sizeof(sa); // устанавливаем длину атрибутов защиты sa.lpSecurityDescriptor = &sd; // устанавливаем адрес SD sa.blnheritHandle = FALSE; // дескриптор мьютекса ненаследуемый // создаем мьютекс hMutex = CreateMutex(&sa, FALSE, L"DemoMutex") ; if (hMutex == NULL) { dwRetCode = GetLastError();
1004 Часть XI. Управление безопасностью в Windows perror("Create mutex failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } printf("The mutex is created.\n"); // освобождаем память delete[] IpSid; de 1 e te [ ] lpDoma inName ; delete[] lpDacl; CloseHandle(hMutex); return 0; } 45.4. Доступ к списку SACL Для установки в дескриптор безопасности объекта списка управления доступом SACL ИСПОЛЬЗуеТСЯ фуНКЦИЯ SetSecurityDescriptorSacl, которая имеет следующий прототип: BOOL SetSecurityDescriptorSacl( PSECURITY_DESCRIPTOR pSecurityDescriptor, // SD объекта BOOL bSaciPresent, // признак присутствия SACL PACL pSacl, // указатель на список SACL BOOL bSaciDefauited // признак SACL, заданного по умолчанию ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ ПОСреДСТВОМ ВЫЗОВа фуНКЦИИ GetLastError. Параметр pSecurityDescriptor должен указывать на дескриптор безопасности, в который функция устанавливает новый список управления доступом SACL. В параметре bSaciPresent должно быть установлено одно из значений false или true. Если установлено значение false, to функция сбрасывает в дескрипторе безопасности флаг se_sacl_present и, в этом случае, параметры psaci и bSaciDefauited игнорируются. Если же в этом параметре задано значение true, то в дескрипторе безопасности устанавливается флаг se_sacl_present.
Глава 45. Управление безопасностью объектов на низком уровне 1005 Параметр psaci должен указывать на список управления доступом SACL, который устанавливается в дескриптор безопасности объекта. Если в этом параметре установлено значение null, to функция устанавливает значение указателя на список управления доступом SACL в дескрипторе безопасности также в null. Это означает отсутствие списка SACL у объекта. В этом случае аудит доступа субъектов к объекту проводиться не будет. В параметре bSaciDefauited должно быть установлено одно из значений false или true. Если установлено значение false, to функция сбрасывает в дескрипторе безопасности флаг se_sacl_defaulted. Если же этот параметр содержит значение true, to список управления доступом объекта определяется системой, при этом в дескрипторе безопасности устанавливается флаг SE_SACL_DEFAULTED. Для получения из дескриптора безопасности информации о списке управления ДОСТУПОМ SACL К объекту ИСПОЛЬЗуетСЯ фуНКЦИЯ GetSecurityDescriptorSacl, которая имеет следующий прототип: BOOL GetSecurityDescriptorDacl( PSECURITY_DESCRIPTOR pSecurityDescriptor, // SD объекта LPBOOL lpbSaclPresent, // признак присутствия SACL PACL *pSacl, // адрес указателя на SACL LPBOOL lpbSaclDefaulted // признак SACL, заданного по умолчанию ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ Посредством ВЫЗОВа фуНКЦИИ GetLastError. Параметр pSecurityDescriptor должен указывать на дескриптор безопасности, из которого функция получает информацию о списке управления доступом SACL. Параметр lpbSaciPresent должен содержать адрес переменной типа bool. Если дескриптор безопасности объекта содержит список управления доступом SACL, то в эту переменную функция запишет значение true. В противном случае в эту переменную будет записано значение false. Параметр psaci должен содержать адрес указателя, в который функция запишет адрес списка SACL. Если объект не имеет списка SACL, то значение указателя не изменится. Параметр lpbSaclDefaulted должен указывать на переменную типа bool, в которую функция запишет значение флага se_sacl_defaulted. Примеры Использования фуНКЦИЙ SetSecurityDescriptorSacl И GetSecurityDescriptorSacl ДЛЯ ДОСТупа К СПИСКУ управления DACL МОЖНО посмотреть в программах, приведенных в гл. 44.
1006 Часть XI. Управление безопасностью в Windows 45.5. Защита файлов и каталогов Прежде чем перейти к описанию функций, используемых для доступа к атрибутам безопасности файла или каталога на низком уровне, перечислим специфические права доступа, которые могут быть установлены в списках управления доступом к файлам и каталогам. Соответственно, такие же режимы доступа пользователь может указать при попытке открытия доступа к файлу или каталогу. Для файлов могут быть установлены следующие специфические режимы доступа, которые соответствуют правам субъектов на доступ к файлам: □ file_append_data — добавление данных; □ file_execute — исполнение. Следующие специфические права доступа могут быть установлены как для файлов, так и для каналов: □ file_read_data — чтение данных; □ FILE_WRITE_DATA — заПИСЬ ДаННЫХ. Для файлов и каталогов могут быть установлены следующие права доступа: □ file_read_ea — чтение расширенных атрибутов; □ file_write_ea — право записывать расширенные атрибуты. Для каталогов могут быть установлены следующие права доступа: □ file_list_directory — перечисление содержимого каталога; □ file_add_file — добавление файлов в каталог; □ file_add_subdirectory — добавление подкаталогов; □ file_traverse — следование через каталог; □ file_delete_child — удаление дочерних каталогов. Следующие права доступа определены для всех объектов типа файл: □ file_read_attributes — чтение атрибутов; □ file_write_attributes — запись атрибутов. Кроме того, для краткого обозначения полного доступа к объектам типа файл определена следующая символьная константа: □ file_all_access — полный доступ к файлу. Родовые права доступа к файлам отображаются в следующие специфические и стандартные права доступа: □ file_generic_read — включает права standard_rights_read, FILE_READ_DATA, FILE_READ_ATTRIBUTES, FILE_READ_EA И SYNCHRONIZE;
Глава 45. Управление безопасностью объектов на низком уровне 1007 П FILE_GENERIC_WRITE — ВКЛЮЧаеТ Права STANDARD_RIGHTS_WRITE, FILE_WRITE_DATA, FILE_WRITE_ATTRIBUTES, FILE_WRITE_EA, FILE_APPEND_DATA И SYNCHRONIZE; □ FILE_GENERIC_EXECUTE — ВКЛЮЧаеТ Права STANDARD_RIGHTS_EXECUTE, FILE_READ_ATTRIBUTES, FILE_EXECUTE И SYNCHRONIZE. Теперь перейдем к функциям, используемым для доступа к атрибутам безопасности файлов и каталогов на низком уровне. Для изменения атрибутов безопасности файла или каталога используется функция setFiieSecurity, которая имеет следующий прототип: BOOL SetFiieSecurity( LPCSTR lpFileName, // имя файла SECURITY_INFORMATION Security In format ion, // управляющие флаги PSECURITY_DESCRIPTOR pSecurityDescriptor // указатель на SD ); Отметим, что если функция изменяет атрибуты безопасности каталога, то эти изменения не распространяются на дочерние объекты каталога. В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ ПОСреДСТВОМ ВЫЗОВа фуНКЦИИ GetLastError. Параметр lpFiieName должен указывать на имя файла или каталога, в котором изменяются атрибуты безопасности. Параметр securityinformation имеет тип security_information, который является синонимом типа dword. В этом параметре должны быть установлены флаги, которые отмечают, какие атрибуты безопасности файла или каталога должны быть изменены. Для установки флагов используются следующие символические константы: □ owner_security_information — владелец объекта; □ group_security_information — первичная группа владельца объекта; □ dacl_security_information — список управления доступом DACL; □ sacl_security_information — список управления доступом SACL. Операционная система Windows 2000 и более поздние версии также поддерживают следующие флаги: □ protected_dacl_security_information — DACL не наследует элементы; □ protected_sacl_security_information — SACL не наследует элементы; □ unprotected_dacl_security_information — DACL наследует элементы; □ unprotected_sacl_security_information — SACL наследует элементы. Параметр pSecurityDescriptor должен указывать на дескриптор безопасности, в котором установлены новые атрибуты безопасности файла или каталога.
1008 Часть XI. Управление безопасностью в Windows ^ Примечание ^ Изменять атрибуты безопасности файла или каталога при помощи функции SetFileSecurity может только владелец этого файла или каталога или пользователь, который имеет соответствующие стандартные права доступа. Кроме того, для изменения содержимого списка управления доступом SACL пользователь должен обладать привилегией se_security_name, которая находится в разрешенном состоянии. Для получения атрибутов безопасности файла или каталога используется функция GetFiieSecurity, которая имеет следующий прототип: BOOL GetFiieSecurity( LPCSTR lpFileName, // имя файла SECURITY_INFORMATION Requestedlnformation, // управляющие флаги PSECURITY_DESCRIPTOR pSecurityDescriptor, // указатель на SD DWORD nLength, // длина буфера для SD LPDWORD lpnLengthNeeded // требуемая длина буфера ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ ПОСреДСТВОМ ВЫЗОВа фуНКЦИИ GetLastError. Параметр lpFiieName должен указывать на имя файла или каталога, атрибуты безопасности которого получает функция. Параметр Securitylnformation Имеет ТИП SECURITY_INFORMATION, КОТОрЫЙ является синонимом типа dword. В этом параметре должны быть установлены флаги, которые отмечают, какие атрибуты безопасности файла или каталога должны быть прочитаны функцией. Для установки флагов используются те же символические константы, что и в соответствующем параметре фуНКЦИИ SetFileSecurity. Параметр pSecurityDescriptor ДОЛЖен указывать на буфер, В КОТОрЫЙ функция запишет дескриптор безопасности файла. Дескриптор безопасности будет записан в относительном формате. В параметре nLength должна быть задана длина буфера, на который указывает параметр pSecurityDescriptor. Параметр lpnLengthNeeded должен указывать на переменную типа dword, в которую функция в случае успешного завершения запишет 0, а в случае неудачи требуемую длину буфера. Е^~ Примечание ^ Читать при помощи функции GetFiieSecurity атрибуты безопасности файла или каталога может только владелец этого файла (каталога) или пользователь,
Глава 45. Управление безопасностью объектов на низком уровне 1009 для которого в список DACL этого файла (каталога) включен разрешающий элемент с правом read_control. Кроме того, для чтения содержимого списка SACL пользователь должен обладать привилегией se_security_name, которая находится в разрешенном состоянии. Листинг 45А Добавление элементов в список DACL файла или каталога #ifndef UNICODE #define UNICODE #endif #include <windows.h> #include <stdio.h> #include <lm.h> int main() { wchar_t wchDirName[248] ; // имя каталога wchar_t wchUserName [UNLEN] ; // имя пользователя ACL *lp01dDacl; // указатель на старый DACL ACL *lpNewDacl; // указатель на новый DACL LPVOID lpAce; // указатель на элемент АСЕ DWORD dwDaclLength =0; // длина DACL DWORD dwSdLength =0; // длина SD DWORD dwSidLength =0; // длина SID DWORD dwLengthOfDomainName =0; // длина имени домена PSID IpSid = NULL; // указатель на разрешающий SID LPTSTR lpDomainName = NULL; // указатель на имя домена SID_NAME_USE typeOfSid; // тип учетной записи SECURITY_DESCRIPTOR *lpSd = NULL; // адрес дескриптора безопасности SECURITY_DESCRIPTOR sdAbsoluteSd; // абсолютный формат SD BOOL bDaclPresent; // признак присутствия списка DACL BOOL bDaclDefaulted; // признак списка DACL по умолчанию DWORD dwRetCode; // код возврата
1010 Часть XI. Управление безопасностью в Windows // читаем имя файла или каталога printf("Input a file or directory name: "); getws (wchDirName) ; // получаем длину дескриптора безопасности if (!GetFileSecurity( wchDirName/ // имя файла DACL^SECURITY.INFORMATION, // получаем DACL IpSd, // адрес дескриптора безопасности О, // определяем длину буфера &dwSdLength)) // адрес для требуемой длины { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) // распределяем память для буфера IpSd = (SECURITY_DESCRIPTOR*) new char [dwSdLength] ; else { // выходим из программы printf("Get file security failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } } // читаем дескриптор безопасности if (!GetFileSecurity( wchDirName/ // имя файла DACL.SECURITY.INFORMATION/ // получаем DACL IpSd/ // адрес дескриптора безопасности dwSdLength, // длина буфера SdwSdLength)) // адрес для требуемой длины { dwRetCode = GetLastError(); printf("Get file security failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode;
Глава 45. Управление безопасностью объектов на низком уровне 1011 } // вводим имя пользователя, которого добавляем в DACL printf("Input a user name: "); _getws (wchUserName) ; // определяем длину SID пользователя if (!LookupAccountName( NULL, // ищем имя на локальном компьютере wchUserName, // имя пользователя NULL, // определяем длину SID SdwSidLength, // длина SID NULL, // определяем имя домена &dwLengthOfDomainName, // длина имени домена StypeOfSid)) // тип учетной записи { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) { // распределяем память для SID IpSid = (SID*) new char[dwSidLength]; lpDomainName = (LPTSTR) new wchar_t [dwLengthOfDomainName] ; } else { // выходим из программы printf("Lookup account name failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } } // определяем SID if(!LookupAccountName( NULL, // ищем имя на локальном компьютере wchUserName, // имя пользователя IpSid, // указатель на SID
1012 Часть XI. Управление безопасностью в Windows SdwSidLength, // длина SID lpDomainName, // указатель на имя домена &dwLengthOfDomainName, // длина имени домена StypeOfSid)) // тип учетной записи { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // получаем список DACL из дескриптора безопасности if (!GetSecurityDescriptorDacl( IpSd, // адрес дескриптора безопасности SbDaclPresent, // признак присутствия списка DACL &lp01dDacl, // адрес указателя на DACL &bDaclDefaulted)) // признак списка DACL по умолчанию { dwRetCode = GetLastError(); printf("Get security descriptor DACL failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // определяем длину нового DACL dwDaclLength = lpOldDacl->AclSize + sizeof (ACCESS_ALLOWED_ACE) - sizeof (DWORD) + dwSidLength; // распределяем память под новый DACL lpNewDacl = (ACL*)new char[dwDaclLength]; // инициализируем новый DACL if (!InitializeAcl( lpNewDacl, // адрес DACL dwDaclLength, // длина DACL ACL_REVISION)) // версия DACL
Глава 45. Управление безопасностью объектов на низком уровне 1013 { dwRetCode = GetLastError(); printf("Lookup account name failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // добавляем новый элемент в новый DACL if (!AddAccessDeniedAce( lpNewDacl, // адрес DACL ACL_REVISION/ // версия DACL FILE_WRITE_ATTRIBUTES, // запрещаем писать атрибуты IpSid)) // адрес SID { dwRetCode = GetLastError(); perror("Add access allowed ace failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // получаем адрес первого АСЕ в старом списке DACL if (!GetAce( lpOldDacl, // адрес старого DACL О, // ищем первый элемент &1рАсе)) // адрес первого элемента { dwRetCode = GetLastError(); printf("Get асе failed.\n"); printf("Error code: %d\n'\ dwRetCode); return dwRetCode; } // переписываем элементы из старого DACL в новый DACL if (bDaclPresent)
1014 Часть XI. Управление безопасностью в Windows if (!AddAce( lpNewDacl, // адрес нового DACL ACL_REVISION, // версия DACL MAXDWORD, // добавляем в конец списка lpAce, // адрес старого DACL lp01dDacl->AclSize - sizeof(ACL))) // длина старого DACL { dwRetCode = GetLastError(); perror("Add access allowed ace failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } } // проверяем достоверность DACL if (! IsValidAcl (lpNewDacl)) { dwRetCode = GetLastError(); perror("The new ACL is invalid.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // создаем новый дескриптор безопасности в абсолютной форме if (!InitializeSecurityDescriptor( &sdAbsoluteSd, // адрес структуры SD SECURITY_DESCRIPTOR_REVISION)) { dwRetCode = GetLastError(); perror("Initialize security descriptor failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // устанавливаем DACL в новый дескриптор безопасности
Глава 45. Управление безопасностью объектов на низком уровне 1015 if (!SetSecurityDescriptorDacl( SsdAbsoluteSd, // адрес дескриптора безопасности TRUE, // DACL присутствует lpNewDacl, // указатель на DACL FALSE)) // DACL не задан по умолчанию { dwRetCode = GetLastError(); perrorC'Set security descriptor DACL failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // проверяем структуру дескриптора безопасности if (!IsValidSecurityDescriptor(&sdAbsoluteSd)) { dwRetCode = GetLastError(); perror("Security descriptor is invalid.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // устанавливаем новый дескриптор безопасности if (!SetFileSecurity( wchDirName, // имя файла DACL_SECURITY_INFORMATION/ // устанавливаем DACL &sdAbsoluteSd)) // адрес дескриптора безопасности { dwRetCode = GetLastError(); printf("Set file security failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // освобождаем память delete[] IpSd; delete[] IpSid;
1016 Часть XI. Управление безопасностью в Windows delete[] lpDomainName; delete[] lpNewDacl; return 0; } 45.6. Защита объектов ядра Напомним, что объектами ядра операционной системы называются те объекты, доступ к которым осуществляется через дескриптор типа handle. В этом разделе будут рассматриваться только те объекты ядра, которые существуют во время исполнения программы. К таким объектам ядра операционной системы относятся, например, процессы, потоки, объекты синхронизации, но не файлы. К объектам ядра также относится и маркер доступа. Для доступа к атрибутам безопасности таких объектов ядра операционной системы МОГУТ ИСПОЛЬЗОВаТЬСЯ специальные фуНКЦИИ SetKernelObjectSecurity и GetKerneiobjectsecurity (см. листинг 45.5). Для доступа к объектам ядра операционной системы обычно используются стандартные режимы доступа. Но такие объекты ядра операционной системы, как процессы и потоки, имеют большое количество специфических режимов доступа, которые и приведены ниже. Для процессов можно установить следующие специфические режимы доступа, которые соответствуют правам субъекта на доступ к процессу: □ process_terminate — прекращение процесса; □ process_create_thread — создание потока; □ process_set_sessionid — установка идентификатора сессии; □ process_vm_operation — работа с виртуальными адресами процесса; □ process_vm_read — чтение из виртуальной памяти процесса; □ process_vm_write — запись в виртуальную память процесса; □ process_dup_handle — дублирование дескриптора процесса; □ process_create_process — создание процесса; □ process_set_quota — квотирование физической памяти процесса; □ process_set_information — установка управляющей информации; □ process_query_information — чтение управляющей информации; □ PROCESS_ALL_ACCESS — ВПОЛНЫЙ ДОСТуП К Процессу. Для потоков существуют следующие специфические режимы доступа: □ thread_terminate — завершение потока; □ thread_suspend_resume — подвешивание и возобновление потока;
Глава 45. Управление безопасностью объектов на низком уровне 1017 П thread_get_context — чтение контекста потока; □ thread_set_context — установка контекста потока; □ thread_set_information — установка управляющей информации; □ thread_query_information — чтение управляющей информации; □ thread_set_thread_token — установка замещающего маркера доступа; □ thread_impersonate — разрешение использовать атрибуты защиты потока; □ thread_direct_impersonation — разрешение потоку сервера замещать поток клиента. Кроме того, специфические режимы доступа существуют также и для объектов синхронизации. Эти права доступа перечислены далее. Для мьютексов можно установить следующие специфические режимы доступа: □ mutex_modify_state — освобождение мьютекса; □ MUTEX_ALL_ACCESS — ПОЛНЫЙ ДОСТуП К МЬЮТеКСу. Для событий можно устанавливать следующие специфические режимы доступа: □ event_modify_state — изменение состояния события; □ EVENT_ALL_ACCESS — ПОЛНЫЙ ДОСТУП К СОбЫТИЮ. Для семафоров можно устанавливать следующие специфические режимы доступа: □ semaphore_modify_state — изменение состояния семафора; □ SEMAPHORE_ALL_ACCESS — ПОЛНЫЙ ДОСТУП К Семафору. Для ожидающих таймеров можно устанавливать следующие специфические режимы доступа: □ TIMER_QUERY_STATE — запрос СОСТОЯНИЯ; □ timer_modify_state — изменение состояния; □ TIMER_ALL_ACCESS — ПОЛНЫЙ ДОСТУП. Для маркеров доступа определены следующие специфические режимы доступа: □ token_assign_primary — право присоединить первичный маркер доступа к процессу; □ token_duplicate — право дублировать маркер доступа; □ token_impersonate — право замещать маркер доступа процесса; □ token_query — право получать информацию из маркера доступа; □ token_query_source — право получить информацию об источнике маркера доступа;
1018 Часть XI. Управление безопасностью в Windows □ token_adjust_privileges — право настраивать привилегии маркера доступа; □ token_adjust_groups — право настраивать свойства фупп в маркере доступа; □ token_adjust_default — право изменять информацию для установки в дескриптор объекта по умолчанию; □ token_adjust_sessionid — право настраивать идентификатор сессии в маркере доступа. Кроме того, для маркеров доступа можно также использовать следующие символические константы, используемые для обозначения комбинации нескольких различных флагов доступа: □ token_all_access — включает все права доступа к маркеру доступа; □ token_read — включает права доступа standard_rights_read и token_query; □ token_write — включает права standard_rights_write, TOKEN_ADJUST_PRIVILEGES, TOKEN_ADJUST_GROUPS И TOKEN_ADJUST_DEFAULT; □ TOKEN_EXECUTE — ВКЛЮЧат ПраВО STANDARD_RIGHTS_EXECUTE. Теперь перейдем к функциям, предназначенным для доступа к атрибутам безопасности объектов ядра операционной системы. Для установки атрибутов безопасности объекта ядра операционной системы используется функция setKemeiobjectsecurity, которая имеет следующий прототип: BOOL SetKernelObj ectSecurity( HANDLE Handle, // дескриптор объекта ядра SECURITY_INFORMATION Security Information, // управляющие флаги PSECURITY_DESCRIPTOR pSecurityDescriptor // дескриптор безопасности ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ ПОСреДСТВОМ ВЫЗОВа фуНКЦИИ GetLastError. Параметр Handle должен содержать дескриптор объекта ядра операционной системы, для которого устанавливаются атрибуты безопасности. Параметр Securitylnformation Имеет ТИП SECURITY_INFORMATION, КОТОрЫЙ является синонимом типа dword. В этом параметре должны быть установлены флаги, которые отмечают, какие атрибуты безопасности объекта ядра операционной системы должны быть изменены. Для установки флагов используются следующие символические константы: □ owner_security_information — владелец объекта; □ group_security_information — первичная группа владельца объекта;
Глава 45. Управление безопасностью объектов на низком уровне 1019 П dacl_security_information — список управления доступом DACL; □ sacl_security_information — список управления доступом SACL. Параметр pSecurityDescriptor должен указывать на дескриптор безопасности, в котором установлены новые атрибуты безопасности объекта ядра операционной системы. ^~ Примечание ^ Изменять атрибуты безопасности объекта ядра операционной системы при помощи функции setKernelobjectsecurity может только владелец этого объекта или пользователь, который имеет соответствующие стандартные права доступа. Кроме того, для изменения содержимого списка управления доступом SACL пользователь должен обладать привилегией se_security_name, которая находится в разрешенном состоянии. Для получения атрибутов безопасности объекта ядра операционной системы ИСПОЛЬЗуетСЯ фуНКЦИЯ GetKernelObjectSecurity, Которая Имеет следующий прототип: BOOL GetKernelObj ectSecurity( HANDLE Handle, // дескриптор объекта ядра SECURITY_INFORMATION Security Information, // управляющие флаги PSECURITY_DESCRIPTOR pSecurityDescriptor // дескриптор безопасности DWORD nLength, // длина буфера для SD LPDWORD lpnLengthNeeded // требуемая длина буфера ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции МОЖНО ПОЛУЧИТЬ ПОСреДСТВОМ ВЫЗОВа фуНКЦИИ GetLastError. Параметр Handle должен содержать дескриптор объекта ядра операционной системы, атрибуты безопасности которого получает функция. Параметр Securitylnformation Имеет ТИП SECURITY_INFORMATION, КОТОрЫЙ является синонимом типа dword. В этом параметре должны быть установлены флаги, которые отмечают, какие атрибуты безопасности объекта ядра операционной системы должны быть прочитаны функцией. Для установки флагов используются те же символические константы, что и в соответствующем параметре функции SetKernelobjectsecurity. Параметр pSecurityDescriptor должен указывать на буфер, в который функция запишет дескриптор безопасности объекта ядра операционной системы. Дескриптор безопасности будет записан в относительном формате. В параметре nLength должна быть задана длина буфера, на который указывает параметр pSecurityDescriptor.
1020 Часть XI. Управление безопасностью в Windows Параметр IpnLengthNeeded должен указывать на переменную типа dword, в которую функция в случае успешного завершения запишет 0, а в случае неудачи требуемую длину буфера. ^ Примечание ^| Получать атрибуты безопасности объекта ядра операционной системы при помощи функции GetKernelobjectsecurity может только владелец этого объекта или пользователь, для которого в список DACL этого объекта включен разрешающий элемент с правом read_control. Кроме того, для чтения содержимого списка SACL пользователь должен обладать привилегией se_security_name, которая находится в разрешенном состоянии. мьютекса #ifndef UNICODE #def ine UNICODE #endif # include <windows.h> #include <stdio.h> int main() { HANDLE hMutex; // дескриптор мьютекса DWORD dwLengthOfSd =0; // длина SD SECURITY_DESCRIPTOR *lpSd = NULL; // дескриптор безопасности мьютекса PSID lpOwner; // указатель на SID владельца BOOL bOwnerDefaulted = FALSE; // признак владельца по умолчанию DWORD dwLengthOfUserName =0; // длина имени учетной записи DWORD dwLengthOfDomainName = 0; // длина имени домена LPTSTR lpUserName = NULL; LPTSTR lpDomainName = NULL; // указатель на имя домена // указатель на имя домена SID_NAME_USE type_of_SID; // тип учетной записи DWORD dwRetCode; // код возврата
Глава 45. Управление безопасностью объектов на низком уровне 1021 II создаем каталог hMutex = CreateMutex(NULL, FALSE, L"DemoMutex") ; if (hMutex == NULL) { dwRetCode = GetLastError(); perror("Create mutex failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } printf("The mutex is created.\n"); // определяем требуемую длину буфера if (!GetKernelObjectSecurity( hMutex, // дескриптор мьютекса OWNER_SECURITY_INFORMATION/ // получаем владельца мьютекса IpSd, // адрес SD О, // определяем длину буфера &dwLengthOfSd)) // требуемая длина буфера { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) // распределяем память для буфера IpSd = (SECURITY_DESCRIPTOR*) new char[dwLengthOfSd]; else { // выходим из программы printfC'Get kernel object security failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } } // чтаем идентификатор безопасности мьютекса if (!GetKernelObjectSecurity( hMutex, // дескриптор мьютекса
1022 Часть XI. Управление безопасностью в Windows OWNER_SECURITY_INFORMATION/ // получаем владельца мьютекса IpSd, // адрес SD dwLengthOfSd, // длина буфера &dwLengthOfSd)) // требуемая длина буфера { dwRetCode = GetLastError(); printfC'Get kernel object security failed.\n"); printfC'The last error code: %u\n"/ dwRetCode); return dwRetCode; } // получаем SID владельца мьютекса if (!GetSecurityDescriptorOwner( IpSd, // адрес дескриптора безопасности SlpOwner, // адрес указателя на SID владельца &bOwnerDefaulted)) // признак владельца по умолчанию { dwRetCode = GetLastError(); printfC'Get security descriptor owner failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // проверяем, есть ли владелец if (lpOwner == NULL) { printfC'The owner is not present. \n") ; return 0; } if (bOwnerDefaulted) printfC'The owner is defaulted. \n") ; // определяем длину имени домена if(!LookupAccountSid( NULL, // ищем на локальном компьютере
Глава 45. Управление безопасностью объектов на низком уровне 1023 lpOwner, // указатель на SID lpUserName, // имя пользователя SdwLengthOfUserName, // длина имени пользователя lpDomainName, // определяем имя домена SdwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) { // распределяем память под имя пользователя и имя домена lpUs erName = (LPTSTR) new wchar_t [ dwLengthO f Us erName ] ; lpDomainName = (LPTSTR) new wchar_t [dwLengthOfDomainName] , } else { printf("Lookup account SID for length failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } } // определяем имя учетной записи по SID if(!LookupAccountSid( NULL, // ищем на локальном компьютере lpOwner, // указатель на SID пользователя lpUserName, // имя пользователя SdwLengthOfUserName, // длина имени пользователя lpDomainName, // определяем имя домена SdwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwRetCode = GetLastError(); printf("Lookup account SID failed.\n"); printf("Error code: %d\n'\ dwRetCode);
1024 Часть XI. Управление безопасностью в Windows return dwRetCode; } wprintf(L"User name: %s\n"/ lpUserName); wprintf(L"Domain name: %s\n"/ lpDomainName) ; // освобождаем память delete[] lpUserName; delete[] lpDoma inName; delete[] IpSd; CloseHandle(hMutex); printf("The mutex is deleted.\n"); return 0; } 45.7. Защита сервисов Прежде чем перейти к функциям, предназначенным для доступа к атрибутам безопасности сервисов, перечислим специфические права доступа, которые могут контролироваться системой безопасности при доступе субъекта к сервису. Ниже приведены символические константы, используемые для задания специфических прав доступа к сервису: □ service_query_config — чтение конфигурации; □ service_change_config — изменение конфигурации; □ service_query_status — чтение состояния; □ service_enumerate_dependents — перечисление зависимостей; □ service_start — запуск сервиса; □ service_stop — остановка сервиса; □ service_pause_continue — приостановка и возобновление сервиса; □ service_interrogate — получать текущее состояние; □ service_user_defined_control — определять управление. Дополнительно для задания режима доступа к сервисам определена символическая константа service_all_access — полный доступ к сервису. Теперь перейдем к описанию функций, используемых для доступа к атрибутам безопасности сервиса. Для установки атрибутов безопасности сервиса
Глава 45. Управление безопасностью объектов на низком уровне 1025 используется функция setserviceobjectsecurity, которая имеет следующий прототип: BOOL SetServiceObjectSecurity( SC_HAKDLE hService, // дескриптор сервиса SECURITY_INFORMATION dwSecuritylnformation, // управляющие флаги PSECURITY_DESCRIPTOR IpSecurityDescriptor // SD ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции можно получить посредством вызова функции GetLastError. Менеджер сервисов может установить следующие коды ошибок при неудачном завершении функции SetServiceObjectSecurity: □ error_access_denied — отказано в доступе; □ error_invalid_handle — недействительный дескриптор сервиса; □ error_invalid_parameter — неправильные параметры; □ error_service_marked_for_delete — сервис отмечен для удаления. Параметры функции setserviceobjectsecurity имеют следующее назначение. Параметр hservice должен содержать дескриптор сервиса, для которого устанавливаются атрибуты безопасности. Этот дескриптор должен быть получен посредством функций CreateService ИЛИ OpenService. Параметр dwsecurityinformation имеет тип security_information, который является синонимом типа dword. В этом параметре должны быть установлены флаги, которые отмечают, какие атрибуты безопасности сервиса должны быть изменены. Для установки флагов используются следующие символические константы: □ owner_security_information — владелец объекта; □ group_security_information — первичная группа владельца объекта; □ dacl_security_information — список управления доступом DACL; □ sacl_security_information — список управления доступом SACL. Параметр IpSecurityDescriptor должен указывать на дескриптор безопас- ности, в котором установлены новые атрибуты безопасности сервиса. ^ Примечание ^^ Изменять атрибуты безопасности сервиса может только владелец этого сервиса или пользователь, который имеет соответствующие стандартные права доступа. Кроме того, для изменения содержимого списка управления доступом SACL пользователь должен обладать привилегией se_security_name, которая включена. 33 Зак. 514
1026 Часть XI. Управление безопасностью в Windows Для получения атрибутов безопасности сервиса используется функция QueryServiceObjectSecurity, которая имеет следующий ПРОТОТИП! BOOL QueryServiceObj ectSecurity( SC_HANDLE hService, // дескриптор сервиса SECURITY_INFORMATION dwSecurityInformation, // управляющие флаги PSECURITY_DESCRIPTOR IpSecurityDescriptor, // SD DWORD cbBufSize, // длина буфера LPDWORD pcbBytesNeeded // требуемая длина буфера ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. Код ошибки в случае неудачного выполнения функции можно получить посредством вызова функции GetLastError. Менеджер сервисов может установить следующие коды ошибок при неудачном завершении функции QueryServiceObjectSecurity: □ error_access_denied — отказано в доступе; □ error_invalid_handle — недействительный дескриптор сервиса; □ error_invalid_parameter — неправильные параметры; □ error_insufficient_buffer — в буфере недостаточно памяти. Параметр hservice должен содержать дескриптор сервиса, атрибуты безопасности которого получает функция. Этот дескриптор должен быть получен посредством функций CreateService ИЛИ OpenService. Параметр dwsecurityinformation имеет тип security_information, который является синонимом типа dword. В этом параметре должны быть установлены флаги, которые отмечают, какие атрибуты безопасности сервиса должны быть прочитаны функцией. Для установки флагов используются те же символические константы, что и в соответствующем параметре функции SetServiceObj ectSecurity. Параметр IpSecurityDescriptor должен указывать на буфер, в который функция запишет дескриптор безопасности сервиса. Дескриптор безопасности будет записан в относительном формате. В параметре cbBufsize должна быть задана длина буфера, на который указывает параметр IpSecurityDescriptor. Параметр pcbBytesNeeded должен указывать на переменную типа dword, в которую функция в случае неудачного завершения запишет требуемую длину буфера. ^ Примечание ^ Получить атрибуты безопасности сервиса может только владелец этого сервиса или пользователь, для которого в список DACL этого объекта включен раз-
Глава 45. Управление безопасностью объектов на низком уровне 1027 решающий элемент с правом read_control. Кроме того, для чтения содержимого списка SACL пользователь должен обладать привилегией se_security_name, которая включена, и правом доступа access_system_security. Листинг 45^|ЩоЬучение владельца сервиса #include <windows.h> #include <stdio.h> int main() { SC_HANDLE hManager; SC_HANDLE hService; // дескриптор менеджера сервисов // дескриптор сервиса DWORD dwLengthOfSd =0; // длина SD SECURITY_DESCRIPTOR *lpSd; // дескриптор безопасности сервиса PSID lpOwner; // указатель на SID владельца BOOL bOwnerDefaulted = FALSE; // признак владельца по умолчанию DWORD dwLengthOfUserName =0; // длина имени учетной записи DWORD dwLengthOfDomainName =0; // длина имени домена LPTSTR lpUserName = NULL; // указатель на имя домена LPTSTR lpDomainName = NULL; // указатель на имя домена SID_NAME_USE type_of^SID; // тип учетной записи DWORD dwRetCode; // код возврата // инициализируем, так как NULL выдаст ошибку IpSd = (SECURITY_DESCRIPTOR*) new char[0]; // связываемся с менеджером сервисов hManager = OpenSCManager ( NULL, // локальная машина NULL, // активная база данных сервисов GENERIC_READ) ; // читаем управляющую информацию if (hManager == NULL)
1028 Часть XI. Управление безопасностью в Windows { dwRetCode = GetLastError(); printf("Open SC manager failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // открываем сервис hService = OpenService( hManager, // дескриптор менеджера сервисов "PlugPlay", // имя сервиса READ_CONTROL); // режим доступа if (hService == NULL) { dwRetCode = GetLastError(); printf("Open service failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // определяем длину идентификатора безопасности сервиса if (!QueryServiceObjectSecurity( hService, // дескриптор сервиса OWNER_SECURITY_INFORMATION/ // получаем владельца сервиса IpSd, • // адрес SD dwLengthOfSd, // длина буфера SdwLengthOfSd)) // требуемая длина буфера { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) // захватываем память под дескриптор безопасности IpSd = (SECURITY_DESCPIPTOR*)new char[dwLengthOfSd]; else { printf("Query Service Object Security failed.\n"); printf("The last error code: %u\n"/ dwRetCode);
Глава 45. Управление безопасностью объектов на низком уровне 1029 return dwRetCode; } } // читаем идентификатор безопасности сервиса if (!QueryServiceObjectSecurity( hService, // дескриптор сервиса OWNER_SECURITY_INFORMATION/ // получаем владельца мьютекса IpSd, // адрес SD dwLengthOfSd, // длина буфера SdwLengthOfSd)) // требуемая длина буфера { dwRetCode = GetLastError(); printf("Query Service Object Security failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // получаем SID владельца сервиса if (IGetSecurityDescriptorOwner( IpSd, // адрес дескриптора безопасности SlpOwner, // адрес указателя на SID владельца SbOwnerDefaulted)) // признак владельца по умолчанию { dwRetCode = GetLastError(); printf("Get security descriptor owner failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // проверяем, есть ли владелец if (lpOwner == NULL) { printf("The owner is not present.\n"); return 0;
1030 Часть XI. Управление безопасностью в Windows if (bOwnerDefaulted) printf("The owner is defaulted.\n"); else printf("The owner is not defaulted.\n"); // определяем длину имени домена if(!LookupAccountSid( NULL, // ищем на локальном компьютере lpOwner, // указатель на SID lpUserName, // имя пользователя &dwLengthOfUserName, // длина имени пользователя lpDomainName, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) { // распределяем память под имя пользователя и имя домена lpUserName = (LPTSTR) new char[dwLengthOfUserName]; lpDomainName = (LPTSTR) new char[dwLengthOfDomainName]; } else { printf("Lookup account SID for length failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } } // определяем имя учетной записи по SID if(!LookupAccountSid( NULL, // ищем на локальном компьютере lpOwner, // указатель на SID пользователя lpUserName, // имя пользователя &dwLengthOfUserName, // длина имени пользователя lpDomainName, // определяем имя домена
Глава 45. Управление безопасностью объектов на низком уровне 1031 &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwRetCode = GetLastError(); printf("Lookup account SID failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } printf("The owner name: %s\n"/ lpUserName) ; printf("The owner domain name: %s\n"/ lpDomainName); II освобождаем память de1ete[] lpUserName; de1e te[] lpDomainName; delete[] IpSd; CloseServiceHandle(hService); CloseServiceHandle(hManager); return 0; } 45.8. Защита ключей реестра Прежде чем перейти к функциям, предназначенным для доступа к атрибутам безопасности ключей реестра, перечислим специфические права доступа, которые могут контролироваться системой безопасности при доступе к реестру. Следующие символические константы используются для задания специфических прав доступа к ключам реестра: □ key_query_value — запрос данных из ключа; □ key_set_value — запись данных в ключ; □ key_create_sub_key — создание дочернего ключа; □ key_enumerate_sub_keys — перечисление ключей; □ key_notify — получение сообщения об изменении в ключе; □ key_create_link — создание ссылки на ключ.
1032 Часть XI. Управление безопасностью в Windows Кроме того, для задания прав доступа к ключам реестра определены следующие символические константы: □ key_read — включает права доступа standard_rights_read, KEY_QUERY_VALUE, KEY_ENUMERATE_SUB_KEYS, KEY_NOTIFY, -SYNCHRONIZE; □ key_write — включает права доступа standard_rights_write, KEY_SET_VALUE, CREATE_SUB_KEY, -SYNCHRONIZE; □ key_execute — включает права доступа key_read и -synchronize; □ key_all_access — полный доступ к ключу реестра. Теперь перейдем к описанию функций, используемых для доступа к атрибутам безопасности ключей реестра (см. листинг 45.7). Для установки атрибутов безопасности КЛЮЧа реестра ИСПОЛЬЗуетсЯ фуНКЦИЯ RegSetKeySecurity, которая имеет следующий прототип: LONG RegSetKeySecuri ty( HKEY hKey, // дескриптор ключа SECURITY_INFORMATION Securitylnformation, // управляющие флаги PSECURITY_DESCRIPTOR pSecurityDescriptor // дескриптор безопасности ); В случае успешного завершения функция вернет значение error_success. В случае неудачного завершения функции код ошибки можно получить посредством вызова функции GetLastError. Параметры функции RegSetKeySecurity имеют следующее назначение. Параметр ъкеу должен содержать дескриптор ключа, для которого устанавливаются атрибуты безопасности. Параметр Securitylnformation имеет ТИП SECURITY_INFORMATION, КОТОрЫЙ является синонимом типа dword. В этом параметре должны быть установлены флаги, которые отмечают, какие атрибуты безопасности ключа реестра должны быть изменены. Для установки флагов используются следующие символические константы: □ owner_security_information — владелец объекта; □ group_security_information — первичная группа владельца объекта; □ dacl_security_information — список управления доступом DACL; □ sacl_security_information — список управления доступом SACL. Параметр pSecurityDescriptor должен указывать на дескриптор безопасно- сти, в котором установлены новые атрибуты безопасности ключа реестра. ^ Примечание ^ Изменять атрибуты безопасности ключа реестра может только владелец этого ключа или пользователь, который имеет соответствующие стандартные права
Глава 45. Управление безопасностью объектов на низком уровне 1033 доступа. Кроме того для изменения содержимого списка управления доступом SACL пользователь должен обладать привилегией se_security_name, которая находится в разрешенном состоянии. Для получения атрибутов безопасности ключа реестра используется функция RegGetKeySecurity, которая имеет следующий прототип: LONG RegGetKeySecurity ( HKEY hKey, // дескриптор ключа SECURITY_INFORMATION Security-Information, // управляющие флаги PSECURITY_DESCRIPTOR pSecurityDescriptor, // SD LPDWORD IpebSecurityDescriptor // длина буфера ); В случае успешного завершения функция вернет значение error_success. В случае неудачного завершения функции код ошибки можно получить посредством ВЫЗОВа функции GetLastError. Параметры функции RegGetKeySecurity имеют следующее назначение. Параметр пкеу должен содержать дескриптор ключа, атрибуты безопасности которого получает функция. Параметр Securitylnformation имеет ТИП SECURITY_INFORMATION, КОТОрЫЙ является синонимом типа dword. В этом параметре должны быть установлены флаги, которые отмечают, какие атрибуты безопасности ключа реестра должны быть получены функцией. Для установки флагов используются такие же символические константы, как и для соответствующего параметра ФУНКЦИИ RegSetKeySecurity. Параметр psecurityDescriptor должен указывать на буфер, в который функция запишет дескриптор безопасности ключа реестра. Дескриптор безопасности будет записан в относительном формате. Параметр IpebSecurityDescriptor должен указывать на переменную типа dword, которая содержит длину буфера. В случае недостаточной длины буфера функция вернет код error_insufficient_buffer, а по этому адресу запишет требуемую длину буфера. ( Замечание ^ Получить атрибуты безопасности ключа реестра может только владелец этого ключа или пользователь, для которого в список DACL этого объекта включен разрешающий элемент с правом read_control. Кроме того, для чтения содержимого списка SACL пользователь должен обладать привилегией se_security_name, которая находится в разрешенном состоянии, и правом ДОСТупа ACCESS_SYSTEM_SECURITY.
1034 Часть XI. Управление безопасностью в Windows # inc lude <windows. h> #include <stdio.h> int main() { HKEY hKey; // ключ реестра DWORD dwLengthOfSd =0; // длина SD SECURITY_DESCRIPTOR *lpSd = NULL; // SD ключа реестра PSID lpOwner; // указатель на SID владельца BOOL bOwnerDefaulted = FALSE; // признак владельца по умолчанию DWORD dwLengthOfUserName =0; // длина имени учетной записи DWORD dwLengthOfDomainName = 0; // длина имени домена LPTSTR lpUserName = NULL; LPTSTR lpDomainName = NULL; // указатель на имя домена // указатель на имя домена SID_NAME_USE type_of_SID; // тип учетной записи DWORD dwRetCode; // код возврата // открываем ключ каталога dwRetCode = RegOpenKeyEx( HKEY LOCAL MACHINE, // локальная машина NULL, 0, KEY_QUERY_VALUE, &hKey); // открыть ключ локальной машины // зарезервировано // получаем данные из ключа // адрес дескриптора ключа if (dwRetCode != ERROR_SUCCESS) { dwRetCode = GetLastError(); printf("RegOpenKeyEx failed.\n"); printf("The last error code: %u\n", dwRetCode) return dwRetCode;
Глава 45. Управление безопасностью объектов на низком уровне 1035 } // определяем длину идентификатора безопасности сервиса dwRetCode = RegGetKeySecurity( hKey, // дескриптор сервиса OWNER_SECURITY_INFORMATION, // получаем владельца ключа реестра IpSd, // адрес SD &dwLengthOfSd); // требуемая длина буфера if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) // захватываем память под дескриптор безопасности IpSd = (SECURITY_DESCRIPTOR*)new char[dwLengthOfSd]; else { printf("RegGetKeySecurity failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } // читаем идентификатор безопасности ключа реестра dwRetCode = RegGetKeySecurity( hKey, // дескриптор сервиса OWNER_SECURITY_INFORMATION, // получаем владельца ключа реестра IpSd, // адрес SD SdwLengthOfSd); // требуемая длина буфера if (dwRetCode != ERROR_SUCCESS) { printf("RegGetKeySecurity failed.\n"); printf("The last error code: %u\n"/ dwRetCode); return dwRetCode; } // получаем SID владельца мьютекса if (IGetSecurityDescriptorOwner( IpSd, // адрес дескриптора безопасности &lpOwner, // адрес указателя на SID владельца &bOwnerDefaulted)) // признак владельца по умолчанию
1036 Часть XI. Управление безопасностью в Windows dwRetCode = GetLastError(); printf("Get security descriptor owner failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } // проверяем, есть ли владелец if (lpOwner == NULL) { printf("The owner is not present.\n"); return 0; } if (bOwnerDefaulted) printf("The owner is defaulted.\n"); // определяем длину имени домена if(!LookupAccountSid( NULL, // ищем на локальном компьютере lpOwner, // указатель на SID lpUserName, // имя пользователя &dwLengthOfUserName, // длина имени пользователя lpDomainName, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) { // распределяем память под имя пользователя и имя домена lpUserName = (LPTSTR) new char [dwLengthOfUserName] ; lpDomainName = (LPTSTR) new char [dwLengthOfDomainName] ; } else { printf("Lookup account SID for length failed.\n"); printf("Error code: %d\n"/ dwRetCode);
Глава 45. Управление безопасностью объектов на низком уровне 1037 return dwRetCode; } } // определяем имя учетной записи по SID if(!LookupAccountSid( NULL, // ищем на локальном компьютере lpOwner, // указатель на SID пользователя lpUserName, // имя пользователя &dwLengthOfUserName, // длина имени пользователя lpDomainName, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwRetCode = GetLastError(); printf("Lookup account SID failed.\n"); printf("Error code: %d\n"/ dwRetCode); return dwRetCode; } printf("Key owner name: %s\n"/ lpUserName); printf("Key owner domain name: %s\n"/ lpDomainName); // освобождаем память de1ete[] lpUserName; delete[] lpDomainName; delete[] IpSd; RegCloseKey(hKey); return 0; } 45.9. Защита объектов пользователя Объектами пользователя будем называть объекты, доступ к которым осуществляется через дескриптор, тип которого отличается от типа handle. Некоторые объекты пользователя могут содержать атрибуты безопасности. К таким объектам относятся, например: станции окна (window station) и рабочие
1038 Часть XI. Управление безопасностью в Windows столы (desktop). Ниже перечислены специфические права доступа, которые могут устанавливаться в элементах списков управления доступом для контроля доступа субъектов к таким объектам пользователя. Следующие специфические права доступа могут использоваться для контроля доступа к станциям окон: □ winsta_enumdesktops — перечисление объектов рабочего стола; □ winsta_readattributes — чтение свойств станции окон; □ winsta_accessclipboard — доступ к общему буферу (clipboard); □ winsta_createdesktop — создание рабочего стола; □ winsta_writeattributes — установка свойств станции окон; □ winsta_accessglobalatoms — доступ к глобальным атомам; □ winsta_exitwindows — завершение работы пользователя; □ winsta_enumerate — перечисление станций окон; □ winsta_readscreen — чтение содержимого экрана. Следующие специфические права доступа могут использоваться для контроля доступа к рабочим столам: □ desktop_readobjects — чтение объектов рабочего стола; □ desktop_createwindow — создание окна на рабочем столе; □ desktop_createmenu — создание меню на рабочем столе; □ desktop_hookcontrol — установка перехватчиков (hooks) на окна; □ desktop_journalrecord — выполнение журнальных записей; □ desktop_journalplayback — просмотр журнальных записей; □ desktop_enumerate — перечисление рабочих столов; □ desktop_writeobjects — запись объектов на рабочий стол; □ desktop_switchdesktop — переключение рабочих столов. Теперь перейдем к описанию функций, используемых для доступа к атрибутам безопасности объектов пользователя (см. листинг 45.8). Для установки атрибутов безопасности объекта пользователя используется функция setuserobjectsecurity, которая имеет следующий прототип: BOOL SetUserObjectSecurity ( HANDLE hObj , // дескриптор объекта пользователя PSECURITY_INFORMATION pSIRequested, // управляющие флаги PSECURITY_DESCRIPTOR pSD // указатель на SD ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. В случае неудачного завершения функции код ошибки
Глава 45. Управление безопасностью объектов на низком уровне 1039 можно получить посредством вызова функции GetLastError. Параметры функции setuserobjectsecurity имеют следующее назначение. Параметр hobj должен содержать дескриптор объекта пользователя, для которого устанавливаются новые атрибуты безопасности. Параметр pSIRequested имеет тип security_information, который является синонимом типа dword. В этом параметре должны быть установлены флаги, которые отмечают, какие атрибуты безопасности объекта пользователя должны быть изменены функцией. Для установки флагов используются следующие символические константы: □ owner_security_information — владелец объекта; □ group_security_information — первичная фуппа владельца объекта; □ dacl_security_information — список управления доступом DACL; □ sacl_security_information — список управления доступом SACL. Параметр psd должен указывать на дескриптор безопасности, в котором установлены новые атрибуты безопасности для объекта пользователя. ^ При мечание Изменять атрибуты безопасности объекта пользователя может только владелец этого объекта или пользователь, который имеет соответствующие стандартные права доступа. Кроме того, для изменения содержимого списка управления доступом SACL пользователь должен обладать привилегией se_security_name, которая находится во включенном состоянии. Для получения атрибутов безопасности объекта пользователя используется функция Getuserobjectsecurity, которая имеет следующий прототип: BOOL GetUserObjectSecurity ( HANDLE hObj , // дескриптор объекта пользователя PSECURITY_INFORMATION pSIRequested, // управляющие флаги PSECURITY_DESCRIPTOR pSD, // указатель на SD DWORD nLength, // длина буфера LPDWORD lpnLengthNeeded // требуемая длина буфера ); В случае успешного завершения функция вернет ненулевое значение, а в случае неудачи — false. В случае неудачного завершения функции код ошибки можно получить посредством вызова функции GetLastError. Параметры функции Getuserobjectsecurity имеют следующее назначение. Параметр hobj должен содержать дескриптор объекта пользователя, атрибуты безопасности которого получает функция. Параметр pSIRequested имеет тип security_information, который является синонимом типа dword. В этом параметре должны быть установлены флаги,
1040 Часть XI. Управление безопасностью в Windows которые отмечают, какие атрибуты безопасности объекта пользователя должны быть получены функцией. Для установки флагов используются такие же символические константы, как и для соответствующего параметра ФУНКЦИИ SetUserObjectSecurity. Параметр psd должен указывать на буфер, в который функция запишет дескриптор безопасности объекта пользователя. Дескриптор безопасности будет записан в относительном формате. Параметр nLength должен содержать длину буфера, на который указывает параметр psd. Длина буфера задается в байтах. Параметр lpnLengthNeeded должен указывать на переменную типа dword, в которую функция в случае неудачного завершения запишет требуемую длину буфера. ^ Примечание ^^ Получить атрибуты безопасности объекта пользователя может только владелец этого объекта или пользователь, для которого в список DACL этого объекта включен разрешающий элемент с правом read_control. Кроме того, для чтения содержимого списка SACL пользователь должен обладать привилегией se_security_name, которая находится в разрешенном состоянии, и правом ДОСТупа ACCESS_SYSTEM_SEOJRITY. Листинг 45.8. Приучение владельца станции окон # include <windows.h> #include <stdio.h> int main() { HWINSTA hWindow = NULL; // дескриптор окна DWORD dwLengthOfSd =0; // длина SD SECURITY_DESCRIPTOR *lpSd = NULL; // дескриптор безопасности мьютекса // получаем информацию о владельце окна SECURITY_INFORMATION si = OWNER_SECURITY_INFORMATION; PSID lpOwner; // указатель на SID владельца BOOL bOwnerDefaulted = FALSE; // признак владельца по умолчанию DWORD dwLengthOfUserName =0; // длина имени учетной записи
Глава 45. Управление безопасностью объектов на низком уровне 1041 DWORD dwLengthOfDomainName = 0; // длина имени домена LPTSTR lpUserName = NULL; // указатель на имя домена LPTSTR lpDomainName = NULL; // указатель на имя домена SID_NAME_USE type_of_SID; // тип учетной записи DWORD dwRetCode; // код возврата // получаем дескриптор станции окон hwindow = GetProcessWindowStation(); // определяем длину идентификатора безопасности окна if (!GetUserObjectSecurity( hwindow, // дескриптор станции окна &si, // получаем владельца окна • IpSd, // адрес SD dwLengthOfSd, // определяем длину &dwLengthOfSd)) // требуемая длина буфера { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) // захватываем память под дескриптор безопасности IpSd = (SECURITY_DESCRIPTOR*)new char[dwLengthOfSd]; else { printf("GetUserObjееtSecurity for length failed.\n"); printf("The last error code: %u\n", dwRetCode); return dwRetCode; } } // читаем идентификатор безопасности окна if (!GetUserObjectSecurity( hwindow, // дескриптор станции окна &si, // получаем владельца окна IpSd, // адрес SD
1042 Часть XI. Управление безопасностью в Windows dwLengthOfSd, // определяем длину &dwLengthOfSd)) // требуемая длина буфера { dwRetCode = GetLastError(); printf("GetUserObjectSecurity failed.\n"); printfC'The last error code: %u\n", dwRetCode); return dwRetCode; } // получаем SID владельца окна if (!GetSecurityDescriptorOwner( IpSd, // адрес дескриптора безопасности SlpOwner, // адрес указателя на SID владельца SbOwnerDefaulted)) // признак владельца по умолчанию { dwRetCode = GetLastError(); printf("Get security descriptor owner failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } // проверяем, есть ли владелец if (lpOwner == NULL) { printfC'The owner is not present.\n"); return 0; } if (bOwnerDefaulted) printfC'The owner is defaulted.\n"); // определяем длину имени домена if(!LookupAccountSid( NULL, // ищем на локальном компьютере lpOwner, // указатель на SID
Глава 45. Управление безопасностью объектов на низком уровне 1043 lpUserName, // имя пользователя &dwLengthOfUserName, // длина имени пользователя lpDomainName, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwRetCode = GetLastError(); if (dwRetCode == ERROR_INSUFFICIENT_BUFFER) { // распределяем память под имя пользователи и имя домена lpUserName = (LPTSTR) new char [dwLengthOfUserName] ; lpDomainName = (LPTSTR) new char [dwLengthOfDomainName] ; } else { printf("Lookup account SID for length failed.\n"); printf("Error code: %d\n", dwRetCode); return dwRetCode; } } // определяем имя учетной записи по SID if(!LookupAccountSid( NULL, // ищем на локальном компьютере lpOwner, // указатель на SID пользователя lpUserName, // имя пользователя &dwLengthOfUserName, // длина имени пользователя lpDomainName, // определяем имя домена &dwLengthOfDomainName, // длина имени домена &type_of_SID)) // тип учетной записи { dwRetCode = GetLastError(); printf("Lookup account SID failed.\n"); printf("Error code: %d\n'\ dwRetCode); return dwRetCode;
1044 Часть XL Управление безопасностью в Windows } printf("Key owner name: %зЛп", lpUserName); printf ("Key owner domain name: %s\n", lpDomainName); // освобождаем память de1e te[] lpUs erName; delete[] lpDomainName; delete[] IpSd; return 0; }
Приложение Описание компакт-диска Папка "Листинги" содержит тексты листингов из книги. Папка "Проекты" содержит проекты с программами из папки "Листинги". ^ Примечание Проекты не содержат исполняемых файлов. Проект восстанавливается по файлам с расширениями: срр, h, dsp, dsw.
Предметный указатель АСЕ 669, 683 ACL 669, 683 AddAccessAllowedAce 945 В BDC 675 beginthreadex 45 D, E, P, R DACL 672, 682 endthreadex 47 PDC 673 RID 679 Администратор системы 662 Адрес: виртуальный 359 линейный 359 логический 359 физический 359 Адресат 237 Адресация: ассиметричная 241 косвенйая 241 прямая 241 симметричная 241 Адресное пространство процесса 40 Алгоритм: активного ожидания 104 занимающийся ожиданием Анализ рисков 663 SACL 672, 682 SAM 672 SDDL831 SEH 327 SID 678, 756 SiispendThread 49 Барьер 131 Блокировка: спин 103 Буфер 242 ввода-вы вода 421
1048 Предметный указатель в Ввод данных: кэширование 422 Ввод-вывод: перекрывающийся 499 Взаимное исключение 97 Входы управления доступом 683 г Граф: распределения ресурсов 156 Группа: глобальная 676 локальная 676 первичная 682 специальная 677 учетных записей 676 Д Действие 95 атомарное 95 неделимое 95 непрерывное 95 условное непрерывное 96 Дескриптор: безопасности 671, 682 наследуемый 67 ненаследуемый 67 объекта 24 Диск: жесткий 417 Диспетчер сервиса 609 Домен 673, 674 лес 675 отношение доверия 675 Доступ: субъектов к объектам 661 к данным 417 з Задача 40 условной синхронизации 97, 1 Запись: логическая 420 структура 420 и Идентификатор: безопасности 678 действительный 932 ограничивающий 920 Идентификатор учетной записи: авторизация 678 относительный 679 Имя файла: длинное 423 Инструкция: EXPORTS 588 LIBRARY 588 непрерывная 95 Исключение 327 обработчик 327 к Канал: анонимный 243 именованный 265 передачи данных 237 Каталог 420 таблиц страниц 363 текущий 477 Клиент: анонимного канала 243 именованного канала 265 почтового ящика 307 Когерентность данных 561 Код: охраняемый 327 последней ошибки 53 Команда: непрерывная 95 Консоль 167 буфер экрана 167 входной буфер 167
Предметный указатель 1049 Контекст: действия 95 потока 31 потока в Windows 41 процесса 40 безопасности субъекта 687 Контроль доступа 661 Критическая секция 97 Куча 393 сериализуемая 393 м Магнитный диск: дорожка 417 зона 419 разбиение на разделы 419 раздел 419 сектор 418 форматирование высокого уровня 419 форматирование низкого уровня 419 цилиндр 417 Макрокоманда _endthreadex 47 Маркер: ограниченного доступа 920 Маркер доступа 671 дублирование 927 замещение 692, 929 Матрица управления доступами 665 Менеджер: потоков 38 безопасности 662 Многозадачность вытесняющая 82 Модель: безопасности 663 управления 667 Монитор безопасности 662 Мьютекс 121 забытый 117 н НЖМД 417 Нить 29 О Обмен данными: между параллельными процессами 237 Обмен сообщениями: асинхронный 242 синхронный 242 Обработка исключений финальная 351 Обслуживание потоков: FCFS 38 FIFO 38 циклическое 38 Объект 661 наследуемый 67 ненаследуемый 67 охраняемый в Windows 671 синхронизации 115 Ожидание: активное 103 Операционная система 19 многопользовательская 20 мультипрограммная 20 мультипроцессорная 20 однопользовательская 20 однопрограммная 20 однопроцессорная 20 реального времени 20 Откат 159 Отображение файла 561 Отправитель 237 сообщения 241, 242 п Память: виртуальная 359 виртуальная страничная 360 (окончание рубрики см. на стр. 1048)
1050 Предметный указатель Память (окончание): кэш 421 реальная 360 физическая 359 потока (динамическая локальная) 594 потока (статическая локальная) 603 процесса логическая 359 Передача данных: потоком 239 сообщениями 239 Политика безопасности 662 дискреционная 664 либеральная дискреционная 664 механизм реализации 662 строгая дискреционная 665 Получатель 237 сообщения 242 Пользовательские программы 19 Порт: завершения ввода-вывода 536 ключ завершения 536 Поток 29 безопасное завершение работы 162 в Windows 41 главный 42 интерфейса пользователя 42 контрольная точка 159 параллельные 30 первичный 42 пользовательский 41 потребитель 108 производитель 108 рабочий 42 системный 41 управления 29 Почтовый ящик 307 Права доступа: специфические 684 стандартные 684 эффективные 875 Правила управления доступом 663 Право 662 Привилегия 662, 687 Приложение 20 консольное 167 Примитив синхронизации 104 семафор 107 условие 105 Приоритет потока: базовый 85 основной 85 уровень 86 Программа: многопоточная 30 однопоточная 30 реентерабельная 33 Протокол 241 Профиль пользователя 669 Процедура: асинхронная 485 завершения ввода-вывода 528 Процесс 40 в Windows 58 восстановление 158 дочерний 59 рабочее множество страниц 363 реального времени 83 родительский 59 с высоким приоритетом 83 с нормальным приоритетом 83 фоновый 83 Псевдодескри птор потока 52 процесса 81 текущего процесса 81 Пул памяти 393 р Разрешения 684 Рандеву 242 Раскрутка стека глобальная 344 Режим: доступа к объекту 666 управления объектом 666
Предметный указатель 1051 Ресурс: контроль доступа 661 монопольный 154 ^перераспределяемый 155 объекты 661 перераспределяемый 155 повторно используемый 155 потребляемый 155 разделяемый 98 совместно используемый 98, 154 субъекты 661 компьютера 19 Ресурсы: аппаратные 19 информационные 19 логические 19 системные 19 физические 19 Риск: анализ 663 управление 663 с Связь: дуплексная 239 полудуплексная 239 Сегмент: виртуальной памяти 362 Семафор: бинарный 108 сильный 108 слабый 108 считающий 108 Сервер: анонимного канала 243 именованного канала 265 почтового ящика 307 Сервис 607 Синхронизация: потоков 96 процессов 96 условная 97 Системные ресурсы 19 Событие 97, 128 Сообщение 239 Состояние: безопасное 663 настороженное состояние потока 486 несигнальное 115 подвешенное состояние потока 36 потока 34 поток блокирован 34 поток готов 34 поток исполняется 34 поток спит 36 приостановленное состояние потока 36 программы 34 процессора 34 сигнальное 115 системы безопасности 663 Список: управления дискреционным доступом 672 управления доступом 669, 672 Стандартный ввод-вывод перенаправление 258 Страница: виртуальной памяти 360 Структура 420 Субъект 661 возможности 669 т Таймер: ожидающий 544 ожидающий непериодический 545 ожидающий периодический 545 синхронизации 544 Топология связи 239 Транзакция 159 Тупик 153
1052 Предметный указатель у Узел: графа 158 Управление: рисками 663 файлами 420 Учетная запись: администратора 674 гостя 674 домена 673 компьютера 673 пользователя 672 системы 674 Ф Файл 420 вид 561 подкачки 360 представление 561 путь 423 страниц 360 указатель 420 Фрейм 327 стека 344 Функции: _controlfp 342 _set_se_translator 348 AbnormalTermination 353 AddAccessAllowedAceEx 952 AddAccessDeniedAce 946 AddAccessDeniedAceEx 953 AddAce971 AddAuditAccessAce 960 AddAuditAccessAceEx 970 AdjustTokenGroups 918 AdjustTokenPrivileges 917 AllocateAndlnitializeSid 761 AllocateLocallyUniqueld 887 AllocConsole 172 BuildExplicitAccessWithName 849 BuildTrusteeWithName 842 BuildTrusteeWithSid 844 CallNamedPipe 293 Cancello 522 CancelWaitableTimer 549 ChangeServiceConfig 637 CharToOem 55 CheckTokenMembership 932 ConnectNamedPipe 268 ControlService 646 ConvertSecurityDescriptoiToString SecurityDescriptor 835 ConvertSidToStringSid 782 ConvertStringSecurityDescriptorTo SecurityDescriptor 839 CopyFile 435 CopyFileEx 437 CopyMemory 383 CopySid 777 CreateConsoleScreen Buffer 188 CreateDirectory 468 CreateDirectoryEx 469 CreateFile 270, 424 CreateFileMapping 563 CreateloCompletionPort 537 CreateMailslot 308 CreateNamedPipe 266 CreatePipe 244 Create Process 58 CreateRestrictedToken 921 CreateService 621 CreateThread 42 CreateWaitableTimer 545, 546 DeleteAce 977 DeleteFile 427 DeleteService 649 DisconnectNamedPipe 268 DllMain 579 DuplicateHandle 75 DuplicateToken 927 EqualPrefixSid 778 EqualSid 778 ExitProcess 64 ExitThread 47 FileTimeToSystemTime 461 FillConsoleOutputAttribute 197 FillCoiisoleOutputCharacter 219
Предметный указатель 1053 FillMemory 383 FindClose 472 FindFirstChangeNotification 479 FindFirstFile 470 FindFirstFileEx 473 FindFirstFreeAce 986 FindNextChangeNotification 480 FindNextFile471 FlushConsolelnputBuffer 212 FlushFileBuffers 430 FlushViewOfFile 573 FormatMessage 53 FreeConsole 177 FreeLibrary 583 FreeLibraryAndExitThread 583 FreeSid 762 GetAce 972 GetAclInformation 981 GetAuditedPemiissionsFromAcl 878 GetBinaryType 466 GetConsoleCursorlnfo 194 GetConsoleMode 226 GetConsoleScreenBufferlnfo 191 GetConsoleTitle 181 GetConsoleWindow 180 GetCurrentDirectory 477 GetCurrentProcess 81 GetCurrentThread 52 GetEffectiveRightsFromAcl 875 GetExceptionCode 330 Get Exception Information 334 GetExplicitEntriesFromAcl 870 GetFileAttributes 446 GetFilelnformationByHandle 459 GetFileSecurity 1008 GetFileSize 449 GetFileSizeEx451 GetFileType 464 GetHandlelnformation 74 GetKernelObjectSecurity 1019 GetLargestConsoleWindowSize 183 GetLastError 53 GetLengthSid 773 GetMailslotlnfo 315 GetNamedPipeHandleState 295 GetNamedPipelnfo 303 GetNamedSecuritylnfo 802 GetNumberOfConsolelnput Events 212 GetNumberOfConsoleMouse Button 214 GetOverlappedResult 518 GetPriorityClass 84 GetProcAddress 584 GetProcessHeap 393 GetProcessPriorityBoost 89 GetProcessWorkingSetSize 380 GetQueuedCompletionStatus 538 GetSecurityDescriptorControl 815 GetSecurityDescriptorDacl 997 GetSecurityDescriptorGroup 810,993 GetSecurityDescriptorLength 797 GetSecurityDescriptorOwner 810,988 GetSecurityDescriptorSacl 1005 GetSecuritylnfo 806 GetServiceDisplayName 644 GetServiceKeyName 641 GetSidldentifierAuthority 774 GetSidLengthRequired 758 GetSidSubAuthority 759 GetSidSubauthorityCount 774 GetStdHandle 178 GetThreadPriority 87 GetThreadPriorityBoost 90 GetTokenlnformation 900 GetTrusteeForm 871 GetTrusteeName 872 GetTrusteeType 871 GetUserObjectSecurity 1039 HeapAlloc 395 HeapCompact 411 HeapCreate 394 HeapDestroy 394 HeapFree 395 HeapLock 403 (продолжение рубрики см. на стр. 1052)
1054 Предметный указатель Функции (продолжение): HeapReAlloc 401 HeapUnlock 403 Heap Validate 406 HeapWalk410 InitializeAcl 943 InitializeSecurityDesriptor 792 InitializeSid 758 InterlockedCompare Exchange 146 InterlockedDecrement 148 InterlockedExchange 144 InterlockedExchangeAdd 150 Interlockedlncrement 148 IsTokenRestricted 923 IsValidAcl 944 IsValidSecurityDescriptor 794 IsValidSid 759 LoadLibrary 582 LoadLibraryEx 582 LockFile 455 LockFileEx 511 LockServiceDatabase 653 LookupAccountName 769 LookupAccountSid 764 LookupPrivilegeDisplayName 891 LookupPrivilegeName 888 Lookup Privilege Value 888 MakeAbsoluteSD 790 MakeSelfRelatives D 789 MapViewOfFile 564 MapViewOfFileEx 565 MoveFile 437, 476 MoveMemory 384 NetApiBufferFree 705 NetLocalGroupAdd 724 NetLocalGroupAddMembers 736 NetLocalGroupDel 754 NetLocalGroupDelMembers 748 NetLocalGroupEnum 729 NetLocalGroupGetlnfo 727 NetLocalGroupGetMembers 745 NetLocalGroupSetlnfo 732 NetLocalGroupSetMembers 742 NetUserAdd 699 NetUserChangePassword 719 NetUserDel 721 NetUserEnum 706 NetUserGetGroups 710 NetUserGetlnfo 704 NetUserGetLocalGroups 712 NetUserSetlnfo 715 OpenProcessToken 894 OpenSCManager 620 OpenService 627 OpenThreadToken 896 OpenWaitableTimer 552 PeekNamedPipe 285 PostQueuedCompletionStatus 539 QueryServiceConfig 634 QueryServiceLockStatus 653 QueryServiceObjectSecurity 1026 QueryServiceStatus 630 QueueUserAPC 486 RaiseException 337 ReadConsole 203 ReadConsolelnput 207, 208 ReadConsoleOutput 221 ReadConsoleOutputAttribute 201 ReadConsoleOutputCharacter 215 ReadFile 433, 506 ReadFileEx 532 ReadProcessMemory 388 RegGetKeySecurity 1033 RegisterServiceCtrlHandler 612 RegisterServiceCtrlHandlerEx 612 RegSetKeySecurity 1032 RemoveDdirectory 473 ReplaceFile 438 ResumeThread 49 ScrollConsoleScreenBuffer 229 SetAclInformation 985 SetConsoleActiveScreenBuffer 189 SetConsoleCursorlnfo 194 SetConsoleMode 225 SetConsoleScreenBufferSize 193 SetConsoleTextAttributes 199 SetConsoleTitle 182 SetConsoleWindowInfo 184
Предметный указатель 1055 SetCurrentDirectory 478 SetEndOfFile 453 SetEntriesInAcl 850 SetFileAttributes 447 SetFilePointer 440 SetFilePointerEx 443 SetFileSecurity 1007 SetHandlelnformation 71 SetKernelObjectSecurity 1018 SetLastError 53 SetMailslotlnfo 321 SetNamedPipeHandleState 299 SetNamedSecuritylnfo 818 SetPriorityClass 84 SetProcessPriorityBoost 89 SetProcessWorkingSetSize 381 SetSecurityDescriptorControl 827 SetSecurityDescriptorDacl 997 SetSecurityDescriptorGroup 793, 992 SetSecurityDescriptorOwner 792, 988 SetSecurityDescriptorSacl 1004 SetSecuritylnfo 823 SetServiceObjectSecurity 1025 SetServiceStatus 612 SetStdHandle 178 SetThreadPriority 87 SetThreadPriorityBoost 90 SetThreadToken 929 SetTokenlnformation 908 SetUnhandledExceptionFilter 340 SetUserObjectSecurity 1038 SignalObjectAndWait 494 Sleep 50 SleepEx 487 StartService 627 StartServiceCtrlDispatcher 610 TerminateProcess 65 TerminateThread 47 TlsAlloc 595 TlsFree 595 TlsGetValue 596 TlsSetValue 595 TransactNamedPipe 289 UnhandledExceptionFilter 340 UnlockFile 456 UnlockFileEx512 UnmapViewOfFile 566 VirtualAlloc 368 VirtualLock 376 Virtual Protect 378 VirtualQuery 385 VirtualUnlock 377 WaitForMultipleObjectsEx 493 WaitForSingleObjectEx 489 WaitNamedPipe 269 WriteConsole 204 WriteConsolelnput 211 WriteConsoleOutput 223 WriteConsoleOutputAttribute 200 WriteConsoleOutputCharacter 217 WriteFile 246, 247, 428, 500 WriteFileEx 529 WriteProcessMemory 389 ZeroMemory 61, 383 безопасная для потоков 32, 33 блокирующие 143 импортируемая 584 ожидания 116 обратного вызова 43 повторно входимая 32 реентерабельная 32 транслятор 348 экспортируемая 580 я Язык системного программирования 104
Объем 1034 страницы
2006 год
Подарите скидку 10%
Посоветуйте эту книгу и получите 27,11 ₽ с покупки её другом.
О книге
Подробно рассматриваются вопросы системного программирования с использованием интерфейса Win32 API. Описываются управление потоками и процессами, включая их диспетчеризацию; синхронизация потоков; передача данных между процессами, с использованием анонимных и именованных каналов, а также почтовых ящиков; структурная обработка исключений; управление виртуальной памятью; управление файлами и каталогами; асинхронная обработка данных; создание динамически подключаемых библиотек; разработка сервисов. Отдельная часть книги посвящена управлению безопасностью объектов в Windows. Каждая тема снабжена практическими примерами использования функций Win32 API, которые представлены работающими листингами. Это позволяет использовать книгу в качестве пособия по системному программированию или справочника для системного программиста.Для программистов.(Компакт-диск прилагается только к печатному изданию.)
Описание книги
Подробно рассматриваются вопросы системного программирования с использованием интерфейса Win32 API. Описываются управление потоками и процессами, включая их диспетчеризацию; синхронизация потоков; передача данных между процессами, с использованием анонимных и именованных каналов, а также почтовых ящиков; структурная обработка исключений; управление виртуальной памятью; управление файлами и каталогами; асинхронная обработка данных; создание динамически подключаемых библиотек; разработка сервисов. Отдельная часть книги посвящена управлению безопасностью объектов в Windows. Каждая тема снабжена практическими примерами использования функций Win32 API, которые представлены работающими листингами. Это позволяет использовать книгу в качестве пособия по системному программированию или справочника для системного программиста.
Для программистов.
(Компакт-диск прилагается только к печатному изданию.)
Книга Александра Побегайло «Системное программирование в Windows» — скачать в pdf или читать онлайн. Оставляйте комментарии и отзывы, голосуйте за понравившиеся.
Возрастное ограничение:
12+
Дата выхода на Литрес:
16 июля 2014 г.
Последнее обновление:
2006
Общее кол-во страниц:
1034
Правообладатель:
БХВ-Петербург
С этой книгой читают
4,6
16
4,1
8
4,3
3
4,3
26
3,8
13
4,1
53
4,6
5340
4,4
19
4,0
84
4,4
12
Другие книги автора
БХВ-Петербург, 2006 — Всего страниц: 1056
Подробно рассматриваются вопросы системного программирования с использованием интерфейса Win32 API. Описываются управление потоками и процессами, включая их диспетчеризацию; синхронизация потоков; передача данных между процессами, с использованием анонимных и именованных каналов, а также почтовых ящиков; структурная обработка исключений; управление виртуальной памятью; управление файлами и каталогами; асинхронная обработка данных; создание динамически подключаемых библиотек; разработка сервисов. Отдельная часть книги посвящена управлению безопасностью объектов в Windows. Каждая тема снабжена практическими примерами использования функций Win32 API, которые представлены работающими листингами. Это позволяет использовать книгу в качестве пособия по системному программированию или справочника для системного программиста. Прилагаемый компактдиск содержит листинги и проекты всех программ, рассмотренных в книге. Файлы для книги можно скачать по ссылке ftp://ftp.bhv.ru/5941577923.zip
О книге «Системное программирование в Windows»
Подробно рассматриваются вопросы системного программирования с использованием интерфейса Win32 API. Описываются управление потоками и процессами, включая их диспетчеризацию; синхронизация потоков; передача данных между процессами, с использованием анонимных и именованных каналов, а также почтовых ящиков; структурная обработка исключений; управление виртуальной памятью; управление файлами и каталогами; асинхронная обработка данных; создание динамически подключаемых библиотек; разработка сервисов. Отдельная часть книги посвящена управлению безопасностью объектов в Windows. Каждая тема снабжена практическими примерами использования функций Win32 API, которые представлены работающими листингами. Это позволяет использовать книгу в качестве пособия по системному программированию или справочника для системного программиста. Для программистов. (Компакт-диск прилагается только к печатному изданию.)
Произведение было опубликовано в 2006 году издательством БХВ-Петербург. Книга входит в серию «В подлиннике. Наиболее полное руководство». На нашем сайте можно скачать книгу «Системное программирование в Windows» в формате pdf или читать онлайн. Здесь так же можно перед прочтением обратиться к отзывам читателей, уже знакомых с книгой, и узнать их мнение. В интернет-магазине нашего партнера вы можете купить и прочитать книгу в бумажном варианте.
686 ₽
583 ₽
|
- Описание
- Детали
- Отзывы (0)
Описание
Подробно рассматриваются вопросы системного программирования с использованием интерфейса Win32 API. Описываются управление потоками и процессами, включая их диспетчеризацию; синхронизация потоков; передача данных между процессами, с использованием анонимных и именованных каналов, а также почтовых ящиков; структурная обработка исключений; управление виртуальной памятью; управление файлами и каталогами; асинхронная обработка данных; создание динамически подключаемых библиотек; разработка сервисов. Отдельная часть книги посвящена управлению безопасностью объектов в Windows. Каждая тема снабжена практическими примерами использования функций Win32 API, которые представлены работающими листингами. Это позволяет использовать книгу в качестве пособия по системному программированию или справочника для системного программиста. Прилагаемый компактдиск содержит листинги и проекты всех программ, рассмотренных в книге….
Детали
Артикул | 956 |
---|---|
ISBN | 5-94157-792-3 |
Количество страниц | 1056 |
Серия | В подлиннике |
Переплет | Твердый переплет |
Печать | Черно-белая |
Год | 2006 |
Габариты, мм | 240 × 170 × 49 |
Вес, кг | 1.183 |
Дополнительные файлы скачать: Зеркало1
Дополнительные файлы скачать (Chrome): Зеркало2
- ✓ Новинки на 2 недели раньше магазинов
- ✓ Цены от издательства ниже до 30%
- ✓ Акции и скидки только для подписчиков
- ✓ Важные новости БХВ
Рекомендуем также
-
Колисниченко Денис Николаевич
Самоучитель Microsoft Windows 8
264 ₽
132 ₽ -
Колисниченко Денис Николаевич
Самоучитель Microsoft Windows 10.
465 ₽
395 ₽ -
Колисниченко Денис Николаевич
Microsoft Windows 10. Первое знакомство.
264 ₽
132 ₽