Quando devo usar o mmap para acessar arquivos?

276

Os ambientes POSIX fornecem pelo menos duas maneiras de acessar arquivos. Há o padrão de chamadas de sistema open(), read(), write(), e amigos, mas também há a opção de usar mmap()para mapear o arquivo para a memória virtual.

Quando é preferível usar um sobre o outro? Quais são as vantagens individuais que merecem, incluindo duas interfaces?

Peter Burns
fonte
16
Veja também mmap () vs. blocos de leitura e este post de Linus Torvalds mencionado em uma das respostas lá.
MvG

Respostas:

299

mmapé ótimo se você tiver vários processos acessando dados somente leitura do mesmo arquivo, o que é comum no tipo de sistema de servidor que escrevo. mmappermite que todos esses processos compartilhem as mesmas páginas de memória física, economizando muita memória.

mmaptambém permite que o sistema operacional otimize as operações de paginação. Por exemplo, considere dois programas; programa Aque lê um 1MBarquivo em um buffer criando com malloce programa B com mmapso arquivo de 1 MB na memória. Se o sistema operacional precisar trocar parte da Amemória, ele deverá gravar o conteúdo do buffer para trocar antes de poder reutilizar a memória. No Bcaso, todas mmapas páginas d não modificadas podem ser reutilizadas imediatamente, porque o SO sabe como restaurá-las a partir do arquivo existente de onde elas foram mmap. (O sistema operacional pode detectar quais páginas não são modificadas, marcando inicialmente mmapas páginas graváveis como somente leitura e detectando falhas seg , semelhante à estratégia Copiar na gravação ).

mmaptambém é útil para comunicação entre processos . Você pode mmapum arquivo como leitura / gravação nos processos que precisam se comunicar e, em seguida, usar primitivas de sincronização na mmap'dregião (é para isso que serve o MAP_HASSEMAPHOREsinalizador).

Um lugar que mmappode ser estranho é se você precisar trabalhar com arquivos muito grandes em uma máquina de 32 bits. Isso ocorre porque mmapé necessário encontrar um bloco de endereços contíguo no espaço de endereço do processo que seja grande o suficiente para caber em todo o intervalo do arquivo que está sendo mapeado. Isso pode se tornar um problema se o seu espaço de endereço ficar fragmentado, onde você poderá ter 2 GB de espaço livre, mas nenhum intervalo individual poderá caber em um mapeamento de arquivo de 1 GB. Nesse caso, talvez seja necessário mapear o arquivo em pedaços menores do que você gostaria de ajustá-lo.

Outro constrangimento potencial mmapem substituição à leitura / gravação é que você precisa iniciar o mapeamento com desvios do tamanho da página. Se você quiser apenas obter alguns dados em offset X, precisará corrigi-los para que sejam compatíveis mmap.

E, finalmente, de leitura / gravação são a única maneira que você pode trabalhar com alguns tipos de arquivos. mmapnão pode ser usado em coisas como tubos e ttys .

Don Neufeld
fonte
10
Você pode usar o mmap () em arquivos que estão crescendo? Ou o tamanho foi corrigido no momento em que você aloca a memória / arquivo mmap ()?
Jonathan Leffler
29
Quando você faz a chamada mmap, precisa especificar um tamanho. Portanto, se você quiser fazer algo como uma operação de cauda, ​​não é muito adequado.
Don Neufeld
5
Afaik MAP_HASSEMAPHOREé específico para o BSD.
Patrick Schlüter
6
@JonathanLeffler Certamente você pode usar mmap () em arquivos que estão crescendo, mas é necessário chamar mmap () novamente com o novo tamanho quando o arquivo atingir o limite do espaço que você alocou inicialmente. O PosixMmapFile do LevelDB fornece um bom exemplo. Mas ele parou de usar o mmap da 1,15. Você pode obter a versão antiga do Github
baotiao 4/15
4
O mmap também pode ser útil caso um arquivo precise ser processado em várias passagens: o custo da alocação de páginas de memória virtual é pago apenas uma vez.
Jib
69

Uma área em que achei o mmap () não ser uma vantagem foi ao ler arquivos pequenos (abaixo de 16K). A sobrecarga da página que falhou ao ler o arquivo inteiro foi muito alta em comparação com apenas uma chamada de sistema read (). Isso ocorre porque o kernel às vezes pode satisfazer uma leitura inteiramente em seu intervalo de tempo, o que significa que seu código não muda. Com uma falha de página, parecia mais provável que outro programa fosse agendado, tornando a operação do arquivo com uma latência mais alta.

Ben Combee
fonte
4
+1 posso confirmar isso. Para arquivos pequenos, é mais rápido obter mallocum pedaço de memória e colocar 1 readnele. Isso permite ter o mesmo código que manipula os mapas de memória manipulados em malloc'ed.
Patrick Schlüter
35
Dito isto, sua justificativa para isso não está certa. O planejador não tem nada a ver com a diferença. A diferença vem dos acessos de gravação para as tabelas de páginas, que é uma estrutura global do kernel que mantém os processos que armazenam qual página de memória e seus direitos de acesso. Essa operação pode ser muito dispendiosa (pode invalidar linhas de cache, pode TLB ausente, a tabela é global e, portanto, deve ser protegida contra acesso simultâneo, etc.). Você precisa de um determinado tamanho de mapa para que a sobrecarga de readacessos seja maior que a sobrecarga da manipulação de memória virtual.
Patrick Schlüter
1
@ PatrickSchlüter Ok, entendo que existe uma sobrecarga no início do mmap () que envolve a modificação da tabela de páginas. Digamos que mapeamos 16K de um arquivo para a memória. Para um tamanho de página de 4K, mmapé necessário atualizar 4 entradas na tabela de páginas. Mas usar readpara copiar em um buffer de 16K também envolve a atualização de entradas da tabela de 4 páginas, sem mencionar que ele precisa copiar o 16K no espaço de endereço do usuário. Então, você poderia elaborar as diferenças de operações na tabela de páginas e como é mais caro mmap?
flow2k
45

mmaptem a vantagem quando você tem acesso aleatório a arquivos grandes. Outra vantagem é que você o acessa com operações de memória (memcpy, ponteiro aritmético), sem se preocupar com o buffer. Às vezes, a E / S normal pode ser bastante difícil ao usar buffers quando você possui estruturas maiores que o seu buffer. O código para lidar com que muitas vezes é difícil de acertar, o mmap é geralmente mais fácil. Dito isto, existem algumas armadilhas ao trabalhar com mmap. Como as pessoas já mencionaram, mmapé muito caro configurar, portanto vale a pena usá-lo apenas para um determinado tamanho (variando de máquina para máquina).

Para acessos seqüenciais puros ao arquivo, nem sempre é a melhor solução, embora uma chamada apropriada madvisepossa atenuar o problema.

Você precisa ter cuidado com as restrições de alinhamento de sua arquitetura (SPARC, itanium). Com as E / S de leitura / gravação, os buffers costumam estar alinhados adequadamente e não se interceptam ao remover a referência de um ponteiro fundido.

Você também deve ter cuidado para não acessar fora do mapa. Isso pode acontecer facilmente se você usar funções de seqüência de caracteres no seu mapa e seu arquivo não contiver um \ 0 no final. Ele funcionará na maioria das vezes quando o tamanho do arquivo não for múltiplo do tamanho da página, pois a última página será preenchida com 0 (a área mapeada sempre terá o tamanho de um múltiplo do tamanho da página).

Patrick Schlüter
fonte
30

Além de outras respostas legais, uma citação da programação do sistema Linux, escrita pelo especialista do Google, Robert Love:

Vantagens de mmap( )

A manipulação de arquivos via mmap( )tem várias vantagens em relação às chamadas padrão read( )e do write( )sistema. Entre eles estão:

  • Ler e gravar em um arquivo mapeado na memória evita a cópia estranha que ocorre ao usar as chamadas read( )ou write( )system, onde os dados devem ser copiados para e de um buffer de espaço do usuário.

  • Além de possíveis falhas de página, a leitura e a gravação em um arquivo mapeado na memória não incorrem em nenhuma chamada do sistema ou sobrecarga de alternância de contexto. É tão simples quanto acessar a memória.

  • Quando vários processos mapeiam o mesmo objeto na memória, os dados são compartilhados entre todos os processos. Os mapeamentos graváveis ​​somente leitura e compartilhados são compartilhados por inteiro; os mapeamentos graváveis ​​privados têm suas páginas ainda não COW (copy-on-write) compartilhadas.

  • Procurar em torno do mapeamento envolve manipulações triviais de ponteiros. Não há necessidade de lseek( )chamada do sistema.

Por esses motivos, mmap( )é uma escolha inteligente para muitos aplicativos.

Desvantagens de mmap( )

Há alguns pontos a serem lembrados ao usar mmap( ):

  • Os mapeamentos de memória são sempre um número inteiro de páginas. Portanto, a diferença entre o tamanho do arquivo de backup e um número inteiro de páginas é "desperdiçada" como espaço livre. Para arquivos pequenos, uma porcentagem significativa do mapeamento pode ser desperdiçada. Por exemplo, com páginas de 4 KB, um mapeamento de 7 bytes desperdiça 4.089 bytes.

  • Os mapeamentos de memória devem caber no espaço de endereço do processo. Com um espaço de endereço de 32 bits, um número muito grande de mapeamentos de vários tamanhos pode resultar na fragmentação do espaço de endereço, dificultando a localização de grandes regiões contíguas livres. Esse problema, é claro, é muito menos aparente com um espaço de endereço de 64 bits.

  • Existe uma sobrecarga na criação e manutenção dos mapeamentos de memória e estruturas de dados associadas dentro do kernel. Essa sobrecarga geralmente é evitada pela eliminação da cópia dupla mencionada na seção anterior, principalmente para arquivos maiores e acessados ​​com frequência.

Por esses motivos, os benefícios mmap( )são percebidos muito quando o arquivo mapeado é grande (e, portanto, qualquer espaço desperdiçado é uma pequena porcentagem do mapeamento total) ou quando o tamanho total do arquivo mapeado é igualmente divisível pelo tamanho da página ( e, portanto, não há espaço desperdiçado).

Miljen Mikic
fonte
13

O mapeamento de memória tem um potencial para uma enorme vantagem de velocidade em comparação com as E / S tradicionais. Ele permite que o sistema operacional leia os dados do arquivo de origem à medida que as páginas do arquivo mapeado na memória são tocadas. Isso funciona criando páginas com falha, que o sistema operacional detecta e, em seguida, o sistema operacional carrega os dados correspondentes do arquivo automaticamente.

Isso funciona da mesma maneira que o mecanismo de paginação e geralmente é otimizado para E / S de alta velocidade, lendo dados nos limites e tamanhos das páginas do sistema (geralmente 4K) - um tamanho para o qual a maioria dos caches do sistema de arquivos é otimizada.

AndyG
fonte
15
Observe que mmap () nem sempre é mais rápido que read (). Para leituras seqüenciais, o mmap () não oferece vantagens mensuráveis ​​- isso se baseia em evidências empíricas e teóricas. Se você não acredita em mim, escreva seu próprio teste.
Tim Cooper
1
Posso fornecer números provenientes do nosso projeto, uma espécie de índice de texto para um banco de dados de frases. O índice tem vários Gigabytes e as chaves são mantidas em uma árvore ternária. O índice ainda está crescendo paralelamente ao acesso de leitura, o acesso fora das partes mapeadas é feito via pread. No Solaris 9 Sparc (V890), o acesso ao cabeçote é entre 2 e 3 vezes mais lento que o memcpydo mmap. Mas você está certo de que o acesso seqüencial não é necessariamente mais rápido.
Patrick Schlüter
19
Só um pouquinho. Não funciona como o mecanismo de paginação, é o mecanismo de paginação. Mapear um arquivo está atribuindo uma área de memória a um arquivo em vez do arquivo de troca anônimo.
Patrick Schlüter
2

Uma vantagem que ainda não está listada é a capacidade de mmap()manter um mapeamento somente leitura como páginas limpas . Se alguém alocar um buffer no espaço de endereço do processo e usá-lo read()para preenchê-lo de um arquivo, as páginas de memória correspondentes a esse buffer estarão sujas desde que foram gravadas.

Páginas sujas não podem ser descartadas da RAM pelo kernel. Se houver espaço de troca, eles poderão ser paginados para troca. Mas isso é caro e em alguns sistemas, como pequenos dispositivos incorporados com apenas memória flash, não há troca. Nesse caso, o buffer ficará preso na RAM até o processo terminar, ou talvez devolva-o madvise().

As mmap()páginas não gravadas estão limpas. Se o kernel precisar de RAM, ele pode simplesmente descartá-los e usar a RAM em que as páginas estavam. Se o processo que teve o mapeamento acessá-lo novamente, isso causa uma falha na página. O kernel recarrega novamente as páginas do arquivo de onde vieram originalmente . Da mesma forma que eles foram preenchidos em primeiro lugar.

Isso não requer mais de um processo usando o arquivo mapeado para ser uma vantagem.

TrentP
fonte
O kernel não pode soltar uma página mmap 'suja' escrevendo seu conteúdo primeiro no arquivo subjacente?
Jeremy Friesner
2
Ao usar read(), as páginas em que os dados são colocados eventualmente não têm relação com o arquivo do qual podem ter vindo. Portanto, eles não podem ser escritos, exceto para trocar de espaço. Se um arquivo é mmap()ede o mapeamento é gravável (em vez de somente leitura) e gravado em, isso depende se o mapeamento foi MAP_SHAREDou MAP_PRIVATE. Um mapeamento compartilhado pode / deve ser gravado no arquivo, mas um particular não pode ser.
TrentP