WWDC Quick Look 💓 By SwiftGGTeam
Modernize PCI and SCSI drivers with DriverKit

Modernize PCI and SCSI drivers with DriverKit

观看原视频

Highlight

macOS Big Sur 为 DriverKit 加入 PCIDriverKit 和 SCSIControllerDriverKit,PCI 与 SCSI controller 驱动可以改写成用户空间的 System Extension,并且对应 kext 在可替代场景开始进入废弃路径。

核心内容

写 PCI 或 SCSI controller 驱动,以前通常意味着进入内核。kext 能碰到整台机器的资源,一个 bug 可能带来 kernel panic。调试也慢:经常要两台机器配合,驱动崩溃后还要重启。

DriverKit 把这个工作移到用户空间。Driver extension 仍然能拿到特定硬件或内核接口所需的权限,但权限由 entitlement 限定,资源也和系统其他部分隔离。驱动进程崩溃时,内核和其他进程继续运行。安装方式也变了:dext 留在 App bundle 里,由 App 通过 SystemExtensions framework 发起 activationRequest,再等待管理员批准。

这场 session 解决的是 DriverKit 在低层硬件上的覆盖缺口。macOS Catalina 已经支持 USB、Serial、Network Interface Controller 和 HID。到 macOS Big Sur,Apple 增加了 PCI 与 SCSI controller 驱动支持,并明确说:当 System Extension 能完成同样工作时,PCI 和 SCSI controller kext 将进入废弃路径。

演讲分成两条线。第一条线是 PCIDriverKit:如何给 PCI 设备授权、打开设备、访问配置空间、处理 interrupt 和 DMA。第二条线是 SCSIControllerDriverKit:如何用 IOUserSCSIParallelInterfaceController 写一个 SCSI dext,并让存储 I/O 在用户空间驱动下仍然跑满实际工作负载。

详细内容

DriverKit 的安装和授权边界

02:28)Driver extension 不应该复制到系统目录。它留在 App bundle 中,由 App 在合适的生命周期节点创建 activationRequest。常见做法是在 App 启动时请求激活,也可以等用户接受许可协议或完成购买后再激活。

DriverKit distribution workflow:
1. Keep the driver extension inside the app bundle.
2. Use the SystemExtensions framework to create an activationRequest.
3. Ask the system to make the extension available.
4. Wait for administrator approval.
5. Sign the dext locally and include all required entitlements during development.

关键点:

  • dext 的分发入口是 App,不是系统目录。
  • activationRequest 由 SystemExtensions framework 创建。
  • 管理员批准是加载 System Extension 的一部分。
  • 开发阶段可以先在本机签名运行,但必须包含驱动所需的 entitlement。

05:39)PCIDriverKit 还要求 PCI entitlement。这个 entitlement 使用和 Info.plist 相同的 IOPCIFamily matching criteria,可以精确到 vendor ID 和 device ID,也可以用 mask 支持同一 vendor 下的多个设备。

PCI entitlement shape:
- A list of PCI matching dictionaries.
- Matching criteria follow IOPCIFamily categories.
- A primary match compares primary vendor ID and device ID.
- A mask can remove the device ID part and match all devices from one vendor.

关键点:

  • entitlement 决定这个 dext 是否有权访问目标 PCI 设备。
  • 设备匹配规则和 IOPCIFamily 使用同一套 criteria。
  • 用 mask 放宽匹配范围时,授权范围会随之变大,需要按产品线控制。
  • 如果 PCI 设备要接入其他 DriverKit family,例如自定义 Ethernet NIC,还要增加对应 family entitlement。

PCIDriverKit 的设备模型

07:15)PCIDriverKit 里唯一的类是 IOPCIDevice。它不用于 subclass。驱动把它当成 PCI provider,通过它访问 PCI 资源。Apple 还强调,访问 PCI 设备前必须成功调用 Open,停止阶段要调用 Close

PCI driver extension model:
1. Use IOPCIDevice as the PCI provider.
2. Call Open before accessing PCI resources.
3. Treat Open success as exclusive access to the PCI device.
4. Route shared access through one PCI resource manager when multiple clients exist.
5. Call Close in the driver stop routine.

关键点:

  • IOPCIDevice 是资源入口,不是业务驱动的父类。
  • Open 建立独占访问;没有打开设备就不应该读写 PCI 资源。
  • 多个 client 共享资源时,驱动需要自己设计一个资源管理层。
  • Close 成功后,不能假设设备状态仍然保留;PCI stack 会禁用 bus mastering 和 memory space。

配置空间、memoryIndex 和设备内存访问

09:25)PCIDriverKit 统一接管设备 mapping。内存访问 API 使用 memoryIndex 和 offset。32-bit BAR 中,memoryIndex 和 BAR index 相同;64-bit BAR 会把两个 BAR 合成一个 memory entry,因此 memoryIndex 按 memory entry 从 0 开始计数。

Memory access model:
- 32-bit BAR2 -> memoryIndex 2.
- 64-bit BAR0 + BAR1 -> memoryIndex 0.
- 64-bit BAR2 + BAR3 -> memoryIndex 1.
- Access device memory by memoryIndex plus offset.

关键点:

  • memoryIndex 表示第几个 memory entry,不总是 BAR index。
  • 64-bit BAR 会改变 index 映射关系。
  • 统一入口让 Apple 以后可以优化设备访问,也方便用 dtrace 观察内存访问。
  • 配置空间读写基本是一对一迁移,但同样需要先有 open session。

11:02)启用 bus mastering 和 memory space 时,PCIDriverKit 不再提供每项操作的显式 kernel API。驱动要读 PCI command register,设置 bus master 和 memory enable bit,再写回设备。

Start routine for a typical PCI driver:
1. Open a session with the PCI provider.
2. Read the PCI command register.
3. Set the bus master bit.
4. Set the memory space enable bit.
5. Write the command register back.
6. Perform memory reads or writes only after these bits are set.

关键点:

  • command register 是启用设备内存访问和 I/O 的关键入口。
  • 这些 offset 和 definition 位于 PCIDriverKit framework headers。
  • 停用 bus mastering 时流程相同,只是把对应 bit mask 掉。
  • stop routine 中调用 Close,表示驱动结束和 PCI 设备的会话。

interrupt 和 DMA 的用户空间流程

12:06)interrupt handler 要先在 .iig 头文件里声明。驱动随后枚举 PCI device 的 interrupt index,找到需要的 interrupt type,例如 MSI,再把 interrupt source 接到某个 DispatchQueue。

Interrupt setup:
1. Declare InterruptOccurred in the .iig header.
2. Iterate through interrupt indexes for the PCI device.
3. Pick the index whose interruptType matches MSI.
4. Choose a DispatchQueue.
5. Create the dispatch source for that interrupt index.
6. Create the interrupt action.
7. Enable the source so the handler is called on interrupt.

关键点:

  • interrupt type 决定驱动应该绑定哪个 interrupt index。
  • 可以使用默认 DispatchQueue,也可以为 interrupt 创建独立队列。
  • interrupt action 把 dispatch source 和处理函数连起来。
  • handler 中可以通过 MMIO 写设备寄存器来清除 interrupt。

13:43)DMA transfer 需要先创建 buffer memory descriptor,再创建 IODMACommand。Apple 明确说,IODMACommand 要指定这是给 PCI device 使用,这样系统才能选择正确的 memory mapper。

DMA transfer workflow:
1. Create a buffer memory descriptor.
2. Set the buffer length before writing into it.
3. Create the DMA specification for the hardware.
4. Create an IODMACommand for the PCI device.
5. Prepare the buffer memory descriptor with the DMA command.
6. Use physicalAddressSegment to get the address for the PCI device.
7. Call Complete on the DMA command after the transfer.

关键点:

  • buffer memory descriptor 描述要交给设备访问的内存。
  • IODMACommand 绑定到 PCI device,避免走错 memory mapper。
  • physicalAddressSegment 产出的地址会写给硬件。
  • transfer 结束后调用 Complete,让物理内存可以重新被其他进程或设备使用。

SCSIControllerDriverKit 的类和队列模型

16:18)SCSI dext 要 subclass IOUserSCSIParallelInterfaceController。Apple 保留了接近 kernel class 的 API 结构,在 API 名称前加 User 前缀。dext 需要 override 标记为 pure virtual 的函数,例如 UserProcessParallelTaskUserDoesHBAPerformAutoSenseUserInitializeController

SCSI dext skeleton:
1. Subclass IOUserSCSIParallelInterfaceController.
2. Override pure virtual functions.
3. Use UserProcessParallelTask to submit I/O requests to hardware.
4. Use UserDoesHBAPerformAutoSense to report auto-sense support.
5. Use UserInitializeController to initialize controller and dext data structures.
6. Add generic DriverKit, transport-specific, and SCSI controller entitlements.

关键点:

  • pure virtual functions 是 framework 调用 dext 的入口。
  • 非 pure virtual functions 由 dext 调用,用于创建 SCSI target 或设置 target property。
  • SCSI controller family entitlement 是访问该 framework 的前置条件。
  • SCSI controller 设备通常基于 PCIe,因此它和 PCIDriverKit 兼容。

20:49)Apple 建议 SCSI dext 使用三个 DispatchQueue:Default Queue、Interrupt Queue 和 Auxiliary Queue。Default Queue 处理来自 kernel 的调用;Interrupt Queue 专门处理 interrupt 和 I/O timeout;Auxiliary Queue 用于创建 SCSI target。

Recommended SCSI queues:
- Default Queue: UserInitializeController, UserProcessParallelTask, and other kernel-originated calls.
- Interrupt Queue: hardware interrupts and IOTimeoutHandlers.
- Auxiliary Queue: SCSI target creation.

关键点:

  • DriverKit 的 dispatch queue 是串行的。
  • interrupt 和 I/O 放到不同队列,可以减少互相等待。
  • 创建 target 会触发 kernel 对 dext 的多次回调,因此不应该堵在 Default Queue 上。
  • UserMapHBAData 适合提前创建 controller-specific taskData,避免在 I/O path 里做预处理。

SCSI I/O path、power 和 termination

23:44)SCSIControllerDriverKit 改变了 I/O path 的责任划分。kernel 调用 UserProcessParallelTask 提交 I/O,并把 SCSIUserParallelTask 的 copy 发送给 dext。kernel 负责准备 IODMACommand 和生成 physical segment,dext 不需要在性能路径上再向 kernel 发起这些 IPC。

SCSI I/O path:
1. Kernel calls UserProcessParallelTask.
2. Dext receives a SCSIUserParallelTask copy.
3. Kernel has already prepared IODMACommand and physical segments.
4. Dext uses fBufferIOVMAddr as the start physical address.
5. Dext posts the request to hardware.
6. Interrupt handler processes completion.
7. Dext calls ParallelTaskCompletion with SCSIUserParallelResponse.

关键点:

  • dext 拿不到 SCSIParallelTask object,拿到的是 SCSIUserParallelTask
  • fBufferIOVMAddr 是 kernel 生成的连续 physical segment 起始地址。
  • 如果硬件需要多个 segment,dext 可以自己准备 ScatterGatherList。
  • ParallelTaskCompletion 是 I/O 完成回调,response 中包含 status、bytes transferred 和 sense data。

28:15)power management 通过 override SetPowerState 完成。DriverKit 支持 off、on、low-power 三种 power state。PCIe 设备主要处理 off 和 on:off 对应 sleep 或 PCI pause,on 对应 system wake。

SCSI dext lifecycle:
1. Override SetPowerState for hardware-specific power transitions.
2. Use delayed acknowledgement when hardware reset or rescan needs time.
3. Override Stop for termination.
4. Close outstanding PCI session in Stop.
5. Cancel dispatch queues and sources.
6. Let the kernel handle outstanding I/O requests during termination.

关键点:

  • SetPowerState 可以先返回,再在硬件恢复后通过 super dispatch 完成 acknowledgement。
  • 示例里 on transition 会发起 hard reset,并触发 SCSI target rescan。
  • Stop 中关闭 PCI session,取消队列和 source。
  • dext 不负责完成 outstanding I/O,termination 时这部分由 kernel 处理。

性能边界

31:55)Apple 用一块 4Gbps Fibre Channel host bus adapter 连接 RAID disk array,经 Thunderbolt 3 接到 MacBook Pro。演示中,Final Cut Pro 从这个外部 SCSI disk 播放 Apple ProRes 视频,磁盘吞吐接近 350 MB/s,没有触发 dropped frames 警告。

Demo setup:
- 4 Gbps Fibre Channel host bus adapter.
- RAID disk array.
- Thunderbolt 3 connection to a MacBook Pro.
- ExampleSCSIDext running as a user-space DriverKit extension.
- Final Cut Pro library stored on the external SCSI disk.
- ProRes playback at around 350 MB/s disk throughput.

关键点:

  • 演示目标是验证用户空间 dext 是否能承担 pro workload。
  • 这个 SCSI dext 通过 PCIe-based controller 工作。
  • session 的性能论证依赖具体 demo,不是抽象承诺。
  • Apple 把性能路径优化点放在 kernel 预处理 DMA 和减少 I/O path IPC 上。

核心启发

1. 做一个 PCIe 设备的 DriverKit 迁移样板

做什么:挑一个已有 PCIe kext,把设备匹配、Open / Close、command register、memory access 先迁到 PCIDriverKit。

为什么值得做:session 明确说 PCI kext 在可由 System Extension 替代时进入废弃路径,早迁移可以提前发现 entitlement、BAR mapping 和 DMA 差异。

怎么开始:先整理 vendor ID 和 device ID,写出 PCI matching dictionaries,再把 Start routine 改成打开 IOPCIDevice、启用 bus mastering 和 memory space、通过 memoryIndex 访问 MMIO。

2. 做一个 SCSI controller dext 原型

做什么:用 IOUserSCSIParallelInterfaceController 建一个最小 SCSI dext,跑通初始化、I/O submit、interrupt completion 和 Stop。

为什么值得做:SCSIControllerDriverKit 负责替代过去的 IOSCSIParallelInterfaceController kext 路线,适合 Fibre Channel、RAID controller、Serial Attached SCSI 等设备。

怎么开始:先 override pure virtual functions,建立 Default、Interrupt、Auxiliary 三个队列;在 UserProcessParallelTask 中读取 SCSIUserParallelTask,硬件完成后调用 ParallelTaskCompletion

3. 做一个驱动安装和恢复控制台

做什么:把 dext 打包进 App,提供激活状态、管理员批准状态、错误日志和重新激活入口。

为什么值得做:DriverKit 的安装动作从系统目录复制变成 App 发起 activationRequest,用户和管理员需要知道扩展是否已经可用。

怎么开始:App 启动时用 SystemExtensions framework 创建 activationRequest,记录每次批准、失败和重试;驱动崩溃后提示用户重新连接设备或重新激活扩展。

4. 做一个 DMA 与 interrupt 诊断工具

做什么:在开发版 dext 中记录 memoryIndex、interrupt type、DMA prepare / Complete 时序和 completion status。

为什么值得做:PCIDriverKit 把设备 memory access 收到统一 API 下,Apple 也提到这能帮助用 dtrace 观察多线程环境里的 PCI 问题。

怎么开始:先把 interrupt 放入独立 DispatchQueue,再给每次 DMA transfer 记录 buffer 长度、physicalAddressSegment 和完成时间,和硬件日志对齐。

5. 做一个 kext 可替代性清单

做什么:给团队现有内核扩展做一张表,标出是否属于 DriverKit 已支持的 USB、Serial、Network Interface Controller、HID、PCI 或 SCSI controller family。

为什么值得做:Apple 已经把 DriverKit 支持范围和 kext deprecation 绑定;属于已支持 family 的驱动,应优先进入迁移计划。

怎么开始:按设备 family 分组,逐项检查 entitlement、provider class、I/O path 和安装方式,把还依赖 kernel-only 行为的点列成迁移风险。

关联 Session

评论

GitHub Issues · utterances