Table of Contents

Laborator 07 - Memoria virtuală

Materiale ajutătoare

Nice to read

Memoria virtuală

Mecanismul de memorie virtuală este folosit de către nucleul sistemului de operare pentru a implementa o politică eficientă de gestiune a memoriei. Astfel, cu toate că aplicațiile folosesc în mod curent memoria virtuală, ele nu fac acest lucru în mod explicit. Există însă câteva cazuri în care aplicațiile folosesc memoria virtuală în mod explicit.

Sistemul de operare oferă primitive de mapare a fișierelor, a memoriei sau a dispozitivelor în spațiul de adresă al unui proces.

Linux

Funcțiile cu ajutorul cărora se pot face cereri explicite asupra memoriei virtuale sunt funcțiile din familia mmap(2). Funcțiile folosesc ca unitate minimă de alocare pagina (adică se poate aloca numai un număr întreg de pagini, iar adresele trebuie să fie aliniate corespunzător).

Maparea fișierelor

În urma mapării unui fișier în spațiul de adresă al unui proces, accesul la acest fișier se poate face similar cu accesarea datelor dintr-un vector. Eficiența metodei vine din faptul că zona de memorie este gestionată similar cu memoria virtuală, supunându-se regulilor de evacuare pe disc atunci când memoria devine insuficientă (în felul acesta se poate lucra cu mapări care depășesc dimensiunea efectivă a memoriei fizice). Mai mult, partea de I/O este realizată de către kernel, programatorul scriind cod care doar preia/stochează valori în regiunea mapată. Astfel nu se mai apelează read, write, lseek - ceea ce adesea simplifică scrierea codului.

Observație

Nu orice descriptor de fișier poate fi mapat în memorie. Socket-urile, pipe-urile, majoritatea dispozitivelor nu permit decât accesul secvențial și sunt incompatibile din această cauză cu conceptele de mapare. Există cazuri în care fișiere obișnuite nu pot fi mapate (spre exemplu, dacă nu au fost deschise pentru a putea fi citite; pentru mai multe informații: man mmap).

mmap

Prototipul funcției mmap ce permite maparea unui fișier în spațiul de adresă al unui proces este următorul:

void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);

Funcția va întoarce în caz de eroare MAP_FAILED. Dacă maparea s-a făcut cu succes, va întoarce un pointer spre o zonă de memorie din spațiul de adresă al procesului, zonă în care a fost mapat fișierul descris de descriptorul fd, începând cu offset-ul offset. Folosirea parametrului start permite propunerea unei anumite zone de memorie la care să se facă maparea. Folosirea valorii NULL pentru parametrul start indică lipsa vreunei preferințe în ceea ce privește zona în care se va face alocarea. Adresa precizată prin parametrul start trebuie să fie multiplu de dimensiunea unei pagini. Dacă sistemul de operare nu poate să mapeze fișierul la adresa cerută, atunci îl va mapa la altă adresă apropiată multiplu de dimensiunea unei pagini. Adresa la care se mapează fișierul este întoarsă de funcție.

Parametrul prot specifică tipul de acces care se dorește; poate fi PROT_READ, PROT_WRITE, PROT_EXEC sau PROT_NONE; dacă zona e folosită altfel decât s-a declarat se va genera un semnal SIGSEGV.

Parametrul flags permite stabilirea tipului de mapare ce se dorește; poate lua următoarele valori (combinate prin SAU pe biți; trebuie să existe cel puțin una: fie MAP_PRIVATE, fie MAP_SHARED):

Este de remarcat că folosirea MAP_SHARED permite partajarea memoriei între procese care nu sunt înrudite. În acest caz, conținutul fișierului devine conținutul inițial al memoriei partajate, și orice modificare făcută de procese în această zonă este copiată apoi în fișier, asigurând persistență prin sistemul de fișiere.

msync

Pentru a declanșa în mod explicit sincronizarea fișierului cu maparea din memorie este disponibilă funcția msync:

int msync(void *start, size_t length, int flags);

unde flags poate fi:

Alocare de memorie în spațiul de adresă al procesului

În UNIX, tradițional, pentru alocarea memoriei dinamice, se folosește apelul de sistem brk. Acest apel crește sau descrește zona de heap asociată procesului. Odată cu oferirea către aplicații a unor apeluri de sistem de gestiune a memoriei virtuale (mmap), a existat posibilitatea ca procesele să aloce memorie folosind aceste noi apeluri de sistem. Practic, procesele pot mapa memorie în spațiul de adresă, nu fișiere.

Procesele pot cere alocarea unei zone de memorie de la o anumită adresă din spațiul de adresare, chiar și cu o anumită politică de acces (citire, scriere sau execuție). În UNIX, acest lucru se face tot prin intermediul funcției mmap. Pentru acest lucru parametrul de flag-uri trebuie să conțină flag-ul MAP_ANONYMOUS.

Maparea dispozitivelor

Există chiar și posibilitatea ca aplicațiile să mapeze în spațiul de adresă al unui proces un dispozitiv de intrare-ieșire. Acest lucru este util de exemplu pentru plăcile video: o aplicație poate mapa în spațiul de adresă memoria plăcii video. În UNIX, dispozitivele fiind reprezentate prin fișiere, pentru a realiza acest lucru nu trebuie decât să deschidem fișierul asociat dispozitivului și să-l folosim într-un apel mmap. Atenție însă, nu toate dispozitivele pot fi mapate în memorie, iar atunci când pot fi mapate, ce înseamnă acest lucru depinde de dispozitiv.

Un alt exemplu de dispozitiv care poate fi mapat este chiar memoria. În Linux se poate folosi fișierul /dev/zero pentru a face mapări de memorie, ca și când s-ar folosi flag-ul MAP_ANONYMOUS.

Demaparea unei zone din spațiul de adresă

Dacă se dorește demaparea unei zone din spațiul de adresă al procesului se poate folosi funcția munmap:

 int munmap(void *start, size_t length);

start reprezintă adresa primei pagini ce va fi demapată (trebuie să fie multiplu de dimensiunea unei pagini). Dacă length nu este o dimensiune care reprezintă un număr întreg de pagini, va fi rotunjit superior. Zona poate să conțină bucăți deja demapate. Se pot astfel demapa mai multe zone în același timp.

Redimensionarea unei zone mapate

Pentru a executa operații de redimensionare a zonei mapate se poate utiliza funcția mremap:

void *mremap(void *old_address, size_t old_size, size_t new_size, unsigned long flags);

Zona pe care old_adress și old_size o descriu trebuie să aparțină unei singure mapări. O singură opțiune este disponibilă pentru flags: MREMAP_MAYMOVE care arată că este în regulă ca pentru obținerea noii mapări să se realizeze o nouă mapare într-o altă zonă de memorie (vechea zona fiind dealocată).

Schimbarea protecției unei zone mapate

Uneori este nevoie ca modul (drepturile de acces) în care a fost mapată o zonă să fie schimbat. Pentru acest lucru se poate folosi funcția mprotect:

int mprotect(const void *addr, size_t len, int prot);

Funcția primește ca parametri intervalul de adrese [addr, addr + len - 1] și noile drepturi de access (PROT_READ, PROT_WRITE, PROT_EXEC, PROT_NONE). Ca și la munmap, addr trebuie să fie multiplu de dimensiunea unei pagini. Funcția va schimba protecția pentru toate paginile care conțin cel puțin un octet în intervalul specificat.

Exemplu

int fd = open("fisier", O_RDWR);
void *p = mmap(NULL, 2*getpagesize(), PROT_NONE, MAP_SHARED, fd, 0);
// *(char*)p = 'a'; // segv fault
mprotect(p, 2*getpagesize(), PROT_WRITE);
  *char*)p = 'a';
munmap(p, 2*getpagesize());

Optimizări

Pentru ca sistemul de operare să poată implementa cât mai eficient accesele la o zona de memorie mapată, programatorul poate să informeze kernel-ul (prin apelul de sistem madvise) despre modul în care zona va fi folosită.

Click to display ⇲

Click to hide ⇱

madvise e utilă mai ales atunci când în spatele memoriei virtuale se află un dispozitiv fizic (de ex., când se mapează fișiere de pe hard-disk, kernel-ul poate citi în avans pagini de pe disc, reducând latența datorată poziționării capului de citire). Prototipul funcției este următorul:

int madvise(void *start, size_t length, int advice);

unde valorile acceptate pentru advice sunt:

  • MADV_NORMAL - regiunea este una obișnuită și nu are nevoie de un tratament special.
  • MADV_RANDOM - regiunea va fi accesată în mod aleator; sistemul de operare nu va citi în avans pagini.
  • MADV_SEQUENTIAL - regiunea va fi accesată în mod secvențial; sistemul de operare ar putea citi în avans pagini.
  • MADV_WILLNEED - regiunea va fi utilizată undeva în viitorul apropiat (nucleul poate decide să preîncarce paginile în memorie).
  • MADV_DONTNEED - regiunea nu va mai fi utilizată; nucleul poate să elibereze zona alocată din memorie, dar zona nu este demapată; nu se garantează păstrarea datelor la accesări ulterioare.


Blocarea paginării

Există o categorie de procese care trebuie să execute anumite acțiuni la momente de timp bine determinate, pentru a se păstra calitatea execuției. Pentru exemplificare, putem considera un player audio/video sau un program ce controlează mersul unui robot biped. Problema cu acest gen de procese este dată de faptul că dacă o anumită pagină nu este prezentă în memorie, va dura un timp până ce ea va fi adusă. Pentru a contracara aceste probleme, sistemele UNIX pun la dispoziție apelurile mlock și mlockall.

int mlock(const void *addr, size_t len);
int mlockall(int flags);

Click to display ⇲

Click to hide ⇱

Funcția mlock va bloca paginarea (nu se va mai face swapout) paginilor incluse în intervalul [addr, addr + len - 1]. Funcția mlockall va bloca paginarea tuturor paginilor procesului, în funcție de flag-uri:

  • MCL_CURRENT - se va bloca paginarea tuturor paginilor mapate în spațiul de adresă al procesului la momentul apelului
  • MCL_FUTURE - se va bloca paginarea noilor pagini mapate în spațiul de adresă al procesului (noi mapări realizate cu funcția mmap, dar și paginile de stivă mapate automat de sistem)

Notă:

Flag-ul MCL_FUTURE nu garantează faptul că paginile de stivă vor fi automat mapate în sistem. Dacă procesul depășește limita de memorie impusă de sistem, va primi semnalul SIGSEGV. Pentru a nu se ajunge în astfel de situații, programul trebuie să folosească mlockall(MCL_CURRENT | MCL_FUTURE) și apoi să aloce dimensiunea maximă a stivei pe care urmează să o folosească (prin declararea unei variabile locale, un vector de exemplu, și accesarea completă a acesteia).


Există, bineînțeles, și funcții ce readuc lucrurile la normal:

int munlock(const void *addr, size_t len);
int munlockall(void);

Astfel, funcția munlock va reporni mecanismul de paginare al tuturor paginilor din intervalul [addr, addr + len - 1], iar funcția munlockall face același lucru pentru toate paginile procesului, atât curente, cât și viitoare. Trebuie notat faptul că, dacă s-au efectuat mai multe apeluri mlock sau mlockall, este suficient un singur apel munlock sau munlockall pentru a reactiva paginarea.

Excepții

Atunci când se detectează o încălcare a protecției la accesul la memorie, se va trimite semnalul SIGSEGV sau SIGBUS procesului. După cum am văzut atunci când am discutat despre semnale, semnalul poate fi tratat cu două tipuri de funcții pe care aici o să le denumim signal și sigaction. Funcția de tip sigaction va primi ca parametru o structură siginfo_t. În cazul semnalelor ce tratează excepții cauzate de un acces incorect la memorie, următoarele câmpuri din această structură sunt setate:

ElectricFence

ElectricFence este un pachet ce ajută programatorii la depanarea problemelor de tipul buffer overrun. Aceste probleme sunt cauzate de faptul că anumite date sunt suprascrise fiindcă nu se fac verificări când se modifică date adiacente. Soluția folosită de Electric Fence este înlocuirea apelurilor standard malloc și free cu implementări proprii. Electric Fence va plasa zona de memorie alocată în spațiul de adrese al procesului, astfel încât ea să fie mărginită de pagini neaccesibile (protejate la scriere și citire).

Din păcate, sistemul de operare și arhitectura procesorului limitează dimensiunea paginii la cel puțin 1-4K, astfel încât dacă zona de memorie alocată nu este multiplu de această dimensiune, există posibilitatea ca programul să poată citi sau scrie și în zone în care nu ar trebui, fără ca sistemul de operare să oprească executia programului. Pentru a preveni situații de acestă natură, Electric Fence alocă zonele de memorie la limita superioară a unei pagini, mapând o pagină neaccesibilă după aceasta. Această abordare nu previne buffer underrun-ul, în care datele sunt citite sau scrise peste limita inferioară.

Pentru a putea verifica și astfel de situații, utilizatorul trebuie să definescă variabila de mediu EF_PROTECT_BELOW înainte de rula programul. În acest caz, Electric Fence va plasa zona de memorie alocată la începutul unei pagini, pagină care la rândul ei este plasată după o pagină inaccesibilă procesului.

De ce este importantă detectarea situațiilor de buffer overrun? Așa cum am explicat și în secțiunea precedentă, astfel de situații vor produce în cele din urmă erori, dar la un moment de timp ulterior, când va fi mai greu să determine cauza erorii cu mijloace de depanare obișnuite. În plus, în situațiile de buffer overrun se pot suprascrie nu numai variabile, ci și alte date importante pentru stabilitatea programului cum ar fi datele de control folosite de rutinele malloc și free. Biblioteca Electric Fence poate determina erorile de buffer overrun doar dacă acestea apar în memoria alocată dinamic (adică în zona heap) cu rutinele malloc și free. Pentru a folosi Electric Fence utilizatorul trebuie să folosească la link-editarea bibliotecii libefence. Pentru a vedea utilitatea acestui pachet, să analizăm programul de mai jos:

ef_example.c
#include <stdio.h>
#include <malloc.h>
 
int main(void)
{
	int i;
	int *data_1, *data_2;
 
	data_1 = malloc(11 * sizeof(int));
 
	for (i = 0; i <= 11; i++)
		data_1[i] = i;
 
	data_2 = malloc(11 * sizeof(int));
 
	for (i = 0; i <= 11; i++)
		data_2[i] = 11 - i;
 
	for (i = 0; i <= 11; i++)
		printf("%d %d\n", data_1[i], data_2[i]);
 
	free(data_1); 
	free(data_2);
 
	return 0;
}

Aparent totul pare în regulă. La execuția programului însă obținem următorul output:

 so@spook$ gcc -Wall -g ef_example.c
 so@spook$ ./a.out
ff: malloc.c:3074: sYSMALLOc: Assertion `(old_top == (((mbinptr) (((char *) 
&((av)->bins[((1) - 1) * 2])) - __builtin_offsetof (struct malloc_chunk, fd)))) 
&& old_size == 0) || ((unsigned long)(old_size) >= (unsigned long)
((((__builtin_offsetof (struct malloc_chunk, fd_nextsize))+((2 * (sizeof(size_t)))
 - 1)) & ~((2 * (sizeof(size_t))) - 1))) && ((old_top)->size & 0x1) && 
((unsigned long)old_end & pagemask) == 0)' failed.

Ceva este clar în neregulă. Dacă folosim biblioteca efence și GDB eroarea va fi vizibilă imediat:

 so@spook$ gcc -Wall -g ef_example.c -lefence
 so@spook$ gdb ./a.out 
 Reading symbols from /home/so/a.out...done.
 (gdb) run
 Starting program: /home/so/a.out 
 [Thread debugging using libthread_db enabled]
 
   Electric Fence 2.1 Copyright (C) 1987-1998 Bruce Perens.
 
 Program received signal SIGSEGV, Segmentation fault.
 0x08048536 in main () at ef.c:12
 12			data_1[i] = i;
 (gdb) print i
 $1 = 11
 (gdb)

Se observă că eroarea apare în momentul în care încercăm să inițializăm al 12-lea element al vectorului, deși vectorul nu are decât 11 elemente.

Pentru mai multe informații despre Electric Fence consultați pagina de manual (man efence).

Windows

În Windows funcțiile de control al memoriei virtuale sau mai bine zis al spațiului de adresă al unui proces nu mai sunt grupate, ca în cazul Unix, într-o singură primitivă oferită de sistemul de operare. Avem funcții pentru maparea fișierelor în memorie și funcții pentru alocarea de memorie fizică în spațiul de adresă al unui proces.

Maparea fișierelor

Pentru a mapa un fișier în spațiul de adresă al unui proces trebuie mai întâi creat un handle către un obiect de tipul FileMapping și apoi realizată efectiv maparea. Funcțiile CreateFileMapping și MapViewOfFile au mai fost prezentate atunci când s-a discutat despre memoria partajată.

Pentru a crea un obiect de tip FileMapping se folosește funcția CreateFileMapping:

HANDLE CreateFileMapping(
   HANDLE hFile,
   LPSECURITY_ATTRIBUTES lpAttributes,
   DWORD flProtect,
   DWORD dwMaximumSizeHigh,
   DWORD dwMaximumSizeLow,
   LPCTSTR lpName
 );

Funcția primește ca parametri handle-ul fișierului care se dorește a fi mapat, atribute de securitate care controlează accesul la handle-ul obiectului FileMapping creat, tipul mapării (PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOPY pentru copy-on-write) și dimensiunea maximă care poate fi mapată cu ajutorul funcției MapViewOfFile. Opțional se poate specifica și un șir care să identifice obiectul FileMapping creat. Dacă mai există un obiect de acest tip, funcția CreateFileMapping nu va crea unul nou, ci îl va folosi pe cel existent. Atenție însă, obiectul trebuie să fi fost creat cu drepturi care să permită procesului apelant să îl deschidă.

Pentru deschiderea unui obiect de tip FileMapping deja creat se mai poate folosi funcția OpenFileMapping:

HANDLE OpenFileMapping(
  DWORD dwDesiredAccess,
  BOOL bInheritHandle,
  LPCTSTR lpName
);

Maparea în spațiul de adrese al procesului se face folosind funcția MapViewOfFile:

LPVOID MapViewOfFile(
   HANDLE hFileMappingObject,
   DWORD dwDesiredAccess,
   DWORD dwFileOffsetHigh,
   DWORD dwFileOffsetLow,
   SIZE_T dwNumberOfBytesToMap
 );

Funcția primește ca parametri un handle către un obiect de tip FileMapping, modul de acces la zona mapată (FILE_MAP_READ, FILE_MAP_WRITE, FILE_MAP_COPY pentru copy-on-write), offset-ul în fișier de unde începe maparea și numărul de octeți de mapat. Funcția va întoarce un pointer în spațiul de adresă al procesului, la zona mapată.

Alocare de memorie în spațiul de adresă al procesului

Pentru alocarea de memorie în spațiul de adresă al procesului se pot folosi funcțiile VirtualAlloc sau VirtualAllocEx:

 LPVOID VirtualAlloc(
   LPVOID lpAddress,
   SIZE_T dwSize,
   DWORD flAllocationType,
   DWORD flProtect
 );
 LPVOID VirtualAllocEx(
   HANDLE hProcess,
   LPVOID lpAddress,
   SIZE_T dwSize,
   DWORD flAllocationType,
   DWORD flProtect
 );

Cu funcția VirtualAllocEx se poate aloca memorie în spațiul de adresă al unui proces arbitrar, specificat în parametrul hProcess. Procesul curent trebuie să aibă drepturi corespunzătoare asupra procesului pe care se încearcă operația (PROCESS_VM_OPERATION). Funcțiile întorc un pointer către adresa de start, iar parametrii așteptați de funcții sunt descriși în spoiler:

Click to display ⇲

Click to hide ⇱

  • lpAddress - adresa de unde începe alocarea; trebuie să fie multiplu de 4KB pentru alocare și 64KB pentru rezervare; dacă parametrul este NULL, sistemul va furniza automat o adresă
  • dwSize - dimensiunea zonei
  • fAllocationType - specifică tipul operației: rezervare (MEM_RESERVE), alocare (MEM_COMMIT) sau renunțare la zonă (MEM_RESET); rezervarea unei zone înseamnă de fapt “punerea deoparte” a unui interval din spațiul de adrese virtuale al procesului, fără a se aloca însă memorie fizică; dacă se folosește MEM_COMMIT, se alocă efectiv memorie (dar doar dacă în prealabil zona vizată a fost rezervată); atunci când se renunță la zonă nucleul poate face discard la paginile din zonă, fără a face însă dezalocarea lor; după această operație datele nu se păstrează
  • flProtect - specifică modul de acces permis la zona alocată: PAGE_EXECUTE, PAGE_EXECUTE_READ, PAGE_EXECUTE_READWRITE, PAGE_EXECUTE_WRITECOPY, PAGE_READONLY, PAGE_READWRITE, PAGE_WRITECOPY, PAGE_NOACCESS, PAGE_GUARD, PAGE_NOCACHE. Modurile _WRITECOPY arată că se va folosi mecanismul copy-on-write. Modul PAGE_GUARD specifică faptul că la primul acces la o astfel de zonă se va genera o excepție STATUS_GUARD_PAGE. PAGE_GUARD și PAGE_NOCACHE se pot folosi împreună cu celelalte moduri.

Demaparea unei zone din spațiul de adresă

Pentru demaparea unei fișier mapat în memorie se folosește funcția UnmapViewOfFile:

BOOL UnmapViewOfFile(
   LPCVOID lpBaseAddress
);

Funcția primește adresa de început a zonei.

Pentru dezalocarea unei zone de memorie din spațiul de adresă se folosesc funcțiile VirtualFree și VirtualFreeEx:

BOOL VirtualFree(
   LPVOID lpAddress,
   SIZE_T dwSize,
   DWORD dwFreeType
);
BOOL VirtualFreeEx(
   HANDLE hProcess,
   LPVOID lpAddress,
   SIZE_T dwSize,
   DWORD dwFreeType
);

Funcția VirtualFreeEx va dezaloca o zonă de memorie din spațiul de adresă al unui proces arbitrar, specificat în parametrul hProcess. Procesul curent trebuie să aibă drepturi corespunzătoare asupra procesului pe care se încearcă operația (PROCESS_VM_OPERATION).

Parametrii lpAddress și dwSize identifică zona de dezalocat. dwFreeType specifică tipul operației: MEM_DECOMMIT, MEM_RELEASE. Prima operație va demapa paginile din spațiul de adresă, dar ele vor rămâne rezervate. Cea de-a doua operație va anula rezervarea întregii zone „puse deoparte” anterior, astfel încât adresa de start trebuie să coincidă cu adresa de start a zonei rezervate, iar dimensiunea trebuie să fie 0.

Schimbarea protecției unei zone mapate

În Windows, schimbarea drepturilor de acces a unei zone mapate se poate face cu ajutorul funcțiilor VirtualProtect și VirtualProtectEx:

BOOL VirtualProtect(
   LPVOID lpAddress,
   SIZE_T dwSize,
   DWORD flNewProtect,
   PDWORD lpflOldProtect
);
BOOL VirtualProtectEx(
   HANDLE hProcess,
   LPVOID lpAddress,
   SIZE_T dwSize,
   DWORD flNewProtect,
   PDWORD lpflOldProtect
);

Funcțiile vor schimba protecția paginilor care au măcar un octet în intervalul [lpAddress, lpAddress + dwSize - 1] la cea specificată în flNewProtect. Vechile drepturi de acces sunt salvate în lpfOldProtect.

Atenție! - Toate paginile din intervalul specificat trebuie să fie din aceeași regiune rezervată cu apelul VirtualAlloc sau VirtualAllocEx folosind MEM_RESERVE. Paginile nu pot fi localizate în regiuni adiacente rezervate prin apeluri separate ale VirtualAlloc sau VirtualAllocEx folosind MEM_RESERVE.

Interogarea zonelor mapate

Pentru a afla informații despre o zonă mapată în spațiul de adresă al unui proces se pot folosi funcțiile VirtualQuery și VirtualQueryEx. Ele vor oferi informații apelantului despre adresa de start a zonei, protecție, dimensiune etc.

DWORD VirtualQuery(
   LPCVOID lpAddress,
   PMEMORY_BASIC_INFORMATION lpBuffer,
   SIZE_T dwLength
);
DWORD VirtualQueryEx(
   HANDLE hProcess,
   LPCVOID lpAddress,
   PMEMORY_BASIC_INFORMATION lpBuffer,
   SIZE_T dwLength
);

Funcțiile primesc ca parametri o adresă din cadrul zonei ce se dorește a fi interogată, un pointer către un buffer alocat ce va primi informații despre zonă și întorc numărul de octeți scriși în buffer. Dacă funcția întoarce 0 înseamnă că nici o informație nu a fost furnizată. Acest lucru se întâmplă dacă funcției îi este pasată o adresă din spațiul kernel.

Informațiile primite vor descrie două zone: zona alocată (cu VirtualAlloc) în care este inclusă adresa dată, și zona care conține pagini de același fel (cu aceeași protecție și stare) în care este inclusă adresa dată:

typedef struct _MEMORY_BASIC_INFORMATION {
   PVOID BaseAddress;
   PVOID AllocationBase;
   DWORD AllocationProtect;
   SIZE_T RegionSize;
   DWORD State;
   DWORD Protect;
   DWORD Type;
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;

Câmpurile AllocationBase și AllocationProtect se referă la zona alocată, iar BaseAddress, RegionSize, Type și Protect la zona ce conține pagini de același fel. State indică starea paginilor din zonă: MEM_COMMIT pentru zonă alocată, MEM_RESERVED pentru zonă rezervată și MEM_FREE pentru zonă nealocată. Type indică dacă în zonă este mapat un fișier (MEM_IMAGE sau MEM_MAPPED) sau nu, și indică de asemenea dacă zona este partajată sau nu (MEM_PRIVATE).

Blocarea paginării

Pentru blocarea paginării pentru un set de pagini (nu se va mai face swapout - în consecință apelurile ulterioare nu mai produc page fault), sistemul de operare Windows pune la dispoziția utilizatorilor funcția VirtualLock:

BOOL VirtualLock(
   LPVOID lpAddress,
   SIZE_T dwSize
);

Funcția primește prin parametri un interval de pagini (alcătuit din paginile care au măcar un octet în intervalul [lpAddress, lpAddess + dwSize]) pentru care se vrea blocarea paginării.

Funcția pentru reactivarea paginării este VirtualUnlock:

BOOL VirtualUnlock(
   LPVOID lpAddress,
   SIZE_T dwSize
);

Excepții

Atunci când sistemul de operare detectează accese incorecte la memorie, va genera o excepție către procesul care a efectuat accesul. Pentru tratarea excepției se pot folosi construcții __try și __except, pentru care este necesar suport din partea compilatorului, sau se poate folosi funcția AddVectoredExceptionHandler.

PVOID AddVectoredExceptionHandler(
   ULONG FirstHandler,
   PVECTORED_EXCEPTION_HANDLER VectoredHandler
);
ULONG RemoveVectoredExceptionHandler(
   PVOID VectoredHandlerHandle
);

Funcția AddVectoredExceptionHandler va adăuga pe lista funcțiilor de executat atunci când se generează o excepție, pe cea primită ca parametru în VectoredHandler. Parametrul FirstHandler indică dacă funcția dorește să fie adăugată la începutul listei sau la sfârșit. Funcția de tratare a excepțiilor trebuie să aibă următoarea semnătură:

LONG WINAPI VectoredHandler(
   PEXCEPTION_POINTERS ExceptionInfo
);
typedef struct _EXCEPTION_POINTERS {
  PEXCEPTION_RECORD ExceptionRecord;
  PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
typedef struct _EXCEPTION_RECORD {
  DWORD ExceptionCode;
  DWORD ExceptionFlags;
  struct _EXCEPTION_RECORD* ExceptionRecord;
  PVOID ExceptionAddress;
  DWORD NumberParameters;
  ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;

În cazul unor excepții cauzate de un acces invalid la memorie, ExceptionCode va fi setat la EXCEPTION_ACCESS_VIOLATION sau EXCEPTION_DATATYPE_MISALIGNMENT, iar ExceptionAddress la adresa instrucțiunii care a cauzat excepția; NumberParameters va fi setat pe 2, iar prima intrare în ExceptionInformation va fi 0 dacă s-a efectuat o operație de citire sau 1 dacă s-a efectuat o operație de scriere. A doua intrare din ExceptionInformation va conține adresa virtuală la care s-a încercat accesarea fără drepturi, fapt care a dus la generarea excepției. Așadar, corespondentul câmpului si_addr din structura siginfo_t de pe Linux este ExceptionInformation pe Windows, NU ExceptionAddress.

Funcția de tratare a excepției înregistrată cu AddVectoredExceptionHandler trebuie să întoarcă EXCEPTION_CONTINUE_EXECUTION, dacă excepția a fost tratată și se dorește continuarea execuției, sau EXCEPTION_CONTINUE_SEARCH pentru a continua parcurgerea listei de funcții de tratare a excepțiilor, în caz că au fost înregistrate mai multe astfel de funcții.

Exerciții

În rezolvarea laboratorului, folosiți arhiva de sarcini lab07-tasks.zip

Observații: Pentru a vă ajuta la implementarea exercițiilor din laborator, în directorul utils din arhivă există un fișier utils.h cu funcții utile.

Platforma este la alegerea voastră. Punctajul maxim se poate obține fie pe Linux, fie pe Windows

Linux

  1. (0.5 punct) Investigarea mapării folosind pmap.
    • Intrați în directorul 1-intro și compilați sursa intro.c.
    • Rulați programul intro. Folosiți ENTER pentru a continua programul.
    • Folosiți comanda
       watch -d pmap $(pidof intro) 

      pentru a urmări modificările asupra memoriei procesului.

    • Urmăriți modificările care apar în urma diferitelor tipuri de mapare din cod.
    • De ce unele biblioteci sunt mapate cu drept de scriere?
    • Analizați mapările făcute de procesul init folosind comanda:
      sudo pmap 1

      .

  2. (1 punct) Scrierea în fișier - write vs mmap.
    • Intrați în directorul 2-compare și inspectați sursele write.c și mmap.c, apoi compilați.
    • Obțineți timpul de execuție al celor două programe folosind comanda time:
       time ./write; time ./mmap
    • Care timp este mai mare și de ce?
    • Hint:
      • Folosiți strace pentru a vedea câte apeluri de sistem se realizează pentru rularea fiecărui program.
    • Ce se întâmplă dacă în programul mmap.c schimbați flagul de creare al memoriei partajate din MAP_SHARED în MAP_PRIVATE? Cum explicați?
    • Hint:
      • man mmap, în special secțiunea despre MAP_PRIVATE.
      • Ce conține fișierul 'test_mmap' în ambele cazuri?
  3. (1 punct) Detectare 'buffer underrun' utilizând ElectricFence.
    • Intrați în directorul 3-efence și urmăriți sursa bug.c.
    • Compilați și rulați executabilul bug.
    • Folosiți ElectricFence pentru a prinde situația de 'buffer underrun'.
    • Hint:
      • Creați și rulați programul ef_bug utilizând makefile-ul Makefile_efence.
        • Instalati pachetul electric-fence in cazul in care biblioteca libefence.so nu se gaseste pe sistem.
      • Setați în bash
        export EF_PROTECT_BELOW=1
    • Explicați de ce bug-ul nu s-a manifestat anterior.
    • Hint: Urmăriți exemplul din secțiunea ElectricFence.
  4. (2 puncte) Copierea fișierelor folosind mmap.
    • Intrați în directorul 4-cp.
    • Completați sursa mycp.c astfel încât să realizeze copierea unui fișier primit ca argument.
    • Pentru aceasta, mapați ambele fișiere în memorie.
    • Realizați copierea ca o simplă copiere de vectori.
    • Hints:
      • Înainte de mapare, aflați dimensiunea fișierului sursă folosind fstat.
      • Nu uitați să trunchiați fișierul destinație la dimensiunea fișierului sursă.
      • Urmăriți comentariile cu TODO.
      • Revedeți secțiunea maparea fișierelor.
    • Puteți testa în felul următor:
      ./mycp Makefile /tmp/Makefile; diff Makefile /tmp/Makefile
    • Verificați cum realizează utilitarul cp copierea de fișiere (folosind mmap sau read/write).
    • De ce credeți că se folosește această variantă?
  5. (4 puncte) Tipuri de acces pentru pagini.
    1. (2 puncte)
      • Intrați în directorul 5-prot și inspectați sursa prot.c.
      • Să se creeze trei zone de memorie în spațiul de adresă, cu drepturi de citire, scriere, respectiv nici un drept.
      • Zonele vor avea dimensiunea de o pagină.
      • Să se testeze comportamentul programului când se fac accese de citire și scriere în aceste zone.
        • Hint: Urmăriți comentariile cu TODO 1.
    2. (2 puncte)
      • Adăugați un handler de tratare a excepțiilor care să remapeze zonele cu protecție de citire și scriere la generarea excepțiilor.
      • Hint: Urmăriți comentariile cu TODO 2.
  6. (0.5 puncte) Page fault-uri.
    • Intrați în directorul 6-faults și urmăriți conținutul fișierului fork-faults.c.
    • Câte page-fault-uri credeți că se realizează la rulare?
    • Compilați fișierul.
    • Folosiți utilitarul pidstat din pachetul sysstat care permite monitorizarea page fault-urilor unui proces (prin intermediul argumentului -r).
    • Rulați programul fork-faults. Folosiți ENTER pentru a continua programul.
    • Folosiți comanda
      pidstat -r -T ALL -p $(pidof fork-faults) 5 100

      pentru a urmări page fault-urile. Rulați comanda pentru fiecare secvență de program.

      • Comanda de mai sus vă afișează câte un mesaj la fiecare 5 secunde. Sincronizați apăsarea tastei ENTER cu afișajul comenzii pidstat.
    • Urmăriți evoluția numărului de page fault-uri pentru cele două procese: părinte și copil. Page fault-urile care apar în cazul unui copy-on-write în procesul copil vor fi vizibile ulterior și în procesul părinte.
  7. (1 punct) Blocarea paginării.
    • Vă aflați într-o situație în care trebuie să procesați în timp real datele dintr-un buffer și vreți să evitați swaparea paginilor.
      • Intrați în directorul 7-paging și completați TODO-urile astfel încât paginarea va fi blocată pentru variabila data pe parcursul lucrului cu aceasta, iar la final va fi deblocată.
      • Hints:
        • Adresa trebuie aliniată la limita unei pagini.
        • Revedeți secțiunea referitoare la blocarea paginării.
      • Cât de mare poate fi DATA_SIZE? Încercați cu diverse valori și explicați comportamentul.
      • Memoria blocată este prin definiție memorie rezidentă - nu poate fi trimisă pe swap. Puteți urmări cum se modifică dimensiunea memoriei rezidente (cea care nu poate fi trimisa pe swap) folosind comanda:
         ps -p $(pidof paging) -o pid,rss,vsz,comm 

        după fiecare pas al programului.

Bonus Linux

  1. (1 so karma) Schimbarea tipului de acces pentru pagini din segmentul de cod.
    • Intrați în directorul 8-hack.
    • Programul apelează funcția foo(). Având determinată pagina în care se află funcția în spațiul de adresă al procesului, i se schimbă drepturile de acces în PROT_READ|PROT_WRITE|PROT_EXEC și se modifică valoarea de retur a funcției (se scrie în segmentul de cod). Analizați cu atenție programul.
    • Analizați comportamentul cu gdb. Având pid-ul procesului afișat la stdout, folosiți pmap pentru a observa pagina cu drepturile schimbate.
    • Observați tipul de acces pentru celelalte pagini din spațiul de adresă al procesului.
    • Modificați drepturile de acces în PROT_READ|PROT_EXEC și recompilați sursa. Ce se întâmplă și de ce?

Windows

  1. (0.5 puncte) Maparea memoriei.
    • Deschideți proiectul 1-intro.
    • Inspectați și compilați sursa intro.c.
    • Rulați proiectul, iar în paralel urmăriți comportamentul programului intro în Task Manager - în special coloanele Mem Usage și Page Faults.
    • Hints:
      • Pentru a vedea o listă completă cu coloanele care pot fi activate, Task Manager→View→Select Columns.
  2. (1 punct) Crearea unor rutine în mod dinamic.
    • Deschideți proiectul 2-dyn și urmăriți sursa dyn.c.
    • Programul alocă memorie în spațiul de adresă al procesului pt a stoca o rutină, de forma dyncode.
      • Rutina va incrementa parametrul primit și va întoarce această valoare. Urmăriți conțintul lui code.
      • Deși în acest caz conținutul rutinei este definit direct în program prin code, el ar putea fi primit în orice alt mod (fișier, etc).
  3. (2 puncte) Mapare fișiere în memorie.
    • Să se scrie un program care copiază un fișier. Programul primește ca argumente numele fișierului sursă, numele fișierului desținatie, mapează în memorie cele două fișiere și copiază conținutul primului fișier folosind memcpy(3).
  4. (4 puncte) Tipuri de acces pentru pagini.
    • Încărcați proiectul 4-prot și inspectați sursa libvm.c.
    1. (2 puncte)
      • Să se creeze trei zone de memorie în spațiul de adresă, cu drepturi de citire, scriere, respectiv nici un drept.
      • Zonele vor avea dimensiunea de o pagină.
      • Să se testeze comportamentul programului când se fac accese de citire și scriere în aceste zone.
      • Hint:
        • Urmăriți comentariile cu TODO 1.
        • În Windows se poate apela VirtualProtect doar pentru o zonă de memorie alocată cu VirtualAlloc.
    2. (2 puncte)
      • Adăugați un handler de tratare a excepțiilor care să remapeze zonele cu protecție de citire și scriere la generarea excepțiilor.
      • Hint:
  5. (1 punct) Detectare 'buffer overrun' - implementare utilitar asemănător cu Electric Fence.
    • Încărcați proiectul 5-ef.
    • Inspectați sursa, ignorând pentru moment funcția MyMalloc.
    • Compilați și rulați proiectul. Ar trebui să apară erori?
    • Completați funcția MyMalloc astfel încât orice depășire a bufferului alocat să producă eroare.
    • Hints:
      • Alocați cu VirtualAlloc memorie de dimensiunea primită ca parametru + încă o pagină la final (o vom numi guard page).
      • Schimbați dreptul de acces pentru pagina de final în PAGE_NOACCESS utilizând VirtualProtect.
      • Întoarceți un pointer la o zonă de memorie cu dimensiunea egală cu dimensiunea cerută, dar care se termină fix înainte de guard page).
      • Urmăriți commentariile cu TODO.
    • Testați din nou folosind de data aceeasta MyMalloc, atât în cazul în care inițializarea vectorului depășește dimensiunea alocată, cât și în cazul în care nu depășește.
  6. (1.5 puncte) Blocarea paginării.
    • Vă aflați într-o situație în care trebuie să procesați în timp real datele dintr-un buffer și vreți să evitați swaparea paginilor.
      • Intrați în directorul 6-lock și completați TODO-urile astfel încât paginarea să fie blocată pentru variabila data pe parcursul lucrului cu aceasta, iar la final să fie deblocată.
      • Hints:
      • Cât de mare poate fi DATA_SIZE? Încercați cu diverse valori și explicați comportamentul.
      • Urmăriți comportamentul programului în Task Manager.

EXTRA

Comparați timpii de execuție ai algoritmilor de numărare a liniilor din fișier, aflați în această arhivă

Soluții

Resurse Utile