Pular para o conteúdo

Buffer Cache

O buffer cache é a camada que fica entre a VFS e um driver de armazenamento real, mantendo blocos de disco lidos ou escritos recentemente em memória para que nem ler o mesmo bloco duas vezes nem escrevê-lo repetidamente precise tocar o disco a cada vez. ext2 e os outros artigos de sistema de arquivos desta wiki descrevem ler um bloco como se sempre significasse lê-lo do disco; na prática, essa leitura quase sempre checa esse cache primeiro.

Todo bloco cacheado é indexado pelo par que o identifica de forma única: a qual dispositivo pertence, e qual número de bloco dentro desse dispositivo. Uma requisição de leitura para um dado dispositivo e número de bloco checa o cache primeiro, comumente através de uma tabela hash indexada exatamente por esse par, e só emite uma leitura real através do driver de armazenamento num cache miss, um bloco que o cache não contém no momento. Um bloco já cacheado, um cache hit, retorna imediatamente da memória, ordens de magnitude mais rápido do que qualquer acesso físico de armazenamento, seja esse armazenamento um disco rotativo ou flash.

struct buffer_head *bread(dev_t dev, uint32_t block) {
struct buffer_head *bh = cache_lookup(dev, block);
if (bh) return bh; // cache hit
bh = cache_alloc(dev, block);
driver_read(dev, block, bh->data); // cache miss: vai ao disco
cache_insert(bh);
return bh;
}

Uma escrita também não vai direto ao disco: ela é aplicada à cópia cacheada em memória, e esse bloco é marcado como dirty, um único bit registrando que essa cópia cacheada não corresponde mais ao que de fato está no disco. Um bloco dirty é escrito depois, seja por um flush periódico em segundo plano (comumente a cada poucos segundos) ou quando pressão de memória força o cache a despejar algo e precisa salvar o conteúdo de um bloco dirty antes de reaproveitar a memória que ocupa, em vez de toda escrita individual disparar sua própria operação imediata de disco.

void bwrite(struct buffer_head *bh) {
memcpy(bh->data, new_data, block_size);
bh->dirty = true; // ainda não escrito no disco
}
void flush_dirty_buffers(void) {
for_each_buffer(bh) {
if (bh->dirty) {
driver_write(bh->dev, bh->block, bh->data);
bh->dirty = false;
}
}
}
Uma escrita caindo no cache e sendo marcada como dirty imediatamente, com a escrita real em disco só acontecendo depois, num flush periódicowrite()retorna imediatamenteBuffer cachebloco atualizadodirty = truesegundos depois, ou sob pressão de memóriaDiscode fato escritojanela onde um desligamento não limpo perde essa escrita

Essa janela entre uma escrita retornar e o bloco de fato chegar ao disco é exatamente o que um desligamento não limpo explora: um bloco dirty ainda sentado só no cache no momento em que a energia se perde nunca chega ao disco, indistinguível depois de uma escrita que nunca tivesse sido emitida em primeiro lugar. Journaling protege a consistência estrutural de um sistema de arquivos através dessa mesma janela, mas essa é uma garantia mais estreita do que proteger o conteúdo de todo bloco dirty: journaling consegue garantir que os metadados do sistema de arquivos permaneçam internamente consistentes depois de uma queda sem garantir que um bloco de dados dirty específico, ainda sentado sem vazar no cache quando a energia se perdeu, sobreviva.

Um bloco já cacheado a partir de uma leitura e um bloco sujado por uma escrita são a mesma estrutura, não dois mecanismos separados empilhados um sobre o outro: o cache não distingue “cache de leitura” de “cache de escrita” internamente, só se um dado bloco cacheado no momento por acaso está dirty. fsync e chamadas similares de flush explícito existem especificamente para forçar um write-back síncrono de um bloco particular, ou de todo bloco dirty pertencente a um arquivo particular, contornando o cronograma periódico normal para um chamador que precisa de uma garantia específica de que seus dados de fato chegaram ao disco antes de prosseguir, o commit de transação de um banco de dados sendo o exemplo canônico. Despejar um bloco limpo (não dirty) do cache para abrir espaço para outra coisa é seguro a qualquer momento, já que o disco já contém uma cópia atualizada, mas despejar um bloco dirty exige escrevê-lo primeiro, o que é o que torna despejo de cache sob pressão de memória ocasionalmente mais lento do que uma alocação comum: a escrita do bloco despejado precisa completar antes de sua memória poder de fato ser reaproveitada.

  1. ^ M. Bach, The Design of the UNIX Operating System, Capítulo 3: a descrição clássica do buffer cache e sua interação com I/O de sistema de arquivos.
  • VFS: a camada logo acima da qual este cache de leituras e escritas de bloco fica.
  • Journaling: uma garantia mais estreita, protegendo a estrutura do sistema de arquivos através da mesma janela de queda à qual os blocos dirty deste cache estão expostos.