Windows磁盘驱动基础教程【一】[转]

Posted on 2009-07-20 17:13  WindFly  阅读(802)  评论(0编辑  收藏  举报

 
 

作者不详
本文讲述Windows磁盘驱动的主要结构功能与编写方法基础。本文描述的内容仅限于软件层面,并不与具体的硬件相关。

1.磁盘驱动基础

     不少人把文件系统驱动和磁盘驱动混为一谈。实际上文件系统驱动应该与磁盘驱动是两类不同的驱动程序。文件系统仅仅考虑数据在存储设备上的保存格式(而不考虑具体是什么存储设备),而磁盘是存储设备的一种。

     在存储设备驱动(storage driver)中,与实际的硬件设备打交道的驱动称为微端口(miniport)驱动,而更上层的驱动称为类驱动(class driver)。这里说的磁盘驱动(disk driver)是一个类驱动。类驱动具体功能通过下层的微端口驱动实现。而自己则抽象出一类设备(如磁盘),供文件系统驱动调用。这样,文件系统就不用自己去和硬件细节打交道了,在它看来所有的磁盘都是一个磁盘设备。这就是类驱动存在的意义。

     如果认为微端口驱动都是硬件驱动的话,则类驱动是硬件驱动之上的软件驱动。

     要学习磁盘驱动的编写,可以参考WINDDK\src\storage\class\disk,这是一个SCSI磁盘类驱动(SCSI disk class driver)的例子。但是这个例子的实现细节过于烦琐。为此我在网上找到一个文件虚拟磁盘(FileDisk)的例子,用这个来说明磁盘驱动的编写方法。你可以在网上下载它。

     磁盘驱动找到物理媒质的时候,生成磁盘设备对象。但是要注意磁盘设备对象与卷(volume)设备对象(volume device driver)的区别。一个磁盘设备对象对应一个物理的磁盘。而卷,则是文件系统驱动找到磁盘设备之后,挂载形成的一种新设备。这种设备可以进行很多操作,比如生成文件,删除文件,修改文件等,这些是与文件系统相关的操作。而磁盘设备对象则没有这些操作。因为磁盘并不知道有文件系统,所以仅仅能进行读,写,获取一些磁盘信息等操作。比文件系统操作简单得多。

     每当你生成一个磁盘驱动对象,系统中就出现一个新的磁盘。是否出现在windows的我的电脑之中,还需要涉及到一些其他信息的返回。但是你确实可以随时生成新的磁盘,无论具体的硬件是否存在。在内核代码中,你可以使用IoCreateDevice来生成一个磁盘设备。

     XP版本的ifs下的例子disk使用IoCreateDisk来生成一个磁盘。似乎这个函数的使用更加简便。而FileDisk的例子,使用IoCreateDevice,可能这个例子开发比较早,或者是为了兼容2000ddk.

     驱动的入口是DriverEntry,你可以首先编写这个函数。你可以在这里生成磁盘设备对象.设备对象都是有名字的。首先你必须确定它们的名字。FileDisk在DriverEntry中生成了一连串设备,这些设备的名字如下:
     "http://www.cnblogs.com/kuangfuhxw/admin/file://device//FileDisk0"
     "http://www.cnblogs.com/kuangfuhxw/admin/file://device//FileDisk1"
     "http://www.cnblogs.com/kuangfuhxw/admin/file://device//FileDisk2"
     "http://www.cnblogs.com/kuangfuhxw/admin/file://device//FileDisk3"
     "http://www.cnblogs.com/kuangfuhxw/admin/file://device//FileDisk4"
     ... 依次类推。

     需要多少个磁盘设备对象和你的需要有关,这个数字事先已经保存在注册表中。对你的简单代码来说,只要:

NTSTATUS
DriverEntry (
   IN PDRIVER_OBJECT    DriverObject,
   IN PUNICODE_STRING RegistryPath
   )
{
     PDEVICE_OBJECT device_object;
     UNICODE_STRING device_name;
     RtlInitUnicodeString(&device_name,L"http://www.cnblogs.com/kuangfuhxw/admin/file://device//FileDisk");
    
     status = IoCreateDevice(
         DriverObject,
         0,      //sizeof(DEVICE_EXTENSION),这里是设备扩展的大小空间
         &device_name,
         FILE_DEVICE_DISK,
         0,
         FALSE,
         &device_object);
     return status;
}     

    
     建议打开ddk帮助看看IoCreateDevice的参数说明。FILE_DEVICE_DISK是ddk中定义的一种设备类型。现在磁盘设备对象已经生成了,只要加载这个驱动,系统将知道增加一个磁盘。不过还有以下的一些问题:

     (1)当设备目录"http://www.cnblogs.com/kuangfuhxw/admin/file://device/"不存在的时候,你的创建会失败。所以应该先创建这个目录,使用ZwCreateDirectoryObject即可。

     (2)你确实创建了一个磁盘设备对象。但是你没有为你的驱动指定分发例程。当windows对这个磁盘有所请求(比如获取磁盘的信息,读写这个磁盘等),你的分发例程会被调用。此时没有写分发例程,因此windows也无法得到这个磁盘的信息,因而它不会起作用。

     (3)你必然要在磁盘设备对象上保留一些私人信息,因此不能把设备扩展大小设置为0。你应该定义设备扩展的数据结构。当然这要看你的需要了。

     (4)你还需要设置一些设备标志。


         *                        *                        *


2 分发例程和设备扩展

     应该给你的驱动指定分发例程。这样你的磁盘设备生成之后,windows会发给你请求,来读取你的磁盘的信息。而这些请求(irp)就会发到你的分发例程中。
     分发例程是一组用来处理不同请求的函数。

NTSTATUS
DriverEntry (
   IN PDRIVER_OBJECT    DriverObject,
   IN PUNICODE_STRING RegistryPath
   )
{

     ...      // 前面生成设备对象...

     // 设置分发例程。请注意仅仅需要5个,比文件系统少多了。
     DriverObject->MajorFunction[IRP_MJ_CREATE]        = FileDiskCreateClose;
     DriverObject->MajorFunction[IRP_MJ_CLOSE]        = FileDiskCreateClose;
     DriverObject->MajorFunction[IRP_MJ_READ]        = FileDiskReadWrite;
     DriverObject->MajorFunction[IRP_MJ_WRITE]        = FileDiskReadWrite;
     DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = FileDiskDeviceControl;

     // 卸载例程。
     DriverObject->DriverUnload = FileDiskUnload;

     return status;
}

     下面的任务你要自己编写FileDiskCreateClose, FileDiskReadWrite, FileDiskDeviceControl这三个函数。

     请注意所有的分发例程的结构都是如此:
NTSTATUS
DispatchFunction(
     IN PDEVICE_OBJECT    DeviceObject,
     IN PIRP          Irp
   );

     DeviceObject是接受请求的设备对象指针,应该是由你的驱动生成的,所以才会发到你的驱动的分发例程。Irp是请求包指针。里边含有请求相关的信息。最后返回执行的结果(成功或者错误代码)。

     从FileDisk看来,分发例程比文件系统驱动要简单得多。DriverObject->DriverUnload是一个特殊的例程,在windows卸载你的驱动的时候被调用。你可以在其中删除设备,关闭打开的文件等等。

     FileDisk定义了一个设备扩展。这个设备扩展被记录在设备对象中,你随时可以通过DeviceObject->DeviceExtension得到它。它的内容如下:

typedef struct _DEVICE_EXTENSION {
   BOOLEAN                media_in_device;
   HANDLE                file_handle;
   FILE_STANDARD_INFORMATION    file_information;
   BOOLEAN                read_only;
   PSECURITY_CLIENT_CONTEXT    security_client_context;
   LIST_ENTRY              list_head;
   KSPIN_LOCK              list_lock;
   KEVENT                request_event;
   PVOID                thread_pointer;
   BOOLEAN                terminate_thread;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
    
     media_in_device是指这个设备是否已经指定了一个文件作为存储媒质。这是一个用文件来虚拟磁盘的驱动。那么一个磁盘应该对应一个实际存在的文件。读写这个磁盘的请求最终转变为对文件的读写。如果一个磁盘设备对象还没有指定文件,那么这个内容是FALSE.

     file_handle是文件句柄。也就是这个虚拟磁盘所对应的文件。

     file_information是这个文件的一些信息。

     read_only是否只读。

     security_client_context 访问文件的时候需要使用的一个线程客户安全性上下文。


     list_head是一个链表头。一部分irp(windows发来的请求包)被放入这个链表中。我们为每个磁盘对象开启一个系统线程(处理线程),专门用来处理这些请求。

     list_lock是为了保证链表读写同步的锁。

     request_event是一个事件。当链表中没有请求的时候,处理请求的系统线程并不做任何事情,而只等待这个事件。当有请求到来,我们把请求放入链表,然后设置这个事件。处理线程就会开始处理这些请求。

     thread_pointer是线程的指针,用来最后等待这个线程的结束。

     terminate_thread是一个标志。如果设置为TRUE,处理线程执行的时候检测到这个,就会把自己终止掉。


         *                        *                        *


3.分发例程-打开与关闭,读与写

     这里需要Create和Close是为了通信。让应用程序可以打开这些磁盘设备来设置一些信息。
     打开关闭非常简单,都是对irp无条件返回成功:

NTSTATUS
FileDiskCreateClose (
   IN PDEVICE_OBJECT    DeviceObject,
   IN PIRP          Irp
   )
{
   PAGED_CODE();
   Irp->IoStatus.Status = STATUS_SUCCESS;
   Irp->IoStatus.Information = FILE_OPENED;
   IoCompleteRequest(Irp, IO_NO_INCREMENT);
   return STATUS_SUCCESS;
}

     PAGED_CODE()是一个调试用的宏。如果一个函数被定义在可分页交换的段中,那么它执行时的中断级别必须满足一定的要求。PAGED_CODE()用来检测是否符合。如果不行,调试版本中这里会出现失败。这个宏仅仅在调试版本下有效。

     例子disk的读写非常复杂。而FileDisk的读写是对文件的读写。所以相对简单一些。基本的过程如下:
     1.得到设备扩展。
     2.检查是否已经打开了文件。如果没有,直接返回失败。
     3.对于长度为0的读写,直接返回成功。
     4.把请求加入设备扩展中的请求队列中,设置事件让处理线程运行处理。
     5.返回等待。

     你固然可以在读写例程中直接读写文件。但是这不符合惯例。读写文件需要消耗的时间比较长,应该让系统尽快得到答复以便可以干其他的事情。此外这个函数很容易重入,我们希望把读写请求的完成序列化,为此我们并不在这里直接读写文件。而是把请求放入队列中。为每个磁盘设备对象生成一个系统线程,来依次处理这些请求。

NTSTATUS
FileDiskReadWrite (
   IN PDEVICE_OBJECT    DeviceObject,
   IN PIRP          Irp
   )
{
     PDEVICE_EXTENSION    device_extension;
     PIO_STACK_LOCATION io_stack;

     // 得到设备扩展
     device_extension = (PDEVICE_EXTENSION) DeviceObject->DeviceExtension;

     ...
              
}

     上面得到了设备扩展,那么我检查这个磁盘是否已经有物理媒质(也就是一个文件用来模拟磁盘空间)。

     // 如果没有打开过文件,就返回失败
     if (!device_extension->media_in_device)
     {
         Irp->IoStatus.Status = STATUS_NO_MEDIA_IN_DEVICE;
         Irp->IoStatus.Information = 0;
         IoCompleteRequest(Irp, IO_NO_INCREMENT);
         return STATUS_NO_MEDIA_IN_DEVICE;
     }         
    
     想得到要读多长的时候要得到Irp的当前栈空间:
    
     io_stack = IoGetCurrentIrpStackLocation(Irp);
     // 读0长的时候立刻成功
     if (io_stack->Parameters.Read.Length == 0)
     {
         Irp->IoStatus.Status = STATUS_SUCCESS;
         Irp->IoStatus.Information = 0;
         IoCompleteRequest(Irp, IO_NO_INCREMENT);
         return STATUS_SUCCESS;
     }
        
     然后就可以把Irp放到队列里去等待完成了:

     // 标志pending
     IoMarkIrpPending(Irp);

     // 把请求写入链表
    ExInterlockedInsertTailList(
         &device_extension->list_head,
         &Irp->Tail.Overlay.ListEntry,
         &device_extension->list_lock
     );

     // 设置事件。让线程循环运行。
     KeSetEvent(
         &device_extension->request_event,
         (KPRIORITY) 0,
         FALSE
         );
     return STATUS_PENDING;

     到此读写例程完成。至于真正的读写完成在处理线程中,请阅读后面关于处理线程的章节。


         *                        *                        *


5.分发例程-磁盘固有的设备控制
    
     设备控制(Device Control)是除了读,写之外最重要的操作之一。对磁盘来说,windows通过发送设备控制请求来询问此磁盘的一些信息。收到设备控制请求之后,应该首先得到控制功能号,然后根据不同的控制功能号进行不同的处理.这些功能号有些是windows固有的,有些是由你自己定义的。

NTSTATUS
FileDiskDeviceControl (
   IN PDEVICE_OBJECT    DeviceObject,
   IN PIRP          Irp
   )
{
     PDEVICE_EXTENSION    device_extension;
     PIO_STACK_LOCATION io_stack;
     NTSTATUS          status;

     // 得到设备扩展
     device_extension = (PDEVICE_EXTENSION) DeviceObject->DeviceExtension;

     // 得到当前设备栈
     io_stack = IoGetCurrentIrpStackLocation(Irp);

     // 判断如果是还没有加载物理媒质就返回失败。但是
     // IOCTL_FILE_DISK_OPEN_FILE是自定义的功能号,专
     // 用来加载物理媒质的,所以排除在外
     if (!device_extension->media_in_device &&
         io_stack->Parameters.DeviceIoControl.IoControlCode !=
         IOCTL_FILE_DISK_OPEN_FILE)
     {
         Irp->IoStatus.Status = STATUS_NO_MEDIA_IN_DEVICE;
         Irp->IoStatus.Information = 0;
         IoCompleteRequest(Irp, IO_NO_INCREMENT);
         return STATUS_NO_MEDIA_IN_DEVICE;
     }

     // 根据不同的功能号处理...
     switch (io_stack->Parameters.DeviceIoControl.IoControlCode)
     {     
     ...
     }
    
     ...
}

 

Copyright © 2024 WindFly
Powered by .NET 8.0 on Kubernetes