Apple - IOKit Fundamentals

本文翻译整理自:IOKit Fundamentals (Updated: 2014-04-09
https://developer.apple.com/library/archive/documentation/DeviceDrivers/Conceptual/IOKitFundamentals/Introduction/Introduction.html


文章目录


一、I/O Kit 基础知识简介

本文档介绍了 I/O Kit 的术语、概念、架构和基本机制。

I/O Kit 是 Apple 用于开发 OS X 设备驱动程序的面向对象框架。

它包含了任何想要为该平台创建设备驱动程序的人所需的基本背景信息。


1、谁应该阅读本文档?

I/O Kit 开发人员通常分为两类,本文档力求对这两类开发人员都有用。

第一种开发人员创建驻留在内核中的设备驱动程序;第二种开发人员使用 I/O Kit 设备接口与硬件通信。

有些章节包含对这两类开发人员都有用的信息,而其他章节包含的信息仅对驻留在内核中的驱动程序的编写者有用。

显然, I/O Kit 基础知识 没有涵盖某些内容。

例如,它没有描述开发工具的使用或特定驱动程序编程接口的使用。

但它确实可以帮助您了解 I/O Kit 的使用方法和原因,使您能够从更具体的文档和示例中获得最大价值。


2、本文档的组织

I/O Kit 基础知识 对 OS X 上的 I/O Kit 和设备驱动程序开发进行了广泛的概念性描述。

它包含以下章节:

  • 什么是 I/O Kit ?
    描述 I/O Kit 的特性和优点,并讨论其设计理念和决策。
  • 架构概述
    对 I/O Kit 的架构、基本概念和基本机制进行高层描述。
  • I/O 注册表
    描述 I/O 注册表,这是一个捕获活动驱动程序对象之间的客户端/提供者关系的动态数据库。
  • 驱动程序和设备匹配
    解释为已注册的提供程序找到最合适的客户端驱动程序的匹配过程。
    它还总结了用户空间中为找到合适的设备及其驱动程序所遵循的流程。
  • 基类
    描述每个驱动程序对象直接或间接继承的基类。
    其中包括对对象构造和处置、驱动程序对象作为 I/O 注册表项以及驱动程序生命周期的讨论。
  • 处理事件
    解释工作循环和事件源的架构和用法,I/O Kit 使用它们在受保护的单线程环境中处理中断和 I/O 请求等事件的机制。
  • 管理数据
    描述如何使用内存游标、内存描述符和相关对象来处理 I/O 传输。
    它还讨论了驱动程序应如何处理硬件约束,例如 DMA 引擎施加的约束。
  • 管理电源
    解释 OS X 电源管理的概念并描述驱动程序对其设备进行电源管理的不同方法。
  • 管理设备移除
    解释如何响应设备移除(热插拔)。
  • I/O Kit 系列参考
    显示每个系列的类层次结构图,并提供可能与通用 I/O Kit 信息不同的系列特定信息。
  • 基类和辅助类层次结构
    为所有不属于特定系列的 I/O Kit 类提供类层次结构图。
  • 文档修订历史
    列出对此文档的更改。
  • 参考书目
    列出有关 OS X 和相关主题的其他信息来源。
  • 词汇表
    定义本文档中使用的关键术语。

3、也可以看看

一旦你吸收了 I/O Kit 基础知识 中的信息,你就可以继续前进并真正创建设备驱动程序。

Apple 提供了几个文档 和其他信息 来源来帮助你完成这项工作:

  • IOKit 设备驱动程序设计指南 描述了设计、编码、调试和构建驻留在内核的设备驱动程序所需的一般步骤。
  • 从应用程序访问硬件 讨论了如何使用 I/O Kit 的"设备接口"功能;它还包括通过 BSD 设备文件的串行和存储 I/O 信息。
  • 内核扩展编程主题 包含一系列教程,向您介绍开发工具并指导您完成创建、调试和打包内核扩展和 I/O Kit 驱动程序(一种内核扩展)所需的步骤。
    它还包括有关内核扩展其他方面的信息。
  • 内核编程指南 概述了 OS X 内核环境的架构和组件(Mach、BSD、网络、文件系统、I/O Kit)。
    所有打算在内核中编程的开发人员(包括设备驱动程序编写者)都应该阅读此文档。
  • Mac 技术概述 对 OS X 进行了整体介绍,对于新接触该平台的开发人员很有用。

当然,您随时可以浏览 I/O Kit 附带的头文件,它们安装在 Kernel.framework/Headers/iokit(内核驻留)和 IOKit.framework/Headers(设备接口)中。

您还可以在 Xcode 中查看开发者文档。

为此,请从 Xcode 菜单中选择"帮助",然后单击"显示文档窗口"。

您可以通过两种方式浏览 BSD 手册页 以获取有关 BSD 和 POSIX API 的更多信息:您可以在终端窗口中 键入man function_name(例如 man gdb),也可以在 OS X 手册页 中查看 HTML 版本。

如果您准备开发一个 通用二进制版本 的设备驱动程序 以在基于 Intel 的 Macintosh 上运行,请首先阅读《通用二进制编程指南,第二版》

然后,参阅 《IOKit 设备驱动程序设计指南》* ,以了解设备驱动程序开发人员特别感兴趣的问题概述。

特定于特定设备类型的相关信息可在 硬件和驱动程序文档 中列出的文档中找到。

Apple 维护着多个网站,开发人员可以在这些网站上获取有关 OS X 的常规信息和技术信息。


二、什么是 I/O Kit ?

I/O Kit 是用于在 OS X 中 创建设备驱动程序 的系统框架、库、工具和其他资源的集合。

它基于以受限形式的 C++ 实现的面向对象编程模型,省略了不适合在多线程内核中使用的功能。

通过对连接到 OS X 系统的硬件 进行建模 并抽象特定类别设备 的通用功能,I/O Kit 简化了设备驱动程序开发过程。

本章讨论了 I/O Kit(以及用它开发的驱动程序)的固有功能、其设计决策以及 I/O Kit 作为产品时的情况。

它还为那些考虑开发内核软件(如设备驱动程序)的人提供了一些注意事项和指导。


1、在你开始之前

您可能已经为其他平台开发了设备驱动程序 --- 可能是 Mac OS 9、BSD 或其他 UNIX 版本。

阅读本文档时,您会发现 I/O Kit 的方法有多么不同。

虽然为 OS X 编写驱动程序 需要新的思维方式和不同的编程方式,但转向这种新方法会让您受益匪浅。

I/O Kit 简化了驱动程序开发并支持多种设备类别。

一旦您掌握了 I/O Kit 的基础知识,您就会发现创建设备驱动程序是一件相对简单且高效的事情。

在你尝试使用 I/O Kit 进行驱动程序开发之前,Apple 强烈建议先决条件。

由于该框架使用面向对象的编程模型,该模型是在有限的子集中实现的C++,了解 C++ 或面向对象概念会有所帮助。

此外,设备驱动程序与应用程序不同,因为它们驻留在内核中,所以必须遵守更严格的规则。

因此,了解内核编程非常有用。

事实上,除非绝对必要,否则不鼓励在内核中编程。

在系统的更高级别上存在许多与硬件和网络通信的替代方案,包括"从内核外部控制设备" 中描述的 I/O Kit 的"设备接口"功能。

有关内核编程替代方案的更多信息,请参阅 "您应该在内核中编程吗?" 。


2、I/O Kit 功能

从一开始,I/O Kit 的基本目标就是适应和增强 OS X 的原生特性和功能,尤其是内核环境的特性和功能。

作为 OS X 的驱动模型,I/O Kit 支持以下功能:

  • 动态和自动设备配置(即插即用)
  • 许多新型设备,包括图形加速和多媒体设备
  • 电源管理(例如"睡眠"模式)
  • 内核对受保护内存的执行------为内核和用户程序提供单独的地址空间
  • 抢占式多任务
  • 对称多处理
  • 不同类型的设备之间共享的通用抽象
  • 增强开发体验------新驱动程序应该易于编写

I/O Kit 通过其新的设备驱动程序模型支持这些内核功能并添加了一些附加功能:

  • 面向对象的框架,实现所有驱动程序和驱动程序类型(系列)之间共享的通用行为
  • 为开发商提供众多住宅
  • 用于处理以下问题的线程、通信和数据管理原语:多处理、任务控制和 I/O 传输
  • 稳健、高效的匹配和加载机制,可完美扩展到所有总线类型
  • I/O 注册表,一个跟踪实例化对象(例如驱动程序实例)并提供有关它们的信息的数据库
  • 这I/O 目录,系统中所有可用的 I/O Kit 类的数据库
  • 一套设备接口------允许"用户空间"中的应用程序和其他软件与驱动程序进行通信的插件机制
  • 整体表现优异
  • 支持客户端和提供者对象的任意复杂分层

I/O Kit 的面向对象编程模型 是在 C++ 的一个受限子集中实现的。

面向对象本身在驱动程序开发中是一个优势,因为它促进了代码的可重用性。

一旦您熟悉了 I/O Kit,您就可以比使用过程模型更快、更有效地编写设备驱动程序。

此外,代码可重用性减少了驱动程序的内存占用;例如,从 Mac OS 9 移植的驱动程序在 OS X 中的大小最多可减少 75%。


3、I/O Kit 的设计原则

OS X 主要是两种操作系统技术的产物:Mac OS 9(及其前身)和 BSD。

鉴于这种血统,人们可能以为 Apple 会采用 Mac OS 9 或 FreeBSD 的设备驱动程序模型。

相反,Apple 选择重新设计该模型。有几个原因促使了这一决定。

首先,无论是 Mac OS 9 驱动模型还是 FreeBSD 驱动模型,都没有提供足够丰富的功能来满足 OS X 的需求。

OS X 内核比其 Mac OS 前身先进得多;它处理内存保护、抢占式多任务、多处理以及 Mac OS 以前版本中不存在的其他功能。

尽管 FreeBSD 能够处理这些功能,但 BSD 模型不提供现代操作系统所期望的其他功能,包括自动配置、驱动程序堆叠、电源管理和设备动态加载。

因此,I/O Kit 背后的主要动机是当前可用的驱动程序模型的不足。

重新设计 I/O 架构必须利用并支持 OS X 的操作系统功能。

为此,I/O Kit 的设计人员决定采用面向对象的编程模型,该模型抽象 OS X 系统的内核功能和硬件,并向操作系统的上层提供此抽象的视图。

此抽象的引人注目的部分是实现 I/O Kit 类中所有设备驱动程序(或设备驱动程序类型)的共同行为。

例如,考虑虚拟内存。

在 Mac OS 9 中,虚拟内存不是操作系统的基本组成部分;它是一个选项。

因此,开发人员在创建驱动程序时必须始终考虑虚拟内存,这会带来一定的复杂性。

相比之下,虚拟内存是 OS X 的固有功能,无法关闭。

由于虚拟内存是一项基本且假定的功能,因此对它的知识已融入系统软件中,驱动程序编写者不必考虑它。

I/O Kit 充当设备驱动程序的基础和协调器。

这与以前的驱动程序模型不同。

在 Mac OS 9 中,所有软件开发工具包 (SDK) 都相互独立,并重复通用功能。

OS X 将 I/O Kit 作为单个驱动程序的一部分提供内核开发工具包 (KDK);KDK 的所有部分都基于通用基础。

OS X 帮助开发人员充分利用硬件复杂性,而无需他们将软件复杂性编码到每个新设备驱动程序中。

在大多数情况下,他们只需添加使其驱动程序与众不同的特定代码即可。

I/O Kit 设计理念的另一部分是使设计完全开放。

所有 I/O Kit 源代码都作为 Darwin 的一部分提供,而不是为了保护开发人员而隐藏 API。

开发人员可以使用源代码来辅助设计(和调试)新驱动程序。


4、I/O Kit 的局限性

尽管 I/O Kit 支持大多数类型的 OS X 系统不支持所有硬件,但它并不完全支持所有硬件。

其中一类设备是用于成像,其中包括打印机,扫描仪,以及数码相机。

I/O Kit 仅为这些设备提供有限的支持,通过 FireWire 和 USB 系列处理与这些设备的通信。

用户空间中的应用程序或其他程序负责控制这些设备的特定特性(有关详细信息,请参阅 从内核外部控制设备)。

如果您的应用程序需要驱动成像设备,则应使用适当的成像软件开发工具包(SDK)。

尽管 I/O Kit 试图表示 OS X 系统中硬件设备和服务之间的层次结构和动态关系,但有些东西很难抽象。

正是在这些抽象的灰色区域(例如当发生分层违规时),驱动程序编写者更加独立。

即使 I/O Kit 表示清晰准确,I/O Kit 系列代码的可重用性也可能受到限制。

所有硬件都可能有自己的怪癖,驱动程序代码必须考虑到这些怪癖。


5、语言选择

苹果考虑了几种 I/O Kit 的编程语言,并选择了C++。

选择 C++ 有几个原因。

C++ 编译器已经很成熟,并且该语言支持系统编程。

此外,已经有一个拥有 C++ 经验的 Macintosh(和 BSD)开发人员大社区。


受限子集不允许使用 C++ 的某些特性,包括

  • 例外
  • 多重继承
  • 模板
  • 运行时类型信息 (RTTI) - I/O Kit 使用其自己的运行时类型系统实现

这些功能被删除是因为它们被认为不适合在多线程内核中使用。

如果您觉得需要这些功能,您应该重新考虑您的设计。

您应该能够使用 I/O Kit 编写您需要的任何驱动程序,并满足这些限制。


在 I/O Kit 驱动程序中使用命名空间

请注意,您可以在 I/O Kit 驱动程序中使用命名空间。

使用命名空间可以帮助您避免名称冲突,并可能使您的代码更易于阅读和维护。

请务必使用反向 DNS 格式作为命名空间名称(例如com.mycompany),以避免潜在的命名空间冲突。

如果您决定在内核 I/O Kit 驱动程序中使用命名空间,请不要在命名空间中声明任何 OSObject 子类,否则您的驱动程序将无法加载。

目前,加载器不支持需要限定的 OSObject 派生类,例如下面显示的类:

c 复制代码
namespace com.mycompany {
    class com.mycompany.driver.myClass : public IOService { // This is not allowed.
        OSDeclareDefaultStructors (com.mycompany.driver.myClass);
    };
};

在 I/O Kit 驱动程序中使用静态构造函数

在 OS X v10.4 中,GCC 4.0 是所有新项目(包括 I/O Kit 驱动程序)的默认编译器。

本节介绍了 GCC 3.3 和 GCC 4.0 之间的特定差异,这些差异可能会影响 OS X v10.3.x 和 OS X v10.4.x 之间内核驱动程序的兼容性。

有关 GCC 3.3(OS X v10.3 中的默认编译器)和 GCC 4.0 之间的差异的更多信息(包括移植指南),请参阅 GCC 移植指南

如果您在使用 GCC 3.3 或更早版本编译的 C++ I/O Kit 驱动程序(或其他 KEXT)中的函数内执行静态构造,请注意,使用 GCC 4.0 编译的相同 KEXT 将无法再成功加载。

这是因为 GCC 4.0 对于在内核环境中获取和释放锁的要求更加严格。

如果您在使用 GCC 4.0 编译的 I/O Kit 驱动程序中执行函数内静态构造,则在尝试加载时可能会看到以下错误:

c 复制代码
kld():Undefined symbols:
__cxa_guard_acquire
__cxa_guard_release

这个问题的解决方案很简单:将静态构造函数移至全局命名空间。

例如,假设您的 I/O Kit 驱动程序包含一个函数内静态构造函数,如以下代码所示:

c 复制代码
class com_mycompany_driver_mystaticclass;
void com_mycompany_driver_myclass::myfunction(void)
{
    static com_mycompany_driver_mystaticclass staticclass;
    staticclass.anotherfunction();
}

您可以通过更改此代码来避免函数内静态构造,从而避免加载错误,如下面的代码所示:

c 复制代码
class com_mycompany_driver_mystaticclass;
static com_mycompany_driver_mystaticclasss staticclass;
void com_mycompany_driver_myclass::myfunction(void)
{
    staticclass.anotherfunction();
}

请注意,如果您使用 -fno-threadsafe-statics 编译器选项,通过 GCC 4.0 编译 KEXT,您可能无需更改代码即可避免与函数内静态构造相关的加载错误,但这可能会导致其他问题。

具体来说,除非您可以通过其他方式保证线程安全,否则使用此选项编译 KEXT 可能会破坏您的代码。


6、I/O Kit 的部件

从物理和电子角度来看,I/O Kit 由许多部分组成:框架和库、开发和测试工具以及信息资源(例如示例项目、文档和头文件)。

本节对这些部分进行了分类,并指出了它们的安装位置和访问方式。


框架和库

I/O Kit 基于三个 C++库。它们都打包在框架里,但只有 IOKit.framework 是一个真正的框架。

内核框架主要是为了 暴露内核头文件,包括libkern 和 IOKit。

这些"库"的代码实际上内置于内核中;但是,驱动程序(加载时)会像库一样链接到内核。

框架或库 描述和位置
内核/IOKit 用于开发驻留在内核的设备驱动程序的库。 头文件位置:Kernel.framework/Headers/IOKit
内核/libkern 包含对所有内核软件开发有用的类的库。 头文件位置:Kernel.framework/Headers/libkern
IOKit 用于开发设备接口的框架。 位置:IOKit.framework

应用程序和工具

你用一把开发应用程序用于构建、管理、调试、检查和打包设备驱动程序。
表 1-2 列出了驱动程序开发中使用的应用程序;这些应用程序安装在 /Developer/Applications 中。

应用 描述
Xcode OS X 的主要开发应用程序。
Xcode 可管理项目、提供功能齐全的代码编辑器、根据任意复杂的规则构建项目、提供用于软件配置的用户界面,并充当调试和文档搜索的前端。
I/O 注册表资源管理器 可以图形方式探索内容和结构I/O 注册表。
包装制造商 为安装程序应用程序创建安装包;用于部署内核扩展(包括设备驱动程序)。

表 1-3 描述了使用 I/O Kit 开发设备驱动程序时使用的命令行工具;所有工具都位于/usr/sbin//sbin中。

注意: 您可以通过在终端应用程序 提供的 shell 中输入命令来查看这些工具的在线文档(在 UNIX 世界中称为手册页)。

该命令是manman 命令的主要参数是您要查看其文档的工具的名称。

例如,要查看kextload工具的手册页,请在终端中输入以下行:

shell 复制代码
man kextload

工具 描述和位置
ioreg 打印 I/O Registry(I/O Registry Explorer 应用程序的命令行版本)的内容。
kextload 加载内核扩展(例如设备驱动程序)或生成用于远程调试的静态链接符号文件。
kextunload 卸载内核扩展(如果可能的话)。
kextstat 打印有关当前加载的驱动程序和其他内核扩展的统计信息。
iostat 显示终端、磁盘和 CPU 操作的内核 I/O 统计信息。
ioclasscount 显示指定类的实例数。
ioalloccount 显示内核中 I/O Kit 对象分配的一些内存统计。
kextcache 压缩并存档内核扩展(包括驱动程序),以便它们可以在启动时自动加载到内核中。
gcc Apple 版本的 GNU C++ 编译器;Xcode 会使用适合 I/O Kit 项目的正确标志集自动调用它。
gdb Apple 版本的 GNU 调试器;Xcode 会使用适合 I/O Kit 项目的正确标志集自动调用它。

其他 I/O Kit 资源

I/O Kit"产品"中包含一些信息资源,特别是文档和头文件。

其中一些资源在上一章 "I/O Kit 基础知识简介" 中进行了描述

I/O Kit 是Darwin 开源项目。

Apple 维护着一个网站,您可以在其中找到与 I/O Kit 和 Apple 管理的其他开源项目相关的大量信息。

以下两个位置特别有趣:


7、您应该在内核中编程吗?

如果您正在考虑为内核环境编写代码,请仔细考虑。

在内核中编程可能是一项困难且危险的任务。

而且通常有一种方法可以在不触碰内核的情况下完成您想要做的事情。

驻留在内核中的软件往往很昂贵。

内核代码"连线"到物理内存中,因此无法被虚拟内存系统调出。

随着越来越多的代码被放入内核,用户空间进程可用的物理内存越来越少。

因此,分页活动可能会加剧,从而降低系统性能。

内核代码本身也不稳定,比应用程序代码更不稳定。

内核环境是一个单一进程,这意味着没有驱动程序与内核中的其他部分之间应有内存保护。

如果在错误的位置访问内存,整个系统就会陷入瘫痪,成为内核恐慌的受害者。

此外,由于内核代码通常为众多用户空间客户端提供服务,因此代码中的任何低效率都会传播到这些客户端,从而影响整个系统。

最后,编写内核软件确实很麻烦。

需要处理一些在应用程序开发领域未知的微妙问题。

而且内核代码中的错误比用户空间软件中的错误更难发现。

考虑到所有这些,信息很明确。

将尽可能少的代码放入内核符合每个人的最佳利益。

并且任何最终进入内核的代码都应该经过精心打磨和严格测试。


代码何时应驻留在内核中

少数情况需要将驱动程序或扩展加载到内核环境中:

  • 该软件由内核环境本身使用。
  • 用户空间程序将频繁使用该软件。
  • 软件需要直接响应主要中断(由 CPU 的中断控制器传送的中断)。

如果你编写的软件不符合上述任何标准,那么它可能就不属于内核。

如果你的软件是磁盘、网络控制器或键盘的驱动程序,那么它应该驻留在内核中。

如果它是作为文件系统的扩展,它应该驻留在内核中。

另一方面,如果它只是偶尔由单个用户空间程序使用,它应该由该程序加载并驻留在其中。

打印机和扫描仪的驱动程序属于后一类。


内核驻留代码的替代方案

Apple 提供了许多技术,让您可以完成想要做的事情,而无需使用内核。

首先是高级 API,它们为您提供一些硬件级别的访问权限。

例如,Open Transport 是许多网络功能的强大资源,并且Quartz Compositor 使您能够使用图形子系统执行一些相当低级的操作。

第二,同样重要的是,I/O Kit 框架的设备接口技术。

通过插件架构,该技术使您的应用程序能够与内核交互以访问硬件。

此外,借助 I/O Kit 的帮助,您还可以使用 POSIX API 来访问系列,存储,或网络设备。

请参阅 从内核外部控制设备以了解设备接口的摘要,并参阅文档 从应用程序访问硬件 以了解该技术的完整讨论。

注意: Objective-C 不提供设备级 I/O 服务。

但是,在 Cocoa 应用程序中,您可以调用 I/O Kit 和 BSD 提供的设备级功能的 C API。

请注意,您可以在OS X Man Pages中查看记录 BSD 和 POSIX 函数和工具的手册页。


三、架构概述

就像任何复杂系统一样,你可以从不同的角度和不同的粒度来看待 I/O Kit 的设计。

本章将向您介绍更重要的I/O Kit 的架构元素和概念领域:

  • 硬件建模、驱动程序对象的分层以及驱动程序系列、驱动程序和核心所扮演的角色
  • 设备驱动程序的运行环境
  • I/O Kit 注册表和 I/O 目录
  • 驱动程序匹配
  • I/O Kit 类层次结构
  • 设备接口

请记住,本章是概述,因此对每个主题的讨论都是有意简短的。

后面的章节将更详细地介绍这些主题中的大多数。

对于设备接口,文档 "从应用程序访问硬件" 非常详细地描述了该技术。


1、驱动程序分层

I/O Kit 设计的核心是模块化、分层的运行时架构,它模拟了通过捕获 I/O 连接中涉及的多个部分(硬件和软件)之间的动态关系,可以了解 OS X 系统的硬件。

连接层(包括驱动程序对象和这些对象所属的系列)以提供程序-客户端关系堆叠。

互连服务或设备链始于计算机的逻辑板(以及控制它的驱动程序),通过发现和"匹配"的过程,将连接扩展到控制系统总线(PCI、USB 等)以及连接到这些总线的各个设备和服务的驱动程序对象层。

您可以使用以下方式查看正在运行的 OS X 系统中驱动程序对象的分层I/O Registry Explorer 应用程序,包含在 OS X 的开发者版本中。

开发者版本还包括一个该应用程序的命令行版本:ioreg,您可以在终端窗口中运行它来显示当前的 I/O 注册表信息。

本节介绍 I/O Kit 的分层架构并描述其主要组成元素:系列、驱动程序和核心。


amilies and Drivers

I/O Kit 系列是一个或多个 C++ 类,它们实现特定类型所有设备所共有的软件抽象。

I/O Kit 包含总线协议系列(例如 SCSI 并行、USB 和 FireWire)、存储(磁盘)设备系列、网络服务系列(包括以太网)、人机界面设备系列(例如鼠标、键盘和操纵杆)以及大量其他设备系列。

A驱动程序通过继承成为某个系列的成员;驱动程序的类几乎总是系列中某个类的子类。

作为系列的成员,驱动程序继承了系列所有成员共有的数据结构(实例变量)和行为。

例如,所有 SCSI 控制器都有某些必须执行的操作,例如扫描 SCSI 总线;SCSI Parallel 系列定义并实现了此扫描功能。

因此,您无需在新的 SCSI 控制器驱动程序中包含扫描代码(除非您需要不同的扫描实现)。

大多数 I/O Kit 开发都涉及编写特定的驱动程序类,每个类都从提供驱动程序所需功能的系列中的超类继承。

例如,以太网控制器的驱动程序从 Network 系列中的 IOEthernetController 超类继承。

驱动程序与其自身系列的交互主要涉及实现该系列调用的成员函数。

这些通常是客户端配置和I/O 请求。

某些系列还定义对象和函数供驱动程序使用。

这些对象和函数的具体性质取决于驱动程序所使用的系列。

但是,驱动程序通常与两个家族合作。

除了驱动程序所属的家族之外,驱动程序类还必须与设备家族针对设备所连接的总线或协议发布的节点。

节点(如驱动程序和节点所详细解释的)是定义给定协议的访问点和通信通道的对象。

家族(通常表示总线,如 PCI 或 USB)通过其创建的节点充当驱动程序的提供者。

驱动程序使用节点连接到 I/O 注册表并与其设备通信。

例如,PCI 以太网驱动程序将使用 PCI 家族中的IOPCIDevice节点连接到 PCI 总线,并通过 PCI 总线进行通信。

驱动程序与节点的主要交互涉 及在节点所连接的总线上 发出请求或命令。

例如,SCSI 设备驱动程序通过节点发出 SCSI 命令块并检查结果。

有关家族的更多信息,特别是家族中超类的性质和组成,请参阅 I/O Kit 家族类


驱动程序和核心

I/O Kit 支持两大类驱动程序对象。

第一种是 nub,即定义访问点和通信通道的对象,通常用于给定协议,例如 PCI、USB 或以太网。

第二种是针对单个设备或服务的特定驱动程序。

特定驱动程序通过 nub 与硬件通信以执行 I/O 操作。

I/O Kit 中的驱动程序和 nub 都必须从 IOService 类继承。

A驱动程序是管理特定硬件的 I/O Kit 对象。

驱动程序以内核扩展的形式编写,通常安装在 Extensions 文件夹中(位于/System/Library/Extensions)。

有关创建和安装内核扩展的更多信息,请参阅 《内核编程指南》 中的 "内核扩展概述"

当为设备选择驱动程序时,但在该驱动程序被加载到内核(作为内核扩展)后,所有必需的系列(就超类及其依赖项而言)都会被加载,以便为驱动程序及其类型的其他驱动程序提供通用功能。

(当然,如果这些系列已经加载,则此步骤不是必需的。)满足驱动程序的所有要求后,将加载驱动程序并将其实例化为对象。

有关此过程的说明,请参阅 I/O 连接的剖析

节点是一个 I/O Kit 对象,它代表设备或逻辑服务的通信通道,并调解对设备和服务的访问。

例如,节点可以代表总线、磁盘、磁盘分区、图形适配器或键盘。

将节点视为设备插槽或连接器的软件表示可能会有所帮助。

节点还提供以下服务:仲裁、电源管理和驱动程序匹配(参见 I/O 注册表和 I/O 目录)。

Nubs 充当两个驱动程序之间的桥梁,进而充当两个系列之间的桥梁。

驱动程序作为其客户端与 nub(以及 nub 的系列)通信,并可以通过其系列发布一个 nub,该 nub 查找(通过匹配)由其提供驱动程序。

通常,驱动程序会为其控制的每个设备或服务发布一个 nub;但是,当驱动程序支持特定硬件时,它可以充当自己的 nub。


I/O 连接的剖析

I/O Kit 的分层架构模拟了系统硬件总线和设备之间的连接链,将通用功能集中到驱动程序可以与之交互的类中。

每一层都是其下一层的客户端,也是其上一层的服务提供者。

I/O Kit 系列定义的广泛层分组定义了一般类型的 I/O 提供者(如网络或 PCI 总线设备)的通用功能。

请考虑图 2-1,它说明了网络系列中基于 PCI 的以太网控制器驱动程序的客户端和提供者对象的典型分层。


图 2-1 驱动对象作为客户端和提供者


如该图所示,您的驱动程序通常位于两个系列之间,从上层系列中的类继承并使用下层系列的服务。

在以太网控制器,驱动程序参与 C++ 对象堆栈,其中包含来自网络和 PCI 系列的类的实例:

IONetworkStack(接口管理对象) 将 I/O Kit 对象连接到 BSD 网络设施。
IO以太网接口(小块) 管理与设备无关的数据传输和接收。
控制器驱动程序(驱动程序) 通过 IOPCIDevice 对象操作以太网控制器。 此对象继承自名为 IOEthernetController 的网络系列类。
IOPCIDevice(核心) 控制器的匹配点;为控制器提供基本的 PCI 总线交互。
IOPCIBridge (驱动程序) 管理 PCI 总线。 (其他对象为 IOPCIBridge 提供服务;它们的具体身份取决于硬件配置。)

另一种查看典型 I/O 连接中驱动程序对象堆栈的方法是从动态角度考虑堆栈。

换句话说,当 OS X 系统发现连接到它的新设备时会发生什么?驱动程序对象堆栈是如何构建的?为此,让我们使用 SCSI 磁盘驱动器的示例;图 2-2 中的创建或发现的一般顺序是从左到右。


图 2-2 SCSI 磁盘驱动程序连接中的驱动程序对象


该图说明了SCSI 磁盘驱动程序,存储系列连接到 PCI 总线。

在建立每个单独的连接时,新创建的驱动程序或 nub 也会添加到 I/O 注册表(在 I/O 注册表和 I/O 目录 中描述)。

连接链分为几个步骤:

  1. PCI 总线控制器驱动程序是PCI 系列,发现 PCI 设备并通过创建节点 ( IOPCIDevice) 宣布其存在。
  2. 节点识别(匹配)适当的设备驱动程序(在本例中为 SCSI 控制器驱动程序),并请求加载该驱动程序。
    加载 SCSI 控制器驱动程序会导致SCSI 并行系列及其所依赖的所有系列也将被加载。
    SCSI 控制器驱动程序被赋予对 IOPCIDevice nub 的引用。
  3. SCSI 控制器驱动程序是 PCI 系列的客户端,也是 SCSI 并行系列服务的提供者,它会扫描 SCSI 总线以查找可能是这些服务的客户端的设备。
    找到这样的设备(磁盘)后,驱动程序会通过创建一个节点 ( IOSCSIDevice) 来宣布该设备的存在。
  4. 节点通过匹配过程找到适合该设备的设备驱动程序(磁盘驱动程序)并请求加载该驱动程序。
    加载磁盘驱动程序会导致存储系列及其所依赖的所有系列也被加载。
    磁盘驱动程序现在是 SCSI 并行系列的客户端和存储系列的成员。
    磁盘驱动程序被赋予对IOSCSIDevice节点的引用。

在许多情况下,应用程序和其他"用户空间"程序可以使用 I/O Kit 的设备接口技术来驱动设备(包括大容量存储设备),从而无需驻留在内核的驱动程序。

有关此插件技术的概述,请参阅 从内核外部控制设备。


2、设备驱动程序的运行环境

I/O Kit 提供了运行时环境为驱动程序编写者提供了几个强大的功能,其中包括:

  • 动态分层驱动程序架构,允许随时加载和卸载驱动程序,并延迟保留昂贵的系统资源,直到需要它们为止
  • 用于在常见 I/O 操作期间管理数据的标准设施
  • 一个强大的系统,用于在 I/O 操作期间保护对驱动程序资源的访问,这使得驱动程序编写者不必编写自己的代码来启用和禁用中断并管理对驱动程序私有资源的锁定
  • 访问 libkern C++ 库(I/O Kit 本身基于此库)中的服务,以管理集合、执行原子操作和字节交换值,以便在不同类型的硬件上使用

以下部分总结了每个特征。


运行时功能

I/O Kit 驱动程序可以随时加载和卸载或激活和停用,通过软件发起的事件(例如启动和关闭网络堆栈时)和硬件发起的事件(例如在总线上添加或移除 USB 设备时)。

几乎所有驱动程序都必须在动态变化的系统环境中工作。

I/O Kit 通过定义标准使此过程更容易驱动程序对象的生命周期。
通过实现"通用 I/O Kit 类"
中总结的一小组函数,您的驱动程序可以妥善处理设备和服务的添加和删除,以及电源管理系统引起的变化。

在 OS X 中,几乎所有 I/O 操作都需要相同的准备:

  • 分页虚拟内存转换为物理内存
  • 将内存连线到位,这样在 I/O 操作期间就不会被调出页面
  • 建筑描述要读取或写入的数据缓冲区的分散/聚集列表

I/O Kit 提供了一组实用程序类来帮助驱动程序为 I/O 操作准备内存并构建分散/聚集列表,包括 IOMemoryDescriptor 和 IOMemoryCursor 类。

有关这些工具的更多信息,请参阅 管理数据一章

在多线程系统中运行的驱动程序必须能够保护其资源免受可重入或并发访问。

I/O Kit 包含一小组用于此目的的类。

工作循环对象运行一个专用线程并管理用于独占访问数据的门控机制。

其他对象称为事件源,使用门控机制来序列化访问关键资源的函数调用,在调用函数之前关闭工作循环门。

有关工作循环和事件源的更多信息,请参阅 处理事件一章

这I/O Kit 本身基于 libkern C++ 库,它提供驱动程序通常需要的服务,包括:

  • 保证原子性的算术和逻辑运算
  • 字节交换值大端字节序和小端格式
  • 常见数据集合(例如字符串、数组和字典)的类

有关 libkern 类的更多信息,请参阅 OS 类以及随 OS X 开发人员包安装的 libkern 参考文档。


内核编程约束

内核代码始终驻留在身体的内存,并且不能由页面调出虚拟内存系统。

这使得内核资源比应用程序资源昂贵得多。

如果出现以下情况,您的驱动程序应该驻留在内核中:

  • 它需要主中断(在这种情况下它必须存在于内核中)
  • 它的主要客户端驻留在内核中;例如,大容量存储驱动程序,因为文件系统堆栈驻留在内核中

例如,磁盘、网络控制器和键盘的驱动程序驻留在内核中。

如果您的驱动程序一次只被一个用户空间程序偶尔使用,则应该由该程序加载并驻留在其中。

扫描仪和打印机等设备的驱动程序驻留在用户空间程序中,使用 I/O Kit 的设备接口机制与设备通信。

有关设备接口的更多信息,请参阅 从内核外部控制设备

即使驱动程序驻留在内核中,您也应该尽量减少驻留在内核的代码量以及该代码执行的处理量。

例如,控制中断驱动硬件设备的专用应用程序应该提供一个驱动程序,该驱动程序将服务中断所需的最少代码放入内核中,使数据可供其客户端使用,然后返回。

有关谨慎使用内核编程的其他原因,请参阅 您应该在内核中编程吗?

如果您的驱动程序必须驻留在内核中,您应该注意以下问题:

  • 最重要的是,内核是一个单一的程序------没有驱动程序与内核其余部分之间的内存保护。
    行为不当的内核驻留驱动程序可能会导致操作系统崩溃或挂起。
  • 一个更微妙的问题是内核中的函数调用堆栈限制为 16 KB。
    注意不要在函数中声明较大的局部变量。
    只要有可能,就应该预先分配缓冲区并重新使用它们。

驻留在内核的驱动程序可以完全访问内核编程接口。

但是,由于其操作级别较低,驱动程序应该只使用Mach 调用而不是BSD 调用。

目前,BSD 内核代码的许多部分对于多线程或多处理来说并不安全。

无论如何,驱动程序很少需要直接执行 Mach 调用,因为 I/O Kit 为驱动程序所需的大多数内核级功能提供了接口。


3、I/O 注册表和 I/O 目录

这I/O Registry 是一个动态数据库,它记录 OS X 系统上参与硬件连接的驱动程序对象网络,并跟踪这些对象之间的提供者-客户端关系。

设备驱动程序必须记录在 I/O 注册表中才能参与大多数 I/O Kit 服务。

I/O 注册表是 I/O Kit 的重要组成部分,因为它支持操作系统的动态功能,允许用户在正在运行的系统中添加或删除设备(特别是 FireWire 或 USB 设备),并让它们立即可用,而无需重新启动。

添加硬件时,系统会自动查找和加载必要的驱动程序,并更新 I/O 注册表以反映新的设备配置;移除硬件时,会卸载相应的驱动程序并再次更新注册表。

注册表始终驻留在系统内存中,不会存储在磁盘上或在启动之间存档。

I/O Registry 将其数据构造为倒置树。

树中的每个对象都来自父节点,可以有一个或多个子节点;如果它是"叶"节点,则没有子节点。

树中的几乎每个节点都代表一个驱动程序对象:一个 nub 或一个实际驱动程序。

这些对象必须从 IORegistryEntry 类(它是 IOService 的超类,而 IOService 是所有驱动程序类的超类)继承。

IORegistryEntry 对象的主要特征是关联属性的列表。

这些属性反映了驱动程序匹配(请参阅 驱动程序匹配)并以其他方式添加有关驱动程序的信息。

注册表中捕获的属性来自每个驱动程序的信息属性列表,该列表是驱动程序 KEXT 中的一个文件,其中包含描述驱动程序的特征、设置和要求的键值对。

另一个动态数据库I/O 目录与 I/O 注册表密切配合。

I/O 目录维护系统上所有可用驱动程序的条目。

当 nub 发现设备时,它会从 I/O 目录中请求该设备系列的所有驱动程序的列表。

您可以使用 I/O Registry Explorer 应用程序和ioreg命令行工具,均包含在 OS X 开发者包中。

您还可以使用 IORegistryEntry 类的成员函数以编程方式探索和操作注册表项的属性。

从应用程序和用户空间中的其他程序,您可以使用 I/O Kit 框架中的 API 在 I/O 注册表中搜索和访问驱动程序信息。

有关 I/O 注册表和 I/O 目录的更多信息,请参阅 "I/O 注册表"一章。

有关 IORegistryEntry 类的更多信息,请参阅 "基类" 一章中的动态驱动程序注册(IORegistryEntry)


4、驱动程序匹配

nubs 的主要功能是提供匹配服务,将驱动程序与设备匹配。

与 Mac OS 8 和 9 不同,驱动程序不会因为安装而自动加载。

在 OS X 中,必须先将驱动程序与现有设备匹配,然后才能加载该驱动程序。

驱动程序匹配是 I/O Kit 的一个过程,其中 nub 在发现特定硬件设备后,搜索最适合该设备的驱动程序。

为了支持驱动程序匹配,每个设备驱动程序都定义了一个或多个个性 指定它可以支持的设备类型。

此信息存储在 XML 字典中,该字典定义在驱动程序包中的信息属性列表。

字典值指定驱动程序是否是特定设备的候选驱动程序。

当一个 nub 检测到一个设备时,I/O Kit 会通过三个不同的阶段为该 nub 查找并加载驱动程序,使用减法过程直到找到成功的候选者。

匹配阶段如下:

  1. 类别匹配 ------ 消除错误设备类别的驱动程序。
  2. 被动匹配 ------ 检查每个剩余驱动程序的特性,了解设备的特定属性,从而消除那些不匹配的驱动程序。
  3. 主动匹配 ------ 剩余的驱动程序候选探测设备以验证它们是否可以驱动它。

当找到匹配的驱动程序时,将加载其代码并创建个性中列出的主要类的实例。

此时驱动程序的生命周期开始。

有关详细信息,请参阅 "基类"一章中的驱动程序对象生命周期。

有关Driver personalities和匹配过程的详细讨论,请参阅 驱动程序和设备匹配章节


5、I/O Kit 类层次结构

I/O Kit 包含几十个 C++ 类,它本身是libkern C++ 库,可加载的基础内核模块。

综合起来,I/O Kit 和 libkern 似乎构成了一个庞大而复杂的类层次结构。

然而,该层次结构的基本结构相当简单,如图2-3所示。

您可以将扩展的 I/O Kit 类层次结构细分为三大类:

  • libkern 的类(由于其前缀为"OS",有时也称为 OS 类)
  • I/O Kit 基类和辅助类
  • I/O Kit 系列的类别

有关 libkern 和 I/O Kit 库的二进制文件和头文件安装位置的信息,请参阅 框架和库。有关此层次结构中的基类(OSObject、OSMetaClass、IORegistryEntry 和 IOService)的功能和接口的详细信息,请参阅 基类


图 2-3 I/O Kit 扩展类层次结构


注意: 附录 "基类和辅助类层次结构" 包含 I/O Kit 基类和辅助类的层次结构图;附录 "I/O Kit 家族参考" 包含大多数 I/O Kit 家族的类层次结构图。


操作系统类

I/O Kit 建立在 libkern C++ 库之上;也就是说,I/O Kit 特定类的根超类是 IORegistryEntry,它继承自 libkern 的 OSObject。

与 I/O Kit 一样,libkern 是用 C++ 的一个子集编写的,适合用于可加载内核模块。

具体来说,libkern C++ 环境不包括 C++ 异常处理和运行时类型信息 (RTTI) 功能。

相反,OS 基类实现了与 RTTI 功能相当的功能,以及其他功能。

扩展层次结构的根源是OSObject 类,与该类密切相关的是 OSMetaClass 类。

所有其他 OS 类都是诸如集合和其他数据容器之类的"辅助"类。

以下总结了这些类所起的作用:

  • OSMetaClass 实现了一种运行时类型信息(RTTI)机制,实现了一定程度的对象自省,并支持通过类名对从 OSObject 派生的对象进行运行时分配。
  • OSObject 具有用于引用计数(retainrelease)的 API、保留对象的内存管理以及不再需要时自动处置对象的 API。
    OSObject 还提供了initfree方法的动态默认实现。
  • 操作系统数据容器是 OSObject 的子类,其实例封装了各种类型的数据值(比如布尔值、数字、字符串)并实现和迭代数组和字典等集合。
    操作系统数据容器在名称和行为上与用户空间对应容器 Core Foundation 容器大致相同。
    由于操作系统和 Core Foundation 类的特征非常相似,因此系统可以轻松地将 Core Foundation 类型转换为操作系统类型,反之亦然。
    例如,当跨越用户内核边界时,CFArray 对象会转换为 OSArray。

OS 类通常适用于为内核编写的所有代码,而不仅仅是设备驱动程序。

例如,实现网络服务或文件系统的内核扩展也可以利用这些类。

OSObject 是内核代码必不可少的通用超类。

首先内核模块(如果要使用 KMOD 引用另一个 KMOD 创建的对象,则这些对象最终必须派生自 OSObject。

大多数 I/O Kit 类都假设传递的对象派生自 OSObject。

如果您要将现有的 C++ 代码移植到 I/O Kit,则无需使用 OS 类。

但如果您决定放弃这些类提供的功能(如引用计数或数据容器),则可能需要自己实现它们。

有关 OSObject 和 OSMetaClass 类的更多信息,请参阅 "基类" 一章中的 libkern 基类


通用 I/O Kit 类

扩展类层次结构的中间组包括 IO Kit 基类------IORegistryEntry 和IOService --- 以及一组用于资源管理、数据管理以及线程和输入控制的辅助类。

这组 I/O Kit 类被指定为"通用",因为所有设备驱动程序类都可能使用它们。

I/O Kit 层次结构的根类是 IORegistryEntry;它继承自IORegistryEntry 是一个 I/O Kit 对象,可以是 I/O Registry 中的一个节点,并具有一个或多个与之关联的属性表(Driver personalities)。

IORegistryEntry 实现了许多功能:

  • 它通过驱动程序attachdetach入口点管理与注册表的连接
  • 它使用以下方式管理定义Driver Personalities的属性表 OSDictionary 对象
  • 它实现了注册表中的锁定,允许以原子方式对注册表进行更新

IOService 是 IORegistryEntry 的唯一直接子类。

几乎所有 I/O Kit 家族超类都直接或间接地继承自 IOService。

最重要的是,IOService 指定动态运行时环境中设备驱动程序的生命周期。

通过匹配虚拟函数对(例如init/ freestart/stopopen/ close),IOService 定义驱动程序对象如何初始化自身、将自身附加到 I/O 注册表、执行所有必要的分配,然后以正确的顺序反转这些操作的影响。

为了支持其对驱动程序生命周期的管理,IOService 提供匹配服务(例如,协助探测)并根据提供程序的存在来实例化驱动程序。

此外,IOService 还包括可用于各种目的的成员函数,包括:

  • 通知和消息传递
  • 电源管理
  • 设备内存(映射和访问)
  • 设备中断(注册、取消注册、启用、引发等等)

有关 IORegistryEntry 和 IOService 类的更多信息,请参阅 "基类" 一章中的 I/O Kit 基类


大多数 I/O Kit 帮助类 都具有几个 与设备驱动程序 运行时环境 相关的函数:

  • 实施工作循环和事件源(中断、计时器和命令)以及相关的锁和队列
  • 实现内存游标和内存描述符来管理 I/O 传输中涉及的数据

有关 I/O Kit 辅助类的更多信息,请参阅处理事件管理数据 章节


I/O Kit 系列类

大多数驱动程序都是 I/O Kit 家族中某个类的子类的实例;反过来,家族类往往是 IOService 的子类。

您的驱动程序类应该是您要编写的驱动程序最合适的家族类的直接子类。

例如,如果您正在编写以太网控制器驱动程序,您的驱动程序类应该是 IOEthernetController 的子类,而不是 IONetworkController(IOEthernetController 的超类)。

I/O Kit 有十几个家族,每个家族都有自己的一组类;这些家族包括以下这些:

  • ADB
  • ATA and ATAPI
  • Audio
  • FireWire
  • Graphics
  • HID (Human Interface Devices)
  • Network
  • PC Card
  • PCI and AGP
  • SBP-2
  • SCSI Architectural Model
  • SCSI Parallel
  • Serial
  • Storage
  • USB

Apple 会在开发过程中 添加更多系列。

如果您需要某个设备的系列,但目前尚未如果支持,您可以尝试编写自己的系列类。

但是,如果当前不存在系列,请不要认为需要新的系列。

在许多情况下,IOService 类提供了驱动程序所需的所有服务;此类"无系列"驱动程序可以支持特定于某些供应商的许多设备。

有关系列的更多信息,请参阅 I/O Kit 系列 一章和附录I/O Kit 系列参考


6、从内核外部控制设备

OS X 最引人注目的功能之一可能是它在进程的虚拟地址空间之间强制执行的不可侵犯的分离。

除非对共享内存进行费力的安排,否则一个进程无法直接接触映射到另一个进程地址空间的数据。

这种分离通过防止内存破坏程序和类似的麻烦导致进程崩溃,增强了系统的稳定性和可靠性。

更重要的是内核地址空间与所有其他进程地址空间之间的分离,有时(从内核的角度来看)这些进程位于"用户空间"。

如果用户空间中的应用程序或其他程序以某种方式侵犯了内核的地址空间,整个系统就会崩溃。

为了使内核和用户空间之间的这种分离更加严密,用户空间中的程序甚至不能直接调用内核 API。

它们必须进行系统调用才能(间接)访问内核 API。

但是,有时用户空间中的程序需要控制或配置设备,因此需要访问内核中的 I/O Kit 服务。

例如,游戏可能需要设置显示器深度或音量,或者磁盘备份程序可能需要充当磁带驱动器的驱动程序。

其他必须以某种方式与内核交互以驱动硬件的应用程序示例包括运行或解释来自扫描仪、操纵杆和数码相机的数据的应用程序。

为了满足这个要求,I/O Kit 包含两种机制:设备接口和POSIX 设备节点。

通过插件架构和定义良好的接口,设备接口机制使用户空间中的程序能够与内核中适合其希望控制的设备类型的节点进行通信。

通过节点,程序可以访问 I/O Kit 服务和设备本身。

对于存储、串行和网络设备,应用程序可以从 I/O Kit 获取所需的信息,以便使用 POSIX API 访问和控制这些设备。

请记住,有些系列服务 I/O Kit 拒绝将其作为设备接口导出到用户空间;这些服务仅在内核内部可用。

例如 PCI 系列。

出于稳定性和安全性的原因,禁止外部访问 PCI 资源。

附录I/O Kit 系列参考列出了导出设备接口的系列。

本节总结了*从应用程序访问硬件* 中的信息。

请参阅本文档以获取设备接口及其使用方法的完整描述。


设备接口机制

设备接口是内核与用户空间进程之间的插件接口。

该接口符合由以下定义的插件体系结构:核心基础插件服务(CFPlugIn),而它又与微软的组件对象模型 (COM)。

在 CFPlugIn 模型中,内核充当插件主机,拥有自己的一组定义良好的 I/O Kit 接口,I/O Kit 框架提供一组插件(设备接口)供应用程序使用。

从概念上讲,设备接口跨越了用户空间和内核之间的界限。

它处理谈判,身份验证和类似任务,就像内核驻留驱动程序一样。

在用户空间方面,它通过其导出的编程接口实现与应用程序(或其他程序)的通信。

在内核方面,它通过该系列的驱动程序对象创建的节点实现与相应 I/O Kit 系列的通信。

从内核的角度来看,设备接口似乎是一个驱动程序,被称为"用户客户端。

"从应用程序的角度来看,设备接口是一组可以调用的函数,应用程序可以通过这些函数将数据传递给内核并从内核接收数据。

这是因为,从元素层面上讲,设备接口是指向函数指针表的指针(尽管它也可以包含数据字段)。

一旦应用程序获得设备接口的实例,就可以调用该接口的任何函数。


图 2-4说明了设备接口的体系结构,其中显示了通过设备接口获取 SCSI 硬盘访问权限的应用程序。

最好将此图视为 图 2-2的变体,图 2-2 显示了驻留在内核的 SCSI 磁盘驱动程序所建立的一系列驱动程序对象连接。


图 2-4 通过设备接口控制 SCSI 设备的应用程序。


一开始,从 PCI 总线驱动程序到 SCSI 设备节点,会发生相同的一系列操作(设备发现、节点创建、匹配、驱动程序加载)。

但随后,SCSI 设备节点会匹配并加载设备接口作为其驱动程序(而不是驻留在内核的驱动程序)。

在应用程序使用设备接口机制访问设备之前,它必须先找到该设备。

它通过一个称为设备匹配。

在设备匹配中,应用程序创建一个"匹配字典",指定目标设备的属性,然后调用 I/O Kit 函数并传入该字典。

该函数搜索 I/O Registry 并返回一个或多个匹配的驱动程序对象,然后应用程序可以使用这些对象加载适当的设备接口。

有关此主题的更多信息,请参阅 设备匹配


如果您开发的自定义驱动程序不是 I/O Kit 系列中某个类的子类,并且您希望应用程序能够访问该驱动程序,则必须编写自己的设备接口。

在用户空间和内核之间进行通信的任何代码都必须使用以下一个或多个功能:

  • BSD 系统调用
  • 机房工控机
  • Mach 共享内存

I/O Kit 主要使用 Mach IPC 和 Mach 共享内存。

相比之下,OS X 的网络和文件系统组件主要使用 BSD 系统调用。


POSIX 设备文件

BSD 是 OS X 内核环境的核心组件,它导出了许多符合 POSIX 标准的编程接口。

这些接口支持通过设备文件与串行、存储和网络设备进行通信。

在任何基于 UNIX 的系统中,例如BSD 中,设备文件是位于/dev代表阻止或字符设备,例如终端、磁盘驱动器或打印机。

如果您知道设备文件的名称(例如 disk0s2mt0),则您的应用程序可以使用 POSIX 函数(例如openreadwriteclose )来访问和控制相关设备。

I/O Kit 在发现设备时会动态创建/dev 中的设备文件。

因此,设备文件集会不断变化;不同的设备可能在任何时候连接到 /dev 中的设备文件,并且同一设备在不同时间可能具有不同的设备文件名。

因此,您的应用程序无法对设备文件名进行硬编码。

对于特定设备,您必须通过涉及设备匹配的过程从 I/O Kit 获取其设备文件的路径。

获得路径后,您可以使用 POSIX API 访问该设备。

请注意,您可以访问使用用户空间的网络服务BSD 套接字 API。

但是,您通常应该使用套接字仅当碳和Cocoa 环境无法提供您所需的功能。


四、I/O 注册表

这I/O 注册表是一个动态数据库,它描述了一组"实时"对象(nubs 或驱动程序),并跟踪它们之间的提供者-客户端关系。

当从系统中添加或移除硬件时,注册表会立即更新以反映设备的新配置。

作为 I/O Kit 的一个动态部分,注册表不存储在磁盘上或在启动之间存档。

相反,它是在每次系统启动时构建的,并驻留在内存中。

用户空间可以通过以下 API 访问 I/O 注册表:I/O Kit 框架。

这些 API 包括强大的搜索机制,可让您在注册表中搜索具有特定特征的对象。

您还可以使用 OS X 开发人员版本提供的应用程序查看计算机上注册表的当前状态。

本章介绍了 I/O Registry 的体系结构以及 Registry 用于表示对象之间关系的平面。

它还概述了设备匹配并介绍了允许您浏览 Registry 的应用程序。


1、I/O 注册表架构和构造

将 I/O 注册表视为一棵树是最有用的:每个对象都是从父节点衍生的节点,并且具有零个或多个子节点。

注册表几乎在所有方面都遵循树的定义,但少数节点有多个父节点除外。

这种情况的主要示例是 RAID 磁盘控制器,其中多个磁盘被组合在一起以显示为单个卷。

然而,除了特殊情况外,将注册表视为树将有助于您直观地了解它的构建和更新方式。

在启动时,I/O Kit 会注册一个平台专家,特定主板的驱动程序对象,它知道系统正在运行的平台类型。

此节点充当 I/O 注册表树的根。

然后,平台专家节点会为该平台加载正确的驱动程序,该驱动程序将成为根的子节点。

平台驱动程序会发现系统上的总线,并为每个总线注册一个节点。

随着 I/O Kit 将每个节点与其相应的总线驱动程序匹配,以及每个总线驱动程序发现与其相连的设备并将驱动程序与其匹配,树将继续增长。

当发现一个设备时,I/O Kit 会从另一个动态数据库(I/O 目录)请求该设备类类型的所有驱动程序列表。

I/O 注册表维护当前运行系统中活动对象的集合,而I/O 目录维护可用驱动程序的集合。

这是驱动程序匹配的三步流程中的第一步,该流程在 驱动程序和设备匹配中进行了描述

课程类型等信息保存在驾驶员的信息属性列表,包含 XML 结构属性信息的文件。

属性列表以键值对字典的形式描述驱动程序的内容、设置和要求。

当读入系统时,此信息将转换为 OS 容器,例如字典、数组和其他类型。

I/O Kit 使用此列表驱动程序匹配;用户应用程序可以在 I/O 注册表中搜索具有特定属性的对象,这个过程称为设备匹配。

您还可以使用 I/O 注册表资源管理器(一个显示注册表的应用程序)查看计算机当前加载的驱动程序的属性列表。

记住 I/O 注册表的树状结构,现在将每个节点想象成一个柱状延伸到第三维。

以平台专家节点为根的二维注册表树现在显示在垂直穿过这些柱状的平面上。

I/O Kit 定义了许多这样的平面(您可以将它们视为一组平行平面在不同的高度切割柱子)。

有关此结构的说明请参见图3-1 。


图 3-1 I/O 注册表中的两个平面


有六个I/O 注册表中定义的平面:

  • Service
  • Audio
  • Power
  • Device Tree
  • FireWire
  • USB

每个平面都通过仅显示存在于该关系中的连接来表达 I/O 注册表中对象之间的不同提供者-客户端关系。

最通用的是服务平面,它按注册表构建期间对象所附加的同一层次结构显示对象。

注册表中的每个对象都是其父级提供的服务的客户端,因此每个对象与注册表树中其祖先的连接在服务平面上都是可见的。

其他平面显示出更具体的关系:

  • 音频平面提供了音频信号链的表示,核心音频框架及其插件用于发现有关系统音频设备之间的音频信号路径的信息。
  • 这电源平面显示 I/O 注册表对象之间的电源相互依赖关系,允许您跟踪从提供商到客户端的电源流,并发现如果特定设备断电,哪些对象可能会受到影响。
  • 这设备树平面代表开放固件设备层次结构。
  • 这FireWire 和每个 USB 平面都代表由这些标准定义的内部层次结构。

记住有关 I/O 注册表中的平面的以下几点非常重要:

  • 所有 I/O 注册表对象都存在于所有平面上,但在任何单个平面上,只有由该平面定义的关系连接的对象才可见。
  • 驱动程序不会附加到任何特定平面上的注册表。
    相反,如果驱动程序与其他对象的提供者-客户端关系符合该平面的定义,则驱动程序可以参与该平面的连接。

2、I/O 注册表资源管理器

OS X 的开发者版本提供了一个名为I/O 注册表资源管理器可用于检查计算机上设备的配置。

I/O 注册表资源管理器提供 I/O 注册表树的图形表示。

默认情况下,它显示服务平面,但您可以选择检查任何平面。

命令行等效项,ioreg,在终端窗口中显示树。

此工具的优点是,如果您想要在电子邮件中发送该信息,则可以剪切和粘贴树的各个部分。

您可以在终端应用程序中的 shell 提示符下键入man ioreg,以获取使用 ioreg 的完整描述。

打开 I/O Registry Explorer 后,会出现一个分割窗口,其中 I/O Registry 对象位于右上方,六个平面位于左上方,所选对象的属性列表位于窗口的下半部分。

对象后面带有一个三角形,表示它是父节点。

您可以通过单击父节点并向右拖动滚动条来显示其子节点,从而遍历 I/O Registry 树。

图 3-2 显示了 I/O Registry Explorer 窗口中属性列表的示例。

工具菜单中的命令可帮助您搜索 I/O 注册表并检查其内容:

  • /Applications/Utilities如果从 Finder 打开了 I/O 注册表资源管理器,"转储注册表字典到输出" 会将 I/O 注册表内容放入控制台日志(可通过 中的控制台应用程序查看)。
  • 检查器 以 ASCII 格式显示当前选定对象的属性列表。
    在主窗口中选择特定属性会导致其值显示在检查器窗口中。
  • 强制注册表更新会 更新 I/O 注册表资源管理器的 I/O 注册表图片,以反映自您首次打开应用程序以来可能发生的任何更改。
  • Find 对您输入的字符串执行不区分大小写的搜索,如果成功,则显示字符串出现的路径,并用冒号分隔对象名称。

图 3-2 I/O Registry Explorer 窗口示例


五、驱动程序和设备匹配

在使用设备(或任何服务提供程序)之前,必须先找到该设备的驱动程序并将其加载到内核中。

I/O Kit 定义了一个灵活的三阶段匹配过程,可将候选驱动程序范围缩小到一个或多个驱动程序。

然后加载最终候选驱动程序(如果有多个候选驱动程序,则加载最符合条件的驱动程序),并让其首先有机会管理设备或服务提供程序。

匹配过程利用驱动程序信息属性列表中以 XML 键值对形式定义的匹配字典。

每个匹配字典指定驱动程序的个性,声明其是否适用于特定类型的设备或服务。

本章讨论了Driver personalities和描述它们的匹配语言。

然后,它描述了匹配过程,该过程使用Driver personalities中的信息来识别检测到的设备的最合适驱动程序。

本章还简要讨论了应用程序用于加载设备接口的设备匹配过程。

有关完整详细信息,请参阅 从应用程序访问硬件。


1、Driver Personalities和匹配语言

每个设备驱动程序都被视为可加载内核扩展(KEXT),必须定义一个或多个个性信息指定了它可以支持的设备类型。

此信息存储在 XML 中驱动程序的 KEXT 包中的信息属性列表 ( Info.plist) 中定义的匹配字典。

从这个意义上讲,字典是键值对的集合,其中 XML 标签<key></key>包含键。

紧跟在键后面的是包含值的标签;这些标签指示值的数据类型;例如,

xml 复制代码
<integer>74562</integer>

将定义一个整数值。

每个匹配的字典本身都包含在 信息属性列表的IOKitPersonalities字典。

个性的字典值指定驱动程序是否是特定设备的候选。

个性中的所有值必须匹配,才能为设备选择驱动程序;换句话说,对这些值执行逻辑 AND。

一些键可能采用空格分隔的值列表,这些值通常以 OR 方式检查。

因此,您可能有一个针对特定 PCI 卡Driver personalities的"型号"键,它采用型号列表,每个型号都标识特定卡供应商支持的型号。

所需的特定键取决于系列。

例如,PCI 卡的驱动程序可以定义一个值,该值将根据 PCI 供应商和设备 ID 寄存器进行检查。

某些系列(例如 PCI 系列)提供了相当复杂的匹配策略。

例如,考虑以下键值对:

c 复制代码
<key>IOPCIMatch</key>
<string>0x00789004&0x00ffffff 0x78009004&0xff00ffff</string>

此表达式用于匹配各种 Adaptec SCSI 卡,由两个复合值组成,每个值都可以是有效匹配。

为了评估这些值,驱动程序系列从 PCI 卡读取 32 位供应商和设备 ID,并使用每个 & 符号右侧的值对其进行屏蔽。

然后将该操作的结果与 & 符号左侧的值进行比较,以确定是否存在匹配。

例 4-1显示了以太网控制器驱动程序的 XML 文件中Driver personalities的部分列表。


例 4-1 以太网控制器 XML 个性部分例

c 复制代码
<key>IOKitPersonalities</key>
    <dict>
        <dict>
            <!-- Each personality has a different name. -->
            <key>Name</key>      <string>PCI Matching</string>
 
            <!-- ... some keys not shown ... -->
 
            <!-- The name of the class IOKit will instantiate when probing. -->
            <key>IOClass</key>   <string>ExampleIntel82558</string>
 
            <!-- IOKit matching properties
              -- All drivers must include the IOProviderClass key, giving
              -- the name of the nub class that they attach to. The provider
              -- class then determines the remaining match keys. A personality
              -- matches if all match keys do; it is possible for a driver
              -- with multiple personalities to be instantiated more than once
              -- if several personalities match.
              -->
            <key>IOProviderClass</key>
                <string>IOPCIDevice</string>
 
            <!-- IOPCIDevice matching uses any of four possible PCI match
              -- criteria. This personality just uses IOPCIMatch to check the
              -- device/vendor ID.
              -->
            <key>IOPCIMatch</key>
                <string>0x12298086</string>
 
            <!-- The initial match score for this personality.-->
            <key>IOProbeScore</key>    <integer>400</integer>
        </dict>
 
        <dict>
            <!-- Can have additional personalities. -->
            <!-- ... (not shown) -->
        </dict>
    </dict>

如例 4-1中所述,每个驱动程序都必须包含一个IOProviderClass键,该键的值用于标识驱动程序所附加到的节点。

在极少数情况下,驱动程序可能会将其声明IOResourcesIOProviderClass 键的值。
IOResources是一个附加到 I/O 注册表根目录的特殊节点,它使资源(如 BSD 内核)在整个系统中可用。

传统上,虚拟设备的驱动程序匹配 IOResources,因为虚拟设备不会发布自己的节点。

这种驱动程序的另一个示例是 HelloIOKit KEXT(如使用 Xcode 创建设备驱动程序 中所述),匹配 IOResources,因为它不控制任何硬件。

重要提示: 任何将 声明IOResourcesIOProviderClass 键值的驱动程序还必须在其个性中包含IOMatchCategory键和一个私有匹配类别值。

这可以防止驱动程序在IOResources nub 上进行独占匹配,从而阻止其他驱动程序在其上进行匹配。

它还可以防止驱动程序与需要在IOResources 上匹配的所有其他驱动程序竞争。
IOMatchCategory 属性的值应与驱动程序 IOClass 属性的值相同,即驱动程序的类名采用反向 DNS 表示法,使用下划线而不是点,例如 com_MyCompany_driver_MyDriver

由于驱动程序可以包含多个匹配的字典,每个字典为驱动程序定义不同的个性,因此可以为不同的设备加载相同的驱动程序代码。

出于竞争的目的,I/O Kit 将每个个性视为驱动程序。

如果在任何单个个性中,该系列所需的所有属性都匹配,则驱动程序的代码将被加载并有机会为该设备运行。

由于各种原因,您的驱动程序可以具有多种个性。

可能是因为驱动程序(在 KEXT 中打包)支持多种类型的设备,或者更常见的是,支持同一类型设备的多个版本。

另一个原因可能是驱动程序支持类似的设备,每个设备都通过不同的总线连接到系统;例如,Zip 驱动器可以连接到 USB、FireWire、SCSI、ATAPI 和其他总线。

由于每个设备都连接到不同的 nub 类,因此它具有不同的匹配值。

驱动程序的个性也可以从设备通用到设备特定。

AppleUSBAudio 驱动程序的个性(例 4-2)说明了这一点。


例 4-2 AppleUSBAudio 驱动程序的Driver personalities

xml 复制代码
<key>IOKitPersonalities</key>
    <dict>
        <key>AppleUSBAudioControl</key>
        <dict>
            <key>CFBundleIdentifier</key>
            <string>com.apple.driver.AppleUSBAudio</string>
            <key>IOClass</key>
            <string>AppleUSBAudioDevice</string>
            <key>IOProviderClass</key>
            <string>IOUSBInterface</string>
            <key>bInterfaceClass</key>
            <integer>1</integer>
            <key>bInterfaceSubClass</key>
            <integer>1</integer>
        </dict>
        <key>AppleUSBAudioStream</key>
        <dict>
            <key>CFBundleIdentifier</key>
            <string>com.apple.driver.AppleUSBAudio</string>
            <key>IOClass</key>
            <string>AppleUSBAudioDMAEngine</string>
            <key>IOProviderClass</key>
            <string>IOUSBInterface</string>
            <key>bInterfaceClass</key>
            <integer>1</integer>
            <key>bInterfaceSubClass</key>
            <integer>2</integer>
        </dict>
        <key>AppleUSBTrinityAudioControl</key>
        <dict>
            <key>CFBundleIdentifier</key>
            <string>com.apple.driver.AppleUSBAudio</string>
            <key>IOClass</key>
            <string>AppleUSBTrinityAudioDevice</string>
            <key>IOProviderClass</key>
            <string>IOUSBInterface</string>
            <key>bConfigurationValue</key>
            <integer>1</integer>
            <key>bInterfaceNumber</key>
            <integer>0</integer>
            <key>idProduct</key>
            <integer>4353</integer>
            <key>idVendor</key>
            <integer>1452</integer>
        </dict>
    </dict>

此匹配字典定义了三种个性:AppleUSBAudioControlAppleUSBAudioStreamAppleUSBTrinityAudioControl

在匹配检测到的 USB Trinity 音频控制设备时,将选择 AppleUSBTrinityAudioControl

对于任何其他音频控制设备,通用个性 ( AppleUSBAudioControl) 将匹配。

人格的一个共同特征是探测分数。

探测分数是一个整数,反映驱动程序是否适合驱动特定设备。

驱动程序可能在其个性中有一个初始探测分数值,并且它可能会实现一个probe功能,允许它根据其驱动设备的适用性修改此默认值。

与其他匹配值一样,探测分数特定于每个系列。

这是因为一旦匹配通过类匹配阶段,只有来自同一家族的个性才能竞争。

有关探测分数以 及 驱动程序在probe函数中执行的操作的更多信息,请参阅 设备探测


2、驱动程序匹配和加载

在启动时以及任何时候添加或删除设备时,都会对每个检测到的设备(或其他服务/System/Library/Extensions驱动程序提供者)。

该过程动态地为设备或服务找到最合适的驱动程序。

"架构概述" 一章中的"驱动程序匹配" 中所述,当总线控制器驱动程序扫描其总线并检测连接到该总线的新设备。

对于每个检测到的设备,控制器驱动程序都会创建一个nub。

然后,I/O Kit 启动匹配过程并从设备获取用于匹配的值(例如,检查 PCI 寄存器)。

一旦为 nub 找到合适的驱动程序,就会注册并加载该驱动程序。

该驱动程序反过来可能会创建自己的 nub(可能通过从其家族继承的行为),这将启动匹配过程以找到合适的驱动程序。


驱动程序匹配

当 nub 检测到设备时,I/O Kit 会使用减法过程,分三个不同阶段为 nub 查找并加载驱动程序。

在每个阶段,会从可能的候选者总池中减去那些不太可能匹配的驱动程序,直到找到成功的候选者。

这匹配过程如下:

  1. 类匹配 步骤中,I/O Kit 通过排除任何与提供程序 服务(即 nub)不匹配的驱动程序来缩小潜在驱动程序列表。
    例如,当搜索 USB 驱动程序时,可以排除所有源自 SCSI 类的驱动程序对象。
  2. 被动匹配 步骤中,将检查驱动程序的个性(在驱动程序的 XML 信息属性列表中指定)是否具有特定于提供商系列的属性。
    例如,个性可能指定特定的供应商名称。
  3. 主动匹配 步骤中,驱动程序的probe函数会根据与之匹配的 nub 进行调用。
    此函数允许驱动程序与设备进行通信并验证它是否确实可以驱动该设备。
    驱动程序会返回一个探测分数,以反映其驱动设备的能力。
    有关更多信息,请参阅 设备探测。在主动匹配过程中,I/O Kit 会加载并探测所有候选驱动程序,然后按从高到低的顺序对它们进行排序探测分数。

然后,I/O Kit 选择探测分数最高的剩余驱动程序并启动它。

如果驱动程序成功启动,则将其添加到I/O Registry 和任何剩余的驱动程序候选都会被丢弃。

如果未成功启动,则启动探测分数第二高的驱动程序,依此类推。

如果可能的候选池中有多个驱动程序,如果两者都声称能够驱动该设备,则更通用的驱动程序通常会输给更具体的驱动程序。


设备探测

在主动匹配阶段,I/O Kit 请求池中剩余的候选驱动程序探测设备,以确定它们是否可以驱动该设备。

I/O Kit 调用在IOService 类,在某些情况下由驱动程序的类重写。

这些函数及其调用顺序是

c 复制代码
init()
attach()
probe()
detach()
free() /* if probe fails */

这些功能构成了驱动程序的第一部分生命周期(有关详细信息,请参阅 "基类" 一章中的"驱动程序对象生命周期")。

请注意,其中四个函数形成互补对,一个嵌套在另一个中:initfree是一对,attachdetach是另一对。

期间主动匹配,候选驱动程序的代码被加载,并且创建个性中列出的主要类。

调用的第一个函数是init,它相当于libkern类的构造函数。

对于 I/O Kit 驱动程序,此函数将包含所选Driver personalities的匹配属性的 OSDictionary 对象作为其唯一参数。

驱动程序可以使用它来识别它已加载的特定个性,确定要生成什么级别的诊断输出,或以其他方式建立基本操作参数。

I/O Kit 驱动程序通常不会覆盖init函数,而是在稍后阶段执行其初始化。

但是,如果您确实要重写init函数(或驱动程序生命周期的几乎任何其他函数),则必须注意做两件事。

第一是调用超类的函数实现。

何时执行此操作取决于函数;例如,在实现中,您应该首先调用超类的init实现,而在free中,您应该将其作为函数的最后一条语句来调用。

第二条一般规则是,您应该在对中的第二个函数中撤消在第一个函数中所做的操作;因此,如果您出于任何原因在init中分配内存,则应该在free中释放该内存。

接下来,调用attach函数(用detach函数括起来)。
attach 的默认实现 通过 I/O 注册表中的注册 将驱动程序附加到 nub;默认实现将detach驱动程序与 nub 分离。

驱动程序可以覆盖默认实现,但很少需要这样做。

attach之后,调用 probe 函数。

如果驱动程序的匹配字典与提供者(nub)被动匹配,则I/O工具包总是调用驱动程序的probe函数。

A.

驱动程序可能会选择不实现probe,在这种情况下,会调用 IOService 的默认实现,只会返回此信息。

probe函数将驱动程序的提供程序和指向探测分数的指针作为参数。

探测分数是一个有符号的 32 位整数,初始化为Driver personalities中指定的值(如果未明确初始化,则初始化为零)。

初始探测分数最高的驱动程序将首先开始操作设备。
probe函数的目的是 为驱动程序提供检查硬件和修改其个性中指定的默认探测分数的机会。

驱动程序可以检查设备特定的寄存器或尝试某些操作,根据其与所检查设备的匹配程度调高或调低其探测分数。

无论发现什么,每个驱动程序都必须使硬件保持与调用时相同的状态,probe以便下一个驱动程序可以探测硬件的原始状态。

如果探测成功,驱动程序在其probe函数中会返回驱动程序对象 (IOService *),否则返回零。

返回的对象通常是驱动程序本身,但驱动程序可以返回更适合提供程序的另一个驱动程序。

探测分数是一个输入输出参数,probe可以根据它发现的 有关设备的信息 进行修改。


驱动程序加载

所有驱动程序探测完设备后,将 attach 探测分数最高的一个,并调用其必须由所有驱动程序实现的启动函数。
start 函数初始化设备硬件并准备运行。

如果驱动程序成功启动,它将返回true;其余候选驱动程序实例将被丢弃,成功启动的驱动程序将继续运行。

如果驱动程序无法初始化硬件,它必须让硬件在 start 调用后保持状态,并返回false

然后,失败的驱动程序将被分离并丢弃,并且下一个探测分数最高的候选驱动程序将有机会启动。

发生这种情况后的一段时间,所有已加载但当前未使用的驱动程序都将被卸载。


3、设备匹配

需要访问设备的用户应用程序必须首先搜索该设备,然后获取适当的设备接口来与其通信。

此过程称为设备匹配。

与驱动程序匹配不同,设备匹配搜索已加载的驱动程序的 I/O 注册表。

要执行设备匹配,请遵循以下基本步骤:

  1. 通过获取 Mach 端口与 I/O Kit 建立连接。
  2. 定义一个字典,指定要在 I/O Registry 中搜索的设备类型。
    通过在字典中设置其他值可以细化搜索。
    例如,可以缩小对 IOMedia 对象的搜索范围以查找所有可弹出的媒体。
    您可以通过参考特定于系列的文档或查看 I/O Registry Explorer 应用程序输出中显示的信息属性列表,在设备头文件(例如IOSCSIDevice.hIOATADevice.h)中找到要匹配的值。
  3. 获取注册表中与您的字典匹配的所有对象的列表并选择适当的设备。
  4. 通过获取设备接口来访问所选设备。
    此步骤在 从内核外部控制设备中有更详细的说明

有关设备匹配的完整描述,请参阅文档"从应用程序访问硬件 " 。


六、基类

I/O Kit 是一个面向对象的框架,主要由数十个(如果不是数百个的话)C++ 类组成。

这些类可以根据其在类层次结构中的继承关系进行组织。

与所有类层次结构一样,I/O Kit 可以描绘为一棵倒置的树,无子节点(没有任何子类的类)是树的叶子。

进一步类比,树干上的类,尤其是树根上的类是层次结构中大多数类继承的类。

这些是基类。


图 5-1 显示了 I/O Kit 类层次的总体轮廓以及其中基类的位置。

等级制度。


图 5-1 I/O Kit 类层次结构的基类


如图所示,I/O Kit 特有的基类有 IOService 和 IORegistryEntry;也包括作为基类(通过继承)的是 libkern library OSObject 和(特殊意义上)OSMetaClass。

鉴于这些类的重要性,理解它们的重要性显而易见。

它们不仅提供 I/O Kit 的所有其他类所继承的行为和数据结构。

它们还定义内核和驱动程序对象的行为结构 :如何创建和处理对象、如何捕获和显示元类信息、驱动程序对象在动态运行时环境中的行为方式以及如何动态建立驱动程序对象之间的客户机/提供者关系。

如果您使用 I/O Kit 编写设备驱动程序,那么您将不得不在早期并在此后频繁处理代码中的基类,因此熟悉它们是个好主意。

本章还概述了一些通常有用的函数和数据类型。

尽管这些函数和类型不适合讨论基类(因为它们不属于任何类),但它们在各种情况下的实用性使它们几乎与任何基类一样重要。


1、libkern 基类

I/O Kit 建立在libkern C++ 库,它是用适合用于可加载的 C++内核模块。

具体来说,libkern C++ 环境不支持多重继承,模板,C++异常处理功能和运行时类型信息 (RTTI)。

省略了 C++ RTTI 功能,因为它不支持按名称动态分配类,这是加载内核扩展所需的功能。

RTTI 还大量使用异常。

但是,libkern C++ 环境定义了自己的运行时类型系统,它确实支持动态加载。

出于成本和稳定性的原因,内核禁止使用异常。

异常会增加代码的大小,从而消耗宝贵的内核内存,并带来不可预测的延迟。

此外,由于 I/O Kit 代码可能被许多客户端线程调用,因此无法保证异常会被捕获。

在任何内核扩展中使用trythrowcatch 均不受支持,并且会导致编译错误。

虽然您不能在 I/O Kit 驱动程序中使用异常,但您的驱动程序应始终在适当的情况下检查返回代码。

Apple 强烈建议您将所有内核 C++ 代码(包括设备驱动程序的代码)基于 libkern 基类 OSObject 和 OSMetaClass,并遵守这些类规定的约定(请参阅 类型转换、对象自省和类信息)。

完全私有于驱动程序的类不需要基于 OSObject,也不需要遵循这些约定。

但是,此类类与 libkern 类的交互将受到限制。

例如,所有 libkern 集合类都存储从 OSObject 继承的对象。

未从 OSObject 继承的自定义类不能存储在 libkern 集合中,例如 OSDictionary 或 OSArray 对象。


重要提示: 目前,加载器不允许使用任何需要限定的 OSObject 子类,例如嵌套类或在命名空间内声明的类(有关命名空间声明的示例,请参阅 语言选择)。

例如,I/O Kit 驱动程序中的以下嵌套类声明将阻止驱动程序加载:

c 复制代码
class com.mycompany.driver.myClass {
    class myNestedClass : public IOService {}; // This is not allowed.
};

对象创建和处置(OSObject)

OSObject 是扩展 I/O Kit 层次结构的根。

它不继承自任何(公共)超类,所有其他 libkern 和 I/O Kit 类(OSMetaClass 除外)都继承自它。

OSObject 实现了支持可加载内核模块所需的动态类型和分配功能。

其虚拟函数和重写运算符定义了如何在内核中创建、保留和处置对象。

OSObject 是一个抽象基类,因此它本身不能被实例化或复制。


对象构造

标准 C++ 构造函数不能在 libkern 中使用,因为这些构造函数使用异常来报告失败;您可能还记得,libkern 选择的受限 C++ 形式排除了异常。

因此 OSObject 类(以及 OSMetaClass 类)的主要目的是重新实现对象构造。

为了构造对象,OSObject 定义了init函数并重写new运算符。
new运算符为对象分配内存并将对象的引用计数设置为 1。

使用new运算符后,客户端必须在新对象上调用init函数来执行使其成为可用对象所需的所有初始化。

如果调用init失败,则客户端必须立即释放该对象。

为了支持 OSObject 的 initnew,OSMetaClass 类实现了与对象构造相关的宏。

这些宏 将类 绑定到内核的运行时类型工具中,并自动定义充当类的构造函数和析构函数的函数。

有关这些宏和 OSMetaClass 的 RTTI 实现的更多信息,请参阅 运行时类型信息 (OSMetaClass)

OSObject 的子类没有明确实现它们的构造函数,并且析构函数,因为它们本质上是通过 OSMetaClass 宏创建的。

此外,你通常既不调用构造函数和析构函数,也不调用 C++ 的newdelete运算符。

这些函数和运算符保留给动态类型和分配工具使用,它们隐式地为类定义它们。

OSObject 代替它们定义了创建和初始化对象的约定。

但是,子类通常会覆盖init函数以执行特定于该类的初始化。

大多数 libkern 和 I/O Kit 类都定义了一个或多个静态函数来创建实例。

命名约定因类而异,但名称通常是类本身的基本名称(首字母小写),或者是with...名称描述初始化参数的某种形式。

例如,OSArray 定义静态创建函数withCapacitywithObjectswithArray;IOTimerEventSource 定义timerEventSource;IOMemoryCursor 定义withSpecification

如果类没有静态创建函数,则必须使用并调用代替 C++ 构造函数的new初始化方法,如例 5-2所示

有关需要指定类的构造函数和析构函数的样板代码的概述,请参阅 类型转换、对象自省和类信息


对象保留和处置

OSObject 定义一个引用计数和自动释放机制来支持内核扩展的安全卸载。

对于此机制,它使用三个虚拟成员函数retainreleasefree ------ 并覆盖delete运算符。

其中,您在代码中应该调用的函数只有retainrelease,并且您应该遵循某些约定来规定何时调用它们。

新创建的对象和复制的对象具有 1 的引用计数。

如果您已创建或复制了 libkern 对象并且不需要将其保留在当前上下文之外,则应调用release

这会减少对象的引用计数。

如果该计数为零,则释放该对象;具体而言,release方法将调用名为 free 的替代析构函数,并最终调用 delete 运算符。

如果您不拥有某个对象(即您没有创建或复制它),并且您想将其保留在当前上下文之外,请调用retain 以增加其引用计数。

如果您没有 创建、复制 或 对某个对象 调用retain,则永远不应调用release

此外,一些返回对象的函数会将所有权传递给调用者,这意味着调用者必须在使用完对象后释放该对象,而其他函数则不需要。

请参阅给定函数的参考文档,以了解您的代码是否需要保留或释放其接收的对象。

切勿显式调用delete运算符来释放对象。

此外,切勿直接调用free运算符来释放对象;但是,您可以(并且在大多数情况下应该)覆盖free函数来释放init函数中分配的内存。


运行时类型信息(OSMetaClass)

尽管 libkern 的 C++ 限制形式排除了本机运行时类型信息(RTTI)设施,OSMetaClass 实现了另一种方法运行时类型化工具确实支持按名称动态分配类。

OSMetaClass 不是真正意义上的基类;没有公共 libkern 或 I/O Kit 类从它继承。

但是,OSMetaClass 提供了对象构造和销毁所必需的 API 和功能。

OSMetaClass 本身是一个抽象类,不能直接构造。


OSMetaClass 为所有基于 libkern 的代码提供的功能包括:

  • 动态跟踪类层次结构的机制
  • 安全装卸内核模块
    运行时类型化工具使系统能够跟踪每个 libkern(和 I/O Kit)类当前存在多少个实例,并将每个实例分配给一个内核模块(KMOD)。
  • 类实例的自动构造和解构
  • 用于动态类型转换、类型发现、成员资格评估和类似内省行为的宏和函数
  • 根据类类型的某些指示(包括 C 字符串名称)动态分配 libkern 类实例

在 libkern 的运行时类型工具中,会为加载到内核中的内核模块 (KMOD) 中的每个类创建一个静态元类实例(OSMetaClass 的派生类)。

该实例封装了有关类的名称、大小、超类、内核模块以及该类的当前实例数的信息。

加载内核模块的过程分为两个阶段,第一阶段由成员preModLoad函数发起,第二阶段由postModLoad函数发起。

preModLoad阶段,OSMetaClass 在单个锁保护线程的上下文中静态构建模块中每个类的元类实例。

postModLoad阶段,OSMetaClass 将构建的元类对象的继承层次结构链接在一起,将元类实例插入到类的全局寄存器中,并记录每个实例从中派生的内核模块。

有关 postModLoad 和相关函数的更多信息,请参阅 OSMetaClass 参考preModLoad文档。

创建的元类信息存储构成了上面列出的 OSMetaClass 功能的基础。

以下部分将详细探讨这些功能中更重要的部分。


对象构造和动态分配

OSMetaClass 的一个特性是它能够根据类类型的指示分配 libkern 对象。

OSMetaClass 的子类可以通过实现以下代码动态地实现这一点:alloc函数;类类型由 OSMetaClass 子类本身提供。

你也可以通过调用以下函数之一来分配任何 libkern 类的实例allocClassWithName函数,提供适当的类类型标识(OSSymbol、OSString 或 C 字符串)。

新分配的对象作为其唯一的实例变量具有保留计数 1,并且未初始化。

分配后,客户端应立即调用对象的初始化函数(initinit变体)。

OSMetaClass 定义了许多运行时类型声明 基于 alloc 函数的宏 和 对象构造宏。

根据类的类型(虚拟或其他),必须将其中一个宏作为类声明和实现中的第一个语句插入:

  • OSDeclareDefaultStructors
    声明类的数据和接口,它们需要运行时类型信息。
    按照惯例,此宏应紧跟在类声明中的左括号之后。
  • OSDeclareAbstractStructors
    声明虚拟类的数据和接口,这些数据和接口是运行时类型信息所必需的。
    按照惯例,此宏应紧跟类声明中的左括号。
    当类具有一个或多个纯虚拟方法时,请使用此宏。
  • OSDefineMetaClassAndStructors
    定义 OSMetaClass 子类以及 OSObject 非抽象子类的主要构造函数和析构函数。
    此宏应出现在实现文件的顶部,紧接在特定类的第一个函数实现之前。
  • OSDefineMetaClassAndAbstractStructors
    定义 OSMetaClass 子类以及 OSObject 子类(抽象类)的主要构造函数和析构函数。
    此宏应出现在实现文件的顶部,紧接在特定类的第一个函数实现之前。
  • OSDefineMetaClassAndStructorsWithInit
    定义 OSMetaClass 子类以及 OSObject 非抽象子类的主要构造函数和析构函数。
    此宏应出现在实现文件的顶部,就在为特定类实现第一个函数之前。
    在加载时构造 OSMetaClass 实例后,将调用指定的初始化例程。
  • OSDefineMetaClassAndAbstractStructorsWithInit
    定义 OSMetaClass 子类以及 OSObject 子类(抽象类)的主要构造函数和析构函数。
    此宏应出现在实现文件的顶部,紧接在为特定类实现第一个函数之前。
    在加载时构造 OSMetaClass 实例后,将调用指定的初始化例程。

有关使用这些宏的更多信息(包括使用示例),请参阅 类型转换、对象自省和类信息。


类型转换、对象自检和类信息

OSMetaClass 定义了许多宏和函数,几乎可以在任何情况下使用。

它们可以帮助您安全地从一种类型转换为另一种类型,发现任意对象的类,确定对象是否从给定的超类继承,找出给定类的实例数,并产生其他有用的信息。
表 5-1总结了这些宏和函数。

函数或宏 描述
OSTypeID 该宏根据类的名称返回类的类型 ID。
OSTypeIDInst 此宏返回给定实例所构建的类的类型 ID。
OSCheckTypeInst 该宏检查一个实例是否与另一个实例属于同一类类型。
OSDynamicCast 此宏动态地将实例的类类型转换为合适的类。 它基本上相当于 RTTI 的dynamic_cast
isEqualTo 此函数验证调用的 OSMetaClass 实例(代表一个类)是否与另一个 OSMetaClass 实例相同。 默认实现执行浅指针比较。
metaCast(多种的) 这组函数确定 OSMetaClass 实例(代表一个类)是否是给定类类型,或者是否从给定类类型继承。 类型可以指定为 OSMetaClass、OSSymbol、OSString 或 C 字符串。
modHasInstance 返回内核模块是否有任何未完成的实例。 通常调用此函数来确定模块是否可以卸载。
getInstanceCount 此函数返回接收器所代表的类的实例的数量。
getSuperClass 此函数返回接收器的超类。
getClassName 此函数返回接收器的名称(作为 C 字符串)。
getClassSize 此函数返回接收器所表示的类的大小(以字节为单位)。

在 libkern 中定义 C++ 类

当实现基于 OSObject 的 C++ 类时,你调用一对基于 OSMetaClass 类的宏。

这些宏将您的类绑定到 libkern通过定义元类和定义构造函数和通过元类执行 RTTI 簿记任务的类的析构函数。

第一个宏 OSDeclareDefaultStructors,声明 C++ 构造函数;按照惯例,将此宏作为类声明的第一个元素插入到头文件中。

例如:

c 复制代码
class MyDriver : public IOEthernetController
{
    OSDeclareDefaultStructors(MyDriver);
    /* ... */
};

然后你的类实现使用伴随宏,OSDefineMetaClassAndStructors,定义构造函数和析构函数,以及提供运行时类型信息的元类。
OSDefineMetaClassAndStructors 以驱动程序的名称及其超类的名称作为参数。

它使用这些来生成代码,允许在内核运行时加载和实例化驱动程序类。

例如,MyDriver.cpp可能以这样的方式开始:

c 复制代码
#include "MyDriver.h"
 
// This convention makes it easy to invoke superclass methods.
#define super    IOEthernetController
 
// You cannot use the "super" macro here, however, with the
//  OSDefineMetaClassAndStructors macro.
OSDefineMetaClassAndStructors(MyDriver, IOEthernetController);

super宏 的定义允许方便地访问超类方法,而不必每次都输入超类的完整名称。

这是 libkern 和 I/O Kit 类实现的常见习惯用法。

你的类实现了一个初始化方法和free方法。

对于非 I/O Kit 类,初始化方法接受所需的任何参数,可以使用任何名称(尽管通常以 开头init),并返回 C++bool值。
free方法始终不接受任何参数 并返回void

驱动程序类的初始化方法 应在执行任何其他操作之前 调用适当的超类初始化方法,如例 5-1所示。

如果超类返回false,则类的初始化方法应中止,释放所有分配的资源并返回false

否则,您的类可以执行其初始化并返回true

当 libkern C++ 运行时系统创建类的实例时,它会将所有成员变量填充为零,因此您无需明确将任何内容初始化为零、false 或 null 值。


例 5-1 实现 init 方法

c 复制代码
bool MyDriver::init(IOPhysicalAddress * paddr)
{
    if (!super::init()) {
        // Perform any required clean-up, then return.
        return false;
    }
    physAddress = paddr;  // Set an instance variable.
    return true;
} 

要使用初始化方法创建实例,请编写如下代码:


例 5-2 创建实例并调用其 init 方法

c 复制代码
MyDriver * pDrv = new MyDriver; // This invokes the predefined constructor
                                //  of MyDriver itself
 
if (!pDrv) {
    // Deal with error.
}
 
if (!pDrv->init(memAddress)) {
    // Deal with error.
    pDrv->release();    // Dispose of the driver object.
}

因为这使得创建实例更加麻烦,所以您可能需要以许多内核 C++ 类的方式编写一种便捷方法,例如:

c 复制代码
MyDriver * MyDriver::withAddress(IOPhysicalAddress *paddr)
{
    MyDriver * pDrv = new MyDriver;
 
 
    if (pDrv && !pDrv->init(paddr)) {
        pDrv->release();
        return 0;
    }
    return pDrv;
}

使用这种便捷方法,您可以使用如下代码创建驱动程序的实例:

c 复制代码
MyDriver * pDrv = MyDriver::withAddress(paddr);
 
if (!pDrv) {
    // Deal with error of not being able to create driver object.
}
else {
    // Go on after successful creation of driver object.
}

类的free方法应该释放实例所持有的所有资源,然后调用超类的free方法,如例 5-3所示:


例 5-3 实现 free 函数

c 复制代码
void MyDriver::free(void)
{
    deviceRegisterMap->release();
    super::free();
    return;
}

再次提醒,您的代码绝不应该直接 对基于 OSObject 类的对象 调用deletefree或操作符。

始终调用 release 来处理此类对象。


2、I/O Kit 基类

所有基于 I/O Kit 的驱动对象都继承自两个基类IORegistryEntry 和IOService。

这些类中的第二个类 IOService 直接继承自 IORegistryEntry,而所有驱动程序对象最终都继承自 IOService。

IORegistryEntry 类将驱动程序对象定义为 I/O Registry 中的节点,而 IOService 则定义驱动程序对象的生命周期以及实现驱动程序常见的其他行为。

IORegistryEntry 和 IOService 之间紧密的继承关系可能会引发人们的猜测,为什么这些类没有设计成一个类。

原因在于性能。

将 IORegistryEntry 作为 IOService 的超类是一种优化,因为就内存占用而言,IORegistryEntry 对象要轻量得多。


动态驱动程序注册(IORegistryEntry)

IORegistryEntry 对象定义 I/O 注册表中的一个节点(或条目)。

正如I/O 注册表 一章所详细解释的那样,I/O 注册表是一个动态数据库,它捕获"实时"驱动程序对象的当前图表,跟踪这些对象之间的客户端/提供商关系,并记录描述其个性的属性。

I/O 注册表在 OS X 的动态功能中起着至关重要的作用;当用户添加或移除硬件时,系统会在驱动程序匹配过程中使用注册表,并立即更新它以反映设备的新配置。

每个 IORegistryEntry 对象都有两个与之关联的字典(即 OSDictionary 的实例)。

一个是该对象的属性表,该对象通常也是一个驱动程序对象。

该属性表是匹配字典指定驱动程序的个性之一。

(有关个性的信息,请参阅 Driver personalities和匹配语言。)IORegistryEntry 对象的另一个字典是平面字典,它指定对象如何连接到注册表中的其他对象。

除了反映驱动程序对象之间的所有客户端/提供者关系外,I/O 注册表还标识这些关系的子集。

注册表树的整体及其子集都称为平面

每个平面通过仅显示存在于该关系中的连接来表达 I/O 注册表中对象之间的不同提供者/客户端关系。

平面关系通常是依赖链之一。

最通用的平面是服务平面,它显示注册表条目的整个层次结构。

注册表中的每个对象都是其父级提供的服务的客户端,因此每个对象与注册表树中其祖先的连接在服务平面上都是可见的。

除了服务平面之外,还有电源、音频、设备、FireWire 和 USB 平面。

有关平面的更多信息,请参阅 I/O 注册表架构和构造

有可能有一个 IORegistryEntry 对象,但它不是 IOService 对象。

这样的对象可以纯粹用于保存与注册表中的该节点相关的信息。

但是,实际上很少需要这样的对象。

这IORegistryEntry 类包含许多驱动程序对象可能有用的成员函数;这些函数分为几类:

  • 属性表函数允许您设置、获取和删除 IORegistryEntry 对象属性表的属性以及序列化属性表。
    一些getProperty函数通过注册表对给定键的属性执行同步、递归搜索。
  • 位置函数允许 IORegistryEntry 对象操纵其在注册表树中的位置。
    它可以定位、识别并附加到或分离另一个 IORegistryEntry 对象。
  • 迭代函数使您的代码能够遍历整个注册表树或其一部分,并可选择在遇到的 IORegistryEntry 对象上调用"应用程序"回调函数。

有关详细信息,请参阅 IORegistryEntry 的参考文档。


基本驱动行为 (IOService)

I/O Kit 中的每个驱动程序对象都是一个类的实例,该类最终继承自IOService 类。

IOService 最重要的是通过互补的虚拟函数对来定义驱动程序的动态运行时环境中的生命周期。

它管理匹配和探测过程,实现默认匹配行为,并注册驱动程序和其他服务。

但 IOService 类还提供了许多其他用途的丰富功能,包括:

  • 访问驱动程序的提供程序、客户端、状态和工作循环
  • 发帖通知并向其他驱动程序对象或服务发送消息
  • 管理设备电源
  • 实现用户客户端(设备接口)
  • 访问设备内存
  • 注册并控制中断处理程序

本节首先介绍驱动对象的生命周期以及 IOService 在该生命周期中的作用。

然后总结其他主要的 IOService API。


驱动程序对象生命周期

I/O Kit 驱动程序可以随时加载和卸载,或者激活和停用。

每个驱动程序的生命周期都遵循相同的模式,如标准驱动程序超类 IOService 定义的一组函数中所述。

其中一些函数必须由驱动程序实现;其他函数由 IOService 实现,但可以由驱动程序覆盖以获得额外的灵活性。


图 5-2 驱动对象生命周期函数


图 5-2 显示了驱动程序对象生命周期中调用的函数序列。

括号中的线显示了这些函数如何分组为互补对。

驱动程序对象类可以重写这些函数中的任何一个,但必须确保在其自身实现的适当位置调用超类对该相同函数的实现。

例如,当您重写互补对的打开函数(例如init或 )时start,您的版本必须在执行自己的初始化之前调用其超类的相应函数,如例 5-1所示。

当您重写关闭函数(例如free或 )时stop,您应该在调用超类中的相应函数之前执行自己的清理工作,如例 5-3所示


驱动程序匹配和加载

第一组函数 initattachprobedetach------在驱动程序匹配过程中被调用,并且加载。

此过程发生在启动时以及添加或删除设备时。

以下段落总结了匹配过程,特别关注所涉及的功能;有关该过程的详细讨论,请参阅 驱动程序匹配和加载。

当服务提供程序检测到设备时,匹配过程就会启动。

通常,此提供程序是总线(如 PCI 总线)的控制器驱动程序,它通过扫描总线来检测设备。

然后,提供程序(通常通过其系列)创建并通过调用 IOService 注册任何所需的 nubs函数registerService;这个调用反过来会触发匹配过程。

在 IOService 的安排下,I/O Kit 使用减法过程,分三个不同阶段为 nub 查找并加载驱动程序。

在每个阶段,从可能候选者的总池中减去那些被认为不可能匹配的驱动程序,直到 找到成功的候选者。

这些阶段包括:

  1. 类匹配 ------ I/O Kit 会消除任何错误提供程序 (nub) 类的驱动程序。
  2. 被动匹配 ------ I/O Kit 检查剩余 driver 的性格,了解其 families 特定属性。
  3. 主动匹配 ------ IOService 会根据驱动程序所匹配的对象调用其余每个驱动程序的probe函数。
    该函数允许驱动程序与设备通信并验证它是否确实可以驱动该设备。
    返回的探测分数反映了驱动程序驱动设备的合适程度。

当找到匹配的驱动程序时,就会加载其代码并创建个性中列出的主要类的实例。

在前两个阶段,驱动程序的生命周期函数都不会被调用。

只有在第三阶段的主动匹配中,驱动程序才会被要求探测设备是否适用,调用第一组功能。

无论驱动程序是被加载来驱动设备还是仅仅被要求探测设备,调用的第一个生命周期函数是init,它是 libkern 中类的构造函数的等价物。

对于 I/O Kit 驱动程序,此函数将 OSDictionary 作为其唯一参数,其中包含与 XML 文件中的个性相匹配的属性。

驱动程序可以使用它来确定它被加载的特定个性,确定要生成的诊断输出级别,或以其他方式建立基本操作参数。

但是,I/O Kit 驱动程序通常不会覆盖init函数,而是在稍后的阶段执行初始化,如下所述。

有关initfree函数的更多信息,请参阅 对象创建和处置 (OSObject)

在驱动程序对象可以探测或启动之前,必须将其附加到 I/O 注册表中。

为此,nub 调用驱动程序的attach函数,该函数通过 I/O 注册表将驱动程序附加到 nub。

互补函数detach将驱动程序从其 nub 中删除。

IOService 为这两个函数提供了默认实现。

驱动程序可以覆盖它们,但很少需要这样做。

如果正在发生主动匹配,那么 nub 接下来会调用驱动程序对象的探测函数。

探测函数返回一个 IOService。

这通常是驱动程序对象本身,但驱动程序可以返回不同类的实例,例如驱动程序的内核扩展包中包含的专门子类。

IOService 的探测默认实现只是返回 this 指针,而不会改变探测分数。

覆盖探测是可选的;大多数驱动程序从属性匹配中获取了足够的信息,不需要覆盖它。

但是,如果您确实覆盖了探测,则必须确保探测不是破坏性的,而让设备处于发现它的状态。

硬件规范通常定义如何进行非破坏性探测。

driver 的start函数与 probe 的实现一样,应该只执行最低限度的系统资源分配,以验证它是否可以操作硬件。

这种保守的方法会延迟内核资源的消耗,直到真正需要它们为止。

每个系列(例如 PCI、USB 或存储)都定义了一对激活和停用函数,以指示驱动程序应准备为 I/O 请求提供服务,并且不再需要驱动程序的服务。

这两个函数通常命名为openclose

大多数驱动程序都实现这些函数来分配和释放所有必要的缓冲区和其他结构,为 I/O 处理做准备。

一些系列定义了额外的激活和停用级别。

例如,网络驱动程序在和中执行 openclose 的操作很少,而是在enabledisable函数中执行设置和拆卸。

无论具体的激活和停用函数是什么,它们都可以在驱动程序的生命周期内多次调用;无论被激活或停用多少次,驱动程序都应该能够正常工作。


驾驶员身份变更

在驱动程序的生命周期中可以多次调用的另一个函数是message函数。

此函数通知驱动程序重要的系统状态变化,例如当磁盘被强制移除时、当电源管理发生变化(睡眠、唤醒)时或当驱动程序被关闭时。

IOService 对此函数的实现不执行任何操作并返回"不支持"结果代码。

有关 IOService 提供的通知和消息传递功能的更多信息,请参阅 通知和消息传递


驱动程序关闭

当驱动程序即将永久关闭时,message函数将以终止方式调用消息(kIOMessageServiceIsTerminated)。

如果驱动程序接受终止,则调用其stop函数。

驱动程序应实现 stop函数 来关闭、释放 或 释放它在其start函数中 打开或创建的任何资源,并使硬件保持驱动程序最初发现它的状态。

假设驱动程序实现了激活和停用功能,stop函数中通常没有什么可做的。

驱动程序关闭的最后阶段是调用free,当驱动程序对象的引用计数达到零时,就会发生这种情况。

在此函数中,驱动程序可以处置其在 init函数中创建的任何资源。


供应商匹配

如果你正在实施提供者驱动对象(即 I/O Kit 家族成员的子类),你可能想要重写 IOService 的matchPropertyTable 成员函数。

当 IOService 为驱动程序对象执行匹配时,它会调用此方法,以便提供程序类除了 IOService 提供的通用匹配条件之外,还可以实现自己的特定匹配条件。

提供程序应检查传递的匹配字典,以查看它是否包含该系列理解的匹配属性,如果确实理解,则使用它们与特定驱动程序对象进行匹配。


通知和消息

IOService 为驱动对象相互之间以及与 I/O Kit 之间的通信提供了两种机制:通知和消息传递。

当活动服务或驱动程序发生特定事件且其属性与给定字典匹配时,通知将发送给感兴趣的客户端。

消息更具针对性,并且从提供商到客户端单向流动。

任何提供商都可以向其任何客户端发送消息,以通知其运行时环境中的某些变化。

正如前面在 驱动程序对象生命周期 中讨论的那样,驱动程序客户端实现了message函数接收并响应来自其提供商的消息。

此功能允许它们适应运行时环境的变化。

这些消息可以通知它们系统状态的变化,例如电源状态的变化、服务暂停或即将终止的服务。

提供商实现messageClient(或messageClients)函数通过调用其客户端的方法来发送消息message

I/O Kit 定义了一些消息,而其他消息可能由系列定义。

请参阅头文件 Kernel.framework/Headers/IOKit/IOMessage.h,以了解 messageClientmessageClients函数可以传递给驱动程序的通用消息。

通知的广播稍微复杂一些。

任何驱动程序对象都可以通过addNotificationinstallNotification函数安装通知处理程序。

通知处理程序设置为在特定驱动程序对象(由匹配属性的字典标识)经历特定类型的状态变化时调用,例如当驱动程序首次发布、随时匹配或终止时。

每个通知处理程序还会被赋予一个优先级编号,以防同时触发同一类型和同一对象的多个通知。

这如果任何驱动程序对象(其个性与提供的匹配字典相匹配)更改为指定状态,则将调用通知处理程序(IOServiceNotificationHandler类型)。

例如,当服务提供者 调用registerServices时,它不仅会启动注册过程,还会向所有对该提供者的发布感兴趣的已注册客户端发送通知。

通知请求由 IONotifier 对象的实例标识,通过该实例可以启用、禁用或删除通知。


驱动程序访问器

为了方便起见,IOService 包括许多访问器成员函数可快速访问驱动程序对象的状态以及与其密切相关的对象。

这些函数返回以下对象或值:

  • 驱动程序的状态(getState),一个位字段,指示驱动程序是否处于非活动状态、已注册、已匹配等状态
  • 驱动程序正在使用的工作循环(getWorkLoop)(有关更多信息,请参阅 处理事件
  • driver 的主要提供者(getProvider),以及OSIterator 对象用于迭代驱动程序的提供程序(如果有多个)(例如,RAID 设备)
  • 驱动程序的主客户端(getClient),以及用于迭代驱动程序客户端的 OSIterator 对象(如果有多个)

其他 IOService 功能

IOService 集成了对许多类别的设备驱动程序有用的功能(除上述概述之外)。

最值得注意的是,此功能包括以下特性:

  • 用户客户端
    newUserClient函数创建一个基于 IOUserClient 的连接,用于与非内核客户端进行通信;客户端通过调用I/O Kit 框架的 IOServiceOpen 功能。
  • 设备内存
    多个 IOService 成员函数可获取、映射和设置分配给内存映射设备的物理内存范围。
    这些函数适用于作为 PCI 设备客户端的驱动程序对象。
  • 中断处理
    IOService 提供用于注册、取消注册、操作和访问中断处理程序的低级函数,这些函数在设备中断的主中断时调用。
    这些函数提供了一种安装中断处理程序的机制,该机制不基于工作循环。

七、I/O Kit 系列

在 I/O Kit 中,系列是定义和实现特定类型所有设备所共有的抽象的类的集合。

它们为开发这些系列的成员(提供者)或客户端驱动程序提供编程接口和通用支持代码。

本章描述了一些与 I/O Kit 系列相关的概念:

  • driver 与 families 的关系
  • 系列作为库,包括库的版本控制和加载
  • 系列的程序结构和命名约定

此外,本章还为那些想要编写自己的 I/O Kit 系列的人提供了一些提示。

有关 Apple 当前提供的 I/O Kit 系列的参考,请参阅附录I/O Kit 系列参考


1、 Drivers 和 Families

I/O Kit 系列是一个实现某些总线协议(例如 PCI 或 USB)或某些通用服务集的库。

但系列提供的支持是通用的。

系列不包含获取硬件的任何细节,因为它无法对其代表的通用层下的特定硬件做出假设。

驱动程序编写者负责编写代码,以在具体和抽象之间架起桥梁------即在硬件和系列定义的抽象之间架起桥梁。

驱动程序必须扩展系列以支持特定硬件或获取特定功能。

以 SCSI 并行系列为例。

SCSI 并行系列封装了定义明确的 SCSI 并行接口 5 规范。

该规范描述的一件事是如何扫描总线和检测设备。

由于这是一个昂贵的操作,许多 SCSI 并行控制器都包含可以缓存有关检测到的设备的信息的固件。

要利用这种缓存优化,您可以设计控制器驱动程序(SCSI 并行系列的成员),以便它覆盖扫描功能以与固件交互。

系列通常执行某些通用任务,例如扫描总线、查询客户端、排队和验证命令、从错误中恢复以及匹配和加载驱动程序。

驱动程序执行以某种方式影响硬件的任务。

继续使用 SCSI 并行系列的示例,作为 SCSI 并行系列的成员,SCSI 并行控制器驱动程序的主要工作是接收来自其系列的 SCSI 命令,在硬件上执行每个命令,并在命令完成时发送通知。

一些 I/O Kit 系列由它们封装的规范明确界定。

其他系列(例如 Audio 系列)则不那么容易定义,因为没有单一规范规定该系列应包含哪些内容。

在这种情况下,Apple 会仔细选择要纳入系列的抽象集,以使其足够灵活和全面。

所有系列都必须宣传其功能,并且由驱动程序堆栈的更高级别来管理这些功能。

驱动程序在与 I/O Kit 系列的关系中既是提供者又是客户端。

作为系列提供者的驱动程序(通过其核心)也是该系列的成员;它应该从系列中描述其导出服务(无论如何增强)的特定类继承。

另一方面,驱动程序是其导入服务(通过系列的核心)的系列的客户端。

例如,SCSI 磁盘驱动程序将从存储系列继承,而不是从 SCSI 并行系列继承,它是后者的客户端(参见 [图 6-1] )。

USB 鼠标驱动程序将从 HID(人机接口设备)系列继承,并成为 USB 系列的客户端。

PCI 音频卡驱动程序将从音频系列继承,并成为 PCI 系列的客户端。


图 6-1 驱动程序与 I/O Kit 家族的关系


2、 families 就是 library

families 被实现为库打包为内核扩展 (KEXT)。

它们在信息属性列表中指定其定义属性,并安装在 /System/Library/Extensions 中。

从机械上讲,系列与普通驱动程序略有不同。

两个相关特征将系列与驱动程序区分开来。

首先,驱动程序使用 OSBundleLibraries 属性来表达对系列的依赖关系;其次,系列仅作为驱动程序将其列为库的副产品进行加载。

驱动程序将其所依赖的库指定为 OSBundleLibraries字典的元素。

I/O Kit 保证在加载驱动程序并将其与其系列链接之前将这些库加载到内核中。

请注意,库本身使用 OSBundleLibraries属性声明它们所依赖的库(内核扩展和内核本身)。

您可以在 OSBundleLibraries 字典中将库指定为键值对,其中键是库的捆绑标识符 (CFBundleIdentifier),值是驱动程序兼容的库的最早版本。

所有版本都以'vers'资源样式表示。
例 6-1给出了 AppleUSBAudio 驱动程序信息属性列表中的一个示例。


例 6-1 OSBundleLibraries 属性

xml 复制代码
<key>OSBundleLibraries</key>
    <dict>
        <key>com.apple.iokit.IOAudioFamily</key>
        <string>1.0.0</string>
        <key>com.apple.iokit.IOUSBFamily</key>
        <string>1.8</string>
    </dict>

尽管 I/O Kit 在加载指定这些依赖项的驱动程序之前会加载库,并按照正确的依赖项顺序加载库,但无法保证它加载没有相互依赖关系的库的顺序。

通常,开发人员应该声明其设备驱动程序或任何其他内核扩展的依赖项。

(如果 KEXT 没有可执行文件,则无需声明依赖项。)他们需要声明哪些依赖项取决于需要解析哪些符号。

如果您包含某个系列或其他库的头文件,或者如果头文件间接包含库,则应该声明该依赖项。

如果您不确定是否存在依赖项,请声明它。


库版本控制

为了能够加载并链接到内核,系列或其他库必须使用两个属性声明其兼容性信息:CFBundleVersionOSBundleCompatibleVersion
CFBundleVersion属性定义兼容性的前向限制,即当前版本。
OSBundleCompatibleVersion 属性通过标识库的最后一个 CFBundleVersion定义的版本来定义兼容性的向后限制,该版本破坏了与以前版本的二进制兼容性。

每次修订驱动程序或系列时,您都应适当增加 CFBundleVersion 值。

只有当修订使二进制文件与以前的版本不兼容时,您才应重置OSBundleCompatibleVersion值(为当前值CFBundleVersion),例如当您删除函数或其他符号,或更改类以致 vtable 布局发生变化时。

如果您正在编写 I/O Kit 系列,请确保为您的库指定一个 OSBundleCompatibleVersion 属性;否则,驱动程序和其他内核扩展无法声明对它的依赖,因此无法链接到它。

对于驱动程序和系列(以及所有内核扩展),请确保在内核模块,并且此值相当于信息属性列表中的 CFBundleVersion

您可以通过 Xcode 中的设置 来设置可执行文件中的版本MODULE_VERSION,在目标的自定义设置列表中(您可以在目标的构建视图中找到它)。


库加载

这KEXT 管理器充当内核加载器和链接器。

在启动时或每当系统检测到新连接的设备时,I/O Kit 都会启动匹配过程以查找适合设备的驱动程序。

找到这样的驱动程序后,KEXT 管理器的工作就是将驱动程序加载到内核中并将其与所依赖的库链接起来。

但在执行此操作之前,KEXT 管理器必须确保首先加载这些库以及这些库所依赖的所有其他库。

为此,管理器会构建驱动程序所需的所有库和其他内核模块的依赖关系树。

它使用OSBundleLibraries属性的内容构建此树,首先是驱动程序,然后是每个所需库。

在构建依赖关系树之后,KEXT 管理器会检查树中距离驱动程序最远的库是否已加载。

如果这些库中的任何一个尚未加载,管理器将加载它并调用其启动例程(该例程因 KEXT 类型而异)。

然后,它以类似的方式继续依赖关系树 --- 链接、加载和启动 --- 直到所有必需的库都已链接和加载。

有关此过程的说明,请参见图 6-2 。


图 6-2 OSBundleLibraries 和依赖关系树


如果 KEXT 管理器在初始化库时遇到问题,或者找不到具有兼容版本的库(基于 的值OSBundleCompatibleVersion),它会停止并(通常)返回失败代码。

已加载的模块会保持加载状态一段时间。

通常,模块不使用时不会立即卸载。

I/O Kit 包含一项功能,可跟踪空闲时间并在空闲一段时间后卸载模块。

重要提示: 明确加载内核扩展的唯一方法是使用kextload命令行实用程序。


3、 families 的程序结构

尽管 I/O Kit 家族彼此之间差异很大,但它们也有一些共同的结构元素。

首先,IOService 是所有 I/O Kit 家族的共同超类;每个家族中至少有一个重要类(可能还有更多)继承自 IOService(有关详细信息,请参阅 I/O Kit 基类)。

每个家族都有一个或多个类向驱动程序提供接口。


典型类别

一个驱动程序系列通常为驱动程序定义两个类:

  • 描述驱动程序家族客户端的 nub 接口的类
  • 驱动程序的超类,是驱动程序家族的成员,因此也是其核心的提供者

换句话说,I/O Kit 家族通常定义一个向上接口和一个向下接口。

这些接口是 I/O 连接中涉及的驱动程序对象分层所必需的。

向上接口(即 nub 接口)向系统的其余部分呈现家族封装的硬件抽象和规则定义。

向下接口为成员驱动程序提供子类化接口。

这些接口共同定义了家族中的向上调用和成员驱动程序应进行的向下调用。

除了这两个类之外,系列通常还定义许多实用类和支持类。

附录I/O Kit 系列参考介绍了其中一些类。

一些系列为特定种类的客户端或成员驱动程序指定子类。

例如,Storage 系列为 nub 对象 (IOBlockStorageDevice) 定义了一个通用块存储类,然后还为某些种类提供了特定的子类:IOCDBlockStorageDevice 和 IODVDBlockStorageDevice。

此外,系列还可以包括设备接口类(作为 IOUserClient 的子类)以及特定于该系列的命令(作为 IOCommand 的子类)。

系列还可以具有各种辅助类和头文件,用于特定于系列的类型定义。

有些系列在对驱动程序的需求不大时,不会为驱动程序提供公共节点或提供程序类。

而且 Apple 并未为所有类型的硬件提供系列。

如果您发现 I/O Kit 没有满足您需求的系列或接口,您可以随时创建直接从 IOService 继承的驱动程序。

如果驱动程序的潜在应用程序很少,则有时需要这种"无系列"驱动程序。

它们必须包含系列中的抽象和功能范围以及驱动程序典型的硬件特定代码。

除了直接从 IOService 继承之外,无系列驱动程序还经常使用 I/O Kit 辅助类,例如 IOWorkLoop、事件源类、IOMemoryCursor 和 IOMemoryDescriptor。


命名和编码约定

通常,Apple 对于系列内类命名的立场是,名称应表明该类代表什么。

通常,此名称由硬件规范决定。

例如,PCI 系列为作为该系列提供者的驱动程序定义了 IOPCIBridge 类。

此名称的原因很简单:PCI 桥(如规范所明确)是 PCI 控制器驱动程序控制的内容。

当系列的类没有明确的命名先例时,I/O Kit 将遵循对于 nub(客户端)类,其命名约定为 IO FamilyName Device,而对于提供程序类,其命名约定为 IO FamilyName Controller。

重要提示: 根据类所代表的内容命名类的一般准则同样适用于驱动程序。

驱动程序应根据其控制的设备命名(但不应有多余的后缀"Driver")。

如果您正在编写自己的 I/O Kit 系列,Apple 建议您对类遵循相同的命名准则。

此外,还有一些其他通用命名约定需要注意。

每个类、函数、类型等都应有一个前缀,以指示编写软件的供应商。

请确保不要使用 Apple 为自己保留的任何前缀(表 6-1)。

字首 意义
OS, os libkern 或其他内核服务
IO, io I/O Kit 或 I/O Kit 系列
MK,mk,mach_ Mach内核
Apple, APPLE, apple, AAPL, aapl, com_apple_ Apple 硬件支持(例如 Apple 提供的驱动程序)

此外,私有的内部符号应带有下划线 _ 前缀,以遵循 Apple 使用的惯例。

请勿从 KEXT 访问这些私有 API。

与驱动程序一样,强烈建议使用反向 DNS 表示法(用下划线代替句点)以避免命名冲突。


4、创建 I/O Kit 系列

有时候,您可能会觉得有必要编写自己的 I/O Kit 系列。

通常,这种情况发生在存在一个标准或协议而该标准或协议没有系列存在,并且您发现基于此协议或标准的设备的驱动程序之间需要互操作性。

一个例子可能是绘图仪和实验室设备的 IEEE488 标准。

如果您决定建立 families ,以下是一些可以帮助您的指导原则:

  • 一开始,一起编写系列和驱动程序代码;暂时不必担心驱动程序和系列之间的功能和接口划分。
    只需集中精力提出一个好的面向对象设计,确定哪些对象是必需的以及它们应该具有哪些关系。
  • 在您拥有可运行的驱动程序并解决了特定设备的堆栈问题后,请将系列代码与硬件特定代码分开。
    一种可能有助于定位系列通用代码(尤其是对于复杂系列)的方法是,为不同的硬件编写两个或更多驱动程序,然后抽象出通用代码。
  • 定义该系列的 nub 对象对驱动程序来说是什么样子 --- 即您的客户端将看到的 API。
    为此,请查看规范并封装重要功能(不必包含很少或从未使用的功能)。
    请记住,大多数系列的 nub 功能很少。
    它们通常封装寻址和仲裁细节。
  • 为将成为您 families 成员的驾驶员定义超类。
  • 保持系列的分层分离严密。
    系列不应包含来自任何其他系列或驱动程序的头文件,也不应定义客户端的超类。

还可能存在需要创建"超家族"的情况:该家族以类似于子类的方式扩展现有家族,但有很大区别;其目标是通用性而不是特异性。

第三方供应商可能希望拥有一个超家族来包含基于不同总线协议的驱动程序的通用代码。

这将消除加载不需要的代码的需要。

例如,鼠标供应商可能有一个能够驱动 USB 和 ADB 鼠标的驱动程序。

如果系统需要 USB 鼠标,您不希望同时加载特定于 ADB 的代码。

因此,供应商可能会编写一个充当服务库的超家族;它将特定于总线协议的代码层分离成子家族,并将剩余的代码放入超家族中。

只会加载特定于当前使用的总线的代码。


八、处理事件

设备驱动程序的工作环境可能是操作系统中最混乱的环境。

许多设备需要一系列命令才能执行单个 I/O 操作。

但是,多个线程可以在任何时间、任何地点进入驱动程序的代码,无论是通过客户端 I/O 请求、硬件中断还是超时事件;在多处理器系统上,线程甚至可以并发运行。

驱动程序必须准备好处理所有这些线程。

这些传入线程(每个线程都有自己的事件)给驱动程序带来了一些问题。

驱动程序需要一种方法来保护其数据结构不被不同的线程访问,因为这种同时访问可能会导致数据损坏甚至更糟。

它需要保证对任何必须完成的单个命令或操作的线程的独占访问,以保持驱动程序数据的完整性,或防止死锁或竞争条件。

在 I/O Kit 中,这种保护由 IOWorkLoop 类及其附带的事件源类提供。


1、工作循环

一个IOWorkLoop对象(或简称为工作循环)主要是一种门控机制,可确保单线程访问硬件使用的数据结构。

对于某些事件上下文,工作循环也是一个线程。

本质上,工作循环是与线程关联的互斥(互斥)锁。

它做几件事:

  • 其门控机制同步事件源之间的动作。
  • 它为事件处理提供了一个可堆叠的环境。
  • 它生成一个专用线程来完成中断控制器传递的间接中断。
    此机制将工作循环驱动程序的中断处理序列化,从而防止多个中断同时访问驱动程序数据。

为了正确看待工作循环的作用,首先要考虑它针对哪些事件源而设计。

在 I/O Kit 中,有五大类异步事件:

  • 中断事件---来自设备的间接(二次)中断
  • 计时器事件------计时器定期传送的事件,例如超时
  • I/O 命令 --- 驱动程序客户端向其提供程序发出的 I/O 请求
  • 电源事件------通常通过调用驱动程序堆栈生成
  • 结构事件------通常涉及 I/O 注册表的事件

I/O Kit 提供了处理这些事件来源:IOInterruptEventSourceIOTimerEventSource, 和IOCommandGate。(您可以使用对象提供的机制来处理电源和结构事件IOCommandGate。)

每个事件源类都定义了一种特定于事件类型的机制,用于在工作循环的受保护上下文中调用单个函数。

如果携带事件的线程需要访问驱动程序的关键数据,则必须通过其中一个类的对象来执行此操作。

通常,客户端驱动程序在start函数中设置工作循环、事件源和事件处理程序。

为了避免死锁和竞争条件,访问同一数据的所有代码都应该共享一个工作循环,向其注册其事件源,以便使用单个门控机制。

当然,工作循环可以在不相关的对象之间安全地共享,并且通常由单个驱动程序堆栈中不同级别的对象共享。

工作循环也可以专用于特定驱动程序及其客户端。

有关更多信息,请参阅 共享和专用工作循环

I/O Kit 工作循环机制提供的功能大致类似于 Mac OS 9 的垂直回溯管理器、时间管理器和延迟任务管理器。


工作循环架构

I/O Kit 的工作循环机制可以减轻上下文切换是某些操作系统中常用的底层事件处理模型的副产品。

为了保证事件处理的单线程上下文,此模型在一个线程上完成所有操作。

不幸的是,将工作转移到线程需要在事件承载线程的上下文中进行切换。

更准确地说,当此线程从正在运行的上下文转到非运行的上下文时,必须保存其当前寄存器状态。

当安全线程完成其工作时,将恢复原始线程的状态,并将控制权转移回线程最初引用的函数。

在线程上下文之间来回切换会消耗周期。

对于 I/O 命令和计时器事件,工作循环模型的工作方式截然不同。

在这些情况下,相应事件源的线程只需获取工作循环持有的互斥锁。

Action例程完成之前,无法处理来自任何其他源的事件当前事件返回。

虽然锁是互斥的,但它并不阻止重入。

此外,您可以在同一个驱动程序堆栈中拥有多个工作循环,这增加了死锁。

然而,工作循环确实避免了自我死锁,因为它们基于递归锁:它们总是检查它们是否是当前拥有锁的线程。

I/O Kit 管理中断事件源的方式确实涉及上下文切换。

中断的完成例程在工作循环的线程上运行,而不是在传递中断的线程上运行。

在这种情况下需要上下文切换,因为中断控制器必须立即将直接(主)中断分发给其他线程来运行完成这些中断的例程。

有关详细信息,请参阅 处理中断。

有两个因素影响工作循环查询其事件源以获取工作。

线程的相对优先级是主要决定因素,其中计时器具有最高优先级。

客户端线程可以修改其自己的优先级,从而加快 I/O 请求的交付(但这可能不会影响它们的处理速度,因为 I/O 请求通常按 FIFO 顺序排队)。

对于中断事件源,它们也具有相对较高的优先级,将它们添加到工作循环的顺序决定了查询它们的顺序。

有关更多详细信息,请参阅 处理中断。

无论事件源和机制如何,工作循环主要用于一件事:运行完成或事件源指定的Action例程。

它保证处理事件的例程是任何给定时间唯一运行的例程。

工作循环的这个方面提出了一个设计要点。

当线程运行代码来处理事件时,其他事件可以异步传递到它们的事件源,但直到处理程序返回后才能处理它们。

因此,事件处理程序不应尝试完成大量工作或执行任何可能阻塞的 操作(即等待其他进程完成),例如分配内存或其他资源。

相反,如果可能的话,它们应该将工作排队或以其他方式推迟到以后处理。


共享和专用工作循环

所有 I/O Kit 服务都可以轻松共享其提供者的工作循环。

驱动程序注册表的基座(代表计算机的逻辑板)始终包含工作循环,因此即使驱动程序本身不创建工作循环,也可以确保它具有工作循环。

驱动程序需要做的就是调用 IOService 的 getWorkLoop 函数来访问其提供者的工作循环。

这样,整个驱动程序对象堆栈或此类对象的子集可以共享一个工作循环。

图 7-1 显示了多个驱动程序对象共享的工作循环如何使用事件源来管理对其门控机制的访问。


图 7-1 共享工作循环的驱动对象


大多数驱动程序不会创建自己的工作循环。

如果硬件不直接在驱动程序中引发中断,或者驱动程序中很少发生中断,则不需要自己的工作循环。

但是,一个驱动程序直接中断(即直接与中断控制器交互的中断)应该创建自己的专用工作循环此类驱动程序的示例包括 PCI 控制器驱动程序(或任何具有提供程序类的类似驱动程序)IOPCIDevice) 和 RAID 控制器驱动程序。

然而,即使这些工作循环也可能由驱动程序的客户端共享,因此重要的是要意识到,无论哪种情况,驱动程序都不能假设它独占了工作循环的使用权。

这意味着驱动程序很少应该启用或禁用其工作循环上的所有事件,因为这样做可能会影响使用工作循环的其他 I/O Kit 服务。

如果驱动程序处理中断或出于其他原因需要自己的工作循环,则应重写 IOService getWorkLoop 函数以创建专用的工作循环,仅供驱动程序及其客户端使用。

如果getWorkLoop没有被重写,驱动程序对象将在其堆栈中获取下一个工作循环。


获取工作循环的示例

要为客户端驱动程序获取工作循环,您通常应该使用提供程序的工作循环,或者在必要时创建自己的工作循环。

要获取提供程序的工作循环,您只需调用 IOService getWorkLoop 函数 并保留返回的对象。

获取工作循环后,您应该立即创建事件源并将其添加到工作循环中(确保已启用它们)。

要为您的驱动程序创建 专用的工作循环,请重写getWorkLoop函数。
例 7-1 说明了 getWorkLoop 的线程安全的实现,它可以延迟且安全地创建工作循环。


例 7-1 创建专用工作循环

c 复制代码
protected:
    IOWorkLoop *cntrlSync;/* Controllers Synchronizing context */
// ...
IOWorkLoop * AppleDeviceDriver::getWorkLoop()
{
    // Do we have a work loop already?, if so return it NOW.
    if ((vm_address_t) cntrlSync >> 1)
        return cntrlSync;
 
    if (OSCompareAndSwap(0, 1, (UInt32 *) &cntrlSync)) {
        // Construct the workloop and set the cntrlSync variable
        // to whatever the result is and return
        cntrlSync = IOWorkLoop::workLoop();
    }
    else while ((IOWorkLoop *) cntrlSync == (IOWorkLoop *) 1)
        // Spin around the cntrlSync variable until the
        // initialization finishes.
        thread_block(0);
 
    return cntrlSync;
}

此代码首先检查是否cntrlSync是有效的内存地址;如果是,则工作循环已存在,因此代码将返回该地址。

然后,它测试是否有其他线程试图通过原子方式比较并将控制器同步器变量从 0 交换为 1(1 不能是工作循环的有效地址)来创建工作循环。

如果没有发生交换,则其他线程正在初始化工作循环,因此cntrlSync函数将等待变量不再为 1。

如果发生交换,则不存在工作循环,也没有其他线程正在创建工作循环。

在这种情况下,该函数将创建并返回工作循环,这将解除可能正在等待的任何其他线程的阻塞。

就像获取共享工作循环时一样,在 start 调用getWorkLoop 来获取工作循环对象(然后保留它)。

创建并初始化工作循环后,必须创建事件源并将其添加到其中。

有关 I/O Kit 中事件源的更多信息,请参阅以下部分。


2、事件源

工作循环可以添加任意数量的事件源。

事件源是与设备驱动程序可以处理的事件类型相对应的对象;目前有硬件中断、计时器事件和 I/O 命令的事件源。

I/O Kit 为这些事件类型中的每一个定义了一个类:IOInterruptEventSourceIOTimerEventSource, 和 IOCommandGate分别。

这些类中的每一个都直接从抽象类 IOEventSource 继承。

事件源对象充当来自特定事件源的事件的队列,并在工作循环上下文要求它们工作时将这些事件移交给工作循环上下文。

创建事件源对象时,您可以指定回调函数(也称为"动作"函数)将被调用来处理事件。

与 Cocoa 环境的目标/动作机制类似,I/O Kit 将事件的目标(通常是驱动程序对象)和要执行的动作作为实例变量存储在事件源中。

处理程序的签名必须符合Action在事件源类的头文件中声明的原型。

根据需要,工作循环依次询问其每个事件源(通过调用它们的checkForWork函数)来处理事件。

如果事件源有排队事件,工作循环将在其自己的受保护上下文中运行该事件的处理程序代码。

请注意,当您向工作循环注册事件源时,事件源会获得工作循环的信号信号量,事件源会使用该信号量来唤醒工作循环。

(有关工作循环如何休眠和唤醒的更多信息,请参阅 IOWorkLoop 文档中的 threadMain 函数。)

客户端驱动程序在其激活阶段(通常是start函数)创建所需的事件源并将其添加到其工作循环中。

驱动程序还必须为每个事件源实现一个事件处理程序,确保函数的签名符合为Action事件源类定义的函数原型。

出于性能原因,事件处理程序应避免执行任何可能阻塞的操作(例如分配内存)并推迟处理大量数据。

有关事件优先级和延迟工作的更多信息,请参阅 工作循环事件处理程序。


对于每种类型的事件源,将事件源添加到工作循环的过程都类似。

它涉及四个简单的步骤:

  1. 获取你的工作循环。
  2. 创建事件源对象。
  3. 将对象添加到工作循环中。
  4. 启用事件源。

处理事件源也有一个常见的程序模式:

  1. 禁用事件源。
  2. 将其从工作循环中移除。
  3. 释放事件源。

以下各节讨论了每个事件源的细节并给出了每种事件源的具体示例。


处理中断

中断通常是驱动程序处理的最重要的事件类型。

它们是连接到计算机的设备通知操作系统已发生异步操作并因此具有一些数据的方式。

例如,当用户移动鼠标或将 Zip 驱动器插入 USB 端口时,会产生硬件中断,受影响的驱动程序会收到此事件的通知。

本节讨论 I/O Kit 中的中断处理,特别关注IOInterruptEventSource及其子类的对象所起的作用。


I/O Kit 中的中断处理

I/O Kit 的中断处理模型不符合标准 UNIX 模型。

I/O Kit 驱动程序几乎总是在间接中断环境中工作,而不是像 UNIX 模型那样处理直接中断。

间接中断的限制较少,允许Mach 调度程序完成其工作。

(间接中断有时称为二次中断和直接中断主中断。

两种中断类型的差异与处理中断的环境有关。

两种类型的事件会触发中断:

  • 基于命令的事件,例如传入的网络数据包和存储介质的读取
  • 异步事件,例如键盘按下

发生中断时,会设置特定的中断线,一旦被中断的线程完成当前指令,控制就会分支到向平台专家注册的中断控制器。

当中断控制器接收到中断时,其线程将成为直接(主)中断的线程。

系统中通常一次只有一个直接中断,并且直接中断上下文在系统中具有最高优先级。

以下列表指示系统中线程的相对优先级:

  1. 直接中断
  2. 计时器和页出
  3. 实时(多媒体)
  4. 间接中断(驱动程序)
  5. 窗口管理器
  6. 用户线程(包括 I/O 请求)

由于直接中断上下文具有极高的优先级,因此其设计职责是尽快将中断移交给优先级较低的线程。

中断控制器必须解码中断发生的原因,将其分配给适当的驱动程序对象,然后返回。

在直接中断模型中,目标驱动程序假定直接中断的上下文。

它必须在这个最高优先级的上下文中处理中断。

直接中断的问题在于,它们既不能降低优先级,也不能被抢占。

在当前中断被处理之前,所有其他中断实际上都被禁用。

在 OS X 多处理环境中,直接中断尤其不能很好地扩展。

对于间接中断,中断控制器将其从中断线读取的中断分派到目标驱动程序的相应中断事件源对象,从而有效地使其在驱动程序的工作循环线程上进行调度。

然后在工作循环线程上运行由事件源定义的完成(或Action)例程来处理中断。工作循环线程的优先级虽然与大多数客户端线程相比很高,但低于承载直接中断的线程。

因此,在工作循环线程中运行的完成例程可能会被另一个直接中断抢占。

I/O Kit 并不禁止访问直接中断上下文,事实上它为此提供了一个单独的编程接口(请参阅 使用没有工作循环的中断处理程序)。

但是,强烈不建议使用直接中断。

工作循环可以有多个IOInterruptEventSource附加到它的对象。

这些对象添加到工作循环的顺序(通过IOWorkLoopaddEventSource 函数)决定了处理不同来源的中断的大致顺序。


[图 7-2] 说明了其中一些概念。

它显示了来自不同源的事件被传递到"附加"到工作循环的相应事件源对象。

与任何事件源对象一样,每个中断事件源充当该类型事件的队列;当队列中有事件时,该对象会向工作循环发出信号,表示有工作要做。

工作循环(即专用线程)唤醒并依次查询每个已安装的事件源。

如果事件源有工作,工作循环将在其自己的受保护线程中运行事件(在本例中为中断)的完成例程。

前一个线程(运行事件源代码的客户端线程)被阻止,直到例程完成对事件的处理。

然后,工作循环移动到下一个中断事件源,如果有工作,则在其受保护的上下文中运行该中断的完成例程。

当没有更多工作要做时,工作循环将进入休眠状态。


图 7-2 工作循环及其事件源


请记住,向工作循环添加中断事件源的顺序决定了特定中断事件的处理顺序。


设置附加到工作循环的中断处理程序

驱动程序通常会在 start 函数中通过调用类的工厂创建方法(例如 interruptEventSource)来 创建中断事件源对象(通常是IOInterruptEventSourceIOFilterInterruptEventSource 类) 。

此方法将驱动程序本身指定为目标,并标识要作为事件源的完成例程调用的操作成员函数(符合为 Action 事件源类定义的类型)。

工厂方法还将驱动程序与处理硬件中断功能的提供程序(通常是 IOPCIDevice 等小块)关联起来。

然后,驱动程序通过IOWorkLoopaddEventSource函数 将事件源注册到工作循环中。

例 7-2 提供了设置中断事件源的示例。


例 7-2 向工作循环添加中断事件源

c 复制代码
myWorkLoop = (IOWorkLoop *)getWorkLoop();
 
interruptSource = IOInterruptEventSource::interruptEventSource(this,
    (IOInterruptEventAction)&MyDriver::interruptOccurred,
    provider);
 
if (!interruptSource) {
    IOLog("%s: Failed to create interrupt event source!\n", getName());
    // Handle error (typically by returning a failure result).
    }
 
if (myWorkLoop->addEventSource(interruptSource) != kIOReturnSuccess) {
    IOLog("%s: Failed to add interrupt event source to work loop!\n",
        getName());
    // Handle error (typically by returning a failure result).
}

在此示例中,如果您未在 interruptEventSource 调用中指定提供程序,则事件源假定客户端将显式调用IOInterruptEventSourceinterruptOccurred方法。

调用此函数会导致异步事件安全地传送到驱动程序的IOInterruptEventSource

源自直接中断的事件在工作循环的线程内处理,该线程绝不会无限期阻塞。

这具体意味着处理中断的完成例程及其调用的任何函数都不得分配内存或创建对象,因为分配可能会无限期地阻塞。

你摧毁了一个驱动程序的停用函数(通常是stop)中的中断事件源。

在释放IOInterruptEventSource对象之前,应先将其禁用,然后将其从工作循环中移除。
例 7-3给出了如何执行此操作的示例。


例 7-3 释放IOInterruptEventSource对象

c 复制代码
if (interruptSource) {
    interruptSource->disable();
    myWorkLoop->removeEventSource(interruptSource);
    interruptSource->release();
    interruptSource = 0;
}

过滤中断事件源

I/O Kit 支持共享中断,驱动程序共享单个中断线。

为此,它定义了IOFilterInterruptEventSource类,IOInterruptEventSource 的一个子类。

Apple 强烈建议第三方设备驱动程序编写者基于IOFilterInterruptEventSource类,而不是IOInterruptEventSource类。

后一个类不能确保中断线的共享是安全的。

该类IOFilterInterruptEventSource遵循与其超类相同的模型,只是除了Action完成例程之外,它还定义了一个特殊的回调函数。

当发生中断时,中断会为共享中断线的每个驱动程序调用此函数。

在此函数中,驱动程序通过指示中断是否是它应该处理的来做出响应。


重要提示: 过滤例程在 主(硬件)中断上下文 中运行。

您应该在此例程中执行尽可能少的工作 --- 只需确定您的设备是否生成中断即可。

有关更多信息,请参阅 IOFilterInterruptEventSource 的文档:
https://developer.apple.com/documentation/kernel/iofilterinterrupteventsource

例 7-4 显示了如何设置和使用IOFilterInterruptEventSource


例 7-4 设置 IOFilterInterruptEventSource

c 复制代码
bool myDriver::start(IOService * provider)
{
    // stuff happens here
 
    IOWorkLoop * myWorkLoop = (IOWorkLoop *) getWorkLoop();
    if (!myWorkLoop)
        return false;
 
    // Create and register an interrupt event source. The provider will
    // take care of the low-level interrupt registration stuff.
    //
    interruptSrc =
        IOFilterInterruptEventSource::filterInterruptEventSource(this,
                    (IOInterruptEventAction) &myDriver::interruptOccurred,
                    (IOFilterInterruptAction) &myDriver::checkForInterrupt,
                    provider);
    if (myWorkLoop->addEventSource(interruptSrc) != kIOReturnSuccess) {
        IOLog("%s: Failed to add FIES to work loop.\n", getName());
    }
    // and more stuff here...
}
 
 
bool myDriver::checkForInterrupt(IOFilterInterruptEventSource * src)
{
    // check if this interrupt belongs to me
 
    return true; // go ahead and invoke completion routine
}
 
 
void myDriver::interruptOccurred(IOInterruptEventSource * src, int cnt)
{
    // handle the interrupt
}

如果过滤例程(例 7-4 中的checkForInterrupt例程)返回true,则 I/O Kit 将自动在工作循环中启动中断处理程序例程。

中断将在硬件中保持禁用状态,直到中断服务例程(例 7-4 中的interruptOccurred)完成。

注意: 在某些情况下,例如伪 DMA 的实现,这种行为可能并不理想。

在这种情况下,您可以选择让过滤例程在工作循环本身上安排工作,然后返回false

如果这样做,中断将不会在硬件中被禁用,并且您可能会在工作循环级服务例程完成之前收到其他主中断。

由于此方案会影响过滤例程和中断服务例程之间的同步,因此除非驱动程序需要伪 DMA,否则应避免这样做。


使用没有工作循环的中断处理程序

IOService 类提供成员函数来注册在工作循环机制之外运行的中断处理程序。

这些处理程序可以在直接中断上下文中调用,并且必须调用提供程序(例如 IOPCIDevice nub)的中断管理代码。

每个中断源只能安装一个处理程序。

它必须准备好创建和运行自己的线程并执行自己的锁定。

很少有驱动程序需要使用以这种方式创建和控制的中断处理程序。

一个合理使用此类中断处理程序的例子是需要将直接中断路由到驱动程序的多功能卡。

如果您学习本课程,请务必小心。

在直接中断上下文中,很少有系统 API 可以安全调用。

有关该主题的更多信息即将发布。


处理计时器事件

设备驱动程序偶尔需要设置定时器,通常是为了实现超时,以便驱动程序可以确定 I/O 请求是否未在合理的时间内完成。
IOTimerEventSource类就是为此目的而设计的。

重要提示: 无法保证 I/O Kit 中超时的绝对准确性。

Mach 调度程序总是可以运行更高优先级的线程,这可能会延迟计时器Action例程的执行。

驱动程序创建一个IOTimerEventSource,带有回调Action函数 的以及调用该函数的时间,然后将其注册到工作循环中运行。

超时后,事件源将与工作循环一起调度。

当工作循环查询工作时,事件源会关闭工作循环门(通过获取工作循环的锁),调用回调函数,然后释放工作循环锁以打开门。
例 7-5显示了如何创建和注册计时器事件源。


例 7-5 创建并注册计时器事件源

c 复制代码
myWorkLoop = (IOWorkLoop *)getWorkLoop();
 
timerSource = IOTimerEventSource::timerEventSource(this,
    (IOTimerEventSource::Action)&MyDriver::timeoutOccurred);
 
if (!timerSource) {
    IOLog("%s: Failed to create timer event source!\n", getName());
    // Handle error (typically by returning a failure result).
    }
 
if (myWorkLoop->addEventSource(timerSource) != kIOReturnSuccess) {
    IOLog("%s: Failed to add timer event source to work loop!\n", getName());
    // Handle error (typically by returning a failure result).
}
 
timerSource->setTimeoutMS(MYDRIVER_TIMEOUT);

驱动程序通常希望同时设置计时器并发出 I/O 请求。

如果 I/O 请求在触发计时器事件之前完成,则驱动程序应立即取消计时器。

如果先触发计时器事件,则驱动程序通常会重新发出超时 I/O 请求(此时它会重置计时器)。

如果您希望计时器事件重复发生,则应在Action处理程序中将计时器重置为所需的间隔。
IOTimerEventSource 类没有设置周期性计时器的机制。

该类确实提供了一些函数,用于以各种粒度(纳秒、微秒等)设置相对和绝对计时器间隔。

例 7-5 使用setTimeoutMS设置具有 特定超时毫秒间隔的计时器。

源自计时器的事件由驱动程序的Action例程处理。

与其他事件处理程序一样,此例程绝不会无限期阻塞。

这具体意味着计时器处理程序及其调用的任何函数都不得分配内存或创建对象,因为分配可能会无限期地阻塞。

要处理计时器事件源,您应该先取消待处理的计时器事件,然后再从工作循环中移除事件源并释放它。

例 7-6 说明了如何在驱动程序的停用函数中执行此操作。


例 7-6 处理计时器事件源

c 复制代码
if (timerSource) {
    timerSource->cancelTimeout();
    myWorkLoop->removeEventSource(timerSource);
    timerSource->release();
    timerSource = 0;
}

I/O 请求和命令门

驱动程序客户端使用 IOCommandGate对象向驱动程序发送 I/O 请求。

命令门控制对工作循环锁的访问,并以此方式序列化对 I/O 请求中涉及的数据的访问。

它不需要线程上下文切换来确保单线程访问。
IOCommandGate事件源只 需在运行其Action例程之前获取工作循环锁;通过这样做,它可以防止同一工作循环上的其他事件源进行调度。

这使其成为一种有效的 I/O 传输机制。

请注意,nub 类通常会定义Action函数 供其自己的客户端使用,这样驱动程序类就不必自己使用命令门。


上涨呼叫和下跌呼叫

客户端通过命令门发起的呼叫称为down 调用。

这些调用总是源自工作循环上下文之外的线程,因此它们可以安全地阻塞而不会导致死锁(只要它们不持有工作循环门)。

所有分配都应在命令门关闭之前在 I/O 请求的下行调用端进行。

中断或计时器事件发起的 Up 调用在工作循环的上下文中发生,并且绝不会无限期地阻塞。

这具体意味着中断和超时处理程序以及它们调用的任何函数都不得分配内存或创建对象,因为分配可能会阻塞,并且可能导致分页死锁。

向上调用可能会导致客户端通知,从而立即导致另一个通过命令门的 I/O 请求。

工作循环可以通过同一线程处理其门的递归关闭,因此这种情况永远不会导致死锁。

但是,由于新请求发生在向上调用的上下文中,因此该请求不能阻塞;不过,这个问题属于发出 I/O 请求的系统客户端,因此作为驱动程序开发人员,您永远不必担心这个问题。


设置和使用命令门

在关闭命令门之前,您应该充分准备 I/O 请求。

I/O 请求涉及三件事:命令本身(特定于系列)、传输中涉及的内存(定义为 IOMemoryDescriptor 对象)以及在命令门上下文中处理请求时调用的函数。

有关 IOMemoryDescriptors 和相关对象的信息,请参阅 管理数据。

命令门应尽可能短地关闭,在此期间执行的工作量尽可能少。

命令门持有工作循环锁的时间越长,争用的可能性就越大。

与所有事件源一样,命令门函数不应分配内存或任何其他无限制资源,因为存在阻塞的危险。

相反,客户端应该在将控制权转移到工作循环上下文之前预先分配所需的资源。

例如,它可以在其start函数中分配一个资源池。

IOCommandGate通过调用commandGate工厂方法创建对象,指定事件源的对象"所有者"(通常是this)和 指向符合Action原型 的函数的指针作为参数。

然后使用IOWorkLoopaddEventSource函数将命令门注册到客户端的工作循环中 例7-7 给出了该过程的一个示例。


例 7-7 创建并注册命令门

c 复制代码
workLoop = (IOWorkLoop *)getWorkLoop();
commandGate = IOCommandGate::commandGate(this,
                 (IOCommandGate::Action)receiveMsg);
if (!commandGate ||
    (workLoop->addEventSource(commandGate) != kIOReturnSuccess) ) {
    kprintf("can't create or add commandGate\n");
    return false;
}

IOCommandGate 类提供了两种在命令门内启动 I/O 请求执行的替代方案。

一种是runCommand成员函数 另一个是runAction成员函数。

这些函数的工作方式类似。

当客户端希望调用Action函数时,它不会直接调用,而是调用 命令门runCommandrunAction函数,并传入所有必需的参数。

然后,命令门获取工作循环锁(即,它关闭命令门),调用Action函数,然后打开门。

这两个函数的不同之处在于它们的灵活性。
runCommand函数使用其他事件源类使用的相同目标/操作机制。

在此机制中,创建的IOCommandGate对象封装(指向)Action函数以及实现此函数的目标(或"所有者")对象。

在此模型中,只能为 I/O 请求调用一个Action函数。

但是,驱动程序通常必须处理多个 I/O 请求源。

如果是这种情况,您可以使用runAction函数在多个命令门中发出 I/O 请求。

此函数允许您定义要在命令门上下文中调用的函数;您必须指定指向此函数的指针作为第一个参数。

重要提示: 不要从中断上下文调用runActionrunCommand函数。

例 7-8 说明了如何使用runCommand函数发出 I/O 请求。


例 7-8 通过命令门发出 I/O 请求

c 复制代码
void ApplePMU::enqueueCommand ( PMUrequest * request )
{
    commandGate->runCommand(request);
}
 
void receiveMsg ( OSObject * theDriver, void * newRequest, void *, void *, void * )
{
    ApplePMU * PMUdriver = (ApplePMU *) theDriver;
    PMUrequest * theRequest = (PMUrequest*)newRequest;
 
    // Inserts the request in the queue:
    theRequest->prev = PMUdriver->queueTail;
    theRequest->next = NULL;
    if ( PMUdriver->queueTail != NULL ) {
        PMUdriver->queueTail->next = theRequest;
    }
    else {
        PMUdriver->queueHead = theRequest;
    }
    PMUdriver->queueTail =  theRequest;
 
    // If we can, we process the next request in the queue:
    if ( (PMUdriver->PGE_ISR_state == kPMUidle) && !PMUdriver->adb_reading) {
        PMUdriver->CheckRequestQueue();
    }
}

在此示例中,runCommand函数用于 间接调用命令门的Action函数:receiveMsg

此示例展示的一个重要策略是如何推迟处理 I/O 请求(当可能需要分配内存和其他资源时),直到命令门中不再有事件排队。

函数将receiveMsg每个传入请求排队,如果没有其他请求待处理,则调用CheckRequestQueue函数来执行实际的 I/O 工作。

典型的做法是在发出 I/O 请求的同时设置超时(使用IOTimerEventSource对象)。

如果 I/O 请求未在合理的时间内完成,则会触发计时器,让您有机会纠正任何问题(如果可能)并重新发出 I/O 请求。

如果 I/O 请求成功,请记住禁用计时器。

有关使用IOTimerEventSource对象的详细信息,请参阅 处理计时器事件

您可以在驱动程序的 停用函数(通常是stop)中销毁命令门事件源。

在释放IOCommandGate对象之前,您应该将其从工作循环中移除。

例 7-9 给出了如何执行此操作的示例。


例 7-9 释放IOCommandGate对象

c 复制代码
if (commandGate) {
    myWorkLoop->removeEventSource(commandGate);
    commandGate->release();
    commandGate = 0;
}

九、管理数据

驱动程序的主要工作是响应客户端请求以及硬件生成的中断等事件,将数据移入和移出系统。

I/O Kit 定义了驱动程序使用一些类来实现此目的的标准机制。

驱动程序使用 IOMemoryDescriptor 和 IOMemoryCursor 类(在 OS X v10.4.7 及更高版本中,还有 IODMACommand 类)对数据缓冲区执行 I/O 前和 I/O 后处理,例如在客户端格式和硬件特定格式之间进行转换。

本章讨论了这些类以及与设备驱动程序和数据管理相关的各种问题。

驱动程序使用 IOWorkLoop 和 IOEventSource 类来处理客户端请求和其他事件,以序列化访问并保护其关键数据。

由于这些机制,驱动程序在处理请求的正常过程中很少需要担心诸如保护关键数据或禁用和启用中断之类的问题。

有关信息,请参阅 处理事件一章。


1、处理 I/O 传输

I/O 传输只不过是系统内存和设备之间一个或多个缓冲区的数据移动。

但是,本文中的"系统内存"是指实际物理内存,而不是 OS X 中用户和内核任务使用的虚拟内存地址空间。

由于设备驱动程序级别的 I/O 传输易受硬件限制,而硬件只能"看到"物理内存,因此需要特殊处理。

OS X 中的输入/输出操作发生在具有任务和线程抢占式调度的虚拟内存系统的上下文中。

在此上下文中,具有稳定虚拟地址的数据缓冲区可以位于物理内存中的任何位置,并且该物理内存位置可以随着虚拟内存的调入和调出而改变。

该数据缓冲区可能在任何给定时间都不在物理内存中。

即使是不受重定位影响的内核内存,CPU 也会在虚拟地址空间中访问它。

对于写入操作,即系统中的数据被发送出去时,如果需要,必须从虚拟内存存储中调入数据缓冲区。

对于读取操作,即缓冲区将由带入系统的数据填充,数据缓冲区的现有内容无关紧要,因此无需调入页面;只需在物理内存中分配一个新页面,并覆盖该页面的先前内容(如果需要,在调出页面之后)。

然而,I/O 传输的一个要求是,在传输期间,系统内存中的数据不能被重新定位。

为了保证设备可以访问缓冲区中的数据,缓冲区必须驻留在物理内存中,并且必须连线,这样它们就不会被调出或重新定位。

然后,必须将缓冲区的物理地址提供给设备。

设备使用完缓冲区后,必须将其解连线,以便虚拟内存系统可以再次将它们调出。

为了帮助您处理这些和其他硬件限制,I/O Kit 提供了几个类供您使用。


内存描述符和内存游标

在内置有虚拟内存,I/O传输需要特殊的准备和完成后处理:

  • I/O 传输所需的空间必须位于物理内存,并且必须连接起来,以便在传输完成之前不能被分页。
  • 软件使用的虚拟内存地址必须转换为物理地址,并且必须将缓冲区地址和长度收集到描述要传输的数据的分散/聚集列表中。
  • 传输完成后,必须解除内存连线,以便将其调出。

在 I/O Kit 中,所有这些工作都由IOMemoryDescriptor 和IOMemoryCursor 类(有关 IODMACommand 类的信息,请参阅 在 64 位系统架构上支持 DMA,该类取代了 OS X v10.4.7 及更高版本中的 IOMemoryCursor)。

I/O 请求通常包含一个 IOMemoryDescriptor 对象,该对象描述传输中涉及的内存区域。

最初,描述采用结构数组的形式,每个结构都由客户端任务标识符 ( task_t)、客户端虚拟地址空间的偏移量和字节长度组成。

驱动程序使用内存描述符来准备内存页面 - 如果需要,将它们分页到物理内存中,然后将它们连接起来 - 通过调用描述符的prepare函数。

当内存准备好后,堆栈较低级别的驱动程序(通常是控制 DMA(直接内存访问)的驱动程序)引擎------然后使用内存游标对象获取内存描述符的缓冲区段,并使用它们生成分散/聚集列表适合与硬件一起使用。

它通过调用getPhysicalSegments函数来实现这一点内存游标并对其接收的段进行任何必要的处理。

当 I/O 传输完成时,发起 I/O 请求的驱动程序将调用内存描述符的complete函数 来解除内存的连接并更新虚拟内存状态。

完成所有这些操作后,它会通知客户端请求已完成。

从 OS X v10.2 开始,IOBufferMemoryDescriptor(IOMemoryDescriptor 的子类)允许在任何任务中分配缓冲区以进行 I/O 或通过映射共享。

在以前的 OS X 版本中,IOBufferMemoryDescriptor 对象只能表示在内核地址空间中分配的缓冲区。

然而,在 OS X v10.2 及更高版本中,对 IOBufferMemoryDescriptor API 的更改支持一种更好的方法来处理非内核客户端要求生成的 I/O。

Apple 建议使用 IOBufferMemoryDescriptor API 将此类 I/O 发送到内核在客户端地址空间中分配的缓冲区。

这将分配方法的控制权交给内核,确保根据虚拟内存系统规定的内部准则分配缓冲区。

用户任务仍然可以指定要分配的缓冲区类型,例如可分页或可共享。

并且,用户任务仍然可以使用从用户客户端收到的vm_address_tvm_size_t变量访问缓冲区。

对于在 OS X v10.2 及更高版本中运行的软件,Apple 建议:

  • 用户任务不再使用malloc或其他用户级库函数在自己的地址空间中分配I/O缓冲区。
  • 用户客户端使用 IOBufferMemoryDescriptor 对象来表示内核分配的缓冲区,而不是表示用户任务分配的缓冲区的 IOMemoryDescriptor 对象。

网络驱动程序是使用 IOMemoryDescriptor 对象的例外。

网络系列改用mbuf结构由 BSD 内核网络堆栈定义。

BSDmbuf结构已经针对处理网络数据包进行了优化,在它们和内存描述符之间进行转换只会带来不必要的开销。

网络系列定义了 IOMemoryCursor 的子类,用于直接从mbuf结构中提取分散/聚集列表,从本质上使网络驱动程序的 I/O 处理方面与任何其他类型的驱动程序相同。

IOMemoryDescriptor 和 IOMemoryCursor 能够处理大多数驱动程序的内存缓冲区管理和重用。

具有特殊要求(例如所有缓冲区内存都是连续的或位于物理内存中的特定区域内)的驱动程序必须执行必要的额外工作才能满足这些要求,但仍使用内存描述符和游标与 I/O Kit 进行交互。


I/O 请求中的内存

尽管上一节讨论了 I/O Kit 驱动程序如何使用 IOMemoryDescriptor 和 IOMemoryCursor 类的对象在设备和系统内存之间传输数据,但它是在相当一般的层面上进行的。

从更详细的层面考虑 I/O 请求也是有益的:从用户进程调用写入或读取数据的那一刻到数据被传输到设备或从设备传输出来的那一刻,发生了什么?IOMemoryDescriptors、IOMemoryCursors 和其他 I/O Kit 对象在这一事件链中扮演了什么角色?

用户空间中的所有读写操作(即由应用程序或其他非内核进程执行的操作)最终都基于 I/O 向量。

I/O 向量是一个结构数组,每个结构都给出了特定进程的连续内存块的地址和长度;此内存以进程的虚拟地址空间表示。

I/O 向量有时称为分散/聚集列表。

首先举一个更具体的例子,一个进程发出一个调用,将一些数据写入设备。

它必须传入一组最小的参数:调用目标的句柄(io_service_t)、命令("write")、I/O 向量数组的基地址以及数组元素的数量。

这些参数会传递给内核。

但内核如何理解它们,特别是数组的基地址,它被输入为void *

内核位于其自己的虚拟地址空间中,而用户进程的虚拟地址空间未经翻译,对它来说毫无意义。


在进一步讨论之前,让我们回顾一下操作系统维护的内存映射。

在 OS X 中,有三种不同类型的地址空间:

  • 单个用户进程(例如应用程序和守护进程)的虚拟地址空间
  • 内核的虚拟地址空间(包括 I/O Kit)
  • 物理地址空间(系统内存)

当用户进程发出 I/O 调用时,该调用(及其参数)会向下传递到内核。

在那里,I/O Kit 将句柄参数转换为从适当的 IOUserClient 子类派生的对象。

用户客户端对象在逻辑上跨越了内核和用户空间之间的边界。

每个设备的每个用户进程都有一个用户客户端。

从内核的角度来看,用户客户端是堆栈顶部的客户端驱动程序,它与其下方的 nub 对象进行通信(参见 [图 8-1] )。


图 8-1 用户客户端在 I/O 命令中的角色


这用户客户端还会在地址空间之间进行转换,将用户进程缓冲区映射到内核的虚拟地址空间。

为此,它根据原始调用中指定的其他参数创建一个 IOMemoryDescriptor 对象。

IOMemoryDescriptor 对象的特殊功能在于它可以在 I/O 操作和系统的每个地址空间中描述一段内存。

它可以在驱动程序堆栈的各个驱动程序之间上下传递,使每个驱动程序都有机会改进或操纵命令。

驱动程序还可以重用内存描述符;在某些情况下,如果连续分配将导致性能下降,最好提前创建一个 IOMemoryDescriptor 池并回收它们。

在用户客户端创建(或重用) IOMemoryDescriptor 对象,它会立即调用内存描述符的prepare成员函数。

调用prepare 首先确保物理内存可用于 I/O 传输。

由于这是一个写入操作,虚拟内存系统可能必须从其存储中分页数据。

如果这是一个读取操作,则虚拟内存(VM 分页器可能需要从物理内存中换出一些其他页面,以便为 I/O 传输腾出空间。

无论哪种情况,当 VM 分页器参与准备物理内存时,都会对总线控制器驱动程序产生影响,这些驱动程序对 DMA 引擎进行编程,或者必须直接处理设备的要求(请参阅 DMA 和系统内存)。

在为 I/O 传输确保了足够的物理内存后,prepare函数会将内存连接起来,使其无法被调出分页。

调用prepare必须在 I/O 命令正式进入 I/O Kit 之前进行,并且还应在请求客户端的线程上进行,并在获取任何锁定之前进行。

切勿在命令门上下文中调用prepare,因为prepare是一个同步调用,可能会无限期 阻塞 (即等待其他进程完成)。
prepare返回时,用户客户端(通常)启动调用以安排 I/O 请求,将内存描述符传递给堆栈下方的驱动程序(通过已建立的接口,如中继 I/O 请求 中所述)。

内存描述符可能会传递给其他驱动程序,每个驱动程序都可能操纵其内容,直到它最终到达靠近硬件本身的对象 - 通常是总线控制器驱动程序或该驱动程序的客户端节点。

此对象安排 I/O 请求并接受命令(有关命令门的信息,请参阅 I/O 请求和命令门)。

在命令门内,它通常会在硬件空闲时将请求排队以进行处理。


图 8-2 I/O 传输中的主要 I/O Kit 对象


最终在驱动程序堆栈的较低级别接收请求的是总线控制器驱动程序(例如 ATA 或 SCSI)。

此较低级别的驱动程序对象必须对控制器的 DMA 引擎进行编程,否则将创建一个分散/聚集列表将数据直接移入和移出设备。

使用 IOMemoryCursor 对象,此驱动程序从内存描述符的缓冲区段生成物理地址和长度,并执行创建分散/聚集列表所需的任何其他工作,该列表具有硬件所需的对齐、字节序格式和大小因子。

要生成段,它必须调用内存光标的getPhysicalSegments函数。

整个过程在工作循环上下文中运行。

传输完成后,通常会生成硬件中断以启动另一个方向的 I/O 传输。

当 I/O 传输完成时,调用的对象prepare将调用内存描述符的complete函数来解除内存连接并更新虚拟内存状态。

重要的是用相应的 complete 来平衡每个prepare

当所有这些完成后,用户客户端会通知原始进程请求已完成。


64 位系统架构的问题

从 OS X v10.3 及更高版本开始,Apple 引入了一些更改,以允许现有的设备驱动程序与新的 64 位系统架构兼容。

需要解决的问题涉及 PCI 总线上可处理 32 位地址的设备与 64 位主内存之间的通信。

随后,在 OS X v10.4.7 中,Apple 引入了IODMACommand类,以允许执行 DMA 的设备驱动程序寻址基于 Intel 的 Macintosh 计算机中的 64 位主内存。

以下部分介绍了这些更改。


64 位系统架构上的地址转换

Apple 通过地址转换解决了这个问题,即将内存块"映射"到 PCI 设备的 32 位地址空间中。

在这种方案中,PCI 设备仍然可以看到 4 GB 的空间,但该空间可以由不连续的内存块组成。

内存控制器的一部分称为 DART(设备地址解析表)在 PCI 地址空间和更大的主内存地址空间之间进行转换。

DART 通过保存一个转换表来处理此问题,该转换表用于在处理器看到的物理地址和 PCI 设备看到的地址(称为 I/O 地址)之间进行映射)。

如果您的驱动程序遵循 Apple 提供的文档化 API,则地址转换过程是透明的。

例如,当您的驱动程序调用 IOMemoryDescriptor 的prepare方法时,映射会自动放置在 DART 中。

相反,当您的驱动程序调用 IOMemoryDescriptor 的release方法时,映射将被删除。

虽然这一直是推荐的过程,但如果在运行 OS X v10.3 或更高版本的驱动程序中未执行此操作,则可能会导致随机数据损坏或崩溃。

请注意,release方法不会取代complete方法。

与往常一样,每次调用prepare都应与调用complete保持平衡。

如果您的驱动程序在 OS X v10.3 系统上遇到困难,您首先应确保遵循以下准则:

  • 始终调用IOMemoryDescriptor::prepare来为 I/O 传输准备物理内存(这也会将映射放入 DART)。
  • 用一个 IOMemoryDescriptor::prepare 来平衡每一个IOMemoryDescriptor::complete,以解除记忆。
  • 请务必调用IOMemoryDescriptor::release以从 DART 中删除映射。
  • 在包含 DART 的硬件上,请注意读写的 DMA 方向。
    在 64 位系统上,如果驱动程序尝试写入设置为读取的 DMA 方向的内存区域,则会导致内核崩溃。

内存子系统中这些变化的一个副作用是 OS X 可能会在内存区域中返回物理上连续的页面范围。

在早期版本的 OS X 中,系统以相反的顺序返回多页内存区域,从最后一页开始,然后向第一页移动。

因此,多页内存区域很少包含物理上连续的页面范围。

在内存区域中看到物理上连续的内存块的可能性大大增加,这可能会暴露驱动程序中的潜在错误,而这些驱动程序以前不必处理物理上连续的页面。

如果您的驱动程序行为不正确或出现问题,请务必检查是否存在这种可能性。

内存子系统更改的另一个结果与驱动程序可能直接从 pmap 层获取的物理地址有关。

由于物理地址和 I/O 地址之间没有一一对应的关系,因此从 pmap 层获取的物理地址在虚拟内存系统本身之外没有任何用途。

使用 pmap 调用(例如pmap_extract)获取此类地址的驱动程序将无法在具有 DART 的系统上运行。

为了防止使用这些调用,OS X v10.3 将拒绝加载使用这些调用的内核扩展,即使在没有 DART 的系统上也是如此。


在 64 位系统架构上支持 DMA

如64 位系统架构上的地址转换中所述,在 OS X v10.3 及更高版本中运行的驱动程序使用已记录的 Apple 提供的 API 在 64 位系统上寻址物理内存时很少(如果有的话)遇到问题,因为 DART 执行的地址转换对它们来说是透明的。

但是,当前基于 Intel 的 Macintosh 计算机不包含 DART,这对需要使用物理地址的设备驱动程序(例如执行 DMA 的设备驱动程序)有影响。

由于目前基于 Intel 的 Macintosh 计算机没有硬件支持的地址转换或重新映射,因此需要访问物理内存的设备驱动程序必须能够寻址 4 GB 以上的内存。

在 OS X v10.4.7 及更高版本中,您可以使用 IODMA 命令类来执行此操作。

重要提示:如果 设备驱动程序在 OS X v10.4.7 及更高版本中运行,并且面向基于 Intel 的 Macintosh 计算机,则必须 将其更新为使用 IODMACommand(如果它使用物理地址)。

如果设备驱动程序在 OS X v10.4.7 及更高版本中运行,并且面向基于 PowerPC 的 Macintosh 计算机,则无需这样做,但建议这样做,尤其是在驱动程序使用物理地址的情况下。

IODMACommand 类取代了 IOMemoryCursor 类:它提供了 IOMemoryCursor 的所有功能,并增加了一种指定硬件寻址能力的方法,以及在必要时将内存复制到反弹缓冲区的功能。

实例化 IODMACommand 对象时,您可以指定以下属性:

  • 硬件可以支持的地址位数(例如 32、40 或 64)
  • 最大段大小
  • 硬件所需的任何对齐限制
  • 最大 I/O 传输大小
  • IODMACommand 返回的物理地址段的格式(例如,32 位或 64 位以及大端、小端或主机端)

在典型情况下,你可以按下列方式使用 IODMACommand 对象:

  1. 为每个 I/O 事务创建一个 IODMACommand 对象(您可以在驱动程序启动时创建一个 IODMACommand 对象池)。
  2. 当 I/O 请求到达时,使用IODMACommand::setMemoryDescriptor来定位代表该请求的 IOMemoryDescriptor 对象。
  3. 调用IODMACommand::prepare(除其他事项外,该函数还分配传输可能需要的映射资源)。
  4. 使用 IODMACommand 函数生成适当的物理地址和长度(IODMACommand::gen64IOVMSegments返回 64 位地址和长度以及IODMACommand::gen32IOVMSegments返回 32 位地址和长度)。
  5. 启动硬件I/O。
  6. 当 I/O 完成时,调用IODMACommand::complete(来完成 DMA 映射的处理),跟随调用IODMACommand::clearMemoryDescriptor(如有必要,从反弹缓冲区复制数据,并释放资源)。

注意: IODMACommand preparecomplete 函数 与 IOMemoryDescriptor 的 preparecomplete 函数不同。

IODMACommand 的prepare 函数包含 DMA 事务的开始和结束,而 IOMemoryDescriptor的 preparecomplete函数连接和断开内存,必须照常调用。

如果您的 DMA 引擎执行复杂的操作,例如执行部分 I/O 或同步对单个 IOMemoryDescriptor 的多个访问,则您应该编写驱动程序,假设内存将被弹回。

您不需要添加检查弹回的代码,因为 IODMACommand 函数,例如synchronize 在不需要时是无操作的。


2、中继 I/O 请求

客户端请求通过驱动程序系列定义的特定函数传递给驱动程序。

例如,存储系列驱动程序通过实现read函数。

类似地,网络系列驱动程序通过outputPacket实现来处理传输网络数据包的请求功能。

一个I/O 请求总是包含一个缓冲区,其中包含要写入的数据或提供要读取的数据的空间。

在 I/O Kit 中,此缓冲区采用除网络外,所有系列的 IOMemoryDescriptor 对象 均使用 BSD 为网络数据包定义的mbuf结构。

这两种缓冲机制为所有驱动程序提供了对驱动程序堆栈上行和下行数据缓冲区的优化管理,最大限度地减少了数据复制,并执行了准备和完成 I/O 操作缓冲区所需的所有步骤。

I/O Kit 家族定义了 I/O 和其他请求接口。

通常,您不必担心驱动程序在可重入上下文中的资源保护,除非它明确放弃该家族或 I/O Kit 通常提供的保护。


3、有关内存描述符的更多信息

内存描述符是一个继承自IOMemoryDescriptor 类,描述数据流如何(取决于方向)放入内存或从内存中提取。

它表示一段内存,用于保存 I/O 传输中涉及的数据,并指定为一个或多个物理或虚拟地址范围(范围是起始地址和长度(以字节为单位))。

IOMemoryDescriptor 对象允许驱动程序堆栈中不同级别的对象引用映射到物理内存、内核的虚拟地址空间或用户进程的虚拟地址空间的同一块数据。

内存描述符提供可以在不同地址空间之间进行转换的函数。

从某种意义上说,它封装了 I/O 传输中涉及的某些数据在整个生命周期内的各种映射。

IOMemoryDescriptor 是一个抽象基类,定义了用于描述物理或虚拟内存。

尽管它是一个抽象类,但 I/O Kit 为直接从该类实例化的对象提供了 IOMemoryDescriptor 的具体通用实现。

I/O Kit 还提供了 IOMemoryDescriptor 的两个专用公共子类:IOMultiMemoryDescriptor 和 IOSubMemoryDescriptor。
表 8-1描述了这些类。

班级 描述
IO多内存描述符 将多个通用内存描述符包装成一个内存描述符。
这通常是为了符合总线协议。
IO子内存描述符 表示来自某些其他 IOMemoryDescriptor 的特定子范围的内存区域。

IOMemoryDescriptor 基类本身定义了几种方法,可以从所有子类的对象中调用这些方法。

一些方法返回描述符的物理连续内存段(用于 IOMemoryCursor 对象),其他方法使用缓存和放置映射选项将内存映射到任何地址空间。

一个相关且通常有用的类是IOMemoryMap。

当您调用 IOMemoryDescriptor 的map方法来映射特定地址空间中的内存描述符时,将返回一个 IOMemoryMap 对象。

IOMemoryMap 类的对象表示由 IOMemoryDescriptor 描述的映射内存范围。

映射可能在内核或非内核任务中,也可能在物理内存中。

映射可以具有各种属性,包括处理器缓存模式。


4、有关记忆游标的更多信息

IOMemoryCursor 在物理内存中布置 IOMemoryDescriptor 对象中的缓冲区范围。

通过正确初始化内存游标,然后在 IOMemoryDescriptor 上调用该对象的getPhysicalSegments 函数,驱动程序可以构建适合特定设备或 DMA 引擎的分散/聚集列表。

分散/聚集列表的生成可以满足硬件施加的段长度、传输长度、字节序格式和对齐的要求。

A总线(例如 USB、ATA 或 FireWire)的控制器驱动程序通常是使用 IOMemoryCursors 的对象。

此类驱动程序应创建 IOMemoryCursor 并将内存光标配置为驱动程序的 DMA 硬件的限制或(如果使用的是 PIO)设备本身的限制。

例如,用于 FireWire SBP-2 协议的内存光标应配置为最大物理段大小为 65535 和无限制传输大小。

您可以通过多种方式配置 IOMemoryCursor。

最明显的方法是提供初始化参数:最大段大小、最大传输长度和指向段函数的指针。

此回调函数类型为SegmentFunction,将单个物理段写入定义分散/聚集列表的向量数组中的元素。

您的驱动程序还可以对提取的段执行后处理、交换字节或以其他方式操作段的内容。

最后,您可以创建 IOMemoryCursor 的子类或使用 Apple 提供的子类之一。

有关此主题的更多信息,请参阅 IOMemoryCursor 子类


DMA 和系统内存

作家总线控制器驱动程序有两个基本考虑当它们影响 I/O 传输时。

它们接收以特定方式布局在内存中的数据,并且必须将这些数据发送到可能期望完全不同的内存布局的目的地。

根据方向,源和目的地可以是 DMA引擎(或特定设备)或 IOMemoryDescriptor 所表示的客户端内存。

此外,来自系统的内存可能受统一缓冲区缓存 (UBC) 的制约,这对驱动程序编写者有影响。

本节讨论其中一些因素。

直接内存访问 (DMA) 是某些总线控制器的内置功能,用于在连接到总线的设备和系统内存(即计算机主板上的物理内存)之间直接传输数据。

DMA 使微处理器无需自己传输数据,从而提高了系统性能。

微处理器可以执行其他任务,而 DMA 引擎负责将数据移入和移出系统。

OS X 系统上的每条总线都有自己的 DMA 引擎,并且它们都不同。

OS X 上的 PCI 总线控制器使用总线主控 DMA,这是一种 DMA,其中控制器代表微处理器控制所有 I/O 操作。

其他总线控制器(例如 ATA、SCSI 或 USB)实现不同的 DMA 引擎。

每个引擎可以有自己的对齐、字节序格式和大小限制。

DMA 的替代方案是程序化输入/输出 (PIO) 接口,通常用于较旧或设计欠佳的硬件。

在 PIO 中,设备和系统内存之间传输的所有数据都经过微处理器。

PIO 比 DMA 慢,因为它需要消耗更多总线周期才能完成相同的数据传输。

OS X 支持总线主控 DMA 和 PIO 来将数据移入和移出系统。

事实上,一些驱动程序可以想象使用这两种模型进行相同的 I/O 传输 - 例如,使用 DMA 处理大多数字节,然后使用 PIO 处理最后几个字节,或者使用 PIO 来处理错误情况。

这统一缓冲区缓存 (UBC) 是一种内核优化,它结合了文件系统缓存和虚拟内存 (VM) 缓存。

UBC 消除了同一页在两个缓存中重复的情况。

相反,内存中只有一个图像,文件系统和 VM 系统中都有指向它的指针。

UBC 的底层结构是通用页面列表 (UPL)。

VM 分页器处理由 UPL 定义的内存;当 I/O 请求基于分页内存时,它被称为符合要求的请求。

不符合要求的请求是未由 UPL 定义的请求。

UPL 内存段具有某些特征;它:

  • 页面大小(4 KB)
  • 页面是否对齐
  • 最大段大小为 128 KB
  • 已经映射到内核的虚拟地址空间

I/O Kit 已将其 API 调整为 UPL 模型,因为它更适合 I/O 传输;此外,它使批量处理 I/O 请求变得更加容易。

IOMemoryDescriptor 对象可能完全或部分由 UPL 定义的内存支持。

如果对象由 UPL 支持,则物理段的数量不能超过预定数量。

提取段(使用 IOMemoryCursor)的总线控制器驱动程序必须分配足够的资源来发出与内存描述符关联的 I/O 请求。


处理硬件限制

Apple 关于 driver 应如何处理硬件约束对于 DMA 控制器驱动程序的客户端来说很慷慨,并且对驱动程序本身也抱有一定的期望。

  • DMA 控制器驱动程序的客户端不受对齐限制。
    通常,UPL 定义的数据(页面对齐和页面大小)应该是最佳的,但这不是必需的。
  • 如果内存是 UPL 定义的,那么为了避免死锁,驱动程序不应分配内存。
    控制器驱动程序必须能够在不分配任何缓冲区的情况下处理 UPL,或者必须在任何特定的基于 UPL 的 I/O 传输之前预先分配足够的资源。
    不符合分页器约束(即不基于 UPL)的操作可以分配缓冲区。
  • 控制器驱动程序必须尽力满足收到的任何请求。
    如有必要,它甚至应该创建缓冲区(甚至使用静态缓冲区),或预分配资源并复制数据。
    (请记住,在 I/O 请求期间分配资源可能会导致死锁。
    如果驱动程序无法执行 I/O 请求,它应该返回适当的错误结果代码。

总之,驱动程序编写者必须准备好处理任何对齐、大小或其他限制的请求。

他们首先应尝试按照 UPL 规范处理请求;如果 UPL 假设成立,他们就绝不应该分配内存,因为这样做可能会导致死锁。

如果请求不符合要求,驱动程序应该(并且可以)尽一切努力满足请求,包括分配资源。


IOMemoryCursor 子类

Apple 针对不同情况提供了 IOMemoryCursor 的几个子类。

如果你的DMA 引擎需要其物理段采用特定的字节序数据格式,您的驱动程序可以使用处理大端和小端数据格式的子类(因此在为 DMA 引擎构建分散/集中列表时不必执行此转换)。

另一个子类使您的驱动程序能够按照系统处理器所需的字节方向布局数据。
表 8-2描述了这些子类。

子类 描述
IONaturalMemoryCursor 提取并按照给定 CPU 的自然字节顺序排列物理段的分散/聚集列表。
IOBigMemoryCursor 提取并布置以大端字节格式编码的物理段的分散/聚集列表。
当 DMA 引擎需要每个段的大端地址和长度时,使用此内存光标。
IO小内存光标 提取并布置以小端字节格式编码的物理段的分散/聚集列表。
当 DMA 引擎需要每个段的小端地址和长度时,使用此内存光标。

当然,您可以创建虚拟 IOMemoryCursor 的子类或其子类之一,以使内存游标准确完成您需要它执行的操作。

但在许多情况下,您可能不必创建子类即可获得所需的行为。

使用提供的内存游标类之一,您可以实现自己的outputSegment回调函数(必须符合SegmentFunction原型)。

内存游标调用此函数以在为 DMA 引擎准备的分散/聚集列表中写出物理段。

在实现此函数时,您可以满足硬件所需的任何特殊布局,例如对齐边界。


十、管理电源

I/O Kit 的电源管理功能旨在最大限度地减少计算机系统的功耗,这对于电池寿命至关重要的便携式计算机尤其重要。

当系统(或系统的一部分)处于睡眠或唤醒状态时,电源管理还会强制执行有序的操作序列,例如保存和恢复状态。

本章重点介绍管理硬件的内核驱动程序的电源管理。

阅读本章可了解 OS X 中的电源管理,并了解您需要提供什么级别的电源管理支持以及如何实现它。

虽然电源管理是一项复杂的技术,但大多数内核驱动程序只需实现最基本的功能即可成功参与 OS X 电源管理。

注意: 如果您正在开发访问硬件的应用程序(例如数码相机、扫描仪、网络摄像头或磁带驱动器的用户空间驱动程序),则可能不需要执行任何电源管理任务。

有关开发充当用户空间驱动程序的应用程序的更多信息,包括有关如何设置应用程序以接收电源事件通知的信息,请参阅 从应用程序访问硬件

你的驱动程序必须履行的电源管理职责的精确集合取决于多种因素,如你的驱动程序的超类提供多少支持、你的设备是否从系统总线(如 PCI)接收电源,以及你的驱动程序需要响应哪些电源事件。

如果您不熟悉 OS X 中的电源管理,您应该首先阅读以下三个部分:

然后,所有驱动程序开发人员都应该阅读 决定如何在驱动程序中实现电源管理,以了解下一步该做什么。

决定需要实现哪种类型的电源管理后,请阅读 实现基本电源管理,如果合适,请阅读实现高级电源管理


1、电力活动

在考虑如何在驱动程序中实现电源管理之前,您需要了解什么是电源事件以及它们如何影响您的设备。

在 OS X 中,电源事件是以下状态之间的转换:

  • 睡觉
  • 唤醒
  • 关机或重启

所有驱动程序都必须响应睡眠事件。

OS X 定义了不同类型的睡眠,这些睡眠可能由于不同的原因而发生。

例如,当用户从 Apple 菜单中选择"睡眠"或关闭笔记本电脑的盖子时,系统睡眠 就会发生;

当用户在"节能器"偏好设置中选择的时间间隔内没有设备或系统活动时,空闲睡眠就会发生。

但是,对于您的驱动程序来说,所有睡眠事件看起来都是一样的。

了解睡眠事件的重要一点是,当系统睡眠时,您的设备可能会关闭,因此您的驱动程序必须准备好在设备被唤醒时初始化设备。

所有驱动程序都必须通过开机来响应系统唤醒事件。

当用户按下键盘上的某个键、按下电源按钮或计算机收到网络管理员唤醒数据包时,都可能发生唤醒。

唤醒后,驱动程序应执行适当的设备状态恢复。

设备驱动程序不必响应关机和重启事件。

驱动程序可以选择使用接收关机和重启通知中描述的技术来获取即将关机或重启的通知,但重要的是要了解没有驱动程序可以阻止关机事件。

另一种类型的事件是设备开机请求,当系统中的某个对象需要空闲或关闭的设备处于可用状态时,就会发生这种情况。

设备开机请求通知使用与睡眠和唤醒通知相同的大部分机制。

虽然大多数驱动程序不需要知道设备开机请求,但某些驱动程序可能需要实现它们,甚至自己发出此类请求。

有关此内容的更多信息,请参阅 启动电源状态更改


2、电源平面:电源依赖关系的层次结构

OS X 在一个树状结构中跟踪所有受电源管理的设备,称为电源平面,用于捕获设备之间的电源依赖关系。

设备(通常是电源平面中的叶对象)通常从其祖先接收电源,并可能为其子级提供电源。

例如,由于 PCI 卡的电源依赖于其所连接的 PCI 总线,因此 PCI 卡被视为PCI 总线的电源子级

同样,PCI 总线被视为连接到它的设备的**电源父级。

**

电源平面是 I/O 注册表的平面之一。

I/O 注册表 中所述,I/O 注册表是设备和驱动程序对象的动态数据库,用于表达它们之间的各种提供者-客户端关系。

要在正在运行的系统中查看电源平面,请打开 I/O 注册表资源管理器应用程序(位于/Developer/Applications/Utilities),然后从弹出菜单中选择 IOPower。

您还可以输入ioreg -p IOPower在命令行上查看当前电源平面的表示。

图 9-1显示了运行 OS X v10.5 的 Power Mac G5 中的电源平面。


图 9-1 I/O Registry Explorer 中显示的电源平面


在图 9-1中,您可以看到电源平面的根、名为IOPMrootDomain 的对象以及代表设备和驱动程序的对象。

您可以忽略许多代表电源连接的IOPowerConnection对象,因为这些对象仅对内部电源管理对象和过程有用。


3、设备和电源状态

电源管理中的基本实体是设备。

从电源管理的角度来看,设备是一个硬件单元,其功耗可以独立于系统电源进行测量和控制。

设备还可以具有一些需要在电源变化时保存和恢复的状态。

在电源管理术语中,"设备"与控制它的设备驱动程序对象同义。

设备必须至少具有两种与其关联的电源状态 - 关闭和打开。

设备还可以具有中间状态,表示全功率和无功率之间的某种功率水平。

这些状态在您在驱动程序中创建的电源状态数组中描述。

(您将在 实施基本电源管理的步骤 3 中学习如何创建此数组并提供电源状态信息)I/O Kit 的电源管理功能使用这些状态来确保电源平面中的所有驱动程序都能获得所需的功率。

每个电源状态由设备在该状态下的功能定义:

  • 处于开启状态的设备会使用最大功率并且具有完整的功能。
  • 关闭的设备不耗电且没有任何功能。
  • 设备可以处于低功耗状态,此时设备仍然可用,但性能或功能水平较低。
  • 设备可以处于不可用的中间状态,但保留一些配置或状态。

I/O Kit 的电源管理功能将多个属性与设备的每种电源状态相关联。

设备驱动程序必须设置这些属性,以确保提供有关设备功能和要求的准确信息。

电源状态属性提供以下信息:

  • 设备在特定状态下的能力
  • 设备对其电源父级的电源要求
  • 设备可以为其子电源提供的电源特性
  • 设备用于存储其电源状态信息的电源状态结构的版本

4、确定如何在驱动程序中实现电源管理

要参与 OS X 电源管理,大多数内核驱动程序只需确保其设备能够正确响应系统睡眠和唤醒事件。

一些内核驱动程序可能需要执行其他任务,例如实现空闲状态或在系统关闭时采取行动,但这些驱动程序并不常见。

为了反映这种区别,OS X 电源管理定义了两种类型的驱动程序:

  • 被动驱动程序实现基本的电源管理来响应系统电源事件;它不会为其设备启动任何与电源相关的操作。
  • 活动驱动程序实现基本电源管理以响应系统电源事件,但它还实现高级电源管理以执行诸如决定设备何时应变为空闲状态、更改设备的电源状态或在系统关闭之前进行处理等任务**。
    **

被动驱动程序的一个例子是大多数 Macintosh 笔记本电脑中都有的 AppleSmartBatteryManager 驱动程序。

AppleSmartBatteryManager 驱动程序向电池状态菜单栏项提供电池状态信息;当系统即将进入睡眠状态时,驱动程序会停止轮询电池以获取状态信息。

主动驱动程序的一个很好的例子是内置音频芯片驱动程序,因为它会自行执行空闲确定,以允许音频硬件在不使用时关闭电源。

如果笔记本电脑或台式机的内置扬声器没有发出声音,音频硬件将进入低功耗模式,直到需要它为止。

可以想象,被动驱动程序的设计和实现比主动驱动程序容易得多。

从本质上讲,被动驱动程序实现一个虚拟方法,并进行三到五次调用来参与电源管理。

另一方面,主动驱动程序的职责始于被动驱动程序的职责,但随着驱动程序需要执行的每个额外任务而增加。

一些 I/O Kit 系列为驱动程序子类提供各种级别的内置电源管理支持。

例如,网络系列 ( IONetworkingFamily) 为子类驱动程序执行一些电源管理初始化任务,而让驱动程序执行其他设备特定的电源管理任务。

在开始设计驱动程序的电源管理实现之前,您应该在 I/O Kit 系列参考 中查找您的 I/O Kit 系列,以了解该系列是否提供任何电源管理支持或需要子类来执行不同或额外的任务。

但请注意,任何提供电源管理功能的 I/O Kit 系列可能仍需要您实现其中的某些部分。

以下 I/O Kit 系列提供某种类型的电源管理功能:

  • 音频系列(在 音频 中描述)
  • FireWire 系列(在 FireWire 中描述)
  • 网络系列(在 网络 中描述)
  • PC 卡系列,包括 Express Card 设备(在 PC 卡 中描述)
  • PCI 系列(在 PCI 和 AGP 中描述)
  • SCSI 架构模型系列(在 SCSI 架构模型 中描述)
  • USB 系列(在 USB 中描述)

即使您的驱动程序是 I/O Kit 系列的子类,且不提供任何电源管理支持,或者您的驱动程序是IOService 的直接子类,只要它仅响应系统发起的电源事件,它仍然可以是被动电源管理参与者。

另一方面,如果您的驱动程序需要确定设备何时处于空闲状态或执行关机前任务,则必须实现高级电源管理。

如果您决定开发被动驱动程序,您应该阅读"实现基本电源管理"以了解如何参与电源管理并响应睡眠和唤醒事件。

您不需要阅读本章中的任何其他部分。

如果您的驱动程序需要成为一个主动电源管理器,您还应该阅读"实现基本电源管理",然后您应该阅读 "实现高级电源管理"以获得有关实现特定任务的指导。


5、实现基本电源管理

根据决定如何在驱动程序中实现电源管理中的定义,被动驱动程序仅响应睡眠和唤醒事件;它不会启动任何电源状态更改活动。

被动驱动程序必须执行以下操作来处理睡眠和唤醒:

  • 连接到电源平面,这样您就可以接收电源变化通知,并确保在设备被告知睡眠和唤醒时考虑到设备的电源依赖关系。
    电源依赖性会影响睡眠和唤醒通知的顺序。
    具体来说,您的驱动程序在电源父级被告知睡眠之前被告知睡眠,而您的驱动程序在其电源父级被告知唤醒之后被告知唤醒。

注意: 一个设备可以有多个电源父级,但请务必了解,在这种情况下,无法保证电源更改的特定顺序。

具体来说,您的设备将在第一个电源父级唤醒后被唤醒,而不是在所有电源父级唤醒后被唤醒。

  • 在系统睡眠之前将硬件状态保存到内存,并在唤醒期间恢复状态。
    您负责编写代码来执行此操作。
  • 当您的设备准备睡眠时,阻止所有硬件访问。
    您可以向设备进入睡眠状态时收到的任何 I/O 请求返回错误,也可以使用门控机制(如IOCommandGate工作循环中的)阻止所有传入线程(有关工作循环的更多信息,请参阅工作循环)

要参与电源管理以便接收电源事件通知、确保您的驱动程序正确连接到电源平面并处理电源状态更改,您需要进行一些调用并实现一个虚拟方法。
IOService类提供了本节中描述的所有方法。

按照下面列出的步骤在您的驱动程序中实现基本的电源管理。

  1. 使用 PMinit 初始化电源管理。
    ()方法分配内部电源管理数据结构,允许内部进程跟踪您的驱动程序。
    在驱动程序的start例程中,调用超类的start方法后,进行以下调用:
c 复制代码
PMinit();
  1. 使用 joinPMtree 连接到电源平面。
    (IOService*)方法将传入的驱动程序对象作为其提供程序的子对象连接到电源平面。
    在驱动程序start例程中,在调用PMinit之后和调用registerPowerDriver之前(如步骤 3 所示),按如下所示进行调用joinPMtree
c 复制代码
provider->joinPMtree(this);
  1. 提供有关设备电源状态的信息并使用电源管理注册您的驱动程序。
    a. 首先,声明一个包含两个结构的数组,用于包含有关设备关闭和打开状态的信息。
    数组中的第一个元素必须包含描述关闭状态的结构,数组的第二个元素必须包含描述打开状态的结构。
    通常,驱动程序会响应睡眠事件将其设备切换到关闭状态,响应唤醒事件将其设备切换到打开状态,如 电源事件中所述
    在您的驱动的start程序例程中,调用joinPMtree之后,填写两个IOPMPowerState结构,如下所示:
c 复制代码
// Declare an array of two IOPMPowerState structures (kMyNumberOfStates = 2).
static IOPMPowerState myPowerStates[kMyNumberOfStates];
// Zero-fill the structures.
bzero (myPowerStates, sizeof(myPowerStates));
// Fill in the information about your device's off state:
myPowerStates[0].version = 1;
myPowerStates[0].capabilityFlags = kIOPMPowerOff;
myPowerStates[0].outputPowerCharacter = kIOPMPowerOff;
myPowerStates[0].inputPowerRequirement = kIOPMPowerOff;
// Fill in the information about your device's on state:
myPowerStates[1].version = 1;
myPowerStates[1].capabilityFlags = kIOPMPowerOn;
myPowerStates[1].outputPowerCharacter = kIOPMPowerOn;
myPowerStates[1].inputPowerRequirement = kIOPMPowerOn;

在某些驱动程序中,您可能会看到此步骤在类似于以下代码的代码中实现:

c 复制代码
static IOPMPowerState myPowerStates[kMyNumberOfStates] = {
   {1, kIOPMPowerOff, kIOPMPowerOff, kIOPMPowerOff, 0, 0, 0, 0, 0, 0, 0, 0},
   {1, kIOPMPowerOn, kIOPMPowerOn, kIOPMPowerOn, 0, 0, 0, 0, 0, 0, 0, 0}
};

  1. 然后,仍然在驱动程序start例程中,使用 (IOService*,IOPMPowerState*,unsignedlong) 向电源管理 注册驱动程序。
    registerPowerDriver方法告诉电源管理,传入的驱动程序对象可以在传入数组中描述的电源状态之间转换设备。
    填写IOPMPowerState结构后,使用您的电源状态数组调用registerPowerDriver,如下所示:
c 复制代码
registerPowerDriver (this, myPowerStates, kMyNumberOfStates);
  1. 使用 setPowerState 处理电源状态更改。
    在驱动程序运行时,您可以在虚拟IOServicesetPowerState方法的实现中,执行处理睡眠和唤醒事件通知的任务。
    下面显示了如何执行此操作的示例:
c 复制代码
IOReturn MyIOServiceDriver::setPowerState ( unsigned long whichState, IOService * whatDevice )
// Note that it is safe to ignore the whatDevice parameter.
{
   if ( 0 == whichState ) {
      // Going to sleep. Perform state-saving tasks here.
   } else {
      // Waking up. Perform device initialization here.
   }
   if ( done )
      return kIOPMAckImplied;
   else
      return (/* a number of microseconds that represents the maximum time required to prepare for the state change */);
}

如果您返回kIOPMAckImplied,则表示您已完成向新电源状态的转换。

如果您不返回kIOPMAckImplied,而是返回设备为电源状态更改做好准备所需的最长时间,则必须确保在完成电源状态转换后调用acknowledgeSetPowerState

如果您在指定的时间长度过去之前没有调用acknowledgeSetPowerState,系统将继续其电源状态更改,就像您一开始就返回kIOPMAckImplied一样。


注意: 如果您正在开发适用于 OS X v10.5 或更高版本的驱动程序,您可以在返回kIOPMAckImplied之前执行所有必要的处理,为setPowerState方法中的状态更改做好准备。

换句话说,您不必返回处理需要多长时间的估计值,在另一个方法中执行处理,然后在处理完成时调用acknowledgeSetPowerState


  1. 当您的驱动程序使用 PMstop 卸载时,从电源管理中取消注册。
    PMstop方法处理所有必要的清理工作,包括从电源平面移除驱动程序。
    由于PMstop可能会使您的硬件进入关闭状态,因此请确保在调用它之前完成所有硬件访问。

重要提示: 此步骤至关重要。

如果您忽略了调用PMstop,则可能会造成泄漏,并且可能会导致计算机下次唤醒时系统崩溃。

在您的驱动程序stop例程中,完成所有可能访问硬件的调用后,请按如下所示进行调用PMstop

c 复制代码
PMstop();

6、实施高级电源管理

本节深入探讨了 I/O Kit 的电源管理功能。

绝大多数驱动程序开发人员不需要了解本节中的信息,因为基本电源管理(如决定如何在驱动程序中实现电源管理中所述)对于大多数设备来说已经足够了。

如果您的设备可以进行被动电源管理,请改为阅读 实现基本电源管理

如果您的驱动程序需要执行高级电源管理任务,例如确定设备空闲状态、在系统即将关闭时采取行动或决定更改设备的电源状态,则应该阅读本节。

当然,主动驱动程序与被动驱动程序共享一些任务,即电源管理的初始化和拆卸。

因此,在阅读本节中的任务之前,您应该先浏览一下"实现基本电源管理" 中的步骤,以了解如何在驱动程序中初始化和终止电源管理。

即使您的驱动程序必须执行高级电源管理任务,它仍然需要调用PMinitjoinPMtreeregisterPowerDriver、和 PMstop,并实现setPowerState,如"实现基本电源管理"中所示

本节介绍活动驱动程序可能需要执行的几项任务。

虽然很少有活动驱动程序会执行所有任务,但大多数会执行至少一项任务。

每个任务都附带一个代码片段,以帮助您在驱动程序中实现它。


定义和使用多个电源状态

设备和电源状态 中所述,有关设备电源状态和功能的信息必须提供给 I/O Kit 电源管理。

虽然大多数设备只有两种必需的电源状态,即关闭和打开,但有些设备还有其他状态。

如实现基本电源管理的步骤 3 所示,您构建了一个IOPMPowerState结构数组,每个结构都包含有关设备在每个电源状态下的功能的信息。

表 9-1描述了 IOPMPowerState 结构中的字段,该结构在头文件IOPM.h中定义。

场地 描述 价值
version 该结构的版本号。 1
capabilityFlags 设备在此状态下的能力。 一面IOPMPowerFlags旗帜。
outputPowerCharacter 此状态下供给的电源。 一面IOPMPowerFlags旗帜。
inputPowerRequirement 此状态下所需的输入功率。 一面IOPMPowerFlags旗帜。
staticPower 设备在此状态下的平均功耗(以毫瓦为单位)。 0
unbudgetedPower 来自单独电源(例如电池)的额外功耗(以毫瓦为单位)。 0
powerToAttain 设备从下一个最低状态进入此状态时所消耗的电量(以毫瓦为单位)。 0
timeToAttain 设备从下一个较低状态进入此状态所需的时间(以微秒为单位);换句话说,就是对硬件进行编程所需的时间。 0
settleUpTime 从下一个较低状态进入此状态后,允许电源稳定下来所需的时间(以微秒为单位)。 0
timeToLower 设备从该状态进入下一个较低状态所需的时间(以微秒为单位);换句话说,就是对硬件进行编程所需的时间。 0
settleDownTime 从该状态进入下一个较低状态后,使电源稳定下来所需的时间(以微秒为单位)。 0
powerDomainBudget 处于此状态的电源父级能够以电子方式传递给其子级的功率(以毫瓦为单位)。 0

如表 9-1所示,某些字段的值可能由IOPMPowerFlags标志提供。

表 9-2显示了您可能使用的IOPMPowerFlags标志。

旗帜 描述
kIOPMPowerOn 设备处于满功率状态。
kIOPMDeviceUsable 设备的客户端可以在这种状态下使用它。
kIOPMMaxPerformance 在此状态下,设备能够发挥其最佳性能。
kIOPMAuxPowerOn PCI 辅助电源已打开(仅供 PCI 系列设备使用)。

电源管理对于您在驱动程序start方法中构建的IOPMPowerState结构数组有以下要求:

  • 描述设备关闭状态的结构IOPMPowerState必须是数组中的第一个元素。
  • IOPMPowerState描述设备开启(即全功率)状态的结构必须是数组中的最后一个元素。
  • 您可以定义任意数量的中间电源状态,但描述它们的IOPMPowerState结构 不能是数组的第一个或最后一个元素。

根据这些规范构建电源状态数组后,调用registerPowerDriver,传递指向数组的指针和电源状态的数量。

例 9-1展示了实现此目的的一种方法。

它还展示了驱动程序如何创建工作循环并设置命令门来同步电源状态更改代码,这在 更改设备的电源状态中进行了描述


例 9-1 构建电源状态数组并注册驱动程序

c 复制代码
enum {
    kMyOffPowerState  = 0,
    kMyIdlePowerState   = 1,
    kMyOnPowerState = 2
};
 
static IOPMPowerState myPowerStates[3] = {
    {1, kMyOffPowerState, kMyOffPowerState, kMyOffPowerState, 0, 0, 0, 0, 0, 0, 0, 0},
    {1,kIOPMPowerOn, kIOPMPowerOn, kIOPMPowerOn, 0, 0, 0, 0, 0, 0, 0, 0},
    {1,kIOPMPowerOn, kIOPMPowerOn, kIOPMPowerOn, 0, 0, 0, 0, 0, 0, 0, 0}
};
bool PMExampleDriver::start(IOService * provider)
{
   /*
    * Create a work loop and set up synchronization
    * using a command gate.
    */
    fWorkloop = IOWorkLoop::workLoop();
    fGate = IOCommandGate::commandGate(this);
 
    if (fGate && fWorkloop) {
        fWorkloop->addEventSource(fGate);
    }
 
     * Initialize power management, join the power plane,
     * and register with power management.
     */
    PMinit();
    provider->joinPMtree(this);
    registerPowerDriver(this, myPowerStates, 3);
 
}

更改设备的电源状态

驱动程序负责更改其设备的电源状态。

大多数电源状态更改请求来自系统即将进入睡眠或唤醒状态时的电源管理。

活动驱动程序也有可能意识到需要更改其设备的电源状态并发起请求。

以下部分介绍了这两项任务。


响应电源状态更改请求

与被动驱动程序一样,主动驱动程序必须重写setPowerState方法并在收到指示时更改其设备的电源状态。

传入setPowerState的序数值 是设备电源状态数组的索引。

如果您正在开发一个驱动程序以在 v10.5 之前的 OS X 版本中运行,则您必须在setPowerState方法中仅执行更改设备电源状态所需的最少处理。

任何其他处理都必须在 setPowerState 方法之外执行,并在完成后调用 acknowledgeSetPowerState

这在 实施基本电源管理的第 4 步中进行了描述

另一方面,如果您的驱动程序将在 OS X v10.5 及更高版本中运行,您可以在返回kIOPMAckImplied之前 在 setPowerState 方法中执行所有必要的处理。

但是,重要的是要了解电源管理从线程调用上下文调用setPowerState方法。

换句话说,电源管理不会使用驱动程序的工作循环执行任何自动同步。

因此,您必须继续使用命令门或其他锁定原语来确保对设备状态的访问是序列化的。

一旦您的驱动程序在完成额外处理后返回kIOPMAckImplied 或 调用acknowledgeSetPowerState,电源管理就会将电源更改标记为已完成。

因此,对于所有驱动程序(无论它们针对哪个版本的 OS X)来说,重要的是避免在设备的电源状态实际发生变化之前将电源更改报告为已完成。

其他电源更改可能取决于您的硬件在您调用之前是否已完成其电源更改acknowledgeSetPowerState


启动电源状态更改

活动驱动程序可能会意识到需要更改其设备的电源状态,无论是通过其自身的机制还是通过其他对象。
IOService类提供了三种方法来帮助完成此任务:

  • makeUsable
  • changePowerStateTo
  • changePowerStateToPriv

驱动程序堆栈中的任何对象(包括用户客户端(在 设备接口机制 中描述))都可以通过调用设备驱动程序上的makeUsable方法来请求将休眠设备激活。
makeUsable方法被解释为将设备置于最高功率状态的请求。

活动驱动程序通常会在其start方法中调用changePowerStateTo方法一次,以设置初始电源状态。

稍后,当它想要更改其设备的电源状态时,活动驱动程序会调用changePowerStateToPriv方法,并传入所需的电源状态。

活动驱动程序可能会这样做来关闭当前未使用的硬件部分。


重要提示: 尽管makeUsablechangePowerStateTochangePowerStateToPriv方法是异步的并且会立即返回,但您必须阻止所有硬件访问,直到收到对您的setPowerState方法的调用。

在调用您的setPowerState方法之前,您无法确定您的设备是否处于可用状态。

因此,应在工作循环上下文中调用这些函数。

电源管理使用传入changePowerStateTochangePowerStateToPriv的状态来确定设备的新电源状态。

具体来说,电源管理选择以下三个值中的最高值作为新电源状态:

  • 电源状态由changePowerStateToPriv设置
  • 电源状态由changePowerStateTo设置
  • 驾驶员电源子级 所需的所有电源状态中的最高状态

以下代码片段显示了驱动程序如何获取设备的当前电源状态(使用OS X v10.5 中引入的getPowerState方法),然后使用 changePowerStateToPriv 请求电源状态更改。

c 复制代码
enum {
    kMyOffState = 0,
    kMyOnState = 1
};
/*
 * Make sure the hardware is in the ON state
 * before accessing it. If it's powered off, call changePowerStateToPriv
 * to put the device in the ON state.
 */
if (getPowerState() == kMyOnState)
{
    /* Device is ON. OK to access hardware. */
} else {
    changePowerStateToPriv( kMyOnState );
    /*
     * Note: If your device has been powered off for a system sleep, you cannot
     * try to adjust your power state upwards. You are locked in your OFF or
     * low-power state until system power is restored on wake.
     */
 
    /*
     * Although changePowerStateToPriv returns immediately,
     * it is _NOT_ safe to touch the hardware yet. You must wait until you
     * receive your setPowerState() call before you can safely modify
     * the hardware.
     */
}

实现空闲状态判定和空闲状态节能

当设备处于空闲状态时,可以关闭设备以节省系统电量,这对于使用电池供电的笔记本电脑尤其重要。

如果出现以下情况,您应该在设备中实现空闲省电功能:

  • 对您的设备的访问是间歇性的,并且设备经常一次几分钟、几小时或几天不使用。
  • 您的设备会消耗大量电量,尽可能将其置于低功耗状态可节省大量电量。

要实现空闲省电,您必须确定设备何时空闲,并指定空闲时间应持续多长时间才能关闭设备。

您可以通过向IOService超类提供设备访问信息来确定空闲时间,超类会使用此信息以及您指定的空闲时间,告诉您的设备在适当的时间关闭电源。
IOService 类提供两种方法积极的驱动程序用来执行此操作:

  • activityTickle
    在设备的访问路径上,activityTickle每次驱动程序或任何其他客户端(包括应用程序)触发硬件访问时,您都会调用。
    这允许电源管理确认您的设备处于可用状态并跟踪设备的最近访问时间。
  • setIdleTimerPeriod
    您可以调用setIdleTimerPeriod来指定看门狗定时器的持续时间,该定时器跟踪设备在应关闭电源之前可以全功率闲置多长时间。
    通过设置空闲时间的持续时间,您可以有效地启动在每次设备访问后开始的倒计时。

当空闲时间结束时设备没有任何活动,电源管理将调用setPowerState方法的实现来降低设备的电源状态。

有关如何实现此方法的更多信息,请参阅 更改设备的电源状态。

当然,您必须首先将设备设置为全功率状态,才能响应设备关闭时收到的任何设备访问请求。

由于您在设备的访问路径调用了activityTickle,因此电源管理会立即收到警报,告知某个实体正在请求访问当前已关闭的设备。

发生这种情况时,IOService超类会在您的设备自动调用makeUsable,这最终导致调用setPowerState方法的实现。

重要提示:启动电源状态更改 中所述,您必须阻止所有硬件访问,直到setPowerState调用您的实现,因为在此之前您无法确定您的设备是否处于可用状态。

最好的方法是使用工作循环来序列化硬件访问。

以下步骤概述了闲置确定的过程:

  1. 指定设备在空闲时应保持高功率状态的时间。
    通常,一分钟是合适的间隔。
    调用setIdleTimerPeriod,传入空闲间隔(以秒为单位),如下所示:
c 复制代码
setIdleTimerPeriod ( 60 );
  1. 每次实体(包括您的驱动程序)启动设备访问时通知电源管理。
    在驱动程序的设备访问路径中,调用activityTickle,如下所示:
c 复制代码
activityTickle ( kIOPMSuperclassPolicy1, myDevicePowerOn );

如上所示,第一个参数activityTicklekIOPMSuperclassPolicy1,表示IOService超类将跟踪设备活动并在空闲时间到期时采取行动。

第二个参数指定此活动所需的电源状态,通常是开启状态。

  1. 当空闲计时器到期时,IOService超类会检查自上次空闲计时器到期以来是否有任何设备活动。

超类通过检查上次调用activityTickle( kIOPMSuperclassPolicy1) 的时间来确定这一点。

  1. 如果自上次定时器到期以来设备一直有活动,则IOService超类将重新启动定时器。

如果没有发生设备活动,则IOService超类 将在您的驱动程序调用 setPowerState将设备断电至下一个最低状态。


可选: 驱动程序可以通过重写IOServicenextIdleTimeout方法 来实现可变的空闲超时行为。

为此,您的nextIdleTimeout实现应该返回设备应在多少"秒后"进入其下一个最低功耗状态。

例如,Graphics 系列调用 nextIdleTimeout来动态调整显示器的空闲睡眠超时。

如果用户在显示器变暗后 很快移动鼠标,则显示驱动程序会记住此情况并增加超时时间,从而有效地等待更长的时间,然后再启动接下来的几个显示器变暗事件。

在您的设备通过此过程关闭到较低状态后,新的activityTickle调用使电源管理将设备的电源提升到活动所需的级别。

如果设备已经处于正确状态,则超类只需从对 activityTickle ( kIOPMSuperclassPolicy1)的调用返回true即可;否则,超类将返回false并继续使设备可用。

尽管 activityTickle的返回值指示设备是否处于可用电源状态,但最好在驱动程序中跟踪设备的当前电源状态,而不是依赖activityTickle返回值来获取此信息。

这是因为activityTickle不会在电源管理工作循环中调用,并且设备的电源状态可能会在activityTickle返回之前发生变化。


接收其他设备电源状态变化的通知

在某些情况下,当另一个驱动程序更改其设备的电源状态时,可能需要通知您的驱动程序。

I/O Kit 电源管理使用一对通知来括住设备电源状态的每次更改。

这些通知通过调用IOService虚拟方法powerStateWillChangeTopowerStateDidChangeTo来传递。

您可以实现这些方法来接收通知并为更改做好准备。

只要满足以下条件,你的驱动程序就可以向另一个驱动程序注册其兴趣:

  • 您的驱动程序感兴趣的驱动程序必须连接到电源平面。
  • 您的驱动程序必须是 IOService 的 C++ 子类,但它不必附加到电源平面本身。

要了解另一个驱动程序何时更改其设备的电源状态,请在驱动程序中执行以下步骤:

  1. 调用IOServiceregisterInterestedDriver方法。
    这可确保电源管理在发出电源变化通知时通知您的驱动程序。
  2. 实现虚拟IOServicepowerStateWillChangeTo方法。
    当设备驱动程序即将改变设备的电源状态时,将调用此方法。
    如果您的驱动程序已准备好进行更改,它应该返回kIOPMAckImplied;如果它需要更多时间准备,它应该返回所需时间的上限(以微秒为单位)。
    如果你的驱动程序返回一个代表所需最大准备时间的数字,它应该调用acknowledgePowerChange方法当它准备好时,它会通知另一个驱动程序。
    如果它没有这样做,并且请求的准备时间已经过去,则另一个驱动程序将继续运行,就好像您的驱动程序已经确认了更改一样。
    此行为可防止电源状态更改因驱动程序故障而停滞。
    重要提示: powerStateWillChangeTo方法不是执行与实际更改电源状态相关的任何任务的地方。
    此类任务应在方法中执行setPowerState,具体内容请参阅 响应电源状态更改请求
  3. 实现虚拟IOService方法powerStateDidChangeTo
    电源状态改变完成后,设备驱动程序将调用此方法。
    电源发生改变并且电源稳定到新水平后,电源管理会通过powerStateDidChangeTo方法将此事实广播给所有感兴趣的对象。
    如果设备将进入低功率状态,感兴趣的驱动程序通常不需要对此通知做太多处理。
    但是,如果设备将进入高功率状态,感兴趣的驱动程序将使用此通知为改变做好准备,例如,恢复状态或对设备进行编程。

powerStateDidChangeTo的实现中,驱动程序可以检查传入的位字段(如表 9-2IOPMPowerFlags中所述)以做出决定;

此位字段派生自电源状态数组的capabilityFlags字段,如表 9-1 中所述。

powerStateWillChangeTo 一样,如果驱动程序已准备好进行更改,则应返回kIOPMAckImplied

如果需要时间准备,则应返回所需的最大时间(以微秒为单位);当您的驱动程序最终准备好进行更改时,它应该调用 acknowledgePowerChange 方法。


重要提示: powerStateDidChangeTo方法不是执行与实际更改电源状态相关的任何任务的地方。

此类任务应在方法中执行setPowerState,具体内容请参阅 响应电源状态更改请求

当您的驱动程序 不再关注其他驱动程序的电源变化时,它应该注销自身以停止接收通知。

为此,请调用IOServicedeRegisterInterestedDriver方法,通常在驱动程序的stop方法中。


接收关机和重启通知

在针对 OS X v10.5 及更高版本的驱动程序中,您可以实现systemWillShutdown方法以接收即将关机或重启的通知。

但重要的是要明白,无论驱动程序收到什么通知,它都无法阻止关机。

您的驱动程序能够延迟关机,但强烈建议不要这样做,因为这会严重降低用户体验。

不要认为你的驱动程序应该实现systemWillShutdown,这样它就可以通过关闭硬件来响应关闭和重新启动通知。

关机时,无论设备当前状态如何,电源都即将从设备中移除。

同样,如果系统正在重启,您的设备将很快重新初始化,并且其当前状态同样不重要。

OS X 中的大多数内置设备驱动程序在系统即将关机或重启时不会关闭其设备,大多数第三方设备驱动程序也应该这样做。

尽管大多数设备驱动程序根本不需要以任何方式处理关机或重启,但驱动程序在关机或重启时运行有两个正当理由:

  • 该架构要求驱动程序在关机时执行代码。
    例如,在基于 Intel 的 Macintosh 中执行 DMA 的所有驱动程序都必须在关机完成之前停止活动 DMA。
  • 驱动程序必须在关机或重启时运行,以避免负面的用户体验。
    例如,音频驱动程序可能需要关闭其设备的放大器,以避免断电时发出可听见的"砰"声。

注意: 在关机路径上,您还可以在其他地方运行代码。

例如,如果您的软件有一个运行到关机的用户空间守护进程,则该守护进程可以捕获SIGTERM内核在关机时发送给所有进程的 。

一般来说,请尝试在关机路径中尽早运行关机代码。

systemWillShutdown方法按从叶到根的顺序在电源平面的所有成员上调用。

只有在其所有电源子级都完成其关闭任务后,才会调用驱动程序的systemWillShutdown方法。

这可确保子对象可以在其父级关闭电源之前处理其关闭或重启任务。

请注意,当系统即将重启或关闭时,无需调用驱动程序的free方法,因为此时所有驱动程序都已卸载并销毁。

当驱动程序收到systemWillShutdown调用时,它会执行必要的任务来准备关闭或重启,然后调用其超类的方法实现。

这很重要,因为系统关闭将暂停,直到所有驱动程序处理完其systemWillShutdown通知为止。

除了将调用super::systemWillShutdown 推迟到正在进行的 I/O 请求完成为止,您还应尽一切可能避免延迟关闭。

例 9-2显示了如何重写 systemWillShutdown和接收关闭或重启的通知。


例 9-2 获取系统关闭或重启的通知

c 复制代码
void MyExampleDriver::systemWillShutdown( IOOptionBits specifier )
{
    if ( kIOMessageSystemWillPowerOff == specifier ) {
        // System is shutting down; perform appropriate processing.
    } else if ( kIOMessageSystemWillRestart == specifier ) {
        // System is restarting; perform appropriate processing.
    }
    /*
     * You must call your superclass's implementation of systemWillShutdown as
     * soon as you're finished processing your shutdown or restart
     * because the shutdown will not proceed until you do.
     */
    super::systemWillShutdown( specifier );
}

保持电源开启以便将来连接设备

为了节省电量,如果设备的所有子设备都已消失,则通常会将其视为空闲设备并要求其关闭电源。

但是,您的设备可能需要保持通电状态,以便随时允许新的子设备连接。

例如,即使没有设备连接到总线,总线也可能需要保持通电状态,因为尝试连接的新设备可能会因尝试访问已关闭的硬件而导致崩溃。

IOService类提供了一种方法,即使设备的所有电源子代都已消失,它仍可让您保持设备电源开启。
clampPowerOn方法允许您指定设备保持最高电源状态的时间长度。

如果您需要在驱动程序中执行此操作,请在最后一个电源子代消失之前调用clampPowerOn方法,如下所示:

c 复制代码
// timeToStayOn is a length of time in milliseconds.
clampPowerOn ( timeToStayOn );

十一、管理设备移除

OS X 是一款包含热插拔功能的操作系统。

用户可以插入和移除外部设备(例如,大容量存储驱动器、CD-RW 驱动器、调制解调器和扫描仪),系统会立即执行必要的操作以使设备可用,或者在移除设备的情况下记录设备缺失。

无需重新启动或关闭系统。

本章描述了你的驱动程序应该如何响应设备的移除。


1、设备移除的阶段

当用户将设备插入系统时,I/O Kit 会使用正常的发现和加载驱动程序的过程来响应该事件。

低级驱动程序会扫描其总线,发现新设备,并启动匹配过程以查找合适的驱动程序。

然后,I/O Kit 会将驱动程序加载到内核中,这样设备就可以使用了。

但是,当用户移除设备时,情况就不同了。

驱动程序堆栈必须拆除,而不是建立。

在释放堆栈中的驱动程序之前,它们必须以协调的方式停止接受新请求并清除所有排队和正在进行的工作;这需要特殊的编程接口和程序。

在设备移除后,I/O Kit 会分三个阶段有序地拆除驱动程序堆栈。

第一阶段使堆栈中的驱动程序对象处于非活动状态,因此它们不会接收任何新的 I/O 请求。

第二阶段清除驱动程序队列中待处理和正在进行的 I/O 请求。

最后,在第三阶段,I/O Kit 调用驱动程序上的相应驱动程序生命周期方法来清理分配的资源并从 I/O 注册表中分离对象,然后释放它们。

[图 10-1] 总结了三个阶段中发生的情况,包括驱动程序堆栈内的调用方向。


图 10-1 设备移除阶段


2、使驱动程序处于非活动状态

就像总线控制器驱动程序扫描总线以检测新插入的设备一样,它也会检测刚刚被移除的设备。

当这种情况发生时,它会在其客户端节点调用terminateterminate方法具有使被调用对象立即处于非活动状态的默认行为。
terminate方法也在客户端上递归调用;它在总线控制器上方的堆栈中的每个对象上被调用,直到堆栈中的所有对象都变为非活动状态。

由于处于非活动状态,每个对象还会通过message方法向其客户端(或在极少数情况下向提供者)发送一条kIOServicesIsTerminated消息。

terminate调用返回到原始调用者(总线控制器驱动程序)时,堆栈中的所有对象都处于非活动状态,但堆栈仍连接到 I/O 注册表。

I/O Kit 假定具有多个提供者(例如 RAID 设备的驱动程序)的对象不想被拆除,因此不会对它们调用terminate

如果这些对象确实想接收terminate消息,它们应该实现requestTerminate方法以返回true

terminate调用是异步的以避免死锁,并且在第一阶段,发生在调用者(总线控制器驱动程序)的线程和工作循环上下文中。


3、清除 I/O 队列

I/O Kit 本身负责协调设备移除过程。

它从总线控制器驱动程序的新非活动客户端开始,并像第一阶段一样向上移动驱动程序堆栈,直到到达叶对象。

它调用willTerminate方法在每个遇到的对象上。

驱动程序应实现方法willTerminate来清除它们拥有的任何 I/O 请求队列。

为此,它们应针对队列中的每个请求向请求者返回适当的错误。

willTerminate在每个对象上调用之后,I/O Kit 会反方向,从叶对象(或多个对象)到驱动程序堆栈的根对象,didTerminate调用每个对象。

堆栈顶部的某些对象(尤其是用户客户端)可能会决定保留已发出但尚未收到响应的 I/O 请求的计数("进行中的" I/O 请求)。

(为确保此计数的有效性,对象应在工作循环上下文中增加和减少计数。)通过这种跟踪机制,它们可以确定是否有任何 I/O 请求尚未完成。

如果是这种情况,它们可以实现didTerminate 来返回延迟响应true,从而将终止延迟到最终 I/O 请求完成。

此时,他们可以通过在自己身上调用didTerminate并返回 false的延迟响应,来发出终止应该继续的信号。

如果驱动程序连接到客户端节点 并将其打开,则 I/O Kit 会对didTerminate假定延迟响应 ( true) 。

终止将继续延迟,直到客户端驱动程序关闭其提供程序。

在第二阶段结束时,不应有任何 I/O 请求排队或"在运行"。

I/O Kit 在其自己的单独线程上完成设备移除过程的这一阶段,并在提供程序的工作循环上下文中对客户端进行所有调用。


4、分离和释放对象

在设备移除过程的第三阶段,I/O套件调用驱动程序生命周期方法stopdetach(按顺序),从叶对象开始,在要拆除的堆栈中的每个驱动程序对象中进行。

驱动程序应实现其停止功能,以关闭、释放或释放其在start功能中打开或创建的任何资源,并使硬件处于驱动程序最初找到的状态。

驱动程序可以通过其在I/O注册表中的条目来实现分离,以将其自身从内核中移除;但是,这种行为是默认的,因此驱动程序通常不需要重写detach
detach方法通常会立即释放驱动程序对象,因为I/O注册表通常会对对象进行最终保留。

I/O Kit 在其自己的单独线程上完成设备移除过程的这一阶段,并在提供程序的工作循环上下文中对客户端进行所有调用。


十二、基类和辅助类层次结构

下表列出了所有不属于特定家族的 I/O Kit 类的类层次结构。

请参阅前面的附录《I/O Kit 家族参考》以获取大多数家族的类层次结构图。



十三、参考书目


1、系统内部

  • Advanced Topics in UNIX: Processes, Files, & Systems, Ronald J. Leach, Wiley, 1996, ISBN 1-57176-159-4
  • The Design and Implementation of the 4.4BSD UNIX Operating System, Marshall Kirk McKusick, et al, Addison-Wesley, 1996, ISBN 0-201-54979-4
  • Panic!: UNIX System Crash Dump Analysis Chris Drake, Kimberly Brown Prentice Hall, 1995, ISBN 0-13-149386-8
  • UNIX Internals: The New Frontiers, Uresh Vahalia, Prentice-Hall, 1995, ISBN 0-13-101908-2

2、网站 - 在线资源

Apple Computer 的开发者网站 ( http://developer.apple.com/index.html ) 是开发者文档的通用存储库。

此外,以下网站还提供更多特定领域的信息。


十四、词汇表

  • active driver
    实现高级电源管理任务(如确定设备空闲状态和执行关机前任务)的设备驱动程序。
    另请参阅 被动驱动程序
  • 基类
    在 C++ 中,另一个类(子类)继承自的类。
    它还可用于指定层次结构中的所有类最终都派生自的类(也称为根类)。
  • BSD
    伯克利软件发行版。
    BSD 以前称为伯克利版 UNIX,现在简称为 BSD 操作系统。
    Darwin 的 BSD 部分基于 4.4BSD Lite 2 和 FreeBSD(4.4BSD 的一种版本)。
  • bundle
    文件系统中的目录,通常用于存储可执行代码以及与该代码相关的软件资源。
    (软件包只能存储资源。)应用程序、插件、框架和内核扩展都属于软件包类型。
    除框架外,软件包都是文件包,在 Finder 中显示为单个文件而不是文件夹。
    另请参阅 内核扩展
  • bus
    一种传输路径,信号可由连接到该路径的设备发送或接收。
    只有信号寻址的设备才会关注这些信号;其他设备会丢弃这些信号。
    总线既存在于 CPU 内部,又将其连接到物理内存和外围设备。
    Darwin 上的 I/O 总线示例包括 PCI、SCSI、USB 和 FireWire。
  • bus master
    通常在单独的 I/O 控制器中,用于引导计算机总线或输入/输出路径上的流量的程序。
    总线主控器实际上控制地址和控制信号流经的总线路径。
    DMA 是一种简单的总线主控形式,总线主控器控制设备和系统内存之间的 I/O 传输,然后在完成传输后向 CPU 发出信号。
    另请参阅 DMA
  • client
    使用其提供者提供的某种服务的驱动程序对象。
    在驱动程序堆栈中,提供者/客户端关系中的客户端距离平台专家较远。
    另请参阅 提供者
  • command gate
    一种控制对工作循环锁的访问的机制,从而序列化对 I/O 请求中涉及的数据的访问。
    命令门不需要线程上下文切换来确保单线程访问。
    IOCommandGate 事件源对象表示 I/O Kit 中的命令门。
  • Darwin
    OS X 核心操作系统或内核环境的另一个名称。
    Darwin 内核环境相当于 OS X 内核加上 BSD 命令环境所必需的 BSD 库和命令。
    Darwin 是开源技术。
  • DMA
    (直接内存访问)某些总线架构的一种功能,使总线控制器能够直接在设备(如磁盘驱动器)和具有物理可寻址内存的设备(如计算机主板上的内存)之间传输数据。
    微处理器不再参与数据传输,从而加快了整个计算机的运行速度。
    另请参阅 总线主控器
  • 设备
    计算机硬件,通常不包括 CPU 和系统内存,可进行控制并可发送和接收数据。
    设备示例包括显示器、磁盘驱动器、总线和键盘。
  • device driver | 设备驱动
    操作系统的一个组件,负责将数据传入和传出设备,以及控制该设备。
    使用 I/O Kit 编写的驱动程序是一个对象,它实现了用于控制硬件的适当 I/O Kit 抽象。
  • device file | 设备文件
    在 BSD 中,设备文件是位于 中的特殊文件,/dev它代表块或字符设备,例如终端、磁盘驱动器或打印机。
    如果程序知道设备文件的名称,它可以使用 POSIX 函数来访问和控制相关设备。
    程序可以从 I/O Kit 获取设备名称(在重新启动或设备移除后不会保留)。
  • device interface | 设备接口
    在 I/O Kit 中,一种使用插件架构的机制,允许用户空间中的程序与内核中适合该程序想要控制的设备类型的节点进行通信。
    通过节点,程序可以访问 I/O Kit 服务和设备本身。
    从内核的角度来看,设备接口显示为称为用户客户端的驱动程序对象。
  • device matching | 设备匹配
    在 I/O Kit 中,应用程序查找要加载的适当设备接口的过程。
    应用程序调用一个特殊的 I/O Kit 函数,该函数使用"匹配字典"来搜索 I/O 注册表。
    该函数返回一个或多个匹配的驱动程序对象,然后应用程序可以使用这些对象加载适当的设备接口。
    也称为设备发现。
  • driver
    见上方 device driver
  • driver matching | 驱动程序匹配
    在 I/O Kit 中,一个进程,其中 nub 在发现特定硬件设备后,搜索最适合驱动该设备的驱动程序。
    匹配要求驱动程序具有一个或多个个性,以指定它是否是特定设备的候选者。
    驱动程序匹配是一个减法过程,涉及三个阶段:类匹配、被动匹配和主动匹配。
    另请参阅 个性
  • driver stack | 驱动程序堆栈
    在 I/O 连接中,一系列彼此之间存在客户端/提供者关系的驱动程序对象(驱动程序和节点)。
    驱动程序堆栈通常是指设备与其客户端应用程序(或多个应用程序)之间的整个软件集合。
  • event source | 事件源
    与设备驱动程序可以处理的事件类型相对应的 I/O 对象;目前有硬件中断、计时器事件和 I/O 命令的事件源。
    I/O Kit 为这些事件类型分别定义了一个类,分别是 IOInterruptEventSource、IOTimerEventSource 和 IOCommandGate。
  • families
    特定类别的所有设备所共有的软件抽象集合。
    系列为驱动程序提供功能和服务。
    系列示例包括协议系列(如 SCSI、USB 和 Firewire)、存储系列(磁盘驱动器)、网络系列以及描述人机界面设备(鼠标和键盘)的系列。
  • fault | 故障
    在虚拟内存系统中,故障是启动页面调入活动的机制。
    当代码尝试访问未映射到物理内存的虚拟地址上的数据时,就会发生中断。
    另请参阅 页面虚拟内存
  • framework | 框架
    一种bundle,将动态共享库与库所需的资源(包括头文件和参考文档)打包在一起。
    请注意,内核框架(包含 I/O Kit 头文件)不包含动态共享库。
    内核框架的所有库类型链接都是使用mach_kernel文件本身和内核扩展完成的。
    这种链接在实现上实际上是静态的(带有 vtable 补丁)
  • idle sleep | 小憩
    当设备或系统在用户在"系统偏好设置"的"节能"面板中指定的时间段内没有活动时,就会出现睡眠状态。
    另请参阅 系统睡眠
  • information property list | 信息属性列表
    包含软件包(例如内核扩展)基本配置信息的属性列表。
    名为Info.plist(或该文件名的特定于平台的变体)的文件包含信息属性列表,并打包在软件包内。
  • interrupt | 中断
    一种异步事件,它暂停当前调度的进程并通过中断处理程序例程暂时转移控制流。
    中断可由硬件(I/O、计时器、机器检查)和软件(监控程序、系统调用或陷阱指令)引起。
  • interrupt handler | 中断处理程序
    发生中断时执行的例程。
    中断处理程序通常处理计算机系统硬件中的低级事件,例如到达串行端口的字符或实时时钟的滴答声。
  • I/O Catalog | 输入/输出目录
    维护 Darwin 系统上所有可用驱动程序条目的动态数据库。
    驱动程序匹配会搜索 I/O 目录以生成候选驱动程序的初始列表。
  • I/O Kit
    Darwin 中驻留在内核的面向对象环境,提供系统硬件模型。
    每种类型的服务或设备都由一个系列中的一个或多个 C++ 类表示;每个可用的服务或设备都由该类的一个实例(对象)表示。
  • I/O Kit framework
    包含 IOKitLib 的框架,使 I/O Registry、用户客户端插件和其他 I/O Kit 服务可从用户空间使用。
    它允许应用程序和其他用户进程访问常见的 I/O Kit 对象类型和服务。
    另请参阅 框架
  • I/O Registry
    描述驱动程序对象集合的动态数据库,每个驱动程序对象代表一个 I/O Kit 实体。
    当硬件添加到系统或从系统移除时,I/O Registry 会发生变化以适应添加或移除。
  • kernel
    完整的 OS X 核心操作系统环境,包括 Mach、BSD、I/O Kit、驱动程序、文件系统和网络组件。
    内核驻留在其自己的受保护内存分区中。
    内核包括在内核任务中执行的所有代码,该任务由文件mach_kernel(位于文件系统根目录)和所有加载的内核扩展组成。
    也称为内核环境。
  • kernel extension(KEXT)
    动态加载的软件包,用于扩展内核的功能。
    KEXT 可以包含零个或一个内核模块以及其他(子)KEXT,每个 KEXT 可以包含零个或一个内核模块。
    Darwin 的 I/O Kit、文件系统和网络组件可以通过 KEXT 进行扩展。
    另请参阅 内核模块
  • kernel module (KMOD) | 内核模块
    打包在内核扩展中的 Mach-O 格式的二进制文件。
    KMOD 是可以加载到内核中的最小代码单元。
    另请参阅 内核扩展
  • lock | 锁
    用于同步对共享资源的访问的数据结构。
    锁最常见的用途是在多线程程序中,其中多个线程需要访问全局数据。
    一次只有一个线程可以持有锁;按照惯例,此线程是在此期间唯一可以修改数据的线程。
    另请参阅 互斥锁
  • Mach
    内核的核心组件,提供线程、任务、端口、进程间通信 (IPC)、调度、物理和虚拟地址空间管理、虚拟内存和计时器等基本服务和抽象。
  • map
    将一个地址空间(物理或虚拟)中的一段内存转换为另一个地址空间中的一段内存。
    虚拟内存管理器通过调整内核和用户进程的 VM 表来实现此目的。
  • matching | 匹配
    参见 设备匹配驱动程序匹配
  • memory descriptor
    一个对象,用于在物理内存中布置内存描述符中的缓冲区范围,从而生成适合特定设备或 DMA 引擎的分散/聚集列表。
    该对象派生自 IOMemoryCursor 类。
    另请参阅 DMA内存描述符
  • memory protection | 内存描述符
    描述数据流如何根据方向放入内存或从内存中提取的对象。
    它表示一段内存,用于保存 I/O 传输中涉及的数据,并指定为一个或多个物理或虚拟地址范围。
    该对象派生自 IOMemoryDescriptor 类。
    另请参阅 DMA内存游标
  • memory protection
    一种内存管理系统,可防止程序修改或破坏其他程序的内存分区。
    虽然 OS X 具有内存保护功能,但 Mac OS 8 和 9 没有。
  • mutex | 互斥锁
    互斥锁定对象,允许多个线程同步对共享资源的访问。
    互斥锁有两种状态:锁定和解锁。
    一旦某个线程锁定了互斥锁,其他试图锁定它的线程将被阻止。
    当锁定线程解锁(释放)互斥锁时,其中一个被阻止的线程(如果有)将获取(锁定)它并使用该资源。
    锁定互斥锁的线程必须是解锁它的线程。
    工作循环锁(由命令门使用)基于互斥锁。
    另请参阅 工作循环
  • notification | 通知
    用于提醒感兴趣的接收者(有时称为观察员)某个事件已发生的程序机制。
  • nub
    一个 I/O Kit 对象,表示检测到的可控制实体,例如设备或逻辑服务。
    nub 可以表示总线、磁盘、图形适配器或任意数量的类似实体。
    nub 通过在两个驱动程序(以及两个系列之间的扩展)之间提供桥梁来支持动态配置。
    另请参阅 设备驱动程序
  • page
    (1) 虚拟内存系统可以在物理内存和后备存储器之间传输信息的最小单位(以字节为单位)。
    在 Darwin 中,一页目前为 4 千字节。
    (2) 作为动词,页面是指物理内存和后备存储器之间的页面传输。
    有关Kernel.framework/Headers/mach/machine/vm_params.h详细信息,请参阅。
    另请参阅 故障虚拟内存
  • passive driver | 被动驱动器
    仅执行基本电源管理任务(如加入电源层和更改设备的电源状态)的设备驱动程序。
    另请参阅 主动驱动程序
  • personality
    一组属性,用于指定驱动程序可以支持的设备类型。
    此信息存储在驱动程序的 KEXT 包中的信息属性列表 (Info.plist ) 文件中定义的 XML 匹配字典中。
    单个驱动程序可以提供一个或多个个性进行匹配;每个个性指定一个要实例化的类。
    在初始化时,会向此类实例传递个性字典的引用。
  • physical memory | 物理内存
    随机存取存储器 (RAM) 芯片中包含的电子电路,用于在执行时临时保存信息。
    进程虚拟内存中的地址映射到物理内存中的地址。
    另请参阅 虚拟内存
  • PIO (Programmed Input/Output) | 程序化输入/输出
    在设备和系统内存之间移动数据的一种方式,其中每个字节都在主机处理器的控制下传输。
    另请参阅 DMA
  • plane
    I/O 注册表中的驱动程序(或服务)对象子集,它们之间具有某种类型的提供者/客户端关系。
    最通用的平面是服务平面,它按注册表构建期间附加对象的同一层次结构显示对象。
    此外还有音频、电源、设备树、FireWire 和 USB 平面。
  • Platform Expert
    特定主板的驱动程序对象,它知道系统正在运行的平台类型。
    平台专家充当 I/O 注册表树的根。
  • plug-in
    可以动态添加到正在运行的系统或应用程序中的模块。
    Core Foundation 插件服务使用 Core Foundation Bundle Services 的基本代码加载功能为 Mac 应用程序提供标准插件架构(称为 CFPlugIn 架构)。
    内核扩展是一种内核插件。
  • port
    一个严重超载的术语,在 Darwin 中具有两个特定含义:
    (1)在 Mach 中,指在单个系统上运行的任务之间进行通信的安全单向通道;
    (2)在 IP 传输协议中,指用于选择传入数据包的接收者或指定传出数据包的发送者的整数标识符。
  • POSIX
    可移植操作系统接口。
    由 ISO/IEC、IEEE 和 The Open Group 支持的操作系统接口标准化工作。
  • power child
    在电源平面中,依赖另一个对象供电的设备的驱动程序。
    另请参阅 电源父级平面
  • power parent
    电源平面中为设备提供电源的对象。
    另请参阅 电源子平面平面
  • preemptive multitasking | 抢占式多任务
    一种多任务处理,其中操作系统可以根据需要中断当前正在运行的程序以运行另一个程序。
  • probe | 探测
    主动匹配的一个阶段,候选驱动程序与设备通信并验证其是否可以驱动该设备。
    驱动程序的probe成员函数被调用来启动此阶段。
    驱动程序返回一个探测分数,该分数反映其驱动设备的能力。
    另请参阅 驱动程序匹配
  • process | 过程
    正在运行的程序的 BSD 抽象。
    进程的资源包括虚拟地址空间、线程和文件描述符。
    在 OS X 中,进程基于一个 Mach 任务和一个或多个 Mach 线程。
  • provider | 提供者
    为其客户端提供某种服务的驱动程序对象。
    在驱动程序堆栈中,提供者/客户端关系中的提供者更接近平台专家。
    另请参阅 客户端
  • release | 发布
    减少对象的引用计数。
    当对象的引用计数达到零时,它将被释放。
    当您的代码不再需要引用保留的对象时,它应该释放它。
    一些 API 会自动代表调用者执行释放,特别是在相关对象被"移交"的情况下。
    保留和释放必须小心平衡;过多的释放可能会因访问释放的内存而导致恐慌和其他意外故障。
    另请参阅 保留
  • retain
    增加对象的引用计数。
    引用计数为正的对象不会被释放。
    (新创建的对象引用计数为 1。)驱动程序可以通过保留对象来确保对象在当前范围之外的持久性。
    许多 API 会自动代表调用者执行保留,特别是用于创建或获取对象访问权限的 API。
    保留和释放必须小心平衡;保留过多会导致有线内存泄漏。
    另请参阅 释放
  • scheduler | 调度程序
    Mach 的一部分,用于确定每个程序(或程序线程)的运行时间,包括分配启动时间。
    程序线程的优先级会影响其调度。
    另请参阅 任务线程
  • service | 服务
    服务是一个 I/O Kit 实体,基于 IOService 的子类,已使用registerService方法发布,并向其他 I/O Kit 对象提供某些功能。
    在 I/O Kit 的分层架构中,每一层都是其下层的客户端,也是其上层的服务提供者。
    服务类型由描述服务属性的匹配字典标识。
    节点或驱动程序可以向其他 I/O Kit 对象提供服务。
  • socket
    在 BSD 衍生系统(例如 Darwin)中,套接字是指用户和内核操作中的不同实体。
    对于用户进程,套接字是使用socket(2) 分配的文件描述符。
    对于内核,套接字是在内核实现调用socket(2)时分配的数据结构。
  • system sleep | 系统睡眠
    当用户从 Apple 菜单中选择"睡眠"或合上笔记本电脑盖子时出现的睡眠状态。
    另请参阅 空闲睡眠
  • task | 任务
    Mach 抽象由虚拟地址空间和端口名称空间组成。
    任务本身不执行任何计算;相反,它是线程运行的上下文。
    另请参阅 进程线程
  • thread | 线
    在 Mach 中,这是 CPU 使用率的单位。
    线程由一个程序计数器、一组寄存器和一个堆栈指针组成。
    另请参阅 任务
  • timer | 计时器
    以指定间隔触发事件的内核资源。
    事件可以只发生一次,也可以重复发生。
    计时器是工作循环的事件源之一。
  • user client | 用户客户端
    I/O Kit 系列提供的接口,使用户进程(无法直接调用驻留在内核的驱动程序或其他服务)能够访问硬件。
    在内核中,此接口显示为驱动程序对象(称为用户客户端);在用户空间中,它称为设备接口,并作为核心基础插件服务 (CFPlugin) 对象实现。
    另请参阅 设备接口
  • user space | 用户空间
    内核所在受保护分区之外的虚拟内存。
    应用程序、插件和其他类型的模块通常在用户空间中运行。
  • virtual address | 虚拟地址
    软件可以使用的内存地址。
    每个任务都有自己的虚拟地址范围,从地址零开始。
    Mach 操作系统让 CPU 硬件仅在必要时将这些地址映射到物理内存上,其他时候则使用磁盘内存。
  • virtual memory | 虚拟内存
    使用磁盘分区或磁盘上的文件来提供通常由 RAM 提供的相同功能。
    OS X 中的虚拟内存管理器为每个任务提供 32 位(最小)受保护的地址空间,并促进该地址空间的有效共享。
  • wired memory
    虚拟内存系统不会调出或移动的内存范围。
    必须将涉及 I/O 传输的内存连线,以防止硬件访问数据的物理重定位。
    在 I/O Kit 中,当描述内存的内存描述符为 I/O 准备内存时(当prepare调用其方法时会发生这种情况),内存即被连线。
  • work loop | 工作循环
    一种门控机制,可确保单线程访问驱动程序使用的数据结构和硬件寄存器。
    具体来说,它是与线程关联的互斥锁。
    工作循环通常附带多个事件源;它们使用工作循环来确保受保护的、门控的上下文来处理事件。
    另请参阅 事件源

十五、I/O Kit 系列参考

本附录详细描述了每个 I/O Kit 系列,特别关注了客户端/提供者关系。

对于大多数系列,它提供了类层次结构图。

它还会告诉您某个系列是否导出设备接口,从而允许应用程序访问该系列所代表的设备。

在尝试编写内核驻留驱动程序之前,您应该认真考虑采用设备接口方法。

有关使用设备接口的信息,请参阅文档"从应用程序访问硬件"

某些类别的设备目前不受 I/O Kit 系列支持。

如果您的设备属于不受支持的类别,您可以编写"无系列"驱动程序、使用 I/O Kit 以外的 SDK 或创建新系列。

有关详细信息,请参阅 无 I/O Kit 系列的设备

您可能会发现检查 I/O Kit 系列或特定设备驱动程序的源代码很有帮助。

为此,请访问Darwin Releases,选择适当的 OS X 版本,然后单击"源"以查看可用的源项目。


1、ADB

这ADB 系列为连接到 Apple Desktop Bus (ADB) 的设备提供支持和访问。

它为 ADB 总线控制器驱动程序 (IOADBController) 和另一个用于 ADB 设备驱动程序的控制器 (请参阅"IOADB设备"。

Bundle标识符

  • com.apple.iokit.IOADBFamily

头文件位于:

  • 内核常驻:Kernel.framework/Headers/IOKit/adb/
  • 设备接口:IOKit.framework/Headers/adb

参考文献及规格

类层次:


设备接口

  • 导出用于读取和写入 ADB 设备上的寄存器的接口。
    该接口在 中定义IOADBLib.h
    此库仅支持轮询模式操作。
    仅支持驻留在内核的客户端的中断操作。

Table A-1 Clients and providers of the ADB family

Client of the nub 核心提供者
行动 驱动插入 ADB 端口的设备。 驱动 ADB 总线控制器
例子 ADB 鼠标的驱动程序是ADB 家族的客户端,但是是 HID 家族的**成员(在 IOHIPointing 类中)。
一个例子IOADBDevice 匹配您的驱动程序并将其加载到内核。 您的驱动程序通过 IOADBDevice 实例与其系列进行通信。 ADB 总线控制器驱动程序应该继承自头文件中定义的 IOADBController 类IOADBController.h
笔记 常见的客户 families 包括 HID families (IOHIPointing 和IOHIKeyboard 类)和 Graphics 系列(IODisplay 类)。 目前 Apple 生产的所有 ADB 总线硬件都得到了 OS X 附带的驱动程序的良好支持。 除 ADB-USB 适配器外,第三方开发人员不需要为 ADB 系列编写驱动程序。

2、ATA 和 ATAPI

这ATA 和 ATAPI 系列提供对 ATA 控制器的支持,以及对 ATA 总线上 ATA 和 ATAPI 设备的访问。

重要提示: ATA 和 ATAPI 系列仍在开发中。

本节中的信息可能会发生变化。

Bundle标识符

  • com.apple.iokit.IOATAFamily

头文件位于

  • 内核常驻:Kernel.framework/Headers/IOKit/ata

参考文献及规格

类别层次


设备接口

  • 虽然该系列本身不导出设备接口,但是SCSI 架构模型系列确实为 ATAPI 设备提供了设备接口支持。

Table A-2 Clients and providers of the ATA and ATAPI family

Client of the nub 核心提供者
行动 驱动连接到 ATA 总线的设备。 驱动 ATA 总线控制器。
例子 ATA 硬盘或 ATAPI DVD-ROM 驱动器。 Storage 系列是客户最常用的系列。
该家族的客户与以下实例匹配IOATADevice,控制器驱动程序会为总线上检测到的每个 ATA 或 ATAPI 设备创建并发布一个 IOATADevice。 它们使用该对象提供的服务与总线上的物理设备进行通信。
笔记 ATA 系列的客户端必须发出封装在IOATACommand 对象。 此命令对象封装了编码单个 ATA/ATAPI 命令所需的所有信息以及命令执行的结果。 第三方开发人员永远不需要创建 ATA/ATAPI 系列的成员。

3、Audio

这Audio 系列支持访问录制或播放音频信号的设备。

它为音频设备提供了灵活的抽象,允许无限数量的通道以及任意的采样率、位深度和采样格式。

Audio 系列采用高分辨率时间基准,作为整个音频和音频信号时序信息的基础。

OS X 中的 MIDI 系统。

(Audio 系列本身不提供任何 MIDI 服务;这些服务由核心 MIDI 框架。

这音频硬件抽象层(Audio HAL)为 OS X 中的应用程序提供所有音频服务。

Audio HAL 可通过以下方式访问:核心音频框架,并在该框架的AudioHardware.h中定义了其编程接口。

音频系列提供音频驱动程序和音频 HAL 之间的链接。

由于音频 HAL 是音频系列的客户端,因此所有音频设备功能都可供音频 HAL 的客户端使用。

Bundle标识符

  • com.apple.iokit.IOAudioFamily

头文件位于

  • 内核常驻:Kernel.framework/Headers/IOKit/audio/

设备接口

  • 虽然该系列不直接导出设备接口,但音频系列确实提供了一个设备接口,音频硬件抽象层 (Audio HAL) 使用该接口访问音频系列提供的所有抽象(参见上面的描述)。

电源管理

  • 音频系列为子类设备驱动程序执行大多数电源管理任务。
    音频驱动程序不必调用 PMinitjoinPMtreeregisterPowerDriverPMstop,因为音频系列负责初始化电源管理、将驱动程序连接到电源平面并将其注册到电源管理以及终止电源管理。
    虽然音频驱动程序不必实现IOService方法setPowerState,但它需要实现IOAudio方法performPowerStateChange来完成改变其设备电源状态的工作。
  • 音频系列通过跟踪活动的音频引擎来实现空闲判断,因此自定义音频驱动程序永远不需要activityTickle自行调用或确定空闲状态。

Table A-3 Clients and providers of the Audio family

Client of the nub 核心提供者
行动 无需驻留在内核的客户端。 使用 Core Audio 框架来访问 Audio HAL。 录制或播放音频信号。
例子 PCI 音频卡、外部 USB 或 FireWire 音频设备以及任何其他产生或消耗音频的设备。
音频驱动程序必须包含以下两个子类IOAudioDevice 和IOAudioEngine。
笔记 音频驱动程序是其他系列的客户端,这些系列提供对该驱动程序支持的硬件的访问。 例如,PCI 音频卡的驱动程序将是 PCI 系列的客户端。

4、FireWire

这FireWire 系列提供对连接到 FireWire 总线的设备的支持和访问(FireWire 是 Apple 的商标,应用于IEEE 1394 标准,有时也称为您可以使用 i.LINK™

FireWire系列与SBP2系列具有很强的亲和力。

使用SBP-2传输协议的驱动程序是FireWire系列中最常见的客户端。

Bundle标识符

  • com.apple.iokit.IOFireWireFamily

头文件位于

  • 内核驻留:Kernel.framework/Headers/IOKit/firewire/
  • 设备接口:IOKit.framework/Headers/firewire

参考文献及规格

类层次


设备接口

  • 提供设备接口,导出用于在 FireWire 总线上发送和接收数据包以及将条目添加到计算机自己的FireWire 配置 ROM。

Table A-4 Clients and providers of the FireWire family

Client of the nub 核心提供者
行动 *单元驱动程序:*驱动与插入 FireWire 总线的设备单元的通信。 *协议驱动程序:*添加对协议的支持,该协议支持通过 FireWire 进行点对点通信或仿真。 驱动 FireWire 总线控制器。
例子 设备驱动程序: FireWire 扬声器的驱动程序。 *协议驱动程序:*通过 FireWire 提供 TCP/IP 的驱动程序。
单位驱动程序 :IOFireWireUnit 表示在设备配置 ROM 中找到的单元,它与驱动程序匹配并加载到内核中。 驱动程序通过此实例与 FireWire 系列进行通信。 协议驱动程序: IOFireWireController 的实例与协议驱动程序匹配并将其加载到内核中。 驱动程序通过此实例与 FireWire 系列进行通信。 驱动程序类应该继承自IOFireWireController 类如头文件中所定义IOFireWireController.h
笔记 最常见的客户 families 是SBP2 families 。 OS X 为所有开放主机控制器接口 (OHCI) 总线控制器。 第三方开发人员通常不需要为 FireWire 系列编写总线控制器驱动程序。

在某些情况下,您可以为 FireWire 设备编写驱动程序,而不是为单元编写驱动程序。

例如,为具有最小配置 ROM(即,仅具有供应商 ID)的设备编写驱动程序。

但是,Apple 强烈反对使用最小配置 ROM。

此外,如果您的驱动程序与 FireWire 单元匹配,通常可以对该设备执行某些操作。


5、Graphics

这图形系列提供支持帧缓冲器和显示设备(监视器)。

Bundle标识符

  • com.apple.iokit.IOGraphicsFamily

头文件位于

  • 内核常驻:Kernel.framework/Headers/IOKit/graphics/
  • 设备接口:IOKit.framework/Headers/graphics

类别层次


设备接口

  • Graphics 系列导出了多个设备接口,因为大多数图形设备的访问都是从用户空间进行的。
    然而,Quartz 层通过窗口系统或 CGDirectDisplay API 仲裁从用户空间对帧缓冲区的访问。
    其他层,例如碳纤维拉伸链轮,为应用程序提供对图形的访问。
  • 图形加速由加载到用户地址空间的模块提供。
    CFPlugIn 接口定义在 中IOGraphicsInterface.h,实现二维加速。
    同样,OpenGL 为三维渲染定义了一个可加载包接口。
    由于没有标准方法来实现此功能,因此特定于硬件的代码可以存在于用户空间代码和内核加载的驱动程序中。

Table A-5 Clients and providers of the Graphics family

Client of the nub 核心提供者
行动 实现对帧缓冲区的支持。
例子
帧缓冲驱动程序必须是IOFramebuffer 类。 IONDRVFramebuffer 类支持原生 Power PC Mac OS 图形驱动程序(称为" ndrv");只要驱动程序正确符合规范,此支持就是自动的。
笔记 对驻留在内核的客户端的支持有限。 由于 Quartz 层拥有显示功能,因此内核通常不直接渲染图形。 Apple 为显示器提供通用支持,因此显示器通常不需要第三方驱动程序。

关于 NDRV 兼容性的说明

如果正确编写,NDRV 图形驱动程序应该可以在 OS X 中运行。

如果编写不正确,OS X 运行时环境中的许多差异可能会导致它们失败、被忽略,甚至导致崩溃。

如果您正在编写 NDRV 驱动程序,请遵循以下规则:

  • 使用虚拟寻址访问卡硬件。
    不要假设卡已映射到其物理分配的地址。
    在 Mac OS 9 中,NDRV 卡是一对一映射的,但在 OS X 中,这不能保证。
    通过AAPL,address"为 PCI Power Macintosh 设计的卡和驱动程序"中记录的属性获取卡硬件的虚拟地址。
  • 仅链接到本机驱动程序库,即 NameRegistryLib、DriverServicesLib 和 VideoServicesLib。
    如果您的卡链接到 InterfaceLib 或任何其他应用程序级库,它可能无法在 OS X 上运行。
  • 请勿访问低内存;这样做会导致 OS X 崩溃(内核崩溃)。
  • Mac OS 9 或 OS X 不支持从中断级别进行名称注册调用。
    它们会在 OS X 中返回错误。
  • OS X 不支持辅助中断。
    如果您的卡不支持垂直空白中断,则无需伪造垂直空白中断 --- 只需不创建 VBL 服务即可。
    Mac OS 9 仍然需要安装 VBL 服务才能移动设备上的光标。
  • 在 OS X 上,堆栈大小限制为 16K;任何 NDRV 调用都不应占用超过 4K 的堆栈。

如果您想对 NDRV 代码进行运行时条件更改,则在 OS X 使用您的驱动程序之前在 PCI 设备属性中设置AAPL,iokit-ndrv属性。

OS X 在硬件中支持 32 位/像素、alpha 混合光标。

如果您的设备支持 alpha 混合直接颜色光标,则它应使用 HardwareCursorDescriptor记录 中设置的以下字段进行调用 VSLPrepareCursorForHardwareCursor

c 复制代码
bitDepth = 32 maskBitDepth = 0 numColors = 0colorEncodings = NULL

HardwareCursorInfo 中的hardwareCursorData缓冲区应指向每像素 32 位 ARGB 数据的缓冲区。

数据未与 alpha 通道预乘。


6、HID

这人机接口设备 (HID) 类是 USB(通用串行总线)架构描述的几种设备类之一。

HID 类主要由人类用来控制计算机系统操作的设备组成。

此类 HID 类设备的示例包括:

  • 键盘和鼠标、轨迹球和操纵杆等指点设备
  • 前面板控件,如旋钮、开关、滑块和按钮
  • 游戏或模拟设备上可能存在的控件,例如数据手套、油门和方向盘

目前,OS X 提供HID 管理器允许应用程序访问操纵杆、音频设备和非 Apple 显示器。

您还可以使用 HID 管理器从另一种 HID 类设备(UPS(不间断电源)设备。

UPS 设备与其他 HID 类设备共享相同的报告描述符结构,并提供电压、电流和频率等信息。

OS X HID 管理器由三层组成:

  • HID 管理器客户端 API 提供应用程序可用于处理 HID 类设备的定义和函数
  • HID 系列提供内核 HID 基础架构,例如基类、内核用户空间内存映射和排队代码以及 HID 解析器
  • Apple 提供的 HID 驱动程序

Bundle标识符

  • com.apple.iokit.IOHIDFamily

头文件位于

  • 内核常驻:Kernel.framework/Headers/IOKit/hid/Kernel.framework/Headers/IOKit/hidsystem/
  • 设备接口:IOKit.framework/Headers/hid/IOKit.framework/Headers/hidsystem/

参考文献及规格

类别层次


设备接口

  • HID 系列通过 HID 管理器客户端 API 导出设备接口。
    HID 管理器包括IOHIDLib.hIOHIDKeys.h(位于/System/Library/Frameworks/IOKit.framework/Headers/hid),它们定义描述设备的属性键、描述设备元素的元素键以及用于与设备通信的设备接口函数和数据结构。
    为选定的 HID 类设备创建设备接口后,您可以使用设备接口函数打开和关闭设备、获取元素的最新值或设置元素值。

Table A-6 Clients and providers of the HID family

Client of the nub 核心提供者
行动 驱动输入设备,例如多键鼠标、轨迹球或操纵杆。
例子
笔记 通用驱动程序提供对大多数简单输入设备的支持。

7、Network

这网络系列为网络控制器提供支持。

网络系列由两个逻辑层组成:

  • 控制器层------此层代表网络控制器。
  • 接口层---此层代表网络控制器发布的网络接口。

Bundle标识符

  • com.apple.iokit.IONetworkingFamily

头文件位于

  • 内核常驻:Kernel.framework/Headers/IOKit/network/
  • 设备接口:IOKit.framework/Headers/network/

类别层次


设备接口

  • 该系列的设备接口通常是BSD 网络堆栈。
    应用程序使用网络堆栈提供的套接字接口间接访问网络系列提供的服务。

电源管理

网络系列为子类设备驱动程序执行大多数电源管理设置和拆卸任务。

如果您正在为可以被动电源管理的网络设备开发驱动程序(大多数网络设备都是这样),则可以通过重写IONetworkController方法registerWithPolicyMaker 并调用IOServiceregisterPowerDriver方法来满足大多数基本电源管理需求。

registerWithPolicyMaker 的实现中,创建一个IOPMPowerState结构数组来定义设备的电源状态并将它们传递给registerPowerDriver

然后,从registerWithPolicyMaker返回kIOReturnSuccess 以告知 Network 系列您的驱动程序可以响应电源管理调用。

registerPowerDriver 的默认实现返回kIOReturnUnsupported。)以下代码片段显示了执行此操作的一种方法:

c 复制代码
IOReturn MyEthernetDriver::registerWithPolicyMaker( IOService * policyMaker )
{
IOReturn ioreturn;
static IOPMPowerState powerStateArray[ kPowerStateCount ] = {
   { 1,0,0,0,0,0,0,0,0,0,0,0 },
   { 1,kIOPMDeviceUsable,kIOPMPowerOn,kIOPMPowerOn,0,0,0,0,0,0,0,0 }
};
fCurrentPowerState = kPowerStateOn;
ioreturn = policyMaker->registerPowerDriver( this, powerStateArray, kPowerStateCount );
return ioreturn;
}

大多数网络设备驱动程序在IONetworkControllerenabledisable方法的实现中 处理与睡眠和唤醒相关的电源变化。 请注意,当设备转换为设置了标志kIOPMDeviceUsable的电源状态时,网络系列会启用该设备。 当当前启用的设备 转换为未设置kIOPMDeviceUsable`标志的电源状态时,网络系列会禁用该设备。

如果您需要执行其他任务来处理睡眠和唤醒,则可以重写IOServicesetPowerState方法。
setPowerState但请注意,如果新电源状态使设备处于不可用状态,则 Network 系列将在您收到对您的实现的调用setPowerState 之前调用disable

相反,Network 系列会在 您收到将设备移至可用状态的setPowerState调用 之后调用enable

如果您的网络设备驱动程序执行 DMA,则应重写OS X v10.5 中引入 IOServicesystemWillShutdown方法。

这对于在基于 Intel 的 Macintosh 计算机上运行的驱动程序尤其重要。

在实现systemWillShutdown时,应确保关闭 DMA 引擎,这会导致端口被禁用。

重要提示:接收关机和重启通知 中所述,调用systemWillShutdown将按照从叶到根的顺序对电源平面中的驱动程序进行。

如果您的驱动程序 从registerWithPolicyMaker返回kIOReturnUnsupported,它将不会连接到电源平面,也不会收到调用systemWillShutdown


Table A-7 Clients and providers of the Network family

Client of the nub 核心提供者
行动 驱动网络控制器。
例子 以太网、令牌环和 FDDI 适配器上的控制器。
驱动程序必须是实现通用网络控制器功能的控制器类的子类的实例,例如 IONetworkController 或基于以下控制器类构建的控制器类IONetworkController 专门用于以太网控制器支持 (IOEthernetController)。 有关更多信息,请参阅下面有关网络系列类的讨论。
笔记 驱动程序通常不是网络系列的客户端。 此系列的主要系统客户端是BSD 网络堆栈中的 DLIL(数据链路接口层)模块。

成员驱动程序还必须创建向 DLIL 注册的 IONetworkInterface 对象;此类注册将驱动程序与en0系统中的网络接口(例如)关联起来。

您可以创建一个网络内核扩展 (NKE) 并将其插入到 IOKit/DLIL 边界上方的各个位置,以拦截 IONetworkInterface 对象和上层之间的数据包、命令或其他事件流量。

这个 families 的另一位客户是内核中的 KDP(内核调试器协议)模块。

驱动程序可以创建一个 IOKernelDebugger 对象,该对象提供调试服务并允许通过驱动程序管理的网络硬件进行内核调试。

只有驱动内置网络控制器的驱动程序才需要提供此支持。


网络家族的其他类别包括:

  • IOOutputQueue --- 协助对出站数据包进行排队。
  • IOPacketQueue --- 代表通用数据mbuf包 FIFO 队列。
  • IOMbufMemoryCursor ------ 将 mbuf数据包转换为物理地址和长度对的分散-集中列表。
  • IONetworkData --- 代表一组统计计数器的容器。
  • IONetworkMedium --- 代表网络设备支持的单一网络介质。

8、PC Card

这PC 卡系列支持 32 位 PC 卡(卡总线 (CardBus)16 位 PC 卡(I/O 和内存),以及Zoom 视频卡。

此支持包括与 ExCA(Intel 82365)和 Yenta 寄存器组兼容的控制器。

Apple 的 PC Card 系列的 Card Services 大部分都符合 1997 PC Card™ 标准。

CardBus 卡本质上是 PCI 设备,只是外形不同。

如果您正在为 CardBus 卡编写驱动程序,则可以选择从以下任一类型进行子类化IOPCIDevice 或PCI and AGPIOCardBusDevice.有关 PCI 系列的更多信息,请参阅参考部分。

该 families 提供的其他课程包括:

  • IOPCCard16Device---代表16位PC卡设备。
  • IOPCCard16Enabler---允许您覆盖 16 位卡的卡配置过程。
    它主要用于 CIS 条目损坏或丢失的卡。
  • IOZoomVideoDevice---代表 Zoom Video 设备。
  • IOPCCardBridge---代表 PC 卡桥;此类是IOPCI2PCIBridge 是IOPCIBridge。

Bundle标识符

  • com.apple.iokit.IOPCCardFamily

头文件位于

  • 内核常驻:Kernel.framework/Headers/IOKit/pccard/

参考文献及规格

  • 卡服务文档可在 PC Card 系列源目录中找到doc(可通过 Darwin CVS 获取)。
    您还可以在同一位置找到一个示例 16 位驱动程序。
  • Apple 开发者连接--- https://developer.apple.com/hardwaredrivers/

类别层次


设备接口

  • 没有任何。

电源管理

  • IOPCCardBridgeIOPCCard16DeviceIOPCCard16EnablerIOCardBusDevice 提供一些电源管理支持。

9、PCI 和 AGP

这PCI 和 AGP 系列为连接到 PCI 和 AGP 总线和 PCI 桥的设备提供支持和访问。

Bundle标识符

  • com.apple.iokit.IOPCIFamily

头文件位于

  • Kernel.framework/Headers/IOKit/pci/

参考文献及规格


类别层次


设备接口

  • 无。
    出于安全原因,不允许应用程序直接访问 PCI 总线硬件。
    相反,应用程序应该与更高级别的服务进行交互,例如 USB 或其他系列的设备接口提供的服务。

匹配属性

  • PCI/AGP 系列允许基于以下条件进行匹配打开固件或 PCI 寄存器。
    请参阅IOPCIDevice 类的详细信息请参见参考文档。

检查 Darwin 开源项目以获取示例 PCI 驱动程序。


Table A-8 Clients and providers of the PCI and AGP family

Client of the nub 核心提供者
行动 驱动插入 PCI 总线的设备。 驱动 PCI 总线控制器或 PCI 桥。
例子 PCI SCSI 控制卡的驱动程序是PCI 家族的客户端 ,但是是SCSI 并行家族的成员。
driver 通过IOPCIDevice 或IOAGPDevice. 其中一个类的实例与您的驱动程序匹配并将其加载到内核中。 PCI 家族成员驱动程序应继承自IOPCIBridge 类。
笔记 最常见的客户端系列是 USB、网络、SCSI、图形和音频系列。 OS X 通过一组通用驱动程序支持大多数 PCI 总线硬件。 一般来说,第三方开发人员不需要为 PCI 和AGP 系列,除非他们正在构建 PCI 扩展底盘或开发具有通用驱动程序未涉及的特殊特性的 PCI 桥驱动程序。

10、SBP-2

这SBP2 系列(串行总线协议 2)为使用SBP-2 传输协议。

SBP2 系列是 FireWire 系列的客户端。

SBP-2 设备需要 FireWire 才能运行。

Bundle标识符

  • com.apple.iokit.IOFireWireSBP2

头文件位于

  • 内核常驻:Kernel.framework/Headers/IOKit/sbp2/
  • 设备接口:IOKit.framework/Headers/sbp2

参考文献及规格

类别层次


设备接口

  • SBP2 系列提供了一个设备接口,该接口导出用于将 SBP-2 ORB 发送到设备的接口。

Table A-9 Clients and providers of the SBP2 family

Client of the nub 核心提供者
行动 驱动使用 SBP-2 传输协议的设备。 提供SBP-2传输服务
例子 典型的 FireWire 硬盘驱动器的驱动程序是SBP2 系列的客户端 ,但却是大容量存储系列的成员。
客户端驱动程序通过以下实例与 SBP2 系列进行通信IOFireWireSBP2LUN. 为配置 ROM 单元目录中找到的每个 LUN(逻辑单元号)创建此类的一个实例;该实例与您的驱动程序匹配并将其加载到内核中。 SBP2 家族成员驱动程序应继承自IOFireWireSBP2Target 类。
笔记 最常见的客户端家族是存储家族。

11、SCSI 并行

这SCSI 并行系列支持并访问连接到并行 SCSI 总线的设备。

该系列支持SCSI 并行接口-5 (SPI-5)并行总线规范。

Bundle标识符

  • com.apple.iokit.IOSCSIParallelFamily

头文件位于

  • 内核常驻:Kernel.framework/Headers/IOKit/scsi/spi
    头文件IOSCSIParallelInterfaceController.h支持 SCSI 卡驱动程序的开发。

参考文献及规格

类别层次


设备接口

  • SCSI 架构模型系列提供对并行 SCSI 设备的设备接口支持。
    请参阅 SCSI 架构模型

Table A-10 Clients and providers of the SCSI Parallel family

Client of the nub 核心提供者
行动 驱动插入 SCSI 总线的设备。 驱动 SCSI 主机适配器或控制器芯片。
例子 SCSI 磁盘驱动器的驱动程序是SCSI 并行系列的客户端 ,但却是存储系列的成员。
客户端驱动程序通过以下实例与 SCSI 并行系列进行通信IOSCSIDevice。 您的驱动程序将匹配此实例并加载到内核中。 另一个有趣的类是IOSCSI命令。 由于 SCSI 并行系列目前支持并行总线,因此成员驱动程序使用IOSCSIParallelInterfaceController 类。
笔记 常见的客户端系列包括存储设备的传输驱动程序。 OS X 包含大多数内置 SCSI 硬件的通用驱动程序。 一般来说,第三方开发人员不需要编写属于 SCSI Parallel 系列的驱动程序,除非他们正在开发扩展卡的驱动程序。

12、SCSI 架构模型

这SCSI 架构模型系列为 SCSI、USB(存储)、FireWire SBP-2 和 ATAPI 设备提供通用客户端支持。

该系列的许多类都属于传输驱动层,总结在 表 A-11中


Table A-11 SCSI Architecture Model family---Transport Driver layer

类型 课程 评论
设备服务联动 IOBlock存储服务IOReducedBlockServices 类IO光盘服务IODVD服务 链接对象理解来自设备服务层(存储系列)的 API,并导出逻辑单元驱动程序(在传输驱动程序层)理解的 API。
逻辑单元驱动程序 IOSCSIPeripheralDeviceType00 IOSCSIPeripheralDeviceType05 IOSCSIPeripheralDeviceType07 IOSCSIPeripheralDeviceType0E 驱动具有文件系统或可启动的大容量存储设备。
外围设备类型 IOSCSIPerpheralDeviceNub 实际上并不是一个 I/O Kit 核心,而是一个查询设备并确定需要哪个逻辑单元驱动程序的对象。
SCSI 协议驱动程序 IOUSB大容量存储类IOATAPI协议传输 IOFireWireSerialBusProtocol-传输IOSCSI并行接口-协议传输 特定于总线的驱动程序的类。 尽管这些类属于其他系列,但它们是 SCSI 架构模型分层的一部分。

逻辑单元驱动程序和外围设备类型节点位于 SCSI 应用程序层,该层最终继承自IOSCSIPrimaryCommandsDevice。

SCSI 协议驱动程序位于 SCSI 协议层,该层继承自IOSCSI协议服务。

SCSI 结构模型系列支持多媒体命令集设备,例如 CD-RW 驱动器。

Bundle标识符

  • com.apple.iokit.IOSCSIArchitectureModelFamily

头文件位于

  • 内核驻留:Kernel.framework/Headers/IOKit/scsi-commands/
  • 设备接口:IOKit.framework/Headers/scsi-commands

类别层次


设备接口

  • SCSI 架构模型系列的库称为SCSITaskLib。
    它包括三个接口:MMC设备接口,SCSITaskDeviceInterface,以及SCSITaskInterface。
    用户客户端类是SCSITaskUserClient。
    如果您需要访问 SCSI 并行设备*,并且*您的应用程序必须在 v10.2 之前的 OS X 版本中运行,请参阅 访问 SCSI 并行设备以获取有关如何执行此操作的信息。
    否则,您应该使用 SCSITaskLib 中的设备接口来访问您的设备(有关如何执行此操作的信息,请参阅 访问 SCSI 架构模型设备)。

电源管理

SCSI 架构模型系列为协议服务驱动程序和逻辑单元驱动程序执行大多数电源管理设置和拆卸任务。

通常,协议服务驱动程序(例如 IOATAPIProtocolTransport)必须能够在关闭和打开状态之间转换物理互连设备。

另一方面,逻辑单元驱动程序(例如 IOSCSIPeripheralDeviceType05)必须能够管理支持 SCSI 多媒体命令规范定义的所有电源状态的多媒体设备。

此外,逻辑单元驱动程序可能需要确定是否需要更改电源状态,在设备未处于适当的电源状态时阻止传入的 I/O,并指定设备在启动时应进入的电源状态。

注意: SCSI 架构模型系列定义了系统睡眠电源状态,这是命令集规范定义的电源状态的补充。

系统睡眠对应于用户在 Apple 菜单中选取"睡眠"或合上笔记本电脑盖子时发生的睡眠。

命令集规范中定义的睡眠状态对应于设备闲置时发生的睡眠。

由于系统睡眠状态下的设备可以断电,因此 SCSI 架构模型系列的处理方式与睡眠不同。

如上面的类层次结构图所示,SCSI 架构模型系列为这两种类型的驱动程序定义了一个公共的超类:IOSCSIProtocolInterface
IOSCSIProtocolInterface类定义了许多电源管理方法,子类驱动程序可以调用这些方法,或者在较少的情况下重写这些方法。

  • 如果您正在开发自定义协议服务驱动程序,则无需执行实现基本电源管理 中概述的步骤,而是必须在驱动程序例程中调用IOSCSIProtocolInterface方法。
    然后,如果需要执行特定于设备的工作来处理电源状态更改,则可以实现方法和。
    InitializePowerManagement``start``HandlePowerOn``HandlePowerOff
  • 同样,如果您正在开发自定义逻辑单元驱动程序,则无需执行实现基本电源管理 中概述的步骤。
    如果您的设备符合相应的命令集规范,则无需重写驱动程序中的任何方法,除非您想要实现自定义电源管理功能。
    例如,您可能希望设备从活动状态直接转换为睡眠状态,而不是通过活动状态和睡眠状态之间的中间状态。
    IOSCSIProtocolInterface类中,SCSI 架构模型系列提供了逻辑单元驱动程序子类可以实现的以下方法:
    • InitializePowerManagement
      此方法的超类实现执行电源管理设置任务。
      子类驱动程序可以重写此方法,以提供有关设备支持的电源状态的信息。
    • TicklePowerManager
      的超类实现TicklePowerManager调用activityTickle方法,从而请求将您的设备更改为其活动电源状态。
      子类驱动程序可以重写此方法来指定与活动状态不同的状态。
    • GetInitialPowerState
      此方法用于指定系统启动时设备应处于的默认状态(通常为活动状态)。
      如果设备在系统启动时应进入其他状态,则子类驱动程序可以覆盖此方法并指定该状态。
    • GetNumberOfPowerStateTransitions
      子类驱动程序可以重写此方法,以报告设备支持的最低和最高规范定义的电源状态之间的转换次数。
      请注意,系统睡眠不计入转换中,因为它不是设备自愿进入的电源状态。
    • HandlePowerChange. 子类驱动程序重写此方法来执行改变设备电源状态的工作。

13、Serial

这串行系列提供对串行字节字符流的支持。

Bundle标识符

  • com.apple.iokit.IOSerialFamily

头文件位于

  • 内核常驻:Kernel.framework/Headers/IOKit/serial/
  • 设备接口:IOKit.framework/Headers/serial/

参考文献及规格

  • termios
    另请参阅相关头文件Kernel.framework/Headers/sys/termios.h

类别层次


设备接口

  • 应用程序可以通过 BSD 设备节点(该系列中最常见的客户端)访问串行系列。
    应用程序可以使用 中的 BSD 设备节点读取和写入数据/dev
    数据还可以路由到通过这些设备节点进行 PPP。
    您可以在 中找到用于设备访问的密钥和其他属性IOKit.framework/Headers/serial/IOSerialKeys.h

Table A-12 Clients and providers of the Serial family

Client of the nub 核心提供者
行动 需要具有基本流量控制的单波段数据流服务。 提供 单频带流媒体服务;换句话说,它不能基于数据包。 尽管它可能是双向的。 驱动程序还可以实现流量控制。 驱动程序必须描述它能够提供的服务。
串口编写器应该子类化IOSerialDriverSync 为其驱动程序,然后发布为IOModemSerialStreamSync 或IORS232SerialStreamSync 类,或者(如果这些都不够的话)一个具体的子类IOSerialStreamSync. I/O Kit 使用这些对象来创建适当的用户客户端接口,以便通过 BSD 进行用户空间访问。
笔记 开发人员应该使用BSD 设备文件机制记录在*从应用程序访问硬件* 中。

14、Storage

这存储系列为随机访问大容量存储设备提供高级支持。

它与将数据传输到所表示的存储空间和从所表示的存储空间传输数据的底层技术是分开的。

底层传输技术的接口在抽象类中声明IOBlockStorageDevice。

存储驱动程序对象通过此接口传达所有大容量存储请求,而无需了解或参与用于与设备或总线通信的命令和机制。

存储系列的范围包括 IOBlockStorageDevice 接口(一端)(提供者方向)和 BSD 接口(另一端)(客户端方向),中间有各种驱动程序和媒体层。
图 A-1说明了此堆栈。


图 A-1 存储系列驱动程序堆栈


每一层由两个对象组成------一个驱动程序对象和它发布的子媒体对象(或多个对象)。

IOStorage 类是驱动程序和媒体对象的通用基类。

它是一个抽象类,声明了子类要实现的基本打开、关闭、读取和写入接口。

它建立了媒体对象可以与驱动程序对象通信的协议,而无需为每个驱动程序创建子类。

读取和写入接口提供对存储空间的字节级访问。

这IOBlockStorageDriver 类是通用块存储驱动程序的通用基类。

它通过IOBlockStorageDevice 接口,并通过 IOStorage 协议连接到存储框架的其余部分。

它通过实现适当的打开和关闭语义、对未对齐传输进行解块、轮询可弹出媒体、实现锁定和弹出策略、创建和拆除媒体对象以及收集和报告统计数据来扩展 IOStorage 协议。

存储系列通过以下子类支持其他基本类型的驱动器,例如 CD 驱动器和 DVD 驱动器IOBlockStorageDriver. 您很少(如果有的话)需要子类化通用块存储驱动程序来处理设备特性;相反,您应该更改底层传输驱动程序来纠正任何不符合要求的行为。

这IOMedia 类是随机访问磁盘设备的抽象。

它相当于其他 I/O Kit 系列中的"设备对象"或"接口对象"。

  • 它表示一个设备或其中一个部件的存在。
  • 它为该设备提供了一个编程接口。
  • 它由实现所需功能的单独驱动程序支持。

IOMedia 为真实和虚拟磁盘设备、磁盘分区(如分区)、磁盘超集(如 RAID 卷)等提供了一致的接口。

它通过为媒体对象实现适当的打开、关闭、读取、写入和匹配语义来扩展 IOStorage 类。

它的属性反映了真实磁盘设备的属性,包括自然块大小和可写性。

存储系列中的其他驱动程序和媒体层称为过滤方案。

这些可选层将媒体对象分开,提供媒体对象之间的某种数据操作或偏移操作。

这些层可以任意堆叠在一起,因为它们既是媒体对象的客户端,也是媒体对象的提供者。

大多数第三方都会为过滤方案层开发驱动程序,如果不是为底层传输技术开发的话。

有关开发过滤方案驱动程序的更多信息,请参阅 IOMedia 过滤方案。

有关为底层传输技术开发驱动程序的更多信息,请参阅 SCSI 架构模型系列文档。

SCSI 架构模型系列是 ATAPI、FireWire、SCSI 和 USB 的通用底层传输技术。

它为应用程序编写者和驱动程序编写者提供了一致的基于 CDB 的访问模型,以及用于纠正设备特性的简单基础结构。

Bundle标识符

  • com.apple.iokit.IOStorageFamily

头文件位于

  • 内核常驻:Kernel.framework/Headers/IOKit/storage/
  • 设备接口:IOKit.framework/Headers/storage

类别层次


设备接口


IOMedia 过滤器方案

A过滤方案是驱动IOMedia 对象。

它既是媒体对象的客户端,又是媒体对象的提供者。

过滤方案驱动程序通过其抽象readwrite成员函数接口接收大容量存储请求,在这些接口中,它可以在将请求传递给其提供者 IOMedia 对象(或多个对象)之前执行数据操作或偏移操作。

有几种不同类型的媒体过滤方案:

  • 一对一 ------ 例如,块级压缩或加密方案将与一个 IOMedia 对象匹配,并生成一个代表未压缩或未加密内容的子 IOMedia 对象。
  • 一对多 ------ 例如,分区方案将与一个 IOMedia 对象匹配,并生成多个子 IOMedia 对象来代表每个不同分区的内容(有关更多信息,请参阅 分区方案)。
  • 多对一 ------ 例如,RAID 方案将与多个 IOMedia 对象匹配,并生成一个代表 RAID 内容的子 IOMedia 对象。
  • 多对多 ------ 多对多方案将匹配多个 IOMedia 对象并产生多个子 IOMedia 对象。

过滤方案驱动程序继承自IOStorage 类并且必然参与 IOStorage 匹配类别(个性中的 IOMatchCategory 的值)。


分区方案

OS X 包含两个标准分区方案:

  • IOApplePartitionScheme,标准 Apple 分区方案驱动程序
  • IOFDiskPartitionScheme,标准 PC 分区方案驱动程序

分区方案驱动程序继承自IOPartitionScheme 类(继承自 IOStorage)与单个 IOMedia 父级匹配,并为其必须表示的每个分区生成一个或多个 IOMedia 子对象。

与所有筛选方案驱动程序一样,它参与 IOStorage 匹配类别。

无需对分区方案进行子类化即可使用分区内开发人员定义的内容。

分区的内容由不同的 IOMedia 对象表示,该对象作为分区方案驱动程序的子对象发布。

每个子媒体对象都具有进一步识别有关分区的已知信息的属性,例如内容提示、大小和自然块大小。

内容提示字段是一个字符串,其格式类似于众所周知的"Apple_Driver"和"Apple_HFS"字符串,或者根据定义,格式为"MyCompany_MyContent"。

它允许唯一地标识具有开发人员定义内容的分区(创建分区时),并允许过滤方案驱动程序自动匹配此类内容,而无需探测磁盘。


IOMedia 属性

表 A-13列出了所有IOMedia 对象。

这些属性可用作 I/O Kit 搜索和通知 API 中的匹配属性,以及用于过滤方案驱动程序匹配目的。


Table A-13 Storage family (IOMedia) properties

钥匙 类型 描述
kIOMediaEjectableKey 布尔值 介质可以弹出吗?
kIOMediaLeafKey 布尔值 媒体是否是媒体树中的叶子?只要客户端过滤方案驱动程序与媒体对象匹配,则为假。
kIOMediaPreferredBlockSizeKey 数字 媒体的自然块大小(以字节为单位)。
kIOMediaSizeKey 数字 媒体的整体大小(以字节为单位)。
kIOMediaWholeKey 布尔值 介质是否位于介质树的根部?对于物理介质表示、RAID 介质表示和类似表示,都是如此。
kIOMediaWritableKey 布尔值 该媒体是否可写?
kIOMediaContentKey String 媒体的内容描述,由客户端过滤方案驱动程序强制执行。
(此内容描述自动从客户端过滤方案驱动程序的kIOMediaContentMaskKey属性中复制。)字符串的格式遵循"MyCompany_MyContent"约定,如果没有客户端过滤方案与对象匹配,则默认为内容提示字符串。 用于用户磁盘实用程序中的信息目的。
kIOMediaContentHintKey String 媒体的内容描述,如对象创建时所暗示的。 字符串的格式遵循"MyCompany_MyContent"约定。 用于过滤方案驱动程序中的匹配目的。
kIOBSDNameKey String 媒体的 BSD 设备节点名称。 此名称在对象创建时动态分配。 用于对媒体内容进行读写访问(请参阅 从应用程序访问 IOMedia)。

媒体对象还具有唯一的 I/O Kit 路径,可以通过标准 I/O Kit API 获取。


从应用程序访问 IOMedia

访问介质上数据的标准用户空间机制是BSD 设备接口。

BSD 设备接口在文件系统层通过不同的读写 API 抽象化,而一般用户应用程序访问则通过readwrite系统调用提供(有关更多文档,请参阅相应的手册页)。

物理磁盘的属性和结构在I/O Kit Registry 对象层次结构及其中每个媒体对象的属性,包括分配给媒体对象的 BSD 设备接口名称。

可以通过标准 I/O Kit 搜索和通知 API 找到特定媒体对象。

用于描述 IOMedia 的字典可能引用特定属性值、特定服务路径或设备树路径,或特定媒体子类。

例如,CD 会显示为IOCDMedia 子类,具有适合 CD 的属性,例如kIOCDMediaTOCKey

这些属性可以组合起来,在系统中唯一地描述媒体对象,或概括为识别具有多个可能匹配项的特定类型的媒体(在迭代器中返回)。

Carbon API 和 Cocoa API 提供了发送文件系统挂载和卸载事件通知以及文件系统内文件访问的机制。

应用程序可以通过Carbon 中的GetVolParms( ) 和BSD 中的 ( ) 获取给定文件系统的关联 BSD 设备接口名称。

可以使用属性通过 I/O Kit API 获取给定 BSD 设备接口名称的关联 IOMedia 对象。
vMDeviceID``getmntinfo``kIOBSDNameKey


15、USB

这USB 系列为连接到通用串行总线 (USB) 的设备提供支持和访问。

此系列的客户端驱动程序有两种基本类型:内核模式驱动程序和用户模式驱动程序。

当驱动程序的客户端也驻留在内核中时(例如 HID 设备、大容量存储设备或网络设备),需要使用内核模式驱动程序。

当只有一个进程可以访问设备(例如打印机和扫描仪)时,首选用户模式驱动程序。

Bundle标识符

  • com.apple.iokit.IOUSBFamily

头文件位于

  • 内核常驻:Kernel.framework/Headers/IOKit/usb/
  • 设备接口:IOKit.framework/Headers/usb

参考文献及规格

Apple 开发者连接--- https://developer.apple.com/hardwaredrivers/

类别层次


设备接口

  • 用户模式客户端使用 I/O Kit 框架中的 API;此 API 定义在 中IOKit/usb/IOUSBLib.h
    客户端使用IOUSBDevice 和内核中存在 IOUSBInterface 类,用于与 USB 设备或 USB 接口进行通信。

内核驻留驱动程序

  • 可以为 USB 设备或 USB 接口编写物理 USB 设备的内核驱动程序。
    物理 USB 设备由一个可以描述任意数量接口的设备描述符组成。
    编写驻留在内核的驱动程序时,您需要决定驱动程序是控制整个 USB 设备还是仅控制 USB 设备的一个接口。

驻留在内核的 USB 驱动程序系列由三个主要类组成:

  • IOUSB设备 :IOUSBDevice 类是物理 USB 设备的抽象。
    每个连接到总线的 USB 设备都有一个 IOUSBDevice 类实例。
    IOUSBDevice 对象的提供程序是IOUSBController 对象(USB 控制器的抽象)。
  • IOUSB接口 :IOUSBInterface 类是 USB 设备接口之一的抽象。
    设备中的每个接口都会实例化一个此类。
    创建时,IOUSBInterface 会为接口描述符中描述的每个端点创建 IOUSBPipe 对象。
    IOUSBInterface 对象的提供程序是 IOUSBDevice 对象。
  • IOUSBPipe :IOUSBPipe 类包含用于与 USB 设备或 USB 接口通信的方法。
    为默认控制端点创建一个 IOUSBPipe 对象,并为接口描述符中描述的每个端点创建一个额外的 IOUSBPipe 对象。
    IOUSBPipe 对象的提供程序是 IOUSBInterface 对象。

驻留在内核的 USB 驱动程序是提供传输服务的系列的客户端,也是其类继承自的系列的成员。

例如,USB 键盘的驱动程序是 IOUSBInterface 对象的客户端(即,IOUSBInterface 对象是驱动程序的提供程序),但键盘驱动程序是 IOHIDFamily 的成员。

USB 系列提供获取键盘按键的机制。

键盘驱动程序支持 HID 系列的方法,用于将这些按键发送到事件系统。

如上所述,您可以编写驱动程序来匹配 USB 设备或 USB 接口。

IOUSBDevice 或IOUSBInterface 类是驱动程序的提供者。

驱动程序本身可以是单独系列的成员(例如 IOHIDFamily 或 IOAudioFamily),也可以是 IOService 系列的成员。

电源管理

所有内核 USB 设备驱动程序都应至少实现基本电源管理,以提高系统的节能效果。

在 OS X v10.5 中,USB 系列引入了对象IOUSBHubPolicyMaker,它是包含电源管理功能的 USB 集线器的抽象。

当您开发在 OS X v10.5 及更高版本中运行的 USB 设备驱动程序并调用时joinPMtree,您的驱动程序将作为对象的电源子级附加到电源平面中IOUSBHubPolicyMaker

(在早期版本的 OS X 中,USB 设备驱动程序的电源父级是一个IOUSBController表示设备所连接的控制器的对象。)

您的 USB 设备驱动程序可以与其IOUSBHubPolicyMaker对象通信以确定其集线器的电源状态。

例如,如果您需要以不同于重启的方式处理即将关机,那么这会很有用。
IOUSBHubPolicyMaker对象支持集线器的以下五种电源状态:

  • 亮起。
    集线器功能齐全,并且至少有一个端口处于活动状态(即未暂停)。
  • 睡眠。
    如果集线器支持睡眠,则其端口处于非活动状态,并且不会向任何连接的设备供电;如果不支持,则睡眠状态与关闭状态相同。
  • 打瞌睡。
    当集线器的所有端口都处于挂起或断开连接状态,并且所有连接的设备都处于关闭或打瞌睡状态时,集线器可以进入这种空闲省电状态。
    并非所有集线器都支持打瞌睡状态。
  • 关闭。
    当系统即将关闭时,集线器进入此状态。
  • 重新启动。
    此状态与关闭状态相同,只是当系统即将重新启动时,集线器会进入此状态。

USB 设备很少支持前四种以上的电源状态,许多设备仅支持开启、睡眠和关闭。

如果您正在开发支持打盹模式的 USB 设备的驱动程序,则应SuspendDevice在将设备的电源状态切换为打盹模式时调用,以便集线器可以暂停设备所连接的端口。

如果连接到集线器的所有设备的驱动程序都这样做,则集线器可以进入打盹状态,这可以显著节省系统电量。

但是,如果您的驱动程序调用SuspendDevice时没有将设备的电源状态也更改为打盹模式,则会阻止集线器进入打盹状态并节省电量。

如果您的设备所连接的集线器不支持睡眠,则当系统进入睡眠状态时,设备必须进入关闭状态。

当然,如果您的设备不支持睡眠,则当系统进入睡眠状态时,它也必须进入关闭状态,即使其集线器支持睡眠。

在电源状态更改期间,请注意,USB 集线器的电源状态不会更新,直到集线器收到调用powerChangeDone,而这只有在所有下游集线器和设备都收到powerChangeDone调用后才会发生。

这意味着,如果您使用OS X v10.5 中引入的getPowerState调用来获取上游集线器的当前电源状态,您可能会收到过时的电源状态信息。

但是,如果您的设备正在更改为开启状态,您可以假设上游集线器实际上处于开启状态,即使它们的电源状态值尚未更新。

Client of the nub 核心提供者
行动 驱动插入 USB 端口的设备。 驱动 USB 总线控制器。
例子 键盘驱动程序是一种接口驱动程序;其提供者是 USB设备 的驱动程序。 键盘驱动程序继承自IOHIKeyboard 类------它是 HID 系列的成员。
IOUSBDevice 驱动程序只能通过默认控制管道与 USB 设备通信。 IOUSBInterface 驱动程序为当前配置的接口描述符中描述的所有端点创建了 IOUSBPipe 对象。 驱动程序使用这些对象与设备通信。 USB 家族成员驱动程序应继承自IOUSBController 类。
笔记 常见的客户 families 包括 HID families (IOHIPointing 和 IOHIKeyboard 类)以及存储系列设备的传输驱动程序。 USB 系列的大多数内核模式客户端都是接口驱动程序,偶尔才是设备驱动程序。 OS X 包含支持所有开放主机控制器接口 (OHCI) 总线控制器。 一般来说,第三方开发商不需要为 USB 系列编写驱动程序。

驱动程序匹配 :USB 家族使用USB 通用类规范,修订版 1.0,用于将设备和接口与驱动程序进行匹配(有关此规范的链接,请参阅上文"参考和规范"部分)。

驱动程序应使用此规范中定义的键来创建定义特定Driver personalities的匹配字典。

规范中有两个键表。

第一个表包含用于查找设备的键,第二个表包含用于查找接口的键。

两个表均按特异性顺序显示键:每个表中的第一个键定义最具体的搜索,最后一个键定义最广泛的搜索。

每个键由表左列中列出的元素组合组成。

为了成功匹配,您必须将恰好一个键的元素添加到您的个性中。

如果您的个性包含任何键都未定义的元素组合,则匹配过程将找不到您的驱动程序。

例如,如果您尝试匹配设备并将代表该设备的供应商、产品和协议的值添加到匹配字典中,则即使设备描述符中具有相同值的设备当前由 I/O 注册表中的 IOUSBDevice nub 表示,匹配过程也会失败。

这是因为没有将供应商编号、产品编号和协议元素组合在一起的键。


16、不包含 I/O Kit 系列的设备

有些类别的设备没有 families 支持I/O Kit。

一般来说,有三个原因导致某个设备可能不受 I/O Kit 系列支持。

  • 其他框架提供对某些设备的支持。
    I/O Kit 并不是最适合代表这些设备的抽象的地方。
    此类设备的示例包括打印机、扫描仪、数码相机和其他成像设备。
    如果您正在为此类设备开发驱动程序,则应使用适当的成像 SDK。
  • 对于某些设备,不可能提供一组有用的通用抽象。
    此类设备可能包括 USB 安全加密狗、数据采集卡和其他特定于供应商的设备。
    这些设备没有足够多的共同特征,因此创建 I/O Kit 系列并不值得。
    例如,尽管所有安全加密狗都通过 USB 连接,但没有一组易于定义的抽象适用于所有此类设备。
    I/O Kit 系列不会为开发人员提供实质性的帮助。
    但是,不应假设编写新驱动程序需要系列。
    在许多情况下,该类IOService提供了编写"无系列"驱动程序所需的一切。
  • 对于许多设备,可以定义一组有用的抽象;但是,由于一个或多个原因,Apple 并未选择创建一个系列。
    这些设备可能属于 Macintosh 市场上不常见的技术。
    或者,Apple 的工程师可能对某些设备没有足够的内部专业知识来创建最佳的系列定义。
    在这些情况下,第三方开发人员有机会通过开发自己的系列来扩展 I/O Kit 模型。
    此外,根据 Apple 公共源代码许可证开发的系列可以发回 Apple,以便可能纳入未来的 OS X 版本中。

影像设备

没有适用于成像设备(照相机、打印机和扫描仪)。

相反,对特定成像设备特性的支持由用户空间代码处理(有关进一步讨论,请参阅 从内核外部控制设备)。

需要支持成像设备的开发人员应使用适当的成像 SDK。


数字视频

加上为您的软件提供数字视频功能,使用QuickTime API。


顺序存取设备(磁带驱动器)

目前没有专门针对以下情况设计的 I/O Kit 系列:顺序访问设备,例如磁带驱动器。

但是,第三方开发人员可以使用SAM 设备接口用于为此类设备创建插件组件。


电话设备

目前没有适用于电话设备。

Apple 正在评估未来电话系列的计划。


特定供应商的设备

对于某些设备,无法提供一组有用且通用的抽象。

由于系列定义了系列内所有设备共享的抽象集,因此为这些设备创建系列是不可行的。

但是,在大多数情况下,编写这些设备的驱动程序时不需要系列。

开发人员应该从继承功能开始IOService,然后使用GetPropertySetProperty调用与驱动程序通信。

在许多情况下,这应该就足够了。

但是,在某些情况下,例如需要高带宽的数据采集卡,开发人员应该创建自己的用户客户端(用于设备接口插件)。

此类对象可以为用户空间库提供共享内存和过程调用接口(请参阅)。
IOUserClient.h您可以在 Darwin 开源站点上的 IOKitExamples 中找到几个很好的例子。


2024-06-04

相关推荐
Cod_Next2 小时前
Mac系统下配置 Tomcat 运行环境
java·macos·tomcat
ZVAyIVqt0UFji4 小时前
iOS屏幕共享技术实践
macos·ios·objective-c·cocoa
程序员鱼皮6 小时前
我发现很多程序员都不会打日志。。
计算机·程序员·开发·编程经验·java程序员
Zhijun.li@Studio7 小时前
Mac下的vscode远程ssh免密码登录
vscode·macos·ssh
SoraLuna7 小时前
「Mac玩转仓颉内测版25」基础篇5 - 布尔类型详解
开发语言·算法·macos·cangjie
黑色叉腰丶大魔王1 天前
《macOS 开发环境配置与应用开发》
macos
Amd7941 天前
Nuxt.js 应用中的 webpack:configResolved事件钩子
webpack·自定义·开发·配置·nuxt.js·构建·钩子
Enougme1 天前
mac安装appuim
macos·appium
青花瓷1 天前
虚拟机苹果OS当中XCode安装后如何增加对ios的支持
macos·ios·xcode
刘小哈哈哈2 天前
iOS 键盘弹出视图精准上移
macos·ios·cocoa