为了正常的体验网站,请在浏览器设置里面开启Javascript功能!
首页 > 第十章 Hello China的驱动程序管理框架

第十章 Hello China的驱动程序管理框架

2018-09-07 50页 doc 325KB 9阅读

用户头像

is_093010

暂无简介

举报
第十章 Hello China的驱动程序管理框架驱动程序管理框架 (The architecture of Hello China,loadable module and device driver) By Garry Xin All right reserved,2005/04/02 目录 3设备驱动程序管理框架 3概述 4设备管理器和IO管理器 12Hello China的设备管理框架 14I/O管理器(I/OManager) 33文件系统的实现 33文件系统与文件的命名 34文件系统驱动程序 34打开一个文件的操作流程 35设...
第十章 Hello China的驱动程序管理框架
驱动程序管理框架 (The architecture of Hello China,loadable module and device driver) By Garry Xin All right reserved,2005/04/02 目录 3设备驱动程序管理框架 3概述 4设备管理器和IO管理器 12Hello China的设备管理框架 14I/O管理器(I/OManager) 33文件系统的实现 33文件系统与文件的命名 34文件系统驱动程序 34打开一个文件的操作流程 35设备驱动程序框架 35设备请求控制块(DRCB) 38设备驱动程序的文件组织结构 39设备驱动程序的功能实现 42设备驱动程序对象 43DriverEntry的实现 44UnloadEntry的实现 45设备对象 45设备对象的定义 45设备对象的命名 46设备对象的类型 47设备对象的设备扩展 48设备的打开操作 49设备命名策略 50设备的中断管理 设备驱动程序管理框架 概述 设备管理是操作系统最核心的管理任务之一,实际上,在一个典型的成熟的操作系统当中,设备管理部分的实现代码,可能占整个操作系统实现代码的一半,可见设备管理部分的复杂性和重要性。 由于硬件设备的多样性,操作系统不可能对每种硬件都自己直接驱动,而是采用一种分层的结构,即由硬件设备制造商(Vendor)提供针对特定硬件的设备驱动程序(Driver),设备驱动程序安装在计算机上,由操作系统调用设备驱动程序来控制硬件。这样就可以避免各种各样的麻烦,保持操作系统良好的可扩充性。 因此,一般来说,操作系统面临的是设备驱动程序,而设备驱动程序再进一步对设备进行驱动或管理,操作系统一般不直接面向具体的硬件设备,要实现这个功能,那么操作系统必须提供一个的接口给设备驱动程序,以便设备驱动程序可以向上跟操作系统交互,操作系统也根据自己提供的这个接口,来调用设备驱动程序的一些功能函数,来简介的操纵硬件。 另外,在设备管理过程当中,还有一些问要解决,诸如: 1、 如何标识和命名一个设备,如果一个设备得到良好的命名,那么用户程序就可以直接根据设备名字来打开设备,进而请求设备提供的服务,相反,如果无法正常的命名设备,那么用户将无法通知操作系统,自己想操纵哪个设备,因此,设备的命名机制,实际上是用户应用程序跟操作系统之间的接口; 2、 如何处理设备中断,一般情况下,设备是通过中断的方式来通知操作系统,特定的事件已经发生(有些情况下,是被动通知的,即操作系统需要被动的查询设备),操作系统在收到中断通知之后,会根据中断号,调用合适的中断处理程序,一般情况下,中断处理程序是在设备驱动程序中的,因此,设备如何把自己特定的中断处理程序注册到操作系统中,也是需要解决的一个问题; 3、 硬件资源的分配问题,所谓硬件资源,即包括中断号、输入/输出端口号、DMA通道、内存映射区域在内的系统资源。这些系统资源的分配,可以有两种方式:其一,静态分配,由人工的方式,手工设置设备所使用的资源情况,然后这些设置保存在一个配置文件中,设备驱动程序分析这个配置文件,获得资源分配情况,或者由设备管理单元通知设备驱动程序,这种方式的好处是,不需要操作系统核心做额外的事情,所有硬件资源都由计算机管理者手工分配,但有一个问题,就是操作起来比较麻烦,尤其是对于初级用户,可能会是一个很大的挑战,而且有可能出现资源冲突的现象,比如,由于错误的分配,两个硬件设备占用了同一个中断。另外一种分配方式,就是动态分配,操作系统核心在加载的时候,通过某种总线,动态的检测存在总线上的设备,并集中分配系统资源,然后通过某种协议通知设备驱动程序,显然,这种方式是一种理想的方式,不需要用户过多的干预,而且由于集中分配,避免了资源冲突问题。但这种方式需要系统总线的支持,有些情况下,系统总线是不支持自动发现设备的,这时候就需要通过手工的方式,静态分配系统资源; 4、 用户线程对设备的调用问题,用户如何通过一种统一的接口,调用各种不同的设备驱动程序提供的服务,也是设备管理模块在设计过程中,需要重点考虑的问题之一。一般情况下,不可能为每种不同的设备,都提供一套特定的调用接口,由用户线程调用,而是由设备管理框架提供一套统一的接口,用户线程通过这套统一的接口,调用不同的设备驱动程序提供的服务。 一般情况下,一个操作系统的设备管理部件,需要通盘考虑上述问题,并针对上述问题,提供合理的解决方案,来实现一个可以真正使用的设备管理部件。 Hello China的设计过程中,对于设备管理部件,在设计的初期,也充分考虑了上述问题,并采取了合适的手段,对上述问题进行了解决。在本文中,我们就Hello China的设备管理模块,进行详细的描述,包括其总体框架模型、每个组成对象的详细设计、各模块之间的接口等。 在本文中,我们把Hello China实现的设备管理体系成为“设备管理框架”,从名字上看出,该软件模块其实是一个框架(Frame),因为该软件模块定义了一些标准接口,这些接口的具体实现,则是在用户编制的程序中实现的,比如设备驱动程序等,设备管理体系仅仅根据用户的请求,调用适当的标准接口,符合框架的概念。 设备管理器和IO管理器 通用的设备管理机制 在正式描述Hello China的设备管理框架前,有必要对通用的操作系统(比如Windows系列、Linux系列等)的设备管理机制进行描述,以便于读者理解这些通用操作系统的设备管理机制,以此为基础,从而可以更好的理解Hello China的设备管理框架。若读者对通用的操作系统设备管理机制非常熟悉,可跳过此节,直接阅读下一节。 为了对设备进行管理,操作系统必须充分收集系统的硬件配置信息,并建立相应的数据库。一般情况下,这个收集设备硬件信息、建立设备信息数据库的过程,是在操作系统启动的过程中进行的。在操作系统启动的过程中,会首先从系统总线开始探测,比如,针对总线,操作系统引导代码会读取适当的端口(比如,CF8H或CF0H),根据读取结果,来判断对应的PCI总线是否存在。若存在,则跳转到PCI总线驱动程序代码,PCI总线驱动程序代码完成PCI设备的枚举和检测。当然,对于ISA等总线类型,也执行类似的操作。 为了维护设备硬件信息,操作系统一般维护一个特定格式的数据库,在操作系统加载期间,由初始化代码检测系统硬件配置,根据检测的信息,填写这个数据库。比如,针对PCI总线,PCI总线驱动程序会依次枚举总线上的设备,并读取设备配置信息(PCI相关的详细信息,请参考“系统总线管理”一章),然后针对每个系统中存在的设备,都会在硬件信息数据库中创建相应的对象(数据结构),然后使用读取的配置信息填充这个对象,这样针对系统中的每条总线,都会进行这样一个检测操作,最终的结果是,操作系统把所有的系统硬件信息都进行了收集,并存放到数据库中进行统一管理。比如,下列图示表示了一个典型的操作系统的硬件数据信息库: 图10-1 操作系统维护的硬件信息数据库 这样,如果要标识一个屋里设备,一种可选的方式是,为每条总线分配一个数字,作为总线号,并为位于该总线上的所有设备,分配唯一的设备号,来表示该总线上的不同设备,这样可以通过总线号+设备号,形成一个唯一的设备标识ID,来定位到具体的设备。 这个硬件信息数据库的位置,会根据操作系统实现上的不同,而位于不同的位置,比如,Windows操作系统,就把这些硬件设备配置信息写到磁盘上(注册表内),而有些操作系统,则会把这些硬件配置信息保存在内存中,一旦计算机重新启动,就会丢失。 完成设备硬件信息的枚举之后,下一步工作,就是为硬件设备分配系统资源了,比如中断向量号、内存映射空间、IO端口地址、DMA通道等,我们称这一步为设备配置。由于这些资源信息都是从一个统一的资源空间中分配,因此操作系统必须保证,为设备分配的所有资源信息,不能出现冲突的情况。举例来说,假设一台计算机外设,采用IO端口的方式进行通信,提供PCI接口,这个时候,在枚举该设备的时候,操作系统可能只会得到该设备所需要的端口范围大小,因此,操作系统需要为该设备分配端口资源范围,这就是设备配置的任务了。当然,有的时候,BIOS可能已经为所有的硬件设备分配了IO端口资源,这时候操作系统需要确认,BIOS为硬件分配的资源不会出现冲突。很可能出现的一种情况就是,BIOS为两个不同的物理设备,分配了相同的系统资源(比如,都分配了IO端口8E0H - 8EFH),这可能是由BIOS软件错误引起的,也可能是由于设备物理硬件原因引起的,操作系统必须能够检测到这种错误,并进行处理(比如,为其中的一台物理设备保留端口号8E0H-8EFH,为另一台物理设备另外分配不同的IO端口资源)。 在对设备完成配置之后,设备还不能被正常使用,因为还没有加载设备的驱动程序。因此,完成设备配置之后,要进入正式使用步骤之前,必须加载设备驱动程序。一般来说,操作系统维护一个硬件设备ID(比如,PCI设备的设备ID)和对应设备的驱动程序的一个映射文件,在完成设备的枚举和配置之后,物理设备的设备ID已经得到(针对不同的总线类型,设备ID的标识方式也不一致),这样操作系统就可以根据设备ID查找映射文件,找到对应的设备驱动程序的文件名,然后把设备驱动程序加载到内存中,这样设备就可以被驱动,从而可以正常使用了,对应的设备驱动程序,提供了对实际物理设备进行操作的软件代码,并以函数指针的形式表现出来。 到目前为止,从理论上说,设备已经可以使用了,因为设备已经被操作系统配置好,而且设备驱动程序已经被加载。但实际上,仅仅靠这些信息,用户应用程序是无法使用设备的,试想,用户应用程序希望通过Ethernet网络接口卡发送一个数据报文,而实际系统中存在两张以太网接口卡,这种情况下,必须采用一种机制,让用户可以唯一指定一个特定的Ethernet接口卡,并唯一指定一个特定的操作(发送操作而不是接收操作)。对于功能的指定,可以通过不同的函数来进行,比如,针对以太网接口卡,驱动程序提供发送、接收、重新启动等一系列接口函数,这样用户就可以调用不同的功能函数,实现特定的功能。而对于设备的标识方式,一种可以选择的方式是,使用设备的物理ID(设备ID)直接进行标识,这样方式在理论上是可行的,但不直观,用户将面临一系列无任何特定意义的数字,非常不容易使用。因此,可以考虑采用字符串的形式,对设备进行标识。 一般操作系统的做法是,对每个具体的设备,都分配一个字符串(具有很明显的描述含义),用来唯一指定一台设备,用户可以直接看到这些设备标识字符串。这个字符串可以由操作系统在枚举设备的时候指定,也可以由设备驱动程序自己指定。设备描述字符串往往需要跟设备驱动程序进行关联,因为只有这样,才可以通过设备标识字符串,直接找到具体的设备,并定位到具体的操作函数,因此,一般情况下,操作系统会单独维护另外一个数据库,这个数据库是由一系列的设备标识字符串加上对应的设备驱动程序操作函数所组成的对象的集合,比如,下列就是一个典型的数据库元素: DeviceIdentifyString ReadOperations; WriteOperations; ResetOperations; OtherOperations. 其中,DeviceIdentifyString是设备的标识字符串,而接下来的一系列函数指针,则是对应驱动程序所提供的功能函数的地址。这样用户在访问具体的设备的时候,就可以通过设备字符串很容易的定位到上述数据库元素,并根据操作需求,定位到具体的操作函数。比如,用户发出一个操作请求: OperateRequest(“Ethernet0”,SendPacket,…); 这样操作系统就可以查找上述数据库(根据“Ethernet0”,找到以后,并根据操作类型(SendPacket),来定位到具体的函数,然后以剩下的参数为调用参数,来调用SendPacket函数。 下列的图形,示例了设备标识字符串数据库的格式: 图10-2 设备标识字符串数据库格式 对于上述数据库(链表)中的每个元素(结构),我们称为设备对象,这个存放设备对象的数据库(一般情况下,采用链表进行存放,因此有时候也称为“设备对象链表”),一般称为设备对象数据库。 显然,上述实现方式中,一个很重要的问题就是,针对不同的物理设备,需要定义不同的操作函数,这样在实现起来,显然是十分困难的,而且几乎是不可能的,因为操作系统无法预先知道所有的硬件设备。为了克服这个问题,操作系统对物理设备进行了抽象,抽象出了一组通用的函数,来操作所有的设备,其中,最典型的两个抽象出来的操作,就是Read和Write,这样任何设备的驱动程序,只需要支持通用的抽象操作(其实是把设备特定的操作,以通用操作的函数原型来实现)即可,比如,针对物理硬盘和Ethernet网卡,都支持Read和Write操作,对于硬盘,在这两个操作中,只需要完成通常的读和写操作即可,但对于Ethernet网卡,则需要在读操作中,实现ReceivePacket功能,而在写操作中,实现SendPacket功能。这样实现后,对于用户程序的接口,也不用提供一个抽象的“OperateRequest”函数了,只需要提供有限的跟抽象操作对应的函数即可,比如,提供给用户一个Read和Write函数,这两个函数跟物理设备对应的驱动程序提供的操作相对应,每当用户针对特定的设备,调用这两个函数的时候,操作系统就会把这种调用映射到对应设备驱动程序的相应函数,从而实现设备的透明访问。一个比较典型的例子就是,Windows操作系统提供的ReadFile函数和WriteFile函数,这两个函数不但可以用于完成文件的读写操作,也可以完成设备的读写操作,实际上,在对物理设备(非文件)调用这两个函数的时候,操作系统就把这些函数的调用,传递到了设备驱动程序相关函数的调用上。 到此为止,用户就可以很容易的访问硬件设备了,比如,用户调用Read(“Harddisk0”,…)(函数参数中,省略的部分为传递的参数),操作系统就会根据设备标识字符串“Harddisk0”查找设备对象链表,找到“Harddisk0”对应的设备对象,然后以用户传递过来的参数为参数(或稍做调整),调用设备驱动程序提供的Read函数。 一切似乎都很完善,但细心的读者可能发现,对于任何针对设备的操作,如果按照上述形式,都需要操作系统完成一个字符串查找工作(根据函数提供的设备标识字符串,查找设备对象数据库),这显然是十分低效的,尤其是设备操作十分频繁的时候。目前,大多数操作系统都提供一个打开(Open)操作,用户在这个函数调用中,指定设备标识字符串作为参数,函数返回的时候,返回一个句柄(Handle),在实现上,这个句柄可能是设备对象的指针,或者其它可以快速检索到设备对象的数据,而后续操作(比如Read、Write等),则不必提供设备标识字符串,而使用Open函数返回的句柄作为参数,这样操作系统就可以省略查找过程,而直接通过句柄快速定位到设备对象,从而调用设备对象的相关操作。比如,对一个物理硬盘的访问,遵循下列顺序: HANDLE hHardDisk = NULL_HANDLE; hHardDisk = Open(“Harddisk0”,…); if(NULL_HANDLE == hHardDisk) //Can not open this device. return FALSE; Read(hHardDisk,…); //Read device using handle. Close(hHardDisk); //Close this device. 在对设备的操作完成之后,为保险或节约系统资源起见,一般需要采用Close函数(由操作系统提供)关闭打开的设备。 显然,这样对设备的访问,就十分完善了,如果读者对Windows API十分熟悉,通过上面的叙述,就应该对Windows操作系统提供的CreateFile、ReadFile、WriteFile、CloseHandle等函数的实现机制,有一定了解了,这些函数,分别与上面介绍的Open、Read、Write、Close对应。 最后补充一点,引入设备对象数据库(设备对象链表)的另外一个目的,是用于存储多设备实例情况下,单个设备实例的特定状态数据。比如,计算机系统配备了两个IDE接口的物理硬盘,由于这两个物理硬盘都是IDE接口,因此只需要一个IDE驱动程序即可,这样为了存储这两个物理硬盘的相关信息,就可以创建两个设备对象,这两个设备对象分别具有不同的标识字符串(比如,“IDEHD0”和“IDEHD1”),以及不同的状态参数(比如,当前磁头的位置、当前需要读写的扇区个数等),但这两个设备对象提供的操作函数,却是一样的,都是IDE接口硬盘驱动程序提供的参作函数。下面是设备对象数据库的一个更详尽的示例: 图10-3 一个设备对象数据库的例子 在这个例子中,所有的设备对象被存储在一个链表数据结构中,第一个设备对象“Harddisk0”,是一个硬盘设备,HDRead和HDWrite函数是硬盘驱动程序提供的操作方法,这两个操作方法的原型,必须与操作系统抽象的一致,而COM1和COM0则是两个串口对象,这两个物理设备对象采用同一个设备驱动程序(COM通信端口驱动程序)提供的操作方法,需要注意的是,ComRead和HDRead的函数原型(参数)必须一样,都必须与操作系统抽象的操作函数保持一致。 综上所述,为了对计算机系统中的设备进行有效管理,操作系统一般情况下需要维护两套数据结构(数据库):硬件信息数据库和设备对象数据库(设备对象链表),其中,硬件信息数据库是操作系统在引导的时候,通过收集硬件信息而建立,用于维护系统中的硬件配置信息以及硬件物理参数,而设备对象数据库,则是由操作系统建立,用于完成特定的设备跟其操作方法(驱动程序)的关联,并提供设备标识字符串,用以标识设备,还用来保存设备运行过程中的状态信息。 Hello China的设备管理机制 为了对物理设备进行管理,一般情况下,操作系统需要维护两个数据库:硬件信息数据库和设备对象数据库,并且提供对这两个数据库的相应操作,比如在数据库中添加记录、删除记录等维护工作。根据面向对象的思想,一个比较简便的实现形式就是,创建两个对象,分别对应上述两个数据库的数据结构以及操作(方法)。 在Hello China的实现中,物理设备管理模块就是遵循这种思路实现的。在当前版本中,实现了下列两个对象:设备管理器对象和IO管理器对象,其中设备管理对象用于管理物理设备的硬件信息数据库,而IO管理器对象则用于完成设备对象数据库的维护。 设备管理器(DeviceManager) 设备管理器对象的定义如下: BEGIN_DEFINE_OBJECT(__DEVICE_MANAGER) #define MAX_BUS_NUM 16 __SYSTEM_BUS SystemBus[MAX_BUS_NUM]; __RESOURCE FreePortResource; __RESOURCE UsedPortResource; BOOL (*Initialize)(__DEVICE_MANAGER*); __PHYSICAL_DEVICE* (*GetDevice)(__DEVICE_MANAGER*, DWORD dwBusType, __IDENTIFIER* lpIdentifier, __PHYSICAL_DEVICE* lpStart); __RESOURCE* (*GetResource)(__DEVICE_MANAGER*, DWORD dwBusType, DWORD dwResType, __IDENTIFIER* lpIdentifier); BOOL (*AppendDevice)(__DEVICE_MANAGER*, __PHYSICAL_DEVICE*); VOID (*DeleteDevice)(__DEVICE_MANAGER*, __PHYSICAL_DEVICE*); BOOL (*CheckPortRegion)(__DEVICE_MANAGER*, __RESOURCE*); __RESOURCE* (*ReservePortRegion)(__DEVICE_MANAGER*, __RESOURCE*, DWORD dwLength); VOID (*ReleasePortRegion)(__DEVICE_MANAGER*, __RESOURCE*); END_DEFINE_OBJECT() 这个对象维护了一个类型是__SYSTEM_BUS的数组(目前定义该数组的大小是16),这个数组用于存储系统中配置的所有总线,对于总线上的物理设备,都是以物理设备对象(__PHYSICAL_DEVICE)的形式,连接到系统总线对象上。上述对象也对系统资源进行了统一管理,FreePortResource和UsedPortResource两个数组,用于记录当前空闲的IO端口号范围和已经占用的IO端口号范围,对于中断和虚拟内存空间的管理,为了实现上的方便,在另外的两个对象(__SYSTEM对象和__VIRTUAL_MEMORY_MANAGER对象)中进行管理,在此不做赘述。 这个对象还提供了用于操作物理设备信息数据库的方法,比如AppendDevice、DeleteDevice、ReservePortRegion等,这些函数完成物理设备的添加、删除、端口号的预留等工作,这些函数的目的,是为设备驱动程序提供接口,让设备驱动程序能够自主的配置硬件(比如,为硬件保留IO端口信息),这些设备不支持自动配置功能,因此需要设备驱动程序自己完成配置工作。而对于支持自动配置功能的设备,比如PCI总线接口的设备,其配置工作无需调用这些函数,而是在Initialize函数中,被该对象一并检测并完成配置,因此,Initialize函数的功能比较复杂,该函数检测系统中的所有总线,并完成总线上设备的枚举和配置,一旦该函数成功执行完毕,物理硬件数据库(实际上是SystemBus数组)中就建立了全面的物理硬件拓扑结构,以及物理硬件的配置信息,当然,这个过程无法检测到不支持自动配置的物理设备,对于不支持自动配置的物理设备,需要驱动程序的初始化代码调用AppendDevice、ReservePortRegion等函数,手工的添加到物理设备信息数据库中。 Initialize函数成功执行完毕之后,就可以根据收集到的物理设备信息,加载驱动程序了,由于Hello China的设计目标是一个嵌入式的操作系统,系统中的硬件事先已经配置好,设备驱动程序也一般会跟操作系统核心一起编译连接,因此,在当前的实现中,无需实现驱动程序加载功能(从存储设备上读取驱动程序,并加载到内存中),但如果将来需要实现这项功能,也比较方便,就是在Initialize函数中,另外添加部分代码,完成物理设备驱动程序的加载工作。 对于__DEVICE_MANAGER对象的详细信息,请参考“系统总线管理”章节。 I/O管理器(IOManager) IO管理器用于管理设备对象数据库(设备对象列表),并提供一组函数(方法),用于操作这个数据库,比如,提供了设备对象的创建、销毁等函数,供硬件驱动程序调用。对于设备驱动程序的管理,也是由IOManager进行,在当前版本的Hello China的实现中,设备驱动程序的加载,采用静态方式,即设备驱动程序代码和操作系统核心连接到一起,作为一个统一的软件模块,加载到目标系统中,这是因为Hello China的设计目标,是一个嵌入式操作系统。但这不影响驱动程序的管理构架,也就是说,即使是采用动态加载驱动程序的方式,也遵循同样的驱动程序管理构架。当前版本的实现中,对于驱动程序的管理,按照下列方式进行: 1、 IOManager针对每个驱动程序,创建一个驱动程序对象(__DRIVER_OBJECT),初始化,然后以该对象为参数,调用驱动程序提供的DriverEntry函数(每个驱动程序必须输出一个DriverEntry函数,作为驱动程序的入口函数); 2、 驱动程序使用输出的操作函数(Read、Write等),填写驱动程序对象的相关成员变量; 3、 驱动程序在DriverEntry中,检查系统中对应的物理设备(通过调用DeviceManager提供的GetDevice函数),针对每个自己支持的设备,驱动程序必须创建一个设备对象(__DEVICE_OBJECT,通过调用IOManager提供的函数创建),并初始化该物理设备对象,比如赋予物理设备对象标识字符串等; 4、 第三步完成之后,由驱动程序创建的物理设备对象,就会插入设备对象链表(由IOManager维护)中,一旦插入设备对象链表,就对应用程序可见了,应用程序就可以采用Open系统调用,打开这个设备,并调用Read、Write等函数,对设备进行操作了。 另,该对象也提供了应用程序调用的标准接口,比如Read、Write等,这些函数跟驱动程序实现的一组标准接口对应,用户调用这些函数的时候,IOManager会把用户的调用,映射到驱动程序提供的相应函数上,对于调用的参数,直接从用户调用的参数传递到驱动程序提供的函数里,或者稍做调整,然后再传递到驱动程序提供的函数上。 Hello China的设备管理框架 在Hello China的实现中,对于设备的管理,是按照下列策略来实现的: 1、 采用一个统一的设备管理对象(IOManager),来集中管理所有的设备、设备驱动程序以及系统资源,并实现系统资源的分配和回收,以及设备驱动程序的加载和卸载; 2、 在设备管理对象和设备驱动程序之间定义了一个标准的交互接口,设备驱动程序和IOManager就是通过这个规定好的接口进行通信; 3、 在设备管理对象和用户线程(核心线程)之间,也定义了一个良好的交互接口,用户线程(核心线程)直接调用IOManager的用户侧接口,请求设备管理对象的服务(最终实现对设备的功能调用); 4、 把文件系统的实现也纳入设备管理框架里面,对文件的访问,也是通过IOManager的用户侧接口进行的; 5、 文件系统在实现的时候,也作为驱动程序来实现,遵循设备驱动程序的体系结构,也遵循设备驱动程序与操作系统的通信机制; 6、 设备驱动程序代码和IOManager代码必须是可重入的,即多个用户线程可以同时调用同一个设备驱动程序功能函数(或IOManager函数),而不会发生不一致的资源访问问题。为了实现这个功能,需要在IOManager的实现中,引入互斥机制,在设备驱动程序的实现中,需要考虑自己可能管理多个设备的情况,并为每个设备建立一套单独的数据环境,来充分保证代码的可重入性; 7、 实现设备的动态发现和枚举,比如,针对PCI总线,IOManager可以动态的发现连接在总线上的设备,并为之分配系统资源(中断号、端口号、内存映射区域等),动态加载这些设备的驱动程序; 8、 即插即用(PnP),实时监视总线状态,对于实时出现在总线上的设备,IOManager会及时做出响应,比如分配资源、加载驱动等,对于从总线上实时拆离的设备,IOManager会及时卸载掉已经加载的驱动程序,并释放这些设备所占用的资源。 上述两点功能,根据最新设计,将在DeviceManager(设备管理器)中实现。 在满足上述功能模型的情况现下,Hello China的设备管理框架如下所示: 图10-4 Hello China的设备管理框架 可以看出,在整个设备管理框架中,IOManager成为核心部件,IOManager提供了两个的接口,对于用户核心线程,称为用户接口(或上行接口,图中蓝色箭头表示),对于设备驱动程序,称为设备接口(或下行接口,图中红色箭头表示),用户线程通过用户接口调用IOManager,进而获得设备服务,设备驱动程序通过设备接口,来调用IOManager提供的服务,或通知IOManager自己的存在。 需要注意的是,设备驱动程序之间也可能相互调用彼此的服务(图中紫色的双向箭头),这种情况的一个典型应用,就是文件系统的实现。比如,用户通过IOManager提供的调用接口,来访问一个文件(打开、读写等),IOManager把这种调用转化为对相应文件系统的调用,相应的文件系统在完成内部表格的修改后,需要对实际的物理存储设备进行操作,这个时候,文件系统驱动程序(在Hello China的实现中,文件系统作为一种特殊的驱动程序来实现)就需要调用实际的物理设备驱动程序,来对实际的物理设备进行操作。 对于中断管理,设备驱动程序在获得系统为自己分配的资源(中断号、端口号、内存映射区域、DMA通道等)后,可以以中断号为参数,调用ConnectInterrupt函数(该函数由核心提供),直接注册自己的中断处理函数,当然,在设备卸载的时候,系统也可以调用DisconnectInterrupt函数,解除自己注册的中断调用,从而释放中断资源。 需要注意的是,一个设备驱动程序,可能管理多个设备(或逻辑的设备功能),设备和设备驱动程序之间的交互接口,由设备驱动程序实现,在IOManager中不作任何假设,另外,之所以把设备和设备驱动程序之间的交互表示为双向(图中的双向黑色箭头),是因为设备可能通过中断的方式,跟设备驱动程序交互。 在下面的各部分中,我们详细介绍该管理框架中涉及到的模块,以及模块之间的接口。 I/O管理器(I/OManager) IO管理器(IOManager)是系统中的全局对象之一,整个系统中,只存在一个这样的对象,该对象提供了面向应用的接口,比如CreateFile,ReadFile等函数,供用户线程调用,来访问具体的设备,该对象还提供了面向设备驱动程序的接口,供设备驱动程序调用,完成诸如创建设备、预留资源、销毁设备等操作。 系统中所有加载的设备驱动程序都归该对象管理,系统中所有用户可以使用的设备,也归该对象管理,因此,该对象可以认为是设备管理框架的核心对象。 驱动程序对象和设备对象 在Hello China的实现中,对于每个加载的设备驱动程序,系统都为之创建一个驱动程序对象,并调用驱动程序的DriverEntry函数(参考本文中设备驱动程序一节)来初始化这个驱动程序对象。驱动程序对象保存了对设备进行操作的所有函数,比如对设备的读函数、对设备的写函数等。 驱动程序对象可以理解为管理设备驱动程序的数据结构,而设备对象则对应于具体的物理设备,即设备对象是对物理设备进行直接管理的数据结构。设备对象由驱动程序创建,一般情况下,是在设备驱动程序加载并初始化的时候创建,一个比较合适的时机,就是在DriverEntry函数中创建。 在设备对象中,有一个指向对应于该设备的设备驱动程序对象的指针,对于设备的所有操作,都是由驱动程序对象提供的函数完成的,通过设备对象指向驱动程序对象的指针,可以找到特定的设备操作函数,进而完成对设备的操作。 为了说明设备对象和设备驱动程序对象的关系,在这里以磁盘驱动程序为例,来说明整个流程: 1、 在系统启动的时候,根据配置文件,或总线检测结果,加载硬盘驱动程序; 2、 完成驱动程序的加载(加载过程包括读入驱动程序文件、重定位、根据文件头找到DriverEntry函数的入口地址等)后,IOManager创建一个设备驱动程序对象,并以该对象为参数,调用硬盘驱动程序的DriverEntry函数; 3、 驱动程序(实际上是DriverEntry函数)根据IOManager提供的配置,对设备进行检测,比如,系统中硬盘的数量、每个硬盘的分区情况等,都在这个检测过程中完成,实际上,检测是一个收集数据的过程; 4、 硬盘驱动程序根据收集的数据,比如,系统中的硬盘数量,以及每个硬盘的分区情况等,创建相应的设备对象(通过调用IOManager提供的CreateDevice函数),一般情况下,针对每个硬盘、每个硬盘分区,分别创建设备对象,比如,假设系统中安装了一个硬盘,该硬盘划分了四个分区,则DriverEntry创建五个设备对象(分别为硬盘设备对象、分区一设备对象、分区二设备对象、分区三设备对象和分区四设备对象); 5、 上述步骤完成之后,硬盘就可以供具体的应用线程使用了。 比如,假设有一个用户线程读取硬盘数据,则具体的过程如下: 1、 用户调用ReadFile函数,发起一个硬盘读取请求(该函数的参数提供了硬盘对象设备对象的地址); 2、 IOManager根据ReadFile提供的设备对象的地址,找到该对象对应的驱动程序对象(设备对象保存了指向驱动程序对象的指针); 3、 IOManager创建一个DRCB对象(参考DRCB对象一节),初始化,然后调用驱动程序对象中,特定的函数(DeviceRead函数); 4、 该函数完成具体的硬盘读写操作,并返回; 5、 IOManager根据返回的结果,填充用户缓冲区,然后返回给用户线程。 需要指出的是,在调用ReadFile函数,读取设备的时候,需要首先打开设备(调用CreateFile函数)。 IOManager对设备对象和设备驱动程序的管理 在当前版本Hello China的实现中,所有驱动程序对象和设备对象,都是由IOManager直接管理的,在实现中,IOManager维护了两个双向链表,一个链表把系统中所有的驱动程序对象连接在一起,另一个链表把系统中所有的设备对象连接在一起,在IOManager的定义中,有两个成员变量: __DEVICE_OBJECT* lpDeviceRoot; __DRIVER_OBJECT* lpDriverRoot; 这两个变量指向两个双向链表的头节点。 整体构架请参考下图: 图10-5 Hello China的设备对象和设备驱动程序对象 IOManager的实现框架 IOManager的定义如下: BEGIN_DEFINE_OBJECT(__IO_MANAGER) __DEVICE_OBJECT* lpDeviceRoot; __DRIVER_OBJECT* lpDriverRoot; //Initializing routine. BOOL (*Initialize)(__COMMON_OBJECT* lpThis); //The interface(functions) to user kernel thread. __COMMON_OBJECT* (*CreateFile)(__COMMON_OBJECT* lpThis, LPSTR lpszFileName, DWORD dwAccessMode, DWORD dwOperationMode, LPVOID lpReserved); BOOL (*ReadFile)(__COMMON_OBJECT* lpThis, __COMMON_OBJECT* lpFileObj, DWORD dwByteSize, LPVOID lpBuffer, DWORD* lpReadSize); BOOL (*WriteFile)(__COMMON_OBJECT* lpThis, __COMMON_OBJECT* lpFileObj, DWORD dwWriteSize, LPVOID lpBuffer, DWORD* lpWrittenSize); VOID (*CloseFile)(__COMMON_OBJECT* lpThis, __COMMON_OBJECT* lpFileObj); BOOL (*IoControl)(__COMMON_OBJECT* lpThis, __COMMON_OBJECT* lpFileObj, DWORD dwCommand, DWORD dwInputLen, LPVOID lpInputBuffer, DWORD lpOutBufferLen, LPVOID lpOutBuffer); BOOL (*SetFilePointer)(__COMMON_OBJECT* lpThis, __COMMON_OBJECT* lpFileObj, DWORD dwWhereBegin, INT dwOffset); VOID (*FileFlush)(__COMMON_OBJECT* lpThis, __COMMON_OBJECT* lpFileObj); //The interface(functions) to device drivers. __DEVICE_OBJECT* (*CreateDevice)(__COMMON_OBJECT* lpThis, LPSTR lpszDevName, __COMMON_OBJECT* lpDrv, LPVOID lpDevExtension, /*__RESOURCE_DESCRIPTOR* lpResDesc, DWORD dwDevType, DWORD dwBlockSize*/ ); VOID (*DestroyDevice)(__COMMON_OBJECT* lpThis, __COMMON_OBJECT* lpDevObj); END_DEFINE_OBJECT() //End to define __IO_MANAGER. 可以看出,IOManager的定义比较复杂,涉及到比较多的函数,但这些对外函数(或接口)总体上可以分为三类: 1、 初始化函数(Initialize),系统初始化的时候,调用该函数初始化IOManager; 2、 对用户的接口,由用户调用,用来访问设备; 3、 对设备驱动程序的接口,由设备驱动程序调用,来获得IOManager的服务。 在下面的部分中,分别对这三类函数进行描述。 初始化函数(Initialize) 初始化函数(Initialize)用来进行一些初始化工作,在Hello China启动的时候调用,该函数完成一些预定的初始化工作,在目前的实现中,该函数完成下列功能: 1、 初始化设备驱动程序,当前版本的Hello China,设计目标为嵌入式操作系统,这样就不需要动态的加载设备驱动程序,设备驱动程序事先已经跟操作系统内核编译在一起了。但设备驱动程序所遵循的框架,也跟动态加载的设备驱动程序一致,不同的是,少了加载的步骤(动态加载设备驱动程序包括从存储设备读入驱动程序、重定位等步骤)。在IOManager的Initialize函数中,会调用每个连接到操作系统核心的设备驱动程序的DriverEntry函数; 2、 其它相关工作。 上述所有工作顺利完成之后,Initialize函数将返回TRUE,若该函数返回FALSE,会导致系统停止引导。 另外一个问题就是,对于跟操作系统核心连接在一起的驱动程序,Initialize函数如何确定其入口点(DriverEntry函数)。为了解决这个问题,当前版本的Hello China定义了下列一个数据结构: BEGIN_DEFINE_OBJECT(__DRIVER_ENTRY_MAP) LPSTR lpszDriverName; BOOL (*DriverEntry)(__DRIVER_OBJECT*); END_DEFINE_OBJECT() 并定义了一个全局数组: __DRIVER_ENTRY_MAP DriverEntryMap[] = { {“Ide hard disk”,IdeDriverEntry}, {“Mouse”,MouseDriverEntry}, {“Keyboard”,KeyboardDriverEntry}, {“Screen”,ScreenDriverEntry}, … …. … {NULL, NULL} }; 这样,在Initialize的实现中,就会遍历这个数组,对于数组中的每个元素,创建一个__DRIVER_OBJECT对象,然后调用对应的DriverEntry函数: BOOL IoMgrInitialize(__IO_MANAGER* lpThis) { BOOL bResult = FALSE; __DRIVER_OBJECT* lpDriver = NULL; DWORD dwLoop = 0L; … … while(DriverEntryMap[dwLoop].lpszDriverName) { lpDriver = ObjectManager.CreateObject(&ObjectManager, NULL, OBJECT_TYPE_DRIVER_OBJECT); if(NULL == lpDriver) //Failed to create driver object. goto __TERMINAL; if(!(DriverEntryMap[dwLoop].DriverEntry)(lpDriver)) //Failed to initialize driver. { PrintLine(“Unable to initialize driver.”); PrintLine(DriverEntryMap[dwLoop].lpszDriverName); } dwLoop ++ } … … bResult = TRUE; __TERMINAL: return bResult; } 因此,对于每个需要静态编联并加载的设备驱动程序,程序开发者都需要在DriverEntryMap数组中手工添加一条记录,该数组的最后一条空记录({NULL,NULL}),是该数组结束的标记。 虽然目前情况下,Hello China没有实现动态设备驱动程序的加载功能,但将来的时候,如果需要,可以按照下列思路,来实现动态设备驱动程序的加载功能: 通过另外一个帮助函数,GetDriverEntry,得到需要动态加载的设备驱动程序的入口地址,该函数原型如下: LPVOID GetDirverEntry(__DEVICE_VENDOR* lpDevVendor,LPSTR lpDrvName); 其中,lpDevVender指向一个设备厂家ID结构,该结构描述了IOManager想要加载的设备的厂家信息,而lpDrvName则指明了加载的设备驱动程序的名字(可以为空)。GetDriverEntry根据厂家信息,查询系统的一个配置文件,找到对应的驱动程序的文件名,然后调用ModuleManager的特定函数,ModuleManager根据文件名,在存储设备上找到合适的驱动程序,然后加载到内存(加载过程包括了重定位、名字解析、初始化等操作),并返回给加载模块的起始地址(返回给GetDriverEntry)。 其中,ModuleManager是模块管理器,用来完成把磁盘上的代码(可执行模块,比如动态链接库、应用程序可执行文件等)加载到内存中,并重定位等功能。 在实现动态设备驱动程序加载的时候,IOManager的Initialize函数,需要调用DeviceManager的相关函数,遍历系统中的硬件配置,对于检索到的每一个硬件,根据该硬件的__DEVICE_VENDOR标识,调用GetDriverEntry函数。 IOManager对应用的接口 IOManager提供了两个方向的接口:对应用程序的接口和对设备驱动程序的接口。其中,对应用程序的接口被应用线程调用,用来访问具体的设备,下列接口(函数)是对应用的接口(函数): · CreateFile,用于打开一个文件或设备,在Hello China当前版本的实现中,所有的设备和文件同等对待,都是用名字来标识,该函数既可以打开某一文件系统中的特定文件,也可以打开一个特定的物理设备; · ReadFile,从文件或设备中读取数据。在当前版本的实现中,该函数采用同步操作模式,即该函数一直等待设备操作完成,而不是中途返回(在Windows API中,实现了一种所谓的异步操作模式,即该函数向操作系统提交一个读取事务,然后直接返回,当操作系统完成事务指定的读写动作后,向发起事务的进程发送一个消息,进程处理该消息,最后完成读写操作),在这个过程中,调用该函数的线程可能被阻塞; · WriteFile,向设备或文件写入数据,实现机制与ReadFile类似; · CloseFile,CreateFile的反向操作,用于关闭CreateFile打开的设备或文件,在这个函数的实现中,如果操作目标是一个文件,则系统直接把相应的文件对象销毁,如果操作的对象是物理设备,则该对象不被销毁,而是递减对象的引用计数; · IOControl,完成设备驱动程序独特的操作,有些操作是不能通过Read、Write等来抽象的,比如针对音频设备的快进、重复播放等,系统提供了该函数,相当于提供了一个万能的接口给用户程序,用户程序可以通过该函数调用,完成任意驱动程序特定的操作功能; · SetFilePointer,移动文件的当前指针; · FlushFile,把位于缓冲区中的文件内容写入磁盘,一般情况下,文件系统的实现大量的使用了缓冲机制,即对文件的写操作先在内存中完成,然后积累到一定的程度后,再由设备驱动程序统一递交到物理设备,这样可以大大提高操作效率,但有的情况下,应用程序可能需要立即把改写的文件内容,写到物理存储设备上,比如,应用程序关闭的时候,这样就需要调用该函数来主动的同步缓存和物理存储介质,需要说明的是,CloseFile在实际关闭文件对象前,总是调用FlushFile来同步缓冲区和物理存储介质。 有的操作系统提供了LockFile函数,该函数用于把打开的文件加锁,实现互斥的访问,在Hello China当前的实现中,没有提供该函数功能,主要是考虑到该函数同能用途可能不是很大,而且可以通过一些替代方式来完成,比如,应用程序可以独占的打开一个文件,也可以在打开文件的时候,指定另外的打开标志,只允许其它应用程序只读的打开文件,等等。 下面对这些函数的实现,进行详细描述。 CreateFile的实现 CreateFile的函数原型如下: __COMMON_OBJECT* (*CreateFile)(__COMMON_OBJECT* lpThis, LPSTR lpszFileName, DWORD dwAccessMode, DWORD dwOperationMode, LPVOID lpReserved); 其中,第一个参数lpThis是一个指向IOManager全局对象的指针,第二个参数lpszFileName,则指明了要打开的设备或文件的名称,dwAccessMode和dwOperationMode两个参数,用于对打开的文件进行控制,对于目标对象是物理设备的情形不适用,最后一个参数保留,用于将来使用。不同的命名规则,用来标识打开的目标对象是设备还是文件,对于设备,lpszFileName的形式遵循\\.\devname的规则,即开始是两个反向斜线,接着一个点号(或DEV字符串),后面再跟着一个反向斜线,最后是设备的名字(即设备标识字符串),而对于文件,则按照文件系统名称+文件路径+文件名的格式,比如“C:\HCN\DATA1.BIN”。详细的命名规则,请参考“设备对象命名规则”一节。 如果调用该函数打开物理设备,则该函数执行下列动作: 1、 从lpszFileName中提取设备标识字符串(lpszFileName头部包含了\\.\字符串,而这部分内容不属于设备标识字符串内容); 2、 以设备标识字符串检索设备对象(__DEVICE_OBJECT)链表,一旦找到匹配的设备对象,则停止查找; 3、 若找到匹配的物理设备对象,递增设备对象的引用计数(dwRefCounter),然后返回设备对象的指针; 4、 若不能查找到物理设备对象,则返回NULL,表示该函数操作失败。 调用该函数打开文件的相关过程,请参考“文件系统的实现”相关内容。 ReadFile的实现 ReadFile函数是IOManager提供给应用程序的一个最重要函数,所有对物理设备或文件的读取访问,都是通过该函数进行的,原型如下: BOOL (*ReadFile)(__COMMON_OBJECT* lpThis, __COMMON_OBJECT* lpFileObj, DWORD dwByteSize, LPVOID lpBuffer, DWORD* lpReadSize); 该函数用于从已经打开的设备(通过调用CreateFile函数)中读取部分数据,其中,lpFileObj参数指定了打开的设备,dwByteSize指定了读取的字节数,而lpBuffer则是一个缓冲区指针,从设备中读取的数据,将被存储在该缓冲区内。若该函数操作成功,则返回TRUE,实际读取的字节数在lpReadSize参数中返回,若该函数操作失败,则返回FALSE。 在CreateFile函数的描述中,我们知道,lpFileObj实际上是一个设备对象的地址,而设备对象包含了一个指向驱动程序对象的指针,通过这个指针,可以直接找到驱动程序提供的DeviceRead函数,并调用这个函数。这个过程如下: … … __DRIVER_OBJECT* lpDrvObj = NULL; lpDrvObj = (__DEVICE_OBJECT*)lpFileObj->lpDriverObject; bResult = lpDrvObj->DeviceRead((__COMMON_OBJECT*)lpDrvObj, (__COMMON_OBJECT*)lpFileObj, lpDrcb); … … 在DeviceRead函数的参数中,第一个是指向驱动程序对象的指针,第二个则是设备对象的指针,因为特定的设备状态,都是存储在设备对象中,而DeviceRead函数需要这些状态数据。第三个参数是一个__DRCB对象,该对象用于跟踪设备请求操作。对于__DRCB对象的初始化过程如下: __DRCB* lpDrcb = NULL; lpDrcb = ObjectManager.CreateObject(&ObjectManager, NULL, OBJECT_TYPE_DRCB); If(NULL == lpDrcb) //Can not create DRCB object. goto __TERMINAL; lpDrcb->dwDrcbStatus = DRCB_STATUS_PENDING; //Pend the DRCB object. lpDrcb->dwRequestMode = DRCB_REQUEST_MODE_READ; //Read operation. lpDrcb->dwOutputLen = dwByteSize; lpDrcb->dwOutputBuffer = lpBuffer; 在上述代码中,把DRCB对象的状态(dwDrcbStatus)初始化为DRCB_STATUS_PENDING,因为该DRCB对象即将被排队(一旦调用DeviceRead函数,DRCB对象就会被排入驱动程序维护的读取或写入队列中),把DRCB对象的请求模式(dwRequestMode,即操作类型)初始化为DRCB_REQUEST_MODE_READ,初始化DRCB的输出缓冲区指针,以及缓冲区的大小,完成上述操作之后,就可以使用该DRCB对象为参数,调用DeviceRead函数了。 上面描述的是一种理想的情况,实际的操作,涉及到一个比较麻烦的问题,就是数据分片问题,比如,应用程序打开了一个以块形式进行操作的物理设备,比如IDE接口的硬盘,这时候,对硬盘的读取操作,必须是一块一块的进行,比如,以512字节为一个块单元。这样如果应用程序采用下列形式调用ReadFile函数: ReadFile((__COMMON_OBJECT*)&IOManager, lpFileObj, 1024, lpBuffer, &dwReadSize); 就会出现一个问题:应用程序请求的数据大小,比物理设备所支持的块大小要大。这种情况下,ReadFile就需要把这种大于设备物理块的请求,进行分片,依次调用DeviceRead函数,来完成读取操作。这样在ReadFile函数中,就需要进行额外的处理,来适应这种情况,这样,ReadFile的完整实现,如下: BOOL ReadFile(__COMMON_OBJECT* lpThis,__COMMON_OBJECT* lpFileObj, DWORD dwByteSize,LPVOID lpBuffer,DWORD* lpdwReadSize) { BOOL bResult = FALSE; __DRIVER_OBJECT* lpDrvObj = NULL; __DEVICE_OBJECT* lpDevObj = NULL; __DRCB* lpDrcb = NULL; DWORD dwBlockSize = 0L; LPVOID lpTmpBuffer = NULL; LPVOID lpTmp = NULL; DWORD dwOrginalSize = dwByteSize; if((NULL == lpThis) || (NULL == lpFileObj) || (0 == dwByteSize) || (NULL == lpBuffer)) goto __TERMINAL; lpDevObj = (__DEVICE_OBJECT*)lpFileObj; lpDrvObj = lpDevObj->lpDriverObject; dwBlockSize = lpDevObj->dwMaxReadSize; dwByteSize = (0 == dwByteSize % dwBlockSize) ? dwByteSize : (dwByteSize + dwBlockSize - (dwByteSize % dwBlockSize)); lpDrcb = ObjectManager.CreateObject(&ObjectManager, NULL, OBJECT_TYPE_DRCB); if(NULL == lpDrcb) //Can not create DRCB object. goto __TERMINAL; lpTmpBuffer = MemAlloc(KMEM_SIZE_TYPE_ANY,dwByteSize); if(NULL == lpTmpBuffer) goto __TERMINAL; lpTmp = lpTmpBuffer; //In order to destroy this buffer,first save it,because the lpTmpBuffer maybe changed in this routine. if(lpdwReadSize) *lpdwReadSize = 0; while(dwByteSize) { lpDrcb->dwStatus = DRCB_STATUS_PENDING; lpDrcb->dwRequestMode = DRCB_REQUEST_MODE_READ; lpDrcb->dwOutputLen = dwBlockSize; lpDrcb->lpOutputBuffer = lpTmpBuffer; lpTmpBuffer += dwBlockSize; //Adjust the buffer. if(!lpDrvObj->DeviceRead((__COMMON_OBJECT*)lpDrvObj, (__COMMON_OBJECT*)lpDevObj, lpDrcb)) break; dwByteSize -= dwBlockSize; //Read next block. *lpdwReadSize += dwBlockSize; } if(0 == dwByteSize) //Read successfully. { MemCopy(lpBuffer,lpTmp,dwOriginalSize); //Save the read data to application’s buffer. *lpdwReadSize = dwOriginalSize; bResult = TRUE; goto __TERMINAL; } if(0 == *lpdwReadSize) //Can not read any data. goto __TERMINAL; // //Read several blocks,but not all acquired blocks,in case of reach the end of device. // MemCopy(lpBuffer,lpTmp,*lpdwReadSize); //Save the read data to applications’s buffer. bResult = TRUE; goto __TERMINAL; __TERMINAL: if(!lpDrcb) ObjectManager.DestroyObject(&ObjectManager, (__COMMON_OBJECT*)lpDrcb); //Destroy DRCBobject. if(lpTmp) KMemFree((LPVOID)lpTmp,KMEM_SIZE_TYPE_ANY,0L); return bResult; } 在开始处,该函数首先把用户请求的数据大小,折合成设备支持的块大小(dwBlockSize变量)的整数倍(大于用户请求的大小的整数倍),然后进入一个循环,一次读取一个设备数据块。 如果一切顺利,while循环会在dwByteSize为0的时候退出,这时候读取的数据的大小,就是根据用户请求的尺寸调整后的数据大小,这是一种最常见的情况,也是一种最理想的情况,这种情况下,DeviceRead函数一次也不会失败。但很可能出现这样一种情况,就是用户请求的数据的大小尚未读取完毕,设备上的数据已经没有了,或者已经达到了文件的末尾。这种情况下,DeviceRead函数会失败,从而导致while循环退出,但这种情况也不是一种失败的情况,ReadFile函数仍然会返回部分数据,lpdwReadSize指针指向的整数变量,便存储了实际读取的数据。 如果DeviceRead函数一次都没有执行成功,这种情况我们认为失败,ReadFile函数会返回FALSE。 WriteFile的实现 WriteFile函数用来被应用程序调用,用于从设备(或文件)中写入一定数量的字节。该函数的原型如下: BOOL (*WriteFile)(__COMMON_OBJECT* lpThis, __COMMON_OBJECT* lpFileObj, DWORD dwWriteSize, LPVOID lpBuffer, DWORD* lpWrittenSize); 其中,lpThis参数指向IOManager,lpFileObj是一个指向打开的设备对象(或文件对象)的指针,dwWriteSize参数指定了希望写入目标设备或文件的字节的数量,而lpBuffer参数则存储了具体的写入内容。lpWrittenSize参数则是一个返回参数,函数成功返回(返回TRUE)后,该参数指定了实际写入的字节数量。正常情况下,可能该参数的返回值等于dwWriteSize,但如果出现设备已经满,或者到达文件结尾的情况,则可能只写入了一部分内容,这个时候,lpWrittenSize参数告诉调用者,多少数据被写入了目标设备或文件。 与ReadFile函数一样,该函数通过lpFileObj参数,找到该设备对象所对应的设备驱动程序对象,然后调用设备驱动程序对象提供的DeviceWrite函数,如下: __DRIVER_OBJECT* lpDrvObj = NULL; lpDrvObj = (__DEVICE_OBJECT*)lpFileObj->lpDriverObject; bResult = lpDrvObj->DeviceWrite(lpDrvObj,lpFileObj,lpDrcb); 其中,lpDrcb是一个指向DRCB对象的指针,该对象被WriteFile函数创建,用于传递参数、跟踪请求过程。代码如下: __DRCB* lpDrcb = NULL; lpDrcb = ObjectManager.CreateObject(&ObjectManager, NULL, OBJECT_TYPE_DRCB); If(NULL == lpDrcb) //Can not create DRCB object. goto __TERMINAL; lpDrcb->dwDrcbStatus = DRCB_STATUS_PENDING; //Pend the DRCB object. lpDrcb->dwRequestMode = DRCB_REQUEST_MODE_WRITE; //Read operation. lpDrcb->dwInputLen = dwWriteSize; lpDrcb->dwInputBuffer = lpBuffer; 与ReadFile不同的是,DRCB对象的dwRequestMode被初始化为DRCB_REQUEST_MODE_WRITE,用来指明这是一个写操作。 与ReadFile一样,WriteFile也会存在写入字节数量跟目标设备块大小不一致,需要协调的情况,也一样会出现不完全写入(只写入一部分)的情况,因此,WriteFile也必须象ReadFile一样,综合考虑这些情况。下面是WriteFile的完整实现代码: BOOL WriteFile(__COMMON_OBJECT* lpThis,__COMMON_OBJECT* lpFileObj, DWORD dwWriteSize,LPVOID lpBuffer,DWORD* lpdwWrittenSize) { BOOL bResult = FALSE; __DRIVER_OBJECT* lpDrvObj = NULL; __DEVICE_OBJECT* lpDevObj = NULL; __DRCB* lpDrcb = NULL; DWORD dwBlockSize = 0L; LPVOID lpTmpBuffer = NULL; LPVOID lpTmp = NULL; DWORD dwOrginalSize = dwWriteSize; if((NULL == lpThis) || (NULL == lpFileObj) || (0 == dwWriteSize) || (NULL == lpBuffer)) goto __TERMINAL; lpDevObj = (__DEVICE_OBJECT*)lpFileObj; lpDrvObj = lpDevObj->lpDriverObject; dwBlockSize = lpDevObj->dwMaxWriteSize; dwWriteSize = (0 == dwWriteSize % dwBlockSize) ? dwWriteSize : (dwWriteSize + dwBlockSize - (dwWriteSize % dwBlockSize)); lpDrcb = ObjectManager.CreateObject(&ObjectManager, NULL, OBJECT_TYPE_DRCB); if(NULL == lpDrcb) //Can not create DRCB object. goto __TERMINAL; lpTmpBuffer = MemAlloc(KMEM_SIZE_TYPE_ANY,dwWriteSize); if(NULL == lpTmpBuffer) goto __TERMINAL; lpTmp = lpTmpBuffer; //In order to destroy this buffer,first save it,because the lpTmpBuffer maybe changed in this routine. if(lpdwReadSize) *lpdwReadSize = 0; MemCopy(lpTmpBuffer,lpBuffer,dwOriginalSize); //Copy the content to be written to a new buffer. while(dwWriteSize) { lpDrcb->dwStatus = DRCB_STATUS_PENDING; lpDrcb->dwRequestMode = DRCB_REQUEST_MODE_WRITE; lpDrcb->dwInputLen = dwBlockSize; lpDrcb->lpInputBuffer = lpTmpBuffer; lpTmpBuffer += dwBlockSize; //Adjust the buffer. if(!lpDrvObj->DeviceWrite((__COMMON_OBJECT*)lpDrvObj, (__COMMON_OBJECT*)lpDevObj, lpDrcb)) break; dwWriteSize -= dwBlockSize; //Read next block. *lpdwWrittenSize += dwBlockSize; } if(0 == dwWriteSize) //Write successfully. { *lpdwReadSize = dwOriginalSize; //Set returned written size to original request size. bResult = TRUE; goto __TERMINAL; } if(0 == *lpdwReadSize) //Can not read any data. goto __TERMINAL; // //Written several blocks,but not all acquired blocks,in case of reach the end of device. // bResult = TRUE; goto __TERMINAL; __TERMINAL: if(!lpDrcb) ObjectManager.DestroyObject(&ObjectManager, (__COMMON_OBJECT*)lpDrcb); //Destroy DRCBobject. if(lpTmp) KMemFree((LPVOID)lpTmp,KMEM_SIZE_TYPE_ANY,0L); return bResult; } 该函数的实现中,首先把要写入的字节数,舍入到目标设备的最大写入块的倍数,然后创建一个新的缓冲区,并把原始缓冲区的内容,拷贝到新的缓冲区,之所以这样做,是因为实际在把写入字节舍入为块的倍数的时候,可能会比原始请求的字节数大(比如,原始请求写入的字节数为108,而块设备的最大写入块大小为256,则舍入的写入块大小将为256)。这种情况下,若仍然使用原来的缓冲区,则可能会出现“超读”的现象,假设即原始缓冲区大小为108字节,而实际写入的时候,需要读取256字节(一个块),这样可能会出现异常。因此,需要WriteFile自己创建一个跟舍入写入尺寸相同的缓冲区,然后把用户请求数据复制到自己创建的缓冲区,在实际写入(调用DeviceWrite函数)的时候,直接从WriteFile自己创建的缓冲区内读取,而不用读取调用者提供的缓冲区。 CloseFile的实现 CloseFile函数用来关闭打开的设备或文件,与CreateFile的功能相反,该函数的原型如下: VOID (*CloseFile)(__COMMON_OBJECT* lpThis, __COMMON_OBJECT* lpFileObj); 在目前Hello China的实现中,该函数没有做很多的工作,而近近是把设备对象的引用计数递减,然后直接返回。实现如下: VOID CloseFile(__COMMON_OBJECT* lpThis,__COMMON_OBJECT* lpFileObj) { DWORD dwFlags = 0L; __DEVICE_OBJECT* lpDevObj = (__DEVICE_OBJECT*)lpFileObj; if(NULL == lpDevObj) return; __ENTER_CIRITCAL_SECTION(NULL,dwFlags); lpDevObj->dwRefCounter --; __LEAVE_CRITICAL_SECTION(NULL,dwFlags); return; } IOControl的实现 ReadFile和WriteFile可以用来完成对设备的读写操作,这对于大多数的存储设备,或许已经足够了,但对于一些其它类型的设备,只采用这两个函数抽象所有的操作,可能会力不从心,比如媒体播放设备,提供了快进、倒退功能,这样的功能,通过读写的操作,是无法实现控制的,因此还必须实现另外的接口。另外,还有一些功能未知的设备,操作系统无法事先预知设备功能,因此也就无法预先定义接口。这种情况下,就需要操作系统定义一种“透传”接口,把应用程序请求的操作,直接透传给设备驱动程序,操作系统不做任何功能上的操作。IOControl函数就是为这种需求而提出的,这个函数的原型如下: BOOL (*IoControl)(__COMMON_OBJECT* lpThis, __COMMON_OBJECT* lpFileObj, DWORD dwCommand, DWORD dwInputLen, LPVOID lpInputBuffer, DWORD lpOutputLen, LPVOID lpOutBuffer); lpFileObj函数是已经打开的设备对象,dwCommand函数则是设备特定的命令代码(操作系统对该参数的含义一无所知),lpInputBuffer和lpInputLen共同确定了一个输入缓冲区,作为dwCommand命令代码的输入参数,lpOutputLen参数和lpOutBuffer参数共同确定了一个缓冲区,该缓冲区用来保存输出结结果。 在Hello China当前的实现中,对于该函数,只做了一个参数转换功能(把输入的参数,通过一个DRCB对象统一管理),然后直接调用设备驱动程序对象提供的IOControl函数。代码如下: BOOL IOControl(__COMMON_OBJECT* lpThis, __COMMON_OBJECT* lpFileObj, DWORD dwCommand, DWORD dwInputLen, LPVOID lpInputBuffer, DWORD dwOutputLen, DWORD dwOutputBuffer) { __DEVICE_OBJECT* lpDevObj = (__DEVICE_OBJECT*)lpFileObj; __DRIVER_OBJECT* lpDrvObj = NULL; __DRCB* lpDrcb = NULL; if(NULL == lpDevObj) //Invalid parameter. return FALSE; lpDrcb = ObjectManager.CreateObject(&ObjectManager, NULL, OBJECT_TYPE_DRCB); if(NULL == lpDrcb) return FALSE; lpDrcb->dwStatus = DRCB_STATUS_PENDING; //The DRCB object will be pended. lpDrcb->dwRequestMode = DRCB_REQUEST_MODE_IOCONTROL; lpDrcb->dwCtrlCommand = dwCommand; lpDrcb->dwInputLen = dwInputLen; lpDrcb->lpInputBuffer = lpInputBuffer; lpDrcb->dwOutputLen = dwOutputLen; lpDrcb->lpOutputBuffer = lpOutputBuffer; lpDrvObj = lpDevObj->lpDriverObject; return lpDevObj->IoControl(lpDrvObj, lpDevObj, lpDrcb); } 为了处理上的简便,DRCB对象的定义中,专门预留了一个成员dwCtrlCommand,用来为IOControl函数服务,该变量直保存了IOControl调用者给定的操作命令。 SetFilePointer的实现 该函数用来指定新的读写位置,比如,在文件系统的实现中,对于每个打开的文件,都有一个当前位置指针与之对应,这样每次读取或写入一定数量的字节,指针跟着向前移动对应数量的字节,这样的实现是合理的。但有的时候,在写入文件或设备的时候,需要写入特定的位置,这样就需要通过该函数,来有目的的移动文件指针。该函数的原型如下: BOOL (*SetFilePointer)(__COMMON_OBJECT* lpThis, __COMMON_OBJECT* lpFileObj, DWORD dwWhereBegin, INT dwOffset); 参数中,lpFileObj是已经打开的设备或文件的指针,dwWhereBegin是一个起始位置标识符,可以取下列各值: · FILE_POSITION_CURRENT:从当前位置开始移动; · FILE_POSITION_BEGIN:从文件或设备的起始位置开始移动; · FILE_POSITION_END:从文件或设备的结束位置开始移动。 而dwOffset参数则指定了具体的移动字节数量,而移动的方向,则是从文件或对象的开始处,向结尾处移动。该参数可以取负值,若取负值,则移动的方向是从结尾方向向开始方向,而移动的偏移量,则是该参数的绝对值大小。 在Hello China当前的实现中,也是直接调用lpFileObj对应的设备驱动程序对象的DeviceSeek函数。 FlushFile的实现 FileFlush函数的目的,是完成缓冲区内容和实际存储设备内容的同步。比如,在存储设备驱动程序中,为了效率上的考虑,可能实现了一种“积累写”的功能,即把多个小规模的写操作请求进行合并,合并成一个写入字节较大的请求,然后一次写入物理介质。这样可以实现效率上的大幅度提升。按照这样的实现,一次写操作,可能并不会被真正执行,而是被作为合并对象,合并到内存中,这样在一些情况下,可能是不符合要求的,因此需要一种手段,来主动的告诉设备驱动程序,立即把需要写入的内容写入设备,完成数据同步。该函数就是实现这个目的的,函数原型如下: VOID (*FileFlush)(__COMMON_OBJECT* lpThis, __COMMON_OBJECT* lpFileObj); 在Hello China的实现中,IOManager没有做太多的工作,也是直接调用设备驱动程序对应的DeviceFlush函数,实现如下: VOID FileFlush(__COMMON_OBJECT* lpThis, __COMMON_OBJECT* lpFileObj) { __DEVICE_OBJECT* lpDevObj = (__DEVICE_OBJECT*)lpFileObj; __DRIVER_OBJECT* lpDrvObj = NULL; __DRCB* lpDrcb = NULL; if(NULL == lpDevObj) //Invalid parameter. return; lpDrvObj = lpDevObj->lpDriverObject; lpDrcb = ObjectManager.CreateObject(&ObjectManager, NULL, OBJECT_TYPE_DRCB); if(NULL == lpDrcb) //Can not create DRCB object. return; lpDrcb->dwStatus = DRCB_STATUS_PENDING; lpDrcb->dwRequestMode = DRCB_REQUEST_MODE_FLUSH; lpDrvObj->DeviceFlush(lpDrvObj, lpDevObj, lpDrcb); return; } IOManager对设备驱动程序的接口 下列函数供设备驱动程序调用: · CreateDevice,该函数创建一个设备对象,并根据函数参数完成初步的初始化功能,一般情况下,设备驱动程序加载完毕,进入初始化阶段(DriverEntry函数)之后,设备驱动程序会检测设备,根据检测结果,来创建相应的设备对象。比如,网卡驱动程序被加载之后,驱动程序会检测系统上是否安装了网卡,如果能够检测到网卡,那么驱动程序会创建一个网卡设备对象; · DestroyDevice,该函数销毁CreateDevice函数创建的设备对象。 为了进一步理解上述几个函数的功能,下面描述一个比较典型的设备驱动程序加载、初始化过程,假设设备驱动程序为硬盘驱动程序: 1、 操作系统加载硬盘驱动程序文件,并完成诸如重定位等工作; 2、 IOManager调用硬盘驱动程序的入口函数(DriverEntry),硬盘驱动程序进入初始化工作; 3、 在硬盘驱动程序的DriverEntry函数内部,会检测系统的硬盘安装情况,比如安装硬盘的个数、每个硬盘的分区情况等; 4、 根据检测结果,预留系统资源(通过调用ReserveResource函数),有的情况下,IOManager会通过DriverEntry函数传递给驱动程序相应的设备资源,这种情况下,驱动程序必须使用系统分配的资源,但也必须显示的通过ReserveResource预留系统分配的资源,算是一个资源确认操作; 5、 根据检测的结果,调用CreateDevice创建相应的设备对象; 6、 如果上述过程一切顺利,初始化结束,设备可以使用。 驱动程序入口(DriverEntry) 驱动程序被加载到内存后,IOManager首先通过某种方式(具体参考下面的章节),找到一个所谓“入口函数”的地址,然后调用这个函数。这个函数就是所谓的驱动程序“入口”。 驱动程序的入口原型如下: BOOL DriverEntry(__DRIVER_OBJECT* lpDriverObject,__RESOURCE_DESCRIPTOR* lpResDesc); 其中,lpDriverObject是IOManager创建的一个驱动程序对象,而lpResDesc则是描述系统资源的数据结构指针。 该函数的具体实现,是由驱动程序本身完成的,一般情况下,驱动程序可以在这个函数内初始化全局数据结构,创建设备对象(根据IOManager分配的系统资源),并设置驱动程序对象的一些变量(比如,各个函数指针等),如果该函数成功执行,那么返回TRUE,这个时候,IOManager就认为驱动程序初始化成功,否则,返回FALSE,那么IOManager就会认为驱动程序初始化失败,于是就卸载掉该驱动程序,释放创建的驱动程序对象。 设备驱动程序的卸载 所谓设备驱动程序卸载,指的是把不再使用的驱动程序从内存中删除掉,以释放内存,供其它应用程序适应。设备驱动程序的卸载发生在操作系统关闭、设备消失(被拔出等)等情况下,在设备驱动程序被卸载的时候,系统(IOManager)调用设备驱动程序的UnloadEntry函数,该函数释放驱动程序申请的资源。 从UnloadEntry返回后,IOManager会删除该驱动程序对应的驱动程序对象。 文件系统的实现 在Hello China驱动程序管理框架的实现中,把文件系统的实现也纳入驱动程序管理框架范围之内,把文件系统作为一种特殊的设备驱动程序来看待。实际上,IOManager提供给用户的接口(CreateFile、ReadFile等)可以直接用来打开、读写文件(从名字的命名上也可以看出)。 在本节中,我们对文件系统的实现,以及跟设备驱动程序管理框架的融合,进行详细的描述。 文件系统与文件的命名 在当前版本的Hello China的实现中,采用类Windows文件命名格式,即使用英文字母(A、B、C、… …)加上冒号(:)来标识一个物理卷,一个物理卷可以是一个硬盘的分区,也可以是一个硬盘,甚至可以是多个硬盘分区(或多个硬盘)的逻辑组合。比如,系统中第一个硬盘分区为C:,第二个硬盘分区为D:,第三个为C:,…… 对于分区上的文件,分两类对待:一类是目录,这类文件可以理解为一个容器,里面装载了文件,目录文件的内容即是容器内部文件的文件名,比如,一个目录下有三个文件: file1.dat file2.dat file3.dat 那么,该目录文件的文件内容可能是这个样子: 图10-6 目录文件的内容 即目录文件也作为普通文件对待,不同的是,目录文件的内容,就是存在于目录下面实际文件(也可能包含目录)的文件名(以及其它控制信息)的集合。 目录可以是嵌套的,比如,一个目录文件(假设为Directory1)下面,包含了另外三个文件和一个目录文件(假设为Directory2),Directory2下面又包含了一个数据文件file1.dat,而且假设这些文件都位于系统中第二个分区上(相应的表示符为D:),那么,file1.dat可以这样表示: D:\Directory1\Directory2\file1.dat 在当前版本的实现中,一个文件的名字,可以由字母和数字组成,也可以由汉字组成,文件名可以使用点(.)来分割成几个部分,最后一部分成为文件的扩展名,一般情况下,文件的扩展名不超过四个字节。当前情况下,下列字符不能出现在文件的命名中: / \ = * ? 文件系统驱动程序 在当前版本的Hello China的实现中,文件系统作为一种特殊类型的设备驱动程序来实现,这样文件系统就可以统一纳入IOManager的管理框架,而且对用户来说,与普通的设备驱动程序是一致的,所不同的是,文件系统驱动程序可能又调用了实际的存储设备驱动程序提供的功能,来访问存储设备驱动程序。 文件系统驱动程序的加载,是在普通设备驱动程序加载之后,再进行的。一般情况下,IOManager首先检测系统总线,判断哪些设备连接到了系统总线上,然后配置相应的资源,并加载设备驱动程序,等这个过程完成之后,IOManager会遍历所有加载的设备驱动程序创建的设备对象,来判断该设备对象是存储设备还是非存储设备。如果是存储设备,那么IOManager会调用适当的函数来读取该存储设备的一些头部信息(比如,一个硬盘分区的前几个扇区),根据存储设备的头部信息,IOManager就会判断出该存储设备被格式化成的文件系统格式(FAT、FAT32、NTFS、EXT2等),根据判断的结果,再加载合适的文件系统驱动程序。 IOManager在把文件系统驱动程序加载到内存之后,会使用合适的参数调用文件系统驱动程序的DriverEntry函数,给文件系统驱动程序一个机会,完成自己的初始化工作,然后,IOManager调用文件系统驱动程序提供的CreateFileSystem函数(每个文件系统驱动程序都需要提供该函数),来创建一个文件系统设备对象,一些存储设备相关的参数(比如,该存储设备位于系统中的第几块硬盘上,是该硬盘的第几个逻辑分区,该逻辑分区的起始扇区号、扇区数量、每扇区的大小等)会通过CreateFileSystem的参数传递给文件系统驱动程序,这样,文件系统驱动程序就会根据传递过来的资源,调用CreateDevice函数来创建一个设备对象(该设备对象的类型是DEV_TYPE_FILE_SYSTEM),这样就完成了该存储设备文件系统的初始化工作。 需要注意的是,IOManager只有在检测到后续同类型的存储设备的时候,就不需要再加载文件系统驱动程序了,因为该驱动程序已经被加载并初始化,IOManager只要直接调用CreateFileSystem函数,创建另外的文件系统设备对象即可。 打开一个文件的操作流程 在Hello China当前版本的实现中,把文件跟普通的设备同等对待,即每当打开一个文件,在操作系统核心内部,实际上是增加了一个设备对象(文件设备对象),对于文件的读写等操作,直接调用设备对象管理的驱动程序提供的服务函数。 用户通过调用IOManager提供的CreateFile函数来打开一个文件,该函数的参数指明了要打开的文件名、打开方式(只读、读写等)等,IOManager按下列步骤进行操作: 1、 首先根据文件名以及命名规范,来确定请求打开的对象是文件还是普通的设备对象(文件名是以文件系统标识符开头的,而设备对象则以字符串\\.\devicename开头); 2、 如果判断结果是普通的设备对象,则转到设备对象打开流程(参考其它章节); 3、 如果判断结果是文件对象,则IOManager首先遍历系统中已经打开的设备链表,用文件名来匹配每个设备名字,如果匹配成功,则说明了该文件已经打开,于是进一步检查该文件的打开方式(先前已经打开的方式),如果允许按照本次请求的打开方式重复打开,则直接给用户返回已经打开的文件对象的地址,如果不允许重复打开,则返回失败标志; 4、 如果遍历打开设备的链表后,没有找到对应的文件,则说明该文件没有被打开,于是从文件名中提取文件系统标识符(比如,C:,D:,E:等); 5、 根据文件系统标识符,来遍历打开的设备链表(实际上,在实现的时候,这两个遍历可以同步执行,在这里是为说明上的方便),尝试寻找对应的文件系统对象(文件系统也作为一个普通设备对象对待); 6、 如果不能找到对应的文件系统对象,则说明目前系统中不存在这个文件,返回失败标志; 7、 如果可以找到对应的文件系统对象,则IOManager调用文件系统对象的DeviceOpen函数(以文件名或DRCB为参数),来打开该文件; 8、 如果文件系统对象的DeviceOpen函数执行成功,则返回被打开文件的句柄,否则返回一个失败标志(NULL),在这里,文件系统对象进一步调用存储设备对象的相应函数,来读取实际的设备数据; 9、 CreateFile根据DeviceOpen函数的返回结果,来给初始调用返回适当的数值。 上面的描述,是为了方便理解起见,进行的一个简化版的描述,实际上,在实现的时候,上述所有过程只需要一个遍历操作就可以完成,在开始的时候,CreateFile首先确定打开的文件是文件,还是普通的设备,如果是普通的设备,则采用设备名直接查找设备链表,采用精确匹配,反之,如果是文件,则在查询设备链表的时候,采用的是最长匹配,因为这样可以大大提高打开效率。比如,设备链表中有下列已经打开的文件(或设备): E: E:\DATA\DATA1.BAT E:\DATA 这个时候,如果需要打开文件E:\DATA\DATA2.DAT,则最长匹配的结果为E:\DATA,于是系统调用这个设备对象的DeviceOpen函数,并以该对象的基地址为参数,来打开目标文件。再比如,如果要打开文件E:\SYS\DATA.DAT,则最长匹配的结果是E:(文件系统设备对象),然后调用这个设备对象的DeviceOpen函数,来打开目标文件。 设备驱动程序框架 设备请求控制块(DRCB) 设备请求控制块(Device Request Control Block)是Hello China的I/O管理框架中的核心数据结构(对象),该对象用来跟踪所有对设备的请求操作,一般由IOManager创建,然后通过设备驱动程序提供的服务函数(比如,DeviceRead、DeviceWrite等)传递给设备驱动程序,然后设备驱动程序根据DRCB里面的参数,就可以确定本次操作的一些特定数据,比如,设备读取的开始地址,读取数据的长度,以及数据读取后应存放的缓冲区位置等,可以认为,DRCB是Hello China设备驱动管理框架的核心。 该对象(数据结构)的定义如下: BEGIN_DEFINE_OBJECT(__DRCB) INHERIT_FROM_COMMON_OBJECT __EVENT* lpSynObject; //Synchronization object. __KERNEL_THREAD_OBJECT* lpKernelThread; //The kernel thread originates this I/O request. DWORD dwDrcbFlag; DWORD dwStatus; DWORD dwRequestMode; //Read,Write,Control,etc. DWORD dwCtrlCommand; //Output buffer’s information. DWORD dwOffset; DWORD dwOutputLen; LPVOID lpOutputBuffer; //Input buffer(parameters)’s information. DWORD dwInputLen; LPVOID lpInputBuffer; __DRCB* lpNext; __DRCB* lpPrev; DRCB_WAITING_ROUTINE WaitForCompletion; DRCB_COMPLETION_ROUTINE OnCompletion; //Called when request is completed. CRCB_CANCEL_ROUTINE OnCancel; //Called when request is canceled. DWORD DrcbExtension[0]; END_DEFINE_OBJECT(); 在这个对象的定义中,大多数成员意义很明确,下列三个数据成员需要着重说明一下: 1、 WaitForCompletion; 2、 OnCompletion; 3、 OnCancel。 这三个都是函数指针,指向了驱动程序管理框架实现的三个函数,这三个函数分别被驱动程序调用。其中,第一个函数(WaitForCompletion)用于等待请求的操作完成,比如,设备驱动程序根据DRCB对象提供的信息,提交了一个物理设备读取请求,由于这个物理设备的执行速度比CPU慢很多,因此,设备驱动程序不能采用等待的方式,来等待设备操作完成,而是采用一种中断的方式,即设备操作完成之后,通过中断的方式通知设备驱动程序,然后设备驱动程序再采取进一步的动作。而这个函数(WaitForCompletion)就是用于这个目的,该函数调用后,相应的用户线程(发起IO请求的线程)就会进入阻塞状态,直到对应的操作完成。这样做的好处是,大大节约了CPU的资源,如果不采用这种方式,而是采用一种忙等待的方式等待设备操作完成,那么会浪费很多CPU资源。 第二个函数是跟第一个函数对应的,这个函数在设备驱动程序完成设备操作后调用,一般情况下是在设备驱动程序的中断处理中调用的,这个函数执行后,就会唤醒原来等待IO操作完成的线程(投入到Ready队列),这样当下一次调度的时候,如果这个线程(发起IO请求的线程)的优先级足够高,那么该线程就可以继续执行了。 为了进一步理解上述过程和配合关系,我们举一个硬盘读写的例子进行说明,假设用户线程想读取一个文件,于是用户线程发起了一个ReadFile的函数调用,后续的执行过程如下: 1、 IOManager创建一个DRCB对象,根据ReadFile函数的参数对该对象进行初始化,然后再根据文件名,找到合适的文件对象(其实是一个设备对象),调用设备对象所对应的驱动程序(文件系统驱动程序)的相应函数(DeviceRead函数,以创建的DRCB对象为参数); 2、 文件系统程序根据ReadFile传递过来的DRCB对象,以及设备对象的设备扩展,找到实际的硬盘设备对象,然后文件系统驱动程序另外创建一个DRCB对象,初始化这个DRCB对象,再以这个新创建的DRCB对象为参数,来调用硬盘设备对象的DeviceRead函数; 3、 硬盘设备对象的DeviceRead函数根据传递过来的DRCB对象,初始化一个硬盘读请求事务,并把该DRCB对象放到驱动程序的等待队列中,然后调用DRCB对象中的WaitForCompletion函数; 4、 硬盘驱动器(控制器)执行实际的读操作,完成以后,给CPU发一个中断; 5、 中断调度机制根据中断号调用实际的中断处理函数(硬盘驱动程序的中断处理函数),中断处理函数从中断控制器读取数据,填充在DRCB指定的缓冲区内,然后把该DRCB对象从等待队列中删除,并调用OnCompletion函数; 6、 OnCompletion函数唤醒等待的线程(返回到硬盘驱动程序的DeviceRead函数继续执行),于是DeviceRead函数把从硬盘上读取的数据填充到IOManager发送过来的DRCB对象中,销毁自己创建的DRCB,并返回; 7、 IOManager把从硬盘读取的数据填充到用户线程指定的缓冲区内,然后从ReadFile函数返回。 可以看出,这个过程比较复杂,而且在上面的描述中,我们省略了数据尺寸不匹配的情况(比如,用户请求4K的数据,而硬盘驱动程序一次只能读取一个扇区的字节,一般情况下为512byte,这种情况下,就需要文件系统驱动程序对原始请求进行分割),实际情况可能比上述情况更加复杂。 最后一个函数用于取消一个IO请求。一般情况下,设备驱动程序可能维护了多个IO请求任务(比如,系统中多个线程同时读取同一个硬盘上的文件),这些IO请求任务使用DRCB对象进行跟踪,并被设备驱动程序以队列的形式进行维护。这样可能出现一种情况,就是一个请求任务可能被取消(比如,对应的用户线程取消了读取请求),这个时候,设备驱动程序就可以直接把对应的DRCB对象从等待队列中删除,然后调用OnCancel函数,来通知上层模块(IOManager)这个取消请求动作。在目前版本的Hello China的实现中,暂不支持IO请求的取消服务。 需要说明的是,上述三个函数的参数,都是对应的DRCB对象,比如,可以这样调用OnCompletion函数: lpDrcb->OnCompletion((__COMMON_OBJECT*)lpDrcb); 由于DRCB对象中包含了发起该IO请求的线程对象(lpKernelThread成员)和一个事件同步对象(lpSynObject),所以这些函数可以很容易的实现线程的阻塞、唤醒等操作。对于一些特定功能的参数,可以通过DRCB对象的DrcbExtension成员访问,该成员用于访问一些驱动程序特定的参数,类似于设备对象的设备扩展。 另外,在__DRCB对象的定义中,为了标志DRCB对象的状态,定义了dwDrcbStatus变量,这个变量可以取下列值: · DRCB_STATUS_INITIALIZED:DRCB对象已经被初始化,但尚未被任何线程应用,一般情况下,调用CreateObject函数,创建一个DRCB对象后,所创建的DRCB对象状态被设置为该值; · DRCB_STATUS_PENDING:DRCB处于排队状态,等待对应的操作完成。比如,读取磁盘上的一个数据块,相应的设备操作命令已经发出,但还没有收到最终相应,这个时候,dwDrcbStatus被设置为该值; · DRCB_STATUS_COMPLETED:DRCB对象跟踪的设备请求操作已经被成功完成; · DRCB_STATUS_FAILED:DRCB对象跟踪的设备请求操作失败,比如,设备在长时间内没有响应,会导致这种情况出现; · DRCB_STATUS_CANCELED:DRCB对象跟踪的设备请求,在完成前被用户取消。比如,用户读取一个硬盘上的数据,驱动程序已经发出请求(这时候,DRCB对象的状态设置为DRCB_STATUS_PENDING),在完成前,用户取消了该读取操作,这时候,操作系统会把该DRCB对象的状态设置为DRCB_STATUS_CANCELED。 另外一个比较重要的成员变量,是dwRequestMode,指明了该DRCB跟踪的请求类型,可以取下列值: · DRCB_REQUEST_MODE_READ:该DRCB对象跟踪的请求类型是一个读取操作,比如,读取存储设备上的一块数据; · DRCB_REQUEST_MODE_WRITE:对应写入设备的操作,欲写入设备的具体数据,由dwInputLen和lpInputBuffer两个参数指定; · DRCB_REQUEST_MODE_CONTROL:IO Control操作,dwCtrlCommand指明了具体的操作类型,一般情况下,dwCtrlCommand取值的具体含义,由设备驱动程序自己定义; · DRCB_REQUEST_MODE_SEEK:在调用DeviceSeek函数的时候,设定该值; · DRCB_REQUEST_FLUSH:在调用DeviceFlush函数的时候,设定该值。 设备驱动程序的文件组织结构 设备驱动程序编译后,以文件的形式存在系统存储设备(比如,硬盘)上,下图示意了典型的驱动程序文件组织结构: 图10-7 设备驱动程序文件的存储结构 其中,DriverEntry和UnloadEntry是用于初始化和资源释放调用的,其它的函数和全局变量,用于实现该设备驱动程序的特定功能。相应地,DriverEntry和UnloadEntry是直接输出的函数,而其它的函数,则在DriverEntry中隐式的输出给IOManager(参见DriverEntry的实现部分)。 设备驱动程序的功能实现 从上面的设备驱动程序文件组织结构中看出,设备驱动程序实现了一个统一的设备操作框架(一组标准函数),这些标准框架函数由IOManager调用(设备驱动程序在初始化时,通过DriverEntry函数通知IOManager这些函数的地址,参考下面相关章节的描述),设备驱动程序实际对设备的操作,就是在这些框架函数中实现的。 一般情况下,对设备的操作可以抽象为读写操作和打开/关闭操作,对应框架中的DeviceRead/DeviceWrite、DeviceOpen、DeviceClose函数,但也有一些其它的操作,比如定位当前位置(DeviceSeek),特殊的控制命令(DeviceCtrl)等,因此,标准框架也为这些特殊的操作定义了接口。 对设备的打开和关闭操作相对比较简单,在设备驱动程序实现的时候,针对打开操作,一般是创建一个设备对象,初始化,并连接到操作系统(确切的说,是IOManger)维护的设备对象链表中,对于关闭操作,设备驱动程序释放关闭设备所对应的系统资源,从系统设备链表中删除该设备对象。 比较复杂的是对设备的读写操作和控制操作(对应DeviceRead/DeviceWrite/DeviceCtrl函数),在本节中,我们对这几个操作进行比较详细的实现描述。需要说明的是,设备不同,这些函数实现的方式和具体功能也不同,在这里描述的是一个相对通用的框架,或者可以看作是一种实现的特例,作为实现具体的设备驱动程序时的参考。 读操作(DeviceRead)的实现 读操作的发起者,可以是用户线程、系统线程,也可以是设备驱动程序(比如,文件系统驱动程序,就需要读硬盘数据),但不论是哪种方式,其入口却只有一个,即所有的读操作都通过IOManager提供的ReadFile函数来展现,当然,在读一个设备的时候,该设备必须已经打开(即建立了设备对象)。在这里,假设一个用户线程发起一个读请求操作,从串行接口读取一个字节的数据,相应的流程如下: 1、 用户线程调用IOManager提供的ReadFile函数(通过系统调用); 2、 ReadFile函数根据用户提供的参数,创建一个DRCB(Device Request Control Block)对象,并初始化该对象,然后ReadFile函数根据用户提供的设备对象地址,进而找到该设备对象对应的设备驱动程序(设备对象维护了指向设备对象驱动程序的后向指针),调用设备驱动程序对象对应的函数,到此为止,所有的操作都是由操作系统核心完成的(确切的说,是IOManager),下面的操作将由设备驱动程序自己完成; 3、 设备驱动程序维护了一个设备请求控制对象队列(DRCB队列),该队列中缓存了所有未完成的设备请求,当DeviceRead函数被调用后,该函数会检查队列的状态是否为空,如果是空,则该函数把该设备请求控制对象插入队列,然后根据DRCB提供的参数,发起一个设备操作请求(操作实际的设备),如果队列不为空,则说明现在仍然有一些请求正在执行中,于是该函数会把该DRCB对象插入队列,然后调用WaitForCompletion函数(该函数由IOManager提供,其指针保存在DRCB对象里面); 4、 在设备驱动程序对象发起一个设备请求操作后,会根据设备工作方式的不同(中断方式或轮询方式),来确定是否调用WaitForCompletion函数,如果是中断方式,则调用该函数,对应的线程进入阻塞状态,等待操作完成,如果是轮询方式,则不调用该函数,直接等待操作的完成(这个时候,其实是进入一个循环),在轮询方式下,设备操作完成或失败后,DeviceRead函数填充DRCB提供的缓冲区,设置DRCB对应的状态字段,然后返回给调用者(ReadFile函数); 5、 在中断方式下,由DeviceRead调用了WaitForCompletion函数,该函数会把当前线程挂起,等待设备操作完成。当设备操作完成之后,设备控制器会发起一个中断,通知操作系统该操作的完成,操作系统会调用该设备对应的中断处理程序,设备驱动程序的中断处理程序从DRCB队列中摘取一个DRCB对象,填充该对象(根据设备的操作结果),调用该对象的OnCompletion函数(该函数唤醒等待该操作的线程),然后检查DRCB队列是否为空,若是,则从中断中返回,否则,会从队列中获取一个DRCB对象,根据该对象指明的操作,再次发起一个设备操作,然后从中断中返回。下图反映了上述调用关系: 图10-8 设备读取操作的组成步骤 上面描述的是没有缓冲的情况,实际上,为了提高访问速度,大多数的设备驱动程序提供了缓冲功能,在内存中创建缓冲区,用于缓存设备上的数据,当读请求到达时,设备驱动程序首先检查请求的内容是否位于本地缓冲区内,如果在,则直接从缓冲区中读出,这样可以大大提高读操作的速度。在实现缓冲的设备驱动程序中,上述流程略有不同,就是在上述第三步中,驱动程序首先检查本地缓冲区,如果读取的内容位于本地缓冲区内,则直接从缓冲区中读出,返回给用户,否则,再发起一个实际的设备操作。 写操作(DeviceWrite)的实现 写操作的过程,跟读操作基本一致,所不同的是,驱动程序提交一个设备的写操作,然后根据设备操作模式(中断模式或轮询模式)来等待或阻塞请求的线程,直到该操作完成。 设备控制(DeviceCtrl)的实现 读写操作不能抽象所有可能的设备操作,比如,对一个音频设备,可能需要控制诸如暂停、重新开始、快进、倒退等操作,这种情况下,读写操作就无法胜任了,还有一种读写操作无法胜任的就是,设备的读写单位可能不一样,比如,针对串行接口,可能是一个字节一个字节的读写,而针对硬盘、光盘等存储设备,则可能是一个数据块一个数据块的读写,在UNIX的实现中,对这两种不同类型的设备分别做了处理(对应于UNIX的字符设备和块设备),但在Hello China的实现中,则没有进行区分,而进行了统一对待,但在读写的时候,客户程序必须首先确定每次操作的字节数(一个字节还是多个字节,具体是多少字节等),客户如何获取每次操作的字节数量,读写操作也是无法胜任的。因此,引入了设备控制操作(DeviceCtrl函数)。 设备控制操作是通过DeviceCtrl函数来实现的,用户通过IOControl函数调用(由IOManager提供)来实现对设备的DeviceCtrl函数的访问。 对于DeviceCtrl功能的输入参数和输出参数,在DRCB对象中做了完善的提供,客户程序在请求设备的控制功能的时候,首先使用合适的参数调用IOManager的IOControl函数,该函数创建一个DRCB,根据IOControl的参数初始化这个对象,然后进一步的调用设备驱动程序对象的DeviceCtrl函数。 DRCB对象中的dwCtrlCommand成员用来指出,设备驱动程序应该执行哪个功能,然后调用适当的功能函数。一般情况下,下列控制功能必须实现: 1、 CONTROL_COMMAND_GET_READ_BLOCK_SIZE,获得设备每次读操作的数据大小,比如,针对串行接口,可以是一个字节,针对磁盘,可以是512字节; 2、 CONTROL_COMMAND_GET_WRITE_BLOCK_SIZE,获得设备每次写操作的数据大小; 3、 CONTROL_COMMAND_GET_DEVICE_ID,获取设备的唯一ID,针对不同的设备,该功能的实现也不一样,而且ID也没有一个统一的编配,这种情况下,系统一致认为所有设备的ID是一个字符串,因此,设备驱动程序可以有选择的实现该功能,如果不能实现,则简单的返回失败结果; 4、 CONTROL_COMMAND_GET_DEVICE_DESC,获取设备的描述信息,设备驱动程序可以在描述信息中,对设备的具体型号、厂家、功能特点等进行描述,比如,“Broadcom 570x Gigabit Integrated Controller”。 其它的功能,设备根据实际的需要来自己定义,比如,针对一个音频控制设备,驱动程序可以定义诸如快进、倒退、循环播放等功能命令,进而完成实现。 设备驱动程序对象 BEGIN_DEFINE_OBJECT(__DRIVER_OBJECT) INHERIT_FROM_COMMON_OBJECT __DRIVER_OBJECT* lpPrev; __DRIVER_OBJECT* lpNext; DWORD (*DeviceRead)(__COMMON_OBJECT* lpDrv, __COMMON_OBJECT* lpDev, DRCB* lpDrcb); DWORD (*DeviceWrite)(__COMMON_OBJECT* lpDrv, __COMMON_OBJECT* lpDev, DRCB* lpDrcb); DWORD (*CreateFileSystem)(__COMMON_OBJECT* lpDev, __COMMON_OBJECT* lpDrv, DRCB* lpDrcb); DWORD (*DeviceCtrl)(__COMMON_OBJECT* lpDrv, __COMMON_OBJECT* lpDev, DRCB* lpDrcb); VOID (*DeviceFlush)(__COMMON_OBJECT* lpDrv, __COMMON_OBJECT* lpDev, DRCB* lpDrcb); DWORD (*DeviceSeek)(__COMMON_OBJECT* lpDrv, __COMMON_OBJECT* lpDev, DRCB* lpDrcb); DWORD (*DeviceOpen)(__COMMON_OBJECT* lpDrv, __COMMON_OBJECT* lpDev, DRCB* lpDrcb); VOID (*DeviceClose)(__COMMON_OBJECT* lpDrv, __COMMON_OBJECT* lpDev, DRCB* lpDrcb); DWORD (*DeviceCreate)(__COMMON_OBJECT* lpDrv, __COMMON_OBJECT* lpDev, DRCB* lpDrcb); DWORD (*DeviceDestroy)(__COMMON_OBJECT* lpDrv, __COMMON_OBJECT* lpDev, DRCB* lpDrcb); END_DEFINE_OBJECT(); DriverEntry的实现 DriverEntry是IOManager调用的函数,该函数给设备驱动程序一个机会,用来做一些初始化工作,同时也注册(向IOManager)对设备进行操作的函数。 一般情况下,该函数内可以做下列工作: 1、 初始化驱动程序的全局变量; 2、 注册用来对设备进行操作的标准函数; 3、 创建自己管理的设备对象。 一般情况下,直接把驱动程序实现的一些对设备操作的标准函数指针赋值给驱动程序对象(作为该函数的参数传递)即可,如下: lpDriverObject->DeviceRead = DeviceRead; lpDriverObject->DeviceWrite = DeviceWrite; lpDriverObject->DeviceCtrl = DeviceControl; … … … 另一项重要的工作,就是创建设备对象,可以通过调用IOManager提供的CreateDevice函数来完成。一般情况下,设备驱动程序只能创建自己可以管理的对象(创建自己不能管理的设备对象也是可以的,但没有任何意义),对对象的资源分配,有两种方式: 1、 接受由IOManager传递过来的资源分配方案,把这些资源分配给设备; 2、 如果设备驱动程序管理的设备,系统资源固定(比如,对于键盘、显示器、IDE接口硬盘等设备,其资源基本上固定),那么可以不接受IOManager提供的资源分配方案,而自己硬性的给设备分配资源,这种方式下,很有可能出现资源冲突。 在创建设备对象的时候,一个很重要的事情就是指定设备的设备扩展,所谓设备扩展,就是紧跟随设备对象后面的一段存储空间,该空间内存储了跟设备相关的一些数据,比如,设备的类型,设备块的大小,当前指针位置,设备的尺寸等。设备扩展的大小只有设备驱动程序知道,因此需要设备驱动程序来指定。 下面是一个典型的创建设备对象,并对之初始化的例子: __DEVICE_OBJECT* lpIdeHardDisk = NULL; lpIdeHardDisk = CreateDevice(“IDE Hard Disk 0”, //Device name. lpDriverObject, //Driver object. NULL, //Resource descriptor. NULL, //Device extension. DEVICE_TYPE_STORAGE, 512); //Device block size. If(NULL == lpIdeHardDisk) //Failed to create device. Return FALSE; InitializeIdeHardDisk(lpIdeHardDisk); //Initialize it. … … … UnloadEntry的实现 当IOManager要卸载一个设备驱动程序的时候,会调用设备驱动程序提供的UnloadEntry函数,一般情况下,设备驱动程序需要在这个函数中做如下事情: 1、 调用DestroyDevice函数,销毁自己创建的(但没有销毁的)所有设备对象; 2、 释放设备驱动程序运行过程中申请的内存资源; 3、 释放所有通过ReserveResource函数预留的资源。 如果设备驱动程序在系统运行的整个过程中都存在,那么该函数可以不做任何事情,简单返回即可,但如果设备驱动程序有可能被动态的加载或卸载,比如一些可移动存储介质的驱动程序,那么设备驱动程序就必须在这个函数中释放所有的资源。如果设备驱动程序在运行的过程中申请了系统资源(比如内存等),但在卸载的时候没有释放,那么会造成资源泄漏。 设备对象 系统中任何打开的设备,都对应一个设备对象,该对象用来记录特定设备的相关信息,也包含了指向该设备驱动程序的后向指针,在CreateFile(IOManager提供的用户侧接口调用)函数成功返回后,就是返回打开或创建的设备对象的地址。 设备对象是用名字来唯一标识的,用户线程通过CreateFile调用打开文件的时候,需要明确指定设备的名字,IOManager就是靠这个名字来找到具体的设备的。 设备对象的定义 在Hello China当前版本的实现中,设备对象的定义如下: BEGIN_DEFINE_OBJECT(__DEVICE_OBJECT) INHERIT_FROM_COMMON_OBJECT __DEVICE_OBJECT* lpPrev; __DEVICE_OBJECT* lpNext; UCHAR DevName[MAX_DEV_NAME_LEN]; __KERNEL_THREAD_OBJECT* lpOwner; DWORD dwDevType; __DRIVER_OBJECT* lpDriverObject; /*DWORD dwStartPort; DWORD dwEndPort; DWORD dwDmaChannel; DWORD dwInterrupt; LPVOID lpMemoryStartAddr; DWORD dwMemLen;*/ DWORD dwRefCounter; DWORD dwBlockSize; DWORD dwMaxReadSize; DWORD dwMaxWriteSize; //DWORD DevExtension[0]; LPVOID lpDevExtension; END_DEFINE_OBJECT(); 在下面的几节中,我们对该定义中的几个重要字段(变量或成员)进行说明。 设备对象的命名 在当前版本的Hello China实现中,对所有的设备,采用设备名唯一标识,这样就需要定义一套规范的命名方式,来对系统中可能存在的设备进行命名。 在当前版本的实现中,系统中可能存在下列几类设备: 1、 普通文件,即存储在存储设备(比如,硬盘、光盘、FLASH卡等)上的数据文件,在Hello China中,数据文件也作为设备对待,与普通设备不同的是,文件设备的驱动程序,是文件系统; 2、 实际存在的物理设备,比如显示卡、网卡、串口、鼠标/键盘等,这些设备是实实在在的物理硬件设备,有相应的驱动程序进行驱动,每种物理设备,完成一项具体的功能; 3、 系统虚拟的设备,这类设备是操作系统(或设备驱动程序)虚拟出来的一种设备,比如,命名管道、RAM存储设备等,这些设备不对应于具体的物理外设,但却完成某项特定的功能,比如,命名管道可以完成进程间通信的功能,RAM存储设备可以把内存的一部分预留出来,虚拟成一个文件系统,供操作系统临时保存文件使用,等等; 4、 网络文件系统,比如,可以通过映射,来把远程计算机上的一个文件(或目录),映射为本地的一个文件系统,这样只要对本地虚拟的文件系统进行访问,就可以间接的访问到远程计算机的文件系统,比较典型的如NFS等。 针对上述几种设备类型,分别定义其命名形式如下: 1、 针对普通文件,采用的命名格式为文件系统标识符加文件路径的方式,比如,系统中存在三个硬盘分区,则每个分区被格式化为一个文件系统,相应的文件系统标识符为(缺省情况下)C:,D:,E:,比如,在文件系统C:下有一个目录Hello China,该目录下有一个名字为cat.dat的文件,于是该文件可以这样命名:C:\Hello China\cat.dat; 2、 对于实际存在的物理设备,采用这样的命名格式:\\dev\device_name,其中两个反斜线和后面的dev,是固定部分,操作系统根据这个固定部分来确定该命名是实际存在的物理设备命名,简化起见,一般把dev省略掉,简化为\\.\device_name的形式,这样省略的另外一个目的,就是避免与下面网络文件系统的命名冲突; 3、 对于系统虚拟的设备,其命名按照实际存在的物理设备的格式,比如,在当前的实现中,对命名管道的命名为\\.\”20ADC1F6-5194-416e-97AB-962A03472410”,其中,后面device_name部分,是采用一个GUID转换来的唯一字符串; 4、 对于远程文件系统,命名格式如下:\\server_name\file_path_name,其中server_name指明了具体的服务器名字,也就是远程计算机的名字,而file_path_name则是远程计算机上的文件路径名。比如,命名服务器shanghai的一个共享文件:shanghai.map,结果为:\\shanghai\shanghai.map。 设备的命名,构成了系统IOManager管理设备的基础,但当前版本的实现中,操作系统在加载设备驱动程序,并创建设备的时候,却不对设备名字做任何检查。因此,如果驱动程序不按照上述规则为系统中的设备命名,则可能会一起混乱。 设备对象的类型 为了管理上的方便,我们把物理设备根据其功能划分成特定的类别,这样就可以对一种设备进行更细致的划分,进而提供更细致的管理和监控。在Hello China当前版本的实现中(V1.0),我们把设备分成以下几类: 1、 DEVICE_TYPE_STORAGE:存储设备,能够提供永久存储功能的功能部件,比如软盘、硬盘(基于IDE或SCSI接口)、光盘、USB接口的存储设备等,只所以这样划分,是因为这些设备都需要有文件系统进行支撑; 2、 DEVICE_TYPE_FILE_SYSTEM:文件系统对象,针对系统中存在的每个文件系统,操作系统都创建一个文件系统对象; 3、 DEVICE_TYPE_NORMAL:普通设备对象,所有不属于上述类别的设备,都属于普通设备对象; 4、 DEVICE_TYPE_FILE,文件对象,任何打开的文件系统中的文件,都被赋予这个对象属性。 在设备驱动程序加载完毕后,IOManager会根据设备的类型(存储设备或非存储设备)来决定是否进行进一步的初始化。针对存储设备,IOManager会尝试为该设备加载一种文件系统,比如,针对硬盘(严格来说,应该是硬盘的每个分区),操作系统会读取硬盘分区的相关信息,判断该硬盘分区的文件系统类型,如果判断的结果是目前能够支持的文件系统,则操作系统就会加载该文件系统的驱动程序,并调用文件系统驱动程序提供的CreateFileSystem函数,创建一个文件系统设备对象,如果判断结果未知(即该分区或者没有被格式化,或者被格式化成了系统目前不支持的文件系统),那么操作系统将会放弃为该分区加载文件系统的尝试。 对于文件系统的实现流程,总结如下: 1、 IOManager遍历已经创建的所有设备对象,检查该对象是否是存储设备(设备对象类型为DEVICE_TYPE_STORAGE); 2、 若是,则会读取该设备对象的相关信息(使用设备驱动程序提供的读写函数),根据这些信息判断该设备被格式化成的文件类型; 3、 如果判断的结果(特定的文件系统类型)为操作系统当前不支持的文件系统,则放弃当前设备,进行下一个设备的检测,若当前操作系统支持该设备对应的文件系统,则会加载对应文件系统的驱动程序,并调用文件系统驱动程序的DriverEntry函数,给文件系统一个初始化的机会; 4、 调用文件系统驱动程序提供的CreateFileSystem函数,并把检测到的存储设备的物理参数(比如,硬盘分区的起始扇区号、所在的硬盘号、扇区数量、每个扇区的大小等)传递给该函数,创建一个文件系统设备对象(实际上是创建一个设备对象,该设备对象的对象类型为DEV_TYPE_FILE_SYSTEM); 5、 完成当前设备对象的检测后,继续进行下一个设备对象的检测,直到所有的设备对象都检测完毕。 设备对象的设备扩展 设备扩展是设备对象定义中最后一个变量(成员)lpDevExtension,可以说该变量是设备对象定义中最重要的一个变量,它为不同的设备,预留了保存各自数据的空间。 正常情况下,物理设备是各种各样的,这些设备之间的差异,最终表现在设备的不同状态数据上,比如,对于硬盘,需要保存诸如硬盘大小、分区个数、扇区大小、扇区数量、操作方式(LBA、CHS等)等等数据,对于网卡,则需要保存诸如MAC地址、MTU大小、缓冲区大小、工作方式(全双工/半双工)、工作速率(1000M/100M/10等)等数据,设备对象不能囊括所有这些不同设备的不同要求,因此只把每种设备必须具有的数据抽象出来,做了明确定义,比如设备名、所占用的系统资源等,而对于设备特定的数据,设备对象没有做明确定义(也不可能定义),而是预留了一个指针,该指针指向特定的设备状态数据,这样不同的设备,其公共部分是相同的(设备对象定义部分),而差异数据,则由设备驱动程序组织,并存放在lpDevExtension指向的存储空间中,这个存储空间就称为设备对象扩展。 在当前版本的实现中,设备对象扩展是由IOManager的CreateDevice函数创建的,在CreateDevice函数的参数中,有一个参数是dwExtSize,该数据指出了设备扩展的尺寸,由设备驱动程序提供。在当前的实现中,CreateDevice仅仅通过调用KMemAlloc申请一块内存地址空间,然后赋予lpDevExtension成员,具体的设备扩展内容,由设备驱动程序自己填写。 设备的打开操作 在Hello China当前版本的实现中,设备的打开操作是通过调用CreateFile函数实现的。该函数是由IOManager提供给用户线程,用户线程访问设备之前,使用该函数打开待访问的设备。该函数不但可以用于打开物理设备,而且还可以用来打开或创建普通的数据文件(从该函数的名字也可以看出这一点),对于打开文件的详细流程,在前面的章节中已经做了介绍,下面的流程,描述了如何打开一个物理设备: 1、 用户调用该函数,其中待打开设备的设备名字作为参数之一; 2、 CreateFile函数(也可以说是IOManager)分析设备名字,如果发现该设备名字是一个普通文件(以文件系统标识符开头,比如C:,D:等),则启用文件打开流程,如果分析结果表示,待打开的设备对象是一个物理设备(以\\.\\devicename开头),则继续下面的设备打开操作流程; 3、 CreateFile查询设备对象链表,以设备名字作为索引关键字,从头开始遍历设备对象链表; 4、 如果遍历完整个链表,没有查找到目标设备对象,则说明对应的设备没有安装,或者安装了但没有启用,这种情况下,CreateFile返回一个空值(NULL),表示打开设备失败; 5、 如果能够在设备对象链表中找到对应的设备,则判断设备的当前状态(打开还是未打开),如果状态为打开,则判断该设备是否允许共享打开(允许两个或以上的线程同时打开该设备),如果允许,则增加设备打开计数,然后返回设备对象指针,如果不允许,则仍然返回一个空值(NULL),指明该操作失败; 6、 调用CreateFile的线程如果得到一个失败的操作结果,可以通过GetLastError调用,获取错误原因。 可以看出,上述操作的关键,也是遍历整个系统设备对象链表,跟文件的打开方式基本一致。另外还可以看出,对设备的打开操作,也是以设备名作为唯一关键字来查询设备的,因此,Hello China当前版本的实现中,要求系统中的所有设备,必须具有不同的设备名字。要达到这个要求,如果任意取设备名,可能会存在冲突的可能,因此,必须采用一些特殊的命名措施,来确保系统中所有设备的名字没有冲突。下面,我们介绍几种可用的设备命名策略,供设备驱动程序实现者参考。 设备命名策略 在Hello China当前版本的实现中,对设备的区分,是按照名字来进行的,也就是说,名字是设备唯一的标识。这样如果设备是由多家厂商提供的,那么就可能产生命名冲突,为解决这个问题,建议设备驱动程序编写者在为设备命名的时候,采用能够产生全球唯一设备名字(字符串)的算法,来产生设备名字,而不要随意的命名。需要指出的是,设备名字不同于设备描述,如果设备供应商想对自己的设备做一些简单的描述,那么可以在设备描述里面进行,系统提供了函数接口,可以让用户很容易的得到设备描述信息。 下面列举了几种可以采用的方法,来生成全球唯一的字符串,作为设备的名字,设备驱动程序编写者可以采用下列命名方式中的一种,对设备进行命名(如果不按照下列给出的命名方式,则可能产生冲突): 采用全球唯一标识符来命名设备 Microsoft公司提供的一个小程序GUIDGEN.EXE(随Microsoft Visual Studio一起发行)可以产生长度为32字节的全球唯一标识符(GUID),比如,下面是该程序产生的一个GUID: {9EABB977-9872-4cfc-A381-2E4D52864FA5} 由于采用了独特的算法,可以确保生成的GUID全球唯一,因此,设备驱动程序开发商可以把上述GUID转换成字符串,来命名自己的设备,比如,对于上述GUID,可以转换成下列形式: “9EABB977-9872-4cfc-A381-2E4D52864FA5” 也可以转换成下列形式: “9EABB97798724cfcA3812E4D52864FA5” 总之,只要把GUID表示成字符串形式,然后作为全局变量定义在设备驱动程序中,作为设备的名字,可以确保不会产生冲突。 采用网络接口卡硬件地址命名设备 另外一种可以作为唯一标识符种子的就是以太网接口卡的物理地址(也称为MAC地址)。以太网接口卡的物理地址有统一的组织管理并分配,可以确保全球唯一,该地址由48比特(6字节)组成,其中前面三字节是分配给特定厂家的,而后面三个字节,则由该厂家分配。在产生设备驱动标识符的时候,可以直接把一块网络接口卡的MAC地址转换成字符串后,作为设备的标识符,比如,网络接口卡的MAC地址为: 00-11-43-98-90-AB 则可以把上述标识符转换成字符串: “00-11-43-98-90-AB” 直接作为设备的标识符,也可以在此基础上,增加一些额外信息作为设备标识符,比如,可以这样操作: “IDE Hard Disk 00-11-43-98-90-AB” 设备的中断管理 特定设备的中断处理函数,由设备驱动程序提供,在当前版本的实现中,Hello China提供了完善的中断连接机制,可以通过ConnectInterrupt调用,把特定的中断处理函数跟相应的中断向量连接。 一般情况下,这个连接是在DriverEntry函数内完成的,在设备初始化的时候,就已经确定了设备所使用的中断号(系统分配,或设备驱动程序自己检测),中断号确定之后,就可以直接调用ConnectInterrupt函数,连接中断处理程序和中断向量了。在当前版本的实现中,对于设备的中断处理程序(中断处理函数),其原型必须符合下列形式: BOOL InterruptHandler(LPVOID lpParam,LPVOID lpEsp); 其中,第一个参数为传递给中断处理程序的特定参数,第二个参数则是中断程序发生之后,堆栈框架的指针,中断处理程序通过该参数(lpEsp),可以访问到中断发生之后的系统堆栈框架。 一般情况下,中断处理程序在开始的时候,需要先决定该中断是不是自己的,因为在许多硬件设计环境中,可能多个设备共同使用一个中断向量(即多个设备连接到中断控制器的同一条输入引脚上),在Hello China当前版本的实现中,对于共同使用一个中断向量的中断处理函数,使用链表的方式连接在一起(串连在一起),每当中断发生的时候,操作系统从链表的头部开始,依次调用中断处理程序,根据中断处理程序的返回(TRUE或FALSE),来决定中断是否得到了处理。如果调用的中断处理程序返回了FALSE,则说明该中断不是由提供刚刚调用的中断处理程序的设备驱动程序来处理,于是会继续调用下一个中断处理程序,直到有一个中断处理程序返回TRUE,或到达链表的末尾。 因此,在编写设备的中断处理程序的时候,在函数的开始处,建议马上采用简单的代码来判断该中断是不是针对自己的,如果是,则进一步处理,否则,马上返回FALSE,以便系统尽快的把中断调度到正确的处理程序上。 那么,中断处理程序如何才能知道该中断是针对自己的,还是不是自己的呢?一般情况下,设备驱动程序可以通过读取设备的寄存器得到,或者通过内部的一操作情况来确定。比如,假设IDE硬盘和网络通信控制器(NIC)连接到同一条中断线上,那么当中断发生的时候,网卡驱动程序就可以读取网卡的状态寄存器,来判断是否有报文到达,如果是,则说明该中断是由网卡发起的,则进行进一步处理,并返回TRUE(表示中断得到了正确的处理),否则,直接返回FALSE。对于IDE接口的硬盘驱动器,则可以采用内部状态来确定是否是自己的中断。因为正常情况下,必须由设备驱动程序发起了请求,比如一个读取操作,当操作结束时,设备才会发生中断,因此,这种情况下,驱动程序会保持一个未完成的操作事务,当收到中断的时候,驱动程序就可以结合事务的状态,并读取适当的状态寄存器,来判断是否是IDE硬盘发起的中断。当然,不同的设备有不同的判断方式,需要结合具体的设备来完成。 “COM1” ComRead ComWrite … … .. … … … “COM0” ComRead ComWrite … … .. “Harddisk0” HDRead HDWrite … … .. DeviceObjectRoot … … … “Harddisk0” ReadDisk WriteDisk “Ethernet0” SendPacket RecvPacket Root 系统总线N 硬件设备1 硬件设备2 硬件设备3 硬件设备4 … …. … 硬件设备N 硬件设备1 硬件设备2 硬件设备3 硬件设备4 … …. … 硬件设备N 系统总线2 系统总线1 总线列表 中断方式 中断处理程序: 根据操作结果填充DRCB; 唤醒等待该操作的线程; 提交新的设备请求; 从中断返回。 DeviceRead: 检查DRCB队列状态; 发起设备设备请求; 等待完成; 返回。 ReadFile: 根据参数创建DRCB对象; 根据设备对象名找到对应的设备对象; 调用设备对象的DeviceRead函数。 File3的控制信息 File3.dat File2的控制信息 File2.dat File1的控制信息 File1.dat Variables Global Variables File Rear Unload Entry UnloadEntry Functionality functions Entry point File Header DeviceRead DeviceWrite DeviceCtrl DeviceSeek CreateFileSystem DeviceFlush DeviceOpen DeviceClose DeviceDestroy DeviceCreate … … … DriverEntry IOManager DeviceObject ……… DriverObject DriverObject DriverObject DriverObject ……… ……… ……… ……… IOManager Hardware Devices Device Dirvers User kernel thread FS Manager Resource Manager
/
本文档为【第十章 Hello China的驱动程序管理框架】,请使用软件OFFICE或WPS软件打开。作品中的文字与图均可以修改和编辑, 图片更改请在作品中右键图片并更换,文字修改请直接点击文字进行修改,也可以新增和删除文档中的内容。
[版权声明] 本站所有资料为用户分享产生,若发现您的权利被侵害,请联系客服邮件isharekefu@iask.cn,我们尽快处理。 本作品所展示的图片、画像、字体、音乐的版权可能需版权方额外授权,请谨慎使用。 网站提供的党政主题相关内容(国旗、国徽、党徽..)目的在于配合国家政策宣传,仅限个人学习分享使用,禁止用于任何广告和商用目的。

历史搜索

    清空历史搜索