Gerenciamento de Memória
Introdução
Endereçamento
Alocação
Memória
virtual
Desempenho
Os tópicos a serem examinados
podem ser encontrados nos seguintes capítulos de livros:
Modern
Operating Systems, Tanenbaum, cap. 3
Operating
Systems Concepts, Peterson/Silberschatz, caps. 7 e 8
Outros bons livros de sistemas
operacionais, nos capítulos sobre gerenciamento de
memória
1. Introdução
O gerenciamento de memória, em
conjunto com o gerenciamento de processos, forma o que se pode chamar
de coração de um sistema operacional. Sua importância
reside fundamentalmente no fato do processador executar instruções
trazidas da memória, sobre dados trazidos da memória e
guardando resultados na memória. Assim, passa a ser crucial o
gerenciamento de como a memória estará ocupada e como
as informações nela serão reconhecidas pelo
processador.
Isso se torna ainda mais crítico
se considerarmos que a memória não é apenas o
conjunto de bytes disponíveis na chamada RAM do computador.
Existem, na realidade, cinco diferentes níveis de memória,
diferindo em tamanho, custo e velocidade, que são:
Registradores
internos da CPU
Cache
Memória
principal
Memória
secundária
Bibliotecas
O gerenciamento de memória se
ocupa da memória principal e das suas interações
com cache e memória secundária. O gerenciamento dos
registradores e de boa parte da cache é feito pelo
compilador, que ao gerar o executável já determina como
os registradores serão ocupados e como a cache pode ser
ocupada para acelerar o processamento. Já o gerenciamento das
bibliotecas é feito pelo próprio usuário (ou
administrador) do sistema, enquanto boa parte do gerenciamento do
disco é feito pelo sistema de arquivos, que foi o tema do
capítulo anterior.
Desse modo, o gerenciamento de memória
se ocupa fundamentalmente do controle de quais dados vão para
a memória, de que forma são armazenados nela e como
podem ser acessados. Isso envolve atividades de endereçamento,
em que se mapeia endereços de disco para endereços de
memória, de alocação, em que se determina quais
espaços serão ocupados por quem, e de memória
virtual, em que se amplia o conceito de memória principal para
um tamanho infinito. Trataremos cada um desses pontos a seguir,
fechando o capítulo com um estudo de como características
de gerenciamento de memória e processos influencia no
desempenho de um sistema.
2. Endereçamento
O problema de endereçamento surge
quando se percebe que os endereços ocupados por um programa no
disco não correspondem aos que ele ocupará na memória.
Isso fica ainda pior quando se examina o código do programa,
em que aparecem instruções do tipo JMP 6000H,
em que a possição 6000H se refere a um dado
deslocamento a partir da origem do programa. Esse endereço
6000H possivelmente não se refere ao endereço
6000H da memória. Esses diferentes conjuntos de
endereços recebem os nomes de espaços de endereços
lógicos, no disco, e endereços físicos, na
memória. O problema de endereçamento passa a ser,
portanto, determinar uma estratégia de conversão entre
endereços lógicos e endereços físicos.
Essa transformação deve ser feita de forma que a
execução do programa seja efetivada com sucesso ao
carregar-se o mesmo na memória. As estratégias para
efetuar esse mapeamento são:
Endereçamento
Absoluto, em que os endereços físicos são
absolutamente iguais aos endereços lógicos;
Relocação
Estática, em que os endereços lógicos são
transformados em endereços físicos, recalculados a
partir da diferença entre as origens do programa em disco e
na memória, no instante do carregamento do programa;
Relocação
dinâmica, que faz o cálculo do endereço
físico apenas no momento da execução de cada
instrução, a partir do conteúdo de um
registrador especial (originalmente chamado de fence register);
Segmentação,
que nada mais é do que o uso de relocação
dinâmica aplicado ao conceito de se dividir o programa em
partes menores, como código, dados, etc.
3. Alocação
Tendo definido o método de
conversão entre endereços lógicos e físicos,
é preciso tratar algo que ocorre momentos antes de se carregar
um arquivo ou programa na memória (observem que daqui por
diante chamaremos programas e arquivos indistintamente como sendo
segmentos). Para que se saiba o endereço base de um segmento é
preciso que se defina em que lugar da memória ele será
alocado. Essa alocação deve ocorrer sob regras bastante
bem definidas para que não ocorram problemas de interferência
entre os processos. Existem duas estruturas básicas de
alocação de memória, que são em espaços
contíguos e em blocos, descritas a seguir:
Espaços
contíguos A alocação de memória
em espaços contíguos é o modelo mais simples de
alocação, em que para o segmento ir para a memória
ele deve caber inteiro em um único trecho, com todos os seus
bytes alocados de modo contínuo. Embora simples esse modelo
de alocação trás diversos inconvenientes, como
fragmentação externa, necessidade de recompactação,
mapeamento complexo e dificuldade na escolha do espaço livre
a ser ocupado. Como se pode observar, a alocação em
espaços contíguos trás mais problemas do que
vantagens.
Algumas
soluções tentadas para corrigir seus problemas
envolvem a alocação em segmentos de tamanho fixo e
segmentos de tamanho fixo reconfiguráveis (sistemas buddy).
Infelizmente, apesar de tais modificações terem facilitado o gerenciamento, elas não eliminaram a fragmentação
externa e ainda criaram a fragmentação interna.
Blocos
A solução para o problema de fragmentação
veio com a organização de espaços usada em
discos. Ao dividir-se a memória em blocos de tamanho fixo, e
permitir-se que um segmento seja quebrado em vários blocos,
eliminou-se definitivamente a fragmentação externa,
uma vez que um segmento apenas deixaria de ser carregado para a
memória caso esta não tivesse blocos livres
suficientes para o segmento (esse problema foi resolvido
posteriormente com a introdução de paginação).
Dentro dessa estratégia um segmento é dividido
entre várias páginas, cada uma podendo ser alocada nos
espaços livres de memória disponíveis no
momento da alocação, independentemente de serem
próximos ou não. Nesse esquema a fragmentação
externa deixa de existir e a interna fica limitada ao tamanho de uma
página. O problema nessa estratégia é calcular
o endereço físico correspondente a um endereço
lógico qualquer e evitar que se façam muitos acessos à
memória em busca de um único dado. Isso é
resolvido por:
Cálculo
de endereço: Dado um endereço lógico
qualquer, seu endereço físico correspondente é
determinado dividindo-o pelo tamanho da página, resultando
em um número inteiro (o quociente) correspondente ao número
da página e outro inteiro (o resto) correspondente ao
deslocamento em bytes dentro da página. O problema nesse
processo é que para acessar o conteúdo do endereço
físico correspondente ao dado endereço lógico
temos que fazer três acessos à memória (um para locallizar a tabela de páginas, outro para localizar a página específica e o terceiro para acessar o endereço desejado), o que
tomaria muito tempo.
Otimização de
acesso: O problema de três acessos pode ser
substancialmente reduzido se fizermos uso de memórias cache
(ou memórias associativas), em que parte das informações
necessárias estariam disponíveis em cache.
O uso de memória alocada em
blocos melhora significativamente o desempenho da memória.
Entretanto ela não resolve o problema de que se um segmento
ocupa mais páginas do que as que estão livres, então
ele não pode ser colocado para executar.
Essa restrição
é desnecessária, uma vez que se sabe que durante a
execução de um programa ele não precisa acessar
todas as suas instruções e todos os seus dados ao mesmo
tempo. Então, se for possível deixar na memória
apenas aquilo que está sendo necessário num dado
instante, poderíamos usar melhor a memória. Isso é
feito através do mecanismo de memória virtual,
examinado a seguir.
4. Memória Virtual
A solução para a alocação
de segmentos maiores do que o espaço disponível na
memória (ou até mesmo maior que ela toda) veio com um
dos conceitos mais importantes de otimização de
programas e sistemas, que é o Princípio da
Localidade. Este princípio diz que os endereços de
memória não têm probabilidade igual de acesso,
sendo mais provável que após executar uma instrução
da página x, que acesse um dado da página y,
é muito mais provável que a próxima instrução
também esteja na página x e também acesse
dados na página y.
Desse modo, se um segmento ocupa
muitas páginas, seria possível dizer que se estamos
acessando um certo número delas num dado instante, então
nos manteríamos acessando-as por mais algum tempo, não
necessitando portanto que as demais páginas estivessem na
memória.
Esse é o princípio de memória
virtual. Seu funcionamento ocorre através de paginação
por demanda, isto é, uma página vai para a memória
apenas no momento em que é requisitada por um processo. O
problema passa a ser, então, o que fazer se não houver
mais páginas livres na memória. A solução
é escolher uma das páginas alocadas para sair da
memória, liberando portanto seu espaço. Essa operação
é conhecida como de swapping, em que se faz o swap-out
de uma página (a escolhida para sair) e o swap-in de
outra (a demandada).
Antes de discutirmos o funcionamento de
memória virtual precisamos definir alguns termos importantes
para fazer a análise dos vários algoritmos de
escalonamento. Esses termos são:
Falta
de página, que é o evento que ocorre quando se
precisa acessar um endereço de uma página que não
está na memória;
Conjunto
residente, que é o conjunto das páginas que estão
na memória em um dado instante;
Tamanho
do conjunto residente, é o número de páginas
ocupadas (pelo sistema ou segmento) num dado momento;
Sequência de referência,
que é uma sequência de páginas que deverão
ser acessadas pelo sistema ao longo do tempo.
Todo o processo de memória
virtual passa a ser o gerenciamento de operações de
swapping, procurando obter o melhor resultado possível
a partir do princípio da localidade. Existem diversos
algoritmos propostos para fazer essa escolha, dentre os quais temos :
FIFO,
que escolhe para sair a página que entrou na memória
há mais tempo.
LRU,
que é um algoritmo de pilha em que o critério de
escolha da página indica que a página excluída
será aquela que não é referenciada há
mais tempo.
Optimal,
também é um algoritmo de pilha, mas escolhe para sair
a página que levará mais tempo para ser novamente
necessária.
Clock-FINUFO,
que faz uma implementação simplificada do LRU, tomando
por base valores aproximados dos reais quanto ao último
acesso à página.
Segunda chance, que é
similar ao FINUFO, porém a página escolhida para sair
teria que ter os bits de acesso e de modificação
zerados.
Além desses algoritmos diversos
outros foram propostos. Alguns trabalhando com o conceito de que cada
processo teria número fixo de páginas, quando a página
a ser retirada seria sempre do processo que precisa de uma nova
página. Outros com o conceito de tempo fixo entre faltas de
páginas, em que o número de páginas de cada
processo é variado para que o intervalo entre duas faltas
consecutivas seja mantido constante.
5. Desempenho
O uso de memória virtual permite
que mais segmentos sejam carregados na memória por vez, o que
permite um aumento no número de processos executando. A partir
disso é interessante perceber algumas situações
que afetam o desempenho do sistema. Antes porém é
preciso definir alguns conceitos básicos:
Nível
de multiprocessamento - indica o número de processos
executando no sistema
Falta de
páginas - indica o número de faltas de páginas
ocorridas no sistema
Ocupação da CPU -
indica a porcentagem de tempo em que a CPU está executando
processos de usuários
Considerando esses aspectos, temos que:
Quanto
mais processos executando melhor o nível de ocupação
da CPU, uma vez que quando um processo é interrompido para
fazer E/S ou por bloqueio, temos vários outros para assumir
seu lugar na CPU;
Quanto
mais processos executando mais falhas de páginas temos, uma
vez que cada processo passa a ocupar menos páginas e, com
isso, passa a ser mais provável que uma página
requisitada não esteja na memória;
Um número elevado de
processos executando acaba tendo um péssimo nível de
ocupação da CPU, uma vez que com o crescimento do
número de processos teriámos uma maior ocupação,
mas com mais processos é capaz de todos terem tão
poucas páginas que ficam o tempo todo causando falta de
páginas e, com isso, não podem ocupar a CPU. Essa
situação recebe o nome de thrashing.
Assim, temos algumas diretrizes para
avaliar o desempenho de um sistema. Basta verificar taxas de ocupação
da CPU, número de faltas de páginas (ou por quanto
tempo o sistema fica ocupado realizando paginação),
taxa de ocupação dos dispositivos de E/S, etc. Com
essas medidas em mãos um analista pode sugerir mudanças
no sistema, como aumento de memória, troca de dispositivos de
E/S, troca de mecanismos de caching (preenchimento da cache
com dados), entre outras. O importante aqui é lembrar
sempre que uma análise cuidadosa do sistema, levando em
consideração todas as variáveis possíveis
e as diretrizes anteriormente indicadas, é uma tarefa que bem
executada resulta em ganhos consideráveis ao sistema e ao seu
financiador, devendo portanto ser uma tarefa bastante bem remunerada.
6. Visão global do sistema
Agora que terminamos a análise individual de cada um dos mecanismos do SO podemos ter fazer uma avaliação de seu funcionamento global. Tomaremos como exemplo a ocorrência de uma falta de página e das ações decorrentes dela.
Na ocorrência de uma falta de página, identificada pelo gerenciamento de memória, é necessário:
ativar ações do gerenciamento de processos, para bloqueio do processo que gerou a falta;
ativar ações do gerenciamento de memória para definição de qual página sairá da memória, já marcando-a como fora da memória para evitar acessos incorretos;
ativar ações do sistema de arquivos para a localização das páginas envolvidas no swapping;
ativar ações do gerenciamento de disco para realizar fisicamente o swapping;
ativar, quando necessário, os mesmos mecanismos quando da conclusão das ações disparadas.
Disso percebe-se que existe muita interação entre os vários mecanismos do SO, o que apenas é possível de ser feito eficientemente se forem implementados modularmente, na forma de {\em threads}, por exemplo.
|