Untitled Page
La memoria condivisa tra processi
Si immaggini di avere due processi che devono scambiarsi dei dati. Questi dati
possono essere variabili booleane, caratteri, interi. Non possono essere classi
in quanto il sistema operativo non sa che cosa sia una classe. Ma possono essere
puntatori a oggetti, il che significa puntatori a indirizzi di memoria.
Quando i processi vengono creati dal sistema operativo, viene per essi allocata
una certa quantità di memoria, la cui entità viene stimata dal compilatore
quando viene compilato il codice. Tale memoria non può essere accessibile da
altri processi, ma solo dal processo per cui è stata allocata.
Ogni volta che il processore esegue un comando, un apposito dispositivo
hardware, il Memory Management Unit, controlla in un solo coclo di clock, che
l'indirizzo di memoria a cui l'istruzione accede sia appartenente allo spazio di
momeria del processo. Se non lo è viene generata un interruzione che causa la
terminazione del processo.
Due processi possono scambiarsi i dati anche tramite un file di testo. La
soluzione presentata in questo articolo è decisamente migliore al livello di
proformance poichè l'accesso alla RAM è più veloce dell'accesso al disco, e
inoltre più sicura poichè i file di testo possono essere aperti e modificati
facilmente. Per i dati in ram è più difficile.
Pertanto qualora ci si trovi in un contesto dove due processi si scambiano
grandi quantità di dati e la lettura deve essere molto veloce, oppure in
contesti dove la sicurezza è un requisito ad alta priorità, si consiglia di
usare la memoria condivisa.
Il processo che scrive i dati in memoria deve implementare una classe Writer,
mentre il processo che li legge deve implementare una classe Reader.
La classe Reader importa e referenzia delle funzioni della dll Kernel32, che
contiene le API di accesso alle funzioni di sistema. La Microsoft non ha mai
reso note le chiamate di sistema di Windows.
1: enum AccessRights
2: {
3: PROCESS_VM_READ = 0x0010,
4: PROCESS_VM_WRITE = 0x0020,
5: PROCESS_VM_OPERATION = 0x0008
6: }
7: [DllImport("kernel32.dll")]
8: static extern IntPtr OpenProcess(AccessRights dwDesiredAcces,
bool bInheritHandle,
int dwProcessId);
9: [DllImport("kernel32.dll")]
10: static extern bool CloseHandle(IntPtr hObject);
11: [DllImport("kernel32.dll")]
12: static extern bool ReadProcessMemory(IntPtr hProcess,
13: IntPtr lpBaseAddress, IntPtr lpBuffer, int nSize,
out int lpNumberOfBytesRead);
14: [DllImport("kernel32.dll")]
15: static extern bool WriteProcessMemory(IntPtr hProcess,
IntPtr lpBaseAddress,
16: IntPtr lpBuffer, int nSize,
out int lpNumberOfBytesWritten);
Cosa sono queste funzioni?
OpenProsess(...) apre un handel a un processo già esistente. I parametri sono i
diritti di accesso, un booleano che indica se i processi figli ne ereditano
l'handle (un puntatore al puntatore al primo indirizzo in RAM del processo), e
un intero che rappresenta l'identificativo del processo. Ritorna l'handle, NULL
in caso di insuccesso.
CloseHandle(...) chiude l'handle di un processo, determinato in base al suo Id
passato per parametro.
ReadProcessMemory(...) legge i dati di un processo in una data area di memoria,
purchè questi siano accessibili, altrimenti l'operazione fallisce. I parametri
sono l'handle al processo, un puntatore al primo indirizzo di memoria da
leggere, un puntatore a un buffer che riceve il contenuto dallo spazio degli
indirizzi del processo specificato, il numero di byte da leggere, il puntatore a
una veriabile che riceve i dati trasmessi nel buffer.
WriteProcessMemory(...) scrive i dati nella memoria di un processo specificato.
I significato dei parametri è analogo a quello dei parametri di
ReadProcessMemory(...).
L'enumeratore AccessRights codifica i valori per indicare i diritti di
accesso.
1: enum AllocationType
2: {
3: MEM_COMMIT = 0x1000,
4: MEM_RESERVE = 0x2000,
5: MEM_RESET = 0x8000
6: }
7: enum DeallocationType
8: {
9: MEM_RELEASE = 0x8000
10: }
11: enum MemoryProtection
12: {
13: PAGE_READONLY = 0x02,
14: PAGE_READWRITE = 0x04
15: }
16:
17: [DllImport("kernel32.dll")]
18: static extern IntPtr VirtualAlloc(IntPtr lpAddress,
int dwSize, AllocationType fAllocationType,
MemoryProtection flProtect);
19: [DllImport("kernel32.dll")]
20: static extern bool VirtualFree(IntPtr lpAddress,
int dwSize,
DeallocationType dwFreeType);
VirtualAlloc a
VirtualFree si occupano di allocare o
deallocare memoria per un processo
AllocationType indica il tipo di allocazione.
MEM_COMMIT indica che si deve allocare memoria per la specifica pagina di
memoria.
MEM_RESERVE indica che si deve usare la memoria gia allocata al processo
utilizzando una parte di essa.
MEM_RESET indica che la memoria compresa tra lpAddress e dwSize non sarà più di
interesse. I valori sono settati a zero e la memoria potrà essere riutilizzata
in seguito e non viene riservata(committed).
MemoryProtection, di cui qui si usano soltanto due dei possibili valori: PAGE_READONLY
per la sola lettura e PAGE_READWRITE per lettura e scrittura.
La funzione VirtualAlloc(...) prende come parametri l'ndirizzo iniziale
dell'area da allocare, la dimensione in byte dell'areail tipo di allocazione
della memoria, la protezione della memoria per l'area di pagine da allocare, un
valore di EMemoryCriticalLevel che indica l'impatto di un'allocazione
non riuscita. ppMem è un puntatore alla memoria allocata oppure NULL se non è
stato possibile soddisfare la richiesta. Quest'ultimo essendo un puntatore
risulta eventualmente modificato dalla funzione.
La funzione VirtualFree rilascia la memoria iniziante per lpAddress e grange
dwSize byte.
DeallocationType qui usa solo il valore MEM_RELEASE che libra la memoria e la
rende disponibile per altri processi. Ma c'è anche il valore MEM_DECOMMIT, che
ha valore esadecimale 4000, che in csharp indica 0x4000,
Poiche sia Reader che Writer devono avere i dati condivisi definiti al medesimo
modo, cioè tipi e nomi di variabili uguali, definisco una struct SharedData che
definisce i dati da condividere.
SharedData quindi può essere così definita
1: namespace Common.TrustedMemory
2: {
3: //Questa struttura deve essere identica a quella
4: //implementata nell'altra applicazione che da vita
// allìaltro processo
5: public unsafe struct SharedData
6: {
7: public char _carattere;
8: public bool _error;
9: public long _numero1;
10: public long _numero2;
11: public char _unAltroCarattere;
12: public CharPointer* _unTesto ;
13: }
14:
15: public unsafe struct CharPointer
16: {
17: public char c;
18: public CharPointer* next;
19: }
20: }
Si noti la parola chiave unsafe che consente l'uso dei
puntatori. Affinchè il progetto possa essere compilato occore spuntare il check
Allow unsafe code tra le opzioni di build del progetto.
Per mettere tutto in funzione Writer chiama il metodo InitSharedMemory, che
inizializza tutto quanto è necessario per il funzionamento delle procedure.
1://Inizializza la memoria condivisa.
2:public unsafe IntPtr InitSharedMem()
3:{
4: //calcolo la grandezza dei dati da condividere
5: int byteCount = Marshal.SizeOf(typeof(SharedData));
6: //alloco la memoria
7: sharedMemPtr = VirtualAlloc(IntPtr.Zero, //Il sistema sceglie l'indirizzo
8: byteCount, //Il numero di byte da allocare
9: AllocationType.MEM_RESERVE | AllocationType.MEM_COMMIT, //riserva la memoria e la alloca
10: MemoryProtection.PAGE_READWRITE); //permette la lettura e la scrittura
11: //Uso il booleano mustRefresc in memoria condivisa
12: //per indicare se l'applicazione deve aggiornare i dati
13: mustRefresh = (bool*)VirtualAlloc(IntPtr.Zero,
14: sizeof(bool),
15: AllocationType.MEM_RESERVE | AllocationType.MEM_COMMIT,
16: MemoryProtection.PAGE_READWRITE).ToPointer();
17: *mustRefresh = false;
18: //Alloco della memoria per l'aggiornamento dei dati
19: refreshPtr = VirtualAlloc(IntPtr.Zero,
20: byteCount,
21: AllocationType.MEM_COMMIT | AllocationType.MEM_RESERVE,
22: MemoryProtection.PAGE_READWRITE);
23:
24: //Scrivo nel registro di sistema un valore con l'indirizzo da leggere
25: WriteRegistry();
26:
27: //Ritorna l'indirizzo della memoriaa condivisa
28: return sharedMemPtr;
29:}
Alla fine di tutto lo writer chiama la funzione CleanUp() che pulisce i dati.
1: void CleanUp()
2: {
3: //Elimina i registri di sistema usati
4: RegistryKey swKey = Registry.LocalMachine.CreateSubKey("Software");
5: swKey.DeleteSubKeyTree("SharedMemTest");
6: swKey.Close();
7: //Dealloca la memoria condivisa non gestita
8: VirtualFree(this.sharedMemPtr, 0, DeallocationType.MEM_RELEASE);
9: unsafe
10: {
11: IntPtr refrPtr = new IntPtr(this.mustRefresh);
12: VirtualFree(refreshPtr, 0, DeallocationType.MEM_RELEASE);
13: }
14: VirtualFree(refreshPtr, 0, DeallocationType.MEM_RELEASE);
15:}
Metodi di lettura e scrittura
La classe Reader offre i metodi per la lettura e per modificare
i dati del Writer.
1: IntPtr processHandle;
2: IntPtr sharedMemPtr;
3: IntPtr mustRefreshPtr;
4: IntPtr newDataPtr;
5:
6: IntPtr sharedData;
7: int dataSize;
8:
9: SharedData data;
10:
11: public SharedData Data
12: {
13: get { return data; }
14: set { data = value; }
15: }
16:
17: //Questa funzione ottiene gli indirizzi
//e le informazioni dal registro di sistema
18: public void InitRead()
19: {
20: //Apre la chiave di registro
21: RegistryKey infoKey = Registry.CurrentUser.OpenSubKey(Writer.KeyName);
22: if (infoKey != null)
23: {
24: //Ottiene l'ID del processo da cui leggere i dati
25: int processId = (int)infoKey.GetValue("ProcessID");
26: //Apre il processo con diritti di lettura e scrittura
27: this.processHandle = OpenProcess(AccessRights.PROCESS_VM_READ |
28: AccessRights.PROCESS_VM_WRITE | AccessRights.PROCESS_VM_OPERATION,
29: false, processId);
30: //Ottiene l'indirizzo della memoria condivisa
31: sharedMemPtr = (IntPtr)(int)infoKey.GetValue("MemPtr");
32: //Ottiene l'indirizzo del bool da impostare per richiedere l'aggiornamento
33: mustRefreshPtr = (IntPtr)(int)infoKey.GetValue("MustRefreshPtr");
34: //Ottiene l'indirizzo della memoria da impostare per l'aggiornamento dei dati
35: newDataPtr = (IntPtr)(int)infoKey.GetValue("NewDataPtr");
36: //Chiude le chiavi di registro
37: infoKey.Close();
38: //Ottiene la dimensione della memoria da leggere
39: .dataSize = Marshal.SizeOf(typeof(SharedData));
40: //Alloca la memoria in cui leggere i dati estranei
41: this.sharedData = Marshal.AllocHGlobal(dataSize);
42: }
43: }
44:
45: //Lettura dei dati
46: public void Read()
47: {
48: int readBytes;
49: //Legge la memoria condivisa del processo
50: bool b = ReadProcessMemory(processHandle, sharedMemPtr, this.sharedData,
this.dataSize, out readBytes);
51: //Converte la memoria non gestita nella struttura gestita (SharedData)
52: this.data = (SharedData)Marshal.PtrToStructure(this.sharedData,
typeof(SharedData));
53: }
54:
55: //Scrittura dei dati
56: public void Write(SharedData newData)
57: {
58: int written;
59: //Valorizza un puntatore ai dati da scrivere
60: Marshal.StructureToPtr(newData, this.sharedData, false);
61: //Scrive nella memoria condivisa i nuovi dati
62: WriteProcessMemory(processHandle, this.newDataPtr, this.sharedData,
this.dataSize, out written);
63: //Imposta nel ptr da scrivere true, per indicare al processo di aggiornare i dati
64: Marshal.StructureToPtr(true, this.sharedData, false);
65: //Scrive nella memoria condivisa il true
66: WriteProcessMemory(processHandle, this.mustRefreshPtr, sharedData,
sizeof(bool), out written);
67:
68: //A questo punto la prossima volta che il processo esterno dovrà elaborare i dati
69: //saprà di dover aggiornare i dati dalla memoria condivisa, dove abbiamo copiato
70: //i nuovi valori
71: }
72:
73: public void EndRead()
74: {
75: //Chiude l'handle del processo
76: CloseHandle(this.processHandle);
77: //Dealloca la memoria non gestita
78: Marshal.FreeHGlobal(sharedData);
79: }
La classe Marshall offre dei metodi che fanno dai intermediari tra l'uso della
memoria gestita, e la memoria non gestita. Tali operazioni sono appunto dette
operazioni di Marshalling.
In paarticolare nella memoria non gestita è necessario il rilascio esplicito
della memoria, cioè occorre dire al sistema operativo di delallocare la memoria.
Tutto ciò nella normale programmazione con il Framework .NET lo evitiamo poichè
ci pensa il garbage collector.
In EndRead() la riga
Marshal.FreeHGlobal(sharedData) dealloca la memoria usata per lo scambio
dei dati.
Funzionamento
Il processo Writer:
1: static SharedMemory.Writer writer = new SharedMemory.Writer();
2: static void Main(string[] args)
3: {
4: }
5:
6: private static void wridteData(){
7: writer.InitSharedMem();
8:
9: SharedMemory.SharedData d = new SharedMemory.SharedData();
10: unsafe
11: {
12: d._numero1 = 12345;
13: d._carattere = 'I';
14: d._numero2 = 11111;
15: d._unTesto = SharedMemory.StringManager.SetString("bla bla bla");
16: Console.WriteLine(SharedMemory.StringManager.GetString(d._unTesto));
17: }
18:
19: d._error = true;
20: d._unAltroCarattere = 'k';
21:
22:
23: writer.Data = d;
24: writer.Elabora();
25: if (Console.ReadLine() != null)
26: {
27: writer.CleanUp();
28: }
29: }
InitSharedMem() imposta tutte le variabili coinvolte nel processo di
condivisione dati.
Quindi si procede alla valorizzazione della struct contenente i dati.
writer.Data = d; valorizza i dati nell'oggetto scrittore che li rende quindi
disponibili al lettore, tramite l'indirizzo in memoria contenuto nel registro di
sistema.
writer.Elabora() si occupa di trasformare i dati, qualora richiesto.
writer.CleanUp() dealloca la memoria utilizzata.
Il processo reader
1: static SharedMemory.Reader reader = new SharedMemory.Reader();
2: static void Main(string[] args)
3: {
4: Console.WriteLine("Premi un tasto per leggere i dati in memoria");
5: while (Console.ReadKey() == null)
6: {
7: ;
8: }
9:
10: reader.InitRead();
11: reader.Read();
12:
13: Console.WriteLine("reader.Data._carattere: {0}", reader.Data._carattere);
14: Console.WriteLine("reader.Data._error: {0}", reader.Data._error);
15: Console.WriteLine("reader.Data._numero1: {0}", reader.Data._numero1);
16: Console.WriteLine("reader.Data._numero2: {0}", reader.Data._numero2);
17: Console.WriteLine("reader.Data._unAltroCarattere: {0}", reader.Data._unAltroCarattere);
18: unsafe
19: {
20: Console.WriteLine("reader.Data._unTesto: {0}", SharedMemory.StringManager.GetString(reader.Data._unTesto));
21: }
22:
23: Console.WriteLine("Premi un tasto per uscire");
24: while (Console.ReadKey() == null)
25: {
26: ;
27: }
28: reader.EndRead();
29:
30: }
InitRead() si occupa di inizializzare il lettore per la lettura dei dati andando
a leggere della struttura dati nel registro di sistema tutto quanto serve per
valorizzare le variabili. Deve essere invocato dal processo che legge i dati
prima di ogni altra cosa.
Read() effettua la lettura. Alla fine di tutto EndRead() dealloca la memoria
utilizzata.