Поделиться через


Управление памятью заглушки сервера

Общие сведения об управлении памятью Server-Stub

Созданные MIDL заглушки выступают в качестве интерфейса между клиентским процессом и серверным процессом. Клиентская заглушка маршалирует все данные, переданные в параметры, помеченные атрибутом [in] , и отправляет их в заглушку сервера. После получения этих данных заглушка сервера восстанавливает стек вызовов, а затем выполняет соответствующую реализованную пользователем серверную функцию. Заглушка сервера также маршалирует данные параметров, помеченные атрибутом [out] , и возвращает их клиентскому приложению.

32-разрядный формат маршалированных данных, используемый MSRPC, является совместимой версией синтаксиса передачи сетевых данных (NDR). Дополнительные сведения об этом формате см. на веб-сайте Open Group. Для 64-разрядных платформ для повышения производительности можно использовать 64-разрядное расширение Microsoft для синтаксиса передачи NDR64.

Отмена маркировки входящих данных

В MSRPC заглушка клиента маршалирует все данные параметров с тегом [in] в одном непрерывном буфере для передачи в заглушку сервера. Аналогичным образом заглушка сервера маршалирует все данные, помеченные атрибутом [out] , в непрерывном буфере для возврата в заглушку клиента. Хотя уровень сетевого протокола под RPC может фрагментировать и пакетизировать буфер для передачи, фрагментация является прозрачной для заглушки RPC.

Выделение памяти для создания кадра вызова сервера может быть дорогостоящей операцией. Заглушка сервера попытается свести к минимуму ненужное использование памяти, когда это возможно, и предполагается, что серверная подпрограмма не будет освобождать или перераспределять данные, помеченные атрибутами [in] или [in, out] . Заглушка сервера пытается по возможности повторно использовать данные в буфере, чтобы избежать ненужного дублирования. Общее правило заключается в том, что если формат маршалированных данных совпадает с форматом памяти, RPC будет использовать указатели на маршалированные данные вместо выделения дополнительной памяти для данных одинакового формата.

Например, следующий вызов RPC определяется структурой, формат которой маршалирован идентичен формату в памяти.

typedef struct RpcStructure
{
    long val;
    long val2;
}

void ProcessRpcStructure
(
    [in]  RpcStructure *plInStructure;
    [out] RpcStructure *plOutStructure;
);

В этом случае RPC не выделяет дополнительную память для данных, на которые ссылается plInStructure; вместо этого он просто передает указатель на маршалированные данные в реализацию функции на стороне сервера. Заглушка сервера RPC проверяет буфер во время процесса распаковки, если заглушка компилируется с помощью флага "-robust" (который является параметром по умолчанию в последней версии компилятора MIDL). RPC гарантирует, что данные, передаваемые в реализацию функции на стороне сервера, являются допустимыми.

Имейте в виду, что память выделяется для plOutStructure, так как никакие данные для нее не передаются на сервер.

Выделение памяти для входящих данных

Могут возникать случаи, когда заглушка сервера выделяет память для данных параметров, помеченных атрибутами [in] или [in, out] . Это происходит, когда формат маршалированных данных отличается от формата памяти или когда структуры, составляющие маршалированные данные, являются достаточно сложными и должны считываться атомарно с помощью заглушки сервера RPC. Ниже приведено несколько распространенных случаев, когда память должна быть выделена для данных, полученных заглушку сервера.

  • Данные являются изменяющимся массивом или соответствующим изменяющимся массивом. Это массивы (или указатели на массивы), для которых задан атрибут [length_is()] или [first_is()] . В NDR маршалируется и передается только первый элемент этих массивов. Например, в приведенном ниже фрагменте кода для данных, переданных в параметре pv , будет выделена память.

    void RpcFunction
    (
        [in] long size,
        [in, out] long *pLength,
        [in, out, size_is(size), length_is(*pLength)] long *pv
    );
    
  • Данные являются строкой размера или несоответствующей строкой. Эти строки обычно являются указателями на символьные данные, помеченные атрибутом [size_is()] . В приведенном ниже примере строка, переданная в серверную функцию SizedString , будет выделена память, тогда как строка, переданная в функцию NormalString , будет использоваться повторно.

    void SizedString
    (
        [in] long size,
        [in, size_is(size), string] char *str
    );
    
    void NormalString
    (
        [in, string] char str
    );
    
  • Данные — это простой тип, размер памяти которого отличается от маршалированного размера, например перечисления16 и __int3264.

  • Данные определяются структурой, в которой выравнивание памяти меньше естественного, содержит любой из указанных выше типов данных или имеет заполнение конечных байтов. Например, следующая сложная структура данных имеет принудительное выравнивание по 2 байта и заполнение в конце.

#pragma pack(2) typedef struct ComplexPackedStructure { char c;
long l; Выравнивание выполняется принудительно во втором байте char c2; будет конечная однобайтовая панель для сохранения 2-байтового выравнивания } '''

  • Данные содержат структуру, которая должна быть маршалирована по полю. Эти поля включают указатели интерфейса, определенные в интерфейсах DCOM; игнорируемые указатели; целочисленные значения, заданные с помощью атрибута [range] ; элементы массивов, определенные с помощью атрибутов [wire_marshal], [user_marshal], [transmit_as] и [represent_as] ; и внедренные сложные структуры данных.
  • Данные содержат объединение, структуру, содержащую объединение, или массив объединений. Только определенная ветвь объединения маршалируется по проводу.
  • Данные содержат структуру с многомерным соответствующим массивом, который имеет по крайней мере одно нефиксальное измерение.
  • Данные содержат массив сложных структур.
  • Данные содержат массив простых типов данных, таких как enum16 и __int3264.
  • Данные содержат массив ссылочных указателей и указателей интерфейса.
  • Данные имеют атрибут [force_allocate] , примененный к указателю.
  • К указателю применяется атрибут [allocate(all_nodes)] .
  • Данные имеют атрибут [byte_count] , примененный к указателю.

Синтаксис передачи 64-разрядных данных и NDR64

Как упоминалось ранее, 64-разрядные данные маршалируются с помощью определенного 64-разрядного синтаксиса передачи, называемого NDR64. Этот синтаксис передачи был разработан для решения конкретной проблемы, которая возникает, когда указатели маршалируются в 32-разрядном NDR и передаются в заглушку сервера на 64-разрядной платформе. В этом случае маршалированные 32-разрядные указатели данных не соответствуют 64-разрядному, и выделение памяти будет происходить неизменно. Чтобы обеспечить более согласованное поведение на 64-разрядных платформах, корпорация Майкрософт разработала новый синтаксис передачи под названием NDR64.

Ниже приведен пример, демонстрирующий эту проблему.

typedef struct PtrStruct
{
  long l;
  long *pl;
}

Эта структура при маршале будет повторно использоваться заглушкой сервера в 32-разрядной системе. Однако если заглушка сервера находится в 64-разрядной системе, данные, маршалированные с помощью NDR, будут иметь длину 4 байта, но требуемый размер памяти составит 8. В результате выделение памяти выполняется принудительно, а повторное использование буфера происходит редко. NDR64 решает эту проблему, делая маршалированные размеры указателя 64-разрядным.

В отличие от 32-разрядного NDR простые области данных, такие как enum16 и __int3264 , не делают структуру или массив сложными в соответствии с NDR64. Аналогичным образом значения конечных панелей не делают структуру сложной. Указатели интерфейса обрабатываются как уникальные указатели на верхнем уровне; в результате структуры и массивы, содержащие указатели интерфейса, не считаются сложными и не требуют выделения памяти для их использования.

Инициализация исходящих данных

После отмены маркировки всех входящих данных заглушка сервера должна инициализировать указатели только для исходящего трафика, помеченные атрибутом [out] .

typedef struct RpcStructure
{
    long val;
    long val2;
}

void ProcessRpcStructure
(
    [in]  RpcStructure *plInStructure;
    [out] RpcStructure *plOutStructure;
);

В приведенном выше вызове заглушка сервера должна инициализировать plOutStructure , так как она не присутствовала в маршалированных данных и является подразумеваемым указателем [ссылка] , который должен быть доступен для реализации функции сервера. Заглушка сервера RPC инициализирует и обнуляет все указатели только для ссылок верхнего уровня с помощью атрибута [out] . Все ссылочные указатели [out] под ним также рекурсивно инициализируются. Рекурсия останавливается на любых указателях с заданными атрибутами [unique] или [ptr] .

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

void ProcessRpcStructure(RpcStructure *plInStructure, rpcStructure *plOutStructure)
{
    plOutStructure = MIDL_user_allocate(sizeof(RpcStructure));
    Process(plOutStructure);
}

plOutStructure — это значение стека, и его изменение не распространяется обратно в RPC. Реализация серверной функции может попытаться избежать выделения, пытаясь освободить plOutStructure, что может привести к повреждению памяти. Затем заглушка сервера выделяет пространство для указателя верхнего уровня в памяти (в регистре указателя на указатель) и простой структуры верхнего уровня, размер которой в стеке меньше ожидаемого.

При определенных обстоятельствах клиент может указать размер выделенной памяти на стороне сервера. В следующем примере клиент указывает размер исходящих данных в параметре размера входящего трафика .

void VariableSizeData
(
    [in] long size,
    [out, size_is(size)] char *pv
);

После отмены распределения входящих данных, включая размер, заглушка сервера выделяет буфер для pv с размером "sizeof(char)*size". После выделения пространства заглушка сервера обнуляет буфер. Обратите внимание, что в этом конкретном случае заглушка выделяет память с помощью MIDL_user_allocate(), так как размер буфера определяется во время выполнения.

Имейте в виду, что в случае интерфейсов DCOM заглушки, созданные MIDL, могут вообще не использоваться, если клиент и сервер совместно используют одно и то же подразделение COM или если реализован ICallFrame . В этом случае сервер не может зависеть от поведения выделения и должен независимо проверять память размера клиента.

Реализации функций на стороне сервера и маршалинг исходящих данных

Сразу после отмены распределения входящих данных и инициализации памяти, выделенной для хранения исходящих данных, заглушка сервера RPC выполняет реализацию функции, вызванной клиентом, на стороне сервера. В настоящее время сервер может изменить данные, специально помеченные атрибутом [in, out] , и заполнить память, выделенную только для исходящих данных (данные с тегом [out]).

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

Логически сервер RPC находится в диапазоне адресов, отличном от пространства адресов клиента, и обычно можно предположить, что он не использует общую память. В результате реализация серверной функции безопасно использовать данные, помеченные атрибутом [in] , в качестве "временной" памяти, не затрагивая адреса памяти клиента. При этом сервер не должен пытаться перераспределить или освободить [в] данные, оставляя управление этими пространствами самой заглушки сервера RPC.

Как правило, реализации функции сервера не требуется перераспределять или освобождать данные, помеченные атрибутом [in, out] . Для данных фиксированного размера логика реализации функции может напрямую изменять данные. Аналогичным образом для данных переменной величины реализация функции не должна изменять значение поля, предоставленное атрибуту [size_is()] . Изменение значения поля, используемого для изменения размера данных, приводит к тому, что клиенту возвращается буфер меньшего или большего размера, который может быть неправильно подготовлен для решения проблемы с аномальной длиной.

Если подпрограмме сервера приходится перераспределять память, используемую данными, помеченными атрибутом [in, out] , вполне возможно, что реализация серверной функции не будет знать, является ли указатель, предоставленный заглушкой, к памяти, выделенной с помощью MIDL_user_allocate() или маршалированного буфера провода. Чтобы обойти эту проблему, MS RPC может гарантировать отсутствие утечки или повреждения памяти, если для данных задан атрибут [force_allocate] . Если задано значение [force_allocate] , заглушка сервера всегда выделяет память для указателя, хотя есть предостережение в том, что производительность будет уменьшаться при каждом использовании указателя.

Когда вызов возвращается из реализации функции на стороне сервера, заглушка сервера маршалирует данные, помеченные атрибутом [out] , и отправляет их клиенту. Имейте в виду, что заглушка не маршалирует данные, если реализация серверной функции создает исключение.

Освобождение выделенной памяти

Заглушка сервера RPC освобождает память стека после возврата вызова из серверной функции независимо от того, возникает ли исключение. Заглушка сервера освобождает всю память, выделенную заглушкой, а также всю память, выделенную с помощью MIDL_user_allocate(). Реализация функции на стороне сервера всегда должна предоставлять RPC согласованное состояние, вызывая исключение или возвращая код ошибки. Если функция завершается сбоем во время заполнения сложных структур данных, она должна убедиться, что все указатели указывают на допустимые данные или имеют значение NULL.

Во время этого прохода заглушка сервера освобождает всю память, которая не является частью маршалированного буфера, содержащего данные [in] . Одним из исключений такого поведения являются данные с заданным атрибутом [allocate(dont_free)] . Заглушка сервера не освобождает память, связанную с этими указателями.

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

Маршалирование связанного списка с помощью RPC — пример

typedef struct _LINKEDLIST
{
    long lSize;
    [size_is(lSize)] char *pData;
    struct _LINKEDLIST *pNext;
} LINKEDLIST, *PLINKEDLIST;

void Test
(
    [in] LINKEDLIST *pIn,
    [in, out] PLINKEDLIST *pInOut,
    [out] LINKEDLIST *pOut
);

В приведенном выше примере формат памяти для LINKEDLIST будет идентичен формату маршалированного провода. В результате заглушка сервера не выделяет память для всей цепочки указателей данных в pIn. Вместо этого RPC повторно использует буфер провода для всего связанного списка. Аналогичным образом заглушка не выделяет память для pInOut, а повторно использует буфер провода, маршалированные клиентом.

Так как сигнатура функции содержит исходящий параметр POut, заглушка сервера выделяет память для хранения возвращаемых данных. Выделенная память изначально обнуляется, а для параметра pNext задано значение NULL. Приложение может выделить память для нового связанного списка и указать pOut-pNext> на него. pIn и связанный список, который он содержит, можно использовать в качестве временной области, но приложение не должно изменять ни один из указателей pNext.

Приложение может свободно изменять содержимое связанного списка, на который указывает pInOut, но оно не должно изменять ни один из указателей pNext , не говоря уже о самой ссылке верхнего уровня. Если приложение решит сократить связанный список, оно не сможет узнать, связан ли какой-либо указатель pNext с внутренним буфером RPC или буфером, выделенным MIDL_user_allocate(). Чтобы обойти эту проблему, добавьте объявление определенного типа для указателей связанных списков, которые принудительно выделяют пользователей, как показано в приведенном ниже коде.

typedef [force_allocate] PLINKEDLIST;

Этот атрибут заставляет заглушку сервера выделять каждый узел связанного списка отдельно, и приложение может освободить сокращенную часть связанного списка, вызвав MIDL_user_free(). Затем приложение может безопасно задать для указателя pNext в конце вновь сокращенного связанного списка значение NULL.