9 完成读操作
除非是一个完整的文件系统,完成读操作似乎是不必要的。过滤驱动一般只需要把请求交给下层的实际文件系统来完成。但是有时候比如加解密操作,我希望从下层读到数据,解密后,我自己来完成这一IRP请求。
这里要谈到IRP的minor function code.以前已经讨论到如果major function code 是IRP_MJ_READ则是Read请求。实际上有些主功能号下面有一些子功能号,如果是IRP_MJ_READ,检查其MINOR,应该有几种情况: IRP_MN_NORMAL,IRP_MN_MDL,IRP_MN_MDL|IRP_COMPLETE(这个其实就是 IRP_MN_MDL_COMPLETE).还有其他几种情况,资料上有解释,但是我没自己调试过,也就不胡说了。只拿自己调试过的几种情况来说说。
先写个函数,得到次功能号。
_inline wd_uchar wd_irpsp_minor(wd_io_stack *irpsp)
{
return irpsp->MinorFunction;
}
enum {
wd_mn_mdl = IRP_MN_MDL,
wd_mn_mdl_comp = IRP_MN_MDL_COMPLETE,
wd_mn_normal = IRP_MN_NORMAL
};
希望你喜欢我重新定义过的次功能号码。
wd_mn_normal的情况完全与上一节同(读者可能要骂了,上一节明明说就是这样的,结果还有其他的情况,那不是骗人么?但是微软的东西例外就是多,我也实在拿他没办法... ).
注意如上节所叙述,wd_mn_normal的情况,既有可能是在Irp->MdlAddress中返回数据,也可能是在Irp->UserBuffer中返回数据,这个取决于Device的标志.
但是如果次功能号为wd_mn_mdl则完全不是这个意思。这种irp一进来看数据,就赫然发现Irp->MdlAddress和Irp->UserBuffer都为空。那你得到数据后把数据往哪里拷贝呢?
wd_mn_mdl的意思是请自己分配一个mdl,然后把mdl指向你的数据所在的空间,然后返回给上层。自然mdl是要释放的,换句话说事业使用完毕要归还,所以又有wd_mn_mdl_comp,意思是一个mdl已经使用完毕,可以释放了。
mdl用于描述内存的位置。据说和NDIS_BUFFER用的是同一个结构。这里不深究,我写一些函数来分配和释放mdl,并把mdl指向内存位置或者得到mdl所指向的内存:
// 释放mdl
_inline wd_void wd_mdl_free(wd_mdl *mdl)
{
IoFreeMdl(mdl);
}
// 这个这个东西分配mdl,缓冲必须是非分页的。可以在dispatch level跑。
_inline wd_mdl *wd_mdl_alloc(wd_void* buf,
wd_ulong length)
{
wd_mdl * pmdl = IoAllocateMdl(buf,length,wd_false,wd_false,NULL);
if(pmdl == NULL)
return NULL;
MmBuildMdlForNonPagedPool(pmdl);
return pmdl;
}
// 这个函数分配一个mdl,并且带有一片内存
_inline wd_mdl *wd_mdl_malloc(wd_ulong length)
{
wd_mdl *mdl;
wd_void *point = wd_malloc(wd_false,length);
if(point == NULL)
return NULL;
mdl = wd_mdl_alloc(point,length);
if(mdl == NULL)
{
wd_free(point);
return NULL;
}
return mdl;
}
// 这个函数释放mdl并释放mdl所带的内存。
_inline wd_void wd_mdl_mfree(wd_mdl *mdl)
{
wd_void *point = wd_mdl_vaddr(mdl);
wd_mdl_free(mdl);
wd_free(point);
}
// 得到地址。如果想往一个MdlAddress里边拷贝数据 ...
_inline wd_void *wd_mdl_vaddr(wd_mdl *mdl)
{
return MmGetSystemAddressForMdlSafe(mdl,NormalPagePriority);
}
另外我通过这两个函数来设置和获取Irp上的mdl.
_inline wd_mdl *wd_irp_mdl(wd_irp *irp)
{
return irp->MdlAddress;
}
_inline wd_void wd_irp_mdl_set(wd_irp *irp,
wd_mdl *mdl)
{
irp->MdlAddress = mdl;
}
一个函数获得UserBuffer.
_inline wd_void * wd_irp_user_buf(wd_irp *irp)
{
return irp->UserBuffer;
}
要完成请求还有一个问题。就是irp->IoStatus.Information.在这里你必须填上实际读取得到的字节数字。不然上层不知道有多 少数据返回。这个数字不一定与你的请求的长度等同(其实我认为几乎只要是成功,就应该都是等同的,唯一的例外是读取到文件结束的地方,长度不够了的情 况)。我用下边两个函数来获取和设置这个数值:
_inline wd_void wd_irp_infor_set(wd_irp *irp,
wd_ulong infor)
{
irp->IoStatus.Information = infor;
}
_inline wd_ulong wd_irp_infor(wd_irp *irp)
{
return irp->IoStatus.Information;
}
也许你都烦了,但是还有事情要做。作为读文件的情况,如果你是自己完成请求,不能忘记移动一下文件指针。否则操作系统会不知道文件指针移动了而反复读同一个地方永远找不到文件尾,我碰到过这样的情况。
一般是这样的,如果文件读取失败,请保持原来的文件指针位置不要变。如果文件读取成功,请把文件指针指到“读请求偏移量+成功读取长度”的位置。
这个所谓的指针是指Irp->FileObject->CurrentByteOffset.
我跟踪过正常的windows文件系统的读行为,我认为并不一定是向我上边说的这样做。情况很复杂,有时动,有时不动(说复杂当然是因为我不理解),但是按我上边说的方法来完成,我还没有发现过错误。
我用以下函数来设置和获取这个。
_inline wd_void wd_file_offset_add(wd_file *file,wd_ulong len)
{
file->CurrentByteOffset.QuadPart += len;
}
_inline wd_void wd_file_offset_set(wd_file *file,wd_lgint offset)
{
file->CurrentByteOffset = offset;
}
_inline wd_lgint wd_file_offset(wd_file *file)
{
return file->CurrentByteOffset;
}
现在看看怎么完成这些请求,假设我已经有数据了。现在假设本设备缓冲标记为0,即正常情况采用Irp->UserBuffer返回数据. 这些当然都是在my_disp_read中或者是其他想完成这个irp的地方做的(希望你还记得我们是如何来到这里),假设其他必要的判断都已经做了:
wd_irpsp *irpsp = wd_irp_cur_io_stack(irp);
switch(wd_irpsp_minor(irpsp))
{
// 我先保留文件的偏移位置
case wd_mn_normal:
{
wd_void *point = wd_irp_user_buf(irp);
// 如果有数据,就往point ...里边拷贝 ...
wd_irp_infor_set(irp,length);
wd_irp_status_set(irp,wd_suc);
wd_file_offset_set(wd_irp_file(irp),offset+length);
return wd_irp_over(irp);
}
case wd_mn_mdl:
{
wd_void *mdl = wd_mdl_malloc(length); // 情况比上边的复杂,请先分配mdl
if(mdl == NULL)
{
// ... 返回资源不足 ...
}
wd_irp_mdl_set(irp,mdl);
wd_irp_infor_set(irp,length);
wd_irp_status_set(irp,wd_suc);
wd_file_offset_set(wd_irp_file(irp),offset+length);
return wd_irp_over(irp);
}
case wd_mn_mdl_comp:
{
// 没有其他任务,就是释放mdl
wd_mdl_mfree(wd_irp_mdl(irp));
wd_irp_mdl_set(irp,0);
wd_irp_infor_set(irp,0);
wd_irp_status_set(irp,wd_status_suc);
return wd_irp_over(irp);
}
default:
{
// 我认为其他的情况不过滤比较简单 ...
}
}
重要提醒:wd_mn_mdl的情况,需要分配一个mdl,并且这个mdl所带有的内存是有一定长度的,这个长度必须与后来的irp-> IoStatus.Information相同!似乎上层并不以irp->IoStatus.Information返回的长度为准。比如明明只读 了50个字节,但是你返回了一个mdl指向内存长度为60字节,则操作系统则认为已经读了60个字节!这非常糟糕。
最后提一下文件是如何结尾的。如果到某一处,返回成功,但是实际读取到的数据没有请求的数据长,这时还是返回wd_status_suc (STATUS_SUCCESS),但是此后操作系统会马上发irp来读最后一个位置,此时返回长度为0,返回状态STATUS_FILE_END即可。
已经费巨大篇幅解释读请求。我不会再讲解写请求了。相信读者有能力自己搞清楚。
10 自己发送Irp完成读请求
关于这个有一篇文档解释得很详细,不过我认为示例的代码有点太简略了,这篇文档在IFS所附带的OSR文档中,请自己寻找。
为何要自己发送Irp?在一个文件过滤驱动中,如果你打算读写文件,可以试用ZwReadFile.但是这有一些问题。Zw系列的Native API使
用句柄。而句柄是有线程环境限制的。此外也有中断级别的限制。再说,Zw系列函数来读写文件,最终还是要发出Irp,又会被自己的过滤驱动
捕获到。结果带来判断上的困难。对资源也是浪费。那么最应该的办法是什么呢?当然是直接对卷设备发Irp了。
但是Irp是非常复杂的数据结构,而且又被微软所构造的很多未空开的部件所处理。所以自己发irp并不是一件简单的事情。
比较万能的方法是IoAllocateIrp,分配后自己逐个填写。问题是细节实在太多,很多无文档可寻。有兴趣的应该看看我上边所提及的
那篇文章“Rolling Your Own”。
有意思的是这篇文章后来提到了捷径,就是利用三个函数:
IoBuildAsynchronousFsdRequest(...)
IoBuildSynchronousFsdRequest(...)
IoBuildDeviceIoControlRequest(...)
于是我参考了他这方面的示例代码,发现运行良好,程序也很简单。建议怕深入研究的选手就可以使用我下边提供的方法了。
首先的建议是使用IoBuildAsynchronousFsdRequest(),而不要使用同步的那个。使用异步的Irp使irp和线程无关。而你的过滤驱动一
般很难把握当前线程(如果你开一个系统线程来专门读取文件那例外)。此时,你可以轻松的在Irp的完成函数中删除你分配过的Irp,避免去追
究和线程相关的事情。
但是这个方法有局限性。文档指出,这个方法仅仅能用于IRP_MJ_READ,IRP_MJ_WRITE,IRP_MJ_FLUSH_BUFFERS,和IRP_MJ_SHUTDOWN.
刚好我这里仅仅要求完成文件读。
用Irp完成文件读需要一个FILE_OBJECT.FileObject是比Zw系列所用的句柄更好的东西。因为这个FileObject是和线程无关的。你可以
放心的在未知的线程中使用他。
自己要获得一个FILE_OBJECT必须自己发送IRP_MJ_CREATE的IRP.这又不是一件轻松的事情。不过我跳过了这个问题。因为我是文件系
统过滤驱动,所以我从上面发来的IRP中得到FILE_OBJECT,然后再构造自己的IRP使用这个FILE_OBJECT,我发现运行很好。
但是又出现一个问题,如果IRP的irp->Flags中有IRP_PAGING(或者说是Cache管理器所发来的IRP)标记,则其FileObject我获得并使
用后,老是返回错误。阅读网上的经验表明,带有IRP_PAGINGE的FileObject不可以使用.于是我避免使用这时的FileObject.我总是使用不带
IRP_PAGING的Irp(认为是用户程序发来的读请求)的FileObject。
好,现在废话很多了,现在来看看构造irp的代码:
_inline wd_irp *wd_irp_fsd_read_alloc(wd_dev *dev,
wd_void *buf,
wd_ulong length,
wd_lgint *offset,
wd_io_stat_block *io_stat)
{
return IoBuildAsynchronousFsdRequest(IRP_MJ_READ,dev,
buf,length,
offset,
io_stat);
}
io_stat我不知道是做何用,我一般填写NULL.类型是PIO_STATUS_BLOCK.buf则是缓冲。在Irp中被用做UserBuffer接收数据。offset是
这次读的偏移量。
以上函数构造一个读irp.请注意,此时您还没有设置FileObject.实际上我是这样发出请求的:
irp = wd_irp_fsd_read_alloc(dev,buf,len,&start,NULL);
if(irp == NULL)
return;
irpsp = wd_irp_next_sp(irp);
wd_irpsp_file_set(irpsp,file);
wd_irp_comp(irp,my_req_comp,context);
请注意wd_irp_next_sp,我是得到了这个irp的下一个IO_STACK_LOCATION,然后我设置了FileObject.接下来应该设置了完成后,我就
可以发送请求了!请求发送完毕后,一旦系统完成请求就会调用你的my_req_comp.
再看看my_req_comp如何收场:
wd_stat my_req_comp(in wd_dev *dev,
in wd_irp *irp,
in wd_void *context)
{
// 请注意,无论成功还是失败,我都得在这里彻底销毁irp
wd_irp_send_by_myself_free(irp);
// 返回这个,仅仅是因为irp我已经销毁,不想让系统再次销毁它而已。
return wd_stat_more_processing;
}
wd_stat_more_prcessing就是STATUS_MORE_PROCESSING_REQUIRED。之所以返回这个,是因为我已经把irp给删除了。我不想windows系
统再对这个irp做什么。所以干脆说我还要继续处理,这样避免了io管理器再去动用这个irp的可能。
最后是那个irp删除函数的代码:
_inline wd_void wd_irp_send_by_myself_free(wd_irp *irp)
{
if (irp->MdlAddress)
{
MmUnmapLockedPages(MmGetSystemAddressForMdl(irp->MdlAddress),
irp->MdlAddress);
MmUnlockPages(irp->MdlAddress);
IoFreeMdl(irp->MdlAddress);
}
IoFreeIrp(irp);
};
因为我自己没有分配过MdlAddress下去过。所以如果下边返回了MDL,我得附带清理它。
好了,祝愿你心情愉快。如果你何我一样懒惰的话,不妨就用上边的简单方法自己发irp来读文件了