Структуры overlapped
С помощью структуры OVERLAPPED (указываемой, например, параметром lpOverlapped функции ReadFile) можно указывать следующую информацию:
• Позицию в файле (64 бита), с которой должно начинаться выполнение операции чтения или записи в соответствии с обсуждением, которое содержится в главе 3.
• Событие (сбрасываемое вручную), которое будет переходить в сигнальное состояние по завершении соответствующей операции.
Ниже приводится определение структуры OVERLAPPED.
typedef struct_OVERLAPPED {
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
HANDLE hEvent;
} OVERLAPPED
Для задания позиции в файле (указателя) должны использоваться оба поля Offset и OffsetHigh, хотя старшая часть указателя (OffsetHigh) во многих случаях равна 0. Не следует использовать поля Internal и InternalHigh, зарезервированные для системных нужд.
Параметр hEvent — дескриптор события (созданного посредством функции CreateEvent). Это событие может быть как именованным, так и неименованным, но оно должно быть обязательно сбрасываемым вручную (см. главу 8), если используется для перекрывающегося ввода/вывода; причины этого будут вскоре объяснены. По завершении операции ввода/вывода событие переходит в сигнальное состояние.
В другом возможном варианте его использования дескриптор hEvent имеет значение NULL; в этом случае программа может ожидать перехода в сигнальное состояние дескриптора файла, который также может выступать в роли объекта синхронизации (см. приведенные далее предостережения). Система использует для отслеживания завершения операций сигнальные состояния дескриптора файла, если дескриптор hEvent равен NULL, то есть объектом синхронизации в этом случае является дескриптор файла.
Примечание
В целях удобства термин "дескриптор файла" ("file handle"), используемый по отношению к дескрипторам, указываемым при вызове функций ReadFile, WriteFile и так далее, будет применяться нами даже в тех случаях, когда речь идет о дескрипторах именованного канала или устройства, а не файла.
При выполнении вызова функций ввода/вывода это событие сразу же сбрасывается системой (устанавливается в несигнальное состояние). Когда операция ввода/вывода завершается, событие устанавливается в сигнальное состояние и остается в нем до тех пор, пока не будет использовано другой операцией ввода/вывода. Событие должно быть сбрасываемым вручную, если его перехода в сигнальное состояние могут ожидать несколько потоков (хотя в наших примерах используется всего один поток), и на момент завершения операции они могут не находиться в состоянии ожидания.
Даже если дескриптор файла является синхронным (то есть созданным без флага FILE_FLAG_OVERLAPPED), структура OVERLAPPED может послужить в качестве альтернативы функции SetFilePointer для указания позиции в файле. В этом случае возврат после вызова функции ReadFile или иного вызова не происходит до тех пор, операция ввода/вывода пока не завершится. Этой возможностью мы уже воспользовались в главе 3. Также обратите внимание на то, что незавершенные операции ввода/вывода однозначно идентифицируются комбинацией дескриптора файла и соответствующей структуры OVERLAPPED.
Ниже перечислены некоторые предостережения, которые следует принимать во внимание.
• Не допускайте повторного использования структуры OVERLAPPED в то время, когда связанная с ней операция ввода/вывода, если таковая имеется, еще не успела завершиться.
• Аналогичным образом, избегайте повторного использования события, указанного в структуре OVERLAPPED.
• Если существует несколько незакрытых запросов, относящихся к одному и тому же перекрывающемуся дескриптору, используйте для синхронизации не дескрипторы файлов, а дескрипторы событий.
• Если структура OVERLAPPED или событие выступают в качестве автоматических переменных внутри блока, обеспечьте невозможность выхода из блока до синхронизации с операцией ввода/вывода. Кроме того, во избежание утечки ресурсов следует позаботиться о закрытии дескриптора до выхода из блока.
Состояния перекрывающегося ввода/вывода
Возврат из функций ReadFile и WriteFile, а также двух указанных выше функций, относящихся к именованным каналам, в случаях, когда они используются для выполнения перекрывающихся операций ввода вывода, осуществляется немедленно. В большинстве случаев операция ввода/вывода к этому моменту завершена не будет, и возвращаемым значением при чтении и записи будет FALSE. Функция GetLastError возвратит в этой ситуации значение ERROR_IO_PENDING.
По окончании ожидания перехода объекта синхронизации (события или, возможно, дескриптора файла) в сигнальное состояние, свидетельствующее о завершении операции, вы должны выяснить, сколько байтов было передано. В этом и состоит основное назначение функции GetOverlappedResult.
BOOL GetOverlappedResult(HANDLE hFile, LPOVERLAPPED lpOverlapped, LPWORD lpcbTransfer, BOOL bWait)
Указание конкретной операции ввода/вывода обеспечивается сочетанием дескриптора и структуры OVERLAPPED. Значение TRUE параметра bWait указывает на то, что до завершения операции функция GetOverlappedResult должна находиться в состоянии ожидания; в противном случае возврат из функции должен быть немедленным. В любом случае эта функция будет возвращать значение TRUE только после успешного завершения операции. Если возвращаемым значением функции GetOverlappedResult является FALSE, то функция GetLastError возвратит значение ERROR_IO_INCOMPLETE, что позволяет вызывать эту функцию для опроса завершения ввода/вывода.
Количество переданных байтов хранится в переменной *lpcbTransfer. Всегда убеждайтесь в том, что с момента ее использования в операции перекрывающегося ввода/вывода структура OVERLAPPED остается неизменной.
Отмена выполнения операций перекрывающегося ввода/вывода
Булевская функция CancelIO позволяет отменить выполнение незавершенных операций перекрывающегося ввода/вывода, связанных с указанным дескриптором (у этой функции имеется всего лишь один параметр). Отменяется выполнение всех инициированных вызывающим потоком операций, использующих данный дескриптор. На операции, инициированные другими потоками, вызов этой функции никакого влияния не оказывает. Отмененные операции завершаются С ошибкой ERROR OPERATION ABORTED.
Пример: использование дескриптора файла в качестве объекта синхронизации
Перекрывающийся ввод/вывод очень удобно и просто реализуется в тех случаях, когда может существовать только одна незавершенная операция. Тогда для целей синхронизации программа может использовать не событие, а дескриптор файла.
Приведенный ниже фрагмент кода показывает, каким образом программа может инициировать операцию чтения для считывания части файла, продолжить свое выполнение для осуществления других видов обработки, а затем перейти в состояние ожидания перехода дескриптора файла в сигнальное состояние.
OVERLAPPED ov = { 0, 0, 0, 0, NULL /* События не используются. */ };
HANDLE hF;
DWORD nRead;
BYTE Buffer[BUF_SIZE];
…
hF = CreateFile( …, FILE_FLAG_OVERLAPPED, … );
ReadFile(hF, Buffer, sizeof(Buffer), &nRead, &ov);
/* Выполнение других видов обработки. nRead не обязательно достоверно.*/
/* Ожидать завершения операции чтения. */
WaitForSingleObject(hF, INFINITE);
GetOverlappedResult(hF, &ov, &nRead, FALSE);
Пример: преобразование файлов с использованием перекрывающегося ввода/вывода и множественной буферизации
Программа 2.4 (atou) осуществляла преобразование ASCII-файла к кодировке UNICODE путем последовательной обработки файла, а в главе 5 было показано, как выполнить такую же последовательную обработку с помощью отображения файлов. В программе 14.1 (atouOV) та же самая задача решается с использованием перекрывающегося ввода/вывода и множественных буферов, в которых хранятся записи фиксированного размера.
Рисунок 14.1 иллюстрирует организацию программы с четырьмя буферами фиксированного размера. Программа реализована таким образом, чтобы количество буферов можно было определять при помощи символической константы препроцессора, но в нижеследующем обсуждении мы будем предполагать, что существуют четыре буфера.
Сначала в программе выполняется инициализация всех элементов структур OVERLAPPED, определяющих события и позиции в файлах. Для каждого входного и выходного буферов предусмотрена отдельная структура OVERLAPPED. После этого для каждого из входных буферов инициируется операция перекрывающегося чтения. Далее с помощью функции WaitForMultipleObjects в программе организуется ожидание одиночного события, указывающего на завершение чтения или записи. При завершении операции чтения входной буфер копируется и преобразуется в соответствующий выходной буфер, после чего инициируется операция записи. При завершении записи инициируется следующая операция чтения. Заметьте, что события, связанные с входными и выходными буферами размещаются в единственном массиве, который используется в качестве аргумента при вызове функции WaitForMultipleObjects.
Рис. 14.1. Модель асинхронного обновления файла
Программа 14.1. atouOV: преобразование файла с использованием перекрывающегося ввода/вывода
/* Глава 14. atouOV
Преобразование файла из кодировки ASCII в кодировку Unicode с использованием перекрывающегося ввода/вывода. Программа работает только в Windows NT. */
#include "EvryThng.h"
#define MAX_OVRLP 4 /* Количество перекрывающихся операций ввода/вывода.*/
#define REC_SIZE 0x8000 /* 32 Кбайт: Минимальный размер записи, обеспечивающий приемлемую производительность. */
#define UREC_SIZE 2 * REC_SIZE
int _tmain(int argc, LPTSTR argv[]) {
HANDLE hInputFile, hOutputFile;
/* Каждый из элементов определенных ниже массивов переменных */
/* и структур соответствует отдельной незавершенной операции */
/* перекрывающегося ввода/вывода. */
DWORD nin[MAX_OVRLP], nout[MAX_OVRLP], ic, i;
OVERLAPPED OverLapIn[MAX_OVRLP], OverLapOut[MAX_OVRLP];
/* Необходимость использования сплошного, двумерного массива */
/* диктуется Функцией WaitForMultipleObjects. */
/* Значение 0 первого индекса соответствует чтению, значение 1 – записи.*/
HANDLE hEvents[2][MAX_OVRLP];
/* В каждом из определенных ниже двух буферных массивов первый индекс */
/* нумерует операции ввода/вывода. */
CHAR AsRec[MAX_OVRLP][REC_SIZE];
WCHAR UnRec[MAX_OVRLP][REC_SIZE];
LARGE_INTEGER CurPosIn, CurPosOut, FileSize;
LONGLONG nRecord, iWaits;
hInputFile = CreateFile(argv[1], GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
hOutputFile = CreateFile(argv[2], GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);
/* Общее количество записей, подлежащих обработке, вычисляемое */
/* на основе размера входного файла. Запись, находящаяся в конце, */
/* может быть неполной. */
FileSize.LowPart = GetFileSize(hInputFile, &FileSize.HighPart);
nRecord = FileSize.QuadPart / REC_SIZE;
if ((FileSize.QuadPart % REC_SIZE) != 0) nRecord++;
CurPosIn.QuadPart = 0;
for (ic = 0; ic < MAX_OVRLP; ic++) {
/* Создать события чтения и записи для каждой структуры OVERLAPPED.*/
hEvents[0][ic] = OverLapIn[ic].hEvent /* Событие чтения.*/
= CreateEvent(NULL, TRUE, FALSE, NULL);
hEvents[1][ic] = OverLapOut[ic].hEvent /* Событие записи. */
= CreateEvent(NULL, TRUE, FALSE, NULL);
/* Начальные позиции в файле для каждой структуры OVERLAPPED. */
OverLapIn[ic].Offset = CurPosIn.LowPart;
OverLapIn[ic].OffsetHigh = CurPosIn.HighPart;
/* Инициировать перекрывающуюся операцию чтения для данной структуры OVERLAPPED. */
if (CurPosIn.QuadPart < FileSize.QuadPart) ReadFile(hInputFile, AsRec[ic], REC_SIZE, &nin[ic], &OverLapIn[ic]);
CurPosIn.QuadPart += (LONGLONG)REC_SIZE;
}
/* Выполняются все операции чтения. Ожидать завершения события и сразу же сбросить его. События чтения и записи хранятся в массиве событий рядом друг с другом. */
iWaits =0; /* Количество выполненных к данному моменту операций ввода/вывода. */
while (iWaits < 2 * nRecord) {
ic = WaitForMultipleObjects(2 * MAX_OVRLP, hEvents[0], FALSE, INFINITE) – WAIT_OBJECT_0;
iWaits++; /* Инкрементировать счетчик выполненных операций ввода вывода.*/
ResetEvent(hEvents[ic / MAX_OVRLP][ic % MAX_OVRLP]);
if (ic < MAX_OVRLP) {
/* Чтение завершено. */
GetOverlappedResult(hInputFile, &OverLapIn[ic], &nin[ic], FALSE);
/* Обработать запись и инициировать операцию записи. */
CurPosIn.LowPart = OverLapIn[ic].Offset;
CurPosIn.HighPart = OverLapIn[ic].OffsetHigh;
CurPosOut.QuadPart = (CurPosIn.QuadPart / REC_SIZE) * UREC_SIZE;
OverLapOut[ic].Offset = CurPosOut.LowPart;
OverLapOut[ic].OffsetHigh = CurPosOut.HighPart;
/* Преобразовать запись из ASCII в Unicode. */
for (i =0; i < REC_SIZE; i++) UnRec[ic][i] = AsRec[ic][i];
WriteFile(hOutputFile, UnRec[ic], nin[ic] * 2, &nout[ic], &OverLapOut[ic]);
/* Подготовиться к очередному чтению, которое будет инициировано после того, как завершится начатая выше операция записи. */
CurPosIn.QuadPart += REC_SIZE * (LONGLONG)(MAX_OVRLP);
OverLapIn[ic].Offset = CurPosIn.LowPart;
OverLapIn[ic].OffsetHigh = CurPosIn.HighPart;
} else if (ic < 2 * MAX_OVRLP) { /* Операция записи завершилась. */
/* Начать чтение. */
ic –= MAX_OVRLP; /* Установить индекс выходного буфера. */
if (!GetOverlappedResult (hOutputFile, &OverLapOut[ic], &nout[ic], FALSE)) ReportError(_T("Ошибка чтения."), 0, TRUE);
CurPosIn.LowPart = OverLapIn[ic].Offset;
CurPosIn.HighPart = OverLapIn[ic].OffsetHigh;
if (CurPosIn.QuadPart < FileSize.QuadPart) {
/* Начать новую операцию чтения. */
ReadFile(hInputFile, AsRec[ic], REC_SIZE, &nin[ic], &OverLapIn[ic]);
}
}
}
/* Закрыть все события. */
for (ic = 0; ic < MAX_OVRLP; ic++) {
CloseHandle(hEvents[0][ic]);
CloseHandle(hEvents[1][ic]);
}
CloseHandle(hInputFile);
CloseHandle(hOutputFile);
return 0;
}
Программа 14.1 способна работать только под управлением Windows NT. Средства асинхронного ввода/вывода Windows 9x не позволяют использовать дисковые файлы. В приложении В приведены результаты и комментарии, свидетельствующие о сравнительно низкой производительности программы atouOV. Как показали эксперименты, для достижения приемлемой производительности размер буфера должен составлять, по крайней мере, 32 Кбайт, но даже и в этом случае обычный синхронный ввод/вывод работает быстрее. К тому же, производительность этой программы не повышается и в условиях SMP, поскольку в данном примере, в котором обрабатываются всего лишь два файла, ЦП не является критическим ресурсом.
Расширенный ввод/вывод с использованием процедуры завершения
Существует также другой возможный подход к использованию объектов синхронизации. Вместо того чтобы заставлять поток ожидать поступления сигнала завершения от события или дескриптора, система может инициировать вызов определенной пользователем процедуры завершения сразу же по окончании выполнения операции ввода/вывода. Далее процедура завершения может запустить очередную операцию ввода/вывода и выполнить любые необходимые действия по учету использования системных ресурсов. Эта косвенно вызываемая (callback) процедура завершения аналогична асинхронному вызову процедуры, который применялся в главе 10, и требует использования состояний дежурного ожидания (alertable wait states).
Каким образом процедура завершения может быть указана в программе? Среди параметров или структур данных функций ReadFile и WriteFile не остается таких, которые можно было бы использовать для хранения адреса процедуры завершения. Однако существует семейство расширенных функций ввода/вывода, которые обозначаются суффиксом "Ех" и содержат дополнительный параметр, предназначенный для передачи адреса процедуры завершения. Функциями чтения и записи являются, соответственно, ReadFileEx и WriteFileEx. Кроме того, требуется использование одной из указанных ниже функций дежурного ожидания.
• WaitForSingleObjectEx
• WaitForMultipleObjectsEx
• SleepEx
• SignalObjectAndWait
• MsgWaitForMultipleObjectsEx
Расширенный ввод/вывод иногда называют дежурным вводом/выводом (alertable I/O). О том, как использовать расширенные функции, рассказывается в последующих разделах.
Примечание
Под управлением Windows 9x расширенный ввод/вывод не может работать с дисковыми файлами и коммуникационными портами. В то же время, средства расширенного ввода/вывода Windows 9x способны работать с именованными каналами, почтовыми ящиками, сокетами и последовательными устройствами.