I/O Kit基础

本文内容来自于Apple官方的I/O Kit Fundamentals

I/O Kit是一套基于C++语言子集构建的框架、库、工具和资源,它的特性包括:

  • 设备的动态及自动配置(即插即用)
  • 电源管理(例如,休眠模式)
  • 抢占式多任务
  • 对称多处理
  • 面向对象的框架,提供各类设备的抽象
  • 多线程、通信、数据管理原语
  • 适用于所有总线的匹配加载(match-and-load)机制
  • I/O Registry,一个保留着所有已初始化对象的数据库
  • I/O Catalog,一个保留着系统内所有可用I/O Kit类的数据库
  • 设备接口,一种允许用户空间程序同驱动交互的插件机制
  • 使用C++语言的一个子集,不支持异常、多重继承、模板和RTTI,但是支持命名空间,推荐使用Reverse-DNS命名法为命名空间起名字

I/O Kit由几类资源构成:

  • 框架和库:I/O Kit基于三个C++库,这三个库都被封装为框架。但是,只有IOKit.framework是一个真正的框架。Kernel.framework的主要用途是提供内核的头文件,包括libkern和IOKit,而这些库的代码实际上被编译进了内核中。
    • Kernel/IOKit:这个库用于开发驻留在内核空间的设备驱动,头文件位于Kernel.framework/Headers/IOKit
    • Kernel/libkern:这个库包含用于开发内核空间软件的类,头文件位于Kernel.framework/Headers/libkern
    • IOKit:用于开发设备接口,位于IOKit.framework
  • 应用程序和工具
    • Xcode:IDE,提供了开发kext的工程模板
    • I/O Registry Explorer:使用图形化的方式探索I/O Registry的内容和结构
    • Package Maker:为应用程序(包括驱动)创建安装包
    • 命令行工具
      • ioreg:使用命令行的界面输出I/O Registry的内容
      • kextload:加载一个内核扩展或者为远程调试生成一个静态链接的符号文件
      • kextunload:卸载一个内核扩展(可能失败,因为有些内核扩展不能卸载)
      • kextstat:显示内核I/O统计信息,包括终端、磁盘和CPU操作
      • ioclasscount:显示指定类的实例个数
      • ioalloccount:显示I/O Kit对象在内核中使用内存数量的信息
      • kextcache:制作内核扩展的缓存文件,加速内核在引导时加载扩展的速度
  • 其他资源:文档和头文件等

架构

驱动的层次

服务和设备的关系图由计算机的主板(以及主板的驱动)开始,经过设备发现和驱动匹配过程,先连接到控制系统总线的驱动对象,最终拓展到每一个连接到这些总线上的设备和服务。

设备族和驱动

I/O Kit设备族(Family)是由一个或多个C++类实现的软件抽象,它抽取了某种类型设备的公共特征。I/O Kit为总线协议(例如,SCSI、USB、FireWare等)、存储设备(磁盘等)、网络服务(以太网等)、HID设备(例如鼠标、键盘、游戏操纵杆等)等等提供了设备族。

驱动程序通过继承的方式将自己划归于某个设备族,驱动程序的类基本上就是设备族类的子类。

一个驱动D通常涉及到两个设备族。除了它所继承的设备族A,驱动类还必须同一个nub通信,而这个nub是由该设备所连接到的总线或者协议所属的设备族B提供的。这里的总线或者协议(例如USB、PCI)设备族B的角色是一个provider,这个provider通过它创建的nub提供它的接口。所谓nub,是一个定义了特定协议的访问入口通讯信道的对象。驱动D通过nub附着到I/O Registry上,并通过nub同它所控制的设备通信。例如,一个PCI以太网驱动会使用一个来自于PCI设备族的IOPCIDevice nub同PCI总线通信。驱动程序通过nub通信的内容主要是向总线发送请求或命令。例如,一个SCSI驱动会通过nub发送SCSI命令并检查结果。

驱动和nub

I/O Kit支持两种大类的驱动对象。第一种是nub,它定义了访问入口和通信信道,通常用于访问一个协议,例如PCI、USB或者以太网。第二种是针对某个设备或者服务的具体驱动程序。具体的驱动程序通过nub同硬件通信,执行I/O操作。

驱动被实现为内核扩展,通常安装在/System/Library/Extensions目录。

当已经为设备选中了某个驱动,但是还没有将驱动程序所在的扩展还没有被加载到内核中,这款驱动程序所需的所有设备族(它的祖先类和依赖关系)都会按需加载起来。在所有依赖关系都满足了之后,就会加载驱动程序并初始化为一个对象。

Nub是一个I/O Kit对象,它代表连接到设备或者逻辑服务的通信信道,作为访问设备或服务的媒介。例如,一个nub可能带包一个总线、一个磁盘、一个磁盘分区、一个图形适配器或者一个键盘。将nub想象成一个设备插槽或者连接器的软件表示可能有助于理解。Nub也提供诸如仲裁、电源管理和设备匹配这样的服务。

Nub是两个驱动(或者更为宽泛地说,两个设备族)之间的桥梁。驱动程序A是nub(以及nub所在设备族)的client;驱动A自己也可以通过它的设备族向外发布一个nub,对于这个nub所匹配到的驱动B来说,驱动A就成了provider。通常,驱动会为它控制的每个设备或服务单独提供一个nub;不过,如果设备比较特殊,驱动也可能作为自己的nub。

I/O连接解析

I/O Kit的分层架构将系统硬件总线和设备抽象为I/O连接的链条。当前层是下层的client,是上层的provider。

驱动对象的client/provider关系

从上图可以看出,你的驱动程序通常会同两个设备族发生关系,一是继承自上层设备族的类,二是使用底层设备族的服务。以以太网控制器为例,驱动涉及到很多网络和PCI设备族的C++对象:

角色 功能
IONetworkStack 管理网络接口的对象 将I/O Kit对象连接到BSD网络模块。
IOEthernetInterface nub 管理同设备无关的数据传输和接收过程。
网卡控制器驱动 驱动程序 通过IOPCIDevice对象操纵以太网控制器。这个对象继承自网络设备族的IOEthernetController。
IOPCIDevice nub 控制器的匹配点;提供同PCI总线上控制器的基本交互。
IOPCIBridge 驱动程序 管理PCI总线。(其他对象为IOPCIBridge提供服务,它们具体的功能依赖于硬件配置。)

另一种看待驱动对象栈的方式是考虑栈的动态视角,也就是,当一个OS X系统发现了一个新设备时会发生什么?驱动对象栈是怎么构造出来的?我们现在以SCSI硬盘为例了解一下这个过程。

SCSI磁盘驱动的驱动对象连接情况

上图描述了一个SCSI磁盘驱动,它属于Storage设备族,连接到PCI总线。随着连接的逐步建立,新创建的驱动和nub也都被同步添加到I/O Registry中。这条连接链条的建立分为以下几个步骤:

  1. PCI总线控制器驱动(属于PCI设备族)发现了一个PCI设备,然后创建了一个nub(IOPCIDevice实例)对外宣告它的存在。
  2. 这个新创建的nub匹配到一个适合于它对应设备的驱动程序——也就是这里的SCSI控制器驱动——并请求加载这个驱动程序。加载SCSI控制器驱动的过程会造成SCSI Parallel设备族和所有所依赖的设备族被加载。IOPCIDevice nub的引用会发送给新创建的SCSI控制器驱动。
  3. SCSI控制器驱动是PCI设备族的client,也是SCSI Parallel设备族的provider。它扫描SCSI总线搜索能够支持的设备。在找到磁盘由,它创建一个nub(IOSCSIDevice)来宣告磁盘设备的存在。
  4. IOSCSIDevice这个nub也会去匹配一个合适的设备驱动,然后请求系统加载这个驱动。新加载的磁盘驱动是SCSI Parallel设备族的client,也是Storage设备族的一员。IOSCSIDevice nub的引用会被发送给新创建的磁盘驱动。

在很多时候,用户空间的应用程序也可以通过I/O Kit的设备接口技术来驱动设备。

设备驱动的运行环境

运行时的特点

I/O Kit的驱动可能会在任意时刻被加载/卸载、激活/去活,触发这些动作的原因可能有USB设备被插入或者拔除、通过软件启用/禁用网络接口等等。I/O Kit为驱动对象定义了一个标准的生命周期。通过实现一组方法,你就可以自如地处理设备的添加和删除,也能应对电源管理系统引入的变化。

在进行几乎任何I/O操作之前,都需要做一下准备工作:

  • 将虚拟内存页面换入到物理内存
  • 将内存标记为联动(wired)内存,以免它在做I/O操作时被换出物理内存
  • 构建(DMA用的)scatter/gather列表来描述要读/写的数据缓冲区

I/O Kit提供了一组工具类来帮助驱动程序开发者准备用于I/O操作的内存及构建scatter/gather列表,例如IOMemoryDescriptor和IOMemoryCursor等类。

驱动程序运行在一个多线程系统中,必须要保护它的数据以免被可重入和并发访问所破坏。I/O Kit针对这个问题也提供了一组类。工作循环(Workloop)对象运行在一个专用的线程内,为独占访问数据提供了闸门机制。其他的称作事件源的对象使用闸门机制来序列化执行函数调用来访问临界资源,并在调用函数之前关闭工作循环的闸门。

作为I/O Kit的基础,libkern C++库者提供了几类通用的操作:

  • 原子化的算术和逻辑操作
  • 字节序的转换
  • 常用容器,例如字符串、数组和字典

内核编程的约束

内核代码总是驻留在物理内存中,不能被pager换出。因此,内核资源要比应用程序资源宝贵得多。如果你的驱动符合下面的条件,才适合放在内核中:

  • 需要处理primary interrupt(一级中断,与之对应的是secondary interrupt)
    • primary interrupt:直接由中断处理器(interrupt handler)处理的中断
    • secondary interrupt:由中断服务线程处理的中断,也就是转了一手,这类中断存在的目的是避免占用中断控制器
  • 它的主要使用者驻留在内核中,例如,由于文件系统栈驻留在内核中,所以大容量存储设备的驱动一定要放在内核中

例如,磁盘、网络控制器、键盘的驱动驻留在内核中。如果你的驱动只是偶尔由一个用户空间程序使用,那么你应该在这个用户空间程序中加载这个驱动。

I/O Registry和I/O Catalog

I/O Registry是一个动态的数据库,它记录着反映了当前硬件连接关系的驱动对象网络的信息,还在这些对象之间维护provider-client的关系。对于绝大多数的I/O服务来说,驱动对象必须被记录在I/O Registry中才能起作用。

当一个硬件被加入到系统中时,系统会自动查找并加载所需的驱动程序并更新I/O Registry来反映这个设备引发的变化;当硬件被移除时,对应的驱动程序会被卸载,然后再次更新I/O Registry。I/O Registry一直驻留在内存中,不会被保存到硬盘中。

I/O Registry将它的数据组织为一棵树,几乎所有的节点都代表一个驱动对象:nub或者一个真正的驱动程序。这些对象必须继承自IORegistryEntry类。IORegistryEntry是IOService的父类,而后者是所有驱动类的父类。IORegistryEntry的核心数据结构是一组关联属性(Associated Properties),这些属性反映了驱动的个性(personality),在搜索驱动时会根据这些属性做匹配,此外,这些属性还可以为驱动额外提供信息。在I/O Registry中保存的属性就来自于每个驱动的属性列表(plist),这个属性列表描述了驱动的特性、配置和要求。

另外一个动态数据库叫I/O Catalog,它和I/O Registry协同工作。I/O Catalog维护者系统中所有可用的驱动。当一个nub发现一个设备时,就会从I/O Catalog请求列出这个设备所在设备族的所有驱动。

驱动的匹配

nub对象的一个主要功能是提供驱动匹配服务,也就是找出最匹配设备的驱动。当一个nub检测到一个设备时,I/O Kit会通过三个步骤逐步筛掉不符合的驱动,剩下的就是最适合设备的驱动。这三个步骤如下:

  1. 类匹配(Class Matching):首先排除掉设备类(device class)不符合的驱动。
  2. 被动匹配(Passive Matching):比对驱动的个性(personality)和设备的特点,排除掉不匹配的驱动。
  3. 主动匹配(Active Matching):调用剩下的驱动去主动探查(probe)设备来验证驱动是否可以真正驱动设备。

当找到匹配的驱动后,就会加载驱动的代码,然后实例化kext的属性列表中个性(personality)指定的主类。

I/O Kit的类层次结构

I/O Kit的类层次可以粗略的分为三大类:

  • libkern提供的类(以OS为前缀,所以也叫OS类)
  • I/O Kit的基类和辅助类
  • I/O Kit各个设备族的类

它们的关系可以用下图表示:

OS类

I/O Kit构建于libkern库之上,I/O Kit相关类的父类是IORegistryEntry,它继承自libkern的OSObject。I/O Kit只支持C++的一个特性子集,不支持异常和RTTI,作为替代,OS的基础类实现了一个类似于RTTI的功能。

OS类的根是OSObject,与之紧密相关的是OSMetaClass类,剩下的类都是辅助类,提供类似于容器等功能。下面简要介绍一下这些类的角色:

  • OSMetaClass:实现了RTTI机制,可以通过类名在运行时分配类的实例。
  • OSObject:支持引用计数的API(retian和release),还提供了init和free的默认实现。
  • 数据容器类:它们继承自OSObject,封装了各种数据类型(例如布尔类型、数值类型、字符串类型等)和容器(例如数组和字典)。

OS类适用于所有内核代码,不仅适用于设备驱动,例如实现网络服务和文件系统的kext都可以使用它。

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

通用I/O Kit类

中间这一层类由I/O Kit的基类(包括IORegistryEntry和IOService)和相关的辅助类组成,这些辅助类提供了诸如资源管理、数据管理、线程和输入控制等功能。

I/O Kit部分的根类是IORegistryEntry,继承了它,就可以将自己作为I/O Registry中的一个节点,还获得了管理一个或者多个属性表的功能。IORegistryEntry的功能如下:

  • 通过attach和detach方法管理到I/O Registry的连接
  • 使用OSDictionary对象管理驱动的个性(personality)
  • 它实现了对I/O Registry的锁定,使得能够原子地更新I/O Registry

IOService是IORegistryEntry的唯一直接子类,几乎所有的I/O Kit设备族类都直接或间接地继承自IOService。更为重要的是,IOService规定了设备驱动的生命周期。通过成对的虚函数(例如init/free、start/stop和open/close),IOService定义了驱动对象如何初始化、如何附着到I/O Registry、如何分配必要的资源,以及这些操作的逆操作。而为了实现对驱动生命周期的管理,IOService提供了匹配服务(辅以设备探查)并负责根据provider来初始化驱动。此外,IOService还包含其他各种功能:

  • 通知和消息
  • 电源管理
  • 设备内存(映射和访问)
  • 设备中断(注册、反注册、使能、触发等等)

大多数辅助类里面都有几个同设备驱动的动态运行环境相关的类:

  • 实现工作循环、事件源(中断、定时器、命令)、锁和队列
  • 实现内存游标(memory cursor)和内存描述符(memory descriptor),管理用于I/O的数据

I/O Kit设备族类

大多数驱动都可以归为某个I/O Kit设备族,此时,就可以使用设备族类作为驱动类的父类。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

随着系统的演化,上面的列表也会随之变化。如果你的设备不属于任何已有的设备族,那么你可以开发自己的设备族类;不过要注意,设备族不是必须的,你完全可以开发一个不属于任何设备族的驱动。

从内核之外控制设备

设备接口(Device-Interface)机制

所谓设备接口,是一种位于内核和用户空间进程之间的插件机制。这个插件的接口符合Core Foundation定义的插件服务(CFPlugin)架构,后者也在大体上同微软的COM模型兼容。在这个CFPlugin模型中,内核的角色是插件的宿主,它提供了一组定义好的I/O Kit接口。I/O Kit为应用程序提供了一组插件(设备接口)以供使用。

概念上,设备接口跨越了用户空间和内核空间之间的边界。类似于驻留在内核中的驱动,它也会处理协商、认证和类似的任务。在用户空间一侧,它通过暴露出来的接口同应用程序通信。在内核一侧,它同nub通信。从内核的角度来看,设备接口看起来就是一个设备驱动。从应用程序的角度来看,设备接口看起来就是一组函数,你通过调用这些接口,可以将数据传送到内核,还可以从它接收到内核发回来的数据。

下面我们来看一个使用设备接口的例子:

通过设备接口控制SCSI设备

一开始,会系统会经历同主流内核的驱动一样的过程:发现设备、创建nub、匹配驱动、加载驱动,直到创建出SCSI设备nub。从这个点开始,流程就会不同。此时,SCSI nub会将设备接口(而不是主流内核的驱动程序)作为它的驱动程序加载起来。

应用程序必须通过“设备匹配”过程先找到设备,然后才能使用设备接口访问它。应用程序首先需要创建一个“匹配字典”,在“匹配字典”中规定目标设备的特性,然后调用一个I/O Kit函数,将这个字典传递给它。这个函数会搜索I/O Registry然后返回一个或者多个匹配到的驱动对象,应用程序可以用这个驱动对象来加载合适的设备接口。

如果现有的设备接口不能满足你的需求,你可以开发自己的设备接口,在设备接口中,你可以使用如下方法在内核和用户空间之间传递数据:

  • BSD系统调用
  • Mach IPC
  • Mach共享内存

I/O Kit主要使用Mach IPC和Mach共享内存;网络和文件系统组件则主要使用BSD系统调用。

POSIX设备文件

BSD是OS X内核环境中的一个核心组件,它对外导出了很多符合POSIX标准的接口,通过设备文件,应用程序可以同串口、存储设备、网络设备通信。在类UNIX系统(例如BSD)中,设备文件是位于/dev目录下的特殊文件,代表着中断、磁盘驱动器、打印机等等设备。只要你知道设备文件的名字(例如disk0s2或mt0),就可以使用open、read、write、close、ioctl来访问并控制相关设备。

I/O Kit会在发现设备后动态地创建/dev下的设备文件。结果就是,/dev下的文件结构会不停的动态变化,甚至同一个设备在拔除并再次插入后,对应的设备文件也可能变化。因此,你在应用程序里不能直接将设备文件名字写死到代码中。对于某个设备,你的应用程序必须从I/O Kit获取它的设备文件的路径。

I/O Registry

I/O Registry是一个维护了当前系统中各种驱动对象(驱动和nub)之间provider-client关系的动态数据库,它会随着系统中连接的设备的情况动态变化。

除了从内核空间可以访问外,I/O Kit框架还提供了从用户空间访问I/O Registry的接口,这些接口提供了强大的搜索机制,你可以从I/O Registry中搜索符合指定特征的对象。

I/O Registry的架构和构造过程

I/O Registry基本上是一棵树,每个节点有一个父节点和0个或若干个子节点。只有极少一部分节点不符合树的定义,它们可能有多个父节点,例子就是RAID磁盘控制器,它将多个磁盘结合起来,对外看起来就是一个磁盘,因此RAID控制器由多个物理磁盘作为它的父节点。抛开这些例外不谈,最好将I/O Registry想象为一棵树,这样更便于理解它的构造和更新过程。

在系统引导时,I/O Kit会为Platform Expert注册一个nub,Platform Expert是针对特定主板的驱动对象,它了解主板的信息。这个Platform Expert nub是I/O Registry树的根节点(用Explorer开的结果似乎有出入),它会为这个平台加载正确的驱动,这些驱动会被添加为这个nub的子节点。Platform Expert的驱动会发现系统中的总线,然后为每个总线注册一个nub,这些nub会继续匹配并加载这些总线的驱动,这些总线驱动会进一步扫描所有连接到总线上的设备并继续注册nub,然后这些nub再继续匹配并记载驱动……一直到加载完所有的驱动。

当发现一个设备时,I/O Kit会从另一个动态数据库I/O Catalog查询出符合设备的类型(class type)的所有驱动。I/O Registry维护着当前系统中活跃的驱动对象的关系,I/O Catalog维护着所有可用的驱动的列表,这也是驱动匹配三步过程中间的第一步。

设备的类型(class type)之类的信息保存在驱动程序kext的属性列表内。在将这个属性列表读入系统时,会将它转化为一个OS字典、数组和其他类型。

除了它们在I/O Registry中体现出来的树形结构之外,各个驱动对象之间还有多种其他类型的关联。为了理解它们之间的其他关联,现在我们把驱动对象所在树形结构的平面向第三个维度扩展,如下图。其中每一个圆柱体代表一个I/O Registry中的驱动对象。我们可以在这个三维空间中横向切出几个平面,每个平面都代表一种类型的关联。

I/O Kit定义了6种类型的关联平面:

  • Service:反映驱动对象在I/O Registry中的连接关系
  • Audio:反映Core Audio框架及插件构成的音频信号链
  • Power:反映设备之间的电源依赖关系
  • Device Tree:反映Open Firmware设备的层级结构(似乎只有非X86的Mac才会用到,X86已经将Open Firmware改成EFI了)
  • FireWire:FireWare总线上的设备连接层级
  • USB:USB总线上的设备连接层级

驱动和设备匹配

驱动个性和匹配语言

每个设备驱动都是一个内核扩展,必须定义一个或多个个性(personality),驱动的个性定义了它能支持什么类型的设备。驱动的个性信息保存在内核扩展的Info.plist中的IOKitPersonalities字典里。对于一个驱动个性来说,必须要满足它定义的所有值才能说它匹配到设备上,也就是它列出的条件之间都是逻辑与(AND)的关系。有些驱动个性的条件的值是一些以空格区分的子值,那么这通常代表这些子值之间是逻辑或(OR)的关系,例如PCI卡的驱动个性可能有一个名为"model"的条件,里面列出了模型的号码,例如"AAA BBB CCC"。

具体一个kext的驱动个性里可以包含哪些条件依赖于具体的设备族。例如,一个PCI卡的驱动可以定义一个用于检查PCI vendor和device ID register的条件。有些设备族,例如PCI设备族,提供了相当详尽的匹配策略。我们来看一个用来匹配SCSI适配器卡的例子:

<key>IOPCIMatch</key>
<string>0x00789004&0x00ffffff 0x78009004&0xff00ffff</string>

上面的值有两个复合值组成,只要匹配上任意一个值就表示满足这个条件。为了匹配这些值,驱动所在的设备族从PCI卡读取32位的vendor ID和device ID,然后用'&'符号右边的值对读到的值做mask,再用得到的值同'&'符号左边的值来比较。

下面我们来看一个以太网控制器的驱动个性的plist的例子:

  <key>IOKitPersonalities</key>
    <dict>
        <dict>
            <!-- 每个个性的名字都要唯一 -->
            <key>Name</key>
            <string>PCI Matching</string>

            <!-- ... 这里省略一些条件 ... -->

            <!-- 下面IOClass这个名字指定的类会在加载这个驱动时被IOKit实例化 -->
            <key>IOClass</key>
            <string>ExampleIntel82558</string>

            <!-- IOKit匹配属性
              -- 所有的驱动都必须包含IOProviderClass,它给出了这个驱动可以附着到的
              -- nub类的名字。这个IOProviderClass指定的对象会读取并检查剩下的条件。
              -- 只有所有的条件都满足才能说这个驱动能够匹配这个设备。如果一个驱动具有
              -- 多个个性,那么它有可能被初始化多次(每个匹配上的个性至少一次)。
              -- (所以一定要关注代码的可重入性和并发性)
              -->
            <key>IOProviderClass</key>
            <string>IOPCIDevice</string>

            <!-- IOPCIDevice可以使用任意4种PCI匹配条件之一:这里使用的是IOPCIMatch
              -- 来检查device/vendor ID。
              -->
            <key>IOPCIMatch</key>
            <string>0x12298086</string>

            <!-- 这个值定义了这个驱动个性的初始匹配得分 -->
            <key>IOProbeScore</key>
            <integer>400</integer>
        </dict>

        <dict>
            <!-- 接下来可以定义其他的个性 -->
            <!-- ... 省略 -->
        </dict>
    </dict>

正如上面的例子注释中所提到的,每个驱动都必须通过IOProviderClass指定这个驱动要附着到哪个nub上。在极为少见的情况下,驱动程序会将IOResources作为IOProviderClass的值。IOResources是一个特殊的nub,它直接连接到I/O Registry的根上,为整个系统提供例如BSD内核这样的资源。传统上,虚拟设备的驱动会匹配到IOResources上,这是因为虚拟设备不会对外发布自己的nub。

注意:任何想要将IOResources作为自己IOProviderClass的驱动都必须带有一个名为IOMatchCategory的键,

驱动匹配和加载

驱动匹配

设备探查

在主动匹配阶段,I/O Kit会要求所有的候选驱动去探查设备来检测它们是否可以驱动这个设备。I/O Kit会依次调用下列定义在IOService中的函数:

  • init()
  • attach()
  • probe()
  • detach()
  • free():只有在探查失败时才会调用

这些函数构成了驱动的生命周期的第一部分。

驱动加载

在所有驱动都探查过设备后,具有最高探查得分(probe score)的驱动都会附着到I/O Registry上,然后会调用它的start函数。start函数会初始化设备硬件并使得设备进入可以接受其他操作的状态。如果驱动成功启动,start需要返回true,此时就会抛弃探查列表中排在后面的驱动;如果驱动启动失败,那么start需要返回false,此时会将这个失败的驱动剥离I/O Registry,然后去探查列表中挑选剩下的探查得分最高的驱动来start。

设备匹配

如果一个用户空间应用程序需要访问一个设备,那么它必须首先搜索到这个设备,然后再获取合适的设备端口才能通这个设备通信。这个过程被称为设备匹配。不同于驱动匹配,设备匹配会在I/O Registry中搜索已经加载的一个驱动。

设备匹配具体包含如下步骤:

  1. 通过一个Mach端口建立同I/O Kit的连接。
  2. 定义一个定义了要在I/O Registry内搜索的设备的类型的字典,字典里的值越多,搜索范围就越精确。具体可以添加哪些值可以查询设备所在设备族的头文件(例如IOSCSIDevice.h和IOATADevice.h)、阅读设备族的文档或者查看I/O Registry Explorer里显示的属性。
  3. 从I/O Registry获得能够匹配这个字典的对象列表,然后选取一个合适的设备。
  4. 获取所选设备的设备接口,访问设备。

基础类

libkern基础类

对象(OSObject)分配和销毁

对象构造
对象保持和销毁

运行时类型信息(OSMetaClass)

对象构造和动态分配
类型转换、对象内省和类信息

如何在libkern中定义C++类

I/O Kit基类

动态驱动注册(IORegistryEntry)

基本驱动行为(IOService)

事件处理

设备驱动的工作环境可能是整个操作系统中最为混乱的。许多设备都需要一整串的命令才能执行一条I/O操作。但是,许多线程都可能在任意时间任意地点跑进驱动程序的代码中,例如客户端的I/O请求、硬件中断、超时事件等;在一个多处理器系统上,这些线程还可能真正在并行执行。驱动程序必须要能处理所有这些情况。在I/O Kit中,IOWorkLoop类和相关的事件源对象为并发访问提供了保护。

工作循环

IOWorkLoop对象(或者直接叫work loop)的作用是作为一个闸门,它确保对硬件所用的数据结构的访问时串行的。对于某些事件上下文,work loop也是一个线程。本质上,work loop是一个同一个线程关联的互斥锁(mutex)。它的功能包括:

  • 它的闸门机制为各种事件源的动作提供了同步机制。
  • 它为事件处理提供了类似于堆栈的环境。
  • 它为由中断控制器传递过来的间接中断专门开出一条线程。这个机制会确保针对该work loop所服务驱动的中断都被串行处理,防止多个中断造成对驱动数据的并发访问。

为了更好地理解work loop的角色,有必要先了解什么是事件源(event source)。在I/O Kit中,有五种类型的异步事件:

  • 中断时间:来源于硬件设备的间接(二级)中断
  • 定时器事件:由定时器发出的周期性事件,例如超时
  • I/O命令:由驱动程序的使用者向provider发出的I/O请求
  • 电源事件:通常由驱动栈的底层生成
  • 结构化事件:通常是同I/O Registry相关的事件

为了处理这些事件源,I/O Kit提供了几个类:IOInterruptEventSource、IOTimerEventSource和IOCommandGate(电源时间和结构化事件都通过IOCommandGate来处理)。每个事件源类都定义了特定于其所代表的事件类型的机制,会在work loop提供的受保护的环境中执行单一的功能。如果一个传播时间的线程需要访问驱动的临界数据,那么它必须经过一个事件源类的对象才能成功。

通常,客户端驱动会在start韩淑丽设置自己的work loop、事件源和事件处理方法。为了避免死锁和竞态访问,访问同一份数据的所有代码都应该共享同一个work loop,向这个work loop注册自己的事件源。显然,你也可以在不相关的对象(没有竞争关系)之间共享work loop,通常一个work loop会在同一个驱动栈的不同层次之间共享。Work loop也可以专用于特定的驱动和它的用户。

工作循环的架构

相对于其他系统使用的事件驱动模型,I/O Kit的work-loop机制对性能的冲击要小于事件驱动模型。为了保证事件处理在串行的上下文中进行,work loop的机制将所有的操作都放到一个线程中执行。不幸的是,从持有事件的线程将事件转给work loop事件处理依然需要做一次上下文切换。

对于I/O命令定时器事件,事件源的线程会获取work loop对应的mutex,在当前的事件处理完成之前,其他的事件无法执行。需要注意,即使有这个mutex,你可以需要保证你的代码是可重入的。你可以在同一个驱动栈中使用多个work loop,但是这样就增大了死锁的风险。不过,work loop确实可以避免自死锁(self-deadlock),这是因为它的实现是基于递归锁(recursive lock)的:work loop会先检查当前线程是否已经持有了mutex。这也是你需要保证自己的代码是可重入的原因。

对于中断事件源来说,work loop的操作确实要涉及到上下文切换。真正处理中断的代码运行在work loopde 线程而不是投递中断的线程上。在这种情况下就需要切换上下文。

有两个因素影响work loop查询事件源的顺序。线程的相对优先级是决定性的因素,其中定时器的优先级最高。一个客户端的线程可以通过修改自己的优先级的方式来提高自己I/O请求的处理顺序,但是这可能无法提高真正的处理速度,这是因为一般I/O请求都会按照FIFO的顺序来排队。中断事件源的优先级也比较高,中断到达work loop的次序决定了它们被处理的次序。

无论处理的是那种事件源、采用的那种运行机制,一个work loop的主要工作只有一件事:运行事件源的completion或者Action方法。Work loop负责保证同一时刻只有一个事件被处理。当一个线程正在处理一个事件时,所有其他的事件可以被异步地投递到它们的事件源,但是在当前事件处理完之前不会被处理。因此,事件处理函数不应该处理需要大量时间的操作或者任何可能会阻塞的操作(例如,等待其他过程完成),例如分配内存或者资源等;事件处理函数应该将这种任务放到队列中或者推迟到后面处理。

共享工作循环和专用工作循环

所有的IOService都可以共享它们的provider的work loop。代表电脑主板的I/O Registry的根节点带有一个work loop,所以即使不重新创建新的work loop,驱动至少也有一个work loop可用。驱动可以通过调用IOService的getWorkLoop来访问其provider的work loop。通过这种方式,整个驱动栈或者驱动栈的一部分可以共享同一个wook loop:

共享work loop

大多数的驱动不会创建自己的work loop。如果硬件不会直接向你的驱动程序发送中断,或者你的驱动很少收到中断,你一般就不需要创建自己的work loop。不过,如果一个驱动直接接收中断(也就是说,它直接同中断控制器交互),那么应该创建它自己的work loop。Examples of such drivers are PCI controller drivers (or any similar driver with a provider class of IOPCIDevice) and RAID controller drivers. Even these work loops may be shared by the driver’s clients, however, so it’s important to realize that in either case, the driver must not assume that it has exclusive use of the work loop. This means that a driver should rarely enable or disable all events on its work loop, since doing so may affect other I/O Kit services using the work loop.

If a driver handles interrupts or for some other reason needs its own work loop, it should override the IOService function getWorkLoop to create a dedicated work loop, used by just the driver and its clients. If getWorkLoop isn’t overridden, a driver object gets the next work loop down in its stack.

示例:获取工作循环

To obtain a work loop for your client driver, you should usually use your provider’s work loop or, if necessary, create your own. To obtain your provider’s work loop, all you have to do is call the IOService function getWorkLoop and retain the returned object. Immediately after getting your work loop you should create your event sources and add them to the work loop (making sure they are enabled).

To create a dedicated work loop for your driver, override the getWorkLoop function. Listing 7-1 illustrates a thread-safe implementation of getWorkLoop that creates the work loop lazily and safely.

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;
}

This code first checks if cntrlSync is a valid memory address; if it is, a work loop already exists, so the code returns it. Then it tests to see if some other thread is trying to create a work loop by atomically trying to compare and swap the controller synchronizer variable from 0 to 1 (1 cannot be a valid address for a work loop). If no swap occurred, then some other thread is initializing the work loop and so the function waits for the cntrlSync variable to stop being 1. If the swap occurred then no work loop exists and no other thread is in the process of creating one. In this case, the function creates and returns the work loop, which unblocks any other threads that might be waiting.

As you would when getting a shared work loop, invoke getWorkLoop in start to get your work-loop object (and then retain it). After creating and initializing a work loop, you must create and add your event sources to it. See the following section for more on event sources in the I/O Kit.

事件源

A work loop can have any number of event sources added to it. An event source is an object that corresponds to a type of event that a device driver can be expected to handle; there are currently event sources for hardware interrupts, timer events, and I/O commands. The I/O Kit defines a class for each of these event types: IOInterruptEventSource, IOTimerEventSource, and IOCommandGate, respectively. Each of these classes directly inherits from the abstract class IOEventSource.

An event-source object acts as a queue for events arriving from a particular event source and hands off those events to the work-loop context when it asks them for work. When you create an event-source object, you specify a callback function (also known as an “action” function) to be invoked to handle the event. Similar to the Cocoa environment’s target/action mechanism, the I/O Kit stores as instance variables in an event source the target of the event (the driver object, usually) and the action to perform. The handler’s signature must conform to an Action prototype declared in the header file of the event-source class. As required, the work loop asks each of its event sources in turn (by invoking their checkForWork function) for events to process. If an event source has a queued event, the work loop runs the handler code for that event in its own protected context. Note that when you register an event source with a work loop, the event source is provided with the work loop's signaling semaphore, which it uses to wake the work loop. (For more information on how the work loop sleeps and wakes, see the threadMain function in IOWorkLoop documentation.)

A client driver, in its activation phase (usually the start function), creates the event sources it needs and adds them to its work loop. The driver must also implement an event handler for each event source, ensuring that the function’s signature conforms to the Action function prototype defined for the event-source class. For performance reasons, the event handler should avoid doing anything that might block (such as allocating memory) and defer processing of large amounts of data. See Work Loops for further information on event priority and deferring work in event handlers.

对于任何一种事件源来说,向work loop中添加事件源的过程都差不多,有四步:

  1. 获得work loop。
  2. 创建事件源对象。
  3. 将事件源添加进work loop。
  4. 启用事件源。

释放事件源也有一个标准的模式:

  1. 禁用事件源。
  2. 从work loop中移除事件源。
  3. 释放事件源。

处理中断

I/O Kit中的中断处理
设置中断处理器并附着到工作循环
过滤中断事件源
独立使用中断处理器(不使用工作循环)

处理定时器事件

I/O请求和命令闸门

向上调用和向下调用
设置并使用命令闸门

管理数据

管理电源

管理设备移除事件

I/O Kit Family参考手册