目录

用户态文件系统,Dokany驱动代码分析


新版本的dokany已经是驱动层通过回调通知式的方式,架构同文章中老版本的描述完全不同,此文章仅供参考 -2024/6/12

前言

Dokany是一个Windows应用层的文件系统,还支持FUSE接口,google的Drive用的就是此驱动来映射驱动盘,此篇文章主要分析Dokany从应用层调用到驱动层的架构原理

应用层篇

  • dokan_fuse是通过fuse_operations这个结构注册的,fuse_operations封装了fuse的文件打开读写等一系列操作。在dokanfuse中,如果API调用出问题,需要对错误码转译一下,会调用以下函数,fuse的错误码和windows的错误码会保存在一张映射表中 int ntstatus_error_to_errno(long win_res); long errno_to_ntstatus_error(int err);

  • 在fuse_loop中,会走到最终dokan的循环的事件处理中,我们这个驱动的处理架构是一个轮询的处理机制,并没有做到通知,通知机制可能在dokan2.x版本实现。

  • Fuse应用层,事件处理的最终入口代码在dokan.c中的DokanLoop调用。在一个while循环中,会首先打开驱动的设备对象句柄。再拿一个驱动中待处理的文件事件,回调我们的方法回填我们给的信息,再发送给驱动处理。

  • EVENT_CONTEXT 和 EVENT_INFORMATION 结构。

这里要详细讲一下事件这个概念,事件在应用层和驱动层都有涉及。在dokan中,事件分为EVENT_CONTEXT 以及 EVENT_INFORMATION,结构定义可以在public.h中找,这些都是变长结构。EVENT_CONTEXT是通过DeviceIoControl的IOCTL_EVENT_WAIT发送给dokan的驱动然后拿到的,在字段MajorFunction代表着其他进程请求的控制码(IRP_MJ_CREATE等一系列IRP请求代表着文件的或者打开或者关闭和读取等一系列的文件操作请求),EVENT_CONTEXT也包含了文件路径及请求的进程ID等。

我们拿到EVENT_CONTEXT的内容后,会根据需要处理的消息,填充一个EVENT_INFORMATION再发给驱动,这个是通过DeviceIoControl的IOCTL_EVENT_INFO转递给驱动处理,EVENT_INFORMATION里面有我们处理后填充的内容,然后驱动再通过一系列处理通过IRP传递给文件系统(这个在驱动分析上会详细说明)

然而,这里会有一个问题,驱动如何知道EVENT_INFORMATION是跟哪一个EVENT_CONTEXT对应上的。具体实现上是两个结构中都有一个SerialNumber,这个字段会在驱动中原子加1并通过EVENT_CONTEXT传递出来,应用层的EVENT_INFORMATION也是回填EVENT_CONTEXT中的SerialNumber值,驱动层遍历事件的SN匹配处理。

以下是一个应用层简单的流程图。

./dokany_sys/1.png

dokany驱动代码分析

在dokay应用层会以轮循的机制来拿EVENT_CONTEXT,并发送EVENT_INFORMATION的过程,在驱动流程处理大概如下图所示

/dokany_sys/2.png

Dokan驱动附加在系统卷设备驱动上,接管文件系统对文件的操作IRP,并将IRP挂入队列待处理。

这里面关键是搞清楚三个队列的处理流程:

PendingIrp队列

所有文件的打开读取等操作,会传递到dokan驱动的附加设备对象上,附加的设备对象再找到创建自身的驱动对象的回调进行处理。举例来说对应的文件打开的操作,会走到DokanDispatchCreate中,这个调用的目的是填充eventContext这个结构。处理完成最后会调用

DokanRegisterPendingIrp,在这里会把eventContext放到PendingIrp的队列中.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
NTSTATUS
DokanRegisterPendingIrp(__in PDEVICE_OBJECT DeviceObject, __in PIRP Irp,
                        __in PEVENT_CONTEXT EventContext, __in ULONG Flags) {
  PDokanVCB vcb = DeviceObject->DeviceExtension;
  NTSTATUS status;

  DDbgPrint("==> DokanRegisterPendingIrp\n");

  if (GetIdentifierType(vcb) != VCB) {
    DDbgPrint("  IdentifierType is not VCB\n");
    return STATUS_INVALID_PARAMETER;
  }

  status = RegisterPendingIrpMain(DeviceObject, Irp, EventContext->SerialNumber,
                                  &vcb->Dcb->PendingIrp, Flags, TRUE,
                                  /*CurrentStatus=*/STATUS_SUCCESS);

  if (status == STATUS_PENDING) {
    DokanEventNotification(&vcb->Dcb->NotifyEvent, EventContext);
  } else {
    DokanFreeEventContext(EventContext);
  }

  DDbgPrint("<== DokanRegisterPendingIrp\n");
  return status;
}

可以看到在DokanRegisterPendingIrp的实现中,会把EventContext放到vcb->Dcb->PendingIrp以及vcb->Dcb->NotifyEvent两个队列中待处理。 在RegisterPendingIrpMain会把这个待处理的IRP标记为Pending状态,最终会调用到API的IoCompleteRequest返回,这时候再返回给应用层的状态是一个异步的待处理过程。

PendingEvent队列

此队列是dokan.dll请求的时挂载的请求队列,对应的代码流程是DokanRegisterPendingIrpForEvent->RegisterPendingIrpMain,可以看到这里客户端请求一个EventContext时会将IRP插入到vcb->Dcb->PendingEvent,再返回Pending状态待处理。

这里对应的就是应用层通过DeviceIoControl发IOCTL_EVENT_INFO的对应驱动处理地方。

按理说,这个处理应该是异步的,但是客户端轮循的时候却是同步的状态,原因是DeviceIoControl这个API内部处理了.

NotifyEvent队列

此队列是在通知线程中处理的,这个通知线程是由GlobalDeviceControl 中 IOCTL_EVENT_START控制码在DokanEventStart启动的。线程回调是NotificationThread->NotificationLoop

NotificationLoop的声明有一个误导,PIRP_LIST PendingIrp这个队列的实际入参是PendingEvent.

VOID NotificationLoop(__in PIRP_LIST PendingIrp, __in PIRP_LIST NotifyEvent)

实际的代码处理过程就是从PendingEvent拿一个dokan.dll的请求事件,再通过NotifyEvent的事件回填一个EventContext结构返回给用户层。

到此,用户态请求,并拿到一个EventContext的流程就通过这三个队列给关联起来了。

用户层的处理的信息驱动关联处理

剩下还有一个问题是,用户层发送EVENT_INFORMATION的过程,EVENT_INFORMATION中才包含了其他进程能请求拿到的真实文件数据。

对应的驱动层是调用DokanCompleteIrp,如下代码摘要

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
 listHead = &vcb->Dcb->PendingIrp.ListHead;

  for (thisEntry = listHead->Flink; thisEntry != listHead;
       thisEntry = nextEntry) {
   PIRP irp;
   PIO_STACK_LOCATION irpSp;
   nextEntry = thisEntry->Flink;

   irpEntry = CONTAINING_RECORD(thisEntry, IRP_ENTRY, ListEntry);

   if (irpEntry->SerialNumber != eventInfo->SerialNumber) {
      continue;
    }

这里就对应上了从PendingIrp队列中对比,找到SerialNumber相同的IRP结束掉,这个调用走完,就是真正的结束掉了进程请求的IRP的处理。

对于一直存在在PendingIrp队列中未处理的IRP要怎么办,其实dokan还有一个超时线程会找到PendingIrp中超时的IRP,再强制返回一个错误,默认的超时设置是15秒的时间,对应的代码在timeout.c中的DokanTimeoutThread,就不详细讲了。