Mapa da memória de um PC

Como é que a memória do seu PC é usada? Onde está a RAM? Onde está a ROM? Eu já falei por aqui que a memória RAM não é contínua, existem “buracos” no grande array de bytes que denominamos “memória”. É importante, se você quer desenvolver um sistema operacional, saber como a memória está organizada!

Para falar em organização de memória podemos usar aproximações diferentes. A primeira é encará-la com base na especificação de um endereço efetivo no modo real, ou seja, segmento:offset. Mas isso só nos permite acesso ao primeiro megabyte de memória, talvez um pouco mais, já que 0xffff:0xffff, se o sinal gate A20 estiver habilitado, especifica o endereço efetivo 0x10ffef, que tem 21 bits de tamanho. Ou seja, 65520 bytes além do limite superior do primeiro megabyte… A outra aproximação é encarar um endereço efetivo como sendo um índice para o grande array da memória, ou seja, usando um endereço efetivo de fato… No caso do modo i386, o endereço efetivo tem 32 bits de tamanho. No caso do x86-64, 48 bits (sim! 48!).

A terceira maneira, que só será útil mais tarde, é encararmos a memória como contendo blocos de 4 kB que não se sobrepõem… Essa é a maneira simples de lidar com memória no modo de paginação…

Dois “tipos” de espaços de memória:

É costume dividir a memória dos PCs em dois tipos: A memória baixa (o primeiro megabyte), que é acessível ao modo real de 16 bits, e a memória “alta”, acima de 1 MB em diante. A memória baixa é endereçável usando-se apenas os 20 primeiros bits de um endereço (por causa do esquema segmento:offset), qualquer endereço além desses corresponde à memória alta.

Em sistemas de 32 ou 64 bits pode haver uma divisão na “memória alta”. Os primeiros 4 GB de memória, no modo protegido, são chamados de “memória baixa”, o que excede isso é a memória “alta”. Essa diferença existe especialmente por causa do modo x86-64 e porque, nessa “memória baixa” temos “buracos” correspondentes ao mapeamento de I/O em memória (Local e I/O APIC e outros controladores e chipsets)… É também nessa memória “baixa” que fica a ROM de seu computador…

É importante notar que, no modo real, a ROM localiza-se nos segmentos mais altos, dentro do primeiro megabyte de memória… Mas, isso é uma “ilusão”… O que temos aqui é uma cópia lógica (via decodificação de endereços nos circuitos do seu computador) ou uma cópia em RAM (chamada Shadow ROM) da ROM que, de fato, localiza-se na “memória baixa” do modo protegido (no final do bloco de 4 GB). Algumas BIOS usam o artifício de guardar versões atualizadas da ROM em memórias Flash, internas, e as copiam para as respectivas regiões durante o power up do sistema…

É bom lembrar que mesmo no modo i386, graças à paginação e a extensão PAE, podemos endereçar até 64 GB de RAM física! E no modo x86-64, até 256 TB!

Então, quando falarmos de “memória baixa” e “memória alta”, temos que especificar o contexto (modo real? modo protegido? O PAE está ativado? Estamos no modo real, i386 ou x86-64?).

O primeiro 1 MB:

Quando você liga ou reseta o computador, o processador “acorda para a vida” no modo real, de 16 bits… Não importa se você tenha um Core i7, 128 GB de RAM e o escambau… O seu processador começa enxergando, no máximo, 1 MB de RAM e trabalhando em 16 bits!

O primeiro 1 MB de RAM é dividido assim:

0x00000 ~ 0x003ff - Tabela de vetores de interrupções
0x00400 ~ 0x004ff - Área de dados da BIOS (BDA - Bios Data Area)
0x00500 ~ 0x005ff - Área de dados "extra" da BIOS
0x00600 ~ 0x9ffff - RAM livre
0xA0000 ~ 0xbffff - Memória de Vídeo
0xc0000 ~ 0xdffff - Áreas reservadas para ROMs e I/O de placas ISA.
0xe0000 ~ 0xfffff - ROM

Se você fizer as contas, a área da “RAM livre” tem 653824 bytes, ou seja 638,5 kB (não chega aos famosos 640 kB que Bill Gates disse que jamais precisaria extrapolar!).

Ainda, a “Memória de Vídeo” aqui refere-se ao modo real apenas. No modo protegido este bloco de 128 kB continua acessível, mas para resoluções maiores não é suficiente. O driver de sua placa de vídeo reconfigura o circuito de vídeo para que este enxergue outra área que conteria RAM como frame buffer. A especificação VESA, por exemplo, informa ao requisitante qual é o endereço do frame buffer e o divide em “bancos”. Neste modo a tela é dividida em pedaços e você precisará mudar de “banco” para plotar pixels no lugar desejado.

A manipulação de vídeo, diretamente, é muito complicada e, hoje em dia, depende muito do fabricante. A nVidia, por exemplo, não divulga documentação alguma sobre suas GPUs, felizmente a ATI e a Intel o fazem…

Depois de 1 MB

Acima do primeiro megabyte temos o restante da memória RAM instalada na sua placa-mãe. Mas, espere! Tem um monte de buracos ai… Alguns PCs colocam um buraco de 1 MB acima dos 15 MB (não me perguntem porque! tenho lá minhas suspeitas!) e, acima disso, garantidamente, temos um buracão entre o endereço efetivo 0xfec00000 e 0xffffffff, reservados para APICs, I/O de outros dispositivos e ROMs. Acessar esses endereços é acessar registradores de dispositivos, chipsets ou ROMs…

Que droga é essa de mapeamento de I/O?… Bem, os dispositivos no seu PC geralmente são acessíveis via um espaço de endereçamento diferente da memória chamado portas de I/O. Instruções como IN e OUT usam esse espaço de endereçamento alternativo para lidar com HDs, diskettes, portas seriais, paralelas etc. Assim, os registradores desses dispositivos não estão na “memória”, mas num lugar diferente, isolado.

Porém, os controladores modernos usam espaços da memória como se fossem portas de I/O. Essas portas são ditas “mapeadas na memória”, mas não fazem parte da RAM…

Um mapa preliminar da memória acima de 1 MB pode ser mais ou menos assim:

    0x00100000 ~     0x00efffff - RAM (14 MB)
    0x00f00000 ~     0x00ffffff - Possível buraco entre 15 MB e 16 MB
    0x01000000 ~     0xfebfffff - RAM* (4096 MB)
    0xfec00000 ~     0xfec00fff - I/O APIC (1 página)
    0xfec01000 ~     0xfedfffff - Outros mapeamentos de I/O (511 páginas)
    0xfee00000 ~     0xfee00fff - Local APIC (1 página)
    0xff000000 ~     0xfffdffff - Mais mapeamentos de I/O (4064 páginas)
    0xfff80000 ~     0xfffdffff - Área extendida da BIOS** (PIIX4 - 80 páginas) 
    0xfffe0000 ~     0xffffffff - ROM (1 MB).
0x000100000000 ~ 0xffffffffffff - RAM (262140 GB max).

A terceira linha, marcada como RAM*, contém outros “buracos”. Por exemplo, ICH9 usa:

0xfec10000 ~ 0xfec38fff - PCI Express Ports 0-6
0xfed0?000 ~ 0xfed0?fff - HPET.

Além de reservar 128 kB (32 páginas) para a LAN onboard, 2 kB (1 página, 4 kB) para USB EHCI etc…

A “Área extendida da BIOS”, marcada com **, pode estar presente ou não, dependendo do chipset. Ela pode estar habilitada no PIIX4, por exemplo.

Repare que todos os blocos começam em endereços com os primeiros 12 bits zerados e terminam em endereços com esses bits setados. Isso significa que podemos mapear páginas diretamente sobre essas regiões. Isso é importante porque temos que desabilitar o uso do cache da CPU para essas regiões.

Ahhh… na região dos “Outros mapeamentos” podemos ter páginas de RAM válida também…

DMA sempre ocorre abaixo dos 16 MB!

DMA é um recurso onde um dispositivo pede para tomar o controle do barramento de endereços e dados pra realizar acesso direto à memória (Direct Memory Access). Isso é feito pelo chip controlador de DMA (DMAC – DMA Controller), onde exceto pelos casos onde um controlador mais especializado for usado, ele usa endereços de 24 bits para ler/gravar dados na memória física diretamente, com um porém: Os 8 bits superiores são fixos.

O DMAC tradicional transfere blocos de 64 kB, no máximo, e os 8 bits superiores do endereço (chamados de “página” pelo controlador) não são incrementados/decrementados… Assim, se o DMAC for programado para acessar memória a partir de 0x100000, ele conseguirá ler/escrever até 0x10ffff. Ao chegar nesse limite ele volta ao endereço 0x100000.

Assim, é prudente reservar blocos de 64 kB na memória abaixo dos 16 MB para buffers de DMA…

Mas, atenção… chipsets como o ICH9 e PIIX possuem uma versão estendida do controlador de DMA que permite uso de endereçamento em 32 bits.

Espaços reservados para o chipset

Anteriormente, mostrei como usar esse serviço da BIOS para obter as regiões “usáveis” de RAM do seu PC. Mas, ela só nos lista o que é RAM usável e o que não é. Não temos como saber se as regiões reservadas são mapeadas para I/O ou ROMs. Outros testes devem ser feitos para determinar isto! Esses testes devem levar em conta arquiteturas de chips de suporte conhecidas. Por exemplo, a maioria dos PCs modernos usa controladores para I/O com padrões conhecidos como o PIIX3 (PCI-to-ISA bridge and IDE Xcelerator)  e ICH9 (Intel I/O Controller Hub). Esses hubs fornecem suporte a SATA, PCIe, Audio, IDE, USB, I/O APIC, HPERF timer, Ethernet onboard, entre outras coisas.

Aliás, se você acha que a interface ISA morreu com o 286 não poderia estar mais enganado… Esses chipsets fornecem uma ponte entre o barramento ISA e o PCIe, por exemplo… Tudo o que você fazia com ISA no final dos anos 80 e início dos 90, continua sendo válido hoje porque ISA ainda está lá, só que escondidinha…

Para saber mais sobre esses controladores, baixe as espcificações do PIIX 3 aqui e a do ICH9 aqui. Se quiser uma versão do PIIX mais recente (a versão 4), baixe aqui. Todos são chipsets da Intel e, portanto, espere por compatibilidade… Ainda, cito o ICH9 e não um de seus antecessores (ou decendentes) porque ele é o mais comum de se achar por ai, hoje em dia, nas placas-mãe… Em máquinas virtuais o padrão PIIX 3 é o mais comum. Outra coisa: O ICH9 também pode ser chamado de 9 series Platform Controller Hub (PCH).

Paginação no modo x86-64

Tá lá no meu livro, mas gosto sempre de mostrar/aprender alguma coisa sob várias perspectivas…

No último post falei um cadinho sobre paginação no modo i386 e só falei da paginação tradicional. Mesmo neste modo existem extensões que podem aumentar o tamanho de uma página (PAE) ou aumentarem a quantidade de memória endereçável (PSE). A distinção é sutil: No modo i386 o endereço linear tem sempre 32 bits de tamanho, mas na extensão PSE uma página pode ser localizada no espaço físico usando um endereço físico de até 36 bits (4 bits adicionais). Daí, ao invés de acessar apenas 4 GB de memória, podemos ter acesso a 64 GB. A diferença entre a extensão PSE e PAE é que a última permite que uma página tenha mais que 4 kB de tamanho.

Ambas as extensões podem ser usadas em conjunto… PSE funciona eliminando a tabela seguinte, aumentando a quantidade de bits no offset do endereço linear. No caso do modo i386, é possível elimiar a tabela PT e deixamos apenas a PDT:

page_pae

Para isso funcionar o registrador de controle CR4 precisa ter o bit PSE (Page Size Enabled ou Extension) habilitado e a entrada PDE precisa ter o bit PS setado. Isso permite que possamos misturar páginas de 4 kB e paginas de 4 MB numa mesma estrutura de árvore, o que dá uma confusão dos diabos porque podemos ter páginas sobrepostas. É por isso que essa extensão é evitada… Mas, às vezes, é útil: No caso de um kernel, por exemplo, podemos estipula que uma página de 4 MB será dedicada apenas para o código (ou dados) do kernel e manter uma única entrada na PDE, fixa…

A extensão PAE é mais complicada… No modo i386, ao invés de termos apenas 2 tabelas (PDT e PT) temos uma terceira, a PDPT (Page Directory Pointer Table) que tem apenas 4 entradas. As outras duas passam a ter 512 entradas e todas as tabelas passam a ter endereços físicos base de 36 bits. Note que o endereço linear continua tendo 32, mas, agora, a página pode estar localizada em qualquer lugar no espaço físico de 64 GB.

Da mesma forma que o PSE, o PAE precisa ser habilitado no registrador CR4, mas não tem bit de habilitação nas entradas de página… E, é claro, que já que temos apenas 512 entradas, cada entrada das tabelas agora tem 64 bits de tamanho, e os campos do endereço linear passam a ter 9 bits (exceto PDPT, que tem 2). Continuamos com 12 bits de offset para páginas de 4 kB… Se habilitarmos o PSE apenas a PT pode ser descartada e passaremos a ter páginas de 2 MB (9 bits da PT que serão agregado aos 12 do offset).

No modo x86-64 PAE é obrigatório porque todas as entradas das tabelas têm que ter 64 bits de tamanho, mas uma tabela é adicionada na hierarquia. Ela é chamada de PML4 (Page Map Level 4) e todas as 4 tabelas têm 512 entradas e, portanto, 9 bits no endereço linear:

p4ml

Embora o PAE seja obrigatório, o PSE não é, mas ainda pode ser usado e é um cadinho estendido: Podemos eliminar a PT e também a PDT, se quisermos, com o bit PS nas entradas de PDT e PDPT, respectivamente. Com isso podemos ter páginas de 4 kB, 2 MB ou 1 GB. De novo, é claro, se o registrador de controle CR4 permitir!

Só para constar: Nunca vi ninguém usando páginas de 1 GB!

E é interessante notar: O endereço linear tem 48 bits de tamanho, não 64! Os 16 bits superiores têm que ser uma cópia do bit 47 sempre, se você não quer tomar um segmentation fault ou um general protection fault na cara! O que não é, ainda, muito claro para mim é se o processador encada esse valor como um “endereço negativo” em complemento 2. Mas isso é de pouca importância prática, já que não deve ser de grande preocupação. Com 48 bits de endereço linear podemos endereçar até 256 TB de memória… Mesmos nos computadores mais novinhos essa é uma quantidade astronômica de memória (até mesmo com o mecanismo de swapping).

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…

Como funciona o ext2fs?

No, agora exinto, projeto Heisenberg OS, tinha deixado para decidir qual file system usar e, ao mesmo tempo, iniciado os estudos a respeito do ext2fs, usado no Linux e outros unixes por ai. Um filesystem é uma estrutura de dados que permite usarmos a abstração de “arquivos” para armazenamento de blocos de dados em dispositivos como HDs, disquetes, pen-drives etc. Ao invés de pedirmos ao sistema “leia 16 setores a partir da trilha 612, cabeça 2, setor 10”, podemos simplesmente pedir “leia 8 kB do arquivo ‘xpto.txt’, do diretório /var/tmp/”.

Como isso funciona? É típico que os dispositivos de armazenamento de blocos (discos) suportem um tamanho de bloco fixo de 512 bytes — que equivale a um único setor — mas, o ext2fs e seus descendentes usam tamanhos de blocos múltiplos de 1 kB (1, 2, 4 ou 8 kB, para ser mais exato). Um tamanho de bloco típico é 4 kB (existe um motivo para isso, explico depois). Assim, este é o tamanho mínimo de dados que podem ser lidos/gravados em disco.

blocks

Além dos blocos, ext2fs usa grupos de blocos… Com blocos de 4 kB temos grupos de 8192 blocos estruturados dessa maneira:

group_blocks4

Um superbloco descreve todo o filesystem. Ele não descreve o grupo de blocos, mas todo o seu disco. Além disso, um superbloco tem sempre o tamanho exato de 1 kB, mesmo que o bloco tenha 4 kB (neste caso os 3 kB restantes ficam inúteis).

O bloco dos descritores de grupo descrevem o grupo corrente e tem uma estrutura bem simples:

struct ext2_group_desc
{
  /* bloco do "bitmap de blocos". */
  unsigned int   bg_block_bitmap;

  /* bloco do "bitmap de i-ndes". */
  unsigned int   bg_inode_bitmap;

  /* bloco inicial da tabela de inodes. */
  unsigned int   bg_inode_table;

  /* Quantidade de blocos livres. */
  unsigned short bg_free_blocks_count; 

  /* Quantidade de inodes livres. */
  unsigned short bg_free_inodes_count;
 
  /* Quantidade de diretórios. */
  unsigned short bg_used_dirs_count;   

  unsigned short bg_reserved0;
  unsigned int   bg_reserved1;
};

Os dois blocos de bitmap, descrevem quais blocos estão ocupados ou livres (bitmap de blocos) e quais entradas da tabela de inodes estão ocupadas ou livres (bitmap de i-nodes). Seguem os n blocos de inodes e, por fim, os blocos que contém dados dos arquivos. Esses dois blocos são conjuntos de bits, onde cada bit indica um bloco ou item da tabela de inodes.

Um inode é um descritor de um arquivo… Ele diz qual é o tipo do arquivo, o tamanho, permissões, etc. Essa estrutura não contém o nome do arquivo, que estará localizado num arquivo especial de diretório… O número do inode é a identificação real de um arquivo, não o seu nome! Na figura acima temos os blocos de inodes e de dados contendo número variável de blocos. Todos as outras estruturas ocupam exatamente 1 bloco…

Determinando o tamanho dos blocos de inodes e dados:

Lembre-se: Cada bloco tem 4 kB de tamanho… Isso significa que no bitmap de blocos temos 4096 vezes 8 bits e cada bit corresponde a um bloco em uso ou livre no grupo. Assim, temos 32768 bits e, consequentemente, 32768 blocos no grupo.

Eis duas rotinas de exemplo de como determinar o primeiro bloco livre e determinar quantos blocos adjacentes estão livres:

#include <stdio.h>
#include <string.h>
#include <limits.h>

/* Tamanho de 1 bloco. */
#define BLOCK_MAP_SIZE  4096

/* Vou usar um "unsigned long" para acelerar as rotinas. */
#define BITMASK_SIZE    (sizeof(unsigned long)*8)

/* Faça de conta que um bitmap de blocos esteja carregado 
   aqui. */
extern unsigned char block_map[];

/* A partir de um bloco livre, conta quantos blocos 
   contiguos também estão livres. */
int count_contiguos_free_blocks(int blocknum)
{
  unsigned long *bptr;

  /* Usa bptr_end como "batente". */
  unsigned long *bptr_end = 
    (unsigned long *)(block_map + BLOCK_MAP_SIZE);

  int local_block_num;
  int count = 0;

  if (blocknum < BLOCK_MAP_SIZE)
  {
    bptr = (unsigned long *)block_map + 
             (blocknum / BITMASK_SIZE);

    local_block_num = blocknum % BITMASK_SIZE;

    while (bptr < bptr_end)
    {
      if (*bptr & (1UL << local_block_num)) 
        break; 

      count++; 

      if (++local_block_num >= BITMASK_SIZE)
      {
        local_block_num = 0;
        bptr++;
      }
    }
  }

  return count;
}

/* Acha o primeiro bloco livre no bitmap */
int get_first_free_block_index(void)
{
  unsigned long *bptr = (unsigned long *)block_map;

  /* Usa bptr_end como "batente". */
  unsigned long *bptr_end =
    (unsigned long *)(block_map + BLOCK_MAP_SIZE);

  int free_block = -1;
  int block_bit = 0;
  int bit_count = 0;

  while (bptr < bptr_end)
  {
    if ((*bptr & (1UL << bit_count)) == 0) 
      return block_bit; 

    block_bit++; 

    if (++bit_count >= BITMASK_SIZE)
    {
      bptr++;
      bit_count = 0;
    } 
  }

  return free_block;
}

A figura anterior nos diz que o bloco contendo o superbloco pode não existir “dependendo do grupo”. É que, ao invés de manter um único superbloco em todo o filesystem ou cópias em todos os grupos, para poupar espaço, a revisão 1 do ext2fs resolveu colocar cópias do superbloco apenas nos grupos 0, 1, 3n, 5n e 7n, onde n é um inteiro e n \geqslant 1. Isso permite manter cópias do superbloco para que, em caso de catástrofe, ele possa ser recuperado a partir de uma das (poucas) cópias espalhadas pelo disco.

Existe um detalhe importante para quem quiser implementar ext2fs por sua própria conta: O primeiro superbloco (no grupo 0) está sempre localizado na posição 1024 do bloco 0, do grupo 0 — o primeiro quilobyte é reservado para um bootloader (se existir um)! Nas demais cópias, o superbloco localiza-se no início do do bloco 0 do grupo (embora eu não tenha certeza absoluta desse fato!).

Para ilustrar a distribuição das cópias do superbloco, eis um mapa dos primeiros 1024 grupos de um disco de 500 GB. Os caracteres ‘S’ marcam o grupo que contém uma cópia do superbloco, os caracteres ‘.’ indicam grupos que não as possui:

Block group map (começa no grupo 0):
===================================
SS.S.S.S.S...............S.S.....................S..............
.................S...........................................S..
................................................................
...................................................S............
................................................................
.......................S........................................
................................................................
................................................................
................................................................
.................................................S..............
................................................................
.........................S......................................
................................................................
................................................................
................................................................
................................................................

Note que os blocos 0, 1, 3, 5, 7 ,9, 25, 27, 49, 81 etc contém as cópias… Com os superblocos dispersos desse jeito, sobra mais espaço para dados e temos um grande número de backups, se necessário!

Mas, o que dermina mesmo o tamanho do grupo é o bloco de bitmap de blocos. Neste bloco, cada bit, dos 32768 (4096 \cdot 8) indica a ocupação de cada um dos blocos do grupo. Neste bitmap teremos 1027 ou 1028 blocos “reservados” para:

  • Superbloco: 1 bloco (se existir a cópia);
  • Bloco descritor do grupo: 1 bloco;
  • Bitmap de blocos do grupo: 1 bloco;
  • Bitmap de inodes do grupo: 1 blocos;
  • inodes: 1024 blocos.

Porque 1024 blocos para os inodes? Pelo mesmo motivo que temos 8192 blocos num grupo. O Bitmap de inodes nos permite mander o estado de 32768 inodes e cada inode tem 128 bytes de tamanho. Portanto, 1 bloco contém 32 inodes. Logo, precisamos de 1024 blocos para conter todos os inodes. 7164 (se houver um superbloco) ou 7165 blocos para uso dos dados.

inodes:

Os inodes, dentro de um bloco são numerados de 0 até 8191, mas no contexto do filesystem eles são numerados de 0 até numero\_de\_grupos \cdot 4096. Isso permite que um inode num grupo faça referência a um inode em outro grupo e isso é útil para casos onde não há alternativa senão fragmentar um arquivo entre blocos…

A estrutura básica de um inode é:

struct ext2_inode {
  unsigned short i_mode;   /* "modo" como em 'chmod'. */
  unsigned short i_uid;    /* UserID do arquivo. */
  unsigned int   i_size;   /* Tamanho, em bytes. */
  unsigned int   i_atime;  /* timestamp do último acesso. */
  unsigned int   i_ctime;  /* timestamp da criação. */
  unsigned int   i_mtime;  /* timestamp da modificação. */
  unsigned int   i_dtime;  /* timestamp da "deleção". */
  unsigned short i_gid;    /* GroupID do arquivo. */
  unsigned short i_links_count;  /* Quantidade de HARD-links. */
  unsigned int   i_blocks; /* Quantidade de blocos. */
  unsigned int   i_flags;  /* Diversos flags. */
  unsigned int   i_reserved1;
  unsigned int   i_block[15];  /* "Ponteiros" para os blocos. */

  unsigned int   i_generation; /* Versão do arquivo (para NFS) */

  /* Informações sobre ACLs (selinux?). */
  unsigned int   i_file_acl;
  unsigned int   i_dir_acl;

  /* Informações sobre "fragmentos". */
  unsigned int   i_faddr;
  struct {
      unsigned char  l_i_frag;
      unsigned char  l_i_fsize;
      unsigned short i_pad1;
      unsigned short l_i_uid_high; /* Parte superio do UserID. */
      unsigned short l_i_gid_high; /* Parte superior do GroupID. */
      unsigned int   l_i_reserved2;
  } linux;
};

Existem muitos detalhes escondidos nessa estrutura. O que nos interessa são os campos i_blocks, que nos dá a quantidade de blocos usados pelo arquivo; e o array i_block[], que nos diz os blocos usados pelo arquivo… Isso pode parecer estranho, já que temos apenas 15 entradas… As 12 primeiras contém o número exato de um bloco de dados; a 13ª entrada contém o número de um bloco que contém um array com números de blocos de dados; a 14ª entrada faz a mesma coisa, mas é uma indireção dupla (contém um número do bloco que contém um array onde cada item é um nº de bloco que aponta para outro array que contém os números de blocos de dados)… A 15ª entrada faz o mesmo, mas com indireção tripla!!! A figura abaixo ilustra.
ext2_inode
Com esse esquema dá para alocar um espaço absurdamente grande para um único arquivo e usar uma única entrada de inode de 128 bytes!!! Se cada bloco tem 4 kB e cada número de bloco tem 32 bits de tamanho, então cada bloco comporta 1024 números de blocos de dados… Na indireção simples 1024 blocos podem ser indicados. Na indireção dupla, 1048576 (1024 tabelas com 1024 entradas). Na tripla: 1073741824. Somando isso tudo aos 12 blocos que já constam no inode temos a possibilidade de usar 1074791436 blocos de 4 kB para um único arquivo, ou seja, cerca de 4 TB!

Repare que, nos inodes, o que vai no array i_block é o número de um bloco (a mesma coisa nas tabelas de blocos, se usadas as indireções), portanto, o conteúdo de um arquivo pode cruzar a fronteira de um único grupo de blocos.

Diretórios:

Um diretório é um arquivo que contém uma lista da seguinte estrutura:

struct ext_dir_entry_2
{
  unsigned int   inode;     /* inode do "aquivo". */
  unsigned short rec_len;   /* Tamanho dessa entrada. */
  unsigned char  name_len;  /* Tamanho do nome do arquivo. */
  unsigned char  file_type; /* "tipo" do arquivo. */
  char           name[];    /* Nome, de até 255 caracteres. */
}

Onde o “tipo” é uma constante que diz se o “arquivo” é: desconhecido, regular, diretório, dispositivo do tipo caracter, dispositivo do tipo block, fifo, socket ou link simbólico. Repare que a única relação entre o nome e os dados do arquivo estão no primeiro campo: O número do inode associado a ele.

Múltiplas aplicações num mesmo servidor… Má idéia!

Essa é uma discussão antiga e os adeptos de ambientes como o Microsoft Internet Information Services não gostam muito dos argumentos contra a hospedagem de diversas aplicações num servidor único… Eu quero apresentar aqui os motivos técnicos do porquê essa prática é uma péssima idéia e, de forma ideal e prática, do porquê aplicações diferentes devem ser implementadas em servidores diferentes. Também quero mostrar uma técnica, empírica, de calcular a quantidade de processadores necessários para que um servidor garanta uma média de carga máxima de 70% com uma pequena latência entre threads, para o dimensionamento máximo de usuários simultâneos…

Web servers, no Linux e no Windows

Ambos ApacheIIS, para citar os mais badalados, trabalham essencialmente da mema forma: Existe um processo pai que cria um socket que “ouve” os pedidos de conexão. Quando uma conexão é recebida, pelo processo pai, um socket de conexão é criado (de acordo com os critérios da configuração do serviço) e um processo (ou thread) filho é criado. Esse processo filho, menos privilegiado do que o pai, será o responsável pelo uso do socket onde o cliente está conectado…

A diferença entre Linux e Windows, neste caso, é que “processos” custam muito caro para o Windows. A Microsoft dá preferência aos “processos leves”, chamadas de threads. No caso do Linux, processos são mais “baratos”. Tudo o que o Linux faz ao criar um novo processo é derivar um que já exista, criando uma cópia parcial do processo original… Threads e processos, no Linux, são coisas mais ou menos equivalentes, exceto pelo fato de que um processo pode exigir uma pilha muito grande (8 MB do Debian, 10 MB no RedHat, por exemplo). Esse dado pode ser lido neste artigo, inclusive alguma coisa sobre dimensionamento do Apache web server.

Threads e processsos

Um processo contém uma thread principal e pode conter diversas threads secundárias. Uma thread é uma “unidade de execução” que, fora o espaço de endereçamento, tem contexto próprio (pilha, armazenamento local e registradores).

Os processadores modernos contém vários “núcleos” de execução. Cada “núcleo” (core) é capaz de executar instruções separado do outro núcleo e em paralelo. É como se cada “núcleo” fosse um “processador lógico” dentro do seu processador físico… A coisa não é bem assim: Cada núcleo contém dois processadores lógicos que executam instruções em paralelo, mas com time slicing. Ou seja, cada processador lógico é colocado em espera, de tempos em tempos, para que o outro (dentro do mesmo núcleo) possa executar alguma coisa… Essa mágica a Intel chama de HyperThreading.

Num processador Core i7, por exemplo, podemos ter 8 processadores lógicos. Neste caso, temos apenas 4 “cores”. Assim, se tivermos 8 threads em execução, é garantido que apenas 4 delas estarão com paralelismo simétrico (outro termo para “realmente em paralelo”).

Por que esse tópico sobre threads, paralelismo e time slicing? É que saber como a “multitarefa” funciona no seu processador é fundamental para configuração de serviços como Apache e IIS… Se meu servidor só tem 8 “cores” e, se desconsiderarmos o time slicing causado pelo agrupamento de processadores lógicos, isso significa que se eu tiver mais que 8 threads em execução, as threads dividirão o tempo de processamento com todas as demais… Acontece que esse fatiamento de tempo é feito por um processo chamado task switching.

Para “chavear tarefas” o processador (e o sistema operacional) mantém registros que contém todo o “contexto” das tarefas. “Contexto”, aqui, significa “o estado dos registradores usados na thread”, todos eles, incluindo registradores que controlam o mapeamento da memória virtual do processo onde a thread se encontra… O “chaveamento” é feito através de uma interrupção (que, em média, ocorre a cada 20 milissegundos), onde o processador pára a thread atual, salta para o kernel, numa rotina chamada task scheduler, guarda o contexto da thread que estava sendo executada, decide se deve ou não “pular” para outra thread (baseado em informações como “prioridade”, por exemplo) e, se for o caso, carrega o contexto da outra thread e salta para ela.

O chaveamento de threads é bem traumático, do ponto de vista de performance. Especialmente porque envolve ajustes ainda mais drásticos para o processador… Sempre que uma thread é chaveada, o processador lógico precisa ser informado das configurações do espaço de endereçamento virtual do processo para onde ele vai saltar. Isso envolve a carga de registrador de controle das tabelas de páginas (memória virtual) que desfaz caches, gastando milhares de ciclos de clock no processo…

Quanto mais threads, mais lerdo…

Se um sistema está configurado para executar uma quantidade de threads maior que a quantidade de processadores lógicos então task switching é inevitável. Mas podemos limitar essa quantidade em valores mais práticos! Um cálculo superficial pode mostrar que se tivermos 200 threads em um único processador lógico e todas elas estejam ativas (fora de estado de espera), e sabendo que cada task switching ocorre em 20 milissegundos, podemos afirmar que cada uma das threads recebe uma fatia de tempo de menos que 20 ms de 4 em 4 segundos.

O cálculo acima leva em consideração que todas essas 200 threads serão executadas em round robin, ou seja, de forma sequencial. Se cada chaveamento ocorre em 20 ms, 200 chaveamentos ocorrerão em 4 segundos. Ou seja, para a primeira thread voltar a ser executada passarão 4 segundos!

É claro que, na prática, isso não acontece bem assim… Leva-se em conta que a maioria das threads estarão em estado de espera (idle) e que teremos, de fato, poucas threads concorrentes num mesmo processador lógico. Mais adiante você obtervará aquilo que chamei de coeficiente de carga efetiva, onde considero esse fator de “uso” das threads.

A carga média total do processador pode ser medida via um parâmetro chamado load average (média de carga). Ele indica, entre outras coisas, a quantidade de task switchings nos processadores lógicos, por intervalo de tempo. Como mostrei, neste artigo, o ideal é que seu servidor seja dimensionado para manter uma média de carga, máxima, de 70% do número de processadores lógicos.

Cálculo empírico para dimensionamento de uma única aplicação

Considerando que a média de carga deve ser mantida, de preferência, em 70% do número de processadores, que o coeficiente de carga efetiva de todas as threads não é de 100%, que operações com sockets são lentas e, por isso, podeos ter algum coeficiente de task switching… A seguinte fórmula pode ser uma ajuda para dimensionamento estimado:

N_p = \left \lceil 2 \cdot K_{la} \cdot K_{el} \cdot K_{ts} \cdot U_{max} \right \rceil

Onde N_p é o número de processadores lógicos que desejamos obter. K_{la} é um coeficiente que considera a média de carga. K_{el} é um “coefiente efetivo de carga” que considera a carga efetiva do conjunto total de threds (ou seja, quantas threads estarão efetivamente exigindo o esforço do processador em qualquer determinado momento). K_{ts} é o coefiente que considera o máximo de tempo entre task switchings de uma thread em particular. E, por fim, U_{max} é o número máximo de threads que você espera que sejam disparadas (aproximadamente o número máximo de usuários simultâneos).

As constantes acima podem ser definidas como: K_{la}=\frac{1}{0.7}, 0 < K_{el} \leqslant 1, K_{ts} = \frac{0.02}{t_{ts}}, Onde a constante 0.7 no recíproco que define K_{la} é a média de carga máxima desejada (empírica). Empírito também é K_{el}, para uma única thread esse coeficiente será sempre maior que zero e quase nunca igual a 1. E o tempo t_{ts} é o intervalo entre chaveamentos de tarefas, em segundos. Esse valor deverá ser sempre maior que 20 ms (o numerador da divisão).

A constante 2 está lá na fórmula porque cada núcleo tem 2 processadores lógicos não simétricos. O cálculo tenta paralelizar ao máximo as threads! Ainda, para máquinas virtuais (VMWare e outros virtualizadores) esse valor cria um balanceamento para o fato de que uma máquina virtual não tem a mesma performance do que uma máquina real com as mesmas características e, se você pensar bem, mesmo um VM Host, hoje em dia, funciona com múltiplos processadores Intel!

Outra consideração é quanto ao valor constante de Kts, 0.02. Ele é uma média do tempo de latência do chaveamento de tarefas tanto do Linux, quanto do Windows. No Linux, você pode obter esse tempo em nanossegundos com:

$ cat /proc/sys/kernel/sched_latency_ns 
18000000

Esse tempo de latência pode ser configurado (no Linux apenas!) mudando alguma variável de configuração do kernel (ou, talvez, no próprio procfs), mas alterá-lo para menos implica que cada thread terá menos tempo disponível para execução, tornando o seu sistema ainda mais lento! E tempos maiores podem causar latências maiores entre as múltimas threads… Usar 20 ms como base de cálculo é uma boa pedida para ambos os sistemas operacionais.

Como temos duas variáveis empíricas (K_{el} e t_{ts}), N_p pode ser escrito em função de U_{max} e das outras duas variáveis…

N_p(U_{max},K_{el},t_{ts}) = \left \lceil \frac{0.0057 \cdot K_{el} \cdot U_{max}}{t_{ts}} \right \rceil

Exemplo de uso: Considere que sua aplicação terá o limite de 300 threads (usuários simultâneos), a carga média de cada thread será de 30% (ou seja, no máximo umas 90 threads estarão 100% ativas) e é aceitável que o o tempo máximo entre task switchings seja de 1 segundo:

N_p(300,0.3,1) = \left \lceil \frac{0.0057 \cdot 0.3 \cdot 300}{1} \right \rceil = 6

Nessas condições conseguimos 6 processadores lógicos. É fácil notar que quanto mais usuários simultâneos, quanto maior a carga efetiva e quanto menor o tempo entre chaveamentos de contextos de tarefa, maior será a quantidade de processadores logicos necessários no servidor.

Os valores de K_{el} e t_{ts} devem ser escolhidos cuidadosamente com base nas características e espectavivas de comportamento da aplicação. Se muito processamento é feito então K_{el} deve refletir isso. Se tivermos uma carga máxima, ou seja, se esse coeficiente for 1, então teríamos um máximo de 18 processadores, se os outros parâmetros forem mantidos. Considero uma carga efetiva de 30% um bom parâmetro mínimo.

A consideração que devo fazer para t_{ts} é que ele deve diminuir, para processos com grande carga efetiva… Infelizmente isso aumentará ainda mais a quantidade de processadores necessários para suportar uma boa performance do sistema! Mas, nem sempre… Por exemplo: se tivermos carga efetiva média de 0.5 (150 das 300 threads “no talo”!), então poderíamos diminuir o tempo t_{ts} para 500 ms, para garantir que todas as threads terão latência pequena. Isso nos dará também 18 processadores lógicos!

Com esses critérios, para atender 300 requisições/segundo (aprox), com tempo entre task switching máximo de 1 segundo, teríamos que ter entre 6 e 18 processadores para este servidor.

Deixe-me lembrá-lo que essa função é empírica. Ela só oferece uma base mais sólida do que a adivinhação.

Voltando ao assunto…

Então, por que colocar dezenas ou centenas de virtual hosts, com aplicações diferentes, num mesmo web servernão é uma boa idéia? Considere o caso de um servidor com 50 aplicações diferentes, cada uma com suporte máximo de 300 usuários, com coeficiente de carga efetiva média de 30% e tempo de latência entre threads de 1 segundo… Temos 15000 threads para atender, com alguma garantia de atendimento de 3000 threads (30%). Para tanto teremos que ter um servidor com, no mínimo, 257 processadores (use a fórmula) e, no máximo, 855!

Neste ponto você pode estar coçando a cabeça e tentando me dizer: “Mas… meu servidor suporta 80 aplicações diferentes, com 8 processadores, sem problemas! Segundo seus cálculos eu teria que ter 410 processadores, no mínimo!”… De fato! Mas, considere que suas aplicações provavelmente têm um coeficiente efetivo de carga média “real” muito pequeno, ou seja, dos 300 usuários simultâneos por aplicação, talvez apenas uns 3 ou 4 estejam requisitando processamento das threads… Com um coefiente efetivo assim, a fórmula mostra que 14 processadores seriam suficientes para dar uma garantia mínima. Ao usar 8, nessas condições, seu servidor encontra-se só um pouco acima da média de carga de 70%.

Considere que, se algumas dessas aplicações começarem a exigir mais que 1% de coeficiente efetivo de carga média, então você terá um problemão de performance nas mãos! Pior ainda, o problema de performance vai ser distribuído entre todas as aplicações no servidor, já que a distribuição de threads entre os processadores lógicos não é tão previsível… Trabalhar com afinidade de threads geralmente leva a baixa performance (porque bagunça com a lógica do task scheduler!).

A moral da história é: Dimensionar um seridor para uma única aplicação, tentando garantir a média de carga de 70% dos processadores, é simples… Se tentar fazer o mesmo para múltiplas aplicações a coisa pode ficar bem complexa… Fica ainda melhor dimensionar servidores “menores” através da divisão de trabalho (usando um proxy, por exemplo, para direcionar requisições entre vários servidores com a mesma aplicação – usando técnicas de clustering e/ou HA [High Availability]).

Manobras radicais com o LVM – Logical Volume Manager

Abstrato

Recentemente tive problemas com o meu computador e resolvi mudar o disco de 500GB para outro de 160Gb, contendo somente o sistema operacional e os dados “hot”, com os quais trabalho.

A idéia é que eu tenha um disco de trabalho livre para se reformatar, substituir, fazer testes, etc, mantendo os meus dados e arquivos a salvo num HD externo, que passa a ser, a partir de agora a minha base.

Estas operações são muito mais fáceis de ser feitas usando o gerenciador de volumes lógicos (lvm) e a vantagem é que o computador pode bootar a partir do usb usando o sistema original.

Isto exige um pouco de conhecimento do funcionamento do lvm, mas explicar isto é tarefa de outros artigos que podem ser buscados no google. (Ex: http://tldp.org/HOWTO/LVM-HOWTO/)

O Caso

Eu tenho em vg_pistache (eu escolhi este nome quando criei pois o meu computador tem o nome pistache) vários volumes lógicos cujo espaço somado não ocupa todo o disco.

Vou ampliando/reduzindo estes volumes na medida da minha necessidade e assim evito os formatos “engessados” de sistema com partições obrigatóriamente de tamanho fixo e que de vez em quando estouram o limite. Também sigo a abordagem de manter partições separadas para / (slash), root, home, var, etc, o que facilita muito a manutenção dos computadores ao longo dos anos.

Aqui, o que pretendo anotar, é uma tarefa específica: manobrar o HD, via usb, para montagens e desmontagens sem necessidade de reboot no sistema. Isto, além das vantagens de segurança, física e lógica (os volumes podem ser criptografados com facilidade), possibilita o transporte fácil entre casa/escritório.

As dicas originais, peguei aqui e decidi então, antes que eu me esqueça e o link original suma, anotar a experiência e o método que utilizei.

As partes

  1. 1 Notebook
  2. 1 HD SATA* de 160 GB
  3. 1 HD SATA de 500 GB
  4. 1 Cradle** USB Seagate® FreeAgent GoFlex™.

* SATA: não precisaria ser SATA, pois se vc tiver um equipamento PATA ou IDE (antigo ) o processo funcioanará pois é feito via usb.
** Cradle = cabo com uma ponta usb e outra com uma base de suporte onde o disco é espetado.

Objetivo

Montar o HD de 500GB do notebook, disponibilizado pelo cradle para acessos de backup e restauração do disco de 160GB que ficará instalado no notebook.

Uma vez montado (fisicamente) na base o disco de 500 GB é reconhecido pelo linux e para se montar (logicamente) um volume existente em um HD (ou usb/hd externo) é necessário o seguinte

  •  Primeiro fazer um reconhecimento de volumes, usando o comando pvscan (via conta root ou sudo), que quer dizer Physical Volume Scan

– Os discos existentes serão listados assim:

root@pistache:/home/marz# pvscan
PV /dev/sdb5 VG vg_pistache lvm2 [465,52 GiB / 179,72 GiB free]
PV /dev/sda5 VG pistache lvm2 [148,81 GiB / 0 free]
Total: 2 [614,33 GiB] / in use: 2 [614,33 GiB] / in no VG: 0 [0 ]

Neste caso o volume que quero acessar está em /dev/sdb5 e não aparece sob /dev pois não havia sido conectado no cradle quando bootei, no qual eu espetei o disco onde estava instalado o meu sistema operacional, com todos os meus dados e arquivos gravados neste volume

  • Em seguida fazer um vgscan que quer dizer Volume Group Scan, que no meu caso retornou isto:
root@pistache:/home/marz# vgscan
Reading all physical volumes. This may take a while...
Found volume group "vg_pistache" using metadata type lvm2
Found volume group "pistache" using metadata type lvm2

Uma vez identificado o os volumes fisicos e os grupos lógicos buscamos pelos volumes lógicos com lvscan

root@pistache:/home/marz# lvscan
inactive '/dev/vg_pistache/root' [5,00 GiB] inherit
inactive '/dev/vg_pistache/usr' [13,00 GiB] inherit
inactive '/dev/vg_pistache/swap_1' [5,80 GiB] inherit
inactive '/dev/vg_pistache/var' [20,00 GiB] inherit
inactive '/dev/vg_pistache/tmp' [5,00 GiB] inherit
inactive '/dev/vg_pistache/vm' [50,00 GiB] inherit
inactive '/dev/vg_pistache/music' [40,00 GiB] inherit
inactive '/dev/vg_pistache/books' [10,00 GiB] inherit
inactive '/dev/vg_pistache/lab' [20,00 GiB] inherit
inactive '/dev/vg_pistache/videos' [20,00 GiB] inherit
inactive '/dev/vg_pistache/images' [40,00 GiB] inherit
inactive '/dev/vg_pistache/secret' [4,00 GiB] inherit
inactive '/dev/vg_pistache/mule' [20,00 GiB] inherit
inactive '/dev/vg_pistache/home' [20,00 GiB] inherit
inactive '/dev/vg_pistache/wine' [1,00 GiB] inherit
inactive '/dev/vg_pistache/kdemarz' [8,00 GiB] inherit
inactive '/dev/vg_pistache/vartemp' [4,00 GiB] inherit
ACTIVE '/dev/pistache/root' [332,00 MiB] inherit
ACTIVE '/dev/pistache/usr' [8,38 GiB] inherit
ACTIVE '/dev/pistache/var' [2,79 GiB] inherit
ACTIVE '/dev/pistache/swap_1' [7,56 GiB] inherit
ACTIVE '/dev/pistache/tmp' [380,00 MiB] inherit
ACTIVE '/dev/pistache/home' [129,38 GiB] inherit

Note aqui que os volumes ATIVADOS já foram instalados e reconhecidos durante o boot, entretanto os inativos não aparecerm sob o /dev, e é isto que precisamos fazer acontecer para que possamos montá-los.

Isto se faz com o seguinte comando

lvchange -a y /dev/vg_pistache/home

Que não retorna mensagens na saída mas podemos verificar o resultado emitindo outro comando lvscan

.
.
.
inactive '/dev/vg_pistache/mule' [20,00 GiB] inherit
ACTIVE '/dev/vg_pistache/home' [20,00 GiB] inherit
inactive '/dev/vg_pistache/wine' [1,00 GiB] inherit
inactive '/dev/vg_pistache/kdemarz' [8,00 GiB] inherit
.
.
.

Pronto, uma vez visto pelo sistema, basta montar normalmente com o comando mount, que no meu caso será facilitado por outra técnica: deixar o volume preparado em fstab.

Eu utilizo a seguinte linha no meu fstab para isto (ver man fstab) http://linux.die.net/man/5/fstab

# Old pistache
/dev/vg_pistache/home /var/local/mirror/home ext4 defaults,noauto,owner,user 0 2

Que quer dizer:

  • /dev/vg_pistache/home: disco a ser montado
  • /var/local/mirror/home: ponto de montagem: o diretório para o ponto de montagem eu já deixei preparado em meu /var/local…
  • Opções para montagem:
    • default: montar com o privilegios normais (rw, suid, dev, exec, auto, nouser e async)
    • noauto: não montar durante o boot
    • owner: somente quem montou é quem pode desmontar (se fosse users, qualquer um monta ou desmonta)
  • 0 – não precisa de “dump” (Ver comando dump)
  • 2 – ordem de checkup (o fsck será feito na segunda passagem, ou seja, depois de checar o / (slash).

Aproveitando a deixa sobre novatos

Quem puder, leia o livro: The Debian Administrator’s Handbook – Debian Squeeze from Discovery to Mastery Raphaël Hertzog and Roland Mas May 9, 2012 (Free).

Lançado este mês, é um ótimo excelente tutorial.

E como distribuição fica a dica: Linux Mint ! (está em primeiro lugar há um ano na lista da distrowatch). Tem uma versão baseada em Debian, em vez do sórdido urubuntu.  Instala tudo que precisa, sem as “exigências” do Debian, com relação a free software. Pena que a versão para Debian vem somente com Xfce e Gnome,

Mais uma: eu estava adiando a migração para o Chrome, talvez por preguiça de testar ou antipatia ao marketing pesado da google, mas tive que ceder. No Debian, basta instalar o pacote Chromium (eu estava  usando o IceWeasel, até hoje). Foi muito direto: importou tudo e é sensivelmente mais rápido e eficiente.