Bagikan melalui


Manajemen Memori Stub Server

Pengantar manajemen memori Server-Stub

Stub yang dihasilkan MIDL bertindak sebagai antarmuka antara proses klien dan proses server. Stub klien membalut semua data yang diteruskan ke parameter yang ditandai dengan atribut [in ], dan mengirimkannya ke stub server. Stub server, setelah menerima data ini, merekonstruksi tumpukan panggilan, lalu menjalankan fungsi server yang diterapkan pengguna yang sesuai. Stub server juga mendandani data parameter yang ditandai dengan atribut [out] dan mengembalikannya ke aplikasi klien.

Format data marshaled 32-bit yang digunakan oleh MSRPC adalah versi sintaks transfer Network Data Representation (NDR) yang sesuai. Untuk informasi selengkapnya tentang format ini, lihat Situs web Buka Grup. Untuk platform 64-bit, ekstensi Microsoft 64-bit ke sintaks transfer NDR yang disebut NDR64 dapat digunakan untuk performa yang lebih baik.

Membatalkan amarshalasi Data Masuk

Di MSRPC, klien membagi marshal semua data parameter yang ditandai sebagai [in] dalam satu buffer berkelanjutan untuk transmisi ke stub server. Demikian juga, server stub membajak semua data yang ditandai dengan atribut [out] dalam buffer berkelanjutan untuk kembali ke stub klien. Meskipun lapisan protokol jaringan di bawah RPC dapat memecah dan mengemas buffer untuk transmisi, fragmentasinya transparan terhadap stub RPC.

Alokasi memori untuk membuat bingkai panggilan server bisa menjadi operasi yang mahal. Stub server akan mencoba meminimalkan penggunaan memori yang tidak perlu jika memungkinkan, dan diasumsikan bahwa rutinitas server tidak akan membebaskan atau merealokasi data yang ditandai dengan atribut [in] atau [in, out ]. Stub server mencoba menggunakan kembali data di buffer jika memungkinkan untuk menghindari duplikasi yang tidak perlu. Aturan umumnya adalah bahwa jika format data marshaled sama dengan format memori, RPC akan menggunakan pointer ke data marshalled alih-alih mengalokasikan memori tambahan untuk data yang diformat secara identik.

Misalnya, panggilan RPC berikut didefinisikan dengan struktur yang format marshaled-nya identik dengan format dalam memorinya.

typedef struct RpcStructure
{
    long val;
    long val2;
}

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

Dalam hal ini, RPC tidak mengalokasikan memori tambahan untuk data yang dirujuk oleh plInStructure; sebaliknya, itu hanya meneruskan pointer ke data marshaled ke implementasi fungsi sisi server. Stub server RPC memverifikasi buffer selama proses unmarshaling jika stub dikompilasi menggunakan bendera "-robust" (yang merupakan pengaturan default dalam versi paling baru dari kompiler MIDL). RPC menjamin bahwa data yang diteruskan ke implementasi fungsi sisi server valid.

Ketahuilah bahwa memori dialokasikan untuk plOutStructure, karena tidak ada data yang diteruskan ke server untuk itu.

Alokasi Memori untuk Data Masuk

Kasus dapat muncul di mana stub server mengalokasikan memori untuk data parameter yang ditandai dengan atribut [in] atau [in, out] . Ini terjadi ketika format data marshaled berbeda dari format memori, atau ketika struktur yang terdiri dari data marsekal cukup kompleks dan harus dibaca secara atomik oleh pangkal server RPC. Tercantum di bawah ini adalah beberapa kasus umum ketika memori harus dialokasikan untuk data yang diterima oleh stub server.

  • Data adalah array yang bervariasi atau array yang bervariasi yang sesuai. Ini adalah array (atau pointer ke array) yang memiliki atribut [length_is()] atau [first_is()] yang diatur di dalamnya. Dalam NDR, hanya elemen pertama dari array ini yang dinamai dan ditransmisikan. Misalnya, dalam cuplikan kode di bawah ini, data yang diteruskan dalam parameter pv akan memiliki memori yang dialokasikan untuk itu.

    void RpcFunction
    (
        [in] long size,
        [in, out] long *pLength,
        [in, out, size_is(size), length_is(*pLength)] long *pv
    );
    
  • Data adalah string berukuran atau string yang tidak sesuai. String ini biasanya merupakan penunjuk ke data karakter yang ditandai dengan atribut [size_is() ]. Dalam contoh di bawah ini, string yang diteruskan ke fungsi sisi server SizedString akan memiliki memori yang dialokasikan, sedangkan string yang diteruskan ke fungsi NormalString akan digunakan kembali.

    void SizedString
    (
        [in] long size,
        [in, size_is(size), string] char *str
    );
    
    void NormalString
    (
        [in, string] char str
    );
    
  • Data adalah jenis sederhana yang ukuran memorinya berbeda dari ukuran marshalnya, seperti enum16 dan __int3264.

  • Data didefinisikan oleh struktur yang perataan memorinya lebih kecil dari perataan alami, berisi salah satu jenis data di atas, atau memiliki byte padding berikutnya. Misalnya, struktur data kompleks berikut telah memaksa perataan 2-byte dan memiliki padding di akhir.

#pragma pack(2) typedef struct ComplexPackedStructure { char c;
panjang l; perataan dipaksa pada byte char c2 kedua; akan ada pad satu byte berikutnya untuk menjaga perataan 2-byte } '''

  • Data berisi struktur yang harus diawasi bidang menurut bidang. Bidang-bidang ini mencakup penunjuk antarmuka yang ditentukan dalam antarmuka DCOM; pointer yang diabaikan; nilai bilangan bulat diatur dengan atribut [rentang] ; elemen array yang ditentukan dengan atribut [wire_marshal], [user_marshal], [transmit_as] dan [represent_as] ; dan struktur data kompleks yang disematkan.
  • Data berisi serikat, struktur yang berisi serikat, atau array serikat. Hanya cabang tertentu dari serikat adalah marshaled pada kawat.
  • Data berisi struktur dengan array konforman multidimensi yang memiliki setidaknya satu dimensi non-tetap.
  • Data berisi array struktur yang kompleks.
  • Data berisi array jenis data sederhana seperti enum16 dan __int3264.
  • Data berisi array ref dan penunjuk antarmuka.
  • Data memiliki atribut [force_allocate] yang diterapkan ke penunjuk.
  • Data memiliki atribut [alokasikan(all_nodes)] yang diterapkan ke penunjuk.
  • Data memiliki atribut [byte_count] yang diterapkan ke penunjuk.

Data 64-bit dan Sintaks Transfer NDR64

Seperti disebutkan sebelumnya, data 64-bit di-marshalled menggunakan sintaks transfer 64-bit tertentu yang disebut NDR64. Sintaks transfer ini dikembangkan untuk mengatasi masalah spesifik yang muncul ketika pointer dinamai di bawah NDR 32-bit dan ditransmisikan ke server-stub pada platform 64-bit. Dalam hal ini, penunjuk data 32-bit marshaled tidak cocok dengan yang 64-bit, dan alokasi memori akan selalu terjadi. Untuk membuat perilaku yang lebih konsisten pada platform 64-bit, Microsoft mengembangkan sintaks transfer baru yang disebut NDR64.

Contoh yang mengilustrasikan masalah ini adalah sebagai berikut:

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

Struktur ini, ketika dinamai, akan digunakan kembali oleh pangkal server pada sistem 32-bit. Namun, jika pangkal server berada pada sistem 64-bit, data NDR-marshaled memiliki panjang 4 byte, tetapi ukuran memori yang diperlukan akan menjadi 8. Akibatnya, alokasi memori dipaksa, dan penggunaan kembali buffer akan jarang terjadi. NDR64 mengatasi masalah ini dengan membuat ukuran marshaled pointer 64-bit.

Berbeda dengan NDR 32-bit, ikatan data sederhana seperti enum16 dan __int3264 tidak membuat struktur atau array kompleks di bawah NDR64. Demikian juga, nilai bantalan berikutnya tidak membuat struktur menjadi kompleks. Pointer antarmuka diperlakukan sebagai pointer unik di tingkat atas; akibatnya, struktur dan array yang berisi pointer antarmuka tidak dianggap kompleks dan tidak akan memerlukan alokasi memori tertentu untuk penggunaannya.

Menginisialisasi Data Keluar

Setelah semua data masuk tidak terpesan, stub server perlu menginisialisasi pointer hanya keluar yang ditandai dengan atribut [out ].

typedef struct RpcStructure
{
    long val;
    long val2;
}

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

Dalam panggilan di atas, stub server harus menginisialisasi plOutStructure karena tidak ada dalam data marshaled, dan itu adalah pointer [ref] tersirat yang harus tersedia untuk implementasi fungsi server. Stub server RPC menginisialisasi dan nol keluar setiap pointer khusus referensi tingkat atas dengan atribut [out ]. Setiap penunjuk referensi [out] di bawahnya juga diinisialisasi secara rekursif. Rekursi berhenti pada pointer apa pun dengan atribut [unik] atau [ptr] yang diatur padanya.

Implementasi fungsi server tidak dapat secara langsung mengubah nilai pointer tingkat atas, dan dengan demikian tidak dapat mengalokasikannya kembali. Misalnya, dalam implementasi ProcessRpcStructure di atas, kode berikut tidak valid:

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

plOutStructure adalah nilai tumpukan dan perubahannya tidak disebarluaskan kembali ke RPC. Implementasi fungsi server dapat mencoba menghindari alokasi dengan mencoba membebaskan plOutStructure, yang dapat mengakibatkan kerusakan memori. Stub server kemudian akan mengalokasikan ruang untuk penunjuk tingkat atas dalam memori (dalam kasus pointer-to-pointer) dan struktur sederhana tingkat atas yang ukurannya pada tumpukan lebih kecil dari yang diharapkan.

Klien dapat, dalam keadaan tertentu, menentukan ukuran alokasi memori dari sisi server. Dalam contoh berikut, klien menentukan ukuran data keluar dalam parameter ukuran masuk.

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

Setelah membatalkan nama data masuk, termasuk ukuran, stub server mengalokasikan buffer untuk pv dengan ukuran "sizeof(char)*size". Setelah ruang dialokasikan, server stub nol keluar buffer. Perhatikan bahwa dalam kasus khusus ini, stub mengalokasikan memori dengan MIDL_user_allocate(), karena ukuran buffer ditentukan pada runtime.

Ketahuilah bahwa dalam kasus antarmuka DCOM, stub yang dihasilkan MIDL mungkin tidak terlibat sama sekali jika klien dan server berbagi apartemen COM yang sama, atau jika ICallFrame diterapkan. Dalam hal ini, server tidak dapat bergantung pada perilaku alokasi, dan perlu memverifikasi memori berukuran klien secara independen.

Implementasi Fungsi Sisi Server dan Marshaling Data Keluar

Segera setelah membatalkan nama pada data masuk dan inisialisasi memori yang dialokasikan untuk berisi data keluar, stub server RPC menjalankan implementasi fungsi sisi server yang disebut oleh klien. Saat ini, server dapat memodifikasi data yang secara khusus ditandai dengan atribut [in, out ], dan dapat mengisi memori yang dialokasikan untuk data hanya keluar (data yang ditandai dengan [out]).

Aturan umum untuk manipulasi data parameter marshalled sederhana: server hanya dapat mengalokasikan memori baru atau memodifikasi memori yang secara khusus dialokasikan oleh rintangan server. Merealokasi atau merilis memori yang ada untuk data dapat berdampak negatif pada hasil dan performa panggilan fungsi, dan bisa sangat sulit untuk di-debug.

Secara logis, server RPC tinggal di ruang alamat yang berbeda dari klien, dan umumnya dapat diasumsikan bahwa mereka tidak berbagi memori. Akibatnya, aman bagi implementasi fungsi server untuk menggunakan data yang ditandai dengan atribut [in] sebagai memori "scratch" tanpa memengaruhi alamat memori klien. Meskipun demikian, server tidak boleh mencoba mengalokasikan ulang atau merilis data [in] , meninggalkan kontrol ruang tersebut ke stub server RPC itu sendiri.

Umumnya, implementasi fungsi server tidak perlu mengalokasikan ulang atau merilis data yang ditandai dengan atribut [in, out ]. Untuk data ukuran tetap, logika implementasi fungsi dapat langsung memodifikasi data. Demikian juga, untuk data berukuran variabel, implementasi fungsi juga tidak boleh memodifikasi nilai bidang yang disediakan ke atribut [size_is() ]. Ubah nilai bidang yang digunakan untuk mengukur hasil data dalam buffer yang lebih kecil atau lebih besar yang dikembalikan ke klien yang mungkin tidak dilengkapi untuk menangani panjang abnormal.

Jika keadaan terjadi di mana rutinitas server harus merealokasi memori yang digunakan oleh data yang ditandai dengan atribut [in, out ], sepenuhnya mungkin bahwa implementasi fungsi sisi server tidak akan tahu apakah pointer yang disediakan oleh rintisan adalah untuk memori yang dialokasikan dengan MIDL_user_allocate() atau buffer kawat marshaled. Untuk mengatasi masalah ini, MS RPC dapat memastikan bahwa tidak ada kebocoran memori atau kerusakan yang terjadi jika atribut [force_allocate] diatur pada data. Ketika [force_allocate] diatur, stub server akan selalu mengalokasikan memori untuk pointer, meskipun peringatan adalah bahwa performa akan menurun untuk setiap penggunaannya.

Ketika panggilan kembali dari implementasi fungsi sisi server, server stub membalut data yang ditandai dengan atribut [out] dan mengirimkannya ke klien. Ketahuilah bahwa stub tidak membasahkan data jika implementasi fungsi sisi server melemparkan pengecualian.

Merilis Memori yang Dialokasikan

Stub server RPC akan merilis memori tumpukan setelah panggilan dikembalikan dari fungsi sisi server, apakah pengecualian terjadi atau tidak. Stub server membebaskan semua memori yang dialokasikan oleh stub serta memori apa pun yang dialokasikan dengan MIDL_user_allocate(). Implementasi fungsi sisi server harus selalu memberi RPC status yang konsisten, baik dengan melempar pengecualian atau mengembalikan kode kesalahan. Jika fungsi gagal selama populasi struktur data yang rumit, fungsi harus memastikan bahwa semua pointer menunjuk ke data yang valid atau diatur ke NULL.

Selama proses ini, stub server membebaskan semua memori yang bukan bagian dari buffer marshaled yang berisi data [in ]. Satu pengecualian untuk perilaku ini adalah data dengan atribut [alokasikan(dont_free)] yang diatur pada mereka - stub server tidak membebaskan memori apa pun yang terkait dengan pointer ini.

Setelah stub server merilis memori yang dialokasikan oleh stub dan implementasi fungsi, stub memanggil fungsi pemberitahuan tertentu jika atribut [notify_flag] ditentukan untuk data tertentu.

Marsekal daftar tertaut melalui RPC -- Contoh

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
);

Dalam contoh di atas, format memori untuk LINKEDLIST akan identik dengan format kawat marshaled. Akibatnya, stub server tidak mengalokasikan memori untuk seluruh rantai penunjuk data di bawah pIn. Sebaliknya, RPC menggunakan kembali buffer kawat untuk seluruh daftar yang ditautkan. Demikian pula, stub tidak mengalokasikan memori untuk pInOut, tetapi sebaliknya menggunakan kembali buffer kawat yang dinamai oleh klien.

Karena tanda tangan fungsi berisi parameter keluar, pOut, stub server mengalokasikan memori untuk berisi data yang dikembalikan. Memori yang dialokasikan awalnya nol keluar, dengan pNext diatur ke NULL. Aplikasi dapat mengalokasikan memori untuk daftar tertaut baru dan mengarahkan pOut-pNext> ke dalamnya. pIn dan daftar tertaut yang dikandungnya dapat digunakan sebagai area coretan, tetapi aplikasi tidak boleh mengubah pointer pNext apa pun.

Aplikasi dapat dengan bebas mengubah konten daftar yang ditautkan yang ditujukan oleh pInOut, tetapi tidak boleh mengubah pointer pNext apalagi tautan tingkat atas itu sendiri. Jika aplikasi memutuskan untuk mempersingkat daftar yang ditautkan, aplikasi tidak dapat mengetahui apakah ada tautan pointer pNext tertentu ke buffer internal RPC atau buffer yang secara khusus dialokasikan dengan MIDL_user_allocate(). Untuk mengatasi masalah ini, Anda menambahkan deklarasi jenis tertentu untuk penunjuk daftar tertaut yang memaksa alokasi pengguna, seperti yang terlihat dalam kode di bawah ini.

typedef [force_allocate] PLINKEDLIST;

Atribut ini memaksa stub server untuk mengalokasikan setiap simpul daftar yang ditautkan secara terpisah, dan aplikasi dapat membebaskan bagian singkat dari daftar yang ditautkan dengan memanggil MIDL_user_free(). Aplikasi kemudian dapat dengan aman mengatur penunjuk pNext di akhir daftar tertaut yang baru dipersingkat ke NULL.