使用 DriverKit 现代化 PCI 和 SCSI 驱动
Modernize PCI and SCSI drivers with DriverKit
2020年6月24日
一句话判断
macOS Big Sur 将 PCI 和 SCSI 驱动也从内核空间移到了用户空间——如果你的硬件产品还在用 Kernel Extension,Apple 给了你最后的时间窗口来做迁移。
这场 Session 讲了什么
DriverKit 是 Apple 在 macOS Catalina 中引入的用户空间驱动框架,用于替代传统的 Kernel Extension (KEXT)。macOS Big Sur 将 DriverKit 的覆盖范围扩展到了 PCI 和 SCSI 设备。这意味着音频接口、RAID 控制器、网络适配器、GPU 等硬件设备的驱动都可以在用户空间运行,不再需要加载内核扩展。
Session 详细介绍了新的 DriverKit 服务类型:IOUserPCIDevice 和 IOUserSCSIDevice。这些服务提供了与内核空间 PCI/SCSI 框架等价的 API,但在用户空间运行。驱动开发者可以通过 PCIDevice 服务访问配置空间、映射内存区域和设置 DMA(Direct Memory Access),通过 SCSIDevice 服务发送 SCSI 命令和处理 I/O 请求。
Apple 强调了 DriverKit 的安全性优势:用户空间驱动崩溃不会导致整个系统崩溃(kernel panic),只会重启驱动进程。这大幅提高了系统稳定性——一个有 bug 的音频驱动不再会让整台 Mac 死机。
值得深挖的点
PCI 设备的内存映射与 DMA
PCIe 设备通常通过 Memory-Mapped I/O (MMIO) 与主机通信。在 KEXT 模式下,驱动直接访问物理内存地址。在 DriverKit 模式下,你需要通过 IVard(I/O Virtual Address)来映射设备的寄存器空间。DMA 操作也需要通过 IOMemoryDescriptor 的用户空间版本来设置缓冲区。这些抽象层确保了用户空间驱动无法直接访问内核内存,提供了安全隔离。
SCSI 命令队列与 I/O 调度
DriverKit 的 SCSI 服务支持命令队列,驱动可以一次接收多个 I/O 请求并按最优顺序执行。这对存储设备的性能至关重要——RAID 控制器需要合并和重排 I/O 请求来最大化吞吐量。Session 介绍了如何使用 SCSITask 对象来构建和提交 SCSI 命令,以及如何通过 completion callback 返回结果。
代码片段
// DriverKit PCI 驱动的基本结构
import DriverKit
class MyPCIDriver: IOService {
var pciDevice: IOUserPCIDevice?
var memoryMap: IOMemoryMap?
override func start(_ provider: IOService) -> Bool {
// 获取 PCI 设备引用
guard let device = provider as? IOUserPCIDevice else {
return false
}
self.pciDevice = device
// 读取 PCI 配置空间
let vendorID = device.configurationRead16(offset: 0x00)
let deviceID = device.configurationRead16(offset: 0x02)
print("PCI 设备: Vendor=\(vendorID), Device=\(deviceID)")
// 启用 PCI 设备
let commandReg = device.configurationRead16(offset: 0x04)
device.configurationWrite16(
offset: 0x04,
value: commandReg | 0x0007 // 启用 I/O、Memory、Bus Master
)
// 映射 BAR0(Base Address Register 0)
do {
let barInfo = try device.getBusMemorySpaceInfo(barIndex: 0)
memoryMap = try device.mapDeviceMemory(
barIndex: 0,
options: .cacheInhibit // MMIO 区域不使用缓存
)
if let map = memoryMap {
let address = map.address
let size = map.size
print("BAR0 映射: 地址=0x\(address), 大小=\(size)")
}
} catch {
print("BAR 映射失败: \(error)")
return false
}
return true
}
override func stop() {
memoryMap = nil
pciDevice = nil
}
}
// SCSI 驱动的命令处理
import DriverKit
class MySCSIDriver: IOService {
var scsiDevice: IOUserSCSIDevice?
override func start(_ provider: IOService) -> Bool {
guard let device = provider as? IOUserSCSIDevice else {
return false
}
self.scsiDevice = device
// 注册 SCSI 命令处理器
device.registerService()
return true
}
// 处理来自系统的 SCSI 命令
func handleCommand(_ task: SCSITask) {
let commandDescriptorBlock = task.commandDescriptorBlock
switch commandDescriptorBlock[0] {
case 0x28: // READ(10) 命令
handleReadCommand(task)
case 0x2A: // WRITE(10) 命令
handleWriteCommand(task)
case 0x00: // TEST UNIT READY
task.setStatus(.good)
task.complete()
default:
task.setStatus(.checkCondition)
task.complete()
}
}
func handleReadCommand(_ task: SCSITask) {
// 从命令描述块中提取逻辑块地址和传输长度
let lba = (UInt32(task.commandDescriptorBlock[2]) << 24) |
(UInt32(task.commandDescriptorBlock[3]) << 16) |
(UInt32(task.commandDescriptorBlock[4]) << 8) |
UInt32(task.commandDescriptorBlock[5])
let transferLength = UInt32(task.commandDescriptorBlock[8])
// 从设备读取数据并填充到任务的缓冲区
// ... 实际的硬件 I/O 操作 ...
task.setStatus(.good)
task.complete()
}
func handleWriteCommand(_ task: SCSITask) {
// 类似 READ,但是将缓冲区数据写入设备
task.setStatus(.good)
task.complete()
}
}
// DriverKit 驱动的 Info.plist 配置
/*
<key>IOKitPersonalities</key>
<dict>
<key>MyPCIDriver</key>
<dict>
<key>CFBundleIdentifier</key>
<string>com.example.driver.mypci</string>
<key>IOClass</key>
<string>MyPCIDriver</string>
<key>IOProviderClass</key>
<string>IOPCIDevice</string>
<key>IOMatchCategory</key>
<string>MyPCIDriver</string>
<key>IOPropertyMatch</key>
<dict>
<key>IOPCIMatch</key>
<string>0x1234abcd</string> <!-- Vendor/Device ID -->
</dict>
<key>DriverKit</key>
<true/>
<key>DriverKitCurrentUserClient</key>
<string>com.example.driver.mypci.client</string>
</dict>
</dict>
*/
最佳实践
- 尽早开始从 KEXT 迁移到 DriverKit——Apple 已经明确表示未来的 macOS 版本将完全禁止加载第三方 KEXT
- 使用
IOUserPCIDevice的配置空间 API 代替直接的硬件寄存器访问,确保兼容性 - DMA 操作使用
IOMemoryDescriptor而非原始指针,利用 DriverKit 的安全隔离机制 - 在用户空间处理设备错误时,实现自动恢复逻辑——驱动进程可能被系统重启
- 使用 System Extension 而非 Kernel Extension 分发驱动,通过
systemextensionsctl安装
还有什么值得关注
- DriverKit 驱动以 System Extension 的形式分发,不需要重启系统即可加载
- Apple 提供了 DriverKit 的 SDK 模板,包含 PCI 和 SCSI 驱动的脚手架代码
- Session 提到 USB 驱动已经在 macOS Catalina 中支持 DriverKit,PCI 和 SCSI 是 2020 年的扩展
- 开发 DriverKit 驱动需要使用 Apple 的 Hardware Developer Tools,需要额外的开发者资质