    УСТРОЙСТВА: ввод/вывод 

    глава 15 

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

    Устройство              Типичное применение 

    Файл                   Устойчивое хранилище произвольных данных. 
    Каталог                 Определение атрибутов и сжатие файлов. 
    Логический диск         Форматирование. 
    Физический диск         Доступ к таблице разделов диска. 
    Последовательный порт   Передача данных по телефонной линии. 
    Параллельный порт      Передача данных принтеру. 
    Почтовый ящик         Широковещательная передача данных «от одного ко 
многим» (обычно по сети между компьютерами под управлением Windows). 
    Именованный канал      Передача данных «от одного к одному» (обычно по 
сети между компьютерами под управлением Windows). 
    Анонимный канал        Передача данных «от одного к одному» (по сети не осуществляется). 
    Гнездо (socket)           Передача данных дейтаграммами или потоком 
(обычно по сети). Приемником может быть любая машина, поддерживающая механизм 
гнезд, — и необязательно под управлением Windows. 
    Консоль                Экранный буфер текстового окна. 

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

    467 

    468 ______________________________Windows для профессионалов | 


    Открытие и закрытие устройств 

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

    Устройство                 Функция, применяемая для его открытия 

    Файл                       CreateFile (pszName — полный или UNC-путь)      
    Каталог                    CreateFile (pszName — имя или UNC-имя каталога 
при флаге FILE_FLAG_BACKUP_SEMANTICS) 
    Логический диск             CreateFile (pszName имеет вид «\\.\х:») 
    Физический диск            CreateFile (pszName имеет вид «\\.\PHYSICALDRIVEx») 
    Последовательный порт      CreateFile (pszName имеет вид «СОМ?») 
    Параллельный порт          CreateFile (pszName имеет вид «LPT?») 
    Почтовый сервер             CreateMailslot (pszName имеет вид «\\.\mailslot\maibtof«affie») 
    Почтовый клиент            CreateMailslot (pszName имеет вид <t\\servername\mauslot\mailslotname») 
    Сервер именованного канала CreateNamedPipe (pszName имеет вид «\\.\pipe\pipename») 
    Клиент именованного канала CreateFile (pszName имеет вид «\\servername\pipe\pipename») 
    Клиент и сервер             CreatePipe анонимного канала 
    Гнездо                        socket, accept, AcceptEx 
    Консоль                       CreateConsoleScreenBuffer, GetStdHandle 

    1. Windows NT позволяет открыть каталог, если при вызове CreateFile 
установлен флаг FILE_FLAG_BACKUP_SEMANTICS. Тогда Вы сможете изменить атрибуты 
каталога (обычный, скрытый и т. п.), а также его временные метки. Windows 95 
не дает открывать каталоги. 
    2. Windows NT позволяет открыть логический диск, если задана строка вида 
«\\.\ж», где х— буква диска. Например, чтобы   ^ открыть диск А, укажите 
«\\.\А:». Открыв диск. Вы сможете отформатировать его или определить емкость 
носителя.   

| Windows 95 не дает открывать логические диски.                   
                                               | 
    3. Windows NT позволяет открыть физический диск, если задана строка вида 
«\\.\PHYSICALDRIVEx», где х — номер фи-   | зического дискового устройства. 
Например, для чтения или записи в физические секторы на первом физическом 
жестком    : диске, укажите «\\.\PHYSICALDRIVEO». Открыв физический диск, Вы 
получаете прямой доступ к таблице разделов жесткого диска. Но открытие 
физического диска — вещь опасная, так как некорректная запись может сделать 
содержимое диска недоступным файловой системе. Windows 95 не дает открывать 
физические диски. 
    Например, чтобы установить скорость передачи для последовательного порта, 
вызовите SetCommConfig. 

    BOOL SetCommConfigCHANOLE hCommDev, LPCOMMCONFIG IpCC, DWORD dwSize); 

    А чтобы задать тайм-аут при чтении данных, используйте: 

    BOOL SetMailslotInfo(HANDLE hMailslot, DWORD IReadTimeout); 

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

    BOOL CloseHandle(HANDLE hObject); 

    Но если устройство является гнездом, вместо нее следует вызвать closesocket. 

    int closesocket(SOCKET s); 


    ________________________Глава 15. Устройства: ввод/вывод 469 


    Если у Вас есть описатель какого-либо устройства, Вы можете определить его 
тип, вызвав GetFileType: 

    DWORD GetFileType(HANDLE hFile); 

    Она возвращает одно из значений, перечисленных в таблице: 

    Флаг                    Описание 

    FILE_TYPE_UNKNOWN      Неизвестный тип файла. 
    FILE_TYPE_DISK           Дисковый файл. 
    FILE_TYPE_CHAR          Символьный файл (обычно LPT или консоль). 
    FILE_TYPE_PIPE            Именованный или анонимный канал. 

    Функция CrecrfeFi/e: подробности 
    Вы уже, очевидно, заметили, что большинство устройств открывается с 
помощью функции CreateFile: 

    HANDLE CreateFile(LPGTSTR pszName, DWORD dwDesiredAccess, 
    DWORD dwShareMode, LPSECURITY_ATTRIBUTES pSecurityAttributes, 
    DWORD dwCreationDistribution, DWORD dwFlagsAndAttrs, 
    HANDLE hTemplateFile); 

    Пусть Вас не обманывает ее имя. Эта функция не только создает и открывает 
дисковые файлы, но и открывает множество других устройств. 
    В 16-разрядной Windows файлы создаются и открываются функциями OpenFile, 
lcreatvi _lopen. Для совместимости они перенесены и в Win32, но Вы старайтесь 
избегать их. В Win32-пpилoжeнияx файлы лучше создавать и открывать, используя 
гораздо более мощную функцию CreateFile. 
    Параметр pszName идентифицирует одновременно и тип, и определенный 
экземпляр устройства. Синтаксис описания различных устройств, распознаваемый 
функцией CreateFile, приведен в следующей таблице. 
    Открывая файл, Вы можете передать полный путь к нему, длина которого не 
должна превышать _МАХ_РАТН символов (260). Однако в Windows NT это ограничение 
можно преодолеть, обратившись к CreateFileW (Unicode-версии CreateFile) и 
указав перед путем такой префикс: «\\?\». Если Вы использовали этот префикс, 
CreateFileW удалит его и разрешит передать путь, содержащий приблизительно до 
32000 Unicode-символов. Но, указав упомянутый префикс, Вы должны задать именно 
полный, а не относительный путь. 
    Параметр dwDesiredAccess определяет способ передачи данных. Он принимает 
одно из четырех значений: 

    Значение                          Описание 

    О                                    Запись или чтение данных с устройства 
не предполагается. Это значение следует использовать, если Вы хотите лишь 
изменить параметры устройства (например, временную метку файла). 
    GENERIC_READ                     Устройство доступно только для чтения. 

    см. след. стр. 


    470 ______________________________Windows для профессионалов 


    продолжение 

    Значение                          Описание 

    GENERIC_WRITE                     Устройство доступно только для записи. Заметьте: 
    установка GENERIC_WRITE не подразумевает автоматической установки 
GENERIC_READ. Это значение используется при передаче данных на принтер, а 
также программами резервного копирования. 
    GENERIC_READ ! GENERIC_WRITE    Устройство доступно как для чтения, так и 
для записи. Это значение встречается чаще всего, поскольку обеспечивает 
свободный обмен данными. 

    Параметр dwShareMode определяет режим совместного использования 
устройства. В отличие от 16-разрядной Windows, в Windows 95 и Windows NT 
типична ситуация, когда к какому-то устройству одновременно обращаются 
несколько компьютеров (в сетевой среде) или процессов (в многопоточной среде). 
А значит. Вы должны решить, надо ли ограничивать доступ к устройству со 
стороны других компьютеров или процессов и, если надо, то — как. Вот 
допустимые значения параметра dwShareMode: 

    Значение                             Описание 

    О                                      Монопольный доступ к устройству. 
Если оно уже открыто, вызов CreateFile даст ошибку. 
    FILE_SHARE_READ                      Устройство доступно только для 
чтения. Если оно 
    уже открыто для записи, вызов CreateFile даст ошибку. Кроме того, если Вы 
открываете устройство для чтения, имейте в виду, что последующий вызов 
CreateFile с требованием предоставить доступ для записи, закончится неудачей. 
    FILE_SHARE_WRITE                     Устройство доступно только для 
записи. Если оно уже открыто на чтение, вызов CreateFile даст ошибку. Кроме 
того, если Вы открываете устройство для записи, имейте в виду, что последующий 
вызов CreateFile с требованием предоставить доступ для чтения, закончится 
неудачей. 
    FILE_SHARE_READ ! FILE_SHARE_WRITE   Устройство доступно как для чтения, 
так и для записи. Если устройство уже открыто в монопольном режиме, вызов 
CreateFile даст ошибку. Повторный вызов CreateFile с требованием предоставить 
доступ для чтения или записи завершится успешно. 
    Четвертый параметр, pSecurityAttributes, указывает на структуру 
SECURITY_ATTRIBUTES, которая позволяет задать атрибуты защиты для связанного с 
устройством объекта ядра и определить, будет ли возращенный описатель 
наследуемым (т. е. получит ли дочерний процесс доступ к устройству). Если Вас 
устраивают атрибуты защиты по умолчанию и Вам не нужен наследуемый описатель, 
передайте NULL вместо этого параметра. 
    Параметр dwCreationDistribution важен, когда CreateFile вызывается для 
открытия именно файла, а не устройства какого-то другого типа. Его допустимые значения: 

    ___________________________Глава 15. Устройства: ввод/вывод 471 


    Идентификатор         Описание 

    CREATE_NEW           Заставляет CreateFile создать новый файл. Если файл с 
указанным именем уже есть, функция даст ошибку. 
    CREATE_ALWAYS        Заставляет CreateFile создать файл независимо от 
того, имеется ли файл с таким именем. Если есть, функция замещает его новым. 
    OPEN_EXISTING         Заставляет CreateFile открыть существующий файл, а 
если его нет, сообщить об ошибке, 
    OPEN_ALWAYS           Заставляет CreateFile открыть существующий файл, а 
если его нет, создать новый. 
    TRUNCATE_EXISTING    Заставляет CreateFile открыть существующий файл, 
обрезав его длину до 0. Если такого файла нет, функция даст ошибку. Этот флаг 
можно использовать только вместе с флагом GENERIC_WRITE. 
    Если Вы открываете с помощью CreateFile устройство, отличное от файла, то 
в параметре dwCreationDistribution передайте OPEN_EXISTING. 
    Параметр dwFlagsAndAttrs служит двум целям: позволяет задавать флаги, 
тонко настраивающие взаимодействие с устройством, а если устройство является 
файлом, то и устанавливать атрибуты. Рассмотрим сначала флаги, потом — 
файловые атрибуты. 
    Большая часть таких флагов сообщает системе, как Вы будете работать с 
устройством. После этого сист^маоптимизирует алгоритмы кэширования, 
обеспечивая более эффективное" выполнение программы. 
    Флаг FILE_FLAG_NO_BUFFERING влияет на ввод/вывод в файловых системах. 
Чтобы повысить производительность, система каптирует данные, записываемые или 
считываемые с дисков. Обычно этот флаг не устанавливается, и диспетчер кэша 
сохраняет в памяти участки, к которым Вы обращались в последний раз. Если Вы 
считали несколько байтов из файла и хотите считать новую порцию, то с большой 
долей вероятности эти данные уже находятся в памяти, и поэтому требуется лишь 
одно обращение к диску вместо двух (что увеличивает производительность 
системы). Но это означает, что часть файловых данных присутствует в памяти в 
двух экземплярах: у диспетчера кэша есть собственный буфер, а Вы, вызвав, 
допустим, функцию ReadFile, копируете эти данные из его буфера в свой. 
    Кроме буферизации, диспетчер кэша способен осуществлять опережающее 
чтение, благодаря чему следующая порция данных, которую Вы, по всей видимости, 
захотите загрузить, уже присутствует в памяти. И вновь скорость работы системы 
возрастает за счет того, что из файла считывается больше данных, чем нужно на 
этот момент. Но, если Вы больше не станете считывать данные из этого файла, 
получится, что память расходуется впустую. (Подробнее об опережающем чтении я 
расскажу, когда мы дойдем до флагов FILE_FLAG_SEQUENTIAL_SCAN и FILE_FLAG_RANDOM_ACCESS.) 
    Установив флаг FILE_FLAG_NO_BUFFERING, Вы заставляете диспетчер кэша 
отказаться от буферизации каких-либо данных и всю ответственность за это 
берете на себя. В зависимости от того, что именно Вы делаете, этот флаг может 
повысить скорость работы Вашего приложения и оптимизировать использование 
памяти. Поскольку драйвер устройства файловой системы записывает данные прямо 
в буфер, предоставляемый Вами, надо соблюдать такие правила: 
    • всегда указывать при доступе к файлу смещения, кратные размеру сектора 
дискового тома'; 
    • считывать и записывать данные блоками, кратными размеру сектора 
дискового тома; 

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


    472 ______________________________Windows для профессионалов 

    • буфер в адресном пространстве Вашего процесса должен начинаться с 
адреса, значение которого делится на размер сектора без остатка. 
    Следующие два флага — FILE_FLAG_SEQUENTIAL_SCAN и FILE_FLAG_RANDOM_ACCESS 
— имеют смысл, только если система буферизует данные. Когда установлен режим 
FILE_FLAG_NO_BUFFERING, оба эти флага игнорируются. 
    Если Вы задали FILE_FLAG_SEQUENTIAL_SCAN, система считает, что Вы 
обращаетесь к файлу последовательно. Считывая порцию данных из файла, система 
на деле считывает информации больше, чем запрошено. Это сокращает число 
обращений к жесткому диску и повышает скорость работы Вашей программы. Если же 
Вы считываете данные из файла в произвольном порядке, система затрачивает 
несколько больше времени и памяти на кэширование данных, к которым Вы не 
обращались. Это нормально, но, если Вы часто выполняете подобные действия, 
лучше установите флаг FILE_FEAG_RANDOM_ACCESS. Он сообщает системе, что 
опережающее чтение данных не требуется. ~~    Последний флаг, связанный с 
управлением кэшем, — FILE_FLAG_WRITE_THROUGH — отключает промежуточное 
кэширование операций записи в файл, сокращая вероятность потери данных. Когда 
Вы устанавливаете этот флаг, система сразу же записывает на диск все 
изменения, вносимые в файл. Однако система по-прежнему поддерживает внутренний 
кэш для файловых данных, и в операциях чтения используются данные, помещенные 
в него. Если этот флаг указан при открытии файла на сетевом сервере, 
\Ут32-функции записи не возвращают управление вызвавшему их потоку, пока 
данные не будут записаны на диск сервера. 
    Вот и все, что касается флагов, связанных с буферизацией. Остальные 
значения параметра dwFlagsAndAttrs функции CreateFile классификации не поддаются. 
    Чтобы система удалила файл после его закрытия, укажите флаг 
FILE_FLAG_DELETE_ON_CLOSE. Обычно он применяется в сочетании с атрибутом 
FILE_ATTRIBUTE_TEMPORARY — тогда Ваша программа сможет создать временный файл, 
что-то записать в него, прочесть и, наконец, закрыть, а система сама удалит 
временный файл — очень удобно! Если Ваш процесс закроет описатель файла, 
который пока еще открыт каким-то другим процессом, система удалит этот файл не 
сразу, а дождется закрытия всех его описателей. 
    Флаг FILE_FLAG_BACKUP_SEMANTICS используют в программах резервного 
копирования и восстановления. Прежде чем открыть или создать какой-либо файл, 
система обычно проверяет атрибуты его защиты и разрешает доступ к нему только 
процессу с соответствующими правами. Но резервное копирование трактуется 
особым образом, и в этом случае можно подавить некоторые виды проверки при 
доступе к файлу. Флаг FILE_FLAG_BACKUP_SEMANTICS заставляет систему 
контролировать права доступа у процесса и, если они достаточны, позволяет 
открыть файл исключительно для резервного копирования или восстановления. С 
помощью этого флага можно также открыть описатель каталога. 

    VWINDOWSY 
    Windows 95 не поддерживает флаг FILE_FLAG_BACKUP_SEMANTICS. 

    Флаг FILE_FLAG_POSIX_SEMANTICS сообщает, что при доступе к файлу следует 
применять правила POSIX. Файловые системы, используемые POSIX, чувствительны к 
регистру букв в именах файлов, и, например, JEFFREY. DOC, Jeffrey.Doc и 
Jeffrey.doc считаются тремя разными файлами. В то же время MS-DOS, 
16-разрядная Windows и Win32 к регистру букв в именах файлов нечувствительны. 
Поэтому будьте крайне осторожны, используя FILE_FLAG_POSIX_SEMANTICS. Файл, 
при создании которого установлен этот флаг, может оказаться недоступным из 
приложений MS-DOS, 16-разрядной Windows и Win32. 
    Последний флаг, FILE_FLAG_OVERLAPPED, указывает, что Вы хотите получить 
асинхронный доступ к файлу. По умолчанию (в отсутствие флага FILE_FLAG_OVERLAP

    ________________________Глава 15. Устройства: ввод/вывод 473 

    PED) устройства открываются для синхронного ввода/вывода, к которому 
привыкли многие разработчики программ для MS-DOS и 16-разрядной Windows. Когда 
Вы читаете данные из файла, Ваша программа приостанавливается до завершения 
этой операции. Потом она вновь получит управление и продолжит работу. 
    Поскольку ввод/вывод на устройствах — операция медленная (по сравнению с 
большинством других), Вам может понадобиться асинхронная связь. В общих чертах 
это выглядит так: Вы вызываете функцию чтения или записи, а она, не дожидаясь 
завершения операции ввода/вывода, тут же возвращает управление — выполнение 
запрошенной операции система берет на себя, используя собственные потоки. 
Закончив, операционная система уведомляет об этом Вашу программу. (Асинхронный 
ввод/вывод особенно полезен для серверных приложений.) Win32 допускает 
несколько способов организации асинхронного ввода/ вывода, но мы рассмотрим их 
чуть позже. 

    WINDOWS 95 
    Windows 95, поддерживая асинхронный ввод/вывод для ряда устройств 
(например, последовательных портов), не поддерживает его применительно к файлам. 

    А теперь перейдем к флагам файловых атрибутов. Они полностью игнорируются 
во всех случаях, кроме одного, — когда Вы создаете новый файл и передаете 
функции CreateFile вместо параметра hTemplateFile значение NULL. Большая часть 
атрибутов должна быть уже знакома Вам: 

    Идентификатор                  Описание 

    FILE_ATTRIBUTE_ARCHIVE        Архивный файл (допускает удаление и 
резервное копирование). Когда CreateFile создает файл, этот атрибут 
устанавливается автоматически. 
    FILE_ATTRIBUTE_HIDDEN         Скрытый файл (не включается в обычный список 
файлов каталога). 
    FILE_ATTRIBUTE_NORMAL        Файл, у которого не установлены другие 
атрибуты. Этот атрибут допустим, только если не используются другие атрибуты. 
    FILE_ATTRIBUTE_READONLY      Файл только для чтения. Приложения могут 
читать файл, но не имеют права записывать в него или удалять его. 
    FILE_ATTRIBUTE_SYSTEM         Файл — часть операционной системы или 
используется исключительно операционной системой. 
    FILE_ATTRIBUTE_COMPRESSED    Файл или каталог сжаты. Если это файл, все 
данные в нем сжаты. Для каталога данный атрибут означает, что новые файлы или 
подкаталоги по умолчанию создаются как сжатые. 
    FILE_ATTRIBUTE_OFFLINE        Файл существует, но данные перемещены в 
хранилище, недоступное в оперативном режиме. Этот флаг применяется в системах 
с иерархической организацией физической памяти. 
    FILE_ATTRIBUTE_TEMPORARY     Файл предполагается использовать 
непродолжительное время. Файловая система стремится поместить его в 
оперативную память, чтобы максимально сократить время доступа к нему. 
    Флаг FILE_ATTRIBUTE_TEMPORARY применяется при создании временных файлов. 
Создавая файл с этим атрибутом, CreateFile пытается разместить его данные не 
на диске, а в оперативной памяти, что ускоряет доступ. Если Вы продолжите 
запись в файл, то в какойто момент система уже не сможет держать его целиком в 
памяти и начнет сбрасывать его 

    474 _____________________________ Windows для профессионалов 

    данные на жесткий диск. Вы можете увеличить производительность системы, 
скомбинировав флаги FILE_ATTRIBUTE_TEMPORARY и FILE_FLAG_DELETE_ON_CLOSE (о 
нем я уже рассказывал). Обычно при закрытии файла система сбрасывает на диск 
данные из кэша — но не делает этого, обнаружив, что файл надо уничтожить после 
его закрытия. 
    Последний параметр функции CreateFile— hTemplateFile— содержит или 
описатель открытого файла, или NULL. Если в hTemplateFile указан описатель 
файла, CreateFile игнорирует флаги атрибутов в параметре dwFlagsAndAttrs и 
использует атрибуты того файла, на который указывает параметр hTemplateFile. 
(Чтобы такая схема сработала, файл надо открыть с флагом GENERIC_READ.) Если 
CreateFile не создает новый файл, а открывает существующий, параметр 
hTemplateFile игнорируется. 

    VWINDOWS 95 
    В Windows 95 параметр hTemplateFile должен быть всегда равен NULL, иначе 
СгеateFile даст ошибку, а последующий вызов GetLastError— код ERROR_NOT_SUPPORTED. 

    Если CreateFile успешно создаст или откроет устройство или файл, она 
вернет описатель этого устройства или файла, а при ошибке — код INVALID_HANDLE_VALUE. 

    /внимдниЕ\ 
    Большинство \Уш32-функций при неудаче возвращают NULL, но CreateFile в 
таких случаях возвращает INVALID_HANDLE_VALUE (определенный как OxFFFFFFFF). 
Мне часто попадались такие фрагменты кода: 

    HANDLE hFile = CreateFile(...); 
    if(hFile == NULL) { 
    // сюда мы никогда не попадем 
    } else { 
    // файл может быть создан, а может и нет 
    } 
    Этот код ошибочен. Вот корректный способ проверки описателя файла: 
    HANDLE hFile = CreateFileC...); 
    if (hFile == INVALID_HANDLE_VALUE) { 
    // файл не создан 
    } else { 
    // файл успешно создан 
    } 


    Операции с файлами 

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

    Позиционирование файлового указателя 

    Когда CreateFile возвращает описатель файла, система связывает с этим 
описателем файловый указатель. Он определяет 64-разрядное смещение внутри 
файла, начиная с которого 

    _________________________Глава 15. Устройства: ввод/вывод 475 

    будет выполняться очередная операция синхронного чтения или записи. 
Сначала файловый указатель устанавливается в 0, и, если Вы вызовете ReadFile 
сразу после обращения к CreateFile, Вы начнете чтение с нулевого смещения. 
Если считать в память 100 байтов файла, система так изменит значение файлового 
указателя, связанного с данным описателем файла, что при следующем вызове 
ReadFile начнет чтение со 101-го байта этого файла. Помните: файловый 
указатель связан с описателем файла, а не с операциями доступа или объектом 
ядра «файл». Взгляните для примера на код: 

    HFILE hFile = CreateFile(...); 
    ReadFile(hFile, IpBuffer, 100, &dwBytesRead, NULL); 
    WriteFile(hFile, IpBuffer, 100, &dwBytesWritten, NULL); 

    В приведенном фрагменте из файла в буфер считываются первые 100 байтов, 
затем те же 100 байтов записываются в файл со смещения 100 до смещения 199. 
Если после вызова WriteFile будет выполнена еще одна операция доступа к файлу, 
она начнет работу со смещения 200. 
    Один и тот же файл можно открыть два и более раз. При каждом открытии 
возвращается новый описатель. Файловый указатель связан с описателем, поэтому 
операции, выполняемые с помощью одного описателя, не влияют на файловый 
указатель, сопоставленный с другим описателем, — даже если оба описателя 
ссылаются на один и тот же файл. Взгляните: 

    HFILE hFilel = CreateFileC'MYFILE. DAT", ...); 
    HFILE hFIle2 = CreateFileC'MYFILE.DAT", ...); 
    ReadFile(hFile1, IpBuffer, 100, &dwBytesRead, NULL); 
    WriteFile(hFile2, IpBuffer, 100, &dwBytesWritten, NULL); 

    В этом фрагменте из файла MYFILE.DAT в буфер считываются первые 100 
байтов. После операции чтения связанный с hFilel файловый указатель смещается 
на 101-й байт. Далее 100 байтов записываются обратно в тот же файл. При этом 
указатель, связанный с hFile2, по-прежнему содержит начальное значение 0. В 
результате первые 100 байтов файла "MYFILE.DAT оказываются перезаписанными 
данными, изначально считанными из файла. Конечный результат — содержимое файла 
не меняется, и после выполнения функций ReadFile и WriteFile оба указателя 
смещены на 101-й байт файла. 
    Если Вам нужен произвольный доступ к файлу, измените значение указателя, 
связанного с описателем файла. Для этого предназначена функция SetFilePointer. 

    DWORD SetFiiePointer(HANDLE hFile, LONG IDistanceToMove, PLONG 
IpDistanceToMoveHigh, DWORD dwMoveMethod); 

    Параметр hFile идентифицирует описатель файла, с которым связан указатель. 
Параметр IDistanceToMove сообщает системе, на сколько байтов Вы хотите 
сместить указатель. Передаваемое число добавляется к текущему значению 
файлового указателя, поэтому отрицательное число позволяет перемещать 
указатель назад. Для большинства файлов возможность смещения указателя (вперед 
или назад) в пределах 32-битного значения вполне достаточна. Но для 
действительно больших файлов может понадобиться 64-битное значение. 
    Для столь массивных файлов используется параметр IpDistanceToMoveHigh. 
Перемещая указатель в пределах плюс-минус 2 Гб относительно текущей позиции, 
вместо IpDistanceToMoveHigh подставьте NULL. А если Вы хотите сместить 
указатель в пределах 18 миллиардов Гб относительно текущей позиции, передайте 
через параметр IpDistanceToMoveHigh адрес переменной, в которой содержатся 
старшие 32 бита нужного Вам значения. 
    В чем смысл такой косвенной передачи? Дело в том, что SetFilePointer 
возвращает предыдущее значение файлового указателя. Если Вас интересуют только 
младшие 32 бита этого указателя, функция возвращает это значение напрямую. А 
если Вам нужны и старшие 32 бита, то SetFilePointer (перед возвратом 
управления) записывает их в переменную, на которую указывает IpDistanceToMoveHigh. 

    476 ______________________________Windows для профессионалов 

    Последний параметр, dwMoveMethod, сообщает функции SetFilePointer, как 
интерпретировать параметры IDistanceToMove и IpDistanceToMoveHigh. Он 
допускает 3 значения: 

    Идентификатор    Описание 

    FILE_BEGIN       Файловому указателю присваивается беззнаковое число, 
определяемое двумя Distance ToMove-параметрами. 
    FILE_CURRENT    К файловому указателю добавляется знаковое число, 
определяемое двумя Distance ГоМоуе-параметрами. 
    FILE_END         Файловый указатель вычисляется как сумма размера файла и 
знакового значения, определяемого двумя DutenceToMove-napaMeTpaMH. При этом их 
значения должны быть отрицательными. 
    Если SetFilePointer не смогла изменить значение файлового указателя, она 
возвращает OxFFFFFFFF, а в буфер по адресу, определяемому параметром 
IpDistanceToMoveHigh, записывает NULL. Поскольку в достаточно большом файле 
вполне возможно успешное позиционирование указателя на смещение OxFFFFFFFF, 
лучше проверить результат выполнения функции, вызвав GetLastError и сравнив 
возвращенное ею значение с NO_ERROR. 

    Установка конца файла 

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

    BOOL SetEndOfFile(HANDLE hFile); 

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

    HFILE hFile = CreateFile(...); 
    SetFilePointer(hFile, 1024, NULL, FILE_BEGIN); 
    SetEndOfFile(hFile); 
    CloseHandle(hFile); 


    Блокировка и разблокировка участков файла 

    Флаги FILE_SHARE_READ и FILE_SHARE_WRITE сообщают, могут ли другие 
процессы открыть файл и в каком режиме. Представьте компанию, у которой 
огромная база данных о клиентах, содержащая миллион записей. Эта база данных, 
вероятно, открывается всеми сотрудниками. Если все только ищут в ней данные, 
проблем нет — файл всегда можно открыть с флагом FILE_SHARE_READ. Ну а если 
кому-то из сотрудников надо внести дополнительные имена и адреса? Он должен 
открыть базу данных на запись. И доступ к ней нужно как-то скоординировать, 
чтобы не было одновременной записи в базу данных двумя сотрудниками. Если бы 
оба могли одновременно записывать в базу данных, ее целостность непременно бы 
нарушилась. Решение этой проблемы — блокировка участков файла. 
    Блокировка аналогична применению флагов FILE_SHARE_*, однако последние 
влияют на весь файл, а блокировка действует лишь на отдельные его участки. 
Например, когда клиент переезжает по новому адресу, Вы должны обновить 
соответствующую запись, но прежде сделать так, чтобы никто другой не получил 
доступ к записи о данном клиенте в тот момент, когда Вы будете ее 
модифицировать. Блокировка участка файла осуществляется вызовом: 

    BOOL LockFile(HANDLE hFile. DWORD dwFileOffsetLow, DWORD dwFileOffsetHigh, 
DWORD cbLockLow, DWORD cbLockHigh); 

    _________________________Глава 15. Устройства: ввод/вывод 477 

    Первый параметр, hFile, идентифицирует описатель файла, участок которого 
Вы хотите блокировать. Следующие (dwFileOffsetLow и dwFileOffsetHigh) 
определяют 64-битное смещение начала блокируемого участка внутри файла, а 
последние два (dwLockLow и dwLockHigh) — число блокируемых байтов. Если Вы 
собираетесь обновить в базе данных 100-ю запись, вызовите LockFile так: 

    LockFile(hFile. sizeof(CUSTOMER_RECORD) * (100 - 1), О, sizeof(CUSTOMER_RECORD), 0); 

    При успешном выполнении LockFile возвращает TRUE. Пока участок блокирован, 
ни один посторонний процесс не сможет ни записать, ни считать оттуда данные. 
Именно поэтому после выхода из функций ReadFik и WriteFile очень важно 
проверить число считанных или записанных байтов, так как вполне вероятно, что 
какой-то процесс уже заблокировал отдельные участки файла. Чтобы Ваша 
программа корректно обрабатывала подобные ситуации, предусмотрите, например, 
возможность закрытия других приложений и повторной попытки чтения или записи. 
    Допускается также блокировка участка за текущими границами файла. Это 
полезно при добавлении записей в конец файла. Вы блокируете участок 
непосредственно за границей файла и помещаете туда новую запись. 
    Заблокировать участок, включающий другой, уже блокированный участок, 
нельзя. Так, повторный вызов LockFile в следующем коде даст ошибку: 

    LockFile(hFile, sizeof(CUSTOMER_RECORD) * (100 - 1), О, 
sizeof(CUSTOMER_RECORD), 0); 
    LockFile(hFile, sizeof(CUSTOMER_RECORD) * (100 - 2), О, 2 * 
sizeof(CUSTOMER_RECORD), 0); 

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

    BOOL UnlockFile(HANDLE hFile, DWORD dwFileOffsetLow, DWORD 
dwFileOffsetHigh, DWORD cbUnlockLow, DWORD cbUnlockHigh); 

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

    LockFile(hFile, sizeof(CUSTOMER_RECORD) * (100 - 1), 0. 
sizeof(CUSTOMER_RECORD), 0); 
    LockFil.e(hFile, sizeof(CUSTOMER_RECORD) * (100 - 2), 0, 
sizeof(CUSTOMER_RECORD), 0); 
    UnlockFile(hFile, sizeof(CUSTOMER_RECORD) * (100 - 2), 0, 2 * 
sizeof(CUSTOMER_RECORD), 0); 

    Первые 2 вызова LockFile блокируют соответственно 100-ю и 99-ю записи базы 
данных, а последующее обращение к UnlockFile — это попытка разблокировать обе 
записи одним вызовом. Она закончится безрезультатно. Если было 2 отдельных 
вызова LockFik, то и UnlockFile надо вызывать дважды (с соответствующими параметрами). 

    478 _______________________________Windows для профессионалов 


    ^ВНИМАНИЕ\ 
    Перед закрытием файла или завершением процесса обязательно 
разблокируйте все блокированные участки файла. 

    Есть еще 2 функции, позволяющие блокировать и разблокировать участки файла: 
    BOOL LockFileEx(HANDLE hFile. DWORD dwFlags, DWORD dwReserved, 
    DWORD nNumberOfBytesToLockLow, DWORD nNumberOfBytesToLockHigh,             
                    i LPOVERLAPPED IpOverlapped);                              
    BOOL UnlockFileEx(HANOLE hFile, DWORD dwReserved, 
    DWORD nNumberOfBytesToUnlockLow, DWORD nNumberOfBytesToUnlockHigh, 
LPOVERLAPPED IpOverlapped): 

    Эти функции — надмножество функций LockFile и UnlockFile. 

    \wiNDows7 
    B Windows 95 функции LockFileEx и UnlockFileEx не реализованы и всегда 
возвращают FALSE, а последующий вызов GetLastError дает код ERROR_CALL_NOT_IM- 
| PLEMENTED. 

    LockFileEx обладает двумя дополнительными возможностями в сравнении с 
LockFile. Как и последняя, она блокирует участок файла так, что другие 
процессы не смогут записывать в него, но при этом Вы вправе разрешить им 
читать из заблокированной области. По умолчанию LockFileEx именно такую 
блокировку и ставит; монопольную блокироку можно запросить, добавив к 
параметру dwFlags флаг LOCKFILE_EXCLUSIVE_LOCK побитовой операцией OR. 
(LockFile реализуется как вызов LockFileEx с этим флагом.) 
    LockFileEx позволяет отложить блокировку, если поток Вашего процесса 
запросит блокировку участка файла, уже захваченного другим процессом. Но 
LockFile сразу возвращает управление, сообщая об ошибке. И если Ваш поток не 
может продолжить работу, не заблокировав данный участок файла, придется 
циклически вызывать LockFile, пока она не заблокирует этот участок и не вернет 
TRUE. Для упрощения программы вызовите LockFileEx— по умолчанию она не 
возвращает управления, не захватив требуемый участок файла. А чтобы функция 
вернула управление немедленно — независимо от того, заблокировала ли она 
участок, — добавьте к dwFlags флаг LOCKFILE_FAIL_IMMEDIATELY побитовой 
операцией OR. 
    О назначении большинства других параметров — hFile, 
nNumberOfBytesToLockLow и nNumberOfBytesToLockHigh — нетрудно догадаться по их 
именам. Параметр dwReserved зарезервирован на будущее и должен быть равен 0. 
Последний параметр, IpOverlapped, указывает на структуру OVERLAPPED: 

    typedef struct OVERLAPPED { 
    DWORD Internal: 
    DWORD InternalHigh: 
    DWORD Offset; 
    DWORD OffsetHigh; 
    HANDLE hEvent; 
    } OVERLAPPED; 
    typedef OVERLAPPED -LPOVERLAPPED; 

    Из всех элементов этой структуры LockFileEx использует только Offset и 
OffsetHigh, a остальные элементы игнорирует. Элементы Offset и OffsetHigh надо 
инициализировать перед : вызовом LockFileEx так, чтобы они определяли первый 
байт блокируемого участка. 
    Для разблокировки участка файла можно вызвать как UnlockFile, так и 
UnlockFileEx. Когда-нибудь UnlockFileEx будет расширена в сравнении с 
UnlockFile, но пока у нее нет никаких дополнительных возможностей. 

    _________________________ Глава 15. Устройства: ввод/вывод 479 


    Синхронный ввод/вывод 

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

    BOOL ReadFile(HANDLE hFile, LPVOID IpBuffer. 
    DWORD nNumber-OfBytesToRead, LPDWORD IpNumberOfBytesRead, LPOVERLAPPED IpOverlapped); 
    BOOL WriteFile(HANDLE hFile, CONST VOID *lpBuffer, 
    DWORD nNumber-OfBytesToWrite, LPDWORD IpNumberOfBytesWritten, LPOVERLAPPED IpOverlapped): 

    Хотя в их именах есть слово «File», эти функции можно использовать для 
работы с любым устройством. Параметр hFile— описатель устройства, к которому 
Вы хотите получить доступ. Открывая устройство, нельзя указывать флаг 
FILE_FLAG_OVERLAPPED, иначе система сочтет, что Вам нужен асинхронный 
ввод/вывод. Параметр IpBuffer указывает на буфер, в который считываются данные 
с устройства (или из которого записываются данные на устройство). Параметры 
nNumberOfBytesToRead и nNumberOfBytesTo Write сообщают функциям ReadFile и 
WriteFile, сколько байтов следует прочесть с устройства или записать на него. 
    Параметры IpNumberOfBytesRead и IpNumberOfBytesWritten содержат адрес 
переменной типа DWORD, в которую функции заносят число успешно переданных 
байтов (с устройства или на устройство). Параметр IpOverlapped (его мы обсудим 
подробнее в разделе, посвященном асинхронному вводу/выводу) при синхронном 
вводе/выводе должен быть NULL. 
    Обе функции — ReadFile и WriteFile— при успешном выполнении возвращают 
TRUE. Кстати, первую можно вызывать только для устройств, открытых с флагом 
GENERIC_READ, а вторую — только для устройств, открытых с флагом GENERIC_WRITE. 

    Сброс содержимого буфера на устройство 

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

    BOOL FlushFileBuffers(HANDLE hFile); 

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

    Асинхронный ввод/вывод 

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

    480 ______________________________Windows для профессионалов 

    считать в память ее содержимое и индексный файл. После щелчка кнопки ОК в 
диалоговом окне File Open программа отобразила бы на экране курсор в виде 
песочных часов — пока база данных открывается и считывается. Считав в память 
записи базы данных, приложению нужно открыть и считать индексный файл. Все это 
время «песочные часы» так и маячили бы на экране, а пользователь был бы лишен 
возможности приступить к работе с записями базы данных, пока не закончится 
чтение всех файлов. 
    Асинхронный файловый ввод/вывод существенно сократит время открытия 
файлов. Если пользователь запустит это приложение на машине с несколькими 
процессорами, то на один процессор можно возложить открытие и чтение записей 
из базы данных, а на другой — открытие и чтение индексного файла. Так как за 
каждой задачей будет закреплен свой процессор, обе станут выполняться 
одновременно. Это сократит время открытия базы данных, и пользователь начнет 
работать с ее записями гораздо раньше. 
    Конечно, индексный файл значительно меньше файла, содержащего собственно 
записи. Наверное, индексный файл был бы загружен в память еще до загрузки 
самих записей из базы данных. Но приложение не может разрешить работу до 
окончания загрузки обоих файлов. Чтобы определить момент полной загрузки 
файлов, придется применить какуюнибудь форму синхронизации потоков. 
    Для асинхронного доступа к устройству Вы сначала открываете его вызовом 
CreateFile, указав в параметре dwFlagsAndAttrs флаг FILE_FLAG_OVERLAPPED. Он 
уведомляет систему, что доступ к устройству будет асинхронным. 
    Win32 допускает 4 метода асинхронного ввода/вывода (они подробно 
рассматриваются в этой главе). В их основе лежит единая идеология. Любая 
операция асинхронного ввода/ вывода начинается с того, что Вы передаете 
соответствующий запрос операционной системе; она упорядочивает запросы и 
выполняет их сама. Пока система занимается запросом, управление возвращается 
Вашему потоку, и он продолжает выполнение — это и есть важнейшая особенность 
асинхронного ввода/вывода. 
    По завершении обработки Вашего запроса операционной системе понадобится 
уведомить Вас о том, что данные уже переданы, приняты или возникла какая-то 
ошибка. Так вот, форма выдачи запроса во всех 4 методах практически идентична 
— они отличаются механизмами уведомления о результатах работы. 
    Краткое описание этих методов Вы найдете в следующей таблице. Я привожу их 
в порядке возрастания сложности, начиная с простейшего. 

    Метод                           Краткое описание 

    Перевод объекта ядра «устройство» Бесполезен, если Вы собираетесь выдать 
сразу несколько в свободное состояние  запросов на ввод/вывод применительно 
к одному устройству. Позволяет выдать запрос на ввод/вывод одному потоку, а 
обработать его — другому. 
    Перевод объекта ядра «событие»     Допускает сразу несколько запросов на 
ввод/вывод в свободное состояние применительно к одному устройству. Позволяет 
выдать запрос на ввод/вывод одному потоку, а обработать его — другому. 
    «Тревожный» ввод/вывод           Допускает сразу несколько запросов на 
ввод/вывод применительно к одному устройству. Поток, выдавший запрос на ввод/вывод, 
обязан его обработать. 
    Порты завершения ввода/вывода    Допускает сразу несколько запросов на ввод/вывод 
применительно к одному устройству. Позволяет выдать запрос на ввод/вывод 
одному потоку, а обработать его — другому. Хорошо масштабируемый и максимально 
гибкий метод. 

    _________________________ Глава 15. Устройства: ввод/вывод 481 


    Перевод объекта ядра «устройство» в свободное состояние 

    Это простейший метод. Чтобы выдать запрос на ввод/вывод, Вы, как и обычно, 
используете функции: 

    BOOL ReadFile(HANDLE hFile, LPVOID IpBuffer, 
    DWORD nNumberOfBytesToRead, LPDWORD IpNumberOfBytesRead, LPOVERLAPPED IpOverlapped); 
    BOOL WriteFile(HANDLE hFile, CONST VOID «IpBuffer, 
    DWORD nNumberOfBytesToWnte, LPDWORD IpNumberOfBytesWritten, LPOVERLAPPED IpOverlapped); 

    Мы уже рассмотрели их в разделе «Синхронный ввод/вывод», но для 
асинхронного ввода/вывода надо передать в параметре IpOverlapped адрес 
инициализированной структуры OVERLAPPED. Слово «overlapped» JB данном 
контексте означает, что выполнение запроса на ввод/вывод перекрывается по^ 
вдемени_с_другими операциями, осуществляемыми Вашим потоком. Вот что 
представляет собой структура OVERLAPPED: 

    typedef struct .OVERLAPPED { 
    DWORD Internal; 
    DWORD Inter-nalHigh; 
    DWORD Offset; 
    DWORD OffsetHigh; 
    HANDLE hEvent; 
    } OVERLAPPED; 
    typedef OVERLAPPED .LPOVERLAPPED; 

    Перед вызовом ReadFile или WriteFile создайте структуру OVERLAPPED и 
инициализируйте ее элементы Offset, OffsetHigh и hEvent. Элементы Offset, 
OffsetHigh используются, только если устройство является файлом. Для устройств 
других типов в эти элементы следует записать 0. 

    /вни*АниЕ\ 
    Элементы Offset и OffsetHigh не игнорируются для устройств, отличных от 
файла. Вы обязаны инициализировать их нулевыми значениями, иначе запрос может 
завершиться ошибкой, a GetLastError вернет ERROR_INVALID_PARAMETER. 

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

    // открываем файл для асинхронного ввода/вывода HANDLE hFile = 
CreateFile(..., FILE_FLAG_OVERLAPPED, ...); 
    // создаем буфер для хранения данных BYTE bBuffer[100]; 
    // создаем переменную типа DWORD для числа считанных байтов DWORD dwNumBytesRead; 
    // инициализируем структуру OVERLAPPED, чтобы сообщить // системе, откуда 
начинать чтение 

    см. след. стр. 

    482 _______________________________Windows для профессионалов 


    OVERLAPPED Overlapped: 
    Overlapped.Offset = 345; 
    Overlapped.OffsetHigh = 0: 
    Overlapped.hEvent = NULL; // объясню потрм 
    // начинаем асинхронное чтение данных 
    ReadFile(hFile, bBuffer, slzeof(bBuffer), &dwNumBytesRead, &0verlapped); 
    // код после ReadFile исполняется параллельно с тем, как 
    // система читает данные из файла в буфер 

    Используя асинхронный ввод/вывод, помните о нескольких вещах. При 
синхронном вводе/выводе с каждым описателем файла связан свой файловый 
указатель. При поступлении очередного запроса на чтение или запись система 
знает, что операция должна начаться со смещения, определяемого файловым 
указателем. Завершив операцию, система обновляет указатель так, чтобы 
следующая файловая операция начиналась с того места, где закончилась предыдущая. 
    При асинхронном вводе/выводе все иначе. Представьте, что было бы без 
структуры OVERLAPPED. Если бы 2 вызова ReadFile последовали непосредственно 
друг за другом, система не знала бы, откуда начать чтение файла во втором 
случае. А ведь Вы, наверное, хотели бы начать с той позиции, на которой должна 
была бы завершиться первая операция. Поэтому во избежание путаницы Microsoft 
спроектировала функции ReadFile и WriteFile так, что начальная позиция для 
каждой операции асинхронного ввода/вывода определяется через структуру OVERLAPPED. 
    Когда Вы вызываете ReadFile или WriteFile, система проверяет, нет ли 
требуемых данных в системном кэше; если они там, копирует их в Ваш буфер и 
возвращает управление. В этот момент данные либо записаны в Ваш буфер, либо 
переданы из него, и теперь буфер можно использовать по своему усмотрению. 
Чтобы узнать число переданных байтов, проверьте значение переменной типа 
DWORD, на которую указывает параметр IpNumberOfBytesRead или 
IpNumberOfBytesWritten. Если Вы запрашивали синхронный ввод/вывод и он 
завершился успешно, ReadFile и WriteFile возвращают TRUE. А если Вы запросили 
асинхронный ввод/вывод, ReadFile и WriteFile в любом случае возвращают FALSE, 
так как запрос на ввод/вывод еще не выполнен. Причин тому может быть много, и, 
чтобы определить истинную, вызовите GetLastError. Если она вернет 
ERROR_IO_PENDING, запрос помещен в очередь и будет выполнен позже. 
    Запрос на ввод/вывод может дать ошибку, так как система вносит 
невыполненные запросы в список фиксированной длины (он размещается в 
оперативной памяти). Если на момент выдачи очередного запроса этот список уже 
заполнен, функции ReadFile и 'WriteFile вернут FALSE, а GetLastError— либо 
ERROR_INVALID_USER_BUFFER, либо ERROR_NOT_ENOUGH_MEMORY. Учтите и еще одно. 
Получив от Вас запрос на ввод/вывод, система должна заблокировать страницы 
памяти, отведенные под Ваш буфер данных2. Этот буфер является частью рабочего 
множества (working set) Вашего процесса. Если в рабочем множестве не хватает 
места, запрос на ввод/вывод не пройдет, и GetLastError вернет код 
ERROR_NOT_ENOUGH_QUOTA. Чтобы расширить рабочее множество, вызовите функцию SetProcessWorkingSetSize: 

    BOOL SetProcessWorkingSetSize(HANDLE hProcess, 
    DWORD dwMinimumWorklngSetSIze, DWORD dwMaximumWorkingSetSize); 

    2. Подробнее о блокировке страниц см. главу 7 (там, где говорится о 
функциях VirtnalLock и VirtualUnlock). 

    _________________________Глава 15. Устройства: ввод/вывод 483 

    Давайте суммируем все, что было сказано. Если Вы выдали новый запрос на 
ввод/ вывод, а функция сообщила об ошибке с кодом (определяемым через 
GetLastError) ERROR_INVALID_USER_BUFFER, ERROR_NOT_ENOUGH_MEMORY или 
ERROR_NOT_ENOUGH_QUOTA, заставьте поток дождаться завершения ранее выданных 
запросов на ввод/ вывод и затем повторите свой запрос. 

    /ВНИМАНИЕ^ 
    Пока запрос на ввод/вывод не обработан, ни буфер данных, ни структуру 
OVEREAPPED уничтожать нельзя! Вы можете удалить соответствующие блоки памяти 
только после того, как запрос будет выполнен. 

    Как только начнется асинхронная файловая операция, поток может продолжить 
инициализацию или заняться какой-то другой работой. Но в конечном счете Вам 
все равно придется как-то синхронизировать свой поток с операцией 
ввода/вывода. Иначе говоря, Вы неминуемо достигнете некоей точки в коде, 
начиная с которой Ваш поток не сможет продолжить работу, пока с устройства не 
будут считаны в буфер все необходимые Вам данные. 
    В Win32 описатель устройства рассматривается как синхронизующий объект, т. 
е. он может находиться либо в занятом, либо в свободном состоянии. Функции 
ReadFilen WriteFile начинают свою работу с того, что сбрасывают описатель 
устройства в занятое состояние. Впоследствии, считав или записав все данные, 
система переводит описатель устройства в свободное состояние. Вызвав 
WaitForSingleObject или WaitForMultipleObjects, поток может определить момент 
окончания асинхронной операции — когда описатель устройства переводится в 
незанятое состояние. Вот расширенный вариант приведенного ранее кода: 

    // открываем файл для асинхронного ввода/вывода 
    HANDLE hFile = Cr-eateFile(, .., FILE_FLAG_OVERLAPPED, ,..); 
    ^ // создаем" буфер для хранения данных 
    BYTE bBuffer[100]; 
    // создаем переменную типа DWORD для числа считанных байтов 
    DWORD dwNumBytesRead; 
    // инициализируем структуру OVEREAPPED, чтобы сообщить 
    // системе, откуда начинать чтение 
    OVEREAPPED Overlapped; 
    Overlapped.Offset = 345; 
    Overlapped.OffsetHigh = 0; 
    Overlapped.hEvent = NUEE; // объясню потом 
    // начинаем асинхронное чтение данных 
    ReadFile(hFile, bBuffer, sizeof(bBuffer), &dwNumBytesRead, «.Overlapped); 
    
    // код после ReadFile исполняется параллельно с тем, как 
    // система читает данные из файла в буфер 
    
    // IIOIOK не может продолжи i ь выполнунпС: до ;цх г.ор, :.и,\ь 
    // все данные не будут считаны в буфер 
    WaitForSingleObject(hFile, INFINITE); 

    см. след. стр. 

    484 _______________________________Windows для профессионалов 


    // инициализация завершена, данные считаны из файла, 
    // поток может продолжить выполнение 

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

    BOOL GetOverlappedResult(HANDLE hFile, LPOVERLAPPED IpOverlapped, LPDWORD 
IpcbTransfer, BOOL fWait); 

    Параметры hFile и IpOverlapped при обращении к GetOverlappedResult должны 
определять тот же описатель файла и ту же структуру OVERLAPPED, что и при 
вызове ReadFile или WriteFile. Параметр IpcbTransfer указывает на переменную 
типа DWORD, в которой функция сообщит число байтов, фактически считанных или 
записанных при операции ввода/вывода. Даже если эта информация Вас не 
интересует, в нем все равно надо передать правильный адрес, чтобы не возникло 
нарушение доступа к памяти. 
    И последний параметр — fWait — Булева переменная, сообщающая функции 
GetOverlappedResult, следует ли ждать окончания «перекрывающейся» файловой 
операции. Если он равен FALSE, функция сразу вернет управление, а если в нем 
передать TRUE, то GetOverlappedResult можно использовать вместо WaitForSingleObject. 
    При успешном выполнении GetOverlappedResult возвращает TRUE. Если параметр 
fWait равен FALSE и операция ввода/вывода еще не завершена, 
GetOverlappedResult возвращает FALSE. Чтобы узнать, произошла ошибка при 
вызове GetOverlappedResult или файловая операция все еще продолжается, 
вызовите GetLastError. Последняя вернет код ERROR_IO_INCOMPLETE, если вызов 
прошел успешно, но операция ввода/вывода пока не закончилась. 
    В Windows 95 функция GetOverlappedResult действует только для 
последовательных портов. 
    Структуру OVERLAPPED нельзя использовать повторно, пока запрос не 
выполнен. Вот пример принципиально ошибочного кода: 

    void Fund (void) { 
    // открываем файл для асинхронного ввода/вывода 
    HANDLE hFile = CreateFile(..., FILE_FLAG_OVERLAPPED, ...); 
    // создаем буфер для хранения данных 
    BYTE bBuffer[100]; 
    Func2(hFile, bBuffer, sizeof(bBuffer)): 
    void Func2 (HANDLE hFile, LPVOID bBuffer, DWORD dwBufSize) { 
    DWORD dwNumBytesRead; 
    // инициализируем структуру OVERLAPPED, чтобы сообщиib системе, 
    // откуда начинать чтение 

    _________________________Глава 15. Устройства: ввод/вывод 485 


    OVERLAPPED Overlapped; 
    memset(&0verlapped, 0, sizeof'(Overlapped)): 
    // начинаем асинхронное чтение 
    ReadFile(hFile, bBuffer, dwBufSize. &dwNumBytesRead, &0verlapped): 
    } 

    Этот фрагмент неверен, так как структура OVERLAPPED, локально объявленная 
в Func2, выходит из области видимости, когда Func2 передает управление функции 
Fund. В то же время система запоминает ее адрес при вызове ReadFile (или 
WriteFile). Завершив ввод/ вывод, система обращается к элементам Internal, 
InternalHigh и hEvent этой структуры. Но, когда структура выйдет из области 
видимости, система будет иметь дело с мусором, попавшимся ей в стеке, и это 
станет причиной трудноуловимой ошибки в программе! 
    В первых бета-версиях Windows NT элементы Internal •я InternalHigh 
структуры OVERLAPPED, которые система обязана обновить по окончании 
ввода/вывода, были зарезервированы для служебных целей. Со временем Microsoft 
стало ясно, что содержащаяся в них информация может быть полезна и нам. И 
чтобы никому не пришлось модифицировать код, уже использующий эти элементы, 
имена оставили без изменений. Если запрос завершился с ошибкой, в элемент 
Internal записывается статус системы, а в InternalHigh — число переданных 
байтов. Такое же значение помещается и в буфер, на который указывает параметр 
IpcbTrasfer функции GetOverlappedResult. 

    Перевод объекта ядра «событие» в свободное состояние 

    Описанный выше метод — прост и понятен, но ограничиваться им не следует, 
так как он не работает при нескольких запросах на ввод/вывод. Допустим, Вам 
нужно проделать сразу несколько асинхронных операций над одним файлом — 
например, считать последовательность байтов из начала файла и одновременно 
записать другую последовательность в его конец. В таком случае, ожидая 
перевода описателя файла в свободное состояние, Вы не сможете синхронизировать потоки. 
    Описатель переходит в свободное состояние, как только завершается одна из 
файловых операций, поэтому, вызвав WaitForSingleObject и передав ей описатель 
файла, Вы не узнаете, какая именно операция завершилась: чтения или записи. 
Очевидно, при выполнении множества параллельных асинхронных запросов нужен 
способ получше. И, к счастью, он есть. 
    Элемент hEvent, последний в структуре OVERLAPPED, идентифицирует объект 
ядра «событие», который надо создать с помощью CreateEvent (подробнее о 
событиях см. главу 10). Завершив выполнение асинхронного запроса на 
ввод/вывод, система сравнивает значение элемента hEvent с NULL. Если hEvent 
отличен от NULL, система, вызвав SetEventn передав hEvent как описатель 
объекта-события, переводит этот объект в свободное состояние. При этом 
описатель файла тоже переводится в свободное состояние. Но, используя события 
для определения момента завершения файловой операции, ждите освобождения 
описателя события, а не файла. 
    Если Вам нужно выполнить сразу несколько запросов на ввод/вывод, создайте 
по одному объекту «событие» на запрос, в каждой структуре OVERLAPPED 
инициализируйте элемент hEvent и вызовите ReadFile win WriteFile. Достигнув 
точки, где не обойтись без синхронизации по окончании выполнения запросов на 
ввод/вывод, вызовите WaitForMultipkObjects, передав ей описатели событий, 
связанные с соответствующими структурами OVERLAPPED. Действуя по этой схеме, 
Вы сможете достаточно просто и надежно выполнять по несколько асинхронных 
операций ввода/вывода, использующих один описатель устройства. 

    486 ______________________________Windows для профессионалов 

    Для синхронизации приложения с асинхронно выполняемой операцией 
ввода/вывода годится и функция GetOverlappedResult. Если ее параметр fWait 
равен TRUE, она сама вызывает WaitForSingleObject и передает ей элемент hEvent 
структуры OVERLAPPED. 
    Но и здесь не все гладко. Если для уведомления об окончании операции 
ввода/вывода Вы используете событие с автосбросом (а не со сбросом вручную), 
Ваш поток может остановиться навечно. Вызвав WaitForSingleObject, Вы ждете 
завершения операции ввода/вывода, но при возврате из этой функции событие 
автоматически перейдет в занятое состояние. Теперь Вы вызываете 
GetOverlappedResult, чтобы определить число успешно переданных байтов, и 
передаете TRUE в параметре  fWait. Но тогда она тоже обращается к 
WaitForSingleObject. После этого функция WaitForSingleObject никогда не вернет 
управления, поскольку файловая операция уже завершена. В итоге 
GetOverlappedResult тоже не вернет управление, и Ваш поток зависнет! 

    «Тревожный» ввод/вывод 

    Когда создается поток, система создает и ассоциирует с_этим потоком 
очередь асинхронных вызовов процедур (asynchronous procedure call, APC). 
АРС-очередь работает так же, как и очередь сообщений, рассмотренная в главе 
11. Однако АРС-очередь реализуется на самом нижнем уровне, в ядре системы, 
тогда как очередь сообщений относится к гораздо более высокому уровню — 
компонентам пользовательского интерфейса операционной системы. Поскольку 
АРС-очередь обслуживается ядром, она значительно эффективнее, а использующие 
ее операции выполняются намного быстрее. 
    Результаты запросов на ввод/вывод можно получать через АРС-очередь 
вызывающего потока. Чтобы выполненные запросы помещались в эту очередь, 
используйте функции ReadFileEx и WriteFileEx: 

    BOOL ReadTileEx(HANDLE hFile, LPVOID IpBuffer, 
    DWORD nNumberOfBytesToRead, LPOVERLAPPED IpOverlapped, 
    LPOVERLAPPED_COMPLETION_ROUTINE IpCompletionRoutine); 
    BOOL WriteFileEx(HANDLE hFile, CONST VOID *lpBuffer, 
    DWORD nNumberOfBytesToWnte, LPOVERLAPPED IpOverlapped, 
    LPOVERLAPPED_COMPLETION_ROUTINE IpCompletionRoutine): 
    
    В Windows 95 функции ReadFileEx, и WriteFileEx не реализованы и возвращают 
FALSE. Последующий вызов GetLastError даеткод ERR6Rl'CTLL_NOT_IMPLEMENTED. 
    Функции ReadFileEx и WriteFileEx передают операционной системе запрос на 
ввод/ вывод и немедленно возвращают управление. В конечном счете система 
обработает Ваш запрос, и ей понадобится как-то сообщить Вам об этом. Мы уже 
рассмотрели 2 метода получения таких уведомлений (за счет освобождения 
объектов ядра «устройство» и «событие»). Когда запрос на ввод/вывод, выданный 
через ReadFileEx или WriteFileEx, завершается, система освобождает описатель 
устройства и добавляет элемент в АРС-очередь потока3. При этом система всегда 
добавляет элемент именно в ту очередь, что связана с потоком, выдавшим запрос 
через ReadFileEx или WriteFileEx. 
    У ReadFileEx/WriteFileEx тот же набор параметров, что и у 
ReadFile/WriteFik, — с двумя отличиями. Во-первых, *Ех-функциям не передается 
указатель на переменную типа 
    3. При использовании этого метода элемент hEvent структуры OVERLAPPED 
игнорируется — можете задействовать его по своему усмотрению. 

    Глава 15. Устройства: ввод/вывод 487 

    DWORD, в которую бы записывалось число переданных байтов; эту информацию 
можно получить только через функцию обратного вызова. А во-вторых, ^Ex-функции 
требуют передачи адреса функции обратного вызова — процедуры завершения 
(completion routine): 

    VOID WINAPI FileIOCompletionRoutine(DWORD fdwError, DWORD cbTransferred, 
LPOVERLAPPED Ipo): 

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

    hFile = CreateFile(.,,); 
    // выполнить первую операцию ReadFileEx 
    ReadFileEx(hFile, ...); 
    // выполнить первую операцию WriteFileEx 
    WriteFileEx(hFile,,..); 
    // выполнить вторую операцию ReadFileEx 
    ReadFileEx(hFile, ..,); 
    SomeFuncO; 

    Если время выполнения SomeFunc достаточно велико, система успеет до 
возврата из этой функции закончить 3 операции ввода/вывода. Пока поток 
выполняет SomeFunc, система добавляет в связанную с ним АРС-очередь элементы, 
соответствующие выполненным запросам. Список может выглядеть так: 

    1-ая операция WriteFileEx завершена 
    2-ая операция ReadFileEx завершена 
    1-ая операция ReadFileEx завершена 

    АРС-очередью управляет система. Как видите, она выполняет запросы в 
произвольном порядке: операция, запрошенная последней, может завершиться 
первой, и наоборот. Каждый элемент АРС-очереди содержит информацию о 
выполненном запросе: код ошибки, число переданных байтов, адрес структуры 
OVERLAPPED (переданный через ReadFileEx или WriteFileEx) и_адрес_пррцелуры завершения. 
    Когда выполнение запроса закончено, он ставится в АРС-очередь потока — 
процедура завершения не вызывается немедленно: ведь Ваш поток может быть занят 
какой-то другой работой и прервать его нельзя. Чтобы обработать элементы 
АРС-очереди, поток должен перейти в «тревожное» (alertable) состояние. А это 
означает только одно: поток достиг в коде некоей точки, в которой он способен 
обрабатывать прерывания. Для перевода потока в «тревожное» состояние в Win32 
предусмотрено 5 функций:

    DWORD SleepEx(DWORD dwTimeout, BOOL fAlertable); 
    DWORD WaitForSingleObjectEx(HANDLE h0b]ect, DWORD dwTimeout, BOOL fAlertable): 
    DWORD WaitForMultipleOb]ectsEx(DWORD cObjects, LPHANDLE IphObjects, BOOL 
fWaitAll, DWORD dwTimeout, BOOL fAlertable); 
    BOOL SignalObjectAndWait(HANDLE hOb]ectTo8ignal, HANDLE hObJectToWaitOn, 
DWORD dwMilliseconds, BOOL fAlertable); 
    DWORD MsgWaitForMultipleObjectsEx(DWORD nCount, LPHANDLE pHandles, DWORD 
dwMilliseconds, DWORD dwWakeMask, DWORD dwFlags); 

    488 ____________________________Windows для профессионалов 

    Последний параметр в первых четырех функциях — Булево значение, которое 
определяет, переводится ли вызывающий поток в «тревожное» состояние. В случае 
функции MsgWaitForMultipleObjectsEx для этого указывают флаг MWMO_ALERTABLE. 
Кстати, функции Sleep, WaitForSingleObjectu WaitForMultipleObjects реализованы 
как вызовы соответствующих им *Бх-функций с передачей FALSE в параметре fAlertable. 
    Когда Вы обращаетесь к одной из 5 перечисленных выше функций и переводите 
поток в «тревожное» состояние, система первым делом проверяет его АРС-очередь. 
Если в ней есть хотя бы один элемент, система не приостанавливает поток — 
вместо этого она извлекает из очереди элемент и в контексте Вашего потока 
вызывает процедуру завершения с передачей кода ошибки обработанного запроса, 
числа переданных байтов и адреса структуры OVERLAPPED. Как только эта 
процедура возвращает управление системе, та проверяет, есть ли еще 
какие-нибудь элементы в АРС-очереди. Если есть, обрабатываются и они. Когда 
очередь опустеет, произойдет возврат из функции, с помощью которой Вы перевели 
поток в «тревожное» состояние. Имейте в виду: при наличии хотя бы одного 
элемента в АРС-очереди поток не заснет! 
    Вызов этих функций приостанавливает поток только при отсутствии элементов 
в АРС-очереди. И если произошло именно так, поток проснется, когда освободится 
ожидаемый Вами объект ядра или что-то появится в АРС-очереди. В последнем 
случае (так как поток в «тревожном» состоянии) система разбудит его и 
опустошит очередь, вызвав при этом процедуру завершения. Далее любая из 5 
функций немедленно возвращает управление Вашему потоку, который больше не 
засыпает и не ждет освобождения объектов ядра. 
    Все 5 функций возвращают код завершения. Если он равен WAIT_IO_COMPLETION, 
значит, Ваш поток продолжает выполнение, так как обработан минимум один 
элемент АРСочереди. А если возвращаются другие значения, поток возобновлен 
из-за окончания времени ожидания, освобождения указанного объекта ядра или 
отказа от объекта-мьютекса (подробнее см. главу 10). 
    В Windows 95 и Windows NT 4 появилась новая функция, позволяющая вручную 
добавить элемент в АРС-очередь: 

    DWORD QueueUserAPC(PAPCFUNG pfnAPC, HANDLE hThread, DWORD dwData); 

    Первый ее параметр — указатель на АРС-функцию с прототипом: 

    VOID WINAPI APCFunc(DVJORD dwParam); 

    Второй параметр функции QueueUserAPC — описатель потока, в очередь 
которого добавляется новый элемент. Заметьте, что им может быть любой поток в 
системе. Если hThread идентифицирует поток стороннего процесса, параметр 
pfnAPC должен содержать адрес функции в его адресном пространстве. И, наконец, 
параметр dwData представляет собой 32-битное значение, передаваемое функции 
обратного вызова. 
    Хотя QueueUserAPC определена как функция, возвращающая значение типа 
DWORD, на самом деле она возвращает Булево значение, сообщая в нем об удачном 
или неудачном выполнении. Можете использовать QueueUserAPC для связи между 
потоками, даже принадлежащими разным процессам. Но передать таким образом 
можно только одно 32битное значение. 
    VviNDowsY функция QueueUserAPC не документирована в Windows 95, тем не 
менее она существует и экспортируется из Kernel32.DLL. Можете смело 
использовать ее как в Windows NT 4, так и в Windows 95. 
    Описанная выше расширенная форма «тревожных» функций наиболее полезна для 
клиент-серверных приложений. Допустим, у Вас есть серверное приложение, 
которое «сторожит» базу данных, и клиентское приложение, которое периодически 
запрашивает информацию у сервера; при этом они взаимодействуют через механизм 
именованных каналов. 

    _________________________ Глава 15. Устройства: ввод/вывод 489 

    Серверное приложение вызывает ReadFileExn передает серверу вместо 
описателя файла описатель именованного канала. Когда клиентское приложение 
передаст серверу информацию по именованному каналу, тот примет клиентский 
запрос асинхронным вызовом, и оттуда произойдет обращение к 
FileIOCompletionRoutine. Эта функция интерпретирует запрос и найдет нужную 
информацию в базе данных, после чего сервер сам вызовет ReadFileEx. Последняя, 
считав информацию из базы данных, обратится к FileIOCompletionRoutine, и 
выбранные данные будут переданы через именованный канал клиентскому приложению. 
    Существенный недостаток «тревожного» ввода/вывода в том, что выдача 
запроса и обработка его результатов должны выполняться одним и тем же потоком. 
В некоторых серверных приложениях это не лучшее решение. Представьте, 
например, что поток выдает запрос на ввод/вывод и остается занят какой-то 
работой. Когда выполнение запроса завершается, было бы удобнее, чтобы 
поступившие данные обработал другой поток. Но, увы, данные будут ждать своей 
очереди в буфере, пока выдавший запрос поток не закончит текущую работу и не 
переведет себя в «тревожное» состояние. Чтобы преодолеть это ограничение, 
Microsoft добавила в Win32 API так называемые порты завершения ввода/вывода (о 
них мы поговорим ближе к концу главы). 

    Программа-пример AlertIO 

    Программа AlertIO (ALERTIO. EXE) (см. листинг на рис. 15-1) иллюстрирует 
«тревожный» ввод/вывод. Файлы исходного кода и ресурсов для этой программы 
находятся в каталоге ALERTIO на компакт-диске, прилагаемом к книге. Она просто 
копирует файл, выбранный пользователем, в новый файл с именем ALERTIO.CPY. При 
запуске на экране появляется диалоговое окно: 

    Pathname: 
    Browse...    |j               RIesize:  0 

    Чтобы выбрать файл, щелкните кнопку Browse (обзор). Закончив, Вы увидите, 
что программа обновила поля Pathname (полный путь) и File Size (размер файла). 
Щелчок кнопки Copy (копировать) приводит к вызову функции FileCopy, которая в 
свою очередь обращается к PrepareDstFile, ответственной за открытие исходного 
файла и создание файла-приемника. Заметьте: в обоих случаях вызывается 
CreateFik с флагом FILE_FLAG_NO_BUFFERING, чтобы система не тратила память 
попусту на собственную копию данных из исходного файла и файла-приемника. 

    /анимдниЕ\ 

    Если оба файла успешно открыты, устанавливается длина файла-приемника, для 
чего вызываются функции SetFilePointer и SetEndOfFile. Установить длину файла 
именно сейчас очень важно, поскольку NTFS поддерживает маркер уровня 
(highwater marker), указывающий наиболее удаленную от начала файла точку, в 
которую шла запись. Если Вы попытаетесь считать данные, лежащие за этой 
точкой, система вернет цепочку нулей. А при записи в аналогичных условиях 
система сначала заполняет нулями участок между маркером и последней точкой 
записи, потом записывает туда Ваши данные и обновляет позицию маркера. Такое 
поведение обусловлено правилами защиты уровня С2, в соответствии с которыми 
нельзя показывать данные, оставленные другими приложениями. Запрос на запись в 
конец файла, в результате которого позиция маркера сдвинется, NTFS выполняет 
синхронно. И если бы программа AlertIO заранее не установила размер 
файла-приемника, ни один из запросов не был бы выполнен асинхронно. 

    490 ______________________________Windows для профессионалов 

    Теперь мы готовы выдать набор запросов на ввод/вывод. Для этого 
инициализируем MAX_PENDING_IO_REQS структур IOREQ. Каждая из них содержит 
структуру OVERLAPPED, применяемую при вызове ReadFileEx и WriteFileEx. При 
чтении ее элементы Offset и OffsetHigh содержат начальную позицию в исходном 
файле. В элемент hEvent заносится NULL, впрочем, туда можно записать что 
угодно, так как при выполнении запросов, инициированных функциями ReadFileEx и 
WriteFileEx, его содержимое игнорируется. 
    Кроме структуры OVERLAPPED, для каждого запроса на ввод/вывод необходим 
буфер в памяти. Я реализовал его, поместив в структуру IOREQ элемент pbData — 
указатель на блок памяти, запрашиваемый через VirtualAlloc. Эта функция 
гарантирует выравнивание блока с учетом гранулярности выделения ресурсов, что 
необходимо при использовании флага FILE_FLAG_NO_BUFFERING (буфер должен 
начинаться по адресу, который без остатка делится на размер сектора тома). 
    После инициализации всех элементов структуры IOREQ вызывается ReadFileEx, 
а счетчик незавершенных запросов на чтение увеличивается на 1. Когда запрос 
выполнен, в АРС-очередь, связанную с потоком AlertIO, добавляется новый 
элемент. Выдав все запросы на ввод/вывод, мы начинаем цикл, продолжающийся, 
пока не будут обработаны эти запросы. Внутри цикла вызывается функция SleepEx 
со значением INFINITE — она переводит поток в «тревожное» состояние. SleepEx 
возвращает управление, только когда обработан один или несколько элементов АРС-очереди. 
    По мере окончания обработки каждого из запросов на чтение, в АРС-очередь 
потока добавляются новые элементы. Когда поток находится в «тревожном» 
состоянии (вызвана SleepEx), из АРС-очереди удаляются элементы и вызывается 
ReadCompletionRoutine. Она приводит параметр ^Overlapped к указателю PIOREQ. 
Тут реализуется очень распространенный прием: мы добавляем в конец структуры 
OVERLAPPED свои элементы и передаем ее адрес функциям ReadFile(Ex) или 
WriteFile(Ex). А когда указатель на структуру OVERLAPPED возвращается нам 
обратно, приводим его к указателю на собственную структуру данных и таким 
образом получаем доступ к своим элементам. 
    Далее ReadCompletionRoutine уменьшает счетчик незавершенных запросов на 
чтение. Поскольку в программе лишь один поток, манипуляции с этими переменными 
не требуют синхронизации. ReadCompletionRoutine округляет число переданных 
байтов до ближайшего большего значения, кратного размеру страниц, что опять же 
необходимо при вызове CreateFile с флагом FILE_FLAG_NO_BUFFERING. Затем 
вызываем WriteFileEx, которая выдает запрос на запись в файл-приемник; при 
этом счетчик числа незавершенных запросов на запись увеличивается на 1. 
    После обработки этого запроса в АРС-очередь потока помещается еще один 
элемент. Когда поток находится в «тревожном» состоянии, данный элемент 
удаляется из очереди и вызывается WriteCompletionRoutine, структура кода 
которой симметрична, структуре кода ReadCompletionRoutine: она уменьшает 
счетчик числа необработанных запросов на запись, и, если мы не пытались читать 
за границей файла, вызывает ReadFileEx, которая помещает в очередь новый 
запрос на чтение исходного файла. 
    Основной цикл завершается, когда SleepEx возвращает управление и больше 
нет необработанных запросов на ввод/вывод. После этого освобождаем все буферы 
и вызываем CopyCleanup, которая закрывает исходный файл и файл-приемник. Затем 
она повторно открывает файл-приемник, не устанавливая на этот раз флаг 
FILE_FLAG_NO_BUFFERING. Так как теперь уже не требуется выравнивания по 
границам секторов, можно сократить длину файла-приемника до размера исходного файла. 

    __________________________Глава 15. Устройства: ввод/вывод 491 


    ALERTIO.C 
    /*.******.*..*****.,.**..*.*,,*..**...***.,*...,**.,*********...***.,.*»,*..*** 
    Модуль: AlertIO.C Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    (tinclude "..\CmnHdr.H"              /* см. приложение В */ 
    ftinclude <windows.h> 
    ^include <windowsx.h> 
    ftinclude <tchar.h> 
    «include <stdio.h> 
    ftinclude <stdlib.h> 
    ffinclude "Resource.H" 
    ll/lllllll/l/lllllllllllllllllllllll/lll/HIIIIII/lllllll/ltlllllllllllllllll/l 
    static BOOL PrepareOstFile(LPCTSTR pszFileSrc, LPCTSTR pszFileDst, 
    PHANDLE phFileSrc, PHANDLE phFileDst, 
    PULARGE_INTEGER pulFileSize, DWORD dwPageSize) { 
    ULARGE_INTEGER ulFileSizeDst; 
    DWORD dwError: 
    BOOL fOk = FALSE; 
    __try { 
    // открываем исходный файл для чтения 
    *phFileSrc = CreateFile(pszFileSrc, GENERIC_READ, 
    FILE_SHARE_READ, NULL, OPEN_EXISTING, 
    FILE_FLAG_NO_BUFFERING : FILE_FLAG_OVERLAPPED, NULL); 
    if (*phFileSrc == INVALID_HANDLE_VALUE) 
    __leave; 
    // создаем файл-приемник, в который будем записывать 
    *phFileDst = CreateFile(pszFileDst, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 
    FILE_FLAG_NO_BUFFERING ; FILE_FLAG_OVERLAPPEO, *phFileSrc); 
    if (*phFileDst == INVALID_HANDLE_VALUE) 
    __leave: 
    // определяем размер исходного файла 
    pulFileSize->LowPart = GetFileSize(*phFileSrc, &pulFileSize->HighPart): 
    // округляем размер исходного файла до ближайшего 
    // большего значения, кратного числу страниц 
    ulFileSizeDst.QuadPart = (pulFileSize->QuadPart + 
    dwPageSize - 1) & -(dwPageSize - 1): 

    см. след. стр. 

    Рис. 15-1. Листинг программы AlertIO 


    492 ______________________________Windows для профессионалов 


    РИС. 15-1 (продолжение) 

    II устанавливаем размер файла-приемника 
    dwError = SetFilePointer(*phFileDst, ulFileSizeDst.LowPart, 
    (PLONG) ulFileSizeDst.HighPart, FILE_BEGIN): 
    if ((dwError == Oxffffffff) && (6etLastError() != NO_ERROR)) 
    __leave; 
    if (!SetEndOfFile(*phFileDst)) 
    __leave; 
    fOk = TRUE: // все прошло удачно 
    } 
    „finally { 
    // если произошла какая-нибудь ошибка, все очищаем 
    if (!f0k) { 
    if (*phFileSrc != INVALID_HANDLE_VALUE) { 
    CloseHandle(*phFileSre); 
    *phFile8rc = INVALID_HANDLE_VALUE; 
    } 
    if (*phFileSrc != INVALID_HANDLE_VALUE) { 
    CloseHandle(*phFileDst): 
    *phFileDst = INVALID_HANDLE_VALUE; 
    \ I 
    } return(fOk); 
    /////////////////////////////////////////////////////////////////////////////// 
    static void CopyCleanup(LPCTSTR pszFileDst, HANDLE hFileSrc, 
    HANDLE hFileDst, PULARGE_INTEGER pulFileSize) { 
    // закрываем исходный файл и файл-приемник 
    CloseHandle(hFileDst); 
    CloseHandle(hFileSrc); 
    // Необходим еще один описатель файла-приемника, открытого 
    // без флага FILE_FLAG_NO_BUFFERING. Это позволит установить 
    // маркер конца файла на позицию, не кратную длине сектора. 
    hFileDst = CreateFile(pszFlleDst, GENERIC_WRITE, 
    FILE_SHARE_READ ; FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL): 
    if (hFileDst != INVALID_HANDLE_VALUE) { 
    // устанавливаем длину файла-приемника равной длине исходного 
    // файла, если длина последнего не кратна числу страниц 

    см. след. стр. 

    ___________________________Глава 15. Устройства: ввод/вывод 493 


    РИС. 15-1 (продолжение) 

    SetFilePointer(hFileOst, pulFileSize->LowPart, (PLONG) 
pulFile8ize->HighPart, FILE_BEGIN); 
    SetEndOfFile(hFileDst); 
    CloseHandle(hFileOst): 
    typedef struct { 
    // размер станиц, используемый на данном компьютере 
    DWORD dwPageSize; 
    // описатели исходного файла и файла-приемника 
    HANDLE hFileSrc, hFileDst; 
    // размер исходного файла 
    ULARGE_INTEGER ulFileSize: 
    // смещение в исходном файле, откуда начнется следующая операция чтения 
    ULARGE_INTEGER ulNextReadOffset; 
    // число незавершенных запросов на ввод/вывод 
    int nReadsInProgress; 
    int nWriTesInProgress; 
    } COPYSTATE, *PCOPYSTATE; 
    COPYSTATE g_cs: 
    // каждый запрос требует структуру OVERLAPPED и буфер 
    typedef struct { 
    OVERLAPPED Overlapped; 
    PBYTE      pbData; 
    } IOREQ, *PIOREQ; 
    ftdefine MAX_PENDING_IO_REQS  4 
    #define BUFFSIZE              (64 * 1024) 
    ////////////////////////////////////////////////////
    VOID WINAPI WriteCompletionHoutine(DWORD dwErrorCode, 
    DWORD dwNurnberOfBytesTransferred, LPOVERLAPPED pOverlapped): 
    /////////////////////////////////////////////////////
    VOID WINAPI ReadCompletionRoutine(DWORD dwErrorCode, 
    DWORD dwNumberOfBytesTransfe^-ed^ LPOVERLAPPED pOverlapped) { 

    см. след. стр. 

    494 ______________________________Windows для профессионалов 


    РИС. 15-1 (продолжение) 

    PIOREQ pIOReq = (PIOREQ) pOverlapped; 
    chASSERT(dwErrorCode == NO_ERROR); 
    n_cs.nReadsInProgress--; 
    // округляем число байтов до ближайшего большего значения, 
    // чтобы закончить запись на границе сектора 
    dwNumPerOfBytesTransferred = 
    (dwNuniberOfBytesTransferred + g_cs. dwPageSize - 1) & ~(g_cs. dwPageSize - 1); 
    chVERIFY(WriteFileEx(g_cs.hFileDst, pIOReq->pbData, 
    dwNumberOfBytesTransf erred, pOverlapped, WriteCompletlonRoutine)); 
    g_cs,nWritesInProgress++; 
    VOID WINAPI WriteCompletionRoutine(DWORD dwErrorCode, 
    DWORD dwNumberOfBytesTransferred, LPOVERLAPPED pOverlapped) { 
    PIOREQ pIOReq = (PIOREQ) pOverlapped; 
    chASSERT(dwErrorCode == NO_ERROR): 
    g_cs.nWritesInProgress-: 
    if (g_cs.ulNextReadOffset.QuadPart < g_cs.ulFileSize.QuadPart) { 
    // мы пока не читали за концом файла; считываем следующую порцию данных 
    p0verlapped->0ffset       = g_cs.ulNextReadOffset.LowPart; 
    pOverlapped->OffsetRigh   = g_cs.ulNextReadOffset.RighPart; 
    chVERIFY(ReadFileEx(g_cs.hFileSrc. pIOReq->pbData, 
    BUFFSIZE, pOverlapped, ReadCompletionRoutine)); 
    g_cs,nReads!nProgress++; 
    // смещение в исходном файле, откуда начнется следующая операция чтения 
    g_cs.ulNextReadOffset.QuadPart += BUFFSIZE; 
    BOOL FileCopy (LPCTSTR pszFileSrc, LPCTSTR pszFileDst) { 
    // сохраняем информацию о каждом запросе на ввод/вывод 
    IOREQ IOReq[MAX_PENDING_IO_REQS] = { 0 }; 
    SYSTEM_INFO si: 
    int nIOReq: 
    BOOL fOk; 
    // инициализируем глобальную переменную COPYSTATE 
    Get8ystemlnfo(&si); 

    см. след. стр. 

    _________________________Глава 15. Устройства: ввод/вывод 495 


    РИС. 15-1 (продолжение) 

    g_cs.dwPage8ize = si.dwPageSize; 
    g_cs. ulNextReadOffset.QuadPart = 0; 
    g_cs.nReadsInProgress = 0; 
    g_cs.nWritesInProgress = 0: 
    // открываем исходный файл для чтения 
    f0k = PrepareDstFile(pszFileSrc, pszFileDst. &g_cs.hFileSrc. &g_cs.hFileDst, 
    &g_cs.ulFileSize, g_cs.dwPageSize): 
    if (!f0k) 
    return(fOk): 
    // запускаем алгоритм копирования, выдавая группу запросов 
    // на чтение из исходного файла 
    for (nIOReq = 0; nIOReq < MAX_PENDING_IO_REQS; nIOReq++) { 
    IOReq[nIOReq].Overlapped.Internal = 0: 
    IOReq[nIOReq].Overlapped.InternalHigh = 0; 
    IOReq[nIOReq].Overlapped.Off set = g_cs.ulNextReadOffset,LowPart; 
    IOReq[nIOReq].Overlapped.OffsetHigh = g_cs,ulNextReadOffset.HighPart; 
    IOReq[nIOReq].Overlapped,hEvent = NULL; 
    IOReq[nIOReq].pbOata = VirtualAlloc(NULL, 
    BUFFSIZE, MEM_RESERVE ; MEM_COMMIT, PAGE_READWRITE); 
    if (g_cs,ulNextReadOffset.QuadPart < g_cs.ulFileSize.QuadPart) { 
    chVERIFY(ReadFileEx(g_cs.hFileSrc, IOReq[nIOReq].pbOata, BUFFSIZE, 
    &IOReq[nIOReq].Over lapped, ReadCompletionRoutine)); 
    g_cs.nReads!nProgress++; 
    g_cs.ulNextReadOffset.QuadPart += BUFFSIZE; 
    } 
    // продолжаем цикл, пока не произойдет ошибка или 
    // файл-приемник не будет полностью записан 
    while ((g_cs.nReadsInProgress > 0) !! (g_cs.nWritesInProgress > 0)) { 
    SleepEx(INFINITE, TRUE): 
    // освобождаем буферы, использованные для копирования 
    for (nIOReq = 0; nIOReq < MAX_PENDING_IO_REQS; nIOReq++) { 
    VirtualFree(IOReq[nIOReq].pbData, 0. MEM_RELEASE): 
    // закрываем оба файла и устанавливаем длину 
    // конечного файла равной длине исходного 
    CopyCleanup(pszFileDst, g_cs.hFileSrc, 
    g_cs.hFileDst, &g_cs.ulFileSize); 
    •'"t.ir'ifTRUF): 

    см. след. стр. 

    496 _______________________________Windows для профессионалов 


    РИС. 15~1 (продолжение) 

    BOOL Dlg_0nlnit0ialog (HWND hwnd, HWND hwndFocus, LPARAM IParam) { 
    // связываем значок с диалоговым окном 
    chSETDLGICONS(hwnd, IDI_ALERTIO, IDI_ALERTIO); 
    // отключаем кнопку Copy, так как файл еще не выбран 
    EnableWindow(GetDlgItem(hwnd, IDOK), FALSE); 
    return(TRUE); 
    void Dlg_OnConimand (HWND hwnd, int id, HWND hwndCtI, UINT codeNotify) { 
    TCHAR szPathname[_MAX_DIR]; 
    BOOL fOk; 
    OPENFILENAME ofn; 
    switch (id) { 
    case IDOK: 
    // копируем исходный файл в файл-приемник 
    Static_GetText(GetDlgItem(hwnd, IDC_SRCFILE), 
    szPathname, sizeof(szPathname)); 
    SetCursor(LoadGursor(NULL, IDC_WAIT)): 
    FileCopy(szPathname, __TEXT("AlertIO.CPY")); 
    break; 
    case IDC_BROWSE: 
    chINITSTRUCT(ofn, TRUE); 
    ofn.hwnddwner = hwnd; 
    of-n.lpstrFilter = __TEXT("*.*\0"): 
    _tcscpy(szPathname, __TEXT("*.*")); 
    ofn,IpstrFile = szPathname; 
    ofn.nMaxFile = sizeof(szPathname); 
    Ofn.Flags = OFN_FILEMUSTEXIST; 
    fOk = GetOpenFileName(&ofn); 
    if (fOk) { 
    HANDLE hFlle; 
    Static_SetText(GetDlgItem(hwnd. IDC_SRCFILE), szPathname): 
    hFile = CreateFile(szPathname, GENERIC_READ, 
    0, NULL, OPEN_EXISTING, 0, NULL); 
    SetDlgItemInt(hwnd. IDC_SRCFILESIZE. 
    GetFileSize(hFile, NULL), FALSE); 
    CloseHandle(hFile); 

    см. след. стр. 

    ________________________Глава 15. Устройства: ввод/вывод 497 


    РИС. 15-1 (продолжение) 

    II активизируем кнопку Copy, если файл выбран 
    // и полный путь корректен 
    GetWindowText(GetDlgItem(hwnd, IDC_SRCFILE), 
    szPathname, sizeof(szPathname)); 
    EnableWindow(GetDlgIteni(hwnd, IOOK), 
    szPathname[0] != __TEXT('(')): 
    if (fOk) { 
    // если пользователь щелкнул кнопку ОК в диалоговом окне 
    // для выбора файла, устанавливаем фокус на кнопку Copy 
    FORWARD_WM_NEXTDLGCTL(hwnd, GetDlgItem(hwnd, IOOK), TRUE, SendMessage); 
    case IDCANCEL: 
    EndDialog(hwnd, id); 
    break; 
    BOOL CALLBACK Dlg_Proc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM IParam) { 
    switch (uMsg) { 
    chHANDLE._DLGMSG(hwnd. WM_INITDIALOG. Dlg_OnInitDialog); 
    chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); 
    } 
    return(FALSE); 
    int WINAPI _tWinMain (HINSTANCE hinstExe. 
    HINSTANCE hinstPrev, LPTSTR pszCmdLine, int nCmdShow) { 
    chWARNIFUNICODEUNDERWIN95(); 
    DialogBox(hinstExe, MAKEINTRESOURCE(IDD_ALERTIO), NULL, Dlg_Proc); 
    return(O): 
    /////////////////////////// конец файла ///////////////////////////

    498 ______________________________Windows для профессионалов 


    Порты завершения ввода/вывода 

    Windows NT разработана как надежная, защищенная операционная система, 
выполняющая приложения, с которыми работают буквально тысячи пользователей. 
Эта способность (обслуживая массы пользователей, обеспечивать защиту и 
надежность) присуща и Windows NT Workstation, но в большей мере свойственна 
Windows NT Server. 

    WINDOWS  B Windows 95 функции портов завершения не реализованы. 

    Существует 2 основные модели построения сервисных приложений: 
    • последовательная— один поток ожидает запрос от клиента (обычно по сети); 
когда поступает запрос, поток пробуждается и обрабатывает его; 
    • параллельная — один поток ожидает запрос от клиента, а когда такой 
запрос поступает, он создает новый поток, который этот запрос и обрабатывает. 
В то же время первый поток возвращается в исходную точку кода и ждет нового 
запроса. Поток, обработавший запрос от клиента, завершается. 
    Главная проблема с последовательной моделью в том, что та плохо 
приспособлена к множеству параллельных запросов. Если 2 клиента потребуют 
обслужить их одновременно, только один запрос удастся обработать сразу, а 
второй придется отложить до окончания обработки первого. Сервис, построенный 
на последовательной модели, не использует преимущества архитектуры SMP. Ясно, 
что последовательная модель годится только для простейших приложений, которые 
выдают небольшое число быстро обрабатываемых запросов. Примером 
последовательного сервера служит Ping. 
    "Из-за ограничений, свойственных последовательной модели, более популярна 
параллельная модель. В ней для обработки каждого клиентского запроса создается 
отдельный поток. Преимущество этой модели в том, что ожидающий запросы поток 
выполняет минимум действий. Большую часть времени он просто спит. При 
поступлении запроса от клиента он пробуждается, создает поток для обработки 
этого запроса и вновь засыпает, ожидая других запросов. Таким образом, 
запросы, поступающие от клиентов, обрабатываются весьма оперативно. Кроме 
того, поскольку каждому запросу соответствует свой поток, серверное 
приложение, построенное на параллельной модели, хорошо масштабируется и 
использует все преимущества SMP-машин. Поэтому при модернизации компьютера 
(добавлением еще одного процессора) производительность серверного приложения 
этого типа возрастает. 
    По мере появления на платформе Windows NT серверных приложений, основанных 
на параллельной модели, разработчики этой операционной системы обнаружили, что 
производительность такого рода программ не столь высока, как хотелось бы. В 
частности, обработка большого числа одновременных запросов приводила к 
параллельному выполнению системой множества потоков. Так как все эти потоки 
были активны (т. е. не приостанавливались), Microsoft стало ясно, что ядро 
Windows NT тратит слишком много времени на переключение между выполняемыми 
потоками и они не получают достаточного для своей работы процессорного времени. 
    Чтобы превратить Windows NT в серьезную серверную платформу, пришлось 
решить и эту проблему. В итоге в Windows NT 3.5 появился новый объект ядра — 
порт завершения ввода/вывода (I/O completion port). В основу портов завершения 
легла такая идея: число 

    _________________________ Глава 15. Устройства: ввод/вывод 499 

    одновременно работающих потоков должно быть ограничено. Скажем, 500 
одновременных запросов не должны порождать 500 потоков. Но каков же этот 
предел? Подумайте, и Вы сразу поймете, что на двухпроцессорной машине нет 
смысла порождать более двух таких потоков одновременно — по одному на 
процессор. Если потоков окажется больше, система будет просто терять 
драгоценное процессорное время на переключение контекста. 
    Создание нового потока для каждого запроса — еще одна причина 
неэффективности параллельной модели. В сравнении с созданием процесса, который 
требует своего адресного пространства объемом 4 Гб, создание потока не так 
накладно, но и далеко не дешево. Производительность серверного приложения 
можно существенно поднять, создав резерв потоков еще в момент его 
инициализации, и они будут находиться в готовности в течение всего времени 
работы сервера. Вот для управления этим пулом и служат порты завершения ввода/вывода. 
    Порты завершения ввода/вывода, вероятно, — сложнейший объект ядра, 
предусмотренный в Win32 API. Чтобы создать порт завершения, вызовите CreateIoCompletionPort: 

    HANDLE CreateIoCompletionPort(HANDLE hFileHandle. 
    HANDLE hExistingCompletionPort, DWORD dwCompletionKey, DWORD 
dwNumberOfConcurentTh reads): 

    Эта функция служит двум целям: создает порт завершения ввода/вывода и 
связывает с ним устройство. Она чрезмерно сложна, и, на мой взгляд, Microsoft 
следовало бы разбить ее на две. Работая с портами завершения ввода/вывода, я 
разделяю эти две операции, для чего придумал 2 компактные функции. Первую я 
назвал CreateNewCompletionPort: 

    HANDLE CreateNewCompletionPort(DWORD dwNumberOfConcurrentThreads) { 
return(CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 
    dwNumberOfConcurrentThreads)); 
    } 

    Эта функция — с единственным параметром, dwNumberOfConcurrentThreads, — 
обращается к CreateIoCompletionPort, передавая в качестве первых трех 
параметров константы, а в качестве четвертого — dwNumberOfConcurrentThreads. 
Как видите, первые три параметра функции CreateIoCompletionPort используются 
только для связывания устройства с портом завершения ввода/вывода (об этом я 
расскажу чуть позже). Чтобы создать порт завершения, надо передать 3 значения: 
INVALID_HANDLE_VALUE, NULL и б соответственно. 
    Параметр dwNumberOfConcurrentThreads сообщает порту завершения 
ввода/вывода максимальное число потоков, готовых к одновременному выполнению. 
Если указать О, число таких потоков будет равно числу процессоров на 
компьютере. Как правило, это именно то значение, которое и нужно, поскольку 
оно помогает избежать лишних переключении контекста. Вы можете увеличить его, 
если обработка клиентских запросов требует продолжительных, редко блокируемых 
вычислений. Можете поэкспериментировать с параметром 
'dwNumberOfConcurrentThreads, присваивая ему разные значения и сравнивая 
производительность своей программы. 
    Вероятно, Вы обратили внимание, что CreateIoCompletionPort—единственная 
функция, создающая объект ядра и не имеющая параметра типа 
LPSECURITY_ATTRIBUTES. A объясняется это тем, что ее планировали использовать 
только в рамках одного процесса. Почему — станет ясно, когда мы познакомимся с 
применением портов завершения. 
    Когда Вы создаете порт завершения ввода/вывода, ядро создает 5 структур 
данных, показанных на рис. 15-2 (он может оказаться очень полезным при чтении 
этого раздела). 

    500 ______________________________Windows для профессионале»! 


    Список устройств 

    Каждая запись содержит: 

    hDevice       dwCompletionKey 

    Элемент добавляется при: 
    • вызове CreateCompfeh'onPort. 

    Элемент удаляется при: 
    • закрытии описателя устройства. 

    Очередь завершения ввода/вывода (FIFO) 

    Каждая запись содержит: 

    dwBylesTransferred dwCompletionKey    pOverlapped        dwError 

    Элемент добавляется при: 
    • завершении обработки запроса на ввод/вывод; 
    • вызове PostQueuedComplelionStafus. 
    Элемент удаляется, когда: 
    • порт завершения удаляет элемент из очереди ожидающих потоков. 

    Рис. 15-2. Внутренняя организация порта завершения ввода/вывода 

    _________________________Глава 15. Устройства: ввод/вывод 501 


    РИС. 15-2 (продолжение) 

    Очередь ожидающих (waiting) потоков (LIFO) 

    Каждая запись содержит: 

    dwThreadId 

    Элемент добавляется при: 
    • вызове потоком GerQueuedCompletionStatus. 
    Элемент удаляется, когда: 
    • очередь завершения ввода/вывода не пустая, а число 
выполняемых потоков не превышает максимального числа параллельных потоков (элемент 
удаляется из очереди завершения ввода/вывода, dwThreadId заносится в список 
запущенных потоков, a GefQueuedCompleft'onStatus возвращает управление). 

    Список запущенных (released) потоков 

    Каждая запись содержит: 
    dwThreadId 
    Элемент добавляется, когда: 
    • порт завершения «будит» поток из очереди ожидающих потоков; 
    • пробуждается приостановленный поток. 
    Элемент удаляется, когда: 
    • поток вновь вызывает GerQueuedCompfetionStatus (dwThreadId вновь 
      регистрируется в очереди ожидающих потоков); 
    • поток вызывает функцию, приостанавливающую его выполнение 
      (dwTnreadId заносится в список приостановленных потоков). 
    Список приостановленных (paused) потоков 
    Каждая запись содержит: 

       dwThreadId 

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

    502 ______________________________Windows для профессионалов 

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

    BOOL AssociateDeviceWithCompletionPort(HANDLE hCompPort, 
    HANDLE hDevice, DWORD dwCompKey) { 
    HANDLE h = CreateIoCompletionPort(hDevice, hCompPort, dwCompKey, 0); 
    return (h == hCompPort); 

    Функция AssociateDeviceWithCompletionPort добавляет элемент в ранее 
созданный список устройств порта завершения. Ей передается описатель порта 
завершения (полученный предшествующим вызовом CreateNewCompletionPort), 
описатель устройства (которое может быть файлом, гнездом, почтовым ящиком, 
каналом и т. п.) и так называемый ключ завершения (completion key) — нужное 
Вам 32-битное значение, безразличное операционной систе'ме~Всякий раз, когда 
Вы связываете устройство с портом, система помещает информацию о нем в список 
устройств этого порта. 

    /внимдниЕ\ 
    Хоть я и советую различать 2 типа обращений к CreateIoCompletionPort, у 
столь запутанного интерфейса есть одно достоинство. Он позволяет одновременно 
и создать порт, и связать с ним устройство. Например, приведенный ниже код 
открывает файл, создает порт завершения и ассоциирует с ним файл. Все запросы 
на файловый ввод/вывод будут выполняться с ключом завершения COMPKEY_FILE. 
Порт разрешит одновременную работу двум потокам.                               

    ftdefine COMPKEY_FILE 1               -——-——., ^ 
    HANDLE hFile = CreateFile(...); 
    HANDLE hPort = CreateIoCompletionPort(NULL, hFile, COMPKEY_FILE, 2); 

    Windows NT 3.5 требует, чтобы при создании порта завершения с ним всегда 
свя- ! зывалось какое-нибудь устройство. Иначе говоря, Windows NT 3.5 не 
разрешает передавать значение INVALID_HANDLE_VALUE во втором параметре функции 
CreateIoCompletionPort. Начиная с версии 3.51, семантика функции изменена 
так, что теперь можно передавать INVALID_HANDLE_VALUE и тем самым создавать 
порты завершения, не связывая с ними устройства. 
    Вторая структура данных — очередь завершения ввода/вывода (I/O completion 
queue). По окончании обработки асинхронного запроса на ввод/вывод система 
проверяет, связано ли устройство с портом завершения, и, если да, использует 
вторую структуру данных — очередь завершения ввода/вывода, помещая в ее конец 
выполненный.запрос. Каждый элемент очереди содержит число переданных байтов, 
32-битный ключ завершения, устанавливаемый при связывании устройства с портом, 
указатель на структуру OVERLAPPED запроса и код ошибки. А теперь вкратце 
обсудим, как из этой очереди удаляются элементы. 

    /внимдниЕ\ 
    При определенных условиях можно выдать запрос, окончание обработки 
которого не будет сопровождаться включением элемента в очередь завершения 
ввода/вывода. Обычно этого не требуется, но иногда удобно. Например, при 
передаче данных через гнезда Вас может не интересовать, отправлены эти данные 
или нет. Чтобы выдать запрос на ввод/вывод без последующего включения элемента 
в очередь завершения, запишите в элемент hEvent структуры OVERLAPPED 
достоверный описатель события и добавьте к нему побитовой операцией OR 
значение 1: 

    см. след. стр. 

    _________________________ Глава 15. Устройства: ввод/вывод 503 


    Overlapped. hEvent = CreateEvent(NULL, TRUE, FALSE, NULL); 
    Overlapped.hEvent |= 1; 
    /iw*'w-\ ReadFile(..., &0verlapped); 

    Теперь можете выдать свой запрос на ввод/вывод, передав адрес структуры 
OVERLAPPED в нужную У/т32-функцию (в данном случае ReadFik). 
    К сожалению, избежать включения элемента в очередь завершения без создания 
события нельзя. Конечно, было бы удобнее написать что-то вроде: 

    Overlapped.hEvent = 1; 
    ReadFile(..., &0verlapped); 

    Но этот код не работает. 
    Кроме того, перед закрытием описателя события сбросьте младший бит: 

    CloseHandle(Overlapped. hEvent &.^_1J_ 

    При инициализации серверное приложение должно создать порт завершения, 
вызвав что-то вроде CreateNewCompletionPort. Затем оно формирует пул потоков, 
обрабатывающих клиентские запросы. Но вот вопрос: сколько потоков должно быть 
в пуле? Ответить достаточно сложно, и я сделаю это позже. А пока сойдемся на 
том, что число потоков должно вдвое превышать чцсло процессоров. Таким 
образом, на двухпроцессорной машине следует создать пул из 4 потоков. 
    Все потоки одного пула должны выполнять одну функцию. Обычно эта функция 
потока сначала инициализирует данные, затем входит в цикл, который завершается 
вместе с серверным приложением. В цикле поток отправляет себя в сон, ожидая 
завершения ввода/ вывода. Для этого он использует функцию GetQueuedCompletionStatus: 

    BOOL GetQueuedCompletipnStatus(HANDLE hCompletionPort, 
    LPDWORD IpdwNumberOfBytesTransferred, LPDWORD IpdwCompletionKey. 
    LPOVERLAPPED *lp0verlapped, DWORD dwMilliseconds); 

    Первый ее параметр, hCompletionPort, определяет порт завершения 
ввода/вывода, мониторингом которого займется поток. Многие серверные 
приложения используют единственный порт, и все сообщения о завершении 
ввода/вывода направляются туда. Основное назначение GetQueuedCompletionStatus 
в том, что вызвавший ее поток засыпает до появления какого-нибудь элемента в 
очереди завершения указанного порта или до истечения таймаута, заданного в 
параметре dwMilliseconds. 
    Третья структура данных, связанная с портами завершения ввода/вывода, — 
очередь ожидающих потоков (waiting thread queue). Когда поток из пула вызывает 
GetQueuedCompletionStatus, его идентификатор помещается в очередь ожидающих 
потоков. Благодаря этому объект ядра «порт завершения ввода/вывода» всегда 
знает, какие потоки ждут сейчас окончания операций ввода/вывода. Когда в 
очереди завершения появляется элемент, порт завершения «будит» один из очереди 
ожидающих потоков. Этот поток получает информацию, связанную с законченной 
операцией ввода/вывода: число переданных байтов, ключ завершения и адрес 
структуры OVERLAPPED. Она передается в параметрах 
IpdwNumberOfBytesTransferred, IpdwCompletionKey и IpOverlapped функции GetQueuedCompletionStatus. 
    Последняя функция может вернуть управление по множеству причин. Если Вы 
неправильно обратились к ней (например, передав неверный описатель порта 
завершения), она вернет FALSE, a GetLastError сообщит код ошибки (в данном 
случае ERROR_INVALID_HANDLE). Если же функция завершилась из-за истечения 
тайм-аута, она вернет FALSE, а GetLastError — WAIT_TIMEOUT. 
    При удалении элемента из очереди завершения переменные, на которые 
указывают параметры IpdwNumberOfBytesTransferred, IpdwCompletionKey и 
IpOverlapped, устанавливаются в соответствии с информацией, помещенной в этот 
элемент. Если удаленный элемент пред

    504 ______________________________Windows для профессионалов 

    ставлял успешно выполненную операцию ввода/вывода, 
GetQueuedCompletionStatus возвращает TRUE. Но если за ним стоял неудавшийся 
запрос, возвращается FALSE, а последующий вызов GetLastError дает код ошибки ввода/вывода. 
    Чтобы определить, почему вызов GetQueuedCompletionStatus оказался 
неудачен: из-за провала операции ввода/вывода, истечения тайм-аута или по 
какой-то другой причине, проверьте значение переменной IpOverlapped, как 
показано ниже: 

    DWORD dwNumberOfBytesTransferred, dwCompletionKey; 
    LPOVERLAPPED IpOverlapped; 
    
    // описатель hIOCompPort инициализируется где-то в другой части программы 
    BOOL fOK = GetQueuedCompletionStatus(hIOCompPort, 
    &dwNumberOfBytesTransferred, &dwCo(npletionKey, &lp0verlapped, 1000); 
    DWORD dwError = GetLastError(); 
    if (fOK) { 
    // обрабатываем успешно завершенный запрос на ввод/вывод 
    } else { 
    if (IpOverlapped != NULL) { 
    // обрабатываем неудачный запрос; 
    // элемент dwError содержит код ошибки 
    } else { 
    if (dwError == WAIT_TIMEOUT) { 
    // истек таймаут при ожидании элемента очереди завершения 
    } else { 
    // ошибка при вызове GetQueuedCompletionStatus: 
    // dwError содержит код ошибки 
    } 

    Элементы из очереди завершения извлекаются по принципу «первым пришел — 
первым вышел» (first-in-first-out, FIFO). Но потоки, вызвавшие 
GetQueuedCompletionStatus, пробуждаются иначе: «последним пришел — первым 
вышел» (last-in-first-out, LIFO). Это сделано для повышения 
производительности. Допустим, 4 потока находятся в очереди ожидающих потоков. 
При появлении завершенного запроса на ввод/вывод пробуждается последний поток, 
вызвавший GetQueuedCompletionStatus. Он и обрабатывает поступивший элемент. 
Закончив, он вновь обращается к GetQueuedCompletionStatus, чтобы снова встать 
в очередь ожидающих потоков. Если в этот момент появляется новый элемент 
(завершенный запрос на ввод/вывод), для его обработки пробудится все тот же поток. 
    Пока очередь завершения ввода/вывода «движется» так медленно, что с 
обработкой запросов справляется один поток, система не будит остальных. При 
использовании алгоритма LIFO ресурсы памяти (вроде стекового пространства)7 
прюйдлежащие потокам, которым не выделяется процессорное время, могут быть 
сброшены на диск. Благодаря этому большое число потоков, ждущих одного порта 
завершения, не создает никаких проблем. Если завершения запросов ждет 
множество потоков, но одновременно завершается лишь несколько запросов, 
большая часть ресурсов простаивающих потоков вытесняется из оперативной памяти. 

    _________________________Глава 15. Устройства: ввод/вывод 505 

    Теперь обсудим, чем так хороши порты завершения ввода/вывода. Во-первых, 
создавая порт завершения, Вы указываете число потоков, которые можно выполнять 
параллельно. Как я уже говорил, обычно оно соответствует количеству 
процессоров в компьютере.4 По мере появления элементов в очереди завершения 
порт будит ожидающие потоки. Однако он разбудит лишь столько потоков, сколько 
Вы указали. Так что, когда завершаются 4 запроса и 4 потока ждут в вызове 
GetQueuedCompletionStatus, порт завершения ввода/вывода будит максимум 2 
потока — остальные продолжат спать. Заканчивая обработку выполненного запроса, 
поток вновь обращается к GetQueuedCompletionStatus. Если в очереди снова 
появляются необработанные элементы, система вновь будит те же потоки, чтобы 
они занялись этими запросами. 
    Если Вы хорошенько подумаете, то заметите одну, казалось бы, странную 
вещь: если порт завершения ввода/вывода ограничивает число одновременно 
пробуждаемых потоков, зачем создавать заведомо больший пул ожидающих потоков?' 
Например, мы запускаем программу на машине с 2 процессорами, создаем порт 
завершения ввода/вывода, указывая, что одновременно можно будить не более 2 
потоков, и тем не менее создаем пул из 4 потоков (вдвое превышающее число 
процессоров). Такое впечатление, что мы порождаем 2 лишних потока, которые 
никогда не пробудятся. 
    Порт завершения ввода/вывода весьма «интеллектуален». Когда он будит 
поток, идентификатор последнего записывается в четвертую структуру данных, 
связанную с портом завершения, — список запущенных потоков (released thread 
Hst) (см. рис. 15-2). Эта структура помогает порту отслеживать исполнение 
разбуженных потоков. Если те обращаются к какойнибудь функции, переводящей 
поток в состояние ожидания, порт узнает об этом и, «вычеркнув» поток из списка 
запущенных, регистрирует его в списке приостановленных потоков (paused thread 
list) — пятой структуре данных, связанной с портом завершения ввода/вывода. 
    Смысл деятельности порта завершения — поддержание в списке запущенных 
потоков ровно того числа потоков, что указано при создании порта. Если 
запущенный поток по какойлибо причине переходит в состояние ожидания, он 
вычеркивается из списка запущенных потоков, включается в список 
приостановленных, и порт «выпускает» другой поток из списка ожидающих. Если 
пробуждается приостановленный поток, он удаляется из списка приостановленных и 
вновь регистрируется в списке запущенных. А это означает, что в данный момент 
в списке запущенных потоков может быть больше элементов, чем Вы задали. 
    Попробуем связать все сказанное воедино. Пусть у нас по-прежнему 
двухпроцессорная машина. Мы создаем порт завершения, позволяющий одновременно 
будить не более 2 потоков, и порождаем 4 потока, ждущих выполнения запросов на 
ввод/вывод. Если в очередь завершения помещено 3 элемента, для их обработки 
будет разбужено только 2 потока. Это сокращает число выполняемых потоков и 
соответственно уменьшает время, затрачиваемое на переключение контекста. И вот 
один из выполняемых потоков вызывает Sleep, WaitForSingleObject, 
WaitForMultipleObjects, SignalObjectAndWait или какую-нибудь другую функцию, 
которая «усыпляет» поток. Порт завершения ввода/вывода немедленно обнаруживает 
это и запускает третий поток. 
    В конечном счете первый поток вновь окажется готов к выполнению. Когда это 
случится, число выполняемых потоков превысит количество процессоров в системе. 
Но порт завершения знает и об этом — он больше не разбудит ни одного потока, 
пока количество готовых к выполнению потоков не станет меньше числа 
процессоров. При этом предполагается, что число готовых к выполнению потоков 
будет превышать заданный максимум только на очень короткие промежутки времени 
и быстро спадать при повторных обращениях к GetQueuedCompletionStatus. Вот 
почему в пуле должно быть больше потоков, чем указано для порта завершения. 
    4. Чтобы определить число процессоров, вызовите функцию GetSystemlnfo (см. 
главу 6). 

    506 ______________________________Windows для профессионалов 

    А теперь самое время обсудить размер этого пула. С одной стороны, запуская 
серверное приложение, нужно сразу создать достаточное количество потоков — 
чтобы не пришлось регулярно создавать новые потоки и уничтожать их. Такие 
операции лишь попусту тратят процессорное время, и чем их меньше, тем лучше. С 
другой стороны, надо бы ограничить максимальное число потоков, так как 
создание лишних потоков приводит к непроизводительному расходу системных 
ресурсов. Даже если большая их часть будет вытеснена из оперативной памяти, 
лучше не тратить на них место в страничном файле понапрасну. 
    Вы, вероятно, захотите поэкспериментировать с различным числом потоков. 
Так, Internet Information Service (US), разработанный Microsoft, использует 
весьма сложный алгоритм для управления пулом потоков. Предельное число 
потоков, создаваемых IIS в пуле, устанавливается динамически. При запуске 
US-сервер позволяет создать не более 10 потоков на процессор. Но в зависимости 
от числа клиентских запросов эта планка может быть поднята до максимума, 
определяемого как объем оперативной памяти в мегабайтах, умноженный на два.5 
    Когда клиентский запрос приводит к выполнению кода из ISAPI DLL, US-сервер 
увеличивает максимально допустимое число потоков в пуле на 1, а когда 
ISAPI-функция возвращает управление, IIS соответственно уменьшает это 
значение. Дело в том, что IIS не знает, чем займется ISAPI-функция. Ведь если 
она войдет в бесконечный цикл, US-сервер потеряет один из своих потоков. 
    До сих пор мы говорили только об увеличении счетчика, указывающего 
максимальное число потоков в пуле. Но изменение этого счетчика еще не означает 
немедленного создания нового потока. Он создается, только если запрос 
поступил, а все потоки в пуле заняты (при этом имеется в виду, что число 
потоков в пуле меньше текущего максимума). IIS ведет счетчик занятых потоков. 
Он уменьшается на 1 перед самым вызовом GetQueuedCompletionStatus и 
увеличивается сразу после возврата из нее.6 
    Ну вот я и рассказал Вам, как поддерживается счетчик максимального числа 
потоков и как потоки добавляются в пул. А что, если число потоков в пуле 
достаточно велико, а запросы поступают редко? Тогда лучше избавиться от лишних 
потоков. IIS делает это, указывая при вызове GetQueuedCompletionStatus 
тайм-аут, равный 24 часам. Если поток в пуле беспробудно спит целые сутки, то 
по истечении этого времени он пробуждается, выходит из пула и завершается. 
    Очень важно помнить, что в пуле должен быть хотя бы один поток, способный 
обрабатывать поступающие запросы. Если все потоки заняты, запрос может быть 
отвергнут. Всегда лучше принять запрос и обработать его — пусть медленно, с 
большим числом переключении контекста, — чем вовсе отвергнуть его. 
    Эмуляция завершенных запросов на ввод/вывод 
    В Windows NT 3.51 Microsoft ввела новую функцию, относящуюся к портам 
завершения, — PostQueuedCompletionStatus: 

    BOOL PostQueuedCompletionStatus(HANDLE hCompletionPort, 
    DWORD dwNumberOfBytesTransferred, DWORD dwCompletionKey, 
    LPOVERLAPPED IpOverlapped); 

    Она позволяет вручную поставить выполненный запрос в очередь завершения 
ввода/вывода порта. Параметр hCompletionPort идентифицирует порт завершения, в 
очередь которого 
    5. Я поинтересовался у разработчиков Microsoft US, как они пришли к такой 
формуле» и получил ответ, что это чистая интуиция. Так что и Вам придется 
«чисто интуитивно» подбирать нужный алгоритм для своей программы. 
    6. Для изменения значения счетчика используйте функции 
Interlockedlncrement и InterlockedDecrement. 

    _________________________Глава 15. Устройства: ввод/вывод 507 

    Вы хотите поместить элемент. Остальные параметры 
(dwNumberOfBytesTransferred, dwCompletionKey и IpOverlapped) определяют 
значения, возвращаемые при вызове GetQueuedCompletionStatus. Когда поток 
извлекает искусственно созданный элемент из очереди завершения, эта функция 
возвращает TRUE, что соответствует успешному выполнению запроса на ввод/вывод. 
    Функция PostQueuedCompktionStatus очень полезна: она дает Вам способ связи 
со всеми потоками в Вашем пуле. Например, когда пользователь завершает 
сервисное приложение, нужно корректно завершить все его потоки. Однако потоки 
ожидают порт ввода/вывода, и, так как соответствующие запросы не поступают, 
они не могут проснуться. Вызвав PostQueuedCompletionStatus по разу для каждого 
потока в пуле, Вы их разбудите. Они проверят значения, возвращенные 
GetQueuedCompktionStatus, обнаружат, что приложение закрывается, освободят все 
ресурсы и корректно завершатся. 
    Но будьте внимательны, используя подобный метод. В приведенном мной 
примере все нормально, так как потоки в пуле завершаются, не вызывая повторно 
GetQueuedCompletionStatus. Но, если Вы хотите о чем-то уведомить потоки и 
после этого разрешить им вновь вызвать GetQueuedCompletionStatus, могут 
возникнуть проблемы. Дело в том, что потоки пробуждаются по принципу 
очередности LIFO. Поэтому, чтобы каждый рабочий поток гарантированно обнаружил 
искусственный элемент очереди завершения, придется задействовать какой-нибудь 
дополнительный механизм синхронизации потоков — иначе возможна ситуация, когда 
один поток получит то же уведомление несколько раз. 

    Программа-пример lOCmpPrt 

    Программа lOCmpPrt (IOCMPPRT.EXE) (см. листинг на рис. 15-3) демонстрирует 
использование портов завершения ввода/вывода. Файлы исходного кода и ресурсов 
для этой программы находятся в каталоге IOCMPPRT на компакт-диске, прилагаемом 
к книге. Она просто копирует файл, выбранный пользователем, в новый файл 
IOCPMPRT.CPY. Запустив программу, Вы увидите диалоговое окно: 

    pathname: 
    Browse... |   |          |                 File size:   0 

    Чтобы выбрать файл, щелкните кнопку Browse (обзор). Закончив, Вы увидите, 
что программа обновила поля Pathname (полный путь) и File Size (размер файла). 
Щелчок кнопки Copy (копировать) приводит к вызову FileCopy, которая в свою 
очередь вызывает РгерагеDstFile (я уже рассказывал о ней, рассматривая 
программу AlertIO). 
    После открытия файла-источника и создания файла-приемника я вызываю 
CreateNewCompletionPort и создаю порт завершения ввода/вывода. (Написанная 
мною функция CreateNewCompletionPort создает порт вызовом \Ут32-функции 
CreateIoCompletionPort.) Создав порт, нужно связать с ним устройства. Для 
этого я вызываю функцию AssociateDeviceWithCompletionPort, тоже написанную на 
основе CreateIoCompletionPort. Теперь все выполненные запросы на ввод/вывод 
помещаются в очередь завершения порта. Каждому устройству назначается свой 
ключ завершения. Для операции чтения используется ключ СОМРKEY_READ, а для 
операции записи — COMPKEY_WRITE. 
    Теперь инициализируем набор запросов (структуры OVERLAPPED) и 
соответствующие буферы памяти. Для выдачи первого запроса на чтение я прибег к 
одному фокусу: вызываю PostQueuedCompletionStatus, чтобы поставить завершенный 
запрос на запись в оче

    508 _______________________________Windows для профессионалов 

    редь порта. При входе в основной цикл программа считает, что какой-то 
запрос на запись уже выполнен. Обрабатывая выполненный запрос, программа 
проверяет, не пытаемся ли мы читать за границей файла, и, если нет, вызывает 
ReadFile, выдающую запрос на чтение. 
    Основной цикл заканчивается, когда необработанных запросов больше нет. А 
если есть, вызывается GetQueuedCompletionStatus; она «усыпляет» поток, и он 
спит, пока в порт завершения не поступит выполненный запрос. После возврата из 
GetQueuedCompletionStatus программа проверяет ключ завершения — dwCompKey. 
Если он равен COMPKEY_READ, значит, завершился запрос на чтение из исходного 
файла. Тогда она вызывает WriteFile, выдающую запрос на запись в 
файл-приемник. А если dwCompKey равен COMPKEY_WRITE, вызывается ReadFile, 
формирующая запрос на чтение из исходного файла. 
    Но вот незавершенных запросов больше нет, и цикл заканчивается. С помощью 
CloseHandle программа уничтожает объект ядра «порт завершения ввода/вывода»; 
затем удаляет буферы в памяти и вызывает функцию CopyCleanup. Та закрывает оба 
файла и подгоняет размер файла-приемника. 

    IOCMPPRT.C 
    Модуль: lOCmpPrt.C 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    *************************************************************
    ffinclude ",.\CmnHdr,H"              /* см. приложение В */ 
    Sinclude <windows.h> 
    Sinclude <windowsx.h> 
    Sinclude <tchar,h> 
    ftinclude <stdio.h> 
    ftinclude Otdlib. h> 
    ftinclude "Resource.H" 
    ////////////////////////////////////////////////////////////    
    HANDLE CreateNewCompletionPort(DWORD cSimultaneousClients) { 
    return(CreateIoCompletionPort(INVALID_HANDLE_VALUE, 
    NULL, 0, cSimultaneousClients)); 
    BOOL AssociateDeviceWithCompletionPort( 
    
    HANDLE hCompPort, HANDLE hDevice, DWORD dwCompKey) { 
    HANDLE h = CreateIoCompletionPort(nDevice, 
    hCompPort, dwCompKey, 0); 
    return(h == hCompPort): 
    ////////////////////////////////////////////////////////////
    static BOOL PrepareDstFile(LPCTSTR pszFileSrc, 
    LPCTSTR pszFileDst, PHANDLE phFileSrc, PHANDLE phFileDst, 

    см. след. стр. 

    Рис. 15-3. Листинг программы lOCmpPrt 


    _________________________ Глава 15. Устройства: ввод/вывод 509 


    РИС. 15-3 (продолжение) 

    PULARGE_INTEGER pulFileSize, DWORD dwPageSize) { 
    ULARGE_INTEGER ulFileSizeDst; 
    DWORD dwError; 
    BOOL fOk = FALSE; 
    _-try { 
    // открываем исходный файл для чтения 
    
    *phFileSrc = CreateFile(pszFileSrc, GENERIC_READ, 
    FILE_SHARE_READ, NULL, OPEN_EXISTING, 
    FILE_FLAG_NO_BUFFERING ; FILE_FLAG_OVERLAPPED, NULL): 
    
    if (*phFile8rc == INVALID_HANDLE,VALUE) 
    __leave; 
    // создаем файл-приемник, в который будем записывать 
    
    *phFileDst = CreateFile(pszFileDst, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 
    FILE_FLAG_NO_BUFFERING ; FILE_FLAG OVERLAPPED, *phFileSrc); 
    
    if (*phFileDst == INVALID_HANDLE_VALUE) 
    __leave; 
    
    // определяем размер исходного файла 
    pulFileSize->LowPart = GetFileSize(*phFileSrc, &pulFileSize->HighPart); 
    
    // округляем размер исходного файла до ближайшего 
    // большего значения, кратного числу страниц 
    ulFileSizeDst.QuadPart = (pulFile8ize->QuadPart + 
    dwPageSize - 1) & ~(dwPageSize - 1); 
    
    // устанавливаем размер файла-приемника 
    dwError = SetFilePointer(*phFlleDst, ulFxIeSizeOst.LowPart, 
    
    (PLONG) ulFileSizeDst,HighPart, FILE_BEGIN): 
    if ((dwError == Oxffffffff) && (GetLastError() != NO_ERROR)) 
    __leave; 
    if (!SetEndOfFile(*phFileDst)) 
    __leave; 
    fOk = TRUE; // все прошло удачно 
    » 
    f 
    „finally { 
    // если произошла какая-нибудь ошибка, все очищаем 
    if (!f0k) { 
    if (*phFileSrc != INVALID_HANDLE_VALUE) { 
    CloseHandle(*phFileSrc); 
    *phFileSrc = INVALID_HANDLE_VALUE; 
    } 

    см. след. стр. 


    510 ______________________________Windows для профессионалов 


    РИС. 15-3 (продолжение) 

    if (*phFileSrc != INVALID_HANDLE_VALUE) { 
    CloseHandle(*phFileDst); 
    *phFileDst = INVALID_HANDLE_VALUE; 
    } 
    f 
    return(fOk); 
    } 
    /////////////////////////////////////////////////////////////////////////////// 
    
    static void CopyCleanup(LPCTSTR pszFileDst, HANDLE hFileSrc, 
    HANDLE hFileOst, PULARGE_INTEGER pulFileSize) { 
    
    // закрываем исходный и конечный файлы 
    CloseHandle(hFileDst); 
    CloseHandle(hFileSrc); 
    
    // Необходим еще один описатель файла-приемника, открытого 
    // без флага FILE_FLAG_NO_BUFFERING. Это позволит установить 
    // маркер конца файла на позицию, не кратную длине сектора. 
    hFileDst = CreateFile(pszFileDst, GENERIC_WRITE, 
    FILE_SHARE_READ ; FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); 
    if (hFileDst != INVALID_HANDLE_VALUE) { 
    
    // устанавливаем длину файла-приемника равной длине исходного 
    // файла, если длина последнего не кратна числу страниц 
    SetFilePointer(hFileDst, pulFileSize->LowPart, 
    (PLONG) pulFileSize->HighPart, FILE_BEGIN); 
    SetEndOfFile(hFileDst); 
    CloseHandle(hFileDst); 
    I / 
    I I 
    /////////////////////////////////////////////////////////////////////////////// 
    typedef struct { 
    
    // размер станиц, используемый на данном компьютере 
    DWORD dwPageSize; 
    
    // описатели исходного файла и файла-приемника 
    HANDLE hFileSrc, hFileDst; 
    
    // размер исходного файла 
    ULARGE_INTEGER ulFileSize; 
    
    // смещение в исходном файле, откуда начнется следующая операция чтения 
    ULARGE_INTEGER ulNextReaddffset; 

    см. след. стр. 

    _________________________Глава 15. Устройства: ввод/вывод 511 
    РИС. 15-3 (продолжение) 
    

    II число незавершенных запросов на ввод/вывод 
    int nReadsInProgress; 
    int nWritesInProgress; 
    } COPYSTATE, *PCOPYSTATE; 
    COPYSTATE g_cs: 
    
    // каждый запрос требует структуру OVERLAPPED и буфер 
    typedef struct { 
    OVERLAPPED Overlapped; 
    PBYTE      pbData; 
    } IOREQ, *PIOREQ: 
    
    Sdefine MAX_PENDING_IO_REQS  4 
    (tdefine BUFFSIZE              (64 * 1024) 
    
    // эти ключи определяют тип выполненного запроса 
    ftdefine COMPKEY_REAO   1 
    ffdefine COMPKEY_WRITE 2 
    
    /////////////////////////////////////////////////////////////////////////////// 
    BOOL FileCopy (LPCTSTR pszFileSrc, LPCTSTR pszFileDst) { 
    
    // нам нужна локальная копия переменной состояния 
    COPYSTATE cs; 
    
    // сохраняем информацию о каждом запросе 
    IOREQ IOReq[MAX_PENDING_IO_REQS] = { 0 }; 
    SYSTEM_INFO si; 
    int nIOReq; 
    BOOL fOk, fDone = FALSE; 
    
    // порт завершения ввода/вывода, куда направляются 
    // выполненные запросы на чтение и запись 
    HANDLE hIOCompPort = NULL; 
    DWORD dwNumBytesXfer, dwCompKey; 
    LPOVERLAPPED pOverlapped; 
    
    // инициализируем глобальную переменную COPYSTATE 
    GetSystemInfo(&si); 
    cs.dwPageSize = si.dwPageSize; 
    cs.ulNextReadOffset.QuadPart = 0; 
    cs.nReadsInProgress = 0; 
    cs.nWritesInProgress = 0: 

    см. слей. стр. 

    512 ______________________________Windows для профессионалов 


    РИС. 15-3 (продолжение) 
    
    I / открываем исходный файл для чтения 
    f0k = PrepareDstFile(pszFileSrc, pszFileDst, &cs.hFileSrc. &cs.hFileDst, 
    &cs,ulFileSize, cs.dwPageSize); 
    if (!f0k) 
    return(fOk); 
    
    // создаем новый порт завершения 
    hIOCompPort = CreateNewCompletionPort(O); 
    chASSERT(hIOCompPort != NULL); 
    
    // Связываем выполненный запрос на ввод/вывод в исходный файл 
    // с портом завершения. Все выполненные запросы имеют ключ COMPKEY_READ, 
    chVERIFY(AssociateDeviceWithCompletionPort(hIOCompPort, 
    cs.hFileSrc, COMPKEY_READ)); 
    
    // Связываем выполненный запрос на ввод/вывод в файл-приемник 
    // с портом завершения. Все выполненные запросы имеют ключ COMPKEY_WRITE. 
    chVERIFY(AssociateDeviceWithCoinpletionPort(hIOCompPort, 
    cs.hFileDst, COMPKEY_WRITE)); 
    // запускаем алгоритм копирования, выдавая группу запросов 
    // на чтение из исходного файла 
    for (nIOReq = 0; nIOReq < MAX_PENDING_IO_REQS; nIOReq++) { 
    IOReq[nIOReq].Overlapped.Internal = 0; 
    IOReq[nIOReq].Overlapped.InternalHigh = 0; 
    IOReq[nIOReq].Overlapped.Offset = 0; 
    IOReq[nIOReq].Overlapped.OffsetHigh = 0; 
    IOReq[nIOReq].Overlapped.hEvent = NULL; 
    IOReq[nIOReq].pbData = VirtualAlloc(NULL, 
    BUFFSIZE, MEM_RESERVE ; MEM_COMMIT, PAGE_READWRITE): 
    cs.nWrites!nProgress++; 
    PostQueuedCompletionStatus(hIOCompPort, 0, 
    COMPKEY_WRITE, &IOReq[nIOReq].Overlapped): 
    \ 
    f 
    I/ продолжаем цикл, пока не произойдет ошибка или 
    // файл-приемник не будет записан полностью 
    while ((cs,nReadsInProgress > 0) !! (cs.nWritesInProgress > 0)) { 
    PIOREQ pIOReq; 
    f0k = GetQueuedCompletionStatus(hIOCompPort, 
    &dwNumBytesXfer, &dwCompKey, &p0verlapped, INFINITE); 
    ChASSERT(fOk ;; (GetLastErrorO == NO_ERROR)); 
    pIOReq = (PIOREQ) pOverlapped; 
    
    switch (dwCortipKey) { 
    case COMPKEY_REAO: 
    cs.nReadsInProgress--; 

    см. след. стр. 

    _________________________Глава 15. Устройства: ввод/вывод 513 


    РИС. 15~3 (продолжение) 
    
    I/ округляем число байтов до ближайшего большего значения, 
    // чтобы закончить запись на границе сектора 
    dwNumBytesXfer = (dwNumBytesXfer + cs.dwPageSize - 1) & 
    -(cs.dwPageSize - 1); 
    f0k = WriteFile(cs.hFileDst, pIOReq->pbData, 
    dwNumBytesXfer, &dwNuffiBytesXfer, pOverlapped); 
    // проверяем, выдан ли запрос 
    chASSERT(fOk ;: (!f0k && (GetLastError() == ERROR_IO_PENDING))); 
    cs.nWritesInProgress++; 
    break: 
    case COMPKEY_WRITE: 
    cs.nWritesInProgress--; 
    
    if (cs.ulNextReadOffset,QuadPart >= cs.ulFileSize.QuadPart) 
    break; 
    // мы пока не читаем за концом файла; 
    
    // считываем следующую порцию данных 
    p0verlapped->0ffset       = cs.ulNextReadOffset.LowPart; 
    pOverlapped->OrfsetHigh   = cs.ulNextReadOffset.HighPart; 
    
    f0k = ReadFile(cs.hFileSrc, pIOReq->pbData, 
    BUFFSIZE, &dwNumBytesXfer, pOverlapped); 
    
    // операция чтения либо закончилась, 
    // либо ждет своей очереди 
    chASSERT(fOk ;; (!f0k && (GetLastError() == ERROR_IO_PENDING))); 
    cs.nReadsInProgress++; 
    
    // смещение в исходном файле, откуда 
    // начнется следующая операция чтения 
    cs.ulNextReadOffset.QuadPart += BUFFSIZE; 
    break; 
    } 
    
    // уничтожаем порт завершения ввода/вывода 
    CloseHandle(hIOCompPort); 
    
    // освобождаем буферы, использованные при копировании 
    for (nIOReq = 0; nIOReq < MAX_PENDING_IO_REQS: nIOReq++) { 
    
    VirtualFree(H)Req[nIOReq].pWata, 0, ME»_RELEASE), 
    } 
    
    II закрываем оба файла и устанавливаем длину 
    // конечного файла равной длине исходного 
    CopyCleanup(pszFileDst, cs.hFileSrc, 
    cs.hFileDst, &cs.ulFileSize); 
    return(TRUE); 
    } 
    ///////////////////////////////////////////////

    см. след. стр. 

    514 ______________________________Windows для профессионалов 


    РИС. 15-3 (продолжение) 

    BOOL DIgJMnitDialog (HWND hwnd, HWND hwndFocus, LPARAM IParam) { 
    
    // связываем значок с диалоговым окном 
    chSETDLGICONS(hwnd, IDI_IOCMPPRT, IDI_IOCMPPRT): 
    
    // отключаем кнопку Copy, так как файл еще не выбран 
    EnableWindow(GetDlgItem(hwnd, IDOK), FALSE); 
    return(TRL)E); 
    1 
    / 
    /////////////////////////////////////////////////////////////////////////////// 
    
    void Dlg_OnCofH(Tiand (HWND hwnd, int id, HWND hwndCtI, DINT codeNotify) { 
    TCHAR szPathname[_MAX_DIR]; 
    BOOL fOk: 
    OPENFILENAME ofn; 
    switch (id) { 
    case IDOK: 
    
    // копируем исходный файл в файл-приемник 
    Static_GetText(GetDlgItem(hwnd, IDC_SRCFILE), 
    szPathname, sizeof(szPathname)); 
    SetCursor(LoadCursor(NULL, IDC_WAIT)); 
    FileCopy(szPathname, __TEXT("IOC(npPrt.CPY")); 
    break: 
    case IDC_BROWSE: 
    chINITSTRUCT(ofn, TRUE); 
    ofn.hwndOwner = hwnd; 
    ofn.lpstrFilter = __TEXT("*.*\0"); 
    _tcscpy(szPathnaiDe, __TEXT("*. *")); 
    ofn.IpstrFile = szPathname; 
    ofn.nMaxFile = sizeof(szPathname): 
    ofn.Flags = OFN_FILEMUSTEXIST; 
    fOk = GetOpenFileName(&ofn); 
    if (fOk) { 
    HANDLE hFile: 
    Static_SetText(GetDlgItem(hwnd, IDC_SRCFILE), szPathname); 
    
    hFile = CreateFile(szPathname, GENERIC_READ, 
    0, NULL, OPEN_EXISTING, 0, NULL); 
    
    SetDlgItemInt(hwnd, IDG_SRCFILESIZE, 
    GetFileSize(hFile, NULL), FALSE); 
    CloseHandle(hFile); 
    ( 

    см. след. стр. 

    _________________________ Глава 15. Устройства: ввод/вывод 515 


    РИС. 15-3 (продолжение) 
    
    II активизируем кнопку Copy, если файл выбран 
    // и полный путь корректен 
    GetWindowText(GetDlgItem(hwnd, IOC_SRCFILE), 
    szPathname, sizeof(szPathname)): 
    EnableWindow(Get01gItefn(hwnd, IDOK), 
    szPathname[0] != __TEXT(•(•)); 
    if (fOk) { 
    
    // если пользователь щелкнул кнопку ОК в диалоговом окне 
    // для выбора файла, устанавливаем фокус на кнопку Copy 
    FORWARD_WM_NEXTDLGCTL(hwnd, 
    GetDlgItein(hwnd, IOOK), TRUE, SendMessage); 
    } 
    break: 
    case IDCANCEL: 
    EndDialog(hwnd, id); 
    break; 
    } 
    / 
    llil/llll//llllll/llil/lll/ll!l/lll/lllllll/ll///l/lltll/l/llllllllllll/ll//lll 
    BOOL CALLBACK Dlg_Proc (HWND hwnd, DINT uMsg, WPARAM wParam, LPARAM 
IParam) { 
    switch (uMsg) { 
    chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog); 
    chHANDLE_DLGMSG(hwnd, WM_COMMAND, 01g_OnCoinmand); 
    retLirn(FALSE); 
    } 
    11111111111111111111/1/illllllllllll/IIHIIIIIIIIItilllll/llllll/lllllllllllll/ 
    int WINAPI _tWinMain (HINSTANCE hinstExe, 
    HINSTANCE hinstPrev, LPTSTR pszCmdLine, int nCmdShow) { 
    chWARNIFUNICODEUNOERWIN95(); 
    DialogBox(hinstExe, MAKEINTRESOURCE(IDD_IOCMPPRT), NULL, Dlg_Proc); 
    return(O); 
    } 
    ///////////////////конец файла ///////////////////////////////


    516 ______________________________Windows для профессионалов 


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

    Win32 SDK, в который включено несколько новых функций, появившихся в 
Windows NT 4, определяет также'макрос HasOverlappedIoCompleted: 
    ffdefine HasOverlappedIoCompleted(lpOverlapped) \ 
((lp0verlapped)->lnternal != STATUS_PENDING) 
    Поскольку это макрос, его можно использовать и в предыдущих версиях 
Windows NT. Он позволяет быстро проверить, выполнен ли определенный запрос на 
ввод/вывод. Для этого достаточно передать ему адрес структуры OVERLAPPED, 
связанной с данным запросом. Если запрос еще выполняется, макрос возвращает 
FALSE, а завершен — TRUE. 
    Если запрос завершен, можно узнать подробности, либо вызвав 
GetOverlappedResult или GetQueuedCompletionStatus, либо переведя поток в 
«тревожное» состояние. И тем самым функция обратного вызова (процедура 
завершения) получит шанс на выполнение. 

    Отмена незавершенного запроса на ввод/вывод 

    В Windows NT 4 появилась новая функция, отменяющая незавершенные запросы 
на ввод/ вывод для определенного устройства: 

    BOOL CancelIO(HANDLE hFile): 

    Заметьте: она отменяет только запросы, инициированные вызывающим потоком. 
Запросы, выданные другими потоками, отменить нельзя. При неудаче функция 
возвращает FALSE. 
    Если запрос на ввод/вывод сделан через ReadFileEx или WriteFileEx, то 
CancelIO ставит элемент в АРС-очередь потока. Если поток в «тревожном» 
состоянии, процедура завершения вызывается с кодом ошибки ERROR_OPERATION_ABORTED. 
    Если выполненные запросы ставятся в очередь порта завершения, происходит 
возврат из GetQueuedCompletionStatus. Последующий вызов GetLastError дает код ERROR_OPERATION_ABORTED. 
    Помните также, что вызов CloseHandle для устройства, по которому есть 
незавершенные запросы на ввод/вывод, влечет отмену этих запросов. 