Операционные Системы
.pdfДля постановки запроса ввода-вывода в очередь драйвера устройства, используются функции ReadFile и WriteFile, где последний параметр указывает на заполненную структуру OVERLAPPED (см. Таблица 17).
Таблица 17. Структура OVERLAPPED.
typedef struct _OVERLAPPED {
//[out] Код ошибки
DWORD Internal;
//[out] Число переданных байт
DWORD InternalHigh;
//[in] младшие 32 бита смещения
DWORD Offset; указателя
//[in] старшие 32 бита смещения указателя
DWORD OffsetHigh;
//[in] Событие, связанное с операцией
HANDLE hEvent;
}OVERLAPPED, *LPOVERLAPPED;
DWORD Offset и OffsetHigh формируют начальную позицию, с которой будет происходить операция ввода-вывода.
HANDLE hEvent описатель события, которое перейдѐт в сигнальное состояние после окончания операции ввода-вывода. Если используется оповестительный (см. далее) вводвывод может содержать любое значение. Если события нет, то установите в NULL.
DWORD Internal служит для хранения кода ошибки операции ввода вывода.
DWORD InternalHigh после окончания операции содержит число переданных байт.
Уведомления о завершении ввода-вывода
Сигнализация объекта ядра «файл»
Каждая операция ввода-вывода над устройством переводит его в занятое состояние, с которым можно синхронизироваться Wait-функциями.
Пример 24. Синхронизация с объектом ядра «файл».
//Открываем файл для асинхронных операций
HANDLE hfile = CreateFile(...,FILE_FLAG_OVERLAPPED,...);
//Буфер для чтения
BYTE bBuffer[100];
//Будем читать начиная с позиции 345
OVERLAPPED o = { 0 }; o.Offset = 345;
//Читаем 100 байт
81
BOOL fReadDone = ReadFile(hfile, bBuffer, 100, NULL,&o);
DWORD dwError = GetLastError();
// Проверяем результат: Пошла асинхронная операция if (!fReadDone && (dwError == ERROR_IO_PENDING))
{
// Ждѐм окончания
WaitForSingleObject(hfile, INFINITE);
}
else if (fReadDone)
{
// Операция завершилась как синхронная
}
else
{
// Операция завершилась с ошибкой
}
Синхронизация с объектом ядра «событие»
С каждой операцией ввода-вывода можно связать объект событие, которое перейдѐт в сигнальное состояние после окончания операции. Описатель события передаѐтся в пятом поле структуры OVERLAPPED.
Пример 25. Синхронизация с объектом ядра «событие».
#define BUFSIZE 4096
//Исходный файл (должен существовать) const char szSrc[] = "c:\\Sample.txt";
//Конечный файл
const char szTgt[] = "c:\\New.txt";
OVERLAPPED oRead = {0, 0, 0, 0, CreateEvent(NULL, FALSE, FALSE, NULL)};
OVERLAPPED oWrite = {0, 0, 1024, 0, CreateEvent(NULL, FALSE, FALSE, NULL)};
// Всегда будем пользоваться VirtualAlloc для буфера char* Buf = (char*) VirtualAlloc(NULL, BUFSIZE,
82
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
int _tmain()
{
// Открытие заданного файла для чтения
HANDLE hSrc = CreateFile(szSrc, GENERIC_READ, 0, NULL, OPEN_EXISTING,
FILE_FLAG_OVERLAPPED, NULL);
// Создание файла для записи
HANDLE hTgt = CreateFile(szTgt, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS,
FILE_FLAG_OVERLAPPED, NULL);
// Если файлы открыты
if ((hSrc != INVALID_HANDLE_VALUE) && (hTgt != INVALID_HANDLE_VALUE))
{
// читаем из исходного файла
BOOL fOK = ReadFile(hSrc, Buf, BUFSIZE, NULL, &oRead);
if (!fOK)
{
if (GetLastError() == ERROR_IO_PENDING)
{
WaitForSingleObject(oRead.hEvent,
INFINITE);
}
else
{
printf("Ошибка чтения!"); CloseHandle(hSrc); return 0;
}
} else printf("Синхронная операция чтения");
// устанавливаем длину целевого файла.
SetFilePointer(hTgt, 1024+oRead.InternalHigh, NULL, FILE_BEGIN);
83
SetEndOfFile(hTgt);
// Записываем число байт, считанных ReadFile fOK = WriteFile(hTgt, Buf, oRead.InternalHigh,
NULL, &oWrite); if (!fOK)
{
if (GetLastError() == ERROR_IO_PENDING)
{
WaitForSingleObject(oWrite.hEvent,
INFINITE);
}
else
{
printf("Ошибка записи!"); CloseHandle(hTgt); return 0;
}
} else printf("Синхронная операция записи");
}
Оповестительный ввод–вывод
Создавая поток, система связывает с ним очередь асинхронно вызываемых процедур (APC – asynchronous procedure call). Выдавая запрос ввода-вывода, вы можете указать драйверу устройства, что необходимо занести запись в APC-очередь вызывающего потока. Для этого применяются две функции ReadFileEx и WriteFileEx:
Таблица 18. Прототипы функций Read Fi le E x и W ri t eFi le E x .
BOOL ReadFileEx(
HANDLE hfile,
PVOID pvBuffer,
DWORD nNumBytesToRead,
OVERLAPPED* pOverlapped,
LPOVERLAPPED_COMPLETION_ROUTINE
pfnCompletionRoutine);
BOOL WriteFileEx(
84
HANDLE hfile, CONST VOID *pvBuffer,
DWORD nNumBytesToWrite, OVERLAPPED* pOverlapped,
LPOVERLAPPED_COMPLETION_ROUTINE pfnCompletionRoutine);
Первые четыре параметра функций совпадают с параметрами ReadFile и WriteFile, в пятом параметре передаѐтся указатель на процедуру APC:
Таблица 19. Прототип процедуры APC.
VOID WINAPI CompletionRoutine(
DWORD dwError,
DWORD dwNumBytes,
OVERLAPPED* po);
DWORD dwError – код ошибки операции ввода-вывода. DWORD dwNumBytes – число переданных байт.
OVERLAPPED* po – указатель на структуру OVERLAPPED.
Когда подсистема ввода-вывода закончит обработку запроса, APC– процедура не вызывается до тех пор, пока поток не перейдѐт в так называемое «тревожное» состояние. Это состояние говорит системе о том, что поток готов обработать все процедуры из APC–очереди. В это состояние поток переходит, вызвав одну из следующих функций:
SleepEx(DWORD dwTime, BOOL fAlertable);
WaitForSingleObjectEx(HANDLE hObject, DWORD dwTime, BOOL fAretable);
WaitForMultipleObjectsEx(DWORD dwObjects, PHANDLE phObjects, BOOL fWaitAll, DWORD dwTimeOut, BOOL fAlertable);
Последний параметр нужно установить в TRUE для перевода потока в «тревожное» состояние. Все функции останавливают поток либо до появления события, либо до появления процедур в APC–очереди. Если в очереди существует хотя бы одна APC–процедура поток не останавливается, а переключается на еѐ выполнение.
Пример 26. Использование оповестительного ввода–вывода.
#define BUFSIZE 4096
// Исходный файл (должен существовать)
85
const char szSrc[] = "e:\\Sample.txt"; // Конечный файл
const char szTgt[] = "e:\\New.txt";
OVERLAPPED oRead = {0, 0, 0, 0, NULL}; OVERLAPPED oWrite = {0, 0, 1024, 0, NULL};
// Открытие заданного файла для чтения
HANDLE hSrc = CreateFile(szSrc, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
// Создание файла для записи
HANDLE hTgt = CreateFile(szTgt, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);
//Процедура обработки завершения записи в файл
VOID CALLBACK IoWriteComplete( DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped )
{
printf("Запись закончена!"); CloseHandle(hTgt);
}
//Процедура обработки завершения чтения из файла
VOID CALLBACK IoReadComplete( DWORD dwErrorCode, DWORD dwNumberOfBytesTransfered, LPOVERLAPPED lpOverlapped
)
{
printf("Чтение закончено"); CloseHandle(hSrc);
}
int _tmain()
{
// Всегда будем пользоваться VirtualAlloc для буфера char* Buf = (char*) VirtualAlloc(NULL, BUFSIZE,
MEM_COMMIT | MEM_RESERVE,PAGE_READWRITE);
// Если файлы открыты
86
if ((hSrc != INVALID_HANDLE_VALUE) && (hTgt != INVALID_HANDLE_VALUE))
{
// читаем из исходного файла
BOOL fOK = ReadFileEx(hSrc, Buf, BUFSIZE, &oRead, IoReadComplete);
//Переходим в тревожное состояние
SleepEx(INFINITE, TRUE); SetFilePointer(hTgt, 1024+oRead.InternalHigh,
NULL, FILE_BEGIN); SetEndOfFile(hTgt);
//Записываем в целевой файл
WriteFileEx(hTgt, Buf, oRead.InternalHigh,
&oWrite, IoWriteComplete);
// Переходим в тревожное состояние
SleepEx(INFINITE, TRUE);
}
// Освобождаем буфер
VirtualFree(Buf, 0, MEM_RELEASE);
}
Порты завершения ввода-вывода
Основная проблема с оповестительным вводом-выводом состоит в невозможности обработки результатов запроса в контексте другого потока. Другие методы позволяют это делать, но это сопровождается существенными сложностями в программировании и отладке этих приложений. Кроме того, одновременная обработка несколькими потоками результатов операций ввода-вывода может привести к существенному падению производительности системы из-за необходимости частого контекстного переключения между работающими потоками. Для решения этих проблем разработан один из самых сложных объектов ядра – порт завершения ввода-
вывода (Input/Otput Completion Port).
Суть метода состоит в следующем. Каждое устройство ввода-вывода связывается с объектом ядра «порт завершения». Порт завершения держит «наготове» пул потоков, которые могут обработать результаты запроса. При
87
завершении операции ввода-вывода, еѐ результаты помещаются в очередь порта. Если число запущенных потоков меньше некоторого ограничения, которое задаѐтся при создании порта, порт ввода вывода активизирует один поток из пула и даѐт ему обработать операцию ввода-вывода. Система реализует LIFO принцип для активизации потоков пула, это значит, что следующий результат ввода-вывода будет обработан тем же потоком.
На сегодняшний день порт ввода-вывода реализует самый эффективный и масштабируемый способ организации асинхронных операций ввода-вывода в системах Windows NT.
Для организации работы с портом завершения ввода – вывода выполните следующие шаги:
1.Создайте объекты «файл» с флагом FILE_FLAG_OVERLAPPED.
2.Создайте объект «порт ввода-вывода» и свяжите его с объектами «файл».
3.Создайте пул потоков, ожидающих результаты ввода-вывода.
4.Используйте функции ReadFile и WriteFile для работы с устройством. Все результаты запросов будут попадать в очередь порта ввода-вывода и обрабатываться потоками из вашего пула.
Таблица 20. Функция Cr ea teIo Co ml et io nP o rt .
HANDLE CreateIoCompletionPort(
HANDLE |
hfile, |
HANDLE |
hExistingCompPort, |
ULONG_PTR CompKey, |
|
DWORD |
dwNumberOfConcurrentThreads); |
HANDLE |
hfile – описатель файла для ассоциации с портом |
HANDLE |
hExistingCompPort – описатель существующего порта для связывания с |
ним файла. |
|
ULONG_PTR CompKey – некоторое число, которое связано с файлом
DWORD dwNumberOfConcurrentThreads – максимальное число одновременно запущенных потоков. Если передать 0, это число устанавливается равным числу процессоров в системе.
Потоки в пуле должны «спать», пока не появится выполненный запрос ввода-вывода. Это достигается функцией GetQueuedCompletionStatus.
Таблица 21. Функция Ge t Q ue ued Co mp le tio n St at u s .
BOOL GetQueuedCompletionStatus(
HANDLE |
hCompPort, |
PDWORD |
pdwNumBytes, |
PULONG_PTR CompKey, |
|
OVERLAPPED** ppOverlapped, |
|
DWORD |
dwMilliseconds); |
HANDLE |
hCompPort – описатель порта ввода-вывода |
|
88 |
PDWORD pdwNumBytes – число переданных байтов PULONG_PTR CompKey – ключ операции ввода-вывода.
OVERLAPPED** ppOverlapped – структура OVERLAPPED, связанная с операцией. DWORD dwMilliseconds – как долго ждать появления результатов запроса.
Возвращаемые значения
TRUE, если появился результат операции за заданное время.
FALSE – в случае ошибки. Если выход по тайм-ауту, то GetLastError() вернѐт
WAIT_TIMEOUT.
Ещѐ одна полезная функция PostQueuedCompletionStatus
предназначена для симуляции завершения операции ввода-вывода. Еѐ удобно использовать для подачи команд в порт ввода-вывода, например, для завершения потоков в пуле.
Таблица 22. Функция P o st Co mp let io n Sta t u s .
BOOL PostQueuedCompletionStatus(
HANDLE hCompPort,
DWORD dwNumBytes,
ULONG_PTR CompKey,
OVERLAPPED* pOverlapped);
HANDLE |
hCompPort – описатель порта ввода-вывода |
|
DWORD |
dwNumBytes – число переданных байт. |
|
ULONG_PTR |
CompKey – ключ операции ввода-вывода. |
|
OVERLAPPED* |
pOverlapped – Указатель на структуру OVERLAPPED |
Пример 27. Использование порта завершения ввода-вывода.
#define BUFSIZE 4096
#define POOL 2 // Число потоков в пуле #define CK_READ 0 //Ключ: чтение закончено #define CK_WRITE 1 // Ключ: запись закончена #define CK_EXIT 0xFF // Ключ: выход из потока
//Исходный файл (должен существовать) const char szSrc[] = "e:\\Sample.txt";
//Конечный файл
const char szTgt[] = "e:\\New.txt";
OVERLAPPED ovr = {0};
// Всегда будем пользоваться VirtualAlloc для буфера char* Buf = (char*) VirtualAlloc(NULL, BUFSIZE,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// Открытие заданного файла для чтения
89
HANDLE hSrc = CreateFile(szSrc, GENERIC_READ, 0, NULL,
OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL);
// Создание файла для записи
HANDLE hTgt = CreateFile(szTgt, GENERIC_WRITE, 0, NULL, OPEN_ALWAYS, FILE_FLAG_OVERLAPPED, NULL);
//Описатель порта ввода-вывода
HANDLE hio = NULL;
//Функция пула потоков обработки результатов вводавывода
DWORD __stdcall IoComplete(LPVOID)
{
BOOL fOK;
DWORD dwBytes, dwKey; LPOVERLAPPED pOvr;
for (;;)
{
// Ждѐм поступления в очередь результата ввода-вывода fOK = GetQueuedCompletionStatus(hio, &dwBytes,
&dwKey, &pOvr, INFINITE); if (fOK)
{
// Проверяем, что завершилось switch (dwKey)
{
//Завершилось чтение case CK_READ:
//Записываем результат чтения в целевой файл pOvr->Offset=1024;
WriteFile(hTgt, Buf, dwBytes, NULL, pOvr);
break;
// Завершилась запись case CK_WRITE:
//Помещаем в очередь сообщения команду на выход из //потока.
90