Archive for June, 2010

Mapeando Arquivos em Memória

26 de June de 2010

Depois de ilustrar algumas das características do Memory Manager sendo um provedor de serviços ao Cache Manager no post anterior, hoje vou demonstrar que meras aplicações User-Mode também podem utilizar tais serviços. Mapeando arquivos em memória a aplicação ganha um intervalo de endereços virtuais que contém o conteúdo do arquivo. O acesso ao conteúdo do arquivo se dá simplesmente desreferenciando um ponteiro, sem a necessidade de chamar as funções ReadFile() ou WriteFile().

Quer uma necessidade para isso? Imagine que sua aplicação precise fazer a busca por uma determinada string em um arquivo, digamos “DriverEntry”. Em um desevolvimento “arroz com feijão”, o handle do arquivo é obtido através da chamada à função CreateFile(), e um buffer recebe o conteúdo parcial do arquivo, vamos supor 200 bytes. Uma simples função de busca da API poderia fazer tal busca no buffer.

Essa solução seria perfeita se não houvesse a possibilidade de a palavra buscada cair nas extremidades do buffer tal como ilustrado abaixo.

Um algoritmo mais espertinho teria que ser utilizado para identificar o prefixo e continuar a busca na próxima leitura do arquivo.

Este é apenas um simples exemplo, mas que ilustra com clareza uma das vantagens de se mapear arquivos. Se houvesse uma simples função que recebesse o path de um arquivo e nos retornasse um ponteiro para o conteúdo dele,  a busca seria bem simples.

Arquivos mapeados em memória também podem facilitar a escrita em seu conteúdo. Escrevendo no ponteiro recebido por tal mapeamento, o Memory Manager vai se encarregar de fazer o I/O necessário para que este novo conteúdo chegue ao disco.

Uma simples função de mapeamento

Aqui vou exemplificar o uso das rotinas que mapeiam um arquivo em memória. Os comentários seguem na explicação.

/****
***     MapFileToMemory
**
**      Rotina que recebe o path de um arquivo que será
**      mapeado em memória para leitura. Um endereço é
**      retornado à rotina chamadora bem como o tamanho
**      do arquivo.
*/
 
DWORD MapFileToMemory(LPCTSTR   tzFileName,
                      LPVOID*   ppMemory,
                      LPDWORD   pdwSize)
{
    HANDLE  hFile = NULL,
            hMapping = NULL;
    DWORD   dwError = ERROR_SUCCESS;
 
    __try
    {
        __try
        {
            //-f--> Zera variáveis de saída.
            *pdwSize = NULL;
            *ppMemory = NULL;
 
            //-f--> Aqui abrimos o arquivo a ser mapeado
            hFile = CreateFile(tzFileName,
                               GENERIC_READ,
                               FILE_SHARE_READ | FILE_SHARE_DELETE,
                               NULL,
                               OPEN_EXISTING,
                               FILE_ATTRIBUTE_NORMAL,
                               NULL);
 
            //-f--> Vericamos se o arquivo foi aberto, senão
            //      o homem do saco vem e nos leva.
            if (hFile == INVALID_HANDLE_VALUE)
                RaiseException(GetLastError(),
                               0,
                               0,
                               NULL);
 
            //-f--> Embora o tamanho do arquivo não seja necessário
            //      nesta função, vamos aproveitar que temos o handle
            //      do arquivo em mãos para obter essa informação para
            //      a rotina chamadora que vai precisar dela.
            *pdwSize = GetFileSize(hFile,
                                   NULL);
 
            //-f--> Aqui criamos um mapeamento do arquivo.
            //      Em kernel seria o equivalente a se criar uma
            //      section do arquivo.
            hMapping = CreateFileMapping(hFile,
                                         NULL,
                                         PAGE_READONLY,
                                         0,
                                         0,
                                         NULL);
 
            //-f--> Prevenindo o homem do saco.
            if (!hMapping)
                RaiseException(GetLastError(),
                               0,
                               0,
                               NULL);
 
            //-f--> Aqui sim o mapeamento é feito e ganhamos o
            //      intervalo de endereços que conterá o conteúdo
            //      do arquivo.
            *ppMemory = MapViewOfFile(hMapping,
                                      FILE_MAP_READ,
                                      0,
                                      0,
                                      0);
 
            //-f--> A mesma treta do saco que já comentei.
            if (!*ppMemory)
                RaiseException(GetLastError(),
                               0,
                               0,
                               NULL);
        }
        __finally
        {
            //-f--> Aqui é onde faremos toda a faxina
            //      fechando os handles que foram abertos.
            if (hFile)
                CloseHandle(hFile);
 
            if (hMapping)
                CloseHandle(hMapping);
        }
    }
    __except(EXCEPTION_EXECUTE_HANDLER)
    {
        //-f--> Oops! Alguma coisa não saiu como foi ensaiado.
        //      Encontre um culpado e faça de conta que não é com você.
        dwError = GetExceptionCode();
    }
 
    return dwError;
}
 

Este exemplo é realmente bem simples, mas sinta-se à vontade para adicionar parâmetros que tornem essa função mais flexível e complexa.

“Fernando, mesmo que o arquivo tenha sido mapeado com succeso, você fecha o handles do arquivo e do mapeamento. Isso não deveria liberar as refências que este programa tem com o arquivo?”

Na verdade, depois de criarmos o mapeamento de arquivo utilizando a rotina CreateFileMapping() que recebe o handle do arquivo, uma referência extra já foi feita ao arquivo e assim já poderíamos fechar o handle dele se quisessemos. O mesmo acontece com a chamada da rotina MapViewOfFile(), que recebe o handle do mapeamento, e que por sua vez possui uma referência indireta ao arquivo mapeado. Ou seja, depois de tudo mapeado podemos fechar todos os handles e deixar as referências indiretas tomarem conta disso.

No próximo código fonte veremos um exemplo simples de utilização dessa função.

/****
***     _tmain
**
**      Simples utilização da função de mapeamento de arquivo.
**      Só pra não dizer que não fiz tudim tudim...
*/
 
int _tmain(int argc, _TCHAR* argv[])
{
    PBYTE   pBuffer;
    DWORD   dwError, dwSize, i;
 
    //-f--> Passa o nome do arquivo e obtém o ponteiro
    //      com seu conteúdo mapeado. Simples assim...
    dwError = MapFileToMemory(_T("C:\\Temp\\Test.txt"),
                              (LPVOID*)&pBuffer,
                              &dwSize);
 
    //-f--> Testar erro nunca é demais.
    if (dwError == ERROR_SUCCESS)
    {
        //-f--> Momentos de suspense antes de tocar o endereço.
        printf("Hit any key to access the buffer at 0x%p...\n", pBuffer);
        _getch();
 
        //-f--> Imprime cada caractere armzenado no arquivo.
        //      "Olha mamãe! Sem o ReadFile()!"
        for (i=0; i
            printf("%c", pBuffer[i]);
 
        //-f--> Aqui o mapeamento é desfeito.
        UnmapViewOfFile(pBuffer);
    }
 
    return dwError;
}
 

Ao final deste exemplo podemos observar a chamada à rotina UnmapViewOfFile(), que recebe simplesmente o ponteiro base do mapeamento do arquivo. Com essa chamada, todas as referências internas são desfeitas e o arquivo finalmente é fechado.

Testando o brinquedo

Para que possamos fazer um teste besta, crie um arquivo texto utilizando o Notepad.exe.

Rodando a aplicação de teste temos a saída como ilustrada abaixo.

Agora na câmera lenta do replay

Com o WinDbg podemos observar o exato momento em que a aplicação acessa o intervalo de endereços referente ao conteúdo do arquivo. Para isso vamos colocar um breakpoint na rotina de leitura de arquivo do driver Ntfs.sys, desta forma poderemos ver a requisição do Memóry Manager ser atendida. Para que isso aconteça, o arquivo texto não pode estar no cache do sistema, então se você já rodou a aplicação de teste ao menos uma vez, você deverá reiniciar o sistema.

Caso você ainda não tenha utilizado o WinDbg e não sabe como conectá-lo ao sistema, então leia este post para um quick start. Depois de conectar o WinDbg ao Kernel do sistema, vá até o diretório onde está a aplicação de teste e a execute, mas ainda não pressione qualquer tecla deixando-a parada como mostra a seguir:

Depois disso pressione Ctrl+Break no WinDbg para que você adquira o controle sobre o sistema depurado, que neste momento vai permanecer congelado.

Para colocar o tal breakpoint na rotina de leitura de arquivo do Ntfs.sys, precisaremos saber onde está essa rotina dentro do driver. Podemos obter essa informação utilizando a extenção !drvobj do WinDbg como exibido abaixo.

kd> !drvobj \FileSystem\Ntfs 2
Driver object (843fd650) is for:
 \FileSystem\Ntfs
DriverEntry:   828f5b75    Ntfs!GsDriverEntry
DriverStartIo: 00000000    
DriverUnload:  00000000    
AddDevice:     00000000    
 
Dispatch routines:
[00] IRP_MJ_CREATE                      8289400a    Ntfs!NtfsFsdCreate
[01] IRP_MJ_CREATE_NAMED_PIPE           8165a013    nt!IopInvalidDeviceRequest
[02] IRP_MJ_CLOSE                       82896fcf    Ntfs!NtfsFsdClose
[03] IRP_MJ_READ                        82818514    Ntfs!NtfsFsdRead
[04] IRP_MJ_WRITE                       82815638    Ntfs!NtfsFsdWrite
[05] IRP_MJ_QUERY_INFORMATION           82895a88    Ntfs!NtfsFsdDispatchWait
[06] IRP_MJ_SET_INFORMATION             8281e950    Ntfs!NtfsFsdSetInformation
[07] IRP_MJ_QUERY_EA                    82895a88    Ntfs!NtfsFsdDispatchWait
[08] IRP_MJ_SET_EA                      82895a88    Ntfs!NtfsFsdDispatchWait
[09] IRP_MJ_FLUSH_BUFFERS               82884349    Ntfs!NtfsFsdFlushBuffers
[0a] IRP_MJ_QUERY_VOLUME_INFORMATION    828b5fc6    Ntfs!NtfsFsdDispatch
[0b] IRP_MJ_SET_VOLUME_INFORMATION      828b5fc6    Ntfs!NtfsFsdDispatch
[0c] IRP_MJ_DIRECTORY_CONTROL           828b5d41    Ntfs!NtfsFsdDirectoryControl
[0d] IRP_MJ_FILE_SYSTEM_CONTROL         8289970e    Ntfs!NtfsFsdFileSystemControl
[0e] IRP_MJ_DEVICE_CONTROL              82879466    Ntfs!NtfsFsdDeviceControl
[0f] IRP_MJ_INTERNAL_DEVICE_CONTROL     8165a013    nt!IopInvalidDeviceRequest
[10] IRP_MJ_SHUTDOWN                    8282b36b    Ntfs!NtfsFsdShutdown
[11] IRP_MJ_LOCK_CONTROL                82823b7a    Ntfs!NtfsFsdLockControl
[12] IRP_MJ_CLEANUP                     828a1d42    Ntfs!NtfsFsdCleanup
[13] IRP_MJ_CREATE_MAILSLOT             8165a013    nt!IopInvalidDeviceRequest
[14] IRP_MJ_QUERY_SECURITY              828b5fc6    Ntfs!NtfsFsdDispatch
[15] IRP_MJ_SET_SECURITY                828b5fc6    Ntfs!NtfsFsdDispatch
[16] IRP_MJ_POWER                       8165a013    nt!IopInvalidDeviceRequest
[17] IRP_MJ_SYSTEM_CONTROL              8165a013    nt!IopInvalidDeviceRequest
[18] IRP_MJ_DEVICE_CHANGE               8165a013    nt!IopInvalidDeviceRequest
[19] IRP_MJ_QUERY_QUOTA                 82895a88    Ntfs!NtfsFsdDispatchWait
[1a] IRP_MJ_SET_QUOTA                   82895a88    Ntfs!NtfsFsdDispatchWait
[1b] IRP_MJ_PNP                         8286137b    Ntfs!NtfsFsdPnp
 
Fast I/O routines:
FastIoCheckIfPossible                   8288187b    Ntfs!NtfsFastIoCheckIfPossible
FastIoRead                              82880c38    Ntfs!NtfsCopyReadA
FastIoWrite                             82881f53    Ntfs!NtfsCopyWriteA
FastIoQueryBasicInfo                    82888c3a    Ntfs!NtfsFastQueryBasicInfo
FastIoQueryStandardInfo                 82888aa6    Ntfs!NtfsFastQueryStdInfo
FastIoLock                              8287bf41    Ntfs!NtfsFastLock
FastIoUnlockSingle                      8287bd75    Ntfs!NtfsFastUnlockSingle
FastIoUnlockAll                         828cd7b3    Ntfs!NtfsFastUnlockAll
FastIoUnlockAllByKey                    828cd958    Ntfs!NtfsFastUnlockAllByKey
ReleaseFileForNtCreateSection           8281e904    Ntfs!NtfsReleaseForCreateSection
FastIoQueryNetworkOpenInfo              8287ad84    Ntfs!NtfsFastQueryNetworkOpenInfo
AcquireForModWrite                      8280c892    Ntfs!NtfsAcquireFileForModWrite
MdlRead                                 828cd0d8    Ntfs!NtfsMdlReadA
MdlReadComplete                         81650af6    nt!FsRtlMdlReadCompleteDev
PrepareMdlWrite                         828cd31f    Ntfs!NtfsPrepareMdlWriteA
MdlWriteComplete                        817f5a9a    nt!FsRtlMdlWriteCompleteDev
FastIoQueryOpen                         82874d03    Ntfs!NtfsNetworkOpenCreate
AcquireForCcFlush                       8281ab35    Ntfs!NtfsAcquireFileForCcFlush
ReleaseForCcFlush                       8281aa9c    Ntfs!NtfsReleaseFileForCcFlush
 

Como você deve estar imaginando, a rotina de leitura do Ntfs é utilizada com muita frequência, o que faria este breakpoint parar muitas vezes sem ter a menor relação com o nosso teste. Para limitar o escopo do breakpoint, vamos fazer com que ele se aplique somente à thread que fará a solicicação que estamos esperando.

Como já expliquei no post anterior, quando o endereço de memória é obtido, o arquivo ainda não foi lido. Quando a aplicação desreferenciar este ponteiro buscando os dados, um page fault será gerado e o Memory Manager vai tomar o controle sobre a thread por meio de um trap de sistema. Essa é a thread que será utilizada para realizar a leitura do arquivo que vai abastecer a página de memória à pedido do Memory Manager. Esta é a razão pela qual nosso programa de teste espera uma tecla ser pressionada antes de acessar o buffer. Isso nos dá a oportunidade de obter a identificação da thread que está aguardando esse evento.

Utilizamos a extenção !process para localizar o nosso programa de teste e também listará suas threads, que em nosso caso é uma única.

kd> !process 0 2 MapFile.exe
PROCESS 84bf6d90  SessionId: 1  Cid: 0b60    Peb: 7ffdf000  ParentCid: 0b40
    DirBase: 1f09b4c0  ObjectTable: 8e77ccd0  HandleCount:   5.
    Image: MapFile.exe
 
        THREAD 84f6cb50  Cid 0b60.0b64  Teb: 7ffde000 Win32Thread: 00000000 WAIT: (WrLpcReply) ...
            84f6cd64  Semaphore Limit 0x1
 
kd> bp /1 /t 84f6cb50 Ntfs!NtfsFsdRead
kd> g
 

Depois de colocado o breakpoint, podemos liberar a execução do sistema e teclar algo na aplicação de teste. Isso fará com que nosso breakpoint interrompa o sistema bem como pretendíamos. Olhando para a pilha de chamadas que temos no momento, podemos evidenciar a execução do trap que foi gerado pela aplicação de teste. Este trap está sendo atendido pelo Memory Manager.

Breakpoint 0 hit
Ntfs!NtfsFsdRead:
82818514 6a40
 
kd> kb
ChildEBP RetAddr  Args to Child
8f395b6c 816f00c3 84438020 84f40290 84f40290 Ntfs!NtfsFsdRead
8f395b84 821a3ba7 84437730 84f40290 00000000 nt!IofCallDriver+0x63
8f395ba8 821a3d64 8f395bc8 84437730 00000000 fltmgr!FltpLegacyProcessingAfterPreCallbacksCompleted+0x251
8f395be0 816f00c3 84437730 84f40290 00000000 fltmgr!FltpDispatch+0xc2
8f395bf8 8167bf2e 84f6cb50 8443a18c 8443a158 nt!IofCallDriver+0x63
8f395c14 816b8d51 00000043 84f6cb50 8443a198 nt!IoPageRead+0x172
8f395cd0 816db03f 00020000 90825810 00000000 nt!MiDispatchFault+0xd18
8f395d4c 8168ebf4 00000000 00020000 00000001 nt!MmAccessFault+0x1fb7
8f395d4c 0018d972 00000000 00020000 00000001 nt!KiTrap0E+0xdc
0015fbc0 0018ec36 00000001 00281a28 00281a78 MapFile!wmain+0x72
0015fc0c 0018eb0f 0015fc20 77554911 7ffdf000 MapFile!__tmainCRTStartup+0x116
0015fc14 77554911 7ffdf000 0015fc60 77ace4b6 MapFile!wmainCRTStartup+0xf
0015fc20 77ace4b6 7ffdf000 77a26775 00000000 kernel32!BaseThreadInitThunk+0xe
0015fc60 77ace489 0018b532 7ffdf000 00000000 ntdll!__RtlUserThreadStart+0x23
0015fc78 00000000 0018b532 7ffdf000 00000000 ntdll!_RtlUserThreadStart+0x1b
 

Como conhecemos o protótipo que uma rotina de dispatch precisa ter, sabemos que o segundo parâmetro da rotina NtfsFsdRead() é o endereço da IRP que o driver recebeu. Utilizando a extensão !irp podemos obter detalhes sobre a IRP. Isso nos permite conhecer o FileObject ao qual está destinado essa solicitação.

kd> !irp 84f40290 
Irp is active with 8 stacks 8 is current (= 0x84f403fc)
 Mdl=8443a1d8: No System Buffer: Thread 84f6cb50:  Irp stack trace.  
     cmd  flg cl Device   File     Completion-Context
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
 [  0, 0]   0  0 00000000 00000000 00000000-00000000    
 
            Args: 00000000 00000000 00000000 00000000
>[  3, 0]   0  0 84438020 84bd0028 00000000-00000000    
           \FileSystem\Ntfs
            Args: 00001000 00000000 00000000 00000000
 

Com o endereço do FileObject em mãos, a extensão !fileobj nos mostrará mais detalhes sobre esse objeto. Assim podemos verificar que de fato o arquivo a ser lido é o nosso arquivo texto que foi mapeado.

kd> !fileobj 84bd0028 
 
\Temp\Test.txt
 
Device Object: 0x84439030   \Driver\volmgr
Vpb: 0x84436e28
Access: Read SharedRead SharedDelete 
 
Flags:  0x44042
    Synchronous IO
    Cache Supported
    Cleanup Complete
    Handle Created
 
FsContext: 0x92a4cd80    FsContext2: 0x92a4ced8
CurrentByteOffset: 0
Cache Data:
  Section Object Pointers: 84d24e74
  Shared Cache Map: 00000000
 

Sabendo que estamos no contexto da thread que fez o acesso, podemos dar uma espiadinha no endereço acessado antes do page fault ser atendido. Esse endereço foi exibido na saída da aplicação de teste antes o breakpoint interromper a execução do sistema.

kd> db 0x00020000
00020000  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020010  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020020  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020030  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020040  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020050  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020060  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
00020070  ?? ?? ?? ?? ?? ?? ?? ??-?? ?? ?? ?? ?? ?? ?? ??  ????????????????
 

“Fernando, por que aparecem sinais de interrogação? Tudo bem que o conteúdo do arquivo ainda não foi copiado para o buffer da aplicação, mas não deveríamos ver lixo ou mesmo zeros?”

Olha, essa sua pergunta foi realmente muito boa, acho que eu mesmo não poderia ter pensado em uma pergunta melhor. Na verdade a resposta para essa pergunta está vinculada àquela reposta tosca do post anterior. O que acontece é que um intervalo de endereços foi reservado para conter as páginas de memória com o conteúdo do arquivo. Como nenhum acesso ainda foi feito, o esse endereço virtual ainda não aponta para nenhuma página física de memória. Sem essa página física não se pode determinar seus dados. Podemos verificar isso utilizando a extenção !vtop do WinDbg, que faz a tradução de endereços virtuais para endereços físicos.

kd> !vtop 0 0x00020000
X86VtoP: Virt 00020000, pagedir 1f09b4c0
X86VtoP: PAE PDPE 1f09b4c0 - 000000001876d801
X86VtoP: PAE PDE 1876d000 - 0000000018908867
X86VtoP: PAE PTE 18908100 - ffffffff00000420
X86VtoP: Virt ffffffff, pagedir 1f09b4c0
X86VtoP: PAE PDPE 1f09b4d8 - 00000000187b0801
X86VtoP: PAE PDE 187b0ff8 - 0000000000128063
X86VtoP: PAE PTE 128ff8 - 0000000000000000
X86VtoP: PAE zero PTE
Virtual address 20000 translation fails, error 0x8007001E.
 
kd> !error 0x8007001E
Error code: (HRESULT) 0x8007001e (2147942430) - The system cannot read from the specified device.
 

A tentativa de tradução desse endereço virtual resulta em um erro. Vamos tentar fazer essa tradução novamente depois que o page fault for atendito. Vamos liberar a execução do sistema até o endereço de retorno para a rotina MmAccessFault(). Este endereço foi obtido na pilha de chamadas da thread e foi destacado nos resultados do comando kd já ilustrado acima.

kd> ga 8168ebf4 
 
nt!KiTrap0E+0xdc:
8168ebf4 85c0            test    eax,eax
 
kd> !vtop 0 0x00020000
X86VtoP: Virt 00020000, pagedir 1f09b4c0
X86VtoP: PAE PDPE 1f09b4c0 - 000000001876d801
X86VtoP: PAE PDE 1876d000 - 0000000018908867
X86VtoP: PAE PTE 18908100 - 8000000018844025
X86VtoP: PAE Mapped phys 18844000
Virtual address 20000 translates to physical address 18844000.
 

Aqui o page fault já foi antendido e o controle será devolvido à aplicação. Neste ponto o Memory Manager realizou as tarefas necessárias para que esse endereço virtual agora pudesse ser traduzido para uma página física. Repetindo a mesma tentativa de tradução que falhou anteriormente, teremos a seguinte saída.

kd> db 0x00020000
00020000  54 65 73 74 65 20 64 65-20 6d 61 70 65 61 6d 65  Teste de mapeame
00020010  6e 74 6f 20 64 65 20 61-72 71 75 69 76 6f 2e 2e  nto de arquivo..
00020020  2e 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00020030  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00020040  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00020050  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00020060  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
00020070  00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................

Liberando a execução do sistema, a aplicação fará o acesso ao buffer e teremos a mesma saída exibida anteriormente. Vale lembrar que para repetir essa experiência, é necessário reiniciar o sistema, pois o conteúdo do arquivo texto agora está no cache do sistema. Isso significa que o page fault não ocorrerá novamente até que esta página seja descartada pelo Cache Manager. Tal evento depende de muitos fatores e pode não acontecer até que a máquina desligue.

Enfim, este post além de trazer uma simples função de mapeamento de arquivo, também traz o mesmo blá-blá-blá técnico de sempre. Espero que tenham gostado.
Até mais! 😉

MapFile.zip

Ponteiro perdido no Kernel pode corromper arquivos?

9 de June de 2010

A nova versão de um driver pode implementar aquela funcionalidade nova que você tanto esperava. Afinal de contas, o time de desenvolvimento de drivers anda sempre muito ocupado, e para conseguir alguma coisa nova é sempre um parto. O único problema é que de vez em outra uma tela azul acontece. Os mais desesperados podem até querer usar o novo driver a qualquer custo, mesmo que uma telinha azul apareça com uma frequência aceitável. Aí surge a perguntinha:

Fernando, existe algum problema se eu for usando esse driver novo até que uma correção para essa tela azul saia?

O principal problema aqui é que não temos a menor idéia do que está causando a tela azul. Esse tipo de classificação (sem a menor idéia) inclui um ponteiro que pode sair escrevendo onde não se deve. Além de sair atropelando estruturas vitais do sistema operacional, esse ponteiro bomba pode também corromper arquivos. Então, mais uma perguntinha:

Mas Fernando, esse driver nem faz manipulação de arquivos. Como é que um ponteiro perdido pode abrir um arquivo e ainda gerar uma escrita em disco para corrompê-lo?

Na verdade, isso não é assim tão difícil. Você já ouviu falar do Cache Manager?

Quem é esse tal de Cache Manager?

Vi uma definição simples de Cache Manager em uma palestra do Plugfest. Vamos ver se consigo reproduzí-la aqui. Vocês desenvolvedores provavelmente já fizeram cache do conteúdo de algum arquivo em uma aplicação para não ter que acessar o tal arquivo toda vez que precisar da informação contida nele, certo? Errado? Tá tá, então do começo hoje.

Lembra lá no prézinho quando a tia ensinou que se você fizer acesso com frequência a um arquivo, você pode manter uma cópia dele em memória e evitar de fazer tanto I/O para ganhar performance? Nesse caso uma área de memória, também conhecida como cache, é carregada com o conteúdo do arquivo. Depois disso, os vários acessos de leitura ao arquivo são substituídos por leituras no cache. Quando uma escrita acontece, o cache é atualizado e a escrita também vai para o arquivo. Em casos onde a escrita é frequente, o cache é atualizado a cada escrita enquanto o arquivo recebe várias modificações de uma vez em intervalos definidos.

O cache implementado na aplicação perde o sentido se um determinado arquivo é compartilhado por mais de uma aplicação, o conteúdo do cache da aplicação “A” tem que ser o mesmo do cache da aplicação “B”, caso contrário, as alterações feitas pela aplicação “A” não seriam vistas pela aplicação “B” e vice versa, sem falar que as atualizações seriam perdidas sem o correto sincronismo que isso exigiria.

Por essa razão é que existe um cache centralizado no sistema. Um módulo no Kernel que mantém páginas de memória contendo o conteúdo dos arquivos recentemente manipulados. Quando um arquivo é aberto, ele é registrado pelo seu respectivo driver de File System no Cache Manager. Mas o Cache Manager não faz tudo sozinho. Na verdade ele faz parte de uma gangue nas “quebrada” que garante a otimização de acessos a arquivos no sistema. Para isso o Cache Manager conta com a ajuda de seus fiéis companheiros, o Vitual Memory Manager e os File System Drivers.

Pulando algumas toneladas de detalhes, digamos que quando uma solicitação de leitura chega a um driver de file system, este a encaminha ao Cache Manager, este então vai satisfazer tal operação apenas copiando o conteúdo desejado do arquivo que já estaria em páginas de memória. Copiar os dados da memória é muito mais rápido que fazer todo o ritual para obter os mesmos dados do disco, mas para isso os dados já deveriam estar carregados na memória.

Fernando, o Cache Manager coloca todo o arquivo na memória?

Quer uma resposta tosca? Sim e Não ;-). O Cache Manager na verdade mapeia o arquivo aberto em memória, e para isso ele conta com as características mais básicas de memória virtual discutidas neste outro post. Um intervalo de endereços é reservado no sistema, mas tais endereços ainda não se refletem em espaços nos chips de memória, ou seja, existe um endereço de memória, mas seu contetúdo ainda está em disco. O Memory Manager protege esses endereços contra acessos que podem ocorrem a eles. Quando um acesso de leitura é realizado nestes endereços, um page fault ocorre e o Memory Manager então precisa recuperar os dados do arquivo que estão no disco e colocá-os em memória. Para fazer isso o Memory Manager vai criar uma solicitação de leitura no I/O Manager para que uma IRP possa ser entregue ao respectivo driver de file system do arquivo em questão.

Pára tudo “perlamor” de Deus! Fernando, você disse logo alí em cima que quando uma solicitação de leitura chega ao driver de file system, esta é encaminhada ao Cache Manager que vai copiar os dados já contidos na memória a fim de antender a solicitação. Mas agora você está dizendo que para carregar tais páginas de memória o Cache Manager troca uma idéia com o Memory Manager, que por sua vêz vai criar uma solicitação de leitura para os drivers de file system. Isso não lhe parece um pouco recursivo?

Eu diria completamente recursivo, mas lembre-se que isso vai ocorrer apenas quando o arquivo ainda não foi lido por nenhum processo, e portanto ainda não está no cache do sistema. Para diferenciar uma solicitação da outra, drivers de file system precisam verificar a flag IRP_NOCACHE nas solicitações que recebem. Quando as solicitações vêm de uma aplicação, estas não carregam a flag IRP_NOCACHE, e desta forma podem ser atendidas pelo Cache Manager, por outro lado quando o Memory Manager precisa suprir as páginas de memória do Cache Manager, tais solicitações precisam ignorar o conteúdo do cache, e por isso carregam a flag IRP_NOCACHE. Paga facilitar o entendimento de toda essa  máquina, observem os passos enumerados de uma leitura de arquivo que ainda não está no cache.

  1. Uma aplicação faz uma solicitação de leitura de um arquivo.
  2. I/O Manager cria uma IRP e a encaminha ao seu respectivo driver de file system.
  3. O driver de file system verifica a ausência da flag IRP_NOCACHE e solicita a cópia dos dados desse arquivo do cache para o buffer da aplicação.
  4. O Cache Manager tenta fazer a cópia acessando as páginas que foram mapeadas do arquivo. Com isso um page fault é gerado por esse acesso e é atendido pelo Memory Manager.
  5. O Memory Manager cria uma nova IRP para atender a necessidade de abastecer o Cache Manager. Essa solicitação é encaminhada recursivamente ao driver de file system.
  6. Dessa vez o driver verifica a presença da flag IRP_NOCACHE e então cria as solicitações que serão atendidas pelos drivers de disco ou de rede.
  7. As solicitações de leitura de mídia são atendidas.
  8. As páginas de memória são abastecidas e o page fault é satisteito.
  9. Memory Manager re-executa a tentativa de leitura do Cache Manager que gerou o page fault, mas desta vez a o acesso de leitura será bem sucedido, pois os dados agora estão no endereço de memória mapeado.
  10. O Cache Manager completa a cópia dos dados para o buffer da aplicação.
  11. O driver de file system completa a solicitação de leitura.
  12. Os dados são retornados para a aplicação que fez a solicitação inicial.

Atenção agora meninos e meninas: A sequência descrita acima ilustra o caso onde o Cache Manager ainda precisa carregar o arquivo em memória. As próximas tentativas de leituras são satisfeitas diretamente pelo Cache Manager, que não vai gerar um page fault. Não vão me matar de vergonha dizendo por aí que o sistema operacional sempre faz toda a sequência para cada leitura de arquivo.

Mais uma vez as regras básicas de memória virtual são aplicadas aqui para que conforme as páginas de memória vão deixando de ser acessadas com tanta frequência, elas perdem lugar nos chips de memória, e assim, se mais tarde forem acessadas novamente, um novo page fault será gerado.

Interessante ver como esses componentes, o I/O Manager, Cache Manager, Virtual Memory Manager, File System Drivers, sem falar dos filtros que ainda podem existir, todos trabalhando juntos como caixas pretas, cada um com seu papel e sem conhecer o funcionamento interno do outro, interagindo entre si apenas através de suas interfaces públicas. Óbviamente que para a felicidade de alguns e talvêz tristeza de outros, não coloquei todos os detalhes aqui, mas podem ser encontrados no conhecido livro da galinha preta.

Mas voltando ao assunto…

De maneira análoga, as escritas também utilizam essa mesma mecânica que envolve mapeamento de arquivos. O simples fato de escrever no intervalo de endereços que é mantido pelo Cache Manager vai fazer com que tal página seja marcada como modificada, e mais tarde o Memory Manager vai querer atualizar essa página em disco. Dessa forma podemos resumir que solicitações de escrita chegam aos drivers de File System e são encaminhadas ao Cache Manager, que vai simplesmente escrever nas páginas referentes ao conteúdo do arquivo e completar a solicitação. Page faults e threads de sistema vão se encarregar de atualizar o que for preciso no momento mais adequado. O importante a notar aqui é pensarmos no Cache Manager como um simples consumidor dos serviços do Memory Manager, tudo que ele precisa fazer é ler ou escrever em páginas de memória, e é aqui que o título do post começa a fazer sentido.

Nada impede um ponteiro retardado de escrever em páginas de memória que fazem referência ao conteúdo de arquivos. Se isso acontece, o restante do sistema vai se encarregar de atualizar as barbaridades desse ponteiro em disco corrompendo o arquivo. É fácil notar que nem é necessário tantos passos para isso acontecer.

  1. Um driver inexperiente come aquele pedaço de pizza que ficou esquecido no micro-ondas e fica bem loco. Depois de dar vexame, falar o que não devia, chorar e dizer que te considera pra caramba, o driver escreve em páginas de memória referente a um arquivo de dados. Tipo um daqueles do SQL que eu nem imagino a extensão.
  2. O coitado do Memory Manager faz seu trabalho para garantir o leitinho das crianças como se nada de errado tivesse acontecido.
  3. O driver de file system vai de embalo e consolida a completa falta de noção do driver, que numa hora dessas já está abraçado com o vaso sanitário.
  4. Esse passo não é ilustrado na sequência acima mas pode ser explicado nesse site.

Nessa hora você vai torcer para que seu driver novinho em folha escreva sobre alguma estrutura vital do sistema para que uma tela azul possa conter a atividade desse inconsequente. Por esse motivo é que o sistema está cheio de testes e verificações para garantir que os dados do usuário não sejam perdidos. Melhor ver uma tela azul do que ter consequências muito piores. Lembre-se do sábio Morphy:

Nada é tão ruim que não possa ser piorado

Ainda existem muitas outras características interessantes sobre o Cache Manager que eu gostaria de descrever aqui, como por exemplo a “Falha de escrita retardada”, mas esse post já está ficando muito grande.


Até mais… 😉