Memória também é um “filesystem”!

Fiquei de dizer para vocês porque blocos de 4 kB são preferíveis num sistema de arquivos como o ext2fs. O motivo é simples: O sistema de paginação dos processadores Intel usam tamanhos de página default de 4 kB!

Paginação é a divisão da memória física em blocos de tamanho conhecido e, através de uma interrupção de hardware interna à CPU, chamada de page fault, podemos alocar, gravar uma página em disco, ler outra página do disco e colocá-la sobre o mesmo bloco anterior etc. Ou seja, o mecanismo de swapping que permite usarmos mais memória do que realmente temos “fisicamente”.

Para que isso funcione temos que gerenciar “descritores” de páginas. Eles estão localizados em tabelas que também tem o tamanho de uma página (4 kB):

page_dirs

O diagrama acima mostra como um endereço linear, no modo i386, é traduzido para um endereço físico usando duas tabelas de páginas: A PDT (Page Directory Table) e a PT (Page Table). A primeira contém 1024 ponteiros (usando endereços físicos) para o início de PTs. As PTs, por sua vez, tem 1024 entradas que contém endereços físicos para o início de uma página de 4 kB, obviamente, na memória física.

Essa estrutura é, também obviamente, organizada em formato de árvore. Onde o nó raiz é a PDT, cujo endereço físico inicial é dado pelo registrador de controle CR3. As duas tabelas têm 1024 entradas porque cada uma delas tem exatamente 4 bytes (32 bits) de tamanho.

Tradução do endereço linear:

Quando seu programa usa um endereço para acessar dados na memória e o modo de paginação está ativado, o endereço não corresponde a uma posição na memória física. Este endereço tem que ser traduzido, via tabelas de páginas. Então, a literatura define dois espaços de endereçamento: O espaço linear e o espaço físico. O espaço físico é o grande array da memória, onde os bytes estão arranjados em sequência, um na frente do outro… No espaço linear isso também ocorre, mas só dentro de uma página (4 kB). A página em si pode estar localizada em qualquer lugar da memória!

Um endereço linear é montado do modo tradicional: O processador obtém um endereço base de um descritor de segmento de acordo com o índice contido num seletor (CS, DS, ES, SS, FS ou GS) e adiciona o ponteiro a ele. Por exemplo:

mov eax,[ebp+4]  ; Aqui o endereço EBP+4 é somado ao
                 ; endereço-base contido no descritor indicado
                 ; pelo seletor SS.

De posse desse valor o processador começa o processo de tradução: O endereço é encarado como contendo os índices para as tabelas PDT e PT. Esses índices apontam para entradas nessas tabelas, chamadas de PDEs e PTEs. Assim, o endereço físico base de uma página de 4 kB é obtido da PTE e somando ao campo offset do endereço linear (não é à toa que o offset tem 12 bits de tamanho. Ele pode variar de 0x000 até 0xfff, ou seja, de zero a 4095) e finalmente temos o endereço físico da DWORD que a instrução acima tentava acessar…

A tradução passa por uma série de verificações nos atributos contidos nas entradas das tabelas antes de ser completada… Dentre os atributos temos bits que informam se a página referenciada está presente na memória ou não, se a página referenciada foi acessada, se alguém já a “sujou”, escrevendo nela, dentre outros… Sempre que há alguma violação nas regras de acesso a página uma page fault acontece, dando oportunidade ao processador de realizar alguma ação para corrigir o problema.

A sequência de tradução começa com a avaliação de uma PDE… Por exemplo, se o bit “presente” estiver zerado e seu código tentar ler ou gravar na página, então a tradução pára e a page fault acontece. Essa não é a única condição de falta, mas é a principal… Passada a verificação da PDE, o mesmo ocorre para a PTE. Se a tradução for bem sucedida, então podemos acessar os dados na página. O processador manterá essa tradução num cache especial, chamado TLB, para que não precise repetir o processo para essa página toda vez que topar com um endereço dentro dela…

E quanto ao “sistema de arquivo”?

Note que não dá para manter todas as páginas na memória… No caso do modo i386, se formos usar esse esquema de paginação (o mais simples, existem outros!), teríamos que manter 1025 tabelas (uma PDT e 1024 PTs), disperdiçando pouco mais de 4 GB de memória (4 kB a mais que 4 GB), ou seja, mais que a quantidade total de memória endereçavel em 32 bits!!! Como a PDT é única, temos que mantê-la, mas podemos usar PDEs “inválidas” para regiões de memória que não estão fisicamente presentes e, quando uma falta ocorrer, verificarmos se esse comportamento está correto (ou fazer alguma coisa com isso)… Isso nos poupa da necessidade de manter 1024 PTs, diminuindo a pressão na memória na contenção das tabelas necessárias para a paginação funcionar.

É claro que isso requer gerenciamento de uma estrutura de árvore onde os nós têm 4 kB de tamanho. Temos que saber quantas tabelas temos, quantas páginas temos, quantas páginas livres temos, quantas estão em uso etc… Muito parecido com o que um sistema de arquivo faz com os blocos em disco…

Sem contar que também temos “grupos de blocos”… Um grupo para o kernel e um grupo para cada processo ativo. Este é o motivo pelo qual todo processo geralmente inicia no endereço 0x00400000 (repare: PDE 0x001 e PTE 0x000) . Obtenha o endereço do símbolo __executable_start, fornecido pelo linker, e você verá que todos os processos no userspace começam nesse enderço linear (a exceção à regra refere-se aos shared objects, que devem ter endereços diferentes para poderem ser acessíveis pelo processo). Como isso é possível? Bem… já que cada processo tem seu próprio grupo de páginas, ou seu próprio “diretório de páginas”, então o comportamento está explicado…

E, diferente do ext2fs, o sistema de arquivo da memória não lida com estruturas de bitmaps para determinar se um bloco está livre ou não. Isso já faz parte da estrutura das entradas de tabelas de páginas…

Alguns sistemas de arquivo de disco também não usam bitmaps para manter registro de ocupação. É o caso do btrfs. Mas, mesmo ele também costuma usar blocos de 4 kB para manter uma relação direta entre uma página, na memória, e um bloco.

Páginas, buffers, caches e o ext2fs:

Como há uma relação direta entre um bloco e uma página na memória, fica fácil para o kernel alocar um bloco de 4 kB e associá-lo aos acessos ao disco. Se você já usou o utilitário free:

$ free -h
             total       used       free     shared    buffers     cached
Mem:          7.7G       5.0G       2.7G       509M       230M       2.2G
-/+ buffers/cache:       2.6G       5.1G
Swap:         7.9G         0B       7.9G

Já reparou, na primeira linha, os campos “buffers” e “cached”? Eles estão diretamente relacionados com a alocação de páginas para conter blocos vindos de discos. A medida que o kernel vai precisando usar essas páginas, ele se livra delas, devolvendo-as para o pool de páginas livres em se a página estiver “suja”, escrevendo-a no dispositivo. É por isso que a quantidade de memória livre, no linux, deve ser obtida na segunda linha (a linha do “-/+ buffers/cache”)… A primeira linha nos dá o estado atual, mas esses buffers e caches podem ser despejados em disco tão logo quanto o sistema precisar de memória. Por isso que, num sistema com 2.7 GB de memória livre temos, na verdade, 5.1 GB livres! :)

O kernel também tenta manter a maior quantidade possível de blocos alocados em páginas na memória, atrasando ao máximo a escrita em disco. O utilitário sync serve para chamar a atenção do kernel para que este grave as páginas “sujas” porque o usuário quer fazer algo importante, como resetar o computador, por exemplo.

Esse comportamento geralmente leva a um grande consumo de memória nos buffers e caches, mas isso não tem lá grande consequência para o usuário. Como disse, o kernel “atrasa” a escrita, mas não indefinidamente…

E, sim… quem toma conta das leituras e escritas no disco e a correspondente alocação de páginas é o kernel! Mesmo porque, lidar com registradores de controle como CR3 e com a estrutura de páginas é tarefa restrita de processo que executa no ring 0… Outro bit de atributo das entradas em tabelas é o U/S (User or Supervisor). Apenas supervisores podem fazer mudanças e “supervisor” significa CPL (Current Privilege Level) igual a zero, 1 ou 2… O privilégio 3 é associado com o userspace, claramente por ser o menos privilegiado.

É raro ver um sistema operacional usando os privilégios 1 e 2. Na prática, apenas o zero e o 3 são usados.

Fazendo uma vaquinha:

Lá no meu livro falo alguma coisa sobre um comportamento chamado de COW (Copy On Write). Quando você forka (verbo esquisito!) um processo no userspace, suas páginas de dados são marcadas como read only. O processo filho recebe uma cópia da estrutura de páginas do processo pai, também com o bit R/W (Read/Write) zerado… Obviamente que, quando um dos processos tentar escrever alguma coisa numa página, uma falta ocorrerá e o tratador da page fault tomará a decisão de criar uma cópia separada da página para o primeiro processo que tentou a escrita e remarcará ambas as páginas como “escrevíveis”.

É assim que o COW funciona! E é por isso que o processo filho “herda” todo o espaço de dados do processo pai! Nenhuma página é, de fato copiada! Somente a estrutura de dados… A cópia só acontece na tentativa de escrita.

Resumão do resumão

O sistema de arquivos ext2fs e seus decendentes, bem como um sistema de arquivos virtual chamado vfs, que é uma camada de abstração acima do ext2fs, têm blocos mapeados diretamente a uma (ou mais) páginas de 4 kB do sistema de gerenciamento de memória. O gerenciamento de memória, por sua vez, lida com estruturas de árvores onde nós (uma entrada numa PDT ou PT) são criados e apagados o tempo todo… O kernel toma conta de conflitos e pode gravar uma página na memória física em disco para dar espaço para uma página dum processo no userspace ou recuperar uma para o kernelspace, realizando o procedimento de swapping

Isso é importante: swapping é um recurso do kernel suportado pelo esquema de paginação. Uma falta não necessariamente ocorre para efetuar swapping. Este é um dos detalhes do subsistema…

Desculpem-me se essa descrição ficou meio “genérica”, mas é importante o entendimento conceitual sobre paginação e seu relacionamento com arquivos antes de pensarmos em explorar o recurso…

Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s