XV6 Gerenciamento de Memória

Introdução

Historicamente, a memória principal sempre foi vista como um recurso escasso e caro. Uma das maiores preocupações dos projetistas foi desenvolver sistemas operacionais que não ocupassem muito espaço de memória e, ao mesmo tempo, otimizassem a utilização dos recursos computacionais. Mesmo atualmente, com a redução de custo e consequente aumento da capacidade da memória principal, seu gerenciamento é um dos fatores mais importantes no projeto de sistemas operacionais.

Enquanto nos sistemas mono programáveis a gerência da memória não é muito complexa, nos sistemas multi programáveis essa gerência se torna crítica, devido à necessidade de se maximizar o número de usuários e aplicações utilizando eficientemente o espaço da memória principal.

Funções Básicas

Em geral, programas são armazenados em memórias secundárias, como disco ou fita, por ser um meio não-volátil e de baixo custo. Como o processador somente executa instruções localizadas na memória principal, o sistema operacional deve sempre transferir programas da memória secundária para a memória principal antes de serem executados. Como o tempo de acesso à memória secundária é muito superior ao tempo de acesso à memória principal, o sistema operacional deve buscar reduzir o número de operações de E/S à memória secundária, caso contrário, sérios problemas no desempenho do sistema podem ser ocasionados.

A gerência de memória deve tentar manter na memória principal o maior número de processos residentes, permitindo maximizar o compartilhamento do processador e demais recursos computacionais. Mesmo na ausência de espaço livre, o sistema deve permitir que novos processos sejam aceitos e executados. Isso é possível através da transferência temporária de processos residentes na memória principal para a memória secundária, liberando espaço para novos processos. Este mecanismo é conhecido como swapping.

Outra preocupação na gerência de memória, é permitir a execução de programas que sejam maiores que a memória física disponível, implementando através de técnicas como overlay e memória virtual.

Em um ambiente de multiprogramação, o sistema operacional deve proteger as áreas de memória ocupadas por cada processo, além da área onde reside o próprio sistema. Caso um programa tente realizar algum acesso indevido à memória, o sistema de alguma forma deve impedi-lo. Apesar de a gerência de memória garantir a proteção de áreas da memória, mecanismos de compartilhamento devem ser oferecidos para que diferentes processos possam trocar dados de forma protegida.

Swapping

Existe situações nas quais não é possível manter todos os processos simultaneamente na memória. Por exemplo, considere a Figura 1. Suponha que o processo 2 faça uma chamada de sistema, solicitando que sua área de memória seja aumentada. Embora existam áreas de memória ainda livre, nenhuma é contígua à área ocupada pelo processo 2. Outro exemplo é a situação na qual um usuário em terminal solicita o disparo de um programa, e não existe memória disponível no momento, mas é política do sistema disparar uma imediatamente todos os programas que são solicitados via um terminal.

Figura 1 - Memória física dividida em partições variáveis.

Uma solução para essas situações é mecanismo chamado de swapping. A gerência de memória reserva uma área do disco para o seu uso. Em determinadas situações, um processo é completamente copiado da memória para o disco. Sua execução é suspensa, ou seja, seu descritor de processo é removido da fila do processador e colocado em uma fila de processos suspensos. É dito que esse processo sofreu um swap-out. Mais tarde, ele sofrerá um swap-in, ou seja, será copiado novamente para a memória. Seu descritor de processo volta então para a fila do processador, e sua execução será retomada. O resultado desse revezamento no disco é que o sistema operacional consegue executar mais processos do que caberia em um mesmo instante na memória.

Swapping impõe aos programas um grande custo em termos de tempo de execução. Copiar todo o processo da memória para o disco e mais tarde de volta para a memória é um operação demorada. É necessário deixar o processo um tempo razoável no disco para justificar tal operação. Por exemplo, em torno de alguns segundos.

Em sistemas nos quais uma pessoa interage com o programa durante a sua execução (chamado antigamente de modo timesharing), o mecanismo de swapping somente é utilizado em último caso, quando não é possível manter todos os processos na memória. A queda no desempenho do sistema é imediatamente sentida pelo usuário no terminal. Para queda no desempenho do sistema é imediatamente sentida pelo usuário no terminal. Para processos que são executados em background, ou seja, desvinculados de um terminal, o mecanismo torna-se mais aceitável.

Swapping pode ser usado tanto com partições fixas quanto com partições variáveis. Caso o processo, no momento do swap-in, volte para uma posição diferente de memória, é necessário corrigir os seus endereços. Isso não é necessário quando se usa um mecanismo baseado em registrador de base, como mostrado na Figura 2. Nesse caso, basta corrigir o conteúdo do registrador de base, e o programa executará corretamente em sua nova posição na memória física.

Figura 2 - Mecanismo de proteção para a memória com registradores de base e limite.

Técnicas de Overlay

Todos os programas estão limitados ao tamanho da área de memória principal disponível para o usuário. Uma solução encontrada para o problema é dividir o programa em módulos, de forma que seja possível a execução independente de cada módulo, utilizando uma mesma área de memória.

Considere um programa que tenha três módulos: um principal, um de cadastramento e outro de impressão, sendo os módulos de cadastramento e de impressão independentes. A independência do código significa que quando um módulo estiver na memória para execução, o outro não precisa necessariamente estar presente. O módulo principal é comum aos dois módulos; logo, deve permanecer na memória durante todo o tempo da execução do programa.

Como podemos verificar na Figura 3, a memória é insuficiente para armazenar todo o programa, que totaliza 9kb. A técnica de overlay utiliza uma área de memória comum, onde os módulos de cadastramento e de impressão poderão compartilhar a mesma área de memória (área de overlay). Sempre que um dos dois módulos for referenciado pelo módulo principal, o módulo será carregado da memória secundária para a área de overlay. No caso de uma referência a um módulo que já esteja na área de overlay, a carga não é realizada; caso contrário, o novo módulo irá sobrepor-se ao que já estiver na memória principal.

A definição das áreas de overlay é função do programador, através de comandos específicos da linguagem de programação utilizada. O tamanho de uma área de overlay é estabelecido a partir do tamanho do maior módulo. Por exemplo se o módulo de cadastramento tem 4kb e o módulo de impressão 2kb, a área de overlay deverá ter o tamanho do maior módulo, ou seja, 4kb.

A técnica de overlay tem a vantagem de permitir ao programador expandir os limites da memória principal. A utilização dessa técnica exige muito cuidado, pois pode trazer implicações tanto na sua manutenção quanto no desempenho das aplicações devido à possibilidade de transferência excessiva dos módulos entre a memória principal e a secundária.

Figura 3 - Técnica de Overlay

XV6

O XV6 aloca a maior parte da memória de espaço do usuário implicitamente: fork aloca a memória necessária para a cópia filho da memória pai, e exec aloca memória suficiente para manter o arquivo executável. Um processo que precisa de mais memória em tempo de execução (talvez para malloc) pode chamar sbrk(n) para aumentar sua memória de dados em n bytes; sbrk retorna a localização da nova memória.

Para o caso específico de uma arquitetura x86, quando o processo executa a tabela de páginas é atualizada diretamente pelo hardware como mostrado na Figura 4.

Figura 4 - x86 Tabela de página.

Da Figura 4, note que o endereço inicial da tabela de página é alocada no registrador CR3. Tal registrador é atualizado quando chaveamos de um processo para outro no XV6. Em particular, tal mudança é feita na função switchuvm do arquivo vm.c

void switchuvm(struct proc *p){if(p == 0) panic("switchuvm: no process");if(p->kstack == 0) panic("switchuvm: no kstack");if(p->pgdir == 0) panic("switchuvm: no pgdir");pushcli();mycpu()->gdt[SEG_TSS] = SEG16(STS_T32A, &mycpu()->ts, sizeof(mycpu()->ts)-1, 0);mycpu()->gdt[SEG_TSS].s = 0;mycpu()->ts.ss0 = SEG_KDATA << 3;mycpu()->ts.esp0 = (uint)p->kstack + KSTACKSIZE;// setting IOPL=0 in eflags *and* iomb beyond the tss segment limit// forbids I/O instructions (e.g., inb and outb) from user spacemycpu()->ts.iomb = (ushort) 0xFFFF;ltr(SEG_TSS << 3);lcr3(V2P(p->pgdir)); // switch to process's address spacepopcli();}

Em particular a mudança ocorre na penúltima linha (lcr3(V2(p->pgdir))). As linhas anteriores atualizam os segmentos presentes no x86, pode ignorar as mesmas. Note o uso da macro V2P. Macros como essa ajudam a mapear endereços reais para virtuais e vice versa.

Quando uma página é criada a mesma vai fazer uso da função kalloc. Quando é liberado o kfree é chamado. As duas chamadas estão no arquivo kalloc.c. O kalloc pega o próximo frame de 4kb e retorna. Tal frame deve ser inserido na tabela de páginas. o kfree marca o frame como livre.

void freerange(void *vstart, void *vend);extern char end[]; // first address after kernel loaded from ELF file
struct run { struct run *next;};
struct { struct spinlock lock; int use_lock; struct run *freelist;} kmem;
// Initialization happens in two phases.// 1. main() calls kinit1() while still using entrypgdir to place just// the pages mapped by entrypgdir on free list.// 2. main() calls kinit2() with the rest of the physical pages// after installing a full page table that maps them on all cores.voidkinit1(void *vstart, void *vend){ initlock(&kmem.lock, "kmem"); kmem.use_lock = 0; freerange(vstart, vend);}
voidkinit2(void *vstart, void *vend){ freerange(vstart, vend); kmem.use_lock = 1;}
voidfreerange(void *vstart, void *vend){ char *p; p = (char*)PGROUNDUP((uint)vstart); for(; p + PGSIZE <= (char*)vend; p += PGSIZE) kfree(p);}
//PAGEBREAK: 21// Free the page of physical memory pointed at by v,// which normally should have been returned by a// call to kalloc(). (The exception is when// initializing the allocator; see kinit above.)voidkfree(char *v){ struct run *r;
if((uint)v % PGSIZE || v < end || v2p(v) >= PHYSTOP) panic("kfree");
// Fill with junk to catch dangling refs. memset(v, 1, PGSIZE);
if(kmem.use_lock) acquire(&kmem.lock); r = (struct run*)v; r->next = kmem.freelist; kmem.freelist = r; if(kmem.use_lock) release(&kmem.lock);}
// Allocate one 4096-byte page of physical memory.// Returns a pointer that the kernel can use.// Returns 0 if the memory cannot be allocated.char*kalloc(void){ struct run *r;
if(kmem.use_lock) acquire(&kmem.lock); r = kmem.freelist; if(r) kmem.freelist = r->next; if(kmem.use_lock) release(&kmem.lock); return (char*)r;}

Aluno

Bruno Adriano Menegotto

Engenharia de Computação - UEPG

REA - Recursos Educacionais Abertos

Sistemas Operacionais - Prof. Dierone Foltran