搜"EDK2 DXE"的人很多,但大部分文章只讲概念。本文从Dispatcher.c源码出发,把五级优先级、Depex后缀表达式评估、Protocol三层结构、Notify订阅机制全部拆清楚,配实战踩坑案例。
一、DXE Core烧的"三把火"
公司换了个新老板,第一件事不是开会,是搞清楚三件事:手头有哪些资源、怎么分配任务、定一套规矩。DXE Core也是这个逻辑。
PEI把"家底"(HOB List)交过来之后,DXE Core烧三把火:建协议数据库(让所有驱动能互相找到)、调度驱动加载(决定谁先跑)、管理硬件资源(内存、IO、中断、DMA)。
PEI和DXE最大的区别:PEI是"临时工"——跑完就扔,模块卸载了内存回收。DXE是"正式员工"——驱动一直驻留在内存直到OS启动,驱动之间通过Protocol永久互连。
二、DXE Dispatcher:和PEI Dispatcher的根本不同
调度主循环
// MdeModulePkg/Core/Dxe/Dispatcher/Dispatcher.cVOIDCoreDispatcher(IN EFI_STATUS*ReturnStatus){// 第一步:找到Busy状态的FV,注册所有DXE驱动到调度队列DiscoverFvAndRegisterDrivers();// 第二步:进入调度循环——和PEI不同,这是多轮循环do{Status=CoreDispatchOneDriver();}while(Status!=EFI_NOT_FOUND);// 第三步:通知Dispatcher Architecture Protocol的注册者Dispatcher=CoreLocateProtocol(&gEfiDispatcherArchProtocolGuid);if(Dispatcher!=NULL){Dispatcher->DispatcherLoaded(Dispatcher);}}PEI vs DXE Dispatcher对比
| 方面 | PEI Dispatcher | DXE Dispatcher |
|---|---|---|
| 调度对象 | PEIM(每个只跑一次) | DXE Driver(驻留内存) |
| 依赖表达 | PPI GUID布尔表达式 | Protocol GUID布尔表达式 + 优先级 |
| 调度策略 | 纯Depex驱动 | Depex + Driver Binding + 优先级排序 |
| FV发现 | 静态(BFV + PPI通知) | 动态(FV HOB + DXE Driver主动通知) |
| 执行后 | PEIM卸载 | 驱动Stay Resident,安装Protocol |
三、五级优先级体系
DXE Dispatcher不是"谁的Depex先满足谁先跑"这么简单。它有五级优先级:
// MdePkg/Include/Pi/PiDxeCis.h#defineDXE_DRIVER_PRIORITY_PLATFORM_PROCESSOR0x03000000// 最早#defineDXE_DRIVER_PRIORITY_PLATFORM_CHIPSET0x03010000#defineDXE_DRIVER_PRIORITY_BUS0x03020000#defineDXE_DRIVER_PRIORITY_DEVICE0x03030000#defineDXE_DRIVER_PRIORITY_PLATFORM_APPLICATION0x03040000// 最晚优先级写在每个DXE驱动的PE/COFF头或FV文件扩展头里。Dispatcher先扫一遍所有注册的驱动,按优先级分组,每组内再按Depex评估。
这个设计解决了PEI阶段一个头疼的问题:PEI里只能靠Apriori强制排序,DXE里不需要了——优先级表达得更自然。
四、Depex评估:后缀表达式
DXE驱动的Depex比PEI的复杂,支持Protocol GUID和SOR(Scheduled Order Rule):
// USB键盘驱动的Depex示例 AND gEfiUsbIoProtocolGuid // 必须有USB IO Protocol gEfiSimpleTextInputProtocolGuid // 且已有至少一个输入设备 ENDDepex是后缀表达式(逆波兰表示法),评估器维护一个操作数栈:
// MdeModulePkg/Core/Dxe/Dispatcher/Dependency.cBOOLEANCoreEvaluateDependency(IN EFI_CORE_DRIVER_ENTRY*DriverEntry){// EFI_DEP_PUSH_GUID → 查Protocol是否存在,压栈// EFI_DEP_AND → 弹两个操作数,逻辑与// EFI_DEP_OR → 弹两个操作数,逻辑或// EFI_DEP_NOT → 弹一个操作数,逻辑非// EFI_DEP_TRUE/FALSE → 压常量// EFI_DEP_END → 评估结束,栈顶就是结果}五、Protocol Database:DXE的"黄页"
三层结构
如果你用过COM、D-Bus或Android的Binder IPC,看到DXE Protocol Database会觉得似曾相识。三层结构:
Handle Database (全局链表) └── IHANDLE (句柄) ├── 唯一Handle ID └── PROTOCOL_INTERFACE 链表 ├── Protocol GUID ├── Protocol Interface 指针 └── 注册这个Protocol的Agent Handle// MdeModulePkg/Core/Dxe/Hand/Handle.htypedefstruct{UINTN Signature;// "hList"LIST_ENTRY AllHandles;// 连到全局Handle ListLIST_ENTRY Protocols;// 这个Handle上装的所有ProtocolUINTN LocateRequest;UINT64 Key;// 唯一ID}IHANDLE;typedefstruct{UINTN Signature;// "pI/F"LIST_ENTRY Link;// 连到IHANDLE.ProtocolsLIST_ENTRY ByProtocol;// 连到PROTOCOL_ENTRY.AllEntriesIHANDLE*Handle;PROTOCOL_ENTRY*Protocol;VOID*Interface;// Protocol具体数据指针PROTOCOL_NOTIFY*Notify;}PROTOCOL_INTERFACE;typedefstruct{UINTN Signature;// "PEnT"LIST_ENTRY AllEntries;// 连到全局Protocol Entry ListEFI_GUID ProtocolID;// GUIDLIST_ENTRY Protocols;// 所有装了这个Protocol的PROTOCOL_INTERFACELIST_ENTRY Notify;// RegisterProtocolNotify的注册者}PROTOCOL_ENTRY;双向索引:从Handle能找到它装了什么Protocol;从Protocol GUID能找到所有装了它的Handle。
六、InstallProtocolInterface:驱动"挂牌营业"
安装流程
每个DXE驱动跑起来后,第一件事是安装自己提供的Protocol。这是链式调用:
InstallProtocolInterface → CoreInstallProtocolInterfaceNotify → InsertTailList // 挂到IHANDLE → InsertTailList // 挂到PROTOCOL_ENTRY → CoreNotifyProtocolEntry // 通知所有等待者// MdeModulePkg/Core/Dxe/Hand/Handle.cEFI_STATUS EFIAPICoreInstallProtocolInterface(IN OUT EFI_HANDLE*UserHandle,IN EFI_GUID*Protocol,IN EFI_INTERFACE_TYPE InterfaceType,IN VOID*Interface){returnCoreInstallProtocolInterfaceNotify(UserHandle,Protocol,InterfaceType,Interface,TRUE// Notify=TRUE,安装后立刻通知等待者);}InstallMultipleProtocolInterfaces
日常开发没人直接调InstallProtocolInterface——太啰嗦。用InstallMultipleProtocolInterfaces一次装好几个:
// 典型调用:一个Handle上装两个ProtocolEFI_HANDLE HostBridgeHandle=NULL;Status=gBS->InstallMultipleProtocolInterfaces(&HostBridgeHandle,&gEfiDevicePathProtocolGuid,&DevicePath,&gEfiPciHostBridgeResourceAllocationProtocolGuid,&ResAlloc,NULL// 参数列表结束);ReinstallProtocolInterface
需要更新已安装的Protocol时不能直接改Interface指针(在跑的驱动可能正在用),必须走Reinstall——三步走:通知断开 → 换指针 → 通知重连。
七、LocateProtocol:驱动找服务
安装是"挂牌",查找是"翻黄页":
// MdeModulePkg/Core/Dxe/Hand/Locate.cEFI_STATUSCoreLocateProtocol(IN EFI_GUID*Protocol,IN VOID*Registration,OUT VOID**Interface){ProtEntry=CoreFindProtocolEntry(Protocol,FALSE);// Protocol可能装了不止一份,返回第一个找到的InterfaceProt=CR(ProtEntry->Protocols.ForwardLink,PROTOCOL_INTERFACE,ByProtocol,PROTOCOL_INTERFACE_SIGNATURE);*Interface=Prot->Interface;returnEFI_SUCCESS;}LocateHandleBuffer用于遍历所有装了某个Protocol的Handle(比如枚举所有PCI设备)。
八、Notify机制:等Protocol的"订阅-发布"
有的驱动依赖的Protocol可能还没安装——比如USB键盘驱动需要EFI_USB_IO_PROTOCOL,但USB控制器驱动还在后面排着队。RegisterProtocolNotify派上用场:
// MdeModulePkg/Core/Dxe/Hand/Notify.cEFI_STATUSCoreRegisterProtocolNotify(IN EFI_GUID*Protocol,IN EFI_EVENT Event,OUT VOID**Registration){ProtEntry=CoreFindProtocolEntry(Protocol,TRUE);// 分配PROTOCOL_NOTIFY,挂在ProtEntry的Notify链表ProtNotify=AllocatePool(sizeof(PROTOCOL_NOTIFY));ProtNotify->Event=Event;// Protocol安装时Signal这个EventInsertTailList(&ProtEntry->Notify,&ProtNotify->Link);// 防竞态:万一本Protocol在Register之前刚好装了if(!IsListEmpty(&ProtEntry->Protocols)){CoreSignalEvent(Event);// 已经有了,直接通知}returnEFI_SUCCESS;}当任何驱动调用InstallProtocolInterface时,CoreNotifyProtocolEntry遍历Notify链表,Signal每个Event。被卡住的驱动从WaitForEvent返回,用LocateProtocol获取刚装好的Interface。
九、实战踩坑:Dispatcher死循环
某次发布前紧急修了一个PCIe驱动bug,重新编译FV。烧进去系统卡在DXE Dispatcher死循环——日志疯狂刷"Dispatch 0xXXXX Status SUCCESS",但就是不走下一个驱动。
根因
修复后的PCIe驱动Depex是AND gEfiPciRootBridgeIoProtocolGuid gEfiPciPlatformProtocolGuid END。每次调度执行后,它在Entry里调用了InstallProtocolInterface装gEfiPciPlatformProtocolGuid——但这个Protocol之前已经装过了!CoreInstallProtocolInterface返回EFI_ACCESS_DENIED(不允许在一个Handle上装同名Protocol两次)。
驱动作者没检查返回值。Dispatcher继续扫描,发现下一个驱动的Depex需要这个Protocol的新版本,但装不上去。这个驱动被放回等待队列,PCIe驱动又被重新调度——形成调度-失败-重调度死循环。
修复
在InstallProtocolInterface之后加ASSERT_EFI_ERROR(Status)。如果有EFI_ACCESS_DENIED,说明设计有问题——一个Protocol不应该被同一个驱动重复安装。
教训:DXE驱动的Entry Point必须检查InstallProtocolInterface的返回值。PEI阶段也许可以懒一点,DXE阶段不行——Dispatcher会把失败吞掉然后重试,让你以为一切正常。
十、实战踩坑总结
| 坑 | 现象 | 根因 | 经验 |
|---|---|---|---|
| Dispatcher死循环 | 日志疯狂刷Dispatch SUCCESS | 重复安装同名Protocol返回ACCESS_DENIED没检查 | Entry里ASSERT_EFI_ERROR |
| Depex评估误解 | 驱动提前执行导致崩溃 | 以为AND是短路评估 | 实际是全量评估后逻辑与 |
| Notify竞态 | 驱动等不到Protocol通知 | Register前Protocol刚装好 | RegisterProtocolNotify内部有二次检查 |
| Reinstall没通知 | 使用者拿到旧Interface | 直接改指针没走Reinstall | 必须三步:通知断开→换指针→通知重连 |
| 优先级设错 | 总线驱动比平台驱动先跑 | 优先级宏填反 | Platform Processor最早,Application最晚 |
源码路径索引
| 内容 | 关键文件 |
|---|---|
| DXE Dispatcher主循环 | MdeModulePkg/Core/Dxe/Dispatcher/Dispatcher.c |
| Depex评估 | MdeModulePkg/Core/Dxe/Dispatcher/Dependency.c |
| 驱动注册与FV发现 | MdeModulePkg/Core/Dxe/Dispatcher/Driver.c |
| Protocol安装/重安装 | MdeModulePkg/Core/Dxe/Hand/Handle.c |
| Protocol查找 | MdeModulePkg/Core/Dxe/Hand/Locate.c |
| Notify机制 | MdeModulePkg/Core/Dxe/Hand/Notify.c |
| DXE服务表 | MdeModulePkg/Core/Dxe/DxeMain/DxeMain.c |
| PI规范 | PI Specification Volume 1, Chapter 7-10 |