Modernize PCI and SCSI drivers with DriverKit
System & Services 高级 26m

使用 DriverKit 现代化 PCI 和 SCSI 驱动

Modernize PCI and SCSI drivers with DriverKit

2020年6月24日

在 Apple 官方观看视频

一句话判断

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 服务类型:IOUserPCIDeviceIOUserSCSIDevice。这些服务提供了与内核空间 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,需要额外的开发者资质
WWDC 2020