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.

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.
.jpg)
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.

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.
.png)
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 space
mycpu()->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.
void
kinit1(void *vstart, void *vend)
{
initlock(&kmem.lock, "kmem");
kmem.use_lock = 0;
freerange(vstart, vend);
}
void
kinit2(void *vstart, void *vend)
{
freerange(vstart, vend);
kmem.use_lock = 1;
}
void
freerange(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.)
void
kfree(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