Archive for September, 2008

Uma pitada de Memória Virtual

11 de September de 2008

Agora vamos deixar de conversinha mole e vamos logo ao que interessa. O exemplo que descrevi em outro post mostra como implementar um driver bem simples que armazena uma lista de strings que são enviadas ao driver através de operações de escrita (WriteFile), e as mesmas strings são retornadas em subseqüentes operações de leitura (ReadFile). Esse é um bom exemplo de como os dados das aplicações vão para os drivers e vice-versa. Existem três maneiras de acessar os dados oferecidos por aplicações a partir de um driver. Neste post vamos dar uma olhadinha em memória virtual para que possamos ter uma base para posts futuros, onde poderei explicar as diferenças entre aquelas tais três maneiras. Vamos nos ater a apenas algumas características básicas da memória virtual para que as coisas que explicarei em seguida façam mais sentido para você, mas se você quiser mais detalhes a respeito de Memória Virtual, você pode ler o Capítulo 7, “Memory Management”, do Windows Internals, ou dar uma olhadinha em uma apresentação feita pelo meu amigo Strauss.

O que é uma página de memória

Página de memória é uma unidade utilizada pelo hardware nas proteções de acesso à memória. Mais sobre proteções de páginas de memória aqui. Essa é uma unidade definida pelo hardware que ajuda a subdividir a memória em espaços menores e gerenciáveis. Apesar de existirem dois tamanhos de páginas (uma pequena de 4KB bytes, e outra grande de 4MB), toda referência ao tamanho de página vista na documentação são referentes apenas às páginas pequenas. O tamanho da página de memória varia com a plataforma de hardware. Em sistemas x86 e x64, as paginas são de 4KB enquanto que em sistemas IA64 a página é de 8KB. O tamanho da página pode ser obtido pela constante PAGE_SIZE.

Espaço de endereçamento

De uma maneira bem resumida, memória virtual é um mecanismo, que em um trabalho conjunto entre hardware e software, permite que processos que rodem no Windows tenham seu próprio espaço de endereçamento. Espaço quem? Espaço de endereçamento, ou Address Space como normalmente vimos na referência, permite que cada processo tenha uma visão privada da memória que esteja utilizando. Ou seja, um processo não pode ler ou escrever em páginas de memória de outro processo. Isso evita que um programa mal escrito possa erroneamente escrever em páginas de memória de outro processo ou mesmo em páginas do sistema operacional, comprometendo assim a estabilidade de todo o sistema. Portanto, podemos assumir que o espaço de endereçamento de um processo só é acessível pelo processo ao qual ele pertence.

Nota: É possível compartilhar memória entre processos, mas mesmo neste caso, a mesma página física de memória é referenciada por dois ou mais espaços de endereçamentos diferentes.

Memória Virtual e Memória Física

Logo após a descoberta do fogo, a memória era endereçada diretamente em modo real. Isso significa que um ponteiro utilizado por uma aplicação acessava o dado exatamente onde ele se encontrava fisicamente nos chips de memória RAM. Páginas de memória virtual associadas aos processos refletem páginas físicas de memória através de um endereçamento virtual. Os bits que compõem o endereço virtual mapeam a memória física que se deseja acessar. Ao deferenciar um ponteiro que contem um endereço virtual, o processador, de maneria transparente ao processo, consulta estruturas preenchidas pelo sistema operacional de forma a traduzir o endereço virtual em real, e dessa forma, encontra a página física onde o dado realmente se encontra.


A disposição das páginas de memória virtual não tem relação com sua disposição física. Isso significa que você quando você faz uma grande alocação de memória, que é composta por várias páginas de memória, você ganha um ponteiro que, do ponto de vista da aplicação, aponta para páginas de memória que estão linearmente distribuídas (uma seguida da outra), enquanto que as páginas de memória física podem estar todas espalhadas pelos chips de RAM.

A memória pode não estar na RAM

A memória utilizada pelas aplicações não se limita à quantidade de memória oferecida pelos chips de RAM instalados em sua placa mãe. Quando os processos, cada vez mais sedentos por memória, vão sendo executados, o espaço disponível em RAM vai sendo compartilhado entre os vários processos. Páginas de memória acessadas com menos frequência são removidas dos chips de RAM e vão para disco (para o Pagefile.sys para ser mais preciso), dando lugar à uma outra página de memória que é necessária naquele momento. Esse processo recebe o nome de paginação. Quando a aplicação finalmente acessar o dado que está naquela página agora em disco, o sistema aloca espaço em RAM física para trazer de volta a página do disco para a RAM. Isso pode resultar em outras páginas que estavam em RAM a serem paginadas para o disco.


Para as aplicações, o processo de paginação é completamente transparente. O desenvolvedor não está nem um pouco preocupado se a região de memória que o programa vai acessar está em disco ou está em RAM. É só acessar o dado e o processador junto com o gerenciador de memória resolve isso para você. Infelizmente, a história não é tão florida para os desenvolvedores de drivers. Aplicações sempre rodam em uma prioridade de thread baixa (PASSIVE_LEVEL) e ao acessar um dado que esteja em uma página em disco, a thread é interrompida pelo gerenciador de memória para que este tenha a oportunidade de acionar os drivers de File System e por conseqüência os drivers de disco, aguardar o I/O ser realizado (apesar de levar um cacalhésimo de segundo, ainda temos que esperar) e então a thread é liberada. Embora haja pessoas que não acreditem nessas coisas, a plataforma NT é completamente preemptiva. Isso significa que uma thread pode ser interrompida por outra de maior prioridade. Em Kernel Mode, uma thread pode estar em prioridades superiores à APC_LEVEL, que é a prioridade em que as paginações são realizadas. Se a thread que acessará o dado estiver em um nível de prioridade muito alta (maior ou igual a DISPATCH_LEVEL), o processador não vai conseguir fazer a paginação e uma tela azul irá aflorar aos seus olhos. Um exemplo típico de execução em alta prioridade é o tratamento de interrupções, mas não vamos nos desviar do assunto.

Uma página pode ser acessada não só por um software, mas também por hardware. Hein? Uma aplicação pode passar o ponteiro de uma região de memória para um driver, que por sua vez, irá utilizar um canal de DMA para preencher este buffer. A cópia de um buffer através do processo de DMA não utiliza ciclos de CPU para ser realizada. Preencher um buffer é algo tão simples que até um chimpanzé autista pode fazer. Assim um grupo de chimpanzés autistas engenheiros criaram um chip que faz isso, mas isso não vem ao caso agora. Este chip pode estar na placa mãe ou na própria placa controlada pelo driver. O chip é programado pelo driver e uma transferência é iniciada. Durante a cópia, nem a aplicação, nem o sistema operacional e nem mesmo o processador ficam cientes do que está acontecendo. O sistema pode então determinar que as páginas utilizadas na transferência, que não estão sendo acessadas por nenhum processo, devem ser paginadas para disco. Se isso ocorrer, o chip de DMA continuará tranferido bytes para aquele endereço físico de RAM e… Bom, já sabe né? Para evitar isso, existem meios de avisar o gerenciador de memória que uma ou mais páginas não devem ser paginadas.

Durante o desenvolvimento de um driver, você pode querer alocar um buffer que será utilizado por uma ISR (Interrupt Service Routine). Como uma ISR é executada em alta prioridade, podemos fazer uma alocação de memória solicitando um buffer que não seja paginável, e assim, garantir que o buffer obtido por esta alocação esteja sempre em RAM. Mas vá com calma, memória não paginável é um recurso escasso e deve ser utilizado com parcimônia. Caso contrário, algumas operações vão deixar de ser realizadas pelo sistema por falta de RAM, mesmo que ainda haja muita memória paginável disponível.


User Space e System Space

Adotando um sistema de 32 bits como exemplo, um ponteiro é capaz de endereçar 4 GB de memória. Destes, 2 GB são reservados para endereçar páginas privadas para cada processo, ou seja, vão compor o espaço de endereçamento privado de cada aplicação. Esta faixa, que vai de 0x00000000 ao 0x7FFFFFFF, recebe o nome de User Space e são os endereços que aplicações acessam em User Mode. Conforme vimos, esta faixa compõe o espaço de endereçamento privado de um processo, protegido contra acessos indevidos de outros processos. Um exemplo seria: Somente as threads do Processo A terão acesso ao User Space do Processo A. O mesmo endereço aponta para diferentes páginas físicas de RAM em diferentes processos, dependendo do contexto do processo que faz o acesso. Os outros 2 GB restantes do endereçamento de 32 bits são reservados ao endereçamento de páginas de sistema. A faixa de endereços de 0x80000000 ao 0xFFFFFFFF define o então chamado System Space, que diferente do User Space, endereçam páginas de memória que não são privados a um determinado processo. Isso significa que o mesmo dado (na mesma página física) pode ser acessado através do mesmo endereço virtual em diferentes processos. Um dado em System Space pode ser acessado apenas em Kernel Mode, enquanto que um dado em User Space pode ser acessado tanto em Kernel Mode quanto em User Mode.


Cagamba! Isso foi uma metralhadora de conceitos? Se você apresentar sintomas de visão turva ou vômito seguido de diarréia e tremedeira, larga a mão de ser frouxo e se prepare para os próximos posts. Caso você não tenha apresentado nenhum destes sintomas, não se preocupe, para algumas pessoas o processo é mais demorado. Em posts futuros vou explicar como os drivers acessam os endereços virtuais oferecidos pelas aplicações, como lidam com a paginação de memória e ainda testam buffers oferecidos pelas aplicações. Um driver mal escrito pode permitir que um parâmetro inválido em uma aplicação cause uma tela azul.

Por hoje chega, mas vale lembrar que o que foi apresentado aqui é apenas a ponta do iceberg. Essa pontinha é o que acredito ser o mais relevante para o desevolvimento de drivers, mas ainda existem toneladas de detalhes referentes à memória virtual.

Have fun! 🙂