Gerenciamento de Memória Memória Principal
Principais tópicos: Aspectos Básicos Alocação de Memória Paginação Segmentação
Com a possibilidade de executar mais do que um processo simultaneamente, surgiu também a necessidade de gerenciar e distribuir o uso da memória principal entre os diferentes processos. A Memória Principal é o depósito primário de instruções e dados dos processos para o processador: Também chamada de RAM (Random Access Memory); É uma memória volátil; De acesso mais lenta que registradores ou cache; De acesso mais rápido que a memória secundária (HD, SSD, Pen drive, etc ).
Se comporta como um vetor. Ex.: 2GB de RAM - 2.147.483.648 bytes Menor endereço: 0 (zero) Maior endereço: 2147483647 Endereço 0 (zero) Endereço 1 (um)... Endereço 2147483646 Endereço 2147483647
Nossos programas utilizam variáveis: conjuntos de bytes reservados a partir de um determinado endereço da memória: Ex.: int utiliza 4 bytes. Inicia no endereço X e tem seu último byte no endereço X + 3; Um mesmo byte da memória não pode ser alocado para duas coisas distintas ao mesmo tempo! Na prática, processos alocam e liberam memória de forma dinâmica. Vários processos realizando estas atividades, de forma intercalada e por um longo período, podem ocasionar uma grande bagunça na memória!
Restrição de Acesso Memória Principal
Restrição de acesso: Através de ponteiros é possível navegar pelo endereçamento de memória. Por definições de segurança, SOs mais modernos restringem os endereços de memória que cada processo pode acessar. Isto é viabilizado por recursos de hardware, sendo que o processador é o responsável por restringir o acesso de um determinado processo às áreas de memória previamente liberadas pelo SO.
0 256000 300040 420940 880000 sistema operacional processo processo processo 300040 base 120900 limite 1024000 Registradores base e limite que definem um espaço de endereçamento lógico (Silberschatz at al., 2013)
Restrição de acesso: Os registradores de base e de limite só podem ter o seu conteúdo alterado quando o processador estiver executando em modo kernel. Desta forma, somente o SO poderá definir os endereços de memória que são acessíveis para cada processo.
base base + limite endereço sim sim CPU < memória não não Monitor de acesso à memória do SO (Silberschatz at al., 2013)
Vinculação de Endereços Memória Principal
Vinculação de Endereços: Cada dado (variável) utilizado por um processo será alocado em uma posição da memória; Em geral os SOs não restringem áreas específicas do acesso de processos, ex.: Endereço 0000 (zero) é geralmente utilizado como endereço de arranque para todo o SO; Ao longo da execução, esta área poderá ser liberada e até se tornar parte um processo do usuário.
Vinculação de Endereços: Como determinar em qual endereço uma variável ficará? Há várias perspectivas: Tempo de compilação Tempo de carga Tempo de execução (run time)
Vinculação de Endereços compilador ou montador outros módulosobjeto programafonte móduloobjeto tempo de compilação biblioteca do sistema linkage editor módulo de carga tempo de carga biblioteca do sistema carregada dinamicamente carregador imagem da memória binária em memória tempo de execução (run time)
Vinculação de Endereços - tempo de compilação: Ao desenvolver um programa, o programador pode determinar especificamente a posição de memória que cada variável estará; Se alguma alteração de hardware for feita (ex.: aumento de memória RAM), talvez seja necessário compilar um novo programa; Ex. de utilização: programas.com, do MS-DOS.
Vinculação de Endereços - tempo de carga: Se durante a programação não for possível determinar o local em memória onde o processo executará, o compilador deverá gerar um código relocável: Os endereços serão definidos no momento da carga, com base na situação da memória.
Vinculação de Endereços - tempo de execução: Se o processo puder ser movimentado de um segmento de memória para outro durante sua execução, então a vinculação deverá ser retardada até o momento de execução; Esta forma é dependente de recursos de hardware específicos; A maioria dos SOs de uso geral utiliza este modelo.
Endereçamento Lógico Memória Principal
Endereçamento Lógico: Os endereços gerados pela CPU são normalmente chamados de endereços lógicos: Também conhecidos como endereços virtuais; Os endereços utilizados para acessar a memória física são chamados de endereços físicos; O mapeamento entre endereço físico e lógico será realizado pela unidade gerenciadora de memória: MMU (memory-management unit);
Endereçamento Lógico: O processo de usuário jamais enxerga os endereços físicos reais; Resumo: Programa trabalha com endereços lógicos; A MMU realiza a conversão entre endereços lógicos e físicos: A arquitetura define a base do funcionamento; O SO fornece diretrizes e politicas sobre alocação para controlar a MMU;
registrador de relocação CPU endereço lógico 346 14000 + endereço físico 14346 memória MMU Realocação dinâmica usando um registrador de realocação. (Silberschatz at al., 2013)
Vinculação Dinâmica Memória Principal
Vinculação Dinâmica: Nossa abordagem, até agora, considera que todas as funções e variáveis estão implantadas de forma estática em um único arquivo executável que é carregado e executado como um processo pelo SO. Se um programa crescer demais em funcionalidades e dados, seu tamanho pode passar a exigir muito espaço em memória: Muitas funcionalidades de programas são pouco utilizadas, então não seria necessário manter todas elas em memória sempre!
Vinculação Dinâmica: SOs modernos, tais como Windows e Linux, suportam a utilização de Bibliotecas Compartilhadas: No Windows, são conhecidas como DLL (Dynamic-Link Library) - biblioteca de vínculo dinâmico; Estas bibliotecas geralmente não tem método principal (main), mas podem conter códigos executáveis (funções), dados e recursos (imagens, icones, sons, etc).
Vinculação Dinâmica: Para a utilização de Bibliotecas Dinâmicas, dois momentos devem ser levados em consideração: 1. Construção da Biblioteca Dinâmica: Momento em que programamos as suas funções, inserimos dados e recursos, além de transformar tudo isto em um único arquivo binário para distribuição;
Vinculação Dinâmica: Para a utilização de Bibliotecas Dinâmicas, dois momentos devem ser levados em consideração: 2. Utilização da Biblioteca Dinâmica: Momento em que desenvolvemos o programa que irá utilizar a biblioteca dinâmica: suas funções, dados e recursos; Precisaremos inserir pequenos stubs (trechos de código que explicam como utilizar as funções, dados e recursos da biblioteca.
Vinculação Dinâmica: Atividade prática. Requisitos: 1. SO Windows; 2. IDE Code::Blocks; 3. Compilador MingW; Primeiros passos: Na IDE crie dois projetos em um workspace (C++): a. Dynamic Link Library - MinhaDLL ; b. Console Application - UsarDLL.
//MinhaDLL (main.h) //Adicione no inicio: #include <iostream> //Adicione ao fim do arquivo: using namespace std;
//MinhaDLL (main.cpp)[1/2] //Adicione no inicio: #define SIZE 1000000 double * dados = NULL; //Adicione em case DLL_PROCESS_ATTACH: if (dados == NULL) { dados = new double[size]; double a; for(a = 0; a < SIZE; a++) { dados[a] = a; } } cout << "DLL carregada!" << endl;
//MinhaDLL (main.cpp)[2/2] //Adicione em case DLL_PROCESS_DETACH: delete dados; cout << "DLL liberada!" << endl;
Vinculação Dinâmica: Quando esta biblioteca for carregada, ela alocará memória para um vetor que utilizaria (apenas um exemplo). Agora implementamos o programa que interage com esta biblioteca. O programa utiliza a única função disponível (padrão do template do Code::Blocks) - void SomeFunction(LPCSTR). O programa carregará a biblioteca em partes, para podermos estudar a alocação da memória.
#include <iostream> #include <windows.h> using namespace std; typedef void(winapi* dllf)(lpcstr); int main() { string linha; cout << "Aperte Enter para carregar DLL" << endl; getline(cin, linha); HINSTANCE dll = LoadLibrary("MinhaDLL.dll"); if (dll!= NULL) { cout << "Aperte Enter para buscar a função" << endl; getline(cin, linha); dllf func = (dllf)getprocaddress(dll, "SomeFunction");
} if (func!= NULL) { cout << "Função SomeFunction encontrada!" << endl; func("olá mundo!"); } else { cout << "Erro ao procurar a função" << endl; } } else { cout << "Não foi possível carregar a DLL" << endl; } cout << "Aperte Enter fechar a biblioteca" << endl; getline(cin, linha); FreeLibrary(dll); cout << "Aperte Enter para sair" << endl; getline(cin, linha); return 0;
Vinculação Dinâmica: Clicar em: Build - Build Workspace Observações: 1. Para executar, o arquivo da DLL deve ser acessível ao nosso programa! 2. Testar observando ocupação de memória do processo! Atividade: Questão da lista de atividades individuais!
Alocação de Memória Contígua Memória Principal
Alocação de Memória Contígua Esta é a primeira forma de alocação de memória para multiprogramação, ela não é mais utilizada, mas auxilia na compreensão dos formatos atuais: O conceito inicial era dividir o grande vetor da memória física em pedaços de tamanhos iguais; Havia uma restrição em relação ao número de processos que poderiam existir; Cada processo ficava limitado ao seu pedaço inicial de memória;
Alocação de Memória Contígua Como alternativa, surgiu a abordagem de armazenar uma lista de blocos de memória livres (sem tamanho fixo). Para alocar um processo, era procurado um lugar na memória com espaço adequado. Surgiram diferentes estratégias de alocação, para encontrar um espaço na memória onde os processos caibam: First fit: procura pelo primeiro espaço de memória com tamanho suficiente para o processo. Best fit: procura pelo menor espaço de memória com tamanho suficiente para o processo. Worst fit: procura pelo maior espaço de memória (prevê o crescimento dos processos).
Alocação de Memória Contígua Fragmentação externa: Todas as estratégias de alocação de memória sofrem de fragmentação externa. Chamamos fragmentação externa quando a memória disponível se quebra em muitas partes pequenas: Quanto mais partes, maior a fragmentação; Geralmente estas partes não são grandes suficiente para armazenar novos dados; Pode haver o total de memória livre, necessária para uma alocação, mas ela estar separada nestas várias partes pequenas. Isto inviabiliza a alocação; Logo, quanto mais fragmentação: pior.
Alocação de Memória Contígua Fragmentação interna: As estratégias de alocação de memória também sofrem de fragmentação interna. Chamamos fragmentação interna quando os dados utilizados, de fato, não são múltiplos dos blocos de alocação: Ex.: um processo necessita de 597K e o SO reserva 600K. 3K estão fragmentados internamente; Alguns sistemas utilizam blocos com tamanhos variáveis para diminuir esta fragmentação; Novamente, quanto mais fragmentação: pior.
Conclusões Entre alocações e desalocações, diversos desafios diferentes surgem para o SO. Como gerenciar os espaços de memória? Como otimizar o uso para não ocorrer fragmentação em excesso? Como lidar com a fragmentação? E com a falta de memória física? Estas e outras respostas veremos na parte sobre Paginação e Segmentação.