Já escrevi um post que descreve os passos de como criar um driver bem simples. Este driver simplesmente mantém uma lista de strings que recebe via IRP’s de escrita e as devolve nas IRP’s de leitura. Muito bem, se você ainda não sabe o que é uma IRP, um outro post tenta explicar o que são as IRP’s e até descreve os passos necessários para utilizar o IRP Tracker para observar as IRP’s indo e vindo. Se fizermos um pequeno teste utilizando o IRP Tracker sobre o driver de exemplo que acabei de comentar, teremos uma saída semelhante a figura abaixo.
Podemos ver as todas as IRP’s que nosso device recebeu desde a IRP_MJ_CREATE até a IRP_MJ_CLOSE. Entre estas IRP’s notamos que algumas delas não foram completadas com sucesso. Como nenhuma rotina foi designada para tratar IRP_MJ_CLEANUP, estas IRP’s são terminam com STATUS_INVALID_DEVICE_REQUEST. Neste post vou comentar um pouco sobre a esta IRP e como seu device interage com os processos que obtiveram um handle para ele.
Para ver detalhes da IRP, dê um duplo clique sobre a linha da IRP no IRP Tracker para que a janela abaixo seja exibida. Nessa janela é possível ver, entre outros detalhes, o FileObject utilizado na IRP. Isso nos será útil neste post durante os testes.
Quando o IRP_MJ_CLEANUP ocorre?
O Object Manager envia uma IRP de CLEANUP a fim de notificar o driver de que o último handle para determinado FileObject foi fechado. Como alguns de vocês devem saber, quando uma aplicação usa o CreateFile para obter um handle para nosso device, isso resulta na criação de um FileObject. As operações subseqüentes utilizando este handle estarão vinculadas a este FileObject. Mais detalhes sobre isso neste post.
Um FileObject não tem uma relação direta com um handle. Um handle pode ser duplicado ou mesmo herdado do processo pai na criação de um novo processo. O resultado destas ações é ter vários handles que serão traduzidos para o mesmo FileObject. Desta forma, nem sempre quando uma aplicação chama a função CloseHandle, uma IRP de CLEANUP ou CLOSE é enviada ao driver.
Para acompanhar os passos abaixo, o código fonte do programa de exemplo está disponível para download ao final deste post. Este programa utiliza o driver de exemplo construído em outro post, o código fonte do driver pode ser baixado daqui. A partir dos fontes você pode compilar e instalar o driver de teste. Se você não sabe como fazer isso, este post pode te ajudar. Depois de instalar o driver de teste, seria interessante poder executar as chamadas abaixo com a ajuda de um depurador, e assim, poder observar os resultados no IRP Tracker a cada linha executada.
É importante ler os comentários do fonte abaixo. Fiquei com preguiça de duplicar essas informações aqui no post.
/****
*** main
**
** Espero que todos saibam que este é o ponto
** de entrada de uma aplicação. Caso contrário,
** você pode estar se preciptando em ler um blog
** de driver para Windows.
*/
int main(int argc, char* argv[])
{
HANDLE h1, h3, h3;
CHAR szBuffer[100];
DWORD dwBytes;
//-f--> Aqui estamos abrindo o primeiro handle
// para o nosso device. Este é o passo 1
// no IRP Tracker. Verifique o valor do
// FileObject para poder comparar em operações
// futuras.
h1 = CreateFile("\\\\.\\EchoDevice",
GENERIC_ALL,
0,
NULL,
OPEN_EXISTING,
0,
NULL);
//-f--> Aqui estamos abrindo o segundo handle
// para o nosso device. Este é o passo 2
// no IRP Tracker. Verifique o valor do
// FileObject para poder comparar em operações
// futuras.
h3 = CreateFile("\\\\.\\EchoDevice",
GENERIC_ALL,
0,
NULL,
OPEN_EXISTING,
0,
NULL);
//-f--> Aqui lançaremos uma IRP_MJ_READ para o
// primeiro FileObject obtido. Passo 3 no
// IRP Tracker. Note que o primeiro FileObject
// é utilizado nesta IRP.
ReadFile(h1,
szBuffer,
sizeof(szBuffer),
&dwBytes,
NULL);
//-f--> Aqui lançaremos uma IRP_MJ_READ para o
// segundo FileObject obtido. Passo 4 no
// IRP Tracker. Note que o segundo FileObject
// é utilizado nesta IRP.
ReadFile(h3,
szBuffer,
sizeof(szBuffer),
&dwBytes,
NULL);
//-f--> A duplicação de handle não é notificada
// ao driver. Apenas o Object Manager sabe
// disso. Não existe nenhum passo correspondente
// no IRP Tracker.
DuplicateHandle(GetCurrentProcess(),
h1,
GetCurrentProcess(),
&h3,
0,
FALSE,
DUPLICATE_SAME_ACCESS);
//-f--> Como o terceiro handle foi obtido atravéz de
// uma duplicação do primeiro, o FileObject
// correspondente é o mesmo do primeiro handle.
// Passo 5 no IRP Tracker. Note que o primeiro
// FileObject é utilizado nesta IRP.
ReadFile(h3,
szBuffer,
sizeof(szBuffer),
&dwBytes,
NULL);
//-f--> Como temos dois handles para o primeiro FileObject,
// fechar um deles não implicará em nenhuma notificação
// ao nosso device. Não existe passo correspondente no
// IRP Tracker.
CloseHandle(h1);
//-f--> Este handle não foi duplicado, assim, quando o fecharmos,
// uma IRP_MJ_CLEANUP seguido de uma IRP_MJ_CLOSE serão
// enviadas ao driver. Passo 6 no IRP Tracker.
CloseHandle(h3);
//-f--> Fechando este handle, teremos o mesmo comportamento
// observado no fechamento do handle h3. Do ponto de
// vista do driver, o primeiro FileObject será destruído
// agora. Passo 7 no IRP Tracker.
CloseHandle(h3);
//-f--> E todos viveram felizes para sempre.
return 0;
}
Obter um handle para um objeto nos assegura que este objeto será válido enquanto não fecharmos o handle. Um objeto só pode ser destruído pelo sistema depois que todos os handles para ele forem fechados. Para isso, o Object Manager mantém dois contadores, o ProcessHandleCount e o SystemHandleCount. O primeiro deles mantém a quantidade de handles abertos para um objeto em um determinado processo. O outro mantém a soma de todos os ProcessHandleCount para o objeto no sistema. Estes contadores são decrementados à medida que os handles são fechados. Quando chegam a zero, uma IRP_MJ_CLEANUP é gerada.
Normalmente a IRP_MJ_CLEANUP nos serve como evento para cancelar qualquer operação assincrona relativa ao FileObject que está sendo finalizado. IRP’s são ligadas às threads que às lançaram e qualquer IRP assincrona deveria ser devidamente cancelada neste momento.
Então para quê serve o IRP_MJ_CLOSE?
Além dos contadores de referência acima citados, existe também o ObjectReferenceCount, que além de ser incrementado quando um novo handle é obtido, também é incrementado quando uma referência de kernel é feita utilizando funções do tipo ObReferenceObject por exemplo. Estas chamadas incrementam o ObjectReferenceCount sem que um novo handle seja gerado. Para quem conhece COM, esta chamada tem o comportamento semelhante ao AddRef. Isso permite que o objeto se mantenha válido para o kernel, mesmo depois que todos os handles para ele foram destruídos. Enfim, quando este contador chegar a zero, então a IRP_MJ_CLOSE é enviada.
Um driver pode associar estruturas de dados a um determinado FileObject utilizando os pointeiros FsContext e FsContext2 conforme eu já comentei neste outro post. Estas estruturas só poderão ser desalocadas quando recebermos o IRP_MJ_CLOSE.
Operações após o IRP_MJ_CLEANUP
Quando recebemos uma IRP_MJ_CLEANUP, não significa que o fim está próximo. Outros componentes do kernel podem lançar novas IRP’s de leitura ou escrita mesmo depois deste evento. Sem falar das IRP_MJ_INTERNAL_DEVICE_CONTROL que podem ser trocadas entre drivers.
Um cenário muito comum no desenvolvimento de File Systems é justamente quando uma refêrencia de um FileObject que é mantida pelo Cache Manager. Mesmo quando todos os handles forem fechados pelas aplicações, o sistema ainda retém esta refêrencia antecipando que alguma aplicação pode querer abrir o mesmo arquivo novamente. O Cache Manager possui system threads que realizam a então chamada “Escrita retardada”. Este recurso concentra várias escritas a um arquivo em uma única operação posterior a fim de diminuir o número de acessos a disco, assim ganhando performance. É muito comum tais escritas serem realizadas depois que o arquivo teve todos os seus handles fechados por aplicações, e assim temos escritas depois do IRP_MJ_CLEANUP.
No final de tudo isso, espero mais uma vez ter mais ajudado que atrapalhado. Assuntos referentes a Cache Manager, Memory Manager e File Systems não são muito triviais, mas são interessantes o suficiente para ler a respeito e entender o que o sistema faz antes de falar que tudo é uma porcaria.
Até a próxima!
TestCleanup.zip