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.