Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
37
Добавлен:
13.02.2015
Размер:
129.14 Кб
Скачать

Модель порта завершения

Последняя модель ввода/вывода, которую мы рассмотрим, это модель "порта завершения"

– completion port. Порт завершения представляет собой специальный механизм в составе ОС, с помощью которого приложение использует объединение (пул) нескольких потоков, предназначенных единственно для цели обработки асинхронных операций ввода/вывода с перекрытием.

Приложения, которые вынуждены обрабатывать многочисленные асинхронные запросы (речь идет о сотнях и тысячах одновременно поступающих запросах – например, на поисковых серверах или популярных серверах типа ( www.microsoft.com), с помощью этого механизма могут обрабатывать I/O- запросы существенно быстрее и эффективнее, чем просто запускать новый поток для обработки поступившего запроса. Поддержка этого механизма включена в Windows NT, Windows 2000, Windows XP и Windows Server 2003 и особенно эффективна для мультипроцессорных систем. Так, демонстрационный программный код, который опубликован в MSDN, рассчитан на 16ти процессорную аппаратную платформу.

Для функционирования этой модели необходимо создание специального программного объекта ядра системы, который и был назван "порт завершения". Это осуществляется с помощью функции CreateIoCompletionPort(), которая асссоциирует этот объект с одним или несколькими файловыми (сокетными) дескрипторами (см. ниже пример в разделе 4.5.1.1) и который будет управлять перекрывающимися I/O операциями, используя определенное количество потоков для обслуживания завершенных запросов.

Для начала нам необходимо создать программный объект - порт завершения I/O, который будет использоваться, чтобы управлять множественными I/O-запросами для любого количества сокетных дескрипторов. Это выполняется вызовом функции CreateIoCompletionPort(), которая определена как:

HANDLE CreateIoCompletionPort( HANDLE FileHandle,

HANDLE ExistingCompletionPort, DWORD CompletionKey,

DWORD NumberOfConcurrentThreads

);

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

1.Чтобы создать объект порта завершения

2.Связать дескриптор с портом завершения

Когда Вы первоначально создаете объект порта завершения, интерес представляет единственный параметр - NumberOfConcurrentThreads; первые три параметра не существенны. Параметр NumberOfConcurrentThreads специфичен, потому что он определяет число потоков, которым позволяется выполниться одновременно на порте завершения. По идее, нам нужен только один поток для каждого отдельного процессора, чтобы обслужить порт завершения и избежать переключения контекста потока. Значение для этого параметра равное 0 сообщает системе разрешить иметь столько потоков, сколько процессоров имеется в системе. Следующий вызов создает порт завершения I/O:

CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL, 0, 0);

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

Рабочие потоки и порты завершения

После того, как порт завершения успешно создан, Вы можете начинать связывать дескрипторы сокета с объектом. Перед этим – и это важно - Вы должны создать один или большее количество рабочих потоков, чтобы обслужить порт завершения, когда пакеты завершения операции для сокета отправлены в объект порта завершения. Можно задаться вопросом, сколько же потоков должно быть создано, чтобы обслужить порт завершения? Это один из более сложных аспектов модели порта завершения, потому что количество потоков, необходимое для

обслуживания запросов Ввода – вывода, зависит от общей концепции проекта вашего приложения. Важно обратить внимание на различие между числом заявленных параллельных потоков при работе с CreateIoCompletionPort() и числом рабочих потоков; они не представляют одно и то же.

Обычно рекомендуется, чтобы функция CreateIoCompletionPort() задавала один поток для каждого отдельного процессора во избежание переключения контекста потока. Параметр NumberOfConcurrentThreads функции CreateIoCompletionPort() явно дает разрешение системе позволять только n потокам работать одновременно на порте завершения. И даже если Вы создаете больше чем n рабочих потоков для порта завершения, только n потокам будет разрешено работать одновременно. (Фактически, система могла бы превысить это значение на короткий промежуток времени, но она же быстро приведет количество потоков до значения, которое Вы определяете в CreateIoCompletionPort().)

Можно задаться вопросом, для каких целей надо создавать большее количество рабочих потоков, чем указано параметром вызова CreateIoCompletionPort()?

Как было упомянуто выше, это зависит от идеологии построения проекта приложения в целом. Некоторые потоки могут приостанавливать своё выполнение (например proxy-сервер - ждёт ответа с помощью функции WaitForSingleObject()) или поток "засыпает" на Sleep() - тогда в это время вместо него с портом завершения сможет работать другой поток. Т.е. если программа будет блокировать поток - тогда лучше создать рабочих потоков несколько больше чем NumberOfConcurrentThreads, с другой стороны — если в вашей программе не будет блокировки - тогда не стоит создавать лишних потоков. Однако по большому счёту можно считать NumberOfConcurrentThread константой.

Как только создано достаточно рабочих потоков, чтобы обслужить запросы ввода/вывода на порте завершения, можно начинать связывать дескрипторы сокета с портом завершения. Это требует вызова функции CreateIoCompletionPort() на уже существующем порте завершения и указания первых трех параметров — FileHandle, ExistingCompletionPort и CompletionKey с соответствующей сокетной информацией.

Параметр FileHandle представляет дескриптор сокета, ассоциированный с портом завершения. Параметр ExistingCompletionPort указывает существующий уже порт завершения, с которым будет связан дескриптор сокета. Параметр CompletionKey задает специфические данные "per-handle data", которые можно связать с конкретным сокетным дескриптором. Прикладные программы могут хранить любой тип информации, связанной с сокетом, используя этот ключ. Мы называем эти данные "per-handle data" или "сокетная информация". Принято хранить дескриптор сокета, используя этот ключ как указатель на структуру данных, содержащую дескриптор сокета и другую "сокет-специфичную" информацию. Функции потока, которые обслуживают порт завершения, могут отыскивать эту специфичную для данного сокета информацию, используя этот ключ.

Поток использует функцию для ожидания пакета завершения, который будет поставлен в очередь в порту завершения, а не ожидает непосредственного исполнения асинхронного ввода/вывода. Потоки, которые блокируются на порту завершения, освобождаются в порядке LIFO. Это означает, что, когда пакет завершения поставлен в очередь порта завершения, система освобождает последний поток, который был заблокирован на порту.

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

1.Создать порт завершения. Четвертый параметр функции оставлен как 0 - только одному рабочему потоку на процессоре будут позволено выполняться в данное время на порте завершения.

2.Определить, сколько процессоров существуют в системе.

3.Создать рабочие потоки для обслуживания завершенных I/O-запросов на порте завершения, используя информацию процессора в шаге 2. В случае этого простого примера, мы создаем один рабочий поток для процессора, потому что мы не ожидаем, что наши потоки когда-либо войдут в временно приостановленное состояние, когда окажется, что рабочих потоков для исполнения не хватает. Когда вызывается функция создания потока CreateThread(), вы должны указать ту функцию, которая будет исполняться в рабочем потоке.

4.Сформировать слушающий сокет, чтобы принимать входные подключений на порту 5150.

5.Принять поступившие подключения функцией accept().

6.Создать структуру данных, чтобы представить "per-handle data" и сохранить дескриптор присоединенного сокета в структуре.

7.Связать новый дескриптор сокета, возвращенный из accept(), с портом завершения, вызывая CreateIoCompletionPort(). Передать структуру с "per-handle data" в функцию CreateIoCompletionPort() через параметр ключа завершения.

8.Начинаем выполнять операции I/O на принятом подключении. По существу, мы будем выдавать один или более асинхронных вызовов WSARec() или WSASend() на новом сокете, используя механизм ввода/вывода с перекрытием. Вызовы этих функций будут исполняться асинхронно и с перекрытием (т.е. одновременно). Когда эти функции завершатся (с тем или иным результатом), рабочий поток обслужит исполненные запросы и будет ждать следующие вызовы.

Далее повторяем шаги 5—8, пока сервер не завершит свою работу.

Текст программы

DWORD WINAPI ServerThread

(

LPVOID CompletionPortID

)

;

//Прототип рабочего потока

HANDLE CompletionPort;

// Дескриптор порта завершения

 

 

WSADATA wsd;

 

// Структура типа WSADATA

 

 

 

 

 

SYSTEM_INFO SystemInfo;

// Системная информация

 

 

 

SOCKADDR_IN InternetAddr;

 

// Структура адреса

сокета

 

 

SOCKET Listen_socket;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

int

i;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

typedef

struct

_PER_HANDLE_DATA

 

 

 

{

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

SOCKET

Accept_socket;

// Дескриптор сокета клиента

 

SOCKADDR_STORAGE ClientAddr;

// Адрес клиента

 

 

//Другая полезная информация,

связанная с дескриптором

} PER_HANDLE_DATA, * LPPER_HANDLE_DATA; // Стартуем WinSock StartWinsock(MAKEWORD(2,2), &wsd);

//Шаг 1:

//Создать порт завершения Ввода - вывода

CompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

//Шаг 2:

//Определяем, сколько процессоров находятся в системе GetSystemInfo(&SystemInfo);

//Шаг 3:

//Создаем рабочие потоки, основанные на числе процессоров, доступных в

//системе. Для нашего случая мы создаем один рабочий поток для

//каждого процессора.

for(i = 0; i < SystemInfo.dwNumberOfProcessors; i++)

{

HANDLE ThreadHandle;

//Создаем рабочий поток сервера, и передаем порт завершения в поток.

//Замечание: вариант содержания функции ServerWorkerThread() определен ниже. ThreadHandle = CreateThread(NULL, 0,ServerWorkerThread, CompletionPort,0, &ThreadId);

//Закрываем дескриптор потока

CloseHandle(ThreadHandle);

}

//Шаг 4:

//Создаем слушающий сокет с перекрытием (флаг WSA_FLAG_OVERLAPPED)

Listen_socket = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED); InternetAddr.sin_family = AF_INET;

InternetAddr.sin_addr.s_addr = htonl(INADDR_ANY); InternetAddr.sin_port = htons(5150);

bind(Listen, (PSOCKADDR) &InternetAddr,sizeof(InternetAddr)); // Формируем очередь входящих запросов

listen(Listen_socket, 5); while(TRUE)

{

PER_HANDLE_DATA *PerHandleData=NULL;

LPDWORD lpCompletionKey, LPOVERLAPPED *lpOverlapped,
structure
DWORD dwMilliseconds );
Параметры этой функции следующие:

SOCKADDR_IN saRemote;

SOCKET Accept_socket;

int RemoteLen;

// Шаг 5:

// Принимаем запрос на соединение

RemoteLen = sizeof(saRemote);

Accept_socket = WSAAccept(Listen_socket, (SOCKADDR *)&saRemote,&RemoteLen);

// Шаг 6:

// Создаем структуру для информации " per-handle data", связанной с сокетом

PerHandleData = (LPPER_HANDLE_DATA) GlobalAlloc(GPTR, sizeof(PER_HANDLE_DATA));

printf("Socket number %d connected\n", Accept_socket);

PerHandleData->Socket = Accept_socket;

memcpy(&PerHandleData->ClientAddr, &saRemote, RemoteLen);

// Шаг 7:

// Ассоциируем сокет клиента с портом завершения

CreateIoCompletionPort((HANDLE)Accept_socket,CompletionPort,(DWORD)

PerHandleData,0);

// Шаг 8:

// Начинаем обрабатывать операции ввода/вывода на присоединенном сокете.

// Выдаем один или несколько вызовов WSASend() или WSARecv()

// на сокете с использованием перекрытого I/O.

// WSARecv() / WSASend()

// Учтите, что функции будут возвращать ошибку SOCKET_ERROR со статусом

// WSA_IO_PENDING, которую необходимо игнорировать (если вызов их не завершится

// мгновенно).

}

DWORD WINAPI ServerWorkerThread(LPVOID lpParam)

{

.....

return0;

}

Каждая завершившаяся тем или иным образом операция ввода/вывода, назначенная на данный порт завершения, сообщает о своем возврате с помощью специального системного пакета, который поступает в очередь завершенных запросов порта. Для обработки этой очереди и предназначены наши рабочие потоки. Достигается это с использованием функции GetQueuedCompletionStatus(), которая пытается извлечь из очереди пакет завершения соответствующей операции. Если в очереди отсутствует пакет завершения, функция ожидает завершения операции I/O.

BOOL GetQueuedCompletionStatus(

HANDLE CompletionPort, // the I/O completion port of interest LPDWORD lpNumberOfBytesTransferred,// to receive number of bytes

// transferred during I/O

// to receive file's completion key // to receive pointer to OVERLAPPED

// optional timeout value

CompletionPort – дескриптор порта завершения

lpNumberOfBytesTransferred - число байт переданных во время ввода-вывода функциями WSASend/WSARecv

lpCompletionKey - здесь нам вернутся данные дескриптора сокета, которые мы задали при привязке сокета к порту заврешния

lpOverlapped – указатель на OVERLAPPED-структуру

dwMilliseconds - таймаут операции в милисекундах (INFINITE - бесконечное ожидание)

Если пакет завершения не появляется в пределах указанного времени, функция возвращает FALSE и устанавливает *lpOverlapped в NULL. Если dwMilliseconds =0 и в очереди отсутствует пакет завершения, функция завершается немедленно.

При успешном завершении функция возвращает положительное число. Если *lpOverlapped указан как NULL и функция не извлекла пакет завершения из порта, она возвращает 0. Если функция не извлекла пакет завершения из порта по таймауту, GetLastError() вернет WAIT_TIMEOUT. Если сокет, ассоциированный с портом, закрыт, GetQueuedCompletionStatus() возвращает ERROR_SUCCESS, с *lpOverlapped не-NULL и lpNumberOfBytes равным 0.

lpOverlapped указывает на структуру OVERLAPPED, снабженную тем, что мы называем данные операции Per-I/O, знание которых может быть важно для рабочего потока при обработке пакета завершения (возвращенные ECHO-данные, как в нашем примере, прием подключения, тип операции - чтение/запись и так далее). Per-I/O данные операции – это любое количество байтов, содержащихся в отдельной структуре, также содержащей ту самую структуру OVERLAPPED, которая передается в функцию, ожидающую структуру OVERLAPPED. Самый простой способ выполнить это - определить новую структуру и разместить структуру OVERLAPPED как поле новой структуры. Например, мы объявляем, что следующая структура данных управляет данными операции Per-I/O:

typedef struct { OVERLAPPED Overlapped;

//любые полезные нам данные: WSABUF DataBuf;

CHAR Buffer[DATA_BUFSIZE]; DWORD BytesSend;

DWORD BytesRecv; DWORD OperationType; DWORD TotalBytes;

.....

} PER_IO_OPERATION_DATA;

После поля обязательной структуры OVERLAPPED мы можем разместить буффер для хранения данных, количество переданных/принятых байт, тип операции - отправка/приём, адрес и порт клиента -практически любую информацию, которая нам может быть полезна, исходя из требований к серверу.

Таким образом, при вызове WinSock-функций мы должны передавать OVERLAPPED-структуру как поле новой структуры. Например таким образом:

PER_IO_OPERATION_DATA PerIoData;

...

WSARecv(socket, &wbuf, 1, &Bytes, &Flags, &(PerIoData.Overlapped),NULL);

Позже в рабочем потоке функция GetQueuedCompletionStatus() возвращается со структурой перекрытия и ключом завершения. Чтобы обеспечения доступа к per-I/O данным, должна использоваться макрокоманда CONTAINING_RECORD. Например:

PER_IO_DATA *PerIoData=NULL; OVERLAPPED *lpOverlapped=NULL;

ret = GetQueuedCompletionStatus(CompPortHandle,&Transferred, (PULONG_PTR)&CompletionKey,&lpOverlapped,INFINITE);

PerIoData = CONTAINING_RECORD(lpOverlapped, PER_IO_DATA, Overlapped);

Использование этого макроса позволяет записывать OVERLAPPED-член структуры PER_IO_DATA в любом месте, а не обязательно первым полем, что может быть важно для команды разработчиков.

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

Для завершения нашего сервера ECHO мы должны написать код функции ServerWorkerThread():

DWORD WINAPI ServerWorkerThread(LPVOID CompletionPortID)

{

HANDLE CompletionPort = (HANDLE) CompletionPortID; DWORD BytesTransferred;

LPOVERLAPPED Overlapped; LPPER_HANDLE_DATA PerHandleData;

LPPER_IO_DATA PerIoData;

DWORD SendBytes, RecvBytes;

DWORD Flags;

while(TRUE)

{

//Ожидаем завершения операции ввода/вывода на любом сокете,

//связанным с данным портом завершения

if(GetQueuedCompletionStatus (CompletionPort, &BytesTransferred,

(LPDWORD)&PerHandleData, (LPOVERLAPPED *) &PerIoData, INFINITE) == 0)

{

// Сначала проверяем возврат на возможную ошибку.

printf ("GetQueuedCompletionStatus failed with error %d\n", GetLastError

());

return0;

}

//Если произошла ошибка типа BytesTransferred=0, что свидетельствует о

//закрытии сокета на удаленном хосте, закрываем свой сокет и очищаем данные,

//связанные с сокетом

if(BytesTransferred == 0 &&(PerIoData->OperationType == RECV_POSTED ││ PerIoData->OperationType == SEND_POSTED))

{

closesocket(PerHandleData->Socket);

GlobalFree(PerHandleData);

GlobalFree(PerIoData);

continue;//Продолжаем цикл

}

//Обслуживаем завершенный запрос. Какая операция была закончена, определяем по

//содержимому поля OperationTypefield в структуре PerIoData

if(PerIoData->OperationType == RECV_POSTED)

{

//Если тип операции был помечен как WSARecv(), выполняем необходимые действия

с

//информацией, имеющейся в поле PerIoData->Buffer

}

//Выдаем следующий запрос на выполнение другой необходимой операции – WSASend()

//или WSARecv(). В нашем случае это WSARecv() – мы продолжаем получать данные Flags = 0;

//Формируем данные для следующего вызова операции с перекрытием ZeroMemory(&(PerIoData->Overlapped),sizeof(OVERLAPPED)); PerIoData->DataBuf.len = DATA_BUFSIZE;

PerIoData->DataBuf.buf = PerIoData->Buffer; PerIoData->OperationType = RECV_POSTED;

//Выполняем вызов WSARecv() и переходим опять к ожиданию завершения WSARecv(PerHandleData->Socket, &(PerIoData->DataBuf), 1, &RecvBytes, &Flags,

&(PerIoData->Overlapped), NULL);

}//End While

}//End ServerWorkerThread()

Следует отметить следующее обстоятельство. Если происходит ошибка во время исполнения даннорй операции с перекрытием, функция GetQueuedCompletionStatus() вернет FALSE. Так как порт завершения – это объект Windows, то в этом случае надо вызывать GetLastError(). Для получения эквивалентного WinSock-кода ошибки, надо обращаться к WSAGetOverlappedResult() с заданием дескриптора сокета и структуры WSAOVERLAPPED, после чего вызов WSAGetLastError() вернет правильный код WinSock-ошибки.

В нашем примере мы не очень заботимся о различении типа операции - т.к. здесь надо просто отправлять и принимать символы. В более "серьёзных" программах использование параметра OperationType в структуре PER_IO_OPERATION_DATA может привести к такому коду для точного понимания, что же происходит с сервером:

Соседние файлы в папке Программирование_распределенных_систем