Blog Stats
  • Posts - 3
  • Articles - 3
  • Comments - 0
  • Trackbacks - 0

 

Condivisione di dati in RAM tra processi

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.

 

Comments have been closed on this topic.
 

 

Copyright © Federico Pranio