
Oi, habrozhiteli! Já escrevemos sobre o livro de Michael Kerrisk,
"API Linux. Guia abrangente .
” Agora, decidimos publicar um trecho do livro "Gerenciando o buffer de E / S de arquivos no kernel"
A redefinição da memória buffer do kernel para arquivos de saída pode ser forçada. Às vezes, isso é necessário se o aplicativo, antes de continuar trabalhando (por exemplo, um banco de dados de log de processo é alterado), deve garantir que a saída seja realmente gravada no disco (ou pelo menos no cache do disco do hardware).
Antes de considerar as chamadas do sistema usadas para controlar o buffer do kernel, vale a pena considerar várias definições relacionadas do SUSv3.
E / S sincronizada com integridade de dados e arquivosNo SUSv3, o conceito de conclusão de E / S sincronizada significa "uma operação de E / S que levou a uma transferência bem-sucedida de dados [para o disco] ou foi diagnosticada como malsucedida".
O SUSv3 define dois tipos diferentes de terminações de E / S sincronizadas. A diferença entre os tipos está relacionada aos metadados ("dados sobre dados") que descrevem o arquivo. O kernel os armazena junto com os dados do próprio arquivo. Os detalhes dos metadados do arquivo serão discutidos na seção 14.4 ao examinar os inodes do arquivo. Enquanto isso, basta observar que os metadados do arquivo incluem informações como informações sobre o proprietário do arquivo e seu grupo, direitos de acesso ao arquivo, tamanho do arquivo, número de links físicos para o arquivo, registros de data e hora mostrando a hora em que o arquivo foi acessado pela última vez, a última vez em que foi modificado e a hora da última alteração de metadados, além de indicadores para blocos de dados.
O primeiro tipo de conclusão de E / S sincronizada no SUSv3 é a conclusão da integridade dos dados. Ao atualizar os dados do arquivo, deve-se garantir a transferência de informações suficientes para permitir a extração adicional desses dados para continuar funcionando.
- para uma operação de leitura, isso significa que os dados do arquivo solicitado foram transferidos (do disco) para o processo. Se houver operações de gravação pendentes que possam afetar os dados solicitados, os dados serão transferidos para o disco antes da leitura.
- para uma operação de gravação, isso significa que os dados especificados na solicitação de gravação foram transferidos (para o disco), como todos os metadados de arquivo necessários para extrair esses dados. O ponto principal a prestar atenção: para garantir que os dados sejam extraídos do arquivo modificado, não é necessário transferir todos os arquivos medaten. Um exemplo do atributo de metadados de um arquivo modificado que precisa ser migrado é seu tamanho (se a operação de gravação aumentar o tamanho do arquivo). Por outro lado, os registros de data e hora do arquivo a ser modificado não precisarão ser transferidos para o disco antes que ocorra a recuperação subsequente dos dados.
O segundo tipo de conclusão de E / S sincronizada definida no SUSv3 é a conclusão da integridade do arquivo. Essa é uma opção avançada para concluir E / S sincronizada com integridade dos dados. A diferença entre esse modo é que, durante a atualização do arquivo, todos os seus metadados são transferidos para o disco, mesmo que isso não seja necessário para a extração subsequente dos dados do arquivo.
Chamadas do sistema para controlar o buffer do kernel durante a E / S de arquivoA chamada do sistema fsync () redefine todos os dados em buffer e todos os metadados associados a um arquivo aberto que possui um descritor fd. Chamar fsync () coloca o arquivo em um estado de integridade (arquivo) após a conclusão da E / S síncrona.
A chamada fsync () retorna o controle somente após a transferência dos dados para o dispositivo de disco (ou pelo menos para o cache).
#include <unistd.h> int fsync(int fd);
Retorna em caso de sucesso 0 ou -1 em caso de erro
A chamada do sistema fdatasync () funciona exatamente como fsync (), mas coloca o arquivo em um estado de integridade (dados) após a conclusão da E / S síncrona.
#include <unistd.h> int fdatasync(int fd);
Retorna em caso de sucesso 0 ou -1 em caso de erro
O uso de fdatasync () reduz potencialmente o número de operações de disco de duas exigidas pela chamada do sistema fsync () para uma. Por exemplo, se os dados do arquivo foram alterados, mas o tamanho permanece o mesmo, chamar fdatasync () apenas força os dados a serem atualizados. (Já foi observado acima que, para concluir uma operação de E / S síncrona com integridade dos dados, não há necessidade de transferir alterações em atributos como a hora em que o arquivo foi modificado pela última vez.) Por outro lado, chamar fsync () também forçará a transferência de metadados para o disco.
Essa redução no número de operações de E / S do disco será útil para aplicativos individuais para os quais o desempenho e a atualização precisa de metadados específicos (por exemplo, carimbos de data / hora) desempenham um papel decisivo. Isso pode levar a melhorias significativas no desempenho de aplicativos que produzem várias atualizações de arquivo por vez. Como os dados e os metadados do arquivo geralmente estão localizados em diferentes partes do disco, a atualização de ambos exigirá pesquisas repetidas para frente e para trás no disco.
No Linux 2.2 e versões anteriores, o fdatasync () é implementado como uma chamada para fsync (), portanto, não oferece nenhum aumento no desempenho.
A partir da versão 2.6.17 do kernel, o Linux fornece uma chamada de sistema não-padrão sync_file_range (). Ele permite que você controle com mais precisão o processo de liberação dos dados do arquivo no disco do que o fdatasync (). Ao ligar, você pode especificar a área a ser descartada no arquivo e definir sinalizadores que definem as condições para bloquear esta chamada. Veja a página de manual sync_file_range (2) para mais detalhes.
A chamada do sistema sync () faz com que todos os buffers do kernel que contêm informações atualizadas do arquivo (ou seja, blocos de dados, blocos de ponteiro, metadados etc.) sejam liberados para o disco.
#include <unistd.h> void sync(void);
Na implementação do Linux, a função sync () retorna o controle somente depois que todos os dados foram transferidos para o dispositivo de disco (ou pelo menos para seu cache). Porém, no SUSv3, é permitido que sync () simplesmente introduza a transferência de dados para a operação de E / S no plano e retorne o controle até que a transferência seja concluída.
Um thread do kernel executado continuamente libera os buffers do kernel modificados para o disco se eles não foram explicitamente sincronizados por 30 segundos. Isso é feito para impedir que os buffers de dados fiquem fora de sincronia com o arquivo de disco correspondente por longos períodos de tempo (e não os exponha ao risco de perda no caso de uma falha do sistema). No Linux 2.6, essa tarefa é executada pelo thread do kernel pdflush. (No Linux 2.4, foi executado pelo thread do kupdated kernel.)
O período (em centésimos de segundo) após o qual o buffer alterado deve ser liberado para o disco pelo código de fluxo pdflush é definido no arquivo / proc / sys / vm / dirty_expire_centisecs. Arquivos adicionais no mesmo diretório controlam outros recursos da operação executada pelo fluxo pdflush.
Ative o modo de sincronização para todos os registros: O_SYNCA especificação do sinalizador O_SYNC ao chamar open () faz com que todas as operações de saída subsequentes sejam executadas no modo síncrono:
fd = open(pathname, O_WRONLY | O_SYNC);
Após essa chamada para open (), cada operação de gravação () executada em um arquivo libera automaticamente os dados e os metadados do arquivo no disco (ou seja, as gravações são executadas como operações de gravação sincronizada com integridade do arquivo).
Nas versões mais antigas do sistema BSD, o sinalizador O_FSYNC era usado para fornecer a funcionalidade incluída no sinalizador O_SYNC. Na glibc, o sinalizador O_FSYNC é definido como sinônimo de O_SYNC.
Impacto no desempenho do sinalizador O_SYNCO uso do sinalizador O_SYNC (ou chamadas frequentes para fsync (), fdatasync () ou sync ()) pode afetar bastante o desempenho. Na mesa A Figura 13.3 mostra o tempo necessário para gravar 1 milhão de bytes em um arquivo que acabou de ser criado (no sistema de arquivos ext2) para vários tamanhos de buffer com o sinalizador O_SYNC definido e desmarcado. Os resultados foram obtidos (usando o programa filebuff / write_bytes.c fornecido no código fonte do livro) usando o kernel “vanilla” versão 2.6.30 e o sistema de arquivos ext2 com um tamanho de bloco de 4096 bytes. Cada linha contém o valor médio obtido após 20 partidas para um determinado tamanho de buffer.
Quadro 13.3 O efeito do sinalizador O_SYNC em uma velocidade de gravação de 1 milhão de bytes
Como você pode ver, especificar o sinalizador O_SYNC leva a um aumento monstruoso no tempo gasto ao usar um buffer de 1 byte mais de 1000 vezes. Observe também a grande diferença que ocorre ao executar registros com o sinalizador O_SYNC entre o tempo decorrido e o tempo de uso da CPU. É uma consequência do bloqueio da execução do programa quando o conteúdo real de cada buffer é liberado no disco.
Nos resultados mostrados na tabela. 13.3, outro fator que afeta o desempenho ao usar O_SYNC não é levado em consideração. As unidades de disco modernas têm um cache interno grande e, por padrão, a configuração do sinalizador O_SYNC simplesmente transfere dados para esse cache. Se você desativar o cache do disco (usando o comando hdparm –W0), o impacto no desempenho do O_SYNC se tornará ainda mais significativo. Com um tamanho de buffer de 1 byte, o tempo decorrido aumentará de 1030 segundos para aproximadamente 16.000 segundos. Com um tamanho de buffer de 4096 bytes, o tempo decorrido aumentará de 0,34 segundos para 4 segundos. Como resultado, se você precisar forçar a liberação dos buffers do kernel para o disco, considere se é possível projetar o aplicativo usando buffers maiores para write () ou considere o uso de chamadas periódicas fsync () ou fdatasync () em vez do sinalizador O_SYNC.
Sinalizadores O_DSYNC e O_RSYNCO SUSv3 define dois sinalizadores de status adicionais de arquivos abertos relacionados à E / S sincronizada: O_DSYNC e O_RSYNC.
O sinalizador O_DSYNC resulta em operações de gravação sincronizadas subsequentes com integridade de dados de E / S finalizada (semelhante ao uso de fdatasync ()). O efeito de sua operação é diferente do efeito causado pelo sinalizador O_SYNC, cujo uso leva a operações de gravação sincronizadas subsequentes com integridade de arquivo (como fsync ()).
O sinalizador O_RSYNC é especificado junto com O_SYNC ou O_DSYNC e leva a uma extensão do comportamento associado a esses sinalizadores durante as operações de leitura. A especificação dos sinalizadores O_RSYNC e O_DSYNC ao abrir o arquivo resulta em operações de leitura sincronizada subsequentes com integridade dos dados (ou seja, antes da conclusão da leitura, todas as entradas de arquivo pendentes são concluídas devido à presença de O_DSYNC). A especificação dos sinalizadores O_RSYNC e O_SYNC ao abrir o arquivo leva a operações de leitura sincronizadas subsequentes com integridade do arquivo (ou seja, antes da conclusão da leitura, todas as entradas de arquivo pendentes são concluídas devido à presença de O_SYNC).
Antes do lançamento da versão 2.6.33 do kernel, os sinalizadores O_DSYNC e O_RSYNC não eram implementados no Linux e essas constantes eram definidas nos arquivos de cabeçalho glibc como definindo o sinalizador O_SYNC. (No caso de O_RSYNC, isso não era verdade, pois O_SYNC não afeta nenhum recurso funcional das operações de leitura.)
A partir da versão 2.6.33 do kernel, o Linux implementa o sinalizador O_DSYNC, e a implementação do sinalizador O_RSYNC provavelmente será adicionada em versões futuras do kernel.
Antes do lançamento do kernel 2.6.33 no Linux, não havia implementação completa da semântica O_SYNC. Em vez disso, o sinalizador O_SYNC foi implementado como O_DSYNC. Em aplicativos vinculados a versões mais antigas da biblioteca GNU C para kernels mais antigos, nas versões 2.6.33 e posteriores do Linux, o sinalizador O_SYNC ainda se comporta como O_DSYNC. Isso é feito para manter o comportamento familiar desses programas. (Para preservar a compatibilidade binária com versões anteriores no kernel 2.6.33, o sinalizador O_DSYNC recebeu o sinalizador O_SYNC antigo e o novo sinalizador O_SYNC inclui o sinalizador O_DSYNC (04010000 e 010000 respectivamente em uma das máquinas). Isso permite aplicativos compilados com novos arquivos de cabeçalho. , obtenha pelo menos a semântica O_DSYNC nos kernels lançados antes da versão 2.6.33.)
13.4 Visão geral do buffer de E / S
Na fig. A Figura 13.1 mostra o esquema de buffer usado (para arquivos de saída) pela biblioteca stdio e pelo kernel, e também mostra os mecanismos para controlar cada tipo de buffer. Se você descer o gráfico até o meio, verá a transferência de dados do usuário pelas funções da biblioteca stdio para o buffer stdio, que funciona no espaço de memória do usuário. Quando esse buffer está cheio, a biblioteca stdio recorre à chamada do sistema write (), que transfere dados para o cache do buffer do kernel (localizado na memória do kernel). Como resultado, o kernel inicia uma operação de disco para transferir dados para o disco.
Na parte esquerda do circuito na fig. 13.1 mostra chamadas que podem ser usadas a qualquer momento para forçar explicitamente a liberação de qualquer um dos buffers. A parte direita mostra as chamadas que podem ser usadas para executar uma redefinição automática, desativando o buffer na biblioteca stdio ou ativando a saída de arquivo de um modo de execução síncrona para chamadas do sistema, para que cada chamada write () seja liberada imediatamente no disco.
13.5 Notificação de E / S do Kernel
A chamada do sistema posix_fadvise () permite que o processo informe o kernel de seu método preferido de acessar dados do arquivo.
O kernel pode (mas não precisa) usar as informações fornecidas pela chamada do sistema posix_fadvise () para otimizar o uso do cache do buffer, aumentando assim o desempenho de E / S no processo e no sistema como um todo. Chamar posix_fadvise () não afeta a semântica do programa.
#define _XOPEN_SOURCE 600 #include <fcntl.h> int posix_fadvise(int fd, off_t offset, off_t len, int advice);
Retorna com sucesso 0 ou um número de erro positivo quando ocorre
O argumento fd é um descritor de arquivo que identifica o arquivo para o qual o kernel precisa ser contatado. Os argumentos offset e len identificam a área do arquivo ao qual a notificação se refere: offset indica o deslocamento inicial da área e len indica seu tamanho em bytes. Definir len como 0 significa que todos os bytes são destinados, começando com deslocamento e terminando com o final do arquivo. (Nas versões do kernel anteriores à 2.6.6, o valor 0 para len era interpretado literalmente como 0 bytes.)
O argumento de aconselhamento mostra a natureza pretendida do acesso do processo ao arquivo. É definido com um dos seguintes valores.
POSIX_FADV_NORMAL - o processo não possui uma notificação especial sobre os padrões de tratamento. Esse é o comportamento padrão se nenhuma notificação for fornecida para o arquivo. No Linux, essa operação define a janela para ler proativamente os dados de um arquivo para o tamanho original (128 KB).
POSIX_FADV_SEQUENTIAL - o processo envolve a leitura seqüencial de dados de compensações menores para maiores. No Linux, essa operação define a janela para ler proativamente os dados de um arquivo para dobrar seu valor original.
POSIX_FADV_RANDOM - o processo envolve acessar dados em ordem aleatória. No Linux, esta opção desativa a leitura proativa de dados de um arquivo.
POSIX_FADV_WILLNEED - o processo envolve acessar a área especificada do arquivo em um futuro próximo. O kernel lê preventivamente os dados para preencher o cache do buffer com os dados do arquivo no intervalo especificado pelos argumentos offset e len. As chamadas read () subsequentes para o arquivo não bloqueiam a E / S do disco, mas simplesmente recuperam dados do cache do buffer. O kernel não garante quanto tempo os dados recuperados do arquivo estão no cache do buffer. Se durante a operação de outro processo ou kernel houver uma necessidade especial de memória, a página será reutilizada. Em outras palavras, se a memória estiver em alta demanda, precisamos garantir um pequeno intervalo de tempo entre a chamada para posix_fadvise () e a chamada (ou chamadas) subsequente para leitura (). (A funcionalidade equivalente à operação POSIX_FADV_WILLNEED é fornecida pela chamada do sistema readahead () específica do Linux.)
POSIX_FADV_DONTNEED - o processo não envolve chamadas para a área de arquivos especificada em um futuro próximo. Dessa maneira, o kernel é notificado de que pode liberar as páginas de cache correspondentes (se houver). No Linux, esta operação é realizada em dois estágios. Primeiro, se a fila de gravação no dispositivo host não estiver cheia de uma série de solicitações, o kernel descartará quaisquer páginas de cache modificadas na área especificada. O kernel tenta liberar todas as páginas de cache da área especificada. Para páginas modificadas nesta área, o segundo estágio será concluído com êxito apenas se tiverem sido gravados no dispositivo base durante o primeiro estágio, ou seja, a fila de gravação no dispositivo não estiver cheia. Como o aplicativo não pode verificar o status da fila no dispositivo, é possível garantir que as páginas do cache sejam liberadas chamando fsync () ou fdatasync () no identificador fd antes de aplicar POSIX_FADV_DONTNEED.
POSIX_FADV_NOREUSE - o processo envolve um acesso único aos dados na área especificada do arquivo, sem reutilizá-los. Assim, o kernel é notificado de que pode liberar páginas após um único acesso a elas. No Linux, esta operação está sendo ignorada.
A especificação posix_fadvise () apareceu apenas no SUSv3, e essa interface não é suportada por todas as implementações do UNIX. No Linux, a chamada posix_fadvise () é fornecida desde a versão 2.6 do kernel.
13.6 Ignorar o cache do buffer: E / S direta
A partir da versão 2.4 do kernel, o Linux permite que um aplicativo ignore o cache do buffer ao executar E / S de disco movendo dados diretamente do espaço de memória do usuário para um arquivo ou dispositivo de disco. Às vezes, esse modo é chamado de E / S direta ou não processada.
As informações fornecidas aqui são apenas para Linux e não são padronizadas no SUSv3. No entanto, algumas opções de acesso direto de E / S para dispositivos ou arquivos são fornecidas pela maioria das implementações do UNIX.
Às vezes, a E / S direta é mal interpretada como um meio de obter alto desempenho de E / S. Mas para a maioria dos aplicativos, o uso de E / S direta pode reduzir significativamente o desempenho. O fato é que o kernel executa várias otimizações para melhorar o desempenho de E / S através do uso de um cache de buffer, incluindo leitura proativa seqüencial de dados, executando E / S em clusters de blocos de disco e permitindo processos acessando o mesmo volume No mesmo arquivo, compartilhe buffers no cache. Todos esses tipos de otimização ao usar E / S direta são perdidos. Destina-se apenas a aplicativos com requisitos especializados de E / S, por exemplo, sistemas de gerenciamento de banco de dados que executam seu próprio cache e otimização de E / S e que não precisam do kernel para perder tempo e memória da CPU para executar as mesmas tarefas.
A entrada / saída direta pode ser realizada em relação a um único arquivo ou em relação a um dispositivo de bloco (por exemplo, um disco). Para fazer isso, ao abrir um arquivo ou dispositivo usando a chamada open (), o sinalizador O_DIRECT é especificado.
O sinalizador O_DIRECT funciona desde a versão 2.4.10 do kernel. O uso desse sinalizador não é suportado por todos os sistemas de arquivos e versões do kernel do Linux. A maioria dos sistemas de arquivos básicos suporta o sinalizador O_DIRECT, mas muitos sistemas de arquivos não UNIX (como o VFAT) não. Você pode testar o suporte para esse recurso testando o sistema de arquivos selecionado (se o sistema de arquivos não suportar O_DIRECT, chamar open () falhará com um erro EINVAL) ou examinando o código-fonte do kernel para isso.
Se um processo abriu o arquivo com o sinalizador O_DIRECT e o outro da maneira usual (ou seja, usando o cache do buffer), não há consistência entre o conteúdo do cache do buffer e os dados lidos ou gravados por meio de E / S direta. Esse desenvolvimento deve ser evitado.
Informações sobre o método desatualizado (agora não recomendado) para obter acesso não processado a um dispositivo de disco podem ser encontradas na página de manual não processada (8).
Restrições de alinhamento para E / S diretaComo a E / S direta (em dispositivos de disco e em relação aos arquivos) envolve acesso direto ao disco, algumas limitações devem ser observadas ao executar a E / S.
- o buffer de dados portátil deve estar alinhado na borda da memória, um múltiplo do tamanho do bloco.
- O deslocamento no arquivo ou no dispositivo a partir do qual os dados transferidos começam deve ser um múltiplo do tamanho do bloco.
- O comprimento dos dados transferidos deve ser múltiplo do tamanho do bloco.
O não cumprimento de qualquer uma dessas restrições resultará em um erro EINVAL. Na lista acima, o tamanho do bloco refere-se ao tamanho do bloco físico do dispositivo (geralmente 512 bytes).
Ao executar E / S direta no Linux 2.4, mais restrições são impostas do que no Linux 2.6: alinhamento, comprimento e deslocamento devem ser múltiplos do tamanho do bloco lógico do sistema de arquivos usado. (Normalmente, os tamanhos de blocos lógicos em um sistema de arquivos são 1024, 2048 ou 4096 bytes.)
Exemplo de programa13.1 O_DIRECT . , ( ) , , , , , , , read(). 4096 .
, :
$ ./direct_read /test/x 512 512 0 Read 512 bytes $ ./direct_read /test/x 256 ERROR [EINVAL Invalid argument] read 512 $ ./direct_read /test/x 512 1 ERROR [EINVAL Invalid argument] read 512 $ ./direct_read /test/x 4096 8192 512 Read 4096 bytes $ ./direct_read /test/x 4096 512 256 ERROR [EINVAL Invalid argument] read 512
13.1 , , , memalign(). memalign() 7.1.4.
#define _GNU_SOURCE /* O_DIRECT <fcntl.h> */ #include <fcntl.h> #include <malloc.h> #include "tlpi_hdr.h" int main(int argc, char *argv[]) { int fd; ssize_t numRead; size_t length, alignment; off_t offset; void *buf; if (argc < 3 || strcmp(argv[1], "–help") == 0) usageErr("%s file length [offset [alignment]]\n", argv[0]); length = getLong(argv[2], GN_ANY_BASE, "length"); offset = (argc > 3) ? getLong(argv[3], GN_ANY_BASE, "offset") : 0; alignment = (argc > 4) ? getLong(argv[4], GN_ANY_BASE, "alignment") : 4096; fd = open(argv[1], O_RDONLY | O_DIRECT); if (fd == -1) errExit("open"); /* memalign() , , . 'buf' , 'alignment', . , , , , 256 , , 512- . '(char *)' ( 'void *', memalign(). */ buf = (char *) memalign(alignment * 2, length + alignment) + alignment; if (buf == NULL) errExit("memalign"); if (lseek(fd, offset, SEEK_SET) == -1) errExit("lseek"); numRead = read(fd, buf, length); if (numRead == -1) errExit("read"); printf("Read %ld bytes\n", (long) numRead); exit(EXIT_SUCCESS); } _______________________________________________________________filebuff/direct_read.c
»
»
Conteúdo»
Trecho20% —
Linux