Resumos dos capítulos :::

Gerenciamento de Memória

 

  1. Introdução

  2. Endereçamento

  3. Alocação

  4. Memória virtual

  5. Desempenho

Os tópicos a serem examinados podem ser encontrados nos seguintes capítulos de livros:

  1. Modern Operating Systems, Tanenbaum, cap. 3

  2. Operating Systems Concepts, Peterson/Silberschatz, caps. 7 e 8

  3. 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:

  1. Registradores internos da CPU

  2. Cache

  3. Memória principal

  4. Memória secundária

  5. 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:

  1. Endereçamento Absoluto, em que os endereços físicos são absolutamente iguais aos endereços lógicos;

  2. 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;

  3. 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);

  4. 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:

  1. 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.

  2. 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:

    1. 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.

    2. 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:

  1. 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;

  2. Conjunto residente, que é o conjunto das páginas que estão na memória em um dado instante;

  3. Tamanho do conjunto residente, é o número de páginas ocupadas (pelo sistema ou segmento) num dado momento;

  4. 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 :

  1. FIFO, que escolhe para sair a página que entrou na memória há mais tempo.

  2. 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.

  3. Optimal, também é um algoritmo de pilha, mas escolhe para sair a página que levará mais tempo para ser novamente necessária.

  4. Clock-FINUFO, que faz uma implementação simplificada do LRU, tomando por base valores aproximados dos reais quanto ao último acesso à página.

  5. 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:

  1. Nível de multiprocessamento - indica o número de processos executando no sistema

  2. Falta de páginas - indica o número de faltas de páginas ocorridas no sistema

  3. Ocupação da CPU - indica a porcentagem de tempo em que a CPU está executando processos de usuários

Considerando esses aspectos, temos que:

  1. 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;

  2. 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;

  3. 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:

  1. ativar ações do gerenciamento de processos, para bloqueio do processo que gerou a falta;

  2. 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;

  3. ativar ações do sistema de arquivos para a localização das páginas envolvidas no swapping;

  4. ativar ações do gerenciamento de disco para realizar fisicamente o swapping;

  5. 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.

 
 
DISCLAIMER
 
 
O conteúdo desta página é de minha total responsabilidade, não representando o pensamento ou posições da UNESP.